【问题标题】:GCC compiles leading zero count poorly unless Haswell specified除非 Haswell 指定,否则 GCC 编译前导零计数很差
【发布时间】: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 == 1clz(x) == 31 的情况 - 然后是x == 2^0 所以lg(x) == 031 - 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=haswellcode 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 clangicc 只是做得不好并添加了像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


【解决方案1】:

这看起来像是 gcc 的一个已知问题:https://gcc.gnu.org/bugzilla/show_bug.cgi?id=50168

【讨论】:

  • 不错,但它看起来像是一个明显的问题。当您使用 64 位 _builtin_clzl(long) 时,它主要是关于额外的符号扩展内容,它返回一个 32 位值。我也注意到了这种行为,但是您可以通过将返回值强制转换为无符号来解决它 - 在任何情况下,这都不适用于我的示例,它只有 32 位(使用__builting_clz(int))。所以这是一个单独的问题。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-07-31
  • 1970-01-01
  • 2018-10-15
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多