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%)。コードの配置を選択する方法が明確ではありません。コードを揃える場所として考えられる場所が 3 つあります。

  1. 関数へのエントリで (たとえばtriad_fma_asm_repeat、以下のコードを参照)
  2. .L1メインループを繰り返す外側のループ(上記)の開始時
  3. 私のメインループの開始時(.L2上記)。

私が見つけたもう 1 つのことは、ソース ファイルに別のルーチンを配置した場合、1 つの命令を変更する (たとえば、命令を削除する) と、それらが独立した関数であっても、次の関数のタイミングに大きな影響を与える可能性があることです。過去に、これが別のオブジェクト ファイルのルーチンに影響を与えるのを見たことがあります。

Agner Fog の最適化アセンブリ マニュアルのセクション 11.5「コードのアライメント」を読みましたが、パフォーマンスをテストするためにコードをアライメントする最良の方法はまだ明確ではありません。彼は、私が実際には従わない内側のループのタイミングの例、11.5 を示しています。

現在、コードから最高のパフォーマンスを得ているのは、さまざまな値とアライメントの位置を推測するゲームです。

アライメントを選択するためのインテリジェントな方法があるかどうか知りたいですか? 内側と外側のループを揃える必要がありますか? 内側のループだけ?関数へのエントリも?短い NOP または長い NOP の使用は重要ですか?

主に Haswell に興味があり、次に SNB/IVB、そして Core2 に興味があります。


NASM と YASM の両方を試してみたところ、これが 1 つの大きな違いであることがわかりました。NASM は、YASM がマルチバイト NOP を挿入する 1 バイトの 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.6CPU の実効周波数に置き換えます。ターボを無効にすることもできます。

;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しませんが、アラインメントが適切になるようにマクロを呼び出します。特に、すべての暗黙的な呼び出しは、セクションが任意の align 呼び出しで指定された最大のアラインメントにアラインされることを保証する必要があります。SECTALIGNSECTALIGN

チェックしないことについての警告ALIGNは、おそらくよりあいまいな場合にのみ適用されます。たとえば、セクションの配置をサポートしない形式にアセンブルする場合、セクションでサポートされるよりも大きな配置を指定する場合、またはSECTALIGN OFFdisable が呼び出された場合などSECTALIGNです。

于 2016-10-06T19:37:24.373 に答える
0

あなたのループは理想的には (ほぼ) クロック サイクルごとに 1 回の繰り返しで実行され、4 つの mu-ops (add/jne は 1 つ) を持つ必要があります。重要な問題は、内側のループ分岐の予測可能性です。最大 16 回の反復までは、タイミング コードで予測される必要があり、常に同じですが、それ以降は苦戦する可能性があります。まず、あなたの質問に答えるために、タイミングの重要な調整は、jne .L2 の後のコードも .L2 の後の最初の命令も 32 バイト境界を越えないようにすることです。私は本当の問題は、それをより速く実行する方法です.16回以上の反復の私の推測が正しい場合、主な目的は分岐予測を機能させることです. タイミング時間を短縮するのは簡単です。すべて予測可能な分岐がいくつかあれば十分です。ただし、最終的なコードをより高速に実行できるかどうかは、rax の実際の値がどのように変化するかに依存し、これはループを呼び出すルーチンにも依存します。

于 2016-02-19T15:02:45.177 に答える