Насколько медленны исключения .NET?

143

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

Некоторые ссылки из ответов: Skeet , Mariani , Brumme .

Горан
источник
13
Есть ложь, проклятая ложь и ориентиры. :)
gbjbaanb
К сожалению, несколько ответов с высоким рейтингом здесь пропустили вопрос о том, «насколько медленны исключения?», И специально попросили избежать темы, как часто их использовать. Один простой ответ на вопрос, который фактически задан: ..... В Windows CLR исключения в 750 раз медленнее возвращаемых значений.
Дэвид Йеске

Ответы:

207

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

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

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

Джон Скит
источник
2
к сожалению, людям говорят, что исключения бесплатны, используйте их для тривиальной «правильной» функциональности, их следует использовать, как вы говорите, когда что-то пошло не так - в «исключительных» обстоятельствах
gbjbaanb
4
Да, люди, безусловно, должны знать, что затраты на производительность связаны с неправильным использованием исключений. Я просто думаю , что это не проблема , когда они будут использоваться надлежащим образом :)
Jon тарелочкам
7
@PaulLockwood: я бы сказал, что если у вас более 200 исключений в секунду , вы злоупотребляете исключениями. Это явно не «исключительное» событие, если оно происходит 200 раз в секунду. Обратите внимание на последнее предложение ответа: «По сути, исключения не должны происходить часто, если у вас нет существенных проблем с корректностью, и если у вас есть существенные проблемы с корректностью, то производительность не самая большая проблема, с которой вы сталкиваетесь».
Джон Скит
4
@PaulLockwood: Я хочу сказать, что если у вас более 200 исключений в секунду, это, вероятно, уже означает, что вы злоупотребляете исключениями. Меня не удивляет, что это часто так, но это означает, что аспект производительности не будет моей первой заботой - злоупотребление исключениями. После того, как я удалил все неуместные использования исключений, я не ожидал, что они станут значительной частью производительности.
Джон Скит
4
@DavidJeske: Вы упустили смысл ответа. Очевидно, что генерировать исключение намного медленнее, чем возвращать нормальное значение. Никто не спорит об этом. Вопрос в том, слишком ли они медленные. Если вы находитесь в подходящей ситуации для метания исключения и это вызывает проблемы с производительностью, то вы , вероятно , есть большие проблемы - потому что это говорит о том , что существует огромное количество неправильно с вашей системой. Как правило, проблема в том , действительно , что вы используете исключения , когда они неуместны , чтобы начать с.
Джон Скит
31

На это есть однозначный ответ от парня, который их реализовал, - Крис Брамм. Он написал отличную статью в блоге на эту тему (предупреждение - это очень долго) (warning2 - это очень хорошо написано, если вы технарь, вы прочтете его до конца, а затем должны наверстать упущенное после работы :) )

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

Крис приводит соответствующую цитату:

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

gbjbaanb
источник
Я могу упомянуть это в реальных тестах, где обнуляемый тип вызывает многократно вызываемое исключение в «это нормальный поток программ», что приводит к значительным проблемам с производительностью. Alwyas помнят, исключения для исключительных случаев, не верьте никому, кто скажет иначе, или у вас получится такая тема на github!
gbjbaanb
8

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

РЕДАКТИРОВАТЬ: Если исключения не выбрасываются, то это означает, что вы делаете новое исключение () или что-то в этом роде. В противном случае исключение приведет к приостановке потока и обходу стека. Это может быть хорошо в небольших ситуациях, но на сайтах с большим трафиком использование исключений в качестве рабочего процесса или механизма пути выполнения, безусловно, вызовет проблемы с производительностью. Исключения сами по себе неплохие и полезны для выражения исключительных условий.

Рабочий процесс исключений в приложении .NET использует исключения первого и второго шансов. Для всех исключений, даже если вы перехватываете их и обрабатываете, объект исключения все еще создается, и фреймворк все еще должен пройтись по стеку, чтобы найти обработчик. Если вы ловите и перебрасываете, конечно, это займет больше времени - вы получите исключение первого шанса, перехватите его, сбросьте его, вызвав еще одно исключение первого шанса, которое затем не найдет обработчик, который затем вызывает исключение второго шанса.

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

Кроме того, согласно моей копии «Тестирование производительности веб-приложений Microsoft .NET», написанной командой ACE:

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

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

