Наскільки повільні винятки .NET?


143

Я не хочу обговорювати питання про те, коли слід і не кидати винятки. Я хочу вирішити просту проблему. У 99% випадків аргумент щодо некидання винятків обертається навколо того, що вони повільні, тоді як інша сторона стверджує (з контрольним тестом), що швидкість - це не проблема. Я читав численні блоги, статті та публікації, що стосуються тієї чи іншої сторони. Так що це?

Деякі посилання з відповідей: Skeet , Mariani , Brumme .


13
є брехня, чортова брехня і орієнтири. :)
gbjbaanb

На жаль, кілька відповідей з високими голосами тут пропустили, що питання задає "наскільки повільні винятки?", І спеціально попросили уникати теми, як часто ними користуватися. Один простий відповідь на запитання, яке, як насправді, задали, це ..... У Windows CLR, винятки в 750 разів повільніше, ніж значення повернення.
Девід Джеске

Відповіді:


207

Я на «не повільній» стороні, а точніше - недостатньо повільно, щоб варто уникати їх у звичайному користуванні ». Я написав про це дві короткі статті . Існують критики щодо аспекту орієнтиру, які здебільшого полягають у тому, що "в реальному житті було б більше стека, щоб пройти, щоб ви підірвали кеш і т. Д.", - але використання кодів помилок для роботи над стеком також підірвати кеш, тому я не вважаю це особливо гарним аргументом.

Просто для того, щоб було зрозуміло - я не підтримую винятки, коли вони не логічні. Наприклад, int.TryParseцілком підходить для перетворення даних від користувача. Це підходить при читанні машинного файлу, де помилка означає "Файл не в тому форматі, який він мав бути, я дійсно не хочу намагатися впоратися з цим, оскільки я не знаю, що ще може бути не так. "

Використовуючи винятки в "лише розумних обставинах", я ніколи не бачив програми, продуктивність якої була значно погіршена винятками. В основному винятки не повинні траплятися часто, якщо у вас не виникають значні проблеми з коректністю, і якщо у вас є значні проблеми з коректністю, то продуктивність не є найбільшою проблемою, з якою ви стикаєтеся.


2
на жаль, людям кажуть, що винятки безкоштовні, використовуйте їх для тривіальної 'правильної' функціональності, їх слід використовувати, як ви говорите, коли все пішло не так - у "виняткових" обставинах
gbjbaanb

4
Так, люди, безумовно, повинні мати на увазі, що існує вартість ефективності, пов’язана з невідповідним використанням виключень. Я просто думаю, що це не проблема, коли вони будуть використані належним чином :)
Джон Скіт

7
@PaulLockwood: Я б сказав, що якщо у вас є 200+ винятків за секунду , ви зловживаєте винятками. Очевидно, це не "виняткова" подія, якщо вона відбувається 200 разів за секунду. Зверніть увагу на останнє речення відповіді: "В основному винятки не повинні траплятися часто, якщо у вас не виникають значні проблеми з коректністю, і якщо у вас є значні проблеми з коректністю, то продуктивність не є найбільшою проблемою, з якою ви стикаєтеся".
Джон Скіт

4
@PaulLockwood: Думаю, що якщо у вас є 200+ винятків за секунду, це, ймовірно, вже вказує на те, що ви зловживаєте винятками. Мене не дивує, що це часто буває, але це означає, що аспект продуктивності не буде моєю першою турботою - зловживання винятками буде. Як тільки я видалив усі неадекватні можливості використання винятків, я не очікував, що вони будуть значною частиною продуктивності.
Джон Скіт

4
@DavidJeske: Ви пропустили точку відповіді. Очевидно, що кидати виняток набагато повільніше, ніж повертати нормальне значення. Ніхто цього не сперечається. Питання в тому, чи вони занадто повільні. Якщо вам підходить ситуація, коли викинете виняток, і це спричиняє проблеми з продуктивністю, то, ймовірно, у вас є більші проблеми - тому що це говорить про те, що у вашій системі існує велика кількість помилок . Зазвичай проблема справді полягає в тому, що ви використовуєте винятки, коли вони недоцільно починати.
Джон Скіт

31

На це є остаточна відповідь від хлопця, який їх реалізував - Кріса Брумма. Він написав чудову статтю в блозі про цю тему (попередження - дуже довге) (попередження2 - це дуже добре написано, якщо ви технік, ви прочитаєте його до кінця, а потім доведеться компенсувати години після роботи :) )

