Использование макросов препроцессора 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, если существует один условный оператор с подсказкой предиктора ветвления?

6

Решение

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

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)
8

Другие решения

В общем, 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

(увидеть 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;
}

Ассамблея выглядит так:

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

2

По вопросам рекламы [email protected]