编译时检查有符号类型右移是否为算术右移

5

我想知道在操作有符号类型时(例如,-2 >> 1是否为-1),检查右移是否是算术右移的最便携方式,并且需要在编译时进行。

我的想法是在编译时以某种方式检查这一点,并能够检测到它,以便我可以编译函数的不同版本(取决于运算符>>是否真正是算术右移)。

通过阅读主题验证特定编译器的C/C++有符号右移是否为算术右移?,我想到了初始化标志的方法。

static const bool is_arithmetic_rs = (((signed int)-1)>>1) == ((signed int)-1));

并且可以像这样在运行时进行测试:

if (is_arithmetic_rs) {
  // some fast algorithm using arithmetic right shifts (using >> operator)
} else {
  // the same algorithm without arithmetic right shifts (much slower)
}

然而,如果可能的话,我希望每次都避免这种分支。为了简单起见,假设我想要实现一个可移植的算术右移;如果每次调用函数都必须检查这个问题,那么将会对性能产生巨大的影响,因此如果可能的话,我希望在编译时完成它。
如果没有可移植的方法来进行此检查,是否有一种方法可以通过最佳努力基础来检查,例如通过ifdefs检查特定的编译器/平台?

2
你是否遇到了很多编译器都出现这个问题?请帮忙记录它们的名称。 - Hans Passant
目前为止,我还没有找到任何相关的内容,但是我也没有进行过搜索。如果我找到了,我会在这里创建一个列表。 - eold
1
我认为这基本上不是问题,就像一补码、符号/大小、填充位、EBCDIC以及标准允许的许多其他荒谬的事情一样。 - R.. GitHub STOP HELPING ICE
1
任何聪明的编译器都会在编译时确定表达式的结果并优化未使用的测试和分支。不必担心它。 - Matthieu M.
@HansPassant 你所说的“get this wrong”是什么意思?这个移位操作的行为是实现定义的 - M.M
7个回答

8
执行此类检查的最佳方法是例如GNU autotools所做的:
  • 在目标平台上编译一个小程序并测试发生了什么

  • 在头文件中设置适当的#define

  • 在您的源文件中包含该头文件

  • 可选地,使用适当定义的宏,以便您不必为每个小事物在代码中添加#ifdef指令。

  • 编译您的主项目

这样,您就可以避免在野外创建支持功能和各种硬件平台和操作系统的怪癖表格 - 更不用说它们的组合了。但是,如果您不在目标上构建代码,则不得不使用预先提供的目标功能表/列表替换第一步。
您应该看一下广泛使用的构建系统,例如GNU autotools或CMake,以便重用现有的宏和特定于平台的信息,并避免创建自己的内容,因此重新发明轮子。

顺便说一句,现在任何一个不错的编译器都应该能够优化掉带有常量表达式的简单测试,因此在必要时使用运行时测试 - 可能需要通过宏来实现 - 不应该对性能造成太大影响。您应该测试和分析您的代码以找出答案。


什么是平台?右移操作的行为是否取决于特定的架构、操作系统或两者都有? - eold
@leden:我刚刚提供了一种通用的方法,适用于所有操作系统/ CPU 可能会有所不同的情况。我不知道 C 中的移位操作是否会因不同的 CPU 而异,尽管我认为在同一 CPU 上的编译器或操作系统中不会有所不同。 - thkala
我不同意,并会尽可能尊重地表达我的观点。GNU autotools本身存在许多问题,我将尽量避免在此处发泄,但也许它们通常使用的最糟糕的方面是对可以使用预处理器或简单条件常量表达式在源代码级别上进行等效或更易检查的事物进行完全反向测试。 OP的问题绝对是这样的情况。 - R.. GitHub STOP HELPING ICE
如果Leden实际上在这种非常奇怪的硬件上运行,那么GNU Autotools在那里可用的几率有多大? - Bo Persson

7
可以通过预处理时的测试来避免分支。
#if ((-1)>>1) == (-1))
...
#else
...
#endif

