Содержание материала

Урок 6

Это урок 6 и его тема CharPos. Данная функция ищет первое вхождение символа в строке, и возвращает его позицию когда найдет. Если ничего не найдено, то возвращается 0. функция из Delphi делает тоже самое, но с различием, что ищется вхождение подстроки в строке. Передача символа в Pos как подстроки возможна и это путь использования Pos как CharPos. В данном уроке мы разработаем CharPos, которая будет примерно в 4 раза быстрее, чем Pos.

Как обычно мы начнем с Паскаль реализации алгоритма.

 

Code:

function CharPos2(Chr : Char; const Str : AnsiString) : Cardinal;

var

I : Integer;

begin

if (Str <> '') then

begin

   I := 0;

   repeat

     Inc(I);

   until((Str[I] = Chr) or (Str[I] = #0));

   if (Str[I] <> #0) then

     Result := I

   else

     Result := 0;

end

else

   Result := 0;

end;

 

 

В функцию предаются два параметры типа Char и string. Параметр string передается как константа. Результат работы функции типа Cardinal. В начале в функции проверяется, что входная строка не пустая и если пустая, то возвращается 0. Если строка есть, то проходим по ней пока не найдем в цикле repeat until до тех пор пока не встретим совпадение с входным символом. Если встретился символ 0, он также является признаком окончания строки и цикла. Поскольку цикл может завершиться в случае нахождения символа и в случае достижения конца строки мы должны знать причину, что бы вернуть правильный результат. Если цикл закончился нахождением символа, то мы вернем переменную счетчика, а иначе вернем 0.

Также возможно использовать длину строки как условие для окончания цикла в случае неуспешного поиска. Этот код будет выглядеть так.

Code:

function CharPos1(Chr : Char; const Str : AnsiString) : Cardinal;

var

StrLenght, I : Integer;

begin

StrLenght := Length(Str);

if StrLenght > 0 then

begin

   I := 0;

   repeat

     Inc(I);

   until((Str[I] = Chr) or (I > StrLenght));

   if I <= StrLenght then

     Result := I

   else

     Result := 0;

end

else

   Result := 0;

end;

 

 

Перед тем как перейти к ассемблерному коду, хорошо бы было посмотреть какая из Паскаль реализаций быстрее. Создадим тестовую программу для проведения измерений.

Code:

const

NOOFLOOPS : Cardinal = 200000;

SCALE : Cardinal = 1000;

 

procedure Benchmark;

var

lpPerformanceCount, StartCount, EndCount : TLargeInteger;

Succes : Boolean;

Str, Str1, FunctionName : AnsiString;

Chr1, Chr2 : Char;

I, CharPos, J, K, Bench, SumBench : Cardinal;

StringArray : array[1..255] of AnsiString;

 

begin

Series1.Clear;

Str1 := 'T';

for J := 1 to 255 do

begin

   StringArray[J] := Str1;

   Str1 := 'A' + Str1;

end;

SumBench := 0;

Chr1 := 'T';

Chr2 := 'X';

for K := 1 to 255 do

begin

   Str := StringArray[K];

   Succes := QueryPerformanceCounter(lpPerformanceCount);

   if Succes then

     StartCount := lpPerformanceCount

   else

     raise Exception.Create('QueryPerformanceCounter failed');

   for I := 1 to NOOFLOOPS dиo

   begin

     CharPos := CharPosFunction(Chr1, Str);

   end;

   for I := 1 to NOOFLOOPS do

   begin

     CharPos := CharPosFunction(Chr2, Str);

   end;

   Succes := QueryPerformanceCounter(lpPerformanceCount);

   if Succes then

     EndCount := lpPerformanceCount

   else

     raise Exception.Create('QueryPerformanceCounter failed');

   Bench := Round((EndCount - StartCount) / SCALE);

   Series1.AddXY(K, Bench, '', clBlue);

   Bench := Round(Bench / K);

   SumBench := SumBench + Bench;

   Update;

end;

FunctionName :=

   FunctionSelectRadioGroup.Items[FunctionSelectRadioGroup.ItemIndex];

ReportRichEdit.Lines.Add(FunctionName + #9 + IntToStr(SumBench));

end;

 


 

Программа измерения строит тестовый массив из 255 AnsiStrings. Первая строка 'T'. 'T' также символ для поиска. Поэтому строка номер 1 наиболее короткая для успешного поиска. Следующие строки равны 'AT', 'AAT' и 'AAAT'. Я надеюсь, что этот шаблон прост и понятен. Также важно провести измерение и для неуспешного поиска. В этом случае для поиска мы используем символ 'X'. Программа измерения делает некоторое количество (NOOFLOOPS) поисков по каждой строке и измеряет время на каждой строке. Поскольку мы хотим, что бы результат был аппроксимирован независимо от длины строки, то полученное время делится на длину строки.

В данном тесте CharPos1 получил результат 767 на P4 1600A, разогнанный до 1920 и CharPos2 получил результат 791. Для сравнения Delphi Pos получил результат всего 2637.

Поскольку CharPos1 незначительно лучше, чем CharPos2, то мы выбрали его для дальнейшей оптимизации. Это ассемблерный код на Delphi 6 откомпилированный с включенной оптимизацией.

Code:

function CharPos14(Chr : Char; const Str : AnsiString) : Cardinal;

var

StrLenght, I : Integer;

begin

{

push ebx

push esi

mov  esi,edx

mov  ebx,eax

}

StrLenght := Length(Str);

{

mov  eax,esi

call @LStrLen

mov  edx,eax

}

if StrLenght > 0 then

{

test edx,edx

jle  @Else1Begin

}

begin

  I := 0;

  {

  xor eax,eax

  }

  repeat

   {

   @RepeatBegin :

   }

   Inc(I);

   {

   inc eax

   }

  until((Str[I] = Chr) or (I > StrLenght));

  {

  cmp bl,[esi+eax-$01]

  jz  @If2

  cmp edx,eax

  jnl @RepeatBegin :

  }

  if I <= StrLenght then

  {

  @If2 :

  cmp edx,eax

  jnl @Exit

  }

   Result := I

   {

   }

  else

   Result := 0;

   {

   xor eax,eax

   pop esi

   pop ebx

   ret

   }

end

else

Result := 0;

{

@Else1Begin :

xor eax,eax

}

{

@Exit :

pop esi

pop ebx

}

end;

 

 

В данный момент здесь нет фрейма стека. Регистры EBX и ESI используются, и поэтому требуется их сохранения и восстановление при выходе из функции. Поскольку функция не имеет своего собственно фрейма стека, то они просто помещаются на верхушку стека текущего фрейма. Входные параметры принимаются в регистрах EAX и EDX и они первым делом копируются в регистры ESI и EBX. Функция Length имеет внутренне секретное имя, которое LStrLen. В данную функцию передается параметр  Str, который передается через регистр EAX. Отсюда мы видим, что функция LStrLen также следует регистровому соглашению о вызове. Str был получен через регистр EDX, затем был скопирован в регистр ESI и затем в EAX. LStrLen возвращает свой результат также в регистре EAX. Этот результат копируется в EDX и сравнивается с 0. TEST EDX, EDX, тоже самое, что и CMP EDX,0 и устанавливает флаги. Инструкция JLE проверяет флаги и передает управление в часть ELSE блока IF-ELSE, если StrLenght меньше или равен нулю. В части ELSE мы видим только одну Паскаль строку, которая Result := 0;. Поскольку наша функция должна вернуть результат в EAX мы создаем значение 0 как XOR EAX с самим собой. Если длина строки больше нуля, то управление продолжается в части блока IF. Первая строка этого блока устанавливает начальное значение счетчика I в ноль. Для этого снова используется инструкция XOR. Тело цикла имеет только одну строку, очень простую для понимания  INC(I); = INC EAX. И ассемблерный, и Паскаль код делают одинаково ;-)

Реализация проверки цикла, это то место где проводится реальная работа. Это сделано с помощью четырех строк на ассемблере.

Code:

cmp bl,[esi+eax-$01]

jz  @If2

cmp edx,eax

jnl @RepeatBegin :

 

Мы видим здесь две инструкции перехода. Последняя начинает цикла, а первая выходит из цикла. Здесь также две инструкции сравнения CMP для установки флагов. Вторая очень простая для понимания. Она сравнивает EAX с EDX. Быстрый взгляд на Паскаль код, показывает, что здесь StrLenght и I в этих регистрах. В действительности мы видим, что в eax находится I, а вверху функции мы видим, что StrLenght находится в EDX.

В строке 4 параметр Chr бил скопирован в регистр EBX, но char размером только в один байт. Поэтому первая инструкция CMP сравнивает, что с в BL, который содержит младший байт EBX. Мы предполагаем, что символ поиска - Chr сравнивается с символом 1, 2, 3… входной строки. Поэтому выражение [ESI+EAX-$01] должно быть указателем на эту строку. EAX это счетчик цикла I, который имеет значение 1, при первой итерации. Регистр ESI должен быть адресом параметра Str, который был принят через регистр EDX, и сразу был скопирован в регистр ESI. -$01 это константа смещения, которая необходима, поскольку первый символ в AnsiString расположен по смещению 0. Это позиция, на которую указывает Str.

А куда же пропал OR из кода Паскаля? Для понимания этого мы должны поговорить насчет оптимизации, называемой частичное выполнение логических выражений. Эта оптимизация применяется к логическому оператору AND, и к логическому оператору OR.

Посмотрим это на примере таблицы истинности для AND.

false and false is false

false and true  is false

true  and false is false

true  and true  is true

Оператор AND возвращает значение TRUE только если оба операнда TRUE. В этом и содержится преимущество при оптимизации при частичном выполнении логических выражений. Если один из операндов FALSE, то нет необходимости проверять другой, поскольку результат все равно FALSE, вне зависимости от его значения.

Таблица истинности для оператора OR:

false or false is false

false or true  is true

true  or false is true

true  or true  is true

Результат для OR истинен, если хотя бы один из операндов или оба операнда также истинны. Если один из операндов истинен, то также нет нужды проверять другой.

Наша проверка прекращения цикла дает преимущество, при выходе из цикла, если первое сравнение будет успешным. Это случается если мы нашли вхождение символа в строке. Если найдено совпадение, то нет нужды проверять на символ ограничитель! Последнее сравнение выполняется в случае отсутствия равенства.

Если бы мы знали, что ни будь о строках и символах переданных нашей функции до вызова, то мы могли бы получить преимущества, просто сменив порядок тестов, так что бы получить значение TRUE первым.

Попробуйте сменить параметр компилятора "complete Boolean evaluation", в свойствах проекта, и посмотрите какой код будет сгенерировать.

Остаток кода уже разобран в более ранних уроках, и мы пропустим его объяснение, лучше сходите и выпейте взамен чашечку кофе ;-)

Теперь можно выполнить некоторую оптимизацию. В начале превратим функцию в чисто ассемблерную. Метки были объяснены в листинге предыдущего кода. Здесь видно, что они следуют Паскаль коду интуитивным образом.

Code:

function CharPos15(Chr : Char; const Str : AnsiString) : Cardinal;

//var

//StrLenght, I : Integer;

 

asm

push ebx

push esi

mov  esi,edx

mov  ebx,eax

//StrLenght := Length(Str);

mov  eax,esi

call System.@LStrLen

mov  edx,eax

//if StrLenght > 0 then

test edx,edx

jle  @Else1Begin

//I := 0;

xor eax,eax

//repeat

@RepeatBegin :

//Inc(I);

inc  eax

//until((Str[I] = Chr) or (I > StrLenght));

cmp  bl,[esi+eax-$01]

jz   @If2

cmp  edx,eax

jnl  @RepeatBegin

//if I <= StrLenght then

@If2 :

cmp  edx,eax

jnl  @Exit

//Result := I

//else

//Result := 0;

xor eax,eax

pop  esi

pop  ebx

ret

//else

//Result := 0;

@Else1Begin :

xor eax,eax

@Exit :

pop  esi

pop  ebx

end;

 


 

Вызов функции LStrLen сделан с префиксом System, иначе компилятор не сможет распознать ее. LStrLen реализована в модуле System.

Секция VAR удалена, поскольку мы не ссылаемся ни к каким переменным по имени.

Code:

function CharPos16(Chr : Char; const Str : AnsiString) : Cardinal;

asm

push ebx

push esi

mov  esi,edx

mov  ebx,eax

//StrLenght := Length(Str);

mov  eax,esi

//call System.@LStrLen

//*************

test eax,eax

jz  @LStrLenExit

mov eax,[eax-$04]

@LStrLenExit :

//*************

mov  edx,eax

//if StrLenght > 0 then

test edx,edx

jle @Else1Begin

//I := 0;

xor eax,eax

//repeat

@RepeatBegin :

//Inc(I);

inc  eax

//until((Str[I] = Chr) or (I > StrLenght));

cmp  bl,[esi+eax-$01]

jz   @If2

cmp  edx,eax

jnl  @RepeatBegin

//if I <= StrLenght then

@If2 :

cmp  edx,eax

jnl  @Exit

//Result := I

//else

//Result := 0;

xor eax,eax

pop  esi

pop  ebx

ret

//else

//Result := 0;

@Else1Begin :

xor eax,eax

@Exit :

pop  esi

pop  ebx

end;

 

 

Первая вещь, которую мы сделаем, это сделаем функцию LstrLen inline. Сделаем это путем трассировки и копированием ее тела из окна CPU view. Она состоит из четырех строк.

Code:

test eax,eax

jz +$03

mov eax,[eax-$04]

ret

 

Если указатель, переданный через EAX, в функцию LStrLen имеет nil, то ничего не делается, а просто производится возврат из функции. Если же указатель действительный, то длина строки расположена, в 4 предшествующих строке байтах. Эти 4 байта возвращаются, через регистр EAX. Для превращения этой функции в inline функцию, мы заменим вызов этой функции этими четырьмя строками. Инструкция JZ передает управление на инструкцию RET. Взамен инструкции RET мы передадим управление на метку LStrLenExit. Инструкция RET осуществляет возврат из функции. Данная инструкция RET должна быть удалена, иначе она вернет управление в CharPos, это не то, что мы хотим. А вот так наша встроенная (inline) функция должна выглядеть.

Code:

test eax,eax

jz  @LStrLenExit

mov eax,[eax-$04]

@LStrLenExit :

 

function CharPos17(Chr : Char; const Str : AnsiString) : Cardinal;

asm

push ebx

push esi

mov  esi,edx

mov  ebx,eax

//StrLenght := Length(Str);

mov  eax,esi

//*************

test eax,eax

//jz  @LStrLenExit

jz  @Else1Begin

mov eax,[eax-$04]

//@LStrLenExit :

//*************

mov  edx,eax

//if StrLenght > 0 then

//test edx,edx

//jle @Else1Begin

//I := 0;

xor eax,eax

//repeat

@RepeatBegin :

//Inc(I);

inc  eax

//until((Str[I] = Chr) or (I > StrLenght));

cmp  bl,[esi+eax-$01]

jz   @If2

cmp  edx,eax

jnl  @RepeatBegin

//if I <= StrLenght then

@If2 :

cmp  edx,eax

jnl  @Exit

//Result := I

//else

//Result := 0;

xor eax,eax

pop  esi

pop  ebx

ret

//else

//Result := 0;

@Else1Begin :

xor eax,eax

@Exit :

pop  esi

pop  ebx

end;

 

 

Теперь мы видим, что Паскаль строка; IF STRLENGHT > 0 THEN, проверяет длину точно также, как первая строка во встроенной LStrLen. Проверка Str на nil вполне достаточно ;-). Вторая строка удалена и первая изменена, чтобы переход был на @Else1Begin вместо простого выхода из встроенной StrLen функции, если Str равен nil. Теперь нет надобности в метке LStrLenExit.

Code:

function CharPos18(Chr: Char; const Str: AnsiString) : Cardinal;

asm

push ebx

push esi

mov  esi,edx

mov  ebx,eax

//StrLenght := Length(Str);

//mov  eax,esi

//if StrLenght > 0 then

//test eax,eax

test esi,esi

jz  @Else1Begin

//mov eax,[eax-$04]

mov eax,[esi-$04]

mov  edx,eax

//I := 0;

xor eax,eax

//repeat

@RepeatBegin :

//Inc(I);

inc  eax

//until((Str[I] = Chr) or (I > StrLenght));

cmp  bl,[esi+eax-$01]

jz   @If2

cmp  edx,eax

jnl  @RepeatBegin

//if I <= StrLenght then

@If2 :

cmp  edx,eax

jnl  @Exit

//Result := I

//else

//Result := 0;

xor eax,eax

pop  esi

pop  ebx

ret

//else

//Result := 0;

@Else1Begin :

xor eax,eax

@Exit :

pop  esi

pop  ebx

end;

 


 

Мы переместили проверку STRLENGHT = 0 и комментарий //IF STRLENGHT > 0 также должен быть перемещен. После встраивания функции стало возможным избавиться от копирования ESI в этих строках.

mov  eax,esi

//*************

test eax,eax

jz  @Else1Begin

mov eax,[eax-$04]

Последние строки переписывают EAX и последнее использованное значение в EAX, которое было скопировано из ESI.

mov  eax,esi

//*************

//test eax,eax

test esi,esi

jz  @Else1Begin

//mov eax,[eax-$04]

mov eax,[esi-$04]

В действительности мы должны также посмотреть в точку возможного перехода Else1Begin и увидим, что значение из EAX также используется здесь. Мы видим, что значение в EAX сразу обнуляется в следующей за меткой строке и поэтому не используется. Так что первая строка кажется лишняя и должна быть удалена.

 

Code:

//mov  eax,esi

test esi,esi

jz  @Else1Begin

mov eax,[esi-$04]

 

function CharPos19(Chr : Char; const Str : AnsiString) : Cardinal;

asm

push ebx

push esi

mov  esi,edx

mov  ebx,eax

//if StrLenght > 0 then

test esi,esi

jz  @Else1Begin

//StrLenght := Length(Str);

//mov eax,[esi-$04]

mov edx,[esi-$04]

//mov  edx,eax

//I := 0;

xor eax,eax

//repeat

@RepeatBegin :

//Inc(I);

inc  eax

//until((Str[I] = Chr) or (I > StrLenght));

cmp  bl,[esi+eax-$01]

jz   @If2

cmp  edx,eax

jnl  @RepeatBegin

//if I <= StrLenght then

@If2 :

cmp  edx,eax

jnl  @Exit

//Result := I

//else

//Result := 0;

xor eax,eax

pop  esi

pop  ebx

ret

//else

//Result := 0;

@Else1Begin :

xor eax,eax

@Exit :

pop  esi

pop  ebx

end;

 

 

Как результат встраивания функции LStrLen мы можем также удалить одну инструкцию. Функция LStrLen возвращает свой результат в EAX, затем он копируется в EDX. MOV EAX, [ESI-$04]. Это можно изменить на MOV EDX, [ESI-$04], а инструкцию MOV EDX, EAX можно удалить.

После этого изменения сменим наш фокус на цикл. Это особенно важно, поскольку это выполняется множество раз, в зависимости от длины строки и позиции, в которой произойдет сравнение.

Code:

function CharPos20(Chr : Char; const Str : AnsiString) : Cardinal;

asm

push ebx

push esi

mov  esi,edx

mov  ebx,eax

//if StrLenght > 0 then

test esi,esi

jz  @Else1Begin

//StrLenght := Length(Str);

mov edx,[esi-$04]

//I := 0;

xor eax,eax

dec  esi

@RepeatBegin :

//Inc(I);

inc  eax

//until((Str[I] = Chr) or (I > StrLenght));

//cmp  bl,[esi+eax-$01]

cmp  bl,[esi+eax]

jz   @If2

cmp  edx,eax

jnl  @RepeatBegin

//if I <= StrLenght then

@If2 :

cmp  edx,eax

jnl  @Exit

//Result := 0;

xor eax,eax

pop  esi

pop  ebx

ret

//Result := 0;

@Else1Begin :

xor eax,eax

@Exit :

pop  esi

pop  ebx

end;

 

Когда мы проанализируем код, то мы увидим, что здесь есть смещение -1 при адресации строки. Нет необходимости вычитать это смещение при каждой итерации. Будет хорошо, если мы один раз уменьшим указатель на Str в ESI до начала цикла. Мы также можем уменьшить счетчик цикла в EAX, но затем мы должны будем увеличить его на единицу при возврате результата.

В самом верху функции два входных параметра копируются в новые регистры. Это излишне и мы должны избавиться от лишнего копирования из обеих, но регистр EAX как счетчик цикла и мы сначала должны найти другой регистр для этой цели.

Code:

function CharPos22(Chr : Char; const Str : AnsiString) : Cardinal;

asm

push ebx

push esi

mov  esi,edx

mov  ebx,eax

//if StrLenght > 0 then

test esi,esi

jz  @Else1Begin

//StrLenght := Length(Str);

mov edx,[esi-$04]

//I := 0;

//xor  eax,eax

xor ecx,ecx

dec  esi

@RepeatBegin :

//Inc(I);

//inc  eax

inc  ecx

//until((Str[I] = Chr) or (I > StrLenght));

//cmp  bl,[esi+eax]

cmp  bl,[esi+ecx]

jz   @If2

//cmp  edx,eax

cmp  edx,ecx

jnl  @RepeatBegin

//if I <= StrLenght then

@If2 :

//cmp  edx,eax

cmp  edx,ecx

jnl  @Exit

//Result := 0;

xor eax,eax

pop  esi

pop  ebx

ret

//Result := 0;

@Else1Begin :

xor eax,eax

pop  esi      //New

pop  ebx      //New

ret           //New

@Exit :

mov  eax, ecx

pop  esi

pop  ebx

end;

 


 

Во всех строках, в которых EAX использовался как I, EAX изменен на ECX. Поскольку I это возвращаемое значение функции при нахождении позиции и возвращаться должно через EAX, мы должны скопировать ECX в EAX до перехода на метку @Exit. Это приводит нас к небольшой проблеме, поскольку переход Else1Begin также осуществляется сюда, и в этой ситуации мы скопируем значение из ECX в EAX, которое мы только что очистили. Это исправляется строками помеченными как «new».

Теперь мы готовы к удалению лишнего копирования EAX. Регистр EBX используется только в одной строке. Это строка CMP BL, [ESI+ECX], которую изменим на CMP AL, [ESI+ECX]. Затем удалим ненужное теперь MOV EBX, EAX. Это устранение лишнего копирования и удаление мертвого кода и мы можем приступить к наиболее важной части оптимизации.

Code:

function CharPos23(Chr : Char; const Str : AnsiString) : Cardinal;

asm

push ebx

push esi

mov  esi,edx

//mov  ebx,eax

//if StrLenght > 0 then

test esi,esi

jz  @Else1Begin

//StrLenght := Length(Str);

mov edx,[esi-$04]

//I := 0;

xor ecx,ecx

dec  esi

@RepeatBegin :

//Inc(I);

inc  ecx

//until((Str[I] = Chr) or (I > StrLenght));

//cmp  bl,[esi+ecx]

cmp  al,[esi+ecx]

jz   @If2

cmp  edx,ecx

jnl  @RepeatBegin

//if I <= StrLenght then

@If2 :

cmp  edx,ecx

jnl  @Exit

//Result := 0;

xor eax,eax

pop  esi

pop  ebx

ret

//Result := 0;

@Else1Begin :

xor eax,eax

pop  esi     

pop  ebx     

ret

@Exit :

mov  eax, ecx

pop  esi

pop  ebx

end;

 

 

Для удаления лишнего копирования EDX (хранит указатель на Str), мы должны освободиться от использования EDX в других местах. Он используется в StrLenght, и мы разместим его в EBX вместо EDX.

Code:

function CharPos24(Chr : Char; const Str : AnsiString) : Cardinal;

asm

push ebx

push esi

mov  esi,edx

//if StrLenght > 0 then

test esi,esi

jz  @Else1Begin

//StrLenght := Length(Str);

//mov edx,[esi-$04]

mov ebx,[esi-$04]

//I := 0;

xor ecx,ecx

dec  esi

@RepeatBegin :

//Inc(I);

inc  ecx

//until((Str[I] = Chr) or (I > StrLenght));

cmp  al,[esi+ecx]

jz   @If2

//cmp  edx,ecx

cmp  ebx,ecx

jnl  @RepeatBegin

//if I <= StrLenght then

@If2 :

//cmp  edx,ecx

cmp  ebx,ecx

jnl  @Exit

//Result := 0;

xor eax,eax

pop  esi

pop  ebx

ret

//Result := 0;

@Else1Begin :

xor eax,eax

pop  esi     

pop  ebx     

ret

@Exit :

mov  eax, ecx

pop  esi

pop  ebx

end;

 

После этого лишнее копирование EDX и MOV ESI, EDX становятся лишними.

Code:

function CharPos25(Chr : Char; const Str : AnsiString) : Cardinal;

asm

push ebx

push esi

//mov  esi,edx

//if StrLenght > 0 then

//test esi,esi

test edx,edx

jz  @Else1Begin

//StrLenght := Length(Str);

//mov ebx,[esi-$04]

mov ebx,[edx-$04]

//I := 0;

xor ecx,ecx

//dec  esi

dec  edx

@RepeatBegin :

//Inc(I);

inc  ecx

//until((Str[I] = Chr) or (I > StrLenght));

//cmp  al,[esi+ecx]

cmp  al,[edx+ecx]

jz   @If2

cmp  ebx,ecx

jnl  @RepeatBegin

//if I <= StrLenght then

@If2 :

cmp  ebx,ecx

jnl  @Exit

//Result := 0;

xor eax,eax

pop  esi

pop  ebx

ret

//Result := 0;

@Else1Begin :

xor eax,eax

pop  esi     

pop  ebx     

ret

@Exit :

mov  eax, ecx

pop  esi

pop  ebx

end;

 


 

Так мы удалили использование ESI и избавились от сохранения и его восстановления. При удалении POP ESI, вспомним, что было три выхода и каждый со своим собственным POP ESI.

Code:

function CharPos26(Chr : Char; const Str : AnsiString) : Cardinal;

asm

push ebx

//push esi

//if StrLenght > 0 then

test edx,edx

jz  @Else1Begin

//StrLenght := Length(Str);

mov ebx,[edx-$04]

//I := 0;

xor ecx,ecx

dec  edx

@RepeatBegin :

//Inc(I);

inc  ecx

//until((Str[I] = Chr) or (I > StrLenght));

cmp  al,[edx+ecx]

jz   @If2

cmp  ebx,ecx

jnl  @RepeatBegin

//if I <= StrLenght then

@If2 :

cmp  ebx,ecx

jnl  @Exit

//Result := 0;

xor eax,eax

//pop  esi

pop  ebx

ret

//Result := 0;

@Else1Begin :

xor eax,eax

//pop  esi     

pop  ebx     

ret

@Exit :

mov  eax, ecx

//pop  esi

pop  ebx

end;

 

В строке после метки If2 есть строка, которая идентична второму сравнению для окончания цикла. В Паскаль эта строка была необходимой, поскольку IF I <= STRLENGHT после цикла, поскольку не было ясно, как закончился цикл. Данная строка порождала лишнею инструкцию CMP EBX, ECX, которая теперь явно не нужна. На самом деле это не так, поскольку есть два перехода на метку If2 и только в одном из них есть проверка. Если мы изменим, эти два перехода так, чтобы только один из них шел на to If2, то мы сможем удалить лишнею проверку. Вместо перехода на If2 при сравнении мы можем сделать переход напрямую на метку Exit.

Code:

function CharPos27(Chr : Char; const Str : AnsiString) : Cardinal;

asm

push ebx

//if StrLenght > 0 then

test edx,edx

jz  @Else1Begin

//StrLenght := Length(Str);

mov ebx,[edx-$04]

//I := 0;

xor ecx,ecx

dec  edx

@RepeatBegin :

//Inc(I);

inc  ecx

//until((Str[I] = Chr) or (I > StrLenght));

cmp  al,[edx+ecx]

//jz   @If2

jz   @Exit

cmp  ebx,ecx

jnl  @RepeatBegin

//if I <= StrLenght then

//@If2 :

//cmp  ebx,ecx

//jnl  @Exit

//Result := 0;

xor eax,eax

pop  ebx

ret

//Result := 0;

@Else1Begin :

xor eax,eax

pop  ebx

ret

@Exit :

mov  eax,ecx

pop  ebx

end;

 

Метка If2 становится лишней и когда мы доходим до этой позиции, то мы знаем, что достигнут конец строки (ограничитель #0) и поэтому на не надо повторно тестировать условие.

Также здесь есть два идентичных куска кода, перед меткой Else1Begin и после ее. Удалим верхний кусок.

Code:

function CharPos28(Chr : Char; const Str : AnsiString) : Cardinal;

asm

push ebx

//if StrLenght > 0 then

test edx,edx

jz   @Else1Begin

//StrLenght := Length(Str);

mov  ebx,[edx-$04]

//I := 0;

xor ecx,ecx

dec  edx

@RepeatBegin :

//Inc(I);

inc  ecx

//until((Str[I] = Chr) or (I > StrLenght));

cmp  al,[edx+ecx]

jz   @Exit

cmp  ebx,ecx

jnl  @RepeatBegin

//Result := 0;

//xor  eax,eax

//pop  ebx

//ret

//Result := 0;

@Else1Begin :

xor eax,eax

pop  ebx

ret

@Exit :

mov  eax,ecx

pop  ebx

end;

На этом наш поиск по удалению лишнего кода закончен. Чистая версия кода выглядит так:

Code:

function CharPos29(Chr : Char; const Str : AnsiString) : Cardinal;

asm

push ebx

//if StrLenght > 0 then

test edx,edx

jz   @Else1Begin

//StrLenght := Length(Str);

mov  ebx,[edx-$04]

//I := 0;

xor ecx,ecx

dec  edx

@RepeatBegin :

//Inc(I);

inc  ecx

//until((Str[I] = Chr) or (I > StrLenght));

cmp  al,[edx+ecx]

jz   @Exit

cmp  ebx,ecx

jnl  @RepeatBegin

@Else1Begin :

//Result := 0;

xor eax,eax

pop  ebx

ret

@Exit :

mov  eax,ecx

pop  ebx

end;

 

При итерации в поиске для нахождения позиции или конца строки, данные строки кода повторяются снова и снова.

inc  ecx

cmp  al,[edx+ecx]

jz   @Exit

cmp  ebx,ecx

jnl  @RepeatBegin


 

Попробуем некоторые варианты и посмотрим, как они исполняются. Наиболее существенно является строка:

cmp  al,[edx+ecx]

Она генерирует две микроинструкции. Одна для загрузки байта по адресу [EDX+ECX] и вторая для сравнения его со значением в AL. Данная строка может быть закодирована также как:

mov ah, byte ptr [edx+ecx]

cmp al, ah

Данный вариант также генерирует две микроинструкции, но это также требует и дополнительный регистр (AH).

Если мы готовы выделить лишний регистр, то это также можно сделать также как:

movzx efx, byte ptr [edx+ecx]

cmp   al, fh

Инструкция MOVZX это пересылка с расширением нуля. Она загружает один байт в младшую часть регистра EFX и заполняет отставшие биты нулями. Конечно, нет такой вещи как регистр efx, но два неиспользуемых регистра ESI и EDI не могут быть доступны на байтовой основе. Поэтому если есть свободный регистр EAX, EBX, ECX или EDX, подставьте это место EDI или ESI и используйте подстановку EBX вместо "EFX".

Данная функция демонстрирует первый вариант.

Code:

function CharPos30(Chr : Char; const Str : AnsiString) : Cardinal;

asm

push ebx

//if StrLenght > 0 then

test edx,edx

jz   @Else1Begin

//StrLenght := Length(Str);

mov  ebx,[edx-$04]

//I := 0;

xor ecx,ecx

dec  edx

@RepeatBegin :

//Inc(I);

inc  ecx

//until((Str[I] = Chr) or (I > StrLenght));

mov  ah, [edx+ecx]

//cmp  al,[edx+ecx]

cmp  al,ah

jz   @Exit

cmp  ebx,ecx

jnl  @RepeatBegin

@Else1Begin :

//Result := 0;

xor eax,eax

pop  ebx

ret

@Exit :

mov  eax,ecx

pop  ebx

end;

 

А эта функция демонстрирует второй вариант.

Code:

function CharPos31(Chr : Char; const Str : AnsiString) : Cardinal;

asm

push ebx

push edi

//if StrLenght > 0 then

test edx,edx

jz   @Else1Begin

//StrLenght := Length(Str);

mov  edi,[edx-$04]

//I := 0;

xor ecx,ecx

dec  edx

@RepeatBegin :

//Inc(I);

inc  ecx

//until((Str[I] = Chr) or (I > StrLenght));

movzx ebx, byte ptr [edx+ecx]

//cmp  al,[edx+ecx]

cmp  al, bl

jz   @Exit

cmp  edi,ecx

jnl  @RepeatBegin

@Else1Begin :

//Result := 0;

xor eax,eax

pop  edi

pop  ebx

ret

@Exit :

mov  eax,ecx

pop  edi

pop  ebx

end;

 

 

Вместо сложения EDX и ECX при расчете адреса в каждой итерации, мы можем их сложить до цикла. Затем если необходимо вычитать их друг из друга для получения счетчика цикла при возврате результата. Это выполняется с помощью инструкции SUB во второй строке поле метки Exit.

Code:

function CharPos32(Chr : Char; const Str : AnsiString) : Cardinal;

asm

push  ebx

push  edi

//if StrLenght > 0 then

test  edx,edx

jz    @Else1Begin

//StrLenght := Length(Str);

mov   edi,[edx-$04]

//I := 0;

xor   ecx,ecx

//dec  edx

add   ecx,edx

@RepeatBegin :

//Inc(I);

//until((Str[I] = Chr) or (I > StrLenght));

movzx ebx, byte ptr [ecx]

inc   ecx

cmp   al, bl

jz    @Exit

//cmp  edi,ecx

test  bl, bl

jnz   @RepeatBegin

@Else1Begin :

//Result := 0;

xor   eax,eax

pop   edi

pop   ebx

ret

@Exit :

mov   eax,ecx

sub   eax,edx

pop   edi

pop   ebx

end;

 

Теперь у нас есть четыре функции для сравнения производительности: CharPos29, CharPos30, CharPos31 и CharPos32.

Результаты на P4 1920 следующие:

CharPos29 716

CharPos30 973

CharPos31 710

CharPos32 702

Победитель функция CharPos32

Результаты на P3 1400 следующие:

CharPos29  949

CharPos30  921

CharPos31  950

CharPos32 1403

Победитель функция CharPos30

Суммарное время

CharPos29 716 + 949  = 1665

CharPos30 973 + 921  = 1894

CharPos31 710 + 950  = 1660

CharPos32 702 + 1403 = 2105

Winner is CharPos31

На P4 выигрышный цикл следующий:

@RepeatBegin :

movzx ebx, byte ptr [ecx]

inc   ecx

cmp   al, bl

jz    @Exit

test  bl, bl

jnz   @RepeatBegin

На P3 выигрышный цикл следующий:

@RepeatBegin :

inc  ecx

mov  ah, [edx+ecx]

cmp  al,ah

jz   @Exit

cmp  ebx,ecx

jnl  @RepeatBegin

При работе на обеих платформах выигрышный цикл следующий:

@RepeatBegin :

inc   ecx

movzx ebx, byte ptr [edx+ecx]

cmp   al, bl

jz    @Exit

cmp   edi,ecx

jnl   @RepeatBegin

Победитель на P4 очень плох на P3 и не может быть использован в библиотеках на других платформах, кроме P4, таких как Delphi RTL. Победитель на P3 выполняется очень отвратительно на P4 и поэтому не должен быть использован в библиотеках для обеих платформ. Победитель для обеих платформ, это функция CharPos31, которая имеет результаты близкие к оптимальным для P4 и также достаточно оптимальные и для P3. Данная функция подходящий выбор для библиотек класса Delphi RTL. Это показывает, что можно оптимизировать функцию для обоих типов процессоров, без потери производительности не более, чем на несколько процентов.

Сравнение производительности P3 и P4 на основе такт-такт всегда немного спектакль. Появляется необоснованная тенденция думать, что P4 имеет as having an inferior design, но это не подтверждается нашим кодом. Взяв победителя для смешанной платформы и сделав приведение результата к 1400 MHz процессору, то мы получим 950 для P3 и 710 * (1920/1400) = 973 для P4. Производительность процессоров почти одинаковая при одинаковой частоте.

 


 

Урок 7

Добро пожаловать на урок номер 7. Темой сегодняшнего урока является плавающая запятая в BASM. Это уже было темой в более раннем уроке, но этот урок даст дополнительную информацию. Мы посмотрим, как кодировать скаляры на SSE2 и как инструкции обслуживаются в конвейерах FP. Сегодняшний пример это расчет полинома третьего порядка.

Code:

function ArcSinApprox1a(X, A, B, C, D : Double) : Double;

begin

Result := A*X*X*X + B*X*X + C*X + D;

end;

 

 

Вместо анализа и оптимизации этой функции мы посмотрим, как реально мы можем ее использовать. Полином третьего порядка может аппроксимировать функцию в ее интервале, [-1;1], с максимальной абсолютной ошибкой 0.086. Это не очень высокая точность, но то что мы разработаем в данном уроке можно будет расширить до более высоких порядков, в той же манере для получения большей точности.

Параметры A, B, C и D определяют форму кривой для функции и значения для аппроксимации в ArcSin с минимальной ошибкой. Для этой цели мы разработаем оптимизатор, который будет использоваться для измерения производительности. Поскольку ARCSIN(0) = 0 мы непосредственно видим, что D=0 и D можно вывести из оптимизации. Мы также знаем, что ArcSin это нечетная функция и поэтому выражение второго порядка B*X*X не используется в аппроксимации. Это поскольку выражение второго порядка  четное и симметрично относительно оси Y. Функции нечетных порядков имеют анти симметрию вокруг оси Y с F(X) = -F(-X). Все это означает, что наша функция может быть уменьшена до

Result := A*X*X*X + C*X;

Тем не менее, мы не поступим так, поскольку это будет более наглядно с полной функцией. ArcSin это особый случай, и мы хотим сделать его обычным, насколько это возможно.

В функции номер 1a имеется 6 умножений и три сложения. Напишем ее в виде формы Хорнера (Horner form). 

Result := ((A*X + B)*X + C)*X + D;

Уменьшив этим до трех умножений и сложений.

Другая форма такая

Result := (A*X + B)*(X*X)+(C*X + D);

Здесь четыре умножения и три сложения.

На современных процессорах очень важно распараллеливания можно извлечь из формулы и как много умножений и сложений она имеет. Современные процессоры, такие как AMD Athlon, Intel P4 и P3 имеют конвейеры. Конвейеры необходимы на процессорах, работающих на высокой частоте, поскольку основные операции сложения, вычитания, умножения или деления не могут быть выполнены за один такт частоты. На P4 есть конвейер называемый FP_ADD, который предназначен для операций сложения и вычитания. Этот конвейер имеет 5 состояний, это означает, что процесс сложения или вычитания может быть разбит на 5 подзадач. Следовательно, сложение и вычитание выполняются за 5 тактов. Преимущество конвейера состоит в том, что хотя операция требует 5 тактов, но зато каждая новая операция может начинаться в каждом такте. Это потому что первое сложение покидает первую подзадачу при втором такте и эта подзадача может начинать сложение для второго числа. Если мы имеем серию сложений, то первое сложение покидает конвейер на такте 5, второе на такте 6 и так далее. Производительность Throughput получается всего в один такт. Параллельность составляет до 5 сложений или вычитаний в конвейере одновременно. Проблема в том, что если второе или следующие сложения связаны с первым сложением, то придется ожидать, когда закончится первое сложение. Мы можем сказать, что здесь есть зависимость данных между двумя инструкциями, и мы видим, что полная латентность для сложения составляет 2 раза по 5 тактов.

Посмотрим на основе нашей функции работу конвейера.

Result := A*X*X*X + B*X*X + C*X + D;

Также видно, что четвертое выражение может выполняться параллельно, и затем сложено в конце действия. A*X это первая инструкция, готовая для обработки в конвейере F_MUL. Латентность для FMUL на P4 составляет 7 тактов и выражение A*X будет готово через 7 тактов. FMUL имеет максимальную пропускную способность (throughput) в 2 такта. Отсюда ясно, что FMUL не полностью конвейеризирован. Конвейер принимает новую инструкцию на такте три, а не на втором. B*X это вторая инструкция, готовая к выполнению и процессор начнет ее выполнение на такте 3. В такте 5 конвейер снова готов к принятию новой инструкции и это будет инструкция C*X. В такте 7 выполнение инструкции A*X будет закончено и выражение (A*X)*X можно будет начать вычислять в такте 8. В такте 10 вычисление выражения B*X будет закончено и процессор начнет выполнению выражения (B*X)*X. В такте 12 также будет закончено выполнение C*X и конвейер F_ADD прибавит значение D. В такте 15 будет закончено вычисление (A*X)*X и можно будет начинать выражение (A*X*X)*X. В такте 17 выражения (B*X)*X и (C*X) + D будут закончены и можно начать работу с конвейером F_ADD. Данное сложение будет закончено на такте 21, где выражение (A*X*X)*X также будет готово. Последнее сложение можно будет начать на такте 22. Осталась только одна операция в действии, и мы должны подождать до полной латентности FADD, которая составляет 5 тактов. На такте 27 последнее сложение будет закончено и работа будет выполнена.

Данные таблицы покажут это в деталях. Левая колонка символизирует конвейер F_MUL , с 7 состояниями 7, а правая конвейер F_ADD на 5 состояний.

F_MUL

F_ADD

A*X

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Такт 1

F_MUL

F_ADD

 

 

A*X

 

 

 

 

 

 

 

 

 

 

 

Такт 2

F_MUL

F_ADD

B*X

 

 

 

A*X

 

 

 

 

 

 

 

 

 

Такт 3

F_MUL

F_ADD

 

 

B*X

 

 

 

A*X

 

 

 

 

 

 

 

Такт 4

F_MUL

F_ADD

C*X

 

 

 

B*X

 

 

 

A*X

 

 

 

 

 

Такт 5

F_MUL

F_ADD

 

 

C*X

 

 

 

B*X

 

 

 

A*X

 

 

 

Такт 6

F_MUL

F_ADD

 

 

 

 

C*X

 

 

 

B*X

 

 

 

A*X

 

Такт 7

F_MUL

F_ADD

(A*X)*X

 

 

 

 

 

C*X

 

 

 

B*X

 

 

 

Такт 8

F_MUL

F_ADD

 

 

(A*X)*X

 

 

 

 

 

C*X

 

 

 

B*X

 

Такт 9

F_MUL

F_ADD

(B*X)*X

 

 

 

(A*X)*X

 

 

 

 

 

C*X

 

 

 

 

Такт 10

F_MUL

F_ADD

 

 

(B*X)*X

 

 

 

(A*X)*X

 

 

 

 

 

C*X

 

Такт 11

F_MUL

F_ADD

 

(C*X)+D

 

 

(B*X)*X

 

 

 

(A*X)*X

 

 

 

 

 

Такт 12

F_MUL

F_ADD

 

 

 

(C*X)+D

 

 

(B*X)*X

 

 

 

(A*X)*X

 

 

 

Такт 13

F_MUL

F_ADD

 

 

 

 

 

(C*X)+D

 

 

(B*X)*X

 

 

 

(A*X)*X

 

Такт 14

F_MUL

F_ADD

(A*X*X)*X

 

 

 

 

 

 

(C*X)+D

 

 

(B*X)*X

 

 

 

Такт 15

F_MUL

F_ADD

 

 

(A*X*X)*X

 

 

 

 

 

 

(C*X)+D

 

 

(B*X)*X

 

Такт 16

F_MUL

F_ADD

 

(B*X*X)+(C*X+D)

 

 

(A*X*X)*X

 

 

 

 

 

 

 

 

 

Такт 17

F_MUL

F_ADD

 

 

 

(B*X*X)+(C*X+D)

 

 

(A*X*X)*X

 

 

 

 

 

 

 

Такт 18

F_MUL

F_ADD

 

 

 

 

 

(B*X*X)+(C*X+D)

 

 

(A*X*X)*X

 

 

 

 

 

Такт 19

F_MUL

F_ADD

 

 

 

 

 

 

 

(B*X*X)+(C*X+D)

 

 

(A*X*X)*X

 

 

 

 

Такт 20

F_MUL

F_ADD

 

 

 

 

 

 

 

 

 

(B*X*X)+(C*X+D)

 

 

(A*X*X)*X

 

Такт 21

F_MUL

F_ADD

 

(A*X*X*X)+ (B*X*X+C*X+D)

 

 

 

 

 

 

 

 

 

 

 

 

Такт 22

F_MUL

F_ADD

 

 

 

(A*X*X*X)+ (B*X*X+C*X+D)

 

 

 

 

 

 

 

 

 

 

Такт 23

F_MUL

F_ADD

 

 

 

 

 

(A*X*X*X)+ (B*X*X+C*X+D)

 

 

 

 

 

 

 

 

Такт 24

F_MUL

F_ADD

 

 

 

 

 

 

 

(A*X*X*X)+ (B*X*X+C*X+D)

 

 

 

 

 

 

Такт 25

F_MUL

F_ADD

 

 

 

 

 

 

 

 

 

(A*X*X*X)+ (B*X*X+C*X+D)

 

 

 

 

Такт 26

F_MUL

F_ADD

 

 

 

 

 

 

 

 

 

Finished

 

 

 

 

Такт 27


 

Порядок обработки инструкций меняется по мере готовности данных и ресурсов. Ресурсы это регистры и конвейеры исполнения. Я не вполне уверен, но я думаю, что инструкции обслуживаются в порядке поступления из программы, за исключением, когда инструкция задерживается. В этой ситуации следующая готовая инструкция начинает обслуживаться. Задержанная инструкция продолжит выполняться, как только причина задержки исчезнет. Задержка может произойти по причине отсутствия ресурса или не готовности данных.

После того, как мы посмотрели, как обслуживаются инструкции в конвейерах P4, мы приступим к измерению. Оптимизатор измерения ищет наилучшую возможность для нашего полинома ArcSin. Он базируется на наиболее простом алгоритме оптимизации, это исчерпывающий поиск. Мы просто пробуем множество комбинаций параметров и запоминаем каждый набор параметров, который дает наилучший результат. A и C начинаются в интервалах [AStart; AEnd] и [CStart; CEnd], а размер шага AStepSize и CStepsize. Это делается с помощью двух вложенных циклов.

Code:

StartA    := 0;

StartC    := -1;

EndA      := 1;

EndC      := 1;

AStepSize := 1E-2;

CStepSize := 1E-3;

OptA      := 9999;

OptC      := 9999;

A         := StartA;

while A <= EndA do

begin

C := StartC;

while C <= EndC do

begin

   Inc(NoOfIterations);

   MaxAbsError := CalculateMaxAbsError(A,C, ArcSinArray);

   if MaxAbsError <= MinMaxAbsError then

   begin

     MinMaxAbsError := MaxAbsError;

     OptA := A;

     OptC := C;

   end;

   C := C + CStepSize;

end;

A := A + AStepSize;

end;

 

Функция CalculateMaxAbsError рассчитывает количество точек X на интервале [-1;1], который определяет интервал функции ArcSin .

Code:

TMainForm.CalculateMaxAbsError(A, C : Double; ArcSinArray : TArcSinArray) : Double;

var

X, Y, D, B, Yref, Error, AbsError, MaxAbsError : Double;

 

begin

B := 0;

D := 0;

MaxAbsError := 0;

X := -1;

repeat

   Yref := ArcSin (X);

   Y := ArcSinApproxFunction(X, A, B, C, D);

   Error := Yref-Y;

   AbsError := Abs(Error);

   MaxAbsError := Max(MaxAbsError, AbsError);

   X := X + XSTEPSIZE;

until(X > 1);

Result := MaxAbsError;

end;

 

 

в каждой точке мы рассчитываем ошибку, вычитая значение Y из нашей функции аппроксимации из ссылки значения Y, полученное из  Delphi RTL функции ArcSin. Ошибка может быть положительной или отрицательной, нас же интересует абсолютное значение. Мы помним, что наибольшее абсолютное значение ошибки получается из двух значений MaxAbsError и AbsError, назначая из MaxAbsError. MaxAbsError инициализируется нулем, и в первом вычисление принимает значение первой ошибки (если она больше нуля). MaxAbsError возвращает результат из функции, после окончания полного цикла. В функции оптимизатора, два значения A и C, которые дают наименьшую максимальную ошибку, запоминаются вместе с действительным значением MinMaxAbsError.

Все, что делается в оптимизаторе это возможность расчета максимально количества комбинаций. По этой причине мы должны оптимизировать оптимизатор ;-), и функцию расчета. В этом уроке наши цели немного отличаются, поскольку все, что мы хотим, это получение правильных измерений для функций, которые мы хотим оптимизировать. Это все равно означает, что код оптимизатора должен занимать как можно меньше тактов, так как используемые в функциях большая часть общего количества использованных тактов. Первая оптимизация оптимизатора, которую мы сделаем, состоит в том, что не надо рассчитывать ссылки функции снова и снова. При возврате, нам не важно, какие значения имели A и C. Сделаем ссылку один раз и запишем значение Yref в массив.

Следующей оптимизации подвержены строки, которые рассчитывают MaxAbsError.

Длинная версия

Yref := ArcSinArray[I];

Error := Yref-Y;

AbsError := Abs(Error);

Короткая версия

AbsError := Abs(ArcSinArray[I]-Y);

Это поможет, поскольку Delphi создает множество лишнего кода, при компиляции FP кода.

Длинная версия компилируется в следующее

Code:

Yref := ArcSinArray[I];

 

mov eax,[ebp-$14]

mov edx,[eax+ebx*8]

mov [ebp-$48],edx

mov edx,[eax+ebx*8+$04]

mov [ebp-$44],edx

 

Error := Yref-Y;

 

fld   qword ptr [ebp-$48]

fsub qword ptr [ebp-$30]

fstp  qword ptr [ebp-$50]

wait

 

AbsError := Abs(Error);

 

fld qword ptr [ebp-$50]

fabs

fstp qword ptr [ebp-$10]

wait

 


 

Здесь множество излишеств в данном коде и мы должны заключить, что 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;

 


 

 

В действительности компилятор также вставил инструкцию MOV и мы можем избавиться от лишних пересылок, но не получим преимущества, поскольку нет удаления мертвого кода. Поэтому производительность остается почти такой же - 43094.

При понимании, где результат записывается в стек, мы сможем оптимизировать строки копирования и перезагрузки их. Результат состоит в том, что здесь уже есть копия переменной Result в стеке. Это уменьшает необходимость извлечения результат из стека FP и загрузки Result из стека. Эта одиночная строка имеет тоже действие, но избыточность удалена.

fst   qword ptr [ebp-$08]

Подобная оптимизация очень часто возможна в коде, сгенерированным компилятором Delphi и об этом важно помнить.

Code:

function ArcSinApprox1f(X, A, B, C, D : Double) : Double;

asm

//Result := A*X*X*X + B*X*X + C*X + D;

fld   qword ptr [esp+$20]

fmul  qword ptr [esp+$28]

fmul  qword ptr [esp+$28]

fmul  qword ptr [esp+$28]

fld   qword ptr [esp+$18]

fmul  qword ptr [esp+$28]

fmul  qword ptr [esp+$28]

faddp

fld   qword ptr [esp+$10]

fmul  qword ptr [esp+$28]

faddp

fadd  qword ptr [esp+$08]

//fstp  qword ptr [esp-$08]

fst  qword ptr [esp-$08]

wait

//fld   qword ptr [esp-$08]

end;

 

 

Данная реализация получила 47939 пункта, и это улучшило результат на 11%.

Следующий вопрос, который мы должны задать себе: А копия Result на стеке используется? Для ответа мы должны проинспектировать код в месте вызова функции.

Y := ArcSinApproxFunction(X, A, B, C, D);

 

call dword ptr [ArcSinApproxFunction]

fstp qword ptr [ebp-$30]

wait

Первая строка после вызова, записывает результат в Y и извлекает из стека. Видя это, мы можем сделать вывод, что результат на стеке не используется, но чтобы быть уверенным мы должны просмотреть также и остаток кода. Если правило для соглашения по регистровому вызову гласит, что результат с плавающей запятой (FP) возвращается в стеке процессора с плавающей запятой, то несколько странно хранить еще и его копию в стеке. Заключаем, что это избыточно копировать Result на стек и затем извлекать его из стека и удали строку, которая делает это.

Code:

function ArcSinApprox1g(X, A, B, C, D : Double) : Double;

asm

//Result := A*X*X*X + B*X*X + C*X + D;

fld   qword ptr [esp+$20]

fmul  qword ptr [esp+$28]

fmul  qword ptr [esp+$28]

fmul  qword ptr [esp+$28]

fld   qword ptr [esp+$18]

fmul  qword ptr [esp+$28]

fmul  qword ptr [esp+$28]

faddp

fld   qword ptr [esp+$10]

fmul  qword ptr [esp+$28]

faddp

fadd  qword ptr [esp+$08]

//fst  qword ptr [esp-$08]

wait

end;

 

Данная функция получила 47405 пункта

Вместо написания всех QWORD PTR [ESP+$XX] строк мы можем писать имена переменных и позволить компилятору рассчитать за нас адреса. Это делает код более безопасным. Если положение переменной будет изменено, то код будет неработоспособным, при использовании жесткой адресации. Это может произойти при смене соглашения по вызову, что конечно бывает редко.

Code:

function ArcSinApprox1g_2(X, A, B, C, D : Double) : Double;

asm

//Result := A*X*X*X + B*X*X + C*X + D;

//fld   qword ptr [esp+$20]

fld   A

//fmul  qword ptr [esp+$28]

fmul  X

//fmul  qword ptr [esp+$28]

fmul  X

//fmul  qword ptr [esp+$28]

fmul  X

//fld   qword ptr [esp+$18]

fld   B

//fmul  qword ptr [esp+$28]

fmul  X

//fmul  qword ptr [esp+$28]

fmul  X

faddp

//fld   qword ptr [esp+$10]

fld   C

//fmul  qword ptr [esp+$28]

fmul  X

faddp

//fadd  qword ptr [esp+$08]

fadd  D

wait

end;

 

Попробуй оба типа строк

fld   qword ptr [esp+$20]

fld   A

и посмотрите в окне CPU view, что компилятор сгенерировал абсолютно идентичный код для обеих версий.

X используется во многих строках и ссылается не стек. И поэтому загружается со стека во внутренние регистры процессора с плавающей запятой каждый раз. Будет быстрее загрузить X один раз в регистровый стек процессора и изменить все ссылки на него.

Code:

function ArcSinApprox1h(X, A, B, C, D : Double) : Double;

asm

//Result := A*X*X*X + B*X*X + C*X + D;

fld   qword ptr [esp+$20]

fld   qword ptr [esp+$28] //New

fxch

//fmul qword ptr [esp+$28]

fmul  st(0),st(1)

//fmul qword ptr [esp+$28]

fmul  st(0),st(1)

//fmul qword ptr [esp+$28]

fmul  st(0),st(1)

fld   qword ptr [esp+$18]

//fmul qword ptr [esp+$28]

fmul  st(0),st(2)

//fmul qword ptr [esp+$28]

fmul  st(0),st(2)

faddp

fld   qword ptr [esp+$10]

//fmul qword ptr [esp+$28]

fmul  st(0),st(2)

ffree st(2)

faddp

fadd  qword ptr [esp+$08]

fst   qword ptr [esp-$08]

wait

end;

 

 

Добавленная, вторая строка загружает X один раз, для всех операция. Поскольку она загружает X на верхушку стека ST(0), а эта позиция нужна как временная переменная, то мы обменяем регистр ST(0) с ST(1), с помозью инструкции FXCH. Мы также можем поменять местами строки 1 и 2 и получить тот же эффект. Все строки умножения st(0) на X

fmul qword ptr [esp+$28]

мы заменим на

fmul  st(0),st(1)

после последнего использования копии X, мы удалим ее инструкцией FFREE.

Данная реализация получила уже 46882 пункта и ухудшила производительность на 1%. Это стало сюрпризом. Инструкция FXCH объявлена Intel, как не занимающая времени, поскольку используется переименование внутренних регистров. Попробуем проверить это, просто удалив ее.

Code:

function ArcSinApprox1i(X, A, B, C, D : Double) : Double;

asm

//Result := A*X*X*X + B*X*X + C*X + D;

fld   qword ptr [esp+$28]

fld   qword ptr [esp+$20]

//fld   qword ptr [esp+$28]

//fxch

fmul  st(0),st(1)

fmul  st(0),st(1)

fmul  st(0),st(1)

fld   qword ptr [esp+$18]

fmul  st(0),st(2)

fmul  st(0),st(2)

faddp

fld   qword ptr [esp+$10]

fmul  st(0),st(2)

ffree st(2)

faddp

fadd  qword ptr [esp+$08]

wait

end;

 


 

 

Теперь функция получила 45393 пункта, и производительность изменилась на 3%. FXCH действительно ни причем, поскольку производительность опять ушла вниз. В чем же дело?

Инструкция WAIT была рассмотрена в более раннем уроке, и в данный момент мы просто удалим ее.

Code:

function ArcSinApprox1j(X, A, B, C, D : Double) : Double;

asm

//Result := A*X*X*X + B*X*X + C*X + D;

fld   qword ptr [esp+$28]

fld   qword ptr [esp+$20]

fmul  st(0),st(1)

fmul  st(0),st(1)

fmul  st(0),st(1)

fld   qword ptr [esp+$18]

fmul  st(0),st(2)

fmul  st(0),st(2)

faddp

fld   qword ptr [esp+$10]

fmul  st(0),st(2)

ffree st(2)

faddp

fadd  qword ptr [esp+$08]

//wait

end;

 

Производительно упала до 44140.

Посмотрим эти удивляющие нас результаты на процессоре P3.

ArcSinApprox1a         63613

ArcSinApprox1b         64412

ArcSinApprox1c         64433

ArcSinApprox1d         65062

ArcSinApprox1e         64830

ArcSinApprox1f         62598

ArcSinApprox1g         79586

ArcSinApprox1h         85361

ArcSinApprox1i         80515

ArcSinApprox1j         80192

Во-первых, видим, что вариант ArcSinApprox1h самый быстрый на P3. Поэтому видно, что загрузка данных из кэш памяти L1 более ощутима на P3, чем на P4, поскольку изменение кода, такое как одноразовая загрузка X дало существенное улучшение производительности на P3, и почти нет на P4. С другой стороны мы можем также сказать, что получение данных из кэш памяти всегда медленнее, чем получение из внутренних регистров. P4 имеет быструю кэш память уровня L1, которая читается только за 2 такта, но внутренние регистры еще быстрее, только один такт. Мы также видим, что P3 на частоте 1400 примерно на 80% быстрее, чем P4 на частоте 1920 в данном коде. Мы знаем, что латентность на P3 короче, но этого недостаточно для объяснения такой большой разницы.

Латентность и ускорение (throughput) по использованным регистрам на P3

FADD latency is 3 clock cycles and throughput is 1

FMUL latency is 5 clock cycles and throughput is 1

На P4

FADD latency is 5 clock cycles and throughput is 1

FMUL latency is 7 clock cycles and throughput is 2

Я не смог найти данных для FLD

Объяснение плохой производительности P4 в данном коде состоит в 2-тактном сквозном проходе по конвейеру (throughput) для FMUL, совместно с медленным доступом до FP регистров процессора. Конвейер FMUL получает доступ до следующей инструкции только за два такта, тогда как P3 за один такт.

Нормализованный к частоте результат

47939 / 1920 = 25

85361 / 1400 = 61

разоблачает, что при приведении частот процессор P3 примерно в 2.5 раза быстрее P4. Это вызывает подлинное удивление. Чтобы P4 имел некоторые шансы, по отношению к P 3, нам мы должны убрать некоторые умножения. Это получается в функции по версии Хорнера.

Code:

function ArcSinApprox3a(X, A, B, C, D : Double) : Double;

begin

Result := ((A*X + B)*X + C)*X + D;

end;

 

 

Это компилируется в

Code:

function ArcSinApprox3b(X, A, B, C, D : Double) : Double;

begin

{

push ebp

mov  ebp,esp

add  esp,-$08

}

Result := ((A*X + B)*X + C)*X + D;

{

fld  qword ptr [ebp+$20]

fmul qword ptr [ebp+$28]

fadd qword ptr [ebp+$18]

fmul qword ptr [ebp+$28]

fadd qword ptr [ebp+$10]

fmul qword ptr [ebp+$28]

fadd qword ptr [ebp+$08]

fstp qword ptr [ebp-$08]

wait

fld  qword ptr [ebp-$08]

}

{

pop  ecx

pop  ecx

pop  ebp

}

end;

 

 

Первые три версии этой функции идентичны и они получают свои очки без сюрпризов. Наша методика измерения не совсем хороша, но дает достаточную точность в текущий момент ;-)

