* При перепечатке материалов ссылка на www.SeoLiga.ru обязательна!
Простая функция Паскаля
31 марта 2009
Начнем с небольшого примера. Это простая функция Паскаля по умножению целого на константу 2. function MulInt2(I : Integer) : Integer; begin Result := I * 2; end; Посмотрим сгенерированный код в окне CPU view. Я компилировал с включенной оптимизацией. function MulInt2_BASM(I : Integer) : Integer; begin Result := I * 2; { add eax,eax ret } end; Здесь мы видим, что параметр передается в функцию в регистре EAX и результат возвращается в том же регистре. Это соглашение по передаче параметров через регистры (register calling convention), которое является соглашением по умолчанию в Delphi. Актуальный код очень простой, умножение на 2 заменяется сложением операнда с самим собой, I + I = 2I. Инструкция RET возвращает управление в строку, следующую за вызовом функции.
Сделаем тот же код, как чистую asm функцию. function MulInt2_BASM2(I : Integer) : Integer; asm //Result := I * 2; add eax,eax //ret end; Заметим, что возврат из функции обеспечивается встроенным ассемблером. Теперь посмотрит на код вызова функции. Вот Паскаль код: procedure TForm1.Button1Click(Sender: TObject); var I, J : Integer;
begin I := StrToInt(IEdit.Text); J := MulInt2_BASM2(I); JEdit.Text := IntToStr(J); end; Важная для нас строка следующая J := MulInt2_BASM2(I); В окне CPU мы видим call StrToInt call MulInt2_BASM2 mov esi,eax После вызова StrToInt из строки выше вызова нашей функции, I находится в регистре EAX. (StrToInt также следует соглашению о передаче параметров через регистры). Функция MulInt2_BASM2 вызывается, и возвращает свой результат в регистре EAX, который в следующей строке копируется в регистр ESI. Замечание об оптимизации: Умножение на два может быть сделано двумя различными путями. С помощью инструкции MUL или сдвигом влево на один разряд. Инструкция MUL описана в руководстве разработчика (Intel IA32 SW developers manual 2) на странице 536. Данная инструкция умножает значение в регистре EAX на другой регистр, результат помещается в регистровую пару EDX:EAX. Регистровая пара необходима, потому что в результате умножения двух 32-битных регистров получается 64-бита, подобно 9*9=81 – два однозначных числа дают результат из двух цифр. Это совпадает с соглашение об использовании регистров, которые должны быть сохранены во время работы функции, и какие можно свободно изменять. Это описано в справочной системе Дельфи. "Выражения asm должны сохранять регистры EDI, ESI, ESP, EBP и EBX, но могут свободно изменять регистры EAX, ECX и EDX." Отсюда мы делаем вывод, что у нас не будет проблем с изменением регистра EDX в инструкции MUL и наша функция может быть реализована следующим образом. function MulInt2_BASM3(I : Integer) : Integer; asm //Result := I * 2; mov ecx, 2 mul ecx end; Также используется регистр ECX, но с этим тоже все в порядке. Так как результат меньше, чем диапазон для integer, то это также корректно возвращается в EAX. Но если I больше половины диапазона integer, то произойдет переполнение и результат будет неверным. Реализация с помощью сдвига влево на один разряд function MulInt2_BASM4(I : Integer) : Integer; asm //Result := I * 2; shl eax,1 end; Время выполнения в данном случае меньше. Мы можем также проконсультироваться с документацией Intel или AMD по таблицам латентности (latency) и по пропускной способности (throughput). От переводчика: в дальнейшем в документе будут использоваться термины - latency и throughput без перевода или латентность, поскольку нет хорошего эквивалента этим терминам или же будет использоваться термин пенальти. Смысл этих терминов следующий, команда может быть выполнена без пенальти (throughput). За минимальное время и с пенальти (latency) за полное, это особенность работы с конвейерами, на мой взгляд, автору стоило заострить эту особенность в данном месте, возможно, это будет сделано позже. Инструкции ADD и MOV выполняются за 0.5 цикла в обоих случаях, Инструкции MUL за 14-18 циклов (latency) и 5 циклов (throughput). Инструкции SHL за 4 цикла (latency) и 1 цикл (throughput). Версия, выбранная в Delphi наиболее эффективна для процессоров P4 и вероятно также для Athlon и P3. Не рассматриваются: версия MUL против IMUL, контроль диапазона, другие соглашения о вызове, измерение производительности, подсчет тактов для других процессоров, подсчет тактов для CALL + RET, расположение адреса возврата и другое.