汇编

时间:2015-10-30 14:40:31

标签: assembly x86 nasm yasm

我们说我有以下主循环

.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以上)。
  4. 我发现的另一件事是,如果我在源文件中添加另一个例程,即更改一条指令(例如删除指令),即使它们是独立函数,也会对下一个函数的时序产生重大影响。 I have even seen this in the past affect a routine in another object file.

    我已阅读第11.5节"代码对齐"在Agner Fog's optimizing assembly manual中,我仍然不清楚调整代码以测试性能的最佳方法。他给出了一个例子,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的有效频率。您可能想要禁用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
    

    这是我用来调用程序集例程并为它们计时的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 manual

    中的以下陈述感到困扰
      

    最后一点需要注意:ALIGN和ALIGNB相对于该部分的开头工作,而不是最终可执行文件中地址空间的开头。例如,当您所在的部分仅保证与4字节边界对齐时,对齐到16字节边界是浪费精力。同样,NASM不会检查该部分的对齐特征是否适合使用ALIGN或ALIGNB。

    我不确定代码段是获取绝对的32字节对齐地址还是仅获得相对的地址。

2 个答案:

答案 0 :(得分:1)

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

  

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

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

关于ALIGN未检查的警告可能仅适用于更加模糊的情况,例如,当汇编为不支持节对齐的格式时,指定的对齐方式大于节支持的对齐方式时,或者已调用SECTALIGN OFF来禁用SECTALIGN

答案 1 :(得分:0)

你的循环应理想地(几乎)每个时钟周期执行一次迭代,有4个mu-ops(add / jne为1)。一个关键问题是内环分支的可预测性。在定时代码中应该预测多达16次迭代,总是相同,但之后你可能会挣扎。首先,为了回答你的问题,时序的关键对齐是确保jne .L2之后的代码和.L2之后的第一条指令都不跨越32字节边界。我认为真正的问题是如何让它运行得更快,如果我猜测&gt; 16次迭代是正确的,关键目标是使分支预测工作。为了缩短你的计时时间应该很容易 - 有几个可预测的分支就足够了。但是,要使最终代码运行得更快,取决于rax的真实值如何变化,这也将取决于调用循环的例程。