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

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

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

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

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

13 ответов

Решение

Проверь это.

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 до строки 17 IL_0021: leave.s IL_002f, Если возникает исключение, элемент управления переходит на линию IL_0025: ldloc.0 в противном случае мы переходим на ярлык IL_002d: leave.s IL_002f и функция возвращает.

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

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

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

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

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

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

Согласно примечанию Криса Брумма: Существует также стоимость, связанная с тем, что 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();
        }

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

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

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

РЕДАКТИРОВАТЬ:
Я попытался увеличить значение цикла с 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, на самом деле быстрее!

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

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

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

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

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

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

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

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

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

Да, try/catch"повредит" производительность (все относительно). Не так много с точки зрения потраченных впустую CPU циклов, но есть и другие важные аспекты, которые следует учитывать:

  • Размер кода
  • Встраивание метода

Контрольный показатель

Во-первых, давайте проверим скорость с помощью некоторых сложных инструментов (например, BenchmarkDotNet). Скомпилировано как Release (AnyCPU), запускать на x64машина. Я бы сказал, что нет никакой разницы, хотя тест действительно скажет нам, что NoTryCatch() немного быстрее:

|            Method |   N |     Mean |     Error |    StdDev |
|------------------ |---- |---------:|----------:|----------:|
|        NoTryCatch | 0.5 | 3.770 ns | 0.0492 ns | 0.0411 ns |
|      WithTryCatch | 0.5 | 4.060 ns | 0.0410 ns | 0.0384 ns |
| WithTryCatchThrow | 0.5 | 3.924 ns | 0.0994 ns | 0.0881 ns |

Анализ

Некоторые дополнительные примечания.

|            Method | Code size | Inlineable |
|------------------ |---------- |-----------:|
|        NoTryCatch |        12 |        yes |
|      WithTryCatch |        18 |          ? |
| WithTryCatchThrow |        18 |         no |

Размер кода NoTryCatch()дает 12 байтов в коде, тогда как try/catch добавляет еще 6 байтов. Кроме того, при написании try/catch у вас, скорее всего, будет один или несколько throw new Exception("Message", ex) утверждения, дальнейшее "раздувание" кода.

Но самое главное здесь - это встраивание кода. В .NET простое существование throwКлючевое слово означает, что метод никогда не будет встроен компилятором (что подразумевает более медленный код, но также и меньший объем памяти). Я недавно тщательно проверил этот факт, так что он все еще актуален в .NET Core. Не уверен, что если try/catch следует тому же правилу. TODO: Verify!

Полный тестовый код

using System;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;

namespace TryCatchPerformance
{
    public class TryCatch
    {
        [Params(0.5)]
        public double N { get; set; }

        [Benchmark]
        public void NoTryCatch() => Math.Sin(N);

        [Benchmark]
        public void WithTryCatch()
        {
            try
            {
                Math.Sin(N);
            }
            catch
            {
            }
        }

        [Benchmark]
        public void WithTryCatchThrow()
        {
            try
            {
                Math.Sin(N);
            }
            catch (Exception ex)
            {
                throw;
            }
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            var summary = BenchmarkRunner.Run<TryCatch>();
        }
    }
}

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

Я проверил глубокий пробный улов.

              static void TryCatch(int level, int max)
        {
            try
            {
                if (level < max) TryCatch(level + 1, max);
            }
            catch
            { }
        }
        static void NoTryCatch(int level, int max)
        {
            if (level < max) NoTryCatch(level + 1, max);
        }
        static void Main(string[] args)
        {
            var s = new Stopwatch();
            const int max = 10000;
            s.Start();
            TryCatch(0, max);
            s.Stop();
            Console.WriteLine("try-catch " + s.Elapsed);
            s.Start();
            NoTryCatch(0, max);
            s.Stop();
            Console.WriteLine("no try-catch " + s.Elapsed);
        }

Результат:

      try-catch 00:00:00.0006393
no try-catch 00:00:00.0009002
Другие вопросы по тегам