Блоки try / catch влияют на производительность, когда не генерируются исключения?

274

Во время проверки кода с сотрудником Microsoft мы наткнулись на большой раздел кода внутри try{}блока. Она и ИТ-представитель предположили, что это может повлиять на производительность кода. Фактически, они предложили, чтобы большая часть кода была за пределами блоков try / catch, и что должны проверяться только важные разделы. Сотрудник Microsoft добавил и сказал, что предстоящий технический документ предостерегает от неправильных блоков try / catch.

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

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

Как блоки try / catch влияют на производительность, когда исключения не генерируются?

Коби
источник
147
«Тот, кто пожертвовал бы правильностью ради производительности, не заслуживает ни того, ни другого».
Джоэл Коухорн
16
Тем не менее, правильность не всегда должна приноситься в жертву производительности.
Дэн Дэвис Брэкетт
19
Как насчет простого любопытства?
Саманта Бранхам
63
@Joel: Возможно, Коби просто хочет узнать ответ из любопытства. Знание того, будет ли производительность лучше или хуже, не обязательно означает, что он собирается делать что-то сумасшедшее со своим кодом. Разве стремление к знаниям само по себе не является хорошей вещью?
ЛукиH
6
Вот хороший алгоритм для того, чтобы знать, делать это изменение или нет. Во-первых, установите значимые ориентированные на клиента цели производительности. Во-вторых, сначала напишите код, который будет правильным и понятным. В-третьих, проверьте это против ваших целей. В-четвертых, если вы достигнете своих целей, рано бросайте работу и отправляйтесь на пляж. В-пятых, если вы не достигли своих целей, используйте профилировщик, чтобы найти код, который работает слишком медленно. В-шестых, если этот код оказывается слишком медленным из-за ненужного обработчика исключений, только тогда удаляйте обработчик исключений. Если нет, то исправьте код, который на самом деле слишком медленный. Затем вернитесь к шагу три.
Эрик Липперт

Ответы:

203

Проверь это.

static public void Main(string[] args)
{
    Stopwatch w = new Stopwatch();
    double d = 0;

    w.Start();

    for (int i = 0; i < 10000000; i++)
    {
        try
        {
            d = Math.Sin(1);
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.ToString());
        }
    }

    w.Stop();
    Console.WriteLine(w.Elapsed);
    w.Reset();
    w.Start();

    for (int i = 0; i < 10000000; i++)
    {
        d = Math.Sin(1);
    }

    w.Stop();
    Console.WriteLine(w.Elapsed);
}

Вывод:

00:00:00.4269033  // with try/catch
00:00:00.4260383  // without.

В миллисекундах:

449
416

Новый код:

for (int j = 0; j < 10; j++)
{
    Stopwatch w = new Stopwatch();
    double d = 0;
    w.Start();

    for (int i = 0; i < 10000000; i++)
    {
        try
        {
            d = Math.Sin(d);
        }

        catch (Exception ex)
        {
            Console.WriteLine(ex.ToString());
        }

        finally
        {
            d = Math.Sin(d);
        }
    }

    w.Stop();
    Console.Write("   try/catch/finally: ");
    Console.WriteLine(w.ElapsedMilliseconds);
    w.Reset();
    d = 0;
    w.Start();

    for (int i = 0; i < 10000000; i++)
    {
        d = Math.Sin(d);
        d = Math.Sin(d);
    }

    w.Stop();
    Console.Write("No try/catch/finally: ");
    Console.WriteLine(w.ElapsedMilliseconds);
    Console.WriteLine();
}

Новые результаты:

   try/catch/finally: 382
No try/catch/finally: 332

   try/catch/finally: 375
No try/catch/finally: 332

   try/catch/finally: 376
No try/catch/finally: 333

   try/catch/finally: 375
No try/catch/finally: 330

   try/catch/finally: 373
No try/catch/finally: 329

   try/catch/finally: 373
