8

Long story short - I am writing a compiler, and reaching the OOP features I am faced with a dilemma involving the handling of destructors. Basically I have two options:

  • 1 - put all destructors for objects that need calling at that point in the program. This option sounds like it will be performance friendly and simple but will bloat the code, since depending on the control flow certain destructors can be duplicated multiple times.

  • 2 - partition destructors for each block of code with labels and "spaghetti jump" only through those that are needed. The upside - no destructors will be duplicated, the downside - it will involve non-sequential execution and jumping around, and also extra hidden variables and conditionals, which will be needed for example to determine whether execution leaves a block to continue execution in the parent block or to break/continue/goto/return, which also increases its complexity. And the extra variables and checks might very well eat up the space being saved by this approach, depending on how many objects and how complex structure and control flow inside of it is.

And I know the usual response to such questions is "do both, profile and decide" and that's what I would do if this was a trivial task, but writing a full featured compiler has proven somewhat arduous so I prefer to get some expert input rather than build two bridges, see which one does better and burn the other one.

I put c++ in the tags because that's the language I am using and am somewhat familiar with it and the RAII paradigm, which is what my compiler is modeling around as well.

Jonas
  • 121,568
  • 97
  • 310
  • 388
  • 2
    Why the downvote? If someone thinks there is something wrong with the question - leave a comment. –  Nov 02 '14 at 11:01
  • 2
    Apparently, downvoting questions is free, so people do that without really thinking.... I have no idea why... – Mats Petersson Nov 02 '14 at 11:02
  • Maybe you get some kind of reward if you downvote enough? Like an imaginary badge or something? –  Nov 02 '14 at 11:03
  • 1
    The topicality of this question is arguable and I see that it has already attracted one close vote for being too broad. It might be more topical, and attract fewer down and close votes over at http://programmers.stackexchange.com which seems to be far more friendly to conceptual and program design questions than SO has become. – High Performance Mark Nov 02 '14 at 11:07
  • I suggest you take a good look at *Inside the C++ Object Model* by Stanley Lippman. But I don't know why you think those are your only two options. The normal behaviour is to *call* destructors, just like any other method, not all this jump stuff. – user207421 Nov 02 '14 at 11:07
  • I'd post this to http://area51.stackexchange.com/proposals/66925/compilers – Marco A. Nov 02 '14 at 11:13
  • 1
    @HighPerformanceMark - too broad to say 1 or 2? I'd hate to be that guy who thinks this is broad... –  Nov 02 '14 at 11:14
  • 3
    As my answer says, not really 1 or 2 that is the right answer, in my view. But it's not a huge subject if you keep to basic principles. – Mats Petersson Nov 02 '14 at 11:17

4 Answers4

4

For the most part, a destructor call can be treated in the same manner as an ordinary function call.

The lesser part is dealing with EH. I've noticed MSC generates a mix of inlined destructor calls in "ordinary" code, and, for x86-64, creates separate cleanup code that itself may or may not have copies of destructor logic in it.

IMO, the simplest solution would be to always call nontrivial destructors as ordinary functions.

If optimization seems possible on the horizon, treat the aforementioned calls like anything else: Will it fit in the cache with everything else? Will doing this take up too much space in the image? Etc..

A frontend may insert "calls" to nontrivial destructors at the end of each actionable block in its output AST.

A backend may treat such things as ordinary function calls, wire them together, make a big block-o-destructor call logic somewhere and jump to that, etc...

Linking functions to the same logic seems quite common. For example, MSC tends to link all trivial functions to the same implementation, destructor or otherwise, optimizing or not.

This is primarily from experience. As usual, YMMV.

One more thing:

EH cleanup logic tends to work like a jump table: For a given function, you can just jump into a single list of destructor calls, depending on where an exception was thrown (if applicable).

