В рамках подготовки к релизу CFW 0.8 была осуществлена небольшая переработка обработки исключений в СRTL и CSRTL. После проведения работ было решено провести сравнение скорости получившихся реализаций с реализацией из стандартной RTL FPC, а также сравнить между собой два подхода к реализации обработки исключений SjLj (используемых FPC в Unix системах) и SEH (обязательный для Win64).
В качестве теста производительности использовалось несколько вариантов кода, построенных на основе циклического выполнения тестируемого блока 100 миллионов раз. Ниже приводятся тестируемые блоки:
- EPass:
Try Inc(Cnt.Pass) Except Inc(Cnt.Catch) End;
- ECall:
Try Inc(Cnt.Pass); Raise TObject(Nil) Except Inc(Cnt.Catch) End;
- FPass:
Try Try Inc(Cnt.Pass) Finally Inc(Cnt.Final) End; Except Inc(Cnt.Catch) End;
- FCall:
Try Try Inc(Cnt.Pass); Raise TObject(Nil) Finally Inc(Cnt.Final) End; Except Inc(Cnt.Catch) End;
- ESEGV:
Try Inc(Cnt.Pass); Asm MOV RAX, [0] End Except Inc(Cnt.Catch) End;
- EIDbZ:
Try Inc(Cnt.Pass); Asm MOV RAX, 1; MOV RDX, 0; MOV RCX, 0; DIV RCX End Except Inc(Cnt.Catch) End;
- EFDbZ:
Try Inc(Cnt.Pass); Asm FLD1; FLDZ; FDIVP; End Except Inc(Cnt.Catch) End;
Сразу оговорюсь что тестирование SjLj проводилось в виртуальной машине, однако замедление от её использования было незначительно: на Windows итерация цикла с простым инкрементом одной или двух глобальных переменных выполнялась за 1.74нс, а на Linux (под VM) за 1.75нс. В таблице ниже приводится прирост времени выполнения одной итерации цикла в наносекундах для каждого из тестов относительно простого инкремента. Для SjLj строки таблицы с пометкой “+DE” указывают что использовался механизм сохранения краткого стека вызовов (до 5-ти адресов) при любом вызове исключения.
EPass | FPass | ECall | FCall | ESEGV | EIDbZ | EFDbZ | |
FPC SjLj | 6,17 | 13,16 | 94,64 | 107,82 | 1298,72 | 1110,14 | 1504,46 |
CRTL-ST SjLj | 4,95 | 9,35 | 14,45 | 26,30 | 1086,98 | 901,63 | 1311,41 |
CRTL-ST SjLj +DE | 4,95 | 9,35 | 14,45 | 26,28 | 1116,49 | 922,32 | 1321,44 |
CRTL-MT SjLj | 8,48 | 17,51 | 27,79 | 40,92 | 1145,94 | 928,34 | 1331,39 |
CRTL-MT SjLj +DE | 8,48 | 17,51 | 27,55 | 40,89 | 1134,49 | 922,60 | 1320,79 |
CSRTL-ST SjLj | 4,10 | 9,27 | 13,77 | 23,47 | 1072,25 | 910,07 | 1313,89 |
CSRTL-ST SjLj +DE | 4,10 | 9,27 | 16,18 | 24,55 | 1088,44 | 898,43 | 1313,14 |
CSRTL-MT SjLj | 4,33 | 9,35 | 12,86 | 23,43 | 1071,31 | 921,91 | 1327,78 |
CSRTL-MT SjLj +DE | 4,33 | 9,35 | 13,75 | 26,52 | 1090,81 | 925,16 | 1318,96 |
CSRTL-ST SjLj opt | 2,37 | 6,63 | 4,45 | 13,22 | |||
FPC SEH | 0,28 | 0,86 | 1994,42 | 2007,93 | 2180,13 | 2429,51 | 2628,00 |
CRTL SEH | 0,28 | 0,86 | 1600,80 | 1587,14 | 1950,47 | 1793,87 | 2092,55 |
CSRTL SEH | 0,28 | 0,86 | 1529,71 | 1503,63 | 1959,39 | 1787,53 | 2077,13 |
Из полученных данных видно что, во-первых SEH имеет далеко не нулевую стоимость Try-блока. Да она небольшая (приблизительно в 8 раз меньше чем у максимально оптимизированного SjLj), но не нулевая. Во-вторых цена вызова исключения для SEH по сравнению с SjLj воистину огромна. Речь идёт о ~340-ка кратной разнице (в более удачном случае ~110 кратной). Обычно на этот аргумент сторонники SEH говорят что возникновение исключения это чрезвычайное событие, а потому его цена не важна. Важнее цена прохождения Try-блока. Но чтобы это стало действительно так необходимо добавлять множество проверок для всех операций, а ведь они далеко не бесплатны. 2.37нс для Try-блока это примерно 8-9 тактов процессора. Много ли проверок можно вписать в такое кол-во инструкций? Я думаю что нет.
Также можно заметить что стоимость обработки аппаратных ошибок довольно велика: грубо говоря 1мкс на ошибку. Это позволяет говорить о том, что идея программной реализации Dirty-флагов для страниц процесса через RO-страницы и обработку ошибок память силами процесса весь неудачная.