ArcSinApprox3a        45076

ArcSinApprox3b        45076

ArcSinApprox3c        45076

Оптимизация следует по тому же шаблону, как и в первой функции. Вот первая BASM версия без оптимизации. Закомментирован код добавленный компилятором.

Code:

function ArcSinApprox3c(X, A, B, C, D : Double) : Double;

asm

//push ebp

//mov ebp,esp

add  esp,-$08

//Result := ((A*X + B)*X + C)*X + D;

fld  qword ptr [ebp+$20]

fmul qword ptr [ebp+$28]

fadd qword ptr [ebp+$18]

fmul qword ptr [ebp+$28]

fadd qword ptr [ebp+$10]

fmul qword ptr [ebp+$28]

fadd qword ptr [ebp+$08]

fstp qword ptr [ebp-$08]

wait

fld  qword ptr [ebp-$08]

pop  ecx

pop  ecx

//pop ebp

end;

 

Первым делом удаляем строку ADD ESP, -$08 и две строки POP ECX. Они устанавливают фрейм стека, но ничего не делают кроме манипулирования указателем стека, который нигде не используется.

Code:

function ArcSinApprox3d(X, A, B, C, D : Double) : Double;

asm

//add  esp,-$08

//Result := ((A*X + B)*X + C)*X + D;

