如何在 C++ 中做整数 log2()?

How to do an integer log2() in C++?

提问人:Peter Smit 提问时间:6/15/2009 最后编辑:Nathan FellmanPeter Smit 更新时间:5/16/2022 访问量:91947

问:

在 C++ 标准库中,我只找到了浮点日志方法。现在,我使用 log 来查找二叉树 ( ) 中索引的级别。floor(2log(index))

代码 (C++):

int targetlevel = int(log(index)/log(2));

恐怕对于某些边缘元素(值为 2^n 的元素)log 将返回 n-1.9999999999999 而不是 n.0。这种恐惧是正确的吗?如何修改我的语句,使其始终返回正确答案?

C++ 浮点精度 对数

评论

0赞 sharptooth 6/15/2009
我不明白这个问题。为什么它会返回 n - 1,9(9)?
1赞 Peter Smit 6/15/2009
因为并非所有整数都可以精确地存储为浮点数。例如,如果 7 不合适,则将其存储为 7.000001 或 6.999999。
0赞 sharptooth 6/15/2009
是的,我知道。但是这个 1,9(9) 从何而来?也许您可以使用 <sup></sup> 来重新格式化问题,而使用<sub></sub>来重新格式化较低的指数?
17赞 David Thornley 6/15/2009
任何整数都可以精确地存储在浮点数中。然而,log() 函数不一定是精确的,即使它是 log(2),对于自然对数或以 10 为基数也是无理的,所以没有理由期待确切的结果。鉴于无法保证确切的结果,担心确切的边境条件是有道理的。
1赞 erikkallen 6/15/2009
你必须有相当大的整数,可能是 2^exponentsize,然后才能准确表示它们。如果在这种情况下有精度损失,那是因为 log(2) 不能被精确地表示出来。你只会为 2^n 调用这个方法吗?如果是这样,您可以四舍五入到最接近的整数(或仅使用接受的答案)

答:

58赞 Igor Krivokon 6/15/2009 #1

您可以改用此方法:

int targetlevel = 0;
while (index >>= 1) ++targetlevel;

注意:这将修改索引。如果需要它保持不变,请创建另一个临时 int。

极端情况是当索引为 0 时。如果索引 == 0,您可能应该单独检查它并抛出异常或返回错误。

评论

0赞 Peter Smit 6/15/2009
while 循环是否将 0 整数计算为 false?
0赞 Igor Krivokon 6/15/2009
如果 index = 0,则 targetlevel 将为 0。在您的代码中,它可能会导致异常。您希望 index = 0 获得什么值?
0赞 Peter Smit 6/15/2009
我的意思是说,当索引 >>= 1 的计算结果为 0 时,循环必须停止。我无法快速找到某个地方,当表达式计算为整数零时,while 循环会真正停止。这当然是逻辑上的,因为这些位与布尔值 false 相同。
0赞 Igor Krivokon 6/15/2009
...实际上,在您的代码中,这并非例外 - 它将计算到负无穷大,然后转换为 int 作为最大负 int 值。
9赞 bobobobo 2/15/2013
请确保指定为 ,否则您的手上有一个非常危险的潜在无限循环错误。indexunsigned int
0赞 CookieOfFortune 6/15/2009 #2

你把你的树投射到多深?您可以将 +/- 0.00000001 的范围设置为数字,以强制其为整数值。

我实际上不确定你会达到像 1.99999999 这样的数字,因为在计算 2^n 值时,你的 log2 不应该失去任何准确性(因为浮点四舍五入到最接近的 2 的幂)。

2赞 maxaposteriori 6/15/2009 #3
int targetIndex = floor(log(i + 0.5)/log(2.0));

评论

0赞 mwfearnley 2/12/2017
对于最难的情况 (),这至少是 ,但当 的双精度结果开始为相邻值返回相同的结果时,会遇到一些问题。2^N-1N=32N=(52-log(52))log
24赞 paxdiablo 6/15/2009 #4

如果您只想要一个快速的整数对数2 操作,以下函数将执行此操作,而不必担心浮点精度:mylog2()

