Использование макросов препроцессора Likely() / Unlikely() в цепочке if-else if

Если у меня есть:

#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)) показать, что последнее утверждение (иначе) очень маловероятно БЕЗ компилятора, влияющего на прогноз ветвления предыдущих проверок?

По сути, пытается ли GCC оптимизировать весь блок if-else if, если есть один условный оператор с подсказкой предиктора ветвления?

2 ответа

Решение

Вы должны сделать это явно:

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)

В общем, GCC предполагает, что условные выражения в том случае, если утверждения будут истинными - есть исключения, но они контекстные.

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 имеет значение true, он уже ведет себя так, как будто 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), чтобы убедиться, что процессор делает прогнозы, которые соответствуют с кодом, который вы генерируете.

Другие вопросы по тегам