快速计算64位整数的log2

Des*_*ume 45 c lookup 64-bit bit-manipulation 32bit-64bit

一个伟大的编程资源,Bit Twiddling Hacks,提出(这里)以下方法来计算32位整数的log2:

#define LT(n) n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n
static const char LogTable256[256] = 
{
    -1, 0, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3,
    LT(4), LT(5), LT(5), LT(6), LT(6), LT(6), LT(6),
    LT(7), LT(7), LT(7), LT(7), LT(7), LT(7), LT(7), LT(7)
};

unsigned int v; // 32-bit word to find the log of
unsigned r;     // r will be lg(v)
register unsigned int t, tt; // temporaries
if (tt = v >> 16)
{
    r = (t = tt >> 8) ? 24 + LogTable256[t] : 16 + LogTable256[tt];
}
else 
{
    r = (t = v >> 8) ? 8 + LogTable256[t] : LogTable256[v];
}
Run Code Online (Sandbox Code Playgroud)

并提到这一点

查找表方法仅需要大约7次操作即可查找32位值的日志.如果扩展为64位数量,则大约需要9次操作.

但是,唉,没有给出关于将算法实际扩展到64位整数的实际方式的任何其他信息.

关于这种64位算法如何看起来的任何提示?

Des*_*ume 66

内部函数非常快,但仍然不足以实现真正的跨平台,独立于编译器的log2实现.因此,如果有人感兴趣,这里是我自己研究这个主题时遇到的最快,无分支,CPU抽象的DeBruijn算法.

const int tab64[64] = {
    63,  0, 58,  1, 59, 47, 53,  2,
    60, 39, 48, 27, 54, 33, 42,  3,
    61, 51, 37, 40, 49, 18, 28, 20,
    55, 30, 34, 11, 43, 14, 22,  4,
    62, 57, 46, 52, 38, 26, 32, 41,
    50, 36, 17, 19, 29, 10, 13, 21,
    56, 45, 25, 31, 35, 16,  9, 12,
    44, 24, 15,  8, 23,  7,  6,  5};

int log2_64 (uint64_t value)
{
    value |= value >> 1;
    value |= value >> 2;
    value |= value >> 4;
    value |= value >> 8;
    value |= value >> 16;
    value |= value >> 32;
    return tab64[((uint64_t)((value - (value >> 1))*0x07EDD5E59A4E28C2)) >> 58];
}
Run Code Online (Sandbox Code Playgroud)

向下舍入到下一个2的较低功率的部分取自2次幂边界,并且从BitScan获取尾随零的数量的部分(其中的(bb & -bb)代码是单个最右边的位,设置为1,在我们将值四舍五入到2的下一个幂之后不需要.

顺便说一句,32位实现是

const int tab32[32] = {
     0,  9,  1, 10, 13, 21,  2, 29,
    11, 14, 16, 18, 22, 25,  3, 30,
     8, 12, 20, 28, 15, 17, 24,  7,
    19, 27, 23,  6, 26,  5,  4, 31};

int log2_32 (uint32_t value)
{
    value |= value >> 1;
    value |= value >> 2;
    value |= value >> 4;
    value |= value >> 8;
    value |= value >> 16;
    return tab32[(uint32_t)(value*0x07C4ACDD) >> 27];
}
Run Code Online (Sandbox Code Playgroud)

与任何其他计算方法一样,log2要求输入值大于零.

  • @DesmondHume - 另一个观察:不是让`tab64 []`表包含`int`值,为什么不将它包含8位值并将表指定为`const int8_t tab64 [64]`...这样,该表只需要64个字节(而不是256个字节).此外,如果包含`__attribute __((aligned(64)))`,那么它也可以保证适合现代处理器上的单个缓存行.如当前所写(具有256个字节且没有对齐),该表可以使用多达5个高速缓存行. (4认同)
  • 为了帮助在便携性和速度之间进行权衡,在英特尔(R)Xeon(R)CPU X5650 @ 2.67GHz**上,查找表平均比内在慢约4倍(约13个周期对比4个周期) ) (3认同)
  • @ArjunShankar不客气。但是,我仍然认为,有一种方法可以通过生成另一个完美的哈希函数来消除表查找行中的这两个操作,即减法和右移。只要我的主要编译器是MSVC和GCC,就不知道我是否有足够的时间来追求zen;) (2认同)

