Каковы реальные издержки try / catch в C #? - PullRequest
90 голосов
/ 09 сентября 2008

Итак, я знаю, что try / catch действительно добавляет некоторые накладные расходы и, следовательно, не является хорошим способом управления потоком процесса, но откуда эти накладные расходы и каково его реальное влияние?

Ответы [ 12 ]

93 голосов
/ 09 сентября 2008

Три замечания:

  • Во-первых, практически нет потери производительности при наличии блоков try-catch в вашем коде. Это не должно учитываться при попытке избежать их включения в ваше приложение. Хит производительности вступает в игру только тогда, когда выдается исключение.

  • Когда выдается исключение в дополнение к операциям разматывания стека и т. Д., Которые происходят, о чем упоминали другие, вы должны знать, что для заполнения элементов исключения происходит целый ряд вещей, связанных с временем выполнения / отражением. класс, такой как объект трассировки стека и различные члены типа и т. д.

  • Я считаю, что это одна из причин, по которой общий совет, если вы собираетесь перебрасывать исключение, состоит в том, чтобы просто throw; вместо того, чтобы бросить исключение снова или создать новое, как в этих случаях все эта информация стека восстанавливается, тогда как в простом броске все это сохраняется.

48 голосов
/ 09 сентября 2008

Я не эксперт по языковым реализациям (так что возьмем это с долей соли), но я думаю, что одна из самых больших затрат - это размотать стек и сохранить его для трассировки стека. Я подозреваю, что это происходит только тогда, когда выдается исключение (но я не знаю), и если это так, то это будет приличную величину скрытой стоимости каждый раз, когда выбрасывается исключение ... так что вы не прыгаете с одного места в коде другому много чего происходит.

Я не думаю, что это проблема, если вы используете исключения для ИСКЛЮЧИТЕЛЬНОГО поведения (поэтому не ваш типичный, ожидаемый путь через программу).

18 голосов
/ 20 сентября 2008

Вы спрашиваете о накладных расходах на использование try / catch / finally, когда исключения не генерируются, или накладных расходах на использование исключений для управления потоком процесса? Последнее несколько похоже на использование динамитной свечи для зажигания свечи на день рождения малыша, и связанные с этим накладные расходы попадают в следующие области:

  • Вы можете ожидать дополнительные ошибки в кеше из-за того, что выброшенное исключение обращается к резидентным данным, которые обычно не находятся в кеше.
  • Вы можете ожидать дополнительных сбоев страницы из-за сгенерированного исключения, получающего доступ к нерезидентному коду и данным, которые обычно не находятся в рабочем наборе вашего приложения.

    • например, для выдачи исключения потребуется, чтобы CLR находил местоположение блоков finally и catch на основе текущего IP-адреса и IP-адреса возврата каждого кадра до тех пор, пока исключение не будет обработано, плюс блок фильтра.
    • дополнительные затраты на строительство и разрешение имен для создания фреймов для диагностических целей, включая считывание метаданных и т. Д.
    • оба из вышеперечисленных пунктов обычно имеют доступ к «холодному» коду и данным, поэтому вероятны серьезные сбои страниц, если у вас вообще будет нехватка памяти:

      • CLR пытается поместить код и данные, которые используются нечасто, далеко от данных, которые часто используются для улучшения локальности, поэтому это работает против вас, потому что вы заставляете холод быть горячим.
      • стоимость ошибок жесткого диска, если таковые имеются, затмит все остальное.
  • Типичные ситуации улова часто бывают глубокими, поэтому вышеупомянутые эффекты, как правило, усиливаются (увеличивая вероятность ошибок страниц).

Что касается фактического влияния стоимости, она может сильно варьироваться в зависимости от того, что еще происходит в вашем коде в данный момент. У Джона Скита есть хорошее резюме здесь , с некоторыми полезными ссылками. Я склонен согласиться с его утверждением о том, что если вы дойдете до того момента, когда исключения значительно повлияют на вашу производительность, у вас возникнут проблемы с точки зрения использования исключений, выходящих за пределы просто производительности.

6 голосов
/ 09 сентября 2008

По моему опыту, самые большие издержки заключаются в создании исключения и его обработке. Однажды я работал над проектом, в котором был использован код, подобный следующему, чтобы проверить, имеет ли кто-то право редактировать какой-либо объект. Этот метод HasRight () использовался повсеместно на уровне представления и часто вызывался для сотен объектов.

