有没有办法绕过 C 中的编译器优化?

Is there anyway to get around this compiler optimization in C?

我想指出,正如 Olaf 所指出的,编译器没有错。


免责声明:我不完全确定此行为是由于编译器优化造成的。

无论如何,在 C 中,我正在尝试确定 8 位字节的第 n 位(n 应介于 0 和 7 之间,包括边界值)是 1 还是 0 .我最初想出了这个解决方案:

#include <stdint.h>
#include <stdbool.h>

bool one_or_zero( uint8_t t, uint8_t n ) // t is some byte, n signifies which bit
{
    return (t << (n - (n % 8) - 1)) >> 7;
}

根据我之前的理解,这将对一个字节执行以下操作:

假设 t = 5n = 2。那么字节t可以表示为0000 0101。我假设 (t << (n - (n % 8) - 1)) 会移动 t 的位,这样 t 就是 1010 0000。这个假设只是在一定程度上是正确的。我还假设下一个位移位 (>> 7) 会移动 t 的位,这样 t 就是 0000 0001。这个假设也只是在一定程度上是正确的。

TL;DR:我认为 return (t << (n - (n % 8) - 1)) >> 7; 行是这样做的:

  1. t0000 0101
  2. 第一次位移发生; t 现在是 1010 0000
  3. 发生第二次位移; t 现在是 0000 0001
  4. t 返回为 0000 0001

虽然我打算这样做,但事实并非如此。相反,我必须编写以下内容才能获得预期结果:

bool one_or_zero( uint8_t t, uint8_t n ) // t is some byte, n signifies which bit
{
    uint8_t val = (t << (n - (n % 8) - 1));
    return val >> 7;
}

我知道添加 uint8_t val 不会消耗大量性能。不过,我想知道两件事:

  1. 我是否必须初始化另一个变量来执行我想要的操作?
  2. 为什么单线和双线的效果不一样?

我的印象是,当编译器优化我的代码时,它将两个位移位合并在一起,所以只发生了一个。这似乎是一件好事,但它并没有 "clear" 其他位。

该代码非常复杂,只是检查整数中的一位。尝试标准方法:

return (t & (1U << n)) != 0;

如果您必须检查 n 是否有效,请添加一个断言。 else 掩码 (n & 7) 或模数 (n % 8)(这将由编译器针对掩码操作进行优化)将强制移位计数在有效范围内。由于该模式将被许多编译器识别,他们可能会将其转换为单个位测试 CPU 指令(如果可用)。

为避免幻数,您应该将模数 8 替换为:(sizeof(t) * CHAR_BIT)。这将遵循 t 可能具有的任何类型。掩码总是比模数小一。

您的代码:

(n - (n % 8) - 1))

如果 n < 8 它产生一个负值(-1 恰好)。 Negative shifts present undefined behaviour,所以任何事情都有可能发生(小心鼻恶魔)。

我相信你是整数提升的受害者

当您有一个表达式时:x operator y 有几件事您应该注意。首先是将表达式的结果(以及过程中的另一个操作数)提升为两个操作数的 "largest" 类型。

在您的示例中,这意味着以下内容:

  1. (t << (n - (n % 8) - 1)) >> 7; 常数 8 是一个 int 因此 n%8 也是一个 int.
  2. (t << (n - (integer) - 1)) >> 7(n - integer - 1)也是一个整数,也就是说临时值(t << integer)会存储在一个int中。这意味着您不会 "cut off" 最重要的位,因为结果存储在(最有可能的)32 位中,而不是您假设的 8 位。

另一方面,如果您暂时将 int 结果存储在 uint8_t 中,您将正确地切断前导位并得到您想要的结果。

您可以通过在计算期间将操作数转换为 uint8_t 来解决此问题:

(t << (uint8_t)(n - (n % 8) - 1))) >> 7;

或者更好的是,使用奥拉夫在回答中建议的面具:

(t & ((uint8_t)1 << n)) != 0