kay*_*kay 46

如果您使用的是GCC,则在这种情况下不需要查找表.

GCC提供内置函数来确定前导零的数量:

内置函数: 从最高位开始,返回x中前导0位的数量.如果x为0,则结果未定义.int __builtin_clz (unsigned int x)

所以你可以定义:

#define LOG2(X) ((unsigned) (8*sizeof (unsigned long long) - __builtin_clzll((X)) - 1))
Run Code Online (Sandbox Code Playgroud)

它适用于任何unsigned long long int.结果向下舍入.

对于x86和AMD64,GCC会将其编译为bsr指令,因此解决方案非常快(比查找表快得多).

工作范例:

#include <stdio.h>

#define LOG2(X) ((unsigned) (8*sizeof (unsigned long long) - __builtin_clzll((X)) - 1))

int main(void) {
    unsigned long long input;
    while (scanf("%llu", &input) == 1) {
        printf("log(%llu) = %u\n", input, LOG2(input));
    }
    return 0;
}
Run Code Online (Sandbox Code Playgroud)

  • @DesmondHume __builtin_clz不是特定于处理器的.GCC将找到一套适用于任何支持架构的指令. (10认同)
  • 如何处理“如果 x 为 0,结果未定义”。在你的例子中?:) (2认同)
  • @ArjunShankar 实际上我考虑过这个问题,但想不出适合这种情况的合适整数。;) 我留给感兴趣的读者向宏添加 if-else 情况。(此外,只有结果是未定义的 [很可能是 0],但如果提供 0,则不会发生崩溃。) (2认同)

Ave*_*nar 16

我试图将O(lg(N))操作中的N位整数的日志基数2转换为乘法,并通过暴力强制查找幻数来查找 64位.不用说它需要一段时间.

然后我找到了德斯蒙德的答案,并决定尝试将他的幻数作为起点.由于我有一个6核处理器,我从0x07EDD5E59A4E28C2/6倍数并行运行它.我很惊讶它立即找到了一些东西.结果是0x07EDD5E59A4E28C2/2工作.

所以这里是0x07EDD5E59A4E28C2的代码,它可以为你节省一个移位和减法:

