Насколько try-catch влияет на производительность

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті

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

Внутри .Net-сборки

В сборке,если проигнорировать заголовки, имеются 3 составляющие:

-Байт-код(Bytecode): Низкоуровневые инструкции,которые определяют тело методов.

-Метаданные(MetaData): Набор таблиц,описывающих более высокий уровень конструкций,например таблицы классов, методов и т.д.
(Думаю вы слышали о таблицах,например — VMT(virtual method table) )

-Кучи(Heaps): Места,где хранятся строковые константы и другие подобные вещи.

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

Как хранятся обработчики исключений.

В .NET CLR есть такое понятие как "защищенный регион«(protection region). Такие регионы являют собой последовательность инструкций, которые имеют соответствующий обработчик. В C# или VB.Net инструкции в «защищенном регионе» являются кодом в блоке try. Существует много разных обработчиков, в том числе типизированных, которые ловят только определенные исключения.

Обратите вниманиена то,что блок try-catch-finally имеет 2 таких региона:
-один на блок try;
-второй на блок finally.
Они будут охватывать схожие наборы инструкций,но иметь разные адреса обработчиков.

Каждый блок,который имеет(comes with) защищенный регион имеет таблицу с ними. Для каждого защищенного региона имеется 4 записи:

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

Что происходит при выполнении.

Это важная часть — когда при выполнении не происходит исключения.
Поскольку защищенные регионы хранятся в таблицах,а не в байт-коде,и потому что CLR не нужно беспокоится о обработчиках исключения,когда оно происходит, никаких явных "штрафов выполнения«(runtime penalty) не происходит. Для обработчика finally это немного по-другому, потому что она работает даже тогда,когда исключения нету. Поскольку JIT-компилятор содержит код, который все-таки должен выполнятся,расходы идут не больше чем на jump или return.

Так вот,это большая часть ответа на вопрос, который был задан, но все же интересно, что происходит,когда исключение все же «выскакивает». При условии, что текущий метод находится в защищенном регионе (то-есть в блоке try-catch), происходит поиск по таблице, что-бы найти, есть ли какие-нибудь инструкции,что бы обработать это исключение. Если имеется
несколько вложенных обработчиков, которые смогли бы обработать "ошибку«(которые в свою очередь упорядоченные по порядку), мы найдем первый правильный обработчик.

Если мы находим обработчик — мы выполняем его.Но,если мы его не находит,то идем дальше и ищем есть ли вообще такой обработчик, пока не найдем нужный нам или обнаружим что исключение не обработано и вызывает завершение программы (что иногда является хорошим решением). Это значит,что затраты на обработку исключение равны тем ресурсам, которые будут потрачены на нахождение нужного нам обработчика.

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

И стоимость блока try-catch =..

Исключение довольно дорогостоящая операция,но иногда необходимая. Если вы «ожидаете» ошибку, то лучше использовать все доступные средства проверки перед тем как подойти к try-catch. (Например вместо или перед отлавливанием FileNotFound — использовать File.Exists). Стоимость блока try-catch в случае, когда ошибок не будет — небольшая, хотя это верно только в большинстве случаев (Позже попытаюсь описать несколько таких).

Пользуйтесь на здоровье try-catch, но помните о поиске нужного обработчика и выбросе исключения. Ведь когда происходит исключение стек закрывается и разбирается для трассировки и поиска handler’a.

p.s. Буду благодарен за найденные очепятки, ошибки и помощь в форматировании
Оригинальная статья -
www.programmersheaven.com/...​hurt-runtime-performance

👍ПодобаєтьсяСподобалось0
До обраногоВ обраному0
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

зависит от вероятности, где тут приводил пример простого теста при вероятности 1/30 это равнозначно, локальный доступ к файловой системе не такой уж медленый, чего не скажешь про сетовой и прочие RPC где еще и сериализация и маршрутизация сьедает львиную долю

не такой уж медленый, чего не скажешь про сетовой
ой не факт, что сетевой доступ медленнее доступа к диску

