www.digitalmars.com         C & C++   DMDScript  

digitalmars.D.announce - On the D Blog--Symphony of Destruction: Structs, Classes, and the GC

reply Mike Parker <aldacron gmail.com> writes:
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/
Mar 04
next sibling parent reply Dukc <ajieskola gmail.com> writes:
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.
Mar 04
next sibling parent "H. S. Teoh" <hsteoh quickfur.ath.cx> writes:
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!
Mar 04
prev sibling next sibling parent Mike Parker <aldacron gmail.com> writes:
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.
Mar 04
prev sibling next sibling parent Steven Schveighoffer <schveiguy gmail.com> writes:
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-
nd-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
Mar 05
prev sibling parent Steven Schveighoffer <schveiguy gmail.com> writes:
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-
nd-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/co e/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
Mar 05
prev sibling next sibling parent Max Samukha <maxsamukha gmail.com> writes:
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.
Mar 05
prev sibling next sibling parent reply Per =?UTF-8?B?Tm9yZGzDtnc=?= <per.nordlow gmail.com> writes:
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!
Mar 18
parent reply Mike Parker <aldacron gmail.com> writes:
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.
Mar 18
parent Per =?UTF-8?B?Tm9yZGzDtnc=?= <per.nordlow gmail.com> writes:
On Thursday, 18 March 2021 at 12:21:46 UTC, Mike Parker wrote:
 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.
Could we at least add some guard in the GC that notifies the user of the reason for getting an exception, preferrably including a source position, when trying to allocate in a destructor run during finalization? Not getting an explanation has stolen hours of my development time on several occasion. And likely happen in the future for other users aswell potentially making them abandon D for other languages.
Mar 19
prev sibling parent reply Per =?UTF-8?B?Tm9yZGzDtnc=?= <per.nordlow gmail.com> writes:
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 allowed and safe thanks to a more resilient GC?
Mar 18
next sibling parent reply Petar Kirov [ZombineDev] <petar.p.kirov gmail.com> writes:
On Thursday, 18 March 2021 at 09:21:27 UTC, Per Nordlöw 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/
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?
Just implementation deficiency. I think it is fixable with some refactoring of the GC pipeline. One approach would be, (similar to other language implementations - see below), that GC-allocated objects with destructors should be placed on a queue and their destructors be called when the GC has finished the collection. Afterwards, the GC can release their memory during the next collection.
 And how does this relate to exception-throwing destructors in 

 or allowed and safe thanks to a more resilient GC?
TL;DR * Go doesn't have exceptions or destructors. You can attach a finalizer function to an object via [0] which will be called before the object will be collected. After the associated finalizer is called, the object is marked as reachable again and the finalizer function is unset. Since all finalizers are called in a separate goroutine, it is not an issue to allocate memory from them, as technically this happens separately from the actual garbage collection. they can't be used to implement the RAII design pattern. They are even less deterministic than destructors of GC-allocated classes in D, as they're only called automatically by the runtime and by an arbitrary thread. Their runtime designed in such a way that memory allocation in destructors is not a problem at all, however the default policy is that thrown exceptions terminate the process, though that could be configured differently. --- Instead of destructors, the recommended idiom in Go is to wrap resources in wrapper structs and implement a Close() method for those types, which the user of the code must not forget to call manually and sometimes check for error. They have `defer`, which are not reliable and should probably be only used as a safety net to detect whether an object was forgotten to be closed manually. If a finalizer takes a long time to complete a clean-up task, it is recommended that it spawns a separate goroutine. --- `~T()` (rather than D's `~this()`) which is lowered to a method that overrides the Object.Finalize() base method like so: class Resource { ~Resource() { /* custom code */ } } // user code // gets lowered to: class Resource { protected override Finalize() { try { /* custom code */ } finally { base.Finalize(); } } } Which means that finalization happens automatically from the most-derived class to the least derived one. This lowering also implies that the implementation is tolerant to exceptions. It is an compile-time error to manually define a `Finalize` method. Finalizers can only be defined by classes (reference types) and not structs (value types). Finalizers are only be called automatically (there's no analog to D's `destroy` or C++'s `delete`) and the only way to force that is using `System.GC.Collect()`, which is almost always bad idea. Finalizers used to be called at the end of the application when targeting .NET Framework, but the docs say that this is no longer the case with the newer the .NET Core, though this may have been addressed after the docs were written. The implementation may call finalizers from any thread, so your code must be prepared to handle that. Given that finalizers are unsuitable for deterministic resource management, it is strongly recommended that class authors should implement the IDisposable [3] interface. Users of classes that implement IDisposable can either manually call IDisposable.Dispose() or they can use the `using` statement [4], which is lowered something like this: using var r1 = new Resource1(); using var r2 = new Resource1(); /* some code */ // vvvvvvvvvvvvvvv { Resource1 r1 = new Resource1(); try { { Resource2 r2 = expression; try { /* some code */ } finally { if (r2 != null) ((IDisposable)r2).Dispose(); } } } finally { if (r1 != null) ((IDisposable)r1).Dispose(); } } IDisposable.Dispose() can be called multiple times (though this is discouraged), so your implementation of this interface must be able to handle this. Finalizer should call the Dispose() function as a safety net. [0]: https://golang.org/pkg/runtime/#SetFinalizer [1]: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/classes#destructors [2]: https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/destructors [3]: https://docs.microsoft.com/en-us/dotnet/api/system.idisposable?view=net-5.0 [4]: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/using-statement
Mar 18
next sibling parent reply Mike Parker <aldacron gmail.com> writes:
On Thursday, 18 March 2021 at 12:27:56 UTC, Petar Kirov 
[ZombineDev] wrote:

 Just implementation deficiency. I think it is fixable with some 
 refactoring of the GC pipeline. One approach would be, (similar 
 to other language implementations - see below), that 
 GC-allocated objects with destructors should be placed on a 
 queue and their destructors be called when the GC has finished 
 the collection. Afterwards, the GC can release their memory 
 during the next collection.