#include <limits.h>

static unsigned int mylog2 (unsigned int val) {
    if (val == 0) return UINT_MAX;
    if (val == 1) return 0;
    unsigned int ret = 0;
    while (val > 1) {
        val >>= 1;
        ret++;
    }
    return ret;
}

#include <stdio.h>

int main (void) {
    for (unsigned int i = 0; i < 20; i++)
        printf ("%u -> %u\n", i, mylog2(i));
    putchar ('\n');
    for (unsigned int i = 0; i < 10; i++)
        printf ("%u -> %u\n", i+UINT_MAX-9, mylog2(i+UINT_MAX-9));
    return 0;
}

上面的代码还有一个小的测试工具,因此您可以检查行为:

0 -> 4294967295
1 -> 0
2 -> 1
3 -> 1
4 -> 2
5 -> 2
6 -> 2
7 -> 2
8 -> 3
9 -> 3
10 -> 3
11 -> 3
12 -> 3
13 -> 3
14 -> 3
15 -> 3
16 -> 4
17 -> 4
18 -> 4
19 -> 4

4294967286 -> 31
4294967287 -> 31
4294967288 -> 31
4294967289 -> 31
4294967290 -> 31
4294967291 -> 31
4294967292 -> 31
4294967293 -> 31
4294967294 -> 31
4294967295 -> 31

它将返回输入值 0 作为未定义结果的指示,因此您应该检查这一点(没有有效的无符号整数的对数会那么高)。UINT_MAX

顺便说一句,有一些非常快的技巧可以做到这一点(找到 2 补码数中设置的最高位),可以从这里获得。我不建议使用它们,除非速度至关重要(我自己更喜欢可读性),但你应该知道它们的存在。

评论

2赞 Todd Lehman 7/22/2014
paxdiablo — 我喜欢你为输入值 0 返回 –1。但是请注意,您实际上并没有返回 ,而是实际上返回 (例如,如果您有 32 位整数,则返回 0xFFFFFFFF),因为您已经声明了函数返回 而不是 .从这个意义上说,是你能在整数中得到的最接近无穷大的。-1~0unsigned intint~0
0赞 David Stone 10/11/2015
@ToddLehman:您实际上返回了 -1。然后,它应用了一个积分提升,对于负数,该升级将值设置为 ,并且从这里开始,该值等于 max 。在某些系统上,不会给你想要的你想要的。 是根据值定义的,而不是根据位表示来定义的。2 ** 32 - nn == -1unsigned~0unsigned
0赞 Todd Lehman 10/13/2015
@paxdiablo — 顺便说一句,您提到 log₂(0) 的“正确”值是无穷大,但实际上不是负无穷大吗?也就是说,$\lim{x \to 0} log x = -\infty$。
1赞 paxdiablo 10/14/2015
@Todd,绝对正确,极限接近负无穷大。但是,由于对数实际上并没有定义为零(尽管有限制),因此我重写了该位以删除它。
3赞 P Daddy 6/15/2009 #5

我从未遇到过您使用的公式的浮点准确性任何问题(并且快速检查从 1 到 231 - 1 的数字没有发现错误),但如果您担心,您可以改用这个函数,它返回相同的结果,并且在我的测试中快了大约 66%:

int HighestBit(int i){
    if(i == 0)
        return -1;

    int bit = 31;
    if((i & 0xFFFFFF00) == 0){
        i <<= 24;
        bit = 7;
    }else if((i & 0xFFFF0000) == 0){
        i <<= 16;
        bit = 15;
    }else if((i & 0xFF000000) == 0){
        i <<= 8;
        bit = 23;
    }

    if((i & 0xF0000000) == 0){
        i <<= 4;
        bit -= 4;
    }

    while((i & 0x80000000) == 0){
        i <<= 1;
        bit--;
    }

    return bit; 
}

评论

