「ねえ、静かにしてください!」 データを揃える









最新のコンパイラでは、ループをベクトル化するタスクは非常に重要で必要です。 ほとんどの場合、ベクトル化が成功すると、アプリケーションのパフォーマンスが大幅に向上します。 これを実現するには多くの方法があり、アプリケーションの予想される「加速」を取得することに関して、さらに多くの微妙な点があります。



今日は、データのアライメント、パフォーマンスとベクトル化への影響、特にコンパイラでのデータの取り扱いについて説明します。 この記事では、他の多くのニュアンスと同様に、非常に詳細な概念を示します。 しかし、ベクトル化中のアライメントの効果に興味があります。 したがって、記事を読んだり、メモリの処理方法を知っているだけであれば、データがブロック単位で読み取られるというニュースは驚くことではありません。



配列要素(および要素だけでなく)を操作する場合、実際には常にサイズが64バイトのキャッシュラインを操作します。 SSEおよびAVXベクトルは、それぞれ16バイトおよび32バイトで位置合わせされている場合、常に同じキャッシュラインに分類されます。 しかし、データが整列されていない場合は、別の「追加の」キャッシュラインをロードする必要があります。 このプロセスはパフォーマンスに重大な影響を与えます。アレイの要素、したがってメモリを一貫性なく扱う場合、すべてがさらに悪化する可能性があります。

さらに、命令自体にも、整列または不均等なデータアクセスがある場合があります。 命令に文字u非境界整列 )が表示される場合、ほとんどの場合、 vmovupdなどの不均等な読み取りと書き込みの命令です。 Nehalemアーキテクチャから始めて、これらの命令の速度は、データが均一であれば、整列された命令と同等になったことに注意する価値があります。 古いバージョンでは、そうではありません。



コンパイラーは、パフォーマンスのための闘争において積極的に私たちを助けることができます。 たとえば、128ビットの不均衡な負荷を2つの64ビットの負荷に分割しようとする場合があります。 コンパイラーが実装できるもう1つの優れたソリューションは、アラインされた場合とアラインされていない場合の両方に対して異なるバージョンを生成することです。 実行時に、どのような種類のデータがあるかが決定され、必要なバージョンに従って実行が進行します。 問題は1つだけです-このようなチェックのオーバーヘッドが高すぎる可能性があり、コンパイラはこの考えを放棄します。 コンパイラーがデータを調整できる場合はさらに良いでしょう。 ところで、ベクトル化中にデータがアライメントされていない場合、またはコンパイラがイコライゼーションについて何も知らない場合、元のループは3つの部分に分割されます。



したがって、速度の低下により、開始アドレスの均一性を実現できます。ループのメインコアに「踏みつけ」て、一定の反復回数を実行する必要があります。 しかし、これはデータを調整し、コンパイラーに伝えることで回避できます。



開発者は、「必要に応じて」データを整列するルールにする必要があります。SSEの場合は16バイト、AVXの場合は32バイト、MICおよびAVX-512の場合は64バイトです。 これをどのように行うことができますか?



C / C ++でアライメントされたメモリを割り当てるために、ヒープは次の関数を使用します。



void* _mm_malloc(int size, int base)
      
      





Linuxには次の機能があります。



 int posix_memaligned(void **p, size_t base, size_t size)
      
      





スタック上の変数には、 __ declspec属性が使用されます。



 __declspec(align(base)) <var>
      
      





またはLinux固有:



 <var> __attribute__((aligned(base)))
      
      





問題は、 __ declspecが gccに認識されていないため、移植性に問題がある可能性があるため、プリプロセッサを使用する必要があることです。



 #ifdef __GNUC__ #define _ALIGN(N) __attribute__((aligned(N))) #else #define _ALIGN(N) __declspec(align(N)) #endif _ALIGN(16) int foo[4];
      
      





興味深いのは、IntelのFortranコンパイラ(バージョン13.0以降)に特別なオプション-alignがあり、それを使用して(宣言時に)データを整列させることができることです。 たとえば、 -align array32byteを使用して、すべての配列が32バイトで整列されることをコンパイラーに通知します。 ディレクティブがあります:



  !DIR$ ATTRIBUTES ALIGN: base :: variable
      
      





今、指示自体について。 位置合わせされていないデータを操作する場合、位置合わせされていない読み取りおよび書き込み命令は、SandyBridge以降でのベクトルSSE操作を除き、非常に遅くなります。 そこでは、多くの条件に従って、同等のアクセス権を持つ命令と同じくらい高速になります。 位置合わせされていないデータを操作するための位置合わせされていないAVXベクトル命令は、最新世代のプロセッサであっても、位置合わせされたデータを操作する同様の命令よりも低速です。



同時に、コンパイラーは、AVXの非境界整列命令を生成することを好みます。境界整列データの場合、それらは同じように高速に動作し、データが境界整列されていない場合、実行が遅くなりますが、実行されるためです。 整列された命令が生成され、データが整列されていない場合、すべてが該当します。



