Ответ 1
Да, это разумно, и компиляторы могут и могут воспользоваться им в правильном сценарии.
В вашем фактическом примере, если could_be
и very_improbable
являются фактически интегральными переменными, не будет смысла вставлять макросы likely
или unlikely
в подвыражение предиката, потому что что может сделать компилятор, чтобы сделать это быстрее? Компилятор может организовать блок if
по-разному в зависимости от вероятного результата ветки, но только потому, что very_improbably
маловероятен, это не помогает: ему все еще нужно сгенерировать код для его проверки.
Возьмем пример, где компилятор может сделать больше работы:
extern int fn1();
extern int fn2();
extern int f(int x);
int test_likely(int a, int b) {
if (likely(f(a)) && unlikely(f(b)))
return fn1();
return fn2();
}
Здесь предикат состоит из двух вызовов f()
с аргументами, а icc
создает другой код для 3 из 4 комбинаций likely
и unlikely
:
Код создан для likely(f(a)) && likely(f(b))
:
test_likely(int, int):
push r15 #8.31
mov r15d, esi #8.31
call f(int) #9.7
test eax, eax #9.7
je ..B1.7 # Prob 5% #9.7
mov edi, r15d #9.23
call f(int) #9.23
test eax, eax #9.23
je ..B1.7 # Prob 5% #9.23
pop r15 #10.12
jmp fn1() #10.12
..B1.7: # Preds ..B1.4 ..B1.2
pop r15 #11.10
jmp fn2() #11.10
Здесь оба предиката, вероятно, верны, поэтому icc
создает прямолинейный код для случая, когда оба являются истинными, выпрыгивая из строя, если либо оказывается ложным.
Код создан для unlikely(f(a)) && likely(f(b))
:
test_likely(int, int):
push r15 #8.31
mov r15d, esi #8.31
call f(int) #9.7
test eax, eax #9.7
jne ..B1.5 # Prob 5% #9.7
..B1.3: # Preds ..B1.6 ..B1.2
pop r15 #11.10
jmp fn2() #11.10
..B1.5: # Preds ..B1.2
mov edi, r15d #9.25
call f(int) #9.25
test eax, eax #9.25
je ..B1.3 # Prob 5% #9.25
pop r15 #10.12
jmp fn1() #10.12
Теперь предикат, скорее всего, неверный, поэтому icc
создает прямолинейный код, ведущий непосредственно к возврату в этом случае, и переходит из строки в B1.5
, чтобы продолжить предикат. В этом случае он ожидает, что второй вызов (f(b)
) будет истинным, поэтому он генерирует падение через код, заканчивающийся в хвост-вызов до fn1()
. Если второй вызов оказывается ложным, он возвращается к той же последовательности, которая уже собрана для случая падения, хотя в первом прыжке (метка B1.3
).
Это также является кодом, созданным для unlikely(f(a)) && unlikely(f(b))
. В этом случае вы можете представить себе, что компилятор меняет конец кода, чтобы поставить jmp fn2()
как провал, но это не так. Важно отметить, что это предотвратит повторное использование более ранней последовательности в B1.3
, и также маловероятно, что мы даже выполняем этот код, поэтому представляется разумным предпочесть меньший размер кода для оптимизации уже маловероятного случая.
Код создан для likely(f(a)) && unlikely(f(b))
:
test_likely(int, int):
push r15 #8.31
mov r15d, esi #8.31
call f(int) #9.7
test eax, eax #9.7
je ..B1.5 # Prob 5% #9.7
mov edi, r15d #9.23
call f(int) #9.23
test eax, eax #9.23
jne ..B1.7 # Prob 5% #9.23
..B1.5: # Preds ..B1.4 ..B1.2
pop r15 #11.10
jmp fn2() #11.10
..B1.7: # Preds ..B1.4
pop r15 #10.12
jmp fn1() #10.12
Это похоже на первый случай (likely && likely
), за исключением того, что ожидание второго предиката теперь ложно, поэтому он переупорядочивает блоки так, что случай return fn2()
является провальным.
Поэтому компиляторы определенно могут использовать точную информацию likely
и unlikely
, и действительно имеет смысл: если вы нарушили вышеуказанный тест на два прикомандированных оператора if
, довольно очевидно, что будут работать отдельные подсказки для веток, поэтому неудивительно, что семантически эквивалентное использование &&
по-прежнему пользуется подсказками.
Вот несколько других заметок, которые не получили обработки "полного текста", если вы добрались до этого:
- Я использовал
icc
, чтобы проиллюстрировать примеры, но для этого теста по крайней мере обаclang
иgcc
выполняют те же основные оптимизации (скомбинировав 3 из 4 случаев по-разному). - Одна "очевидная" оптимизация, которую мог бы сделать компилятор, зная вероятности суб-предикатов, заключается в том, чтобы отменить порядок предикатов. Например, если у вас есть
likely(X) && unlikely(Y)
, вы можете сначала проверить условиеY
, так как оно очень вероятно позволит вам проверять ярлык Y 1. По-видимому, gcc может сделать эту оптимизацию для простых предикатов, но я не смог уговорить icc или clang в это сделать. Оптимизация gcc, по-видимому, довольно хрупкая: исчезает, если вы немного измените предикат, хотя в этом случае оптимизация будет намного лучше. - Компиляторы не могут выполнять оптимизацию, если они не могут гарантировать, что преобразованный код будет вести себя "как бы", он был скомпилирован непосредственно в соответствии с семантикой языка. В частности, они имеют ограниченную возможность переупорядочивать операции, если они не могут доказать, что операции не имеют побочных эффектов. Помните об этом при структурировании ваших предикатов.
1Конечно, это разрешено только тогда, когда компилятор может видеть, что X
и Y
не имеют побочных эффектов, и может оказаться неэффективным, если Y
намного дороже проверить по сравнению с X
(так как любой преимущество избежания проверки на Y
завышено высокой стоимостью дополнительных оценок X
).