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

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

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

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

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

Ответы [ 10 ]

183 голосов
/ 21 августа 2009

Проверьте это.

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
95 голосов
/ 30 августа 2009

После просмотра всех статистических данных с 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 и функция возвращает.

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

59 голосов
/ 21 августа 2009

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

32 голосов
/ 21 августа 2009

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

Показательные показатели Рико Мариани: Цена исключения: когда бросать, а когда нет

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

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

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

22 голосов
/ 25 августа 2009

Структура в этом примере отличается от 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 / Catch оказывает небольшое влияние на производительность.

EDIT:
Я попытался увеличить значение цикла с 10000000 до 1000000000 и снова запустил в Release, чтобы получить некоторые различия в выпуске, и результат был такой:

   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, на самом деле быстрее!

13 голосов
/ 21 августа 2009

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

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

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

10 голосов
/ 21 августа 2009

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

7 голосов
/ 29 августа 2009

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

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

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

4 голосов
/ 28 августа 2014

Теоретически, блок 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() перед оператором2 записал свое значение в x. Тогда catch выполнит Thread.ResetAbort() [через делегата q], что позволит продолжить выполнение с оператором 3. Такая последовательность событий, конечно, была бы исключительно невероятной, но компилятор должен генерировать код, который работает в соответствии со спецификацией, даже когда происходят такие невероятные события.

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

3 голосов
/ 31 августа 2009

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

...