Z b*_*son 5 x86 assembly nasm yasm
假设我有以下主循环
.L2:
vmulps ymm1, ymm2, [rdi+rax]
vaddps ymm1, ymm1, [rsi+rax]
vmovaps [rdx+rax], ymm1
add rax, 32
jne .L2
Run Code Online (Sandbox Code Playgroud)
我想时间的方式是把它放在另一个像这样的长循环中
;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
Run Code Online (Sandbox Code Playgroud)
我发现的是我选择的对齐方式会对时序产生重大影响(最高可达+ -10%).我不清楚如何选择代码对齐方式.我可以想到三个地方,我可能想要对齐代码
triad_fma_asm_repeat下面的代码中).L1上面)重复我的主循环.L2上图). 我发现的另一件事是,如果我在源文件中放入另一个例程,即更改一条指令(例如删除指令),即使它们是独立函数,也会对下一个函数的时序产生重大影响.我甚至在过去看到过影响另一个目标文件中的例程.
我在Agner Fog的优化装配手册中阅读了第11.5节"代码对齐",但我仍然不清楚调整代码以测试性能的最佳方法.他给出了一个例子,11.5,计时内循环,我并没有真正遵循.
目前,从我的代码中获得最高性能是一种猜测不同值和对齐位置的游戏.
我想知道是否有一种智能方法可以选择对齐方式?我应该对齐内圈和外圈吗?只是内循环?该功能的入口?使用短期或长期NOP是否重要?
我最感兴趣的是Haswell,其次是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)
Run Code Online (Sandbox Code Playgroud)
到目前为止,我没有观察到性能与此有显着差异.似乎它的对齐与指令长度无关.但是Agner在对齐代码部分写道:
使用更长的指令比使用大量单字节NOP更有效.
如果你想使用对齐并亲自看看效果,你可以找到我使用的汇编和C代码.替换double frequency = 3.6为CPU的有效频率.您可能想要禁用turbo.
;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
Run Code Online (Sandbox Code Playgroud)
这是我用来调用程序集例程并为它们计时的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("");
}
}
Run Code Online (Sandbox Code Playgroud)
我对NASM手册中的以下声明感到困扰
最后告诫答:ALIGN和ALIGNB相对部分,而不是最终的可执行文件的地址空间的开始的开始工作.例如,当您所在的部分仅保证与4字节边界对齐时,与16字节边界对齐是浪费精力.同样,NASM不会检查该部分的对齐特征是否适合使用ALIGN或ALIGNB.
我不确定代码段是获取绝对的32字节对齐地址还是仅获得相对的地址.
理想情况下,您的循环应该(大约)在每个时钟周期执行一次迭代,具有四个 mu 操作(add/jne 是一个)。一个关键问题是内循环分支的可预测性。最多 16 次迭代应该在计时代码中进行预测,并且始终相同,但之后您可能会遇到困难。首先,为了回答您的问题,时序的关键对齐是确保 jne .L2 之后的代码和 .L2 之后的第一条指令都不会跨越 32 字节边界。我认为真正的问题是如何让它运行得更快,如果我对 > 16 次迭代的猜测是正确的,那么关键目标是使分支预测发挥作用。要缩短你的计时时间应该很容易 - 有几个都是可预测的分支就足够了。然而,为了使最终代码运行得更快,取决于 rax 的实际值如何变化,这也取决于调用循环的例程。