取下一个2的幂次方

287

我想编写一个函数,返回最接近且大于输入参数的2的幂次方数。例如,如果输入参数是789,则输出应为1024。是否有一种方法可以不使用任何循环,只使用一些位运算符来实现此目的?


相关: 找到大于或等于给定值的最小二次幂的算法 是一个C++问题。C++20引入了std:bit_ceil,它允许编译器针对目标系统进行最优化处理,但对于大多数CPU具有的位扫描、popcount或其他常见位操作,尚未提供可移植的ISO C等效物。可移植的C代码必须更少效率和/或更复杂。

给定一个整数,如何使用位操作找到下一个最大的二次幂?是一个与语言无关的版本的问题,其中包括一些C++11和17的constexpr,使用GNU扩展。

回答这个问题不需要可移植性;各种平台的快速版本都是有用的。


6
可能的解决方案请见此链接: http://graphics.stanford.edu/~seander/bithacks.html#RoundUpPowerOf2Float - Stefan
4
为了澄清一下,您需要最接近的2的幂次方(例如65将给您64,但100将给您128),还是最接近且大于目标值的2的幂次方(例如65将给您128,100也是如此)? - Kim Reece
1
有多个与此问题匹配的问题。例如:https://dev59.com/6nRC5IYBdhLWcg3wP-dh - Yann Droneaud
9
@Nathan,你提供的链接比这个问题晚了8个月。 - Joseph Quinsey
显示剩余3条评论
31个回答

216

可以查看Bit Twiddling Hacks。需要获取以2为底的对数,然后加1。例如32位值:

向上取整到下一个最高的2次幂。

unsigned int v; // compute the next highest power of 2 of 32-bit v

v--;
v |= v >> 1;
v |= v >> 2;
v |= v >> 4;
v |= v >> 8;
v |= v >> 16;
v++;
其他宽度的扩展应该是很明显的。 在"给定一个整数,如何使用位运算找到下一个最大的二次幂?"的回答中,提供了一些解释如何工作以及一些输入的位模式示例。

27
这不是最有效的解决方案,因为许多处理器都具有用于计算前导零的特殊指令,可用于高效地计算log2。请参见https://en.wikipedia.org/wiki/Find_first_set。 - Simon
15
@Simon:这是一种便携式解决方案。针对所有架构都没有通用高效算法。 - phuclv
9
如果这个数本身是2的幂次方,会怎样呢? - Litherum
21
这个讨论串的引用仍然很好,但是这个答案(以及大多数其他答案)已经过时。CPU 有一条指令可以帮助解决这个问题(实际上在那个时候就已经有了?)。 来自:https://jameshfisher.com/2018/03/30/round-up-power-2.htmluint64_t next_pow2(uint64_t x) { return x == 1 ? 1 : 1<<(64-__builtin_clzl(x-1)); }对于32位:uint32_t next_pow2(uint32_t x) { return x == 1 ? 1 : 1<<(32-__builtin_clz(x-1)); }如果你使用 GCC(我想包括 Clang?),但最好找到调用 CLZ 的代码,而不是复制粘贴所有选项。 - MappaM
7
这个答案仍然非常有用,是实现所需的最佳便携式方法。如果x>UINT32_MAX,则您的64位版本会产生未定义的行为,并且不是无分支的。此外,GCC和Clang默认使用“-mtune = generic”(大多数发行版也是如此),因此,除非使用类似于“-march = native”的选项,否则您的代码将不会扩展到x86_64上的“lzcnt”指令 - 它实际上会扩展为更慢的某个libgcc例程。因此,您提出的替代方案是不可移植的、有缺陷的,并且(通常)更慢。 - Craig Barnes
显示剩余15条评论

97
next = pow(2, ceil(log(x)/log(2)));

这个方法是通过找到将2提高到哪个幂次方得到x的数字(取该数字的对数,除以所需基数的对数,详见维基百科)。然后使用ceil将其四舍五入至最近的整数幂。

与其他位运算方法相比,这是一种更通用(即更慢!)的方法,但了解数学知识总是有好处的,不是吗?


