Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

Out of interest (its decades since I last used C++) - do C++ exceptions cause an overhead generally or only if they are thrown?


With the more common modern compilers (Microsoft, GCC, LLVM, ICC, many others) there is zero runtime overhead to programming with exceptions unless they are thrown. This is possible because the unwind information is stored externally to the function code. There is still an additional overhead in terms of space, since the .eh_frame (or equivalent) sections need to loaded and possibly relocated by the linkloader at program startup, although that's minimal work if addresses are in self-relative tables like in the ARM EABI.

The alternative to having the exception-handling code out of line is, of course, handling all exceptional conditions in line, which not only makes the code harder to read but makes the generated object code bigger and, if it fails to fit in a cache line or causes unwholesomely large numbers of branch evaluations (see spectre and meltdown) results in slower or more insecure code. The real answer to C++ exceptions is the classic C-style programming where all errors are just ignored.

Of course, no amount of reasoning or hard data survives in the face of religious belief, and 80% of programmers out there are part of one cargo cult or another, so carry on with what you were doing. It's probably good enough.


No, the alternative to exceptions is a language that forces you to explicitly handle errors at the call site. See: Rust.


Not exactly. Rust has panics, which are arguably exceptions stripped down to their most practical forms.


I've had major regressions in performance when a thrown exception was added.

We narrowed it down to the fact that the compiler wasn't free to A) inline the function, B) reorder internal operations in the function which came before vs. after the exception being thrown.


Yes, exceptions affect optimizations. The crux is that anything that might throw is a barrier to any other statement with observable side effects before or after it. Neither can be moved to the other side of the potential thrower. However, explicit error handling adds a branch and explicit return inatead of a potentially throwing statement, so if the compiler is rightfully pessimistic, no performance is lost. In the other hand, you can make the compiler ignore the potential for exceptions by declaring functions and methods nothrow. This will make the compiler ignore all considerations for potential exceptions in callers and lead to better optimizations in some cases. But sticking nothrow onto things is hard because when a nothrow fuction does happen to throw, you're in UB land.


> because when a nothrow fuction does happen to throw, you're in UB land

IIRC that was the originally proposed behavior for noexcept, but it was changed at some point to call std::terminate instead.


Yikes, of course it should be noexcept instead of nothrow in my previous post!

However, I don't find any referenc to noexcept having gotten defined behaviour. Do you have any pointers?


In the current version of the standard on the GitHub repo [0], Section 14.5 Paragraph 5 says:

> Whenever an exception is thrown and the search for a handler (14.4) encounters the outermost block of a function with a non-throwing exception specification, the function std::terminate is called (14.6.1).

I don't know when precisely noexcept was changed to call std::terminate, but I did find N3103 (included in the 2010-08 post-Rapperswil mailing [1]), which argued that the standard should require calling std::terminate to avoid security issues.

[0]: https://github.com/cplusplus/draft [1]: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2010/n310...


False, exceptions can affect codegen even if they are never thrown (including cases where they cannot possibly be thrown, e.g. no throw statement is even present).


> classic C-style programming where all errors are just ignored

That's a mischaracterization of most C code written by professionals, because (with appropriate compiler flags) return codes have to be explicitly ignored. Contrast with languages where most functions don't even have a return code, and the compiler/interpreter doesn't complain about failing to catch possible exceptions. I see this a lot in Python code (which I generally like and have liked since 1.5 BTW), a bit less so in C++, etc. Even "elite" programmers in those languages tend to be sloppy that way, which I rarely see in C programmers with more than a couple of years' experience.


> That's a mischaracterization of most C code written by professionals

I've been a professional programmer for 40 years, much of that in C. I've seen a lot of code, some of it in production in critical systems for many years. It is not a mischaracterization, it is a description of the state of the art.


I'm sorry you've worked on so much crappy code. I have 30+ years' mostly-C experience myself, I've seen a lot of code that doesn't check or handle errors as well as it should, but code that routinely ignores errors (like e.g. most Python code) is a distinct minority. I wouldn't work on code that was "in production in critical systems" with that misfeature for very long before seeking a job where I could work with actual professionals.


Python's default (stop and raise the exception in the caller) is almost always what you want. A stack of ten function calls should not have ten try blocks; that's a waste of effort and very hard to read.