1赞 Todd Lehman 7/22/2014
事实上,使用 log(number)/log(base) 方法的危险与其说是以 2 为基数,不如说是其他数字。例如,给出 2.999999999999999996(其中 2 而不是 3)具有 IEEE 双精度语义。log(1000) / log(10)floor
1赞 Todd Lehman 7/22/2014
但也要注意,由于 IEEE 双精度值只有 53 位尾数(52 加上一个可理解的前导 1 位),因此对于大于 2⁵³ 的数字,log(number)/log(base) 方法完全崩溃,这是 64 位整数的一个非常大的子集。因此,虽然将 log(number)/log(base) 与 32 位整数一起使用是安全的,但使用 64 位整数则会遇到麻烦。
86赞 Matt J 6/15/2009 #6

如果您使用的是最新的 x86 或 x86-64 平台(您可能是),请使用该指令,该指令将返回无符号整数中最高设置位的位置。事实证明,这与 log2() 完全相同。下面是一个使用内联 ASM 调用的简短 C 或 C++ 函数:bsrbsr

#include <stdint.h>
static inline uint32_t log2(const uint32_t x) {
  uint32_t y;
  asm ( "\tbsr %1, %0\n"
      : "=r"(y)
      : "r" (x)
  );
  return y;
}

评论

11赞 Steve Jessop 6/15/2009
在 ARM 上,您需要 clz,它返回 31 减去您想要的值。GCC 有 __builtin_clz,大概在 x86 上使用 bsr。
3赞 Instantiation 9/9/2013
为避免减法,请改用。 它也适用于 x86。__builtin_ctzint log2 (int x){return __builtin_ctz (x);}
18赞 nixeagle 10/11/2013
@user2573802这是错误的。 这不是.__builtin_ctz(9) = 0log2(9)
3赞 Eddy_Em 2/9/2015
static inline uint32_t log2(const uint32_t x){return (31 - __builtin_clz (x));}在 intel 和 ARM 上都有效(但在 ARM 上 0 的结果是错误的:log2(0) = 4294967295)。因此,英特尔 bsr 的完整类似物是:static inline uint32_t log_2(const uint32_t x){if(x == 0) return 0;return (31 - __builtin_clz (x));}
6赞 Snaipe 4/7/2017
@Eddy_Em不确定你对 log2(0) 的看法是什么,因为从数学上讲,log(0) 对于所有基数都是未定义的。它返回 INT_MAX 并不比返回 0 更“正确”。
2赞 David Thornley 6/15/2009 #7

这不是标准的,也不一定是可移植的,但它通常可以工作。我不知道它的效率如何。

将整数索引转换为具有足够精度的浮点数。假设精度足够,表示将是准确的。

查找 IEEE 浮点数的表示形式,提取指数,并进行必要的调整以找到以 2 为底的对数。

评论

0赞 Todd Lehman 7/22/2014
这里的“足够精度”等于IEEE双精度(64位又名 在 C) 中,用于处理 32 位整数和 IEEE 扩展双精度(80 位又名 在 C) 中,用于处理 64 位整数。doublelong double
0赞 bobobobo 2/15/2013 #8

这个函数我在这里写的

// The 'i' is for int, there is a log2 for double in stdclib
inline unsigned int log2i( unsigned int x )
{
  unsigned int log2Val = 0 ;
  // Count push off bits to right until 0
  // 101 => 10 => 1 => 0
  // which means hibit was 3rd bit, its value is 2^3
  while( x>>=1 ) log2Val++;  // div by 2 until find log2.  log_2(63)=5.97, so
  // take that as 5, (this is a traditional integer function!)
  // eg x=63 (111111), log2Val=5 (last one isn't counted by the while loop)
  return log2Val ;
}
1赞 Brent Bradburn 2/21/2013 #9

此函数确定表示数值间隔所需的位数:[0..maxvalue]。

unsigned binary_depth( unsigned maxvalue )
   {
   int depth=0;
   while ( maxvalue ) maxvalue>>=1, depth++;
   return depth;
   }

通过从结果中减去 1,你得到 ,这是 2 的幂的精确表示。floor(log2(x))log2(x)x

