Переделал вроде системку для тестирования на 64 бита. Для реализации выбрал все же вариант, когда указатели на строки готовятся заранее, до решения системы, несмотря на существенный расход памяти при таком подходе. Выбрал вот по какой причине: я тестирую производительность операций с плавающей точкой, а расстановка указателей основана на целочисленная операциях. Чем больше целочисленных операций будет использовано для реализации операций с плавающей точкой, тем менее точно будет измеряться производительность.
В процессе обнаружил пару любопытных моментов.
Во-первых, так как не придумал способа, как сгенерировать гарантированно совместную СЛАУ, то просто включил в код проверку на то, что очередной диагональный элемент существенно больше машинного нуля для соответствующего типа данных.
В принципе, так даже правильнее, так как в реальности сходимость решения надо проверять всегда. На производительности это практически никак не сказалось.
А потом сразу проверил, насколько точно решаются СЛАУ. И выяснил, что тип single (или float в С-подобных языках) дает достаточно большую ошибку для СЛАУ больших размерностей, в районе несколько сотен переменных, ближе к тысяче.
Критерием для меня служила ошибка, превышающая 5% относительно точного решения. И больше половины решенных СЛАУ больших размерностей имеют максимальную ошибку больше 5%. Таким образом, я пришел к выводу, что СЛАУ размерностью более 500 переменных нет смысла тестировать на типе single.
С типом double же подобных проблем не было, по крайне мере для тех размерностей, что я смог проверить: максимальная ошибка была крайне мала.
Во-вторых, начал реализовывать тип extended (long double) и получил на ровном месте некоторую странную ошибку. Через какое-то время вспомнил, что AMD рекомендовало избегать x87 инструкций в 64-битном коде, заменяя их скалярными аналогами из SSE. А Embarcodero последовал этому совету весьма простым способом, элементарно приравняв тип extended к типу double в 64-разрядном режиме.
То есть код-то для решения таким способом у меня есть, а вот кода, который генерирует СЛАУ, получается нет, так как он на Pascal у меня написан. Сначала хотел описать свой тип, что бы таки реализовать тест на расширенном вещественным, но потом что-то заленился. Может быть, потом как-нибудь.
А зачем он нужен, этот расширенный тип? Банальный ответ: для повышения точности промежуточных вычислений. Но таких случаев в действительности не очень много. На практике, например, я разницу точности в решениях СЛАУ с использованием x87 и scalar SSE не увидел, хотя сильно глубоко там и не копал.
Что получилось в результате? Первое: 64-битный код оказался почти всегда быстрее 32-битного, причем на малых размерностях значительно (почти на 50%). В основном, оптимизация на большем количестве регистров позволила практически полностью отказаться от операций со временной памятью. Ну и хранение коэффициентов всех СЛАУ в одном массиве улучшило коэффициент попадания в кэш.
Но на больших размерностях все же 32-битный режим совсем чуть-чуть быстрее. Может, тут все же сыграли отрицательную роль 64-битные указатели?
Ну и выложу напоследок код, вдруг кто-то увидит недостатки и предложит улучшения.
Сначала типы, приведу только для систем на single:
TSingleArray = array[0..65535] of single;
PSingleArray = ^TSingleArray;
PPointerArray = ^TPointerArray;
TLESInfoP = packed record
Data : Pointer;
Systems : PPointerArray;
Rows : PPointerArray;
LESDataSize : UInt64;
LESCount : UInt64;
LESSize : word;
ElementSize : byte;
end;
TLESInfoP ‒ это структура, которая собирает в кучу общие сведения о том, сколько и какие системы хранятся в общем массиве Data.
Systems ‒ это массив указателей на Rows, i-ый элемент которого указывает на первую строку i-ой системы. Ну а Rows просто указывает строки.
На а теперь код, сначала на Pascal, который я затем перевел на ассемблер. Что бы понятнее было.
procedure SolveTestLES(var P : TLESInfoP; LESNum : UInt64);overload;
begin
case P.ElementSize of
4 : SolveSingleLESP(P.Systems[LESNum], P.LESSize);
8 : SolveSingleLESP64(P.Systems[LEsNum], P.LESSize);
10 : SolveSingleLESP80(P.Systems[LEsNum], P.LESSize);
else
raise Exception.Create('Wrong LES element size!');
end;
end;
Это что бы было понятно, при чем тут TLESInfoP. На самом деле вся работа в SolveSingleLESP. Тут я допустил некоторую путаницу: Single в название, это типа одиночное уравнение решаем, а не используемый тип данных.
procedure SolveSingleLESP(P : PPointerArray; n : word);overload;
var
i : word;
begin
for i := 0 to n-1 do
begin
if FindSingleMaxP(P, i, n) <> 0 then
exit;
Make1RowP(P[i], i, n);
MakeNextRowsP(P, i, n);
end;
for i := n-2 downto 0 do
ReverseGJ(P, i, n);
end;
function FindSingleMaxP(P : PPointerArray; index, n : word):byte;
var
i : word;
Max : word;
A : Pointer;
x : extended;
begin
Max := index;
x := abs(PSingleArray(P[index])[index]);
for i := index+1 to n-1 do
if abs(PSingleArray(P[i])[index]) > x then
begin
Max := i;
x := abs(PSingleArray(P[i])[index]);
end;
if Max <> index then
begin
A := P[index];
P[index] := P[Max];
P[Max] := A;
end;
if x < c_LowDiagonalSingle then
Result := 1
else
Result := 0;
end;
procedure Make1RowP(P : PSingleArray; index, n : word);overload;
var
i : word;
d : extended;
begin
d := 1/P[index];
for i := index+1 to n do
P[i] := P[i]*d;
end;
procedure MakeNextRowsP(P : PPointerArray; index, n : word);overload;
var
i, j : word;
S, R : PSingleArray;
begin
R := P[index];
for i := index+1 to n-1 do
begin
S := P[i];
for j := index+1 to n do
S[j] := S[j]-R[j]*S[index];
end;
end;
procedure ReverseGJ(P : PPointerArray; index, n : word);overload;
var
i : word;
S : PSingleArray;
d : extended;
begin
d := PSingleArray(P[index+1])[n];
for i := index downto 0 do
begin
S := P[i];
S[n] := S[n] - d* S[index+1];
end;
end;
Ну а теперь все тоже самое, только на ассемблере (логика немного изменена по сравнению с Pascal ‒ вместо общей процедуры, которая вызывает три конкретных, здесь просто три отдельных процедуры):
procedure SolveTestSingleLESASMP(var P : TLESInfoP; LESNum : UInt64); // P - RCX, LESNum - RDX;
asm
// Free modifiing RAX, RDX, RCX, R8, R9, R10, R11
// R8 - P, R9w - n, R10w - index
push RBX;
mov R8, P.Systems;
movzx R9, word ptr P.LESSize; // в R9w - n;
mov R8, [R8+LESNum*8]; // R8 указатель на указатель на первую строку СЛАУ RDX теперь свободен
xor R10, R10; // R10w - i;
xor RBX, RBX; // готовим RBX для работы с индексами
@Loop1:
call FindSingleMaxPASM;
test al, al;
jnz @exit;
call Make1RowPASM;
call MakeSingleNextRowsPASM
inc R10w;
cmp R10w, R9w;
jb @Loop1;
dec R10w;
@Loop2:
dec R10w;
call ReverseSingleGJPASM;
test R10w, R10w;
jnz @Loop2;
@exit:
pop RBX;
end;
procedure FindSingleMaxPASM;
asm
// R8 - P, R9w - n, R10w - index
fld dword ptr c_LowDiagonalSingle;
lea RCX, [R10d+1];
mov BX, R10w; // BX - Max
mov R11, [R8 + R10*8];
fld dword ptr [R11 + R10*4];
fabs;
cmp CX, R9w;
jae @exit; // нечего вычислять
@Loop:
mov R11, [R8+RCX*8];
fld dword ptr [R11 + R10*4];
fabs;
fucomi ST(0), ST(1);;
jbe @GL;
fxch;
mov bx, cx; // Max := i
@GL:
fstp st(0);
inc cx;
cmp cx, R9w;
jb @Loop;
cmp bx, R10w;
je @exit; // максимум в первой строке, обмен не нужен
mov RCX, [R8+RBX*8];
mov RAX, [R8+R10*8];
mov [R8+RBX*8], RAX;
mov [R8+R10*8], RCX;
@exit:
xor dx, dx;
fucomip ST(0), ST(1);
mov cx, 1;
fstp ST(0);
cmovb ax, cx;
cmovae ax, dx;
end;
procedure Make1RowPASM;
asm
// R8 - P, R9w - n, R10w - index
mov R11, [R8 + R10*8];
fld1;
fld dword ptr [R11 + R10*4];
mov BX, R10w;
fdivp;
@Loop:
fld st(0); // копируем d;
inc BX;
fmul dword ptr [R11 + RBX*4]; // в ST(0) - P[i]*d
fstp dword ptr [R11 + RBX*4]; // созраняем ST(0) в P[i]
cmp BX, R9w;
jb @Loop;
fstp st(0); // очищаем стек
end;
procedure MakeSingleNextRowsPASM;
asm
// R8 - P, R9w - n, R10w - index
lea EBX, [R10d+1]; // BX - i
xor RDX, RDX;
cmp BX, R9w;
jae @exit;
@Loop1:
mov RCX, [R8 + RBX*8]; // RCX - S
mov DX, R10w;
fld dword ptr [RCX+R10*4];
@Loop2:
fld st(0);
inc DX;
fmul dword ptr [R11+RDX*4];
fsubr dword ptr [RCX+RDX*4];
fstp dword ptr [RCX+RDX*4];
cmp DX, R9W;
jb @Loop2;
fstp st(0);
inc BX;
cmp BX, R9w;
jb @Loop1;
@exit:
end;
procedure ReverseSingleGJPASM;
asm
// R8 - P, R9w - n, R10w - index
mov RDX, [R8+R10*8+8]; // RDX - R
lea EBX, [R10d+1];
fld dword ptr [RDX+R9*4];
@Loop:
dec BX;
mov R11, [R8+RBX*8];
fld st(0);
fmul dword ptr [R11+R10*4+4];
fsubr dword ptr [R11+R9*4];
fstp dword ptr [R11+R9*4];
test BX, BX;
jnz @Loop;
fstp st(0);
end;
Спасибо что код выложили, давно я паскаль не читал, но вроде все ясно и понятно :) Правда я чуток потерялся: в тексте Вы пишете scalar SSE, а в ASM используете fmul, fsubr, и другие fXXX - это ведь обычные x87 инструкции?
ОтветитьУдалитьНа сколько я знаю, x87 очень плохо парализуемые, потому что выполняются на стеке. Я бы попробовал задействовать addpd, mulpd и др. SSE инструкции, которые не используют стек, хотя бы для внутреннего цикла в MakeSingleNextRowsPASM. Я думаю что даже если использовать половинки 128-битых регистров - уже должно быть быстрее, но возможно придется вручную развернуть цикл, скажем по 4, чтобы компилятор лучше выполнял инструкции параллельно, ну а там до полной загрузки 128-битного регистра останется совсем чуть-чуть.
А, я, наверное, не очень ясно цели обозначил, да. Короче, так как есть разные технологии решения задачи, то я решил их все оттестировать и между собою сравнить. Первая технология по хронологии создания - это x87, поэтому пока ее и тестирую. Потом сделаю scalar SSE, ну а дальше уж буду SIMD пробовать.
УдалитьВ результате получилось, что то, что я написал на ассемблере, работает на FPU. А чисто паскалевская часть в 64-битном режиме - на scalar SSE.
Ну и так как у меня есть чисто паскалевский вариант решения СЛАУ, то можно немного сравнить scalar SSE, скомилированный Pascal и мой ассемблерный x87. Кстати, могу сказать, что вроде 64-битный паскаль лучше плавающую точку компилит, чем 32-битный.
Мой ассемблерный 64-битный код на FPU быстрее, чем паскалевский на scalar SSE. Но ускорение меньше по сравнению с 32-битной режимом, когда Pascal тоже генерирует код под x87.
>x87 очень плохо парализуемые
Это верно, х87 - настоящий паралитик, ;) и стековая организация регистров не очень удачно поддается распараллеливанию. Кроме того, судя по документации AMD, на FPU только два исполнительных устройства: по одному на сложение и умножение. То есть в лучшем случае удастся ускорить лишь в два раза. Чуть позже может попытаюсь это как-то организовать.
PS: жаль, что у гугла комментарии нельзя редактировать. Пришлось удалить и создать заново.
Понял, тогда жду продолжения экспериментов!
УдалитьА Вы смотрели на сгенерированный Delphi ассемблер для scalar SSE, на сколько он хорош и логичен, особенно для внутреннего цикла?
> Это верно, х87 - настоящий паралитик, ;)
Как я удачно в слове ошибся, русский язык не мой конек еще со школы :)
Нет, на код не смотрел чего-то. Но, собственно, вот он для MakeNextRowsP. Не очень-то он оптимален. Ну и видимо, низкая погрешность за счет того, что числа с одинарной точностью конвертируются в двойную, а потом вычисляются. Правда, тут есть нюанс: это для конфигурации Debug, возможно, в этом случае оптимизация как-то ограничена.
Удалитьgauss64.pas.739: begin
push rbp
push r13
push rdi
push rsi
push rbx
mov rbp,rsp
gauss64.pas.740: R := P[index];
movzx rax,dx
mov r11,[rcx+rax*8]
mov eax,edx
add ax,$01
mov ebx,r8d
sub bx,$01
cmp ax,bx
jnbe MakeNextRowsP + $8E
add bx,$01
gauss64.pas.743: S := P[i];
movzx r9,ax
mov r10,[rcx+r9*8]
mov r9d,edx
add r9w,$01
mov esi,r8d
cmp r9w,si
jnbe MakeNextRowsP + $84
add si,$01
gauss64.pas.745: S[j] := S[j]-R[j]*S[index];
movzx rdi,r9w
movzx r13,r9w
cvtss2sd xmm0,qword ptr [r10+r13*4]
movzx r13,r9w
cvtss2sd xmm1,qword ptr [r11+r13*4]
movzx r13,dx
cvtss2sd xmm2,qword ptr [r10+r13*4]
mulsd xmm1,xmm2
subsd xmm0,xmm1
cvtsd2ss xmm0,xmm0
movss dword ptr [r10+rdi*4],xmm0
add r9w,$01
gauss64.pas.744: for j := index+1 to n do
cmp r9w,si
jnz MakeNextRowsP + $44
nop
gauss64.pas.746: end;
add ax,$01
gauss64.pas.741: for i := index+1 to n-1 do
cmp ax,bx
jnz MakeNextRowsP + $27
nop
gauss64.pas.747: end;
mov rsp,rbp
pop rbx
pop rsi
pop rdi
pop r13
pop rbp
ret
Похоже конвертация из-за включенного EXCESSPRECISION ключика: http://docwiki.embarcadero.com/RADStudio/Rio/en/Floating_point_precision_control_(Delphi_for_x64) , опасный он какой-то этот дефолтный режим, выделил локальную переменную из выражения и получил другой результат. Хотели как лучше, а получилось как всегда!
УдалитьЕще S[index] постоянно из памяти в регистр загружается, может ему подсказать и вынести в переменную? И забавно что "movzx r13,r9w" продублировано, такое ощущение что он сам себе не доверяет, или может быть cvtss2sd портит r13 ? Но вроде не должна.
Clang в debug режиме тоже не особо что-то делает, скорее как написано так и компилирует, интересно было бы после оптимизации посмотреть.
О оказывается это даже не создатели паскаля придумали такой опасный режим, а x87 использует 80-бит для промежуточных операций по умолчанию. А в 64-битный паскаль его добавили просто чтобы эмулировать похожее поведение. Как по мне так это какая-то подстава, ну не должно r=x*y*z и a=x*y; r=a*z давать разный результат, это ведь кучу оптимизаций убивает, прям связывает руки компилятору и человеку.
Удалить>Похоже конвертация из-за включенного EXCESSPRECISION
УдалитьДа, интересный режим. Но это они для повышения точности промежуточных расчетов сделали. В принципе, это нормальный подход, что бы обеспечить приемлемую точность расчетов за счет снижения производительности.
>x87 использует 80-бит для промежуточных операций по умолчанию
Ну да, это в общем-то, хорошо известный факт. Приятный подход для научных расчетов, ну и он аппаратно реализован. В научных расчетах, вообще равенство не очень-то сильно уважают, из-за вечных ошибок при операциях с плавающей точностью.
>не должно r=x*y*z и a=x*y; r=a*z давать разный результат
Тут ценится именно погрешность вычислений, поэтому гораздо важнее не то, что бы получить одинаковый результат, важнее получить более точный результат.
Это в играх на точность плевать, главное, что бы побыстрее было. :)
>Еще S[index] постоянно из памяти в регистр загружается, может ему подсказать и вынести в переменную?
Да, я в каком-то варианте так делал. Но у меня нет цели написать идеальный паскалевский код. Это так, заготовка для ассемблера. Хотя да, надежда была, что он сообразит, вычислит и вынесет из цикла. Но нет, тут как в старые добрые времена, нужно самому думать )))
>И забавно что "movzx r13,r9w"
Кстати, да. Меня еще поражает вход/выход из процедуры: сохранение и восстановления кучи регистров в стеке.
> В принципе, это нормальный подход, что бы обеспечить приемлемую точность расчетов за счет снижения производительности.
УдалитьНу не знаю, я за явное указание типа, нужно считать в double, приведи тип к double, а если не хватает точности, то есть библиотеки которые программно реализуют 128-битные и даже большей точности типы, не на столько быстро, но зато все честно. Но это так, мысли в слух.
> Ну да, это в общем-то, хорошо известный факт.
Я знал что x87 поддерживает 80-битные типы, но почему-то думал там также как и в SSE на уровне отдельных команд задается формат данных.
> В научных расчетах, вообще равенство не очень-то сильно уважают
Это да, но как мне кажется повторяемость вычислений важна и полезна: порефакторил код, получил абсолютно такой же результат, значит ничего не сломал (ну или почти ничего), а если результат чуть-чуть другой, то ходи гадай: толи ошибся, толи потеря точности, а если потеря, то на сколько критично, а вдруг наоборот точнее стало, а это лучше или хуже и т.д. Для меня, как инженера, такой код - это головная боль.
> Это в играх на точность плевать, главное, что бы побыстрее было. :)
Ага, а еще в нейронных сетях, там порой и 16-битных типов хватает, зато быстро и меньше места занимает, но это уже другая история, о GPU.
> Но у меня нет цели написать идеальный паскалевский код. Это так, заготовка для ассемблера.
Я понимаю, просто интересно было, на что современный паскаль способен.
Если я правильно понимаю современный подход - это постараться остаться в высокоуровневом языке, но подсказать компилятору (иногда изучая ассемблер), в том же C, достаточно популярны Intrinsics, это когда код вызывает сходу обычные функции, но на деле они транслируются напрямую в инструкции процессора, плюс еще куча разных других способов дать подсказку и даже приказать (скажем компиляция упадет, если компилятор не смог векторизовать цикл). Ассемблер - это совсем сурово.
Это так, лирическое отступление. Вы ведь сейчас не продакшен код пишете, который нужно будет поддерживать долгие годы? :)
> Хотя да, надежда была, что он сообразит, вычислит и вынесет из цикла.
Я предполагаю, его смутило что S изменяется в цикле, хотя может он даже и не пытался.
>Вы ведь сейчас не продакшен код пишете, который нужно будет поддерживать долгие годы? :)
УдалитьНет конечно, просто развлекаюсь )))