から Ira Baxterの回答、INCおよびDEC命令がnotキャリーフラグ(CF)に影響するのはなぜですか?
ほとんどの場合、
INC
とDEC
は部分的な条件コードの更新を行うため、今は避けています。これにより、パイプラインストールがおかしくなり、ADD
/SUB
しないでください。したがって、問題にならない場合(ほとんどの場所)、ストールを回避するためにADD
/SUB
を使用します。INC
/DEC
を使用するのは、コードを小さく保つ場合のみです。たとえば、1つまたは2つの命令のサイズが十分に異なるキャッシュラインに適合させる場合などです。これはおそらく無意味なナノ[文字通り!]最適化ですが、私はコーディングの習慣がかなり古いです。
そして、addが発生しないのに、なぜパイプラインストールが発生する可能性があるのかを尋ねたいと思います。結局のところ、ADD
とINC
の両方がフラグレジスタを更新します。唯一の違いは、INC
がCF
を更新しないことです。しかし、なぜそれが重要なのでしょうか?
最近のCPUでは、add
はinc
より遅くなることはありません(間接的なコードサイズ/デコード効果を除く)が、通常は高速でもないため、inc
コードサイズの理由で。特に、この選択が同じバイナリで何度も繰り返される場合(たとえば、コンパイラライターの場合)。
inc
は、1バイト(64ビットモード)または2バイト(オペコード0x40..F inc r32
/dec r32
ショートフォームを32ビットモードで保存し、REXとして再利用します。 x86-64のプレフィックス)。これにより、合計コードサイズにわずかなパーセンテージの違いが生じます。これは、命令キャッシュのヒット率、iTLBのヒット率、およびディスクからロードする必要のあるページ数に役立ちます。
inc
の利点:
add
のより良いマイクロフュージョンを相殺する可能性があります。 ( マイクロアーチガイドのSandybridgeセクションにあるAgner Fogの表9.1を参照 。)パフォーマンスカウンターは、問題段階のuopsを簡単に測定できますが、uopキャッシュとuop-cache読み取りにどのようにパックされるかを測定するのは困難です。帯域幅の影響。inc
の後にCFを読み取ることができるCPUでの利点です。 (Nehalem以前ではありません。)最新のCPUには1つの例外があります。Silvermont/Goldmont/Knight's Landinginc
/dec
を1uopとして効率的にデコードしますが、allocate/rename(別名問題)ステージ。追加のuopは部分的なフラグをマージします。 inc
スループットはクロックあたりわずか1ですが、独立したadd r32, imm8
の場合は0.5c(または0.33c Goldmont)です。これは、フラグのマージuopsによって作成されたdepチェーンのためです。
P4とは異なり、レジスタの結果にはフラグのfalse-depがないため(以下を参照)、順不同の実行により、フラグの結果を何も使用しない場合に、レイテンシクリティカルパスからフラグがマージされます。 (しかし、OOOウィンドウはHaswellやRyzenのような主流のCPUよりもはるかに小さいです。)2つの独立したuopsとしてinc
を実行することは、おそらくほとんどの場合Silvermontにとって有利です。ほとんどのx86命令は、すべてのフラグを読み取らずに書き込み、これらのフラグ依存関係チェーンを壊します。
SMont/KNLには、デコードと割り当て/名前変更の間にキューがあります( Intelの最適化マニュアル、図16-2 を参照)。したがって、発行中に2 uopsに拡張すると、デコードストールからのバブルを埋めることができます(1オペランドなどの命令で) mul
またはpshufb
。デコーダーから複数のuopを生成し、マイクロコードで3〜7サイクルのストールを引き起こします。または、Silvermontでは、3つを超えるプレフィックス(エスケープバイトと必須プレフィックスを含む)を持つ命令だけです。 REX +任意のSSSE3またはSSE4命令。ただし、〜28 uopループバッファがあるため、小さなループはこれらのデコードストールの影響を受けないことに注意してください。
inc
/dec
は、1としてデコードされ、2として発行される唯一の命令です:Push
/pop
、call
/ret
、および3つのコンポーネントを含むlea
もこれを行います。 KNLのAVX512は命令を収集します。出典: Intelの最適化マニュアル 、17.1.2 Out-of-Order Engine(KNL)。スループットのペナルティはほんのわずかであり(他に大きなボトルネックがある場合でもそうでない場合もあります)、「一般的な」チューニングにinc
を使用することは一般的に問題ありません。
Intelの最適化マニュアルでは、部分フラグストールのリスクを回避するために、一般的にinc
よりもadd 1
を推奨しています。しかし、Intelのコンパイラはデフォルトではそれを行わないため、将来のCPUがP4のようにすべてのケースでinc
を遅くする可能性はそれほど高くありません。
Clang5.0およびIntelのICC17(Godbolt上) サイズだけでなく速度(-O3
)を最適化する場合は、inc
を使用してください。 -mtune=pentium4
はinc
/dec
を回避しますが、デフォルトの-mtune=generic
はP4にあまり重点を置きません。
ICC17 -xMIC-AVX512
(gccの-march=knl
と同等)はinc
を回避しますが、これはおそらくSilvermont/KNLにとって一般的には良い策です。ただし、通常はinc
を使用することはパフォーマンスの低下ではないため、ほとんどのコードでinc
/dec
を使用することは、「一般的な」チューニングに適しています。クリティカルパスの一部ではありません。
Silvermontを除いて、これはPentium4から残されたほとんど古い最適化アドバイスです。最近のCPUでは、anyフラグを書き込んだ最後のinsnによって書き込まれなかったフラグを実際に読み取った場合にのみ問題が発生します。 例:BigInteger adc
loops。 (その場合、CFを保持する必要があるため、add
を使用するとコードが破損します。)
add
は、すべての条件フラグビットをEFLAGSレジスタに書き込みます。レジスタリネーミングにより、書き込み専用がアウトオブオーダー実行で簡単になります。 書き込み後の書き込みおよび読み取り後の書き込みの危険性 を参照してください。 add eax, 1
とadd ecx, 1
は、互いに完全に独立しているため、並行して実行できます。 (Pentium4でさえ、条件フラグビットの名前を残りのEFLAGSとは別に変更します。これは、add
でさえ、割り込みが有効で、他の多くのビットが変更されないままになるためです。)
P4では、inc
およびdec
はすべてのフラグの以前の値に依存するため、互いに並行して実行したり、前のフラグ設定を実行したりすることはできません。指示。 (例:add eax, [mem]
/inc ecx
は、追加のロードがキャッシュで失敗した場合でも、inc
をadd
の後まで待機させます。)これはfalse依存関係。部分フラグ書き込みは、フラグの古い値を読み取り、CF以外のビットを更新してから、完全なフラグを書き込むことによって機能します。
他のすべての異常なx86CPU(AMDを含む)は、フラグのさまざまな部分の名前を個別に変更するため、内部的にはCFを除くすべてのフラグに対して書き込み専用の更新を行います。 (ソース: Agner Fogのマイクロアーキテクチャガイド )。 adc
やcmc
のように、実際にフラグを読み取ってから書き込む命令はごくわずかです。しかし、shl r, cl
(以下を参照)も。
少なくともIntelP6/SnBuarchファミリの場合はadd dest, 1
がinc dest
よりも望ましい場合:
Memory-destination:add [rdi], 1
can micro-Fuse the store and the load + add on Intel Core2 and SnB-family したがって、2つの融合ドメインuops/4つの非融合ドメインuops。inc [rdi]
は店舗をマイクロフューズすることしかできないため、3F/4Uです。
Agner Fogの表によると、AMDとSilvermontはmemory-dest inc
とadd
を単一のマクロ-op/uopと同じように実行します。
ただし、add [label], 1
によるuop-cache効果には注意してください。同じuopには32ビットアドレスと8ビットイミディエートが必要です。
変数カウントシフト/ローテーションの前フラグへの依存関係を解消し、フラグの部分的なマージを回避するには:shl reg, cl
は、不幸なCISC履歴のため、フラグへの入力依存関係があります: シフトカウントが0の場合、変更しないでおく必要があります 。
Intel SnBファミリでは、可変カウントシフトは3 uopsです(Core2/Nehalemの1から増加)。 AFAICT、2つのuops読み取り/書き込みフラグ、および独立したuopはreg
とcl
を読み取り、reg
を書き込みます。これは、レイテンシ(1c +不可避のリソースの競合)がスループット(1.5c)よりもよく、フラグへの依存を壊す命令と混合した場合にのみ最大スループットを達成できるという奇妙なケースです。 ( 私はこれについてもっと投稿しました アグナーフォグのフォーラムに)。可能な場合はBMI2shlx
を使用します。これは1uopで、カウントは任意のレジスタに含めることができます。
とにかく、変数カウントinc
の前にCF
(フラグを書き込むが、shl
は変更しない)は、最後にCFを書き込んだものに誤った依存関係を残し、SnB/IvBがフラグをマージするための追加のUOP。
Core2/Nehalemは、フラグの誤った依存さえも回避します。Meromは、クロックごとにほぼ2シフトで6つの独立したshl reg,cl
命令のループを実行し、cl = 0またはcl = 13で同じパフォーマンスを発揮します。クロックごとに1を超えるものは、フラグへの入力依存性がないことを証明します。
shl edx, 2
とshl edx, 0
(即時カウントシフト)でループを試しましたが、Core2、HSW、またはでdec
とsub
の間に速度の違いは見られませんでしたSKL。 AMDについては知りません。
更新:Intel P6ファミリでの優れたシフトパフォーマンスには、回避する必要のある大きなパフォーマンスの甌穴が犠牲になります。命令がフラグに依存する場合-シフト命令の結果:フロントエンドは、命令がretiredになるまで停止します。(出典: Intelの最適化マニュアル、(セクション3.5.2.6:部分フラグ)ストールを登録) )。したがって、shr eax, 2
/jnz
は、Sandybridge以前のIntelでのパフォーマンスにとってかなり壊滅的だと思います。 Nehalem以前が気になる場合は、shr eax, 2
/test eax,eax
/jnz
を使用してください。 Intelの例では、これがcount = cl
だけでなく、即時カウントシフトにも当てはまることを明確にしています。
Intel Coreマイクロアーキテクチャー(これはCore 2以降を意味します)に基づくプロセッサーでは、即時シフト1は、部分的なフラグストールが発生しないように特別なハードウェアによって処理されます。
Intelは、実際には、暗黙の1
によってシフトする即時のない特別なオペコードを意味します。短いエンコード(元の8086オペコード shr eax,1
を使用)で書き込み専用(部分的)を生成するため、D1 /5
の2つのエンコード方法にはパフォーマンスの違いがあると思いますフラグが生成されますが、長いエンコーディング(C1 /5, imm8
と即時1
)は、実行時まで0の即時チェックが行われませんが、異常なメカニズムでフラグ出力を追跡しません。
ビットをループすることは一般的ですが、2番目のビットごと(または他のストライド)をループすることは非常に一般的ではないため、これは妥当な設計上の選択のようです。これは、コンパイラがtest
からのフラグ結果を直接使用するのではなく、シフトの結果をshr
することを好む理由を説明しています。
更新:SnBファミリの変数カウントシフトについて、Intelの最適化マニュアルには次のように記載されています。
.5.1.6可変ビットカウントのローテーションとシフト
Intelマイクロアーキテクチャコード名Sandy Bridgeでは、「ROL/ROR/SHL/SHR reg、cl」命令には3つのマイクロopがあります。 フラグ結果が不要な場合、これらのマイクロオペレーションの1つが破棄され、多くの一般的な使用法でパフォーマンスが向上します。これらの命令が後で使用される部分的なフラグの結果を更新する場合、3つのマイクロオペレーションフロー全体が実行およびリタイアパイプラインを通過する必要があり、パフォーマンスが低下します。 Intelマイクロアーキテクチャコード名Ivy Bridgeでは、更新された部分フラグの結果を使用するために3つのマイクロオペレーションフロー全体を実行すると、さらに遅延が発生します。
以下のループシーケンスについて考えてみます。
loop: shl eax, cl add ebx, eax dec edx ; DEC does not update carry, causing SHL to execute slower three micro-ops flow jnz loop
DEC命令はキャリーフラグを変更しません。したがって、SHL EAX、CL命令は、後続の反復で3つのマイクロオペレーションフローを実行する必要があります。 SUB命令は、すべてのフラグを更新します。したがって、
DEC
をSUB
に置き換えると、SHL EAX, CL
が2つのマイクロオペレーションフローを実行できるようになります。
フラグの読み取り時に部分フラグのストールが発生します発生したとしても。 P4は、マージする必要がないため、部分フラグのストールが発生することはありません。代わりに、誤った依存関係があります。
いくつかの回答/コメントが用語を混同しています。それらは誤った依存関係を説明しますが、それを部分フラグストールと呼びます。これは一部のフラグのみを書き込んだために発生するスローダウンですが、「partial-flag stall」という用語は、部分的なフラグの書き込みをマージする必要がある場合に、SnB以前のIntelハードウェアで発生するものです。 Intel SnBファミリCPUは、ストールせずにフラグをマージするために追加のuopを挿入します。ネハレム以前は約7サイクル失速しました。 AMDCPUのペナルティがどれほど大きいかはわかりません。
(部分レジスターのペナルティは必ずしも部分フラグと同じではないことに注意してください。以下を参照してください)。
### Partial flag stall on Intel P6-family CPUs:
bigint_loop:
adc eax, [array_end + rcx*4] # partial-flag stall when adc reads CF
inc rcx # rcx counts up from negative values towards zero
# test rcx,rcx # eliminate partial-flag stalls by writing all flags, or better use add rcx,1
jnz
# this loop doesn't do anything useful; it's not normally useful to loop the carry-out back to the carry-in for the same accumulator.
# Note that `test` will change the input to the next adc, and so would replacing inc with add 1
他の場合、例えば部分的なフラグ書き込みとそれに続く完全なフラグ書き込み、またはinc
によって書き込まれたフラグのみの読み取りは問題ありません。 SnBファミリCPUでは、 inc/dec
は、add/sub
と同じjcc
とマクロ融合することもできます。
P4の後、Intelは、深刻なボトルネックを回避するために、-mtune=pentium4
を使用して再コンパイルしたり、手書きのasmを変更したりすることをほとんど諦めました。 (特定のマイクロアーキテクチャの調整は常に重要ですが、P4は以前のCPUで高速だった多くの機能を廃止するのは珍しいことでした、したがって既存のバイナリでは一般的でした。)P4は人々にx86のRISCのようなサブセットを使用し、JCC命令のプレフィックスとして分岐予測ヒントもありました。 (トレースキャッシュが十分ではなかったり、デコーダーが弱くてトレースキャッシュミスのパフォーマンスが低下したりするなど、他にも深刻な問題がありました。非常に高いクロッキングの哲学全体が電力密度の壁にぶつかったことは言うまでもありません。 。)
IntelがP4(netburst uarch)を放棄したとき、以前のP6ファミリCPU(PProからPIII)から部分フラグ/部分レジスタ処理を継承したP6ファミリデザイン(Pentium-M/Core2/Nehalem)に戻りました。ネットバーストのミスステップの日付。 (P4のすべてが本質的に悪いわけではなく、一部のアイデアはSandybridgeに再登場しましたが、全体的なNetBurstは広く間違いと見なされています。)一部の非常にCISCの命令は、マルチ命令の代替案よりもまだ低速です。 enter
、 loop
、またはbt [mem], reg
(regの値は使用されるメモリアドレスに影響するため)が、これらはすべて古いCPUでは低速でした。コンパイラはすでにそれらを避けています。
Pentium-Mは、部分登録のハードウェアサポートをさらに改善しました(マージペナルティが低くなりました)。 Sandybridgeでは、Intelはパーシャルフラグとパーシャルregの名前変更を維持し、マージが必要な場合にそれをはるかに効率的にしました(失速なしまたは最小限のストールで挿入されたuopのマージ)。 SnBは内部で大きな変更を加え、Nehalemから多くのアイデアを継承し、P4からいくつかのアイデアを継承しているにもかかわらず、新しいuarchファミリーと見なされています。 (ただし、SnBのデコードされたuopキャッシュはトレースキャッシュではないではないので、netburstのトレースキャッシュが解決しようとしたデコーダーのスループット/電力の問題に対する非常に異なるソリューションです。 )
たとえば、inc al
とinc ah
はP6/SnBファミリCPUで並行して実行できますが、eax
を後で読み取るにはマージが必要です。
PPro/PIIIは、完全なregを読み取るときに5〜6サイクル停止します。 Core2/Nehalemは、部分的なregのマージuopを挿入している間、2〜3サイクルだけストールしますが、部分的なフラグはさらに長いストールです。
SnBは、フラグの場合のように、ストールせずにマージuopを挿入します。 Intelの最適化ガイドによると、AH/BH/CH/DHをより広いregにマージする場合、マージするuopを挿入するには、他のuopを割り当てることができない発行/名前変更サイクル全体が必要です。しかし、low8/low16の場合、マージされたuopは「フローの一部」であるため、問題/名前変更サイクルで4つのスロットの1つを占有する以外に、フロントエンドのスループットに追加のペナルティを引き起こさないようです。
IvyBridge(または少なくともHaswell)では、Intelはlow8およびlow16レジスタの部分レジスタの名前変更を削除し、high8レジスタ(AH/BH/CH/DH)のみに変更しました。 high8レジスタの読み取りには、余分なレイテンシがあります。また、setcc al
は、Nehalem以前(およびおそらくSandybridge)とは異なり、raxの古い値に誤って依存しています。詳細については、 このHSW/SKL部分レジスタパフォーマンスQ&A を参照してください。
(HaswellはUOPなしでAHをマージできると以前に主張しましたが、それは真実ではなく、Agner Fogのガイドが言っていることでもありません。私はあまりにも速くスキミングし、残念ながら多くのコメントや他の投稿で間違った理解を繰り返しました。)
AMD CPUとIntel Silvermontは、部分的なreg(フラグ以外)の名前を変更しないため、mov al, [mem]
は、eaxの古い値に誤って依存しています。 (利点は、後で完全なregを読み取るときに、部分的なregのマージの速度低下がないことです。)
通常、add
の代わりにinc
がAMDまたは主流のIntelでコードを高速化するのは、コードが実際にinc
。つまり通常add
は、コードが壊れる場合にのみ役立ちますですが、上記のshl
のケースに注意してください。それについては、それは誤った依存関係です。
あなたがdoを実際にCFを変更しないままにしたい場合、SnBファミリー以前のCPUは部分フラグストールに関して深刻な問題を抱えていますが、SnBファミリーではCPUに部分的にマージさせるオーバーヘッドがありますフラグは非常に低いため、これらのCPUをターゲットにする場合は、ループ条件の一部としてinc
またはdec
を使用し続け、ある程度展開することをお勧めします。 (詳細については、前にリンクしたBigInteger adc
Q&Aを参照してください)。結果を分岐する必要がない場合は、lea
を使用して、フラグにまったく影響を与えずに算術演算を実行すると便利です。
命令のCPU実装によっては、レジスタの部分的な更新によってストールが発生する場合があります。 Agner Fogの最適化ガイド、62ページ によると、
歴史的な理由により、
INC
およびDEC
命令はキャリーフラグを変更せずに残し、他の算術フラグは書き込まれます。これにより、フラグの以前の値に誤って依存し、追加のμopが発生します。これらの問題を回避するには、ADD
とSUB
の代わりに常にINC
とDEC
を使用することをお勧めします。たとえば、INC EAX
はADD EAX,1
に置き換える必要があります。
「部分フラグストール」の83ページおよび「部分フラグストール」の100ページも参照してください。