fld  qword ptr [ebp+$20]

fmul qword ptr [ebp+$28]

fadd qword ptr [ebp+$18]

fmul qword ptr [ebp+$28]

fadd qword ptr [ebp+$10]

fmul qword ptr [ebp+$28]

fadd qword ptr [ebp+$08]

fstp qword ptr [ebp-$08]

wait

fld  qword ptr [ebp-$08]

//pop  ecx

//pop  ecx

end;

 

Данная функция получила 43535 пункта.

Обе лишние строки, копирующие результат на стек и обратно, удалены одновременно.

Code:

function ArcSinApprox3e(X, A, B, C, D : Double) : Double;

asm

//Result := ((A*X + B)*X + C)*X + D;

fld  qword ptr [ebp+$20]

fmul qword ptr [ebp+$28]

fadd qword ptr [ebp+$18]

fmul qword ptr [ebp+$28]

fadd qword ptr [ebp+$10]

fmul qword ptr [ebp+$28]

fadd qword ptr [ebp+$08]

//fstp qword ptr [ebp-$08]

wait

//fld  qword ptr [ebp-$08]

end;

Этот вариант получил 47237 пункта, и улучшение составило 8.5%

 


 

Затем изменим код, таким образом, чтобы X загружался только один раз.

Code:

function ArcSinApprox3f(X, A, B, C, D : Double) : Double;

