获得小于某个数的 2 的幂的最快方法是啥?
Posted
技术标签:
【中文标题】获得小于某个数的 2 的幂的最快方法是啥?【英文标题】:What is the fastest way to get the power of 2 less than a certain number?获得小于某个数的 2 的幂的最快方法是什么? 【发布时间】:2014-08-09 15:36:55 【问题描述】:我正在使用这个逻辑:
while ((chase<<(++n))< num) ;
其中 chase=1
,n=0
最初和 num
是我想要找到的 2 的幂的值,它刚好小于它。
循环之后我只是申请
chase=1;
chase<<(n-1);
虽然我得到了正确的答案,但我只是想找到最快的方法。有没有更快的方法?
【问题讨论】:
您可能会发现this question 的答案很有用。 查找表不能被打败 @DavidHeffernan:除非您要查找的值非常小,否则查找表会占用大量内存。而且,实际上在寄存器中进行算术运算可能更快(机器每 nS 执行几条指令,而不是平均访问时间为 40nS 的随机存储器访问。最后,大多数现代机器都有特殊的指令来执行此操作,这需要最多几个时钟周期。因此,查找表可以被击败。 关注@jotik 的指针,考虑这个具体的答案:***.com/a/24748637/120163 更少?或小于或等于? 【参考方案1】:对于正整数v
2 小于或等于 到v
的幂是2^[log2(v)]
(即1 << [log2(v)]
),其中[log2(v)]
中的[]
代表四舍五入向下。 (如果您需要小于v
的 2 的幂,您可以轻松调整算法。)
对于非零 v
,[log2(v)]
是 v
二进制表示中最高 1 位的索引。
您必须已经了解以上所有内容,因为这正是您在原始代码中所做的。但是,它可以更有效地完成。 (当然,分析并查看新的“更高效”解决方案是否实际上对您的数据更有效。)
用于查找最高 1 位索引的通用平台无关技巧基于 DeBruijn 序列。这是 32 位整数的实现
unsigned ulog2(uint32_t v)
/* Evaluates [log2 v] */
static const unsigned MUL_DE_BRUIJN_BIT[] =
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
;
v |= v >> 1;
v |= v >> 2;
v |= v >> 4;
v |= v >> 8;
v |= v >> 16;
return MUL_DE_BRUIJN_BIT[(v * 0x07C4ACDDu) >> 27];
还有其他的位算法解决方案,可以在这里找到:https://graphics.stanford.edu/~seander/bithacks.html
但是,如果您需要最高效率,请注意,许多现代硬件平台本机支持此操作,并且编译器提供了用于访问相应硬件功能的内部函数。例如,在 MSVC 中可能如下所示
unsigned ulog2(uint32_t v)
/* Evaluates [log2 v] */
unsigned long i;
_BitScanReverse(&i, v);
return (unsigned) i;
在 GCC 中可能如下所示
unsigned ulog2(uint32_t v)
/* Evaluates [log2 v] */
return 31 - __builtin_clz(v);
如果你需要 64 位版本的函数,你可以用同样的方式重写它们。或者,由于对数的良好特性,您可以通过将 64 位值分成两个 32 位一半并将 32 位函数应用于最高阶的非零一半来轻松构建它们。
【讨论】:
你能给个DeBruijn序列的链接吗!!会很有帮助的!! @Prateek Gupta:DeBruijn 序列是一个非常经典的概念,您只需在 Google 上搜索“DeBruijn 序列”即可找到大量高质量信息。甚至***页面也是一个很好的起点。【参考方案2】:与位移和条件跳转相比,答案取决于您的机器执行内存访问的速度。假设您使用的是 16 位数字。然后您可以执行 16 步基于 while 的算法,或者首先分配一个 65K 数组,然后进行查找。如果您需要进行多次比较并且数字或多或少是随机的(并非都非常小或很大),我会说查找最好的交易。但是,对于 32 位数字,您需要一个 4GB 的数组,这听起来不太合理。对于 64 位数字,这是不可行的。
【讨论】:
【参考方案3】:获得小于某个数的 2 的最大幂的最快方法是使用查找数组。
int lk[] = -9999999, 0, 1, 2, 2, 4, 4, 4, 4, 8, ...;
printf("the greatest power of two less than %d is %d.\n", num, lk[num]);
【讨论】:
对于像 2^31 这样的输入数字,我需要多大的数组? 一个 2^31 数组,如果你想这样做的话。 这可能确实是最快的方法......可能......但是:你是认真的吗??? 半认真的,是的!我的意思是这回答了这个问题......我相信 OP 真的想问一个不同的问题。【参考方案4】:这个问题太模糊,无法给出一个答案。 您无法仅从源代码中确定“速度”。
OP 的问题类似于“什么是最快的砖?”
某些 C 代码的运行速度取决于使用的编译器、使用的架构(操作系统和硬件),有时甚至取决于使用的运行时环境。 (在许多操作系统上,某些环境变量控制 C 库函数的工作方式。如果用户愿意,几乎所有 C 库函数都可以通过自定义函数插入,而无需在 Linux 中重新编译。)
这甚至不是特定于 C 的,而是所有编程语言通用的。甚至处理器执行本机二进制代码的速度也往往因处理器类型而异。 x86 处理器系列就是一个很好的例子。
您通常可以判断积木是否慢,因为某些功能往往总是很慢,或者因为积木明显劣势(算法或其他方面)——例如,比执行其功能所需的复杂。
只有足够精确地定义环境,才能分辨出哪块砖是最快的。
但是,很容易忘记,积木也有许多不同类型,从乐高积木到建筑材料再到无法正常工作的电子产品。换句话说,有几个与 C 相关的标准:K&R C、C89、C99、C11、POSIX C 标准,甚至还有常用的编译器内置和向量扩展。
注意:OP指定“2小于[argument]的幂”,这意味着对于无符号整数,
argument result
0 0 (Invalid argument; all powers of two are larger)
1 0 (Any negative number; but we use unsigned integers)
2 1
3 2
4 2
5 4
6 4
7 4
8 4
9 8
等等。
许多 CPU 体系结构都有一个汇编指令来查找数字中设置的最高位,例如该问题的另一个答案中描述的 AndreyT。要查找小于指定参数的 2 的幂,请检查单独设置的最高位是否等于参数,如果是,则返回下一个较小的 2 幂:
#include <stdint.h>
#if defined(__GNUC__)
static int highest_bit_set(uint32_t value)
if (sizeof (unsigned int) == sizeof value)
return 31 - __builtin_clz(value);
else
if (sizeof (unsigned long) == sizeof value)
return 31 - __builtin_clzl(value);
else
exit(127); /* Weird architecture! */
#elif defined (__MSC_VER)
static int highest_bit_set(unsigned long value)
unsigned long result;
if (_BitScanReverse(&result, value))
return result;
else
return -1;
#endif
uint32_t smaller_power_of_two(uint32_t value)
uint32_t result;
if (!value)
return 0;
result = ((uint32_t)1) << highest_bit_set(value);
if (result == value)
return result >> 1;
return result;
GCC 在编译时优化了sizeof
条件,但不会产生任何接近x86 最佳汇编的地方。对于x86,使用bsr
指令的AT&T汇编函数可以简化为
smaller_power_of_two:
movl 4(%esp), %edx
movl $1, %eax
cmpl %eax, %edx
jbe .retzero
bsrl %ecx, %edx
sall %cl, %eax
movl %eax, %ecx
shrl %ecx
cmpl %eax, %edx
cmovbe %ecx, %eax
ret
.retzero:
xorl %eax, %eax
ret
它有两个比较,一个分支和一个条件移动。在 Cortex-M4 微控制器上,GCC-4.8 生成以下漂亮的汇编代码(为简洁起见,省略了序言和 abi 标志):
smaller_power_of_two:
cbz r0, .end1
clz r3, r0
movs r2, #1
rsb r3, r3, #31
lsl r3, r2, r3
cmp r3, r0
it ne
movne r0, r3
beq .end2
.end1:
bx lr
.end2:
lsr r0, r3, r2
bx lr
随机内存访问的成本往往不可忽略,因为缓存是有限的,而且与典型 CPU 的算术逻辑能力相比,RAM 访问速度很慢。根据我的经验,与使用一些简单的算术运算(加法、减法、按位与/或/非/异或)计算值相比,x86 架构上的查找表往往会导致代码更慢。缓存未命中通常会出现在其他地方(因为查找表占用了宝贵的缓存,否则会花费在缓存一些其他数据上),因此仅考虑使用查找表的函数并不能产生对实际情况的可靠估计。世界表现,只是一个不切实际的“如果世界是完美的” 类型的最优估计。
假设一个通用 C 编译器,没有编译器内置或内联汇编,清除最低有效位集的旧 value & (value - 1)
技巧通常非常快:
uint32_t smaller_power_of_two(uint32_t value)
uint32_t result;
if (!value)
return (uint32_t)0;
do
result = value;
value &= value - (uint32_t)1;
while (value);
return result;
上面至少计算一次 value,最多计算其中设置的位数,在每次迭代期间使用赋值、AND 和减法(减法)。我可能会在我自己的代码中使用上述版本,除非我有关于所用架构和编译器的更多信息。
在某些架构上,条件分支非常慢,因此基于二的下一个更大幂的变体可能会更快:
#include <stdint.h>
uint32_t smaller_power_of_two(uint32_t value)
if (value > (uint32_t)2147483648UL)
return (uint32_t)2147483648UL;
else
if (value < (uint32_t)2)
return (uint32_t)0;
value--;
value |= value >> 1;
value |= value >> 2;
value |= value >> 4;
value |= value >> 8;
value |= value >> 16;
return (value + (uint32_t)1) >> 1;
如果没有 if
子句,这只会为 1 到 2147483648(包括 1 到 2147483648)之间的参数产生正确的结果。尽管如此,这段代码还是不能很好地流水线化,所以在超标量 CPU 上它不会像它可能的那样充分利用 CPU。
如果条件分支(基于整数比较)很快,则可以使用二分查找。对于 32 位无符号整数,对于任何可能的输入值,最多需要评估六个 if
条件,以得到正确的结果:
uint32_t smaller_power_of_two(uint32_t value)
if (value > (uint32_t)65536UL)
if (value > (uint32_t)16777216UL)
if (value > (uint32_t)268435456UL)
if (value > (uint32_t)1073741824UL)
if (value > (uint32_t)2147483648UL)
return (uint32_t)2147483648UL;
else
return (uint32_t)1073741824UL;
else
if (value > (uint32_t)536870912UL)
return (uint32_t)536870912UL;
else
return (uint32_t)268435456UL;
else
if (value > (uint32_t)67108864UL)
if (value > (uint32_t)134217728UL)
return (uint32_t)134217728UL;
else
return (uint32_t)67108864UL;
else
if (value > (uint32_t)33554432UL)
return (uint32_t)33554432UL;
else
return (uint32_t)16777216UL;
else
if (value > (uint32_t)1048576UL)
if (value > (uint32_t)4194304UL)
if (value > (uint32_t)8388608UL)
return (uint32_t)8388608UL;
else
return (uint32_t)4194304UL;
else
if (value > (uint32_t)2097152UL)
return (uint32_t)2097152UL;
else
return (uint32_t)1048576UL;
else
if (value > (uint32_t)262144UL)
if (value > (uint32_t)524288UL)
return (uint32_t)524288UL;
else
return (uint32_t)262144UL;
else
if (value > (uint32_t)131072UL)
return (uint32_t)131072UL;
else
return (uint32_t)65536UL;
else
if (value > (uint32_t)256U)
if (value > (uint32_t)4096U)
if (value > (uint32_t)16384U)
if (value > (uint32_t)32768U)
return (uint32_t)32768U;
else
return (uint32_t)16384U;
else
if (value > (uint32_t)8192U)
return (uint32_t)8192U;
else
return (uint32_t)4096U;
else
if (value > (uint32_t)1024U)
if (value > (uint32_t)2048U)
return (uint32_t)2048U;
else
return (uint32_t)1024U;
else
if (value > (uint32_t)512U)
return (uint32_t)512U;
else
return (uint32_t)256U;
else
if (value > (uint32_t)16U)
if (value > (uint32_t)64U)
if (value > (uint32_t)128U)
return (uint32_t)128U;
else
return (uint32_t)64U;
else
if (value > (uint32_t)32U)
return (uint32_t)32U;
else
return (uint32_t)16U;
else
if (value > (uint32_t)4U)
if (value > (uint32_t)8U)
return (uint32_t)8U;
else
return (uint32_t)4U;
else
if (value > (uint32_t)2U)
return (uint32_t)2U;
else
if (value > (uint32_t)1U)
return (uint32_t)1U;
else
return (uint32_t)0U;
具有分支预测和推测执行的现代 CPU 对这种结构的预测或推测效果不佳。在 32 位微控制器上,最高位设置的扫描版本通常更快,而在 8 位和 16 位微控制器上,比较特别慢(因为这些值实际上由多个本机字组成)。但谁知道呢,也许有一种硬件架构可以在 32 位立即数寄存器比较上实现快速条件跳转;这很可能是此类架构中最快的。
对于参数范围较大但结果范围较小的更复杂的函数,具有参数范围端点的查找表(可选匹配结果数组)以及二进制搜索通常是一个非常快速的选择。对于手头的功能,我不会使用这种方法。
那么,其中哪一个是最快的? 视情况而定。
上面的例子并不是唯一可能的方法,只是一些相对快速的方法。如果参数范围更小或更大,我可能会有不同的例子。如果参数不是整数而是浮点数,您可以使用frexp()
获得二的幂;如果结果正好是 1,则将幂减一。在使用 IEEE-754 binary32 或 binary64 格式且参数存储在内存中的架构上,您只需将参数类型双关为无符号整数并屏蔽掉它的某些位,即可获得 2 的幂。
某些库(Intel MKL、AMD Core Math Library)甚至操作系统内核在运行时选择函数的最佳(最快)版本是有原因的。
【讨论】:
我的意思是,很明显,如果我们正在寻找一个“最快”的算法,那么这取决于。但很可能 OP 正在寻找一些方法来做到这一点,在他的机器上进行测试。 @Alex:不,我不同意:我确实相信 OP(以及通过网络搜索找到这个问题和这些答案的其他人)正在寻找 “最快” 或“最佳” 方式来做一些操作。涉及“最快”、“最好”等问题非常普遍,源于典型人类的基本竞争力;他们是很自然的问题。以上是关于获得小于某个数的 2 的幂的最快方法是啥?的主要内容,如果未能解决你的问题,请参考以下文章