4
从C99起,如果你的工具支持,也可以直接使用log2。但GCC和VS似乎不支持:( - Matthew Read
22
要注意浮点数精度问题。log(pow(2,29))/log(2)的结果为29.000000000000004,所以返回的是230而不是229。我认为这就是为什么存在log2函数的原因? - endolith
81
这可能至少耗费了200个周期,而且还不正确。为什么这有那么多赞? - Axel Gneiting
4
@SuperflyJon,但是它提到了位运算符,我假设除非另有说明,否则任何问题都隐含正确性。 - BlackJack
3
请更新答案并使用log2()。我没有阅读评论,最终在竞赛中遭受了巨大的损失。:( - Abhishek
显示剩余4条评论

75

我认为这个也可以工作:

int power = 1;
while(power < x)
    power*=2;

答案是power


32
问题要求不使用循环结构,这是可以理解的。虽然其他一些函数非常聪明,但对于不需要高性能的代码而言,我更喜欢简单易懂、容易验证正确性的答案。 - Tim MB
2
这不是返回最接近2的幂,而是比X大的幂。仍然非常好。 - CoffeDeveloper
4
可以使用一些位运算的“魔术”代替乘法,例如 power <<= 1 - vallentin
6
那应该由编译器自动进行优化。 - MarkWeston
9
如果 x 太大(即没有足够的位数来表示下一个2的幂),请注意无限循环。 - alban
1
@MarkWeston https://godbolt.org/z/ajvGsTs5b 目前看来,Clang trunk ARM 64位似乎没有对循环进行优化。 - nmr

55
unsigned long upper_power_of_two(unsigned long v)
{
    v--;
    v |= v >> 1;
    v |= v >> 2;
    v |= v >> 4;
    v |= v >> 8;
    v |= v >> 16;
    v++;
    return v;

}

76
如果您能加上出处就更好了(除非您是发现它的人)。这段内容来自于“位操作技巧页面”。 - florin
3
这是针对32位数字的吗?64位扩展? - Jonathan Leffler
6
@florin,如果v是64位类型,你能否在16位后面添加“c |= v >> 32”? - Evan Teran
2
使用有符号整数时要小心。大于0x4000_0000_0000_0000的值将返回0x8000_0000_0000_0000。 - Nathan
5
只适用于特定位宽的代码应该使用固定宽度类型而不是最小宽度类型。该函数应该取一个 uint32_t 并返回它。 - Craig Barnes
显示剩余7条评论

47
如果您正在使用GCC,您可能需要查看Lockless Inc公司的“优化next_pow2()函数”。该页面介绍了一种使用内置函数builtin_clz()(计算前导零)并直接使用x86 (ia32)汇编指令bsr(位扫描反转)的方法,就像在另一个答案所描述的那样,在gamedev网站的链接中。这段代码可能比以前的答案描述的更快。
顺便说一句,如果您不打算使用汇编指令和64位数据类型,则可以使用此方法。
/**
 * return the smallest power of two value
 * greater than x
 *
 * Input range:  [2..2147483648]
 * Output range: [2..2147483648]
 *
 */
__attribute__ ((const))
static inline uint32_t p2(uint32_t x)
{
#if 0
    assert(x > 1);
    assert(x <= ((UINT32_MAX/2) + 1));
#endif

    return 1 << (32 - __builtin_clz (x - 1));
}

3
请注意,此函数返回大于等于x的最小2的幂。将(x-1)更改为x会使该函数返回大于x的最小2的幂。 - Guillaume
3
您可以在Visual C++中使用"_BitScanForward"函数。 - KindDragon
你也可以使用 __builtin_ctz() - MarkP
@MarkP __builtin_ctz() 对于将任何非2的幂次方数舍入到下一个2的幂次方不会有用。 - Yann Droneaud
3
请在您的答案中添加一个链接,指向其他编译器内置按位运算函数的维基百科列表:https://en.wikipedia.org/wiki/Find_first_set#Tool_and_library_support。请同时提供一个64位版本。我提议使用以下C++11函数:`constexpr uint64_t nextPowerOfTwo64(uint64_t x) { return 1ULL << (sizeof(uint64_t) * 8 - __builtin_clzll(x)); }` - oHo
显示剩余4条评论

27

在标准的c++20中,这已经包括在<bit>中了。答案非常简单。

#include <bit>
unsigned long upper_power_of_two(unsigned long v)
{
    return std::bit_ceil(v);
}

注意: 我给出的解决方案是针对++,而不是c,我会在这个问题上给出答案,但它被关闭为重复问题!


2
我同意_"用C解决这个问题"不是"用C++解决这个问题"_的重复。你已经证明了这一点。因此,我重新打开了你提供的链接中的重复问题。请随意将你的C++答案移动到那里。 - Drew Dormann
我已将C++问题的链接添加到此C问题中(甚至提及了std::bit_ceil),因此这不再需要成为此处的答案。请删除。您在此处的回答错误地说明了链接的问题已关闭;正如Drew所说,它已重新打开。 - Peter Cordes

24

再说一句,虽然我使用了循环,但是这比数学运算符要快得多

二的幂次方 "floor" 选项:

int power = 1;
while (x >>= 1) power <<= 1;

二的幂次方 "ceil" 选项:

int power = 2;
x--;    // <<-- UPDATED
while (x >>= 1) power <<= 1;

更新

如评论中所提到的,ceil 函数存在错误,导致结果不正确。

以下是完整的函数:

unsigned power_floor(unsigned x) {
    int power = 1;
    while (x >>= 1) power <<= 1;
    return power;
}

unsigned power_ceil(unsigned x) {
    if (x <= 1) return 1;
    int power = 2;
    x--;
    while (x >>= 1) power <<= 1;
    return power;
}

4
如果x是2的幂,则结果不正确。需要一个微型程序来测试输入是否是2的幂。 #define ISPOW2(x) ((x)> 0 &&!((x)&(x-1))) - pgplus1628
更高效的方法是 if (x == 0) return 1; /* 或者 0 (我通常使用这个) */ x--; /* 程序的其他部分 */ - yyny
好的解决方案!但是“向上取整到2的幂次方”的选项不正确。例如,当x = 2时,结果应该是2而不是4 - MZD

14

对于任何无符号类型,基于Bit Twiddling Hacks的构建:

#include <climits>
#include <type_traits>

template <typename UnsignedType>
UnsignedType round_up_to_power_of_2(UnsignedType v) {
  static_assert(std::is_unsigned<UnsignedType>::value, "Only works for unsigned types");
  v--;
  for (size_t i = 1; i < sizeof(v) * CHAR_BIT; i *= 2) //Prefer size_t "Warning comparison between signed and unsigned integer"
  {
    v |= v >> i;
  }
  return ++v;
}

编译器在编译时就知道迭代的次数,因此实际上不存在循环。


4
请注意,这个问题是关于C语言的。 - martinkunev
1
@martinkunev 只需手动替换 UnsignedType 并处理它。我相信 C 程序员可以扩展这个简单的模板,忽略 std::is_unsigned<UnsignedType>::value 断言。 - user877329
3
@user877329 当然,如果有人想将其翻译成C语言,用JavaScript回答也是不错的选择。 - martinkunev
@martinkunev 在 JavaScript 中使用 UnsignedType?无论如何,这个解决方案展示了如何为任何 UnsignedType 进行操作,它恰好是用 C++ 编写的,而不是伪代码 [ sizeof(v)*CHAR_BIT 而不是 UnsignedType 对象中位数之类的东西 ]。 - user877329

13

尽管这个问题被标记为c,我还是想说一句。幸运的是,C++ 20 中将包括 std::ceil2std::floor2 函数(参见这里)。它们是consexpr 模板函数,当前的GCC实现使用位移,并适用于任何整数无符号类型。


8
最近,它被重命名为 bit_ceil。http://open-std.org/JTC1/SC22/WG21/docs/papers/2020/p1956r1.pdf - Wolfgang Brehm
5
bit_floorbit_ceil现在可以从<bit>头文件中在C++20中使用。 - SU3

10

对于IEEE浮点数,你可以像这样操作。

int next_power_of_two(float a_F){
    int f = *(int*)&a_F;
    int b = f << 9 != 0; // If we're a power of two this is 0, otherwise this is 1

    f >>= 23; // remove factional part of floating point number
    f -= 127; // subtract 127 (the bias) from the exponent

    // adds one to the exponent if were not a power of two, 
    // then raises our new exponent to the power of two again.
    return (1 << (f + b)); 
}
如果您需要整数解并且可以使用内联汇编语言,则 BSR 可以在 x86 上为您提供整数的 log2。它计算设置了多少个右位,这正好等于该数字的 log2。其他处理器也有类似的指令(通常),例如 CLZ,根据您的编译器,可能会有可用的内置函数来为您完成工作。

这是一个有趣的问题,尽管与我的问题无关(我只想四舍五入整数),但我会试一下这个方法。 - Naveen
在阅读浮点数维基百科文章后,我想到了这个。除此之外,我还用它来计算整数精度下的平方根。这也很好,但更加无关紧要。 - Jasper Bekkers
这违反了严格别名规则。在某些编译器上可能无法工作或发出警告。 - martinkunev

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接