Чи спроби / ловити блоки шкодять продуктивності, коли винятки не кидаються?


274

Під час огляду коду з працівником Microsoft ми зіткнулися з великим розділом коду всередині try{}блоку. Вона та представник ІТ припустили, що це може вплинути на ефективність коду. Насправді вони пропонували, що більша частина коду повинна знаходитися поза блоками спробу / лову, і щоб перевіряти лише важливі розділи. Співробітник Microsoft додав і заявив, що майбутня довідка про застереження від неправильних блоків спробу / лову.

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

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

Як блоки try / catch впливають на ефективність, коли винятки не кидаються?


147
"Той, хто пожертвував би правильністю для виконання, того не заслуговує".
Joel Coehoorn

16
проте сказане, правильність не завжди повинна бути принесена в жертву для виконання.
Dan Davies Brackett

19
Як щодо простої цікавості?
Саманта Бранхем

63
@Joel: Можливо, Кобі просто хоче дізнатися відповідь з цікавості. Знання того, чи буде продуктивність кращою чи гіршою, не означає, що він збирається зробити щось божевільне зі своїм кодом. Хіба не прагнення до пізнання заради себе є доброю справою?
ЛукаХ

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 не вплинула на попередню?
JoshJordan

28
Такі програми навряд чи здаються хорошими кандидатами для тестування впливу обробки виключень, надто багато того, що відбувається у звичайних блоках спробу {} catch {}, буде оптимізовано. Я, можливо, буду на обіді ...
LorenVS

30
Це збірка налагодження. JIT не оптимізує їх.
Бен М

7
Це зовсім не так, подумайте. Скільки разів ви використовуєте спробу лову в циклі? Більшу частину часу ви будете використовувати цикл у try.c
Athiwat Chunlakhan

9
Дійсно? "Як блоки спробу / лову впливають на продуктивність, коли винятки не кидаються?"
Бен М

105

Після перегляду всіх статистику по з TRY / улову і без TRY / улову, цікавість змусили мене дивитися за , щоб побачити , що генерується для обох випадків. Ось код:

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)після того, як речі є такими ж, як і для методу без спроб / лову до рядка сімнадцять IL_0021: leave.s IL_002f. Якщо трапляється виняток, стрибки управління на рядок IL_0025: ldloc.0інакше переходимо до мітки IL_002d: leave.s IL_002fі функція повертається.

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


33
Ну, IL включає блок пробування / лову в тих же позначеннях, що і в C #, тому це насправді не показує, скільки означає накладні витрати на спробу / ловлення за кадром! Тільки те, що IL не додає набагато більше, не означає те саме, що до компільованого асемблерного коду щось не додано. IL - просто загальне представлення всіх мов .NET. Це НЕ машинний код!
трепетно

64

Ні. Якщо тривіальні оптимізації спроба / остаточно заблокувати перешкоди насправді мають вимірний вплив на вашу програму, ви, мабуть, не повинні використовувати .NET в першу чергу.


10
Це прекрасний момент - порівняйте з іншими пунктами нашого списку, цей має бути незначним. Ми повинні довіряти основним особливостям мови, щоб правильно поводитися та оптимізувати те, що ми можемо контролювати (sql, індекси, алгоритми).
Кобі

3
Подумайте про тугу петлю товариша. Прикладіть цикл, де ви читаєте та десеризуєте об'єкти з потоку даних сокета на ігровому сервері та намагаєтесь стиснути якнайбільше. Отже, ви MessagePack для серіалізації об'єктів замість бінарногоформатора, а ArrayPool <byte> використовуєте замість просто створення байтових масивів тощо. У цих сценаріях, який вплив має декілька (можливо вкладених), спробуйте блокувати блоки в тісному циклі. Деякі оптимізації буде пропущено компілятором, також змінна виняток переходить до Gen0 GC. Я тільки говорю, що є "Деякі" сценарії, де все має вплив.
tcwicks

35

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

Підказки виступу Ріко Маріані: Вартість винятку : коли кидати, а коли не робити

Перший вид вартості - це статична вартість наявності взагалі обробки в коді. Керовані винятки насправді тут порівняно добре, що означає, що статична вартість може бути набагато нижчою, ніж, наприклад, у C ++. Чому це? Ну, статична вартість дійсно виникає у двох видах місць: По-перше, фактичні сайти спробувати / нарешті / зловити / кинути, де є код для цих конструкцій. По-друге, у некерованому коді є цінна вартість, пов’язана з відстеженням усіх об'єктів, які повинні бути знищені у випадку, якщо буде викинуто виняток. Існує велика кількість логіки очищення, яка повинна бути присутнім, і підлість - це навіть той код, який не відповідає '

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

Відповідно до зауваження Кріса Брумма: Існує також вартість, пов’язана з тим, що деяка оптимізація не проводиться JIT за наявності улову


1
Що стосується C ++, це те, що дуже великий шматок стандартної бібліотеки викидає винятки. У них немає нічого факультативного. Ви повинні спроектувати свої об'єкти за допомогою якоїсь політики виключення, і як тільки ви це зробите, більше немає схованих витрат.
Девід Торнлі

