-
Type: Improvement
-
Resolution: Unresolved
-
Priority: Major - P3
-
None
-
Affects Version/s: None
-
Component/s: None
-
Server Programmability
These are a handful of ideas that should reduce the performance and/or hot-code size impact of using invariants. Some of these are mutually exclusive. And some should only be done in production builds because they may harm debuggability. They can either be things we turn on late in the release cycle (eg after branching) or decided based on kDebugBuild. Because they only change behavior in the case where an invariant is violated and we are going to terminate, I think the normal concerns around changing debug vs release behavior don't apply, because code can't rely on this behavior. A few options involve inline asm. This should be very simple asm and should only be done for linux on AArch64 and x64_64.
- Wrap the failure path in a lambda with noinline and cold attributes, similar to how we do for xassert. This makes it easier for the compiler to pull it out to a separate function in the .text.cold section.
- Last time I tried this it was complicated by us taking either a bool expression or a Status in the normal invariant. We may want to add a separate invariantStatusOK(), which might also help with other things here.
- Mark all fatal functions with [[gnu::leaf]]. Combined with noreturn and noexcept, this means that the compiler is able to assume that they never end up back at the caller because they can't return, throw, or longjmp.
- Rather than taking separate file/line/exprStr arguments (where file and exprMsg each take 2 instructions to fill on aarch64), pass a single pointer to a statically allocated struct with that info. If we really want to minimize the impact we could lay out the struct using inline asm where the string pointers are encoded with their offsets from the struct pointer. This is both smaller (32 bits) and doesn't require a relocation at startup.
- We could consider restricting the message-taking invariant to only accept constexpr strings, so that they could also be encoded into the struct.
- MONGO_(un)likely currently uses __builtin_expect which is a fairly weak hint and by default implies only a 90/10% split probability. We may want to use __builtin_expect_with_probability to use a 100/0% split, either with the existing macro or a new MONGO_very_(un)likely which is used only with fatal asserts and tassert since they are expected to never happen in production.
- (Probably prod only) Consider trapping rather calling an invariant failure function. This is what libc++ does by default when you enable hardening.
- We could either try to minimize size impact by using __builtin_trap() which will let the compiler common all invariant failures within a function to a single trap, or we could maximize debuggability by using inline asm to ensure that each invariant gets its own trap instruction, so that we can get precise file/line info from the trapping instruction address.
- If we use inline asm, we could arrange to put the Status (a single pointer) into a specific register (probably the one used for return values) and fish it out of the ucontext passed to the signal handler.
- We could also use inline asm to encode a mapping from the instruction address of the trap (which is passed to the signal handler) to a pointer to the statically allocated struct described above which has all of the information we currently print on failure.