asm

//Result := ((A*X + B)*X + C)*X + D;

fld   qword ptr [ebp+$20]

fld   qword ptr [ebp+$28]

fxch

//fmul qword ptr [ebp+$28]

fmul  st(0),st(1)

fadd  qword ptr [ebp+$18]

//fmul qword ptr [ebp+$28]

fmul  st(0),st(1)

fadd  qword ptr [ebp+$10]

//fmul qword ptr [ebp+$28]

fmul  st(0),st(1)

ffree st(1)

fadd qword ptr [ebp+$08]

wait

end;

 

Данная функция получила 47226 пункта и без изменения производительности.

Инструкция FFREE может быть удалена, за счет использования инструкции FMULP вместо FMUL, но для этого мы должны сменить два используемых регистра. Только эти два регистра используются и A*B = B*A, так что нет проблем сделать это. Этим мы не удаляем некоторую избыточность, и оба пути дают одинаковый результат.

Code:

function ArcSinApprox3g(X, A, B, C, D : Double) : Double;

asm

//Result := ((A*X + B)*X + C)*X + D;

fld   qword ptr [ebp+$20]

fld   qword ptr [ebp+$28]

fxch  st(1)

fmul  st(0),st(1)

fadd  qword ptr [ebp+$18]

fmul  st(0),st(1)

