Насколько дороги исключения в C#?

Насколько дороги исключения в C#? Кажется, что они не невероятно дороги, пока стек не глубокий; однако я прочитал противоречивые сообщения.

Есть определенный отчет, который не был опровергнут?

11 ответов

Решение

Джон Скит написал исключения и производительность в.NET в январе 2006 г.

Какие были обновлены исключения и производительность Redux (спасибо @Gulzar)

На что Рико Мариани присоединился к "Истинной цене исключений.NET" - решение


Также ссылка: Кшиштоф Квалина - Обновление Руководства по проектированию: Исключение

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

Программа обработала менее 29914 миллисекунд, чтобы обработать миллион исключений, что составляет 33 исключения в миллисекунду. Это достаточно быстро, чтобы сделать исключения жизнеспособной альтернативой кодам возврата для большинства ситуаций.

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

Измерено на ноутбуке с Intel Core2 Duo T8100 с частотой 2,1 ГГц и версией.NET 4.0 в сборке релиза, не работающей под отладчиком (что замедлит работу).

Это мой тестовый код:

static void Main(string[] args)
{
    int iterations = 1000000;
    Console.WriteLine("Starting " + iterations.ToString() + " iterations...\n");

    var stopwatch = new Stopwatch();

    // Test exceptions
    stopwatch.Reset();
    stopwatch.Start();
    for (int i = 1; i <= iterations; i++)
    {
        try
        {
            TestExceptions();
        }
        catch (Exception)
        {
            // Do nothing
        }
    }
    stopwatch.Stop();
    Console.WriteLine("Exceptions: " + stopwatch.ElapsedMilliseconds.ToString() + " ms");

    // Test return codes
    stopwatch.Reset();
    stopwatch.Start();
    int retcode;
    for (int i = 1; i <= iterations; i++)
    {
        retcode = TestReturnCodes();
        if (retcode == 1)
        {
            // Do nothing
        }
    }
    stopwatch.Stop();
    Console.WriteLine("Return codes: " + stopwatch.ElapsedMilliseconds.ToString() + " ms");

    Console.WriteLine("\nFinished.");
    Console.ReadKey();
}

static void TestExceptions()
{
    throw new Exception("Failed");
}

static int TestReturnCodes()
{
    return 1;
}

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

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

В моем случае исключения были очень дорогими. Я переписал это:

public BlockTemplate this[int x,int y, int z]
{
    get
    {
        try
        {
            return Data.BlockTemplate[World[Center.X + x, Center.Y + y, Center.Z + z]];
        }
        catch(IndexOutOfRangeException e)
        {
            return Data.BlockTemplate[BlockType.Air];
        }
    }
}

В это:

public BlockTemplate this[int x,int y, int z]
{
    get
    {
        int ix = Center.X + x;
        int iy = Center.Y + y;
        int iz = Center.Z + z;
        if (ix < 0 || ix >= World.GetLength(0)
            || iy < 0 || iy >= World.GetLength(1)
            || iz < 0 || iz >= World.GetLength(2)) 
            return Data.BlockTemplate[BlockType.Air];
        return Data.BlockTemplate[World[ix, iy, iz]];
    }
}

И заметил хорошее увеличение скорости примерно на 30 секунд. Эта функция вызывается как минимум 32K раз при запуске. Код не настолько ясен, как задумано, но экономия была огромной.

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

     for(; ; ) {
        iValue = Level1(iValue);
        lCounter += 1;
        if(DateTime.Now >= sFinish) break;
     }

против

     for(; ; ) {
        try {
           iValue = Level3Throw(iValue);
        }
        catch(InvalidOperationException) {
           iValue += 3;
        }
        lCounter += 1;
        if(DateTime.Now >= sFinish) break;
     }

Разница в 20 раз. Второй фрагмент в 20 раз медленнее.

Просто чтобы поделиться своим личным опытом: я работаю над программой, которая анализирует файлы JSON и извлекает из них данные с помощью NewtonSoft.

Я переписал это:

  • Вариант 1, с исключениями.