No try/catch/finally: 330

   try/catch/finally: 373
No try/catch/finally: 352

   try/catch/finally: 374
No try/catch/finally: 331

   try/catch/finally: 380
No try/catch/finally: 329

   try/catch/finally: 374
No try/catch/finally: 334
Бен М
источник
24
Можете ли вы попробовать их в обратном порядке, чтобы убедиться, что компиляция JIT не повлияла на первую?
ДжошДжордан
28
Подобные программы вряд ли кажутся хорошими кандидатами для тестирования воздействия обработки исключений, слишком большая часть того, что происходит в обычных блоках try {} catch {}, будет оптимизирована. Я могу пообедать на этом ...
LorenVS
30
Это отладочная сборка. JIT не оптимизирует их.
Бен М
7
Это совсем не так, подумайте об этом. Сколько раз вы используете try catch в цикле? Большую часть времени вы будете использовать цикл в try.c
Ативат Чунлахан
9
В самом деле? «Как блоки try / catch влияют на производительность, когда исключения не генерируются?»
Бен М
105

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

C #:

private static void TestWithoutTryCatch(){
    Console.WriteLine("SIN(1) = {0} - No Try/Catch", Math.Sin(1)); 
}

MSIL:

.method private hidebysig static void  TestWithoutTryCatch() cil managed
{
  // Code size       32 (0x20)
  .maxstack  8
  IL_0000:  nop
  IL_0001:  ldstr      "SIN(1) = {0} - No Try/Catch"
  IL_0006:  ldc.r8     1.
  IL_000f:  call       float64 [mscorlib]System.Math::Sin(float64)
  IL_0014:  box        [mscorlib]System.Double
  IL_0019:  call       void [mscorlib]System.Console::WriteLine(string,
                                                                object)
  IL_001e:  nop
  IL_001f:  ret
} // end of method Program::TestWithoutTryCatch

C #:

private static void TestWithTryCatch(){
    try{
        Console.WriteLine("SIN(1) = {0}", Math.Sin(1)); 
    }
    catch (Exception ex){
        Console.WriteLine(ex);
    }
}

MSIL:

.method private hidebysig static void  TestWithTryCatch() cil managed
{
  // Code size       49 (0x31)
  .maxstack  2
  .locals init ([0] class [mscorlib]System.Exception ex)
  IL_0000:  nop
  .try
  {
    IL_0001:  nop
    IL_0002:  ldstr      "SIN(1) = {0}"
    IL_0007:  ldc.r8     1.
    IL_0010:  call       float64 [mscorlib]System.Math::Sin(float64)
    IL_0015:  box        [mscorlib]System.Double
    IL_001a:  call       void [mscorlib]System.Console::WriteLine(string,
                                                                  object)
    IL_001f:  nop
    IL_0020:  nop
    IL_0021:  leave.s    IL_002f //JUMP IF NO EXCEPTION
  }  // end .try
  catch [mscorlib]System.Exception 
  {
    IL_0023:  stloc.0
    IL_0024:  nop
    IL_0025:  ldloc.0
    IL_0026:  call       void [mscorlib]System.Console::WriteLine(object)
    IL_002b:  nop
    IL_002c:  nop
    IL_002d:  leave.s    IL_002f
  }  // end handler
  IL_002f:  nop
  IL_0030:  ret
} // end of method Program::TestWithTryCatch

Я не эксперт по IL, но мы можем видеть, что локальный объект исключения создается в четвертой строке .locals init ([0] class [mscorlib]System.Exception ex)после этого, все примерно так же, как для метода без try / catch до строки семнадцать IL_0021: leave.s IL_002f. Если возникает исключение, элемент управления переходит к строке, в IL_0025: ldloc.0противном случае мы переходим к метке IL_002d: leave.s IL_002fи функция возвращается.

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

