Jump to page: 1 2
Thread overview
On the D Blog--Symphony of Destruction: Structs, Classes, and the GC
Mar 04
Dukc
Mar 19
apz28
March 04
This post is 3+ years overdue. I initially put it off for the lack of a purpose-built tool in the language or the library to distinguish between normal destruction and finalization (when the GC is invoked by the destructor). After we got the `GC.inFinalizer` thing and having made a few stalled attempts to get the thing written, I finally sat down a couple of weeks ago and forced myself to finish it.

The result is not what I had originally intended, as the topic turned out to be much more involved than I had realized. What was supposed to be one post very quickly became two, and now it looks like there will be at least four before I'm done. (I didn't even get to the GC.inFinalizer thing in this first post.) Object destruction in D has dark corners that I had never knew existed until recently, and I expect that as I experiment with them and talk with some battle-hardened warriors like Adam, I'll find myself with many more words to write on the topic.

The blog:
https://dlang.org/blog/2021/03/04/symphony-of-destruction-structs-classes-and-the-gc-part-one/

Reddit:
https://www.reddit.com/r/programming/comments/lxkcxp/symphony_of_destruction_structs_classes_and_the/

The GC series to date:
https://dlang.org/blog/the-gc-series/


March 04
On Thursday, 4 March 2021 at 13:54:48 UTC, Mike Parker wrote:
>
> The blog:
> https://dlang.org/blog/2021/03/04/symphony-of-destruction-structs-classes-and-the-gc-part-one/

"Some examples: attempting to index an associative array can trigger an attempt to allocate a RangeError if the key is not present; a failed assert will result in allocation of an AssertError; calling any function not annotated with @nogc means GC operations are always possible in the call stack. These and any such operations should be avoided in the destructors of GC-managed objects."

I don't understand this part. If an assert was failing, the program is going to terminate anyway, so InvalidMemoryOperationError is no problem. Well, it might obfuscate the underlying error if there is no stack trace, but banning `assert`ing in anything that could be called by a destructor sounds too drastic to me. Even the lowest level system code tends to contain asserts in D, at least in my codebase. If asserting is banned, destructors can do faily much nothing. I'd think it's much more practical to redefine the assert failure handler if InvalidMemoryOperationError due to a failed assert is a problem.
March 04
On Thu, Mar 04, 2021 at 11:42:58PM +0000, Dukc via Digitalmars-d-announce wrote:
> On Thursday, 4 March 2021 at 13:54:48 UTC, Mike Parker wrote:
[...]
> If an assert was failing, the program is going to terminate anyway, so InvalidMemoryOperationError is no problem. Well, it might obfuscate the underlying error if there is no stack trace, but banning `assert`ing in anything that could be called by a destructor sounds too drastic to me. Even the lowest level system code tends to contain asserts in D, at least in my codebase. If asserting is banned, destructors can do faily much nothing. I'd think it's much more practical to redefine the assert failure handler if InvalidMemoryOperationError due to a failed assert is a problem.

This is precisely why Walter (and others) have said that assert failures should not throw anything, they should simply terminate (perhaps calling a user-defined panic function right before aborting, if special handling is needed).

That, or we take Mike's advice to pretend that class dtors don't exist.


T

-- 
The diminished 7th chord is the most flexible and fear-instilling chord. Use it often, use it unsparingly, to subdue your listeners into submission!
March 05
On Thursday, 4 March 2021 at 23:42:58 UTC, Dukc wrote:

>
> I don't understand this part. If an assert was failing, the program is going to terminate anyway, so InvalidMemoryOperationError is no problem. Well, it might obfuscate the underlying error if there is no stack trace, but banning `assert`ing in anything that could be called by a destructor sounds too drastic to me. Even the lowest level system code tends to contain asserts in D, at least in my codebase. If asserting is banned, destructors can do faily much nothing. I'd think it's much more practical to redefine the assert failure handler if InvalidMemoryOperationError due to a failed assert is a problem.

Yes, this can be worked around. Passing -checkaction=C to the compiler will use the C assert handler and no exception will be thrown (which, IMO, should be the default behavior of asserts anyway). But even then, I believe as a general rule that any code which touches an assert has no place in a finalizer. And that's because asserts are inherently deterministic.

A properly-written assert is used to verify an expectation that the program is in a specific state at a specific point in its execution. If the program is not in that state at that point, then we know we've got an error in our code. It's because of this determinism that we can remove asserts from released code and expect that nothing will break, and it's why we don't assert on conditions that are beyond our control (like user input).

Because finalizers are non-deterministic, they kill that "at a specific point in the program's execution" part, rendering any asserts they touch unreliable. It is realistically possible that an assert invoked in a finalizer never triggers during development, then the program is released with asserts removed, and then it breaks out in the wild because a finalizer is invoked at a point when the program isn't in the expected state.

This doesn't make *destructors* useless, but *finalizers* really are mostly useless most of the time, IMO. As D programmers, we need to consciously be aware of the distinction since the language isn't.