Кори Фой
источник
1
Ничто из того, что вы написали, не противоречит утверждению о том, что исключения являются медленными, только если они выбрасываются. Вы говорили только о тех ситуациях , когда они будут брошены. Когда вы «значительно повысили производительность», удалив исключения: 1) Были ли они истинными ошибочными условиями или просто ошибкой пользователя ?
Джон Скит
2) Вы работали под отладчиком или нет?
Джон Скит
Единственное, что вы можете сделать с исключением, если не выбросить его, это создать его как объект, который не имеет смысла. Находиться под отладчиком или нет, не имеет значения - все равно будет медленнее. Да, есть ловушки, которые будут происходить с подключенным отладчиком, но он все еще медленный
Cory Foy
4
Я знаю - я был частью команды Premier в MSFT. :) Скажем так, много - тысячи секунд в некоторых крайних случаях, которые мы видели. Ничего подобного соединению с живым отладчиком и просто просмотру исключений так быстро, как вы могли читать. Ex медленны - поэтому подключается к БД, поэтому вы делаете это, когда это имеет смысл.
Кори Фой
5
Кори, я думаю, что смысл «только медленно, когда их бросают» в том, что вам не нужно беспокоиться о производительности из-за простого присутствия блоков catch / finally. Т.е. сами по себе они не приводят к снижению производительности, а только к появлению фактического экземпляра исключения.
Ян Хорвилл
6

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

Часто в обычной логике приложения вы выполняете цикл, где одно и то же действие повторяется тысячи / миллионы раз. В этом случае, с помощью некоторого очень простого профилирования (см. Класс Stopwatch), вы можете сами убедиться, что генерирование исключения вместо простого оператора if может оказаться существенно медленнее.

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

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

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

Я могу ошибаться, но, похоже, вы не уверены в правдивости тех «эталонов», о которых вы читали. Простое решение: попробуйте сами.

ясень
источник
Я думал, что все эти функции "try" тоже используют исключения?
Грег
1
Эти функции «Try» не генерируют внутренние исключения для сбоя при анализе входного значения. Однако они по-прежнему генерируют исключения для других ситуаций с ошибками, таких как ArgumentException.
Ash
Я думаю, что этот ответ становится ближе к сути проблемы, чем любой другой. Утверждение «использовать исключения только в разумных обстоятельствах» на самом деле не отвечает на этот вопрос - реальное понимание состоит в том, что использование исключений C # для потока управления намного медленнее, чем обычные условные конструкции. Вы могли бы быть прощены за то, что думали иначе. В OCaml исключения являются более или менее GOTO и приемлемым способом реализации разрыва при использовании императивных функций. В моем конкретном случае замена в тесном цикле int.Parse () плюс try / catch против int.TryParse () позволила значительно повысить производительность.
Хью W
4

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

Джонатан С Дикинсон
источник
3
К сожалению, числа были бы полезны :( Такие вещи, как операции с сокетами, должны значительно перевешивать затраты на исключение, конечно, когда не выполняется отладка. Если вы когда-нибудь сравните их полностью, мне было бы очень интересно увидеть результаты.
Джон Скит,
3

Просто чтобы добавить свой собственный недавний опыт к этому обсуждению: в соответствии с большей частью того, что написано выше, я обнаружил, что генерирование исключений является чрезвычайно медленным при повторном выполнении, даже без отладчика. Я только увеличил производительность большой программы, которую я пишу, на 60%, изменив примерно пять строк кода: переключение на модель кода возврата вместо генерирования исключений. Конечно, этот код выполнялся тысячи раз и потенциально вызывал тысячи исключений, прежде чем я его изменил. Поэтому я согласен с приведенным выше утверждением: генерировать исключения, когда что-то важное действительно идет не так, а не как способ управления потоком приложений в любых «ожидаемых» ситуациях.

Рэй Присамент
источник
2

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

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

Quibblesome
источник
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

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

Sklivvz
источник
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");
linquize
источник
1

В режиме выпуска накладные расходы минимальны.

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

leppie
источник
1

В Windows CLR для цепочки вызовов глубины 8 создание исключения в 750 раз медленнее, чем проверка и распространение возвращаемого значения. (см. ниже для ориентиров)

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

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

Вот сокращенные результаты моего теста исключений и возвращаемых значений для Windows CLR.

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

Небольшое замечание о производительности, связанной с отловом исключений.

Когда путь выполнения входит в блок try, ничего волшебного не происходит. Здесь нет инструкции try и никаких затрат, связанных с входом или выходом из блока try. Информация о блоке try хранится в метаданных метода, и эти метаданные используются во время выполнения всякий раз, когда возникает исключение. Механизм выполнения перемещается по стеку в поисках первого вызова, который содержался в блоке try. Любые накладные расходы, связанные с обработкой исключений, возникают только при возникновении исключений.

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

При написании классов / функций для использования другими, кажется, трудно сказать, когда уместны исключения. Есть несколько полезных частей BCL, которые мне пришлось бросить и пойти на пинвоук, потому что они выдают исключения, а не возвращают ошибки. В некоторых случаях вы можете обойти это, но для других, таких как System.Management и Performance Counters, есть случаи, когда вам нужно делать циклы, в которых BCL часто генерирует исключения.

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

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

Анонимный трус
источник
2
Похоже, что это не соответствует заявлению автора « Я не хочу обсуждать, когда следует и не выбрасывать исключения ».
hrbrmstr