www.digitalmars.com         C & C++   DMDScript  

digitalmars.D - Raymond Chen's take on so-called zero cost exceptions

reply Walter Bright <newshound2 digitalmars.com> writes:
"The presence of exceptions means that the code generation is subject to 
constraints that don’t show up explicitly in the code generation: Before 
performing any operation that could potentially throw an exception, the
compiler 
must spill any object state back into memory if the object is observable from
an 
exception handler. (Any object with a destructor is observable, since the 
exception handler may have to run the destructor.)
Similarly, potentially-throwing operations limit the compiler’s ability to 
reorder or eliminate loads from or stores to observable objects because the 
exception removes the guarantee of mainline execution."

https://devblogs.microsoft.com/oldnewthing/20220228-00/?p=106296

This is what I've been saying.
Feb 28 2022
next sibling parent reply "H. S. Teoh" <hsteoh quickfur.ath.cx> writes:
On Mon, Feb 28, 2022 at 12:33:17PM -0800, Walter Bright via Digitalmars-d wrote:
 "The presence of exceptions means that the code generation is subject
 to constraints that don’t show up explicitly in the code generation:
 Before performing any operation that could potentially throw an
 exception, the compiler must spill any object state back into memory
 if the object is observable from an exception handler. (Any object
 with a destructor is observable, since the exception handler may have
 to run the destructor.) Similarly, potentially-throwing operations
 limit the compiler’s ability to reorder or eliminate loads from or
 stores to observable objects because the exception removes the
 guarantee of mainline execution."
 
 https://devblogs.microsoft.com/oldnewthing/20220228-00/?p=106296
 
 This is what I've been saying.
How is this any worse than explicitly checking for error codes? Isn't the optimization situation of: MyObj obj; mayThrow(); ... // obj.dtor called different from: MyObj obj; if (mayError() == ERROR) goto END; ... END: // obj.dtor called ? T -- Computers aren't intelligent; they only think they are.
Feb 28 2022
parent reply deadalnix <deadalnix gmail.com> writes:
On Monday, 28 February 2022 at 21:39:03 UTC, H. S. Teoh wrote:
 How is this any worse than explicitly checking for error codes? 
 Isn't the optimization situation of:

 	MyObj obj;
 	mayThrow();
 	...
 	// obj.dtor called

 different from:

 	MyObj obj;
 	if (mayError() == ERROR)
 		goto END;
 	...
 	END:
 	// obj.dtor called

 ?


 T
obj can be kept in a register in the goto case, it cannot in the exception case. So you'll have a couple extra load/store vs extra branches.
Feb 28 2022
parent reply Elronnd <elronnd elronnd.net> writes:
On Tuesday, 1 March 2022 at 01:39:05 UTC, deadalnix wrote:
 obj can be kept in a register in the goto case, it cannot in 
 the exception case. So you'll have a couple extra load/store vs 
 extra branches.
Nope, it can be kept in a register in the exception case too. See: https://godbolt.org/z/zP1P3xvr3 The pushes and pops of RBX are necessary in both cases, because it is a caller-saved register. (And, well, they are also necessary for stack alignment, so be wary of taking too many conclusions from a microbenchmark.) Beyond that, note that the exception-using versions have fewer instructions in the hot path, fewer branches, and exactly the same number of memory accesses as the manually-checking versions. (GCC generates better code, but clang's implementation of 'h' is more representative, which is why I show both; *usually* you can't run both the happy path and the sad path branchlessly.)
Feb 28 2022
next sibling parent Elronnd <elronnd elronnd.net> writes:
On Tuesday, 1 March 2022 at 05:06:22 UTC, Elronnd wrote:
 The pushes and pops of RBX are necessary in both cases, because 
 it is a caller-saved register
callee-saved, of course.
Feb 28 2022
prev sibling parent reply deadalnix <deadalnix gmail.com> writes:
On Tuesday, 1 March 2022 at 05:06:22 UTC, Elronnd wrote:
 On Tuesday, 1 March 2022 at 01:39:05 UTC, deadalnix wrote:
 obj can be kept in a register in the goto case, it cannot in 
 the exception case. So you'll have a couple extra load/store 
 vs extra branches.
