There are various ways in software to handle error conditions. In C or Go, one returns error code. Other programming languages like C++ or Java prefer to throw exceptions. One benefit of using exceptions is that it keeps your code mostly clean since the error-handling code is often separate.
It is debatable whether handling exceptions is better than dealing with error codes. I will happily use one or the other.
What I will object to, however, is the use of exceptions for control flow. It is fine to throw an exception when a file cannot be opened, unexpectedly. But you should not use exceptions to branch on the type of a value.
Let me illustrate.
Suppose that my code expects integers to be always positive. I might then have a function that checks such a condition:
int get_positive_value(int x) { if(x < 0) { throw std::runtime_error("it is not positive!"); } return x; }
So far, so good. I am assuming that the exception is normally never thrown. It gets thrown if I have some kind of error.
If I want to sum the absolute values of the integers contained in an array, the following branching code is fine:
int sum = 0; for (int x : a) { if(x < 0) { sum += -x; } else { sum += x; } }
Unfortunately, I often see solutions abusing exceptions:
int sum = 0; for (int x : a) { try { sum += get_positive_value(x); } catch (...) { sum += -x; } }
The latter is obviously ugly and hard-to-maintain code. But what is more, it can be highly inefficient. To illustrate, I wrote a small benchmark over random arrays containing a few thousand elements. I use the LLVM clang 12 compiler on a skylake processor. The normal code is 10000 times faster in my tests!
normal code | 0.05 ns/value |
exception | 500 ns/value |
Your results will differ but it is generally the case that using exceptions for control flow leads to suboptimal performance. And it is ugly too!
To make the cost of exceptions more explicit, consider Java. When an exception is thrown, a full backtrace is stored. This involves quite some memory allocation.
Now if you are in a language with elegant and efficient optionals such as Rust, you can get some very nice code to handle nonstandard cases.
But things get more tricky when you need to, e.g., parse numbers from a stream. What if you expect a number, but it’s syntax is invalid? In Java, the method then no longer can return a native int, but you would need to trust the hotspot compilers escape analysis to eliminate all overhead from returning a double boxed Optional.
Some tools (e.g., ELKI https://github.com/elki-project/elki/blob/146bcb9fbc428e9c4bccdfde3e6f17ae18a38ebd/elki-core-util/src/main/java/elki/utilities/io/ParseUtil.java#L162 ) use preallocated exceptions without a stack trace in performance critical paths, where exceptions are somewhat needed to not pay other overheads (boxing) and a lot of boilerplate code to unbox and handle the exceptions yourself.
I don’t know if something similar would be possible in C?
The Rust solution here seems very nice and elegant to me.
I think that optimizing for the most common path and then adjusting for the extraordinary path is the best way to do things.
Exceptions should be rare, building the context for an exception is done dynamically as far as I can remember. There is a fair amount of discussion about exactly this topic here on StackOverflow: https://stackoverflow.com/questions/13835817/are-exceptions-in-c-really-slow
To build onto the comment of E.S., here is more historical context as to why C++ does not store the context except precisely where an exception occurs: https://stackoverflow.com/questions/3311641/c-exception-throw-catch-optimizations
The current accepted answer as of May 16, 2022 links to the clang-dev mailing list describing why exceptions are done the way they are: https://lists.llvm.org/pipermail/cfe-dev/2015-March/042035.html
Lemire does make an excellent point and I wholly agree.
I have rather harsh opinion on error codes, mostly for the meat space aspect where context may not easily be understood or there is cascade of missing context(s) on the scheme of returning codes up the stack of a call that goes deep. But that is probably a discussion for a later time.
The result of “0.05 ns/value” is highly dubious. If your test machine has 5GHz and you manage to execute 4 instructions per cycle, you still consume 0.05 ns/instruction. The code looks like it would take more than 1 instruction per value.
You use the result, so the compiler probably doesn’t remove the entire loop. But the core looks simple enough for auto-vectorization. Depending on your view, that may make the comparison unfair.
Then again, even translated to scalar code I would expect throughput around 1ns/value, still 500x faster than the exception variant.
I am not sure why you are saying that the result is dubious. You would expect most compilers to autovectorize the exception-free routine. That’s the desired output.