如何在 C++ 中做一个整数 log2()?
Posted
技术标签:
【中文标题】如何在 C++ 中做一个整数 log2()?【英文标题】:How to do an integer log2() in C++? 【发布时间】:2010-11-02 22:27:12 【问题描述】:在 C++ 标准库中,我发现只有一个浮点日志方法。现在我使用 log 来查找二叉树 (floor(2log(index))
) 中索引的级别。
代码(C++):
int targetlevel = int(log(index)/log(2));
恐怕对于某些边缘元素(值为 2^n 的元素),log 将返回 n-1.999999999999 而不是 n.0。这种恐惧正确吗?如何修改我的陈述,使其始终返回正确答案?
【问题讨论】:
我不明白这个问题。为什么它会返回 n - 1,9(9)? 因为并非所有整数都可以精确地存储为浮点数。如果 7 不合适,它将被存储为 7.000001 或 6.999999 例如。 是的,我知道。但是这个 1,9(9) 是从哪里来的呢?也许您可以使用 重新格式化问题,将 用于较高的索引,将 用于较低的索引? 任何整数都可以精确地存储在浮点数中。但是,log() 函数不一定精确,即使它是 log(2),对于自然对数或以 10 为底的函数都是不合理的,因此没有理由期望得到精确的结果。鉴于无法保证确切的结果,担心确切的边界条件是有道理的。 你必须有相当大的整数,可能是 2^exponentsize 才能精确表示。如果您在这种情况下失去精度,那是因为 log(2) 无法准确表示。你会只为 2^n 调用这个方法吗?如果是这样,您可以四舍五入到最接近的整数(或只使用接受的答案) 【参考方案1】:如果您使用的是最新的 x86 或 x86-64 平台(并且您可能是),请使用 bsr
指令,该指令将返回无符号整数中最高设置位的位置。事实证明,这与 log2() 完全相同。这是一个简短的 C 或 C++ 函数,它使用内联 ASM 调用 bsr
:
#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;
【讨论】:
而在 ARM 上,您需要 clz,它返回 31 减去您想要的值。 GCC 有 __builtin_clz,大概是在 x86 上使用 bsr。 为避免减法,请改用__builtin_ctz
。 int log2 (int x)return __builtin_ctz (x);
它也适用于 x86。
@user2573802 这是错误的。 __builtin_ctz(9) = 0
不是 log2(9)
。
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));
@Eddy_Em 不确定您对 log2(0) 的看法是什么,因为从数学上讲,log(0) 对于所有碱基都是未定义的。它返回 INT_MAX 并不比返回 0 更“正确”。【参考方案2】:
您可以改用此方法:
int targetlevel = 0;
while (index >>= 1) ++targetlevel;
注意:这将修改索引。如果您需要它保持不变,请创建另一个临时 int。
极端情况是 index 为 0 时。您可能应该单独检查它并在 index == 0 时抛出异常或返回错误。
【讨论】:
while 循环是否将 0 整数计算为假? 如果 index = 0,targetlevel 将为 0。在您的代码中,它可能会导致异常。您希望 index = 0 的值是多少? 我的意思是说,当 index >>= 1 的计算结果为 0 时,循环必须停止。我无法快速找到当表达式计算为整数零时 while 循环将真正停止的地方.这当然是逻辑,因为这些位与布尔值 false 相同。 ...实际上,在您的代码中也不例外 - 它将计算为负无穷大,然后转换为 int 作为最大负 int 值。 请务必将index
指定为unsigned int
,否则您将面临一个非常危险的潜在无限循环错误。【参考方案3】:
如果你只想要一个快速的整数 log2 运算,下面的函数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
,作为未定义结果的指示,因此您应该检查这一点(没有有效的无符号整数会有这么高的对数)。
顺便说一下,here 提供了一些非常快速的技巧来做到这一点(找到 2 的补码中设置的最高位)。我不建议使用它们,除非速度至关重要(我自己更喜欢可读性),但你应该知道它们的存在。
【讨论】:
paxdiablo — 我喜欢你在输入值为 0 时返回 –1。但是请注意,你并没有实际上返回-1
,而是实际上取而代之的是~0
(例如,如果您有32 位整数,则为0xFFFFFFFF),因为您已声明函数返回unsigned int
而不是int
。从这个意义上说,~0
是最接近整数的无穷大。
@ToddLehman:您实际上返回的是 -1。然后它应用了一个整数提升,对于负数将值设置为2 ** 32 - n
,并且由于这里的n == -1
,该值等于最大值unsigned
。在某些系统上,~0
不会给您想要的。 unsigned
是根据值定义的,而不是根据位表示。
@paxdiablo — 顺便说一下,你提到 log₂(0) 的“正确”值是无穷大,但它实际上不是负无穷大吗?即 $\limx \to 0 log x = -\infty$.
@Todd,绝对正确,极限接近负无穷大。但是,由于实际上并未将对数定义为零(尽管有限制),因此我重写了该位以将其删除。【参考方案4】:
Base-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(这是初始 -(n == 0)
正在检查的内容),则返回 –1。如果您从没想过会使用n == 0
调用它,您可以用int 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 上,整数除法指令仍然非常慢。)
【讨论】:
非常漂亮。使用合适的编译器和正确的指令集,条件动作可能都被实现为谓词指令,因此不会出现分支错误预测;这都是以典型现代处理器可以达到的(超标量)速率在寄存器中进行的纯计算。 @IraBaxter — 谢谢...令人惊讶的是,在log2
的情况下,这种与常量列表进行比较的方法(在我的系统上)比移位和检查零快约 60% . (我想是因为现代指令流水线缓存。)也就是说,使用if (n >> k) ...
进行移位并与零比较实际上比使用if (n >= (UINT64_C(1) << k)) ...
与 64 位常量进行比较要慢 60%。【参考方案5】:
这已在上面的 cmets 中提出。使用 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);
【讨论】:
找不到assert_se
的文档——我认为它可能只是assert
。
使用unsigned x
,这与floor(log2(x))
匹配所有32 位值(零除外)。我在 x86 上使用 sizeof(int)==4 对 gcc 4.8.2 进行了详尽的测试。【参考方案6】:
从C++20开始可以使用
std::bit_width(index) - 1
非常简短、紧凑、快速且易读。
跟the answer provided by Igor Krivokon的思路一样。
【讨论】:
【参考方案7】:如果您使用的是 C++11,您可以将其设为 constexpr 函数:
constexpr std::uint32_t log2(std::uint32_t n) noexcept
return (n > 1) ? 1 + log2(n >> 1) : 0;
【讨论】:
【参考方案8】:我从未对您使用的公式的浮点精度有任何问题(快速检查从 1 到 2 的数字31 - 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;
【讨论】:
确实,使用 log(number)/log(base) 方法的危险并不在于以 2 为底,而在于其他数字。例如,log(1000) / log(10)
给出 2.9999999999999996(floor
是 2 而不是 3),具有 IEEE 双精度语义。
但还要注意,由于 IEEE 双精度值只有 53 位尾数(52 加上一个可理解的前导 1 位),log(number)/log(base) 方法完全分崩离析大于 2⁵³ 的数字,这是 64 位整数的一个非常大的子集。因此,虽然使用 32 位整数的 log(number)/log(base) 是安全的,但使用 64 位整数是在自找麻烦。【参考方案9】:
int targetIndex = floor(log(i + 0.5)/log(2.0));
【讨论】:
这对于最困难的情况(2^N-1
)是明确定义的,至少达到N=32
,但在N=(52-log(52))
左右遇到问题,当@的双精度结果时987654325@ 开始为相邻值返回相同的结果。【参考方案10】:
这不是标准的,也不一定是可移植的,但它通常可以工作。不知道效率如何。
将整数索引转换为足够精度的浮点数。假设精度足够,表示将是准确的。
查找 IEEE 浮点数的表示,提取指数,并进行必要的调整以找到以 2 为底的对数。
【讨论】:
“足够的精度”在这里等于 IEEE 双精度(C 中的 64 位又名double
),用于处理 32 位整数和 IEEE 扩展双精度(80 位又名 long double
在 C) 中用于处理 64 位整数。【参考方案11】:
上面有类似的答案。这个答案
-
适用于 64 位数字
让您选择舍入类型和
包括测试/示例代码
功能:
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 *** 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;
【讨论】:
【参考方案12】:此函数确定表示数字区间需要多少位:[0..maxvalue]。
unsigned binary_depth( unsigned maxvalue )
int depth=0;
while ( maxvalue ) maxvalue>>=1, depth++;
return depth;
通过从结果中减去 1,您会得到 floor(log2(x))
,当 x
是 2 的幂时,这是 log2(x)
的精确表示。
xyy-1 00-111 022132143253 2632732843
【讨论】:
这可以很容易地推广到支持任何“基数”(数字基数)——只需使用/=radix
(除以基数)代替>>=1
。【参考方案13】:
int log2(int x)
return sizeof(int)*8 - 1 - __builtin_clz(x);
假设你的 x > 0
【讨论】:
__builtin_clz
不是 C++ 中的标准函数。【参考方案14】:
您将树投影到多深?你可以设置一个范围说... +/- 0.00000001 到数字来强制它为一个整数值。
我实际上并不确定您是否会达到 1.99999999 这样的数字,因为您的 log2 在计算 2^n 值时不应该失去任何准确性(因为浮点四舍五入到最接近的 2 次幂)。
【讨论】:
【参考方案15】:这个函数是我写的here
// 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 ;
【讨论】:
【参考方案16】:重写 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 与 -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
为常量时,在编译时计算结果。
【讨论】:
【参考方案17】:鉴于浮点数的工作方式(粗略地说,尾数 * 2^exponent),那么任何不超过 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;
any 整数可以表示为浮点数是不正确的——只有那些位数少于尾数的整数才能表示。在 32 位浮点数中,这是 23 位的价值。
【讨论】:
以上是关于如何在 C++ 中做一个整数 log2()?的主要内容,如果未能解决你的问题,请参考以下文章
如果你要求一个整数并且用户输入“b”,你会在 C++ 中做啥?