5

假设我有以下主循环

.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%)。我不清楚如何选择代码对齐方式。我可以想到三个地方我可能想要对齐代码

  1. 在函数的入口处(参见triad_fma_asm_repeat下面的代码)
  2. 在重复我的主循环的外循环(.L1上面)的开始处
  3. 在我的主循环开始时(.L2上面)。

我发现的另一件事是,如果我在源文件中放入另一个例程,那么即使它们是独立的函数,更改一条指令(例如删除一条指令)也会对下一个函数的时序产生重大影响。过去我什至看到这会影响另一个目标文件中的例程。

我已阅读Agner Fog 的优化组装手册中的第 11.5 节“代码对齐”,但我仍然不清楚对齐代码以测试性能的最佳方法。他举了一个例子,11.5,计时一个我没有真正遵循的内循环。

目前从我的代码中获得最高性能的是猜测不同值和对齐位置的游戏。

我想知道是否有一种智能的方法来选择对齐方式?我应该对齐内环和外环吗?只是内循环?函数的入口也是?使用短 NOP 还是长 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)

到目前为止,我还没有观察到性能上有显着差异。看来,对齐与指令长度无关。但是 Agner 在对齐代码部分写道:

使用更长的指令比使用大量单字节 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 相对于节的开头起作用,而不是最终可执行文件中地址空间的开头。例如,当您所在的部分只能保证与 4 字节边界对齐时,对齐到 16 字节边界是浪费精力。同样,NASM 不会检查该部分的对齐特性是否适合使用 ALIGN 或 ALIGNB。

我不确定代码段是获得一个绝对的 32 字节对齐地址还是只有一个相对地址。

4

2 回答 2

2

关于您关于相对(节内)对齐和绝对(在运行时内存中)的最后一个问题 - 您不必太担心。就在您引用的手册部分警告ALIGN不检查部分对齐的下方,您有这个:

ALIGN 和 ALIGNB 都隐式调用 SECTALIGN 宏。有关详细信息,请参阅第4.11.13节。

所以基本上ALIGN检查对齐是否合理,但它确实调用SECTALIGN宏以便对齐合理的。特别是,所有隐式SECTALIGN调用都应确保该部分与任何 align 调用指定的最大对齐方式对齐。

关于不检查的警告ALIGN可能只适用于更模糊的情况,例如,当组装成不支持节对齐的格式时,当指定的对齐大于节所支持的对齐时,或者当SECTALIGN OFF被调用禁用时SECTALIGN

于 2016-10-06T19:37:24.373 回答
0

理想情况下,您的循环应该(大约)在每个时钟周期执行一次迭代,有四个 mu-ops(add/jne 是一个)。一个关键问题是内循环分支的可预测性。应该在时序代码中预测最多 16 次迭代,始终相同,但之后您可能会遇到困难。首先,为了回答您的问题,时序的关键对齐是确保 jne .L2 之后的代码和 .L2 之后的第一条指令都不会跨越 32 字节边界。我认为真实的问题是如何让它运行得更快,如果我对 > 16 次迭代的猜测是正确的,那么关键目标是让分支预测工作。要使您的计时时间更短应该很容易 - 拥有多个可预测的分支就足够了。然而,要使最终代码运行得更快,取决于 rax 的实际值如何变化,这也取决于调用循环的例程。

于 2016-02-19T15:02:45.177 回答