bool HasRight(string rightName, DomainObject obj) {
  try {
    CheckRight(rightName, obj);
    return true;
  }
  catch (Exception ex) {
    return false;
  }
}

void CheckRight(string rightName, DomainObject obj) {
  if (!_user.Rights.Contains(rightName))
    throw new Exception();
}

Когда тестовая база данных пополнилась тестовыми данными, это привело к очень заметному замедлению при открытии новых форм и т. Д.

Так что я реорганизовал его следующим образом, который - согласно последующим быстрым и грязным измерениям - примерно на 2 порядка быстрее:

bool HasRight(string rightName, DomainObject obj) {
  return _user.Rights.Contains(rightName);
}

void CheckRight(string rightName, DomainObject obj) {
  if (!HasRight(rightName, obj))
    throw new Exception();
}

Короче говоря, использование исключений в нормальном потоке процесса примерно на два порядка медленнее, чем при использовании аналогичного потока процесса без исключений.

3 голосов
/ 09 марта 2009

Я недавно написал статью об этом, потому что в то время было много людей, спрашивающих об этом. Вы можете найти его и тестовый код на http://www.blackwasp.co.uk/SpeedTestTryCatch.aspx.

В результате у блока try / catch накладные расходы незначительны, но они настолько малы, что их следует игнорировать. Однако, если вы запускаете блоки try / catch в циклах, которые выполняются миллионы раз, вы можете по возможности переместить блок за пределы цикла.

Ключевая проблема производительности с блоками try / catch заключается в том, что вы фактически перехватываете исключение. Это может добавить заметную задержку для вашего приложения. Конечно, когда дела идут плохо, большинство разработчиков (и многие пользователи) признают паузу как исключение, которое должно произойти! Ключевым моментом здесь не является использование обработки исключений для обычных операций. Как следует из названия, они являются исключительными, и вы должны сделать все возможное, чтобы их не выбросили. Вы не должны использовать их как часть ожидаемого потока программы, которая работает правильно.

3 голосов
/ 12 сентября 2008

Не говоря уже о том, находится ли он внутри часто вызываемого метода, он может повлиять на общее поведение приложения.
Например, я считаю использование Int32.Parse плохой практикой в ​​большинстве случаев, поскольку он генерирует исключения для чего-то, что может быть легко перехвачено иначе.

Итак, для завершения всего написано здесь:
1) Используйте блоки try..catch для обнаружения непредвиденных ошибок - практически без снижения производительности.
2) Не используйте исключения для исключенных ошибок, если вы можете избежать этого.

2 голосов
/ 15 августа 2017

Вопреки общепринятым теориям, try / catch может иметь существенное влияние на производительность, и вот, выбрасывается ли исключение!

  1. Отключает некоторые автоматические оптимизации (по проекту) , а в некоторых случаях вводит отладочный код, как вы можете ожидать от средства отладки . Всегда найдутся люди, которые не согласятся со мной по этому вопросу, но язык требует этого, и разборка показывает это, так что эти люди по определению словаря бредовые .
  2. Это может отрицательно повлиять на обслуживание. Это на самом деле самая важная проблема здесь, но, поскольку мой последний ответ (который был почти полностью посвящен этому) был удален, я постараюсь сосредоточиться на менее существенная проблема (микрооптимизация) в отличие от более существенной проблемы (макрооптимизация).

Первый был рассмотрен в нескольких сообщениях в блогах Microsoft MVP на протяжении многих лет, и я надеюсь, что вы можете легко их найти, но StackOverflow заботится о столько о контенте , поэтому предоставлю ссылки на некоторые из них как наполнитель свидетельство:

Также есть этот ответ , который показывает разницу между дизассемблированным кодом с использованием и без try / catch.

Кажется настолько очевидным, что - это накладные расходы, которые явно наблюдаются при генерации кода, и эти накладные расходы даже, кажется, признаются людьми, которые ценят Microsoft! И все же я повторяю интернет ...

Да, существуют десятки дополнительных инструкций MSIL для одной тривиальной строки кода, и это даже не распространяется на отключенную оптимизацию, так что технически это микрооптимизация.


