xorps命令を追加すると、cvtsi2ssを使用してこの関数が作成され、追加が最大5倍速くなるのはなぜですか?

LRFLEW

Googleベンチマークを使用して関数を最適化することをいじっていたところ、特定の状況でコードが予期せず遅くなる状況に遭遇しました。私はそれを実験し始め、コンパイルされたアセンブリを見て、最終的に問題を示す最小限のテストケースを思いつきました。これが私が思いついた、この減速を示すアセンブリです。

    .text
test:
    #xorps  %xmm0, %xmm0
    cvtsi2ss    %edi, %xmm0
    addss   %xmm0, %xmm0
    addss   %xmm0, %xmm0
    addss   %xmm0, %xmm0
    addss   %xmm0, %xmm0
    addss   %xmm0, %xmm0
    addss   %xmm0, %xmm0
    addss   %xmm0, %xmm0
    addss   %xmm0, %xmm0
    retq
    .global test

この関数は、関数宣言に関するGCC / Clangのx86-64呼び出し規約に従いますextern "C" float test(int);コメントアウトされたxorps命令に注意してください。この命令のコメントを外すと、関数のパフォーマンスが劇的に向上します。i7-8700K、Googleのベンチマークが示す機能で自分のマシンを使用して、それをテストせずにxorps機能しながら、指示することは、8.54ns(CPU)をとるとのxorps命令は1.48nsをとります。さまざまなOS、プロセッサ、プロセッサ世代、およびさまざまなプロセッサメーカー(IntelとAMD)を搭載した複数のコンピュータでこれをテストしましたが、すべて同じようなパフォーマンスの違いがあります。を繰り返すaddss命令はスローダウンを(ある程度まで)より顕著にしますが、このスローダウンは、ここにある他の命令(たとえばmulss)を使用して、またはすべて%xmm0が何らかの方法で値に依存している限り、命令を組み合わせて使用しても発生します。xorps 関数呼び出しを呼び出すだけでパフォーマンスが向上することを指摘しておく価値があります。ループxorps外の呼び出しで(Googleベンチマークが行うように)ループでパフォーマンスをサンプリングすると、パフォーマンスが低下します。

これは、命令を排他的に追加するとパフォーマンスが向上する場合であるため、これはCPUの非常に低レベルの何かが原因であると思われます。これはさまざまなCPUで発生するため、これは意図的なものである必要があるようです。ただし、これが発生する理由を説明するドキュメントは見つかりませんでした。ここで何が起こっているのか説明がありますか?この問題は複雑な要因に依存しているようです。元のコードで見た速度低下は、インライン化せず、特定のコンパイラ(Clang)を使用せずに、特定の最適化レベル(-O2、場合によっては-O1、-Osではない)でのみ発生したためです。 、ただしGCCではありません)。

ピーター・コーデス

cvtsi2ss %edi, %xmm0floatをXMM0の下位要素にマージして、古い値に誤って依存するようにします。(同じ関数を繰り返し呼び出すと、ループで運ばれる長い依存関係チェーンが1つ作成されます。)

xor-zeroingは、depチェーンを切断し、異常なexecがその魔法を働かせることを可能にします。したがってaddss、レイテンシー(4サイクル)ではなくスループット(0.5サイクル)のボトルネックになります。

CPUはSkylakeの派生物であるため、これらは数値です。以前のIntelには、FMAユニットで実行する代わりに、専用のFP-add実行ユニットを使用して3サイクルのレイテンシ、1サイクルのスループットがあります。https://agner.org/optimize/おそらく、関数呼び出し/ retのオーバーヘッドによりaddss、パイプライン化されたFMAユニットの8つの飛行中のuopsのレイテンシー*帯域幅の積から期待される完全な8倍のスピードアップを見ることができませんxorps単一の関数内のループからdep-breakingを削除すると、そのスピードアップが得られるはずです


