www.digitalmars.com         C & C++   DMDScript  

digitalmars.D.learn - Drawbacks of exceptions being globally allocated

reply Tejas <notrealemail gmail.com> writes:
What is the drawback of the following "simple" ``` nogc``` 
exception creation technique?

```d
import std;
void main() nogc
{
     try{
         __gshared a = new Exception("help");
         scope b = a;
         throw b;
     }
     catch(Exception e){
         printf("caught");
     }
}
```
Aug 14 2021
next sibling parent reply Tejas <notrealemail gmail.com> writes:
On Saturday, 14 August 2021 at 11:41:36 UTC, Tejas wrote:
 What is the drawback of the following "simple" ``` nogc``` 
 exception creation technique?

 ```d
 import std;
 void main() nogc
 {
     try{
         __gshared a = new Exception("help");
         scope b = a;
         throw b;
     }
     catch(Exception e){
         printf("caught");
     }
 }
 ```
I mean, there has to be a gotcha, some weird multi-threading case where this catastrophically breaks and is therefore not recommended, right? Otherwise, why can't we just use this instead of fretting with ```DIP 1008```? I even tried accessing the actual object in the code below, it didn't crash the program :D ```d import std; void main() nogc { try{ __gshared a = new Exception("help"); scope b = a; throw b; } catch(Exception e){ printf("caught\n"); printf(cast(char*)e.msg);//new code } } ```
Aug 14 2021
parent reply Alexandru Ermicioi <alexandru.ermicioi gmail.com> writes:
On Saturday, 14 August 2021 at 13:24:22 UTC, Tejas wrote:
 ...
I don't think there are any gotchas here. The problem with this technique, is when your exceptions aren't just simple labels but also carry some additional data, say for example specific error type, and subject that, caused this. In such cases you can't have a gloablly shared instance. Let's say it's doable but has lot's of drawbacks. Regards, Alexandru.
Aug 14 2021
parent reply Tejas <notrealemail gmail.com> writes:
On Saturday, 14 August 2021 at 15:28:36 UTC, Alexandru Ermicioi 
wrote:
 On Saturday, 14 August 2021 at 13:24:22 UTC, Tejas wrote:
 ...
I don't think there are any gotchas here. The problem with this technique, is when your exceptions aren't just simple labels but also carry some additional data, say for example specific error type, and subject that, caused this. In such cases you can't have a gloablly shared instance. Let's say it's doable but has lot's of drawbacks. Regards, Alexandru.
Thank you. If you're willing to help further, would you please tell me why there is a GC allocation in the code below that uses ```emplace```? Will such code truly work if GC is never linked in the program? ```d import core.lifetime:emplace; import core.stdc.stdlib:malloc; int foo(int) nogc { auto mem = cast(Exception)malloc(__traits(classInstanceSize, Exception)); auto memo = emplace!(Exception,string)(mem, "HOHOH"); //scope b = a; throw memo; } void test() nogc { try { foo(1); } catch(Exception e) { } } void main() { import std.stdio; import core.memory; auto stats1 = GC.stats(); test(); auto stats2 = GC.stats(); writeln(stats1); writeln(stats2); } Output: Stats(0, 0, 0) Stats(1376, 1047200, 1360) ```
Aug 14 2021
parent reply Paul Backus <snarwin gmail.com> writes:
On Saturday, 14 August 2021 at 15:58:17 UTC, Tejas wrote:
 If you're willing to help further, would you please tell me why 
 there is a GC allocation in the code below that uses 
 ```emplace```? Will such code truly work if GC is never linked 
 in the program?
https://run.dlang.io/is/XEc2WJ ``` onlineapp.d(26): Error: ` nogc` function `D main` cannot call non- nogc function `core.memory.GC.stats` ``` Looks like `GC.stats` uses the GC.
Aug 14 2021
parent Tejas <notrealemail gmail.com> writes:
On Saturday, 14 August 2021 at 23:14:51 UTC, Paul Backus wrote:
 On Saturday, 14 August 2021 at 15:58:17 UTC, Tejas wrote:
 If you're willing to help further, would you please tell me 
 why there is a GC allocation in the code below that uses 
 ```emplace```? Will such code truly work if GC is never linked 
 in the program?