Резюме: вони повільні. Вони реалізовані як винятки Win32 SEH, тож деякі навіть перейдуть кордон 0 CPU до меж! Очевидно, що в реальному світі ви будете робити багато інших робіт, тому випадковий виняток взагалі не буде помічений, але якщо ви використовуєте їх для потоку програми, очікуйте, що ваш додаток буде забитий. Це ще один приклад роботи з маркетинговою машиною MS, яка робить нас незаслужними. Я пригадую один мікрософт, який розповідав нам, як вони зазнали абсолютно нульових накладних витрат, що є повним гріхом.

Кріс дає відповідну цитату:

Насправді, CLR внутрішньо використовує винятки навіть у некерованих частинах двигуна. Однак існує серйозна проблема довгострокової роботи з винятками, і це необхідно враховувати у вашому рішенні.


Я можу згадати про це в реальних тестах, коли нульовий тип викликає багаторазовий виняток у "це нормальний потік програми", який закінчився значними проблемами з продуктивністю. Але пам’ятайте, винятки є для виняткових випадків. Не вірте нікому, хто каже інше, або ви закінчитеся з такою ніткою Github!
gbjbaanb

8

Я поняття не маю, про що говорять люди, коли кажуть, що вони повільні, лише якщо їх кидають.

EDIT: Якщо Винятки не кидаються, це означає, що ви робите новий Exception () або щось подібне. Інакше виняток призведе до того, що нитка буде призупинена, а стек буде пройдено. У невеликих ситуаціях це може бути нормально, але на веб-сайтах з високим трафіком покладання на винятки, оскільки робочий процес або механізм шляху виконання, безумовно, викличуть у вас проблеми з роботою. Винятки, самі по собі, не є поганими і корисні для вираження виняткових умов

Робочий процес виключення в додатку .NET використовує винятки першого та другого шансів. За всіма винятками, навіть якщо ви їх ловите та обробляєте, об’єкт винятку все-таки створюється, і рамка все одно мусить ходити стеком, щоб шукати обробник. Якщо ви ловите і повторно скидаєте, звичайно, що триватиме більше часу - ви отримаєте виняток з першого шансу, ловите його, повторно скидаєте, спричиняючи ще одне виключення з першої шанси, яке потім не знаходить обробника, який потім викликає виняток другого шансу.

Виняток становлять також об’єкти в купі - тому якщо ви кидаєте тонни винятків, то ви викликаєте як продуктивність, так і проблеми з пам’яттю.

Крім того, згідно моєї копії "Тестування продуктивності веб-додатків Microsoft .NET", написаної командою ACE:

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

Мій власний досвід у цій галузі показав, що скорочення винятків суттєво сприяло роботі. Звичайно, є й інші речі, які ви враховуєте під час тестування продуктивності - наприклад, якщо ваш дисковий введення / вивід знімається або ваші запити знаходяться за лічені секунди, то це має бути вашим фокусом. Але пошук та усунення винятків має бути важливою частиною цієї стратегії.


1
Ніщо, що ви написали, не суперечить твердженню, що винятки є лише повільними, якщо їх кидають. Ви говорили тільки про тих ситуаціях , коли вони будуть кинуті. Коли ви "значно допомогли в продуктивності", видаливши винятки: 1) Чи були вони справжніми помилками чи просто помилками користувача ?
Джон Скіт

2) Ви бігали під налагоджувачем, чи ні?
Джон Скіт

Єдине можливе, що ви можете зробити за винятком, якщо не кинути його, це створити його як об'єкт, що безглуздо. Перебувати під налагоджувачем чи не має значення - це все одно буде повільніше. Так, є гачки, які відбуватимуться із доданим налагоджувачем, але це все ще повільно
Cory Foy

4
Я знаю - я був частиною команди Прем'єр-міністра в MSFT. :) Скажімо, багато - тисячі секунди в деяких екстремальних випадках, які ми бачили. Нічого подібного до з’єднання з живим налагоджувачем і просто бачити винятки так швидко, як ви могли прочитати. Екс повільні - так це підключення до БД, тому ви робите це, коли це має сенс.
Cory Foy

5
Кори, я думаю, що "лише повільно, коли їх кидають" полягає в тому, що вам не доведеться турбуватися про продуктивність через просто наявність улову / нарешті блокує. Тобто вони самі по собі не спричиняють показника ефективності, а лише випадки фактичного виключення.
Ян Хорвілл

6

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

Часто в звичайній логіці програми ви виконуєте циклічне циклічне повторення, коли одна і та ж дія повторюється тисячі / мільйони разів. У цьому випадку, за допомогою дуже простого профілювання (див. Клас "Секундомір"), ви можете самі бачити, що викидання виключення замість того, щоб сказати просте, якщо заява може виявитися значно повільніше.