конечно, смотря что сравнивать пинг в подсети или запись 100 гигов контента, но мы же говорим о равнозначных простых операциях проверки

Хорошая статья об оптимизации производительности habrahabr.ru/post/166341

зависит от уровня тех самых exceptions, существует поверие что если писать свои менеджеры памяти ну или просто структуры данных с динамическим распределением памяти иногда AV получить проще и обработать быстрее чем кучу проверок — что выделили, хватит ли итд... и как результат был эксперимент в котором подобная система на exception работала в 2 раза быстрее

Исключение довольно дорогостоящая операция,но иногда необходимая. Если вы «ожидаете» ошибку, то лучше использовать все доступные средства проверки перед тем как подойти к try-catch.
вот тут есть один пропущеный момент — иногда проверка ошибки * частосту проверок > обработка исключительной ситуации, пусть мы ее и ждем и пошли дальше

к примеру того же файла

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

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

вот и выросло поколение которое не знает что лежит ниже вм.

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

В CLS написано

A protected region is defined in the metadata..
А в завершении написано что затраты таки идут(Сейчас подправим мой кривой инглиш).
Примеры есть,будет время — напишу о них.

дык, cls даже код хранит в таблицах, что не мешает ему компилится в банарник и затем выполняться
я очень рекомендую нагуглить как єксепшны реализованы на уровне ассемблера в с++ (а может, если повезет и в дотнете)
это будет во много раз более полезно, чем смотреть только на уровень vm и il

Спасибо за совет))

p.s. (шепотом)компилит clr,a cls- спецификация. ;)

я еще «банарник» написал :)
реально я еще в году 2008 разочаровался в дотнете, поєтому путаюсь в названиях... хотя идеи классные, плюшек много.

Я сам себе гугл)
Если нету исключение — обычный jmp — тот же return,правильно?
Когда происходит исключение,ищем нужный хендлер и тоже jmp.

   15:              try
16: {
00000038 90 nop
17: }
00000039 90 nop
0000003a 90 nop
0000003b EB 2D jmp 0000006A
18: catch (ArgumentException)
0000003d 89 45 B0 mov dword ptr [ebp-50h],eax
19: {
00000040 90 nop
20: throw new ArgumentException();
00000041 B9 24 00 81 63 mov ecx,63810024h
00000046 E8 CD F8 D1 FF call FFD1F918
0000004b 89 45 AC mov dword ptr [ebp-54h],eax
0000004e 8B 4D AC mov ecx,dword ptr [ebp-54h]
00000051 E8 FA D3 28 63 call 6328D450
00000056 8B 4D AC mov ecx,dword ptr [ebp-54h]
00000059 E8 7A 29 F7 65 call 65F729D8
21: }


Ниже в цепочке кечов тот же jmp в nop

лучше смотреть с divide by zero (при том как параметр, что бы 0 не выводился)
throw всетаки просто jump в обертке

Можно переформулировочку о параметре или пример на любом языке.

    18:              catch (DivideByZeroException)
0000003d 89 45 B0 mov dword ptr [ebp-50h],eax
19: {
00000040 90 nop
20: throw new DivideByZeroException();
00000041 B9 E8 C4 80 63 mov ecx,6380C4E8h
00000046 E8 7D F7 E7 FF call FFE7F7C8
0000004b 89 45 AC mov dword ptr [ebp-54h],eax
0000004e 8B 4D AC mov ecx,dword ptr [ebp-54h]
00000051 E8 CA 10 42 63 call 63421120
00000056 8B 4D AC mov ecx,dword ptr [ebp-54h]
00000059 E8 2A 28 0D 66 call 660D2888
21: }
//всегда заканчивается так
0000006a 90 nop
0000006b 90 nop
0000006c C7 45 E0 00 00 00 00 mov dword ptr [ebp-20h],0
00000073 C7 45 E4 FC 00 00 00 mov dword ptr [ebp-1Ch],0FCh
0000007a 68 D1 29 2A 00 push 2A29D1h
0000007f EB 00 jmp 00000081

я еще раз повторю — throw это jump в обертке
попробуйте написать чет типа
return arg1/arg2;

