Ответ 1
JIT не выполняет оптимизацию на блоках "protected" / "try", и я предполагаю, что в зависимости от кода, который вы пишете в блоках try/catch, это повлияет на вашу производительность.
private void TryCatchPerformance()
{
int iterations = 100000000;
Stopwatch stopwatch = Stopwatch.StartNew();
int c = 0;
for (int i = 0; i < iterations; i++)
{
try
{
// c += i * (2 * (int)Math.Floor((double)i));
c += i * 2;
}
catch (Exception ex)
{
throw;
}
}
stopwatch.Stop();
WriteLog(String.Format("With try catch: {0}", stopwatch.ElapsedMilliseconds));
Stopwatch stopwatch2 = Stopwatch.StartNew();
int c2 = 0;
for (int i = 0; i < iterations; i++)
{
// c2 += i * (2 * (int)Math.Floor((double)i));
c2 += i * 2;
}
stopwatch2.Stop();
WriteLog(String.Format("Without try catch: {0}", stopwatch2.ElapsedMilliseconds));
}
Выход, который я получаю:
With try catch: 68
Without try catch: 34
Итак, похоже, что использование блока try-catch кажется скорее быстрее?
То, что я нахожу еще более странным, это то, что когда я заменяю вычисление в теле for-loops чем-то более сложным, например: c += i * (2 * (int)Math.Floor((double)i));
Разница гораздо менее драматична.
With try catch: 640
Without try catch: 655
Я делаю что-то неправильно здесь или есть логическое объяснение для этого?
JIT не выполняет оптимизацию на блоках "protected" / "try", и я предполагаю, что в зависимости от кода, который вы пишете в блоках try/catch, это повлияет на вашу производительность.
Сам блок try/catch/finally/fault фактически не имеет накладных расходов в оптимизированной сборке релизов. Хотя часто добавляется дополнительный IL для catch и, наконец, блоков, когда исключение не выбрасывается, мало различий в поведении. Вместо простого ret, обычно есть разрешение на более поздний ret.
Истинная стоимость блоков try/catch/finally возникает при обработке исключения. В таких случаях должно быть создано исключение, должны быть помещены метки обхода стека, и, если обращение к исключению и доступное к нему свойство StackTrace, происходит стекирование стека. Самой тяжелой операцией является трассировка стека, которая следует за ранее установленными метками обхода стека, чтобы создать объект StackTrace, который может использоваться для отображения местоположения, в котором произошла ошибка, и вызовов, через которые он проходил.
Если в блоке try/catch нет поведения, тогда будет преобладать дополнительная стоимость "оставить на ret" или просто "ret", и, очевидно, будет заметная разница. Однако в любой другой ситуации, когда в предложении try есть какое-то поведение, стоимость самого блока будет полностью отменена.
Обратите внимание, что у меня есть только Моно:
// a.cs
public class x {
static void Main() {
int x = 0;
x += 5;
return ;
}
}
// b.cs
public class x {
static void Main() {
int x = 0;
try {
x += 5;
} catch (System.Exception) {
throw;
}
return ;
}
}
Разборка этих:
// a.cs
default void Main () cil managed
{
// Method begins at RVA 0x20f4
.entrypoint
// Code size 7 (0x7)
.maxstack 3
.locals init (
int32 V_0)
IL_0000: ldc.i4.0
IL_0001: stloc.0
IL_0002: ldloc.0
IL_0003: ldc.i4.5
IL_0004: add
IL_0005: stloc.0
IL_0006: ret
} // end of method x::Main
и
// b.cs
default void Main () cil managed
{
// Method begins at RVA 0x20f4
.entrypoint
// Code size 20 (0x14)
.maxstack 3
.locals init (
int32 V_0)
IL_0000: ldc.i4.0
IL_0001: stloc.0
.try { // 0
IL_0002: ldloc.0
IL_0003: ldc.i4.5
IL_0004: add
IL_0005: stloc.0
IL_0006: leave IL_0013
} // end .try 0
catch class [mscorlib]System.Exception { // 0
IL_000b: pop
IL_000c: rethrow
IL_000e: leave IL_0013
} // end handler 0
IL_0013: ret
} // end of method x::Main
Основное различие, которое я вижу, - a.cs, идет прямо к ret
в IL_0006
, тогда как b.cs должно leave IL_0013
в IL_006
. Мое лучшее предположение с моим примером состоит в том, что leave
является (относительно) дорогим прыжком при компиляции в машинный код - это может быть или не быть, особенно в вашем цикле for. То есть, у try-catch нет неотъемлемых накладных расходов, но перепрыгивание через улов имеет стоимость, как любая условная ветвь.
фактическое вычисление настолько минимально, что точные измерения очень сложны. Мне кажется, что попытка catch может добавить очень небольшое фиксированное количество дополнительного времени в рутину. Мне было бы сложно догадаться, не зная ничего о том, как исключения реализованы на С#, что это в основном просто инициализация путей исключений и, возможно, небольшая нагрузка на JIT.
Для любого фактического использования время, затрачиваемое на вычисление, будет настолько перегружать время, затраченное на попытку попыток, что стоимость try-catch может быть принята как почти нулевая.
Проблема первая в вашем тестовом коде. вы использовали секундомер .Elapsed.Milliseconds, который показывает только миллисекунду часть времени Истекшее время, используйте TotalMilliseconds, чтобы получить всю часть...
Если исключение не выбрасывается, разница минимальна
Но реальный вопрос: "Нужно ли проверять исключения или позволить С# обрабатывать бросок исключения?"
Ясно... ручка одна... Попробуйте запустить это:
private void TryCatchPerformance()
{
int iterations = 10000;
textBox1.Text = "";
Stopwatch stopwatch = Stopwatch.StartNew();
int c = 0;
for (int i = 0; i < iterations; i++)
{
try
{
c += i / (i % 50);
}
catch (Exception)
{
}
}
stopwatch.Stop();
Debug.WriteLine(String.Format("With try catch: {0}", stopwatch.Elapsed.TotalSeconds));
Stopwatch stopwatch2 = Stopwatch.StartNew();
int c2 = 0;
for (int i = 0; i < iterations; i++)
{
int iMod50 = (i%50);
if(iMod50 > 0)
c2 += i / iMod50;
}
stopwatch2.Stop();
Debug.WriteLine( String.Format("Without try catch: {0}", stopwatch2.Elapsed.TotalSeconds));
}
Вывод: ОБОРОТ: посмотрите ниже! С try catch: 1.9938401
Без try catch: 8.92E-05
Удивительные, только 10000 объектов, с 200 исключениями.
КОРРЕКЦИЯ: Я запустил свой код в окне DEBUG и VS Written Exception to Output.. Это результаты RELEASE Гораздо меньше накладных расходов, но все же улучшение на 7500%.
С попыткой catch: 0.0546915
Проверка одного: 0.0007294
С try catch Выбрасывание моего собственного объекта Exception: 0.0265229
См. обсуждение реализации try/catch для обсуждения того, как работают блоки try/catch, и как некоторые реализации имеют высокие накладные расходы, а некоторые имеют нулевые служебные данные, когда не происходит никаких исключений.
Разница всего в 34 миллисекунды меньше, чем погрешность ошибки для теста, подобного этому.
Как вы заметили, когда вы увеличиваете продолжительность теста, разница просто отпадает, а производительность двух наборов кода фактически одинакова.
При выполнении такого рода тестов я пытаюсь прокрутить каждую часть кода в течение как минимум 20 секунд, предпочтительно дольше и в идеале в течение нескольких часов.