Nope, it can be kept in a register in the exception case too. See: https://godbolt.org/z/zP1P3xvr3 The pushes and pops of RBX are necessary in both cases, because it is a caller-saved register. (And, well, they are also necessary for stack alignment, so be wary of taking too many conclusions from a microbenchmark.) Beyond that, note that the exception-using versions have fewer instructions in the hot path, fewer branches, and exactly the same number of memory accesses as the manually-checking versions. (GCC generates better code, but clang's implementation of 'h' is more representative, which is why I show both; *usually* you can't run both the happy path and the sad path branchlessly.)
I'm not sure that gcc's code is actually better, it's pretty much the same, with the cold section split out. The only notable difference in is to use add/mov vs lea for gcc. Interestingly, i submitted a patch to split exception code in cold section for LLVM the way GCC does it, but it ended up not being merged :/ In any case, your example show that even in the presence of exception, register promotion is still possible, which I think strengthen my original point: of all the solutions available, exception are the one that imposes the smallest cost on the non exception path.
Mar 01 2022
parent reply Elronnd <elronnd elronnd.net> writes:
On Tuesday, 1 March 2022 at 12:51:08 UTC, deadalnix wrote:
 I'm not sure that gcc's code is actually better, it's pretty 
 much the same, with the cold section split out. The only 
 notable difference in is to use add/mov vs lea for gcc.
Yes: 1. hot/cold splitting 2. lea vs add/mov (marginal, but still smaller) 3. branchless h I think all of these qualify gcc's code as better.
 Interestingly, i submitted a patch to split exception code in 
 cold section for LLVM the way GCC does it, but it ended up not 
 being merged :/
Curious, why not?
Mar 01 2022
parent deadalnix <deadalnix gmail.com> writes:
On Tuesday, 1 March 2022 at 19:20:04 UTC, Elronnd wrote:
 Interestingly, i submitted a patch to split exception code in 
 cold section for LLVM the way GCC does it, but it ended up not 
 being merged :/
Curious, why not?
The perf benefits weren't so obvious expect for huge binaries. Huge binaries ended up being optimizable in other ways, such as bolt, PGO+LTO and the benefit wasn't so obvious in the end.
Mar 01 2022
prev sibling next sibling parent reply Guillaume Piolat <first.last gmail.com> writes:
On Monday, 28 February 2022 at 20:33:17 UTC, Walter Bright wrote:
 "The presence of exceptions means that the code generation is 
 subject to constraints that don’t show up explicitly in the 
 code generation
In my C++ years, exceptions in combination with RAII were the best barrier of defense against leaks and bugs in error paths. Also they would allow C++ constructors to fail, and error codes didn't. Worse, with error codes, people routinely conflated runtime errors and unrecoverable errors. Performance is imo a false concern here since 1. either code that should be fast is devoid of eror handling in the first place 2. correctness of whole codebases is at stake 3. you can always make a correct program faster, but there will be noone to realize the incorrect program is incorrect.
Feb 28 2022
next sibling parent reply "H. S. Teoh" <hsteoh quickfur.ath.cx> writes:
On Mon, Feb 28, 2022 at 11:01:46PM +0000, Guillaume Piolat via Digitalmars-d
wrote:
 On Monday, 28 February 2022 at 20:33:17 UTC, Walter Bright wrote:
 "The presence of exceptions means that the code generation is
 subject to constraints that don’t show up explicitly in the code
 generation