March 05
On Thursday, 4 March 2021 at 13:54:48 UTC, Mike Parker wrote:

> The blog:
> https://dlang.org/blog/2021/03/04/symphony-of-destruction-structs-classes-and-the-gc-part-one/

"The destructors of all stack-allocated structs in a given scope are invoked when the scope exits."

Please add a note that temporaries are scoped to the full expression, not the block.
March 05
On 3/4/21 6:42 PM, Dukc wrote:
> On Thursday, 4 March 2021 at 13:54:48 UTC, Mike Parker wrote:
>>
>> The blog:
>> https://dlang.org/blog/2021/03/04/symphony-of-destruction-structs-classes-and-the-gc-part-one/ 
>>
> 
> "Some examples: attempting to index an associative array can trigger an attempt to allocate a RangeError if the key is not present; a failed assert will result in allocation of an AssertError; calling any function not annotated with @nogc means GC operations are always possible in the call stack. These and any such operations should be avoided in the destructors of GC-managed objects."
> 
> I don't understand this part. If an assert was failing, the program is going to terminate anyway, so InvalidMemoryOperationError is no problem. Well, it might obfuscate the underlying error if there is no stack trace, but banning `assert`ing in anything that could be called by a destructor sounds too drastic to me. Even the lowest level system code tends to contain asserts in D, at least in my codebase. If asserting is banned, destructors can do faily much nothing. I'd think it's much more practical to redefine the assert failure handler if InvalidMemoryOperationError due to a failed assert is a problem.

And technically, a range error does not allocate.

https://github.com/dlang/druntime/blob/306bd965ea9d83bad7e5444ff9d5e1af1a6d934a/src/core/exception.d#L472-L485

But there is still a possibility of allocation if you happen to rehash an AA.

-Steve
March 05
On 3/4/21 6:42 PM, Dukc wrote:
> On Thursday, 4 March 2021 at 13:54:48 UTC, Mike Parker wrote:
>>
>> The blog:
>> https://dlang.org/blog/2021/03/04/symphony-of-destruction-structs-classes-and-the-gc-part-one/ 
>>
> 
> "Some examples: attempting to index an associative array can trigger an attempt to allocate a RangeError if the key is not present; a failed assert will result in allocation of an AssertError; calling any function not annotated with @nogc means GC operations are always possible in the call stack. These and any such operations should be avoided in the destructors of GC-managed objects."
> 
> I don't understand this part. If an assert was failing, the program is going to terminate anyway, so InvalidMemoryOperationError is no problem. Well, it might obfuscate the underlying error if there is no stack trace, but banning `assert`ing in anything that could be called by a destructor sounds too drastic to me. Even the lowest level system code tends to contain asserts in D, at least in my codebase. If asserting is banned, destructors can do faily much nothing. I'd think it's much more practical to redefine the assert failure handler if InvalidMemoryOperationError due to a failed assert is a problem.

One further note: you want to avoid InvalidMemoryOperationError AT ALL COSTS if you can. Why? Because it identifies not the file or line which triggered the invalid memory operation, but the place where it's thrown in druntime (currently here: https://github.com/dlang/druntime/blob/306bd965ea9d83bad7e5444ff9d5e1af1a6d934a/src/core/exception.d#L539) with NO stack trace.

So if you get an IMOE, you will have no idea why. This is a defect in D in my opinion, and needs fixing. I've spent hours chasing these types of things down.

-Steve
March 18
On Thursday, 4 March 2021 at 13:54:48 UTC, Mike Parker wrote:
The blog:
> https://dlang.org/blog/2021/03/04/symphony-of-destruction-structs-classes-and-the-gc-part-one/

Reminds me of longing for an (optional) compiler warning or, even better, a deprecation when

- destructors of GC-managed objects that perform any operation that can potentially result in a GC allocation request and /or
- allocating structs on the GC heap that have destructors

.

In the mean time a good rule of thumb is to qualify all class destructors as @nogc. I suggest you add this advice to the article, Mike.

Thanks!
March 18
On Thursday, 4 March 2021 at 13:54:48 UTC, Mike Parker wrote:
> The blog:
> https://dlang.org/blog/2021/03/04/symphony-of-destruction-structs-classes-and-the-gc-part-one/

Btw, what is the motive behind D's GC not being able to correctly handle GC allocations in  class destructors.

Is it by design or because of limitations in D's current GC implementation?

And how does this relate to exception-throwing destructors in other managed languages such as C# and Go; are they forbidden or allowed and safe thanks to a more resilient GC?
March 18
On Thursday, 18 March 2021 at 08:15:01 UTC, Per Nordlöw wrote:

>
> In the mean time a good rule of thumb is to qualify all class destructors as @nogc. I suggest you add this advice to the article, Mike.
>

I actually don't agree with that. I'll be discussion the solution in the next article:

if(!GC.inFinalizer) {
...
}

It's perfectly fine to perform GC operations in destructors when they aren't invoked by the GC.
« First   ‹ Prev
1 2