プラグマベクトルunaligned / alignedディレクティブを使用して、使用する命令セットをコンパイラに指示できます。



たとえば、次のコードを検討してください。



 void mult(double* a, double* b, double* c) { int i; #pragma vector unaligned for (i = 0; i < N; i++) c[i] = a[i] * b[i]; }
      
      





そのため、AVX命令を使用すると、次のアセンブラコードが取得されます。



 ..B2.2: vmovupd (%rdi,%rax,8), %xmm0 vmovupd (%rsi,%rax,8), %xmm1 vinsertf128 $1, 16(%rsi,%rax,8), %ymm1, %ymm3 vinsertf128 $1, 16(%rdi,%rax,8), %ymm0, %ymm2 vmulpd %ymm3, %ymm2, %ymm4 vmovupd %xmm4, (%rdx,%rax,8) vextractf128 $1, %ymm4, 16(%rdx,%rax,8) addq $4, %rax cmpq $1000000, %rax jb ..B2.2
      
      





この場合、ディレクティブを使用したため、同じピールループが存在しないことに注意してください。

unalignedalignに置き換え、データがアライメントされ、対応するアライメントされた命令を安全に生成できることをコンパイラに保証すると、次のようになります。



 ..B2.2: vmovupd (%rdi,%rax,8), %ymm0 vmulpd (%rsi,%rax,8), %ymm0, %ymm1 vmovntpd %ymm1, (%rdx,%rax,8) addq $4, %rax cmpq $1000000, %rax jb ..B2.2
      
      





後者の場合は、 ab 、およびcが揃っていると高速に動作ます。 そうでなければ、すべてが悪いでしょう。 最初のケースでは、コンパイラがvmovntpdを使用できず、追加の命令vextractf128が登場したという事実のため、データが整列していれば、少し遅い実装になります。



もう1つの重要なポイントは、開始アドレスの均等性と相対配置の概念です。 次の例を考えてみましょう。



 void matvec(double a[][COLWIDTH], double b[], double c[]) { int i, j; for(i = 0; i < size1; i++) { b[i] = 0; #pragma vector aligned for(j = 0; j < size2; j++) b[i] += a[i][j] * c[j]; } }
      
      





ここでの唯一の質問は、 ab 、およびcが 16バイトでアライメントされている場合、指定されたコードが機能するかどうかであり、SSEを使用してコードを収集しますか? 答えはCOLWIDTHの値によって異なります。 奇数の長さの場合(SSEレジスタの長さ/サイズdouble = 2、次にCOLWIDTHを2で割る必要があります)、アプリケーションは実行を予想よりはるかに早く終了します(配列の最初の行を通過した後)。 その理由は、2行目の最初のデータ項目が位置合わせされていないためです。 そのような場合は、各行の末尾にダミー要素(「穴」)を追加して、新しい行を整列させ、いわゆるパディングを行う必要があります。 この場合、ベクトル命令のセットと使用するデータのタイプに応じて、COLWIDTHでこれを行うことができます。 既に述べたように、SSEの場合、これは偶数である必要があり、AVXの場合、4で割る必要があります。

開始アドレスのみが整列していることがわかっている場合は、属性を介してこの情報をコンパイラに提供できます。



 __assume_aligned(<array>, base)
      
      





Fortranのアナログ:

 !DIR$ ASSUME_ALIGNED address1:base [, address2:base] ...
      
      





Haswellでマトリックス乗算の簡単な例を試し、コードのディレクティブに応じて、アプリケーションの速度をWindowsのAVX命令と比較しました。



  for (j = 0;j < size2; j++) { b[i] += a[i][j] * x[j];
      
      





32バイトで整列されたデータ:

 _declspec(align(32)) FTYPE a[ROW][COLWIDTH]; _declspec(align(32)) FTYPE b[ROW]; _declspec(align(32)) FTYPE x[COLWIDTH];
      
      





この例は、Intelのコンパイラへのサンプルと共に使用され、すべてのコードがそこにあります。 したがって、ループの前にpragma vetor alignディレクティブを使用すると、ループの実行時間は2.531秒でした。 存在しない場合、3.466に増加し、剥離サイクルが現れました。 おそらく、コンパイラーはアライメントされたデータを知らなかったでしょう。 mP2OPT_vec_alignment = 6を使用て生成を無効にすることにより、サイクルはほぼ4秒間実行されました。 興味深いことに、このような例では、コンパイラが実行時データ検証を永続的に生成し、いくつかのループオプションを実行したため、コンパイラの「トリック」は非常に困難でした。その結果、非境界整列データの処理速度がわずかに低下しました。



一番下の行は、データを整列するときに、特にパフォーマンスに関する潜在的な問題をほとんど常に取り除くことです。 しかし、データをそれ自体で整列させるだけでは十分ではありません。知っていることをコンパイラーに通知する必要があります。そうすれば、最も効率的なアプリケーションを出力として取得できます。 主なことは、小さなトリックを忘れないことです!



All Articles