【问题标题】:Using Likely() / Unlikely() Preprocessor Macros in if-else if chain在 if-else if 链中使用 Likely()/Unlikely() 预处理器宏
【发布时间】:2016-12-26 00:45:14
【问题描述】:

如果我有:

#define likely(x)       __builtin_expect((x),1)
#define unlikely(x)     __builtin_expect((x),0)

if (A)
    return true;
else if (B)
    return false;
...
else if (Z)
    return true;
else
    //this will never really happen!!!!
    raiseError();
    return false;

如果编译器不影响先前检查的分支预测,我可以在最后一个条件检查(如else if (likely(Z)))周围放置可能()来表示最终语句(else)是非常不可能的吗?

基本上,如果存在带有分支预测器提示的单个条件语句,GCC 是否会尝试优化整个 if-else if 块?

【问题讨论】:

  • 唯一确定的方法是构建(当然启用了优化)并检查生成的代码。比较使用和不使用likely 生成的代码。
  • 为什么不把unlikely 放在所有其他条件周围?
  • @KerrekSB 这就是我想要阻止的。除了没有一个为真的条件之外,所有条件的可能性都相同。
  • @JoachimPileborg 确实,但我希望有人已经知道,因为对于不定期查看汇编程序的人来说,这不是一项简单的检查。
  • 您有没有自己不尝试的原因? godbolt.org/g/MYRQeO

标签: c++ gcc macros compiler-optimization likely-unlikely


【解决方案1】:

你应该明确说明:

if (A)
  return true;
else if (B)
  return true;
...  
else if (Y)
  return true;
else {
  if (likely(Z))
    return true;

  raiseError();
  return false;
}

现在编译器清楚地了解您的意图,并且不会重新分配其他分支概率。代码的可读性也提高了。

附:我建议您也可能和不太可能以 Linux 内核的方式重写以防止静默积分转换:

#define likely(x)      __builtin_expect(!!(x), 1)
#define unlikely(x)    __builtin_expect(!!(x), 0)

【讨论】:

    【解决方案2】:

    一般来说,GCC 假定 if 语句中的条件为真 - 有例外,但它们是上下文相关的。

    extern int s(int);
    
    int f(int i) {
      if (i == 0)
        return 1;
      return s(i);
    }
    

    生产

    f(int):
            testl   %edi, %edi
            jne     .L4
            movl    $1, %eax
            ret
    .L4:
            jmp     s(int)
    

    同时

    extern int t(int*);
    int g(int* ip) {
      if (!ip)
        return 0;
      return t(ip);
    }
    

    产生:

    g(int*):
            testq   %rdi, %rdi
            je      .L6
            jmp     t(int*)
    .L6:
            xorl    %eax, %eax
            ret
    

    (见godbolt

    注意f 中的分支是jne(假设条件为真),而g 中的条件假设为假。

    现在与以下比较:

    extern int s(int);
    extern int t(int*);
    
    int x(int i, int* ip) {
      if (!ip)
        return 1;
      if (!i)
        return 2;
      if (s(i))
        return 3;
      if (t(ip))
        return 4;
      return s(t(ip));
    }
    

    产生

    x(int, int*):
            testq   %rsi, %rsi
            je      .L3         # first branch: assumed unlikely
            movl    $2, %eax
            testl   %edi, %edi
            jne     .L12        # second branch: assumed likely
            ret
    .L12:
            pushq   %rbx
            movq    %rsi, %rbx
            call    s(int)
            movl    %eax, %edx
            movl    $3, %eax
            testl   %edx, %edx
            je      .L13       # third branch: assumed likely
    .L2:
            popq    %rbx
            ret
    .L3:
            movl    $1, %eax
            ret
    .L13:
            movq    %rbx, %rdi
            call    t(int*)
            movl    %eax, %edx
            movl    $4, %eax
            testl   %edx, %edx
            jne     .L2       # fourth branch: assumed unlikely!
            movq    %rbx, %rdi
            call    t(int*)
            popq    %rbx
            movl    %eax, %edi
            jmp     s(int)
    

    在这里我们看到了一个上下文因素:GCC 发现它可以在这里重用L2,因此它决定认为最终的条件不太可能,以便它可以发出更少的代码。

    让我们看看你给出的例子的汇编:

    #define likely(x)       __builtin_expect((x),1)
    #define unlikely(x)     __builtin_expect((x),0)
    
    extern void raiseError();
    
    int f(int A, int B, int Z)
    {
      if (A)
        return 1;
      else if (B)
        return 2;
      else if (Z)
        return 3;
    
      raiseError();
      return -1;
    }
    

    大会looks like this:

    f(int, int, int):
            movl    $1, %eax
            testl   %edi, %edi
            jne     .L9
            movl    $2, %eax
            testl   %esi, %esi
            je      .L11
    .L9:
            ret
    .L11:
            testl   %edx, %edx
            je      .L12       # branch if !Z
            movl    $3, %eax
            ret
    .L12:
            subq    $8, %rsp
            call    raiseError()
            movl    $-1, %eax
            addq    $8, %rsp
            ret
    

    请注意,当 !Z 为真时,生成的代码会分支,它已经表现得好像 Z 很可能。如果我们告诉它 Z 很可能会发生什么?

    #define likely(x)       __builtin_expect((x),1)
    #define unlikely(x)     __builtin_expect((x),0)
    
    extern void raiseError();
    
    int f(int A, int B, int Z)
    {
      if (A)
        return 1;
      else if (B)
        return 2;
      else if (likely(Z))
        return 3;
    
      raiseError();
      return -1;
    }
    

    现在we get

    f(int, int, int):
            movl    $1, %eax
            testl   %edi, %edi
            jne     .L9
            movl    $2, %eax
            testl   %esi, %esi
            je      .L11
    .L9:
            ret
    .L11:
            movl    $3, %eax    # assume Z
            testl   %edx, %edx
            jne     .L9         # but branch if Z
            subq    $8, %rsp
            call    raiseError()
            movl    $-1, %eax
            addq    $8, %rsp
            ret
    

    这里的重点是,在使用这些宏时应该小心谨慎,并仔细检查代码前后的代码以确保获得预期的结果,并进行基准测试(例如使用 perf)以确保处理器正在与您生成的代码一致的预测。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2021-10-12
      • 1970-01-01
      • 1970-01-01
      • 2017-04-19
      • 1970-01-01
      • 1970-01-01
      • 2022-11-29
      • 1970-01-01
      相关资源
      最近更新 更多