At lot of what we learned about performance in the old days is no longer true. I'd say every time I've fired up the profiler to try to speed something up I've gotten a surprise and rarely is it because of inefficient code on my part. (And the only one of those I recall is where I used n^2 routines knowing n was small--only the use had evolved, when I wrote it n was generally around 20 and almost never over 50. When I got asked to look into it there was a case where n was around 2000.)
99% of your code doesn't need optimization, period. If it's not inside at least two loops it almost certainly doesn't meaningfully contribute to the total runtime.
That's only true for application code. If you write libraries, you can't know how your library functions will be called and have to assume that most of them could end up in performance-critical code paths.
From Joe Duffy's series of blog posts on Midori[1]:
> Error codes fail the test of “zero overhead for common cases; pay for play for uncommon cases”:
> There is calling convention impact. You now have two values to return (for non-void returning functions): the actual return value and the possible error. This burns more registers and/or stack space, making calls less efficient. Inlining can of course help to recover this for the subset of calls that can be inlined.
> There are branches injected into callsites anywhere a callee can fail. I call costs like this “peanut butter,” because the checks are smeared across the code, making it difficult to measure the impact directly. In Midori we were able to experiment and measure, and confirm that yes, indeed, the cost here is nontrivial. There is also a secondary effect which is, because functions contain more branches, there is more risk of confusing the optimizer.
> This might be surprising to some people, since undoubtedly everyone has heard that “exceptions are slow.” It turns out that they don’t have to be. And, when done right, they get error handling code and data off hot paths which increases I-cache and TLB performance, compared to the overheads above, which obviously decreases them.
Nobody seems to mention this when comparing, but code to handle the case of getting a bad error code is almost always very poorly exercised: therefore, buggy.
That is in contrast to the code run during an exception, the destructors, which run all the damn time.
If you do make the effort to test your error-handling code, that is a significant expense on top of the reduced performance. Maintaining those tests as the code changes from under them is another expense.
Exactly this. It's hardly ever mentionned, but as error paths are less tested, code that is written without exceptions (and RAII) will be wrong a lot more often.
Could you compile methods with error codes into two versions - one that handles error codes and one that ignores them. Then when you want to return an error code, you also transfer control to the version of each method that handles the codes.
That's basically how exceptions work in a modern C++ ABI. And it's why taking an exception is so slow--throw has to lookup the caller in a huge map to find the address of the caller's catch handler to jump to, then that function has to do the same, etc, all the way up the stack.
In principal exceptions should be much faster than explicit error checking, and as in the article this is often the case. But modern CPUs are really good at speculating through conditional branches. To achieve "zero cost" exceptions compilers often have to do non-trivial code reordering and duplication to segregate the non-exception and exception branches. Sometimes this necessary rewriting can have a negative impact on performance.
One feature I've wanted is an error return keyword to complement 'return val;' something like 'error_return val;' Then a feature where if a function does an error return but the caller doesn't check the error the compiler inserts a exception or a panic.
So you could write code like
log_stuff(stuff); // on an error will panic.
var rtn = log_stuff(stuff);
if(rtn.error)
{
// handle error
}
try
{
log_stuff(stuff);
}
catch()
{
// jumps here on an error
}
21 comments
[ 2.8 ms ] story [ 58.5 ms ] threadSlides and missing graph on slide 45 available at: https://sec.ch9.ms/ecn/events/GoingNative12/GN12Cpp11Style.p...
>Although it should fail gracefully, it does not need to be optimised for failure.
That case is easy to hit with compilers or annotation and such features. It is still one thing to optimize but impact is typically big.
> Error codes fail the test of “zero overhead for common cases; pay for play for uncommon cases”:
> There is calling convention impact. You now have two values to return (for non-void returning functions): the actual return value and the possible error. This burns more registers and/or stack space, making calls less efficient. Inlining can of course help to recover this for the subset of calls that can be inlined.
> There are branches injected into callsites anywhere a callee can fail. I call costs like this “peanut butter,” because the checks are smeared across the code, making it difficult to measure the impact directly. In Midori we were able to experiment and measure, and confirm that yes, indeed, the cost here is nontrivial. There is also a secondary effect which is, because functions contain more branches, there is more risk of confusing the optimizer.
> This might be surprising to some people, since undoubtedly everyone has heard that “exceptions are slow.” It turns out that they don’t have to be. And, when done right, they get error handling code and data off hot paths which increases I-cache and TLB performance, compared to the overheads above, which obviously decreases them.
[1] http://joeduffyblog.com/2016/02/07/the-error-model/
That is in contrast to the code run during an exception, the destructors, which run all the damn time.
If you do make the effort to test your error-handling code, that is a significant expense on top of the reduced performance. Maintaining those tests as the code changes from under them is another expense.
The conclusion is that panics have less overheads than returning errors. I'm curious to actually hear more
In principal exceptions should be much faster than explicit error checking, and as in the article this is often the case. But modern CPUs are really good at speculating through conditional branches. To achieve "zero cost" exceptions compilers often have to do non-trivial code reordering and duplication to segregate the non-exception and exception branches. Sometimes this necessary rewriting can have a negative impact on performance.
So you could write code like
No, thank you. I'll keep my Result even if it was 200% slower.
Btw. Is there anything that prevents compiler from compiling error handling code the way exception throwing would be?
https://llvm.org/devmtg/2019-10/slides/Kumar-HotColdSplittin...