https://run.dlang.io/is/XEc2WJ ``` onlineapp.d(26): Error: ` nogc` function `D main` cannot call non- nogc function `core.memory.GC.stats` ``` Looks like `GC.stats` uses the GC.
Then why is it (0,0,0) the first time it gets used? I actually stole this from [here](https://github.com/dlang/dmd/pull/8508#issuecomment-538310708) and assumed the author knew what he was doing. FWIW, using ```DRT-gcopt=verbose:2``` gives grand total GC time as 0 so maybe there is something up with ```GC.stats``` only.
Aug 14 2021
prev sibling parent reply =?UTF-8?Q?Ali_=c3=87ehreli?= <acehreli yahoo.com> writes:
On 8/14/21 4:41 AM, Tejas wrote:

 What is the drawback of the following "simple" ``` nogc``` exception
 creation technique?

 ```d
 import std;
 void main() nogc
 {
      try{
          __gshared a = new Exception("help");
          scope b = a;
          throw b;
      }
      catch(Exception e){
          printf("caught");
      }
 }
 ```
So, there would be many exception objects one for each place that an exception can be thrown. Functions like enforce() would have to take a reference to the exception object that is associated with that local scope. I don't have a clear idea on whether it would work or whether it would be cumbersome to use or not. I wrote the following by misunderstanding you. I thought you you were proposing just one exception object for the whole program. I am still posting it because it is something I realized relatively recently. Even though this feature is probably never used, in D, multiple exception objects are chained. For example, you can throw e.g. in a destructor when there is an active exception in flight and that second object gets attached to the first one in linked list fashion. This may be useful in some cases but in general, these colatteral exceptions don't carry much information and I don't think anybody looks at them. Usually, the first one is the one that explains the error case. All such collateral exceptions are accessible through the Throwable.next function. However, even if D did not have such a feature and it had only a single exception that could be thrown (like in C++), the reality is, there can be infinite number of exceptions objects alive. This fact is true for C++ as well and this fact is one of the main reasons why exceptions are not allowed in safety-critical systems: When you can't limit the number of exception objects, you can't guarantee that the system will not run out of memory. Here is how even in C++ there can be infine exception objects. (Note: Yes, there is only one in flight but there is no limit on the number of caught exception objects that are alive.) try { foo(); } catch (Exception exc) { // Note: We caught the exception; so it's not in flight anymore bar(); // For the following to work, exc must be alive even after bar() writeln(exc.msg); } Now imagine bar() had a try-catch of its own where it caught another exception. During the execution of bar's catch clause, there are two exception objects alive. So, the problem with your proposal is, there is no room for the exception that was throw during bar's execution. Ali
Aug 14 2021
next sibling parent reply Tejas <notrealemail gmail.com> writes:
On Sunday, 15 August 2021 at 00:15:32 UTC, Ali Çehreli wrote:
 On 8/14/21 4:41 AM, Tejas wrote:

 [...]
exception
 [...]
So, there would be many exception objects one for each place that an exception can be thrown. Functions like enforce() would have to take a reference to the exception object that is associated with that local scope. [...]
I just want ``` nogc``` exceptions ;_; Please tell me that the ```GC.stats``` thing I posted is irrelevant so that I can go back to using ```emplace```. I wanted to allocate a class on the heap without being forced to use templates... but I guess that simply isn't possible(I know I can use ```mixin```, but that's even worse).
Aug 14 2021
next sibling parent Tejas <notrealemail gmail.com> writes:
On Sunday, 15 August 2021 at 02:09:08 UTC, Tejas wrote:
 On Sunday, 15 August 2021 at 00:15:32 UTC, Ali Çehreli wrote:
 On 8/14/21 4:41 AM, Tejas wrote:

 [...]
exception
 [...]
