快速可分性测试(2,3,4,5,...,16)?

时间:2011-08-01 09:32:57

标签: c++ c math assembly bit-manipulation

最快的可分性测试是什么?比如说,给定一个小端架构和一个32位有符号整数:如何快速计算一个数字可以被2,3,4,5整除,......最多16?

警告:给定的代码仅为示例。每一行都是独立的!使用模运算的明显解决方案在许多处理器上都很慢,这些处理器没有DIV硬件(像许多ARM一样)。有些编译器也无法进行这样的优化(例如,如果除数是函数的参数或依赖于某些东西)。

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

16 个答案:

答案 0 :(得分:39)

在每种情况下(包括2整除):

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

使用低阶位掩码进行Anding只是模糊处理,使用现代编译器并不比以可读方式编写代码更快。

如果你必须测试所有案例,你可以通过将if中的一些案例放在另一个案例中来提高绩效:如果2的可分性已经失败,那么它就没有必要测试可分性4,例如。

答案 1 :(得分:22)

在所有人中找出除法指令的替代品(包括x86 / x64上的模数)并不是一个坏主意,因为它们非常慢。比大多数人意识到的更慢(甚至更慢)。那些暗示“%n”,其中n是变量的人给出了愚蠢的建议,因为它总是会导致使用除法指令。另一方面,“%c”(其中c是常量)将允许编译器确定其库中可用的最佳算法。有时它会是分裂指令,但很多时候它不会。

this documentTorbjörnGranlund显示无符号32位多元化所需的时钟周期比率:在Sandybridge上为4:26(6.5x),在K10上为3:45(15x)。对于64位,各自的比率是4:92(23x)和5:77(14.4x)。

“L”列表示延迟。 “T”列表示吞吐量。这与处理器并行处理多个指令的能力有关。 Sandybridge可以每隔一个周期发出一个32位乘法,或者每个周期发出一个64位乘法。对于K10,相应的吞吐量相反。对于分区,K10需要在它开始另一个序列之前完成整个序列。我怀疑Sandybridge是一样的。

使用K10作为示例意味着在32位除法(45)所需的周期期间,可以发出相同数量(45)的乘法,并且倒数第二个和最后一个将完成分割完成后的一个和两个时钟周期。可以在45次乘法中完成大量工作。

值得注意的是,随着从K8-K9到K10的演变,div的效率降低:从32到64位的39到45和71到77个时钟周期。

gmplib.org上的Granlund page和斯德哥尔摩的Royal Institute of Technology包含更多好东西,其中一些已经合并到gcc编译器中。

答案 2 :(得分:21)

如同@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


修改

答案 3 :(得分:15)

脸上有点舌头,但假设你得到了其余的答案:

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;

答案 4 :(得分:8)

假设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的幂:

    pointed out作为James Kanze,您可以使用is_divisible_by_d = (number % d == 0)。编译器非常聪明,可以将其实现为(number & (d - 1)) == 0,这非常有效但是模糊不清。

然而,当d不是2的幂时,看起来上面显示的混淆比当前编译器更有效。 (稍后会详细介绍)。

  • d为奇数时:

    该技术采用is_divisible_by_d = number * a <= b形式,其中abcleverly obtained constants。请注意,我们所需要的只是1次乘法和1次比较:

  • d是偶数但不是2的幂:

    然后,写d = p * q,其中p是2的幂,q是奇数,并使用"tongue in cheek"建议的unpythonic,即{ {1}}。同样,只执行1次乘法(在is_divisible_by_d = is_divisible_by_p && is_divisible_by_q的计算中)。

许多编译器(我使用godbolt测试了clang 5.0.0,gcc 7.3,icc 18和msvc 19)将is_divisible_by_q替换为number % d == 0。他们使用一种聪明的技术(参见Olof Forshellanswer中的引用)来通过乘法和位移来代替除法。他们最终做了2次乘法。相比之下,上述技术仅执行1次乘法。

2018年10月1日更新

看起来上面的算法很快就会进入GCC(已经在主干中):

https://gcc.gnu.org/bugzilla/show_bug.cgi?id=82853

海湾合作委员会的实施似乎更有效率。实际上,上面的实现有三个部分:1)除数的偶数部分的可分性; 2)除数的奇数部分的可除性; 3)(number / d) * d == number连接前两个步骤的结果。通过使用在标准C ++ (&&)中无效的汇编指令,GCC将这三个部分组合成一个部分,这与奇数部分的可分性非常相似。好东西!有了这个实现,最好(为了清晰度和性能)一直回到ror

答案 5 :(得分:7)

这些数字的LCM似乎是720720.它非常小,因此您可以执行单模运算并将余数用作预计算LUT中的索引。

答案 6 :(得分:7)

首先,我提醒你,二进制形式的bn ... b2b1b0中的数字有值:

number = bn*2^n+...+b2*4+b1*2+b0

现在,当你说数字%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一次)

让我们调用数字n,让我们说Divisible_by_i是一个布尔值。作为中间值,假设Congruence_by_i是与ni一致的值。

