【发布时间】:2017-03-20 06:47:41
【问题描述】:
GCC 支持 __builtin_clz(int x) 内置函数,它计算参数中前导零(连续最高有效零)的数量。
除其他外0,这对于高效实现lg(unsigned int x) 函数非常有用,该函数采用x 的以2 为底的对数,向下舍入1:
/** 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 好得多。
现在,计数前导零 操作本质上是 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 行实际上是一个 not edi 用于实际重要的底部 4 位,它与 neg edi 相差一个3,它将 bsr 的结果转换为clz。然后执行31 - clz(x)。
但是对于-mtune=haswell,code simplifies 完全符合预期的单个bsr 指令:
lg(unsigned int):
bsr eax, edi
ret
我不清楚为什么会这样。 bsr 指令在 Haswell 之前已经存在了几十年,而且行为是,AFAIK,没有改变。这不仅仅是针对特定拱门的调整问题,因为bsr + 一堆额外的指令不会比普通的bsr 更快,而且使用-mtune=haswell @987654326 @ 在较慢的代码中。
64 位 输入和输出的情况是even slightly worse:在关键路径中有一个额外的movsx 似乎什么都不做,因为clz 的结果永远不会消极的。同样,-march=haswell 变体最适合使用单个 bsr 指令。
最后,让我们看看非 Windows 编译器领域的大玩家icc and clang。 icc 只是做得不好并添加了像neg eax; add eax, 31; neg eax; add eax, 31; 这样的冗余内容 - wtf?无论-march如何,clang 都做得很好。
0 比如扫描一个位图寻找第一个设置位。
1 0 的对数是不确定的,因此使用 0 参数调用我们的函数是未定义的行为。
2 这里,LSB 是第 0 位,MSB 是第 31 位。
3 回想一下-x == ~x + 1 的二进制补码。
【问题讨论】:
-
GCC 有很多问题,我到处都看到(并且确实)抱怨它。慢慢地连MSVC都比它好,哈哈。
-
公平地说,它在某些情况下生成了一些非常好的代码,比竞争对手更好,但在其他情况下它有点丢球。我有一种感觉,有很多资金投入到 LLVM 中,因此
clang正在慢慢超越其他选项。
标签: c performance gcc x86 intel