【问题标题】:Can a modern C compiler optimize a combination of bit accesses?现代 C 编译器可以优化位访问的组合吗?
【发布时间】:2014-06-30 17:42:02
【问题描述】:

如果设置了input 的第 1、3、5、7、9、11、13 或 15 位之一,我希望 var 不等于 FALSE

一个似乎相当普遍的解决方案是:

int var = 1 & (input >>  1) ||
          1 & (input >>  3) ||
          1 & (input >>  5) ||
          1 & (input >>  7) ||
          1 & (input >>  9) ||
          1 & (input >> 11) ||
          1 & (input >> 13) ||
          1 & (input >> 15);

但是,我担心这会导致编译器生成不必要的长代码。

以下代码也将产生所需的结果。会不会更有效率?

int var = input & 0b1010101010101010;

谢谢!

【问题讨论】:

  • 请注意,二进制文字 (0b...) 是对 C 的一种非标准扩展。它们将出现在 C++14 中;因此,它们可能会到达 C1x,但不在 C11(当前标准)中。这并不是说它们不能被使用。这只是一个警告,并非每个编译器都会支持它,尽管所有编译器都会支持 0xAAAA。
  • 请注意,您的替代方案不会产生与原来相同的结果;你需要写:int var = (input & 0xAAAA) != 0; 以获得相同的 0 或 1 输出。
  • 为什么要写第一个怪物?如果使用位掩码进行与运算就足够了,那么就这样做。
  • @JonathanLeffler 用于信息二进制文字已在 C89 中提出并被拒绝(在 C89 和 C99 基本原理文档中:“由于缺乏先例和实用性不足,添加二进制常量的提议被拒绝")
  • @ouah:我同意这些评估,截至 1989 年和 1999 年。如果(并且它仍然是 if)C++14 经过并添加二进制文字,然后 C1x 支持的格局发生了巨大变化。他们会有“先例”和经验。它们可能会被添加到大多数 C 编译器中以与 C++ 兼容。 C++14 的另一个有趣的补充是数字中的引号分隔符:0xFFFF'FFFF'FFAB'7891 等。我很困惑选择 ' 而不是 _。引号的使用使临时解析器更加难以管理(而下划线会简单得多)。

标签: c optimization compiler-construction bit-shift


【解决方案1】:

你的第二个例子不等价

你想要的是(使用非标准二进制文字):

int var = !!(input & 0b1010101010101010));

或使用十六进制文字(这些是标准的):

int var = !!(input & 0xaaaa));

更改:使用十六进制常量和双重否定(相当于 != 0)。
这假定input 不是volatile,也不是原子类型。

一个好的编译器应该针对相同的指令进行优化(大多数现代编译器已经足够好了)。

最终,测试和测量,大多数编译器都会输出生成的汇编代码,你甚至不需要反汇编器!

【讨论】:

  • 顺便说一句,一些编译器提供 0b 作为扩展,因此 op 的示例可能适用于他们的编译器。
  • @Vality:好的,non-existant 太强了,改成非标准的。
  • 太棒了,谢谢。 +1 好答案,这完全解释了这种情况,特别是需要双 !.
【解决方案2】:

如果input 是易失性的,如果设置了第 1 位,编译器将需要读取一次,第 1 位的两次被清除但 3 被设置,如果第 1 位和第 3 位被清除但 5 被设置,则编译器需要读取 3 次,等等。编译器可能有办法优化代码以进行单独的位测试,但必须单独测试位。

如果input 不是易失性的,编译器可以优化代码,但我并不特别期望它。但是,无论多么古老,我都希望任何编译器都能优化

int var = (input & (
  (1 << 1) | (1 << 3) | (1 << 5) | (1 << 7) |
  (1 << 9) | (1 << 11) | (1 << 13) | (1 << 15)
) != 0);

这似乎就是你所追求的。

【讨论】:

  • 你可能没想到,但 gcc 优化了 OP 中的代码就好了(假设 input 是 unsigned int)。
  • @rici:我希望一些编译器会找到优化,但不会特别指责未能找到优化的编译器。我自己更喜欢避免这种结构,因为依赖它们的代码的性能有时会受到看似微小的变化的严重不利影响。如果目标是测试是否设置了任何各种位标志,则显式生成复合掩码可能有助于明确该意图。
  • OP中的代码很可能是由宏产生的。 (SET1(input) || SET3(input) || ...)。我不会写那样的代码;我会给标志赋予有意义的名称并使用位域(显然不是每个人的首选),但我已经看到很多使用 MEANINGFUL_NAMES 作为宏的代码,所以每个人都有自己的。但是 gcc 找到了优化绝对不会让我感到惊讶。我预料到了,但令我感到惊讶的是,clang 只到了一半(见@harold 的回答)。
  • @rici:确实,宏可能是获得这种东西的最常见方式,但另一方面,如果宏直接从 I/O 端口读取(这在嵌入式系统中很常见) ,那么只有在定义端口的标头拒绝使它们易变(这是一个常见的遗漏)时,才可能进行优化。如果头文件的未来版本将 I/O 端口声明为 volatile正确性所必需的,则性能可能会从优秀变为糟糕。
【解决方案3】:

这将取决于处理器和可用的指令,以及优化编译器的性能。我怀疑在您的情况下,这些代码行中的任何一行都会编译为相同的指令。

但我们可以做得比怀疑更好,您可以自己检查。使用 gcc,使用 -S 编译器标志让它输出它生成的程序集。然后你可以自己比较它们。

【讨论】:

  • 可用的说明可能是相关的。许多 DSP 和其他一些 CPU 具有专门用于处理交错位的指令。编译器可能会尝试使用这些指令,尤其是在他的第一个示例中。
  • 啊,好吧,我不是很喜欢 DSP,所以我不知道他们的特殊能力。但如果你知道,我会相信你的话:-)
【解决方案4】:

正统的解决方案应该是使用被遗忘的位域来映射你的标志,比如

struct
{
  bool B0: 1;
  bool B1: 1;
  bool B2: 1;
  bool B3: 1;
  bool B4: 1;
  bool B5: 1;
  bool B6: 1;
  bool B7: 1;
  bool B8: 1;
  bool B9: 1;
  bool B10: 1;
  bool B11: 1;
  bool B12: 1;
  bool B13: 1;
  bool B14: 1;
  bool B15: 1;
} input;

并使用表达式

bool Var= input.B1 || input.B3 || input.B5 || input.B7 || input.B9 || input.B11 || input.B13 || input.B15;

我怀疑优化编译器会使用单次屏蔽技巧,但老实说我没有尝试过。

【讨论】:

  • 当你可以使用位掩码时,为什么还要用位域来混淆读者?!?
  • 它们是为此目的而创建的,当然应该采用有意义的命名。可读且可移植的代码。您甚至可以使用 mask= { false, true, false, true, false, true, false, true, false, true, false, true, false, true, false, true }; 创建所需的位掩码
  • Afaik,当计算机的内存小于 1 MB 时,发明了位域来保存位,以便能够以三位存储一个值。在问题中检查的位模式并不暗示这些位是独立的布尔值,它暗示输入是一些数字结果,或者是优化为int 的位向量,以便可以一起操作所有位。将此表示形式包装成 struct 会剥夺您所有这些可能性。你不能比较结构,你不能或/和它们在一起,你只能分配它们并访问它们的成员。
  • 然后您可以将标志存储在bools(字节)和pand的数组中,并使用SSE。
  • 是的,如果它是非算术类型。但无论哪种情况,我都不会使用位域。
【解决方案5】:

处理的好坏取决于编译器。

我已经测试了这段代码的一个小变化:

int test(int input) {
  int var = 1 & (input >>  1) ||
      1 & (input >>  3) ||
      1 & (input >>  5) ||
      1 & (input >>  7) ||
      1 & (input >>  9) ||
      1 & (input >> 11) ||
      1 & (input >> 13) ||
      1 & (input >> 15);
  return var != 0;
}

结果

对于 x64,全部用 -O2 编译

海合会:

xor eax, eax
and edi, 43690
setne   al
ret

非常好。这正是您所希望的转变。

叮当声:

testw   $10922, %di             # imm = 0x2AAA
movb    $1, %al
jne .LBB0_2
andl    $32768, %edi            # imm = 0x8000
shrl    $15, %edi
movb    %dil, %al
.LBB0_2:
movzbl  %al, %eax
ret

是的,这有点奇怪。大多数测试都放在一起......除了一个。我看不出它为什么会这样做,也许其他人可以对此有所了解。

真正的惊喜,ICC:

    movl      %edi, %eax                                    #7.32
    movl      %edi, %edx                                    #8.26
    movl      %edi, %ecx                                    #9.26
    shrl      $1, %eax                                      #7.32
    movl      %edi, %esi                                    #10.26
    shrl      $3, %edx                                      #8.26
    movl      %edi, %r8d                                    #11.26
    shrl      $5, %ecx                                      #9.26
    orl       %edx, %eax                                    #7.32
    shrl      $7, %esi                                      #10.26
    orl       %ecx, %eax                                    #7.32
    shrl      $9, %r8d                                      #11.26
    orl       %esi, %eax                                    #7.32
    movl      %edi, %r9d                                    #12.25
    orl       %r8d, %eax                                    #7.32
    shrl      $11, %r9d                                     #12.25
    movl      %edi, %r10d                                   #13.25
    shrl      $13, %r10d                                    #13.25
    orl       %r9d, %eax                                    #7.32
    shrl      $15, %edi                                     #14.25
    orl       %r10d, %eax                                   #7.32
    orl       %edi, %eax                                    #7.32
    andl      $1, %eax                                      #7.32
    ret                                                     #15.21

好的,所以它对其进行了一些优化 - 没有分支,1 &amp; 被滚动在一起。但这令人失望。

结论

您的里程可能会有所不同。为了安全起见,当然可以直接使用简单版,而不是依赖编译器。

【讨论】:

  • 奇怪的是,icc 代码与 clanggcc 生成的代码基本相同,对于等效程序,所有 ||s 都更改为 |。去图吧。
猜你喜欢
  • 2013-09-11
  • 1970-01-01
  • 1970-01-01
  • 2019-01-10
  • 2020-11-19
  • 2012-12-29
  • 1970-01-01
  • 1970-01-01
  • 2014-01-16
相关资源
最近更新 更多