Каковы реальные накладные расходы на использование try / catch в C #?

94

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

Дж. К. Граббс
источник

Ответы:

52

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

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

Майк Стоун
источник
33
Точнее: попробовать дешево, улов дешево, бросить дорого. Если вы избегаете попытки поймать, бросок по-прежнему стоит дорого.
Программист Windows,
4
Хммм - в комментариях не работает разметка. Повторить
HTTP 410,
2
@Windows программист Статистика / источник, пожалуйста?
Kapé
100

Здесь следует отметить три момента:

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

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

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

Шон Остин
источник
41
Также: когда вы повторно генерируете исключение как «throw ex», вы теряете исходную трассировку стека и заменяете ее трассировкой CURRENT стека; редко то, что нужно. Если вы просто «выбросите», то исходная трассировка стека в исключении сохраняется.
Эдди
@Eddie Orthrow new Exception("Wrapping layer’s error", ex);
binki
21

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

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

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

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

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

HTTP 410
источник
6

По моему опыту, самые большие накладные расходы связаны с созданием исключения и его обработкой. Однажды я работал над проектом, в котором код, подобный приведенному ниже, использовался для проверки, имеет ли кто-то право редактировать какой-либо объект. Этот метод 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();
}

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

Тоби
источник
1
Зачем вам создавать здесь исключение? Вы можете справиться с ситуацией отсутствия прав на месте.
ThunderGr
@ThunderGr - вот что я изменил, сделав его на два порядка быстрее.
Тоби
5

Вопреки общепринятым теориям, 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может вызывать беспорядки в трех измерениях , поскольку он может перемещаться вверх по стеку не только в другие части того же метода, но и в другие классы и методы. , все из которых будут наблюдаться разработчиком технического обслуживания, трудный путь ! Однако нам говорят, что «goto опасно», хех!

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

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


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

AKA

Как 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 try-block
finally finally-block