Я опубликовал ответ несколько лет назад, который был удален, поскольку он был сосредоточен на производительности программистов (макро-оптимизация).

Это прискорбно, поскольку отсутствие экономии времени на процессор в течение нескольких наносекунд, скорее всего, компенсирует многие часы ручной оптимизации людьми. За что ваш начальник платит больше: час вашего времени или час при работающем компьютере? В какой момент мы выдернем вилку и признаем, что пришло время просто купить более быстрый компьютер ? ​​

Очевидно, мы должны оптимизировать наши приоритеты , а не только наш код! В своем последнем ответе я обратил внимание на различия между двумя фрагментами кода.

Использование try / catch:

int x;
try {
    x = int.Parse("1234");
}
catch {
    return;
}
// some more code here...

Не используется try / catch:

int x;
if (int.TryParse("1234", out x) == false) {
    return;
}
// some more code here

Рассмотрим с точки зрения разработчика технического обслуживания, который с большей вероятностью будет тратить ваше время, если бы не профилирование / оптимизация (описанная выше), что, вероятно, даже не потребовалось бы, если бы не было try / catch проблема, затем при прокрутке исходного кода ... У одной из них есть четыре лишние строки стандартного мусора!

По мере того, как в класс вводится все больше и больше полей, весь этот шаблонный мусор накапливается (как в исходном, так и в разобранном коде) намного выше разумных уровней. Четыре дополнительные строки на поле, и они всегда одни и те же строки ... Разве нас не учили избегать повторения? Я полагаю, что мы могли бы скрыть try / catch за какой-то самодельной абстракцией, но ... тогда мы могли бы также просто избегать исключений (то есть использовать Int.TryParse).

Это даже не сложный пример; Я видел попытки создания новых классов в try / catch. Учтите, что весь код внутри конструктора может быть дисквалифицирован из-за определенных оптимизаций, которые в противном случае были бы автоматически применены компилятором. Что может быть лучше для теории, что медленный * компилятор , в отличие от компилятор делает именно то, что ему говорят ?

Предполагая, что указанным конструктором выдается исключение, и в результате возникает некоторая ошибка, разработчик с плохим техническим обслуживанием затем должен отследить его. Это может быть не такой простой задачей, так как в отличие от спагетти-кода goto кошмара, try / catch может вызвать беспорядок в трех измерениях , так как он может двигаться вверх стек не только в других частях того же метода, но и в других классах и методах, и все это будет наблюдать разработчик технического обслуживания, трудный путь ! И все же нам говорят, что "Гото опасно", хе!

В конце, о котором я упоминаю, try / catch имеет свое преимущество: оно предназначено для отключения оптимизаций ! Это, если хотите, помощь в отладке ! Это то, для чего оно было разработано, и это то, что должно использоваться как ...

Думаю, это тоже положительный момент. Его можно использовать для отключения оптимизаций, которые в противном случае могли бы нанести вред безопасным, нормальным алгоритмам передачи сообщений для многопоточных приложений, и для выявления возможных условий гонки;) Это единственный сценарий, который я могу придумать, использовать try / catch. Даже у этого есть альтернативы.


Какие оптимизации отключают try, catch и finally?

A.K.A

Как try, catch и finally полезны в качестве средств отладки?

они препятствуют написанию. Это исходит из стандарта:

12.3.3.13 Операторы try-catch

Для выписки stmt вида:

try try-block
catch ( ... ) catch-block-1
... 
catch ( ... ) catch-block-n
  • Состояние определенного присваивания v в начале try-block совпадает с состоянием определенного присваивания v в начале STMT .
  • Состояние определенного присваивания v в начале catch-block-i (для любого i ) такое же, как состояние определенного присваивания v в начале stmt .
  • Состояние определенного присваивания v в конечной точке stmt определенно назначается, если (и только если) v определенно назначается в конце -точка try-block и каждого catch-block-i (для каждого i от 1 до n ).

Другими словами, в начале каждого try оператора:

  • все назначения, сделанные видимым объектам до ввода оператора try, должны быть завершены, что требует блокировки потока для запуска, что делает его полезным для отладки условий гонки!
  • компилятору не разрешено:
    • исключить неиспользуемые назначения переменных, которые определенно были назначены до оператора try
    • реорганизовать или объединить любое из его внутренних назначений (т.е. см. Мою первую ссылку, если вы еще этого не сделали).
    • поднимать назначения над этим барьером, чтобы задерживать присвоение переменной, которая, как она знает, не будет использоваться позднее (если вообще будет), или превентивно перемещать более поздние назначения вперед, чтобы сделать возможной другую оптимизацию ...