So, there would be many exception objects one for each place that an exception can be thrown. Functions like enforce() would have to take a reference to the exception object that is associated with that local scope. [...]
I just want ``` nogc``` exceptions ;_; Please tell me that the ```GC.stats``` thing I posted is irrelevant so that I can go back to using ```emplace```. I wanted to allocate a class on the heap without being forced to use templates... but I guess that simply isn't possible(I know I can use ```mixin```, but that's even worse).
In nogc code, of course, like how for stack allocation there's simple ```scope c = new C(ctor args...)```, but to allocate on heap is ```auto c = heapAlloc!(A, ctor args...)```. I wanted to remove the ```!```
Aug 14 2021
prev sibling parent reply Mathias LANG <geod24 gmail.com> writes:
On Sunday, 15 August 2021 at 02:09:08 UTC, Tejas wrote:
 On Sunday, 15 August 2021 at 00:15:32 UTC, Ali Çehreli wrote:
 On 8/14/21 4:41 AM, Tejas wrote:

 [...]
exception
 [...]
So, there would be many exception objects one for each place that an exception can be thrown. Functions like enforce() would have to take a reference to the exception object that is associated with that local scope. [...]
I just want ``` nogc``` exceptions ;_; Please tell me that the ```GC.stats``` thing I posted is irrelevant so that I can go back to using ```emplace```. I wanted to allocate a class on the heap without being forced to use templates... but I guess that simply isn't possible(I know I can use ```mixin```, but that's even worse).
You can't really have ` nogc` allocated Exception without circumventing the type system. Personally I gave up on ` nogc` just because of how inconvenient it is. Regarding the broader topic, at Sociomantic, we had pre-allocated Exception. After years working there, I grew to see the `throw new Exception` you see everywhere as an anti-pattern. Exceptions aren't convenient to use. At the very least, we should have a way to print a formatted message, however nothing currently offers this. A simple way to achieve this is the following: https://github.com/bosagora/agora/blob/113c89bd63048a7b98b8e9a2a664bd0eb08ebc84/source/agora/common/Ensure.d The gist of it is a statically allocated Exception (via module ctor) that is thrown by our `ensure` method. This is not `pure` unfortunately (because accessing a global mutable is not pure), and not ` nogc` (because `snformat` / `formattedWrite` aren't ` nogc`), but it doesn't allocate. As Ali mentioned, having a single Exception in the program breaks Exception chaining. But Exception chaining is one of the lowest ROI feature of D: It took a lot of effort to implement correctly, and is barely, if at all, used. There have even been talks (involving Walter) of deprecating it. If your goal is to never *link* in the GC, then you can barely use Phobos. If your goal is to never use the GC, I would say, why ? Using the GC for long-lived object and/or infrequent allocation doesn't hurt. GC collections are only triggered when you call new, so as long as you keep your critical path free of allocations, you're in the clear. Another little piece of code you might find interesting: https://github.com/sociomantic-tsunami/ocean/blob/adb31c84baa2061d07aaa0cb7a7d14c3cc98309b/src/ocean/core/Test.d#L305-L340 This uses a built-in method (`gc_stats`) which predates `GC.stats` and doesn't work in D2, but I'm sure you'll easily figure out how to adapt it :)
Aug 14 2021
parent Tejas <notrealemail gmail.com> writes:
On Sunday, 15 August 2021 at 03:45:07 UTC, Mathias LANG wrote:
 On Sunday, 15 August 2021 at 02:09:08 UTC, Tejas wrote:
 [...]
You can't really have ` nogc` allocated Exception without circumventing the type system. Personally I gave up on ` nogc` just because of how inconvenient it is. [...]
You think I should bother with ```gc_stats``` or stick to ```DRT-gcopt=verbose:2```? I'm trying to transpile the C++98 standard library so that we finally have a true ` nogc` standard alternative. C++98 and not C++11/14/17 because DIP 1040 is stuck in limbo. That is why I'm insisting on pure nogc stuff
Aug 14 2021
prev sibling parent reply Alexandru Ermicioi <alexandru.ermicioi gmail.com> writes:
On Sunday, 15 August 2021 at 00:15:32 UTC, Ali Çehreli wrote:

 Even though this feature is probably never used, in D, multiple 
 exception objects are chained. For example, you can throw e.g. 
 in a destructor when there is an active exception in flight and 
 that second object gets attached to the first one in linked 
 list fashion.

 This may be useful in some cases but in general, these 
 colatteral exceptions don't carry much information and I don't 
 think anybody looks at them. Usually, the first one is the one 
 that explains the error case.
