最速の分割可能性テストは何ですか?たとえば、リトルエンディアンアーキテクチャと32ビットの符号付き整数が与えられた場合、数値が2、3、4、5、...最大16で割り切れることを非常に高速に計算する方法は?
警告:指定されたコードは単なる例です。すべての行は独立しています! (多くのARMのように)DIVハードウェアを持たない多くのプロセッサでは、モジュロ演算を使用した明白な解決策は遅いです。一部のコンパイラは、そのような最適化を行うことができません(除数が関数の引数であるか、または何かに依存している場合など)。
Divisible_by_1 = do();
Divisible_by_2 = if (!(number & 1)) do();
Divisible_by_3 = ?
Divisible_by_4 = ?
Divisible_by_5 = ?
Divisible_by_6 = ?
Divisible_by_7 = ?
Divisible_by_8 = ?
Divisible_by_9 = ?
Divisible_by_10 = ?
Divisible_by_11 = ?
Divisible_by_12 = ?
Divisible_by_13 = ?
Divisible_by_14 = ?
Divisible_by_15 = ?
Divisible_by_16 = if(!number & 0x0000000F) do();
と特別な場合:
Divisible_by_2k = if(number & (tk-1)) do(); //tk=2**k=(2*2*2*...) k times
AT ALLは、除算命令(x86/x64でのモジュロを含む)の代替案を理解するのは非常に遅いため、ALLです。ほとんどの人が理解するよりも遅い(またははるかに遅い) 。nが変数である「%n」を提案するものは、常に除算命令の使用につながるため、愚かなアドバイスを与えています。一方、「%c」(cは定数)は、コンパイラが決定できるようにします。そのレパートリーで利用可能な最高のアルゴリズム。除算命令になることもありますが、そうでない場合もあります。
このドキュメント TorbjörnGranlundは、符号なし32ビットマルチ:divに必要なクロックサイクルの比率がSandybridgeで4:26(6.5x)、K10で3:45(15x)であることを示しています。 64ビットの場合、それぞれの比率は4:92(23x)と5:77(14.4x)です。
「L」列はレイテンシーを示します。 「T」列はスループットを示します。これは、複数の命令を並行して処理するプロセッサの機能と関係があります。 Sandybridgeは、1サイクルおきに32ビット乗算を1つ、またはサイクルごとに64ビット乗算を1つ発行できます。 K10の場合、対応するスループットは逆になります。分割の場合、K10は別のシーケンスを開始する前にシーケンス全体を完了する必要があります。 Sandybridgeでも同じだと思います。
K10を例として使用すると、32ビット除算(45)に必要なサイクル中に、同じ数(45)の乗算を発行でき、これらの最後から2番目と最後の1つが1つと2つを完了します。除算が完了した後のクロックサイクル。 45回の乗算で多くの作業を実行できます。
K8-K9からK10への進化に伴い、divの効率が低下していることにも注目してください。
Granlundの page gmplib.orgおよび Royal Institute of Technology ストックホルムには、さらに多くの機能が含まれており、その一部はgccコンパイラに組み込まれています。
すべての場合(2で割り切れる数を含む):
if (number % n == 0) do();
また、下位ビットのマスクを使用することは難読化であり、最新のコンパイラを使用すると、コードを読み取り可能な方法で記述するよりも高速になることはありません。
すべてのケースをテストする必要がある場合は、一部のケースを別のケースのif
に入れることで、パフォーマンスを向上させることができます。たとえば、2による分割がすでに失敗している場合、4による分割をテストしても意味がありません。 。
@ James で述べたように、コンパイラーに単純化させてください。 n
が定数の場合、すべての降下コンパイラーはパターンを認識し、より効率的な同等のものに変更できます。
たとえば、コード
#include <stdio.h>
int main() {
size_t x;
scanf("%u\n", &x);
__asm__ volatile ("nop;nop;nop;nop;nop;");
const char* volatile foo = (x%3 == 0) ? "yes" : "no";
__asm__ volatile ("nop;nop;nop;nop;nop;");
printf("%s\n", foo);
return 0;
}
g ++-4.5 -O3でコンパイルすると、x%3 == 0
の関連部分は
mov rcx,QWORD PTR [rbp-0x8] # rbp-0x8 = &x
mov rdx,0xaaaaaaaaaaaaaaab
mov rax,rcx
mul rdx
lea rax,"yes"
shr rdx,1
lea rdx,[rdx+rdx*2]
cmp rcx,rdx
lea rdx,"no"
cmovne rax,rdx
mov QWORD PTR [rbp-0x10],rax
これは、Cコードに変換して戻すと、
(hi64bit(x * 0xaaaaaaaaaaaaaaab) / 2) * 3 == x ? "yes" : "no"
// equivalatent to: x % 3 == 0 ? "yes" : "no"
ここには部門は含まれていません。 (0xaaaaaaaaaaaaaaab == 0x20000000000000001L/3
に注意してください)
編集:
頬に少し舌がありますが、残りの答えが得られると仮定します:
Divisible_by_6 = Divisible_by_3 && Divisible_by_2;
Divisible_by_10 = Divisible_by_5 && Divisible_by_2;
Divisible_by_12 = Divisible_by_4 && Divisible_by_3;
Divisible_by_14 = Divisible_by_7 && Divisible_by_2;
Divisible_by_15 = Divisible_by_5 && Divisible_by_3;
number
はunsigned
(32ビット)であると想定します。次に、16までの分割可能性を計算する非常に高速な方法を次に示します(測定していませんが、アセンブリコードはそのように示しています)。
_bool divisible_by_2 = number % 2 == 0;
bool divisible_by_3 = number * 2863311531u <= 1431655765u;
bool divisible_by_4 = number % 4 == 0;
bool divisible_by_5 = number * 3435973837u <= 858993459u;
bool divisible_by_6 = divisible_by_2 && divisible_by_3;
bool divisible_by_7 = number * 3067833783u <= 613566756u;
bool divisible_by_8 = number % 8 == 0;
bool divisible_by_9 = number * 954437177u <= 477218588u;
bool divisible_by_10 = divisible_by_2 && divisible_by_5;
bool divisible_by_11 = number * 3123612579u <= 390451572u;
bool divisible_by_12 = divisible_by_3 && divisible_by_4;
bool divisible_by_13 = number * 3303820997u <= 330382099u;
bool divisible_by_14 = divisible_by_2 && divisible_by_7;
bool divisible_by_15 = number * 4008636143u <= 286331153u;
bool divisible_by_16 = number % 16 == 0;
_
d
による分割可能性に関しては、次のルールが適用されます。
d
が2の累乗である場合:
指摘 by James Kanze なので、is_divisible_by_d = (number % d == 0)
を使用できます。コンパイラーはこれを_(number & (d - 1)) == 0
_として実装するのに十分賢いです。これは非常に効率的ですが難読化されています。
ただし、d
が2の累乗でない場合、上記の難読化は現在のコンパイラよりも効率的であるように見えます。 (これについては後で詳しく説明します)。
d
が奇数の場合:
この手法は_is_divisible_by_d = number * a <= b
_の形式を取ります。ここで、a
とb
は 巧妙に取得された定数 です。必要なのは1つの乗算と1つの比較だけであることに注意してください。
d
が偶数であるが2の累乗ではない場合:
次に、_d = p * q
_と記述します。ここで、p
は2の累乗であり、q
は奇数であり、 "舌の頬" を使用します npythonic 、つまり_is_divisible_by_d = is_divisible_by_p && is_divisible_by_q
_。この場合も、(_is_divisible_by_q
_の計算で)1回の乗算のみが実行されます。
多くのコンパイラー(clang 5.0.0、gcc 7.3、icc 18、およびmsvc 19を godbolt を使用してテストしました)は、_number % d == 0
_を_(number / d) * d == number
_に置き換えます。彼らは巧妙な手法( Olof Forshell 's answer のリファレンスを参照)を使用して、除算を乗算とビットシフトで置き換えています。彼らは2つの乗算を行うことになります。対照的に、上記の手法は1つの乗算のみを実行します。
2018年10月1日更新
上記のアルゴリズムがまもなくGCCに導入されるようです(すでにトランク内にあります)。
https://gcc.gnu.org/bugzilla/show_bug.cgi?id=8285
GCCの実装はさらに効率的だと思われます。実際、上記の実装には3つの部分があります。1)除数の偶数部分による分割可能性。 2)除数の奇数部分による分割可能性。 3)_&&
_は、前の2つのステップの結果を接続します。アセンブラ命令を使用する これは標準のC++では効率的に利用できません (ror
)、GCCは3つの部分を1つの部分にまとめます。これは、奇数部分による分割可能性と非常によく似ています。素晴らしいもの!この実装を利用できるようにするため、常に_%
_にフォールバックする方が(明快さとパフォーマンスの両方で)優れています。
Update 12-Dec-2019
このテーマに関する私の記事が公開されました: Quick Modular Calculations(Part 1)、Overload Journal 154、December 2019、pages 11-15 。
これらの数値のLCMは720720のようです。非常に小さいため、単一の係数演算を実行し、残りを事前計算されたLUTのインデックスとして使用できます。
まず、バイナリのbn ... b2b1b0の形式の数値には次の値があることを思い出してください。
_number = bn*2^n+...+b2*4+b1*2+b0
_
さて、number%3と言うと、次のようになります。
_number%3 =3= bn*(2^n % 3)+...+b2*1+b1*2+b0
_
(私は= 3 =を使用して3を法とする合同を示しました)。 _b1*2 =3= -b1*1
_にも注意してください
ここで、+と-を使用して16の除算すべてを記述し、場合によっては乗算を記述します(乗算は、異なる場所にシフトされた同じ値のシフトまたは合計として記述できることに注意してください。たとえば、_5*x
_はx+(x<<2)
を意味します。 x
を1回だけ計算する)
番号をn
と呼び、_Divisible_by_i
_がブール値であるとしましょう。中間値として、_Congruence_by_i
_がn
モジュロi
と合同な値であると想像してください。
また、_n0
_はnのビット0を意味し、_n1
_はビット1を意味するなどとしましょう。
_ni = (n >> i) & 1;
Congruence_by_1 = 0
Congruence_by_2 = n&0x1
Congruence_by_3 = n0-n1+n2-n3+n4-n5+n6-n7+n8-n9+n10-n11+n12-n13+n14-n15+n16-n17+n18-n19+n20-n21+n22-n23+n24-n25+n26-n27+n28-n29+n30-n31
Congruence_by_4 = n&0x3
Congruence_by_5 = n0+2*n1-n2-2*n3+n4+2*n5-n6-2*n7+n8+2*n9-n10-2*n11+n12+2*n13-n14-2*n15+n16+2*n17-n18-2*n19+n20+2*n21-n22-2*n23+n24+2*n25-n26-2*n27+n28+2*n29-n30-2*n31
Congruence_by_7 = n0+2*n1+4*n2+n3+2*n4+4*n5+n6+2*n7+4*n8+n9+2*n10+4*n11+n12+2*n13+4*n14+n15+2*n16+4*n17+n18+2*n19+4*n20+n21+2*n22+4*n23+n24+2*n25+4*n26+n27+2*n28+4*n29+n30+2*n31
Congruence_by_8 = n&0x7
Congruence_by_9 = n0+2*n1+4*n2-n3-2*n4-4*n5+n6+2*n7+4*n8-n9-2*n10-4*n11+n12+2*n13+4*n14-n15-2*n16-4*n17+n18+2*n19+4*n20-n21-2*n22-4*n23+n24+2*n25+4*n26-n27-2*n28-4*n29+n30+2*n31
Congruence_by_11 = n0+2*n1+4*n2+8*n3+5*n4-n5-2*n6-4*n7-8*n8-5*n9+n10+2*n11+4*n12+8*n13+5*n14-n15-2*n16-4*n17-8*n18-5*n19+n20+2*n21+4*n22+8*n23+5*n24-n25-2*n26-4*n27-8*n28-5*n29+n30+2*n31
Congruence_by_13 = n0+2*n1+4*n2+8*n3+3*n4+6*n5-n6-2*n7-4*n8-8*n9-3*n10-6*n11+n12+2*n13+4*n14+8*n15+3*n16+6*n17-n18-2*n19-4*n20-8*n21-3*n22-6*n3+n24+2*n25+4*n26+8*n27+3*n28+6*n29-n30-2*n31
Congruence_by_16 = n&0xF
_
または因数分解された場合:
_Congruence_by_1 = 0
Congruence_by_2 = n&0x1
Congruence_by_3 = (n0+n2+n4+n6+n8+n10+n12+n14+n16+n18+n20+n22+n24+n26+n28+n30)-(n1+n3+n5+n7+n9+n11+n13+n15+n17+n19+n21+n23+n25+n27+n29+n31)
Congruence_by_4 = n&0x3
Congruence_by_5 = n0+n4+n8+n12+n16+n20+n24+n28-(n2+n6+n10+n14+n18+n22+n26+n30)+2*(n1+n5+n9+n13+n17+n21+n25+n29-(n3+n7+n11+n15+n19+n23+n27+n31))
Congruence_by_7 = n0+n3+n6+n9+n12+n15+n18+n21+n24+n27+n30+2*(n1+n4+n7+n10+n13+n16+n19+n22+n25+n28+n31)+4*(n2+n5+n8+n11+n14+n17+n20+n23+n26+n29)
Congruence_by_8 = n&0x7
Congruence_by_9 = n0+n6+n12+n18+n24+n30-(n3+n9+n15+n21+n27)+2*(n1+n7+n13+n19+n25+n31-(n4+n10+n16+n22+n28))+4*(n2+n8+n14+n20+n26-(n5+n11+n17+n23+n29))
// and so on
_
これらの値が負になる場合は、正になるまでi
を追加します。
ここですべきことは、_Congruence_by_i
_がi
未満になるまで(そして明らかに_>= 0
_になるまで)、これらの値を同じプロセスで再帰的にフィードすることです。これは、3または9で余りを見つけたいときに行うことと似ています。覚えていますか?数字を合計します。複数の数字がある場合は、1桁だけになるまで、結果の数字をもう一度増やします。
_i = 1, 2, 3, 4, 5, 7, 8, 9, 11, 13, 16
_の場合:
_Divisible_by_i = (Congruence_by_i == 0);
_
そして残りのために:
_Divisible_by_6 = Divisible_by_3 && Divisible_by_2;
Divisible_by_10 = Divisible_by_5 && Divisible_by_2;
Divisible_by_12 = Divisible_by_4 && Divisible_by_3;
Divisible_by_14 = Divisible_by_7 && Divisible_by_2;
Divisible_by_15 = Divisible_by_5 && Divisible_by_3;
_
編集:追加の一部は最初から回避できることに注意してください。たとえば、_n0+2*n1+4*n2
_は_n&0x7
_と同じであり、同様に_n3+2*n4+4*n5
_は_(n>>3)&0x7
_であるため、各数式で各ビットを個別に取得する必要はありません。操作の明確さと類似性のためです。各式を最適化するには、自分で作業する必要があります。オペランドをグループ化し、演算を因数分解します。
テストとして(i%N)== 0を使用する必要があります。
私のコンパイラー(かなり古いバージョンのgcc)は、私が試したすべてのケースに適したコードを生成しました。ビットテストが適切である場合、それはそれを行いました。 Nが定数の場合、どのケースでも明らかな「除算」は生成されず、常に「トリック」が使用されました。
コンパイラにコードを生成させるだけで、ほぼ確実に、あなたよりもマシンのアーキテクチャについて詳しく知ることができます:)そして、これらは、コンパイラよりも優れたものを考える可能性が低い簡単な最適化です。
しかし、それは興味深い質問です。別のコンピューターでコンパイルする必要があるため、各定数のコンパイラーが使用するトリックをリストすることはできません。
これはおそらくコードでは役に立ちませんが、場合によっては頭の中でこれを行うのに役立つ巧妙なトリックがあります。
3で割る場合:10進数で表される数値の場合、すべての桁を合計し、合計が3で割り切れるかどうかを確認できます。
例:12345 => 1+2+3+4+5 = 15 => 1+5 = 6
、3で割り切れる(3 x 4115 = 12345)
。
さらに興味深いことに、同じ手法がX-1のすべての要素に対して機能します。ここで、Xは数値が表されるベースです。したがって、10進数の場合は3または9で除算を確認できます。16進数の場合は3、5または15で除算を確認できます。8進数の場合は7で除算を確認できます。
前の質問 で、ベースNをN-1の因数である約数をチェックする高速アルゴリズムを示しました。 2の異なる累乗間の基本変換は簡単です。それはほんの少しのグループ化です。
したがって、ベース4では3のチェックが簡単です。 5のチェックはbase16で簡単で、7(および9)のチェックはbase64で簡単です。
非プライム除数は自明であるため、11と13のみがハードケースです。 11の場合、基本1024を使用できますが、その時点では、小さい整数にはあまり効率的ではありません。
除算は、基本的に除数の逆数を乗算する乗算によって、2の累乗でない定数で置き換えることができます。この方法で正確な結果を得るための詳細は複雑です。
Hacker's Delight は、これについて第10章で詳しく説明します(残念ながらオンラインでは利用できません)。
商から、別の乗算と減算で係数を取得できます。
考慮すべき1つのこと:16までの分割可能性のみを気にするので、実際には16までの素数による分割可能性をチェックするだけで済みます。これらは2、3、5、7、11、および13です。
ブール値(div2 = trueなど)で追跡しながら、各素数で数値を割ります。 2番と3番は特殊なケースです。 div3がtrueの場合は、div3でもう一度除算して、div9を設定してみてください。 2つとその能力は非常に単純です(注:「&」はプロセッサーが実行できる最速のことの1つです):
_if n & 1 == 0:
div2 = true
if n & 3 == 0:
div4 = true
if n & 7 == 0:
div8 = true
if n & 15 == 0:
div16 = true
_
これで、ブール値div2、div3、div4、div5、div7、div8、div9、div11、div13、およびdiv16ができました。他のすべての番号は組み合わせです。たとえば、div6は(div2 && div3)と同じです。
したがって、5つまたは6つの実際の除算を行うだけで済みます(6は、数値が3で割り切れる場合のみ)。
私自身は、ブール値の単一レジスタのビットを使用する可能性があります。たとえば、bit_0はdiv2を意味します。その後、マスクを使用できます。
if (flags & (div2+div3)) == (div2 + div3): do_6()
div2 + div3は事前計算された定数である場合があることに注意してください。 div2がbit0で、div3がbit1の場合、div2 + div3 == 3です。これにより、上記の「if」が次のように最適化されます。
if (flags & 3) == 3: do_6()
だから今...除算なしのmod:
_def mod(n,m):
i = 0
while m < n:
m <<= 1
i += 1
while i > 0:
m >>= 1
if m <= n: n -= m
i -= 1
return n
div3 = mod(n,3) == 0
...
_
参考:上記のMsalterの投稿をご覧ください。いくつかの素数には、mod(...)の代わりに彼の手法を使用できます。
すべての整数値のモジュロ削減に役立つ方法は、ビットスライスとポップカウントを使用します。
mod3 = pop(x & 0x55555555) + pop(x & 0xaaaaaaaa) << 1; // <- one term is shared!
mod5 = pop(x & 0x99999999) + pop(x & 0xaaaaaaaa) << 1 + pop(x & 0x44444444) << 2;
mod7 = pop(x & 0x49249249) + pop(x & 0x92492492) << 1 + pop(x & 0x24924924) << 2;
modB = pop(x & 0x5d1745d1) + pop(x & 0xba2e8ba2) << 1 +
pop(x & 0x294a5294) << 2 + pop(x & 0x0681a068) << 3;
modD = pop(x & 0x91b91b91) + pop(x & 0xb2cb2cb2) << 1 +
pop(x & 0x64a64a64) << 2 + pop(x & 0xc85c85c8) << 3;
これらの変数の最大値は48、80、73、168、および203で、すべて8ビット変数に適合します。 2回目のラウンドは並行して実行できます(またはいくつかのLUTメソッドを適用できます)
mod3 mod3 mod5 mod5 mod5 mod7 mod7 mod7 modB modB modB modB modD modD modD modD
mask 0x55 0xaa 0x99 0xaa 0x44 0x49 0x92 0x24 0xd1 0xa2 0x94 0x68 0x91 0xb2 0x64 0xc8
shift *1 *2 *1 *2 *4 *1 *2 *4 *1 *2 *4 *8 *1 *2 *4 *8
sum <-------> <------------> <-----------> <-----------------> <----------------->
分割可能性の高速テストは、数値が表されるベースに大きく依存します。底が2の場合、2の累乗で割り切れる「高速テスト」しかできないと思います。2進数は2で割り切れます。ん その番号の最後のn個の2進数が0の場合。他のテストでは、通常、%
よりも速いものを見つけることはできないと思います。
少し邪悪で難読化されたビットをいじると、15で分割可能になります。
32ビットの符号なし数値の場合:
_def mod_15ish(unsigned int x) {
// returns a number between 0 and 21 that is either x % 15
// or 15 + (x % 15), and returns 0 only for x == 0
x = (x & 0xF0F0F0F) + ((x >> 4) & 0xF0F0F0F);
x = (x & 0xFF00FF) + ((x >> 8) & 0xFF00FF);
x = (x & 0xFFFF) + ((x >> 16) & 0xFFFF);
// *1
x = (x & 0xF) + ((x >> 4) & 0xF);
return x;
}
def Divisible_by_15(unsigned int x) {
return ((x == 0) || (mod_15ish(x) == 15));
}
_
_3
_に基づいて、_5
_および_mod_15ish
_に対して同様の分割可能性ルーチンを構築できます。
処理する64ビットのunsignedintがある場合は、各定数を_*1
_行の上に明白な方法で拡張し、_*1
_行の上に行を追加して、32ビットで右シフトを実行します。 _0xFFFFFFFF
_のマスク。 (最後の2行は同じままにすることができます)_mod_15ish
_は同じ基本コントラクトに従いますが、戻り値は_0
_と_31
_の間になります。 (つまり、維持されるのは_x % 15
_ == mod_15ish(x) % 15
です)