TheVillageIdiot
источник
33
Что ж, IL включает блок try / catch в той же записи, что и в C #, так что это на самом деле не показывает, сколько издержек означает try / catch за кулисами! То, что IL не добавляет намного больше, не означает то же самое, что не добавляется что-либо в код скомпилированной сборки. IL - это просто общее представление всех языков .NET. Это НЕ машинный код!
благоговение
64

Нет. Если тривиальные оптимизации, которые исключает блок try / finally, действительно оказывают ощутимое влияние на вашу программу, вам, вероятно, не следует использовать .NET в первую очередь.

Джон Кугельман
источник
10
Это отличный момент - сравните с другими пунктами в нашем списке, этот должен быть крошечным. Мы должны доверять базовым языковым возможностям для правильного поведения и оптимизировать то, что мы можем контролировать (sql, индексы, алгоритмы).
Коби
3
Подумай о плотных петлях, приятель. Пример цикла, в котором вы читаете и десериализуете объекты из потока данных сокета на игровом сервере и пытаетесь сжать как можно больше. Таким образом, вы используете MessagePack для сериализации объектов вместо бинарного форматирования и используете ArrayPool <byte> вместо простого создания байтовых массивов и т. Д. В этих сценариях, каково влияние нескольких (возможно, вложенных) блоков try в узком цикле. Некоторые оптимизации будут пропущены компилятором, а переменная исключения отправляется в Gen0 GC. Все, что я говорю, это то, что есть «некоторые» сценарии, где все оказывает влияние.
tcwicks
35

Довольно полное объяснение модели исключений .NET.

Особенности Рико Мариани: исключительная стоимость: когда бросать, а когда нет

Первый вид затрат - это статические затраты на обработку исключений в вашем коде вообще. Управляемые исключения здесь действительно сравнительно хороши, и я имею в виду, что статическая стоимость может быть намного ниже, чем, скажем, в C ++. Почему это? Ну, статическая стоимость действительно возникает в двух местах: во-первых, на реальных сайтах try / finally / catch / throw, где есть код для этих конструкций. Во-вторых, в неуправляемом коде есть скрытая стоимость, связанная с отслеживанием всех объектов, которые должны быть уничтожены в случае возникновения исключения. Должно присутствовать значительное количество логики очистки, и хитрая часть в том, что даже код, который не '

Дмитрий Заславский:

Согласно примечанию Криса Брумма: Существует также стоимость, связанная с тем, что JIT не выполняет некоторую оптимизацию при наличии улова.

Arul
источник
1
Что касается C ++, так это то, что очень большой кусок стандартной библиотеки будет генерировать исключения. В них нет ничего лишнего. Вы должны разработать свои объекты с какой-то политикой исключений, и как только вы это сделаете, больше не будет скрытых затрат.
Дэвид Торнли
Заявления Рико Мариани совершенно не соответствуют родному C ++. «Статическая стоимость может быть намного ниже, чем, скажем, в C ++» - это просто неправда. Хотя я не уверен, каким был механизм исключения в 2003 году, когда была написана статья. C ++ действительно ничего не стоит, когда исключения не генерируются, независимо от того, сколько у вас блоков try / catch и где они находятся.
BJovke
1
@BJovke C ++ «Обработка исключений с нулевой стоимостью» означает только то, что во время выполнения исключений нет затрат времени выполнения, но все же есть большие затраты на размер кода из-за того, что весь код очистки вызывает деструкторы для исключений. Кроме того, хотя по обычному пути кода не генерируется никакого кода, специфичного для исключения, стоимость все равно не равна нулю, поскольку возможность исключений все еще ограничивает оптимизатор (например, материал, необходимый в случае исключения, должен остаться где-то где-то -> значения можно отбрасывать менее агрессивно -> менее эффективное распределение регистров)
Даниэль
24

Структура отличается в примере от Ben M . Это будет расширено внутриfor цикла, что приведет к плохому сравнению между этими двумя случаями.

