Издательский дом ООО "Гейм Лэнд"СПЕЦВЫПУСК ЖУРНАЛА ХАКЕР #71, ОКТЯБРЬ 2006 г.

война миров

КРИС КАСПЕРСКИ АКА МЫЩЪХ

Спецвыпуск: Хакер, номер #071, стр. 071-024-3


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

Исходный текст подопытной функции выглядит так (ключевой фрагмент демонстрационной программы CRC.c):

CRC(unsigned char *p, int n)

{

int a; unsigned char crc=0;

for (a=0;a<n;a++) crc += p[a];

return 0 — crc;

}

Компилируем ее Microsoft Visual C++ в режиме максимальной оптимизации (ключ Ox — «cl.exe /Ox crc.c») и загружаем полученный obj в дизассемблер (смотри листинг 1).

Сразу бросается в глаза, что компилятору не хватило регистров (!) и счетчик контрольной суммы зачем-то задвинулся в локальную переменную. Впрочем, это не сильно сказалось на производительности, поскольку «задвижение» произошло после выхода из цикла, но сам факт! А ведь чтобы исправить ситуацию, всего-то и требовалось заменить «MOV byte ptr [ESP+5+var_1],CL/MOV EAX,[ESP+1+var_1]/AND EAX, 0FFh» на «MOVZX EAX,CL», что гораздо короче и намного быстрее, особенно если n невелико, и функция вызывается большое количество раз. Другое замечание — компилятору потребовалось целых 5 (!) регистров, в то время как для решения данной задачи вполне достаточно 3-х: один — на сумматор CRC, один — на указатель, и еще один — на адрес конца. Ассемблер-пример с ручной оптимизацией приведен на листинге 2.

18h ассемблерных байт (и 12 команд) против 34h откомпилированных байт (и 23 команд) — для классического цикла это хороший результат. Что же тогда говорить о нетривиальных вещах: сложных структурах данных, циклах с высоким уровнем вложенности, многомерных массивах и так далее. С другой стороны, не все так плохо. Компилятор успешно распознал конструкцию «return 0 — crc» и вместо тупого вычитания из нуля подобрал адекватную машинную команду NEG, означающую «дополнение до нуля».

А что на счет GCC? Для начала возьмем древнюю, но все еще широко используемую версию 2.95, отличающуюся стабильностью и высокой скоростью трансляции. На самом высоком уровне оптимизации (ключ -O3: «gcc crc.c -O3 -o crc»), компилятор генерирует код, показанный на листинге 3.

В отличие от MS VC, компилятору GCC хватило всего 4-х регистров без обращения к локальным переменным, а сам код уложился в 30h байт, что на 4 байта короче, чем у конкурента. Но до ручной ассемблерной оптимизации все равно еще далеко. Однако, если присмотреться к телу цикла повнимательнее, можно обнаружить, что GCC, в отличие от MS VC, совместил счетчик цикла с инкрементом указателя, то есть откомпилированный цикл исполняется практически с той же самой степенью эффективности, что и ручной. «Практически» — потому что в отличие от нас компилятор использовал сложную адресацию «ADD AL, [EDX+EBX]», напрягающую процессор и требующую нескольких экстра-тактов на декодирование (впрочем, к последним версиям P-4 и Athlon это уже не относится).

Назад на стр. 071-024-2  Содержание  Вперед на стр. 071-024-4