【问题标题】:How undefined are __builtin_ctz(0) or __builtin_clz(0)?__builtin_ctz(0) 或 __builtin_clz(0) 的未定义程度如何?
【发布时间】:2013-10-31 21:58:03
【问题描述】:

背景

长期以来,gcc has been providing 内置了许多位旋转函数,特别是尾随和前导 0 位的数量(也适用于 long unsignedlong long unsigned,它们有后缀 l 和 @ 987654326@):

——内置函数:int __builtin_clz (unsigned int x)

返回 x 中前导 0 位的数量,从最高有效位开始 位置。如果x 为0,则结果未定义。

——内置函数:int __builtin_ctz (unsigned int x)

返回 x 中尾随 0 位的数量,从最低有效位开始 位置。如果x 为0,则结果未定义。

然而,在我测试的每个在线(免责声明:仅限 x64)编译器上,clz(0)ctz(0) 都返回底层内置类型的位数,例如

#include <iostream>
#include <limits>

int main()
{
    // prints 32 32 32 on most systems
    std::cout << std::numeric_limits<unsigned>::digits << " " << __builtin_ctz(0) << " " << __builtin_clz(0);    
}

Live Example

尝试的解决方法

std=c++1y 模式下的最新 Clang SVN 主干使所有这些函数都放松了 C++14 constexpr,这使得它们可以在 SFINAE 表达式中用于围绕 3 ctz / @ 的包装函数模板987654339@ 内置 unsignedunsigned longunsigned long long

template<class T> // wrapper class specialized for u, ul, ull (not shown)
constexpr int ctznz(T x) { return wrapper_class_around_builtin_ctz<T>()(x); }

// overload for platforms where ctznz returns size of underlying type
template<class T>
constexpr auto ctz(T x) 
-> typename std::enable_if<ctznz(0) == std::numeric_limits<T>::digits, int>::type
{ return ctznz(x); }

// overload for platforms where ctznz does something else
template<class T>
constexpr auto ctz(T x) 
-> typename std::enable_if<ctznz(0) != std::numeric_limits<T>::digits, int>::type
{ return x ? ctznz(x) : std::numeric_limits<T>::digits; }

这个 hack 的好处是,为 ctz(0) 提供所需结果的平台可以省略额外的条件来测试 x==0(这似乎是一种微优化,但当您已经降到内置的bit-twiddling函数,它可以产生很大的不同)

问题

内置函数系列clz(0)ctz(0) 的未定义程度如何?

  • 他们可以抛出std::invalid_argument 异常吗?
  • 对于 x64,对于当前的 gcc 发行版,它们会返回底层类型的大小吗?
  • ARM/x86 平台有什么不同吗(我无权测试这些平台)?
  • 上述 SFINAE 技巧是一种明确定义的分离此类平台的方法吗?

【问题讨论】:

  • 如果您可以在 gcc/gmp/glibc 中找到文件 longlong.h,请查找宏 COUNT_LEADING_ZEROS_0...

标签: c++ undefined-behavior constexpr c++14 bit-manipulation


【解决方案1】:

值未定义的原因是它允许编译器使用结果未定义的处理器指令,而这些指令是获得答案的最快方式。

但重要的是要了解,不仅结果未定义;他们是不确定的。例如,根据 Intel 的指令参考,返回当前时间的低 7 位的指令是有效的。

这就是它变得有趣/危险的地方:编译器编写者可以利用这种情况来生成更小的代码。考虑一下您的代码的这个非模板专业化版本:

using std::numeric_limits;
template<class T>
constexpr auto ctz(T x) {
  return ctznz(0) == numeric_limits<T>::digits || x != 0
       ? ctznz(x) : numeric_limits<T>::digits;
}

这适用于决定为 ctznz(0) 返回 #bits 的处理器/编译器。但是如果处理器/编译器决定返回伪随机值,编译器可能会决定“我可以为 ctznz(0) 返回任何我想要的,如果我返回#bits,代码会更小,所以我会” .然后代码最终总是调用 ctznz,即使它产生了错误的答案。

换句话说:编译器的未定义结果不能保证与运行程序的未定义结果一样是未定义的。

真的没有办法解决这个问题。如果您必须使用 __builtin_clz,并且源操作数可能为零,则必须始终添加检查。

【讨论】:

    【解决方案2】:

    不幸的是,即使 x86-64 实现也可能有所不同 - 与英特尔的 instruction set referenceBSFBSR,源操作数值为 (0),离开目标未定义,并且设置ZF(零标志)。因此,微架构或 AMD 和 Intel 之间的行为可能不一致。 (我相信 AMD 不会修改目的地。)

    较新的LZCNTTZCNT 指令并不普遍。两者都仅在 Haswell 架构中存在(针对英特尔)。

    【讨论】:

    • Tnx 这个答案。但是“未定义”是否意味着它依赖于平台,并且至少每个ctz(0) 调用都是确定性的,并且总是在该平台上给出相同的答案(即不是未定义的行为),这样我的 SFINAE hack 才有意义?跨度>
    • 虽然只有 AMD 以零源记录 bsr/bsf 不修改 dest,但我曾经能够测试(或听说过)的每个 Intel 处理器也能做到这一点。英特尔只是没有那样记录它。
    • @harold——这意味着英特尔在未来的架构中不受这种行为的约束——无论这种变化多么不可能。对于其他人 - 请不要使用“未记录”的指令语义走捷径。
    • @BrettHale 这种行为稳定的时间实际上比我活着的时间还要长。它不会改变。像往常一样,英特尔的公开文档只是不完整(或错误,或两者兼而有之)。更有可能的是,它实际上是 AMD 复制的已定义行为(如一些内部文档中所写),但英特尔从未将其纳入其公开规范。如果他们真的要改变它,他们会在他们制造第一个 OoO 处理器时这样做,那时他们有机会摆脱依赖。
    • @BrettHale Intels 文档确实 is 充满了错误,但客观上它已经是错误的,只是 也许 在这种情况下不是 - 但我们怎么样知道吗?它不能被信任,它肯定不是上帝的话语那种规范。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-06-25
    • 2019-07-16
    • 2020-03-19
    • 2017-05-16
    • 1970-01-01
    相关资源
    最近更新 更多