Ivy Bridgeシステム用にMSVC(Windows)とGCC(Linux)でコンパイルされたコードのパフォーマンスに大きな違いが見られます。コードは密行列乗算を行います。 GCCではピークフロップの70%、MSVCでは50%しか得られません。それらが両方とも次の3つの組み込み関数を変換する方法の違いを分離したと思う。
___m256 breg0 = _mm256_loadu_ps(&b[8*i])
_mm256_add_ps(_mm256_mul_ps(arge0,breg0), tmp0)
_
GCCはこれを行います
_vmovups ymm9, YMMWORD PTR [rax-256]
vmulps ymm9, ymm0, ymm9
vaddps ymm8, ymm8, ymm9
_
MSVCはこれを行います
_vmulps ymm1, ymm2, YMMWORD PTR [rax-256]
vaddps ymm3, ymm1, ymm3
_
これらの2つのソリューションがパフォーマンスに大きな違いをもたらすことができるかどうか、そしてなぜその理由を誰かに説明してもらえますか?
MSVCは1つ少ない命令を使用していますが、負荷をマルチに結び付けているため、より依存性が高くなっています(負荷を順不同にできない可能性があります)。 Ivy Bridgeは1つのクロックサイクルで1つのAVXロード、1つのAVXマルチ、および1つのAVXアドを実行できますが、これには各操作が独立している必要があります。
たぶん問題は他の場所にあるのでしょうか?以下の最も内側のループのGCCおよびMSVCの完全なアセンブリコードを見ることができます。ここでループのC++コードを確認できます ループを展開してIvy BridgeとHaswellで最大スループットを達成
g ++ -S -masm = intel matrix.cpp -O3 -mavx -fopenmp
_.L4:
vbroadcastss ymm0, DWORD PTR [rcx+rdx*4]
add rdx, 1
add rax, 256
vmovups ymm9, YMMWORD PTR [rax-256]
vmulps ymm9, ymm0, ymm9
vaddps ymm8, ymm8, ymm9
vmovups ymm9, YMMWORD PTR [rax-224]
vmulps ymm9, ymm0, ymm9
vaddps ymm7, ymm7, ymm9
vmovups ymm9, YMMWORD PTR [rax-192]
vmulps ymm9, ymm0, ymm9
vaddps ymm6, ymm6, ymm9
vmovups ymm9, YMMWORD PTR [rax-160]
vmulps ymm9, ymm0, ymm9
vaddps ymm5, ymm5, ymm9
vmovups ymm9, YMMWORD PTR [rax-128]
vmulps ymm9, ymm0, ymm9
vaddps ymm4, ymm4, ymm9
vmovups ymm9, YMMWORD PTR [rax-96]
vmulps ymm9, ymm0, ymm9
vaddps ymm3, ymm3, ymm9
vmovups ymm9, YMMWORD PTR [rax-64]
vmulps ymm9, ymm0, ymm9
vaddps ymm2, ymm2, ymm9
vmovups ymm9, YMMWORD PTR [rax-32]
cmp esi, edx
vmulps ymm0, ymm0, ymm9
vaddps ymm1, ymm1, ymm0
jg .L4
_
MSVC/FAc/O2/openmp/Arch:AVX ...
_vbroadcastss ymm2, DWORD PTR [r10]
lea rax, QWORD PTR [rax+256]
lea r10, QWORD PTR [r10+4]
vmulps ymm1, ymm2, YMMWORD PTR [rax-320]
vaddps ymm3, ymm1, ymm3
vmulps ymm1, ymm2, YMMWORD PTR [rax-288]
vaddps ymm4, ymm1, ymm4
vmulps ymm1, ymm2, YMMWORD PTR [rax-256]
vaddps ymm5, ymm1, ymm5
vmulps ymm1, ymm2, YMMWORD PTR [rax-224]
vaddps ymm6, ymm1, ymm6
vmulps ymm1, ymm2, YMMWORD PTR [rax-192]
vaddps ymm7, ymm1, ymm7
vmulps ymm1, ymm2, YMMWORD PTR [rax-160]
vaddps ymm8, ymm1, ymm8
vmulps ymm1, ymm2, YMMWORD PTR [rax-128]
vaddps ymm9, ymm1, ymm9
vmulps ymm1, ymm2, YMMWORD PTR [rax-96]
vaddps ymm10, ymm1, ymm10
dec rdx
jne SHORT $LL3@AddDot4x4_
_
編集:
合計浮動小数点演算を_2.0*n^3
_(nは正方行列の幅)として計算し、omp_get_wtime()
で測定した時間で除算することにより、コードをベンチマークします。ループを数回繰り返します。以下の出力では、100回繰り返しました。
すべてのコアのIntel Xeon E5 1620(Ivy Bridge)ターボ上のMSVC2012からの出力は3.7 GHzです
_maximum GFLOPS = 236.8 = (8-wide SIMD) * (1 AVX mult + 1 AVX add) * (4 cores) * 3.7 GHz
n 64, 0.02 ms, GFLOPs 0.001, GFLOPs/s 23.88, error 0.000e+000, efficiency/core 40.34%, efficiency 10.08%, mem 0.05 MB
n 128, 0.05 ms, GFLOPs 0.004, GFLOPs/s 84.54, error 0.000e+000, efficiency/core 142.81%, efficiency 35.70%, mem 0.19 MB
n 192, 0.17 ms, GFLOPs 0.014, GFLOPs/s 85.45, error 0.000e+000, efficiency/core 144.34%, efficiency 36.09%, mem 0.42 MB
n 256, 0.29 ms, GFLOPs 0.034, GFLOPs/s 114.48, error 0.000e+000, efficiency/core 193.37%, efficiency 48.34%, mem 0.75 MB
n 320, 0.59 ms, GFLOPs 0.066, GFLOPs/s 110.50, error 0.000e+000, efficiency/core 186.66%, efficiency 46.67%, mem 1.17 MB
n 384, 1.39 ms, GFLOPs 0.113, GFLOPs/s 81.39, error 0.000e+000, efficiency/core 137.48%, efficiency 34.37%, mem 1.69 MB
n 448, 3.27 ms, GFLOPs 0.180, GFLOPs/s 55.01, error 0.000e+000, efficiency/core 92.92%, efficiency 23.23%, mem 2.30 MB
n 512, 3.60 ms, GFLOPs 0.268, GFLOPs/s 74.63, error 0.000e+000, efficiency/core 126.07%, efficiency 31.52%, mem 3.00 MB
n 576, 3.93 ms, GFLOPs 0.382, GFLOPs/s 97.24, error 0.000e+000, efficiency/core 164.26%, efficiency 41.07%, mem 3.80 MB
n 640, 5.21 ms, GFLOPs 0.524, GFLOPs/s 100.60, error 0.000e+000, efficiency/core 169.93%, efficiency 42.48%, mem 4.69 MB
n 704, 6.73 ms, GFLOPs 0.698, GFLOPs/s 103.63, error 0.000e+000, efficiency/core 175.04%, efficiency 43.76%, mem 5.67 MB
n 768, 8.55 ms, GFLOPs 0.906, GFLOPs/s 105.95, error 0.000e+000, efficiency/core 178.98%, efficiency 44.74%, mem 6.75 MB
n 832, 10.89 ms, GFLOPs 1.152, GFLOPs/s 105.76, error 0.000e+000, efficiency/core 178.65%, efficiency 44.66%, mem 7.92 MB
n 896, 13.26 ms, GFLOPs 1.439, GFLOPs/s 108.48, error 0.000e+000, efficiency/core 183.25%, efficiency 45.81%, mem 9.19 MB
n 960, 16.36 ms, GFLOPs 1.769, GFLOPs/s 108.16, error 0.000e+000, efficiency/core 182.70%, efficiency 45.67%, mem 10.55 MB
n 1024, 17.74 ms, GFLOPs 2.147, GFLOPs/s 121.05, error 0.000e+000, efficiency/core 204.47%, efficiency 51.12%, mem 12.00 MB
_
アライメントの問題について説明したので、これは次のように思われます。 http://en.wikipedia.org/wiki/Out-of-order_execution
G ++はスタンドアロンのロード命令を発行するため、プロセッサは命令を並べ替えて、必要な次のデータをプリフェッチすると同時に、加算と乗算を行うことができます。 MSVCでmulにポインターをスローすると、loadとmulが同じ命令に結び付けられるため、命令の実行順序を変更しても何の助けにもなりません。
編集:すべてのドキュメントを備えたIntelのサーバーは今日、それほど怒っていないので、順不同の実行が答えの一部である理由についてのより多くの研究があります。
まず、MSVCバージョンの乗算命令がCPUの順序が乱れたエンジンで最適化できる個別のμ-opにデコードできるというコメントは完全に正しいようです。ここでの楽しい部分は、最新のマイクロコードシーケンサーがプログラム可能なため、実際の動作はハードウェアとファームウェアの両方に依存するということです。生成されたアセンブリの違いは、それぞれ異なる潜在的なボトルネックと戦おうとするGCCとMSVCにあるようです。 GCCバージョンは、順不同のエンジンに余裕を与えようとします(既に説明したように)。ただし、MSVCバージョンは最終的に「micro-op fusion」と呼ばれる機能を利用します。これは、μ-opの廃止の制限によるものです。パイプラインの終わりは、ティックごとに3 µ-opのみを終了できます。特定の場合、Micro-op Fusionは、2つの異なる実行ユニット(メモリ読み取りと算術)でmustを実行し、結び付ける2つのµ-opを取ります。ほとんどのパイプラインで単一のμ-opになります。融合µ-opは、実行単位の割り当ての直前に2つの実際のµ-opに分割されます。実行後、opsは再び融合され、1つとしてリタイアできます。
順不同エンジンは融合されたµ-opのみを認識するため、乗算から負荷opを引き離すことはできません。これにより、次のオペランドがバスに乗るのを待つ間にパイプラインがハングします。
すべてのリンク!!!: http://download-software.intel.com/sites/default/files/managed/71/2e/319433-017.pdf
http://www.agner.org/optimize/microarchitecture.pdf
http://www.agner.org/optimize/optimizing_Assembly.pdf
http://www.agner.org/optimize/instruction_tables.ods (注:Excelは、このスプレッドシートが部分的に破損しているか、さもなければ大ざっぱなものであると訴えているので、ご自身の責任で開いてください。悪意のあるものであり、残りの調査によると、Agner Fogは素晴らしいです。Excelの回復手順を選択した後、大量のすばらしいデータが見つかりました)
http://www.syncfusion.com/Content/downloads/ebook/Assembly_Language_Succinctly.pdf
大幅な編集:うわー、ここでの議論にいくつかの興味深い更新がありました。パイプラインのどの程度が実際にマイクロオプフュージョンの影響を受けるかについて間違えたと思います。ループ条件チェックの違いから予想よりもパフォーマンスが向上する可能性があります。この場合、未融合の命令により、GCCは比較とジャンプを最後のベクトルロードと算術ステップでインターリーブできます。
vmovups ymm9, YMMWORD PTR [rax-32]
cmp esi, edx
vmulps ymm0, ymm0, ymm9
vaddps ymm1, ymm1, ymm0
jg .L4
Visual StudioでGCCコードを使用すると、実際にパフォーマンスが向上することを確認できます。 LinuxのGCCオブジェクトファイルをVisual Studioで動作するように変換する でこれを行いました。 4つのコアすべてを使用すると、効率は50%から60%になりました(単一のコアでは60%から70%)。
Microsoftは、64ビットコードからインラインアセンブリを削除しました。また、 コードを変更せずに類似させることができないように、64ビットディセンブラーを破損しました ( ただし、32ビットバージョンは引き続き動作します =)。彼らは明らかに組み込み関数で十分だと思っていましたが、このケースが示すように、組み込み関数は間違っています。
たぶん、融合命令は別個の組み込み関数である必要がありますか?
しかし、最適でない組み込みコードを生成するのはマイクロソフトだけではありません。以下のコードを http://gcc.godbolt.org/ に入れると、Clang、ICC、およびGCCの機能を確認できます。 ICCはMSVCよりもさらに悪いパフォーマンスを示しました。vinsertf128
を使用していますが、その理由はわかりません。 Clangが何をしているのかはわかりませんが、GCCには異なる順序(より多くのコード)で近いようです。
これは、Agner Fogが「組み込み関数を使用することのデメリット」に関して、マニュアルで「 アセンブリ言語でのサブルーチンの最適化 」を書いた理由を説明しています。
コンパイラは、プログラマが意図したよりも効率の悪い方法でコードを変更または実装できます。コンパイラーによって生成されたコードを調べて、プログラマーが意図した方法で最適化されているかどうかを確認する必要がある場合があります。
これは、組み込み関数を使用する場合には期待はずれです。つまり、64ビットアセンブリコードを作成するか、プログラマが意図したとおりに組み込み関数を実装するコンパイラを見つける必要があります。この場合、GCCのみがそのように見えます(おそらくClang)。
#include <immintrin.h>
extern "C" void AddDot4x4_vec_block_8wide(const int n, const float *a, const float *b, float *c, const int stridea, const int strideb, const int stridec) {
const int vec_size = 8;
__m256 tmp0, tmp1, tmp2, tmp3, tmp4, tmp5, tmp6, tmp7;
tmp0 = _mm256_loadu_ps(&c[0*vec_size]);
tmp1 = _mm256_loadu_ps(&c[1*vec_size]);
tmp2 = _mm256_loadu_ps(&c[2*vec_size]);
tmp3 = _mm256_loadu_ps(&c[3*vec_size]);
tmp4 = _mm256_loadu_ps(&c[4*vec_size]);
tmp5 = _mm256_loadu_ps(&c[5*vec_size]);
tmp6 = _mm256_loadu_ps(&c[6*vec_size]);
tmp7 = _mm256_loadu_ps(&c[7*vec_size]);
for(int i=0; i<n; i++) {
__m256 areg0 = _mm256_set1_ps(a[i]);
__m256 breg0 = _mm256_loadu_ps(&b[vec_size*(8*i + 0)]);
tmp0 = _mm256_add_ps(_mm256_mul_ps(areg0,breg0), tmp0);
__m256 breg1 = _mm256_loadu_ps(&b[vec_size*(8*i + 1)]);
tmp1 = _mm256_add_ps(_mm256_mul_ps(areg0,breg1), tmp1);
__m256 breg2 = _mm256_loadu_ps(&b[vec_size*(8*i + 2)]);
tmp2 = _mm256_add_ps(_mm256_mul_ps(areg0,breg2), tmp2);
__m256 breg3 = _mm256_loadu_ps(&b[vec_size*(8*i + 3)]);
tmp3 = _mm256_add_ps(_mm256_mul_ps(areg0,breg3), tmp3);
__m256 breg4 = _mm256_loadu_ps(&b[vec_size*(8*i + 4)]);
tmp4 = _mm256_add_ps(_mm256_mul_ps(areg0,breg4), tmp4);
__m256 breg5 = _mm256_loadu_ps(&b[vec_size*(8*i + 5)]);
tmp5 = _mm256_add_ps(_mm256_mul_ps(areg0,breg5), tmp5);
__m256 breg6 = _mm256_loadu_ps(&b[vec_size*(8*i + 6)]);
tmp6 = _mm256_add_ps(_mm256_mul_ps(areg0,breg6), tmp6);
__m256 breg7 = _mm256_loadu_ps(&b[vec_size*(8*i + 7)]);
tmp7 = _mm256_add_ps(_mm256_mul_ps(areg0,breg7), tmp7);
}
_mm256_storeu_ps(&c[0*vec_size], tmp0);
_mm256_storeu_ps(&c[1*vec_size], tmp1);
_mm256_storeu_ps(&c[2*vec_size], tmp2);
_mm256_storeu_ps(&c[3*vec_size], tmp3);
_mm256_storeu_ps(&c[4*vec_size], tmp4);
_mm256_storeu_ps(&c[5*vec_size], tmp5);
_mm256_storeu_ps(&c[6*vec_size], tmp6);
_mm256_storeu_ps(&c[7*vec_size], tmp7);
}
MSVCは、あなたが要求したとおりのことを行いました。 vmovups
命令を発行する場合は、_mm256_loadu_ps
組み込み。