私は何も最適化したくありません、私は誓います、私は好奇心からこの質問をしたいだけです。ほとんどのハードウェアには、単一のコマンドであるビットシフトのアセンブリコマンド(例:shl
、shr
)があることを知っています。ただし、シフトするビット数は重要ですか(ナノ秒単位、またはCPUタクト単位)。言い換えれば、次のいずれかがどのCPUでも高速ですか?
x << 1;
そして
x << 10;
そして、この質問で私を憎まないでください。 :)
CPUに依存する可能性があります。
ただし、最新のCPU(x86、ARM)はすべて、「バレルシフタ」を使用しています。これは、一定時間で任意のシフトを実行するように特別に設計されたハードウェアモジュールです。
つまり、肝心なのは...いいえ。変わりはない。
一部の組み込みプロセッサには、「1つシフト」命令しかありません。このようなプロセッサでは、コンパイラはx << 3
を((x << 1) << 1) << 1
に変更します。
Motorola MC68HCxxは、この制限がある最も人気のあるファミリの1つだったと思います。幸いなことに、このようなアーキテクチャは現在では非常にまれであり、ほとんどの場合、シフトサイズが可変のバレルシフタが含まれています。
多くの最新の派生物を備えたIntel8051も、任意のビット数をシフトすることはできません。
これには多くの場合があります。
多くの高速MPUには、バレルシフタ、マルチプレクサのような電子回路があり、一定時間でシフトを実行します。
MPUのビットシフトが1つしかない場合、x << 10
は通常、10シフトまたは2シフトのバイトコピーで実行されるため、遅くなります。
しかし、x << 10
がx << 1
よりも速いであるという一般的なケースが知られています。 xが16ビットの場合、下位6ビットのみが考慮されます(他はすべてシフトアウトされます)。したがって、MPUは下位バイトのみをロードする必要があるため、x << 10
の間、8ビットメモリへのアクセスサイクルは1回だけです。 2つのアクセスサイクルが必要です。アクセスサイクルがシフトより遅い(そして下位バイトをクリアする)場合、x << 10
は速くなります。これは、低速の外部データRAMにアクセスしているときに高速のオンボードプログラムROMを備えたマイクロコントローラに適用される場合があります。
ケース3に加えて、コンパイラはx << 10
の有効ビット数を考慮し、16x16乗算を16x8乗算に置き換えるなど(下位バイトは常にゼロであるため)、さらに操作を低幅のビットに最適化する場合があります。
一部のマイクロコントローラーには左シフト命令がまったくなく、代わりにadd x,x
を使用することに注意してください。
ARMでは、これは別の命令の副作用として実行できます。したがって、潜在的には、どちらにも遅延はまったくありません。
これが 私のお気に入りのCP で、x<<2
はx<<1
の2倍の時間がかかります:)
8ビットプロセッサでは、x<<1
は実際には16ビット値のx<<10
よりもはるかに遅いである可能性があると考えられます。
たとえば、x<<1
の妥当な翻訳は次のようになります。
byte1 = (byte1 << 1) | (byte2 >> 7)
byte2 = (byte2 << 1)
一方、x<<10
はもっと単純です:
byte1 = (byte2 << 2)
byte2 = 0
x<<1
がx<<10
よりも頻繁に、さらに遠くにシフトすることに注目してください。さらに、x<<10
の結果はbyte1の内容に依存しません。これにより、操作がさらに高速化される可能性があります。
それはCPUとコンパイラの両方に依存します。基盤となるCPUにバレルシフタを使用した任意のビットシフトがある場合でも、これはコンパイラがそのリソースを利用する場合にのみ発生します。
データのビット単位の幅の外側に何かをシフトすることは、CおよびC++では「未定義の動作」であることに注意してください。署名されたデータの右シフトも「実装定義」です。速度についてあまり心配するのではなく、異なる実装で同じ答えが得られることを心配してください。
ANSI Cセクション3.3.7からの引用:
3.3.7ビット単位のシフト演算子
構文
shift-expression: additive-expression shift-expression << additive-expression shift-expression >> additive-expression
制約
各オペランドは整数型でなければなりません。
セマンティクス
積分昇格は、各オペランドで実行されます。結果のタイプは、プロモートされた左オペランドのタイプです。右のオペランドの値が負であるか、プロモートされた左のオペランドのビット単位の幅以上の場合、動作は未定義です。
E1 << E2の結果は、E1の左シフトE2ビット位置です。空のビットはゼロで埋められます。 E1がunsignedタイプの場合、結果の値はE1に数量を掛けたものです。2はE2の累乗で、E1のタイプがunsigned longの場合はULONG_MAX + 1を法として、それ以外の場合はUINT_MAX +1になります。 (定数ULONG_MAXおよびUINT_MAXはヘッダーで定義されています。)
E1 >> E2の結果は、E1が右シフトされたE2ビット位置です。 E1に符号なしの型がある場合、またはE1に符号付きの型と非負の値がある場合、結果の値は、E1の商を数量で割った値であり、2の累乗はE2です。 E1に符号付きタイプと負の値がある場合、結果の値は実装定義です。
そう:
x = y << z;
"<<":y×2z (未定義オーバーフローが発生した場合);
x = y >> z;
">>":実装-符号付きで定義(ほとんどの場合、算術シフトの結果:y/2z)。
一部の世代のIntelCPU(P2またはP3?AMDではありませんが、私が正しく覚えていれば)では、ビットシフト操作は途方もなく遅いです。 1ビットのビットシフトは、加算を使用できるため、常に高速である必要があります。考慮すべきもう1つの質問は、一定のビット数によるビットシフトが可変長シフトよりも速いかどうかです。オペコードが同じ速度であっても、x86では、ビットシフトの非定数の右側のオペランドがCLレジスタを占有する必要があります。これにより、レジスタ割り当てに追加の制約が課せられ、プログラムの速度も低下する可能性があります。
いつものように、それは周囲のコードコンテキストに依存します:例:配列インデックスとして_x<<1
_を使用していますか?またはそれを何か他のものに追加しますか?いずれの場合も、シフト数が少ない(1または2)と、コンパイラーがちょうどシフトしなければならない場合よりもさらに最適化できることがよくあります。スループット全体とレイテンシーとフロントエンドのボトルネックのトレードオフは言うまでもありません。小さなフラグメントのパフォーマンスは一次元ではありません。
ハードウェアシフト命令は、_x<<1
_をコンパイルするためのコンパイラの唯一のオプションではありませんが、他の答えはほとんどそれを前提としています。
_x << 1
_は、符号なしおよび2の補数の符号付き整数の場合は_x+x
_とまったく同じです。コンパイラーは、コンパイル中にターゲットとするハードウェアを常に認識しているため、このようなトリックを利用できます。
Intel Haswell の場合、add
のクロックスループットは4ですが、即時カウントのshl
のクロックスループットは2です。 (命令テーブル、および x86 タグwikiの他のリンクについては、 http://agner.org/optimize/ を参照してください)。 SIMDベクトルシフトはクロックあたり1(Skylakeでは2)ですが、SIMDベクトル整数加算はクロックあたり2(Skylakeでは3)です。ただし、レイテンシーは同じです:1サイクル。
shl
の特別な1つシフトエンコーディングもあり、カウントはオペコードに暗黙的に含まれています。 8086には、即時カウントシフトはなく、1つとcl
レジスタのみでした。これは主に右シフトに関連します。これは、メモリオペランドをシフトしない限り、左シフトに追加できるためです。ただし、後で値が必要になった場合は、最初にレジスタにロードすることをお勧めします。ただし、とにかく、_shl eax,1
_または_add eax,eax
_は_shl eax,10
_より1バイト短く、コードサイズは直接(デコード/フロントエンドのボトルネック)または間接的に(L1Iコードキャッシュミス)パフォーマンスに影響を与える可能性があります。
より一般的には、小さなシフトカウントは、x86のアドレッシングモードでスケーリングされたインデックスに最適化できる場合があります。最近一般的に使用されている他のほとんどのアーキテクチャはRISCであり、スケールインデックスアドレッシングモードはありませんが、x86は、これについて言及する価値のある一般的なアーキテクチャです。 (たとえば、4バイト要素の配列にインデックスを付ける場合、_int arr[]; arr[x<<1]
_のスケール係数を1増やす余地があります)。
コピー+シフトの必要性は、x
の元の値がまだ必要な状況では一般的です。ただし、ほとんどのx86整数命令はインプレースで動作します。(宛先はadd
やshl
。)x86-64 System V呼び出し規約は、レジスタに引数を渡します。最初の引数はedi
に、戻り値はeax
になります。したがって、_x<<10
_も返す関数です。コンパイラにコピー+シフトコードを出力させます。
LEA
命令を使用すると、シフトアンドアッド (アドレッシングモードのマシンエンコーディングを使用するため、シフトカウントは0から3になります)。結果を別のレジスタに入れます。
gccとclangはどちらも、Godboltコンパイラエクスプローラーで確認できるように、これらの関数を同じ方法で最適化します :
_int shl1(int x) { return x<<1; }
lea eax, [rdi+rdi] # 1 cycle latency, 1 uop
ret
int shl2(int x) { return x<<2; }
lea eax, [4*rdi] # longer encoding: needs a disp32 of 0 because there's no base register, only scaled-index.
ret
int times5(int x) { return x * 5; }
lea eax, [rdi + 4*rdi]
ret
int shl10(int x) { return x<<10; }
mov eax, edi # 1 uop, 0 or 1 cycle latency
shl eax, 10 # 1 uop, 1 cycle latency
ret
_
2つのコンポーネントを備えたLEAは、最近のIntelおよびAMDCPUで1サイクルのレイテンシーと2クロックあたり2のスループットを備えています。 (Sandybridge-familyおよびBulldozer/Ryzen)。 Intelでは、_lea eax, [rdi + rsi + 123]
_のレイテンシは3cで、クロックスループットは1つだけです。 (関連: コラッツの推測をテストするために、このC++コードが私の手書きのアセンブリよりも速いのはなぜですか? これについて詳しく説明します。)
とにかく、copy + shift by 10には、別のmov
命令が必要です。最近の多くのCPUではレイテンシーがゼロになる可能性がありますが、それでもフロントエンドの帯域幅とコードサイズが必要です。 ( x86のMOVは本当に「無料」でしょうか?なぜこれをまったく再現できないのですか? )
また、関連: x86で2つの連続したリール命令のみを使用してレジスタに37を掛ける方法は? 。
コンパイラーは周囲のコードを自由に変換できるため、実際のシフトがないか、他の操作と組み合わされます。
たとえば、if(x<<1) { }
はand
を使用して、上位ビットを除くすべてのビットをチェックできます。 x86では、_test eax, 0x7fffffff
_の代わりに_jz .false
_/_shl eax,1 / jz
_のようなtest
命令を使用します。この最適化は、あらゆるシフトカウントで機能し、大量のシフトが遅いマシン(Pentium 4など)または存在しないマシン(一部のマイクロコントローラー)でも機能します。
多くのISAには、シフトだけでなくビット操作命令があります。例えばPowerPCには、ビットフィールドの抽出/挿入命令がたくさんあります。またはARMには、他の命令の一部としてソースオペランドのシフトがあります(したがって、シフト/回転命令は、シフトされたソースを使用するmove
の特殊な形式にすぎません)。
Cはアセンブリ言語ではないことを忘れないでください。効率的にコンパイルするようにソースコードを調整するときは、常にoptimizedコンパイラ出力を確認してください。