Home > database >  Do likelihood attributes make sense with a single if statement?
Do likelihood attributes make sense with a single if statement?

Time:01-25

Cppreference and this documentation do not state explicitly that likelihood attributes won't work with a single if statement. Or, I just do not understand what is meant by alternative path of execution. So that's my question, will the attribute, say, [[unlikely]], work in the case below?

if (condition) [[unlikely]] {
    do_stuff();
}

CodePudding user response:

Yes, it makes sense. The alternative, [[likely]], path is the one where the condition is false, that is, the path not calling do_stuff();. That becomes the path it'll try to optimize for.

Example:

#include <iostream>

inline void do_stuff() {
    std::cout << "Surprise!\n";
}

int main(int argc, char**) {
    if (argc == 0) [[likely]] {
        do_stuff();
    }
}

Assembler with [[likely]]:

.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

With [[unlikely]] (and without attribute at all):

.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

Those are two slightly different outcomes and without knowing too much about assembler, I'd say the effect of putting [[likely]] there is clear. It looks to me that putting [[likely]] there made it inline the function while [[unlikely]] left it as a function call.

CodePudding user response:

It makes sense and clang respects it nicely. Try compiling:

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 at -O3 yields:

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

Notice that both of these function tail-call optimize the call to do_stuff() (by jmping into it rather than calling it).

The difference is that with the unlikely case, control flows naturally into ret (return instruction), branching only to jmp do_stuff() if the condition is satisfied (jne stands for jump not equal meaning not equal to zero, i.e., jump on condition).

With the likely case, on the other hand, control flows naturally into the jmp do_stuff(), branching only to ret if the reverse of the condition is satified (je stands for jump equal meaning equal to zero, i.e., jump on !condition).

This is how it should be. x86_64 processors predict first encounters of a branch as not taken, so the optimal layout is for the expected paths to follow naturally and for the unexpected ones to be branched into.

  • Related