In my C++ years, exceptions in combination with RAII were the best barrier of defense against leaks and bugs in error paths. Also they would allow C++ constructors to fail, and error codes didn't. Worse, with error codes, people routinely conflated runtime errors and unrecoverable errors. Performance is imo a false concern here since 1. either code that should be fast is devoid of eror handling in the first place 2. correctness of whole codebases is at stake 3. you can always make a correct program faster, but there will be noone to realize the incorrect program is incorrect.
IIRC, we had this discussion before some time ago, and Adam pointed out that in D's early days it used to have its own exception-handling implementation that involved passing exception status via a spare register, supposedly faster than the baroque C++-style libunwind / (whatever the Windows equivalent is) implementation. But later on, in the name of C++ compatibility, that got replaced with libunwind / Windows EH. My point is, if you're dealing with code that may fail, you've got to handle the error case *somehow*. Whether it's via a return code, or libunwind, or passing some status in a spare register that the compiler automatically inserts a check for. The syntax is really irrelevant. Whether you write: void mayFail() { throw new Exception(""); } void myFunc() { MyObj obj; mayFail(); ... // obj.dtor invoked } or: int mayFail() { return ERROR; } void myFunc() { MyObj obj; if (mayFail() == ERROR) // returns error status goto EXIT; ... EXIT: // obj.dtor invoked } the semantics are essentially the same. If one implementation of exceptions is not as performant, why can't we switch to a more efficient implementation? After all, the compiler can, in theory, implement `throw` in the first code snippet above by lowering it into the equivalent of the second code snippet. Say, by using a spare register to indicate the error (if mayFail returns a value besides its error status). The actual implementation of how exceptions are handled isn't really tied to the surface syntax of the program. One way or another you need to handle the error condition somehow; what about exceptions makes it fundamentally less efficient than handling an error code? If error codes are somehow fundamentally more efficient, why can't the compiler just rewrite throwing functions into functions that return error codes, with the compiler inserting error code checks into the caller as needed? T -- Why waste time learning, when ignorance is instantaneous? -- Hobbes, from Calvin & Hobbes
Feb 28 2022
parent reply Araq <rumpf_a web.de> writes:
On Tuesday, 1 March 2022 at 06:44:05 UTC, H. S. Teoh wrote:
 If error codes are somehow fundamentally more efficient, why 
 can't the compiler just rewrite throwing functions into 
 functions that return error codes, with the compiler inserting 
 error code checks into the caller as needed?
Exactly. It's entirely feasible. And it is what Swift does: https://www.mikeash.com/pyblog/friday-qa-2017-08-25-swift-error-handling-implementation.html (It's not universally faster than table based exceptions but it seems preferable for embedded devices.)
Feb 28 2022
parent deadalnix <deadalnix gmail.com> writes:
On Tuesday, 1 March 2022 at 06:50:54 UTC, Araq wrote:
 On Tuesday, 1 March 2022 at 06:44:05 UTC, H. S. Teoh wrote:
 If error codes are somehow fundamentally more efficient, why 
 can't the compiler just rewrite throwing functions into 
 functions that return error codes, with the compiler inserting 
 error code checks into the caller as needed?
Exactly. It's entirely feasible. And it is what Swift does: https://www.mikeash.com/pyblog/friday-qa-2017-08-25-swift-error-handling-implementation.html (It's not universally faster than table based exceptions but it seems preferable for embedded devices.)
Yes, this is doable and indeed done in some cases. The main benefit is that the resulting binaries are smaller, which is important on embeded devices. Exceptions handling typically causes a ~20% increase of the binaries. Another benefit is that it is much cheaper in the exceptional case. The flip side of this is that it imposes a greater cost in the non throwing case.
Mar 01 2022
prev sibling parent Ola Fosheim =?UTF-8?B?R3LDuHN0YWQ=?= <ola.fosheim.grostad gmail.com> writes:
On Monday, 28 February 2022 at 23:01:46 UTC, Guillaume Piolat 
wrote:
 Performance is imo a false concern here since 1. either code 
 that should be fast is devoid of eror handling in the first 
 place 2. correctness of whole codebases is at stake 3. you can 
 always make a correct program faster, but there will be noone 
 to realize the incorrect program is incorrect.
Yes, that is my experience as well. You try to avoid the need for error-handling and if errors can arise you delay the handling of it. Examples: - In a parser you can build a log, then check the log after parsing. - In a raytracer you can tag a pixel as failed and check it at the bottom level and retry with better solvers (or a slightly different angle or whatever). Complex error situations usually are most relevant in I/O, but in order to get the highest performance I/O you have to special case all the I/O code to the platform and use whatever response mechanism the platform provides. If you are creating a library abstraction you won't get optimal performance anyway and exceptions tend to have smaller impact than other sources for latency. I probably would not want to use unchecked exceptions for embedded, but that is a different discussion.
Mar 01 2022
prev sibling next sibling parent deadalnix <deadalnix gmail.com> writes:
On Monday, 28 February 2022 at 20:33:17 UTC, Walter Bright wrote:
 "The presence of exceptions means that the code generation is 
 subject to constraints that don’t show up explicitly in the 
 code generation: Before performing any operation that could 
 potentially throw an exception, the compiler must spill any 
 object state back into memory if the object is observable from 
 an exception handler. (Any object with a destructor is 
 observable, since the exception handler may have to run the 
 destructor.)
 Similarly, potentially-throwing operations limit the compiler’s 
 ability to reorder or eliminate loads from or stores to 
 observable objects because the exception removes the guarantee 
 of mainline execution."

 https://devblogs.microsoft.com/oldnewthing/20220228-00/?p=106296

 This is what I've been saying.