fadd  qword ptr [ebp+$10]

//fmul  st(0),st(1)

fmulp st(1),st(0)

//ffree st(1)

fadd qword ptr [ebp+$08]

wait

end;

 

Данная реализация получила 47416 пункта.

Затем мы удалим инструкцию WAIT.

Code:

function ArcSinApprox3h(X, A, B, C, D : Double) : Double;

asm

//Result := ((A*X + B)*X + C)*X + D;

fld   qword ptr [ebp+$20]

fld   qword ptr [ebp+$28]

fxch  st(1)

fmul  st(0),st(1)

fadd  qword ptr [ebp+$18]

fmul  st(0),st(1)

fadd  qword ptr [ebp+$10]

fmulp st(1),st(0)

fadd qword ptr [ebp+$08]

//wait

end;

 

Теперь функция получила 47059 пункта.

Последняя вещь, которую мы сделаем, это строки, производящие загрузку X и A, и удалим инструкцию FXCH.

Code:

function ArcSinApprox3i(X, A, B, C, D : Double) : Double;

asm

//Result := ((A*X + B)*X + C)*X + D;

fld   qword ptr [ebp+$28]

fld   qword ptr [ebp+$20]

//fld   qword ptr [ebp+$28]

//fxch  st(1)

fmul  st(0),st(1)

