6

如果我有:

#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 块?

4

2 回答 2

10

您应明确说明:

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

  raiseError();
  return false;
}

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

PS我建议您也可能和不太可能以Linux内核的方式重写以防止无声的整体强制转换:

#define likely(x)      __builtin_expect(!!(x), 1)
#define unlikely(x)    __builtin_expect(!!(x), 0)
于 2016-08-19T01:57:41.720 回答
3

一般来说,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

(见神螺栓

请注意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;
}

程序集如下所示

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

现在我们得到

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)以确保处理器做出的预测是一致的使用您正在生成的代码。

于 2016-09-28T03:18:48.470 回答