Но мы теперь получили несколько сюрпризов. Во-первых, функция не компилируется. 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. Зеленая стрелочка показывает позицию стека. Код вызова выглядит так:
Code: |
push dword ptr [ebp-$0c] push dword ptr [ebp-$10] call SecondOrderPolynomial1 |
Два указателя помещаются на стек. Один из них это указатель на X. Но что за второй указатель. Просматриваем окно памяти и видим, что первый указатель это указатель на X, а второй нулевой указатель. При трассировке внутрь функции мы видим, что первые две строки повторяются. Компилятор автоматически вставляет инструкции PUSH EBP и MOV EBP, ESP. Поскольку инструкция PUSH уменьшает указатель стека на 4, то ссылка на X оказывается неверной. После того, как были убраны две первые строки, все пришло в норму.
Теперь после окончания анализа кода и понимания, что он делает, мы можем приступить к его оптимизации.
Для начала уберем два строки FSTP/FLD поскольку они лишние.
Code: |
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; |
Есть также одна ссылка на фрейм стека, которая не нужна.
Code: |
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; |
После удаления этих шести строк, наша функция уменьшилась до следующего:
Code: |
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 три раза. Было бы более эффективным загрузить его один раз и повторно использовать.
Code: |
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. Использование ее убирает одну загрузку из памяти, которая может привести к потерям тактов, если данные неверно выровнены.
Code: |
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; |
Просьба писать ваши замечания, наблюдения и все остальное,
что поможет улучшить предоставляемую информацию на этом сайте.
ВСЕ КОММЕНТАРИИ МОДЕРИРУЮТСЯ ВРУЧНУЮ, ТАК ЧТО СПАМИТЬ БЕСПОЛЕЗНО!