Zero-overhead Destructors in C

CNXSoft: This is another guest by Blu, this time about C programming, and specifically destructors in C programming language

If you asked seasoned C++ developers what their favorite features in the C++ language might be, chances are that destructors would be on everybody’s shortlist. As many other C++ developers, I too tend to do my occasional share of C, and if there’s one feature I dearly miss in C that is destructors precisely in their capacity of automating the release of resources at the right moment. But first a disclaimer is in order: many people call simple application of destructors ‘RAII’ ‘Resource Acquisition Is Initialization’; I find this acronym unnecessarily awkward and obfuscating an otherwise straightforward concept, so you won’t see this acronym through the end of this text. Instead, I’ll be using ‘end-of-scope’ action.

Traditionally, in the language of C end-of-scope (more often end-of-function) actions are achieved via deliberate arrangement of the control flow, often via goto’s and collector labels, occasionally via long jumps. Unfortunately, goto’s and jumps have the propensity to not make the code any easier to read once they accumulate in sufficient quantities. I’ve cut my teeth in BASIC and assembly, and I still read plenty of assembly to this day, but I’m not fond of goto’s and try to use them sparingly in C. Had C had destructors I wouldn’t have to worry ‘What exactly do I need to clean up upon encountering this error now and do I already have a label for that?’ such questions normally do not stand with destructors.

In terms of functionality, destructors are trivial to implement in C for every local variable that needs end-of-scope action, you register that into some kind of a per-scope ‘registry’, and then explicitly act to the content of that registry upon leaving said scope easy-peasy. Unfortunately, there is a performance price associated with that doing container iterations at run-time, perhaps concluded with potentially-expensive indirect calls. Now that is something C++ compilers don’t do they just ‘know’ what local variables have what corresponding destructors, and call those directly, virtual destructors notwithstanding, at end-of-scope conditions. Ergo, you won’t see much C code mimicking C++ destructors in contrast to other popular languages C developers do not take overhead lightly.

Contemporary C (and more so C++) compilers are ultra-sophisticated code-semantics analysis tools. They have to be in order to optimize effectively today’s gargantuan code bases. One of their fundamental ‘tricks of the optimization trade’ is ‘constant folding’ detection of compile-time constants and the subsequent move of related computations from run-time to compile-time, where that would make sense. So, dealing with C++ compiler optimizations day-in and day-out, and having just gone through yet another ‘goto’s, goto’s everywhere’ piece of otherwise-magnificent C code, I had to ask myself ‘Can we use constant folding to implement proper, zero-overhead destructors in C?’

Again, we don’t have to be overly-smart with the general approach that remains the same. We just have to ‘persuade’ the C compiler to ‘acknowledge’ the constants in our improvised implementation of destructors, and fold the associated run-time computations accordingly. Very much to the same effect a C++ compiler does with real C++ destructors. So, let’s get to business.

The code we will be examining will contain an implementation of the proposed mechanism and an example use case. For that we need a structure to which we will apply constructors and destructors:


And here are our example constructor (ctor) and destructor (dtor):


According to those, our structure Foo is initialized by setting its member ‘val’ to 42, and de-initialized by setting the same member to 43 just arbitrary code we can easily tell apart. Eventually, we print out the name of the function, the address of the ‘self’ variable and its val member at the end of each function. Clearly, printing the name of the function *and* the value of val presents an informational redundancy to the printf reader, but hey, we are generous today : )

Let’s say we have the following declarations in our main:


If our proposed ctor and dtor magically worked, we would expect the following scenarios to hold true:


In order to make that a reality, we need to follow our original plan of introducing a ‘registry’ of all constructed variables per scope. For one, our constructor will need to know of such a registry. But more importantly, we need to decide what to keep in that registry. Clearly, we need an association between destructors and variable addresses, or in other words, we need destructor closures. We devise such a structure (‘term_t’ for ‘terminal’):


Now, a registry of those constitutes some kind of a storage, and what is more natural in C than an array? We modify our constructor accordingly:


Now, we clearly did something extra here we also supplied the registry size as an arg to the constructor, which size we subsequently pass down to the tail-call that does the actual job:


At this point we have a constructor and a destructor that can work with a registry of destructor closures, if we had any. So let’s provide some, shall we?


Finally, we need something to act upon those destructor-closure registries:


Of course, since we are discussing C here, nobody will call our ‘term_scope’ function unless we place it accordingly ourselves:


Voila, we have ensured the desired behavior from our test scenarios above. But now follows the ‘billion-dollar’ question: is that mechanism zero-overhead at runtime? It clearly does ‘stuff’ at runtime, so can a compiler optimize that out via constant folding?

Long story short it could, under a couple of pre-conditions. I will spare you all the versions of the code which do not work (i.e. will never get properly optimized by the compiler) you can experiment and find those for yourself. I will just mention that for the presented scheme to be successfully constant-folded, the compiler needs to be certain of the sizes of the term_t arrays, ergo there are two things we have to ensure:

  1. The constructor (Foo_ctor) and the registrar function (insert_dtor) have to be inlined
  2. The registry-size term ‘count’ needs to be a stand-alone variable placing that in any kind of structure breaks the magic.

Ok, back to the code at hand. It does look somewhat unwieldy, so let’s sprinkle some macro beautifiers over it:


Those would allow us to write our example in the following manner:


Not so bad now, eh? You can get all discussed code from here.

As a final note, this technique works to a zero-overhead result in gcc and clang. Other popular compilers might or might not cnstant-fold our expressions for instance Intel’s icc and Microsoft’s msvc fail as of their current versions. Hopefully their constant-folding optimization abilities improve in the future.

Share this:

Support CNX Software! Donate via cryptocurrencies, become a Patron on Patreon, or purchase goods on Amazon or Aliexpress

Radxa Orion O6 Armv9 mini-ITX motherboard
Subscribe
Notify of
guest
The comment form collects your name, email and content to allow us keep track of the comments placed on the website. Please read and accept our website Terms and Privacy Policy to post a comment.
18 Comments
oldest
newest
Boardcon CM3588 Rockchip RK3588 System-on-Module designed for AI and IoT applications