Я новичок в оптимизации инструкций.
Я сделал простой анализ простой функции dotp, которая используется для получения точечного произведения двух массивов с плавающей запятой.
Код C выглядит следующим образом:
float dotp(
const float x[],
const float y[],
const short n
)
{
short i;
float suma;
suma = 0.0f;
for(i=0; i<n; i++)
{
suma += x[i] * y[i];
}
return suma;
}
Я использую тестовый фрейм, предоставленный Agner Fog в Интернете testp.
Массивы, которые используются в этом случае, выравниваются:
int n = 2048;
float* z2 = (float*)_mm_malloc(sizeof(float)*n, 64);
char *mem = (char*)_mm_malloc(1<<18,4096);
char *a = mem;
char *b = a+n*sizeof(float);
char *c = b+n*sizeof(float);
float *x = (float*)a;
float *y = (float*)b;
float *z = (float*)c;
Затем я вызываю функцию dotp, n = 2048, repeat = 100000:
for (i = 0; i < repeat; i++)
{
sum = dotp(x,y,n);
}
Я скомпилирую его с помощью gcc 4.8.3 с параметром компиляции -O3.
Я компилирую это приложение на компьютер, который не поддерживает инструкции FMA, поэтому вы можете видеть только инструкции SSE.
Код сборки:
.L13:
movss xmm1, DWORD PTR [rdi+rax*4]
mulss xmm1, DWORD PTR [rsi+rax*4]
add rax, 1
cmp cx, ax
addss xmm0, xmm1
jg .L13
Я делаю некоторый анализ:
μops-fused la 0 1 2 3 4 5 6 7
movss 1 3 0.5 0.5
mulss 1 5 0.5 0.5 0.5 0.5
add 1 1 0.25 0.25 0.25 0.25
cmp 1 1 0.25 0.25 0.25 0.25
addss 1 3 1
jg 1 1 1 -----------------------------------------------------------------------------
total 6 5 1 2 1 1 0.5 1.5
После запуска мы получим результат:
Clock | Core cyc | Instruct | BrTaken | uop p0 | uop p1
--------------------------------------------------------------------
542177906 |609942404 |1230100389 |205000027 |261069369 |205511063
--------------------------------------------------------------------
2.64 | 2.97 | 6.00 | 1 | 1.27 | 1.00
uop p2 | uop p3 | uop p4 | uop p5 | uop p6 | uop p7
-----------------------------------------------------------------------
205185258 | 205188997 | 100833 | 245370353 | 313581694 | 844
-----------------------------------------------------------------------
1.00 | 1.00 | 0.00 | 1.19 | 1.52 | 0.00
Вторая строка - это значение, считанное из регистров Intel; третья строка делится на номер ветки, "BrTaken".
Итак, мы видим, что в цикле имеется 6 команд, 7 uops, в соответствии с анализом.
Количество uops, выполняемых в порте port0 port1 port 5 port6, похоже на то, что говорит анализ. Я думаю, возможно, планировщик uops делает это, он может попытаться сбалансировать нагрузки на порты, я прав?
Я совершенно не понимаю, почему существует только около 3 циклов за цикл. Согласно таблице agner , время ожидания команды mulss
равно 5, и между циклами есть зависимости, насколько я вижу он должен принимать не менее 5 циклов за цикл.
Может ли кто-нибудь пролить свет?
=============================================== ===================
Я попытался написать оптимизированную версию этой функции в nasm, разворачивая цикл в 8 раз и используя инструкцию vfmadd231ps
:
.L2:
vmovaps ymm1, [rdi+rax]
vfmadd231ps ymm0, ymm1, [rsi+rax]
vmovaps ymm2, [rdi+rax+32]
vfmadd231ps ymm3, ymm2, [rsi+rax+32]
vmovaps ymm4, [rdi+rax+64]
vfmadd231ps ymm5, ymm4, [rsi+rax+64]
vmovaps ymm6, [rdi+rax+96]
vfmadd231ps ymm7, ymm6, [rsi+rax+96]
vmovaps ymm8, [rdi+rax+128]
vfmadd231ps ymm9, ymm8, [rsi+rax+128]
vmovaps ymm10, [rdi+rax+160]
vfmadd231ps ymm11, ymm10, [rsi+rax+160]
vmovaps ymm12, [rdi+rax+192]
vfmadd231ps ymm13, ymm12, [rsi+rax+192]
vmovaps ymm14, [rdi+rax+224]
vfmadd231ps ymm15, ymm14, [rsi+rax+224]
add rax, 256
jne .L2
Результат:
Clock | Core cyc | Instruct | BrTaken | uop p0 | uop p1
------------------------------------------------------------------------
24371315 | 27477805| 59400061 | 3200001 | 14679543 | 11011601
------------------------------------------------------------------------
7.62 | 8.59 | 18.56 | 1 | 4.59 | 3.44
uop p2 | uop p3 | uop p4 | uop p5 | uop p6 | uop p7
-------------------------------------------------------------------------
25960380 |26000252 | 47 | 537 | 3301043 | 10
------------------------------------------------------------------------------
8.11 |8.13 | 0.00 | 0.00 | 1.03 | 0.00
Таким образом, мы можем видеть, что кэш данных L1 достигает 2 * 256 бит /8.59, он очень близок к пику 2 * 256/8, использование составляет около 93%, в модуле FMA используется только 8/8.59, пик - 2 * 8/8, использование составляет 47%.
Итак, я думаю, что я достиг узкого места L1D, которого ожидает Питер Кордес.
=============================================== ===================
Особая благодарность Боанну, исправить так много грамматических ошибок в моем вопросе.
=============================================== ==================
От Питера ответьте, я понимаю, что только "прочитанный и написанный" регистр будет зависимостью, "записи только для писателя" не будут зависимостью.
Итак, я пытаюсь уменьшить регистры, используемые в цикле, и я пытаюсь развернуть на 5, если все в порядке, я должен встретить одно и то же узкое место, L1D.
.L2:
vmovaps ymm0, [rdi+rax]
vfmadd231ps ymm1, ymm0, [rsi+rax]
vmovaps ymm0, [rdi+rax+32]
vfmadd231ps ymm2, ymm0, [rsi+rax+32]
vmovaps ymm0, [rdi+rax+64]
vfmadd231ps ymm3, ymm0, [rsi+rax+64]
vmovaps ymm0, [rdi+rax+96]
vfmadd231ps ymm4, ymm0, [rsi+rax+96]
vmovaps ymm0, [rdi+rax+128]
vfmadd231ps ymm5, ymm0, [rsi+rax+128]
add rax, 160 ;n = n+32
jne .L2
Результат:
Clock | Core cyc | Instruct | BrTaken | uop p0 | uop p1
------------------------------------------------------------------------
25332590 | 28547345 | 63700051 | 5100001 | 14951738 | 10549694
------------------------------------------------------------------------
4.97 | 5.60 | 12.49 | 1 | 2.93 | 2.07
uop p2 |uop p3 | uop p4 | uop p5 |uop p6 | uop p7
------------------------------------------------------------------------------
25900132 |25900132 | 50 | 683 | 5400909 | 9
-------------------------------------------------------------------------------
5.08 |5.08 | 0.00 | 0.00 |1.06 | 0.00
Мы можем видеть 5/5.60 = 89.45%, он немного меньше, чем urolling на 8, что-то не так?
=============================================== ==================
Я пытаюсь развернуть цикл на 6, 7 и 15, чтобы увидеть результат. Я снова разворачиваю 5 и 8, чтобы удвоить подтверждение результата.
Результат следующий: мы видим, что на этот раз результат намного лучше, чем раньше.
Хотя результат нестабилен, коэффициент разворачивания больше и результат лучше.
| L1D bandwidth | CodeMiss | L1D Miss | L2 Miss
----------------------------------------------------------------------------
unroll5 | 91.86% ~ 91.94% | 3~33 | 272~888 | 17~223
--------------------------------------------------------------------------
unroll6 | 92.93% ~ 93.00% | 4~30 | 481~1432 | 26~213
--------------------------------------------------------------------------
unroll7 | 92.29% ~ 92.65% | 5~28 | 336~1736 | 14~257
--------------------------------------------------------------------------
unroll8 | 95.10% ~ 97.68% | 4~23 | 363~780 | 42~132
--------------------------------------------------------------------------
unroll15 | 97.95% ~ 98.16% | 5~28 | 651~1295 | 29~68
=============================================== ======================
Я пытаюсь скомпилировать функцию с gcc 7.1 в сети https://gcc.godbolt.org"
Опция компиляции - "-O3 -march = haswell -mtune = intel", которая похожа на gcc 4.8.3.
.L3:
vmovss xmm1, DWORD PTR [rdi+rax]
vfmadd231ss xmm0, xmm1, DWORD PTR [rsi+rax]
add rax, 4
cmp rdx, rax
jne .L3
ret