в случае когда arg1/arg2 неизвестны компилятору

Ничего нового не вижу хД
double d = 1;
double d1 = 0;
double b = d/d1; По другому никак. Ругается студия.

    15:              try
16: {
0000004b 90 nop
17: double d = 1;
0000004c D9 E8 fld1
0000004e DD 5D BC fstp qword ptr [ebp-44h]
18: double d1 = 0;
00000051 D9 EE fldz
00000053 DD 5D B4 fstp qword ptr [ebp-4Ch]
19: double b = d/d1;
00000056 DD 45 BC fld qword ptr [ebp-44h]
00000059 DC 75 B4 fdiv qword ptr [ebp-4Ch]
0000005c DD 5D AC fstp qword ptr [ebp-54h]
20: }
0000005f 90 nop
00000060 90 nop
00000061 EB 4B jmp 000000AE
21: catch (DivideByZeroException e)
00000063 89 45 94 mov dword ptr [ebp-6Ch],eax
00000066 8B 45 94 mov eax,dword ptr [ebp-6Ch]
00000069 89 45 A8 mov dword ptr [ebp-58h],eax
22: {
0000006c 90 nop
23: throw new DivideByZeroException("asd",e);
0000006d B9 E8 C4 80 63 mov ecx,6380C4E8h
00000072 E8 51 F7 E3 FF call FFE3F7C8
00000077 89 45 90 mov dword ptr [ebp-70h],eax
0000007a B9 0D 00 00 70 mov ecx,7000000Dh
0000007f BA 34 0D 77 00 mov edx,770D34h
00000084 E8 A7 79 EF 65 call 65EF7A30
00000089 89 45 8C mov dword ptr [ebp-74h],eax
0000008c FF 75 A8 push dword ptr [ebp-58h]
0000008f 8B 55 8C mov edx,dword ptr [ebp-74h]
00000092 8B 4D 90 mov ecx,dword ptr [ebp-70h]
00000095 E8 3E CA A1 63 call 63A1CAD8
0000009a 8B 4D 90 mov ecx,dword ptr [ebp-70h]
0000009d E8 E6 27 F9 65 call 65F92888
24: }

ведь постоянные mov-call — поиск обработчика?

а почему не int
floatы можно на ноль делить, там не будет эксепшина


    15:              try
16: {
00000046 90 nop
17: int i = 0;
00000047 33 D2 xor edx,edx
00000049 89 55 C0 mov dword ptr [ebp-40h],edx
18: double b = 1 / i;
0000004c B8 01 00 00 00 mov eax,1
00000051 99 cdq
00000052 F7 7D C0 idiv eax,dword ptr [ebp-40h]
00000055 89 45 94 mov dword ptr [ebp-6Ch],eax
00000058 DB 45 94 fild dword ptr [ebp-6Ch]
0000005b DD 5D B8 fstp qword ptr [ebp-48h]
19: }
0000005e 90 nop
0000005f 90 nop
00000060 EB 4B jmp 000000AD
20: catch (DivideByZeroException e)
00000062 89 45 A0 mov dword ptr [ebp-60h],eax
00000065 8B 45 A0 mov eax,dword ptr [ebp-60h]
00000068 89 45 B4 mov dword ptr [ebp-4Ch],eax
21: {
0000006b 90 nop
22: throw new DivideByZeroException("asd",e);
0000006c B9 E8 C4 80 63 mov ecx,6380C4E8h
00000071 E8 52 F7 EE FF call FFEEF7C8
00000076 89 45 9C mov dword ptr [ebp-64h],eax
00000079 B9 0D 00 00 70 mov ecx,7000000Dh
0000007e BA 34 0D 54 01 mov edx,1540D34h
00000083 E8 A8 79 F9 65 call 65F97A30
00000088 89 45 98 mov dword ptr [ebp-68h],eax
0000008b FF 75 B4 push dword ptr [ebp-4Ch]
0000008e 8B 55 98 mov edx,dword ptr [ebp-68h]
00000091 8B 4D 9C mov ecx,dword ptr [ebp-64h]
00000094 E8 3F CA AB 63 call 63ABCAD8
00000099 8B 4D 9C mov ecx,dword ptr [ebp-64h]
0000009c E8 E7 27 03 66 call 66032888
23: }