int LogBase2(uint64_t n)
{
    static const int table[64] = {
        0, 58, 1, 59, 47, 53, 2, 60, 39, 48, 27, 54, 33, 42, 3, 61,
        51, 37, 40, 49, 18, 28, 20, 55, 30, 34, 11, 43, 14, 22, 4, 62,
        57, 46, 52, 38, 26, 32, 41, 50, 36, 17, 19, 29, 10, 13, 21, 56,
        45, 25, 31, 35, 16, 9, 12, 44, 24, 15, 8, 23, 7, 6, 5, 63 };

    n |= n >> 1;
    n |= n >> 2;
    n |= n >> 4;
    n |= n >> 8;
    n |= n >> 16;
    n |= n >> 32;

    return table[(n * 0x03f6eaf2cd271461) >> 58];
}
Run Code Online (Sandbox Code Playgroud)

  • 它对于除 0 之外的所有输入都是正确的。对于 0,它返回 0,这可能对您使用它的用途有效。移位的行将 n 舍入到小于下一个 2 的幂的 1。它基本上将前导 1 位之后的所有位设置为 1。这将所有可能的输入减少到 64 个可能的值:0x0、0x1、0x3、0x7、0xf 、0x1f、0x3f 等。将这 64 个值与数字 0x03f6eaf2cd271461 相乘,即可在前 6 位中得到另外 64 个唯一值。移位 58 只是将这 6 位定位为用作表中的索引。 (3认同)
  • 我想这是可行的,因为你的“神奇数字”`m`是一个[DeBruin序列](https://en.wikipedia.org/wiki/De_Bruijn_sequence) B(2, 6),以六个`0`开头,后跟六个“1”。对于这样的序列,乘以“00011111”形式的“n”而不是“00010000”仍然有效。现在的运算是“m * (2^(n+1) - 1)”或“m * 2^(n+1) - m”,而不是“m * 2^n”。由于序列以六个“0”开头,乘以“2^(n+1)”仍然会产生完美的哈希值。由于接下来有六个“1”,减法*总是*将进位位传播到结果的高六位中。 (3认同)

Tod*_*man 8

Base-2整数对数

这是我为64位无符号整数所做的.这计算base-2对数的下限,相当于最高位的索引.这种方法对于大数字来说非常快,因为它使用的是一个展开的循环,它始终以log264 = 6步执行.

从本质上讲,它所做的是逐渐减去序列{0≤k≤5:2 ^(2 ^ k)} = {2³²,2¹⁶,2⁸,2⁴,2²,2¹} = {4294967296,65536,256 ,16,4,2,1}并且对减去的值的指数k求和.

int uint64_log2(uint64_t n)
{
  #define S(k) if (n >= (UINT64_C(1) << k)) { i += k; n >>= k; }

  int i = -(n == 0); S(32); S(16); S(8); S(4); S(2); S(1); return i;

  #undef S
}
Run Code Online (Sandbox Code Playgroud)

请注意,如果给出无效输入0(这是初始-(n == 0)检查的内容),则返回-1 .如果您从未期望调用它n == 0,则可以替换int i = 0;初始化程序并assert(n != 0);在函数入口处添加.

基数为10的整数对数

基数为10的整数对数可以使用类似的方法计算 - 最大的平方测试为10 15,因为log 102⁶⁴⁶⁴19.2659...(注意:这不是实现基数10整数对数的最快方法,因为它使用整数除法,更快的实现方法是使用具有指数增长值的累加器,并与累加器进行比较,实际上是进行一种二分搜索.)

int uint64_log10(uint64_t n)
{
  #define S(k, m) if (n >= UINT64_C(m)) { i += k; n /= UINT64_C(m); }

  int i = -(n == 0);
  S(16,10000000000000000); S(8,100000000); S(4,10000); S(2,100); S(1,10);
  return i;

  #undef S
}
Run Code Online (Sandbox Code Playgroud)


Arj*_*kar 5

这是一个非常紧凑快速的扩展,不使用额外的临时对象:

r = 0;

/* If its wider than 32 bits, then we already know that log >= 32.
So store it in R.  */
if (v >> 32)
  {
    r = 32;
    v >>= 32;
  }

/* Now do the exact same thing as the 32 bit algorithm,
except we ADD to R this time.  */
if (tt = v >> 16)
  {
    r += (t = tt >> 8) ? 24 + LogTable256[t] : 16 + LogTable256[tt];
  }
else
  {
    r += (t = v >> 8) ? 8 + LogTable256[t] : LogTable256[v];
  }
Run Code Online (Sandbox Code Playgroud)

这是一个用 s 链构建的if,同样没有使用额外的临时对象。但可能不是最快的。

  if (tt = v >> 48)
    {
      r = (t = tt >> 8) ? 56 + LogTable256[t] : 48 + LogTable256[tt];
    }
  else if (tt = v >> 32)
    {
      r = (t = tt >> 8) ? 40 + LogTable256[t] : 32 + LogTable256[tt];
    }
  else if (tt = v >> 16)
    {
      r = (t = tt >> 8) ? 24 + LogTable256[t] : 16 + LogTable256[tt];
    }
  else 
    {
      r = (t = v >> 8) ? 8 + LogTable256[t] : LogTable256[v];
    }
Run Code Online (Sandbox Code Playgroud)

  • (`unsigned long long` 或 `uint64_t`) (2认同)

kre*_*ieg 5

如果您正在寻找 C++ 答案,并且您找到了这里,并且由于它归结为计数零,那么您std::countl_zero根据 godbolt.org 调用得到了 which bsrstd::countl_zero从 C++20 开始可用,您可能需要添加-std=gnu++2a到编译器命令行

  • 这个相同的 C++ 版本还添加了函数 [`std::bit_width(x)`](https://en.cppreference.com/w/cpp/numeric/bit_width),其结果是 `1 + ⎣log2(x)⎦ ` 如果 `x &gt; 0` 和 `0` 如果 `x == 0`。 (3认同)