> Python's default (stop and raise the exception in the caller)

No, Python's default behavior is an uncaught exception causing program termination with a stack trace, which is not generally what anyone wants. You have to add code to get non-default behavior.

> A stack of ten function calls should not have ten try blocks

While I agree with that, it's kind of missing the point. Exceptions can be used well. So can return codes. The problem is that when letting an error/exception be "someone else's problem" is the default, that tends to be what everyone does. It becomes nobody's problem, except the user who's left staring at an inscrutable program-termination message. It's the Volunteers' Dilemma in code.

A good error-handling paradigm would require that errors be explicitly handled, passed on, or suppressed. Exceptions let them be invisible. C-style error returns are a bit cumbersome, but still better for correctness. My favorite approach right now is Zig's, based on error returns but with extra features (e.g. defer and try) to make common idioms less cumbersome. I've heard Rust is similar, but haven't really looked into it.


Yes and no.

I haven't measured the exact effect of this, but - although that if the wind doesn't throw the ship doesn't slow - having an exception-al path through the code will stop some compiler optimizations. I have seen this first hand but I also remember Walter Bright mentioning it either here or elsewhere.

If this is definitely the case maybe someone could chime in why exactly (I've never fiddled with a backend in the right places to deal with exceptions)


By default, every function call--including external function calls--can cause an exception to be thrown. Any unresolved function call now causes an extra edge in the control-flow graph that goes to what is effectively a return in the function. This extra edge is also harder to manipulate for normal CFG optimizations (e.g., splitting critical edges or duplicating nodes).


Yes, I also was pretty oblivious to this until Walther Bright mentioned it in the D forums a few months back. I went back and did some simple tests with the compiler explorer and sure enough, I got some extra overhead in the generated code with exceptions enabled as soon as the compiler could not prove the absence of exceptions.


SJLJ have overhead (but no one really uses that anymore). The vast majority of modern envs (which boil down to DWARF and MSVC) only have overhead when thrown.


Theoretically true, but colleagues of mine have done benchmarks showing MSVC has a slight overhead with exceptions enabled, even if none are thrown. Worth benchmarking yourself if you need to be sure.


Is that recorded somewhere? The benchmarks I've done have been pretty all over the place and seem to be around jiggling the I$ alignment. Sometimes helps, sometimes hurts.


I have a site for testing these things: https://droplet.fwsnet.net/

I wrote some tests, not sure if they are perfect:

    #include <stdexcept>
    
    volatile int i = 1;
    __attribute__((noinline)) int test() {
        return i;
    }
    
    int main()
    {
        if (test() == 0) throw std::runtime_error("");
        return 0;
    }
====> Instruction count: 612 Memory usage: 116 KB, top: 116 KB Compile time: 745326 micros Execution time: 149 micros Binary size: 98 KB

The above is with exception, below is without:

    volatile int i = 1;
    __attribute__((noinline)) int test() {
        return i;
    }
    
    int main()
    {
        if (test() == 0) return -1;
        return 0;
    }
====> Instruction count: 251 Memory usage: 16 KB, top: 16 KB Compile time: 441626 micros Execution time: 53 micros Binary size: 3 KB

Just remember to pick newlib as the target, as that is the thinnest, and the no-exceptions variant will be only 16kb.


Using full process startup/shutdown for a microbenchmark is a bit specious.

Also, what are your compile flags? godbolt shows a pretty big difference with -O2.


To monocasa, which I can't reply to:

I agree completely, it really does run everything from start to finish. Sometimes that's what you want, and sometimes you don't. I could break on main as an option, and then start the benchmarking after main. It would include teardown, unless you call _exit() at the end of main.

The compile flags are -O2, no linker GC.


I'm afraid not sorry. Though it probably wasn't a great benchmark. They were only investigating whether enabling exceptions has a cost if not thrown on MSVC. So a single example where there was a penalty was enough to cast doubt on exception usage. Enough that nobody felt like sticking their neck out to fight the cause, anyway ;)


Well, there’s a slight code size overhead.


FreePascal uses SJLJ, at least on Linux


Only when they are thrown. (for reference: https://www.youtube.com/watch?v=COEv2kq_Ht8)




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: