在if-else if链中使用Likely()/不可思议()预处理器宏

时间:2016-08-18 23:48:26

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

如果我有:

#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;

我是否可以在最后一个条件检查中放置possible(),如else if (likely(Z)),表示最终语句(else)不太可能,而编译器不会影响先前检查的分支预测?

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

2 个答案:

答案 0 :(得分:9)

你应该明确说明:

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。我建议你重写Linux内核的方式也可能不太可能,以防止沉默的整体演员:

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

答案 1 :(得分: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)以确保处理器正在制作预测与您生成的代码一致。