另外,假设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的数字的余数时的情况类似,还记得吗?总结数字,如果它有多个数字,那么一些数字会再次增加结果的数字,直到你只得到一位数。

现在为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*n2n&0x7相同,同样n3+2*n4+4*n5(n>>3)&0x7,因此对于每个公式,您不必单独获取每个位,我写的就像为了操作的清晰和相似性。要优化每个公式,您应该自己处理它;集团操作数和分解操作。

答案 7 :(得分:6)

您应该使用(i%N)== 0作为测试。

我的编译器(一个相当旧版本的gcc)为我尝试的所有案例生成了良好的代码。 比特测试是合适的,它就是这样做的。在N是常数的情况下,它没有为任何情况产生明显的“鸿沟”,它总是使用一些“技巧”。

让编译器为您生成代码,它几乎肯定会比您更了解机器的体系结构:)这些都是简单的优化,您不太可能想到比编译器更好的东西。< / p>

但这是一个有趣的问题。我无法列出编译器为每个常量使用的技巧,因为我必须在另一台计算机上编译。但是如果没有人打败我,我会在稍后更新此回复:)

答案 8 :(得分:5)

这可能对代码没有帮助,但在某些情况下,有一个巧妙的技巧可以帮助你做到这一点:

除以3:对于以十进制表示的数字,您可以对所有数字求和,并检查总和是否可被3整除。

示例:12345 => 1+2+3+4+5 = 15 => 1+5 = 6,可被3 (3 x 4115 = 12345)整除。

更有趣的是,相同的技术适用于X-1的所有因子,其中X是表示数字的基础。因此,对于十进制数,您可以检查除以3或9.对于十六进制,您可以检查除以3,5或15.对于八进制数,您可以检查除以7。

答案 9 :(得分:4)

previous question中,我展示了一种快速算法来检查基数N中是否为N-1因子的除数。不同2的幂之间的基本变换是微不足道的;那只是一点点分组。

因此,在4号基地检查3很容易;在基数16中检查5很容易,在基数64中检查7(和9)很容易。

非素数除数是微不足道的,所以只有11和13个是困难的。对于11,你可以使用1024,但在那时它对小整数来说效率不高。

答案 10 :(得分:3)

你可以通过乘法用非幂二乘法代替除法,基本上乘以你的除数的倒数。通过这种方法获得精确结果的细节很复杂。

Hacker's Delight在第10章详细讨论了这个问题(遗憾的是,网上没有提供)。

你可以通过另一次乘法和减法得到模数。

答案 11 :(得分:3)

要考虑的一件事:因为你只关心16岁以下的可分性,你真的只需要通过最多16个素数来检查可分性。这些是2,3,5,7,11和13。

用每个素数除以你的数字,用布尔值跟踪(例如div2 = true)。第二和第三是特殊情况。如果div3为真,请再次尝试除以3,设置div9。两个和它的功能非常简单(注意:'&amp;'是处理器可以做的最快的事情之一):

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&amp;&amp; div3)

相同

因此,您只需要进行5或6个实际分割(仅当您的数字可被3整除时才为6)。

对于我自己,我可能会在一个寄存器中使用位来获取我的布尔值;例如 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
...

btw:对于32位数字,上述代码的最坏情况是通过任一循环的31次

仅供参考:上面看了Msalter的帖子。对于某些素数,他的技术可以用来代替mod(...)。

答案 12 :(得分:2)

可以帮助模数减少所有整数值的方法使用位切片和popcount。

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位变量。第二轮可以并行进行(或者可以应用一些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   <-------> <------------> <----------->  <-----------------> <----------------->

答案 13 :(得分:1)

快速的可分性测试在很大程度上取决于数字代表的基数。如果base是2,我认为你只能通过2的幂来进行“快速测试”。二进制数可以被2 n 整除,如果该数字的最后n个二进制数字是0对于其他测试,我认为通常不会发现比%更快的任何内容。

答案 14 :(得分: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));
}

您可以根据35mod_15ish建立类似的可分性例程。

如果您要处理64位无符号整数,请以显而易见的方式将每个常量扩展到*1行以上,并在*1行上方添加一行以进行右移32掩码为0xFFFFFFFF的位。 (最后两行可以保持不变)mod_15ish然后服从相同的基本合同,但返回值现在介于031之间。 (所以维持的是x % 15 == mod_15ish(x) % 15

答案 15 :(得分:-1)

以下是我还没有看到其他人建议的一些提示:

一个想法是使用switch语句,或预先计算一些数组。然后,任何体面的优化器都可以直接索引每个案例。例如:

// tests for (2,3,4,5,6,7)
switch (n % 8)
{
case 0: break;
case 1: break;
case 2: do(2); break;
case 3: do(3); break;
case 4: do(2); do(4) break;
case 5: do(5); break;
case 6: do(2); do(3); do(4); break;
case 7: do(7); break;
} 

您的应用程序有点模棱两可,但您可能只需要检查小于n = 16的素数。这是因为所有数字都是当前或以前的素数的因子。因此,对于n = 16,您可能只能以某种方式检查2, 3, 5, 7, 11, 13。只是一个想法。