* При перепечатке материалов ссылка на www.SeoLiga.ru обязательна! RSS



Программирование с помощью BASM в Delphi
31 марта 2009

Это вторая глава введения в программирование с помощью BASM в Delphi. В первой главе было короткое введение в целочисленный код, а в этой главе введение в код с плавающей запятой. В нашем примере мы рассчитаем полином второго порядка. Параметры A, B и C, которые определяют полином, закодированы как локальные константы. В функцию передается переменная X типа double и результат также типа double. Функция выглядит так.
function SecondOrderPolynomial1(X : Double) : Double;
const
A : Double = 1;
B : Double = 2;
C : Double = 3;
begin
Result := A*X*X + B*X + C;
end;
Просмотр кода в окне CPU показывает следующее.
function SecondOrderPolynomial2(X : Double) : Double;
const
A : Double = 1;
B : Double = 2;
C : Double = 3;

begin
{
push ebp
mov ebp,esp
add esp,-$08
}
Result := A*X*X + B*X + C;
{
fld qword ptr [A]
fmul qword ptr [ebp+$08]
fmul qword ptr [ebp+$08]
fld qword ptr [B]
fmul qword ptr [ebp+$08]
faddp st(1)
fadd qword ptr [C]
fstp qword ptr [ebp-$08]
wait
fld qword ptr [ebp-$08]
}
{
pop ecx
pop ecx
pop ebp
}
end;
Попробую объяснить ассемблерный код, строка за строкой. Код begin выглядит в коде так.
begin
{
push ebp
mov ebp,esp
add esp,-$08
}
Здесь устанавливается фрейм стека для функции. Фрейм стека просто часть памяти, которая выделена в стеке. Фрейм стека доступен через два указателя, указатель базы и указатель стека. Указатель базы это регистр EBP и указатель стека это регистр ESP. Эти два регистра резервированы только для использования в качестве этих указателей. Первая инструкция PUSH EBP сохраняет указатель базы. В строке MOV EBP, ESP устанавливается новая база для адресации по стеку. В строке ADD ESP, -$08 указатель стека смещается на 8 вниз. Как курьез, стек увеличивается вниз, и более понятной командой было бы его установка с помощью инструкции SUB ESP, 8. Новый фрейм стека устанавливается с помощью этих трех строк, поверх старого фрейма, который был размещен функцией, которая вызвала нашу функцию SecondOrderPolynomial.
Следующая строка Паскаля компилируется в 9 строк на ассемблере.
Result := A*X*X + B*X + C;
{
fld qword ptr [A]
fmul qword ptr [ebp+$08]
fmul qword ptr [ebp+$08]
fld qword ptr [B]
fmul qword ptr [ebp+$08]
faddp st(1)
fadd qword ptr [C]
fstp qword ptr [ebp-$08]
wait
fld qword ptr [ebp-$08]
}
Для тех, кто использует калькуляторы HP для расчетов с плавающей запятой, данный код очень прост для понимания. В первой строке, FLD QWORD PTR [A], загружается константа A в регистр стека с плавающей запятой. Строка, FMUL QWORD PTR [EBP+$08], умножает A на X. Это понятно при просмотре Паскаль кода, но что означает "QWORD PTR [EBP+$08]". QWORD PTR означает "указатель на двойное слово, которое размером с double (64 бита). Значение указателя между квадратными скобками [EBP+$08]. Регистр EBP это указатель базы и $08 это – да просто 8. Поскольку стек при увеличении движется вниз, то это смещение на 8 байт вверх относительно указателя базы в текущем фрейме. Здесь находится переданный параметр X, помещенный сюда вызывающей функцией. При соглашение о регистром вызове, значение не помещается в 32-разрядный регистр, но оно хорошо помещается в регистр с плавающей запятой. Borland решил передавать параметры с плавающей запятой двойной точности через стек, но передача через регистры с плавающей запятой, была бы более эффективной. Следующие три строки не требуют специального пояснения, but the line, но инструкция FADDP ST(1), нуждается в объяснении. Все инструкции с плавающей запятой начинаются с префикса f. add это сложение. ST(1) это название регистра с плавающей запятой номер 1, который является вторым, поскольку первый регистр это ST(0)! Регистры с плавающей запятой скомпонованы в стек и инструкции по умолчанию работаю с верхушкой стека, которая равна ST(0). FADDP ST(1) идентична инструкции FADDP ST(0), ST(1) - складывает содержимое регистров ST(0) и ST(1), результат помещается в регистр ST(1). P в FADDP означает POP ST(0) из стека. Таким путем результат помещается в ST(0). Строка FADD QWORD PTR [C] заканчивает вычисление, и единственная вещь, которая осталась, это помещения результата в ST(0). Результат и так уже там, поэтому две следующие строки кода излишни.
fstp qword ptr [ebp-$08]
fld qword ptr [ebp-$08]
Они просто копируют результат на стек и обратно. Такая затрата времени и энергии :-). Инструкция WAIT обеспечивает обработку возможных исключений при выполнении операций с плавающей запятой. Смотри руководство Intel SW Developers Manual Volume 2, страницу 822 для полного понимания этого.
Осталось объяснить еще три строки кода.
{
pop ecx
pop ecx
pop ebp
}
end;
Они возвращают фрейм стека, путем восстановления старого содержимого регистров ESP и EBP. Понятнее был бы следующий код.
add esp, 4
pop ebp
это также было бы более эффективным, и я не понимаю, почему компилятор увеличивает указатель стека таким странным методом. Вспоминаем, что регистр ECX можно использоваться свободно, назначать ему любые значения, поскольку они все равно не будет использовано далее.
Осталось также объяснить, что скрывается за [A] в строке fld qword ptr [A]. Мы знаем, что A должен быть указателем на место, где хранится само A в памяти. Адрес A закодирован в инструкции. Вот полная строка из окна CPU.
00451E40 DD05803C4500 fld qword ptr [B]
00451E40 это адрес инструкции в exe файле. DD05803C4500 это машинный код строки FLD QWORD PTR [B], которая более понятна для человеческого разума. При просмотре руководства Intel SW Developers Manual Volume 2, страница 280, мы увидим, что код команды для FLD равен D9, DD, DB или D9C0, в зависимости от типа данных. Мы узнаем, что DD это код для FLD DOUBLE. Остается еще 05803C4500. 05 это (Не знаю, может быть, кто-то поможет мне!), а 803C4500 это 32-битный адрес константы A.
Попробуем теперь преобразовать эту функцию в чистый ассемблерный код.
function SecondOrderPolynomial3(X : Double) : Double;
const
A : Double = 1;
B : Double = 2;
C : Double = 3;

