C++ Attribute: Likely, Unlikely(en.cppreference.com) |
C++ Attribute: Likely, Unlikely(en.cppreference.com) |
PGO is also a pain to use in some situations. You need to be able to regularly exercise all of the main paths in the program under instrumentation, preferably automated, using a configuration as close to release build as possible. That's hard to do when your release build lacks automation support, has nondeterministic behavior by design, cross-compiles to another platform, or requires networked services to exercise main paths. I don't even know how people deal with PGO when there is a requirement for deterministic builds.
Exactly! Often it is simply impossible for the compiler to know. In this respect it is similar to std::unreachable.
[1] https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p04...
and look at code in the wild
[2] https://github.com/search?q=[[likely]]+language%3Ac%2B%2B&ty...
PGO is a mixed blessing and detracts a bit from the thrust of the article. The more obvious conclusion is to continue using builtin_expect (on the boolean guard of a branch) which works great and has done for ages.
However better not give data to the function that contradicts the condition if you don't want to figth nasal daemons.
if (unlikely_condition) {
// Don’t inline this
expensive_operation();
}
It’s a good idea to check the generated assembly when using these as they can lead to weird reordering of the code.The compiler can make sure that the body for the likely condition is inline with the rest of the code, while the unlikely condition (e.g. the else block of a likely if) can be outlined behind a forward branch
Keeping the unlikely code further aside and behind a branch helps the happy path stay hot and well-predicted
__attribute__(hot)
and __attribute(cold)
1. <https://gcc.gnu.org/onlinedocs/gcc-13.2.0/gcc/Common-Functio...>; Since GCC 4.3, released March 5, 20082. <https://gcc.gnu.org/onlinedocs/gcc-13.2.0/gcc/Label-Attribut...>; Since GCC 4.8, released March 3, 2013
likely/unlikely are ultimately about branches - predict that a likely branch is taken and an unlikely branch is not taken. Note that there is some default logic in GCC even if the branches aren't tagged (for example, pointers are assumed to usually not be NULL). Failing that it usually generates the code in the order the source was laid out, but I don't think there's any "probability" weight here, just inertia, so it's easy for the optimizer to change it even by accident.
Note that the default probabilities are 90% and 10% (I've seen other software use 93.75% = 15/16); you can specify other probabilities if meaningful. Notably, choosing 50% encourages the generation of `cmov`.
hot/cold is ultimately about code size and section layout. Keep the hot code sections in cache, keep the cold code sections out of cache (and optimize it for size more than speed). Branches from non-cold to cold code are automatically tagged unlikely (not sure about hot to non-hot, or cold to anything), which is what makes people think they're related.
When I say “pessimally” I mean I usually check the unlikely cases right away and then put the normal case last:
Blah foo (something& arg) {
if (is_invalid (arg)) return blah(0);
if (is_inactive (arg)) return blah(1);
// ok do all the normal stuff
}
It makes the code clearer but slightly slower. I could always write a conditional for the hot path up front, but code is for human readers, right?So when people say “this clutters the code” they are right, but most of the time you just don’t worry about it — it need only clutter a few functions in your hot loops, where you’re willing to rewrite it anyway regardless of how ugly it gets.
It’s like looking at the standard library source: super cluttered, but it has to handle all sorts of weird corner cases and is called a lot. Normal code can ignore all that in more than 99.99% of the cases.
if (!is_valid)
if (!is_active)Does C or C++ actually make any promises that it’ll assume the “true” branch of the conditional will be taken? I always assumed that the compiler could make whatever weird decision it wants for that sort of thing.
TBH I’d probably just write normal stuff as a function, and then call that function directly in cases where performance is really crucial, if it can be done safely… if such a case exists…
tl;dr: these attributes are absolutely full of footguns because the standard is not explicit about precedence and nesting, and you should probably avoid them and prefer to spend time investing in PGO. It’s very easy to make sane-looking code containing these attributes which does the exact opposite of what you intended.
Note that this issue does not exist with the equivalent C macros — those generally behave as expected. But you should probably just invest in PGO instead of static hints there, too.
So either you aren't using PGO at all, which is fine if squeezing out these kinds of optimizations isn't that important to you, but then what's the point having these static hints?
Or you are using PGO, in which case there's no point in having these static hints because PGO will identify the likely and unlikely scenarios on your behalf. If PGO doesn't identify likely and unlikely branches, then the reason is because your profile isn't representative of how your program will actually be run in production, but in that case the solution is to provide a more representative profile instead of using [[likely]] and [[unlikely]].
That doesn't mean it shouldn't exist and isn't useful.
I believe this is the proposal that added them:
https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p04...
The "references" section has links to GCC and Clang builtins:
https://gcc.gnu.org/onlinedocs/gcc/Other-Builtins.html
http://llvm.org/docs/BranchWeightMetadata.html#built-in-expe...
Except the standard’s likely and unlikely attributes invented new syntax that is not drop-in compatible with clang’s and gcc’s attributes.
Where clang and gcc would use:
if (__builtin_expect(x > 0, 1)) { … }
the standard uses: if (x > 0) [[likely]] { … }They can agree at ISO level but even then it isn't enough, as proven by the whole set of issues that are currently being ignored on platforms where breaking the ABI is tabu.
These are useful when there’s static knowledge about control flow that could assist the optimizer, e.g. with inlining decisions.
For example: It’s not uncommon to have “bi-modal” functions, where simple checks guard simple actions, followed by much more complex logic to handle everything else.
Are those checks for exceptional cases like an invariant violation? Think of an I/O write function confirming the device is open.
Or are they the “fast path” for the most common invocations? Think of std::vector::push_back() checking for available capacity.
The answer helps the optimizer immensely in deciding whether to “partially inline” the simple code into callers or not.
In general the profiler is a better tool, but there are rare exceptions and if those apply to you c++ gives you the control you need.
[[likely]] return 2;
or @!l return 2;
? Which one is more understandable if you're reading and not familiar to the syntax?Edit. It's because assume does not map to expect, it maps to builtin_assume. So that's just another way to write undefined c++.
Not a huge deal execution-time-wise, but from a reader’s PoV, the way I write it says, “ok, the special cases don’t apply to the body so I don’t have to worry that the index will be out of range (or whatever) and can just focus on the logic”.
"Standard Attributes in C and C++"
Anyway, while it is possible that some attributes can cause UB if misused, I very much doubt that's possible with [[likely]] and [[unlikely]], as they are just hints for the optimizer, and the optimizer is supposed to preserve semantical guarantees.