That is just an assumption. There could be designs where original exception gets wrapped in another one to comply with some interface, and in such cases, having entire chain visible, is useful. Also exceptions carry the stack trace which is useful, in debugging, allowing you to know possible location of the bug. Regarding exception chaining, do you mean that it will automatically get chained, even without explicitly passing it as constructor of wrapping exception? If so, it indeed might be best to remove such functionality, and just force user to do this by himself. He will then be able to decide whether chained exception does or does not carry any useful meaning. Regards, Alexandru
Aug 15 2021
parent reply =?UTF-8?Q?Ali_=c3=87ehreli?= <acehreli yahoo.com> writes:
On 8/15/21 2:10 AM, Alexandru Ermicioi wrote:

 This may be useful in some cases but in general, these colatteral
 exceptions don't carry much information and I don't think anybody
 looks at them. Usually, the first one is the one that explains the
 error case.
 That is just an assumption.
Agreed but it's based on hands-on experience as well as exposure to these forums. :)
 There could be designs where original
 exception gets wrapped in another one
Wrapping is different and yes, it is useful. There have been cases where I hit a ConvException which only tells me a conversion failed. I do catch and augment it in outer contexts to saying something similar ot "That happened while doing this".
 Regarding exception chaining, do you mean that it will automatically get
 chained, even without explicitly passing it as constructor of wrapping
 exception?
Yes. That's the functionality which isn't very useful because the collateral exceptions are usually because of the main one: Imagine the file system is full and a destructor cannot flush a file. The destructor's error is not interesting in this case. class Main : Exception { this() { super("Main failed"); } } class Dtor : Exception { this() { super("The destructor failed"); } } struct S { ~this() { throw new Dtor(); } } import std.stdio; void main() { try { auto s = S(); throw new Main(); } catch (Exception exc) { stderr.writeln("Failed: ", exc.msg); stderr.writeln("This failed too: ", exc.next.msg); // (Of course, real code should stop when 'next' is null.) } } That output contains two automatically chained exceptions: Failed: Main failed This failed too: The destructor failed Ali
Aug 15 2021
next sibling parent Alexandru Ermicioi <alexandru.ermicioi gmail.com> writes:
On Sunday, 15 August 2021 at 16:23:25 UTC, Ali Çehreli wrote:
 That output contains two automatically chained exceptions:

 Failed: Main failed
 This failed too: The destructor failed

 Ali
Hmm, wasn't aware of such use case (results of too much java :)). Considering this case I'd say it is better to keep it, because having more info than less is better for debugging. Even in your example, you already catch an use case that wasn't accounted for, that may or may not require fixing, i.e. it is better to know it then be in blissfull unawareness. Though it is annoying to view those chained stacks, since they have repetitions. It would be nice if stack traces of nested exceptions would just show lines up to next exception thrown similar to how java does. Regards, Alexandru
Aug 15 2021
prev sibling parent reply Tejas <notrealemail gmail.com> writes:
On Sunday, 15 August 2021 at 16:23:25 UTC, Ali Çehreli wrote:
 On 8/15/21 2:10 AM, Alexandru Ermicioi wrote:

 This may be useful in some cases but in general, these
colatteral
 exceptions don't carry much information and I don't think
anybody
 looks at them. Usually, the first one is the one that
explains the
 error case.
 That is just an assumption.
Agreed but it's based on hands-on experience as well as exposure to these forums. :)
 There could be designs where original
 exception gets wrapped in another one
Wrapping is different and yes, it is useful. There have been cases where I hit a ConvException which only tells me a conversion failed. I do catch and augment it in outer contexts to saying something similar ot "That happened while doing this".
 Regarding exception chaining, do you mean that it will
automatically get
 chained, even without explicitly passing it as constructor of
wrapping
 exception?