fadd  qword ptr [ebp+$18]

fmul  st(0),st(1)

fadd  qword ptr [ebp+$10]

fmulp st(1),st(0)

fadd qword ptr [ebp+$08]

end;

 

 

Эта реализация функции получила 46544 и производительность упала!

Теперь сравним производительность по версии Хорнера с функцией, получившей наибольшую производительность на P4.

ArcSinApprox1g 47939

ArcSinApprox3g         47416

На P3

ArcSinApprox1h         85361

ArcSinApprox3h 87604

Различие не большое, но обычная функция немного быстрее на P4 и медленнее на P3. Обычная функция имеет больше вычисление, но параллелизм это сгладил. Вариант Хорнера имеющий маленький параллелизм и латентность проявляется в полной мере. Это особо плохо на P4.

Держим это в уме и продолжаем с третьим решением, которое выглядит так.

Code:

function ArcSinApprox4b(X, A, B, C, D : Double) : Double;

begin

{

push  ebp

mov  ebp,esp

add   esp,-$08

}

Result := (A*X + B)*(X*X)+(C*X + D);

{

fld     qword ptr [ebp+$20]

fmul  qword ptr [ebp+$28]

fadd  qword ptr [ebp+$18]

fld     qword ptr [ebp+$28]

fmul  qword ptr [ebp+$28]

fmulp st(1)

fld     qword ptr [ebp+$10]

fmul  qword ptr [ebp+$28]

fadd   qword ptr [ebp+$08]

faddp st(1)

fstp   qword ptr [ebp-$08]

wait

fld   qword ptr [ebp-$08]

}

{

pop ecx

pop ecx

pop ebp

}

end;

 

 

Опыт уже позволяет нам сделать это просто и быстро ;-)

Данная версия сделана так, как это сделала Delphi

Code:

function ArcSinApprox4c(X, A, B, C, D : Double) : Double;

asm

//push ebp

//mov ebp,esp

add   esp,-$08

//Result := (A*X + B)*(X*X)+(C*X + D);

fld     qword ptr [ebp+$20]