xyy-1
00-1
110
221
321
432
532
632
732
843

评论

1赞 Brent Bradburn 8/27/2013
这可以很容易地推广到支持任何“基数”(数基数)——只需使用(除以基数)代替 ./=radix>>=1
12赞 zbyszek 3/15/2014 #10

这在上面的评论中已经提出。使用 gcc 内置函数:

static inline int log2i(int x) {
    assert(x > 0);

    return sizeof(int) * 8 - __builtin_clz(x) - 1;
}

static void test_log2i(void) {
    assert_se(log2i(1) == 0);
    assert_se(log2i(2) == 1);
    assert_se(log2i(3) == 1);
    assert_se(log2i(4) == 2);
    assert_se(log2i(32) == 5);
    assert_se(log2i(33) == 5);
    assert_se(log2i(63) == 5);
    assert_se(log2i(INT_MAX) == sizeof(int)*8-2);
}

评论

0赞 Brent Bradburn 5/21/2015
找不到文档——我想它可以是.assert_seassert
0赞 Brent Bradburn 5/21/2015
使用,这匹配所有 32 位值(零除外)。我在 x86 上使用 gcc 4.8.2 进行了详尽的测试,大小为 (int)==4。unsigned xfloor(log2(x))
20赞 Todd Lehman 7/15/2014 #11

以 2 为底的整数对数

这是我对 64 位无符号整数所做的操作。这将计算以 2 为底的对数的下限,该底数相当于最高有效位的索引。这种方法对于大数字来说非常快,因为它使用一个展开的循环,该循环始终以 log₂64 = 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
}

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

以 10 为底的整数对数

以 10 为底的整数对数可以使用类似的方式计算——要测试的最大平方是 10¹⁶,因为 log₁₀2⁶⁴ ≅ 19.2659...

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
}