As I understand, finalizers in D are run because the GC needs more memory *now*. Deferring release of memory until the next collection would defeat the purpose. We would need to decouple collection cycles from allocation. Am I missing something?
Mar 18
next sibling parent Steven Schveighoffer <schveiguy gmail.com> writes:
On 3/18/21 8:55 AM, Mike Parker wrote:
 On Thursday, 18 March 2021 at 12:27:56 UTC, Petar Kirov [ZombineDev] wrote:
 
 Just implementation deficiency. I think it is fixable with some 
 refactoring of the GC pipeline. One approach would be, (similar to 
 other language implementations - see below), that GC-allocated objects 
 with destructors should be placed on a queue and their destructors be 
 called when the GC has finished the collection. Afterwards, the GC can 
 release their memory during the next collection.
As I understand, finalizers in D are run because the GC needs more memory *now*. Deferring release of memory until the next collection would defeat the purpose. We would need to decouple collection cycles from allocation. Am I missing something?
I think this is the proper way to look at it. We are running a collection cycle because more memory is needed. If you allocate inside the GC, likely you would trigger another GC. However, there are probably ways around this. For instance, you can allocate memory without triggering a GC, and we can probably try that instead. AIUI, the stop-the-world phase is only for scanning. Once scanning is done, there is nothing to say we can't change the pool data while looking for blocks to finalize. Most likely, some of them will free up blocks that then can be used by a finalizer allocation. Would be a good SAOC project. -Steve
Mar 18
prev sibling parent apz28 <home home.com> writes:
On Thursday, 18 March 2021 at 12:55:17 UTC, Mike Parker wrote:
 On Thursday, 18 March 2021 at 12:27:56 UTC, Petar Kirov 
 [ZombineDev] wrote:

 Just implementation deficiency. I think it is fixable with 
 some refactoring of the GC pipeline. One approach would be, 
 (similar to other language implementations - see below), that 
 GC-allocated objects with destructors should be placed on a 
 queue and their destructors be called when the GC has finished 
 the collection. Afterwards, the GC can release their memory 
 during the next collection.
This need to improve which allow to call free memory in destructor For realloc..., when size = 0, it needs to call free.... https://github.com/dlang/druntime/blob/3a32cc0305d4dd066f719d4c2df97337c86ea7ff/src/core/internal/gc/impl/conservative/gc.d#L444 vs https://github.com/dlang/druntime/blob/3a32cc0305d4dd066f719d4c2df97337c86ea7ff/src/core/internal/gc/impl/conservative/gc.d#L678
Mar 19
prev sibling parent Paulo Pinto <pjmlp progtools.org> writes:
On Thursday, 18 March 2021 at 12:27:56 UTC, Petar Kirov 
[ZombineDev] wrote:
 On Thursday, 18 March 2021 at 09:21:27 UTC, Per Nordlöw wrote:
 [...]
Just implementation deficiency. I think it is fixable with some refactoring of the GC pipeline. One approach would be, (similar to other language implementations - see below), that GC-allocated objects with destructors should be placed on a queue and their destructors be called when the GC has finished the collection. Afterwards, the GC can release their memory during the next collection. [...]
isn't required if the Dispose() method is available. This is done as performance improvement for using structs with determistic destruction and avoid implicit convertions to references when interfaces are used.
Mar 19
prev sibling parent James Blachly <james.blachly gmail.com> writes:
On 3/18/21 5:21 AM, Per Nordlöw 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-
nd-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 safe thanks to a more resilient GC?
It is a frustrating rough edge, esp for non-experts; I cut myself when I tried to use a custom logging function (which of course GC allocates) in class destructors.
Mar 22