fmul  qword ptr [ebp+$28]

fadd  qword ptr [ebp+$18]

fld     qword ptr [ebp+$28]

fmul  qword ptr [ebp+$28]

fmulp //st(1)

fld     qword ptr [ebp+$10]

fmul  qword ptr [ebp+$28]

fadd  qword ptr [ebp+$08]

faddp //st(1)

fstp   qword ptr [ebp-$08]

wait

fld   qword ptr [ebp-$08]

pop   ecx

pop   ecx

//pop  ebp

end;

 


 

Удаляем фрейм стека и две строки, которые пишут результат на стек

Code:

function ArcSinApprox4d(X, A, B, C, D : Double) : Double;

asm

//add  esp,-$08

//Result := (A*X + B)*(X*X)+(C*X + D);

fld  qword ptr [ebp+$20]

fmul qword ptr [ebp+$28]

fadd qword ptr [ebp+$18]

fld  qword ptr [ebp+$28]

fmul qword ptr [ebp+$28]

fmulp //st(1)

fld  qword ptr [ebp+$10]

fmul qword ptr [ebp+$28]

fadd qword ptr [ebp+$08]

faddp //st(1)

//fstp qword ptr [ebp-$08]

wait

//fld  qword ptr [ebp-$08]

//pop  ecx

//pop  ecx

end;

 

Загружаем X только раз

Code:

function ArcSinApprox4e(X, A, B, C, D : Double) : Double;

asm

//Result := (A*X + B)*(X*X)+(C*X + D);

fld   qword ptr [ebp+$20]

fld   qword ptr [ebp+$28]

//fmul qword ptr [ebp+$28]

fxch

fmul  st(0),st(1)

fadd  qword ptr [ebp+$18]

//fld  qword ptr [ebp+$28]

fld   st(1)

//fmul  qword ptr [ebp+$28]

fmul  st(0),st(2)

fmulp

fld   qword ptr [ebp+$10]

//fmul  qword ptr [ebp+$28]

fmul  st(0),st(2)

fadd  qword ptr [ebp+$08]

faddp

ffree st(1)

wait

end;

Удаляем FXCH и WAIT.

Code:

function ArcSinApprox4f(X, A, B, C, D : Double) : Double;

asm

//Result := (A*X + B)*(X*X)+(C*X + D);

fld   qword ptr [ebp+$28]

fld   qword ptr [ebp+$20]

//fxch

fmul  st(0),st(1)

fadd  qword ptr [ebp+$18]

fld   st(1)

fmul  st(0),st(2)

fmulp

fld   qword ptr [ebp+$10]

fmul  st(0),st(2)

fadd  qword ptr [ebp+$08]

faddp

ffree st(1)

//wait

end;

Переопределяем FFREE ST(1)

Code:

function ArcSinApprox4g(X, A, B, C, D : Double) : Double;

asm

//Result := (A*X + B)*(X*X)+(C*X + D);

fld   qword ptr [ebp+$28]

fld   qword ptr [ebp+$20]

fmul  st(0),st(1)

fadd  qword ptr [ebp+$18]

fld   st(1)

fmul  st(0),st(2)

fmulp

fld   qword ptr [ebp+$10]

fmul  st(0),st(2)

ffree st(2)

fadd  qword ptr [ebp+$08]

faddp

//ffree st(1)

end;

заменяем FMUL/FFREE на FMULP

Code:

function ArcSinApprox4h(X, A, B, C, D : Double) : Double;

asm

//Result := (A*X + B)*(X*X)+(C*X + D);

fld   qword ptr [ebp+$28]

fld   qword ptr [ebp+$20]

fmul  st(0),st(1)

fadd  qword ptr [ebp+$18]

fld   st(1)

fmul  st(0),st(2)

fmulp

fld   qword ptr [ebp+$10]

//fmul  st(0),st(2)

fmulp st(2),st(0)

//ffree st(2)

fadd  qword ptr [ebp+$08]

faddp

end;

Очищаем код и видим, что компилятор еще использует EBP и излишне модифицирует ESP.

Code:

function ArcSinApprox4i(X, A, B, C, D : Double) : Double;

asm

//Result := (A*X + B)*(X*X)+(C*X + D);

fld   qword ptr [ebp+$28]

fld   qword ptr [ebp+$20]

fmul  st(0),st(1)

fadd  qword ptr [ebp+$18]

fld   st(1)

fmul  st(0),st(2)

fmulp

fld   qword ptr [ebp+$10]

fmulp st(2),st(0)

fadd  qword ptr [ebp+$08]

faddp

end;

 

 

Теперь большой вопрос, насколько хорошо эта функция работает.

ArcSinApprox4a        45228

ArcSinApprox4b        45239

ArcSinApprox4c        45228

ArcSinApprox4d        51813

ArcSinApprox4e        49044

ArcSinApprox4f        48674

ArcSinApprox4g        48852

ArcSinApprox4h        44914

ArcSinApprox4i        44914

Мы видим, что в результате «optimizations» на шагах от d до i мы получили «оптимизацию наоборот» на P4, исключая шаг g.

На P3

ArcSinApprox4a        68871

ArcSinApprox4b        68871

ArcSinApprox4c        68634

ArcSinApprox4d        86806

ArcSinApprox4e        85727

ArcSinApprox4f        83542

ArcSinApprox4g        80548

ArcSinApprox4h        88378

ArcSinApprox4i        85324

Мы видим, что оптимизационные шаги d и h очень хороши, а шаги e, f g и I плохие. Вполне возможно, что оптимальной реализации нет. Мы можем выбрать вариант h и удалить оставшиеся и просто сделать несколько вариантов и это путь к быстрой оптимизации.

Так какая же функция победитель? Чтобы найти его мы выберем самую быструю реализацию по каждому решению

На P4

ArcSinApprox1f        47939

ArcSinApprox3g        47416

ArcSinApprox4d        51813

Последняя версия самая быстрая. Параллелизм очень важен на современных процессорах и версия 4 бьет остальных на 9%.

На P3

ArcSinApprox1h        85361

ArcSinApprox3h        87604

ArcSinApprox4h        88378

Версия 4 победитель на P3, но с меньшим преимуществом.

Процессор P4 имеет набор инструкций SSE2, который содержит инструкции для точных расчетов с плавающей запятой. Главная идея этих инструкций The в данном наборе это использование SIMD расчетов. SIMD - это аббревиатура для Single Instruction Multiple Data. «множество данных» (Multiple data) здесь это переменные двойной точности с плавающей запятой (64 bit) и две переменные этих данных могут быть сложены, вычтены, умножены или поделены одной инструкцией. В SSE2 также есть несколько инструкций для скалярных вычислений, которые вычисляют пару этих данных, подобно обычным данным с плавающей запятой на FPU. Наибольшая разница между обычной математикой с плавающей запятой и SSE2 скалярной математикой, в том, что математика с плавающей запятой выполняется на расширенной точности и результат округляется до двойной точности, при копировании в переменную двойной точности в RAM/кэш. Математика SSE2 двойной точности и регистры также двойной точности. Код примеров в данном уроке выполняет несколько вычислений и точность FPU двойная. Если мы загрузим данные, выполним все вычисления и запишем результат, то результат будет только немного меньше, чем при расширенной точности, пока он еще на стеке FPU, и будет округлен до двойной точности, при копировании в переменную. SSE2 вычисления с другой стороны менее точные, в регистре результат также менее точный. При одном вычислении результат будет двойной точности, но когда мы выполним серию вычислений, то накопленная ошибка будет значительно больше. Поскольку FPU выполняет все вычисления с расширенной точностью и хранит промежуточные результаты в регистрах, то можно выполнить много вычислений, прежде чем ошибка станет значимой, ниже двойной точности.


 

Мы видим, что точность SSE2 равная двойной или менее точности двойной точности для IA32 плавающей запятой. В чем же преимущество? Есть два преимущества. Регистры не размещаются на стеке, что делает более простым управление кодом и второе то, что вычисления с двойной точностью быстрее, чем с расширенной точностью. Мы должны выбрать скалярные инструкции SSE2, чтобы иметь меньшую латентность, чем для IA32.

Fadd latency is 5

Fsub latency is 5

Fmul latency is 7

Fdiv latency is 38

 

Addsd latency is 4

Subsd latency is 4

Mulsd

Divsd latency is 35

Руководство по оптимизации P4 не имеет данных по латентности и по throughput для инструкции MULSD!

Мы видим, что латентность меньше на один такт для скаляров SSE2 в основном, и на 3 такта для деления.

Показатели для Throughput (в случае срабатывания конвейера) следующие

Fadd throughput is 1

Fsub throughput is 1

Fmul throughput is 2

Fdiv throughput is 38

 

Addsd throughput is 2

Subsd throughput is 2

Mulsd

Divsd latency is 35

Здесь мы видим сюрприз для ADDSD и SUBSD, результат в два раза хуже, по сравнению с FADD и Fsub.

Все, что можно подумать про SSE2, это то, что оно для встраиваемого оборудования, и то, что SIMD вычисления двух наборов данных в параллель просто удлиняет ваши руки!

Из руководства “Optimizations for Intel P4 and Intel Xeon” таблицы латентности и throughput на странице C-1 показывают, что все инструкции с плавающей запятой SSE2 выполняются на том же конвейере, что и старые инструкции с плавающей запятой. Это означает, что SIMD сложение из примера генерирует две микроинструкции, которые выполняются в конвейере  F_ADD. На первом такте число номер 1 вводится в конвейер, а на втором такте вводится число номер 2. поскольку латентность составляет 4 такта первое число покидает конвейер на такте 3, а второе число на такте 4. Это заставляет нас считать, что скалярное сложение SSE2 должно генерировать латентность в 3 такта и throughput в 1 такт. Из этих таблиц кажется, что SIMD версия ADD, ADDPD, имеет туже самую латентность и throughput, как и скалярная версия ADDSD. Или же здесь ошибка в таблицах, или скалярные инструкции также генерируют две микроинструкции, одна из которых «скрытая», и не имеет эффекта. Обращайтесь к Интел!

Для проверки чисел из таблицы мы создадим некоторый специальный код и померим инструкции.

Code:

procedure TMainForm.BenchmarkADDSDLatency;

var

RunNo, ClockFrequency : Cardinal;

StartTime, EndTime, RunTime : TDateTime;

NoOfClocksPerRun, RunTimeSec : Double;

const

ONE : Double = 1;

NOOFINSTRUCTIONS : Cardinal = 895;

 

begin

ADDSDThroughputEdit.Text := 'Running';

ADDSDThroughputEdit.Color := clBlue;

Update;

StartTime := Time;

for RunNo := 1 to MAXNOOFRUNS do

begin

   asm

   movsd xmm0, ONE

   movsd xmm1, xmm0

   movsd xmm2, xmm0

   movsd xmm3, xmm0

   movsd xmm4, xmm0

   movsd xmm5, xmm0

   movsd xmm6, xmm0

   movsd xmm7, xmm0

 

   addsd xmm0, xmm1

   addsd xmm0, xmm1

   addsd xmm0, xmm1

   addsd xmm0, xmm1

   addsd xmm0, xmm1

   addsd xmm0, xmm1

   addsd xmm0, xmm1

 

   //Repeat the addsd block of code such that there are 128 blocks

 

   end;

end;

EndTime := Time;

RunTime := EndTime - StartTime;

RunTimeSec := (24 * 60 *60 * RunTime);

ClockFrequency := StrToInt(ClockFrequencyEdit.Text);

NoOfClocksPerRun := (RunTimeSec / MaxNoOfRuns) * ClockFrequency * 1000000 /

   NOOFINSTRUCTIONS;