Следующее является более точным для сравнения, когда весь код для проверки (включая объявление переменной) находится внутри блока Try / Catch:

        for (int j = 0; j < 10; j++)
        {
            Stopwatch w = new Stopwatch();
            w.Start();
            try { 
                double d1 = 0; 
                for (int i = 0; i < 10000000; i++) { 
                    d1 = Math.Sin(d1);
                    d1 = Math.Sin(d1); 
                } 
            }
            catch (Exception ex) {
                Console.WriteLine(ex.ToString()); 
            }
            finally { 
                //d1 = Math.Sin(d1); 
            }
            w.Stop(); 
            Console.Write("   try/catch/finally: "); 
            Console.WriteLine(w.ElapsedMilliseconds); 
            w.Reset(); 
            w.Start(); 
            double d2 = 0; 
            for (int i = 0; i < 10000000; i++) { 
                d2 = Math.Sin(d2);
                d2 = Math.Sin(d2); 
            } 
            w.Stop(); 
            Console.Write("No try/catch/finally: "); 
            Console.WriteLine(w.ElapsedMilliseconds); 
            Console.WriteLine();
        }

Когда я запустил оригинальный тестовый код от Ben M , я заметил разницу в конфигурации Debug и Releas.

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

Conclution :
На основании этих испытаний, я думаюмы можем сказатьчто Try / Поймать делает оказывает незначительное влияние на производительность.

РЕДАКТИРОВАТЬ:
я попытался увеличить значение цикла с 10000000 до 1000000000, и снова запустил в выпуске, чтобы получить некоторые различия в выпуске, и результат был следующим:

   try/catch/finally: 509
No try/catch/finally: 486

   try/catch/finally: 479
No try/catch/finally: 511

   try/catch/finally: 475
No try/catch/finally: 477

   try/catch/finally: 477
No try/catch/finally: 475

   try/catch/finally: 475
No try/catch/finally: 476

   try/catch/finally: 477
No try/catch/finally: 474

   try/catch/finally: 475
No try/catch/finally: 475

   try/catch/finally: 476
No try/catch/finally: 476

   try/catch/finally: 475
No try/catch/finally: 476

   try/catch/finally: 475
No try/catch/finally: 474

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

трепет
источник
1
Я тоже это заметил, иногда быстрее с try / catch. Я прокомментировал это на ответ Бена. Однако, в отличие от 24 избирателей, мне не нравятся такие сравнительные тесты, я не думаю, что это хороший показатель. В этом случае код работает быстрее, но будет ли он всегда?
Коби
5
Разве это не доказывает, что ваша машина выполняла множество других задач одновременно? Истекшее время никогда не является хорошей мерой, вам нужно использовать профилировщик, который записывает время процессора, а не истекшее время.
Колин Десмонд
2
@ Коби: Я согласен, что это не лучший способ для сравнения, если вы собираетесь опубликовать его в качестве доказательства того, что ваша программа работает быстрее, чем другие или что-то еще, но может дать вам как разработчику представление о том, что один метод работает лучше, чем другой , В этом случае, я думаю, мы можем сказать, что различия (по крайней мере, для конфигурации выпуска) игнорируются.
благоговение
1
Вы не рассчитываете try/catchздесь. Вы рассчитываете 12 попыток входа в критическую секцию против 10М циклов. Шум петли уничтожит любое влияние try / catch. если вместо этого вы помещаете try / catch в узкий цикл и сравниваете с / без, вы в конечном итоге получите стоимость try / catch. (Без сомнения, такое кодирование обычно не является хорошей практикой, но если вы хотите рассчитать время, затрачиваемое на конструкцию, вы так и сделаете). В настоящее время BenchmarkDotNet - это инструмент для обеспечения надежного времени выполнения.
Авель
15

Я проверил фактическое воздействие try..catchв узком цикле, и оно само по себе слишком мало, чтобы быть проблемой производительности в любой нормальной ситуации.