请注意,一个好的编译器会将此处的整数除法运算优化为乘法指令,因为除总是由一个常数来计算。(这很重要,因为与乘法指令相比,即使在最快的现代 CPU 上,整数除法指令仍然非常慢。

评论

2赞 Ira Baxter 8/10/2014
非常美。有了合适的编译器和正确的指令集,条件操作可能全部作为预测指令实现,因此不会出现分支错误预测;它都是寄存器中以典型现代处理器可以实现的(超标量)速率进行纯计算。
0赞 Todd Lehman 8/10/2014
@IraBaxter — 谢谢...令人惊讶的是,在这种情况下,这种与常量列表进行比较的方法(在我的系统上)比移位和检查零快约 60%。(我想是因为现代指令管道缓存。也就是说,与零进行比较实际上比与 64 位常量进行比较慢 60%。log2if (n >> k) {...}if (n >= (UINT64_C(1) << k)) {...}
1赞 Ichthyo 11/14/2022
有趣的事实:在微基准测试中更快(GCC 8 / Debian / -O3 / 1000 个随机数 1000 次重复)。此功能:7.6ns,5ns,位移:20ns,身份功能:0.7nsstd::ilogb(double)log2std::ilogb
6赞 Kelmar 3/3/2016 #12

如果您使用的是 C++11,则可以将其设为 constexpr 函数:

constexpr std::uint32_t log2(std::uint32_t n) noexcept
{
    return (n > 1) ? 1 + log2(n >> 1) : 0;
}
2赞 Trade-Ideas Philip 4/23/2017 #13

上面也有类似的答案。这个答案

  1. 适用于 64 位数字
  2. 允许您选择舍入类型和
  3. 包括测试/示例代码

功能:

    static int floorLog2(int64_t x)
    { 
      assert(x > 0);
      return 63 - __builtin_clzl(x);
    }

    static int ceilLog2(int64_t x)
    {
      if (x == 1)
        // On my system __builtin_clzl(0) returns 63.  64 would make more sense   
        // and would be more consistent.  According to stackoverflow this result  
        // can get even stranger and you should just avoid __builtin_clzl(0).     
        return 0;
      else
        return floorLog2(x-1) + 1;
    }

测试代码:

for (int i = 1; i < 35; i++)
  std::cout<<"floorLog2("<<i<<") = "<<floorLog2(i)
           <<", ceilLog2("<<i<<") = "<<ceilLog2(i)<<std::endl;
2赞 wonder.mice 6/17/2018 #14

重写托德·雷曼(Todd Lehman)的答案,使其更通用:

#include <climits>

template<typename N>
constexpr N ilog2(N n) {
    N i = 0;
    for (N k = sizeof(N) * CHAR_BIT; 0 < (k /= 2);) {
        if (n >= static_cast<N>(1) << k) { i += k; n >>= k; }
    }
    return i;
}

Clang with 展开循环:-O3

0000000100000f50    pushq   %rbp
0000000100000f51    movq    %rsp, %rbp
0000000100000f54    xorl    %eax, %eax
0000000100000f56    cmpl    $0xffff, %edi
0000000100000f5c    setg    %al
0000000100000f5f    shll    $0x4, %eax
0000000100000f62    movl    %eax, %ecx
0000000100000f64    sarl    %cl, %edi
0000000100000f66    xorl    %edx, %edx
0000000100000f68    cmpl    $0xff, %edi
0000000100000f6e    setg    %dl
0000000100000f71    leal    (,%rdx,8), %ecx
0000000100000f78    sarl    %cl, %edi
0000000100000f7a    leal    (%rax,%rdx,8), %eax
0000000100000f7d    xorl    %edx, %edx
0000000100000f7f    cmpl    $0xf, %edi
0000000100000f82    setg    %dl
0000000100000f85    leal    (,%rdx,4), %ecx
0000000100000f8c    sarl    %cl, %edi
0000000100000f8e    leal    (%rax,%rdx,4), %eax
0000000100000f91    xorl    %edx, %edx
0000000100000f93    cmpl    $0x3, %edi
0000000100000f96    setg    %dl
0000000100000f99    leal    (%rdx,%rdx), %ecx
0000000100000f9c    sarl    %cl, %edi
0000000100000f9e    leal    (%rax,%rdx,2), %ecx
0000000100000fa1    xorl    %eax, %eax
0000000100000fa3    cmpl    $0x1, %edi
0000000100000fa6    setg    %al
0000000100000fa9    orl %ecx, %eax
0000000100000fab    popq    %rbp

当为常量时,在编译时计算结果。n

评论

0赞 Todd Lehman 4/20/2022
干得好!看到程序集输出很酷。
0赞 GizmoGremlin 6/11/2019 #15

给定浮点数的工作方式(粗略地说,尾数 * 2^ 指数),那么任何不超过 2^127 的 2 次幂的数字都将被准确表示,而不会出错。

这确实给出了一个简单但相当棘手的解决方案 - 将浮点数的位模式解释为整数,然后只看指数。这是上面 David Thornley 的解决方案。

float f = 1;
for (int i = 0; i < 128; i++)
{
    int x = (*(int*)(&f)>>23) - 127;
    int l = int(log(f) / log(2));

    printf("i = %d, log = %d, f = %f quick = %d\n",
        i, l, f, x);
    f *= 2;
}

任何整数都可以表示为浮点数是不正确的 - 只有那些位数少于尾数才能表示的整数。在 32 位浮点数中,这是 23 位的值。

4赞 Ecto 12/1/2019 #16

使用 GCC:

int log2(int x) {
    return sizeof(int)*8 - 1 - __builtin_clz(x);
}

假设你的 X 是 > 0

评论

0赞 Sneftel 9/21/2020
__builtin_clz不是 C++ 中的标准函数。
0赞 Chris_F 7/20/2023
@Sneftel是低级库将实现的函数类型,低级库始终使用编译器内部函数。log2
29赞 Zabuzard 9/21/2020 #17

C++20 开始,您可以使用

std::bit_width(index) - 1

非常短小精悍,快速且可读性强。

它遵循与伊戈尔·克里沃孔(Igor Krivokon)提供的答案相同的想法。