Насправді я одного разу прочитав, що команда .NET в Microsoft представила методи TryXXXXX в .NET 2.0 для багатьох базових типів FCL саме тому, що клієнти скаржилися на те, що продуктивність їхніх додатків була настільки повільною.

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

Тепер Microsoft рекомендує методи TryXXX слід застосовувати особливо в цій ситуації, щоб уникнути таких можливих проблем з продуктивністю.

Я можу помилитися, але це здається, що ви не впевнені в правдивості "тестів", про які ви читали. Просте рішення: Спробуйте самі.


Я подумав, що між ними ці "пробні" функції також використовують винятки?
greg

1
Ці функції "Спробувати" не викидають винятки внутрішньо за невдало проаналізувати вхідне значення. Однак вони все ще кидають винятки для інших ситуацій помилок, таких як ArgumentException.
Еш,

Я думаю, що ця відповідь наближається до суті питання, ніж будь-яка інша. Скажімо, що "використовувати винятки лише в розумних обставинах" насправді не дає відповіді на питання - справжнє розуміння полягає в тому, що використання винятків C # для потоку управління набагато повільніше, ніж звичайні умовні конструкції. Вам можуть бути прощені думки про інше. У OCaml винятки - це більш-менш GOTO та прийнятий спосіб реалізації перерви при використанні імперативних функцій. У моєму конкретному випадку заміна в щільному циклі int.Parse () плюс try / catch vs. int.TryParse () дала значне підвищення продуктивності.
Х'ю Ш

4

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


