Мы видим, что точность 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 подошел к концу.
И теперь вы знаете почти все, о программировании с плавающей запятой ;-)
- << Назад
- Вперёд
Просьба писать ваши замечания, наблюдения и все остальное,
что поможет улучшить предоставляемую информацию на этом сайте.
ВСЕ КОММЕНТАРИИ МОДЕРИРУЮТСЯ ВРУЧНУЮ, ТАК ЧТО СПАМИТЬ БЕСПОЛЕЗНО!