【问题标题】:Is there a bit-wise trick for checking the divisibility of a number by 2 or 3?检查一个数字是否可以被 2 或 3 整除有一点技巧吗?
【发布时间】:2015-03-06 23:15:20
【问题描述】:

我正在寻找等效于(num%2) == 0 || (num%3) == 0 的逐位测试。

我可以用num&1 替换num%2,但我仍然坚持使用num%3 和逻辑或。

这个表达式也等同于(num%2)*(num%3) == 0,但我不确定它有什么帮助。

【问题讨论】:

  • 我认为:对于 3,代码可以检查 &0x03, &0x06, &0x09, &0x0C, &0x0F 应该捕获所有可以被 3 整除的值
  • @user3629249 0x12 呢?
  • 请注意,具有常数除数的模运算可以非常有效地实现,并且常用的编译器已经包含了这些有效的方法。如果num 是无符号整数类型,生成的代码可能会快一点。您可能希望检查二进制可执行文件的反汇编(例如通过objdump)以确认。如果速度是目标,您可能还想考虑使用逻辑或| 而不是布尔或||
  • @njuffa:是的,我现在看到了((num%2)|(num%3)) == 0 选项。好主意,谢谢(介意我将其编辑到问题中吗?)。至于另一个建议,我可以在调试代码时直接查看反汇编,但我正在寻找能够不管底层架构(编译器+硬件)工作的东西。当然,一些 DSP 可能会在硬件级别上支持这一点(因此,与它们一起提供的编译器也会支持),但它是一个依赖于平台的解决方案。我正在寻找一个不可知的(跨平台)解决方案。
  • @njuffa 大多数编译器使用古老的“除以常数”技巧,然后乘回并比较数字是否与以前相同。它肯定比使用实际除法要快,但我从未见过编译器正确地做到这一点。我已经在 GCC、Clang 和 ICC 上尝试过。

标签: java python c++ c bit-manipulation


【解决方案1】:

是的,虽然它不是很漂亮,但您可以做一些类似于旧的“将所有十进制数字相加,直到只剩下一个”的技巧来测试一个数字是否可以被 9 整除,除了二进制和可被 9 整除3. 您也可以对其他数字使用相同的原理,但是基数/除数的许多组合会引入烦人的比例因子,因此您不再只是对数字求和。

无论如何,16n-1 可以被 3 整除,所以可以使用基数 16,即对半字节求和。然后你只剩下一个半字节(嗯,真的是 5 位),你可以查一下。因此,例如在 C#(略微测试)中编辑:蛮力测试,绝对有效

static bool IsMultipleOf3(uint x)
{
    const uint lookuptable = 0x49249249;
    uint t = (x & 0x0F0F0F0F) + ((x & 0xF0F0F0F0) >> 4);
    t = (t & 0x00FF00FF) + ((t & 0xFF00FF00) >> 8);
    t = (t & 0x000000FF) + ((t & 0x00FF0000) >> 16);
    t = (t & 0xF) + ((t & 0xF0) >> 4);
    return ((lookuptable >> (int)t) & 1) != 0;
}

我的评论x * 0xaaaaaaab <= 0x55555555 中的技巧通过模乘逆技巧起作用。 0xaaaaaaab * 3 = 1 mod 232,这意味着0xaaaaaaab * x = x / 3当且仅当
x % 3 = 0。 "if" 因为0xaaaaaaab * 3 * y = y(因为1 * y = y),所以如果x 的形式为
3 * y,那么它将映射回y。 “仅当”,因为没有两个输入映射到相同的输出,所以不能被 3 整除的所有内容都将映射到高于通过将任何内容除以 3 可以获得的最高值(即0xFFFFFFFF / 3 = 0x55555555)。

您可以在Division by Invariant Integers using Multiplication (T. Granlund and P. L. Montgomery) 中阅读更多相关信息(包括更通用的形式,包括轮换)。

你的编译器可能不知道这个技巧。例如:

uint32_t foo(uint32_t x)
{
    return x % 3 == 0;
}

在适用于 x64 的 Clang 3.4.1 上,