你可以使用 #define IS_ARITHMETIC_RS (-2 >> 1 == -1),然后在代码中添加大量的 #if IS_ARITHMETIC_RS ... #else - Marlon
这就是我一直在寻找的。这种方法可能会有什么问题吗? - eold
4
预处理器能保证给出与编译器完全相同的结果吗?我非常怀疑,特别是在交叉编译时。 - marton78
1
它“应该”也可以进行交叉编译。但是你的疑虑是有道理的,因为预处理器通常是与编译器弱耦合的单独工具。更安全的方法是使用与纯C相同的条件表达式(“if(...){...}”而不是“#if ...”),并让编译器检测到其中一个分支永远不会被执行,因为测试表达式是常量。大多数编译器不会生成代码。许多编译器还会生成警告...一些编译器确实会生成无用的代码:( - Giuseppe Guerrini
当然,没有预处理器的解决方案仅适用于函数内部(而预处理器也可以应用于定义、初始化、包含等),并且两个分支都必须在语法上正确。 - Giuseppe Guerrini

5

我更多是提供评论而非答案(但显然我没有信誉)

这里有几个答案使用预处理器检查,比如

#if ((-1)>>1) == (-1))

就我个人而言,我不会相信预处理器告诉我编译器生成了什么样的代码。


3

你是否实际验证过编译器在可用情况下不会将除法优化为算术移位?

否则我认为你可以使用模板。

template <bool B>
void do_work();

template <>
void do_work<true>()
{
    // Do stuff with H/W.
}

template <>
void do_work<false>()
{
    // Do slow stuff with S/W.
}

do_work<(-2 >> 1) == -1>();

然后可以通过内联函数使其更易于使用:

inline real_do_work()
{
    do_work<(-2 >> 1) == -1>();
}

这个除法在我的情况下有问题,因为它向零舍入,但我实际上需要向下舍入。顺便说一句,使用模板的解决方案非常好,我想知道与标准的#if相比,这种方法的优缺点是什么? - eold
@leden 它基本上将逻辑分割为两个模板函数,而不使用预处理器使用#if来在每种情况下放置正确的代码。 - Mark B

1

试试这个:

#define SAR(x,y) ((x)>=0) ? ((x)>>(y)) : (~(~(x)>>(y)))

一个好的编译器会将其优化为((x)>>(y)),假设CPU是正常的。
欢迎反馈哪些编译器是好的。

1
gcc -O3不会优化掉这个(版本4.5.2),而clang -O1会(版本2.8)。 - Christoph
令人沮丧。这应该是微不足道的。假设 >> 是有符号类型的算术运算,~>> 可交换,并在简化后,两个分支包含相同的代码。 - R.. GitHub STOP HELPING ICE

1

受到Giuseppe'sR..'s答案的启发:

#if -2 >> 1 == -1
#define rshift_ar(X, Y) ((X) >> (Y))
#elif ~(~(-2) >> 1) == -1
#define rshift_ar(X, Y) ((X) >= 0 ? (X) >> (Y) : ~(~(X) >> (Y)))
#else
#error "unsupported shifting semantics"
#endif

@imallett:已经过了几年,但我认为你是对的——右括号应该放在==之前。 - Christoph
我认为它仍然是不正确的。~-21,所以无论如何 >> 的结果都是 0。此外,您的宏对于正数也无法工作。 - geometrian
@imallett:条件应该保持不变 ~(~(-2) >> 1) == ~(1 >> 1) == ~0 == -1 - Christoph
重点是它始终是正确的,无论移位类型如何。我看到移位已经针对简单参数进行了修复 :) - geometrian
@imallett:关键是它始终是正确的,无论移位类型如何——这就是为什么它排在第二位的原因;它是一个健全性检查(例如,符号-幅度表示应该无法通过它)。 - Christoph

0

任何一个好的编译器都可以轻松处理所有这些预处理器的魔法。你确定你上面给出的代码真的会在输出中生成一个分支吗?我非常怀疑,因为static const bool可以作为编译时常量进行评估,所以你的if (is_arithmetic_rs)false部分将被编译器消除,对于任何高于-O1的优化级别。请参见我的答案here

此外,我怀疑预处理器的输出是否保证与编译器的输出相同,特别是在不同移位的平台之间进行交叉编译时。


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