Currently, we have several ways to define non-POD globals. The many different ways (base::LazyInstance, base::Singleton, base::NoDestructor, CR_DEFINE_STATIC_LOCAL, and a function-local static) all support the same set of basic restrictions:
- the global should be lazily constructed
- the global must not generate global constructors or destructors.
To simplify the situation, I'd like to propose that we standardize on using base::NoDestructor for leaky non-POD globals.
Why use base::NoDestructor?
- unlike CR_DEFINE_STATIC_LOCAL or defining a function-local static that leaks the object, the object is stored inline. In theory, this should be more efficient since we don't need to deref a pointer to follow it to the heap block where it actually lives.
- unlike base::LazyInstance and base::Singleton, it relies on the built-in thread-safety of function-local statics in C++11. The synchronization logic in base::LazyInstance and base::Singleton has had bugs in the past, such as https://crrev.com/527445. Being able to remove this subtle code would be nice.
Note: while most code in Chromium doesn't need to be thread-safe, we have thread-safe statics enabled globally, so we're already paying the cost globally. Perhaps compilers will provide a way to opt-out in the future, so we only pay this cost for things that really need to be thread-safe… - unlike base::LazyInstance, it supports forwarding arguments to the type's constructor. This makes it easier to support more complex initialization.
What are the disadvantages of base::NoDestructor?
- The storage cost for an object allocated in base::NoDestructor is paid up front, since the storage is inline. In contrast, we don't pay the storage cost for CR_DEFINE_STATIC_LOCAL or a heap-allocated function-local static until the object is accessed. To mitigate this, we could conditionally heap-allocate for objects larger than a certain size (in fact, this is what Blink does).
- We may not actually trust the toolchain implementation to be free of things like priority-inversion bugs either.
For most things, I think getting rid of the extra pointer hop is a win. And if we need to tweak the allocation strategy to avoid putting too much data in bss, it's fairly simple to tweak later on.
Why get rid of base::Singleton?
The only thing it provides that the other helpers don't is an attempt to enforce that a given type is only instantiated once globally, but the only way to implement this in C++ encourages potential ODR violations.
Most uses can be replaced with base::NoDestructor; things that really need the current destroy-at-exit behavior can use base::LazyInstance::DestroyAtExit.
Thoughts?
Daniel