defube
  • 2,395
  • 1
  • 22
  • 34
  • "For a given function, you can just jump into a single list of destructor calls, depending on where an exception was thrown" - that does sound like destructors for the same object will indeed be duplicated in different lists for different PC ranges? –  Nov 02 '14 at 11:46
  • Yes, you have a list of destructors for each point in the code that can throw an exception. You generated code to call all the destructors, and use the "where it may be thrown" as a key to where to jump to. Do try some simple code and see what an existing compiler does - with optimisation and without. – Mats Petersson Nov 02 '14 at 11:55
  • @MatsPetersson - that's basically what I was interested in, whether unique lists for each possible range are created or the destructors are "mentioned" only once in the end of their respective blocks and some more complex logic is used to jump only through the ones needed and skip the rest. –  Nov 02 '14 at 12:08
  • From what I've seen, destructors may well occur multiple times. – Mats Petersson Nov 02 '14 at 12:16
2

I don't know how commercial compilers come up with the code, but assuming we ignore exceptions at this point [1], the approach I would take is to make a call to the destructor, not inline it. Each destructor would contain the complete destructor for that object. Use a loop to deal with destructors of arrays.

To inline the calls is an optimisation, and you shouldn't do that unless you "know it pays off" (code-size vs. speed).

You will need to deal with "destruction in the enclosing block", but assuming you don't have jumps out of the block, that should be easy. Jumps out of block (e.g. return, break, etc) will mean that you have to jump to a piece of code that cleans up the block you are in.

[1] Commercial compilers have special tables based on "where was the exception thrown", and a piece of code generated to do that cleanup - typically reusing the same cleanup for many exception points by having multiple jump labels in each chunk of cleanup.

Mats Petersson
  • 126,704
  • 14
  • 140
  • 227
  • I do also implement exception handling, so the same question would apply there as well - inline and duplicate or spaghetti jump. But I have still not decided whether exception handling data is to be allocated from the get go or only when an exception is raised, so at this point I don't know if I will reuse exception table data for all destructors, but sure sounds like a good idea. –  Nov 02 '14 at 11:17
  • BTW by "inline" I mean put all destructors in that place, not inline as in "function inlining" although trivial enough constructors will be inlined. That's why in some cases destructors will be duplicated, say if a local is destroyed upon returning from the main or a sub block. –  Nov 02 '14 at 11:22
  • 1
    @user3735658 So please use standard terminology in standard ways. Otherwise nobody knows what you're talking about. – user207421 Nov 02 '14 at 11:26
  • @user3735658 Not really. You're still taking about jumping, and about duplicating destructors. – user207421 Nov 02 '14 at 11:31
  • @EJP - because that still stands. The problem was using `inline` in a more general context. –  Nov 02 '14 at 11:35
  • 1
    I would definitely recommend solving one problem at a time. Come up with a way to "do the destruction of the objects in this block", then deal with what to do with exceptions as a separate (but related) subject. And optimisation comes later. I don't see them using the same code - but there may be "later on" optimisation that can be done in this. – Mats Petersson Nov 02 '14 at 11:53
2

Compilers use a mix of both approaches. MSVC uses inline destructor calls for normal code flow and clean up code blocks in reverse order for early returns and exceptions. During normal flow, it uses a single hidden local integer to track constructor progress thus far, so it knows where to jump upon early returns. A single integer is sufficient because scope always forms a tree (rather than say using a bitmask for each class that has or has not been constructed successfully). For example, the following fairly short code using a class with a non-trivial destructor and some random returns sprinkled throughout...

    ...
    if (randomBool()) return;
    Foo a;
    if (randomBool()) return;
    Foo b;
    if (randomBool()) return;

    {
        Foo c;
        if (randomBool()) return;
    }

    {
        Foo d;
        if (randomBool()) return;
    }
    ...

