[HN Gopher] Current hardware trends make C++ exceptions harder t...
___________________________________________________________________
Current hardware trends make C++ exceptions harder to justify
Author : ingve
Score : 206 points
Date : 2022-02-22 17:33 UTC (5 hours ago)
(HTM) web link (www.open-std.org)
(TXT) w3m dump (www.open-std.org)
| fpoling wrote:
| 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.
| erwincoumans wrote:
| 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.
| mhh__ wrote:
| 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?"
| CyberRabbi wrote:
| > (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.
| com2kid wrote:
| > Even if you avoid malloc,
|
| 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.
| worik wrote:
| Why do you use C++ in those domains?
|
| Curious as to the decision criteria.
| cyber_kinetist wrote:
| 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!)
| mhh__ wrote:
| 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
| erwincoumans wrote:
| 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).
|
| See the (unreadable) generated code from Eigen+CppAd
| Codegen here: https://github.com/google-research/tiny-
| differentiable-simul...
| DoctorDabadedoo wrote:
| Ecosystem and performance.
|
| 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.
| erwincoumans wrote:
| 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.
|
| What else do you suggest?
| ncmncm wrote:
| 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.
| Koshkin wrote:
| Well, with a C++ compiler there's always hope that they
| will pick up the good parts, eventually.
| TillE wrote:
| "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.
| Koshkin wrote:
| Careful with lambdas, they are not without an overhead.
| zozbot234 wrote:
| 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++.
| erwincoumans wrote:
| 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.
| addaon wrote:
| 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.
| RcouF1uZ4gsC wrote:
| > It breaks the existing ABI, and all shared libraries would have
| to be compiled with the new model, as otherwise unwinding breaks.
|
| I am convinced that maintaining ABI is a huge technical debt to
| C++. And that is largely occasioned by shared libraries.
|
| Rust IMO made a fantastic decision to emphasize source
| distribution (and making it super easy via cargo and crates).
| synergy20 wrote:
| you do pay the compilation time price though, sometimes it
| takes really a long time to rebuild.
| zozbot234 wrote:
| 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.
| Someone wrote:
| 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.
|
| https://en.wikipedia.org/wiki/Readers-writer_lock mentions _Read-
| preferring RW locks_ , but (from cursory reading) doesn't say how
| much that can help.
| superjan wrote:
| 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?
| chippiewill wrote:
| The article discusses this.
|
| RW lock is an option that helps but involves breaking the ABI
| of EVERY shared library.
| asveikau wrote:
| > 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.
| _b wrote:
| 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.
| adrian17 wrote:
| 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.
| dureuill wrote:
| > 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?
| cryptonector wrote:
| The problem is that if you have exception-happy code, then it
| becomes a scalability limiter for threading.
| adrian17 wrote:
| 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).
| zabzonk wrote:
| > As illustrational example consider this small code fragment:
|
| Consider this crap small code fragment. Any function that returns
| void is almost certainly wrong.
| turminal wrote:
| Enlighten us, please
| zabzonk wrote:
| 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.
| torstenvl wrote:
| 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."
| zabzonk wrote:
| You generally want to test if an "I/O or dynamic memory
| allocation" worked.
|
| BTW, I am basically a C++ programmer, not some functional
| maniac. But whenever I see a void return type, I think
| something is wrong.
| burnished wrote:
| 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.
| MauranKilom wrote:
| Good thing printf returns something, otherwise it might wrong
| or useless!
| worik wrote:
| No it is not.
| Koshkin wrote:
| You are right. That's why in good old languages they were
| called 'procedures.'
| zabzonk wrote:
| And those languages were badly designed. Which is why we
| don't have "procedures" or "subroutines" in modern
| programming languages" - just functions.
| john567 wrote:
| 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.
| edflsafoiewq wrote:
| My problems with Result/expected/etc
|
| 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.
| exdsq wrote:
| 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.
| zozbot234 wrote:
| > 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.
| gpderetta wrote:
| 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.
|
| [1] https://forums.swift.org/t/on-the-proliferation-of-try-
| and-s...
| zozbot234 wrote:
| "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.
| musingsole wrote:
| > one that will never return an error
|
| 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?
| lanstin wrote:
| 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.
| stickfigure wrote:
| > This is not "noise" but needed information.
|
| 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.
| wvenable wrote:
| > 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.
| lanstin wrote:
| 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.
| gpderetta wrote:
| Pragmatically, shit happens.
| wvenable wrote:
| 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.
| kccqzy wrote:
| > 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.
| mwcampbell wrote:
| > Panics do not need to be caught and handled. You can (and
| should) transform panics into abort.
|
| I think this strong stance needs some justification,
| especially since it's not the default in Rust.
| lanstin wrote:
| It is in Go Lang which shares error handling philosophy
| with GP.
| ziml77 wrote:
| Go panics do not simply 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)."
|
| From https://go.dev/blog/defer-panic-and-recover
| throw10920 wrote:
| > Why do we need to have ambient control flow?
|
| 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.
| zozbot234 wrote:
| > 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.
| jdlshore wrote:
| 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.
| jeffbee wrote:
| That's what you get from _disabling_ exceptions: a call to
| std::abort instead of a throw.
| InfiniteRand wrote:
| In some ways that's throwing an exception into the calling
| environment in the form of an error code
| WkndTriathlete wrote:
| Haskell's IO monad says, "Hi! With me you don't need
| exceptions and you don't need to put an if statement after
| every function call."
| gpderetta wrote:
| How is the implicit control flow in do notation different
| from exceptions?
|
| If anything, the issue is between checked and unchecked
| exceptions.
| mibsl wrote:
| 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).
| mjw1007 wrote:
| 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.
| pjmlp wrote:
| C++ is the only language where exceptions are such an
| ideology war.
|
| All the other ones that have born with exceptions don't have
| this issue, including Ada.
| Koshkin wrote:
| _There are only two kinds of languages: the ones people
| complain about and the ones nobody uses._
|
| -- Bjarne Stroustrup
| pyjarrett wrote:
| 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.
| pjmlp wrote:
| FPS rate should not drive what 99% of other C++
| developers are able to use the language for.
| masklinn wrote:
| I'm not sure that's true.
|
| 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.
| ncmncm wrote:
| No legitimate conclusions can be drawn from Java or C#
| experience, besides that it is best to stay well clear of
| them.
| pjmlp wrote:
| 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++.
| CyberRabbi wrote:
| > 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.
| wvenable wrote:
| 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.
| jstimpfle wrote:
| > 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.
| wvenable wrote:
| 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.
| ithkuil wrote:
| 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).
| mort96 wrote:
| 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.
| masklinn wrote:
| 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.
| mikepurvis wrote:
| 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.
| chippiewill wrote:
| 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.
| mikepurvis wrote:
| 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.
| gpderetta wrote:
| But you have exactly the same options in C++.
| mikepurvis wrote:
| 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.
| masklinn wrote:
| Really? What's the factory function invoked for a copy or
| a move?
| gpderetta wrote:
| Whatever you want it to be. By default of the compiler
| will generate calls to move and copy constructors.
| aphexairlines wrote:
| It's also easy to provide a default factory:
| https://doc.rust-lang.org/std/default/trait.Default.html
| mikepurvis wrote:
| 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().
| rr808 wrote:
| Best to create object then initialize it in a separate
| method.
| masklinn wrote:
| Because that meshes so well with copy and move ctors, const
| and reference members, or inheritance.
| edflsafoiewq wrote:
| It meshes well with move ctors at least, since objects
| need a "dead state" they can be put into when moved from.
| Koshkin wrote:
| 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.
| ncmncm wrote:
| By "best" you mean, of course, "worst".
| gpderetta wrote:
| Amen.
| jmyeet wrote:
| > Root cause
|
| > 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?
| nly wrote:
| std::expected doesn't require language changes, it's a library
| type. If anything it shows how C++ is multi-paradigm
| ModernMech wrote:
| 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.
| passivate wrote:
| >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..
| jerf wrote:
| 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.
| JAlexoid wrote:
| 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
| zozbot234 wrote:
| 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.
| jcelerier wrote:
| 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.
| Koshkin wrote:
| > _C++ just can 't seem to stabilize_
|
| It's not like C++ is oscillating. It continues to
| improve, which is a good thing.
| JAlexoid wrote:
| Adding more and more features isn't necessarily an
| "improvement".
| MauranKilom wrote:
| 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.
| zozbot234 wrote:
| > 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.
| dureuill wrote:
| I'm not convinced they're comparable.
|
| 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.
| jerf wrote:
| 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.
| pjmlp wrote:
| My first contact with RAII was in 1993 with Turbo C++ 1.0,
| it is hardly a hot new thing.
| ModernMech wrote:
| 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.
| KerrAvon wrote:
| 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.
| ncmncm wrote:
| Destructors have been in C++ since the mid-80s. Anybody
| who did not know about RAII in 1993 did not know any C++.
| duped wrote:
| 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.
| andi999 wrote:
| How do you get memory without new nowadays?
| kaetemi wrote:
| make_unique, etc.
| usefulcat wrote:
| 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.
| fbkr wrote:
| 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.
| masklinn wrote:
| > 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.
| Inityx wrote:
| 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.
| ncmncm wrote:
| 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.)
| astrange wrote:
| 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.
| jeffbee wrote:
| 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.
| kevin_thibedeau wrote:
| You also have to abandon operator new and STL unless you want
| to pretend they never fail.
| jeffbee wrote:
| 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.
| Inityx wrote:
| 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.
| jmyeet wrote:
| Except if that's incompatible with libraries you're using.
|
| For the record, I like absl::StatusOr<>.
| layer8 wrote:
| 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.
| dtech wrote:
| 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.
|
| [1] https://www.artima.com/articles/the-trouble-with-checked-
| exc...
| hashmash wrote:
| 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.
| layer8 wrote:
| 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.
| mcguire wrote:
| " _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.
| lanstin wrote:
| 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.")
| nybble41 wrote:
| > 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.
| taeric wrote:
| 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.
| nybble41 wrote:
| 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.
| taeric wrote:
| 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?
| zmmmmm wrote:
| > errors are part of a system's API
|
| 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.
|
| [1] https://www.artima.com/articles/the-trouble-with-
| checked-exc...
| taeric wrote:
| 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.)
| layer8 wrote:
| 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.
| taeric wrote:
| 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?
| layer8 wrote:
| > 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.
| jkaptur wrote:
| I'm relatively new to C++ and I found absl::StatusOr<T> and its
| helper macros to be extraordinarily pleasant.
| alskdjflaskjdhf wrote:
| 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).
| kirbyfan64sos wrote:
| It has helper macros now? I liked StatusOr but had to write
| my own helper macros for it at the time.
| trimbo wrote:
| Ex: https://google.github.io/or-
| tools/cpp/status__macros_8h_sour...
| [deleted]
| vlovich123 wrote:
| 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)
| interroboink wrote:
| 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..."
| plorkyeran wrote:
| > 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.
| Findecanor wrote:
| Could the mutex be wrapped in a rwlock? New code that don't
| share state would be able to unwind concurrently while old code
| would be locked out.
| The_rationalist wrote:
| ziml77 wrote:
| 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.
| tneumann wrote:
| 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.
| PaulDavisThe1st wrote:
| 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_.
| lanstin wrote:
| True but also libraries and people thinking differently.
| Numeric is not real-time, tho it has many common
| characteristics.
| olvy0 wrote:
| I do the same.
|
| 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.
| masklinn wrote:
| > 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?
| HelloNurse wrote:
| 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.
| masklinn wrote:
| > 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.
| im3w1l wrote:
| 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.
| xrikcus wrote:
| 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.
| JAlexoid wrote:
| Errors and exceptions aren't necessarily low in all cases.
|
| And in any case this is about how these exceptions impact
| parallel processing - and the impact is not negligible.
| CJefferson wrote:
| 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.
| MauranKilom wrote:
| 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).
| gpderetta wrote:
| 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.
| GnarfGnarf wrote:
| I manage 1.2MLOC C++ developed over 25 years. I am pleased to say
| we never had to use exceptions. Just check the return value.
| aeldidi wrote:
| 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.
| tsimionescu wrote:
| Very interesting that exceptions have much less overhead on the
| happy path than Rust/Haskell style Either types (std::expected):
|
| > For fib we see a slowdown of approx. 60% compared to
| traditional exceptions, which is still problematic.
| masklinn wrote:
| > 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.
| fpoling wrote:
| 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.
| brundolf wrote:
| I'm curious whether Rust's Result gets special optimization
| treatment from the compiler that std::expected doesn't
| yakkityyak wrote:
| I'm not convinced a recursive implementation of `fib` is a
| reasonable example to draw such a conclusion.
| fwsgonzo wrote:
| It really isn't.
|
| People need to stop making trivial benchmarks and draw ...
| silly ... conclusions.
|
| http://www.eecs.northwestern.edu/~robby/courses/322-2013-spr.
| ..
|
| 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.
|
| Zero-overhead exceptions by Herb Sutter: http://www.open-
| std.org/jtc1/sc22/wg21/docs/papers/2018/p070...
| tneumann wrote:
| 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++.
| fpoling wrote:
| 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.
| [deleted]
| forrestthewoods wrote:
| > Nevertheless the overhead is so high that std::expected is not
| a good general purpose replacement for traditional exceptions.
|
| This is basically the path Rust has chosen. I'm curious if it's
| _actually_ too slow for C++. I feel like the answer must be no.
| ncmncm wrote:
| 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.
| synergy20 wrote:
| same as golang, I doubt that causes serious performance issues
| but have no proof.
| synergy20 wrote:
| 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.
| Rarebox wrote:
| 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.
| kaetemi wrote:
| Gamedevs usually don't handle any errors at all.
| synergy20 wrote:
| 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.
| fpoling wrote:
| Note Google explicitly configure runtime to instantly crash
| on memory allocation errors. With that STL works indeed
| nicely without exceptions.
| lanstin wrote:
| 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."
| cryptonector wrote:
| (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.
| shiado wrote:
| 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...
| CyberRabbi wrote:
| 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.
| chippiewill wrote:
| 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).
| fpoling wrote:
| 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.
| grandinj wrote:
| 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.
| brundolf wrote:
| 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
| passivate wrote:
| Do you still have any notes from your profiling?
| nicoburns wrote:
| 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!
| CJefferson wrote:
| 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.
| anarazel wrote:
| 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.
| mlinksva wrote:
| Thanks a lot for your perf work on LibreOffice, which
| https://gerrit.libreoffice.org/q/Grandin seems to confirm!
| addaon wrote:
| 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.
| downut wrote:
| 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_.
| olvy0 wrote:
| 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.
| henrydark wrote:
| As per the piece, open 128 documents on a 128 core machine, and
| you'll see the difference.
| netr0ute wrote:
| How many people actually use a 128 core machine though? The
| absolute biggest AMD Threadripper you can get only has 64.
| mcguire wrote:
| No one needs more than 64 cores?
| nybble41 wrote:
| > 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.
|
| [0] https://bizon-
| tech.com/bizon-x6000.html#959:8714;960:4858;96...
| jrockway wrote:
| Those Threadrippers have two vCPUs per core, so you can
| definitely have 128 things that appear to proceed in
| parallel.
| tneumann wrote:
| A dual socket EPYC costs about 15K. You can rent some
| online for 400$ a month. This gives you 128 cores for a
| relatively low price.
| henrydark wrote:
| 96 virtual cores is a popular choice on AWS, being cost
| effective in some industry scenarios. I'm certain the
| analysis of the piece applies
| mhh__ wrote:
| This is mostly my opinion too.
|
| 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)
| tneumann wrote:
| 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.
| jcelerier wrote:
| > 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.
|
| here's the list of binaries that link against libstdc++.so.6
| in my /usr/bin:
| https://paste.ofcode.org/gfZJwx4puVx7Uxy9a7BBU3 - don't
| forget those please :)
| mcguire wrote:
| Are you arguing that C++ is unsuitable for massive parallel
| data processing tasks?
| jcelerier wrote:
| No, of course not. But let's not change the language in
| ways that benefits those use cases at the detriment of
| more common use cases.
|
| Of course, if there are ways to keep more or less the
| same semantics, while increasing performance, by all
| means it should be done !
| garbagecoder wrote:
| No, he's arguing that he'd prefer it not be if that means
| breaking support for end user apps which is what the OP
| is arguing for.
| PaulDavisThe1st wrote:
| for ones that throw an exception for 1% of a unit
| computation, perhaps yes.
|
| But as has been noted, that's a lot of failures and
| exceptions may not be the appropriate mechanism to deal
| with this type of failure.
| jandrese wrote:
| I think he's arguing that C++ exceptions are unsuitable
| for parallel data processing, at least in cases where
| exceptions are regularly thrown.
| ncmncm wrote:
| There is the red flag: "regularly thrown". "Regularly" is
| opposite to "exceptionally".
| jcelerier wrote:
| 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.
| gpderetta wrote:
| Well of course, they are slow so they are unsuitable. The
| issue is whether they need to be slow.
| [deleted]
| tjungblut wrote:
| If you run into contention issues have you tried scaling
| horizontally? Running on 128 single core VMs should mitigate
| what you see.
| nyanpasu64 wrote:
| 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).
| CyberDildonics wrote:
| That would turn communicating between threads into
| communicating between VMs, which is a disastrous change in
| itself.
| gpderetta wrote:
| It would. On the other hand multiprocessing with
| explicitly shared memory could be a fast (but potentially
| painful) workaround.
| CyberDildonics wrote:
| 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.
| gpderetta wrote:
| Of course. Using VMs here would very deep into
| overengineering territory.
| tjungblut wrote:
| how do you know the threads need to communicate with each
| other?
| ncmncm wrote:
| If you are seeing 1% of operations failing, I can guarantee
| you are Doing It Wrong.
| CyberDildonics wrote:
| 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?
| tneumann wrote:
| The main problem is the unwinding lock. Memory allocation
| is a bit unfortunate, too, but much less so.
|
| Note that I am trying to fix the unwinding problem, I have
| submitted a patch to libunwind to eliminate the contention
| during unwinding:
|
| https://reviews.llvm.org/D120243
|
| 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.
| drmeister wrote:
| 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.
| zwieback wrote:
| 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).
| wvenable wrote:
| > and 1% of your tasks fail
|
| That's a lot of failures. I'd question whether or not you
| should be using exceptions for something that happens 1% of
| the time all the time.
|
| Admittedly you might not have any choice in the matter if
| it's a dependency that is failing.
| infogulch wrote:
| 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.
| IshKebab wrote:
| 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.
| jolmg wrote:
| > but I imagine it is very rare to use exceptions for
| normal flow control - that is, to continue execution
| after an exception
|
| Exceptions are at least a very central part of the normal
| flow control of parser combinator libraries.
| gpderetta wrote:
| You are not wrong. The exceptional exception guideline
| feels circular. I would use exceptions significantly more
| if I could rely on their performance.
| Asooka wrote:
| 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.
| cesaref wrote:
| 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).
| CyberRabbi wrote:
| 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.
| nemothekid wrote:
| 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?
| xienze wrote:
| > 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?
| jgod wrote:
| Games cares about it. But you're right that it's only very
| high-performance contexts.
| kfitch42 wrote:
| 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.
| ncmncm wrote:
| 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.
| rr808 wrote:
| 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.
| Dork1234 wrote:
| It would be interesting to see a breakdown of exception overhead
| for C++/Rust/Haskell/Julia/Swift in multi-threaded workloads.
|
| Sometimes that happens rarely but blocks many threads is a huge
| pain to deal with.
| lanstin wrote:
| 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.
| gumby wrote:
| I find this paper quite unconvincing.
|
| 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.
| JoeAltmaier wrote:
| 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.
| gumby wrote:
| > But there's a design pattern where nearly everything is
| returned as an exception.
|
| _shudder_ The worst of cargo cults!
| [deleted]
| josephg wrote:
| > 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.
| LightMachine wrote:
| > 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.
|
| Jesus
| gumby wrote:
| > 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.
| JoeAltmaier wrote:
| 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!
| NoSorryCannot wrote:
| 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.
| cryptonector wrote:
| How exceptional exceptions really are depends on the code being
| run.
|
| The serialization of stack unwinding issue is a real serious
| problem.
| gumby wrote:
| > 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.
| cryptonector wrote:
| I agree. I'm not sure breaking the ABI to fix the unwinder
| serialization issue should be a show-stopper, considering
| that there still isn't a C++ ABI.
| gpderetta wrote:
| There are platform specific ABIs and there have been for
| many years. Many platforms want to preserve the ABI at
| all costs.
|
| In any case this doesn't seem to be an ABI issue
| cryptonector wrote:
| I would think it shouldn't be an ABI issue, but maybe
| part of the unwinding code gets statically linked into
| shared objects?
| gumby wrote:
| 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.
| gpderetta wrote:
| I'm certainly not an expert but as far as I understand, fixing
| the exception global lock need not break the ABI on gcc/glibc.
| See https://gcc.gnu.org/bugzilla/show_bug.cgi?id=71744
| gpderetta wrote:
| 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.
| randyrand wrote:
| 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.
|
| So now what? No one likes code that has 2+ ways of indicating
| method failure. Especially when those 2 ways of failure
| reporting don't compose well. It's unnecessary complexity! If
| you are a library author, do your users need to check for both
| Exceptions and Error codes?
|
| A standard way of indicating function failure is more important
| than the micro optimization exceptions give us, especially
| considering exceptions often inadvertently destroy perf.
|
| 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.
| talaketu wrote:
| "exceptions are exceptional" means what?
| pavon wrote:
| 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.
| Jtsummers wrote:
| 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.
| gumby wrote:
| 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.
| jcelerier wrote:
| 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.
| timClicks wrote:
| They're rare. Therefore their runtime costs have little
| impact on the overall running time. At least that's my
| understanding of the argument.
| shrimpx wrote:
| 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.)
| fivea wrote:
| > If you write a jpeg-processing service, it's intuitive to
| raise an exception on a malformed jpeg (..)
|
| Not really. Having to parse a malformed doc is not an
| exceptional situation: it's a basic use case, and one which
| is very close to the happy path.
| shrimpx wrote:
| Your opinion being that you should _not_ use exceptions in
| that type of case. In which cases should you use
| exceptions?
| gumby wrote:
| 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.
| whatshisface wrote:
| 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.
| foldr wrote:
| 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.
| spc476 wrote:
| 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.
| jcelerier wrote:
| Not in C++ I believe ?
| foldr wrote:
| 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.
| mpyne wrote:
| It's library dependent. iostreams (part of C++ std lib)
| doesn't throw exceptions by default but I believe it can
| be configured to throw if you want.
| zmmmmm wrote:
| 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.
| rossmohax wrote:
| If not exception, then how to fail constructor?
| CyberRabbi wrote:
| The "Herbceptions" proposal addressed this. http://www.open-
| std.org/jtc1/sc22/wg21/docs/papers/2018/p070...
| synergy20 wrote:
| you can still set a static flag or something in ctor then check
| it, but you can't do anything about dtor indeed if you need
| throw there.
| Koshkin wrote:
| Except you are not supposed to throw in a destructor.
| worik wrote:
| It has been many years since I used C++
|
| But returning null is an obvious choice.
|
| Combined types which return the structure or an error are
| another. Has the advantage of returning error information, why
| failure.
|
| There are many ways
| roger10-4 wrote:
| You can't return null from a constructor (constructors have
| no return)
| Calavar wrote:
| > But returning null is an obvious choice.
|
| 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.
| fpoling wrote:
| Signal failure via an out parameter reference. For destructors
| store the reference in the object and call an error method on
| errors.
| ncmncm wrote:
| That is the worst of all possible choices.
|
| Just throw.
| mannykannot wrote:
| I think that would be problematical in those cases where the
| special constructors are called, and complicate using
| constructors in expressions and argument lists.
| _b wrote:
| 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.
| addaon wrote:
| 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.
| ncmncm wrote:
| You have always had the choice available to do something
| overwhelmingly worse than throwing. That is not a reason to
| do it.
| throwaway5486nv wrote:
| Thaxll wrote:
| That's why we disable exceptions in video games.
| Negitivefrags wrote:
| We disable exceptions in video games due to lock contention
| when throwing exceptions on multiple threads in high core count
| situations?
|
| No.
|
| We disable exceptions in video games for dumb historical
| reasons that no longer apply.
| Thaxll wrote:
| Dumb? https://www.youtube.com/watch?v=GC4cp4U2f2E
| pjmlp wrote:
| Depends on the point of view.
|
| https://youtu.be/6hC9IxqdDDw
| interroboink wrote:
| 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.
| CyberRabbi wrote:
| 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.
| interroboink wrote:
| 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).
| alfazaleng wrote:
| worik wrote:
| Begs the question: Why C++?
|
| 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.
| trinovantes wrote:
| I still use C++ for the occasional projects that a quick Python
| script can't easily solve
|
| - I've been using C/C++ since my first year of undergrad so I'm
| already familiar/comfortable with it
|
| - Java has too much boilerplate (my mind is still stuck in Java
| 8 so maybe things have changed since then)
|
| - I found myself spending more time fighting the Rust/Go
| compiler than actually solving my problems
|
| - I don't want to package a web browser for a simple 1-2 UI
| application
| ska wrote:
| > We have many new languages, we always had C.
|
| 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.
| Koshkin wrote:
| Little things... Destructors; overloading; namespaces; and yes,
| exceptions. Things I can no longer live without. Generics
| (templates) are nice to have, too.
| criddell wrote:
| 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.
| addaon wrote:
| 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.
| freedomben wrote:
| 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.
| ncmncm wrote:
| More people start using C++ professionally in any given week
| that the sum total paid to code Rust.
|
| Rust is not "displacing" C++ anywhere beyond the HN echo
| chamber.
| lanstin wrote:
| 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.
| physicsguy wrote:
| If you don't want to use LLVM then you don't have much choice.
| Koshkin wrote:
| 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.
| AnimalMuppet wrote:
| > 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.
___________________________________________________________________
(page generated 2022-02-22 23:01 UTC)