128ビットの符号なし整数Aと64ビットの符号なし整数Bがあります。A % B
を計算する最速の方法は何ですか。これは、AをBで除算したときの(64ビット)剰余です。
これをCまたはアセンブリ言語のいずれかで実行したいと考えていますが、32ビットx86プラットフォームをターゲットにする必要があります。これは残念なことに、128ビット整数のコンパイラサポートや、1つの命令で必要な操作を実行するx64アーキテクチャの機能を利用できないことを意味します。
編集:
これまでの回答ありがとうございます。しかし、提案されたアルゴリズムはかなり遅いようです-128ビット64ビット除算を実行する最速の方法は、64ビット32ビット除算に対するプロセッサのネイティブサポートを活用することではないでしょうか。いくつかの小さな部門で大きな部門を実行する方法があるかどうか誰かが知っていますか?
Re:Bはどのくらいの頻度で変化しますか?
主に私は一般的なソリューションに興味があります-AとBが毎回異なる可能性がある場合、どのような計算を実行しますか?
ただし、2番目に考えられる状況は、BがAほど頻繁に変化しないことです。各Bで除算されるのは200もの場合があります。この場合、答えはどのように異なりますか?
Russian Peasant Multiplication の除算バージョンを使用できます。
残りを見つけるには、(疑似コードで)実行します。
X = B;
while (X <= A/2)
{
X <<= 1;
}
while (A >= B)
{
if (A >= X)
A -= X;
X >>= 1;
}
係数はAのままです。
64ビットの数値のペアで構成される値を操作するには、シフト、比較、および減算を実装する必要がありますが、それはかなり自明です(おそらく、左シフト1をX + X
として実装する必要があります)。 。
これは最大で255回ループします(128ビットA)。もちろん、ゼロ除数の事前チェックを行う必要があります。
おそらく、完成したプログラムを探しているかもしれませんが、多精度演算の基本的なアルゴリズムは、Knuthの Art of Computer Programming 、第2巻にあります。オンラインで説明されている除算アルゴリズムを見つけることができます ここ 。アルゴリズムは任意の多精度演算を処理するため、必要以上に一般的ですが、64ビットまたは32ビットの桁で行われる128ビット演算では、アルゴリズムを簡略化できるはずです。妥当な量の作業(a)アルゴリズムを理解し、(b)アルゴリズムをCまたはアセンブラーに変換する準備をしてください。
また、 Hacker's Delight もチェックしてみてください。これは、非常に巧妙なアセンブラーや他の低レベルのハッカー(多精度演算を含む)でいっぱいです。
A = AH*2^64 + AL
:
A % B == (((AH % B) * (2^64 % B)) + (AL % B)) % B
== (((AH % B) * ((2^64 - B) % B)) + (AL % B)) % B
コンパイラが64ビット整数をサポートしている場合、これがおそらく最も簡単な方法です。 MSVCの32ビットx86での64ビットモジュロの実装は、毛深いループで満たされたアセンブリ(VC\crt\src\intel\llrem.asm
勇敢なため)、私は個人的にはそれで行きます。
これは、ほとんどテストされていない、部分的に速度が変更されたMod128by64「ロシアの農民」アルゴリズム関数です。残念ながら私はDelphiユーザーなので、この関数はDelphiで動作します。 :)しかし、アセンブラはほとんど同じなので...
function Mod128by64(Dividend: PUInt128; Divisor: PUInt64): UInt64;
//In : eax = @Dividend
// : edx = @Divisor
//Out: eax:edx as Remainder
asm
//Registers inside rutine
//Divisor = edx:ebp
//Dividend = bh:ebx:edx //We need 64 bits + 1 bit in bh
//Result = esi:edi
//ecx = Loop counter and Dividend index
Push ebx //Store registers to stack
Push esi
Push edi
Push ebp
mov ebp, [edx] //Divisor = edx:ebp
mov edx, [edx + 4]
mov ecx, ebp //Div by 0 test
or ecx, edx
jz @DivByZero
xor edi, edi //Clear result
xor esi, esi
//Start of 64 bit division Loop
mov ecx, 15 //Load byte loop shift counter and Dividend index
@SkipShift8Bits: //Small Dividend numbers shift optimisation
cmp [eax + ecx], ch //Zero test
jnz @EndSkipShiftDividend
loop @SkipShift8Bits //Skip 8 bit loop
@EndSkipShiftDividend:
test edx, $FF000000 //Huge Divisor Numbers Shift Optimisation
jz @Shift8Bits //This Divisor is > $00FFFFFF:FFFFFFFF
mov ecx, 8 //Load byte shift counter
mov esi, [eax + 12] //Do fast 56 bit (7 bytes) shift...
shr esi, cl //esi = $00XXXXXX
mov edi, [eax + 9] //Load for one byte right shifted 32 bit value
@Shift8Bits:
mov bl, [eax + ecx] //Load 8 bits of Dividend
//Here we can unrole partial loop 8 bit division to increase execution speed...
mov ch, 8 //Set partial byte counter value
@Do65BitsShift:
shl bl, 1 //Shift dividend left for one bit
rcl edi, 1
rcl esi, 1
setc bh //Save 65th bit
sub edi, ebp //Compare dividend and divisor
sbb esi, edx //Subtract the divisor
sbb bh, 0 //Use 65th bit in bh
jnc @NoCarryAtCmp //Test...
add edi, ebp //Return privius dividend state
adc esi, edx
@NoCarryAtCmp:
dec ch //Decrement counter
jnz @Do65BitsShift
//End of 8 bit (byte) partial division loop
dec cl //Decrement byte loop shift counter
jns @Shift8Bits //Last jump at cl = 0!!!
//End of 64 bit division loop
mov eax, edi //Load result to eax:edx
mov edx, esi
@RestoreRegisters:
pop ebp //Restore Registers
pop edi
pop esi
pop ebx
ret
@DivByZero:
xor eax, eax //Here you can raise Div by 0 exception, now function only return 0.
xor edx, edx
jmp @RestoreRegisters
end;
少なくとももう1つの速度最適化が可能です! 「Huge Divisor Numbers Shift Optimization」の後、除数の上位ビットをテストできます。それが0の場合、追加のbhレジスタを65番目のビットとして使用して格納する必要はありません。したがって、ループの展開された部分は次のようになります。
shl bl,1 //Shift dividend left for one bit
rcl edi,1
rcl esi,1
sub edi, ebp //Compare dividend and divisor
sbb esi, edx //Subtract the divisor
jnc @NoCarryAtCmpX
add edi, ebp //Return privius dividend state
adc esi, edx
@NoCarryAtCmpX:
@cafが受け入れた回答は本当に素晴らしいものでしたが、何年も見られなかったバグが含まれています。
そのテストや他のソリューションをテストするために、私はテストハーネスを投稿し、コミュニティウィキにしています。
unsigned cafMod(unsigned A, unsigned B) {
assert(B);
unsigned X = B;
// while (X < A / 2) { Original code used <
while (X <= A / 2) {
X <<= 1;
}
while (A >= B) {
if (A >= X) A -= X;
X >>= 1;
}
return A;
}
void cafMod_test(unsigned num, unsigned den) {
if (den == 0) return;
unsigned y0 = num % den;
unsigned y1 = mod(num, den);
if (y0 != y1) {
printf("FAIL num:%x den:%x %x %x\n", num, den, y0, y1);
fflush(stdout);
exit(-1);
}
}
unsigned Rand_unsigned() {
unsigned x = (unsigned) Rand();
return x * 2 ^ (unsigned) Rand();
}
void cafMod_tests(void) {
const unsigned i[] = { 0, 1, 2, 3, 0x7FFFFFFF, 0x80000000,
UINT_MAX - 3, UINT_MAX - 2, UINT_MAX - 1, UINT_MAX };
for (unsigned den = 0; den < sizeof i / sizeof i[0]; den++) {
if (i[den] == 0) continue;
for (unsigned num = 0; num < sizeof i / sizeof i[0]; num++) {
cafMod_test(i[num], i[den]);
}
}
cafMod_test(0x8711dd11, 0x4388ee88);
cafMod_test(0xf64835a1, 0xf64835a);
time_t t;
time(&t);
srand((unsigned) t);
printf("%u\n", (unsigned) t);fflush(stdout);
for (long long n = 10000LL * 1000LL * 1000LL; n > 0; n--) {
cafMod_test(Rand_unsigned(), Rand_unsigned());
}
puts("Done");
}
int main(void) {
cafMod_tests();
return 0;
}
Mod128by64の「ロシアの農民」除算機能の両方のバージョンを作成しました:クラシックと速度を最適化しました。最適化された速度は、私の3Ghz PCで毎秒1000.000以上のランダム計算を実行でき、従来の関数より3倍以上高速です。 128 x 64の計算と64 x 64ビットのモジュロの計算の実行時間を比較すると、この関数は約50%遅くなります。
古典的なロシアの農民:
function Mod128by64Clasic(Dividend: PUInt128; Divisor: PUInt64): UInt64;
//In : eax = @Dividend
// : edx = @Divisor
//Out: eax:edx as Remainder
asm
//Registers inside rutine
//edx:ebp = Divisor
//ecx = Loop counter
//Result = esi:edi
Push ebx //Store registers to stack
Push esi
Push edi
Push ebp
mov ebp, [edx] //Load divisor to edx:ebp
mov edx, [edx + 4]
mov ecx, ebp //Div by 0 test
or ecx, edx
jz @DivByZero
Push [eax] //Store Divisor to the stack
Push [eax + 4]
Push [eax + 8]
Push [eax + 12]
xor edi, edi //Clear result
xor esi, esi
mov ecx, 128 //Load shift counter
@Do128BitsShift:
shl [esp + 12], 1 //Shift dividend from stack left for one bit
rcl [esp + 8], 1
rcl [esp + 4], 1
rcl [esp], 1
rcl edi, 1
rcl esi, 1
setc bh //Save 65th bit
sub edi, ebp //Compare dividend and divisor
sbb esi, edx //Subtract the divisor
sbb bh, 0 //Use 65th bit in bh
jnc @NoCarryAtCmp //Test...
add edi, ebp //Return privius dividend state
adc esi, edx
@NoCarryAtCmp:
loop @Do128BitsShift
//End of 128 bit division loop
mov eax, edi //Load result to eax:edx
mov edx, esi
@RestoreRegisters:
lea esp, esp + 16 //Restore Divisors space on stack
pop ebp //Restore Registers
pop edi
pop esi
pop ebx
ret
@DivByZero:
xor eax, eax //Here you can raise Div by 0 exception, now function only return 0.
xor edx, edx
jmp @RestoreRegisters
end;
速度を最適化したロシアの農民:
function Mod128by64Oprimized(Dividend: PUInt128; Divisor: PUInt64): UInt64;
//In : eax = @Dividend
// : edx = @Divisor
//Out: eax:edx as Remainder
asm
//Registers inside rutine
//Divisor = edx:ebp
//Dividend = ebx:edx //We need 64 bits
//Result = esi:edi
//ecx = Loop counter and Dividend index
Push ebx //Store registers to stack
Push esi
Push edi
Push ebp
mov ebp, [edx] //Divisor = edx:ebp
mov edx, [edx + 4]
mov ecx, ebp //Div by 0 test
or ecx, edx
jz @DivByZero
xor edi, edi //Clear result
xor esi, esi
//Start of 64 bit division Loop
mov ecx, 15 //Load byte loop shift counter and Dividend index
@SkipShift8Bits: //Small Dividend numbers shift optimisation
cmp [eax + ecx], ch //Zero test
jnz @EndSkipShiftDividend
loop @SkipShift8Bits //Skip Compute 8 Bits unroled loop ?
@EndSkipShiftDividend:
test edx, $FF000000 //Huge Divisor Numbers Shift Optimisation
jz @Shift8Bits //This Divisor is > $00FFFFFF:FFFFFFFF
mov ecx, 8 //Load byte shift counter
mov esi, [eax + 12] //Do fast 56 bit (7 bytes) shift...
shr esi, cl //esi = $00XXXXXX
mov edi, [eax + 9] //Load for one byte right shifted 32 bit value
@Shift8Bits:
mov bl, [eax + ecx] //Load 8 bit part of Dividend
//Compute 8 Bits unroled loop
shl bl, 1 //Shift dividend left for one bit
rcl edi, 1
rcl esi, 1
jc @DividentAbove0 //dividend hi bit set?
cmp esi, edx //dividend hi part larger?
jb @DividentBelow0
ja @DividentAbove0
cmp edi, ebp //dividend lo part larger?
jb @DividentBelow0
@DividentAbove0:
sub edi, ebp //Return privius dividend state
sbb esi, edx
@DividentBelow0:
shl bl, 1 //Shift dividend left for one bit
rcl edi, 1
rcl esi, 1
jc @DividentAbove1 //dividend hi bit set?
cmp esi, edx //dividend hi part larger?
jb @DividentBelow1
ja @DividentAbove1
cmp edi, ebp //dividend lo part larger?
jb @DividentBelow1
@DividentAbove1:
sub edi, ebp //Return privius dividend state
sbb esi, edx
@DividentBelow1:
shl bl, 1 //Shift dividend left for one bit
rcl edi, 1
rcl esi, 1
jc @DividentAbove2 //dividend hi bit set?
cmp esi, edx //dividend hi part larger?
jb @DividentBelow2
ja @DividentAbove2
cmp edi, ebp //dividend lo part larger?
jb @DividentBelow2
@DividentAbove2:
sub edi, ebp //Return privius dividend state
sbb esi, edx
@DividentBelow2:
shl bl, 1 //Shift dividend left for one bit
rcl edi, 1
rcl esi, 1
jc @DividentAbove3 //dividend hi bit set?
cmp esi, edx //dividend hi part larger?
jb @DividentBelow3
ja @DividentAbove3
cmp edi, ebp //dividend lo part larger?
jb @DividentBelow3
@DividentAbove3:
sub edi, ebp //Return privius dividend state
sbb esi, edx
@DividentBelow3:
shl bl, 1 //Shift dividend left for one bit
rcl edi, 1
rcl esi, 1
jc @DividentAbove4 //dividend hi bit set?
cmp esi, edx //dividend hi part larger?
jb @DividentBelow4
ja @DividentAbove4
cmp edi, ebp //dividend lo part larger?
jb @DividentBelow4
@DividentAbove4:
sub edi, ebp //Return privius dividend state
sbb esi, edx
@DividentBelow4:
shl bl, 1 //Shift dividend left for one bit
rcl edi, 1
rcl esi, 1
jc @DividentAbove5 //dividend hi bit set?
cmp esi, edx //dividend hi part larger?
jb @DividentBelow5
ja @DividentAbove5
cmp edi, ebp //dividend lo part larger?
jb @DividentBelow5
@DividentAbove5:
sub edi, ebp //Return privius dividend state
sbb esi, edx
@DividentBelow5:
shl bl, 1 //Shift dividend left for one bit
rcl edi, 1
rcl esi, 1
jc @DividentAbove6 //dividend hi bit set?
cmp esi, edx //dividend hi part larger?
jb @DividentBelow6
ja @DividentAbove6
cmp edi, ebp //dividend lo part larger?
jb @DividentBelow6
@DividentAbove6:
sub edi, ebp //Return privius dividend state
sbb esi, edx
@DividentBelow6:
shl bl, 1 //Shift dividend left for one bit
rcl edi, 1
rcl esi, 1
jc @DividentAbove7 //dividend hi bit set?
cmp esi, edx //dividend hi part larger?
jb @DividentBelow7
ja @DividentAbove7
cmp edi, ebp //dividend lo part larger?
jb @DividentBelow7
@DividentAbove7:
sub edi, ebp //Return privius dividend state
sbb esi, edx
@DividentBelow7:
//End of Compute 8 Bits (unroled loop)
dec cl //Decrement byte loop shift counter
jns @Shift8Bits //Last jump at cl = 0!!!
//End of division loop
mov eax, edi //Load result to eax:edx
mov edx, esi
@RestoreRegisters:
pop ebp //Restore Registers
pop edi
pop esi
pop ebx
ret
@DivByZero:
xor eax, eax //Here you can raise Div by 0 exception, now function only return 0.
xor edx, edx
jmp @RestoreRegisters
end;
32ビットコードを指定した質問は知っていますが、64ビットの回答は他の人にとって有用または興味深いかもしれません。
そして、はい、64b/32b => 32b除算は、128b%64b => 64bの有用なビルディングブロックになります。 libgccの___umoddi3
_(以下にリンクされているソース)は、そのようなことを行う方法のアイデアを提供しますが、2N/N => N除算の上に2N%2N => 2Nのみを実装し、4N%2N =ではありません> 2N。
より幅広い多精度ライブラリが利用可能です。 https://gmplib.org/manual/Integer-Division.html#Integer-Division 。
64ビットマシンのGnu Cは ___int128
_ type を提供し、libgcc関数は乗算と除算を行います。ターゲットアーキテクチャで可能な限り効率的に。
x86-64の _div r/m64
_ 命令は128b/64b => 64b除算を実行します(2番目の出力として剰余も生成します)が、商がオーバーフローするとエラーになります。そのため、_A/B > 2^64-1
_の場合は直接使用できませんが、gccで使用できます(またはlibgccが使用するのと同じコードをインライン化することもできます)。
これは、( Godboltコンパイラエクスプローラ )を1つまたは2つのdiv
命令にコンパイルします(これは libgcc 関数呼び出し内で発生します)。より速い方法があった場合、libgccはおそらく代わりにそれを使用します。
_#include <stdint.h>
uint64_t AmodB(unsigned __int128 A, uint64_t B) {
return A % B;
}
_
関数が呼び出す___umodti3
_関数は、完全な128b/128bモジュロを計算しますが、この関数の実装は、除数の上位半分が0であるという特殊なケースをチェックします libgccソースを参照 。 (libgccは、ターゲットアーキテクチャに応じて、そのコードから関数のsi/di/tiバージョンを構築します。 _udiv_qrnnd
_ は、符号なし2N/N => Nを実行するインラインasmマクロですターゲットアーキテクチャの分割。
x86-64(およびハードウェア除算命令を備えた他のアーキテクチャ)の場合、高速パス(When high_half(A) < B
; div
がエラーにならないことを保証する場合)は、2つの分岐のみをとらない、いくつかの綿毛順序が狂ったCPUがかみ合うために、と単一の_div r64
_命令、これは最新のx86 CPUで約50-100サイクルかかります Agner Fogのinsnテーブル によると、 div
と並行して他の作業が発生する可能性がありますが、整数除算ユニットはあまりパイプライン化されず、div
は多くのuopsにデコードします(FP除算とは異なります)。
フォールバックパスは、div
が64ビットのみの場合、64ビットB
命令を2つだけ使用しますが、_A/B
_は64ビットに適合しないため、_A/B
_は直接エラーになります。
Libgccの___umodti3
_は___udivmoddi4
_をラッパーにインライン化するだけで、残りを返すだけであることに注意してください。
B
による繰り返しモジュロ存在する場合、B
の 固定小数点乗算逆数 を計算することを検討する価値があるかもしれません。たとえば、コンパイル時の定数では、gccは128bより狭い型の最適化を行います。
_uint64_t modulo_by_constant64(uint64_t A) { return A % 0x12345678ABULL; }
movabs rdx, -2233785418547900415
mov rax, rdi
mul rdx
mov rax, rdx # wasted instruction, could have kept using RDX.
movabs rdx, 78187493547
shr rax, 36 # division result
imul rax, rdx # multiply and subtract to get the modulo
sub rdi, rax
mov rax, rdi
ret
_
x86の_mul r64
_命令は64b * 64b => 128b(rdx:rax)乗算を行い、128b * 128b => 256b乗算を構築するためのビルディングブロックとして使用して、同じアルゴリズムを実装できます。必要なのは完全な256bの結果の上位半分のみなので、乗算をいくつか節約できます。
最近のIntel CPUは非常に高いパフォーマンスmul
:3cレイテンシ、1クロックあたり1つのスループットを備えています。ただし、必要なシフトと加算の正確な組み合わせは定数によって異なるため、実行時に乗法逆数を計算する一般的なケースは、JITコンパイルバージョンまたは静的コンパイルバージョンとして使用するたびにそれほど効率的ではありません(事前計算オーバーヘッドに加えて)。
損益分岐点となるIDK。 JITコンパイルの場合、一般的に使用されるB
値の生成コードをキャッシュしない限り、再利用は最大で200を超えます。 「通常の」方法では、200回の再利用の範囲になる可能性がありますが、128ビット/ 64ビット除算のモジュラー乗法逆数を見つけることは、IDKにどのくらいの費用がかかるでしょう。
libdivide はこれを行うことができますが、32および64ビットタイプに対してのみです。それでも、それはおそらく良い出発点です。
私はいくつかの考えを共有したいと思います。
MSNが提案しているのと同じくらい簡単ではありません。
式では:
(((AH % B) * ((2^64 - B) % B)) + (AL % B)) % B
乗算と加算の両方がオーバーフローする可能性があります。それを考慮に入れて、一般的な概念をいくつかの変更を加えて使用することはできると思いますが、何かがそれを本当に恐ろしくするだろうと私に言います。
MSVCで64ビットのモジュロ演算がどのように実装されているか知りたくて、何かを見つけようとしました。私はアセンブリを本当に知りません、そしてVC\crt\src\intel\llrem.asmのソースなしで私が利用できたのはExpressエディションだけでした、しかし私は少し遊んだ後何が起こっているのかいくつかの考えを得ることができたと思いますデバッガーと逆アセンブリ出力を使用します。正の整数と除数> = 2 ^ 32の場合の剰余の計算方法を理解しようとしました。もちろん負の数を処理するコードはいくつかありますが、私はそれを掘り下げませんでした。
ここに私がそれを見る方法があります:
除数> = 2 ^ 32の場合、除数と除数の両方が、除数を32ビットに合わせるために必要なだけ右にシフトされます。言い換えると、除数を2進数で書くのにn桁かかり、n> 32の場合、除数と被除数の両方のn-32最下位桁が破棄されます。その後、64ビット整数を32ビット整数で除算するハードウェアサポートを使用して除算が実行されます。結果は正しくないかもしれませんが、結果が最大で1ずれる可能性があることは証明できると思います。除算後、除数(元の除数)に結果を掛け、その積を被除数から差し引きます。次に、必要に応じて(除算の結果が1でずれていた場合)、除数を加算または減算することで修正されます。
64ビットによる32ビット除算のハードウェアサポートを利用して、128ビット整数を32ビット1で除算するのは簡単です。除数が2 ^ 32未満の場合、次のように4除算を実行して剰余を計算できます。
配当が次の場所に保存されていると仮定します。
DWORD dividend[4] = ...
残りは入ります:
DWORD remainder;
1) Divide dividend[3] by divisor. Store the remainder in remainder.
2) Divide QWORD (remainder:dividend[2]) by divisor. Store the remainder in remainder.
3) Divide QWORD (remainder:dividend[1]) by divisor. Store the remainder in remainder.
4) Divide QWORD (remainder:dividend[0]) by divisor. Store the remainder in remainder.
これらの4つのステップの後、変数の残りはあなたが探しているものを保持します。 (エンディアネスが間違っていても私を殺さないでください。私もプログラマーではありません)
除数が2 ^ 32-1よりも大きい場合、良いニュースはありません。 MSVCが使用していると私が確信している前述の手順で、シフト後の結果が1以下であるという完全な証拠はありません。ただし、破棄される部分は約2 ^ 31倍の除数より少なく、被除数は2 ^ 64未満で、約数は2 ^ 32-1より大きいという事実と関係があると思います。なので、結果は2 ^ 32未満です。
被除数が128ビットの場合、ビットを破棄するトリックは機能しません。したがって、一般的には、最良の解決策はおそらくGJまたはcafによって提案されたものです。 (まあ、それはおそらくビットの破棄が機能したとしても最高でしょう。128ビット整数の除算、乗算減算、および修正は遅くなるかもしれません。)
浮動小数点ハードウェアの使用も考えていました。 x87浮動小数点ユニットは、64ビット長の80ビット精度フォーマットを使用します。 64ビット×64ビット除算の正確な結果が得られると思います。 (残りは直接ではなく、「MSVC手順」のように乗算と減算を使用した残りも)。被除数> = 2 ^ 64および<2 ^ 128の場合、浮動小数点形式で格納すると、「MSVC手順」で最下位ビットを破棄するのと似ているように見えます。たぶん誰かがその場合のエラーを証明し、それが役に立つと思うかもしれません。 GJのソリューションよりも高速になる可能性があるかどうかはわかりませんが、試してみる価値はあります。
解決策は、正確に解決しようとしていることに依存します。
例えば。 64ビット整数を法とするリングで算術演算を行う場合、 Montgomerys reduction を使用すると非常に効率的です。もちろん、これは同じ係数を何度も使用し、リングの要素を特別な表現に変換することで利益が得られることを前提としています。
このMontgomerys削減の速度の非常に大まかな見積もりを与えるために:私は、2.4Ghzコア2で1600 nsで64ビットのモジュラスと指数でモジュラー指数を実行する古いベンチマークを持っています。この指数は、約96モジュラー乗算(およびモジュラー削減)、したがってモジュラー乗算あたり約40サイクルが必要です。
原則として、除算は低速で、乗算は高速で、ビットシフトも高速です。これまでの回答で私が見てきたことから、ほとんどの回答はビットシフトを使用したブルートフォースアプローチを使用しています。別の方法があります。それがより速いかどうかはまだわかりません(別名プロファイル)。
除算する代わりに、逆数を掛けます。したがって、A%Bを検出するには、まずB ... 1/Bの逆数を計算します。これは、収束のニュートンラフソン法を使用するいくつかのループで実行できます。これを適切に行うには、テーブル内の初期値の適切なセットに依存します。
逆数に収束するニュートンラフソン法の詳細については、 http://en.wikipedia.org/wiki/Division_(digital) を参照してください。
逆数が得られると、商Q = A * 1/Bになります。
残りのR = A-Q * B。
これがブルートフォースよりも速いかどうかを判断するには(32ビットレジスターを使用して64ビットと128ビットの数値をシミュレートするため、さらに多くの乗算があるため、プロファイリングします。
コード内でBが定数の場合、逆数を事前に計算し、最後の2つの式を使用して単純に計算できます。これは、ビットシフトよりも速いと確信しています。
お役に立てれば。
最近のx86マシンを使用している場合、SSE2 +用の128ビットレジスタがあります。基本的なx86以外のアセンブリを記述しようとしたことはありませんが、そこにいくつかのガイドがあると思います。
128ビットの符号なしと63ビットの符号なしで十分な場合は、最大63サイクルのループで実行できます。
これを1ビットに制限することにより、MSNのオーバーフロー問題を提案されたソリューションと考えてください。これを行うには、問題を2つのモジュラー乗算に分割し、最後に結果を追加します。
次の例では、上位が64ビットの最上位に対応し、下位が64ビットの下位に対応し、divは除数です。
unsigned 128_mod(uint64_t upper, uint64_t lower, uint64_t div) {
uint64_t result = 0;
uint64_t a = (~0%div)+1;
upper %= div; // the resulting bit-length determines number of cycles required
// first we work out modular multiplication of (2^64*upper)%div
while (upper != 0){
if(upper&1 == 1){
result += a;
if(result >= div){result -= div;}
}
a <<= 1;
if(a >= div){a -= div;}
upper >>= 1;
}
// add up the 2 results and return the modulus
if(lower>div){lower -= div;}
return (lower+result)%div;
}
唯一の問題は、除数が64ビットの場合、1ビットのオーバーフロー(情報の損失)が発生し、誤った結果が生じることです。
オーバーフローを処理するためのきちんとした方法を私が理解していないことは私を困惑させます。