Ну вот, наконец-то добрался до SSE. И предварительные результаты несколько обескураживающие. Я, конечно не ожидал, что использование векторных команд увеличит производительность пропорционально размеру вектора, все же даже решение СЛАУ, которое прямо просятся решаться векторными операциями, на 100% не векторизуется.
Но все же я надеялся хотя бы на двукратный рост на типе single (float), которому на SSE соответствует вектор из 4 элементов. На практике же максимальное ускорение относительно решения на Scalar SSE составило всего 1.66 раза, что на AMD FX-4350 соответствует решению 158 уравнений в секунду.
Очевидно, что на маленьких СЛАУ вышло даже небольшое замедление. На СЛАУ из 3 уравнений ускорение получилось 0.93.
Но самая печаль на типе double. В SSE ему соотвествует вектор всего из двух элементов. Максимальное ускорение достигнуто на СЛАУ из 192 переменных и составляет 1.18 раза. На маленьких размерностях (3 и 6 уравнений) ускорение меньше единицы, далее растет, после максимума на 192 уравнениях плавно снижается, приближаясь к единице 1536 уравнениях.
Одна из возможных причин состоит в том, что SSE не умеет выполнять арифметические операции с невыровненными на границу 16 байт данными. А при решении СЛАУ они гарантированно не выровнены.
AMD рекомендует в таких случая выполнять сохранение в две команды, записывая за раз по половине векторного регистра. Попробовал, ощутимой разницы не увидел: на каких-то размерностях стало быстрее, на других же наоборот.
Вроде бы в AVX эту "недоработку" пофиксили и арифметические команды выполняют и над невыровненными данными.
Тут кстати обнаружил, что моя версия Delphi не умеет в AVX. Надо подумать, как красивее обойти это органичение.
Ну и напоследок пример кода для double, возможно, мне опять какой-нибудь очевидный косяк замылился.
procedure MakeDoubleNextRowsP_SSE;
asm
// R8 - P, R9w - n, R10w - index
lea EBX, [R10d+1]; // BX - i
cmp BX, R9w;
jae @exit;
@Loop1:
mov RCX, [R8 + RBX*8]; // RCX - S
lea EDX, [R10d+1];
lea EAX, [R9d-7];
movsd XMM0, [RCX+R10*8];
unpcklpd XMM0, XMM0;
cmp EDX, EAX;
jg @Loop2_4;
@Loop2:
movapd XMM1, XMM0;
movupd XMM2, [R11+RDX*8];
mulpd XMM1, XMM2;
movupd XMM2, [RCX+RDX*8];
subpd XMM2, XMM1;
movupd [RCX+RDX*8], XMM2;
movapd XMM1, XMM0;
movupd XMM2, [R11+RDX*8+16];
mulpd XMM1, XMM2;
movupd XMM2, [RCX+RDX*8+16];
subpd XMM2, XMM1;
movupd [RCX+RDX*8+16], XMM2;
movapd XMM1, XMM0;
movupd XMM2, [R11+RDX*8+32];
mulpd XMM1, XMM2;
movupd XMM2, [RCX+RDX*8+32];
subpd XMM2, XMM1;
movupd [RCX+RDX*8+32], XMM2;
movapd XMM1, XMM0;
movupd XMM2, [R11+RDX*8+48];
mulpd XMM1, XMM2;
movupd XMM2, [RCX+RDX*8+48];
subpd XMM2, XMM1;
movupd [RCX+RDX*8+48], XMM2;
add DX, 8;
cmp EDX, EAX;
jle @Loop2;
@Loop2_4:
add EAX, 4;
cmp EDX, EAX;
jg @Loop2_2;
movapd XMM1, XMM0;
movupd XMM2, [R11+RDX*8];
mulpd XMM1, XMM2;
movupd XMM2, [RCX+RDX*8];
subpd XMM2, XMM1;
movupd [RCX+RDX*8], XMM2;
movapd XMM1, XMM0;
movupd XMM2, [R11+RDX*8+16];
mulpd XMM1, XMM2;
movupd XMM2, [RCX+RDX*8+16];
subpd XMM2, XMM1;
movupd [RCX+RDX*8+16], XMM2;
add DX, 4;
@Loop2_2:
add EAX, 2;
cmp EDX, EAX;
jg @Loop2_1;
movapd XMM1, XMM0;
movupd XMM2, [R11+RDX*8];
mulpd XMM1, XMM2;
movupd XMM2, [RCX+RDX*8];
subpd XMM2, XMM1;
movupd [RCX+RDX*8], XMM2;
add DX, 2;
@loop2_1:
cmp DX, R9w;
ja @Loop2_Fin;
movapd XMM1, XMM0;
mulsd XMM1, [R11+RDX*8];
movsd XMM2, [RCX+RDX*8];
subsd XMM2, XMM1;
movsd [RCX+RDX*8], XMM2;
@Loop2_Fin:
inc BX;
cmp BX, R9w;
jb @Loop1;
@exit:
end;
У меня тоже прирост далеко не в 4 раз на double и AVX:
ОтветитьУдалить- 192 уравнений: 1.8
- 1536: 1.7
- 3072: 1.5
- 6144: 1.3
Но у моего процессора, судя по описанию, еще есть 64 MB L4 кэш (видео память) возможно это объясняет не единичное ускорение.
В целом меньше чем ожидаемое ускорения объяснимо: векторные инструкции ускоряют только работу с данными которые уже в L1 кэше, а в данном алгоритме узкое место - это память, а не математика, по крайней мере в его стандартной реализации.
А не пробовали посмотреть куда тратится время?
Я, чтобы примерно оценить где узкое место, измерял следующее:
- T_base - время решения одной СЛАУ
- T_no_loop3 - время "решения" СЛАУ с закоментированном телом внутреннего цикла.
- T_extra_loop3 - время "решения" с продублированном 3-м циклом (не телом, а именно циклом целиком).
Далее считал:
- T_loop3 = T_base - T_no_loop3 - время затраченное целиком на 3-й цикл, включая загрузку данных если необходимо.
- T_loop3_cached = T_extra_loop3 - T_base - время достаточное для 3-го цикла если все данные уже в L1. 1-й цикл - прогревает кэш, а 2-й дополнительный цикл проходит уже по горячим данным.
- T_loop3_mem = T_loop3 - T_loop3_cached - очень примерное время затраченное на ожидание загрузки данных в L1 кэш
В моем случае для 192 уравнений (Scalar AVX, Vector AVX) выше описанные времена:
- T_base: 1.395, 0.770 ms (x1.8)
- T_no_loop: 0.076, 0.076 ms - время одинаковое, так как только 3-й цикл векторизирован (я не добиваюсь единицы на главной диагонали)
- T_loop3: 1.319, 0.694 ms (x1.9, уже лучше так как исключили невекторизированный код)
- T_loop3_cached: 1.101, 0.365 ms (x3, не x4 как ожидалось, но все же лучше чем x1.8)
- T_loop3_mem: 0.218 ms, 0.328 ms (почему векторизация увеличила это время, я не знаю, возможно погрешность измерений)
Для 1536 все аналогично (Scalar AVX, Vector AVX):
- T_base: 920, 541 ms (x1.7)
- T_no_loop: 19, 19 ms
- T_loop3: 902, 523 ms (x1.7)
- T_loop3_cached: 563, 185 (x3)
- T_loop3_mem: 339, 338 ms Время получилось одинаковым, вроде так и должно быть, но как то прям не верится.
Судя по тем цифрам что я наблюдаю у себя, чем больше СЛАУ, тем увеличивается ожидание загрузки данных (T_loop3_mem), которое не уменьшается простым переходом на векторные инструкции, это объясняет падение ускорения от векторизации.
> А при решении СЛАУ они гарантированно не выровнены.
Вроде можно выровнять следующим образом: изначально выровнять строки по 128 байтам, перед векторизированным циклом выполнять короткий Scalar SSE цикл (от 0 до 3-х итераций для float и 0 до 1-й итерации для double в зависимости от текущей колонки), но на сколько это ускорит и ускорит ли вообще я без понятия :)))
> возможно, мне опять какой-нибудь очевидный косяк замылился
Вроде норм, я с ходу не вижу как улучшить :)
> выровнять строки по 128 байтам
Удалитьопечатался, конечно по 128 битам, а не байтам
Этот комментарий был удален автором.
Удалить> У меня тоже прирост далеко не в 4 раз на double и AVX: - 192 уравнений: 1.8
УдалитьНу, если уж на AVX такой прирост... Тогда для SSE вроде норм.
> А не пробовали посмотреть куда тратится время?
Нет, так глубоко не копал. А ты прямо очень серьезно к вопросу подошел.
> Судя по тем цифрам что я наблюдаю у себя, чем больше СЛАУ, тем увеличивается ожидание загрузки данных (T_loop3_mem)
Судя по всему, именно так. Все же процессор работает намного быстрее, чем память. Даже скалярные инструкции слишком быстрые в случае гаусса, при выходе за пределы L3 производительность сильно проседает. Ну а в случае векторных так вообще, особенно при использовании AVX.
В связи с чем возникает резонный вопрос необходимости AVX-512. Не даром AMD его вообще не торопится реализовывать...
> Ну, если уж на AVX такой прирост... Тогда для SSE вроде норм.
УдалитьАга, на серверном варианте бы запустить с 8 канальной памятью, думаю разница была бы куда интереснее
> А ты прямо очень серьезно к вопросу подошел.
Это точно :))), меня почему-то очень заинтересовало как переписать алгоритм так, чтобы он более эффективно кэш использовал. Как я уже писал ранее, придумал пересчитывать строки блоками. И все конечно, как позже оказалось, было уже придумано до меня :( Даже статья есть в английской википедии: https://en.wikipedia.org/wiki/Loop_nest_optimization И именно по такому принципу реализовано LU-разложение в том же OpenBLAS.
Текущий мой рекорд: 5.3 СЛАУ из 1536 уравнений за секунду, почти в 3 раза быстрее чем стандартный алгоритм. А еще интересно что оптимизированный алгоритм больше зависит от векторизации, что в принципе ожидаемо, на 384 уравнений Vector AVX в 2.5 раза быстрее чем его Scalar AVX версия для double.
> В связи с чем возникает резонный вопрос необходимости AVX-512
Это точно, Линус того же мнения: https://www.realworldtech.com/forum/?threadid=193189&curpostid=193190
> Текущий мой рекорд: 5.3 СЛАУ из 1536 уравнений за секунду, почти в 3 раза быстрее чем стандартный алгоритм.
УдалитьДа, это прям супер, для больших размерностей. Впрочем, кажется, пока система вмешается в L2, а может даже и в L3, этот метод особых преимущество не дает? Или я не прав?
На самых крохотных - медленнее, так как больше индексов инкрементируется и в целом код чуток посложнее, а дальше обработка блоками потихоньку начинает проявлять себя:
Удалить- < 48 уравнений медленнее, нужно переходить на обычный алгоритм.
- 48: 54107 (x1.14)
- 96: 10666 (x1.60)
- 192: 2162 (x1.66)
- 384: 345 (x2.08)
- 768: 38 (x1.92)
- 1536: 5.288 (x2.86)
- 3072: 0.617 (x3.43)
- 6144: 0.051 (x3)