But it rarely matters how much work you put into something to make it faster. What matters is what you can avoid to do at all.
I have pretty much zero knowledge of CPU-level issues and barely know enough x86 assembler to read my compiler's output. I have never cared for optimizations like inlining code, vectorization or tail call elimination. I have tried inlining a few times, and the speed-ups it brought were negligible. (And the slow-downs that too much of inlining brings, by increasing code size and thereby increasing cache pressure, must be very subtle and basically unmeasurable, while being potentially enormous. And the disadvantages from a maintainability standpoint are considerable as well).
If you have to care about these things, then maybe you're doing too much work. (Well, I guess I would use vectorization/SIMD if I had to write a video codec, but I've never done that).
Exceptions are pricey pricey things that take time and create some minorly interesting VM/GC behaviour that can impact your application. If at all possible they should be avoided in executed code.
> But it rarely matters how much work you put into something to make it faster. What matters is what you can avoid to do at all.
Using Result<> types to handle error cases is a very good example of doing just that: avoid the big hit of exceptions and poor exception handling in cacadingg situations with sensible return types that let you avoid a buttload of delicate work.
This is all very vague and handwavey. Not sure about the .NET world but in other systems the costs of exceptions is not significant and they can be used successfully even in the most demanding low-latency applications.
> Using Result<> types to handle error cases is a very good example of doing just tha
Result types don't scale. I suppose it's the sort of thing that programmers have to keep rediscovering for themselves but there's a reason why even Haskell eventually "discovered" exceptions (and I suspect the Rust guys will get there eventually). In the real world where you have lots of deeply nested function calls some of which cross component boundaries good error handling requires three things: contracts (you need to be able to express possible errors at boundaries), context (you need to know exactly what resources have been allocated so they can be cleaned up when an error is caught) and recovery (you need to be able to return the application to well-defined state/point so computation can continue). Frankly, Result types don't give you any of these. What you end up with is a very generic set of errors (necessary to avoid a combinatorial explosion of error types) and a poor-man's implementation of exceptions (see panic/recover in golang). Or I suppose you can write code that doesn't involve deeply nested call-graphs and multiple components and everything is perfectly "flat" and stateless... but this wouldn't be like any real application I've ever seen.
The real issue is this: figuring out how to do exceptions in a lazy language is simply not obvious. Work was needed, e.g. https://www.microsoft.com/en-us/research/wp-content/uploads/...
Even so, exceptions are generally not used for error handling in Haskell, yet life seems to go on. You'll see code using `throwError` or `catchError`, but usually these will just be providing a nice bit of sugar on top of an Either or Maybe type. So it seems like the takeaway should be that Result types do scale, with proper library+language support.
However, sometimes the errors are actually part of the fabric of the domain itself. They're expected to occur very frequently. The data it encodes is valuable. The way they're handled is important.
Nevertheless, you're right. More often than not, you're better of using traditional exception handling and preventing errors from occurring in the first place.
This is where I want to make a key distinction between exceptions and errors ala Railway programming. Performance with exceptions doesn't really matter because exceptions are supposed to be exceptional.
With Railway oriented programmer, the whole point is that you're encoding errors into the logic of some type of system. These types of errors may not only be not exceptional, they may a very regular part of the application.
Like I told another commenter, a friend of mine uses them in an application where they easily would throw thousands of exceptions per second if they used exceptions. Not only would it undermine the performance of their system, it's using the wrong tool for the job.
How large, and why? Only when exceptions are thrown, or also during the happy path?
From what I understand, with Monads the code check errors after each expression (like in C/Go), but behind the scene. Isn't it the case? If so, wouldn't that incur an overhead that is not present with exceptions, where normal code runs without checks and where "throws" perform a sophisticated jump?