Претензії Ріко Маріані абсолютно неправильні для рідного С ++. "статична вартість може бути набагато нижчою, ніж скажімо на C ++" - Це просто не відповідає дійсності. Хоча, я не впевнений, якою була конструкція механізму винятку в 2003 році, коли стаття була написана. C ++ насправді взагалі не коштує, коли винятки не кидаються, незалежно від того, скільки у вас є блоків спробу / лову та де вони є.
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.

У цій версії я помітив різницю у версії налагодження (насправді більше, ніж в іншій версії), але різниці у версії Release не було.

Conclution :
На підставі цих випробувань, я думаюми можемо сказатищо Try / Спіймати робить має незначний вплив на продуктивність.

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 насправді швидша!


1
Я теж це помітив, іноді це швидше із спробою / ловом. Я прокоментував це відповідь Бена. Однак, на відміну від 24 виборців, мені не подобається подібне тестування, я не думаю, що це хороший показник. Код у цьому випадку швидший, але чи завжди це буде?
Кобі

5
Це не доводить, що ваша машина одночасно виконувала різні завдання? Час, що минув, ніколи не є хорошою мірою, вам потрібно використовувати профілер, який записує час процесора, а не минулий час.
Колін Десмонд

2
@Kobi: Я погоджуюсь, що це не найкращий спосіб орієнтування, якщо ви збираєтесь опублікувати це як доказ того, що ваша програма працює швидше, ніж інші або щось подібне, але ви можете, як розробник, вказувати на те, що один метод працює краще, ніж інший . У цьому випадку я думаю, що ми можемо сказати, що відмінності (принаймні для конфігурації випуску) є незнаймими.
трепет

1
Ви тут не терміни try/catch. Ви призначаєте 12 спроб / зловити вхідний критичний розділ проти 10-ти циклів. Шум петлі усуне будь-який вплив, який має спроба / улов. якщо замість цього помістити спробу / ловити всередину тісного циклу і порівняти з / без, ви закінчитеся з вартістю спробу / лову. (без сумніву, таке кодування взагалі не є хорошою практикою, але якщо ви хочете вчасно накладати накладні витрати на конструкцію, саме так ви робите). На сьогоднішній день BenchmarkDotNet - це інструмент для надійних термінів виконання.
Авель

15

Я перевіряв фактичний вплив try..catchу вузькому циклі, і він занадто малий сам по собі, щоб викликати занепокоєння у будь-якій нормальній ситуації.

Якщо цикл робить дуже мало роботи (у моєму тесті я це робив x++), ви можете виміряти вплив обробки винятків. Цикл, за винятком обробки, зайняв приблизно десять разів більше часу.

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


11

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


8

Спроба / зловити HAS впливає на продуктивність.

Але це не має величезного впливу. Складність спробувати / зловити, як правило, O (1), як і просте завдання, за винятком випадків, коли вони розміщені в циклі. Тож вам доведеться їх розумно використовувати.

Ось посилання на продуктивність спробу / лову (хоча це не пояснює складності, але це мається на увазі). Ознайомтеся з розділом " Викиньте менше винятків "


3
Складність O (1), це не означає занадто багато. Наприклад, якщо ви обладнаєте розділ коду, який дуже часто викликається за допомогою пробного вловлювання (або ви згадуєте цикл), O (1) s може в кінцевому підсумку доміряти число.
Csaba Toth

6

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

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

Тепер існує послідовність подій, де оператор3 може виконуватись без виконання2. Навіть якщо нічого в коді для thing2не може викинути виняток, можливо, інший потік міг би скористатися Interlocked.CompareExchangeсповіщенням, яке qбуло очищено, і встановити його Thread.ResetAbort, а потім виконати команду a, Thread.Abort()перш ніж statement2 записав її значення x. Тоді команда the catchExecute Thread.ResetAbort()[через делегата q] дозволить виконувати виконання з оператором3. Така послідовність подій, звичайно, була б надзвичайно малоймовірною, але компілятор необхідний для створення коду, який працює відповідно до специфікацій, навіть коли трапляються такі неймовірні події.

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


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

Див. Дискусію про реалізацію спробу / лову, щоб обговорити, як працюють блоки "пробувати / ловити" та як деякі реалізації мають високі накладні витрати, а деякі - нульові накладні витрати, коли не відбувається жодних винятків. Зокрема, я думаю, що 32-бітова реалізація Windows має високі витрати, а 64-бітова реалізація - ні.


Я описав два різні підходи до впровадження винятків. Підходи однаково застосовуються до C ++ та C #, а також до керованого / некерованого коду. Яких з них обирала MS для свого C #, я точно не знаю, але архітектура управління винятками програм на рівні машин, що надаються MS, використовує більш швидку схему. Я був би трохи здивований, якби реалізація C # для 64 біт не використовувала його.
Іра Бакстер
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.