在 if-else if 链中使用 Likely() / Unlikely() 预处理器宏

Using Likely() / Unlikely() Preprocessor Macros in if-else if chain

如果我有:

#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)) 一样在最后一个条件检查周围放置 likely() 来表示如果编译器不影响先前检查的分支预测,最后的语句(else)是不太可能的吗?

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

你应该明确说明:

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

  raiseError();
  return false;
}

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

P.S。我建议你重写 also likely 和 unlikely 的方式 Linux kernel do to protect from silent integral casts:

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

一般来说,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    , %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    , %eax
        testl   %edi, %edi
        jne     .L12        # second branch: assumed likely
        ret
.L12:
        pushq   %rbx
        movq    %rsi, %rbx
        call    s(int)
        movl    %eax, %edx
        movl    , %eax
        testl   %edx, %edx
        je      .L13       # third branch: assumed likely
.L2:
        popq    %rbx
        ret
.L3:
        movl    , %eax
        ret
.L13:
        movq    %rbx, %rdi
        call    t(int*)
        movl    %eax, %edx
        movl    , %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    , %eax
        testl   %edi, %edi
        jne     .L9
        movl    , %eax
        testl   %esi, %esi
        je      .L11
.L9:
        ret
.L11:
        testl   %edx, %edx
        je      .L12       # branch if !Z
        movl    , %eax
        ret
.L12:
        subq    , %rsp
        call    raiseError()
        movl    $-1, %eax
        addq    , %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    , %eax
        testl   %edi, %edi
        jne     .L9
        movl    , %eax
        testl   %esi, %esi
        je      .L11
.L9:
        ret
.L11:
        movl    , %eax    # assume Z
        testl   %edx, %edx
        jne     .L9         # but branch if Z
        subq    , %rsp
        call    raiseError()
        movl    $-1, %eax
        addq    , %rsp
        ret

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