循环中复制与去重相同条件代码的性能

Jos*_*vin 4 c optimization performance assembly x86-64

假设我有这样的代码,称之为版本 1:

while (some_condition) {
    // .. work that may trigger rare_condition ...

    if (rare_condition) {
        // .. rare work here ..
        continue;
    }

    // .. work that may trigger rare_condition ...

    if (rare_condition) {
        // .. rare work here ..
        continue;
    }

    // .. more work
}
Run Code Online (Sandbox Code Playgroud)

假设这两种情况下的“稀有作品”是相同的。我们可以等效地编写版本 2:

while (some_condition) {
    // .. work that may trigger rare_condition ...

    if (rare_condition) {
        goto rare_work;
    }

    // .. work that may trigger rare_condition ...

    if (rare_condition) {
        goto rare_work;
    }

    // .. more work
    continue;

  rare_work:
    // .. rare work here ..
}
Run Code Online (Sandbox Code Playgroud)

编译器通常应该足够聪明,可以使if()检查结果直接跳转到rare_work,而不是跳转到包含跳转到 的块rare_work。编译器甚至可以为我们将版本 1 转换为版本 2,因为它减少了汇编总量,增加了适应指令缓存的机会。

我的问题是:如果指令缓存不是问题,是否有理由期望其中一个或另一个性能更好?这是假设我们关心低级微优化,并且愿意在汇编中编写代码(如果我们必须这样做的话)以强制获取一个版本或另一个版本。我意识到端口可用性之类的事情可能会根据工作指令的实际情况而改变,我只是想知道在进行此类分析之前是否有任何高级理由预期会出现差异。

Lun*_*din 5

我的问题是:如果指令缓存不是问题,是否有理由期望其中一个或另一个性能更好?

这里的性能问题是分支的数量。如果存在分支预测未命中,那就会导致性能下降。而最好的对策就是尽量减少分支的数量。由于您的代码是伪代码,因此无法判断这样做是否可行。但这才是手动优化应该关注的重点,而不是代码重新排序。


从纯粹的可读性角度来看,完全有可能在没有任何“意大利面条式编程”(又名 continue/goto)的情况下重写循环,并且仍然保持相同的性能:

while(some_condition) 
{
  bool is_rare;
    
  is_rare = work1();
  if(is_rare)
  {
    rare();
  }
  else
  {
    is_rare = work2();
    if(is_rare)
    {
      rare();
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

所有功能都在哪里static inline。在 gcc x86 -O3 中尝试此操作时,我得到的汇编代码与您的版本几乎相同goto,并rare()在循环底部内联。这至少证明我们不必在C级别写意大利面条。

人工示例代码:https://godbolt.org/z/M8EznYr1z.L5:rare()内联函数。

do_stuff:
        sub     rsp, 24
.L12:
        movzx   eax, BYTE PTR [rsp+15]
        test    al, al
        je      .L1
.L14:
        mov     edi, OFFSET FLAT:.LC0
        call    puts
        cmp     BYTE PTR rare_[rip], 0
        jne     .L5
        mov     edi, OFFSET FLAT:.LC2
        call    puts
        cmp     BYTE PTR rare_[rip], 0
        je      .L12
.L5:
        mov     edi, OFFSET FLAT:.LC1
        call    puts
        movzx   eax, BYTE PTR [rsp+15]
        test    al, al
        jne     .L14
.L1:
        add     rsp, 24
        ret
Run Code Online (Sandbox Code Playgroud)