Yes. That's the functionality which isn't very useful because the collateral exceptions are usually because of the main one: Imagine the file system is full and a destructor cannot flush a file. The destructor's error is not interesting in this case. class Main : Exception { this() { super("Main failed"); } } class Dtor : Exception { this() { super("The destructor failed"); } } struct S { ~this() { throw new Dtor(); } } import std.stdio; void main() { try { auto s = S(); throw new Main(); } catch (Exception exc) { stderr.writeln("Failed: ", exc.msg); stderr.writeln("This failed too: ", exc.next.msg); // (Of course, real code should stop when 'next' is null.) } } That output contains two automatically chained exceptions: Failed: Main failed This failed too: The destructor failed Ali
Do you see anything wrong with the following `emplace`-allocated, RAII following exceptions: ```d import std; import core.stdc.stdlib; class Main : Exception { this() nogc{ super("Main Failed"); } } class Dtor : Exception { this() nogc{ super("The destructor failed"); } } T heapAllocate(T, Args...)(Args args) nogc{ auto size = __traits(classInstanceSize, T); auto memory = malloc(size)[0 .. size]; auto instance = emplace!(T,Args)(memory, args); return instance; } struct S { ~this() nogc { scope a = heapAllocate!Dtor(); throw a; } } void main() nogc{ try { auto s = S(); scope a = heapAllocate!Main(); throw a; } catch (Exception exc) { printf("Failed: %s\n", cast(char*)exc.msg); printf("This failed too: %s\n", cast(char*)exc.next.msg); // (Of course, real code should stop when 'next' is null.) } } ``` Is this good enough for general use now? Any other drawbacks?
Aug 15 2021
parent reply Paul Backus <snarwin gmail.com> writes:
On Sunday, 15 August 2021 at 18:47:27 UTC, Tejas wrote:
 Do you see anything wrong with the following 
 `emplace`-allocated, RAII following exceptions:

 [...]

 Is this good enough for general use now? Any other drawbacks?
It only works if you're throwing and catching in the same function. Otherwise you are essentially returning a pointer to an expired stack frame, which is UB.
Aug 15 2021
next sibling parent reply Tejas <notrealemail gmail.com> writes:
On Sunday, 15 August 2021 at 20:23:03 UTC, Paul Backus wrote:
 On Sunday, 15 August 2021 at 18:47:27 UTC, Tejas wrote:
 Do you see anything wrong with the following 
 `emplace`-allocated, RAII following exceptions:

 [...]

 Is this good enough for general use now? Any other drawbacks?
It only works if you're throwing and catching in the same function. Otherwise you are essentially returning a pointer to an expired stack frame, which is UB.
Agh >_< if I remove the `scope`and replace it with `auto`? No longer having anything to do with the stack or RAII, just using malloc + emplace instead of GC? Yeah it might leak memory unless the catch block explicitly frees the exception object, but other than that?
Aug 15 2021
parent Paul Backus <snarwin gmail.com> writes:
On Monday, 16 August 2021 at 02:26:04 UTC, Tejas wrote:
  Agh >_<

 if I remove the `scope`and replace it with `auto`?
 No longer having anything to do with the stack or RAII, just 
 using malloc + emplace instead of GC?

 Yeah it might leak memory unless the catch block explicitly 
 frees the exception object, but other than that?
Yeah, other than needing to manually free the exception it should be fine. Though at that point, you might just be better off using statically-allocated exceptions.
Aug 15 2021
prev sibling parent reply Tejas <notrealemail gmail.com> writes:
On Sunday, 15 August 2021 at 20:23:03 UTC, Paul Backus wrote:
 On Sunday, 15 August 2021 at 18:47:27 UTC, Tejas wrote:
 Do you see anything wrong with the following 
 `emplace`-allocated, RAII following exceptions:

 [...]

 Is this good enough for general use now? Any other drawbacks?