Наверное я так и не найду,то что вы хотите мне показать. Уговорили,иду спать (=

я просто изначально имел ввиду код типа
main(args) {
int arg1 = args[0];
int arg2 = args[1];
Console.WriteLn(my_div_function(arg1, arg2));
}

String my_div_function(arg1, arg2) {
int res = 0;
try {
res = (arg1/arg2).ToString();
}
catch(Exception e) {
return “Error” ;
}
return res.ToString();
}

Есть разная реализация исключений, например SEH она и так встроена, хотите вы или нет, но клиентский код выполняеться в этой среде, и соответсвено экспешен в ней для вас часто дешевле чем просто условие, насчет CLR надо смотреть куда ведет кол, часто ведут к С функциям винды и опа, снова SEH и не так уж дорого что кстати легко проверяеться, пожалуйста не путайте стоимость try-catch в класическом С, когда создание структур и прочей инфраструктуры требовали больше усилий и нативной реализации среды там где вы сравниваете

вот простой пример
Use 1/2 probability
Exception: 11.0007
Cndition: 1
Use 1/3 probability
Exception: 9.0005
Cndition: 1.0001
Use 1/4 probability
Exception: 8.0005
Cndition: 2.0001
....
Use 1/30 probability
Exception: 5.0002
Cndition: 5.0003

int n = 100;
Random r = new Random();
for (int j = 2; j <= 30; ++j)
{
using (StreamWriter w = new StreamWriter("test" + (j + 1)))
{
w.WriteLine("Yahoo!!!!!!");
}
List<int> l = new List<int>();
for (int i = 0; i < n; ++i)
{
l.Add(r.Next(j));
}
DateTime s1 = DateTime.Now;
foreach (int i in l)
{
try
{
string temp = File.ReadAllText("test" + i);
}
catch (Exception ex)
{
// just ignore
}
}
DateTime s2 = DateTime.Now;
foreach (int i in l)
{
if (File.Exists("test" + i))
{
string temp = File.ReadAllText("test" + i);
}
}
DateTime f = DateTime.Now;
Console.WriteLine("Use 1/{0} probability", j);
Console.WriteLine("Exception: {0}", (s2 — s1).TotalMilliseconds);
Console.WriteLine("Cndition: {0}", (f — s2).TotalMilliseconds);
}
}

но если вы не много измените просто чуть-чуть
using (StreamWriter w = new StreamWriter(@"\\localhost\d$\temp\test" + (j + 1)))
}
то
Use 1/4 probability
Exception: 245.014
Cndition: 217.0124
Use 1/5 probability
Exception: 290.0166
Cndition: 294.0168

ну и понеслась.... для системы есть куда более дешевые эксепшены и дорогие чеки (кстати которые посути теже эксепшены только обернутые в функции)

Поднял выше,так как кода не видно
Вот,наконец)

