ТЕХНИКА ОПТИМИЗАЦИИ ПРОГРАММ

       

Наглядная демонстрация качества машинной оптимизации


Разговор о качестве машинной оптимизации был бы неполным без конкретных примеров. Показатели производительности – слишком абстрактные величины. Они дают пищу для размышлений, но не объясняют: почему откомпилированный код оказался хуже. Как говорится, пока не увижу своими глазами, пока не пощупаю своими руками – не поверю!

Что ж, такая возможность у нас есть! Давайте изучим непосредственно сам ассемблерный код, сгенерированный компилятором. Для экономии бумажного пространства ниже приведен лишь один пример – результат компиляции программы пузырьковой сортировки компилятором MicrosoftVisual C++. Пример довольно показательный, ибо ощутимо улучшить его качество, не вывихнув при этом себе мозги, практически невозможно. Да вы смотрите сами. Если не владеете ассемблером – не расстраивайтесь, в листинге присутствуют подробные комментарии (надеюсь, вы понимаете, что их вставил не компилятор?).

.text:004013E0 ; ---------- S U B

R

O

U

T

I

N

E

----------

.text:004013E0

.text:004013E0

.text:004013E0 с_sort             proc near            ; CODE XREF: sub_401420+DAp

.text:004013E0

.text:004013E0 arg_0        = dword ptr  8

.text:004013E0 arg_4        = dword ptr  0Ch

.text:004013E0

.text:004013E0             push   ebx

.text:004013E0       ;      сохраняем регистр EBX, т.к. функция обязана сохранять

.text:004013E0       ;      модифицируемые регистры, иначе программа рухнет

.text:004013E0       ;

.text:004013E1             mov    ebx, [esp+arg_4]

.text:004013E1       ;      загружаем в ebx самый правый аргумент – количество элементов

.text:004013E1       ;      точно так поступил бы и человек

.text:004013E1       ;      ага, локальные переменные адресуются непосредственно через ESP

.text:004013E1       ;      это экономит один регистр (EBP), высвобождая его для других нужд

.text:004013E1       ;      человек так не умеет… вернее, не то, что бы совсем не умеет,

.text:004013E1       ;      но адресация через ESP

заставляет заново вычислять


.text:004013E1       ;      местоположение локальных переменных при всяком перемещении

.text:004013E1       ;      верхушки стека (в частности при передаче функции аргументов),

.text:004013E1       ;      что ну очень утомительно…

.text:004013E1       ;

.text:004013E5             push    ebp

.text:004013E6             push    esi

.text:004013E6       ;      сохраняем еще два регистра

.text:004013E6       ;      вообще-то, человек мог воспользоваться и командой PUSHA,

.text:004013E6       ;      сохраняющей все регистры общего назначения, что было бы

.text:004013E6       ;      намного короче, но в то же время, увеличило бы потребности

.text:004013E6       ;      программы в стековом пространстве и несколько снизило бы

.text:004013E6       ;      ее скорость

.text:004013E6       ;

.text:004013E7             cmp     ebx, 2

.text:004013E7       ;      сравниваем значение аргумента n

с константой 2

.text:004013E7       ;

.text:004013EA             push    edi

.text:004013EB             jl      short loc_40141B

.text:004013EB       ;      а вот здесь пошла оптимизация кода под ранние процессоры P-P MMX

.text:004013EB       ;      сравнение содержимого EBX

и анализ результата сравнения разделен

.text:004013EB       ;      командой сохранения регистра EDI. Дело в том, что P-P MMX

.text:004013EB       ;      могли спаривать команды, если они имели зависимость по данным

.text:004013EB       ;      впрочем, в данном конкретном случае такая оптимизация излишня

.text:004013EB       ;      т.к. процессор пытается предсказать направление перехода еще до

.text:004013EB       ;      его реального исполнения. Впрочем, перестановка команд –

.text:004013EB       ;      карман не тянет и никому не мешает

.text:004013EB       ;

.text:004013ED             mov     ebp, [esp+0Ch+arg_0]

.text:004013ED       ;      загружаем в EBP значение кране левого аргумента –

.text:004013ED       ;      указателя на сортируемый массив. Как уже сказано, так

.text:004013ED       ;      адресовать аргументы умеют только компиляторы,



.text:004013ED       ;      человеку это не под силу

.text:004013ED       ;

.text:004013F1

.text:004013F1 loc_4013F1:                      ; CODE XREF: C_Sort+39j

.text:004013F1       ;      цикл начинается с нечетного адреса. Плохо!

.text:004013F1       ;      это весьма пагубно сказывается на производительность

.text:004013F1       ;

.text:004013F1             xor     esi, esi

.text:004013F1       ;      очищаем регистр ESI, выполняя логического XOR

над ним самим

.text:004013F1       ;      вот на это человек – способен!

.text:004013F1       ;

.text:004013F3             cmp     ebx, 1

.text:004013F6             jle     short loc_40141B

.text:004013F6       ;      эти две команды лишние.

.text:004013F6       ;      Для человека очевидно, если EBX

>= 2, то он всегда больше одного

.text:004013F6       ;      А вот для компилятора это – вовсе не факт! (Ну темный он)

.text:004013F6       ;      Дело в том, что он превратил возрастающий цикл for

.text:004013F6       ;      в убывающий цикл do/while с пост условием