• Состояние определенного присваивания v в начале блока try совпадает с состоянием определенного присваивания v в начале stmt .
• Состояние определенного присваивания v в начале блока finally такое же, как состояние определенного присваивания v в начале stmt .
• Состояние определенного присваивания v в конечной точке stmt определенно назначается, если (и только если) либо: o v определенно назначается в конечной точке try-block o vопределенно назначается в конечной точке блока определенно назначен по другой причине при этой передаче потока управления, то он все равно считается определенно назначенным.)finally-block Если выполняется передача потока управления (например, инструкция goto ), которая начинается в блоке try и заканчивается за пределами блока try , то v также считается определенно назначенным для этой передачи потока управления, если v определенно назначено в конечная точка блока finally . (Это не только если - если 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
аутичный
источник
3

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

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

дотмад
источник
3

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

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

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

BlackWasp
источник
2

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

Хафтор
источник
Мне не удалось связаться с вашим блогом (время ожидания соединения истекло; вы используете try/ catchслишком много? Хе-хе), но вы, похоже, спорите со спецификацией языка и некоторыми MVP MS, которые также писали блоги по этой теме, предоставляя измерения вопреки вашему совету ... Я открыт для предположения, что проведенное мной исследование неверно, но мне нужно прочитать вашу запись в блоге, чтобы увидеть, что в нем говорится.
аутичный
В дополнение к сообщению в блоге @ Hafthor, вот еще одно сообщение в блоге с кодом, специально написанным для проверки различий в скорости и производительности. Согласно результатам, если исключение возникает даже в 5% случаев, код обработки исключений в целом работает в 100 раз медленнее, чем код обработки исключений. Статья специально нацелена на try-catchблок против tryparse()методов, но концепция та же.
2

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

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

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


источник
@Ritchard T, почему? По сравнению с программистом и затратами на качество это незначительно.
ThunderGr
1

Мне очень нравится сообщение в блоге Хафтора , и, чтобы добавить свои два цента к этому обсуждению, я хотел бы сказать, что для меня всегда было легко заставить СЛОЙ ДАННЫХ генерировать только один тип исключения (DataAccessException). Таким образом, мой БИЗНЕС-СЛОЙ знает, какого исключения ожидать, и улавливает его. Затем, в зависимости от других бизнес-правил (например, если мой бизнес-объект участвует в рабочем процессе и т. Д.), Я могу создать новое исключение (BusinessObjectException) или продолжить работу без повторной / выброса.

Я бы сказал, не стесняйтесь использовать try..catch всякий раз, когда это необходимо, и используйте его с умом!

Например, этот метод участвует в рабочем процессе ...

Комментарии?

public bool DeleteGallery(int id)
{
    try
    {
        using (var transaction = new DbTransactionManager())
        {
            try
            {
                transaction.BeginTransaction();

                _galleryRepository.DeleteGallery(id, transaction);
                _galleryRepository.DeletePictures(id, transaction);

                FileManager.DeleteAll(id);

                transaction.Commit();
            }
            catch (DataAccessException ex)
            {
                Logger.Log(ex);
                transaction.Rollback();                        
                throw new BusinessObjectException("Cannot delete gallery. Ensure business rules and try again.", ex);
            }
        }
    }
    catch (DbTransactionException ex)
    {
        Logger.Log(ex);
        throw new BusinessObjectException("Cannot delete gallery.", ex);
    }
    return true;
}

источник
2
Дэвид, не могли бы вы заключить вызов DeleteGallery в блок try / catch?
Роб
Поскольку DeleteGallery - это логическая функция, мне кажется, что создание исключения в ней бесполезно. Для этого потребуется, чтобы вызов DeleteGallery был заключен в блок try / catch. Мне кажется более значимым if (! DeleteGallery (theid)) {// handle}. в этом конкретном примере.
ThunderGr
1

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

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

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

Вандо
источник
Компилятор отсутствует во время выполнения. Там должно быть накладными расходами на попытки / улов блоки таким образом , что CLR может обрабатывать исключения. C # работает на .NET CLR (виртуальной машине). Мне кажется, что накладные расходы самого блока минимальны, когда нет исключения, но стоимость обработки исключения CLR очень велика.
ThunderGr
-4

Давайте проанализируем одну из самых больших возможных затрат на блок 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

Не считая незначительного пробела, можно заметить, что эти два эквивалентных фрагмента кода имеют почти одинаковую длину в байтах. Последний содержит отступ на 4 байта меньше. Это плохо?

Чтобы добавить оскорбления к травме, ученик решает зациклить, в то время как ввод может быть проанализирован как int. Решение без try / catch может выглядеть примерно так:

while (int.TryParse(...))
{
    ...
}

Но как это выглядит при использовании try / catch?

try {
    for (;;)
    {
        x = int.Parse(...);
        ...
    }
}
catch
{
    ...
}

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

Одна из меньших затрат заключается в том, что блоки try / catch действительно отключают определенные оптимизации: http://msmvps.com/blogs/peterritchie/archive/2007/06/22/performance-implications-of-try-catch-finally.aspx . Думаю, это тоже положительный момент. Его можно использовать для отключения оптимизаций, которые в противном случае могли бы нанести вред безопасным, нормальным алгоритмам передачи сообщений для многопоточных приложений, а также для выявления возможных состояний гонки;) Это единственный сценарий, который я могу придумать для использования try / catch. Даже у этого есть альтернативы.

Себастьян Рамадан
источник
1
Я почти уверен, что TryParse пытается {int x = int.Parse ("xxx"); вернуть истину;} поймать {вернуть ложь; } внутри. Вопрос не в отступах, а только в производительности и накладных расходах.
ThunderGr
@ThunderGr Или прочитайте новый ответ, который я опубликовал. Он содержит больше ссылок, одна из которых является анализом значительного повышения производительности, когда вы избегаете Int.Parseв пользу Int.TryParse.
аутист