Borland Assembler (BASM) уроки для начинающих (урок 7 часть 2) - Delphi, Pascal, ObjectPascal - Программирование
Навигация по сайту
Сайт:

Дополнительно:

Файловый архив:

Каталог статей:

Форум:


Категории раздела
Delphi, Pascal, ObjectPascal [18]
Программирование на Delphi, Pascal, ObjectPascal
C, C++, C# [7]
Программирование на C, C++, C#
ПХП (PHP) [6]
Все что связано с программированием на PHP.
DirectX [0]
Программирование с использованием графического API DirectX
OpenGL [0]
Программирование с использованием графического API OpenGL
Работа с базами данных (БД) [0]
Работа с базами данных MySQL и т.д. Разработка, теории, алгоритмы.
Сетевое программирование [0]
Сетевое программирование, организация сетей.
Программирование игр [0]
Все что связано с программированием игр, организацией их разработки.
Работа с мультимедиа данными [0]
Загрузка, обработка, воспроизведение и все что связано со звуком и видео.
Работа с устройсвами ввода и вывода [0]
Программирование устройств ввода и вывода. Работа с геймпадом, рулем и многим другим.
Программирование HTML 5 игр [0]
Программирование HTML 5 игр, html верстка, JS (JavaScript)
Остальное [0]
Все остальное, что не попадает ни под одну категорию.

Мини-Опрос
Какой ОС Вы пользуетесь?
Всего ответов: 1026

Партнеры сайта
....

 Главная » Статьи » Программирование » Delphi, Pascal, ObjectPascal » Borland Assembler (BASM) уроки для начинающих (урок 7 часть 2)

Borland Assembler (BASM) уроки для начинающих (урок 7 часть 2)

19:55

В действительности компилятор также вставил инструкцию 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
Fp latency is 38


Addsd latency is 4
Subsd latency is 4
Mulsd
psd latency is 35

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

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

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

Fadd throughput is 1
Fsub throughput is 1
Fmul throughput is 2
Fp throughput is 38
 
Addsd throughput is 2
Subsd throughput is 2
Mulsd
psd 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 подошел к концу.

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


Категория: Delphi, Pascal, ObjectPascal | Просмотров: 1167 | Добавил: Конструктор (15.10.2012) | Рейтинг: 0.0/0
Источник: http://www.kansoftware.ru/?tid=5097 |
HTML ссылка на материал:
BB ссылка на материал:
Похожие материалы :
Возможно вам будет интересно:
Всего комментариев: 0
Добавлять комментарии могут только зарегистрированные пользователи.
[ Регистрация | Вход ]
Мы в социальных сетях

Поиск
Поиск по всему сайту:
Поиск по разделу:

Панель пользователя
Здравствуйте, Гость


Ник:
Пароль:
Запомнить :

Ваш IP: 54.157.224.64

Случайные конструкторы

Случайные движки

Случайные статьи

Статистика
Онлайн всего: 5
Гостей: 5
Пользователей: 0

На сайте были:
Конструктор , ZiP , Filinshein , Kapitan , vagrand , FireOfSteel , proto1ype , RenardOn , KRein

При полном или частичном копировании материалов сайта ссылка на Make-Games.ru обязательна. Make-Games.ru © 2008 - 2016 Хостинг от uWeb
Топ Разработка игр Рейтинг@Mail.ru