«

Мар 24 2017

Обработка исключений: SjLj vs SEH

В рамках подготовки к релизу CFW 0.8 была осуществлена небольшая переработка обработки исключений в СRTL и CSRTL. После проведения работ было решено провести сравнение скорости получившихся реализаций с реализацией из стандартной RTL FPC, а также сравнить между собой два подхода к реализации обработки исключений SjLj (используемых FPC в Unix системах) и SEH (обязательный для Win64).

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

  1. EPass: Try Inc(Cnt.Pass) Except Inc(Cnt.Catch) End;
  2. ECall: Try Inc(Cnt.Pass); Raise TObject(Nil) Except Inc(Cnt.Catch) End;
  3. FPass: Try Try Inc(Cnt.Pass) Finally Inc(Cnt.Final) End; Except Inc(Cnt.Catch) End;
  4. FCall: Try Try Inc(Cnt.Pass); Raise TObject(Nil) Finally Inc(Cnt.Final) End; Except Inc(Cnt.Catch) End;
  5. ESEGV: Try Inc(Cnt.Pass); Asm MOV RAX, [0] End Except Inc(Cnt.Catch) End;
  6. EIDbZ: Try Inc(Cnt.Pass); Asm MOV RAX, 1; MOV RDX, 0; MOV RCX, 0; DIV RCX End Except Inc(Cnt.Catch) End;
  7. 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-страницы и обработку ошибок память силами процесса весь неудачная.

Добавить комментарий