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

 

Мы видим, что точность 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 подошел к концу.

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

 

 

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

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

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

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


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