Jump to page: 1 2
Thread overview
@nogc and exceptions
Sep 12, 2014
Jakob Ovrum
Sep 12, 2014
monarch_dodra
Sep 12, 2014
Dmitry Olshansky
Sep 13, 2014
Jakob Ovrum
Sep 13, 2014
Dmitry Olshansky
Sep 13, 2014
Jakob Ovrum
Sep 12, 2014
Andrej Mitrovic
Sep 13, 2014
Jakob Ovrum
Sep 12, 2014
Vladimir Panteleev
Sep 12, 2014
Johannes Pfau
Sep 12, 2014
Vladimir Panteleev
Sep 13, 2014
Jakob Ovrum
Sep 13, 2014
Jakob Ovrum
Sep 12, 2014
Marc Schütz
Sep 12, 2014
Johannes Pfau
Sep 13, 2014
Marc Schütz
Sep 14, 2014
Jakob Ovrum
Sep 19, 2014
Dicebot
Sep 19, 2014
Jakob Ovrum
Sep 19, 2014
Dicebot
September 12, 2014
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?
September 12, 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.
September 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 walked

Can 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?
September 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?
September 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.
September 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
September 12, 2014
Am Fri, 12 Sep 2014 12:59:22 +0000
schrieb "Marc Schütz" <schuetzm@gmx.net>:

> 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...

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).

September 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 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?

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.)
September 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.
September 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.
« First   ‹ Prev
1 2