Здесь множество излишеств в данном коде и мы должны заключить, что Delphi сделала плохую работу по оптимизации кода с плавающей запятой. Попробую дать несколько разъяснений этого кода. В начале Паскаль назначает одну переменную типа double другой. Делается это с помощью пар инструкций MOV, одна для младших четырех байт переменной, а вторая для старшей части. Первая строка ассемблерного кода загружает адрес массива в регистр EAX, который используется как база для адресации в массиве. В EBX находится I, и он умножается на 8, поскольку элемент массива занимает 8 байт. Смещение на 4 байта, в последней из двух строк (в строке это скрыто!), это смещение до старшей части элемента.
Yref размещен во фрейме стека [EBP-$48] и загружается в первой строке FP кода. Y размещен во фрейме стека [EBP -$30] и он вычитается из Yref инструкцией FSUB. Результат Error и он записывается во фрейме стека [EBP-$50].
Последняя строка Паскаль кода компилируется в четыре строки ассемблерного кода, в котором сначала загружается Error. Сохранение и загрузка Error излишне и оптимизатор должен удалить это. FABS это функция ABS и вероятно одна из наиболее коротких реализации функций ;-). Компилятор Delphi не имеет inline оптимизации, но применяет это, как «компьютерную магию» к небольшому количеству функций, одна из которых ABS. Последняя строка записывает AbsError на стек.
Короткая версия компилируется в следующее
Code: |
mov eax,[ebp-$14] fld qword ptr [eax+ebx*8] fsub qword ptr [ebp-$30] fabs fstp qword ptr [ebp-$10] wait |
В данной версии нет лишнего кода, и компилятор должен был сделать такой же код и для длинной версии. Все строки кода присутствуют и в длинной версии, но весь лишний код удален. Первая строка загружает базовый адрес массива в EAX. Вторая строка загружает элемент I, который находится в регистре EBX, на верхушку стека FP. Третья строка вычитает Y из Yref. Четвертая строка это функция Abs. Пятая строка записывает результат в переменную AbsError.
Имеются странности с измерения, которые я не могу объяснить. Результаты измерений сильно изменяются при выполнении. Если клавиатура используется, то при нажатии клавиши, мы получаем различные очки, чем при нажатии мышкой! Единственный кто наверно сможет это объяснить, это Нобель Прайз (Nobel Prize) из Delphi ;-)
Другой иррациональной вещью, является то, что Delphi не выравнивает переменные с двойной точностью должным образом. Они должны быть выровнены по границе 8 байт, а Delphi их выравнивает на границу 4 байта. Пенальти, которое мы можем получить, придет из кэш памяти первого уровня, в отличие от кэш памяти второго уровня она не разделена. При загрузке переменной, она может оказаться разделенной между двумя строка кэш памяти, что потребует двойного времени на ее загрузку. Поскольку переменные двойной точности имеют размер в 8 байт, а строка кэш L1 на P4 размером в 64 байта, то одна из восьми переменных может оказаться разнесенной по разным строкам. На P3 ширина кэш L1 составляет 32 байта, и это может произойти для одного из четырех чисел.
Идеально когда переменные длиной в 4 байта выравнивались бы на границу в 4 байта и восьми байтные на границу в восемь байт соответственно. Что бы сделать это понятным представим себе первую строку в кэш памяти первого уровня, куда будут загружены наши переменные. Первая строка начинается по адресу 0, так, что память из адреса 0 будет загружена в нее. Наша первая переменная выровнена и занимает первые 8 байт в строке 1. переменная номер два занимает байты 9-16 ..., переменная номер восемь байты 57-64 и не пересекает границы строки. Если переменная выровнена на границу 4 байт, то первая переменная размещается в строке по байту 4, а восьмая по байту 61. Первые 4 байта ее находятся в строке 1, но следующие 4 байта уже в строке 2. Процессор загружает младшие 4 байта, затем загружает старшие 4 байта, вместо того, чтобы загрузить все это за один раз.
По причине такого выравнивания чисел двойной точности в Delphi, наши измерения нестабильны, как хотелось бы. Выравнивание можно изменить, при перекомпиляции специально измененного кода. Я выбрал (плохой выбор) не включать код по выравниванию переменных в измерении, но я дам пример, как это сделать несколько позже.
Code: |
function ArcSinApprox1a(X, A, B, C, D : Double) : Double; begin Result := A*X*X*X + B*X*X + C*X + D; end; |
Данная функция получила 43243 пункта при измерении на моем P4 1600 MHz (разогнанным до 1920 MHz).
Дельфи от компилировало это так
Code: |
function ArcSinApprox1b(X, A, B, C, D : Double) : Double; begin { push ebp mov ebp,esp add esp,-$08 } Result := A*X*X*X + B*X*X + C*X + D; { fld qword ptr [ebp+$20] fmul qword ptr [ebp+$28] fmul qword ptr [ebp+$28] fmul qword ptr [ebp+$28] fld qword ptr [ebp+$18] fmul qword ptr [ebp+$28] fmul qword ptr [ebp+$28] faddp st(1) fld qword ptr [ebp+$10] fmul qword ptr [ebp+$28] faddp st(1) fadd qword ptr [ebp+$08] fstp qword ptr [ebp-$08] wait fld qword ptr [ebp-$08] } { pop ecx pop ecx pop ebp } end; |
Код из окна CPU view не откомпилируется, поскольку здесь есть инструкция FADDP ST(1), но мы удалим ST(1). По умолчанию инструкция FADDP оперирует с ST(0), ST(1) и поэтому нет необходимости писать это.
Code: |
function ArcSinApprox1c(X, A, B, C, D : Double) : Double; asm //push ebp //Added by compiler //mov ebp,esp //Added by compiler add esp,-$08 //Result := A*X*X*X + B*X*X + C*X + D; fld qword ptr [ebp+$20] fmul qword ptr [ebp+$28] fmul qword ptr [ebp+$28] fmul qword ptr [ebp+$28] fld qword ptr [ebp+$18] fmul qword ptr [ebp+$28] fmul qword ptr [ebp+$28] faddp //st(1) fld qword ptr [ebp+$10] fmul qword ptr [ebp+$28] faddp //st(1) fadd qword ptr [ebp+$08] fstp qword ptr [ebp-$08] wait fld qword ptr [ebp-$08] pop ecx pop ecx //pop ebp //Added by compiler end; |
Во-первых, мы видим, что не надо устанавливать фрейм стека. Стек в действительности используется для записи временной переменной для результата и переписывается снов в строках
fstp qword ptr [ebp-$08]
wait
fld qword ptr [ebp-$08]
но для этого используется указатель базы, а не указатель стека. Строки, в которых используется EBP + смещение до параметров, которые расположены относительно указателя базы, и который равен фрейму стека вызывающей функции. Указатель стека не используется нигде в функции и изменение его не имеет значение. Инструкция MOV EBP, ESP, добавленная компилятором вместе со строкой ADD ESP, -$08 создает восьмибайтный фрейм. Поскольку эти строки изменяют регистр EBP, то его необходимо сохранить в стеке. В действительности мы можем удалить только строку ADD ESP, 8 и две строки POP ECX, назначение которых вычесть число 8 из ESP.
Code: |
function ArcSinApprox1d(X, A, B, C, D : Double) : Double; asm //add esp,-$08 //Result := A*X*X*X + B*X*X + C*X + D; fld qword ptr [ebp+$20] fmul qword ptr [ebp+$28] fmul qword ptr [ebp+$28] fmul qword ptr [ebp+$28] fld qword ptr [ebp+$18] fmul qword ptr [ebp+$28] fmul qword ptr [ebp+$28] faddp fld qword ptr [ebp+$10] fmul qword ptr [ebp+$28] faddp fadd qword ptr [ebp+$08] fstp qword ptr [ebp-$08] wait fld qword ptr [ebp-$08] //pop ecx //pop ecx end; |
Данная реализация функции получила 42391 пункта (ранее 43243) и немного улучшила производительность.
Компилятор вставил строку MOV EBP, ESP и мы может уменьшить избыточность, используя Esp вместо EBP.
Code: |
function ArcSinApprox1e(X, A, B, C, D : Double) : Double; asm //Result := A*X*X*X + B*X*X + C*X + D; //fld qword ptr [ebp+$20] fld qword ptr [esp+$20] //fmul qword ptr [ebp+$28] fmul qword ptr [esp+$28] //fmul qword ptr [ebp+$28] fmul qword ptr [esp+$28] //fmul qword ptr [ebp+$28] fmul qword ptr [esp+$28] //fld qword ptr [ebp+$18] fld qword ptr [esp+$18] //fmul qword ptr [ebp+$28] fmul qword ptr [esp+$28] //fmul qword ptr [ebp+$28] fmul qword ptr [esp+$28] faddp //fld qword ptr [ebp+$10] fld qword ptr [esp+$10] //fmul qword ptr [ebp+$28] fmul qword ptr [esp+$28] faddp //fadd qword ptr [ebp+$08] fadd qword ptr [esp+$08] //fstp qword ptr [ebp-$08] fstp qword ptr [esp-$08] wait //fld qword ptr [ebp-$08] fld qword ptr [esp-$08] end; |
Просьба писать ваши замечания, наблюдения и все остальное,
что поможет улучшить предоставляемую информацию на этом сайте.
ВСЕ КОММЕНТАРИИ МОДЕРИРУЮТСЯ ВРУЧНУЮ, ТАК ЧТО СПАМИТЬ БЕСПОЛЕЗНО!