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

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

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

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

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

EDIT: я добавляю щедрость. Есть интересные ответы, но я хотел бы получить больше информации.

10 ответов


проверяем его.

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

увидев всю статистику с 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 и возвращает функция.

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


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


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

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

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

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

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


структура отличается в Примере от Бен М. Он будет расширен над головой внутри внутреннего 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();
        }

когда я запустил исходный тестовый код из Бен М, я заметил разницу как в Debug, так и в Debug Освободить конфигурацию.

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

заключение:
Основываясь на этих тестах, я думаю, мы можем сказать, что Try / Catch тут имеют небольшое влияние на производительность.

EDIT:
Я попытался увеличить значение цикла с 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 на самом деле быстрее!


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

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

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


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


try / catch влияет на производительность.

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

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


теоретически блок 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 на основе того факта, что statement2 гарантированно выполняется до statement3. Если компилятор может распознать, что thing1 не имеет побочных эффектов, а thing2 фактически не использует x, он может безопасно опустить thing1 вообще. Если [как в этом случае] thing1 был дорогим, это может быть серьезной оптимизацией, хотя случаи, когда thing1 дорог, также являются теми, которые компилятор с наименьшей вероятностью оптимизирует. Предположим, код был изменен:

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

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

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


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