There are a bit more to this though. First the exception ABI on windows is 100% atrocious and impose way more constraints than the usual libunwind based ones. But most importantly, second, the argument is fallacious, as comparing code with and without exception makes no sense. What you want to compare is the code that uses exception vs the code that uses *something else* for error handling. While that something else is almost always more expensive than exception on the non throwing path. The numbers from the original paper show this.
Feb 28 2022
prev sibling next sibling parent Adam D Ruppe <destructionator gmail.com> writes:
On Monday, 28 February 2022 at 20:33:17 UTC, Walter Bright wrote:
 https://devblogs.microsoft.com/oldnewthing/20220228-00/?p=106296
"Zero-cost exceptions are great"
 This is what I've been saying.
Feb 28 2022
prev sibling parent reply forkit <forkit gmail.com> writes:
On Monday, 28 February 2022 at 20:33:17 UTC, Walter Bright wrote:
 This is what I've been saying.
Yeah, well, you can switch gears more quickly by having paddles shifters on the steering wheel. But so many of us have no problem with the location of the gear selector that we're all familiar with. I feel like my mechanic is telling me I should move to paddle shifters, cause then I can change more quickly. But what's it gonna cost me? And can I both, in case I don't like (or can't get used to) the paddle shifters. So the point being made in the article, is kinda... pointless.. really. i.e. It's the other questions that immediately arise that need to be answered (or even asked would be a nice start).
Feb 28 2022
next sibling parent reply meta <meta gmail.com> writes:
On Tuesday, 1 March 2022 at 06:57:26 UTC, forkit wrote:
 On Monday, 28 February 2022 at 20:33:17 UTC, Walter Bright 
 wrote:
 This is what I've been saying.
Yeah, well, you can switch gears more quickly by having paddles shifters on the steering wheel. But so many of us have no problem with the location of the gear selector that we're all familiar with. I feel like my mechanic is telling me I should move to paddle shifters, cause then I can change more quickly. But what's it gonna cost me? And can I both, in case I don't like (or can't get used to) the paddle shifters. So the point being made in the article, is kinda... pointless.. really. i.e. It's the other questions that immediately arise that need to be answered (or even asked would be a nice start).
D is in a position where it can, and should, do what ever it wants to do, that gives us an advantage over the competition, Nim for example didn't wait what ever someone says on a forum to implement RC, Zig didn't wait for anybody to do the way they do error handling, and soon compile time error for unused things despite many people against it [0] Nobody should wait for you to become comfortable to improve things People attach to a language for its vision, not because it sticks to whatever he though was good 20 years ago Someone mentioned Swift, they didn't wait for anybody to implement the Actor model at language level, same for adding language features to make SwiftUI play nice We try, we make mistakes, we learn, we change, we teach, we evolve [0] https://github.com/ziglang/zig/issues/335
Feb 28 2022
parent reply forkit <forkit gmail.com> writes:
On Tuesday, 1 March 2022 at 07:08:24 UTC, meta wrote:
 ... Nobody should wait for you to become comfortable to improve 
 things
....
Lucky you are not my mechanic. Otherwise, you would have just lost a customer ;-)
Feb 28 2022
parent meta <meta gmail.com> writes:
On Tuesday, 1 March 2022 at 07:18:29 UTC, forkit wrote:
 On Tuesday, 1 March 2022 at 07:08:24 UTC, meta wrote:
 ... Nobody should wait for you to become comfortable to 
 improve things
....
Lucky you are not my mechanic. Otherwise, you would have just lost a customer ;-)
It's ok, customers aren't eternal, we need to refresh the pool of developers and attract new souls, whom find exception handling to be a bad idea ;) However we also need to retain existing ones It's hard to balance the two, but the former is important, it makes it so the product last when the creator is no longer here.. it's life That's how my company still makes that mobile game as a service, the same game, since the beginning of iPhones, still kicking strong with new users, the game is totally different today than what it was in the past.. totally worth it
Mar 01 2022
prev sibling parent reply forkit <forkit gmail.com> writes:
On Tuesday, 1 March 2022 at 06:57:26 UTC, forkit wrote:
 On Monday, 28 February 2022 at 20:33:17 UTC, Walter Bright
We need to refocus on why exception handling was introduced, and not just on it's 'cost'. Only then do we have a subject worthy of discussion: "..exception handling was introduced to solve some of these problems" - CppCon 2019: Ben Saks “Back to Basics: Exception Handling and Exception Safety” https://www.youtube.com/watch?v=W6jZKibuJpU
Feb 28 2022
next sibling parent reply IGotD- <nise nise.com> writes:
On Tuesday, 1 March 2022 at 07:50:27 UTC, forkit wrote:
 We need to refocus on why exception handling was introduced, 
 and not just on it's 'cost'.
