В широко известных книгах: Юров Assembler. 2-е изд., 2003; Магда Ассемблер для процессоров Intel., 2006 использование SSE описано практически без примеров.
В руководствах Intel® 64 and IA-32 Architectures Optimization Reference Manual, 2013 и Intel® 64 and IA-32 Architectures Software Developer’s Manual, на мой взгляд, изложение чрезвычайно сжатое.
Предлагаю в этой теме обмениваться опытом. Будет здорово, если другие участники приведут свои примеры.
Начну с довольно тривиального примера сравнения двух чисел с плавающей точкой.1. [Floating Point] Использование comiss (comisd) и cmpps (cmppd)В SSE для (скалярного) сравнения двух single (double) есть инструкция, модифицирующая флаги ZF, PF и CF:
comiss xmm1, xmm2/r32 (
comisd xmm1, xmm2/r64, соответственно).
Также есть инструкции
cmpss (
cmpsd) для (скалярного) сравнения двух single (double), которая заносит в приемник маску: единицы, если условие выполнено и нули в противном случае:
cmpss xmm1, xmm2/r32, imm8 (
cmpsd xmm1, xmm2/r64 imm8).
и для сравнения упакованных (packed) single (double):
cmpps xmm1, xmm2/m128, imm8 (cmppd xmm1, xmm2/m128, imm8)Значения масок обычно используется двумя способами:
A. Старший бит каждой маски переносится в регистр CPU
movmskps reg, xmm (
movmskpd reg, xmm)
и побитно анализируется (
командами, модифицирующими флаги, например
bt) с последующим переходом на нужную ветвь. Либо значение reg используется в качестве индекса для перехода по адресу (хранимому в массиве адресов) или для загрузки констант из массива констант.
B. При помощи масок и логических инструкций (
pand,
pandn,
por,…) модифицируются данные непосредственно в регистрах xmm.
Поскольку условные переходы и зависимость по данным замедляют скорость выполнения программы, то по возможности надо использовать второй способ (способ B).
Пример простейшей ситуации, где можно использовать второй способ.
Если условие выполнено, то необходимо прибавить некоторую константу, а если не выполнено, то прибавлять не надо. В этом случае достаточно хранить константу в некоторой переменной в памяти и выполнить
pand xmm1, m128где m128 — переменная, хранящая константу, xmm1 — регистр с масками (результатами сравнения).
А затем сложить с результатом
pandВ качестве другого банального примера рассмотрим задачу вычисления суммы элементов массива, которые превосходят некоторую величину. Будем считать элементы массива и величину (с которой элементы сравниваются) числами удвоенной точность. Возможно три основных варианта:
1. Скалярный 1. Загружать в регистр xmm по очереди элементы массива и сравнивать при помощи
comisd. Затем в случае выполнения условия — накапливать сумму, а в случае не выполнения — обходить сложение при помощи инструкции условного перехода.
2. Скалярный 2. Загружать в регистр xmm по очереди элементы массива, копировать в другой регистр и сравнивать копию с заданной величиной при помощи
cmppd. Затем выполнять pand маски со значением элемента массива и безусловно выполнять сложение (в результате pand c содержащей нули маской будет получен ноль и прибавление нуля не приведёт к изменению суммы).
Прим. В AVX2 многие инструкции SSE стали не «разрушающими» (с тремя операндами; результат сравнения заносится в третий операнд). В этом наборе инструкций копировать элемент массива для сравнения не нужно.
3. Упакованный (packed). Выполняется накопление сумм четных и нечетных элементов массива одновременно. Для сравнения используется
cmppd, как в варианте 2. После вычисления сумм четных и нечетных элементов они складываются (инструкция горизонтального сложения). Если массив содержит нечетное число элементов, то перед началом цикла подсчета сумм четных и нечетных элементов, выполняется скалярное накопление суммы как в варианте 2.
Ниже вставлен исходный текст 64-битного проекта Embarcadero Delphi XE7.
Прим. 1. Код легко может быть модифицирован под Borland Delphi 7 (BD7). Потребуется только выровнять вручную массивы на границу 128 бит и заменить использование 64-битных регистров общего назначения на 32-битные.
Upd 17.11.2015 BD7 не поддерживает SSE3.
movddup придется заменить на movsd + копирование в старшую часть xmm3;
haddpd — на перенос double из старшей половины xmm0 в младшую половину xmm1 и обычное сложение.[/Upd]Если элементы массива не упорядочены, то на Sandy Bridge (SB) c медленной памятью:
Вариант 3 (sum3) оказывается быстрее варианта 1 для всех тестированных длин массива: от 2 до 100000. Для малых длин время sum3 составляет 0.8 и менее от времени sum1. Для больших длин — менее 0.2
Вариант 2 (sum2), как и ожидается, быстрее варианта 3 для очень коротких массивов (2, 3 элемента). Для размеров массива приблизительно от 100 и до 100 000 вариант 2 медленнее 3 чуть более чем в два раза.
Несмотря на то, что в варианте 2 присутствует дополнительное копирование, pand и всегда выполняется сложение — вариант 2 почти для всех размеров массива, если и медленнее, то совсем незначительно, а для массивов большой длины — составляет менее 0.5 от времени выполнения варианта 1.
Пример несколько искусственный, но он мне показал насколько вредно использовать условные переходы (даже на SB).
program Sum64;
uses sysutils;
{$APPTYPE CONSOLE}
const
N = 5000;
Rep = 500000;
type
TDArray = Array[1..N] of Double;
var
DArray : TDArray;
f: Double;
procedure Sum1;
{rdx - @DArray; rcx - N; xmm0 - sum(a), xmm1 - a, xmm3 - f}
asm
mov rax, Rep
@rep:
{Вычисление суммы}
mov rcx, N
pxor xmm0, xmm0
pxor xmm1, xmm1
pxor xmm3, xmm3
lea rdx, DArray
movsd xmm3, [f]
@next:
movsd xmm1, [rdx+ rcx*8-8]
comisd xmm1, xmm3
jbe @continue
addsd xmm0, xmm1
@continue:
sub rcx, 1
jnz @next
{Вычисление суммы end}
sub rax, 1
jnz @rep
end;
procedure Sum2;
{rdx - @DArray; rcx - N; xmm0 - sum(a), xmm1 - a, xmm2 - copy a, xmm3 - f}
asm
mov rax, Rep
@rep:
{Вычисление суммы}
mov rcx, N
pxor xmm0, xmm0
pxor xmm1, xmm1
pxor xmm2, xmm2
pxor xmm3, xmm3
lea rdx, DArray
movsd xmm3, [f]
@next:
movsd xmm1, [rdx+ rcx*8-8]
movaps xmm2, xmm1
cmppd xmm1, xmm3,6 {Сравниваем с f?}
pand xmm1, xmm2 {Обнуляем если меньше f}
addpd xmm0, xmm1
sub rcx, 1
jnz @next
{Вычисление суммы end}
sub rax, 1
jnz @rep
end;
Procedure Sum3;
{rdx - @DArray; rcx - N; xmm0 - sum(a), xmm1 - a, xmm2 - copy a, xmm3 - f}
asm
mov rax, Rep
@Rep:
{Вычисление суммы}
lea rdx, DArray
pxor xmm0, xmm0
pxor xmm1, xmm1
pxor xmm2, xmm2
mov rcx, N
movddup xmm3, [f] {Переносим с дублироваием f}
btr rcx, 0 {Кол-во элеменов массива нечётноё?}
jnc @next {Нет - переходим на суммирование упакованных}
movsd xmm1, [rdx+ rcx*8] {Да - загружаем нечетный элемент}
movaps xmm2, xmm1 {Делаем копию для срвнения}
cmpsd xmm1, xmm3,6 {Сравниваем с f?}
pand xmm1, xmm2 {Обнуляем если меньше или равно f}
addsd xmm0, xmm1 {Накапливаем сумму}
jrcxz @res {Если больше элементов нет, то обходим цикл}
@next:
movaps xmm1, [rdx+ rcx*8-16] {Загружаем два очередных элемента}
movaps xmm2, xmm1 {Делаем копию для срвнения}
cmppd xmm1, xmm3,6 {Сравниваем с f?}
pand xmm1, xmm2 {Обнуляем если меньше f}
addpd xmm0, xmm1
sub rcx, 2
jnz @next
@res:
haddpd xmm0, xmm0
{Вычисление суммы end}
sub rax, 1
jnz @Rep
end;
var
i: integer;
Time1, Time2, d1, d2, d3: TDateTime;
t: text;
begin
f:= 0.5;
randomize; for i:= 1 to N do DArray[i]:= random;
Time1:= Time;
Sum1;
Time2:= Time;
d1:= Time2-Time1;
Time1:= Time;
Sum2;
Time2:= Time;
d2:= Time2-Time1;
Time1:= Time;
Sum3;
Time2:= Time;
d3:= Time2-Time1;
Assign(t, 'sumtst.txt');
{$I-}
Append(t);
if IOResult <> 0
then Rewrite(t);
{$I+}
Writeln(t, 'N= ', N, ' Rep=', Rep);
if d3 > 0
then writeln(t, 'd2/d1=', d2/d1:6:3, ' d3/d1=', d3/d1:6:3, ' d3/d2=', d3/d2:6:3)
else writeln(t, 'd1=', d1, ' d2=', d2,', ', ' d3=', d3);
close(t);
end.