验证特定编译器的C/C++有符号右移是否为算术右移?

18

根据 C/C++ 标准 (参见此链接),在 C 和 C++ 中,>> 操作符对于有符号数字不一定是算术移位。具体是否将 0 填充(逻辑移位)或符号位填充(算术移位)由编译器实现决定。

对于实现对有符号整数进行逻辑右移的编译器,这段代码能否在编译时断言(失败)?

#define COMPILE_TIME_ASSERT(EXP) \
    typedef int CompileTimeAssertType##__LINE__[(EXP) ? 1 : -1]

#define RIGHT_SHIFT_IS_ARITHMETIC \
    ( (((signed int)-1)>>1) == ((signed int)-1) )

// SHR must be arithmetic to use this code
COMPILE_TIME_ASSERT( RIGHT_SHIFT_IS_ARITHMETIC );

3
你的编译失败会对那些使用逻辑移位的机器有什么影响?为什么你的软件不能在这样的机器/编译器上使用?写代码时让它能够在带符号数的右移是算术还是逻辑情况下都能正常工作,这难道不是更好的选择吗? - Jonathan Leffler
6
我正在使用位运算实现无分支选择(Branch-free selection, BFS)。该方法需要进行算术移位才能正常工作。我在BFS头文件中加入了COMPILE_TIME_ASSERT(RIGHT_SHIFT_IS_ARITHMETIC)。代码需要使用RIGHT_SHIFT_IS_ARITHMETIC宏定义来选择传统或无分支路径。使用无分支代码可以大幅提升PS3/XBOX360 CPU的速度,因为分支预测失误会导致性能下降。 - Adisak
1
顺便说一句,在编译时断言失败并明确注明原因要比代码神秘地失败要好得多...基本上它会说这些例程不受此编译器(或CPU)支持。 - Adisak
哈!我在Xenon上尝试了同样的更改,得出了相同的结论。 - Crashworks
1
备用建议:如果在编译时找不到一个好的方法来执行这个测试,那么在main()函数的顶部进行一个快速的运行时测试也没有问题,并且在运行时测试未能给出所需结果时,会提供一个信息丰富的诊断/建议并导致崩溃。虽然这不如编译时错误好,但由于在测试中不会被忽视,它避免了意外启用了错误代码后发货的可能性。 - Jeremy Friesner
显示剩余4条评论
3个回答

6

我认为这看起来很不错!你还可以设置编译器以发出汇编文件(或在调试器中加载编译后的程序),查看 signed int i; i >> 1; 所发出的操作码,但这不像你的解决方案那样自动化。

如果您找到一个不实现有符号数算术右移的编译器,请告诉我。


是的,我基本上希望它自动化...查看操作码并不是这个要求的合理期望,因为我将在其他团队可能在各种平台上使用的库中使用它。 - Adisak
UNISYS 2200系统的编译器对于有符号类型使用逻辑右移。"表达式E1 >> E2的结果是将E1(解释为位模式)向右移动E2位。即使E1表达式是有符号整数类型,右移也是逻辑的(即在左侧填充零)。" http://public.support.unisys.com/2200/docs/cp14.0/pdf/78310422-011.pdf - phuclv
有符号右移:哪个编译器使用逻辑移位? - phuclv

1
为什么要使用assert?如果编译器的移位运算符不能满足您的需求,您可以通过符号扩展结果来优雅地解决这个问题。此外,有时候运行时已经足够好了。毕竟,编译器的优化器可以将运行时转换为编译时。
template <typename Number>
inline Number shift_logical_right(Number value, size_t bits)
{
    static const bool shift_is_arithmetic = (Number(-1) >> 1) == Number(-1);
    const bool negative = value < 0;
    value >>= bits;
    if (!shift_is_arithmetic && negative) // sign extend
        value |= -(Number(1) << (sizeof(Number) * 8 - bits));
}

static const bool 可以在编译时进行评估,因此如果保证 shift_is_arithmetictrue,每个值得一提的编译器都会消除整个 if 子句和构造 const bool negative 的死代码。

注意:代码改编自 Mono 的 encode_sleb128 函数:here

更新

如果您真的想在没有算术移位的机器上中止编译,最好不要依赖预处理器。您可以使用 static_assert(或 BOOST_STATIC_ASSERT):

static_assert((Number(-1) >> 1) == Number(-1), "Arithmetic shift unsupported.");

有很多代码(例如使用掩码的无分支选择)只有在具有算术有符号移位的编译器上才有意义。在它们上面优雅地模拟算术移位可能比原始代码慢得多,因此消除了“优化”。 - Adisak
好的,不过我的第二点仍然有效:不要依赖预处理器。请参见更新。 - marton78
1
static_assert() 是 C++11x 的特性,我们不能使用 boost。但是我所做的检查不依赖于预处理器,我可以使用以下代码进行检查,但这比我写的示例更难阅读和理解:typedef int CompileTimeAssertArithmeticShift[( (((signed int)-1)>>1) == ((signed int)-1) ) ? 1 : -1]; - Adisak

0
从您的各种评论中可以看出,您谈论使用这个跨平台。请确保您的编译器保证在编译为特定平台时,其编译时运算符的行为与运行时一致。
浮点数可能会出现不同的行为。如果您将其转换回int,那么您的编译器是否在单精度、双精度或扩展精度中进行常量表达式计算?
例如:
constexpr int a = 41;
constexpr int b = (a / 7.5);

我所说的是,在跨越如此多不同体系结构工作时,您应确保编译器在运行时保证与编译时相同的行为。
一个编译器可能在内部进行符号扩展,但在目标上不生成预期的操作码。唯一确定的方法是在运行时测试或查看汇编输出。
查看汇编输出并非世界末日...有多少不同的平台?既然这很关键,请对于5种不同的架构执行“工作”,查看1-3行汇编输出。你不必深入整个汇编输出(通常不需要),而只需轻松地找到你要的那一行。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接