事前にわからない2つの値を乗算する場合、x86アセンブラの乗算命令を打ち負かすことは事実上不可能です。
いずれかのオペランドの値が事前にわかっている場合は、少数の加算を使用することで乗算命令を打ち負かすことができる場合があります。これは、既知のオペランドが小さく、バイナリ表現に数ビットしかない場合に特に効果的です。未知の値xに2^p + 2 ^ q + ... 2 ^ rで構成される既知の値を掛けるには、ビットp、qの場合にx * 2 ^ p + x * 2 ^ q + .. x * 2*rを追加するだけです。 、...およびrが設定されます。これは、アセンブラで左シフトして追加することで簡単に実行できます。
; x in EDX
; product to EAX
xor eax,eax
shl edx,r ; x*2^r
add eax,edx
shl edx,q-r ; x*2^q
add eax,edx
shl edx,p-q ; x*2^p
add eax,edx
これに関する重要な問題は、レジスタの依存関係によって制約されたスーパースカラーCPUを想定すると、これを行うのに少なくとも4クロックかかることです。最近のCPUでは、乗算にかかるクロックは通常10以下であり、このシーケンスが時間より長くなる場合は、乗算を実行することをお勧めします。
9を掛けるには:
mov eax,edx ; same effect as xor eax,eax/shl edx 1/add eax,edx
shl edx,3 ; x*2^3
add eax,edx
これは乗算を打ち負かします。2クロックしかかかりません。
あまり知られていないのは、LEA(実効アドレスのロード)命令を使用して、高速の小さな定数を乗算することです。最悪の場合、実行時間が1クロックしかかからないLEAは、スーパースカラーCPUによる他の命令とオーバーラップすることがよくあります。
LEAは、本質的に「小さな定数乗数で2つの値を加算する」ことです。t、x、yが任意のレジスタである場合、k = 1,2,3(Intelリファレンスマニュアルを参照)に対してt = 2 ^ k * x+yを計算します。x == yの場合、xの1,2,3,4,5,8,9倍を取得できますが、xとyを個別のレジスタとして使用すると、中間結果を結合
して他のレジスタ(たとえば、t)に移動できます。 )、これは非常に便利であることがわかります。これを使用すると、1つの命令を使用して9を掛けることができます。
lea eax,[edx*8+edx] ; takes 1 clock
LEAを注意深く使用すると、少数のサイクルでさまざまな固有の定数を乗算できます。
lea eax,[edx*4+edx] ; 5 * edx
lea eax,[eax*2+edx] ; 11 * edx
lea eax,[eax*4] ; 44 * edx
これを行うには、定数乗数を1、2、3、4、5、8、および9を含むさまざまな係数/合計に分解する必要があります。これを実行できる小さな定数の数は注目に値しますが、それでも3つしか使用しません。 4つの指示。
他の通常のシングルクロック命令(SHL / SUB / NEG / MOVなど)の使用を許可すると、純粋なLEAだけでは効率的に実行できない定数値を乗算できます。31を掛けるには:
lea eax,[4*edx]
lea eax,[8*eax] ; 32*edx
sub eax,edx; 31*edx ; 3 clocks
対応するLEAシーケンスは長くなります。
lea eax,[edx*4+edx]
lea eax,[edx*2+eax] ; eax*7
lea eax,[eax*2+edx] ; eax*15
lea eax,[eax*2+edx] ; eax*31 ; 4 clocks
これらのシーケンスを理解するのは少し難しいですが、組織的な攻撃を設定することができます。
LEA、SHL、SUB、NEG、MOVはすべてシングルクロック命令のワーストケースであり、他の命令に依存しない場合はゼロクロックであるため、このようなシーケンスの実行コストを計算できます。これは、動的プログラミングアルゴリズムを実装して、そのような命令の可能な限り最良のシーケンスを生成できることを意味します。これは、クロックカウントが特定のCPUの整数乗算よりも小さく(経験則として5クロックを使用)、すべてのレジスタを使い果たしていないか、少なくともレジスタを使い果たしていない場合にのみ役立ちます。すでに忙しい(流出を避ける)。
私は実際にこれをPARLANSEコンパイラに組み込みました。これは、Aの構造要素のサイズが既知の定数である構造A[i]の配列へのオフセットを計算するのに非常に効果的です。賢い人は答えをキャッシュする可能性があるので、同じ定数を乗算するたびに再計算する必要はありません。そのようなシーケンスを生成する時間はあなたが期待するよりも短いので、私は実際にはそれをしませんでした。
1から10000までのすべての定数を乗算するために必要な命令のシーケンスを出力することは、やや興味深いことです。それらのほとんどは、最悪の場合5〜6命令で実行できます。結果として、PARLANSEコンパイラは、ネストされた構造体の最も厄介な配列でさえインデックスを作成するときに、実際の乗算を使用することはほとんどありません。