31

バックグラウンド

C++ で記述された数値計算ソフトウェアの次の重要なループは、基本的に 2 つのオブジェクトをそのメンバーの 1 つによって比較します。

for(int j=n;--j>0;)
    asd[j%16]=a.e<b.e;

aおよびbクラスは次のASDとおりです。

struct ASD  {
    float e;
    ...
};

この比較を軽量メンバー関数に入れることの影響を調査していました。

bool test(const ASD& y)const {
    return e<y.e;
}

そしてそれを次のように使用します:

for(int j=n;--j>0;)
    asd[j%16]=a.test(b);

コンパイラはこの関数をインライン化していますが、アセンブリ コードが異なり、10% を超えるランタイム オーバーヘッドが発生するという問題があります。私は質問する必要があります:

質問

  1. コンパイラが異なるアセンブリ コードを生成するのはなぜですか?

  2. 生成されたアセンブリが遅いのはなぜですか?

編集: 2 番目の質問は、@KamyarSouri の提案 (j%16) を実装することで回答されました。アセンブリ コードはほぼ同じに見えます ( http://pastebin.com/diff.php?i=yqXedtPmを参照)。唯一の違いは、18、33、48 行目です。

000646F9  movzx       edx,dl 

素材

このグラフは、私のコードの 50 回のテスト実行に対する FLOP/s (倍率まで) を示しています。

ここに画像の説明を入力

プロットを生成する gnuplot スクリプト: http://pastebin.com/8amNqya7

コンパイラ オプション:

/Zi /W3 /WX- /MP /Ox /Ob2 /Oi /Ot /Oy /GL /D "WIN32" /D "NDEBUG" /D "_CONSOLE" /D "_UNICODE" /D "UNICODE" /Gm- / EHsc /MT /GS- /Gy /arch:SSE2 /fp:正確 /Zc:wchar_t /Zc:forScope /Gd /analyze-

リンカー オプション: /INCREMENTAL:NO "kernel32.lib" "user32.lib" "gdi32.lib" "winspool.lib" "comdlg32.lib" "advapi32.lib" "shell32.lib" "ole32.lib" "oleaut32. lib" "uuid.lib" "odbc32.lib" "odbccp32.lib" /ALLOWISOLATION /MANIFESTUAC:"level='asInvoker' uiAccess='false'" /SUBSYSTEM:CONSOLE /OPT:REF /OPT:ICF /LTCG /TLBID :1 /DYNAMICBASE /NXCOMPAT /MACHINE:X86 /ERRORREPORT:QUEUE

4

2 に答える 2

32

簡潔な答え:

あなたのasd配列は次のように宣言されています:

int *asd=new int[16];

したがって、 代わりに配列型を に変更しintて、戻り値の型として使用します。bool.
bool

いずれにせよ、test関数の戻り値の型を配列の型と一致させてください。

詳細については、一番下までスキップしてください。

長い答え:

手動でインライン化されたバージョンでは、1 つの反復の「コア」は次のようになります。

xor         eax,eax  
 
mov         edx,ecx  
and         edx,0Fh  
mov         dword ptr [ebp+edx*4],eax  
mov         eax,dword ptr [esp+1Ch]  
movss       xmm0,dword ptr [eax]  
movss       xmm1,dword ptr [edi]  
cvtps2pd    xmm0,xmm0  
cvtps2pd    xmm1,xmm1  
comisd      xmm1,xmm0  

コンパイラのインライン バージョンは、最初の命令を除いて完全に同一です。

代わりに:

xor         eax,eax

それは持っています:

xor         eax,eax  
movzx       edx,al

さて、それは1 つの余分な命令です。どちらも同じことを行います-レジスタをゼロにします。これは私が見る唯一の違いです...

このmovzx命令は、すべての新しいアーキテクチャで 1 サイクルのレイテンシと0.33サイクルの相互スループットを備えています。ですから、これが 10% の違いをもたらすとは想像できません。

どちらの場合も、ゼロ化の結果は 3 命令後にのみ使用されます。したがって、これが実行のクリティカル パス上にある可能性は非常に高いです。


私はインテルのエンジニアではありませんが、私の推測は次のとおりです。

最新のプロセッサのほとんどは、レジスタの名前をゼロ レジスタのバンクに変更することで、ゼロ化操作 ( などxor eax,eax) を処理します。実行ユニットを完全にバイパスします。ただし、この特別な処理により、(部分的な) レジスタが 経由でアクセスされたときにパイプライン バブルが発生する可能性があります。movzx edi,al

さらに、コンパイラのインライン バージョンには、次のような誤った依存関係もあります。eax

movzx       edx,al  
mov         eax,ecx  //  False dependency on "eax".

順不同の実行がこれを解決できるかどうかは、私にはわかりません。


さて、これは基本的に、MSVC コンパイラのリバース エンジニアリングの問題に変わりつつあります...

ここでは、なぜ余分なmovzxものが生成されるのか、それがとどまるのかについて説明します。

ここで重要なのはbool戻り値です。どうやら、boolデータ型はおそらく MSVC 内部表現内に格納された 8 ビット値です。したがって、ここから暗黙的に変換すると、次のboolようになります。int

asd[j%16] = a.test(b);
^^^^^^^^^   ^^^^^^^^^
 type int   type bool

8 ビット -> 32 ビット整数昇格があります。movzxこれが、MSVC が命令を生成する理由です。

インライン展開が手動で行われる場合、コンパイラはこの変換を最適化するのに十分な情報を取得し、すべてを 32 ビット データ型 IR として保持します。

ただし、戻り値を持つ独自の関数にコードを挿入するとbool、コンパイラは 8 ビットの中間データ型を最適化できません。したがって、movzx滞在します。

両方のデータ型を同じ ( または のいずれintbool) にすると、変換は必要ありません。したがって、問題は完全に回避されます。

于 2011-12-21T01:54:31.370 に答える
1

lea esp,[esp]7バイトのi-cacheを占有し、ループ内にあります。他のいくつかの手がかりにより、コンパイラはこれがリリースビルドなのかデバッグビルドなのかわからないように見えます。

編集:

lea esp,[esp]ループに入っていません。周囲の指示の中での位置は私を誤解させました。これで、16バイトの境界で実際のループを開始するために、意図的に7バイトが無駄になり、その後に2バイトが無駄になっているように見えます。これは、Johennes Gererが観察したように、これが実際に物事をスピードアップすることを意味します。

ただし、これがデバッグビルドなのかリリースビルドなのか、コンパイラはまだ不明のようです。

別の編集:

ペーストビンの差分は、前に見たペーストビンの差分とは異なります。この回答は今すぐ削除できますが、すでにコメントがありますので、そのままにしておきます。

于 2011-12-21T01:36:14.390 に答える