次のメインループがあるとしましょう
.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 つあります。
- 関数へのエントリで (たとえば
triad_fma_asm_repeat
、以下のコードを参照) .L1
メインループを繰り返す外側のループ(上記)の開始時- 私のメインループの開始時(
.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.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 バイトにアラインされた絶対アドレスを取得しているか、相対アドレスのみを取得しているかはわかりません。