Имеют ли атрибуты вероятности смысл с одним оператором if?
Cppreference и эта документация прямо не заявляют, что атрибуты правдоподобия не будут работать с однимif
заявление. Или я просто не понимаю, что подразумевается под альтернативным путем исполнения . Итак, это мой вопрос, будет ли атрибут, скажем,[[unlikely]]
, работает в случае ниже?
if (condition) [[unlikely]] {
do_stuff();
}
2 ответа
Да, это имеет смысл. Альтернативный путь — это тот, где условиеfalse
, то есть путь, не вызывающийdo_stuff();
. Это становится путем, для которого он попытается оптимизировать.
Пример:
#include <iostream>
inline void do_stuff() {
std::cout << "Surprise!\n";
}
int main(int argc, char**) {
if (argc == 0) [[likely]] {
do_stuff();
}
}
Ассемблер с:
.LC0:
.string "Surprise!\n"
main:
test edi, edi
jne .L4
sub rsp, 8
mov edx, 10
mov esi, OFFSET FLAT:.LC0
mov edi, OFFSET FLAT:_ZSt4cout
call std::basic_ostream<char, std::char_traits<char> >& std::__ostream_insert<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*, long)
xor eax, eax
add rsp, 8
ret
.L4:
xor eax, eax
ret
_GLOBAL__sub_I_main:
sub rsp, 8
mov edi, OFFSET FLAT:_ZStL8__ioinit
call std::ios_base::Init::Init() [complete object constructor]
mov edx, OFFSET FLAT:__dso_handle
mov esi, OFFSET FLAT:_ZStL8__ioinit
mov edi, OFFSET FLAT:_ZNSt8ios_base4InitD1Ev
add rsp, 8
jmp __cxa_atexit
С (и вообще без атрибута):
.LC0:
.string "Surprise!\n"
main:
test edi, edi
je .L8
xor eax, eax
ret
.L8:
push rax
mov edx, 10
mov esi, OFFSET FLAT:.LC0
mov edi, OFFSET FLAT:_ZSt4cout
call std::basic_ostream<char, std::char_traits<char> >& std::__ostream_insert<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*, long)
xor eax, eax
pop rdx
ret
_GLOBAL__sub_I_main:
sub rsp, 8
mov edi, OFFSET FLAT:_ZStL8__ioinit
call std::ios_base::Init::Init() [complete object constructor]
mov edx, OFFSET FLAT:__dso_handle
mov esi, OFFSET FLAT:_ZStL8__ioinit
mov edi, OFFSET FLAT:_ZNSt8ios_base4InitD1Ev
add rsp, 8
jmp __cxa_atexit
Это два немного разных результата, и, не зная слишком много об ассемблере, я бы сказал, что эффект от их размещения очевиден. Мне кажется, что положить[[likely]]
он сделал встроенную функцию, в то время как[[unlikely]]
оставил это как вызов функции.
Это имеет смысл, и clang уважает это. Попробуйте скомпилировать:
void do_stuff(void);
void unlikely(int condition){
if (condition) [[unlikely]] {
do_stuff();
}
}
void likely(int condition){
if (condition) [[likely]] {
do_stuff();
}
}
https://godbolt.org/z/hzahxzT3v
Clang при -O3 дает:
unlikely(int): # @unlikely(int)
test edi, edi
jne .LBB0_2
ret
.LBB0_2:
jmp do_stuff()@PLT # TAILCALL
likely(int): # @likely(int)
test edi, edi
je .LBB1_1
jmp do_stuff()@PLT # TAILCALL
.LBB1_1:
ret
Обратите внимание, что обе эти функции tail-call оптимизируют вызовdo_stuff()
(кjmp
входить в него, а не вызывать его).
Разница в том, что в маловероятном случае управление естественным образом переходит в (инструкцию возврата), переходя к только в том случае, если выполняется условие (jne
означаетjump not equal
значение не равно нулю, т.е. прыгать наcondition
).
С другой стороны, в вероятном случае управление естественным образом перетекает вjmp do_stuff()
, разветвляясь только наret
если выполняется обратное условие (je
означаетjump equal
равно нулю, т.е. прыгать на!condition
).
Вот как это должно быть. Процессоры x86_64 предсказывают первые встречи с ветвью как невыполненную, поэтому оптимальная компоновка состоит в том, чтобы ожидаемые пути следовали естественным образом, а неожиданные разветвлялись.