digitalmars.D - GC: memory collected but destructors not called
- Tomer Filiba (35/35) Nov 10 2014 The following code does not invoke S.~this. If I change `struct
- ketmar via Digitalmars-d (6/38) Nov 10 2014 it's a bug, it was recently filled and Walter (afair) made a PR with
- Tomer Filiba (10/52) Nov 10 2014 Thanks for the info, ketmar.
- Marco Leise (8/21) Nov 10 2014 That collides with the use of @nogc on S to mean that this
- Steven Schveighoffer (30/60) Nov 10 2014 Only classes call dtors from the GC. Structs do not. There are many
- Rainer Schuetze (8/15) Nov 11 2014 So, would you object to actually call the destructor for GC collected
- Steven Schveighoffer (28/44) Nov 11 2014 I think in general, it's a problem of the expectation of where structs
- Shachar Shemesh (14/24) Nov 11 2014 Isn't "structs meant only for stack variables" a semantic thing? The D
- Steven Schveighoffer (14/40) Nov 12 2014 The programmer being the user of the struct or the designer? It's
- Rainer Schuetze (5/9) Nov 14 2014 I think if someone uses "new" to allocate a struct on the GC heap, he
- Steven Schveighoffer (22/31) Nov 14 2014 The issue I have is, how do you enforce that as the author of the struct...
- Rainer Schuetze (19/52) Nov 14 2014 You can define "new" in the struct:
- Steven Schveighoffer (3/11) Nov 14 2014 This doesn't stop array allocation, or allocation as part of a class.
- Shachar Shemesh (4/9) Nov 11 2014 How is this any different? If one should not be allowed, how is the
- "Marc =?UTF-8?B?U2Now7x0eiI=?= <schuetzm gmx.net> (5/18) Nov 12 2014 Supposedly, a struct destructor will only access resources that
- Shachar Shemesh (8/11) Nov 12 2014 Either a struct's destructor can be run from the context of a GC, in
- "Marc =?UTF-8?B?U2Now7x0eiI=?= <schuetzm gmx.net> (11/27) Nov 12 2014 I think it's helpful to ask the question who's responsible for
- Steven Schveighoffer (5/15) Nov 12 2014 I'm not defending the status quo, I'm just saying what happens today.
- Uranuz (8/8) Nov 12 2014 If we will have something like *scoped destructor* (that will be
- "Marc =?UTF-8?B?U2Now7x0eiI=?= <schuetzm gmx.net> (9/18) Nov 12 2014 Don't know what you mean here. Isn't that just a normal
- Steven Schveighoffer (8/15) Nov 12 2014 GC I would use as a last resort.
- eles (36/37) Nov 12 2014 I might be wrong but my view is that in presence of a GC and
The following code does not invoke S.~this. If I change `struct S` to `class S` - it does. Memory consumption remains constant, meaning memory is collected, but destructors are not called. import std.stdio; import std.conv; struct S { string s; ~this() { writeln("~S"); } } void main() { auto i = 0; S[] arr; while (true) { arr ~= S("hello " ~ text(i++)); if (arr.length == 1_000_000) { writeln(&arr[8888], " = ", arr[8888].s); arr.length = 0; } } } Is it a bug? How can I effectively implement RAII with this behavior? The situation is, I allocate resources for my users and return them a handle. I can't control what my users do with this handle, they might as well append it to a dynamic array/insert to AA, and remove it eventually. Also, the handle might be shared between several logical components, so it's not that one of them can explicitly finalize it. Any workaround for this? Perhaps disallow certain types be allocated by the GC (I would actually want this feature very much)? -tomer
Nov 10 2014
On Mon, 10 Nov 2014 08:10:08 +0000 Tomer Filiba via Digitalmars-d <digitalmars-d puremagic.com> wrote:The following code does not invoke S.~this. If I change `struct=20 S` to `class S` - it does. Memory consumption remains constant,=20 meaning memory is collected, but destructors are not called. =20 import std.stdio; import std.conv; =20 struct S { string s; =20 ~this() { writeln("~S"); } } =20 void main() { auto i =3D 0; S[] arr; =20 while (true) { arr ~=3D S("hello " ~ text(i++)); if (arr.length =3D=3D 1_000_000) { writeln(&arr[8888], " =3D ", arr[8888].s); arr.length =3D 0; } } } =20 =20 Is it a bug? How can I effectively implement RAII with this=20 behavior?it's a bug, it was recently filled and Walter (afair) made a PR with fix, but it's not yet merged.Any workaround for this?made your own array implementation which manually calls dtors. or wait for next DMD release with this bug fixed. ;-)
Nov 10 2014
Thanks for the info, ketmar. By the way, what do you guys think of adding nogc to structs, in which case they cannot be instantiated by the GC? e.g., nogc struct S {...} auto s = new S(); // does not compile S s; // all is well -tomer On Monday, 10 November 2014 at 08:28:03 UTC, ketmar via Digitalmars-d wrote:On Mon, 10 Nov 2014 08:10:08 +0000 Tomer Filiba via Digitalmars-d <digitalmars-d puremagic.com> wrote:The following code does not invoke S.~this. If I change `struct S` to `class S` - it does. Memory consumption remains constant, meaning memory is collected, but destructors are not called. import std.stdio; import std.conv; struct S { string s; ~this() { writeln("~S"); } } void main() { auto i = 0; S[] arr; while (true) { arr ~= S("hello " ~ text(i++)); if (arr.length == 1_000_000) { writeln(&arr[8888], " = ", arr[8888].s); arr.length = 0; } } } Is it a bug? How can I effectively implement RAII with this behavior?it's a bug, it was recently filled and Walter (afair) made a PR with fix, but it's not yet merged.Any workaround for this?made your own array implementation which manually calls dtors. or wait for next DMD release with this bug fixed. ;-)
Nov 10 2014
Am Mon, 10 Nov 2014 09:13:09 +0000 schrieb "Tomer Filiba" <tomerfiliba gmail.com>:Thanks for the info, ketmar. By the way, what do you guys think of adding nogc to structs, in which case they cannot be instantiated by the GC? e.g., nogc struct S {...} auto s = new S(); // does not compile S s; // all is well -tomerThat collides with the use of nogc on S to mean that this struct itself wont use the GC, but may be used in a GC context, like as part of a class allocated on the GC heap. Well, in general it means "does not call GC functions". -- Marco
Nov 10 2014
On 11/10/14 3:10 AM, Tomer Filiba wrote:The following code does not invoke S.~this. If I change `struct S` to `class S` - it does. Memory consumption remains constant, meaning memory is collected, but destructors are not called. import std.stdio; import std.conv; struct S { string s; ~this() { writeln("~S"); } } void main() { auto i = 0; S[] arr; while (true) { arr ~= S("hello " ~ text(i++)); if (arr.length == 1_000_000) { writeln(&arr[8888], " = ", arr[8888].s); arr.length = 0; } } } Is it a bug? How can I effectively implement RAII with this behavior?Only classes call dtors from the GC. Structs do not. There are many hairy issues with structs calling dtors from GC. Most struct dtors expect to be called synchronously, and are not expecting to deal with multithreading issues. Note that structs inside classes WILL call dtors. For example, imagine you have a struct that increments/decrements a reference count: struct refCounter(T) { T t; // assume this to be a reference type this(T t) { t.inc();} ~this() {if(t !is null) t.dec();} } Of course, you would need more machinery to deal with copies, but let's just assume that is also implemented. You can see how this would be some kind of useful guard using RAII on the stack. Now, imagine you wanted to put this on the GC heap, and the GC would call struct dtors. And let's say the program is multi-threaded. First, the memory referred to by t isn't guaranteed to be alive, it could have already been finalized and freed. Second, the thread that calls the destructor may not be the thread that owns t. This means that two threads could potentially be calling t.inc() or t.dec() at the same time, leading to race conditions. Note, even D's std.stdio.File is not able to deal with this properly.The situation is, I allocate resources for my users and return them a handle. I can't control what my users do with this handle, they might as well append it to a dynamic array/insert to AA, and remove it eventually. Also, the handle might be shared between several logical components, so it's not that one of them can explicitly finalize it.Is the resource a GC resource? If so, don't worry about it. If not, use a class to wrap the resource. At this point, that is all that D supports.Any workaround for this? Perhaps disallow certain types be allocated by the GC (I would actually want this feature very much)?That would be a nice feature I think. -Steve
Nov 10 2014
On 10.11.2014 15:19, Steven Schveighoffer wrote:Now, imagine you wanted to put this on the GC heap, and the GC would call struct dtors. And let's say the program is multi-threaded. First, the memory referred to by t isn't guaranteed to be alive, it could have already been finalized and freed. Second, the thread that calls the destructor may not be the thread that owns t. This means that two threads could potentially be calling t.inc() or t.dec() at the same time, leading to race conditions.So, would you object to actually call the destructor for GC collected structs? I don't think that threading problems in the implmentation of the destructor should prohibit this. The reference count in your example also doesn't work for heap allocated structs that are never collected or for structs inside classes. The pull request is almost ready to be merged, please chime in: https://github.com/D-Programming-Language/druntime/pull/864
Nov 11 2014
On 11/11/14 2:46 PM, Rainer Schuetze wrote:On 10.11.2014 15:19, Steven Schveighoffer wrote:I think in general, it's a problem of the expectation of where structs will live. It's obvious we know classes will be on the heap, so we can write those dtors accordingly. Most struct dtors are written for when the struct is on the stack. The way around this is to have 2 functions for destruction -- as Tango does. One is called during synchronous destruction (i.e. when a struct goes out of scope, or when destroy is called), and the other is called during both synchronous and asynchronous destruction (when the GC is collecting). But even that solution does not allow the struct that I wrote to properly deal with the GC. If the struct has a reference to GC memory, it CANNOT access it during GC destruction to decrement the count, as the memory may be gone. It is why all the claims that we can accomplish what we want with reference counting backed by the GC all never seem to satisfy my doubts.Now, imagine you wanted to put this on the GC heap, and the GC would call struct dtors. And let's say the program is multi-threaded. First, the memory referred to by t isn't guaranteed to be alive, it could have already been finalized and freed. Second, the thread that calls the destructor may not be the thread that owns t. This means that two threads could potentially be calling t.inc() or t.dec() at the same time, leading to race conditions.So, would you object to actually call the destructor for GC collected structs? I don't think that threading problems in the implmentation of the destructor should prohibit this. The reference count in your example also doesn't work for heap allocated structs that are never collected or for structs inside classes.The pull request is almost ready to be merged, please chime in: https://github.com/D-Programming-Language/druntime/pull/864At this point, I am not super-concerned about this. I cannot think of any bullet-proof way to ensure that struct dtors for structs that were meant only for stack variables can be called correctly from the GC. This pull doesn't change that, and it does have some nice new features that we do need for other reasons. In other words, putting a struct in the GC heap that was written to be scope-destroyed is an error before and after this pull. Before the pull, the dtor doesn't run, which is wrong, and after the pull the dtor may cause race issues, which is wrong. So either way, it's wrong :) I also am strapped for free cycles to review such PRs. I trust you guys know what you are doing :) -Steve
Nov 11 2014
On 11/11/14 22:41, Steven Schveighoffer wrote:At this point, I am not super-concerned about this. I cannot think of any bullet-proof way to ensure that struct dtors for structs that were meant only for stack variables can be called correctly from the GC.Isn't "structs meant only for stack variables" a semantic thing? The D compiler cannot possibly know. Shouldn't that be the programmer's choice?This pull doesn't change that, and it does have some nice new features that we do need for other reasons. In other words, putting a struct in the GC heap that was written to be scope-destroyed is an error before and after this pull. Before the pull, the dtor doesn't run, which is wrong, and after the pull the dtor may cause race issues, which is wrong. So either way, it's wrong :)I disagree. The first is wrong. The second is a corner case the programmer needs to be aware of, and account for. The difference is that, in the first case, the programmer is left with no tools to fix the problem, while in the second case this is simply a bug in the program (which, like I said in another email, also happens with the current implementation when the struct is inside a class). In other words, the second case exposes a second (more direct and more likely to be handled) path to an already existing problem, while the first puts the programmer up against a new problem with no work around. Shachar
Nov 11 2014
On 11/12/14 12:05 AM, Shachar Shemesh wrote:On 11/11/14 22:41, Steven Schveighoffer wrote:The programmer being the user of the struct or the designer? It's impossible to force the user to avoid using a struct on the GC, it would be enforcement by comment. But even then, in your dtor, there are issues with accessing what a dtor would normally access. Tell me how you implement reference counting smart pointer when you can't access the reference count in the dtor...At this point, I am not super-concerned about this. I cannot think of any bullet-proof way to ensure that struct dtors for structs that were meant only for stack variables can be called correctly from the GC.Isn't "structs meant only for stack variables" a semantic thing? The D compiler cannot possibly know. Shouldn't that be the programmer's choice?This pull doesn't change that, and it does have some nice new features that we do need for other reasons. In other words, putting a struct in the GC heap that was written to be scope-destroyed is an error before and after this pull. Before the pull, the dtor doesn't run, which is wrong, and after the pull the dtor may cause race issues, which is wrong. So either way, it's wrong :)I disagree. The first is wrong. The second is a corner case the programmer needs to be aware of, and account for.The difference is that, in the first case, the programmer is left with no tools to fix the problem, while in the second case this is simply a bug in the program (which, like I said in another email, also happens with the current implementation when the struct is inside a class).Sure there are tools, you can wrap the struct in a class if you like pain and suffering. Having the struct dtor called without the wrapper is the same issue.In other words, the second case exposes a second (more direct and more likely to be handled) path to an already existing problem, while the first puts the programmer up against a new problem with no work around.This means all struct dtors need to be thread aware, and this is not what you want in a language where the type information dictates whether it can be shared or not. -Steve
Nov 12 2014
On 11.11.2014 21:41, Steven Schveighoffer wrote:In other words, putting a struct in the GC heap that was written to be scope-destroyed is an error before and after this pull. Before the pull, the dtor doesn't run, which is wrong, and after the pull the dtor may cause race issues, which is wrong. So either way, it's wrong :)I think if someone uses "new" to allocate a struct on the GC heap, he must be aware of the consequences of using the GC. What happens within destruction/finalization is just the same as if it had been wrapped in a class, and that's what everyone(?) expects.
Nov 14 2014
On 11/14/14 6:25 AM, Rainer Schuetze wrote:On 11.11.2014 21:41, Steven Schveighoffer wrote:The issue I have is, how do you enforce that as the author of the struct? Basically, I think you have 2 situations with struct dtors: 1. the struct author intends a struct to strictly be used on the stack (or on some other heap, such as C heap), and never expects to be GC destructed. 2. the struct author allows it to be GC stored, and faces the consequences of that. Even with these 2 options, allowing GC storage precludes certain things. For example, the RefCounted struct goes through great pains to ensure its payload is NOT on the GC heap, because the dtor will not work if the payload is GC based (it may be already deallocated). The issues are so numerous and hairy, that option 2 should really only be left to "experts." I think we need a better story for GC-stored structs than just letting people do it. All this being said, having the GC perform what it is supposed to is also good. Bottom line -- I think the pull is the right thing to do (clearly, not calling dtors in some cases is not correct), but the problems are not all solved. We need to continue working on this until it's quite simple to make a struct that behaves correctly no matter where you put it. -SteveIn other words, putting a struct in the GC heap that was written to be scope-destroyed is an error before and after this pull. Before the pull, the dtor doesn't run, which is wrong, and after the pull the dtor may cause race issues, which is wrong. So either way, it's wrong :)I think if someone uses "new" to allocate a struct on the GC heap, he must be aware of the consequences of using the GC. What happens within destruction/finalization is just the same as if it had been wrapped in a class, and that's what everyone(?) expects.
Nov 14 2014
On 14.11.2014 16:44, Steven Schveighoffer wrote:On 11/14/14 6:25 AM, Rainer Schuetze wrote:You can define "new" in the struct: struct S { new(size_t sz); int x; } This causes a link error with "new S;" Unfortunately disable has no effect (bug?). You can also generate a runtime error within "new".On 11.11.2014 21:41, Steven Schveighoffer wrote:The issue I have is, how do you enforce that as the author of the struct? Basically, I think you have 2 situations with struct dtors: 1. the struct author intends a struct to strictly be used on the stack (or on some other heap, such as C heap), and never expects to be GC destructed.In other words, putting a struct in the GC heap that was written to be scope-destroyed is an error before and after this pull. Before the pull, the dtor doesn't run, which is wrong, and after the pull the dtor may cause race issues, which is wrong. So either way, it's wrong :)I think if someone uses "new" to allocate a struct on the GC heap, he must be aware of the consequences of using the GC. What happens within destruction/finalization is just the same as if it had been wrapped in a class, and that's what everyone(?) expects.2. the struct author allows it to be GC stored, and faces the consequences of that.Even with these 2 options, allowing GC storage precludes certain things. For example, the RefCounted struct goes through great pains to ensure its payload is NOT on the GC heap, because the dtor will not work if the payload is GC based (it may be already deallocated). The issues are so numerous and hairy, that option 2 should really only be left to "experts." I think we need a better story for GC-stored structs than just letting people do it.I understand there are some complications involved, especially with stuff like RefCounted. With precise scanning, we might be able to sort finalization according to (likely) dependencies, but this will not be 100% safe and slow down collection even more. There are so many ways people can do something silly/wrong, I don't think we can foresee everything. In addition to allocating it on the heap, the struct could be copied to some global, be kept forever in an associative array, be part of a class or just memset with zeros.All this being said, having the GC perform what it is supposed to is also good. Bottom line -- I think the pull is the right thing to do (clearly, not calling dtors in some cases is not correct), but the problems are not all solved. We need to continue working on this until it's quite simple to make a struct that behaves correctly no matter where you put it.Yeah, even if not fulfilling all desires it is progress in the right direction ;-)
Nov 14 2014
On 11/14/14 11:55 AM, Rainer Schuetze wrote:On 14.11.2014 16:44, Steven Schveighoffer wrote:This doesn't stop array allocation, or allocation as part of a class. -SteveThe issue I have is, how do you enforce that as the author of the struct?You can define "new" in the struct: struct S { new(size_t sz); int x; }
Nov 14 2014
On 10/11/14 16:19, Steven Schveighoffer wrote:Only classes call dtors from the GC. Structs do not. There are many hairy issues with structs calling dtors from GC. Most struct dtors expect to be called synchronously, and are not expecting to deal with multithreading issues. Note that structs inside classes WILL call dtors.How is this any different? If one should not be allowed, how is the other okay? Shachar
Nov 11 2014
On Wednesday, 12 November 2014 at 04:59:33 UTC, Shachar Shemesh wrote:On 10/11/14 16:19, Steven Schveighoffer wrote:Supposedly, a struct destructor will only access resources that the struct itself manages. As long as that's the case, it will be safe. In practice, there's still a lot that can go wrong.Only classes call dtors from the GC. Structs do not. There are many hairy issues with structs calling dtors from GC. Most struct dtors expect to be called synchronously, and are not expecting to deal with multithreading issues. Note that structs inside classes WILL call dtors.How is this any different? If one should not be allowed, how is the other okay?
Nov 12 2014
On 12/11/14 11:29, "Marc =?UTF-8?B?U2Now7x0eiI=?= <schuetzm gmx.net>" wrote:Supposedly, a struct destructor will only access resources that the struct itself manages. As long as that's the case, it will be safe. In practice, there's still a lot that can go wrong.Either a struct's destructor can be run from the context of a GC, in which case it should run when the struct is directly allocated on the heap, or it is not, in which case the fact it is run when the struct is inside a class should be considered a bug. Today it happens for structs nested in classes, but not allocated directly. I don't see any situation in which this is not a bug. Shachar
Nov 12 2014
On Wednesday, 12 November 2014 at 13:56:08 UTC, Shachar Shemesh wrote:On 12/11/14 11:29, "Marc =?UTF-8?B?U2Now7x0eiI=?= <schuetzm gmx.net>" wrote:I think it's helpful to ask the question who's responsible for destroying an object. If it's the GC, then it's finalization, if it's no the GC, it's destruction. Both a destructor and a finalizer need to clean up themselves, and any other object they own. This includes embedded structs, but not GC managed objects created by the constructor. This applies to both structs and classes as the owning objects, and manual/automatic management as well as GC. But indeed, what's implemented today is inconsistent.Supposedly, a struct destructor will only access resources that the struct itself manages. As long as that's the case, it will be safe. In practice, there's still a lot that can go wrong.Either a struct's destructor can be run from the context of a GC, in which case it should run when the struct is directly allocated on the heap, or it is not, in which case the fact it is run when the struct is inside a class should be considered a bug. Today it happens for structs nested in classes, but not allocated directly. I don't see any situation in which this is not a bug. Shachar
Nov 12 2014
On 11/11/14 11:59 PM, Shachar Shemesh wrote:On 10/11/14 16:19, Steven Schveighoffer wrote:I'm not defending the status quo, I'm just saying what happens today. But adding struct dtor calls to the GC will not solve the problems identified here. -SteveOnly classes call dtors from the GC. Structs do not. There are many hairy issues with structs calling dtors from GC. Most struct dtors expect to be called synchronously, and are not expecting to deal with multithreading issues. Note that structs inside classes WILL call dtors.How is this any different? If one should not be allowed, how is the other okay?
Nov 12 2014
If we will have something like *scoped destructor* (that will be executed at scope exit) could it help to release some resources? I worry about this problem too because even using class to hold resource I expirience some *delays* in relesing them. For example I have database connection opened. And I want to close it when I finished my job. Relying on GC I sometimes experiece problems like *too many DB connections*, because GC frees it not enough quickly.
Nov 12 2014
On Wednesday, 12 November 2014 at 20:00:31 UTC, Uranuz wrote:If we will have something like *scoped destructor* (that will be executed at scope exit) could it help to release some resources?Don't know what you mean here. Isn't that just a normal destructor?I worry about this problem too because even using class to hold resource I expirience some *delays* in relesing them. For example I have database connection opened. And I want to close it when I finished my job. Relying on GC I sometimes experiece problems like *too many DB connections*, because GC frees it not enough quickly.I'd say database connections and file descriptors simply shouldn't be managed by the GC. It is good at managing memory, but not for other things. It's better to have a connection pool, and from that pool take a reference to the DB connection, use it as long as you need it, and then give it back. This can be implemented nicely with scope(exit).
Nov 12 2014
On 11/12/14 3:00 PM, Uranuz wrote:If we will have something like *scoped destructor* (that will be executed at scope exit) could it help to release some resources? I worry about this problem too because even using class to hold resource I expirience some *delays* in relesing them. For example I have database connection opened. And I want to close it when I finished my job. Relying on GC I sometimes experiece problems like *too many DB connections*, because GC frees it not enough quickly.GC I would use as a last resort. Let's say you leak the object, forget to dispose it. Do you want it to leak the DB resource too? Basically, you want a close() or dispose() method on your object, then you can let the GC clean the memory, and synchronously close the DB connection. -Steve
Nov 12 2014
On Monday, 10 November 2014 at 14:19:26 UTC, Steven Schveighoffer wrote:Is the resource a GC resource? If so, don't worry about it.I might be wrong but my view is that in presence of a GC and undre the abstraction of R Chen, it is wrong to think about memory as being a resource anymore. If the abstraction is that of a machine with infinite memory, then the very mechanism of freeing memory shall be abstracted for. This asks for decoupling the memory management of the management of the other resources and, in particular, RAII-like management. The management of memory and any other resources is pretty much identical under C++ since they share the same paradigm. In GC languages, this is no longer the case. So, the real question (that will also cover destructors throwing exceptions and so on) is not what is allowed inside a destructor (aka finalizer) but *what is allowed in a constructor* of a GC-managed object. Since for symmetry purposes the destructor should undo what the constructor did, this cuts the problem as follows: 1) in GC-entities, the destructor should not deal with releasing memory. Not only this job is no longer his, but *there is no need for this kind of job*. Memory is infinite. 2) from symmetry, the constructor should not allocate memory as it would care about its lifetime. 3) the question in that case is how the other resources are managed? 3a) If they are to be released in the destructor, then the destructor's job should be only this (no memory dealloc). In this case, the resources should be taken in the constructor. 3b) if the destructor disappears completely, then the place to acquire resources is no longer in the constructor anymore, since those will never be released. Ideally, a transparent mechanism to allocate memory would be needed, without explicit allocation. In this case, the symmetry would be conserved: the constructor will only acqire resources (but not memory!) and those resources are released, symmetrically, in the destructor, at the end of the lifetime.
Nov 12 2014