Если цикл делает очень мало работы (в моем тесте я сделал x++ ), вы можете измерить влияние обработки исключений. Цикл с обработкой исключений выполнялся примерно в десять раз дольше.

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

Guffa
источник
11

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

RHicke
источник
8

Попробовать / поймать оказывает влияние на производительность.

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

Вот справочник по производительности try / catch (хотя и не объясняет его сложность, но подразумевается). Взгляните на раздел « Бросьте меньше исключений »

Исаак
источник
3
Сложность - это O (1), это не значит слишком много. Например, если вы снабдите секцию кода, которая очень часто вызывается try-catch (или вы упомянули цикл), O (1) могут в итоге добавить измеримое число.
Чаба Тот
6

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

Action q;
double thing1()
  { double total; for (int i=0; i<1000000; i++) total+=1.0/i; return total;}
double thing2()
  { q=null; return 1.0;}
...
x=thing1();     // statement1
x=thing2(x);    // statement2
doSomething(x); // statement3

Компилятор может быть в состоянии оптимизировать оператор Statement1, основываясь на том факте, что оператор2 гарантированно будет выполняться перед оператором3. Если компилятор может распознать, что thing1 не имеет побочных эффектов, а thing2 фактически не использует x, он может полностью пропустить thing1. Если бы [как в этом случае] вещь 1 была дорогой, это могло бы стать основной оптимизацией, хотя случаи, когда вещь 1 дорогая, также являются теми, которые компилятор с наименьшей вероятностью оптимизировал бы. Предположим, что код был изменен:

x=thing1();      // statement1
try
{ x=thing2(x); } // statement2
catch { q(); }
doSomething(x);  // statement3

Теперь существует последовательность событий, где оператор 3 может выполняться без выполнения оператора 2. Даже если ничто в коде для thing2не может вызвать исключение, возможно, что другой поток может использовать очищенное Interlocked.CompareExchangeзамечание qи установить его в значение Thread.ResetAbort, а затем выполнить оператор Thread.Abort()before до записи его значения x. Затем catchвыполняется Thread.ResetAbort()[через делегата q], что позволяет продолжить выполнение с оператором 3. Такая последовательность событий, конечно, была бы исключительно невероятной, но компилятор должен генерировать код, который работает в соответствии со спецификацией, даже когда происходят такие невероятные события.

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

Supercat
источник
5

Хотя « профилактика лучше, чем обработка », с точки зрения производительности и эффективности мы могли бы выбрать опробование вместо предварительной вариации. Рассмотрим следующий код:

Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
for (int i = 1; i < int.MaxValue; i++)
{
    if (i != 0)
    {
        int k = 10 / i;
    }
}
stopwatch.Stop();
Console.WriteLine($"With Checking: {stopwatch.ElapsedMilliseconds}");
stopwatch.Reset();
stopwatch.Start();
for (int i = 1; i < int.MaxValue; i++)
{
    try
    {
        int k = 10 / i;
    }
    catch (Exception)
    {

    }
}
stopwatch.Stop();
Console.WriteLine($"With Exception: {stopwatch.ElapsedMilliseconds}");

Вот результат:

With Checking: 20367
With Exception: 13998
Тед Оддман
источник
4

См. Обсуждение реализации try / catch для обсуждения того, как работают блоки try / catch, и как некоторые реализации имеют высокие издержки, а некоторые - нулевые, когда не возникает исключений. В частности, я думаю, что 32-битная реализация Windows имеет большие издержки, а 64-битная реализация - нет.

Ира Бакстер
источник
Я описал два разных подхода к реализации исключений. Подходы в равной степени применимы к C ++ и C #, а также к управляемому / неуправляемому коду. Какие из них выбрал MS для своего C #, я точно не знаю, но архитектура обработки исключений приложений машинного уровня, предоставляемая MS, использует более быструю схему. Я был бы немного удивлен, если бы реализация C # для 64 битов не использовала это.
Ира Бакстер