...can expand to pseudocode like below on x86, where the constructor progress is incremented immediately after each constructor call (sometimes by more than one to the next unique value) and decremented (or 'popped' to an earlier value) immediately before each destructor call. Note that classes with trivial destructors do not affect this value.

    ...
    save previous exception handler // for x86, not 64-bit table based handling
    preallocate stack space for locals
    set new exception handler address to ExceptionCleanup
    set constructor progress = 0
    if randomBool(), goto Cleanup0
    Foo a;
    set constructor progress = 1 // Advance 1
    if randomBool(), goto Cleanup1
    Foo b;
    set constructor progress = 2 // And once more
    if randomBool(), goto Cleanup2

    {
        Foo c;
        set constructor progress = 3
        if randomBool(), goto Cleanup3
        set constructor progress = 2 // Pop to 2 again
        c.~Foo();
    }

    {
        Foo d;
        set constructor progress = 4 // Increment 2 to 4, not 3 again
        if randomBool(), goto Cleanup4
        set constructor progress = 2 // Pop to 2 again
        d.~Foo();
    }

// alternate Cleanup2
    set constructor progress = 1
    b.~Foo();
// alternate Cleanup1
    set constructor progress = 0
    a.~Foo();

Cleanup0:
    restore previous exception handler
    wipe stack space for locals
    return;

ExceptionCleanup:
    switch (constructor progress)
    {
    case 0: goto Cleanup0; // nothing to destroy
    case 1: goto Cleanup1;
    case 2: goto Cleanup2;
    case 3: goto Cleanup3;
    case 4: goto Cleanup4;
    }
    // admitting ignorance here, as I don't know how the exception
    // is propagated upward, and whether the exact same cleanup
    // blocks are shared for both early returns and exceptions.

Cleanup4:
    set constructor progress = 2
    d.~Foo();
    goto Cleanup2;
Cleanup3:
    set constructor progress = 2
    c.~Foo();
    // fall through to Cleanup2;
Cleanup2:
    set constructor progress = 1
    b.~Foo();
Cleanup1:
    set constructor progress = 0
    a.~Foo();
    goto Cleanup0;
    // or it may instead return directly here

The compiler may of course rearrange these blocks anyway it thinks is more efficient, rather than putting all the cleanup at the end. Early returns could jump instead to the alternate Cleanup1/2 at the end of the function. On 64-bit MSVC code, exceptions are handled via tables that map the instruction pointer of when the exception happened to respective code cleanup blocks.

Dwayne Robinson
  • 2,034
  • 1
  • 24
  • 39
1

An optimizing compiler is transforming the internal representations of the compiled source code.

It usually build a directed (usually cyclic) graph of basic blocks. When building this control flow graph it is adding the call to the destructors.

For GCC (it is a free software compiler - and so is Clang/LLVM -, so you could study its source code), you probably could try to compile some simple C++ test case code with -fdump-tree-all and then see that it is done at gimplification time. BTW, you could customize g++ with MELT to explore its internal representations.

BTW, I don't think that how you deal with destructors is that important (notice that in C++ they are implicitly called at syntactically defined places, like } of their defining scope). Most of the work of such a compiler is in optimizing (then, dealing with destructors is not very relevant; they nearly are routines like others).

Basile Starynkevitch
  • 223,805
  • 18
  • 296
  • 547
  • I would think that using clang++ as a source for understanding how it works would be a better choice - the code is a lot easier to understand (it's still a HUGE amount of code, so I'm not saying it's TRIVIAL) – Mats Petersson Nov 02 '14 at 11:30
  • But I am much more familiar with GCC than with Clang. – Basile Starynkevitch Nov 02 '14 at 11:31
  • Clang is definitely "cleaner", GCC looks like one big blob of mess. –  Nov 02 '14 at 11:36
  • Not exactly true.... But indeed GCC is a little bit messier than Clang. However, it is able of better optimizations. – Basile Starynkevitch Nov 02 '14 at 11:37
  • "BTW, I don't think that how you deal with destructors is that important." - because user explicit routines are "on the explicit spot" while destructors are implicit and may be invoked on different places as control flow changes blocks. –  Nov 02 '14 at 11:38