asm
push ebp
mov ebp,esp
add esp,-$08
//Result := A*X*X + B*X + C;
fld qword ptr [A]
fmul qword ptr [ebp+$08]
fmul qword ptr [ebp+$08]
fld qword ptr [B]
fmul qword ptr [ebp+$08]
faddp //st(1)
fadd qword ptr [C]
fstp qword ptr [ebp-$08]
wait
fld qword ptr [ebp-$08]
pop ecx
pop ecx
pop ebp
end;
Но мы теперь получили несколько сюрпризов. Во-первых, функция не компилируется. FADDP ST(1) не распознается, как допустимая комбинация команды и операндов. Снова консультируемся с руководством от Интел, мы узнаем, что FADDP существует только в одной версии. Она работает с ST(0), ST(1) и нет необходимости писать FADDP ST(0), ST(1) и только краткая форма FADDP единственно допустимая. После маскирования ST(1) наконец стало компилироваться.
Второй сюрприз. Вызов функции с X = 2 должен рассчитать Y = 2^2+2*2+3 = 11. Но SecondOrderPolynomial3 возвращает 3! Снова открываем окно просмотра FPU, так как окно CPU и трассируем код, наблюдая, что происходит. Видно, что A=1 корректно загружается в ST(0) в строке 4, но в строке 5, которая производит умножение A на X, 1 на 2, результат в ST(0) что-то очень маленький, в действительности 0. Это означает, что X близок к 0 вместо 2. Могут быть неверным две вещи. Вызывающий код передает неверное значение X или мы неправильно адресуем X. Сравнивая код вызова функций SecondOrderPolynomial3 и SecondOrderPolynomial1, мы видим, что он одинаков и поэтому не может быть причиной ошибки. Было бы большим сюрпризом, если бы Delphi делала это неверно! Пробуем опять трассировать код вызова, наблюдая за окном просмотра памяти в окне просмотра CPU. Зеленая стрелочка показывает позицию стека. Код вызова выглядит так:
push dword ptr [ebp-$0c]
push dword ptr [ebp-$10]
call SecondOrderPolynomial1
Два указателя помещаются на стек. Один из них это указатель на X. Но что за второй указатель. Просматриваем окно памяти и видим, что первый указатель это указатель на X, а второй нулевой указатель. При трассировке внутрь функции мы видим, что первые две строки повторяются. Компилятор автоматически вставляет инструкции PUSH EBP и MOV EBP, ESP. Поскольку инструкция PUSH уменьшает указатель стека на 4, то ссылка на X оказывается неверной. После того, как были убраны две первые строки, все пришло в норму.
Теперь после окончания анализа кода и понимания, что он делает, мы можем приступить к его оптимизации.
Для начала уберем два строки FSTP/FLD поскольку они лишние.
function SecondOrderPolynomial4(X : Double) : Double;
const
A : Double = 1;
B : Double = 2;
C : Double = 3;

