>Even after the static variable has been initialised, the overhead of accessing it is still considerable: a function call to __cxa_guard_acquire(), plus atomic_load_explicit(&__b_guard, memory_order::acquire) in __cxa_guard_acquire().
No. The lock calls are only done during initialization, in case two threads run the initialization concurrently while the guard variable is 0. Once the variable is initialized, this will always be skipped by "je .L3".
That's a nice trick, but contrary to function statics, it is susceptible to SIOF.
This kind of optimization is useful only on extraordinarily hot paths, so I wouldn't generally recommend it.
> On ARM, such atomic load incurs a memory barrier---a fairly expensive operation.
Not quite, it is just a load-acquire, which is almost as cheap as a normal load. And on x86 there's no difference.
One thing where both GCC and Clang seem to be quite bad at is code layout: even in the example in the article, the slow path is largely inlined. It would be much better to have just a load, a compare, and a jump to the slow path in a cold section.
In my experience, in some rare cases reimplementing the lazy initialization explicitly (especially when it's possible to use a sentinel value, thus doing a single load for both value and guard) did produce a noticeable win.
> For this we need a certain old, but little-known feature of UNIX linkers
STOP WRITING NON-PORTABLE CODE YOU BASTARDS.
The correct answer is, as always, “stop using mutable global variables you bastard”.
Signed: someone who is endlessly annoyed with people who incorrectly think Unix is the only platform their code will run on. Write standard C/C++ that doesn’t rely on obscure tricks. Your co-workers will hate you less.
Author here. This is part of SPDK-based server code. Transportability outside of UNIX (mostly Linux) is entirely irrelevant. By the way, the global variables here are immutable after the initialisation, that's the point.
Unless you missed it from another comment in this thread, the optimization you made is based on a wrong assumption and misreading the asm code: "je .L3" will skip all the costs you consider.
Funny enough I recently wrote my own hack using this linker feature in C, to implement an array of static counter definitions that can be declared anywhere and then written out (e.g., to prometheus) in one place.
Note that as I later found out, this doesn't work with Mac OS's linker, so you need to use some separate incantations for Mac OS.
The rabbit hole I just went down is called C/C++ Statement Expressions [1] which are a GCC extension:
#define FAST_STATIC(T) \
*({ \
\ // statements separated by semicolons
reinterpret_cast<T *>(ph.buf); \ // the value of the macro as a statement
})
The reinterpret_cast<T*>(...) statement is a conventional C++ Expression Statement, but when enclosed in ({}), GCC considers the whole kit and kaboodle a Statement Expression that propagates a value.
There is no C equivalent, but in in C++, since C++11 you can achieve the same effect with lambdas:
auto value = [](){ return 12345; }();
As noted in the linked SO discussion, this is analogous to a JS Immediately-Invoked Function Expression (IIFE).
“Dynamic initialization of a block-scope variable with static storage duration or thread storage duration is performed the first time control passes through its declaration
[…]
this would initialise everything correctly: by the time foo() is called, its b has already been initialised.”
I guess that means this trick can change program behavior, especially if the function containing the static is never called in a program’s execution.
The way block scope statics are handled in C++ is a mistake. Block scope statics that don't depend on any non-static local variables should be initialized when the program starts up. E.g.:
void fun(int arg)
{
static obj foo(arg); // delayed until function called (dependency on arg)
static obj bar(); // inited at program start (no dependency on arg)
}
In other words, any static that can be inited at program startup should be, leaving only the troublesome cases that depend on run-time context.
11 comments
[ 2.8 ms ] story [ 34.2 ms ] threadNo. The lock calls are only done during initialization, in case two threads run the initialization concurrently while the guard variable is 0. Once the variable is initialized, this will always be skipped by "je .L3".
> On ARM, such atomic load incurs a memory barrier---a fairly expensive operation.
Not quite, it is just a load-acquire, which is almost as cheap as a normal load. And on x86 there's no difference.
One thing where both GCC and Clang seem to be quite bad at is code layout: even in the example in the article, the slow path is largely inlined. It would be much better to have just a load, a compare, and a jump to the slow path in a cold section. In my experience, in some rare cases reimplementing the lazy initialization explicitly (especially when it's possible to use a sentinel value, thus doing a single load for both value and guard) did produce a noticeable win.
Why not just use constinit (iff applicable), construct_at, or lessen the cost with -fno-threadsafe-statics?
STOP WRITING NON-PORTABLE CODE YOU BASTARDS.
The correct answer is, as always, “stop using mutable global variables you bastard”.
Signed: someone who is endlessly annoyed with people who incorrectly think Unix is the only platform their code will run on. Write standard C/C++ that doesn’t rely on obscure tricks. Your co-workers will hate you less.
Note that as I later found out, this doesn't work with Mac OS's linker, so you need to use some separate incantations for Mac OS.
https://github.com/abseil/abseil-cpp/blob/master/absl/base/n...
Which is basically the only usage of std::launder I have seen
There is no C equivalent, but in in C++, since C++11 you can achieve the same effect with lambdas:
As noted in the linked SO discussion, this is analogous to a JS Immediately-Invoked Function Expression (IIFE).[1] https://stackoverflow.com/questions/76890861/what-is-called-...
“Dynamic initialization of a block-scope variable with static storage duration or thread storage duration is performed the first time control passes through its declaration
[…]
this would initialise everything correctly: by the time foo() is called, its b has already been initialised.”
I guess that means this trick can change program behavior, especially if the function containing the static is never called in a program’s execution.