未定义的是__builtin_ctz(0)还是__builtin_clz(0)?

时间:2013-10-22 20:40:47

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

背景

很长一段时间,gcc has been providing有许多内置的比特函数,特别是尾部和前导0比特的数量(也适用于long unsignedlong long unsigned后缀lll):

  

- 内置函数: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 {3}周围的包装函数模板。 {1}} / ctz内置clzunsignedunsigned long

unsigned long long

此hack的好处是,为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; } 提供所需结果的平台可以省略额外的条件来测试ctz(0)(这可能看起来是微优化,但是当你已经失败时到内置的比特功能水平,它可以产生很大的不同)

问题

内置函数族x==0clz(0)如何未定义?

  • 他们可以抛出ctz(0)例外吗?
  • 对于x64,它们是否会为当前的gcc发行版返回底层类型的大小?
  • 是ARM / x86平台的任何不同(我无法访问它来测试那些)?
  • 上面的SFINAE技巧是一种明确定义的方法来分离这些平台吗?

2 个答案:

答案 0 :(得分:13)

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

但重要的是要明白,不仅结果不明确;他们是不确定的。例如,在给出英特尔指令参考的情况下,它对于返回当前时间的低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,源操作数可能为零,则必须始终添加检查。

答案 1 :(得分:11)

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

较新的LZCNTTZCNT指令无处不在。两者都只出现在Haswell架构中(针对英特尔)。