Avoid exception throwing in performance-sensitive code
lemire.me
lemire.me
When a new RuntimeException is thrown half the time, the example runs about 650 times slower when compared to a function which adds up the integers without using exceptions. If I define an exception subclass which doesn't fill in the stack trace, then it runs about 10 times slower. If I throw a singleton exception instance, then the performance is identical.
The reason why the performance is identical is because HotSpot inlined the code and converted the immediate throw-catch into a simple goto. It wasn't smart enough to see that the stack trace wasn't needed, nor was it smart enough to see that allocating new instances wasn't needed either. I had to make those transformations manually.
The article focuses on C++, which has a notion of object destructors (most – but not all – programming languages don't have the destructors).
Implications for the exception handling are manyfold: upon an entry into a «try» block, a C++ compiler has to account for all objects created at the method (or function) scope up until this point and register their corresponding destructors in the exception unwinding table. Then, since C++ allows objects to be created on the stack (via RAII or an explicit object declaration), the call frame has to be correctly accounted for as well. Both of which are computationally expensive things to do.
When an exception is thrown out, the «throw» statement results in a reverse walk back of the registered destructors first (apart from the objects created on the heap), and then adjusting the frame pointer and placing an exception object on the stack before returning from the method's (or function's) exception handler.
All of that takes many CPU cycles and wreaks havoc on instruction scheduling, pipelines, the TLB and stuff, therefore making the exception handling very expensive in C++ with little room left for optimisations. Exception handling performance in earlier revisions of C++ was abysmal. It is also all C++ specific and does not apply to other programming languages.
Java, for instance, doesn't do that, and leaves the heap clean-up (where all Java objects are created anyway) to the garbage collector, so the exception handling is less taxing in Java – at the exception raising point.
P.S. The above is a gross oversimplification of how the exception handling works in C++, but it should it give a rough idea of why the author has observed a slowdown at an orders of magnitude scale.
This is very interesting. Both GCC and clang do not do that, as they represent exceptions as abnormal edges out of a basic block and don't optimize further. I guess in Java exceptions are common enough that it is worth the additional effort of transforming some of these cases into normal jumps when the destination is seen, while in C++ it is more of a vicious circle of exceptions not being optimized because they are uncommon and being used sparingly because they are optimized.
It is of course possible that Java exceptions semantics are such that they might be easier to optimize (lots of observable side effects in C++ unfortunately).
If the error will stop the program flow and show a warning dialog to the user, it's useless to think too much about performance. More or less the same if it's going to log some message and abort the operation.
What is usually frowned upon is using the exception as a kind of goto for normal flow of the program. Exceptions should be... the exception.
Otherwise all this performance brouhaha is a waste of time.
That's not how idiomatic C++ is. Nor Rust. Nor Go.
One of Java's (incl standard library) main design flaws is overuse of exceptions.
It should not be emulated. It's too late to fix Java, but that doesn't make it a good idea.
I would not call std::optional in C++ any form of checked exception, and the difference isn't that std::optional doesn't carry value-missing metadata.
C++ throws on memory allocation error, but that's about it. Memory allocation errors are special in all languages. Because of lazy allocation and overcommit, unless you specifically set your environment to work otherwise, your program will probably just crash when it gets its first page fault that can't be honored.
Open a file? fstream sets .is_open() (or its operator bool, so just "if (!f)")
Write fails? Sets .fail()
POSIX stuff usually return an error code, and set errno.
Modern C++ has std::optional.
But there's of course another answer to this, and that is "C++ has zero cost abstractions", meaning for example if you don't check for nullptr, then neither will the language. There's no NullPointerException because C++ just says that this is Undefined Behavior.
Oh, here's one: If you use dynamic_cast to try to downcast into the wrong type, that'll throw an exception. But first of all: don't downcast, and second of all: This is not an "unexpected input to function". This is a complete programming error and it's probably best to terminate. I.e. this is something Go style would panic about, not return an error.
Do you have more specific examples about unexpected input to a function that you would want to return an error for?
Ugh. Actually std::stoi() violates this pattern. If std::optional existed in C++11 it would probably have been used here.
Actually no, not in Java! You can catch java.lang.OutOfMemoryError just like any other exception. If the memory pressure is high though, it's possible that another OOM would be thrown from the code that handles it.
But when memory pressure is high you can get killed at any time. E.g. on Linux the OOM killer might decide that it's best for the system that your process dies, even if you've not done memory allocations or needed to page fault for hours.
IIRC OpenBSD doesn't overcommit memory, but in my experience its system stability is much worse when memory is low.
If you exit the program on error, this does work in some cases like command-line utilities, but your users would not be happy if your GUI app crashes when you open a malformed file.
Yes, I called it "idiomatic C++" before, but reasonable people disagree about the best option.
Smarter people than me have written good things in the E section of the C++ Core Guidelines. The people involved have overlap with the C++ standards committee:
http://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#...
At least if you use exceptions for errors C++ doesn't need "finally", because it has working RAII, unlike Java.
try(FileInputStream in=new FileInputStream(file)){
// do something with the file data
}catch(IOException x){
x.printStackTrace();
}
It's been a very long time since I last wrote "something.close()".But since you brought it up ;) I bounce back and forth between which approach I like. Sometimes exceptions seem bulky and unwieldy and honestly a bit lazy. Returning an error feels verbose and annoying and bulky as well. But it also feels like returning an error forces you to think about what should happen, where exceptions let you kick the can down the road.
Neither are great, but I find that code that uses exceptions ends up being poorer in design and functioning, but also errors-on-return tends to be harder to read.
:)
Exceptions are just “goto whatever catch is in the call stack”.
It’s still completely obvious when you see a throw statement that it can throw, and static analysis can tell you exactly what can be thrown by each function.
“Who will catch this” is supposed to be opaque. It’s like wondering who will call this function.
If you want to goto some specific point in the code, just call it.
It is the same "problem" as not knowing where a return will return to.
You can't "just" search for function name, you have to rely on code analysis tools.
Making code more opaque coz you can get thru the mess of it via tooling is terrible direction
Goto is used often in C parsers for a reason, because it is easier to reason that way when you're validating something rather than using deeply nested branches. People shit on goto because of dijkstra's paper but few people have even read that paper and fewer even know that it is the origin of "x considered harmful" meme.
Goto is a boon. Anytime people use exceptions like how you are you're just admitting you're itching for a goto and your language doesn't provide it. And thus you shouldn't clutch your pearls when others use it in c.
This fits within my understanding of a reasonable use for exceptions - usually these are in the form of assertions, which themselves throw exceptions but can be turned off at runtime if you don't expect to ever see this condition in real production circumstances.
the example shows an extra time of 500nanoseconds when not throwing. this is 0.005 milliseconds. if your api takes more than 1ms then changing it to not throw will not be noticeable. even if the exception time cost was 10x higher and it took 0.05ms longer it would not be noticeable
I know the article is about performance but from a sheer programmjng perspective brqnches are there for a reason.
Whenever I'm writing Python or Java I can't help but feel anxious about calling a function and having absolutely no idea if or what exceptions it might throw, and then having to resort to digging through documentation or class hierarchies to figure it out.
Not to mention, Go has proven conclusively to me that exceptions are exactly the right pattern for error propagation - the 99.9% pattern is "function returns error with message; callers add context; top-level caller aborts and logs + returns message to user, or sometimes retries". This is exactly how exceptions work out of the box, without the need to pollute all code between the error source and the top-level caller.
If anything, I'd like to see a language seeking to add smarter information to exception stack traces - not just the function + code line, but also some information about local variables might be doable, and would supplant the one occasional gap between Go-style hand-built context and Java/Python auto-generated exceptions.
This is in an era of tech where everyone is building BAD distributed systems, with non-optimized databases, a fur ball of slow HTTP queries, and giant payloads. Exceptions are the least of your concerns.
This is exactly why it's good. It forces developers to think about things outside the happy path business logic. And it does this at the point where they know best what such an error could mean.
Proponents of exceptions let their users catch the exceptions they overlooked.
If you ask me Go is not forcing this enough, the ML style Result types in Rust are a much better abstraction here.
How did we manage to get this far before people ~~went insane~~ decided that this is somehow favourable?
Conversely, sometimes there are really simple cases that are totally ignored by programmers because they forget that an exception might be thrown. The worst offender in my experience is in Python where people assume the existence of dictionary keys and get KeyError exceptions, or NoneType exceptions. Without type hints, there's no mechanism to warn you as you write the code that these things could occur. So programmers don't plan for it. If there are explicit errors, the programmer is forced to consider the case that the key they want doesn't exist, or that this variable isn't actually the type they expect (static typing helps here too).
In general, I think this prevents footguns when you are working on large programs. It's worth the verbosity.
You have a headache and take painkillers, believing it's going to cure it. No, it doesn't cure the headache, it just masks it out.
The cure is teaching people how to actually program the machines. That's all it takes. Decades of abstraction and piling up more and more nonsense eventually led to the situation we are in now, where people praise bullshit which wouldn't be necessary if people had actually learned how to program the machine and how to do it properly.
Instead people need more and more help to do even the simplest things because they've never learned how to do things the right way. So now idiots require tools preventing them from making mistakes they wouldn't be making if they were properly educated!
result, err := call()
if err != nil {
return err
}
to let result = call()?;
Completely unobtrusive, but makes failure awareness an obligation. result, err := call()
if err != nil {
return fmt.Errorf("Error while trying to call: %v", err)
}
Essentially manually building a stack trace. If just doing "return err", you end up with calls to a REST service failing with messages like `couldn't parse "" as int` even in internal logs, which isn't helpful. With exceptions (in any language except C++), the stack trace is often decent enough.Does ? add any kind of context implicitly, or do you have to actually pattern match manually to add context?
let result = call().map_err(|e| SomeError::MoreData(e))?;
Or if using one of the common error libraries to have non-specific error kinds: let result = call().context("some data")?;
or let result = call().with_context(format!("something happened: {}", other_thing))?;
For something that more directly matches the behavior of the go code, this (using the `anyhow` crate) is a possible match: let result = call().map_err(|e| anyhow::Error::msg(format!("Error while trying to call: {:?}", e)?;
Though one would normally avoid this when using anyhow (or in rust in general) as it means we're flattening the error instead of generating a list of causes. //without context
some_function()?;
//with context, using anyhow
some_function().context("function returned error")?;
//with wrap_err, using eyre
some_function().wrap_err("function returned error eyre")?;Instead, the trend was to avoid using checked exceptions at all, which perpetuated the crazy situation we're in where library code can suddenly abort and the author shrugs and says "shoulda read the docs".
> Instead, the trend was to avoid using checked exceptions at all
Java's type system was far too weak for that to be at all practical. Early Java not only didn't have first-class functions, it didn't even have generics. Even today checked exceptions are horribly cumbersome - for example, it's simply not possible to write a wrapper function that accepts a function and calls it, and throws the same set of checked exceptions that the inner function does.
If Java had had proper ML-style types from day 1, like Rust does, then it could have done "errors are values" in a practical way. But forcing people to use checked exceptions would have resulted in either C-style errno codes, or the whole language collapsing under its own weight.
My point is less that Java could have done this differently with the other type choices that they made and more that it's a shame that Java's implementation of checked exceptions has poisoned the concept for so many. I think a language with strong type inference and a checked exception mechanism would be better than Rust's Result type because you get the stack trace.
It is possible, and straightforward, to do that if the set of checked exceptions is of a statically known size, and the wrapper function can propagate the exceptions rather than catch and rethrow them. In my experience, that is a very common situation (although not universal).
If you want to catch and rethrow, that can also be done, but it requires an evil hack, and the wrapper will need to take a reified type for the exception.
In see no difference. Except for the fact that you don't have the stacktrace (in Go), and unroll the Stack manually. In any case, there'll be a generic catch (whatever you call it) on the top level of your daemon to recover from the failed request and handle the next.
As for stack traces: checked exceptions are semantically almost identical to error-as-value but they do give you a stack trace. They give you all the static guarantees of error-as-value plus the benefit of being able to know where the error came from.
I completely agree with you on that one. The only thing I'd want to add is that aside from example code, usually you don't care a lot about what specific errors might pop up, unless you're at the top level of your program. This might be the request handler, or some UI loop. On the other hand, where you care about errors, you usually don't care about results or their types.
I've been in the industry long enough that I vividly remember the catch-and-wrap orgies in Javaland of 2003. A lot of it was caused by the inferior type system of Java, which hinders composability (such as when a layer is moved to another node and suddendly you have to deal with remoting exceptions). But it also goes to show that knowing a specific class of error is often overrated in application code, and should be, well, the exception.
Actually go runs perfectly fine if you just do nothing with the error, and then you are flying past errors like it is nobody's business. Of course you can you static analysis to tell you if you forgot to handle an error, and most people do, but that is not really a language feature.
In Java a programmer is also forced to make a decision to add explicit exception handling or not. But if they choose to not add it, the program won't just pretend like all is fine and dandy, it will stop the execution of the current function and pass the exception up the stack.
Unrecoverable errors are things like stack overflows or out of bounds array access. There is no reasonable way to soldier on after this, so the program should just end. Trying to continue the program in such situations only leads to pain. Like array accesses out of bounds that allow you to read unrelated memory.
But it’s still an evolving area. For example, failure to allocate memory - is that recoverable or unrecoverable? Initially it was thought that it was unrecoverable, and programs would panic if memory failed to allocate. This seemed reasonable, until folks tried to use Rust within the Linux kernel. Within the kernel, failure to allocate memory is recoverable. Rust is evolving the semantics here.
All this to say, yes, Rust does allow you to define functions that either fail in a recoverable way, in which case the calling function should handle it. Or they fail in an unrecoverable way in which case there’s nothing the calling function can do to recover. Thankfully, panics in third party code are relatively rare so this doesn’t happen in practice.
No, I wholeheartedly disagree with this. It's the equivalent of exit(1) some way down the stack. Whats recoverable or not depends on the use case and is a decision to be made by the caller of a function, not the implementor.
Otherwise I agree with you, that library code should not fail/crash/exit(1) just because of some judgement about recoverability, and out to clean up after itself before passing control back to the caller. If the user wants to fix some ENOSPC deep in my library by shelling out to "rm -rf /" and then trying again, that's fine by me, and this should be reflected in the API.
Do you mean that e.g. an out-of-bounds error will panic? If that's the case, you can always access arrays/slices with some checked access, that will return a Result/Option and cannot panic. But it would be a PITA if you couldn't skip that.
Usually your HTTP framework will already have this implemented, i.e. a panic in a request handler will be "caught", converted to some 500 response, and should not affect other requests.
Other points of "catch all errors" don't need that. And then there are a lot of places where you do handle errors, if they are conceptually a Result. I know you can just catch SpecificError, but the ergonomics are just horrible in terms of control flow.
> there are a lot of places where you do handle errors, if they are conceptually a Result
Yet, what's conceptually a result lies in the eye of the beholder, and should not be dictated by an API designer IMHO. Rust's ? is a step in the right direction, but I'd argue since you care about specific errors in maybe 0.1% of invocations tops (in production code), and that's a stretch, the ? should actually work the other way round. And if the mechanism does not provide a way to select specific errors (such as a proper catch clause) the errors it exposes as a result should include runtime errors as well.
- You say there is no difference between unchecked exceptions and Go's errors
- I say yes there is since Go forces users to handle errors explicitly
- You say that's not technically true in all cases.
OK. Yes. I should have said "nudges users" instead of force. It's a shortcoming of the language. It is still really hard for me to see unchecked exceptions and value-based error handling as the same thing. One of them encourages doing nothing and hoping that bubbling up is the right answer. Very often, especially in a multi-threaded context, it is not.Where do I say that? I say that Go programmers, in 99.9% of cases, do manually what exceptions do automatically. In terms of cumbersome, error values are the equivalent of checked exceptions. The equivalent of runtime exceptions are panics.
> Go programmers, in 99.9% of cases, do manually what exceptions do automatically
Our experience working with Go must be very different.
Grepping for "err := " and looking at the first 10 results in my team's codebase.
* 4 cases where the error is just returned
* 1 case where the error is returned only if it matches a certain type (otherwise logged)
* 1 case where the error is logged as a warning.
* 1 case where the error is logged, some metric is incremented, and then execution continues as usual. (a fail-open authentication check)
* 1 case where the error is returned as different error type.
* 1 case where the error is returned, but only after accessing the result (this is a strange design / antipattern), and annotating it with a human-readable explanation.
* 1 case where the error is treated as a boolean condition, and is not returned. (the error condition is "does not exist").
So in this sample it matches the "automatic behavior" in only about half the cases. In other cases, substituting the existing behavior with exception's automatic behavior would cause severe bugs.(Obviously, without source code access the following is guesswork) The other cases might either not be required (since you'll get a stacktrace anyway and don't need to leave breadcrumbs) or be part of some general infrastructure, say interceptor, that logs interesting things on boundaries. At least that's my day to day experience comparing Go and Java.
The only thing that I'd add to the unchecked exceptions is noexcept with compile-time checking. Something along the lines:
1. You can declare method as `nothrows`. Compiler will ensure that no exceptions are thrown (`java.lang.Error` can still be thrown, but you're not supposed to deal with it in any way in most code).
2. You can declare method as `throws Exception1, Exception2`. Compiler will ensure that only subclasses of those exceptions are thrown from this method.
3. You can omit any declaration. In this case compiler will compute list of possible exceptions implicitly (it's a union of all exceptions thrown by all called methods).
So basically it'll allow to: document list of thrown exceptions and it'll allow to statically check that other exceptions are not thrown, so this documentation is compiler-checked.
Of course this idea needs battle testing, but I think that I'd like it. You can either opt-in and write code documenting all thrown exceptions (which is good for libraries) or you can opt-out and write simple code without bothering with exceptions (which is good for applications).
Also there should be proper support for generic exceptions, so I can write Function<T, R, E> and E would be of type containing union of all thrown exceptions. That's required for example for moving exception signatures from lambdas in functional collections.
Recoverable error states are part of your function's interface. Languages with exceptions make this implicit: in order to know how to interface with a function's error states you have to go digging through the docs, which hopefully document the exceptions it throws. If they don't, then you either have to trace the entire call tree or just wait for the library to throw something so you know what you're supposed to be catching.
On the other hand, if a language requires that recoverable errors be documented in the function signature (either as a checked exception or error-as-value), you know that you're either handling the exceptions or intentionally letting them propagate. There are no surprises at runtime because the set of all possible error states is known statically.
And even though C++ dropped exception specifications, they still kept the difference between might throw anything or doesn't throw at all, and there is also the paper to reintroduce them Swift style.
- .NET library has documentation comments, with <exception> tags
- It could look at my code and see what exceptions get thrown.
- It could be clever enough to know that new might throw OutOfMemoryException
- Clever enough to know checked arithmetic might throw OverflowException
- Might understand where/what exceptions get caught and do not bubble up.
- Smart enough to understand if value can possibly be null and properties are accessed, thus may result in ArgumentNullException.
- As for 3rd party code, I don't know if .pdb provide enough info to know whether particular function call may throw particular exception? But it's all IL, so that should be enough to infer from 3rd party code what kind of exceptions I might encounter:
// throw new ArgumentNullException("serviceProvider");
IL_0013: ldstr "serviceProvider"
IL_0018: newobj instance void [mscorlib]System.ArgumentNullException::.ctor(string)
IL_001d: throw
Eh, what a dream.However, it is worth taking it a step further and focus on "why the error occurred" instead of "an error has occurred".
An example of this is Code Contracts. Like Intellisense, live code analysis with contracts would tell you "the method you call will throw <exception-type> with the input you give".
Not only will that cover giving the user information about which error conditions that can arise, it will also give you the reason why.
C# Anders Hejlsberg, C# designer has a word on checked exceptions for which I agree with him that we should somehow address it more efficiently: https://www.artima.com/articles/the-trouble-with-checked-exc...
Looks like what I'd like in the meantime is soft-checked-exceptions. Well, just for documentation purposes. And I'll leave the code for myself. I just want to take into consideration various failure modes and now I can just do generic error handling or waste too much time. Yes, what you are saying, live code analysis would be good enough not to screw up C# language itself with bad design decisions.
They don't really. Because they force you to handle errors even in code that couldn't care less about errors because that's the responsibility of a higher level up the stack. And worse, they assume that an exception is a valid reason to just stop program execution altogether (when it rarely is).
See Erlang for how you should handle exceptions (you have a supervision tree with various restart strategies).
These are more practical (and good thing Rust isn't rigid at the expense of pragmatism).
And of course since panics are not the answer to everything exceptional, there's now a `catch_unwind`, too
C++ doesn't do this (in the standard library, setting a good tone for idiomatic code).
Older languages don't abuse the exception idea. Nor do newer ones.
(Go is a bad example though, since it took the convenience of Java style exceptions away, but replaced them with nothing)
You can try doing what most of my coworkers do and just not care.
The second one is more cumbersome with return values if the possible errors originate deep in the stack.
Beyond a simple retry, Exceptions are not specific enough for handling errors.
You really can't get more specific than that. If exceptions still feel "not specific enough for handling errors", perhaps it's because you're only thinking of most trivial examples, as given by 101 tutorials and people arguing against exception handling?
To me, once I am building my own expansive taxonomy of exceptions, I am much happier using Optional, or Result/Either type return values.
Why would you want to do that? You don't do that with Result/Either.
> To me, once I am building my own expansive taxonomy of exceptions, I am much happier using Optional, or Result/Either type return values.
I may be missing something, because to me, this doesn't follow. Optional type is "result or nothing"; and with Result/Either type, you either use something generic (e.g. symbol, string), or go very specific (even if it's just one of the dozen different "newtype" names for symbol/string). To me, this choice with Result/Either is exactly equivalent to choosing an exception taxonomy. You're doing the same work either way.
If you do very close exception handling, you lose what I consider to be 'the point of exceptions'. Hence I don't think that should be done.
The core of my argument is then that error handling beyond 'just retry' needs a lot of detail about the error. And I believe this detail is easier to encode if the error handling is close to the error. Since exception handling is meant to be further away from the error, it isn't as suited to this kind of error handling.
> Add syntactic sugar for working with the Result type which models common exception handling constructs.
The whole error handling story with Rust can be summarized as "we have results but want to have exceptions".
[1] https://rust-lang.github.io/rfcs/0243-trait-based-exception-...
Additionally, on top of the linked RFC being nearly 9 years old it doesn't at all indicate "we want to have exceptions".
The ? operator allows propogating the errors if they can't be immediately handled (similar to monadic `do` notation, or early returns)
Compare this to checked exceptions, where (even in an ideal world) you'd need a separate type parameter for the error type, plus a bunch of extra language features and syntax to make it work. And then what happens if you want to use the same function with _no_ error type?
For a concrete example, try using Java's `stream().map()` with checked exceptions.
Of course there are implementations of checked exceptions which are much closer to Result than Java's implementation, and in those cases I would agree with you.
Back in 2005, I was involved in a replatforming of the legacy COBOL and TRANSACT HP3000 mainframe codebase to the modern system. The code was transpiled into an [unholy mess of] C# and ASP.NET. The Transact code was extremely procedural and had mainframe forms interspread in it. Each block of code that ran between the form display and wait for input block turned into a function in C#, and all of them were chained in the main function with gotos. To get out of the function, the transpiled code would throw new Exception("with some custom message indicating step to go to next"), it'll be caught upstack, message parsed and then goto'ed to that. So yeah, control flow with Exceptions
I am shuddering just thinking about it now, it was impossible to read. Under medium load - and I was in charge of load testing - it threw some astronomical numbers of exceptions/sec and it was just so ugly.
Try as I might I couldn't dissuade them to change the ways they returned from the function. It all went to production. The customer just threw really big machines and lots of them and they just exceptioned all day long.
Performance-sensitive code or not, exceptions for exceptional situations are not going to hinder the app's performance until the exceptional situation becomes common (unless your language does lots of exception setup work on the happy path - which most have stopped doing by now).
Exceptions should never be used if the control flow has means to recovery. For example, if-else statements with more different, downstream instructions.
I’d argue throwing well-nested exceptions, catching them and retrying is the most elegant solution to this problem there is.
try:
my_dict[key]
catch KeyException:
pass
rather than if key in my_dict:
my_dict[key]
is astounding. I don't know what the performance difference is in Python though. thing = my_dict.get(key)
if thing is None:
...Is there an easy way to get bytecode for Python snippets?
https://jobjo.github.io/2019/04/24/ocaml-has-some-new-shiny-...
I think stack traces take up much of the time, including converting it [1]. Without them it could probably be a lot faster. Also see hashmash's post.
But other than that, there is the logic issue, not using exceptions for normal flow control makes sense at least to me too independent of any performance questions.
[1] For a Java example: https://ionutbalosin.com/2018/06/getting-the-stack-trace-ver...
I'll admit that this has enraged me on a few occasions when all I have to work with is a logged error message of
strconv.ParseInt: parsing "": invalid syntax
With no clues as to where the hell the error actually happened, so I have to start grepping the app's code, then the code of its dependencies, then the code of the dependencies' dependencies.Of course, you can’t enforce this is dependencies, but at least tracking this to the first layer dependencies of your code should be fairly easy right?
TBH, Go style errors seem more flexible if some care is put into using them, however they are extremely unhelpful if they are used improperly (even if it’s not your own code).
It's a reasonably common issue with running Go apps, you're relying on the coder to make good decisions to give you useful insights.
Other ecosystems I've used err more in favour of the person running the thing - compare the more widespread Go logging libs to, say, Java, where the sysop has very fine-grained control if they need it.
Each of the three paradigms has pros and cons in terms of code simplicity and runtime cost (in normal/error path), I don't think there is a clear winner.
To clarify a bit, in CL, when throwing an exception, you can optionally register one or more "restarts", which are essentially lambdas of 0 or more parameters. When the exception is thrown, the stack is walked to find the appropriate handler, but it is not unwound. Whoever catches the exception will also receive these restart lambdas. If they chose to call one of the lambdas (passing it the proper parameters), stack unwinding will not happen at all, and execution will continue from the place the exception was thrown. Only if none of the restarts are invoked is the stack unwound, and execution continued from the catch block.
For a somewhat trivial example, the CL runtime throws an exception whenever a variable that was not defined is being read. That exception includes a restart that allows you to define a value for the variable - if this is called, execution will continue where the variable was being read, using this value for the undefined variable. Of course, this would be crazy to do automatically, but it is very nifty when debugging, as this option is presented to you in the REPL.
Having a single type handle this with a bunch of utility functions associated is great.
Inside a single library its not too bad. You get probably an enum type with some errors. But adding stacktraves or anything is a pain if possible at all.
As a result, raising and catching exns in OCaml is cheap. List.fold_until in Base, for example, is implemented with exceptions: https://github.com/janestreet/base/blob/ae169dc8097b3da8e99d... (With_return is internally implemented by raising an exception in a try-with.)
Implementations of functional programming languages often have powerful, yet fast!, non-local control-flow primitives. GHC recently sprouted delimited continuations (https://ghc.gitlab.haskell.org/ghc/doc/users_guide/9.6.1-not...), for example, and Lisps have had that for much longer. It's easy to program in an imperative language for a long time and think that straight-line control flow is the end-all, be-all for performance—but it doesn't have to be the case.
In most cases, when you don't control what you expect, exceptions are not great. In a constrained embedded system or a trading system, they can be amazing.
Besides, exceptions in C++ are known to have negative impacts on overall performance even if you don't use them. (see: https://preshing.com/20110807/the-cost-of-enabling-exception...)
The Itanium ABI and 64-bit Windows have near-zero cost when exceptions aren't thrown.
Rust has the #[cold] attribute for this exact reason - to mark functions or branches as 'cold', placing them into a separate section to reduce the hot path's code size.
With exceptions the error paths don’t even have to exist in your hot code.
The cost of exceptions when they aren’t thrown has come a long way since 2011 (the time of that post) in clang and gcc and I’m 99% sure is almost always zero now.
int sum = 0;
for (int x : a) {
try {
sum += get_positive_value(x);
} catch (...) {
sum += -x;
}
}
you need to fire whoever does that...This means even reasonable "control flow" cases like network errors for example can be terrible.
The more general case - and the real thing this article is complaining about - is the use of exceptions for normal control flow, which is something I agree is awful (aeons ago exceptions under .net's debugger were orders of magnitude slower than outside the debugger, but it meant that if you tried to debug logic involving an ANTLR generated parser your life was misery as - at least then - ANTLR used exceptions for parser control flow \o/)
The major issue that stood out to me is that in a multi-threaded environment, exception unwinding is effectively single-threaded. This means that if you have C++ code that throws a lot of exceptions, you are going to see a lot of threads getting blocked by lock contention
function fallible(a) {
try {
return anything(a)
} catch {
const b = somethingElse(a)
return anotherThing(b)
}
}
… than if your somethingElse case handles anotherThing, or if you do more work in the try block. In some cases exception throwing outperforms if conditions as long as both the try and catch blocks only do one thing each. while read_character {
handle_character
} except_when end_of_file {
return string
}
and it did this primarily because it was typesafe: read_character always returned a character, and the "except" cases could return what they were declared to return. "except" was far from exceptional.There's nothing ineffecient about that, it's meatly linked up at compile/load time, and exceptions that go up the stack simply unwind the stack just as returning values up the stack do.
What gets users confused is conflating this with the idea of hardware interrupts and system signals, external exceptional events that can occur at any time, interrupting the flow of control and needing registers (cpu state) to be saved so the process can be restored and continue when the interrupt is handled. (and which CLU handled through the same system) Such interrupts and signals do have a superficial resemblance to excepts that are unexceptional, and to not so unusual and recoverable errors like out-of-disk-space on a file write, which is only code you're going to write for an important high availability or headless system, or a nice fat rich word processor that people will sit in all day long.
I'm just explaining this all because when I read discussions like this I'm constantly thinking "but...but... did you think of...?"
Exceptions have to walk up the stack until a suitable handler is found, that handler can't know ahead of time where the value is coming from or if it will ever arrive - just what type it will be if it arrives. Code emitting exceptions also have no knowledge who (if anyone) is going to handle their output. It is a nonlocal goto in reverse.
Compared to regular functional returns there are so many unknowns. I'd prefer returning an option value any day of the week.
This is no different from code returning a regular value. When writing `return -EINVAL;` or `return 0, fmt.Errorf("")` or `return Err(something)`, you have no idea who (if anyone) is going to handle your output.
Also, one reasonable way of implementing exceptions could be exactly to translate all functions that can throw exceptions to functions returning a Result type, and translating function calls to such functions to the equivalent of pattern matching on that return. Of course, this mostly forces the language to only support checked exceptions (otherwise this overhead would be added to all function calls, even those that can't actually fail).
Most languages that implement exceptions have chosen a different trade-off though: make exceptions costly to throw, but make sure they have 0 cost on the happy path. Happy-path code becomes more efficient than it is possible to be in a language which uses return values for errors (since there is no need to check the result before using it), but the unhappy path becomes significantly worse.
I'd also point out that in gcc and clang support nodiscard and warn_unused_result giving a function some ability to force callers to handle returns. Go, rust and even java (thanks to errorprone) have similar guardrails.
I think you also need to balance the marginal efficiency wins of not checking for errors on return with the overall robustness of your program. The likelihood that an error condition is properly handled is heavily predicated on your ability to know that it might occur in the first place. In a language where exceptions are common place make this very challenging because they heavily rely on unchecked exceptions. Languages that value error checking have tended to shun exception style in favor of returning option types.
That's exactly what the lightweight exception proposal for C++ argued for. It additionally used some ABI tricks like storing the discriminant in a flag register when returning from an exception throwing function allowing for very cheap and compact pattern matching.
So doing this in python is ok (fastest way to check if a key is in a dict is catching KeyError for example, if I remember correctly)
All this theoretical mumbo jumbo is just noise. Very few of us are dealing with the type of programming every day where exceptions can actually become a noticeable bottleneck.
A few years ago, I got hit by the high cost of an hidden exception (used for flow control by the JDK) while using LocalDate#format to parse a valid date. It was fun to troubleshoot and fix OpenJDK https://unportant.info/using-exceptions-for-flow-control-is-...
I would be interested in reading similar articles for other languages.
Without stack traces, exceptions are just a type of goto.
There is some explanation as to why this happens in this SO response [0]. The gist is that the dynamic nature of exception handling means that the compiler needs to consult runtime type information to decide where to jump when the exception is thrown, which means trawling through some relatively lengthy data structures. Adding to the problem, these data structures are not normally used a lot, so they are very likely not to be cached - though this may change for a program that actually throws exceptions in a hot loop, and the difference may not be as stark.
[0] https://stackoverflow.com/questions/13835817/are-exceptions-...
What half of the language am I missing?
Anyhow! I use exceptions and page faults to optimize my loops. There's no need to check a loop's condition every iteration when you can abuse an intentional error to do it for you. Also works great using page faults!
That's why Rust's Result<T, E> is vastly superior because it out of box pushes the users to very efficient and idiomatic error handling mechanism which is based on its enum types that enjoy a lot of compiler optimizations.
You can also achieve fairly good results with struct-based custom Result<T, E> implementations in C# but until the language gets the support of proper discriminated unions, it will always be inferior.
Thankfully, in C# and, I assume, in other exception-based languages the compiler is smart enough to recognize exceptions as cold paths and correctly reorder emitted code to minimize their impact when you do not throw them. But traversing try-catch blocks still tends to pessimize the codegen significantly hence heavy reliance of C# on various throw helpers to keep hot paths clean.
Anyway, if the callee throws an exception, it will be just as costly as any other even if you can examine the `Value/Task<T>` for `.IsFaulted` and `.Exception`.
Possibly counterintuitively, if thrown, it will also dwarf the overhead of allocating state machine object and executing `MoveNext()` decoration. Hence even in async there is value to be had in not using exceptions in performance-sensitive scenarios.
Spoiler, each throw/catch costs 20 microseconds
"On their face, the benefits of using exceptions outweigh the costs, especially
in new projects. However, for existing code, the introduction of exceptions has
implications on all dependent code. If exceptions can be propagated beyond a
new project, it also becomes problematic to integrate the new project into
existing exception-free code. Because most existing C++ code at Google is not
prepared to deal with exceptions, it is comparatively difficult to adopt new
code that generates exceptions." (https://google.github.io/styleguide/cppguide.html#Exceptions)
So, basically, Google forbids exceptions for historical reasons, not because of performance.But, sadly, countless companies parroted this section of Google's style guide for all the wrong reasons (mostly just cargo culting Google) leading to unfortunate fragmentation of error handling in the C++ library ecosystem.
https://google.github.io/styleguide/cppguide.html#Exceptions
One could argue this is something of a technical debt issue, as the rationale notes:
> "Given that Google's existing code is not exception-tolerant, the costs of using exceptions are somewhat greater than the costs in a new project. The conversion process would be slow and error-prone. We don't believe that the available alternatives to exceptions, such as error codes and assertions, introduce a significant burden."
> "Our advice against using exceptions is not predicated on philosophical or moral grounds, but practical ones. Because we'd like to use our open-source projects at Google and it's difficult to do so if those projects use exceptions, we need to advise against exceptions in Google open-source projects as well. Things would probably be different if we had to do it all over again from scratch."
I generally consider exceptions in C++ harmful and avoid them if possible. It is not, unfortunately, always possible, as exceptions are the only way the language defines for constructors to fail.
Once this decision was made, investing time in optimizing programs that use exceptions for regular control flow became very very low in the priority list of all C++ compilers. This would include recognizing cases where an exception could be replaced with a single if/else, and optimizations for the unhappy path in general.
I don’t think there’s anything in the standard that prevents the compiler from optimizing away said exception completely, but neither gcc or clang do so even in extremely trivial cases where the exception is identical to an if statement:
Would love to know why people would do this, though. Surely everyone masters if-else ssatements well before they even learn what a try-catch statement is!?
It boiled down to it was simpler to abuse exceptions. You could instead return objects, but the code simply ended up being more to write.
At some point I tried to write things the 'proper' way by returning a Result object with the possible states, but it ended up being more complex than just throwing exceptions.
Performance critical code like game engines, etc used to avoid exceptions like the plague. Not entirely surprised to find that’s still the case.
How? As long as RAII is used it's basically for free.
I believe that those idioms are useful and great even for non-exceptional code, but that's another story.
Wow, I thought I had seen bad code, but even I've never seen this. My condolences, OP.