digitalmars.D - nogc and exceptions
- Jakob Ovrum (92/92) Sep 11 2014 There is one massive blocker for `@nogc` adoption in D library
- monarch_dodra (25/52) Sep 12 2014 I think option "b)" is the right direction. However, I don't
- Dmitry Olshansky (8/26) Sep 12 2014 Agreed.
- Jakob Ovrum (5/10) Sep 12 2014 We can't use the GC as the whole point is to mark library code
- Dmitry Olshansky (5/15) Sep 13 2014 Then call it "exception heap" or some such, and make sure it's somehow
- Jakob Ovrum (13/33) Sep 12 2014 We can't change existing instances of `throw` to use such a
- Andrej Mitrovic via Digitalmars-d (19/22) Sep 12 2014 Can we amend the spec to say self-referencing is ok? Then we could
- Jakob Ovrum (8/35) Sep 12 2014 The exception chain would be unable to chain further exceptions.
- Vladimir Panteleev (18/22) Sep 12 2014 Another problem with this is that you'll need to change every
- Johannes Pfau (12/40) Sep 12 2014 I think if we could avoid dynamic allocations for most exceptions
- Vladimir Panteleev (2/7) Sep 12 2014 But then we can't have exception stack traces.
- Jakob Ovrum (22/39) Sep 12 2014 While we should ideally make the exception mechanism as fast as
- Jakob Ovrum (9/16) Sep 12 2014 I don't think this is a big problem, `@nogc` will tell you where
- "Marc =?UTF-8?B?U2Now7x0eiI=?= <schuetzm gmx.net> (9/22) Sep 12 2014 Related: Last time I checked the runtime caches unwinding or
- Johannes Pfau (5/17) Sep 12 2014 Yes, in order to avoid allocating a stack trace helper you need to cast
- "Marc =?UTF-8?B?U2Now7x0eiI=?= <schuetzm gmx.net> (3/24) Sep 13 2014 That's not what I mean, please see here:
- Jakob Ovrum (7/23) Sep 13 2014 Indeed the entire exception mechanism does not account for
- Dicebot (9/9) Sep 19 2014 I am obviously in favor of simply calling recurrent mutable
- Jakob Ovrum (13/22) Sep 19 2014 That might be sufficient for a particular application like
- Dicebot (8/20) Sep 19 2014 This is pretty much saying that anything should be able to throw
There is one massive blocker for ` nogc` adoption in D library code: allocation of exception objects. The GC heap is an ideal location for exception objects, but ` nogc` has to stick to its promise, so an alternative method of memory management is desirable if we want the standard library to be widely usable in ` nogc` user code, as well as enabling third-party libraries to apply ` nogc`. If we don't solve this, we'll stratify D code into two separate camps, the GC-using camp and the ` nogc`-using camp, each with their own set of library code. I can think of a couple of ways to go: 1) The most widely discussed path is to allocate exception instances statically, either in global memory or TLS. Currently, this has a few serious problems: 1a) If the exception is chained, that is, if the same exception appears twice in the same exception chain - which can easily happen when an exception is thrown from a `scope(exit|failure)` statement or from a destructor - the chaining mechanism will construct a self-referencing list that results in an infinite loop when the chain is walked, such as by the global exception handler that prints the chain to stderr. This can easily be demonstrated with the below snippet: --- void main() { static immutable ex = new Exception(""); scope(exit) throw ex; throw ex; } --- Amending the chaining mechanism to simply *disallow* these chains would neuter exception chaining severely, in fact making it more or less useless: it's not realistically possible to predict which exceptions will appear twice when calling code from multiple libraries. 1b) Exceptions constructed at compile-time which are then later referenced at runtime (as in the above snippet) must be immutable (the compiler enforces this), as this feature only supports allocation in global memory, not in TLS. This brings us to an unsolved bug in the exception mechanism - the ability to get a mutable reference to an immutable exception without using a cast: --- void main() { static immutable ex = new Exception(""); try throw ex; catch(Exception e) // `e` is a mutable reference { // The exception is caught and `e` aliases `ex` } } --- Fixing this would likely involve requiring `catch(const(Exception) e)` at the catch-site, which would require users to update all their exception-handling code, and if they don't, the program will happily compile but the catch-site no longer matches. This is especially egregious as error-handling code is often the least tested part of the program. Essentially D's entire exception mechanism is not const-correct. 1c) Enhancing the compiler to allow statically constructing in TLS, or allocating space in TLS first then constructing the exception lazily at runtime, would allow us to keep throwing mutable exceptions, but would seriously bloat the TLS section. We can of course allocate shared instances in global memory and throw those, but this requires thread-safe code at the catch-site which has similar problems to catching by const. 2) The above really shows how beneficial dynamic memory allocation is for exceptions. A possibility would be to allocate exceptions on a non-GC heap, like the C heap (malloc) or a thread-local heap. Of course, without further amendments the onus is then on the catch-site to explicitly manage memory, which would silently break virtually all exception-handling code really badly. However, if we assume that most catch-sites *don't* escape references to exceptions from the caught chain, we could gracefully work around this with minimal and benevolent breakage: amend the compiler to implicitly insert a cleanup call at the end of each catch-block. The cleanup function would destroy and free the whole chain, but only if a flag indicates that the exception was allocated with this standard heap mechanism. Chains of exceptions with mixed allocation origin would have to be dealt with in some manner. If inside the catch-block, the chain is rethrown or sent in flight by a further exception, the cleanup call would simply not be reached and deferred to the next catch-site, and so on. Escaping references to caught exceptions would be undefined behaviour. To statically enforce this doesn't happen, exception references declared in catch-blocks could be made implicitly `scope`. This depends on `scope` actually working reasonably well. This would be the only breaking change for user code, and the fix is simply making a copy of the escaped exception. Anyway, I'm wondering what thoughts you guys have on this nascent but vitally important issue. What do we do about this?
Sep 11 2014
On Friday, 12 September 2014 at 03:37:10 UTC, Jakob Ovrum wrote:2) The above really shows how beneficial dynamic memory allocation is for exceptions. A possibility would be to allocate exceptions on a non-GC heap, like the C heap (malloc) or a thread-local heap. Of course, without further amendments the onus is then on the catch-site to explicitly manage memory, which would silently break virtually all exception-handling code really badly. However, if we assume that most catch-sites *don't* escape references to exceptions from the caught chain, we could gracefully work around this with minimal and benevolent breakage: amend the compiler to implicitly insert a cleanup call at the end of each catch-block. The cleanup function would destroy and free the whole chain, but only if a flag indicates that the exception was allocated with this standard heap mechanism. Chains of exceptions with mixed allocation origin would have to be dealt with in some manner. If inside the catch-block, the chain is rethrown or sent in flight by a further exception, the cleanup call would simply not be reached and deferred to the next catch-site, and so on. Escaping references to caught exceptions would be undefined behaviour. To statically enforce this doesn't happen, exception references declared in catch-blocks could be made implicitly `scope`. This depends on `scope` actually working reasonably well. This would be the only breaking change for user code, and the fix is simply making a copy of the escaped exception. Anyway, I'm wondering what thoughts you guys have on this nascent but vitally important issue. What do we do about this?I think option "b)" is the right direction. However, I don't think it is reasonable to have the "catch" code be responsible for the cleanup proper, as that would lead to a closed design (limited allocation possibilities). I like the option of having "exception allocators" that can later be explicitly called in a "release all exceptions" style, or plugged into the GC, to be cleaned up automatically like any other GC allocated exception. This would make the exceptions themselves still nogc, but the GC would have a hook to (potentially) collect them. For those that don't want that, then they can make calls to the cleanup at deterministic times. This, combined with the fact that we used an (unshared) allocator means the cleanup itself would be 0(1). Finally, if somebody *does* want to keep exceptions around, he would still be free to do so *provided* he re-allocates the exceptions himself using a memory scheme he chooses to use (a simple GC new, for example). ... well, either that, or have each exception carry a callback to its allocator, so that catch can do the cleanup, regardless of who did the allocation, and how. GC exceptions would have no callback, meaning a "catch" would still be nogc. An existing code that escapes exceptions would not immediately break. Either way, some sort of custom (no-gc) allocator seems in order here.
Sep 12 2014
12-Sep-2014 15:03, monarch_dodra пишет:I like the option of having "exception allocators" that can later be explicitly called in a "release all exceptions" style, or plugged into the GC, to be cleaned up automatically like any other GC allocated exception. This would make the exceptions themselves still nogc, but the GC would have a hook to (potentially) collect them. For those that don't want that, then they can make calls to the cleanup at deterministic times. This, combined with the fact that we used an (unshared) allocator means the cleanup itself would be 0(1). Finally, if somebody *does* want to keep exceptions around, he would still be free to do so *provided* he re-allocates the exceptions himself using a memory scheme he chooses to use (a simple GC new, for example). ... well, either that, or have each exception carry a callback to its allocator, so that catch can do the cleanup, regardless of who did the allocation, and how. GC exceptions would have no callback, meaning a "catch" would still be nogc. An existing code that escapes exceptions would not immediately break. Either way, some sort of custom (no-gc) allocator seems in order here.Agreed. I think that the total amount of live (not garbage) exceptions on heap is small for any typical application. Thus just special casing the hell out of exception allocation in the GC (and compiler) is IMO perfectly satisfactory hack. -- Dmitry Olshansky
Sep 12 2014
On Friday, 12 September 2014 at 16:33:50 UTC, Dmitry Olshansky wrote:Agreed. I think that the total amount of live (not garbage) exceptions on heap is small for any typical application. Thus just special casing the hell out of exception allocation in the GC (and compiler) is IMO perfectly satisfactory hack.We can't use the GC as the whole point is to mark library code ` nogc`. It should work even if the user has ripped the GC out of druntime with a custom build.
Sep 12 2014
13-Sep-2014 06:01, Jakob Ovrum пишет:On Friday, 12 September 2014 at 16:33:50 UTC, Dmitry Olshansky wrote:Then call it "exception heap" or some such, and make sure it's somehow separate. It still means GC got to scan it. -- Dmitry OlshanskyAgreed. I think that the total amount of live (not garbage) exceptions on heap is small for any typical application. Thus just special casing the hell out of exception allocation in the GC (and compiler) is IMO perfectly satisfactory hack.We can't use the GC as the whole point is to mark library code ` nogc`. It should work even if the user has ripped the GC out of druntime with a custom build.
Sep 13 2014
On Friday, 12 September 2014 at 11:03:09 UTC, monarch_dodra wrote:I think option "b)" is the right direction. However, I don't think it is reasonable to have the "catch" code be responsible for the cleanup proper, as that would lead to a closed design (limited allocation possibilities).Exceptions using other alocators simply don't set the flag.I like the option of having "exception allocators" that can later be explicitly called in a "release all exceptions" style, or plugged into the GC, to be cleaned up automatically like any other GC allocated exception. This would make the exceptions themselves still nogc, but the GC would have a hook to (potentially) collect them. For those that don't want that, then they can make calls to the cleanup at deterministic times.We can't change existing instances of `throw` to use such a manually managed heap without silently causing user code to leak.Finally, if somebody *does* want to keep exceptions around, he would still be free to do so *provided* he re-allocates the exceptions himself using a memory scheme he chooses to use (a simple GC new, for example).Yes, but we can't let existing code that escapes exceptions run into memory corruption because we changed the allocator. We need `scope`.... well, either that, or have each exception carry a callback to its allocator, so that catch can do the cleanup, regardless of who did the allocation, and how. GC exceptions would have no callback, meaning a "catch" would still be nogc. An existing code that escapes exceptions would not immediately break.I think this would depend on having multiple proposed exception allocation strategies in the first place. We know when the cleanup happens and we roughly know the allocation pattern (exceptional paths are rarely hit etc.), so I think we should focus on finding/creating an allocator ideal for this pattern, then apply it to Phobos.
Sep 12 2014
On 9/12/14, Jakob Ovrum via Digitalmars-d <digitalmars-d puremagic.com> wrote:the chaining mechanism will construct a self-referencing list that results in an infinite loop when the chain is walkedCan we amend the spec to say self-referencing is ok? Then we could make the default exception handler *stop* if it finds a self-referencing exception (e.g. for stack traces), and for custom user code which walks through exceptions it would either have to be fixed. We could also provide a helper function for walking through exceptions: try { ... } catch (Exception ex) { // some UFCS or object.d built-in method which // stops walking when ".next is this" foreach (caught; ex.walk) { } } Or does the problem have a bigger scope than just walking?
Sep 12 2014
On Friday, 12 September 2014 at 11:38:18 UTC, Andrej Mitrovic via Digitalmars-d wrote:On 9/12/14, Jakob Ovrum via Digitalmars-d <digitalmars-d puremagic.com> wrote:The exception chain would be unable to chain further exceptions. Each exception in the chain needs its own unique `next` pointer. Also, the two links to the same exception could appear anywhere in the list. I think some sort of caching of exceptions already encountered would be required, which is a lot slower and more complex than the existing mechanism.the chaining mechanism will construct a self-referencing list that results in an infinite loop when the chain is walkedCan we amend the spec to say self-referencing is ok? Then we could make the default exception handler *stop* if it finds a self-referencing exception (e.g. for stack traces), and for custom user code which walks through exceptions it would either have to be fixed. We could also provide a helper function for walking through exceptions: try { ... } catch (Exception ex) { // some UFCS or object.d built-in method which // stops walking when ".next is this" foreach (caught; ex.walk) { } } Or does the problem have a bigger scope than just walking?
Sep 12 2014
On Friday, 12 September 2014 at 03:37:10 UTC, Jakob Ovrum wrote:I can think of a couple of ways to go:1) The most widely discussed path is to allocate exception instances statically, either in global memory or TLS. Currently, this has a few serious problems:Another problem with this is that you'll need to change every instance of "new FooException" to something else. Here's a crazy idea that will never fly: 1. Opt-in reference counting for classes. This needs language/compiler support because currently we can't have both reference counting and inheritance. For example, you could annotate Throwable as refcounted, and all descendants get it automatically. The ref-counting overhead of exceptions should be acceptable (even with locks), since exceptions should be exceptional. 2. Bring back the currently-deprecated new/delete operator overloading. If we could have reference-counted classes that are allocated on the C heap, and keep the "new FooException" syntax, the problem could be solved globally and transparently. Reference counting implies that copies done using memcpy/unions/etc. will not be tracked, but nobody does that with exception objects, right?
Sep 12 2014
Am Fri, 12 Sep 2014 12:47:44 +0000 schrieb "Vladimir Panteleev" <vladimir thecybershadow.net>:On Friday, 12 September 2014 at 03:37:10 UTC, Jakob Ovrum wrote:I think if we could avoid dynamic allocations for most exceptions completely that'd be better. IIRC some people said that exceptions are mainly slow because of memory allocation. So if we could avoid that, there are more benefits. I suggest looking at the C++ implementation. There's the throw-by-value catch-by-reference idiom. C++ must store/copy this exception somewhere, maybe they have a clever solution. (We basically need some fixed-size per thread memory where we can store the exception and stack trace info. But we need a fallback because of exception chaining or big exceptions.)I can think of a couple of ways to go:1) The most widely discussed path is to allocate exception instances statically, either in global memory or TLS. Currently, this has a few serious problems:Another problem with this is that you'll need to change every instance of "new FooException" to something else. Here's a crazy idea that will never fly: 1. Opt-in reference counting for classes. This needs language/compiler support because currently we can't have both reference counting and inheritance. For example, you could annotate Throwable as refcounted, and all descendants get it automatically. The ref-counting overhead of exceptions should be acceptable (even with locks), since exceptions should be exceptional. 2. Bring back the currently-deprecated new/delete operator overloading. If we could have reference-counted classes that are allocated on the C heap, and keep the "new FooException" syntax, the problem could be solved globally and transparently. Reference counting implies that copies done using memcpy/unions/etc. will not be tracked, but nobody does that with exception objects, right?
Sep 12 2014
On Friday, 12 September 2014 at 21:36:31 UTC, Johannes Pfau wrote:I suggest looking at the C++ implementation. There's the throw-by-value catch-by-reference idiom. C++ must store/copy this exception somewhere, maybe they have a clever solution.But then we can't have exception stack traces.
Sep 12 2014
On Friday, 12 September 2014 at 21:36:31 UTC, Johannes Pfau wrote:I think if we could avoid dynamic allocations for most exceptions completely that'd be better. IIRC some people said that exceptions are mainly slow because of memory allocation. So if we could avoid that, there are more benefits.While we should ideally make the exception mechanism as fast as possible, it mustn't be a priority, lest we compromise more important parts of the design for the red herring that is performance. If a program is slow because of exception handling it's not using exceptions correctly. It should replace that part of the code with a solution that uses error codes. This is the mantra that goes for all languages with exceptions that I know, except Python and maybe Java. Also, we know the approximate size of exceptions and their allocation pattern. An efficient heap allocator could be designed to take advantage of this. That's not to say I'm against any non-heap solution if we can think of something really good, but we should keep our priorities straight.I suggest looking at the C++ implementation. There's the throw-by-value catch-by-reference idiom. C++ must store/copy this exception somewhere, maybe they have a clever solution. (We basically need some fixed-size per thread memory where we can store the exception and stack trace info. But we need a fallback because of exception chaining or big exceptions.)I think this is essentially global storage allocation. C++ does not do exception chaining so this is much more viable there. Also, when we decide to use TLS instead of the mess that would be shared exceptions, we introduce a massive chunk of required data for each new thread in every application that depends on Phobos and/or other libraries using the new exception allocator.
Sep 12 2014
On Friday, 12 September 2014 at 12:47:46 UTC, Vladimir Panteleev wrote:On Friday, 12 September 2014 at 03:37:10 UTC, Jakob Ovrum wrote:I don't think this is a big problem, ` nogc` will tell you where the allocations are, and it's fairly easy to just grep and replace. It's also worth noting that code using enforce et al. could be updated automatically. Further, you only need to do this if you want your library code to be ` nogc`. Existing code using the GC-heap will keep working (except for the `scope` thing I proposed...).I can think of a couple of ways to go:1) The most widely discussed path is to allocate exception instances statically, either in global memory or TLS. Currently, this has a few serious problems:Another problem with this is that you'll need to change every instance of "new FooException" to something else.
Sep 12 2014
On Friday, 12 September 2014 at 03:37:10 UTC, Jakob Ovrum wrote:1b) Exceptions constructed at compile-time which are then later referenced at runtime (as in the above snippet) must be immutable (the compiler enforces this), as this feature only supports allocation in global memory, not in TLS. This brings us to an unsolved bug in the exception mechanism - the ability to get a mutable reference to an immutable exception without using a cast:Related: Last time I checked the runtime caches unwinding or stack trace information in the exception. It does this even for immutable exceptions...Escaping references to caught exceptions would be undefined behaviour. To statically enforce this doesn't happen, exception references declared in catch-blocks could be made implicitly `scope`. This depends on `scope` actually working reasonably well. This would be the only breaking change for user code, and the fix is simply making a copy of the escaped exception.Care must also be taken when the exception is forwarded to another thread, like `receive()` does. `scope` forcing to copy the exception would solve a part of that, but to be completely correct, the exception would either have to shared, or a deep copy would be necessary.
Sep 12 2014
Am Fri, 12 Sep 2014 12:59:22 +0000 schrieb "Marc Sch=C3=BCtz" <schuetzm gmx.net>:On Friday, 12 September 2014 at 03:37:10 UTC, Jakob Ovrum wrote:Yes, in order to avoid allocating a stack trace helper you need to cast the exception from its .init property, IIRC. There's some code in druntime which does that (the out-of-memory error handling code).1b) Exceptions constructed at compile-time which are then=20 later referenced at runtime (as in the above snippet) must be=20 immutable (the compiler enforces this), as this feature only=20 supports allocation in global memory, not in TLS. This brings=20 us to an unsolved bug in the exception mechanism - the ability=20 to get a mutable reference to an immutable exception without=20 using a cast:=20 Related: Last time I checked the runtime caches unwinding or=20 stack trace information in the exception. It does this even for=20 immutable exceptions...
Sep 12 2014
On Friday, 12 September 2014 at 21:31:45 UTC, Johannes Pfau wrote:Am Fri, 12 Sep 2014 12:59:22 +0000 schrieb "Marc Schütz" <schuetzm gmx.net>:That's not what I mean, please see here: http://forum.dlang.org/thread/ftakrucgtfcicfbkzwbs forum.dlang.org#post-xmvzmufjywcsxviooivl:40forum.dlang.orgOn Friday, 12 September 2014 at 03:37:10 UTC, Jakob Ovrum wrote:Yes, in order to avoid allocating a stack trace helper you need to cast the exception from its .init property, IIRC. There's some code in druntime which does that (the out-of-memory error handling code).1b) Exceptions constructed at compile-time which are then later referenced at runtime (as in the above snippet) must be immutable (the compiler enforces this), as this feature only supports allocation in global memory, not in TLS. This brings us to an unsolved bug in the exception mechanism - the ability to get a mutable reference to an immutable exception without using a cast:Related: Last time I checked the runtime caches unwinding or stack trace information in the exception. It does this even for immutable exceptions...
Sep 13 2014
On Saturday, 13 September 2014 at 08:19:10 UTC, Marc Schütz wrote:On Friday, 12 September 2014 at 21:31:45 UTC, Johannes Pfau wrote:Indeed the entire exception mechanism does not account for immutability at all which is not surprising considering it was implemented in the days of D1 and there's a lot of type erasure going on when druntime functions are called, but it does mean any sighting of a non-mutable exception anywhere is a big red flag until we overhaul exceptions to account for immutability.Am Fri, 12 Sep 2014 12:59:22 +0000 schrieb "Marc Schütz" <schuetzm gmx.net>:That's not what I mean, please see here: http://forum.dlang.org/thread/ftakrucgtfcicfbkzwbs forum.dlang.org#post-xmvzmufjywcsxviooivl:40forum.dlang.orgRelated: Last time I checked the runtime caches unwinding or stack trace information in the exception. It does this even for immutable exceptions...Yes, in order to avoid allocating a stack trace helper you need to cast the exception from its .init property, IIRC. There's some code in druntime which does that (the out-of-memory error handling code).
Sep 13 2014
I am obviously in favor of simply calling recurrent mutable exception chains illegal (probably even immutable ones until we fix const correctness of druntime exception handling). Reason is simple : it doesn't require any changes in existing code and is exactly the way we already do it in Sociomantic :) To make it reliable Exception chain may need to be updated to doubly-linked list to be able to efficiently verify that new exception is not already present in the chain. I don't see any costly overhead implications from that though.
Sep 19 2014
On Friday, 19 September 2014 at 07:57:24 UTC, Dicebot wrote:I am obviously in favor of simply calling recurrent mutable exception chains illegal (probably even immutable ones until we fix const correctness of druntime exception handling). Reason is simple : it doesn't require any changes in existing code and is exactly the way we already do it in Sociomantic :) To make it reliable Exception chain may need to be updated to doubly-linked list to be able to efficiently verify that new exception is not already present in the chain. I don't see any costly overhead implications from that though.That might be sufficient for a particular application like Sociomantic's, but it's not sufficient for library code in general. Such chains aren't logic errors and can easily occur in the wild. The point of exception chaining is so exceptions can be thrown in destructors and such. In library code, these destructors don't know what exception is in flight, so they can only assume it could be any exception. If self-referencing chains were illegal and exceptions were statically allocated, then the conclusion is that these destructors can't throw anything after all, because that exception might already be in flight. As such, exception chaining would be completely useless.
Sep 19 2014
On Friday, 19 September 2014 at 11:36:29 UTC, Jakob Ovrum wrote:That might be sufficient for a particular application like Sociomantic's, but it's not sufficient for library code in general. Such chains aren't logic errors and can easily occur in the wild. The point of exception chaining is so exceptions can be thrown in destructors and such. In library code, these destructors don't know what exception is in flight, so they can only assume it could be any exception. If self-referencing chains were illegal and exceptions were statically allocated, then the conclusion is that these destructors can't throw anything after all, because that exception might already be in flight. As such, exception chaining would be completely useless.This is pretty much saying that anything should be able to throw anything. For me it sounds as flawed application - if recurrent chaining happens when handling destructor, most likely there is a fundamental programming error in application logic (some sort of cyclic dependency) and it should be pointed to and fixed. Probably there is some convincing example of such legal code but I can't imagine it on my own.
Sep 19 2014