web-dev-qa-db-ja.com

高速分割可能性テスト(2、3、4、5、..、16による)?

最速の分割可能性テストは何ですか?たとえば、リトルエンディアンアーキテクチャと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
34
psihodelia

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コンパイラに組み込まれています。

22
Olof Forshell

すべての場合(2で割り切れる数を含む):

if (number % n == 0) do();

また、下位ビットのマスクを使用することは難読化であり、最新のコンパイラを使用すると、コードを読み取り可能な方法で記述するよりも高速になることはありません。

すべてのケースをテストする必要がある場合は、一部のケースを別のケースのifに入れることで、パフォーマンスを向上させることができます。たとえば、2による分割がすでに失敗している場合、4による分割をテストしても意味がありません。 。

39
James Kanze

@ 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に注意してください)


編集:

21
kennytm

頬に少し舌がありますが、残りの答えが得られると仮定します:

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;
15
unpythonic

numberunsigned(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_の形式を取ります。ここで、ab巧妙に取得された定数 です。必要なのは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

10
Cassio Neri

これらの数値のLCMは720720のようです。非常に小さいため、単一の係数演算を実行し、残りを事前計算されたLUTのインデックスとして使用できます。

7
ruslik

まず、バイナリの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_であるため、各数式で各ビットを個別に取得する必要はありません。操作の明確さと類似性のためです。各式を最適化するには、自分で作業する必要があります。オペランドをグループ化し、演算を因数分解します。

7
Shahbaz

テストとして(i%N)== 0を使用する必要があります。

私のコンパイラー(かなり古いバージョンのgcc)は、私が試したすべてのケースに適したコードを生成しました。ビットテストが適切である場合、それはそれを行いました。 Nが定数の場合、どのケースでも明らかな「除算」は生成されず、常に「トリック」が使用されました。

コンパイラにコードを生成させるだけで、ほぼ確実に、あなたよりもマシンのアーキテクチャについて詳しく知ることができます:)そして、これらは、コンパイラよりも優れたものを考える可能性が低い簡単な最適化です。

しかし、それは興味深い質問です。別のコンピューターでコンパイルする必要があるため、各定数のコンパイラーが使用するトリックをリストすることはできません。

6
jcoder

これはおそらくコードでは役に立ちませんが、場合によっては頭の中でこれを行うのに役立つ巧妙なトリックがあります。

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で除算を確認できます。

5
Roddy

前の質問 で、ベースNをN-1の因数である約数をチェックする高速アルゴリズムを示しました。 2の異なる累乗間の基本変換は簡単です。それはほんの少しのグループ化です。

したがって、ベース4では3のチェックが簡単です。 5のチェックはbase16で簡単で、7(および9)のチェックはbase64で簡単です。

非プライム除数は自明であるため、11と13のみがハードケースです。 11の場合、基本1024を使用できますが、その時点では、小さい整数にはあまり効率的ではありません。

4
MSalters

除算は、基本的に除数の逆数を乗算する乗算によって、2の累乗でない定数で置き換えることができます。この方法で正確な結果を得るための詳細は複雑です。

Hacker's Delight は、これについて第10章で詳しく説明します(残念ながらオンラインでは利用できません)。

商から、別の乗算と減算で係数を取得できます。

3
starblue

考慮すべき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
...
_

ところで:上記のコードの最悪のケースは、32ビットの数値のいずれかのループを31回通過することです

参考:上記のMsalterの投稿をご覧ください。いくつかの素数には、mod(...)の代わりに彼の手法を使用できます。

3
Jim

すべての整数値のモジュロ削減に役立つ方法は、ビットスライスとポップカウントを使用します。

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   <-------> <------------> <----------->  <-----------------> <----------------->
3
Aki Suihkonen

分割可能性の高速テストは、数値が表されるベースに大きく依存します。底が2の場合、2の累乗で割り切れる「高速テスト」しかできないと思います。2進数は2で割り切れます。 その番号の最後のn個の2進数が0の場合。他のテストでは、通常、%よりも速いものを見つけることはできないと思います。

1
Armen Tsirunyan

少し邪悪で難読化されたビットをいじると、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です)

0
Daniel Martin