It only works if you're throwing and catching in the same function. Otherwise you are essentially returning a pointer to an expired stack frame, which is UB.
Guess I should've verified before accepting this as true. ```d import std; import core.lifetime:emplace; import core.stdc.stdlib:malloc; T heapAllocate(T, Args...)(Args args) nogc{ auto size = __traits(classInstanceSize, T); auto memory = malloc(size)[0 .. size]; auto instance = emplace!(T,Args)(memory, args); return instance; } void throws() nogc{ scope a = heapAllocate!(Exception)("works fine with scope, apparently"); throw a; } void main() nogc { try{ throws(); } catch(Exception exp){ printf("%s", cast(char*)exp.msg); } } ``` That is why I was using heapAllocate, because using `new` on scope allocates exception on stack, and if you exit, then the exception is basically freed before it even gets caught. It fails even when you catch within the same function. But allocate on heap and giving ownership to a `scope` qualified variable is no problem at all. No `signal 11` killing my process ^_^
Aug 15 2021
parent reply Mathias LANG <geod24 gmail.com> writes:
On Monday, 16 August 2021 at 05:36:07 UTC, Tejas wrote:
 That is why I was using heapAllocate, because using `new` on 
 scope allocates exception on stack, and if you exit, then the 
 exception is basically freed before it even gets caught. It 
 fails even when you catch within the same function.

 But allocate on heap and giving ownership to a `scope` 
 qualified variable is no problem at all. No `signal 11` killing 
 my process ^_^
You are relying on an accept-invalid though. The compiler *should not* accept that code, but currently erroneously does so.
Aug 15 2021
parent reply Tejas <notrealemail gmail.com> writes:
On Monday, 16 August 2021 at 05:47:10 UTC, Mathias LANG wrote:
 On Monday, 16 August 2021 at 05:36:07 UTC, Tejas wrote:
 That is why I was using heapAllocate, because using `new` on 
 scope allocates exception on stack, and if you exit, then the 
 exception is basically freed before it even gets caught. It 
 fails even when you catch within the same function.

 But allocate on heap and giving ownership to a `scope` 
 qualified variable is no problem at all. No `signal 11` 
 killing my process ^_^
You are relying on an accept-invalid though. The compiler *should not* accept that code, but currently erroneously does so.
okay that's it, the following is my final try for this thing: ```d import std; import core.lifetime:emplace; import core.stdc.stdlib:malloc,free; T heapAllocate(T, Args...)(Args args) nogc{ auto size = __traits(classInstanceSize, T); auto memory = malloc(size)[0 .. size]; auto instance = emplace!(T,Args)(memory, args); return instance; } void throws() nogc{ auto/*no more scope*/ a = heapAllocate!(Exception)("works fine with scope, apparently"); throw a; } void main() nogc { try{ throws(); } catch(Exception exp){ scope(exit)free(cast(void*)exp);//new code printf("%s", cast(char*)exp.msg); } } ``` There are 0 problems with this, right? I just have to remember to free the exception in the `catch`?
Aug 15 2021
parent reply Alexandru Ermicioi <alexandru.ermicioi gmail.com> writes:
On Monday, 16 August 2021 at 06:12:14 UTC, Tejas wrote:
 ...
Fyi, check out std.exeprimental.allocator package. You can use allocators from there to do allocation of exceptions, on the heap or any other region.
Aug 15 2021
parent Tejas <notrealemail gmail.com> writes:
On Monday, 16 August 2021 at 06:17:22 UTC, Alexandru Ermicioi 
wrote:
 On Monday, 16 August 2021 at 06:12:14 UTC, Tejas wrote:
 ...
Fyi, check out std.exeprimental.allocator package. You can use allocators from there to do allocation of exceptions, on the heap or any other region.
Yes, I know about Mallocator. would've been nice to be able to do it just via the language and runtime features that are nogc. And I'll have to do deallocation manually with that as well anyways. When transpiling the C++ code, I really don't want to mix standard libraries, hence the desire to keep things contained just to the compiler/runtime as much as possible. Of course, things like `Octal` are going to force me to use phobos anyways, so I'm thinking to just introduce a new scope and import these packages there in order to prevent names from clashing, but I also don't want an excessive amount of `{...}` littering the transpiled code... Ah, I'll see to all this later, thank you very much for your time, everyone
Aug 15 2021