Yes, features always comes at a cost. Bounds checking costs CPU cycles but most people are OK with the extra cost. Another thing that I don't understand. Exceptions have been around for a long time. In the 90s and beginning of 2000s there wasn't much talk about the cost of exceptions. 20 years later and computers are a magnitude faster, suddenly exceptions are too expensive.
Mar 01 2022
next sibling parent Ola Fosheim =?UTF-8?B?R3LDuHN0YWQ=?= <ola.fosheim.grostad gmail.com> writes:
On Tuesday, 1 March 2022 at 10:27:10 UTC, IGotD- wrote:
 Another thing that I don't understand. Exceptions have been 
 around for a long time. In the 90s and beginning of 2000s there 
 wasn't much talk about the cost of exceptions. 20 years later 
 and computers are a magnitude faster, suddenly exceptions are 
 too expensive.
I think it has more to do with microbenchmarking between competing solutions when choosing a language for a project. In some cases you can get better performance with one solution in comparison to another. I guess it also could matter if you transpile to C++ from other languages as that can lead to "dumb" code that no proficient C++ programmer would write.
Mar 01 2022
prev sibling parent reply meta <meta gmail.com> writes:
On Tuesday, 1 March 2022 at 10:27:10 UTC, IGotD- wrote:
 On Tuesday, 1 March 2022 at 07:50:27 UTC, forkit wrote:
 We need to refocus on why exception handling was introduced, 
 and not just on it's 'cost'.
Yes, features always comes at a cost. Bounds checking costs CPU cycles but most people are OK with the extra cost. Another thing that I don't understand. Exceptions have been around for a long time. In the 90s and beginning of 2000s there wasn't much talk about the cost of exceptions. 20 years later and computers are a magnitude faster, suddenly exceptions are too expensive.
You can disable bounds checking, and it's not the same, you know before hand the cost and how it affects your program as a whole, which is not the case for exception handling It also is a way to design your software, proper error handling is verbose but leads to better and more portable results, syntax is simpler, easier to read and to follow With exception handling, you never know what will throw, or if something was already catched, or if you catch or cast the wrong Base type, etc (thanks OOP for yet another level of complexity btw) Error codes are just better in every aspects, scales from embedded to what ever complex solution you have They are even better when the language understands what is an 'error' at the semantic level Again, error handling is better in every aspects Also my use cases for D are system level softwares (graphics/audio engine, low level networking, automation) I should mention that I'm not asking for enforcing error handling to everyone, I just want to make sure I can live and keep programming by not having to use exceptions at all :) Also It is a personal opinion, you can't make me use them if I consider it to be a bad practice, even if some people disagree with that statement
Mar 01 2022
next sibling parent meta <meta gmail.com> writes:
I should also mention that I work a lot with C/C++ code base, we 
disabled exceptions because the C++ code inherited a C codebase, 
we found that simple error code works better for us, so we didn't 
bother using what C++ had to offer, hence my stance against 
exception handling, it's just better, simpler..
Mar 01 2022
prev sibling parent forkit <forkit gmail.com> writes:
On Tuesday, 1 March 2022 at 12:13:51 UTC, meta wrote:
 ... Also my use cases for D are system level softwares 
 (graphics/audio engine, low level networking, automation)

 I should mention that I'm not asking for enforcing error 
 handling to everyone, I just want to make sure I can live and 
 keep programming by not having to use exceptions at all :)

 ...
but you can already do this (i.e. program by not having to use exceptions at all). dmd, for example, doesn't use exceptions (as per previous post from Walter). And it's in D. I presume you're talking about exceptions in phobos? I'm not sure how much of phobos was designed for such low-level programming. But in any case, it's all open-source, and can be modified (or used to build your own libraries). So there is nothing holding you back ;-)
Mar 01 2022
prev sibling parent Guillaume Piolat <first.last gmail.com> writes:
On Tuesday, 1 March 2022 at 07:50:27 UTC, forkit wrote:
 On Tuesday, 1 March 2022 at 06:57:26 UTC, forkit wrote:
 On Monday, 28 February 2022 at 20:33:17 UTC, Walter Bright
We need to refocus on why exception handling was introduced, and not just on it's 'cost'.
+1000 The counter-hype of being anti-exceptions is even worse than being anti-OOP... 10% facts and 90% cyclical hype
Mar 01 2022