3
Цифри були б корисні, на жаль: (Такі речі, як операції з сокетом, повинні значно перевищувати витрати на винятки, звичайно, коли вони не налагоджуються. Якщо ви коли-небудь орієнтуєтеся на це в повному обсязі, мені буде дуже цікаво побачити результати.
Jon Skeet

3

Просто додати свій власний останній досвід до цієї дискусії: відповідно до більшості написаного вище, я виявив, що викиди винятків є надзвичайно повільними, коли виконуються повторно, навіть без налагодження роботи. Я просто збільшив продуктивність великої програми, яку я пишу на 60%, змінивши приблизно п’ять рядків коду: перехід на модель повернення коду замість того, щоб викидати винятки. Звичайно, відповідний код працював тисячі разів і, можливо, кидав тисячі винятків, перш ніж я його змінив. Тож я згоден з твердженням вище: кидайте винятки, коли щось важливе насправді йде не так, а не як спосіб контролю потоку додатків у будь-яких "очікуваних" ситуаціях.


2

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

Їх напевно варто використовувати над кодами помилок, переваги - величезні ІМО.


2

У мене ніколи не було проблем із продуктивністю з винятками. Я багато використовую винятки - ніколи не використовую коди повернення, якщо можу. Вони є поганою практикою, і, на мою думку, пахнуть кодом спагетті.

Я думаю, що все зводиться до того, як ви використовуєте винятки: якщо ви використовуєте їх, як коди повернення (кожен виклик методу в стеку ловить і знову відновлює), то так, вони будуть повільними, тому що у вас є накладні витрати кожного кожного лову / кидка.

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

Зрештою, вони є дійсною мовною ознакою.

Просто щоб довести свою думку

Будь ласка, запустіть код за цим посиланням (занадто великий для відповіді).

Результати на моєму комп’ютері:

marco@sklivvz:~/develop/test$ mono Exceptions.exe | grep PM
10/2/2008 2:53:32 PM
10/2/2008 2:53:42 PM
10/2/2008 2:53:52 PM

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


2

Але моно викидає виключення на 10 разів швидше, ніж автономний режим .net, а автономний режим .net викидає виняток на 60 разів швидше, ніж режим налагодження .net. (Тестові машини мають таку ж модель процесора)

int c = 1000000;
int s = Environment.TickCount;
for (int i = 0; i < c; i++)
{
    try { throw new Exception(); }
    catch { }
}
int d = Environment.TickCount - s;

Console.WriteLine(d + "ms / " + c + " exceptions");

1

У режимі випуску накладні витрати мінімальні.

Якщо ви не будете використовувати винятки для контролю потоку (наприклад, немісцеві виходи) рекурсивно, я сумніваюся, ви зможете помітити різницю.


1

У CLR Windows, для ланцюжка викликів на глибині 8, викидання винятку відбувається в 750 разів повільніше, ніж перевірка та поширення повернутого значення. (див. нижче для орієнтирів)

Ця висока ціна на винятки пояснюється тим, що CLR Windows інтегрується з тим, що називається Windows Structured Exception Handling . Це дає змогу належним чином вилучити викиди та перенести їх на різні версії та мови. Однак це дуже дуже повільно.

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

Ось скорочені результати з мого еталону винятків та повернених значень для CLR Windows.

baseline: recurse_depth 8, error_freqeuncy 0 (0), time elapsed 13.0007 ms
baseline: recurse_depth 8, error_freqeuncy 0.25 (0), time elapsed 13.0007 ms
baseline: recurse_depth 8, error_freqeuncy 0.5 (0), time elapsed 13.0008 ms
baseline: recurse_depth 8, error_freqeuncy 0.75 (0), time elapsed 13.0008 ms
baseline: recurse_depth 8, error_freqeuncy 1 (0), time elapsed 14.0008 ms
retval_error: recurse_depth 5, error_freqeuncy 0 (0), time elapsed 13.0008 ms
retval_error: recurse_depth 5, error_freqeuncy 0.25 (249999), time elapsed 14.0008 ms
retval_error: recurse_depth 5, error_freqeuncy 0.5 (499999), time elapsed 16.0009 ms
retval_error: recurse_depth 5, error_freqeuncy 0.75 (999999), time elapsed 16.001 ms
retval_error: recurse_depth 5, error_freqeuncy 1 (999999), time elapsed 16.0009 ms
retval_error: recurse_depth 8, error_freqeuncy 0 (0), time elapsed 20.0011 ms
retval_error: recurse_depth 8, error_freqeuncy 0.25 (249999), time elapsed 21.0012 ms
retval_error: recurse_depth 8, error_freqeuncy 0.5 (499999), time elapsed 24.0014 ms
retval_error: recurse_depth 8, error_freqeuncy 0.75 (999999), time elapsed 24.0014 ms
retval_error: recurse_depth 8, error_freqeuncy 1 (999999), time elapsed 24.0013 ms
exception_error: recurse_depth 8, error_freqeuncy 0 (0), time elapsed 31.0017 ms
exception_error: recurse_depth 8, error_freqeuncy 0.25 (249999), time elapsed 5607.3208     ms
exception_error: recurse_depth 8, error_freqeuncy 0.5 (499999), time elapsed 11172.639  ms
exception_error: recurse_depth 8, error_freqeuncy 0.75 (999999), time elapsed 22297.2753 ms
exception_error: recurse_depth 8, error_freqeuncy 1 (999999), time elapsed 22102.2641 ms

І ось код ..

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace ConsoleApplication1 {

public class TestIt {
    int value;

    public class TestException : Exception { } 

    public int getValue() {
        return value;
    }

    public void reset() {
        value = 0;
    }

    public bool baseline_null(bool shouldfail, int recurse_depth) {
        if (recurse_depth <= 0) {
            return shouldfail;
        } else {
            return baseline_null(shouldfail,recurse_depth-1);
        }
    }

    public bool retval_error(bool shouldfail, int recurse_depth) {
        if (recurse_depth <= 0) {
            if (shouldfail) {
                return false;
            } else {
                return true;
            }
        } else {
            bool nested_error = retval_error(shouldfail,recurse_depth-1);
            if (nested_error) {
                return true;
            } else {
                return false;
            }
        }
    }

    public void exception_error(bool shouldfail, int recurse_depth) {
        if (recurse_depth <= 0) {
            if (shouldfail) {
                throw new TestException();
            }
        } else {
            exception_error(shouldfail,recurse_depth-1);
        }

    }

    public static void Main(String[] args) {
        int i;
        long l;
        TestIt t = new TestIt();
        int failures;

        int ITERATION_COUNT = 1000000;


        // (0) baseline null workload
        for (int recurse_depth = 2; recurse_depth <= 10; recurse_depth+=3) {
            for (float exception_freq = 0.0f; exception_freq <= 1.0f; exception_freq += 0.25f) {            
                int EXCEPTION_MOD = (exception_freq == 0.0f) ? ITERATION_COUNT+1 : (int)(1.0f / exception_freq);            

                failures = 0;
                DateTime start_time = DateTime.Now;
                t.reset();              
                for (i = 1; i < ITERATION_COUNT; i++) {
                    bool shoulderror = (i % EXCEPTION_MOD) == 0;
                    t.baseline_null(shoulderror,recurse_depth);
                }
                double elapsed_time = (DateTime.Now - start_time).TotalMilliseconds;
                Console.WriteLine(
                    String.Format(
                      "baseline: recurse_depth {0}, error_freqeuncy {1} ({2}), time elapsed {3} ms",
                        recurse_depth, exception_freq, failures,elapsed_time));
            }
        }


        // (1) retval_error
        for (int recurse_depth = 2; recurse_depth <= 10; recurse_depth+=3) {
            for (float exception_freq = 0.0f; exception_freq <= 1.0f; exception_freq += 0.25f) {            
                int EXCEPTION_MOD = (exception_freq == 0.0f) ? ITERATION_COUNT+1 : (int)(1.0f / exception_freq);            

                failures = 0;
                DateTime start_time = DateTime.Now;
                t.reset();              
                for (i = 1; i < ITERATION_COUNT; i++) {
                    bool shoulderror = (i % EXCEPTION_MOD) == 0;
                    if (!t.retval_error(shoulderror,recurse_depth)) {
                        failures++;
                    }
                }
                double elapsed_time = (DateTime.Now - start_time).TotalMilliseconds;
                Console.WriteLine(
                    String.Format(
                      "retval_error: recurse_depth {0}, error_freqeuncy {1} ({2}), time elapsed {3} ms",
                        recurse_depth, exception_freq, failures,elapsed_time));
            }
        }

        // (2) exception_error
        for (int recurse_depth = 2; recurse_depth <= 10; recurse_depth+=3) {
            for (float exception_freq = 0.0f; exception_freq <= 1.0f; exception_freq += 0.25f) {            
                int EXCEPTION_MOD = (exception_freq == 0.0f) ? ITERATION_COUNT+1 : (int)(1.0f / exception_freq);            

                failures = 0;
                DateTime start_time = DateTime.Now;
                t.reset();              
                for (i = 1; i < ITERATION_COUNT; i++) {
                    bool shoulderror = (i % EXCEPTION_MOD) == 0;
                    try {
                        t.exception_error(shoulderror,recurse_depth);
                    } catch (TestException e) {
                        failures++;
                    }
                }
                double elapsed_time = (DateTime.Now - start_time).TotalMilliseconds;
                Console.WriteLine(
                    String.Format(
                      "exception_error: recurse_depth {0}, error_freqeuncy {1} ({2}), time elapsed {3} ms",
                        recurse_depth, exception_freq, failures,elapsed_time));         }
        }
    }
}


}

5
Окрім пропуску точки питання, будь ласка, не використовуйте DateTime.Now для орієнтирів - використовуйте секундомір, призначений для вимірювання минулого часу. Тут це не повинно бути проблемою, оскільки ви вимірюєте досить тривалі періоди часу, але варто ввійти в звичку.
Джон Скіт

Навпаки, питання "чи є винятки повільними", період. Він спеціально просив уникати теми, коли потрібно кидати винятки, оскільки ця тема затьмарює факти. Які результати винятків?
Девід Джеске

0

Ось швидка примітка щодо продуктивності, пов'язаної із вилученням винятків.

Коли шлях виконання входить у блок "спробувати", нічого магічного не відбувається. Немає інструкції "спробувати", і жодних витрат, пов'язаних із входом або виходом із блоку спробу. Інформація про спробу блоку зберігається у метаданих методу, і ці метадані використовуються під час виконання під час кожного винятку. Двигун виконання йде по стеку, шукаючи перший дзвінок, який містився у блоці спробу. Будь-які накладні витрати, пов’язані з обробкою винятків, виникають лише тоді, коли викиди винятків


1
Однак наявність винятків може впливати на оптимізацію - методи з явними обробниками винятків важче вбудувати, і переупорядкування інструкцій ними обмежено.
Еймон Нербонна

-1

Під час написання занять / функцій для використання іншими, здається, важко сказати, коли винятки підходять. Є кілька корисних частин BCL, які мені довелося піти і піти на пінове, тому що вони викидають винятки замість повернення помилок. У деяких випадках ви можете обійти його, але для інших, таких як System.Management and Counter Counter, є звичаї, в яких потрібно робити петлі, в яких BCL часто кидає винятки.

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

У моєму власному коді винятки викидаються лише тоді, коли речі настільки виняткові, що потрібно переглянути погляд стека і побачити, що пішло не так, а потім виправити це. Тому я, як правило, переписав частини BCL для використання помилок на основі шаблону Try .. замість виключень.


2
Це, мабуть, не відповідає заяві плаката " Я не хочу обговорювати питання про те, коли робити та не кидати винятки ".
hrbrmstr
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.