asm
//push ebp
//mov ebp,esp
add esp,-$08
//Result := A*X*X + B*X + C;
fld qword ptr [A]
fmul qword ptr [ebp+$08]
fmul qword ptr [ebp+$08]
fld qword ptr [B]
fmul qword ptr [ebp+$08]
faddp //st(1)
fadd qword ptr [C]
//fstp qword ptr [ebp-$08]
wait
//fld qword ptr [ebp-$08]
pop ecx
pop ecx
pop ebp
end;
Есть также одна ссылка на фрейм стека, которая не нужна.
function SecondOrderPolynomial5(X : Double) : Double;
const
A : Double = 1;
B : Double = 2;
C : Double = 3;

asm
//push ebp
//mov ebp,esp
//add esp,-$08
//Result := A*X*X + B*X + C;
fld qword ptr [A]
fmul qword ptr [ebp+$08]
fmul qword ptr [ebp+$08]
fld qword ptr [B]
fmul qword ptr [ebp+$08]
faddp //st(1)
fadd qword ptr [C]

wait

//pop ecx
//pop ecx
//pop ebp
end;
После удаления этих шести строк, наша функция уменьшилась до следующего:
function SecondOrderPolynomial6(X : Double) : Double;
const
A : Double = 1;
B : Double = 2;
C : Double = 3;

asm
//Result := A*X*X + B*X + C;
fld qword ptr [A]
fmul qword ptr [ebp+$08]
fmul qword ptr [ebp+$08]
fld qword ptr [B]
fmul qword ptr [ebp+$08]
faddp
fadd qword ptr [C]
wait
end;
X загружается из памяти в FPU три раза. Было бы более эффективным загрузить его один раз и повторно использовать.
function SecondOrderPolynomial7(X : Double) : Double;
const
A : Double = 1;
B : Double = 2;
C : Double = 3;

asm
//Result := A*X*X + B*X + C;
fld qword ptr [ebp+$08]
fld qword ptr [A]
fmul st(0), st(1)
fmul st(0), st(1)
fld qword ptr [B]
fmul st(0), st(2)
ffree st(2)
faddp
fadd qword ptr [C]
wait
end;
Расскажем о магии данного кода. Во-первых, в первой строке загружаем X. Во второй строке загружаем A. В третьей строке умножаем A на X. В четвертой строке умножаем a*X, расположено в ST(0) на X. Так мы выполнили первое вычисление. Загружаем B и умножаем его на X, этим выполняем второе вычисление. Это последняя необходимость в X и мы освобождаем регистр ST(2), в котором оно хранится. Теперь складываем вычисления 1 и 2 и выкидываем вычисление 2 из стека. Единственно, что нам осталось, это прибавить C. Результат теперь в регистре ST(0) и все остальные регистры освобождены. Теперь мы проверяем на возможные ошибки вычислений и заканчиваем. Теперь кажется, что лишних операций нет и код вполне оптимальный.
Осталась еще инструкции для загрузки часто используемых констант в арифметический сопроцессор, одна из них это 1которая может быть загружена инструкцией fld1. Использование ее убирает одну загрузку из памяти, которая может привести к потерям тактов, если данные неверно выровнены.
function SecondOrderPolynomial8(X : Double) : Double;
const
//A : Double = 1;
B : Double = 2;
C : Double = 3;

asm
//Result := A*X*X + B*X + C;
fld qword ptr [ebp+$08]
//fld qword ptr [A]
fld1
fmul st(0), st(1)
fmul st(0), st(1)
fld qword ptr [B]
fmul st(0), st(2)
ffree st(2)
faddp
fadd qword ptr [C]
wait
end;
На этом данный урок закончен.


Теги: программирование iphone, история развития языков программирования Borland Delphi

Статьи по теме:

ExactRecordCount
Аудиоконференции
Методо-ориентированные ППП
Свойство Expression для TQRExpr
SearchKey
ReadOnly
Компонент TQRExpr
Компонент ТМето
Общие сведения о сокетах
Метод Prepare
Пространства имен
Свойство Copies
Сервис сетевой печати
Подсказки в Delphi
Свойство Page
| Borland Delphi | vitek |
 


Пн Вт Ср Чт Пт Сб Вс
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30


     



Rambler's Top100

Данный сайт или домен продается ICQ: 403-353-727

© 2009 Seoliga.ru | Borland Delphi | Программирование с помощью BASM в Delphi. Регион сайта: Москва и Санкт-Петербург