Exceptions are exceptional so in principle it doesn't matter (within reason) how long it takes to throw one as long as it costs nothing not to do so. So measuring the cost of repeated throws IMHO doesn't cast light on any useful case, and the approaches that add runtime cost for the path not taken, even Herb Sutter's, are not acceptable.
His code transformation example is simply the compiler behaving properly, unless it can't make that transformation even when foo() is declared noexcept.
The high core count is a real issue and a legitimate reason for an ABI break. Exceptions are the kind of below the surface plumbing that can't reasonably be implemented in regular code.
And in that regard the paper does make a good suggestion, though it then dismisses it! The tree approach described in section 3.4 seems like the right kind of fix.
Ultimately there's a spectrum of branching (`return` -> `break` -> `goto` -> `throw`) all of which need consideration in light of multicore deployment.
But there's a design pattern where nearly everything is returned as an exception. It's the normal path in some code.
I deplore that - its side-effects writ large. But those that use it, find it reasonable and sensible.
It's become hard to distinguish right and wrong when it comes to CPU optimization. Different versions of the 'same' CPU can have wildly different sweet spots.
> It's become hard to distinguish right and wrong when it comes to CPU optimization. Different versions of the 'same' CPU can have wildly different sweet spots.
Indeed, though I am shocked when people post benchmarks in which they are obviously unaware of this issue. It's why compilers have all those architecture switches.
If I understand your comment correctly: in the case of the posted paper, the compiler optimization issue the author mentioned was a semantics issue, not an architecture issue. If your comment was about optimizing for the multiprocessor case, I apologize.
You're right, the issue is convoluted and nuanced. There are architecture issues, cache sizes and layers to consider, cost of misses and writes, alignment and bus sizes.
Oh for the good ol days when register size was all you had to think about!
> But there's a design pattern where nearly everything is returned as an exception. It's the normal path in some code.
The problem isn’t that C++ exceptions are slow when used the way the compiler expects. The problem is a mismatch between how C++ expects you to use the feature, and how people are actually using the feature.
I can’t find the story now, but I read about this happening with Ruby on Rails. Someone dug into why rails was so slow at Twitter, and found the way rails was looping through an array was that it would loop unbounded, and catch and discard the array out of bounds exception at the end of the loop. Whenever Ruby throws, it allocates 10k of memory and fills it with all sorts of information for debugging - including a full stack trace. And it was doing this work in the background every time someone iterated through an array. Fixing this resulted in a massive speed up at Twitter, and presumably across the entire rails ecosystem.
I saw the same thing at (big tech company) a decade or so ago. I was working on a project which used GWT. We brought some people in to help us optimize, because the program was too slow. The first thing they found was that the JS VM was spending most of its time in the exception handler for some reason. Turned out one of our engineers had a habit of using throw & catch as a way to do multi level returns in complex code in Java. But the exception was being converted to a javascript exception by GWT. And javascript exceptions are (were?) super slow.
Y’all gotta stop using exceptions like that. I know it feels clever. But in most programming languages, using exceptions for control flow will kill your performance.
> Whenever Ruby throws, it allocates 10k of memory and fills it with all sorts of information for debugging - including a full stack trace. And it was doing this work in the background every time someone iterated through an array.
Thats the one - thankyou! Its interesting that google doesn't index the transcript of youtube talks.
Great detail I'd forgotten: At the time twitter took 460ms of compute to process each request. Almost all the CPU time was in bcopy, constructing the big string backtraces for exceptions (that were then being immediately discarded).
I think, given a language that supports elegantly catching exceptions, it is inevitable that things like that will be written. And why not? The language makes them nice to write, which signals to users that it is something you should be using a lot.
I think the approach Go, Rust and others take there is the right way to go. By making panics annoying or even impossible to recover from in some cases, they ensure that they are only used for their intended purpose and are not abused as a general purpose control flow mechanism.
> it is inevitable that things like that will be written. And why not?
We can't just blame the programming language for programmers not understanding how computers work.
I think as computers have gotten faster, and languages higher level, we've stopped talking about computers as mechanical devices. And this is a really important perspective to have.
Can you answer these questions about your program?
- How big is your binary / JS bundle? What parts take up most of the space?
- When your program runs, what does the computer spend most of its time executing? What parts of your program are the slowest, and why?
- How big is the memory footprint? Which parts of your program use the most RAM?
For binary programs (like rust / Go / C), which patterns are easier or harder for the optimizer?
If there are two ways to design your code, how do you discover which approach will run faster?
This stuff shouldn't be considered advanced concepts. An architect understands the building they've designed. A chef knows what their food tastes like. When you program, you're making a thing. You should understand what you made and how it will be executed on the computer.
I'm not sure what you're arguing with here. The need to understand what you're doing is not at odds with designing our tools to make it easy to do the right thing. Quite the opposite, knowing what we are doing helps us write good tools and good tools help us know what we are doing.
Exceptions have some of the properties of a discriminated union, which is why they're used this way. Sometimes it seems parsimonious compared to the alternatives.
Yes, the blind following of "patterns" because you think that is what makes for "good code" is a perfect example. Especially when fitting the predefined pattern bends your implementation otherwise out of shape.
A design pattern is a way of communication. If you turn it into a procrustean tool you don't even understand why they exist.
Most design patterns are anti-patterns: they represent a failure of the language to offer a way to abstract the pattern into a component or native language construct.
It is usually better if it can be captured in a library. But sometimes, particularly in weak languages, nothing but core language support will do. A fancy core language feature should embarrass the language designer.
Sometimes, a language evolves to the point where a core feature could be as well implemented as a library component. That is a marker of real progress, because now the feature can be offered in a dozen variations in a dozen libraries, each tailored to local conditions, where the core feature had to ruthlessly ignore all but the most generic use cases.
But it's not reasonable, sensible or performant. Thankfully, I've never seen anyone abuse exceptions like that in C# or any other language, is it really a thing in C++?
I wish unfortunately that's not my experience at work one developer used exception as a control flow with the excuse that it was on a non performance critical part of the code..
Annoyed me very much as it made gdb's 'catch throw' useless.
So I reimplemented this code and the result was easier to read IMHO but of course I'm biased..
The only way exceptions should be exceptional is that they should rarely be used in any code base. At best, the are a micro optimization that helps you a tiny bit in the success case. They should be used when standard return value errors are measured to be impacting perf (ie. exceedingly rarely). Unfortunately, C++ language authors made them basically a requirement for OOP and RAII.
Imagine writing a parser. Can you use exceptions? It's hard to say! In fact, often impossible to say! How often an exception is thrown is highly dependent on the input data. If you're in a controlled environment with nearly always well formed data, it might be a perf win! But there's a ton of complexity in just deciding which type of error to use, and thats on top of the complexity of now having multiple ways of returning errors!
No one likes code that has 2+ ways of indicating method failure. If you are a library author, do your users need to check for both Exceptions and Error codes? If you are a nice author, you'll just choose 1 way to indicate failure and wrap where necessary.
If you want to use exceptions, they should be used in very limited scope when perf measurements indicate they would help, tightly wrapped in a catch, and then immediately converted to some other standard error type.
Even better, we should just let the choice of when to use exceptions up to an optimizing compiler, and provide the compiler a function to convert between exceptions and our more general purpose error type of choice.
Ok, I'll bite. Disclaimer: I'm not a software engineer, FPGA engineer by training. The use model I have in my mind is that my functions all work 99.999% of the time, but occasionally something happens where basically I want to throw up, I want the whole thing to collapse. Exceptions are great for me. I'm in an industry where "stop" is an okay solution in the rare occasion something goes wrong. So what's the alternative? I have to overload my return values annd then put in a load of special cases to propogate or return the exceptional cases.
I am aware there are functional languages that have other ways of dealing with this, or languages with things like optional values etc. But fundamentaly I like the idea that there's just a mechanism to just collapse, and collapse in a way that is traceable.
Having said that, my experience essentially does not involve catching exceptions, to me that's what is almost unthinkable (read: probably a bad hack). I guess my use case is not performance but separateing normal code function from catastrophic errors. Could you explain, do you think this is a fundamental mistake in the way I'm approaching this?
> my experience essentially does not involve catching exceptions
Which is part of the problem. If you're not catching the exception, you don't need exceptions. You just need a way to log stuff, such as a trace, and exit the program. New languages such as Rust and Go encourage this explicitly - that is, use the ordained error path in the return value for expected errors, and only panic for catastrophic errors (or use panic in hotpath where return overhead has unacceptable performance impact and errors are super rare).
> Programs bugs are not recoverable run-time errors and so should not be reported as exceptions or error codes. — We must express preconditions, but using a tool other than exceptions... migrate std:: away from throwing exceptions for precondition violations.
”If you're not catching the exception, you don't need”
Exactly. But the upstream caller of your function might need it.
And that’s the beauty of it. If I don’t know how to handle a particular error in one context, I can just leave it and let it propagate upwards.
For example downloadFile()->parseHttpResponseStrean()->readSocket()
If readSocket throws a timeoutError I don’t need every http library to explicitly deal with it. What can they even do about it, apart from retrying? The application level code calling the downloadFile function is still free to catch the error and gracefully show an error to the user or use a default file from disk if they please.
Just take a look at any golang project, it has more error handling than business logic. If you are writing flight avionics sw this might be a good thing, but for my cat pic sharing MVP I’m happy to just print “unhandled internal error” when the DB stops responding due to unusual hug of death overload. There is nothing else that can be done to recover this error anyway.
On top of this it still isn’t bullet proof. There are infinite number of errors that can happen almost anywhere, especially when IO is involved; Disconnects, Time-outs, StackOverflow, ArrayOutOfBounds, DivideByZero, Overflow, NoSuchKeyInDictionary. And so on and so on.
Going back to go, Many low quality libraries still use panic, which gives you two ways to handle errors, one which
is considered exotic so you don’t normally put recover() in place, then get bitten at worst moment.
Another example being Java where checked exceptions force beginners to handle errors, where most people don’t know what to do about it, so they just log.error() and return a dummy value, giving no way for upstream to detect such error and causing the program to blow up later instead. That said, checked exceptions is probably the best middle ground between verbosity and safety. It’s just greatly misunderstood.
When you are writing a library, you don't know what the program that calls you might have set up to handle failures, but you know exactly how to get to it: throw, and you are there, and it is now for the caller to decide what to do, thus not your problem. A library that exits has stolen control away from its rightful owner.
Very few languages are as finely tuned to the needs of libraries and users of libraries as C++. Exceptions are an essential part of that compact. Failing to abide by it makes your library a bad citizen.
This just spectacularly bad advice and policy. Point performance is the worst way to decide about error handling.
The overlap between "things that fail and the caller knows something useful to do", and "things that fail and somebody maybe six levels back in the call chain might know something to do" is remarkably thin. Most things are the latter. Once in a while, it makes sense to provide one for each: (1) open a file, and it had better succeed or you might have to bail out, or (2) see if a file is there.
The reason exception is the default, aside from that there are plenty of ways to return an error report, is that when you need code to clean up, the destructors are right there. They get exercised every time through the code, so are well-tested. How often is your special-snowflake error handler ever exercised? And the next one? And the next one? Even before you have given a moment's thought to error handling, your destructors are cleaning up, correctly. So, even if you haven't written code to handle it, your program has remained in a well-defined state. And, very often, you don't even need to write them: they are auto-generated from things the compiler already knows.
If you have set up a high-level catch block, you get there without fuss, and that code knows something correct to do, even if it is only to shut down cleanly. If you left a record of what was about to be attempted, that code knows what to try next.
> Point performance is the worst way to decide about error handling.
My point is exactly that. It's more important to have a single common way of doing error handling than it is to have a small perf improvement. That's why I recommend return values. You'll never hit a perf problem with them.
Exceptions cannot be used in code hot paths (~thousands of iterations) when it's not a small perf problem anymore. Therefore, they cannot be your only method for signaling failure. And therefore, assuming we care about consistency and composability of code, it's far better to use return value based error handling unless you have good reason (almost never). Ideally with syntatic sugar from the compiler (see Rust, Zig, Swift, etc).
> when you need code to clean up, the destructors are right there.
Exceptions are not needed to achieve this. C++ calls the same 'well tested' destructors when you return from a function.
The one point in your favor, that I acknowledged above, is that C++ requires exceptions to report failure from Constructors and Destructors, for OOP, std::vector initialization, for much of the standard library etc. Thats a travesty.
Without exceptions, you will hit a performance problem on every single call, successful or not. That is what happens at Google, and in Rust: almost every last call site in the whole system has a "if" check. I.e., it is both badly obscured, and also slow.
You are arguing that programmers should not be obliged to make judicious choices. But that is exactly the job of every programmer. Dodging consequential choices practically defines a bad programmer.
Your assertion that destructors handle errors reported by return value is disingenuous, if not actively dishonest. The code checking for and acting on a failure is not in a destructor, and is never exercised except when you succeed in provoking exactly that failure.
I can't imagine error-checking if's being a source of slowness. How are exceptions generated in the first place? Using if's. And anyway, even well structured code is full of if's (e.g. array iteration), and it's not a problem. Especially for error-checking, when the error case is rare, the CPU will correctly predict the branch to take most of the time, so the cost of the if is negligible.
The only call where if's should probably be minimized is on hot paths, where there shouldn't be any I/O or other exception code anyway.
I would agree that manual stack unwinding (many stack frames deep) by propating error values up the call chain is bad. The problem here is not the if's, but that the program structure is unnecessarily convoluted. This can happen when specific code is calling generic code a lot (which can be avoided by having the generic code call the specific code instead).
> You are writing bad code, and should be ashamed.
That "if" that guards the throw is the same "if" as would guard the error return. It is the second "if", in the caller, checking the returned result, that up-to-doubles your branch-prediction cache footprint. On, yes, the hot path.
Program structure corruption is another problem. They add.
Which is what I said. And I still don't see how most code that potentially returns errors would be on a hot path. Code that is really on a hot path should probably not call into generic code anyway, and/or that called code should be inlined.
I don't know, as a C programmer, somehow I rarely run into these situations where I have to check error return values. I think the reason is that I work hard to avoid wrappers around wrappers around wrappers. Mostly error checking is required when interacting with the OS - i.e. for I/O, which is not critical paths.
A simple example would be memory allocation. A good C programmer allocates memory upfront. A bad C++ programmer could go, "eh, if it's so convenient I'll simply declare this std::vector locally, and if allocation fails it'll throw an exception and RAII and exceptions will solve the error handling issue magically without me having to type a single keystroke". Of course allocating each time is a lot slower than allocating only a single time upfront; no matter how much faster or convenient and individual allocation and error checking would be. This example shows how the perception of speed can often be warped because we're measuring the wrong thing entirely.
What is bad code is often not clear cut, and neither is how to improve it.
Inlined code still has all the "if" checks of the original code, just without the actual "jsr" and "ret" instructions in between.
Comparing a good C coder to a bad C++ coder is meaningless: By definition, a bad coder makes bad choices, whatever the language.
Bad code is slow in a place where speed matters. It is improved by making it fast. Language choice does not affect this.
What language choice does affect is whether you can afford to spend the attention needed to make the code fast. C++ provides tools to offload busywork, freeing your attention to apply to what matters. It remains the programmer's job to choose that.
I still can't imagine a situation where speed matters and where exceptional situations are likely and also hard to handle in a performant way with explicit error checking. Do you have any examples?
It suffices that exceptional situations are possible and must be checked for.
It is true that in C++, as in Rust, it is much more common than in C to have very short functions that call other very short functions that, all composed together, do a job that, absent abstraction, might all be coded in a single larger function tailored to the specific use case; and the compiler squeezes out most of the calls and generates object code as if for that single, longer function. Therefore, there are many more places where an error return report would need to bubble up through, and that would cripple performance to handle the naive way, without exceptions.
It might be that some languages that have a native "result" type, and automatically generate checking code at the call site, can squeeze out most of the intermediate-level checks when composing inline functions together, e.g. if actual errors mostly occur at leaf nodes of the call tree. That could mitigate the heavy overhead cost of the method. But I don't know if any compilers do such an optimization, or if they do, how well it really works. Coding an optimizer is hard.
> Therefore, there are many more places where an error return report would need to bubble up through, and that would cripple performance to handle the naive way, without exceptions.
Do you have any evidence for these claims? If a called function is quite long by itself, then there is hardly any added overhead if the client checks the returned code and that check would be "redundant".
For example, if I make a syscall and check the return value, there will be a duplicated checking effort with the kernel code in a way - but compared to the cost of calling into the kernel that overhead is a negligible cost for the benefit of modularization (kernel vs userspace application). This overhead will be very close to unmeasureable, and does not justify the introduction of exceptions which are an additional mechanism to return values which introduce syntactic and binary incompatibilities (i.e. non-orthogonal functionality).
Oh, by the way - wouldn't exceptions and stack unwinding have to do just as much work per stack frame? It can't really just skip over all frames in a single instruction.
I think the best solution to cruft is generally to not have it in the first place - instead of introducing a mechanism to skip over layers upon layers of "abstraction", it's better to just not have these layers. There should not be a large call chain that unwraps multiple stack frames in a row. The best code is code that just does what needs to be done in a straightforward way without using any library cruft. Nothing to "optimize" away this way.
Give any concrete example where exceptions are useful in order to work around cruft, and we can see if there is a better way to write it that does not require exceptions.
Frankly it seems hilarious to me that we're having a discussion about optimizing a few if's because they allegedly add too much overhead for the work of skipping over layers of cruft.
In the past years I have seen the need to use longjmp() (which is in a way a mechanism to get exceptions for C) only one single time, when I was coding an interpreter that got embedded into a longer running application. I didn't know how to skip through recursive layers of parsing calls in the recursive descent parser. But I still feel like there is a better way to write the parser (like, pushing work to parse to a data structure instead representing it as a nested stack frame). This could also benefit error reporting for example.
What you are calling "layers of cruft", other people call abstraction. That abstraction enables people to have code exactly tailored for a specific use without writing it over and over, slightly changed each time, for each use.
In C, of course, you have no choice; you write the whole function over and over. C programs are, e.g., filled with custom one-off hash tables, because you can't write a performant, general hash table library in C. People using C++ and Rust do not code their own hash tables, because they cannot match the performance of well-tested and tuned library components.
As a consequence, modern C++ and Rust coders get better-than-C performance for a lot less work, and ship with many fewer bugs, by relying on mature, well-tuned libraries. That is worth a lot of what you call cruft.
That said, it is not uncommon to find C++ and Rust coders aping what they see in general-purpose libraries by adding superfluous cruft in their own programs, that provides no such benefit. But that is a complaint for another day.
> wouldn't exceptions and stack unwinding have to do just as much work per stack frame? It can't really just skip over all frames in a single instruction.
Taking that back. I forgot for a second that we're focusing on the cases where the exception is not thrown.
> That's why I recommend return values. You'll never hit a perf problem with them.
> Exceptions cannot be used in code hot paths (~thousands of iterations) when it's not a small perf problem anymore.
You should read the paper in the link. Both of your claims here are exactly backward.
The problem is "just" that exceptions don't scale across multiple threads due to internally using a global lock. They are otherwise much faster than return values (4x faster in the trivial example)
> TL;DR On modern 64-bit PC architectures, C++ exceptions only add unreachable code with destructor calls into functions and their effect on performance is below 1%, but such low values are difficult to measure. Handling rare errors with return values requires additional branching that slows down the program in realistic scenarios by about 5% and are also less convenient. If an exception is actually thrown, stack unwinding costs about 2 µs per stack frame.
5% cost to using error codes. In exchange for that 5% you don't fall off a performance cliff if you hit a bunch of errors unexpectedly, and can confidently use them in hot paths and on CPUs with multiple cores.
I'll take the lower perf complexity and greater perf robustness of error codes any day. Reliability and predictability is more important to me than a 5% perf cost.
Your "better comparison" finds that error codes are slower than exceptions? It's right there at the top in bold even if you don't want to read the whole thing:
> TL;DR On modern 64-bit PC architectures, C++ exceptions only add unreachable code with destructor calls into functions and their effect on performance is below 1%, but such low values are difficult to measure. Handling rare errors with return values requires additional branching that slows down the program in realistic scenarios by about 5% and are also less convenient.
Also I see you "cherry picked" your counter example using the sqrt test, which is less influenced by per-call overhead. Using instead the fib test, which is basically just a test of call overhead, we get instead:
12ms-14ms (exceptions) vs. 22-23ms (boost::LEAF)
Both the paper & your link are in agreement - exceptions are faster than error return values.
> 5% cost to using error codes. In exchange for that 5% you don't fall off a performance cliff if you hit a bunch of errors unexpectedly, and can confidently use them in hot paths and on CPUs with multiple cores.
You also get more fragility since error handling is now manual. I'd rather have performance and reliability, which only exceptions can provide. They don't provide that today thanks to the global mutex, but the point of the paper & related explorations is that that both can be fixed, and that fixing it provides a superior performing solution. And a more reliable one.
Unless you're arguing that it's better to just always have a 5% performance tax & manual, error-prone error handling? We shouldn't attempt to fix the toolchain at all? The paper isn't arguing that people should use exceptions today. The paper is arguing that exceptions should be fixed so that they can be used in the future.
> Unless you're arguing that it's better to just always have a 5% performance tax & manual, error-prone error handling?
Exceptions today are untenable for a library author because you don't know which input data your callers will use, and therefore can't predict how often errors will be thrown.
Ideally we'd fix c++ with better manual propagation of errors and compiler warnings, like Rust or Zig.
Even if the global mutex is fixed, the single threaded perf numbers are terrible if you hit a bunch of unexpected errors. That seems unfixable.
I'd rather have 5% slower code, than 5% faster code with occasional 1000% slow downs (in the worst case).
> I'd rather have 5% slower code, than 5% faster code with occasional 1000% slow downs (in the worst case).
Perhaps, but that's a strawman through & through. C++ exceptions don't have that performance cliff when thrown, per both the paper in the OP & in your link. They are fairly cheap to throw assuming a single-threaded application.
That is, the fib example with 10% failure rate has the same performance using exceptions & using Boost::LEAF.
Unless you're calling it "unfixable" because there isn't actually anything to fix in the first place...
It's an exploration in scaling, how could that possibly have been a "mistake"? The paper isn't advocating for throwing an exception instead of returning NaN in the specific case of calculating a square root. It's instead only exploring the overhead & scaling of various error handling types.
They should be used when standard return value errors are measured to be impacting perf (ie. exceedingly rarely)
I disagree with this. I think they should be used whenever "standard" return value error handling starts to obfuscate the normal operation (happy flow) of the code. This impact on developer performance happens a lot sooner than the measurable machine performance.
Until getting the ? operator in Rust, I always thought I really liked exceptions. After all, what was the alternative? `if err != nil { return nil, err }` everywhere just to propagate errors up and probably ultimately have the process panic when there's an error? "How obnoxious", I thought; the vast majority of the time I'd rather crash and be restarted than attempt to recover and risk severely corrupting state.
Turns out I strongly prefer explicit errors over exceptions, I just also strongly prefer error handling not consuming 75% of the lines on my screen.
And in the cases where I really do prefer a panic, there's always `.unwrap()`
Turns out I need to read more carefully. Improvements can be done in a non-abi breaking way, but more radical changes are, according to the authors ABI breaking.
> How exceptional exceptions really are depends on the code being run.
Well yes, but EH is basically trying to be the best way to do a highly non-local goto with a dynamic target. If that part is your bottleneck than indeed you may have a more specialized requirement.
I do on occasion build a homemade version of a standard library datatype because I have a specialized need and only need implement the subset our code will call. That doesn't invalidate the more general implementations in the standard library (which tend to be quite good, even corner cases).
Likewise sometimes I check for a null pointer being returned or other local error flag. That doesn't mean I don't think exceptions are a good idea.
> The serialization of stack unwinding issue is a real serious problem.
That is the important part of the paper and as I wrote in my comment, I am disappointed that a possible solution, written and tested by the paper's author, was dismissed.
Th problem is that the corpus of deployed libraries is a de facto ABI. Even with dylib versioning, catching the case of API version is hard.
I think it could be done by changing the mangling algorithm (basically: compile fails if new-ABI versions are not available) but I haven't thought enough about it to remove the words "I think" from the beginning of the sentence.
Since this is about immutable "side car" data structures used during unwinding: could this be achieved using a soft ABI transition as opposed to a hard, flag day ABI break?
Basically, going forward the compiler would emit two versions of the unwinding tables by default: the legacy one and the one that allows fixing the thread contention issue. The unwinder is changed to use the efficient version by default with the ability to fall back to locking if it encounters a shared object that only has the old version.
Space conscious users could choose to emit only one version by using a compiler flag, and after a decade or so the compiler is switched to only emit the new version by default (but the unwinder could retain the capability to understand the old format for a few decades longer).
It seems like most of the table contents should even be shareable between versions? The locking seems to be only there to guard against shared object loading/unloading which should only affect the topmost level of the data structure.
Admittedly I have no idea how those tables are actually defined today.
It only took ~two decades to remove a.out support from the Linux kernel :)
(I wonder when software engineers will more commonly think about their work on a generational time scale. Like how the cathedral builders of old must have thought about their work...)
The problem is that in many practical situations you don’t know which situation is “exceptional”.
If you write a jpeg-processing service, it’s intuitive to raise an exception on a malformed jpeg, but there’s no guarantee that only 1% of the jpegs users upload to the service are malformed.
In other words, we treat exceptions as exceptions from our code expects, not what’s statistically unlikely in the input space, which is in many cases impossible to predict with accuracy. (E.g., even if only 1% of your inputs are malformed over all time, on some Thursday you may be hit with 80% bad inputs, making the performance drop across the service unacceptable.)
And it's one for which exceptions are a perfect fit. Even if at the entry point of your "parseJpeg()" function you instead return a variant<result, error>, internally propagating that error via exceptions is the "correct" design. Otherwise you're littering the code with branches, which makes the expected common path slower.
As a bonus not only is using exceptions for this faster, it's also less error prone. There's much less risk of any intermediate function or helper abstraction failing to propagate an error code properly.
Well, it would be if exceptions weren't avoided like the plague in C++ because they have a, well, bad implementation, which can't be meaningfully fixed without ABI breakges (as the paper covers)
My experience in using exceptions (and restartable conditions) over the last 40 years is that exceptions are for things you don't have the ability or "knowledge" (i.e. state) to handle locally.
So a function that ingests a file and processes it may throw an exception if the file isn't found so that the UI can catch it and ask the user for an alternative filename (or to give up and not open a file at all).
If you're connecting to a remote machine and don't get a response, you might throw an exception because you don't know if the user typed the name wrong.
While if you are already talking to a machine and it stops responding it's reasonable to wait a moment and retry, as if could be a transient network brown-out which is something you can deal with on your own.
When something happens that violates your assumptions about your own program's behavior, throwing it into a state where it doesn't know what happens next. Kind of like a panic.
This would mean that attempting to open a file that doesn't exist shouldn't throw an exception. But that is exactly what it does in the standard libraries of many languages with exceptions.
It should be up to the application to throw or not, not a library. I write a system service. If it can't find the configuration file, it can't continue, so it throws an exception. If it can't open a file that contains state from a previous run (maybe because it's the first time it's running) that's fine, the program can run without it and thus, no exception.
The C++ standard IO library doesn't enable exceptions by default, but IIRC that's just a relic of the fact that it dates back to when C++ didn't have exceptions.
This is a bit of a stretch. The purpose of the service has to be processing jpegs where they are expected to be bad. Moreover, they have to be mostly bad; i.e. the service is specifically intended for finding rare good jpegs in a deluge of bad ones, as quickly as possible.
In a situtaion where are more bad jpegs than good ones, we still don't necessarily care that they cause the slow path, if the purpose of the service is doing meaningful processing with good jpegs.
In C++ the convention is that exceptions shouldn't be used for things you expect to happen in the normal execution of the program, in a way that would harm performance. For example, it is better to explicitly check if an item is in a map than to rely on exception handling to branch to the case where the item doesn't exist. Generating an exception for FileNotFound would be fine for a single file selected by the user in a UI, but you'd probably avoid it if checking for the existence of a large number of files based on a pattern. Most exceptions should either be a bug, or exhaustion of resources.
This is in contrast to say python where the convention is to rely heavily on exceptions as part of the normal flow of the code, sometimes described as "asking forgiveness, not permission". It is not uncommon for a method argument to support multiple types, and to discern them by treating the object like one type and if you get an exception, then try treating it like another type. Likewise, if you're not sure an item is in a dict, you just try to access it, and catch the exception if it isn't. This has performance impacts, but so does everything else about python, so it isn't worth optimizing.
I'm not really a fan of this pattern in python. As far as I can tell, it's all done in the name of duck typing: if the returned object appears to have the right property, that's good enough. But the problem comes when take that object and pass it on to some other function. It may have appeared like a duck to you, but 10 functions later, it's slightly off and you get a difficult to understand TypeError or AttributeError.
Where I do think this pattern makes sense is in trying to use system resources. Checking that a file exists or a process is alive before deleting or killing it is a recipe for difficult to track down Time of Check to Time of Use bugs. I'm curious if you think this is also an anti-pattern in c++ and if so, how you properly deal with the TOCTU race conditions?
In C++ I would consider the best option in those cases would be for the function that performs the operation to return an error rather than throw an exception. That way you avoid the race condition, but also don't have to worry about the overhead of calling it in a tight loop.
If the library you are using does throw exceptions, then I would probably start with just using exception handling. Then if I notice poor performance, add an initial check, purely as an optimization, while keeping the exception handling to deal with race condition.
The reason they care called "exceptions" is they are not part of the normal behavior of the function (/block, algorithm) and aren't something that can be handled locally. Something that is exceptional is unusual, out of the typical scope of things. In English there is a phrase, "the exception to the rule" -- because the rule is what normally happens.
So if you are trying to hold a lock you don't throw an exception, you just wait and try again. Perhaps you can't reach that host; try again a few tiles before giving up and throwing an exception. But if you try to write to removable media and the device won't open, all, your program isn't going to mount a tape itself: throw an exception and let the problem be handled at a higher level.
In this aphorism, “prove” has an archaic meaning: “tests the rule”.
The point is that handling the host unreachable is almost always semantically higher level than the code opening the connection. This the code opening doesn’t implement the policy of what to do when the situation occurs. In the case, say, of a hard-coded address then it’s possible the author of that piece of code does know how to handle a host unreachable case.
Yes, this is said, but the meaning of "prove" (which is related to "probe") has not really changed very much at all compared to "test" - a word which in this sense is a metonym from the fired pot used for assay of metal. The meaning in "mathematical proof" or "prove beyond reasonable doubt" or "prove your love" doesn't really diverge significantly from this so-called archaic meaning - a demonstration to show.
On the software abstraction, exceptions are simply a convenient implicit control flow device for handling the alternate path when an intended state cannot be achieved. I don't follow the paper's argument that "current exception design is suboptimal for efficient implementation" - but I would not be surprised considering the lifetime issues.
> I don't follow the paper's argument that "current exception design is suboptimal for efficient implementation"…
The argument of the paper is really that exception handling shouldn’t use a global lock, a problem made worse by the proliferation of processor cores on modern chips.
Exceptional means "unusual" (by the dictionary definition). In theory, exceptions should be thrown in in exceptional (rare, unusual) circumstances. Things like running out of memory, or dealing with random poorly constructed data inputs are reasonable circumstances to use exceptions. However, if your input is consistently mangled, an exception may not be the appropriate way to handle it since it becomes a normal thing (if for no other reason than performance) depending on how you want to handle the problem and whether that performance cost is worth it.
The primary definition (OED) is "forming an exception", which is why I think this cliche is a tautology ("exceptions are for forming an exception") that does nothing to guide me on whether an exception is appropriate in a given case.
Btw, we don't say "exceptions are for infrequent conditions", because that's not what they're for.
I quite like the etymological "taken out", because it carries a notion of special handling - a control flow aspect that is the main point of using them.
It means that in normal usage conditions, if you install your software on a clean computer and run it and nothing weird happens outside of your program, then no exception should ever be thrown.
> Traditional C++ exceptions have two main problems:
> 1) the exceptions are allocated in dynamic memory because of inheritance and because of non-local constructs like std::current_exception. This prevents basic optimizations like transforming a throw into a goto, because other parts of the program should be able to see that dynamically allocated exception object. And it causes problems with throwing exceptions in out-of-memory situations.
> 2) exception unwinding is effectively single-threaded, because the table driven unwinder logic used by modern C++ compilers grabs a global mutex to protect the tables from concurrent changes. This has disastrous consequences for high core counts and makes exceptions nearly unusable on such machines.
That's really interesting. I've become very anti-exceptions in recent years far various much-discussed reasons (eg hard to follow, false economy, difficult if not impossible to write threadsafe C++ code in particular, use of exceptions as flow control is an anti-pattern).
One of the porposals is a value-or-error type object, which is basically what Rust has. I really like Rust's enums and match expressions.
It seems so difficult to make changes like this to C++ at this point, at what point do you just have to start again?
Yep, these are used extensively at Google (which is where the abseil library came from, and which is famously anti-cpp-exceptions) and they work very well. If I somehow found myself writing a new C++ project I'd probably reach for abseil (and some of the other parts of the Google toolchain: GoogleTest for testing, bazel for builds).
This is the great strength and weakness of C++. Increasingly the answer to C++'s rough edges is "We don't do things that way anymore. Everyone does X now", where X is the hot new thing. RAII is the best example I can think of, where some people insist that no one would ever use the "new" and "delete" keywords anymore. Except for all the C++ devs that do, and all the C++ code that exists that does and must be maintained.
It leads to the current situation where you have C++ "the language" which is everything, and then C++ "the subset that everyone uses" where that subset constantly changes with time and development context.
I think I agree with the sentiment but not the example, no one seriously advocates for no-new/no-delete (collections must still be written, somewhere) but rather that new/delete are generally code smell and there are idioms that can help isolate bugs. Part of maintaining old code is updating to those idioms.
But yea this kind of thing hit me recently on the interview circuit. I wrote some correct C++ (in that it was correct for the idioms in vogue when I last wrote C++ regularly for money) but I got feedback I wasn't as senior as they hoped due to my (lack of) C++ knowledge. Part of that was a shitty interviewer, but it's also just a fundamental part of the language. If you leave for a few years or try and change shops you find that everything under you has been removed or a completely different subset of the language is being used elsewhere. The complete lack of an ecosystem just reinforces that.
To be fair I imagine the same happening to someone coming to a Java interview still writing Java 8, or writing C# as if .NET Framework 4.8 is the latest version (C# 7.3).
I kinda look to when the thing finally stabilizes as a sign as to how bad the problem was. For instance, Javascript front end was a nightmare for a long time, but it seems to have finally stabilized into a reasonable stable configuration with a couple of winners, some minor specialized choices, and the endless churn is now a minor sideshow instead of something that is changing the default choice every six months. There was a bad problem there, but it seems to have been satisfactorily conquered for now. (I expect as static typing creeps ever more deeply into the JS ecosystem that at some point that may cross a critical point and cause some more churn, but at least for now things seem more stable.)
While C++'s churn frequency seems to be higher than the Javascript front end churn frequency, as an outsider, it still seems like "best practices" on C++ are churning around 1.5-2 years, it's been happening for my entire career, and it's still happening. If I seem a bit unsympathetic to the claims that the problems are solved if you just write your C++ code this way now, it's because I first heard that in 1998 or so, for a set of common practices now considered laughably out of date, of course.
At some point it becomes more cost-effective to just "churn" on to Rust next time, because even though in that same time frame Rust is a younger language that was going through its early design iteration phase it still seems like it has settled into a lower-frequency churn rate lately for "best practices" than C++.
There's probably some interesting and deeply profound reason why C++ just can't seem to stabilize across what is approaching an entire human generation, but I'm nowhere near interested enough in learning it to actually learn the amount of C++ it would take to find it.
> it still seems like it has settled into a lower-frequency churn rate lately for "best practices" than C++.
Rust is still adding a ton of new language features, especially around async, compile time code evaluation and the type system (const generics, GAT/HKT, existential types etc.). We'll very likely see further developments in more areas next, e.g. to match C++ developments in parallel and heterogenous compute (GPU's and the like), or to add forms of proof-carrying code, etc.
A lot of that is not what I mean by "churn". What I mean by "churn" is changes in best practice. Python has been adding a lot of features, but with the possible exception of static typing support, most of them haven't made many changes to what constitutes best practices. They might make "nicer ways to write that code" but the old styles haven't been deemed wrong. async is also not exactly what I mean; this allows new code to be written that mostly couldn't before. This one is only a partial miss though since it did deprecate some older libraries, but those libraries weren't really deemed "the right answer" either.
C++ is constantly changing what a "best practice" is. The latest hotness from three-generations-ago churn is now considered broken.
C++ gets new features that provide a better way to solve common coding problems. This is not accidental, and not a problem: the new features were added because they offer that better way.
Failing to use the new feature is just failing to write code the best way that is available right now. In 2013 you had no choice but to do it the old way; but you don't have to anymore, because the language and std library have caught up with you.
Being responsive to the needs of its community of programmers is job one for a language committee. If new, better ways don't arise, your language has stagnated.
You're right it's not accidental, but the problem is that when you continually add new features you end up creating a confusing mess. Yes, the new features may respond to some need in the community, but by adding it you've also:
- Introduced possibly unforeseen issues because features are never added in isolation; they interact with one another, and the more features you have the harder it is to test them all.
- Created confusion because now all the old information on the internet is out of date
- Everyone needs to update their tooling to support the new features.
- Make it harder for new people to start learning the language
> Failing to use the new feature is just failing to write code the best way that is available right now. In 2013 you had no choice but to do it the old way; but you don't have to anymore, because the language and std library have caught up with you.
This is a nice idea but it doesn't reflect reality. If the C++ user survey [0] is to be believed, a full 67% of users were restricted from using the latest C++ at the time (either fully or certain features).
> Being responsive to the needs of its community of programmers is job one for a language committee.
This is true but it also must be balanced against mission scope creep and design considerations. Being everything to everyone is not design.
People not using the latest Standard are waiting for support within their particular environment, not because they want to stay with a less performant version of the language.
People not using the latest Standard are waiting for support within their particular environment, not because they want to stay with a less performant version of the language.
> Failing to use the new feature is just failing to write code the best way that is available right now. In 2013 you had no choice but to do it the old way; but you don't have to anymore, because the language and std library have caught up with you.
What I hear you saying is that failing to use new features is a failure to write the best code available, and that before maybe you didn't have a choice but not anymore.
And yet, huge chunk of C++ devs are forbidden by their organizations from using various C++ features that have been added throughout the years. It's not just C++ 17 but it goes back even further. This is the whole point of TFA. So it's not just that people are "waiting" for tools to catch up, unless you have evidence of this.
Organizational inertia is a problem, but not a problem that a language or Standard can fix.
When an organizational logjam is broken, programmers can immediately switch over to the better, more modern way of coding enabled by the newer Standard they are allowed to use. If they were on C++98, and now they can use C++14, they are still better off than before, even if the current Standard is C++20: they will be able to code according to best practices for C++14, which are better than for C++98.
C++20 is adding a module system to C++. That's a major change to the compilation model.
It is also adding concepts, a major change in the way templates are to be written. The very article we're commenting on is discussing how exceptions should possibly be replaced by another error report mechanism, several proposals are in flight for this. There's also the "destructive moves" proposals that have the potential to change a lot how we write types.
A telltale sign that these changes are major is that the entire std has to be "modularized" to support modules, and to be modified to support concepts. Similarly if exceptions are to be revised a large chunk of exception using functions from the std would need to be modified.
On the Rust side, I think the only change that has even a comparable impact is const generics (and maybe specialization).
Existential types and GAT will change how to express some traits (allowing a LendingIterator for example), but I don't expect they will affect a large portion of Rust's std.
Also of note is that the Rust changes come to add new systems orthogonal to the existing features (const generics fill an obvious void compared to C++, same with GAT and existential types where Rust's expressivity is limited in comparison with C++ atm). By contrast in C++, the module system comes to replace the headers, and a change to exceptions would replace the current exception system, creating churn.
That's probably correct, I did not use concepts at the moment (stuck in C++14 right now). I certainly have my beefs with both features (modules that are orthogonal to namespace, no standard way to find modules, new and exciting ways of committing ODR violations, generally a complicated module system with quirks when there is so much prior art on modules in other languages, concepts being structural and not nominal, concepts being a "lower bound" on behavior and not an "upper bound" (thus not eliminating duck typing)), but my larger point is the scope of these changes to the language, not their (purported) benefit that I'm by and large unable to assess right now.
> it still seems like "best practices" on C++ are churning around 1.5-2 years, it's been happening for my entire career, and it's still happening
I have to disagree with this quite strongly. What "best practices" churn are you seeing? Your reference example of "don't use new & delete" (which I agree with) was a single best practice change that happened ~10 years ago. Similarly https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines is like 5-7 years old now, and afaik hasn't had any significant revisions?
The "churn" was really pre-C++11 to post-C++11. It was more like a python 3 moment than churn, other than it's taking a long, long time for code bases to catch up.
I think you're mostly right, but there is a bunch of fluctuation around just how much ugly template magic you're supposed to use vs. plain old imperative control flow (<algorithm>, ranges, I'm looking at you!)
Sure, but that in itself is also not an argument. The space of programming languages in general is moving forward and languages keep adding more (usually higher-level) features. C++ is mostly trying to keep up.
"I will contend that conceptual integrity is the most important consideration in system design. It is better to have a system omit certain anomalous features and improvements, but to reflect one set of design ideas, than to have one that contains many good but independent and uncoordinated ideas."
No, a language can stay current by evolving to fit its purpose. A good example of this is Matlab. Matlab is older than C++ and is still heavily used today, enough that it's one of the few remaining programming languages to support an actual business.
Matlab is not the same language it was in the 70s. But the core language is still largely about manipulating arrays and it does it well. It has evolved by adding functionality through toolboxes and creating new, innovative developer tooling. But it hasn't jumped on every PL bandwagon that has driven by over the last 50 years.
Matlab is one of the top 20 programming languages in the world still after 60 years according to the TIOBE index [0]. It has maintained a large user-base and achieved profitability over this time, not by adopting every PL trend that has come and gone, but by adapting to new developments while staying focused on its essence as a language. It proves you can stay current without doing what C++ is doing.
Matlab's usage is down a bit since a peak at 2017, but over the last 20 years it's up over 300%, and it's done so by being a for-profit language. Mathworks is a billion dollar company, which is quite an achievement in the PL space in 2022.
Meanwhile C++ usage is up over the last couple years but the long term trend has been a steady decline over the last 20 years [1]. From 14% to a low of 4%, now back up to around 8%.
Citing TIOBE instantly demonstrates a fatally bankrupt argument: changes in TIOBE ratings have essentially nothing to do with actual usage, or with anything else quantifiable.
TIOBE is statistical noise. You would equally meaningfully cite your tea leaves, or crows flying overhead.
Okay, well then I suppose you have a better citation for your unsupported assertion that Matlab has been in decline for many years. I've backed up most of my assertions with citations, I think it's time you brought some sources to the discussion. What is your basis for anything that you've been saying here?
I mean, if TIOBE was really statistical noise as your claim, there wouldn't be clear trends in industry reflected in the data, like the rise of Python in the last few years. And yet we see it, so clearly it's measuring something.
That is code for "it is evolving and I cannot be bothered to keep up".
All languages that are actually useful evolve. They get features that other people need, and you don't, just yet. Some of those features do turn out not to be perfect, because they are made by humans.
Comparing an old language to a new language, the new language will have many fewer of those both because it leaves many old things behind, and because it gets benefit of hindsight for the rest. Give it time, and it will accumulate "incoherence" of its own; the faster it evolves, the faster that happens.
The alternative is for a language not to be used. Then, it can stay pristine and "coherent", and not useful.
> "it is evolving and I cannot be bothered to keep up"
No, I actually teach C++ and have been keeping up with it for decades. It was the second language I learned in 1994 and I still code in it professionally today.
> Give it time, and it will accumulate "incoherence" of its own; the faster it evolves, the faster that happens.
Like I pointed out with Matlab, it’s a much older language compared to c++ yet is mostly coherent, far more than c++. This has less to do about C++’s age and the March of time or the human condition. Otherwise more old languages would be as incoherent as c++ yet that’s not the case.
Please leave the personal attacks out of this, thanks. I've said nothing personally against you and yet you've turned to calling into question my profession rather than the points I've raised. I understand maybe it may feel like I'm attacking you personally as I'm criticizing a language which I gather you are very fond of, but criticizing C++ is not criticizing you, and I would appreciate if you show me the same respect I've shown you. That's not what this site is for, and if you want to engage in that kind of back and forth I'd kindly decline.
My students give me high marks, my department (which includes faculty who have contributed to C++ spec over the years) is satisfied with my teaching, and I graduate students that go on to work at top companies and research labs around the world. I'm doing my job just fine, let's stick to talking about C++.
Adding features from literally every other language, just to create a mess - isn't really a selling point.
Right now, to learn C++ in a generic way, you need to learn pretty much all of programming paradigms and all of their variations - which is not a thing I would consider a plus.
>Right now, to learn C++ in a generic way, you need to learn pretty much all of programming paradigms and all of their variations - which is not a thing I would consider a plus.
I disagree, I would recommend learning enough where the tool starts providing value to YOU for YOUR problem domain and then pause/resume as needed. The main purpose of any programming language is to be productive in it and use it to solve a problem. In my opinion, there is no reason to learn more than you need about C++ unless you were a compiler author, on the C++ standards committee, or something similar.
"Generic," maybe, but in practice many people learn C++ as one of, as it were, two languages - one for application code developers and the other, for those who build libraries and provide APIs.
One of the reasons why I try to avoid C++ is that it's an unopinionated multi paradigm kitchen sink language.
There are great uses and great features, but there are so many of them and everyone has their own opinions.... Even in this thread there's a clear subset of people who "adore" C++ exceptions
So interesting, to me a language being opinionated is the main reason to put it on the do-not-use bin. I strongly believe that the best way to develop is through embedded domain-specific languages adapted to individual problems, and opinionated languages are always way too limiting regarding that.
It is not the job of a general-purpose language to insert its own opinions. That is the system designer's job. Fighting with the language's opinions is a recipe for failure.
When it's an embedded dsl you always have the escape hatch of the entire host language - and it is not shocking to use it: being in an eDSL does not mean that you have to agree to it religiously, it's always a case-by-case engineering tradeoff.
Whereas when in an opinionated language and you cannot do what you want... Ugly hacks such as people using bash scripts, mustache templates, etc ... to preprocess their code to get what they want quickly happen.
There's no such thing as an "unopinionated" kitchen sink language. Language features have all sorts of unforeseen interactions that must be handled somehow, and good high-level design is needed to ensure that the interactions are sensible.
That's not my point. The point is that this feature once was the hot new thing, and in the future there will be a hot new way to do the same thing in addition to all the old ways, because that's how C++ evolves.
And I would have to say the average C++ dev did not know about RAII in 1993.
For the sake of pedantry only: I don't think we called it RAII in 1993-1996, but the technique was in use in that period, though it wasn't standardized in any way. IIRC, Mac developers would have been widely exposed to it by Metrowerks PowerPlant during that time.
>It leads to the current situation where you have C++ "the language" which is everything, and then C++ "the subset that everyone uses" where that subset constantly changes with time and development context.
But what exactly is wrong with that? I don't quite understand your argument here..
They’re probably referring to preferring std::make_unique or std::make_shared to bare new/delete. Using either of the former makes the ownership semantics clear and avoids the need to remember to call delete at the appropriate time.
As well as smart pointers as others have mentioned, standard library containers (other than smart pointers) are another way to handle dynamic allocations in certain situations. The vector container is probably the best example here, it alone provides massive safety and usability benefits over using new or malloc directly!
I've been using `expected`, i.e. value-or-error type, for a while in C++ and it works just fine, but the article shows it has some noticeable overhead for the `fib` workload for instance. Not sure if the Rust implementation has a different design to make it perform better though.
> Not sure if the Rust implementation has a different design to make it perform better though.
Prolly not, I expect the issue comes from the increase in branches since a value-based error reporting has to branch on every function return. Even if the branch is predictible, it’s not free.
And fib() would be a worst-case scenario as it does very little per-call, the constant per-call overhead would be rather major.
It's also worth noting that Rust does also have stack-unwinding error propagation, in the form of `panic`/`catch_unwind`, which can be used as a less-ergonomic optimization in situations like this. Result types like this also don't color the function, since you can just explicitly panic, which would be inlined at the call site and show similar performance to C++ exceptions.
This is very much non-idiomatic. Panics are not intended as “application” error reporting, but rather as “programming” error.
The intended use-case of catch_unwind is to protect the wider program e.g. avoid breaking a threadpool worker or a scheduler on panic, or transmit the information cross threads or to collation tools like sentry.
Using up scarce branch-prediction slots is a good way to make your program unoptimizable. Time wasted because you ran out will not show up anywhere localized on your profile. (Likewise blowing any other cache.)
Using up BTB slots is an interesting problem but in practice doesn't seem to be a big issue. If it was, ISAs would use things like hinted branches but instead they've been taking them away. Code size is more important but hot/cold splitting can help there.
A problem with using exceptions instead is they defeat the return address prediction by unwinding the stack.
That hinted branches are not useful tells us nothing about the importance of branch predictor footprint. When hinting branches gives you a bigger L1 cache footprint, it has a high cost. Compilers nowadays use code motion to implement branch hinting, which does not burn L1 cache. (Maybe code motion is what you mean by "hot/cold splitting"?)
Anyway the hint we really need, no ISA has: "do not predict this branch". (We approximate that with constructs that generate a "cmov" instruction, which anyway is not going away.)
How does using exceptions defeat return address prediction? You are explicitly not returning, so any prediction would be wrong anyway. In the common case, you do return, and the predictor works fine.
> When hinting branches gives you a bigger L1 cache footprint, it has a high cost.
It was the same size on PPC, and on x86 using recommended branch directions (but not prefixes).
> Compilers nowadays use code motion to implement branch hinting, which does not burn L1 cache. (Maybe code motion is what you mean by "hot/cold splitting"?)
Hot/cold splitting is not just sinking unlikely basic blocks, it's when you move them to the end of the program entirely.
That doesn't hint branches anymore, though; Intel hasn't recommended any particular branch layout since 2006.
> How does using exceptions defeat return address prediction? You are explicitly not returning, so any prediction would be wrong anyway.
Anything that never returns is a mispredict there; most things return. What it does instead (read the DWARF tables, find the catch block, indirect jump) is harder to predict too since it has a lot of dependent memory reads.
It suffices, for cache footprint, for cold code to be on a different cache line, maybe 64 bytes away. For virtual memory footprint, being on another page suffices, ~4k away. Nothing benefits from being at the "end of the program".
Machines do still charge an extra cycle for branches taken vs. not, so it matters whether you expect to take it.
Negligibly few things never return; most of those abort. Performance of those absolutely does not matter.
Why should anyone care about predicting the catch block a throw will land in after all the right destructor calls have finished? We have already established that throwing costs multiple L3 cache misses, if not actual page faults.
> Nothing benefits from being at the "end of the program".
It's not about the benefit, that's just the easiest way to implement it - put it in a different TEXT section and let the linker move it.
Although, there is a popular desktop ARM CPU with 16KB pages.
> Machines do still charge an extra cycle for branches taken vs. not, so it matters whether you expect to take it.
Current generation CPUs can issue one taken branch or 2 not-taken branches in ~1 cycle (although strangely Zen2 couldn't), but yes it is better to be not taken iff not mispredicted. (https://www.agner.org/optimize/instruction_tables.pdf)
> Negligibly few things never return; most of those abort. Performance of those absolutely does not matter.
Throwing an exception isn't a return, nor longjmp/green threads/whatever. Sometimes they're called abnormal or non-local returns, but according to your C++ compiler your throwing function can be `noreturn`.
Error path performance is important since there are situations like network I/O where errors aren't at all unexpected. If you're writing a program you can just special case your hotter error paths, but if you're designing the language/OS/CPU under it then you have to make harder decisions.
> Why should anyone care about predicting the catch block a throw will land in after all the right destructor calls have finished? We have already established that throwing costs multiple L3 cache misses, if not actual page faults.
More prediction is always better. The earlier you can issue a cache miss the earlier you get it back.
For instance, that popular desktop ARM CPU can issue 600+ instructions at once (according to Anandtech). That's a lot of unnecessary stalls if you mispredict.
And so its vendor has their own language, presumably compatible with it, which doesn't support exceptions.
Specifically, a prediction is not better when it makes no difference. Then, it is worse, because it consumes a resource that would better be applied in a different place where it could make a difference.
Exceptions are the perfect example of a case where any prediction expenditure would be wasted; except predicting that no exception will be thrown. It is always better to predict no exception is thrown, because only the non-throwing case can benefit.
Running ahead and pre-computing results that would be thrown away if an exception is thrown is a pure win: With no exception, you are far ahead; with an exception, there is no useful work to be sped up, it is all just overhead anyway.
This is similar to a busy-wait: you are better off to have leaving the wait predicted, because that reduces your response latency, even though history predicts that you will continue waiting. This is why there is now a special busy-wait instruction that does not consume a branch prediction slot. (It also consumes no power, because it just sleeps until the cache line being watched shows an update.)
You don't have to start over with the whole language. Just use -fno-exceptions in your project and dictate the use of std::optional, or absl::StatusOr, or whatever your favorite variant return type may be. For the examples in the article, it may be perfectly fine to not support failure, to simply std::abort whenever the sqrt of a non-positive is requested and rename the function sqrt_or_die.
Right, I use an allocator that just aborts. Imagining your program can recover from alloc failures has always struck me as fanciful, or at least out of the realm of my experience.
It's a legitimate thing in embedded and other memory-constrained circumstances, when you have something like a large cache and an allocation failure can trigger manual pruning or GC.
Regarding maintainability ("hard-to-follow"), I’ve become a big fan of Java’s checked exceptions (and also their causal chaining and adding of "suppressed" exceptions, which would be nice to have for C++ destructors). I effectively see them as a sum type together with the return type, just using different syntax. It’s an important reason why I stick to the language, because no other language has that kind of statically-typed exceptions.
As the article explains, the problems in C++ are more an ABI issue than a programming language issue (except for the by-reference vs. by-value semantics). You could implement exceptions internally by variant-like return values, for example, similar to how error passing is done in Rust, while still having it look like exceptions on the language level. It would be fun for future languages and runtimes to more easily be able to switch the underlying mechanism, or possibly to be able to use different implementation mechanisms for different parts of a program as needed.
Java's checked exceptions are generally regarded as a mistake. There's a reason no other languages has them, and newer JVM languages (Groovy, Clojure, Scala, Kotlin) treat all exceptions as runtime. Anders Hejlsberg (creator of Delphi, C# and Typescript) also has an excellent article on their problems [1]. In modern Java I see nearly only runtime exceptions used, especially because that's necessary for most Java 8+ lambda API's.
Especially when used as an error ADT they're awful because they mix everything from "you have to handle this 90% of the time" to "the universe physics just changed, sorry" into one construct. Much better to use something like Vavr's Try and explicitly propagate the error as a value.
Most of the software I write is designed to be fault-tolerant, and checked exceptions are fantastic way of detecting potential faults. The problem is that checking is baked into the exception definition instead of its usage.
If I declare "throws NullPointerException", then this should mean I want it to be a checked exception. This should force the caller to catch the exception, or declare throwing it, or to simply throw it out again without declaring it. This would effectively convert the checked exception into an unchecked exception.
Converting a checked exception to an unchecked exception is possible in Java, and I do it whenever it makes the most sense instead of wrapping the exception, which is just a mess. Unfortunately, there's no way to make an unchecked exception behave as if it was a checked exception. It's easier to reduce fault tolerant checks then to add more in.
Some might argue that converting an exception to be unchecked is a bad idea, but this sort of thing is done all the time in JVM languages that are designed to interoperate with Java. If I call a Scala library from Java, and that library is doing I/O, then IOException has now effectively become unchecked.
The distinction some Java programmers make (including myself) is to treat RuntimeExceptions as indicating interface contract violations (usually preconditions), or more generally, bugs. That is, whenever a RuntimeException occurs, it means that either the caller or the callee has a bug. When existing APIs use RuntimeExceptions to indicate any other error condition, they are wrapped/converted into a checked exception ASAP.
I understand your point about usage-dependend checking. However, I believe it is mistaken. Consider a call chain A -> B -> C -> D, where D throws a checked exception of type X, which C converts into an unchecked exception (still type X). At the same time, B also calls other methods that happen to throw a checked X, and thus B throws a checked X itself, which now, unbeknownst to B, also includes the X from D. B documents the semantic of its X exception, which may not fit the ones thrown by D. Now A catches X, believing the semantics as documented by B, but actually also catches X from D, which was concealed (made unchecked) by C. This breaks checked exceptions in their role as part of an interface contract.
The fact that unchecked exception may originate from arbitrarily deep in the call chain is also the reason why they are unsuitable for defining interface contracts, A function declaring certain semantics for a particular unchecked exception can't realistically ensure those semantics if any function it calls itself must be assumed to also throw exceptions of that type (because it is unchecked). Effectively, you can't safely document "exception X means condition Y" for your function if any nested calls may also throw X for unknown reasons.
Any exception thrown in response to a bug is a fundamental design failure.
At the point where it is evident the program has a bug, there is nothing known, anymore, about the state of the program. It means that data structures defining the state are inconsistent to a wholly unknown degree, and any further computation depending on or,worse, altering that state can do no better than compound the failure.
I always delete code I find that tries to recover from a bug. It is impossible to recover from a bug; the best that can be done is to bail out as fast as possible.
The problem that you outlined is a different one than whether or not exceptions are checked. The further an exception is thrown (call chain depth), the more context that gets lost. This can be fixed with more specialized exception types.
My example of declaring an NPE as checked isn't something I would actually encourage. It conveys very little context, and exceptions which extend RE should signal a bug in the code. I should have selected a better example.
The problem is strongly tied to whether exceptions are checked if you want to have the type system support you in checking and validating your reasoning about the contracts between callers and callees. With unchecked exceptions, unless you have extreme discipline and document exception types for every function, and take care to use dedicated exception types for all non-bug exceptions, effectively emulating what the type-checker does for you with checked exceptions; unless you do all that, it's a fool's errand in terms of ensuring (being able to prove the correctness of) your interface contracts.
Side stepping that you're using a runtime NullPointerException as an example, Java's checked exceptions are just not particularly good at what you want to do.
If you have a function that has an abnormal result as its API, it should have that as part of its return value, because returning as values is what you do with results. Checked exceptions in contrast don't compose. See for example this code:
This is not allowed, because a the lambda in map can't throw. Even if that was allowed map would need a generic "throws Exception" as its API, which would be awful. With checked exceptions the only possibilities you have is catch the exception locally at the point of calling the function or stop as soon as you encounter the failure and bubble it up.
Instead, if you make the part of the return value you can do whatever. I'll use Vavr Try as an example, but you can also do this in Java 17 with sealed interfaces or in a miriad other ways.
Now you can still handle failure locally, but you can also check the whole list and make a decision based on that. Or you can propagate the results including failures because this is not the best place to handle them. That's what I mean with composes vs not composes.
I think the question is more on if the implementation of how the jvm does exceptions somehow less affected by core count?
That is, the checked part is just a language implementation, right? The jvm doesn't really make much of a distinction. (This is meant as a check to my assumption.)
Yes, that's right, as static type checks generally are. However, the C++ performance issues are unrelated to whether exceptions are statically and/or runtime checked. Furthermore, Java exceptions are not particularly efficient, in particular because they collect the current stack trace on creation by default, which is a relatively expensive operation.
I thought you could tune away the trace on creation behavior.
Regardless, I'd be interested in seeing if this is a performance bottleneck. I'd guess it is only relevant on dataset processing. Closer you are to a place that legitimately can toss to a user, more likely you are to not care?
That is, if the common case of an exception is to stop and ask for intervention, is this a concern at all?
> I thought you could tune away the trace on creation behavior.
You can when you implement your own exception type, but not in general (and doing so would break too many things).
Exceptions are thrown and caught quite frequently in Java for "expected" cases, for example when attempting to parse a number from a string which is not a valid number. It's generally not a performance problem, and stack trace collection is probably heavily optimized in the JVM. Nevertheless, it's certainly still a lot slower than C++ single-threaded exceptions. You have to realize that even a factor of 100 slower may be unnoticeable for many use cases, because so much else is going on in the application.
Java adopted a feature that was initially introduced in CLU, adopted by Mesa/Cedar, Modula-2+ and Modula-3, was being considered for ongoing ISO C++ standardization at the time.
Correct, but also pedantic and doesn't change the point. There's an implied "mainstream" or "currently serious contenders to start new projects in" adjective in "no other languages has them".
"Java's checked exceptions are generally regarded as a mistake."
By people who do not want to believe that errors are part of a system's API and prefer to just write the happy path and let any exception kill the process. And who don't mind getting called at 2:00 am because a dependency buried deep in a subsystem threw an exception that you'd never heard of before.
This is a really interesting and nuanced point here. The article linked above [1] talks a bit about it. The problem is, they both are and aren't part of the API in the strict sense.
In the sense that they specify a contractual behaviour they are part of the API of a function. But in the sense that they are something the caller should / needs to specifically care about, they sit in between. That is, in the vast majority of cases, the caller does not care specifically what exception occurred. Generally they want to clean up resources and pass the error up the chain. It is "exceptional" that a caller will react in a specific way to to a specific type of exception. So this is where Java goes wrong because it forces the fine grained exception handling into the client when the majority case (and preferred case generally) is the opposite. It makes you treat the minority case as the main case. There are ways to work around / deal with this but nearly all of them are bad. The article talks about some of the badness.
I do think it's interesting though that Rust has taken off and is generally admired for a very similar type of feature (compiler enforced memory safety). I am really curious how that will age, but so far it seems like it is holding up.
Errors are part of the system's API, but Java's checked exceptions aren't really that.
Case in point, the many checked exceptions in the Java standard library that are never, ever thrown. Ever. Such as ByteArrayOutputStream#close(). In fact the docs on that method even say it doesn't do anything, and won't throw an exception. But there's still a checked exception that you have to handle, because it came from the interface.
Which is part of why checked exceptions are a mistake. If Java wasn't so aggressively OOP, then maybe there's a good idea there. Like maybe checked exceptions in C++ would work, as you're not relying (as much) on inheritance to provide common functionality. But as soon as interfaces & anonymous types (eg, lambdas) enter the scene, it starts falling over.
Also in terms of API design, checked exceptions are quite limiting. Especially when working with asynchronous APIs, as checked exceptions are inherently coupled to synchronous calling conventions.
And there's also then the problem of not a whole lot of your code base is an "API surface" (hopefully anyway), and it's really vague how a middle layer should handle checked exceptions. Just propagate everything? But that's a maintenance disaster & leaks all sorts of implementation details. Just convert everything to a single type? Well now you can't catch specific errors as easily.
> By people who do not want to believe that errors are part of a system's API…
The point was that you should be returning errors, not throwing them. Runtime exceptions (null reference, division by zero, out of memory, etc.) ought to indicate a fatal error in the (sub)program or runtime environment. You can trap these, and report them, but it's usually a mistake to try to case-match on them. Unlike errors, which are predictable, enumerable elements of the design, runtime exceptions should be treated as an open set.
I disagree with this. But, I'm also a fan of the condition system in Common Lisp.
That is, if the problem is likely one that needs operator/user intervention, the non local semantics of exceptions makes a ton of sense. Indeed, it is useful to have a central handler of "things went wrong" in ways that is cumbersome if every place is responsible for that.
If you read the article by Anders Hejlsberg, he's not arguing against centralized handling of exceptions—the handling of runtime exceptions is expected to be centralized near the main program loop. That, however, is a general-purpose handler which won't have much logic related to any particular kind of exception; it just reports what happened and moves on. You don't need checked exceptions for that.
The condition system in Common Lisp (which I am also a fan of BTW) is designed around dealing with conditions when they occur, whereas most of the alternatives focus on the aftermath. In particular, conditions don't unwind the stack before running their handlers, which makes it possible to correct the issue and continue, though handlers can naturally choose to perform non-local returns instead. More to the point, there is no requirement to annotate Common Lisp functions with the conditions they may raise, which makes them more akin to unchecked exceptions.
Fair. Sounds like you are more claiming that most functions would be better returning a result type, but some will be better with more?
I view this as I want my engine to mostly just work. It may need to indicate "check engine" sometimes, though. And that, by necessity, has to be a side channel?
I think that is my ultimate dream. I want functions to have a side channel to the user/operator that is not necessarily in the main flow path. At large, I lean on metrics for this. But sometimes there are options. How do you put those options in, without being a burden for the main case where they are not relevant?
> I want functions to have a side channel to the user/operator that is not necessarily in the main flow path.
That is the essence of the Common Lisp condition system, and you can get there in most languages with closures, or at least function pointers, and exceptions or some other non-local return mechanism using a combination of callback functions for the conditions and unchecked exceptions for the default, unhandled case. The key is that you don't try to catch the exceptions, except at the top level where they are simply reported to the user. Instead you register your condition handler as a callback function so that it will be invoked to resolve the issue without unwinding the stack. It helps to have variables with dynamically-scoped values for this, though you can work around the absence of first-class support as long as you have thread-local storage.
C++ actually uses this model for its out-of-memory handling. You can register a callback with std::set_new_handler() to be invoked if memory allocation with `operator new` fails; if it returns then allocation is retried, and only if there is no handler is an exception thrown. Unfortunately this approach didn't really catch on in other areas.
I'm not sure callbacks alone can be equivalent to a resumable conditions system. You really need full coroutines in the general case. Anyway, what you are proposing is more of a partial alternative to exceptions, since the caller has to be aware of what's 'handled' in advance, whereas conditions may additionally unwind up to a predefined restart point or fail up to the caller similar to a non-handled exception.
> I'm not sure callbacks alone can be equivalent to a resumable conditions system.
I agree, but I was not relying solely on callbacks. You do need some form of non-local return (such as exceptions or continuations) to implement the "resumable" aspect with a choice of restart points, in addition to the callbacks.
> You really need full coroutines in the general case.
I'm having a hard time imagining an error-handling scenario that would require the full power of coroutines—in particular the ability to jump back into the condition handler after a restart. In any case, most languages (even C, if you work at it) can express coroutines in some form or another.
> Anyway, what you are proposing is more of a partial alternative to exceptions, since the caller has to be aware of what's 'handled' in advance, whereas conditions may additionally unwind up to a predefined restart point or fail up to the caller similar to a non-handled exception.
Clearly there has been a breakdown in communication, as I though this was exactly what I described. The handler callbacks are "ambient environment" (i.e. per-thread variables with dynamically-scoped values) so there is no particular need for the caller to be aware of them unless it wishes to alter the handling of a particular condition. Restart points can be implemented by (ab)using unchecked exceptions for control flow to unwind the stack, or more cleanly via continuations if the language supports them.
The compiler makes them part of the API. And what a lot of people do is just throw in a bunch of blanket catches with empty code. Although some of this is server vs. desktop software - the article was about complex GUI apps, not long running servers. Tho I personally think long-running servers shouldn't use exceptions. Each call to something out of the running code stack frame should explicitly decide what to do on failure or "didn't hear back." That's how your server gets to be bullet proof (and by bullet proof, I don't mean "auto-restarts on unhandled exception.")
Counterpoint: I do a lot of perf work on LibreOffice, which makes extensive use of exceptions, and I have never ever even seen the exception throwing show up on a profile, let alone become a problem.
I think this paper started with a conclusion, and worked backwards to justify it.
I am the the original author, and trust me, I am describing a real world problem. I run massive parallel data processing tasks on machines with 128 cores, and unfortunately some of them produce errors deep within the processing pipeline. From a programming perspective exceptions would be ideal for that scenario, but they cause severe performance problems.
Just think about this: If you have a 100 cores, and 1% of your tasks fail, one core is constantly unwinding. And due to the global lock you quickly get a queue of single threaded unwinding tasks. And things become worse, we expect to have machine with 256 cores soon, and there it is even more dangerous to throw.
If you do not believe me look here: http://wg21.link/p0709 It lists quite a few applications that explicitly forbid exceptions due to performance concerns.
The "exceptions should be exceptional aka rare" line sounds aspirational, but I wonder how true it is if you survey the general landscape of actual libraries.
On top of that, you never know the context in which your users will call your library.
Are you encoding a single file on a local machine? Or 1000 files on some HPC 128-core machine?
Do we really want to be in a world where all libraries need to be written with and without exceptions since we can't know the context they'll be used in?
Why is the error occurring in the first place? In my opinion, exceptions/errors should be rare. If it's a software problem, it should eventually be fixed. If it's an infrastructure problem, it should be fixed. It's not just something that should continue to happen without any mitigation what so ever over time.
Not all errors have "fixes". If I navigate to "https://news.ycombinator.com/nope" it returns an error to me. Is that a software problem or an infrastructure problem? And what's the fix? How do you "fix" a bad input to the system from an external actor?
So no, errors/exceptions are not rare depending on the context. So now do you use exceptions and accept that it doesn't scale across core counts? Or do you use return values and accept that it just makes all your code slower in all cases?
1% error rate sounds like a lot. But really how many HTTP errors do web servers return on average? Or other such systems? It's definitely more than 0%, and those services also scale nicely across multiple cores, so... Now we're just bikeshedding over the example numbers, not the core issue.
Probably not that rare to use exceptions but I imagine it is very rare to use exceptions for normal flow control - that is, to continue execution after an exception. It's a common pattern in Python but not C++.
I've done it exactly once and I ended up removing it because it makes debugging exceptions that you care about really annoying.
I agree with the premise - C++ exceptions are not a good choice for signalling numerical errors in high-performance computing environments, where said errors can happen with some frequency.
However, I do not agree with the conclusion - what is the difference between removing exceptions and simply not using them in your project? All compilers already let you compile code without exceptions and the product I work on compiles all numerical code that way. I don't see what benefit you expect to reap from changing the fundamental way C++ exceptions are implemented and work. At the high end, you want a custom error solution fit for your particular task, I don't think we can have a generic exception framework that will work for every high-performance computing project.
I've worked in low latency trading, and with large distributed back testing setups with hundreds of high core count machines, and not once felt the need to disable exceptions, or for that matter, felt the impact of them happening. A fair bit of noexcept stuff was useful to improve runtime performance, but that was about it.
I would suggest you need to address that 1% of failing tasks and determine what the issue is, as frankly, you are solving the wrong problem. If I might suggest a solution, it sounds like you are using threading when process farms might be a better solution.
(and if i'm way off the mark with my suggestion, apologies, i'm trying to help and have little information to work with).
Even reducing it to .1% we'll still have 1000 cores or at least threads before too long. GPUs already have thousands. Prosumer CPUs already have 128 threads. Epyc supports SMT, and will be 512 threads on dual socket Bergamo in 2023 I think on the roadmap. There is an intermediate at 384 threads dual socket Genoa in 2022.
Low latency is not the issue per se. It’s more like being able to guarantee a worst case upper bound runtime. Maybe your system can tolerate indeterminate but rare worst case run times but many useful systems cannot. E.g. flight control systems but also ideally any interactive system.
Well i'm now working in a hard realtime domain (audio, noone dies when it goes wrong, but it's still hard realtime) and i've still got exceptions turned on. But no exceptions are thrown during runtime, they're there for exceptions. Yes, there's overhead in places, but we noexcept some code paths to reduce that overhead in some very hot code paths.
If you allow exceptions in your real-time audio loop then when an exception occurs the only useful thing to do would be to take down the entire process. Handling exceptions as a norm in your audio loop would be unacceptable because it could never be done in a way that would always meet the deadline. In that case, if you cannot usefully handle the exception, why have your exceptional situation cause an exception in the first place? You’re just as well off calling abort().
> If you allow exceptions in your real-time audio loop then when an exception occurs the only useful thing to do would be to take down the entire process.
as a user of audio software, I would really prefer them to have the occasional glitch instead of crashing, potentially loosing work. Like, how it is even a discussion ?
Sure but that isn’t the issue. We’re discussing the architecture of an audio loop, e.g. handling exceptions as a norm in the codebase. If it is avoided then there is no large behavioral difference between calling abort() and throwing an exception.
... I hope I just don't understand. Say you have some audio node in your engine which at some point fails because of some error condition, for instance it tried to load a preset in an invalid format which was supposed to be sanitized beforehand, the preset tried to load a sample and the OS returned -ENOPERM, whatever.
Surely you agree that it's better to have this throw an exception, and a big try.. catch around your audio callback so that just this tick, which loads the preset, fails, and then the program continues normally at the next tick, just with the wrong preset for a node, rather than calling abort() which entirely kills the program and looses what the user was working on ?
> Surely you agree that it's better to have this throw an exception, and a big try.. catch around your audio callback so that just this tick
Yes I do agree but my point is that if any exception is being thrown for the sake of being handled then your Audio code is bad.
Generally your Audio loop should encounter no errors at all (it should be a pure process) so what the OP is saying is that excepting/aborting is fine in that case because it would truly be exceptional. I can’t imagine what the exceptional situation would be but to the extent one exists, I agree with that. My only point is that throwing an exception when there is no handler is no better than calling abort(), obviating the need for enabling exceptions.
> and audio handlers should never lock mutexes yet I have seen my fair share of them in the wild.
They are incorrect. Only bounded-time synchronization primitives can be used. E.g. lock-free queues.
> every thread or event loop should always have a catch-all handler at top level.
Two things. 1) There should be no reason for a properly coded real time audio loop to have a top level handler because exceptions should not be thrown because they cannot be guaranteed to be handled within the necessary time constraints.
2) top level exception handlers divorced from context cannot be handled, only ignored. Ignoring exceptions is very dangerous because there is generally no way for the top level handler to be sure that the rest of the program is in a correct state (unless it knows all code is exception-safe, which is rare). Continuing to run can result in data corruption. Exceptions leaving the context that has the information to handle them is a programming error, just like a failed assertion.
I'm not sure I understand the appeal of C++ exceptions. For years I've been told that Rust and Go's lack of advanced exception support is a crutch; and that error returns or sum types were unimaginative.
Now, in this thread, exceptions are supposed to be used rarely. I don't see the difference between an exception and Rust/Go's `panic`. If the error is rare enough, then chances are you cannot gracefully recover. If the error isn't rare; then why are you using exceptions for control flow?
Exceptions have always supposed to be used rarely - that's the point! The most obvious example is 'new' failing and returning std::bad_alloc. Given that new is implicit all over the place (pushing to a vector for example) the code would get very cluttered if you had to cope with this everywhere, so the pragmatic decision was taken to use exception handling to give apps that cared the chance to deal with them, and those that don't a sensible error message by default when the exception escapes main.
It's interesting that the lack of enforced exception handling was seen as a strength when exceptions were added to the language, compared to now when the lack of enforced exception handling is sometimes seen as a weakness. Programming language design and expectations are a moving target, so it'll be interesting to see how these things develop over time.
And yes, Rust and Go take a different approach. Is there right and wrong? No, i'd say just different approaches which will play to the strengths of some problem domains and coding styles.
> If the error is rare enough, then chances are you cannot gracefully recover.
that does not match my experience. Generally, rolling back to the state at the beginning of whatever user interaction caused the exception is good enough in a large amount of cases and a much better experience for end-users than abort()
Not only is this relevant to HPC environments, it also impacts the other end of the spectrum in the embedded space. When dealing with real-time requirements, you are much more concerned with the worst case performance as opposed to the average case or happy path performance.
This article makes it very clear that exceptions complicate analysis of performance. The non-local nature of exceptions mean I can't analyze performance in isolation. E.g. I can test that thread 1 always meets its deadlines (even with exceptions being thrown), then I can test that thread 2 always meets its deadlines.... but if thread 1 and thread 2 happen to throw exceptions at nearly the some time I might miss both deadlines. Who knows, maybe this means a thruster fires too long and that insertion burn fails ... and Mars has a new crater instead of a lander.
And, once you throw -fno-exceptions you are no longer using standard C++, which the standard library assumes. So, using anything that would throw exceptions on memory allocation failures is a no-go. You can work around this with extensive use of allocators (that reference enough static memory to avoid any possible out-of-memory situation)... but this is not looking like idiomatic C++ anymore, and most off-the-shelf libraries are unusable.
A completely local exceptions implementation (e.g. Herbceptions) would solve this.
Using local allocators where they offer some benefit certainly is idiomatic C++. Freeing all objects so allocated by simply reclaiming the blocks from a local allocator is also idiomatic C++.
Major subsystems that use no memory except what is passed in from above is also idiomatic C++.
C++ is a big tent. Things you do routinely in one part of a program, such as at startup, may be very different from what you do in a main loop, or in termination cleanup. Things my program does may be very different from what your program does.
> I am the the original author, and trust me, I am describing a real world problem.
for a more-or-less niche part of "real world". The overwhelming majority of desktop GUI apps rely on some C++ system - Qt, gtkmm, Wx, Blink, Gecko, FLTK, etc etc... and it is not an issue for those, for what exceptions are commonly used for (a write failing because the user disconnected the USB drive while it was copying, a system resource limit exhausted.. things like that). As much as massive parallel data processing tasks matter, I'd really prefer my language to not side-step writing end-user apps for something that happens at $bigcompany or $bigresearchlab.
If exceptions are regularly thrown the software has a bad design and must be fixed. Non-exceptional stuff must of course not be handled through exceptions - no exception should ever be thrown if the software operates as it is expected to.
You mention the problem of having to recompile for ABI-breaking improvements. Just out of curiosity - in the environment you're describing, how much of the code cannot be rebuilt? As I was reading I was assuming that most situations where exception handling causes performance issues are probably also situations where you have most if not all the source code available. This wasn't the case in my formative C++ years when 3rd party libs were expensive and distributed as binaries (on floppies).
> I run massive parallel data processing tasks on machines with 128 cores
So, not to be rude here, but this may be a real-world scenario in your world, but not in everyone else’s. I get the sense the kind of stuff you’re working on would benefit from things like using assembly as well. But for desktop apps, games, compilers, other command line tools… who cares about the performance of throwing exceptions?
All of this hinges around the global lock, although I'm not sure what you mean exactly. Are you talking about memory allocation, a lock that exception handling takes or something else?
It require application support, unfortunately, as there is currently no way that libunwind can figure out if a shared library has been added or removed. But if you are willing to indicate that from within the application the performance problem is mostly fixed. The memory allocation issue remains, but I can live with that. I cannot live with single threaded unwinding.
Thanks for that patch - I'll watch this with great interest.
Is there a problem if dynamic libraries invoke dlopen/dlclose they also need to call the sync function - correct?
I'm asking because we've developed a Common Lisp implementation that interoperates with C++ and it uses exception handling to unwind the stack (https://github.com/clasp-developers/clasp.git). We hit this global lock in unwinding problem a lot - it causes us a lot of grief.
That could make sense for processes, which would also get around the exception unwind lock, but I don't know how that would work with every thread moved into a separate VM.
If you want to avoid a global process lock, multiple processes will have less orchestration and communication overhead than multiple VMs (though still a lot more than necessary with threads within a process).
In 30 years of c++ I've never seen error checking + throw in a tight numeric intensive loop like this. Sure, somebody could do it. I'd have a chat with a coworker who wrote something like that.
I only use exceptions for cleanly unwinding the stack when an unrecoverable error occurs. I design and implement my code so that is rare.
Totally agree, I do the same, up to and including having a chat with a coworker who wrote code like that. I just did that last week, in fact.
Code that uses exceptions like this is a code smell, especially when performance is important. Using exceptions as control flow instead of if/loops is not a good design, IMO.
Note that this is in C++. I consider this style of writing not idiomatic, despite the STL doing it. I use either error return codes or std::optional (itself having a set of problems but IMO better than exceptions).
I'm more willing to accept this kind of code in Java, where it's more or less idiomatic. Less so in C#, where the tendency in the last 15 years has been to use the Try__ method instead of throwing exceptions.
I wouldn't necessarily expect to see throwing show up on a statistical profile. I would expect to see (and very much have seen) throwing showing up as a huge latency spike, leading to user-visible non-responsiveness. Especially in a cold situation (first throw after launch, or throwing after pages have been ejected), a surprising number of memory pages need to be populated to throw an exception.
When I read the title, I was assuming it was going to be about worsened code generation when building with exception support. Not about, basically, throwing exceptions as fast as possible. That seems an odd concern given the current performance.
Counter-conterpoint: I've worked on two applications C++ which we had to "de-exception", as exception handling was taking >25% of the time when we profiled.
You could argue we were using "too many exceptions", but to me it's the obvious way to unwind in C++.. except it's not fast enough, so we had to switch to a proto-Rust (this was before Rust) style system.
It probably depends a whole lot on whether exceptions are used only for exceptions, or for control-flow. The author seemed to be treating this question fairly pessimistically
My argument against exceptions is basically everything other than performance. To make a performance decision you have to build the code incrementally with feedback from how it is actually going to be used e.g. if almost every parse fails then you probably don't want to throw whereas if your code almost always succeeds you probably want your register back (i.e. use exceptions)
I feel like the main problem with exceptions isn't the performance, it's the non-local control flow and the fact that it makes enumerating all possible failure modes almost impossible. IMO the only way that exceptions could be justified (other than being the status quo) is if they were much faster than Result/Maybe types. Performance parity doesn't make them worthwhile!
That seems like an awfully short sighted argument. Less than 20 years ago we were all using single-core CPUs. Now most of us have at least 6 cores if not more in a battery powered handheld device that we don't even expect to do much compute work on (aka, smartphones). And 8-16 cores w/ SMT are not an unusual consumer laptop/desktop configuration.
The articles suggestion that it's only a matter of time for 128+ core CPUs to be common, at least in the server space, is probably the least contentious argument it makes?
128 cores is only 2x 64 cores, and 64 cores is a whopping 4x what AMD's top dog desktop CPU offers, so if you have to go this far for exceptions to be slow, then maybe they don't matter for most users.
Your math is a bit off, confusing threads & cores. 128 threads is only 4x of what AMD's top desktop CPU offers, and is what current AMD server CPUs offer in a single socket system. And over the past 20 years consumer CPUs have 32x'd their thread count. A 4x increase over the next 10+ years really doesn't seem all that far-fetched.
Also, the scaling problems show up far before 128 threads are used. The scaling issues start showing up at just 4-8 threads, depending on the error rate.
Also also, C++ is used in a hell of a lot more places than just consumer desktops. So the current limitations of consumer desktops is fully irrelevant anyway.
> The absolute biggest AMD Threadripper you can get only has 64.
You can get a machine with dual 64-core AMD EPYC 7662 CPUs[0] for a total of 128 cores. It will cost you almost $20k, though—for the most basic configuration.
I am glad more people are raising the issue with C++ exceptions. Unfortunately I don’t think the argument made in this article is compelling enough. Bjarne is against replacing the current model and has written an entire article responding to various criticisms of the current model. http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p194...
In particular he has already responded to the efficiency argument with the counter argument that current implementations can be optimized. Even if that optimization process breaks ABI compatibility, it’s still better than breaking source compatibility, which is usually what is being proposed. He is right about that so I don’t think efficiency arguments are going to sway him.
I used to embrace C++ exceptions until I had to use C++ in non-conventional environments. For me C++ exceptions are wholly inappropriate for real-time programming since it’s difficult to statically quantify how much time an exception handling sequence may take. There’s also the issue of it requiring malloc() which has its own issues from an interface standpoint in the real-time context. To avoid unbounded malloc, you’d have to set aside a per-thread area for exception storage and require that you never throw an exception value past a certain size.
Probably worth mentioning that while Bjarne is the creator of C++ and his opinion holds a lot of weight, he's also one of the few language creators who doesn't hold a BDFL hat (and hasn't held that kind of control for a very long time).
If the current model stays in place, then there would be no possibility to reconcile the de-facto fork of C++ into code disabling exceptions and code using it with the former can be running on more CPUs than the latter. In turn that leads to monstrosity like file system access API that try to have exception and exception-less versions of each function.
I find this analysis strange. Yes, C++ does need ergonomic ways to return an error without dynamic allocation (Rust's magic of ? combined with the From/Into traits is nice), but I don't know why you'd analyze the performance impact when you have many failures. If a failure is that common, then you aren't supposed to be using exceptions. It's not really an exceptional circumstance at that point.
That argument is only true in single threaded applications, at least given today's exception implementations. The more threads you have, the more problematic exceptions become. On the large machine you start to see performance problems with 0.1% failure rate, which is not that much. An core counts continue to rise.
Again, most experienced C++ programmers would say that even a 0.1% "normal" failure rate indicates a situation that should not be handled by exceptions. You seem to be describing a style of programming where there's a bunch of work to be done, most efforts to do the work will succeed, a few will predictably (albeit randomly, perhaps) fail. While I would concede that you might conclude that exceptions are the perfect tool to use to deal with the failures...
1) you're describing a fairly unusual application behavior
2) it's only because of the performance goals that the speed of exceptions matters
I'm the lead dev of a cross-platform DAW where our "inner loop" is real-time constrained by hardware. We use exceptions freely (in a multicore, multithreaded context) with no performance issues, because if they ever happen, things must stop.
If our system (algo trading) throws an exception, we collect core dumps from all threads, error messages go to all of our dashboards, and the system waits for a graceful restart. Exceptions almost always are due to something that should NEVER happen, and we will push out a fix ASAP. It is "exceptional" because we expect the call rate to be 0.00000%.
I keep reading how terrible exceptions are, and the examples are almost always in hot inner loops. If a junior dev put this code in a PR (a senior dev should NOT be doing this), we would require them to fix it.
Do you do that because that's how you want the language to work or because that's how the performance constraints required you to design your system?
Your argument seems to be that "well, yeah, of course exceptions should suck, you shouldn't ever use them" and just... being happy with that? The purpose of the paper is to say "hey, maybe exceptions shouldn't be a dumpster fire piece of hot garbage?"
I reserve exceptions for a really exceptional situations.
My motto is that if an exception happens, the program should crash since it's an unrecoverable error. That's just my opinion by I try to enforce that in my own codebase.
My main beef with exceptions is that sneak past the type system. Unlike Java with its exception specification, in C++ we have no idea what to catch. Yes, there's documentation, but it's often hard to keep it in sync with the code. I've been bitten many times in the last couple of years by surprising exceptions jumping from deep inside innocent function calls, either because of a deep dependency or because someone just threw one and didn't document it (that someone was also a younger me on occasion...)
So my design criteria for exception is, is this error here unrecoverable for the application?
The 2 examples in OP's paper are 2 functions, and we have no context. If they were a part, say, of a console application that was used in a nightly cron job, and there was no user to ask for proper input, then yes, maybe crashing the application is acceptable. I would still have liked to print something to a log.
If this was a part of an interactive program, then a proper error code / std::optional / other error object about the illegal value and its place in the array should be returned, with a proper error message displayed to the user about logged. This I'd do by having a top-level catch handler. A single one for all the application.
Why am I "not supposed to use exceptions"? I don't understand why exception should be used when errors occur occasionally, or when they occur 50,000/sec. They are a control flow method for when errors occur.
I'd add a (major) inconvenience is problem investigation, often exceptions point you to root cause and all you need it to break on the first exception (think after running big codebase, for an 1h just to get where problem is), but if they are thrown willy-nilly this debugging strategy is MUCH less convenient...
For the same reason you may want to use or not use memory allocations when doing something 1 time per second or 1 million times per second. Performance is not amenable to arguments about degree (especially when talking about this many orders of magnitude in degree).
But again, the question is whether exceptions need to be slow.
If I'm writing a string_to_int function today I would return an optional<int> (or some Either variant). But if exceptions were cheap I would use them. If I'm catching an exception in the context of immediate caller and the compiler inlines the calle, it ought to be able to optimise the exceptional path away. But it doesn't happen.
> . They are a control flow method for when errors occur.
no, not any errors, exceptional errors. Input data coming from "outside" being invalid is not exceptional. The filesystem suddenly becoming inaccessible during a copying or caching operation, the OS not being able to join a thread, your logging backend becoming unable to log because the disk is full, or a regular expression being incorrect are what comes to mind when I think of exceptional situations (unless your program allows users to input regexps). If your program runs on a normal computer in normal conditions, no exceptions should ever be thrown.
> I don't know why you'd analyze the performance impact when you have many failures. If a failure is that common, then you aren't supposed to be using exceptions.
The problem is in that case you get into a probabilistic estimation, which is a nice way to say you roll the dice on your performances: what’s the ratio at which exceptions are too costly or sufficiently cheap? And how does that impact your level of service if e.g. exceptions are extremely rare but they tend to all affect the same request or workload?
If the program fails too often or too unfairly, why should you blame how such failures are handled? The only reasonable "probabilistic estimation" is that something that happens 10% of the time is normal and it shouldn't be treated as an "exception", even if actually thrown exceptions were fast.
> If the program fails too often or too unfairly, why should you blame how such failures are handled?
Because the discussion is about methods of reporting and handling failure?
> The only reasonable "probabilistic estimation" is that something that happens 10% of the time is normal and it shouldn't be treated as an "exception"
That makes no sense whatsoever, and doesn't address the question.
And even if a ctor fails at a rate of 0.9, it has to report errors via exceptions, because that's the only mechanism available to ctors.
You could use an output pointer parameter. You could construct an object which self-reports as invalid. You could take an error handler callback. You could use a global variable.
Or I think my preference would be a private constructor, and a friend-function that returns value-or-error.
Adding to that once exceptions start being thrown, that ratio changes because the cost is so high. It's not hard for a service to reach a failure rate just high enough that it overwhelms the system.
Another problematic area is RTTI and dynamic_cast. Chromium disables both that and exceptions. As a replacement in few places where dynamic_cast can be useful Chromium adds a virtual function to the base class to return the subclass or null.
Chromium code replaces exceptions with boolean flags and logging or code to kill the current process for bad cases like out-of-bound access.
Why do we need to have ambient control flow? This is what exception handling is, it's a hidden control flow.
I don't use them. I just create an error type and pass that around.
The only legitimate exception I will accept is when you access invalid memory. That's a special case and depending on the environment something extraordinary must happen.
But exceptions and exception handling just creates annoying code. It doesn't add value, not really.
1. They generate syntactic noise at every point they touch the call graph: function signatures, calls, returns.
2. In particular, if a change causes a deeply nested function that used to always succeed to be able to error, the entire path up the call graph needs to get Resultified.
3. Since the caller must be aware of them, generic code generally has to be Result-aware too.
4. They aren't a total solution, exceptions usually have to exist anyway (eg panic), for things like oom/assert/etc. So you're usually paying the cost of them anyway.
5. You only get what the callee gives you. With exceptions, you can get a backtrace to the cause of the error by default, with no effort needed on the part of the callee.
> 2. In particular, if a change causes a deeply nested function that used to always succeed to be able to error, the entire path up the call graph needs to get Resultified.
This is a pro not a con. It now shows clearly that all these function calls are now failable. Of course at higher levels you may have additional assumptions that you know won't make the low-level functions fail, and you are welcome not to change everything as well.
> 4. They aren't a total solution, exceptions usually have to exist anyway (eg panic), for things like oom/assert/etc. So you're usually paying the cost of them anyway.
Panics do not need to be caught and handled. You can (and should) transform panics into abort.
"For a real-world example of panic and recover, see the json package from the Go standard library. It encodes an interface with a set of recursive functions. If an error occurs when traversing the value, panic is called to unwind the stack to the top-level function call, which recovers from the panic and returns an appropriate error value (see the ‘error’ and ‘marshal’ methods of the encodeState type in encode.go)."
> They generate syntactic noise at every point they touch the call graph: function signatures, calls, returns.
This is not "noise" but needed information. A function that can error out should not have the same signature as one that will never return an error. Similarly, call-site special syntax (like '?' in Rust) helps address the concerns raised by hidden control flow.
> Since the caller must be aware of them, generic code generally has to be Result-aware too.
True, but the Rust standard library includes a zero type that can be used to mark a Result-aware function as infallible, making it easy to wrap it with a non-Result type.
That is incredibly domain-dependent. In most general business processing, that information is just noise.
Building a web app? 99.9% of the time, you let exceptions get caught by the http server and return 500 to the client. In a few rare cases where you want to do something else, you catch. If you don't catch, the client still gets 500 - a perfectly acceptable fallback.
Building a GUI app? 99.9% of the time, exceptions in the UI loop should just display an error message to the user in a modal dialog and then get ignored. Sure, you can do something else, but the error dialog is a reasonable fallback.
There is no good reason to torture the whole call stack to accommodate these problem domains.
> A function that can error out should not have the same signature as one that will never return an error.
Functions that never fail are pretty rare compared to ones that do. It's easier to just assume that all functions can fail. Mentally it's a much simpler model.
This post is the saddest thing I have ever read in computing. So much power comes about (on the CPU, in the call stack) by being able to assume that things proceed with mathematical tractable properties, as computable functions, plain old general recursion. Distributed things, sure, you have to have protocols and so one to regain the determinism, and even then perfection is no longer achievable, but within your big FSM of CPU/memory/disk, it's all pre-calculated in some platonic ideal of computable, recursively enumerable Platonic world of simplicity and knowability about which we can reason.
One errant cosmic particle and all determinism goes out the door as well.
The story of computing is not one of mathematics, it's of people and it's of change. Software is about codifying decisions and without perfect knowledge of the universe those decisions are always going to be wrong in some way. And that's even without introducing the infallibility of programmers. Errors are simply part of the process.
ECC and it should be a few cosmic rays :0) but I wouldn't try to write code to work with random bit changes, but implement distributed protocols that can handle a bit of hostility in the data center. Although when the people calling your libraries are bad enough in your call chain, I have found it worth changing the opaque void * from a pointer to a hash value that you look up in an "allocated" structures hash and can return an error rather than dereferencing when they call stuff 5 minutes after calling "de-alloc." (To stop complaints from people calling de-alloc, then calling "do-something" then crashing then blaming my code because I de-ref the opaque pointer.
I agree that functions should specify in their signature wether and how they fail, but checked exceptions can do that. Additional call site syntax is indeed just noise. I strongly agree with David Abrahams[1] on this. A better solution would be noexcept regions were the compiler would statically guarantee that they can't be left via exceptional control flow.
"noexcept" regions would be even noisier than the established pattern of non-fallible calls as the default and some lightweight syntax (such as '?' in Rust) to indicate fallibility. Sure, if literally all calls were fallible the '?' or equivalent would be redundant to call syntax, but everyone knows that this is not the case. And it's important that the failure-prone case be acknowledged as such.
Code that is error-safe is so rare. Why adopt a pattern that elevates the normal case ("here be errors") to information you have to disclose at every turn?
Oh my, this sentiment is common. Errors can't happen inside a Turing machine. The errors are just when you step outside the process to interact with externalities. Computations should be thought of as having errors in it them, any more than the integers do. Network or disk calls maybe have all sorts of things happen. You have to think about the failures whenever you have succumbed to reaching outside of your call stack for answers. Because those remote or shared by other processes resources might not reach the same level of certainty, and your perfect code might or might not need the answers or be able to work around their lack.
An FYI that Monads are useful for removing that (left, right) boilerplate from composed functions. This was the issue I had where they finally clicked for me.
Once you are using monads you are now doing the thing that person hates, though: removing the control flow from the code (as it got moved into the monad); really the syntax for exceptions is nothing more than a hardcoded-into-the-language error monad.
Exceptions come naturally from the realization that you mostly have to propagate errors to where they can be suitably handled or logged. And all that propagation code heavily detracts from the meaning of the code when you are writing it or reading it. And messing up the propagation is a common source of issues (historically).
With "exceptional" errors are are only 2 real recovery options: restart the operation or terminate the operation. Neither of these are typically decided on anywhere where an error might occur in the call stack.
I also have to imagine there's a decent performance boost. With Rust's and Go's (and C's, usually) approach of having 'if (error) { return error; }' all over the place (with syntax sugar or not), there will be a lot of extra branches in the happy path. Sure, those branches are predictable and thus fast, but they're not instant, and they take up icache space in the happy path. Modern exception implementations can almost exclusively slow down the exceptional code, and code which propagates exceptions without catching or throwing will look identical to code with no error handling at all.
I'm sure the gains aren't tremendous, but lots of C++ design decisions are for slight performance improvements at the cost of less safety. Other examples are unchecked array access by default and unchecked overflow. Whether these are the right decisions or not is debatable, but at least it's consistent.
EDIT: Of course, as the article points out, if you have a high error frequency and many cores, exceptions cause significant performance issues. But in the case where exceptions are _actually_ exceptional, and especially in cases where the only real response to an exception is to log an error and exit, exceptions are exceptionally good (pardon the pun) from a performance perspective.
Not all errors are "exceptional". For example, consider an API that lets you open a file (local or remote); it may be very common that a file doesn't exist and it may be natural to handle that case by checking a "file not found" error (checking if the file exists before accessing it incurs in an extra cost and it's also racy).
> Exceptions come naturally from the realization that you mostly have to propagate errors to where they can be suitably handled or logged.
I agree of course that errors must be handled and logged as appropriate, but the implication that this has to happen by popping from the call stack is not justified at all.
Logging can be done but I can't see how you could handle errors.
If I'm processing 10 transactions and transaction 8 fails, needs to be retried a couple of times before being skipped for 9, I don't know how you'd do that other than going up the stack to where you're looping over the transaction list.
Not everything is a transaction; and not all transactions are processed serially in non-overlappend time slices. Most systems will process multiple pieces of work concurrently (and can't just throw a new OS thread at each of them).
The implication is that you can't just back out of a call stack like in a quickly hacked-up script. Stack frames aren't containers for whole transactions. Rather, pieces of work are tracked in explicitly allocated data structures, and worked on during more than 1 function call. The exception model is not compatible with this.
One of the core problems is C++’s design basically requires it: there’s no other way to error from a ctor, and since ctors are used as hooks in many operations the dishonest rejoinder of “just use a factory” doesn’t work in any capacity.
Doesn’t some of the reliance on constructors for everything come from how constness is fetishized in C++? Not that it's all bad to have those checks in place, but Python doesn't have this issue with out of control constructors in part because (almost) everything in Python is just unapologetically mutable.
You could potentially argue that, but certainly Rust doesn't encounter this issue despite being const by default because they simply don't have constructors in the first place.
Yeah I knew I was going to get called out with a Rust comparison. I think the Rust approach basically acknowledges the issue with what C++ did— that automatic initialization is cute but ultimately wasn't worth what it ended up costing in terms of hidden control flow, poor error handling, static initialization issues, etc.
Anyway, Rust basically deals with it by giving the class designer the choice to supply factory functions or punt on it, making the user initialize every field themselves each time. And I think most agree that this is a good approach; it's the best of C++ (factories) with a better fallback than a default constructor.
True, but the naming does matter. Calling Thing::default() clearly communicates that your just getting baseline values and not a lot of magical other initialization stuff going on— with a Thing::Thing() in C++, you're really at the mercy of whatever the project conventions are for how "fat" the constructor is going to be.
I think the naming is also important for cases where there are potentially multiple reasonable defaults, even something as basic as the difference between Vector3::Vector3() and Vector3::zero().
> Calling Thing::default() clearly communicates that your just getting baseline values and not a lot of magical other initialization stuff going on— with a Thing::Thing() in C++, you're really at the mercy of whatever the project conventions are for how "fat" the constructor is going to be.
In C++ the constructor without arguments is called default constructor.
Of course the expectations depend on conventions, but usually it's something from uninitialized garbage to an empty state.
Sure, but the point is that providing factories requires extra work and consideration, so a lot of C++ classes instead lean on the default option of a constructor. Rust's removal of that forces the class designer to choose.
This is incorrect. You are not supposed to make any assumptions about the state of an object that has been destructed or moved from. A “dead state” would still be a state, whereas a “dead object” should be thought of as having no state at all, i.e. not being an object any longer.
Because, properly-used, it saves you a lot of effort.
Returning an error type unwinds the stack. If I have a low-level computation that has a number of error cases, and I want the high-level code to intelligently handle some of those error cases and continue processing, that's not doable using return values. Error codes bind the decision of which error-recovery strategy to take (which is only present at a high level) with the details of that strategy (which is only present at a low level).
As a trivial, obviously-fake example - if I have a high-level GUI library that makes use of a low-level division function, I might occasionally divide by zero. Depending on what the GUI library was doing, I might want my divideBy(x, y) function to return 0, 1, the first argument, the second argument, or not return anything because that section of the code will be completely aborted.
Without ambient control flow, if you just have return values, you have to check the return value for every single division operation you perform - and you'll have to re-implement code paths where a division operation failed.
What if you have a long-running operation? If you return an error value when that operation happens, but it turns out the nature of the operation allows you to ignore that error and continue, you'll have to re-start the entire computation. If you hard-code the lower-level logic to ignore errors and continue, then you'll also end up ignoring errors that you really shouldn't.
Error types do not solve these problems - in fact, they require that you duplicate lots of code to, say, make multiple variants of a library that are identical except when it comes to error-handling - or just cause your applications to lose lots of error-handling nuance and bail early on lots of exceptional circumstances that could be recovered from.
> What if you have a long-running operation? If you return an error value when that operation happens, but it turns out the nature of the operation allows you to ignore that error and continue, you'll have to re-start the entire computation.
This is just as much of a problem with exceptions. "Fix the error and continue" needs some equivalent to resumable conditions, which are generally implemented at the "low level" using coroutines. My understanding is that async-await as a language feature might be able to express these with relative ease, but exceptions alone clearly do not suffice.
I didn't actually say the word "exceptions" anywhere in my comment. That's because I agree that exceptions are not enough - but the solution that is enough, namely conditions and restarts, also requires "ambient control flow" to use the language of the parent. I was arguing for that and against return values, not for exceptions.
There are some kinds of errors that can’t be handled locally, but do need to be handled globally, or generically higher in the call chain. Continuing execution after the error occurs will make the problem worse. Exceptions allow you to cease execution without putting an if statement after every function call.
Haskell's IO monad actually has excellent exception support, including async exceptions and masking them in critical sections.
There are `Maybe` and `Either` and they're great at streamlining error handling in pure code, but when it comes to IO most libraries just throw exceptions (including the standard library).
One of the motivating examples for exceptions, at the time when they were beginning to appear in mainstream languages like Ada, was to allow people to write arithmetic expressions using familiar notation, while still having a place to put an error handler for the overflow case.
Perhaps the lesson of the last forty years or so is that this convenience wasn't worth adding such a heavyweight feature to the language, but it seems to me that modern languages are still weak at handling overflow.
Ada exceptions are fundamentally different from C++ since they only carry type data with a possible message and no other user-defined data.
Old school C++ used to have additional try/catch blocks (and performance hit) inserted to ensure that functions match the given exception signature. Also, C++ usually focuses on performance, so all of the bookkeeping required for exceptions could historically be the last 3% or so difference between making your FPS rate or not.
A lot of C++ developers outside of games don't use exceptions. It introduces a latent and hidden goto up the call chain in your code, and require additional bookkeeping by the program which slows things down. They're also dangerous when you update code because if you introduce a new exception in a function you need to update all of the call-sites. When doing this with monads or sum types, this refactor can be supported by the compiler as a function signature change.
It boggles my mind that Ada decided to include them. Unless you go through the entire call chain or use SPARK, you don't know for certain that one of the called subprograms won't throw. Even then, you could still get a `Storage_Error` or `Program_Error` for various reasons.
Unfortunely C++ suffers from having people that are so crazy about performance, yet never bother to learn how to use a profiler.
That is what boggles my mind, how one can be writing C++ as if they were fitting Assembly into 8 bit home computers, but never bother to learn how to use something like V-Tune.
By the way, there are other languages besides Ada with exceptions used for systems programming, and if they aren't as big as C++ it is only due to how market adoption has evolved, which had lots of factors not "does the language do exceptions".
I do work in software performance. The people I know who don't use exceptions work at HFT firms and in games, but in general, my point is that there's two reasons for not using exceptions:
1. performance
2. not wanting jumps or hidden control flow
Note that with exceptions you're forced to consider C++ exception guarantees for code, which can result in convoluting your code to ensure specific guarantees are met (like using swap to ensure that containers retain previous values), which often gets ignored or implemented incorrectly. Also, with exception specifications being deprecated, you have to rely on documentation or reading code to understand what exceptions might be inherent in other code.
Exceptions are a big issue in Javascript, though mostly because they're absolutely terrible.
Whether to use exceptions or not is a common question in e.g. C#, and several APIs are duplicated to have both exception-based and values-based variants. And Python is oft criticised for using exceptions more than once every blue moon.
It is a big difference to offer both kinds of APIs, or to make endless flamewars with compiler runtime forks, which is what disabling exceptions and RTTI mean in practice, a fork from ISO C++.
> The only legitimate exception I will accept is when you access invalid memory.
If all memory accesses were done with a function call, e.g. read(void *addr), then by your logic there would be no legitimate need for exceptions.
If you extrapolate into the other direction, exceptions are convenient from a syntactic POV because it avoids littering every operation with an explicit error return mechanism.
I always avoid C++ exceptions, and also try to avoid dynamic memory allocations, smart pointers and RTTI etc whenever possible. This is pretty common in latency and (pseudo) real-time performance critical work, such as robotics control and 3d gaming.
Memory allocation is the big think that new players often miss.
A lot of time I see people saying D is unusable (excitedly) for a certain usecase, and you know what if you use the GC a lot it might be, but then I then see them write code that uses raw malloc and free willy nilly. GC can bite you in the ass, but so will basically all memory allocation if you don't understand the real trends of your program i.e. "Nature is a language, can't you read?"
> (pseudo) real-time performance critical work, such as robotics control and 3d gaming.
In reality all interactive applications are real-time performance critical. Any normal user would say that an unresponsive application is unacceptable. Sadly the entire stack of our contemporary desktop runtime environments grew out of background batch processing systems. This means that an application becoming unresponsive can be a sign of normal system operation.
Even if you avoid malloc, any memory access can trigger swapping which can block your main thread indeterminately. Or if you are running a high enough amount of CPU-hungry processes concurrently, your main thread can block indeterminately.
Millions of programmers carry on building applications for these systems blissfully unaware of these fundamental flaws. Application runtimes like Electron flourish, riddled with thousands of unbounded operations on every mouse click.
Way too many people believe the difference between malloc/new+free/delete vs a GC is that one is deterministic and the other isn't.
(It doesn't help that textbooks still teach this!)
Both are subject to the whims of the system's memory and, if swap is enabled, IO systems.
And unless you understand your call graph very well, constructing an object that constructs other objects is not something you are likely to be capable of calculating the performance of in a non-GC language.
Same goes for destructors.
The actual difference is that in language without GC's typically have explicit syntax for dynamic memory allocation (though it can happen by surprise in C++ from time to time!), which means if you are writing latency sensitive code, you can just avoid dynamic allocation altogether.
But there is no reason why GC languages can't do the same! In fact, newer GC languages sometimes do, and nowadays C# give you more control over stack allocation, so careful C# coding can also avoid using dynamic memory.
If you need any sort of numerical programming, C++ is basically unmatched. It's fast and efficient (have total control over memory and heap allocations), while able to create complex math abstractions thanks to its template system and operator overloading.
For example, Eigen is the only library (not in C++, but in the entire programming space) that can optimize your math expressions at compile-time. You can also perform techniques like auto-differentiation by using templates. Meanwhile, Rust still doesn't have full const generics, which are needed to create a math library that's both efficient and easy to use (nalgebra still depends on typenum, which is much uglier than even the most esoteric C++ template stuff you can find!)
Agreed. Eigen can be relatively slow: in our Tiny Differentiable Simulator, with C++ templatized code, we generate C code using CppAdCodegen (by tracing a sim step) which is easily 5 times faster than the original Eigen code. And together with openmp it runs very fast: Ant OpenAI gym simulation at 2 million steps per second on an AMD Ryzen 3900x at 12 cores/24 threads).
The D library "Mir GLAS" also does the eigen style optimizations, IIRC.
It's not unique to C++ but yes Eigen is by far and away the most mature option in that space. D templates are significantly better than C++ ones in basically every way, but the work is already done in C++ so I can't be too boastful
There are other as performant languages out there that have a "small" footprint (C, Rust, Zig, etc.), but the combo with the libraries heavily used in robotics, mostly for image and mathematics computation, makes it hard to look away from it: C++ standard library, OpenCV, Eigen, Boost, PCL, ROS.
There may come a day when there is enough momentum and support to look elsewhere, but right now C++ is king in this domain. There is a lot of Python going on too, but usually not for the same type of things.
I work in the same domains, plus hard embedded real-time. Some projects end up choosing C, some C++. C has the advantage of simplicity and more mature tooling, C++ has the advantage of a stronger type system (able to move some things from correct-by-testing to correct-by-construction) and a medium-weight code generation system in the form of templates.
C is fine too. I pick a C++ subset that is closer to C than to full modern C++17. Full control over memory access, hardware access and execution. I haven't learned enough Rust yet, that could be fine too, but Rust lacks still many libraries in Robotics and Game development.
In other words, you pick exclusively the worst features of C++, and ignore all the actually useful, helpful features that enable you to write better code. If you just want C, use C.
"Modern C++ bad" is such a weird HN meme, fortunately it's not an opinion I see a lot elsewhere. Old C++ was awful, the new stuff makes it actually usable.
Personally I like how even in embedded code where you don't want to include the standard library, you can still efficiently use language constructs like constexpr, lambdas, etc.
I didn't mean to say "modern C++ is bad", but I'd prefer a small subset that I can be sure no surprises happen: no hidden memory allocations and such. In fact, we use modern C++ and Eigen and CppAd Codegen in some of our projects: the generated C code by tracing a full sim step is about 5 times faster, likely due to no memory allocations and cache friendly memory access.
Careful coding to get runtime properties you value is just careful coding. It is not a language subset, and there is strictly negative value in a subset "closer to C".
When coding carefully, you can still take full advantage of all of the most powerful features of the language without penalty. Maybe you watch your cache footprint, and use memory supplied by the caller, and avoid branches that would often be mis-predicted. That's just programming.
Lambda is designed to be a zero-overhead feature. In the worst case, it's the same as passing an additional void* ctx argument to a plain old function, which is the most common pattern in C/pre-lambda C++.
But it's not. A lambda is merely syntactic sugar for an object (a functional) and as such it can carry a state (the term is 'closure' in this case); depending on what is being captured, its creation can even involve a heap allocation.
It is: "zero-overhead" is defined to mean you could not open-code it yourself any better.
If you specify a capture-by-copy, you have specified a copy. If that copy involves an allocation, you have specified an allocation. There is exactly zero extra overhead: in the overwhelmingly most common uses, not even a call through a pointer.
The good news is that the compiler knows all about lambdas, so can optimize the hell out of them.
By "careful" I mean avoiding unnecessary use or the temptation of capturing "everything." All too often I see people get enamored with it or fall into the trap of following patterns popular in other, slower, languages.
Not sure I get your point - the exact same is true of a C function which is passed a void* ctx object, since those objects have to be allocated and their lifecycle has to be managed. It's still a zero overhead feature.
A pointer argument passed in a register is not what we mean when we say "overhead".
Without the lambda, you would instead need to pass the pointer manually, for identically the same cost. But then the compiler would not understand as well what you were doing, and would be unable to optimize it as well. Here, the lambda gives you negative overhead, vs. what you would have written.
I've never liked exceptions, its always awkward combination with returning null or throwing exceptions, or error codes. It always ends up breaking some abstraction making it leaky - eg read_doc throws a file exception, or web exception or higher level exception that has no useful detail.
I'm really heartened in the last few years that FP popularity has caused more people to avoid exceptions and even golang doesn't support them.
FTA: The root cause is that the unwinder grabs a global mutex to protect the unwinding tables from concurrent changes from shared libraries
Is it unavoidable that that hurts performance badly for the common case? I would think such concurrent changes are rare, so if there’s an asymmetric way to protect against that that’s faster in the happy path exists, that would be a big improvement.
It is avoidable by using locks like you mention, the article actually says so, but it causes an ABI change. They go on to say that this makes it undesirable, but compared to what alternative exactly?
This paper confirmed what I suspected for some time: exceptions are still the only basically-zero-overhead solution available, which ironically completely justifies their existence in usage patterns where exceptions are actually exceptional. There are times when writing/profiling Rust when I wish I had access to exceptions instead of `Result` propagation.
I do agree with the mentioned design issues though.
> There are times when writing/profiling Rust when I wish I had access to exceptions instead of `Result` propagation.
This introduces a whole bunch of design issues, but isn't `panic` (in unwinding mode) basically C++'s exceptions, implementation-wise? Couldn't we use `panic` + `catch_unwind` as a poor man's exception system, should the performance situation really require so?
Also, could we maybe add an attribute `#[exceptional]` to enum variants in a match, that would result in a match implementation closer to exceptions, implementation wise?
Yes, if A: you have code that's expected to "fail" relatively many times, and B: this code is also multithreaded. A lot of code doesn't match one or both of these conditions while still being perf-sensitive.
Further, as you can see in other comments, it's not an uncommon belief that exceptions simply shouldn't be used for A (and thus it's reasonable that they aren't optimized for something they aren't supposed to be used for).
Rust just punts to the C ABI for shared libraries. Which is sensible since the whole point of those is to share code system-wide, and the C ABI is the de-facto common interface for "foreign" code on systems where shared libraries are widely deployed.
It's unclear to me if the author is saying that the global mutex for unwinding the stack interferes with the non-exception-throwing code paths, or are they just saying that the exceptions themselves bottleneck and are so inefficient that this becomes a significant impact on overall throughput?
It does seem like at least in theory it should be possible to create a non-locking / blocking exception unwinder, if there is no actual contention between the threads. If that can be done then it seems like the solution should be to do that rather than abandon a whole language feature. This is a bit like the Python GIL question. I would say if the language spec means you have to have a "GIL" in any context in a high performance language like C++ then it ought to be addressed at the spec level.
> 2) exception unwinding is effectively single-threaded, because the table driven unwinder logic used by modern C++ compilers grabs a global mutex to protect the tables from concurrent changes.
> The second problem could potentially be fixed by a sophisticated implementation, but that would definitively be an ABI break and it would require careful coordination of all components involved, including shared libraries.
This seems like a perfect application for an rwlock. Threads that throw acquire for reading, so can occur in parallel. Threads that load or unload shared libraries (a pretty rare occurrence outside of process start) acquire for writing and therefore block reads from the table from throwing threads. The shared library loader shouldn't throw during this piece.
Come to think of it, throwing should also be pretty rare. C++ exceptions are not really about common control flow, but truly exceptional circumstances. So the high cost serializing parallel throwers doesn't even sound like that huge of a deal. But as I've said, it seems pretty easy to improve upon it.
[Edit: It would seem the article does say something about how an rwlock is not feasible with the current implementation ... It doesn't sound terribly convincing]
Wouldn’t changing the global mutex into a read/write be a simple way to fix things? Shared libraries changing the exception table at the same time as exceptions being thrown seems rare. Might also be fixable in an API-preserving way…
Edit: nope. This idea is discussed later in the paper (not fully ruled out but the answer may still require ABI changes for more subtle reasons)
> A less radical change would be to change the global mutex into an rwlock, but unfortunately that is not easily possible either. Unwinding is not a pure library function but a back and forth between the unwinder and application/compiler code, and existing code relies upon the fact that it is protected by a global lock. In libgcc the callback from dl_iterate_phdr manipulates shared state, and switching to an rwlock leads to data races. Of course it would make sense to change that, but that would be an ABI break, too.
I think this is addressed in the article in the section starting "A less radical change would be to change the global mutex into an rwlock, but unfortunately that is not easily possible either..."
That’s an interesting idea. Might be an elegant way. I was thinking that the exclusive lock could be updated to be RW by hiding the read information in unused bits of the exclusive lock state that’s mutated atomically.
I find the paper’s argument thin on why this could only be done in an ABI incompatible way.
One can mix the error abstracts, we have to anyways(cmath methods and NaN). When the distance between the error and the caller that can act on the error is large, it makes much more sense to throw in a large number of cases. Otherwise the cost is code that would have been error neutral becoming error opinionated and that is a software engineering cost.
But look at some of the costs we have to go to great lengths to get around. std::sqrt has to check for invalid inputs and that means a branch, often even when the code is checked prior or an ASSUME( f > 0.0 ) type thing is expressed. This inhibits auto-vectorization. On some compilers ASSUME( not std::isless( f, 0.0 ) ) can tell the compile that the number is real and >= 0 which elides the branch. But the math-error/errno issue is a big hinderance too.
A lot of the costs is the branch itself and telling the compiler it is not needed can boost hot path perf more than the rare cold paths.
Don't write constructors that can fail unless it is failure that would be appropriate to crash for. That works out a lot better than it might naively sound. It is hard for people to reason about the possibility of constructors/destructors failing, so actually rather nice to just forbid it.
And for those super-rare cases where it's appropriate to crash / fail hard in a constructor but you still have requirements about reporting or even recovery... setjmp/longjmp still exist.
I think that would be problematical in those cases where the special constructors are called, and complicate using constructors in expressions and argument lists.
Well, the "environment" may decide for you in some cases, but there is no default action as such. You log an error and either terminate or ignore - depending on which causes less harm.
It's not because C++ is not an everything-is-a-reference language.
> Combined types which return the structure or an error are another. Has the advantage of returning error information, why failure.
Sum types are great, but changing constructors to return sum types would break all existing code. It would also lead to a whole slew of questions about how the construction of arrays of values would work. (Does an array of an objects now turn into an array of wrapper types? Byebye SIMD optimization!) It would also break the consistency of constructing POD vs non POD types, which would make writing templates that need to generalize over both a huge PITA.
FWIW, constructors do not "create" (I assume you mean allocate) memory. That's the job of operator new. A constructor, given a block of untyped memory, will construct an object in it.
I think the dilemma for C++ exceptions is that if the programmer actually thinks something should never happen, it is almost always better to just crash. But if the programmer thinks it might happen sometimes, then it is risky to predict it is will be super rare, as often this code will be called in different contexts in the future, so it is safer to use normal returns and flow control. As a result, throwing an exception is basically never the best thing to do.
C++ STL is a strong selling point for c++, disabling exceptions mean you lose all of STL in the library, unless you're fine to use STL without any error reporting at all.
In the gaming case(no rtti, no smart pointer, no exceptions(thus meaning ctor|dtor are "unsafe")), what else do you leave with c++ then?
I use c++ but I'm always struggling with yes-or-no for exceptions.
c++ is deeply rooted with exceptions, bad or good.
There's very little you need exceptions for with STL. Data structures, algorithms, etc. all work just fine without exceptions.
C++ without exceptions is great. Google doesn't use exceptions and they still use STL. Gamedevs usually don't use exceptions and they're fine. Just use some other mechanism for errors, like error codes, absl::Status, std::expected, etc.
Google did not do exceptions due to legacy code base reason, in its announcement it says for new code it will do exception, it just had too many old code and can't do exceptions.
Most STL operations and iteration etc will throw exception for errors, if you disable exception, any of those errors, be it recoverable or not, will just std::terminate, which might not be ideal.
I interviewed at Google once and during some whiteboard forgot to check malloc for NULL return, then mentioned, "oh yeah at <former company> malloc never returns NULL" and the interviewer commented "at Google, malloc spawns a new data center."
> The root cause is that the unwinder grabs a global mutex to protect the unwinding tables from concurrent changes from shared libraries.
OK, so replace the mutex with a reader writer lock. Shared library loading is incredibly rare, and dear god I hope nobody seriously does it and expects it to work during unwinding.
Legacy projects (I am not being pejorative, they are important) which must be maintained aside, should not C++ be deprecated?
We have many new languages, we always had C. What does C++ give us in 2022 that makes up for the enormous cognitive load of understanding and keeping up with it.
C++ is still a wonderfully performant language, and there are still domains where it is clearly a leader (networking, games/graphics, etc). Rust is slowly displacing it but there's still a lot of road left. The maturity, stability, prior art are also worth something. The pitfalls are really not that big and dangerous, although to someone who doesn't do C++ I can see why it has that perception. Additionally, if you aren't a functional programming lover (like I've become lately), C++ is one of the funnest languages to work in (as long as the codebase follows could principles).
That said while I used to use C++ for nearly everything, these days I use Elixir for app dev whenever I can, and Ruby and bash for scripts. However if I were going to write a desktop app today I'd most likely go for C++ so I could use Qt. I do really need to try out new GTK though, sounds like it's gotten really great.
We've talked about Rust in my monthly beering meetings with other programmers, so it's definitely gaining mindshare to some extend. And it's being prepped for Linux kernel inclusion. Once that lands it will be pretty respectable.
Do many people in the meetings comment about the functional programming feature of rust? That to me is one of the greatest benefits as functional patterns are (at least were) harder in C++ and were definitely less common (which makes code harder for others to understand).
In your sample of C++ people, is there much interest in functional programming?
In this group, my friends are all former C programmers, not so much C++. I mean one does what one has to. I think I'm the main Lisp fan, and no Haskell or anything (although I went to high school with a guy that loves Haskell and lazy eval and mathematics of infinite sequences made fully instantiated). I think the idea of pure functions being safe in a way state mutating functions are not is deeply appreciated by C programmers.
In larger groups, I have never found much appreciation for functional programming, and side-effect free APIs/design philosophies to be particularly popular. Usually there's one smart person that loves it, and they make some systems that are 10x as performant as the standard Java whatever and then no one else gets it and it's rewritten into regular OO Java and the person moves on to a more enlightened job.
The respectable comment was conditional on it landing in the Linux kernel - I'd be impressed by that. I'm not really aware of any large projects written in Rust, which I find to be a bit of a red flag. There was a project to re-write all the core-utils and such from Gnu in Rust, but I don't think it's gone anywhere. It wouldn't be that much fun because you'd have to replicate all the long options and existing complexity.
Believe it or not, there are a lot of us who enjoy C++ and think the language is getting better all the time. IMHO, it's a pretty good time to learn C++.
As for what do you get? Well, compared to C you can get higher programmer productivity and compared to any language other than C, you get great performance.
I find that it's not rare to get better performance from C++ than C. As a trivial example, generic container code in C is likely to be run-time generic and sit on memcpy etc; the same functionality in idiomatic C++ is likely to be compile-time generic and be able to use fixed-size copy/move operations instead.
The short answer is probably that (unsurprisingly) C still does not scratch the particular itches that C++ was created for, and non of the new languages hit all of them well either. Plus network effect.
Little things... Destructors; overloading; namespaces; and yes, exceptions. Things I can no longer live without. Generics (templates) are nice to have, too.
You will learn to live without overloading and exceptions.
Generics is an absolutely must have. In 1997 I was was giving up on C++ when I discovered the standard template library. I was lucky to be using Visual C++ on Microsoft. The (only) good thing good thing about that compiler is it implemented the STL using Stepinov's reference.
Stepanov defined STL in 1994. By 1997 there were SFA implementations that followed the standard. Most "improved" it and made a mess of it, like GCC.
(I may have remembered that wrong, mēh)
It completely changed the way I thought about computer programming. Before that I was always programming in a style suited to assembly programming
> What does C++ give us in 2022 that makes up for the enormous cognitive load of understanding and keeping up with it.
If I were starting a greenfield project today, I might choose C++ for it, depending on what it was. (Pick the best tool for the job, and all that.) But if I picked C++, I would not use the full enormity of the entire C++ language specification. I would use the parts that helped with the program I was trying to write, and explicitly not use the rest of it.
There are still some environments where security/safety does not matter much and modern alternatives don't target. Like firmware for a RC project, or a non networked or sandboxed platform game where tooling for other languages is missing (like consoles). If you already know C++ and risks down the road very well.
That's not true. GCC, for instance, is a GNU compiler collection. (True, it does not include Rust, the last time I checked.) Other than that, there's always Java, Common Lisp, Haskell, and D. Plenty of choice there.
They're hardly comparable languages are they? If I want to write some embedded software or work on a POWER9/10 machine at work, I'm basically limited to C or C++.
It is always easy to find places where performance doesn't matter, and even Python is fast enough, including startup code in programs where performance otherwise does matter.
You cannot draw useful inferences from those cases.
I would also strongly argue you can’t draw useful inferences from do_fib.
To rephrase it another way. In how many contexts is std::expect insufficient? It’s easy to find places where nothing short of hand rolled SIMD is fast enough. You cannot draw useful inferences from those cases.
I like how Zig implements exceptions. It is similar to the restricted exception value type proposal for C++, but it also has a nice bonus of recording some stack trace on the unwind path. Plus in Zig any call to a function has to either catch the exception or annotate the call somewhat similar to ? in Rust.
It is a fact that C++ exceptions are largely low overhead, and that you also don't have to use them. In fact, C++ can make everyone happy, because you can choose which parts you like.
Personally, knowing how exceptions are implemented and having implemented small parts of the ABI, I can safely say that I will be using exceptions where appropriate in all my C++ projects. Parts where real-time behavior is needed we can use custom containers that don't randomly exit on failure. On low-memory hardware it is beneficial to have either tiny exception footprint or no exceptions at all.
It is true that C++ exceptions as they are implemented can be improved upon, breaking ABI. There are also other contenders (Herbceptions) that show great promise, bringing another way of handling exceptions that is not source compatible. Either way, many who work with custom C++ code do not care much about ABI, as everything is source compiled, and such would benefit from a new ABI generation.
Of course the fib example is extreme. But some code bases do a lot of calls, and people care about calling overhead. I think a moderate calling overhead like, e.g., with Herbecptions is acceptable, because usually people doing something useful in a function that will mask the calling overhead. But other approaches are really to expensive to justify, they violate the zero-overhead promise of C++.
It is not trivial in practice to make code safe in presence of exceptions even if one follows the best C++ practices. The errors can be very subtle and hard to identify. It is another reason besides the performance and code bloat why Chromium disables them.
> Very interesting that exceptions have much less overhead on the happy path than Rust/Haskell style Either types
C++ genuinely traded “error” cases (made them even costlier) so that the happy path of exceptions would be cheaper. I don’t remember whether that’s the case in C++, but in some language/implementations (used to be a big issue in V8) just having a try/except would drastically deoptimise a function, even if the exception never happened.
Just write code with a bunch of threads that are all logging a few M / second and see if the CPUs are running equally hot or 1 is 100% and the rest are mostly idle.
How do you guys handle using other libraries? Everything I can see uses stuff like std::vector and the like at API boundaries, and I'm not aware of a way to construct an std::vector without throwing an exception.
For AAA / high-performance / console / close-to-the-metal video games, at least.
There are tons of games where exceptions are perfectly fine. Though still more often used for "probably going to crash soon" situations than otherwise, I'd wager.
Since exception handling in desktop runtime environments usually takes an unbounded amount of time, it’s not good practice to use them in your main loop since you have to guarantee a new frame at 60Hz.
You can definitely do it but you’d be conceptually allowing for frame skips in your codebase. It could be difficult to audit and remove this assumption if down the road you wanted to tighten up your main loop code.
Agreed about main loop — that requires special care.
But there's often lots of other stuff going on, such as IO in other threads. You wouldn't want that stuff in your main loop for the same reasons, so since it's segregated anyway, exceptions aren't so bad (usually).
This somehow reminded me, wasn't there a competition years back to see who could generate the most compiler error output with the least amount of C++? A few too many templates and you could generate terabytes. Edit: don't think it was this but this is still a fun read https://codegolf.stackexchange.com/questions/1956/generate-t...
What does 'wrong' mean in this context? I mean, if you're against side effects and thus believe functions should always return a value that seems reasonable. Except that for a toy example where the purpose is to exercise some hardware and measure results it does not seem to be applicable.
Well that's unfortunate. I suspect the fault lies with C, the early versions of which did not have the 'void' keyword, and 'int' was assumed as the default return type - which also meant that when such "function" did not include a 'return' statement it "returned" whatever garbage was left in the register. Instead of adding the keyword 'void' (to be abused later as the empty arg list) they could have added 'proc' instead, but that would have complicated the parsing, and so they went with the 'void' pseudo-type as an indication of the fact that the function is not really a function but a procedure.
Any function that does not return a value must have a side effect, else why call it. Calling functions that have side effects (and particularly don't indicate if the side effect was succesful), is not a great idea.
Are there any software projects of substantial scale that are purely FP?
Even things like I/O or dynamic memory allocation have side effects. So while FP has some great ideas implemented at scale (first class functions and MapReduce perhaps the most well-known), they don't seem very useful by themselves.
EDIT: I'm not objecting to the idea "functions should return information," I'm objecting to the idea that side effects are "not a great idea."
(Exceptions are bad, full stop. It should all have been monadic all along, with Maybe/Either/Result.)
That said, exceptions should have been allocated on the stack, and catch handlers should have been closures that get called with the exception object. I guess this can't be implemented now.
As for multi-threading unwinding, the issue there is that unwinding tables are part of the shared objects whence the associated object code comes, and it often has to be possible to unload loaded shared objects, so now what? The situation shouldn't be bleak though: a shared object should never be unloaded while there are threads executing its code, so it should be possible to arrange a slow unload-time operation to update the the process-wide unwinding tables -- think of user-land RCU if you like.
The exception handling code should not have to synchronize around unwinding, except that when unwinding completes it might have to notice that there's a pending unload, so wake the thread that is waiting for unwinding. Or maybe not even, because maybe unloading could do something truly breathtaking like check that no thread's stack includes return addresses from the object to be unloaded, and then unwinders would never need to step into the unwinding tables from that object.
Exceptions are exceptional so in principle it doesn't matter (within reason) how long it takes to throw one as long as it costs nothing not to do so. So measuring the cost of repeated throws IMHO doesn't cast light on any useful case, and the approaches that add runtime cost for the path not taken, even Herb Sutter's, are not acceptable.
His code transformation example is simply the compiler behaving properly, unless it can't make that transformation even when foo() is declared noexcept.
The high core count is a real issue and a legitimate reason for an ABI break. Exceptions are the kind of below the surface plumbing that can't reasonably be implemented in regular code.
And in that regard the paper does make a good suggestion, though it then dismisses it! The tree approach described in section 3.4 seems like the right kind of fix.
Ultimately there's a spectrum of branching (`return` -> `break` -> `goto` -> `throw`) all of which need consideration in light of multicore deployment.