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

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