digitalmars.D.announce - On the D Blog--Symphony of Destruction: Structs, Classes, and the GC
- Mike Parker (22/22) Mar 04 2021 This post is 3+ years overdue. I initially put it off for the
- Dukc (19/21) Mar 04 2021 "Some examples: attempting to index an associative array can
- H. S. Teoh (10/20) Mar 04 2021 This is precisely why Walter (and others) have said that assert failures
- Mike Parker (25/36) Mar 04 2021 Yes, this can be worked around. Passing -checkaction=C to the
- Steven Schveighoffer (6/28) Mar 05 2021 And technically, a range error does not allocate.
- Steven Schveighoffer (11/33) Mar 05 2021 One further note: you want to avoid InvalidMemoryOperationError AT ALL
- Max Samukha (5/7) Mar 05 2021 "The destructors of all stack-allocated structs in a given scope
- Per =?UTF-8?B?Tm9yZGzDtnc=?= (12/13) Mar 18 2021 Reminds me of longing for an (optional) compiler warning or, even
- Mike Parker (8/11) Mar 18 2021 I actually don't agree with that. I'll be discussion the solution
- Per =?UTF-8?B?Tm9yZGzDtnc=?= (8/15) Mar 19 2021 Could we at least add some guard in the GC that notifies the user
- Per =?UTF-8?B?Tm9yZGzDtnc=?= (8/10) Mar 18 2021 Btw, what is the motive behind D's GC not being able to correctly
- Petar Kirov [ZombineDev] (108/118) Mar 18 2021 Just implementation deficiency. I think it is fixable with some
- Mike Parker (6/13) Mar 18 2021 As I understand, finalizers in D are run because the GC needs
- Steven Schveighoffer (14/28) Mar 18 2021 I think this is the proper way to look at it. We are running a
- apz28 (6/16) Mar 19 2021 This need to improve which allow to call free memory in destructor
- Paulo Pinto (7/17) Mar 19 2021 Small correction, since .NET 5 / C# 9, implementing IDisposable
- James Blachly (4/17) Mar 22 2021 It is a frustrating rough edge, esp for non-experts; I cut myself when I...
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 2021
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 2021
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 2021
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 2021
On 3/4/21 6:42 PM, Dukc wrote:On Thursday, 4 March 2021 at 13:54:48 UTC, Mike Parker wrote: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. -SteveThe 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.
Mar 05 2021
On 3/4/21 6:42 PM, Dukc wrote:On Thursday, 4 March 2021 at 13:54:48 UTC, Mike Parker wrote: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. -SteveThe 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.
Mar 05 2021
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 2021
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 2021
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 2021
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 2021
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 2021
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: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.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 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 2021
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 2021
On 3/18/21 8:55 AM, Mike Parker wrote:On Thursday, 18 March 2021 at 12:27:56 UTC, Petar Kirov [ZombineDev] wrote: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. -SteveJust 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 2021
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: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#L678Just 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.
Mar 19 2021
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: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.[...]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. [...]
Mar 19 2021
On 3/18/21 5:21 AM, Per Nordlöw wrote:On Thursday, 4 March 2021 at 13:54:48 UTC, Mike Parker wrote: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.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?
Mar 22 2021