GCCは、誤った依存関係について非常に「注意」する傾向があり、万が一の場合に備えて、追加の命令(フロントエンド帯域幅)を費やして依存関係を解消します。フロントエンドでボトルネックが発生するコード(または合計コードサイズ/ uop-cacheフットプリントが要因となるコード)では、レジスターが実際に時間内に準備ができていれば、パフォーマンスが低下します。

Clang / LLVMは無謀で無謀であり、通常、現在の関数に書き込まれていないレジスタへの誤った依存関係を回避することを気にしません。(つまり、レジスタが関数エントリで「コールド」であると想定/ふりをします)。コメントに示されているように、clangは、同じ関数への複数の呼び出しではなく、1つの関数内でループするときにxor-zeroingによってループ実行のdepチェーンを作成することを回避します。

Clangは、コードサイズや命令を32ビットレジスタと比較して保存しない場合もありますが、理由もなく8ビットGP整数部分レジスタを使用します。通常は問題ないでしょうが、呼び出し元(または兄弟関数呼び出し)がまだそのregへのキャッシュミスの負荷を抱えている場合は、長いdepチェーンに結合したり、ループで運ばれる依存関係チェーンを作成したりするリスクがあります。たとえば、と呼ばれます。


OoO execが短から中程度の長さの独立したdepチェーンをオーバーラップする方法の詳細については、長さ増やすために、2つの長い依存関係チェーンを持つループに対するlfenceの影響理解するを参照してくださいまた、関連:Agnerの命令テーブルとは異なり、mulssがHaswellで3サイクルしかかからないのはなぜですか?(複数のアキュムレータを使用したFPループの展開は、FMAレイテンシを非表示にするために、複数のアキュムレータを使用してドット積を展開することです。

https://www.uops.info/html-instr/CVTSI2SS_XMM_R32.htmlには、さまざまなアーチにわたるこの命令のパフォーマンスの詳細があります。


AVXを使用できる場合は、これを回避できますvcvtsi2ss %edi, %xmm7, %xmm0(xmm7は、最近書き込んでいないレジスタ、またはEDIの現在の値につながるdepチェーンの初期のレジスタです)。

で述べたように、sqrtsd命令のレイテンシーが入力に基づいて変化するのなぜですか?Intelプロセッサ

このISA設計の疣贅は、IntelがPentiumIII上のSSE1で短期的に最適化したおかげです。P3は、128ビットレジスタを2つの64ビットハーフとして内部的に処理しました。上半分を変更せずに、スカラー命令を単一のuopにデコードします。(しかし、それでもPIIIsqrtssに誤った依存関係を与えます)。AVXは最終的にvsqrtsd %src,%src, %dst、少なくともメモリではない場合のレジスタソース、および同様vcvtsi2sd %eax, %cold_reg, %dstに近視眼的に設計されたスカラーint-> fp変換命令これを回避できます
(GCCは、レポートの最適化逃し:805868907180571を。)

cvtsi2ss/sdがレジスタの上位要素をゼロにした場合、このばかげた問題は発生しません/ xor-zeroing命令を振りかける必要はありません。Intelに感謝します。(別の戦略は、SSE2を使用することであるmovd %eax, %xmm0ないゼロ拡張し、全128ビットのベクトルで動作INT-> FP変換を充填した。INT-> FPスカラー変換が2つのuopであり、そしてここでも、フロートのために破ることができますベクトル戦略は1+ 1です。ただし、int-> fpパック変換のコストがシャッフル+ FP uopの場合は2倍にはなりません。)

これは、AMD64が32ビット整数レジスタへの書き込みを変更せずに(マージ)するのではなく、完全な64ビットレジスタに暗黙的にゼロ拡張することによって回避した問題です。32ビットレジスタのx86-64命令が、完全な64ビットレジスタの上部をゼロにするのはなぜですか?(8ビットおよび16ビットレジスタを書き込むと、AMD CPU、およびHaswell以降のIntelに誤った依存関係発生します)。

この記事はインターネットから収集されたものであり、転載の際にはソースを示してください。

侵害の場合は、連絡してください[email protected]

編集
0

コメントを追加

0

関連記事

Related 関連記事

ホットタグ

アーカイブ