movl    %edi, %eax
movl    $2863311531, %ecx       # imm = 0xAAAAAAAB
imulq   %rax, %rcx
shrq    $33, %rcx
leal    (%rcx,%rcx,2), %eax
cmpl    %eax, %edi
sete    %al
movzbl  %al, %eax
ret

G++ 4.8:

mov eax, edi
mov edx, -1431655765
mul edx
shr edx
lea eax, [rdx+rdx*2]
cmp edi, eax
sete    al
movzx   eax, al
ret

应该是什么:

imul eax, edi, 0xaaaaaaab
cmp eax, 0x55555555
setbe al
movzx eax, al
ret

【讨论】:

  • 是的,确实不是很漂亮。我将对它进行一些测试。谢谢:)
  • @barakmanos 您可以通过乘法、旋转(在这种情况下实际上没有旋转,但通常可能有旋转)和比较来做一些更漂亮的事情——但这不是按位计算的。你对它感兴趣吗?
  • 只要它不包含循环 - 是的(我没有在问题中写它以保持清晰和简单)。谢谢。
  • 是同时捕获 2 和 3,还是仅捕获 3?另外,您确定没有任何东西可以“以某种方式”捕获 GCD 为 6 大于 1 的任何数字吗?
  • 嗯,这只是为了被3整除,我不知道如何将它们组合成一件事,会考虑一下
【解决方案2】:

我想我参加这个聚会有点晚了,但这里有一个比 harold 的解决方案更快(也更漂亮)的解决方案:

bool is_multiple_of_3(std::uint32_t i)
{
    i = (i & 0x0000FFFF) + (i >> 16);
    i = (i & 0x00FF) + (i >> 8);
    i = (i & 0x0F) + (i >> 4);
    i = (i & 0x3) + (i >> 2);
    const std::uint32_t lookuptable = 0x49249249;
    return ((lookuptable >> i) & 1) != 0;
}

它是 C++11,但这对于这段代码并不重要。它还针对 32 位无符号整数进行了蛮力测试。对于前四个步骤中的每一步,它至少为您节省了一个位摆弄操作。它还可以完美地扩展到 64 位 - 一开始只需要一个额外的步骤。

最后两行显然无耻地取自 harold 的解决方案(很好,我不会这么优雅地这样做)。

可能的进一步优化:

  • 前两步中的 & 操作将被优化掉,只需在具有它们的架构(例如 x86)上使用低半寄存器。
  • 第三步最大可能的输出是60,第四步是15(当函数参数是0xFFFFFFFF时)。鉴于此,我们可以省略第四步,使用 64 位 lookuptable 并直接转移到第三步之后的那个。这对于 32 位模式下的 Visual C++ 2013 来说是个坏主意,因为右移变成了对执行大量测试和跳转的代码的非内联调用。但是,如果 64 位寄存器本身可用,那应该是个好主意。
  • 如果函数被修改为采用 64 位参数,则需要重新评估上述要点。最后两步的最大输出(即在开头添加一个步骤后的第 4 步和第 5 步)将分别为 7521,这意味着我们不能再消除最后一步。

前四步是基于一个32位数字可以写成这样的事实

(high 16 bits) * 65536 + (low 16 bits) = 
(high 16 bits) * 65535 + (high 16 bits) + (low 16 bits) = 
(high 16 bits) * 21845 * 3 + ((high 16 bits) + (low 16 bits))

所以当且仅当右括号能被 3 整除时,整个东西才能被 3 整除。依此类推,因为这适用于 256 = 85 * 3 + 116 = 5 * 3 + 14 = 3 + 1。 (当然,这通常适用于 2 的偶数次幂;奇数次幂比最接近的 3 的倍数小一。)

在某些情况下,输入到以下步骤中的数字将分别大于 16 位、8 位和 4 位,但这不是问题,因为我们不会丢弃任何高位右移时。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2020-10-23
    • 1970-01-01
    • 2015-01-26
    • 2010-10-25
    • 1970-01-01
    • 1970-01-01
    • 2011-06-26
    • 2011-01-06
    相关资源
    最近更新 更多