Насколько дороги исключения в 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 входа в ваши приложения, и если приложение постоянно генерирует исключение, вы будете иметь дело с тысячами исключений в день. Конечно, многие скажут, что брандмауэр позаботится о таких атаках, однако не все тратят деньги на управление выделенным брандмауэром или дорогостоящей службой защиты от спама, лучше, чтобы ваш код был подготовлен к этим сценариям.