web-dev-qa-db-ja.com

高度に最適化された行列乗算コードのMSVCとGCCのパフォーマンスの違い

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
_
31
Z boson

アライメントの問題について説明したので、これは次のように思われます。 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.intel.com/content/dam/www/public/us/en/documents/manuals/64-ia-32-architectures-optimization-manual.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://cs.nyu.edu/courses/fall13/CSCI-GA.3033-008/Microprocessor-Report-Sandy-Bridge-Spans-Generations-243901.pdf

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
21
iwolf

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);
}
6
Z boson

MSVCは、あなたが要求したとおりのことを行いました。 vmovups命令を発行する場合は、_mm256_loadu_ps組み込み。

3
Ben Voigt