GCC支持__builtin_clz(int x)
内置,它计算参数中前导零(连续最重要的零)的数量。
除了 0 之外,这对于有效实现lg(unsigned int x)
函数非常有用,该函数取x
的基数为2的对数,向下舍入 1 < / SUP>:
/** return the base-2 log of x, where x > 0 */
unsigned lg(unsigned x) {
return 31U - (unsigned)__builtin_clz(x);
}
这是以直截了当的方式运作 - 特别是考虑案例x == 1
和clz(x) == 31
- 然后x == 2^0
所以lg(x) == 0
和31 - 31 == 0
我们得到了正确的答案结果。较高的x
值的工作方式类似。
假设内置程序有效实现,这比备用pure C solutions更好。
现在发生这种情况, count前导零操作本质上是x86中bsr
指令的对偶。返回参数中最重要的1位 2 的索引。因此,如果有10个前导零,则第一个1位位于参数的第21位。一般来说,我们有31 - clz(x) == bsr(x)
,所以bsr
实际上直接实现了我们想要的lg()
函数,没有多余的31U - ...
部分。
事实上,您可以在该行之间阅读并看到__builtin_clz
函数是在bsr
时实现的:它被定义为未定义的行为如果参数是零,当然“前导零”操作完全明确定义为32(或int
的任何位大小)具有零参数。所以__builtin_clz
肯定是通过有效地映射到x86上的bsr
指令来实现的。
但是,看看GCC实际上做了什么,在-O3
处使用其他默认选项:adds a ton of extra junk:
lg(unsigned int):
bsr edi, edi
mov eax, 31
xor edi, 31
sub eax, edi
ret
xor edi,31
行对于实际重要的底部4位实际上是not edi
,这是来自neg edi
的一个 3 的转变bsr
成clz
的结果。然后执行31 - clz(x)
。
然而,对于-mtune=haswell
,code simplifies完全符合预期的单bsr
条指令:
lg(unsigned int):
bsr eax, edi
ret
为什么这种情况对我来说非常不清楚。在Haswell之前,bsr
指令已经存在了几十年,并且AFAIK的行为没有改变。这不仅仅是针对特定arch的调整的问题,因为bsr
+一堆额外指令不会比普通bsr
更快,而且还使用{较慢的代码中{1}} still results。
64位输入和输出的情况为even slightly worse:关键路径中有一个额外的-mtune=haswell
,因为来自{{{{}}的结果似乎什么都不做1}}永远不会消极。同样,movsx
变体最适合单clz
条指令。
最后,让我们检查一下非Windows编译器空间中的大玩家icc
and clang
。 -march=haswell
只是做得不好而且添加了bsr
等多余的东西 - wtf?无论icc
如何,neg eax; add eax, 31; neg eax; add eax, 31;
都能做得很好。
0 例如扫描第一个设置位的位图。
1 0的对数是未定的,因此使用0参数调用我们的函数是未定义的行为。
2 这里,LSB是第0位,MSB是第31位。
3 回想一下两个补充中的clang
。