ADDSDThroughputEdit.Text := FloatToStrF(NoOfClocksPerRun, ffFixed, 9, 1);

ADDSDThroughputEdit.Color := clLime;

Update;

end;

 

 

Все инструкции ADDSD оперируют на тех же самых двух регистрах и поэтому они не могут быть выполнены параллельно. Вторая инструкция должна ждать окончания первой, поэтому будет задействована полная латентность.

Для измерения производительности throughput вставим данный блок 128 раз

addsd xmm1, xmm0

addsd xmm2, xmm0

addsd xmm3, xmm0

addsd xmm4, xmm0

addsd xmm5, xmm0

addsd xmm6, xmm0

addsd xmm7, xmm0

здесь нет зависимости от данных, и они могут быть выполнены параллельно. Xmm0 используется как источник данных в каждой строке, но это строка не создает зависимости данных.

Результаты прогона данного кода показывают, что латентность равна 4 тактам, а throughput равна двум тактам. Это соответствует цифрам из таблицы.

Закодируем три функции для скаляров SSE2 и выполним измерения. Восемь регистров SSE2 называются как XMM0-XMM7, и Delphi не имеет возможности показать их в окне просмотра регистров. Поэтому мы должны создать свой собственный просмотр, созданием глобальной (или локальной) переменной для каждого регистра, поместить его в окно просмотра (watch window) и добавить функцию для копирования содержимого в переменные. Это несколько неудобно и я с надеждой смотрю в сторону Борланд, по созданию окна просмотр XMM регистров. Данный код показывает, как Я сделал это.

Code:

var

XMM0reg, XMM1reg, XMM2reg, XMM3reg, XMM4reg : Double;

 

function ArcSinApprox3i(X, A, B, C, D : Double) : Double;

asm

//Result := ((A*X + B)*X + C)*X + D;

 

fld   qword ptr [ebp+$20]

movsd xmm0,qword ptr [ebp+$20]

 

movsd XMM0reg,xmm0

movsd XMM1reg,xmm1

movsd XMM2reg,xmm2

movsd XMM3reg,xmm3

 

fld   qword ptr [ebp+$28]

movsd xmm1,qword ptr [ebp+$28]

 

movsd XMM0reg,xmm0

movsd XMM1reg,xmm1

movsd XMM2reg,xmm2

movsd XMM3reg,xmm3

 

fxch  st(1)

fmul  st(0),st(1)

mulsd xmm0,xmm1

 

movsd XMM0reg,xmm0

movsd XMM1reg,xmm1

movsd XMM2reg,xmm2

movsd XMM3reg,xmm3

 

fadd  qword ptr [ebp+$18]

addsd xmm0,qword ptr [ebp+$18]

 

movsd XMM0reg,xmm0

movsd XMM1reg,xmm1

movsd XMM2reg,xmm2

movsd XMM3reg,xmm3

 

fmul  st(0),st(1)

mulsd xmm0,xmm1

 

movsd XMM0reg,xmm0

movsd XMM1reg,xmm1

movsd XMM2reg,xmm2

movsd XMM3reg,xmm3

 

fadd  qword ptr [ebp+$10]

addsd xmm0,qword ptr [ebp+$10]

 

movsd XMM0reg,xmm0

movsd XMM1reg,xmm1

movsd XMM2reg,xmm2

movsd XMM3reg,xmm3

 

fmulp st(1),st(0)

mulsd xmm0,xmm1

 

movsd XMM0reg,xmm0

movsd XMM1reg,xmm1

movsd XMM2reg,xmm2

movsd XMM3reg,xmm3

 

fadd  qword ptr [ebp+$08]

addsd xmm0,qword ptr [ebp+$08]

 

movsd XMM0reg,xmm0

movsd XMM1reg,xmm1

movsd XMM2reg,xmm2

movsd XMM3reg,xmm3

 

movsd [esp-8],xmm0

fld   qword ptr [esp-8]

 

movsd XMM0reg,xmm0

movsd XMM1reg,xmm1

movsd XMM2reg,xmm2

movsd XMM3reg,xmm3

 

wait

end;

 

 

Код не использует регистры XMM4-XMM7, и поэтому не было нужды создавать их просмотр. Код просмотра XMM располагается после каждых двух строк SSE2 кода. Все строки, кроме двух последних это код с плавающей запятой, и SSE2 код, добавлен так, что бы каждая операция выполнялась как операция с плавающей запятой, так и как SSE2. данный путь делает возможным трассировать код и проверять, что делает SSE2 версия, сравнительно классической версии. Откройте окно FPU view, и смотрите, как изменяется стек FP, и одновременно как изменяются регистры XMM. Я разработал SSE2 код, просто добавляя SSE2 инструкции сразу после каждой строки FP кода.

fld   qword ptr [ebp+$20]

movsd xmm0,qword ptr [ebp+$20]

MOVSD копирует одну переменную двойной точности, из памяти по адресу [EBP+$20], в регистр XMM. “qword ptr” не требуется, но я сохранил это, что бы снять различие между SSE2 и FP кодом.

Наибольшая разница между FP кодом и скалярным SSE2 кодом, состоит в том, что регистры FP организованы в виде стека, а регистры SSE2 нет. В первое время, при кодировании SSE2 кода, я просто игнорировал это, и затем после того, как я сделал все необходимые SSE2 строки, я вернулся назад, прошелся по всем строкам, строка за строкой и откорректировал их так, что бы они работали с корректным парой переменная/регистр. Активируя функции, определенными значениями, следуя двум следующим видам (например: X=2, A=3, B=4, C=5, D=6), и мы увидим, что сначала загружается “2”, затем “3”, затем 2 умножается на “3” и “2” переписывается “6” и так далее.

Скалярным SSE2 соответствием для FMUL является MULSD. Суффикс SD означает Scalar Double (Скаляр Двойная точность).

fxch  st(1)

fmul  st(0),st(1)

mulsd xmm0,xmm1

Скалярным SSE2 соответствием для FADD является ADDSD.

fadd  qword ptr [ebp+$18]

addsd xmm0,qword ptr [ebp+$18]

Продолжаем таким же образом, строка за строкой.

FP код оставляет результат в ST(0), а SSE2 код оставляет результат в регистре XMM. Затем результат копируется из регистра XMM в ST(0) через ячейку памяти на стек.

movsd [esp-8],xmm0

fld   qword ptr [esp-8]

Эти две строки выполняют именно это. В ESP-8, восемь байт находятся выше верхушки стека, есть также еще несколько мест, которые мы могли бы использовать, как временное место для хранения результата. Первая строка копирует XMM0 во временное место, и затем последняя строка загружает его в стек FP. Эти две строки дают перегрузку, что делает маленькие SSE2 функции менее эффективными, чем их FP аналоги.

После двойной проверки SSE2 кода, мы можем удалить инструментальный код, так же как и старый FP, оставив только скалярную SSE2 функцию с действительно необходимым кодом, без лишней перегрузки.

Code:

function ArcSinApprox3j(X, A, B, C, D : Double) : Double;

asm

//Result := ((A*X + B)*X + C)*X + D;

movsd xmm0,qword ptr [ebp+$20]

movsd xmm1,qword ptr [ebp+$28]

mulsd xmm0,xmm1

addsd xmm0,qword ptr [ebp+$18]

mulsd xmm0,xmm1

addsd xmm0,qword ptr [ebp+$10]

mulsd xmm0,xmm1

addsd xmm0,qword ptr [ebp+$08]

movsd [esp-8],xmm0

fld   qword ptr [esp-8]

end;

Теперь это станет более красивым, после удаления не нужного подчеркивания “qword ptr”.

Code:

function ArcSinApprox3j(X, A, B, C, D : Double) : Double;

asm

//Result := ((A*X + B)*X + C)*X + D;

movsd xmm0, [ebp+$20]

movsd xmm1, [ebp+$28]

mulsd xmm0,xmm1

addsd xmm0, [ebp+$18]

mulsd xmm0,xmm1

addsd xmm0, [ebp+$10]

mulsd xmm0,xmm1

addsd xmm0, [ebp+$08]

movsd [esp-8],xmm0

fld   qword ptr [esp-8]

end;

Заменим указатели на имена параметров

Code:

function ArcSinApprox3j(X, A, B, C, D : Double) : Double;

asm

//Result := ((A*X + B)*X + C)*X + D;

movsd xmm0, A

movsd xmm1, X

mulsd xmm0,xmm1

addsd xmm0, B

mulsd xmm0,xmm1

addsd xmm0, C

mulsd xmm0,xmm1

addsd xmm0, D

movsd [esp-8],xmm0

fld   qword ptr [esp-8]

end;

И наконец, проверим, как работает данная версия?

Результат равен 45882 пунктам.

Данная версия немного медленнее, чем версия с плавающей запятой, которая получила 48292 пункта. Мы должны разобраться, в чем причина этого. Толи причина в перегрузки в двух последних строках, то ли в 2-тактном throughput инструкций ADDSD и MULSD? Перегрузка может быть удалена, путем передачи параметра как выходного (OUT параметр) или мы должны встроить (inline) в функцию. Было бы очень интересно для нас насколько велико преимущество от встраивания такой относительно маленькой функции. Во первых, мы избавляемся от передачи пяти параметров с двойной точностью, каждый из которых занимает восемь байт. Посмотрим насколько много кода используется для этого.

push dword ptr [ebp+$14]

push dword ptr [ebp+$10]

push dword ptr [ebp+$34]

push dword ptr [ebp+$30]

push dword ptr [ebp+$2c]

push dword ptr [ebp+$28]

push dword ptr [ebp+$24]

push dword ptr [ebp+$20]

push dword ptr [ebp+$1c]

push dword ptr [ebp+$18]

call dword ptr [ArcSinApproxFunction]

fstp qword ptr [ebp+$08]

Не менее десяти инструкций PUSH, каждая помещает в стек только четыре байта, половина от каждого параметра. Заметим, что регистровое соглашение о вызове, смотрит серьезно на их имена и передает параметры вместо использования FP стека. Затем мы должны иметь пять инструкций FLD, которые могли бы устранить ненужность загрузки параметров со стека в функцию. Это значит, что пять FLD инструкций в функции могли бы быть заменены пятью инструкциями FLD, в точке вызова и десять PUSH инструкции ушли бы в небытие. Это могло бы драматическим образом увеличить быстродействие. Встраивание функции вместо вызова, так же уменьшило перегрузку, за счет отсутствия пары инструкций CALL/RET, которая конечно меньше, чем перегрузка от такого количества PUSH, и это дало нам следующую производительность, на преобразованной в register2 соглашении об вызове ;-).

Inlined ArcSinApprox3i 156006

Inlined ArcSinApprox3j 160000

Улучшение составляет 400%.

Я подлинно желаю Борланду ввести истинное соглашение по вызову для параметров с плавающей запятой в самом ближайшем будущем.

SSE2 версия только на 3% быстрее, чем IA32 версия. Но это больше относится к должной реализации SSE2.

На этом урок 7 подошел к концу.

И теперь вы знаете почти все, о программировании с плавающей запятой ;-)

 

 

Добавить комментарий

Не использовать не нормативную лексику.

Просьба писать ваши замечания, наблюдения и все остальное,
что поможет улучшить предоставляемую информацию на этом сайте.

ВСЕ КОММЕНТАРИИ МОДЕРИРУЮТСЯ ВРУЧНУЮ, ТАК ЧТО СПАМИТЬ БЕСПОЛЕЗНО!


Защитный код
Обновить