Не пытайтесь ли вы блокировать блокировки, если исключения не выбрасываются?

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

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

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

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

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

Ответ 1

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

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

Ответ 2

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

С#:

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

С#:

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

Я не эксперт в области ИЛ, но мы видим, что локальный объект исключения создается на четвертой строке .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, а функция возвращает.

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

Ответ 3

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

Ответ 4

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

Rico Mariani Performance Tidbits: Исключительная стоимость: когда бросать и когда не в

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

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

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

Ответ 5

Структура в примере отличается от 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, так и в версии Release.

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

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

Ответ 6

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

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

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

Ответ 7

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

Ответ 8

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

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

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

Ответ 9

Теоретически блок 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

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

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

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

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

Ответ 10

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