Урок 5
Добро пожаловать на пятый урок. Его тема циклы. Мы увидим, как компилятор реализует циклы, и какую оптимизацию мы можем сделать в них. Мы также проверим эффективность этой оптимизации.
Code: |
function ForLoop(Start, Stop : Integer) : Integer; var I : Integer;
begin Result := 0; for I := Start to Stop do begin Result := Result + I; end; end; |
В данном примере нет ничего полезного, кроме примера для изучения циклов. Посмотрим, что же компилятор наворотил нам в этом примере. В данном примере мы попробуем, что ни будь новое, и откомпилируем с отключенной оптимизацией.
Code: |
function ForLoopNonOpt(Start, Stop : Integer) : Integer; var I : Integer;
begin { push ebp mov ebp,esp add esp,-$14 mov [ebp-$08],edx mov [ebp-$04],eax } Result := 0; { xor eax,eax mov [ebp-$0c],eax } for I := Start to Stop do { mov eax,[ebp-$04] mov edx,[ebp-$08] sub edx,eax jl +$15 inc edx mov [ebp-$14],edx mov [ebp-$10],eax } begin Result := Result + I; { mov eax,[ebp-$10] add [ebp-$0c],eax inc dword ptr [ebp-$10] } end; { dec dword ptr [ebp-$14] jnz -$0e mov eax,[ebp-$0c] } { mov esp,ebp pop ebp ret } end; |
Как мы видим, компилятор сгенерировал кучу кода, где или совсем нет оптимизации или ее мало. Как обычно первые три строки это установка стекового фрейма. В данном примере он на 20 байт больше (16 hex). Две следующие строки копируют переменные Start и Stop на стек. Start передается в EAX и Stop передается в EDX, в соответствии с соглашением об вызове. Следующие две строки создают значение 0 и копируют его на стек [EBP-$0C], это место для хранения переменной Result. Теперь мы готовы к выполнению тела цикла. Перед началом цикла необходимо убедиться, что цикл действительно должен выполняться. Если Stop больше чем Start, то это как раз тот случай. Start и Stop извлекаются из стека в регистры EAX и EDX. Мы вычисляем выражение Stop-Start и если результат отрицательный, то цикл не выполняется и управление передается в конец цикла инструкцией JL (jump low). В следующей строке увеличивается значение Stop, и оно копируется на стек [EBP-$14]. У нас нет имени для этой локальной переменной в данной точке. Данная особенность потребует дополнительных объяснений. Эта переменная (NoName) введена компилятором для оптимизации и это немного странно, поскольку мы отключили оптимизацию. Доступ до этой неименованной переменной есть в строке DEC DWORD PTR [EBP-$14]. Эта строка уменьшает ее значение на единицу, в конце каждой итерации и проверяется, что она не достигла нуля. Инструкция DEC устанавливает флаги, и инструкция JNZ делает переход на начало цикла, при условии, что NoName <> 0. Мы должны считать, что это используется как счетчик цикла и что она бежит от Start до Stop. Это действительно делается так, но это не используется для управления циклом. Преимущество в том, что это сохраняет инструкции при сравнении I со Stop. Но это также и увеличивает стоимость инструкции DEC NoName. На P4 latency/throughput инструкции CMP составляет 0.5/0.5 цикла, а для DEC оно 1/0.5. Поэтому это можно считать «де оптимизацией». Значения latency и throughput для P4 можно найти в «Intel Pentium 4 and Xeon Processor Optimization» руководстве от Intel.
Вернемся к строке MOV [EBP-$10], EAX. Она копирует переменную I на стек. Тело цикла состоит из одной строки Паскаля Result := Result + I. Она транслируется в три строки на ассемблере. Первые две строки загружают переменную I в регистр EAX и затем прибавляют ее к переменной Result на стеке [EBP-$0C]. Третья строка увеличивает переменную I. На этом мы заканчиваем объяснения кода цикла и у нас остались только две вещи. Переменная Result должна быть скопирована в регистр EAX, который используется для возврата результата из функции, в соответствии с соглашением о вызове. Последние три строки восстанавливают фрейм стека и возвращают управление обратно в программу.
В упражнении мы превратим это в ассемблерный код, так что бы это соответствовало Паскаль коду и нашему пониманию циклов. Мы начнем с преобразования в чистый ассемблерный код. Сделаем это путем закомментирования Паскаль кода и раскомментирования ассемблерного кода. Определим две метки LoopEnd и LoopStart, которые нам потребуются. Изменим два перехода так, что бы они указывали на метки.
Code: |
function ForLoopBASM1(Start, Stop : Integer) : Integer; asm push ebp mov ebp,esp add esp,-$14 mov [ebp-$08],edx mov [ebp-$04],eax //Result := 0; xor eax,eax mov [ebp-$0c],eax //for I := Start to Stop do mov eax,[ebp-$04] mov edx,[ebp-$08] sub edx,eax jl @LoopEnd inc edx mov [ebp-$14],edx mov [ebp-$10],eax //begin @LoopStart : //Result := Result + I; mov eax,[ebp-$10] add [ebp-$0c],eax inc dword ptr [ebp-$10] //end; dec dword ptr [ebp-$14] jnz @LoopStart @LoopEnd : mov eax,[ebp-$0c] mov esp,ebp pop ebp //ret end; |
первое, что мы сделаем, так это удалим локальную переменную NoName.
Code: |
function ForLoopBASM2(Start, Stop : Integer) : Integer; asm push ebp push ebx //New mov ebp,esp add esp,-$14 mov [ebp-$08],edx mov [ebp-$04],eax //Result := 0; xor eax,eax mov [ebp-$0c],eax //for I := Start to Stop do mov eax,[ebp-$04] mov edx,[ebp-$08] sub edx,eax jl @LoopEnd //inc edx //NoName intialize //mov [ebp-$14],edx //NoName intialize mov [ebp-$10],eax //begin @LoopStart : //Result := Result + I; mov eax,[ebp-$10] add [ebp-$0c],eax inc dword ptr [ebp-$10] //end; //dec dword ptr [ebp-$14] //NoName decrement mov ebx, [ebp-$10] //New mov ecx, [ebp-$08] //New cmp ebx, ecx //New //jnz @LoopStart jbe @LoopStart //New @LoopEnd : mov eax,[ebp-$0c] mov esp,ebp pop ebx //New pop ebp //ret end; |
Просьба писать ваши замечания, наблюдения и все остальное,
что поможет улучшить предоставляемую информацию на этом сайте.
ВСЕ КОММЕНТАРИИ МОДЕРИРУЮТСЯ ВРУЧНУЮ, ТАК ЧТО СПАМИТЬ БЕСПОЛЕЗНО!