Эффект выравнивания кода в основных петлях синхронизации в сборке
Скажем, у меня есть следующий основной цикл
.L2:
vmulps ymm1, ymm2, [rdi+rax]
vaddps ymm1, ymm1, [rsi+rax]
vmovaps [rdx+rax], ymm1
add rax, 32
jne .L2
То, как я буду это делать, это поместить его в другой длинный цикл, подобный этому
;align 32
.L1:
mov rax, rcx
neg rax
align 32
.L2:
vmulps ymm1, ymm2, [rdi+rax]
vaddps ymm1, ymm1, [rsi+rax]
vmovaps [rdx+rax], ymm1
add rax, 32
jne .L2
sub r8d, 1 ; r8 contains a large integer
jnz .L1
Что я нахожу, так это то, что выбранное выравнивание может существенно повлиять на время (до +10%). Мне непонятно, как выбрать выравнивание кода. Есть три места, о которых я могу подумать, где я могу захотеть выровнять код
- При входе в функцию (см., например,
triad_fma_asm_repeat
в коде ниже)
- В начале внешнего цикла (
.L1
выше), который повторяет мой основной цикл
- В начале моего основного цикла (
.L2
выше).
Еще одна вещь, которую я обнаружил, заключается в том, что если я добавлю другую процедуру в свой исходный файл, то изменение одной команды (например, удаление инструкции) может существенно повлиять на время следующей функции, даже если они являются независимыми функциями. Я даже видел, что это в прошлом влияло на процедуру в другом объектном файле.
Я прочитал раздел 11.5 "Выравнивание кода" в Agner Fog оптимизирует сборку руководства, но мне все еще не ясно, как наилучшим образом выровняйте мой код для тестирования производительности. Он приводит пример, 11.5, о временном цикле, который я действительно не соблюдаю.
В настоящее время получение самой высокой производительности из моего кода - игра угадывания разных значений и мест выравнивания.
Я хотел бы знать, есть ли интеллектуальный метод для выбора выравнивания? Должен ли я выровнять внутренний и наружный овраги? Только внутренняя петля? Запись в функцию также? Используете ли короткие или длинные проблемы NOP?
Меня интересует Хасуэлл, затем SNB/IVB, а затем Core2.
Я попробовал NASM и YASM и обнаружил, что это одна из областей, где они значительно различаются. NASM только вставляет однобайтные инструкции NOP, где YASM вставляет многобайтовые NOP. Например, выравнивая внутренний и внешний контуры выше 32 байтов, NASM вставил 20 инструкций NOP (0x90), где, когда YASM вставил следующее (из objdump)
2c: 66 66 66 66 66 66 2e data16 data16 data16 data16 data16 nopw %cs:0x0(%rax,%rax,1)
33: 0f 1f 84 00 00 00 00
3a: 00
3b: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
До сих пор я не наблюдал существенной разницы в производительности с этим. Похоже, что это выравнивание не имеет значения длины инструкции. Но Агнер пишет в разделе кода выравнивания:
Более эффективно использовать более длинные инструкции, которые ничего не делают, чем использовать много однобайтовых NOP.
Если вы хотите сыграть с выравниванием и посмотреть сами эффекты, вы можете найти как сборку, так и код C, который я использую. Замените double frequency = 3.6
на эффективную частоту вашего CPU. Вы можете отключить турбо.
;nasm/yasm -f elf64 align_asm.asm`
global triad_fma_asm_repeat
;RDI x, RSI y, RDX z, RCX n, R8 repeat
;z[i] = y[i] + 3.14159*x[i]
pi: dd 3.14159
section .text
align 16
triad_fma_asm_repeat:
shl rcx, 2
add rdi, rcx
add rsi, rcx
add rdx, rcx
vbroadcastss ymm2, [rel pi]
;neg rcx
;align 32
.L1:
mov rax, rcx
neg rax
align 32
.L2:
vmulps ymm1, ymm2, [rdi+rax]
vaddps ymm1, ymm1, [rsi+rax]
vmovaps [rdx+rax], ymm1
add rax, 32
jne .L2
sub r8d, 1
jnz .L1
vzeroupper
ret
global triad_fma_store_asm_repeat
;RDI x, RSI y, RDX z, RCX n, R8 repeat
;z[i] = y[i] + 3.14159*x[i]
align 16
triad_fma_store_asm_repeat:
shl rcx, 2
add rcx, rdx
sub rdi, rdx
sub rsi, rdx
vbroadcastss ymm2, [rel pi]
;align 32
.L1:
mov r9, rdx
align 32
.L2:
vmulps ymm1, ymm2, [rdi+r9]
vaddps ymm1, ymm1, [rsi+r9]
vmovaps [r9], ymm1
add r9, 32
cmp r9, rcx
jne .L2
sub r8d, 1
jnz .L1
vzeroupper
ret
Вот код C, который я использую для вызова процедур сборки и времени их
//gcc -std=gnu99 -O3 -mavx align.c -lgomp align_asm.o -o align_avx
//gcc -std=gnu99 -O3 -mfma -mavx2 align.c -lgomp align_asm.o -o align_fma
#include <stdio.h>
#include <string.h>
#include <omp.h>
float triad_fma_asm_repeat(float *x, float *y, float *z, const int n, int repeat);
float triad_fma_store_asm_repeat(float *x, float *y, float *z, const int n, int repeat);
float triad_fma_repeat(float *x, float *y, float *z, const int n, int repeat)
{
float k = 3.14159f;
int r;
for(r=0; r<repeat; r++) {
int i;
__m256 k4 = _mm256_set1_ps(k);
for(i=0; i<n; i+=8) {
_mm256_store_ps(&z[i], _mm256_add_ps(_mm256_load_ps(&x[i]), _mm256_mul_ps(k4, _mm256_load_ps(&y[i]))));
}
}
}
int main (void )
{
int bytes_per_cycle = 0;
double frequency = 3.6;
#if (defined(__FMA__))
bytes_per_cycle = 96;
#elif (defined(__AVX__))
bytes_per_cycle = 48;
#else
bytes_per_cycle = 24;
#endif
double peak = frequency*bytes_per_cycle;
const 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;
for(int i=0; i<n; i++) {
x[i] = 1.0f*i;
y[i] = 1.0f*i;
z[i] = 0;
}
int repeat = 1000000;
triad_fma_repeat(x,y,z2,n,repeat);
while(1) {
double dtime, rate;
memset(z, 0, n*sizeof(float));
dtime = -omp_get_wtime();
triad_fma_asm_repeat(x,y,z,n,repeat);
dtime += omp_get_wtime();
rate = 3.0*1E-9*sizeof(float)*n*repeat/dtime;
printf("t1 rate %6.2f GB/s, efficency %6.2f%%, error %d\n", rate, 100*rate/peak, memcmp(z,z2, sizeof(float)*n));
memset(z, 0, n*sizeof(float));
dtime = -omp_get_wtime();
triad_fma_store_asm_repeat(x,y,z,n,repeat);
dtime += omp_get_wtime();
rate = 3.0*1E-9*sizeof(float)*n*repeat/dtime;
printf("t2 rate %6.2f GB/s, efficency %6.2f%%, error %d\n", rate, 100*rate/peak, memcmp(z,z2, sizeof(float)*n));
puts("");
}
}
Меня беспокоит следующая инструкция в Руководство NASM
Окончательная оговорка: ALIGN и ALIGNB работают относительно начала раздела, а не в начале адресного пространства в конечном исполняемом файле. Например, выравнивание до 16-байтовой границы, когда разрез, в котором вы находитесь, гарантированно выравнивается с 4-байтной границей, является пустой тратой усилий. Опять же, NASM не проверяет, что характеристики выравнивания раздела разумны для использования ALIGN или ALIGNB.
Я не уверен, что сегмент кода получает абсолютный 32-байтовый выровненный адрес или только относительный.
Ответы
Ответ 1
Что касается вашего последнего вопроса об относительном (внутрисегментном) выравнивании и абсолютном (в памяти во время выполнения), вам не нужно слишком беспокоиться. Как раз под разделом руководства, которое вы указали, которое предупреждает о ALIGN
, не проверяя выравнивание раздела, у вас есть следующее:
И ALIGN, и ALIGNB неявно вызывают макрос SECTALIGN. Подробнее см. В разделе 4.11.13.
Таким образом, в основном ALIGN
не проверяет правильность выравнивания, но вызывает макрос SECTALIGN
, чтобы выравнивание было разумным. В частности, все неявные вызовы SECTALIGN
должны гарантировать, что раздел будет выровнен с наибольшим выравниванием, заданным любым вызовом выравнивания.
Предупреждение о ALIGN
не проверяется, вероятно, применимо только к более неясным случаям, например, при сборке в форматах, которые не поддерживают выравнивание разделов, при указании выравнивания больше, чем поддерживаемого секцией, или когда SECTALIGN OFF
вызывается для отключения SECTALIGN
.
Ответ 2
Ваш цикл должен идеально (примерно) выполнить за одну итерацию за такт, имея четыре му-оп (add/jne - один). Критический вопрос - предсказуемость ветки внутреннего цикла. До 16 итераций он должен быть предсказан в коде времени, всегда один и тот же, но после этого вы можете борется. Во-первых, чтобы ответить на ваш вопрос, выравнивание ключей для синхронизации заключается в том, чтобы гарантировать, что ни код после jne.L2, ни первая команда после .L2 не пересекают 32-байтовую границу. Я предполагаю, что вопрос реальный заключается в том, как заставить его работать быстрее, и если моя гипотеза о > 16 итерациях верна, основная задача состоит в том, чтобы сделать предсказание ветвления. Чтобы ваши временные моменты были короче, должно быть легко - достаточно иметь несколько ветвей, которые все предсказуемы. Однако, чтобы сделать конечный код быстрее, зависит от того, как изменяются реальные значения rax, и это будет зависеть также от процедуры, которая вызывает цикл.