.text:004013F6       ;      /* убывающие циклы с постусловием реализуются на

.text:004013F6       ;      х86-процессорах намного более эффективно */

.text:004013F6       ;      но для этого компилятор должен быть уверен, что цикл

.text:004013F6       ;      исполняется хотя бы раз – вот он и помещает

.text:004013F6       ;      в код дополнительную и абсолютно лишнюю в данном случае

.text:004013F6       ;      проверку. Впрочем, она не отнимает много времени

.text:004013F6       ;

.text:004013F8             lea     eax, [ebp+4]

.text:004013F8       ;      быстрое сложить EBP

с 4 и записать результат в EAX

.text:004013F8       ;      об этом трюке знают далеко не все программисты,

.text:004013F8       ;      и обычно выполняют данную задачу в два этапа

.text:004013F8       ;      MOV EAX, EBX\ADD EAX, 4

.text:004013F8       ;

.text:004013FB             lea     edi, [ebx-1]

.text:004013FB       ;      быстро вычесть из EBX



единицу и записать результат в EDI

.text:004013FB       ;     

.text:004013FE

.text:004013FE loc_4013FE:                      ; CODE XREF: C_Sort+35j

.text:004013FE             mov     ecx, [eax-4]

.text:004013FE       ;      Оп

с! Команда, расположенная в начале цикла пересекает

.text:004013FE       ;      границу 0x10 байт, что приводит к ощутимым задержка исполнения

.text:004013FE       ;      Да, забыл сказать, эта инструкция загружает ячейку src[a-1]

.text:004013FE       ;      в регистр ECX

.text:004013FE       ;

.text:00401401             mov     edx, [eax]

.text:00401401       ;      загружаем ячейку src[a] в регистр EDX

.text:00401401       ;

.text:00401403             cmp     ecx, edx

.text:00401403       ;      сравниваем ECX (src[a-1]) и

EDX (src[a])

.text:00401403       ;      вообще-то, это можно было реализовать и короче

.text:00401403       ;      CMP    ECX, [EAX], избавлюсь от команды MOV EDX, [EAX]

.text:00401403       ;      однако, это ни к чему, т.к. значение [EAX] нам потребуется ниже

.text:00401403       ;      при обмене переменными и эта команда все равно бы вылезла там.

.text:00401403       ;

.text:00401405             jle     short loc_401411

.text:00401405       ;      переход на ветку loc_401411, если ECX

<= EDX

.text:00401405       ;      в противном случае – выполнить обмен ячеек

.text:00401405       ;

.text:00401407             mov     [eax-4],     edx

.text:0040140A             mov     [eax],       ecx

.text:0040140A       ;      обмен

ячейками. Вообще-то, можно было бы реализовать это и

.text:0040140A       ;      через XCHG, что было бы на несколько байт короче, но у этой

.text:0040140A       ;      инструкции свои проблемы – не на всех процессорах она работает

.text:0040140A       ;      быстрее…

.text:0040140A       ;

.text:0040140C             mov     esi, 1

.text:0040140C       ;      устанавливаем ESI (флаг f) в

.text:0040140C       ;      человек мог бы сократить это на несколько байт, записав это так:



.text:0040140C       ;      MOV    ESI,   ECX. Поскольку ECX

> EDX, то ECX

!=0,

.text:0040140C       ;      при условии, конечно, что EDX

>= 0.

.text:0040140C       ;      разумеется, компиляторам такое не по зубам, однако, это

.text:0040140C       ;      алгоритмическая оптимизация и к качеству кодогенерации она

.text:0040140C       ;      не имеет никакого отношения

.text:0040140C       ;

.text:00401411 loc_401411:                      ; CODE XREF: C_Sort+25j

.text:00401411             add     eax, 4

.text:00401411       ;      увеличиваем EAX (a) на

4 (sizeof(int))

.text:00401411       ;

.text:00401414             dec     edi

.text:00401414       ;      уменьшаем счетчик цикла на единицу

.text:00401414       ;      (напоминаю, компилятор превратил наш for

в убывающий цикл)

.text:00401414       ;

.text:00401415             jnz     short loc_4013FE

.text:00401415       ;      переход к началу цикла, пока EDI

не равен нулю

.text:00401415       ;      некоторые человеки здесь лепят LOOP, который хоть и компактнее

.text:00401415       ;      но исполняется значительно медленнее

.text:00401415       ;

.text:00401417             test    esi, esi

.text:00401417       ;      проверка флага f на равенство нулю

.text:00401417       ;

.text:00401419             jnz     short loc_4013F1

.text:00401419       ;      повторять цикл, пока f

равен нулю

.text:0040141B

.text:0040141B loc_40141B:

.text:0040141B             pop     edi

.text:0040141C             pop     esi

.text:0040141D             pop     ebp

.text:0040141E             pop     ebx

.text:0040141E       ;      восстанавливаем все измененные регистры

.text:0040141E       ;

.text:0040141F             retn

.text:0040141F       ;      выходим из функции

.text:0040141F C_Sort             endp

.text:0040141F

Итак, какие у противников компиляторов есть претензии к качеству кода? Смогли бы они реализовать эту же процедуру хотя бы процентов на десять эффективнее? Согласитесь, здесь есть чему поучиться даже весьма квалифицированным программистам!


Содержание раздела