是否有按位技巧来检查数字被 2 或 3 整除

Is there a bit-wise trick for checking the divisibility of a number by 2 or 3?

本文关键字:数字 整除 检查 是否      更新时间:2023-10-16

>我正在寻找相当于(num%2) == 0 || (num%3) == 0的按位测试。

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

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

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

无论如何,16

n-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)可以获得的最高值。

您可以在使用乘法除以不变整数(T. Granlund和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

我想我参加这个派对有点晚了,但这里有一个比哈罗德的解决方案更快(也稍微漂亮一点)的解决方案:

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 位 - 开始时只需要一个额外的步骤。

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

可能的进一步优化:

  • 前两步中的&操作将通过在具有它们的架构(例如 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 的最接近的倍数少 1。

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