Аналогичная история сохраняется для каждого catch утверждения; предположим, что в вашем операторе try (или в вызываемом им конструкторе или функции и т. д.) вы присваиваете этой бессмысленной переменной (скажем, garbage=42;), компилятор не может удалить этот оператор, нет Неважно, насколько это не имеет отношения к наблюдаемому поведению программы. Назначение должно иметь завершено до ввода блока catch.

Для чего это стоит, finally рассказывает аналогично унизительную историю:

12.3.3.14 Операторы try-finally

Для попробуйте оператор stmt формы:

try try-block
finally finally-block

• Состояние определенного присваивания v в начале try-block совпадает с состоянием определенного присваивания v в начале STMT .
• Состояние определенного присваивания v в начале finally-block совпадает с состоянием определенного присваивания v в начале stmt .
• Определенное состояние присваивания v в конечной точке stmt определенно назначается, если (и только если) либо: o v определенно назначается в конечной точке try-block o v определенно назначается в конечной точке finally-block Если выполняется передача потока управления (например, оператор goto ), который начинается в try-block и заканчивается вне try-block , тогда v также считается определенно назначенным для этой передачи потока управления, если v определенно назначено в конечной точке finally-block . (Это не единственный случай, если - если v определенно назначено по другой причине для этой передачи потока управления, то оно все еще считается определенно назначенным.)

12.3.3.15 Операторы try-catch-finally

Анализ определенного присваивания для try - catch - finally оператор формы:

try try-block
catch ( ... ) catch-block-1
... 
catch ( ... ) catch-block-n
finally finally-block

выполняется так, как если бы оператор был try - finally оператор, включающий try - catch оператор:

try { 
    try   
    try-block
    catch ( ... ) catch-block-1
    ...   
    catch ( ... ) catch-block-n
} 
finally finally-block
2 голосов
/ 23 октября 2008

Гораздо проще писать, отлаживать и поддерживать код, свободный от сообщений об ошибках компилятора, предупреждений анализа кода и стандартных принятых исключений (особенно исключений, которые выбрасываются в одном месте и принимаются в другом). Поскольку это проще, код в среднем будет лучше написан и менее глючит.

Мне кажется, что затраты на программиста и качество являются основным аргументом против использования try-catch для потока процесса.

Компьютерные накладные расходы на исключения незначительны по сравнению и, как правило, незначительны с точки зрения способности приложения соответствовать требованиям к реальной производительности.

2 голосов
/ 09 сентября 2008

В прошлом году я сделал запись в блоге на эту тему. Проверьте это. Суть в том, что блок try почти не требует затрат, если не возникает исключение - и на моем ноутбуке исключение составляло около 36 мкс. Это может быть меньше, чем вы ожидали, но имейте в виду, что те результаты, где на мелком стеке. Кроме того, первые исключения очень медленные.

1 голос
/ 17 января 2010

Мы можем прочитать в «Прагматике языков программирования» Майкла Л. Скотта, что современные компиляторы не добавляют никаких накладных расходов в общем случае, то есть, когда не возникает никаких исключений. Поэтому каждая работа выполняется во время компиляции. Но когда исключение генерируется во время выполнения, компилятору необходимо выполнить двоичный поиск, чтобы найти правильное исключение, и это будет происходить для каждого нового выполненного вами выброса.

Но исключения - это исключения, и эта стоимость вполне приемлема. Если вы попытаетесь выполнить обработку исключений без исключений и вместо этого использовать коды ошибок возврата, вероятно, вам потребуется оператор if для каждой подпрограммы, и это приведет к накладным расходам в реальном времени. Вы знаете, что оператор if преобразуется в несколько инструкций по сборке, которые будут выполняться каждый раз, когда вы вводите свои подпрограммы.

Извините за мой английский, надеюсь, что он вам поможет. Эта информация основана на цитированной книге, более подробную информацию см. В главе 8.5 «Обработка исключений».

...