try
{
    name = rawPropWithChildren.Value["title"].ToString();
}
catch(System.NullReferenceException)
{
    name = rawPropWithChildren.Name;
}

К этому:

  • Вариант 2, без исключений.
if(rawPropWithChildren.Value["title"] == null)
{
    name = rawPropWithChildren.Name;
}
else
{
    name = rawPropWithChildren.Value["title"].ToString();
}

Конечно, у вас нет контекста, чтобы судить об этом, но вот мои результаты:
(В режиме отладки)

  • Вариант 1, с исключениями.38,50 с

  • Вариант 2, с исключениями.06.48 с

Чтобы дать немного контекста, я работаю с тысячами свойств json, которые могут быть нулевыми. Исключений было выброшено слишком много, например, в течение 15% времени выполнения. Ну, не совсем точно, но их бросали слишком много раз. Я хотел исправить это, поэтому я изменил свой код и не понимал, почему время выполнения было намного быстрее. Это было из-за моей плохой обработки исключений.

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

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

TL; DR;

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

Они бесплатны?Нет.

Вы обычно получаете то, за что платите?В большинстве случаев да.

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

Объяснение:

Меня очень интересуют истоки этого вопроса. Насколько я могу судить, это остаточное отвращение к малопригодным исключениям C ++. Исключения .Net содержат обширную информацию и позволяют создавать аккуратный и аккуратный код без чрезмерных проверок на успех и регистрации. Я объясню большую часть их преимуществ в другом ответе .

За 20 лет программирования я ни разу не убирал бросок или ловлю, чтобы сделать что-то быстрее (не сказать, что я не мог, просто чтобы сказать, что был ниже висящий фрукт, и после этого никто не жаловался).

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

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

Ниже приведен код linqpad для этого теста (включая отрисовку графика).

Но суть здесь в том, что идея о том, что «исключения работают медленно», порождает вопрос « медленнее, чем что?». Если исключение с глубоким стеком стоит 500 микросекунд, имеет ли значение, возникает ли оно в ответ на уникальное ограничение, на создание которого базе данных потребовалось 3000 микросекунд? В любом случае, это демонстрирует обобщенное избегание исключений по соображениям производительности не обязательно приведет к более производительному коду.

Код для проверки производительности:

      void Main()
{
    var loopResults = new List<Results>();
    var exceptionResults = new List<Results>();
    var totalRuns = 10000;
    for (var colCount = 1; colCount < 20; colCount++)
    {
        using (var conn = new SqlConnection(@"Data Source=(localdb)\MSSQLLocalDb;Initial Catalog=master;Integrated Security=True;"))
        {
            conn.Open();

            //create a dummy table where we can control the total columns
            var columns = String.Join(",",
                (new int[colCount]).Select((item, i) => $"'{i}' as col{i}")
            );
            var sql = $"select {columns} into #dummyTable";
            var cmd = new SqlCommand(sql,conn);
            cmd.ExecuteNonQuery();

            var cmd2 = new SqlCommand("select * from #dummyTable", conn);

            var reader = cmd2.ExecuteReader();
            reader.Read();

            Func<Func<IDataRecord, String, Boolean>, List<Results>> test = funcToTest =>
            {
                var results = new List<Results>();
                Random r = new Random();
                for (var faultRate = 0.1; faultRate <= 0.5; faultRate += 0.1)
                {
                    Stopwatch stopwatch = new Stopwatch();
                    stopwatch.Start();
                    var faultCount=0;
                    for (var testRun = 0; testRun < totalRuns; testRun++)
                    {
                        if (r.NextDouble() <= faultRate)
                        {
                            faultCount++;
                            if(funcToTest(reader, "colDNE"))
                                throw new ApplicationException("Should have thrown false");
                        }
                        else
                        {
                            for (var col = 0; col < colCount; col++)
                            {
                                if(!funcToTest(reader, $"col{col}"))
                                    throw new ApplicationException("Should have thrown true");
                            }
                        }
                    }
                    stopwatch.Stop();
                    results.Add(new UserQuery.Results{
                        ColumnCount = colCount,
                        TargetNotFoundRate = faultRate,
                        NotFoundRate = faultCount * 1.0f / totalRuns,
                        TotalTime=stopwatch.Elapsed
                    });
                }
                return results;
            };
            loopResults.AddRange(test(HasColumnLoop));

            exceptionResults.AddRange(test(HasColumnException));

        }

    }
    "Loop".Dump();
    loopResults.Dump();

    "Exception".Dump();
    exceptionResults.Dump();

    var combinedResults = loopResults.Join(exceptionResults,l => l.ResultKey, e=> e.ResultKey, (l, e) => new{ResultKey = l.ResultKey, LoopResult=l.TotalTime, ExceptionResult=e.TotalTime});
    combinedResults.Dump();
    combinedResults
        .Chart(r => r.ResultKey, r => r.LoopResult.Milliseconds * 1.0 / totalRuns, LINQPad.Util.SeriesType.Line)
        .AddYSeries(r => r.ExceptionResult.Milliseconds * 1.0 / totalRuns, LINQPad.Util.SeriesType.Line)
        .Dump();
}
public static bool HasColumnLoop(IDataRecord dr, string columnName)
{
    for (int i = 0; i < dr.FieldCount; i++)
    {
        if (dr.GetName(i).Equals(columnName, StringComparison.InvariantCultureIgnoreCase))
            return true;
    }
    return false;
}