00000000 55                   push        ebp 
00000001 8B EC mov ebp,esp
00000003 57 push edi
00000004 56 push esi
00000005 53 push ebx
00000006 83 EC 48 sub esp,48h
00000009 8B F1 mov esi,ecx
0000000b 8D 7D AC lea edi,[ebp-54h]
0000000e B9 12 00 00 00 mov ecx,12h
00000013 33 C0 xor eax,eax
00000015 F3 AB rep stos dword ptr es:[edi]
00000017 8B CE mov ecx,esi
00000019 89 4D C4 mov dword ptr [ebp-3Ch],ecx
0000001c 89 55 C0 mov dword ptr [ebp-40h],edx
0000001f 83 3D 00 10 51 00 00 cmp dword ptr ds:[00511000h],0
00000026 74 05 je 0000002D
00000028 E8 C7 46 22 66 call 662246F4
0000002d 33 D2 xor edx,edx
0000002f 89 55 B4 mov dword ptr [ebp-4Ch],edx
00000032 33 D2 xor edx,edx
00000034 89 55 B8 mov dword ptr [ebp-48h],edx
00000037 90 nop
26: int res = 0;
00000038 33 D2 xor edx,edx
0000003a 89 55 BC mov dword ptr [ebp-44h],edx
27: try
28: {
0000003d 90 nop
29: res = (arg1/arg2);
0000003e 8B 45 C4 mov eax,dword ptr [ebp-3Ch]
00000041 99 cdq
00000042 F7 7D C0 idiv eax,dword ptr [ebp-40h]
00000045 89 45 BC mov dword ptr [ebp-44h],eax
30: }
00000048 90 nop
00000049 90 nop
0000004a EB 1A jmp 00000066
31: catch (Exception e)
0000004c 89 45 AC mov dword ptr [ebp-54h],eax
0000004f 8B 45 AC mov eax,dword ptr [ebp-54h]
00000052 89 45 B8 mov dword ptr [ebp-48h],eax
32: {
00000055 90 nop
33: return "Error";
00000056 8B 05 98 21 78 02 mov eax,dword ptr ds:[02782198h]
0000005c 89 45 B4 mov dword ptr [ebp-4Ch],eax
0000005f E8 21 FD F8 65 call 65F8FD85
00000064 EB 15 jmp 0000007B
00000066 90 nop
34: }
35: return res.ToString();

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

Все хендлеры вверху.Это не полный код.

  00000000 55                   push        ebp 
00000001 8B EC mov ebp,esp
00000003 57 push edi
00000004 56 push esi
00000005 53 push ebx
00000006 83 EC 48 sub esp,48h
00000009 8B F1 mov esi,ecx
0000000b 8D 7D B8 lea edi,[ebp-48h]
0000000e B9 0F 00 00 00 mov ecx,0Fh
00000013 33 C0 xor eax,eax
00000015 F3 AB rep stos dword ptr es:[edi]
00000017 8B CE mov ecx,esi
00000019 89 4D C4 mov dword ptr [ebp-3Ch],ecx
0000001c 83 3D 00 10 7B 00 00 cmp dword ptr ds:[007B1000h],0
00000023 74 05 je 0000002A
00000025 E8 A2 48 79 5E call 5E7948CC
0000002a 33 D2 xor edx,edx
0000002c 89 55 BC mov dword ptr [ebp-44h],edx
0000002f 33 D2 xor edx,edx
00000031 89 55 C0 mov dword ptr [ebp-40h],edx


edi,[ebp-48h] — вон тот,похожий,но перед ним еще
mov [ebp-54h] — вот наверное и поиск.

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

Увы, сама методология оценки неверна.

ну хоть трай на очном примере просветляется)
А пока — все что есть у меня,это кусок disAsm кода и хиленький ИЛ.

А Вы сходите, например, на
www.agner.org/optimize
там в PDF «Optimizing software in C++» есть описание Exceptions and error handling.

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

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

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

Мне понравилась она,решил оставить,может еще кому-нить поможет. Более-менее ситуация прояснилась с работой vm и jit

Проблема в тому, що багато індивідуумів використовують try/catch для обробки ОЧІКУВАНИХ помилок, таким взагалі нема чого про якийсь там перфоманс розказувати...Всякі Pokemon exceptions catсhing і так далі ідуть як норма. Справді «неочікуваних, виключних» ситуацій не так вже і багато, якщо подумати, в середньостатичтичній бізнес-софтині на .NET

Згодний) Але навряд чи це така велика проблема.

в try/catch нормально обрабатывать всякие HttpWebRequest Exception или ошибки sql-запросов, а они являются ожидаемьіми.

скорее потому что проверка условием дороже try-catch если уже случиться, к примеру как вы проверите sql? кучей предварительных селектов еще и с локом БД что бы ничего не поменялось к моменту операции, например не правильное поле запроса — для этого надо выселектить системную таблицу, найти обьект, распарсить запрос и сверить права пользователя итд... все это дорого поэтому ожидать проще в плане кода и дешевле в плане ресурсов

Підписатись на коментарі