<<
を使用して乗算し、>>
を使用して数値をpythonで除算することができます。バイナリシフトを使用すると、除算よりも10倍高速です。通常の方法を乗算します。
<<
と>>
を使用するのが*
と/
よりもはるかに高速なのはなぜですか?
*
および/
を非常に遅くするために行われている背後のプロセスは何ですか?
ビットシフトと除算を行う2つの小さなCプログラムを見てみましょう。
#include <stdlib.h>
int main(int argc, char* argv[]) {
int i = atoi(argv[0]);
int b = i << 2;
}
#include <stdlib.h>
int main(int argc, char* argv[]) {
int i = atoi(argv[0]);
int d = i / 4;
}
これらはそれぞれgcc -S
でコンパイルされ、実際のアセンブリがどうなるかを確認します。
ビットシフトバージョンでは、atoi
の呼び出しから返されます。
callq _atoi
movl $0, %ecx
movl %eax, -20(%rbp)
movl -20(%rbp), %eax
shll $2, %eax
movl %eax, -24(%rbp)
movl %ecx, %eax
addq $32, %rsp
popq %rbp
ret
分割バージョンの間:
callq _atoi
movl $0, %ecx
movl $4, %edx
movl %eax, -20(%rbp)
movl -20(%rbp), %eax
movl %edx, -28(%rbp) ## 4-byte Spill
cltd
movl -28(%rbp), %r8d ## 4-byte Reload
idivl %r8d
movl %eax, -24(%rbp)
movl %ecx, %eax
addq $32, %rsp
popq %rbp
ret
これを見るだけで、ビットシフトと比較して、除算バージョンにはさらにいくつかの命令があります。
鍵は彼らが何をするかです。
ビットシフトバージョンでは、主要な命令はshll $2, %eax
です。これは論理左シフトです-除算があり、それ以外はすべて値を移動しています。
除算バージョンでは、idivl %r8d
を確認できますが、そのすぐ上にcltd
(longをdoubleに変換)と、スピルおよびリロードに関するいくつかの追加ロジックがあります。この追加作業は、ビットではなく数学を扱っていることを知っているため、ビット数学だけを行うことで発生する可能性のあるさまざまなエラーを回避するために必要になることがよくあります。
いくつかの簡単な乗算を実行してみましょう:
#include <stdlib.h>
int main(int argc, char* argv[]) {
int i = atoi(argv[0]);
int b = i >> 2;
}
#include <stdlib.h>
int main(int argc, char* argv[]) {
int i = atoi(argv[0]);
int d = i * 4;
}
このすべてを通過するのではなく、1つの行が異なります。
$ diff mult.s bit.s 24c24 > shll $ 2、%eax --- <sarl $ 2、%eax
ここで、コンパイラーはシフトを使用して数学を実行できることを識別できましたが、論理シフトの代わりに算術シフトを実行します。これらを実行した場合、これらの違いは明白です-sarl
は符号を保持します。つまり、-2 * 4 = -8
とは異なり、shll
はそうではありません。
これを簡単なPerlスクリプトで見てみましょう。
#!/usr/bin/Perl
$foo = 4;
print $foo << 2, "\n";
print $foo * 4, "\n";
$foo = -4;
print $foo << 2, "\n";
print $foo * 4, "\n";
出力:
16 16 18446744073709551600 -16
うーん... -4 << 2
は18446744073709551600
です。これは、乗算と除算を処理するときに期待するものとは正確には一致しません。その通りですが、整数の乗算ではありません。
したがって、時期尚早の最適化に注意してください。コンパイラーが最適化するようにします。これは、ユーザーが実際に何をしようとしているのかを認識しており、バグが少なく、より適切に機能するでしょう。
既存の回答は実際にはハードウェアの側面を扱っていなかったので、ここでその角度について少し説明します。従来の知識では、乗算と除算はシフトよりもはるかに遅くなりますが、今日の実際の話はより微妙です。
たとえば、乗算はハードウェアに実装するためのcomplex演算であることは確かに事実ですが、必ずしもslowerになるとは限りません。結局のところ、add
はxor
(または一般にビット単位の演算)よりも実装がかなり複雑ですが、add
(およびsub
)通常は、ビット単位の演算子と同じくらい高速になる、動作専用の十分なトランジスタを入手します。したがって、ハードウェア実装の複雑さを速度の目安として見ることはできません。
それでは、シフトと、乗算やシフトなどの「完全な」演算子の比較を詳しく見てみましょう。
ほとんどすべてのハードウェアで、一定量(つまり、コンパイラーがコンパイル時に決定できる量)のシフトはfastです。特に、通常は1サイクルのレイテンシで発生し、1サイクルあたり1以上のスループットで発生します。一部のハードウェア(たとえば、一部のIntelおよびARMチップ)では、定数による特定のシフトは、別の命令に組み込むことができるため(「Intel上のlea
、ARMの最初のソースの特殊なシフト機能)。
可変量だけシフトすると、灰色の領域が増えます。古いハードウェアでは、これは時々非常に遅く、速度は世代ごとに変化しました。たとえば、IntelのP4の最初のリリースでは、可変量のシフトは非常に遅く、シフト量に比例した時間が必要でした。そのプラットフォームでは、乗算をreplaceシフトに使用することで利益を上げることができます(つまり、世界は逆さまになっています)。以前のIntelチップとその後の世代では、可変量のシフトはそれほど苦痛ではありませんでした。
現在のIntelチップでは、可変量のシフトは特に速くはありませんが、ひどくもありません。 x86アーキテクチャは、変数シフトに関しては異常な方法で操作を定義しているため、厳しい状態にあります。シフト量0は条件フラグを変更しませんが、他のすべてのシフトは変更します。これは、後続の命令がシフトによって書き込まれた条件コードを読み取るか、または前の命令を読み取るかをシフトが実行するまで決定できないため、フラグレジスタの効率的な名前変更を阻害します。さらに、シフトはフラグレジスタの一部にのみ書き込みを行うため、部分的なフラグストールが発生する可能性があります。
その結果、最近のIntelアーキテクチャでは、可変量のシフトには3つの「マイクロ演算」が必要ですが、他のほとんどの単純な演算(加算、ビットごとの演算、乗算も)は1つしかかかりません。このようなシフトは、最大で2サイクルごとに実行できます。 。
最近のdesktopおよびlaptopハードウェアの傾向は、乗算を高速演算にすることです。実際、最近のIntelおよびAMDチップでは、サイクルごとに1つの乗算を発行できます(これを相互スループットと呼びます)。 latencyただし、乗算は3サイクルです。つまり、開始から3サイクル後に任意の乗算のresultを取得しますが、サイクルごとに新しい乗算を開始することができます。どちらの値(1サイクルまたは3サイクル)がより重要かは、アルゴリズムの構造によって異なります。乗算が重要な依存関係チェーンの一部である場合、レイテンシは重要です。そうでない場合、相互スループットまたは他の要因がより重要になる場合があります。
彼らの重要なポイントは、現代のラップトップチップ(またはそれ以上)では、乗算は高速な演算であり、コンパイラーが「丸め」を取得するために発行する3または4命令シーケンスよりも高速である可能性が高いということです。強さの削減シフトのために右。変数シフトの場合、Intelでは、上記の問題があるため、乗算も一般的に推奨されます。
小型のフォームファクタープラットフォームでは、完全で高速な32ビットまたは特に64ビットの乗算器を構築すると、多くのトランジスタと電力が必要になるため、乗算はさらに遅くなる可能性があります。最近のモバイルチップでの乗算のパフォーマンスの詳細を誰かが記入できれば、大歓迎です。
除算は、乗算よりもハードウェア的にはより複雑な演算であり、実際のコードではあまり一般的ではありません。つまり、割り振られるリソースが少なくなる可能性があります。最近のチップの傾向は、より高速な分周器に向かっていますが、最近の最上位のチップでさえ、分周するのに10〜40サイクルかかり、部分的にパイプライン化されています。一般に、64ビットの除算は32ビットの除算よりもさらに低速です。他のほとんどの演算とは異なり、除算は引数に応じて可変数のサイクルを取る場合があります。
可能であれば、除算してシフトで置換しないでください(またはコンパイラに実行を許可しますが、アセンブリの確認が必要になる場合があります)。
BINARY_LSHIFTおよびBINARY_RSHIFTは、BINARY_MULTIPLYおよびBINARY_FLOOR_DIVIDEよりもアルゴリズム的に単純なプロセスであり、必要なクロックサイクルが少なくなる場合があります。つまり、2進数があり、Nだけビットシフトする必要がある場合、必要なのは、その桁数だけ桁をシフトして、ゼロで置き換えることだけです。 バイナリ乗算は一般により複雑です 、ただし Dadda乗数 のような手法を使用するとかなり高速になります。
確かに、最適化コンパイラーは、2の累乗で乗算/除算し、適切な左/右シフトで置き換える場合を認識できます。逆アセンブルされたバイトコードを見ると、pythonは明らかにこれを行いません:
>>> dis.dis(lambda x: x*4)
1 0 LOAD_FAST 0 (x)
3 LOAD_CONST 1 (4)
6 BINARY_MULTIPLY
7 RETURN_VALUE
>>> dis.dis(lambda x: x<<2)
1 0 LOAD_FAST 0 (x)
3 LOAD_CONST 1 (2)
6 BINARY_LSHIFT
7 RETURN_VALUE
>>> dis.dis(lambda x: x//2)
1 0 LOAD_FAST 0 (x)
3 LOAD_CONST 1 (2)
6 BINARY_FLOOR_DIVIDE
7 RETURN_VALUE
>>> dis.dis(lambda x: x>>1)
1 0 LOAD_FAST 0 (x)
3 LOAD_CONST 1 (1)
6 BINARY_RSHIFT
7 RETURN_VALUE
ただし、私のプロセッサでは、乗算と左/右シフトのタイミングが似ていることがわかり、フロア除算(2の累乗による)は約25%遅くなっています。
>>> import timeit
>>> timeit.repeat("z=a + 4", setup="a = 37")
[0.03717184066772461, 0.03291916847229004, 0.03287005424499512]
>>> timeit.repeat("z=a - 4", setup="a = 37")
[0.03534698486328125, 0.03207516670227051, 0.03196907043457031]
>>> timeit.repeat("z=a * 4", setup="a = 37")
[0.04594111442565918, 0.0408930778503418, 0.045324087142944336]
>>> timeit.repeat("z=a // 4", setup="a = 37")
[0.05412912368774414, 0.05091404914855957, 0.04910898208618164]
>>> timeit.repeat("z=a << 2", setup="a = 37")
[0.04751706123352051, 0.04259490966796875, 0.041903018951416016]
>>> timeit.repeat("z=a >> 2", setup="a = 37")
[0.04719185829162598, 0.04201006889343262, 0.042105913162231445]