public static bool HasColumnException(IDataRecord r, string columnName)
{
    try
    {
        return r.GetOrdinal(columnName) >= 0;
    }
    catch (IndexOutOfRangeException)
    {
        return false;
    }
}

public class Results
{
    public double NotFoundRate { get; set; }
    public double TargetNotFoundRate { get; set; }
    public int ColumnCount { get; set; }
    public double ResultKey {get => ColumnCount + TargetNotFoundRate;}
    public TimeSpan TotalTime { get; set; }


}

Объекты исключений Barebones в C# довольно легкие; обычно это способность инкапсулировать InnerException это делает его тяжелым, когда дерево объектов становится слишком глубоким.

Что касается окончательного отчета, я не знаю ни одного, хотя краткий профиль dotTrace (или любой другой профилировщик) для потребления памяти и скорости будет довольно легко сделать.

Падение производительности с исключениями, кажется, находится в точке создания объекта исключения (хотя он слишком мал, чтобы вызывать какие-либо проблемы в 90% случаев). Поэтому рекомендуется профилировать ваш код - если исключения вызывают снижение производительности, вы пишете новый высокопроизводительный метод, который не использует исключения. (Пример, который приходит на ум, будет (TryParse введен, чтобы преодолеть проблемы перфорации с Parse, который использует исключения)

При этом исключения в большинстве случаев не приводят к значительному снижению производительности в большинстве ситуаций, поэтому MS Design Guideline должна сообщать о сбоях, создавая исключения

Недавно я измерил исключения C# (throw and catch) в цикле суммирования, который вызывал арифметическое переполнение при каждом добавлении. Выброс арифметического переполнения составил около 8,5 микросекунд = 117 KiloExceptions/sec на четырехъядерном ноутбуке.

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

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

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

1-й. Поскольку большая часть интерфейса пользователя будет отключена от API, генерирующего исключение. например, мобильное приложение, использующее rest api. Тот же API также можно использовать для веб-интерфейса на основе angular js.

В любом сценарии вместо исключения будут предпочтительны коды возврата.

2-й. Теперь хакеры случайным образом пытаются взломать все веб-утилиты. В таком сценарии, если они постоянно атакуют API входа в ваши приложения, и если приложение постоянно генерирует исключение, вы будете иметь дело с тысячами исключений в день. Конечно, многие скажут, что брандмауэр позаботится о таких атаках, однако не все тратят деньги на управление выделенным брандмауэром или дорогостоящей службой защиты от спама, лучше, чтобы ваш код был подготовлен к этим сценариям.

Другие вопросы по тегам