Live data from Hacker News

Avoid exception throwing in performance-sensitive code

lemire.me

131–140 of 243 posts

Re: Avoid exception throwing in performance-sensitive code

#131
post #43

Exceptions are starting to feel like a legacy programming paradigm to me. Rust & Go have, at least in a practical programming context, shown that errors-as-values has far less footguns and encourages better error handling practices than exceptions, which often are treated as an afterthought or end up being abused like in this post. Whenever I'm writing Python or Java I can't help but feel anxious about calling a func…

At least for Go (haven't used Rust), I entirely disagree. After ~3 years of professional programming with the language, `if err != nil` is still constantly annoying me when writing code, but especially and most importantly, when reviewing code. Not to mention, Go has proven conclusively to me that exceptions are exactly the right pattern for error propagation - the 99.9% pattern is "function returns error with messag…

This is also how errors-as-values work in Rust. Functions that may fail return Result - putting an ? at the end of a failable function call returns T on success, otherwise it propagates E. It reduces

    result, err := call()
    if err != nil {
        return err
    }
to

    let result = call()?;
Completely unobtrusive, but makes failure awareness an obligation.

Re: Avoid exception throwing in performance-sensitive code

#132

Earlier quoted context omitted.

You might like how Rust does error handling. Rather than a function returning a triple of (val, error) where the error can be ignored, functions return Result . If you want to get the T, you must write code that handles both possibilities. If your function instead wants the T and propagate the E upwards if it exists, you can do that with one character - “?”

I indeed like Rust's approach a lot more than Go's. What I like less still is that it gives the impression that it's even possible to define functions that cannot fail. This is not true. One just has to look at how runtimes deal with stack overflow errors to see how the good old Java RuntimeException creeps in in various forms (e.g. panics) because checked exceptions and it's recent incarnation as error values are a…

Rust makes a distinction between recoverable and unrecoverable errors. Recoverable errors are the E in Result. You can take action and recover, depending on what kind of E it is.

Unrecoverable errors are things like stack overflows or out of bounds array access. There is no reasonable way to soldier on after this, so the program should just end. Trying to continue the program in such situations only leads to pain. Like array accesses out of bounds that allow you to read unrelated memory.

But it’s still an evolving area. For example, failure to allocate memory - is that recoverable or unrecoverable? Initially it was thought that it was unrecoverable, and programs would panic if memory failed to allocate. This seemed reasonable, until folks tried to use Rust within the Linux kernel. Within the kernel, failure to allocate memory is recoverable. Rust is evolving the semantics here.

All this to say, yes, Rust does allow you to define functions that either fail in a recoverable way, in which case the calling function should handle it. Or they fail in an unrecoverable way in which case there’s nothing the calling function can do to recover. Thankfully, panics in third party code are relatively rare so this doesn’t happen in practice.

Re: Avoid exception throwing in performance-sensitive code

#133
post #52

Earlier quoted context omitted.

Returning an option sumtype is also typesafe. Exceptions have to walk up the stack until a suitable handler is found, that handler can't know ahead of time where the value is coming from or if it will ever arrive - just what type it will be if it arrives. Code emitting exceptions also have no knowledge who (if anyone) is going to handle their output. It is a nonlocal goto in reverse. Compared to regular functional re…

> Code emitting exceptions also have no knowledge who (if anyone) is going to handle their output. This is no different from code returning a regular value. When writing `return -EINVAL;` or `return 0, fmt.Errorf("")` or `return Err(something)`, you have no idea who (if anyone) is going to handle your output. Also, one reasonable way of implementing exceptions could be exactly to translate all functions that can thro…

A normal function return is handled directly by the caller, even if the caller decides to completely ignore it. In the case of exceptions it continues to implicitly pop the stack to attempt to find a handler. The explicit vs implicit nature is quite different.

I'd also point out that in gcc and clang support nodiscard and warn_unused_result giving a function some ability to force callers to handle returns. Go, rust and even java (thanks to errorprone) have similar guardrails.

I think you also need to balance the marginal efficiency wins of not checking for errors on return with the overall robustness of your program. The likelihood that an error condition is properly handled is heavily predicated on your ability to know that it might occur in the first place. In a language where exceptions are common place make this very challenging because they heavily rely on unchecked exceptions. Languages that value error checking have tended to shun exception style in favor of returning option types.

Re: Avoid exception throwing in performance-sensitive code

#134

Earlier quoted context omitted.

Rust do still has exceptions, they're just called "results". You can't really have values as exceptions unless you plug a lot of ad-hoc constructs into your language so that they eventually become similar to exceptions [1]: > Add syntactic sugar for working with the Result type which models common exception handling constructs. The whole error handling story with Rust can be summarized as "we have results but want to…

No! Exceptions have a very well defined meaning. Exceptions in Rust are non-aborting panics.

When people talk about "exceptions", they mean resumable exceptions that are intended to be caught. But "catching" an unwinding panic in Rust is so (deliberately!) limited, cumbersome, and unidiomatic that it doesn't qualify as an implementation of resumable exceptions. The only reason the ability to "catch" an unwinding panic even exists in Rust is to prevent unwinding across FFI boundaries, which would be UB; it's a correctness mechanism, not an error-handling mechanism.

Re: Avoid exception throwing in performance-sensitive code

#136

Earlier quoted context omitted.

At least for Go (haven't used Rust), I entirely disagree. After ~3 years of professional programming with the language, `if err != nil` is still constantly annoying me when writing code, but especially and most importantly, when reviewing code. Not to mention, Go has proven conclusively to me that exceptions are exactly the right pattern for error propagation - the 99.9% pattern is "function returns error with messag…

This is also how errors-as-values work in Rust. Functions that may fail return Result - putting an ? at the end of a failable function call returns T on success, otherwise it propagates E. It reduces result, err := call() if err != nil { return err } to let result = call()?; Completely unobtrusive, but makes failure awareness an obligation.

I know about ? in Rust - the one thing that isn't very clear to me is how often it is enough. That is, with Go, it's quite typical to do something like:

  result, err := call()
  if err != nil {
    return fmt.Errorf("Error while trying to call: %v", err)
  }
Essentially manually building a stack trace. If just doing "return err", you end up with calls to a REST service failing with messages like `couldn't parse "" as int` even in internal logs, which isn't helpful. With exceptions (in any language except C++), the stack trace is often decent enough.

Does ? add any kind of context implicitly, or do you have to actually pattern match manually to add context?

Re: Avoid exception throwing in performance-sensitive code

#137

Earlier quoted context omitted.

In Go the programmer is forced to make a decision on what to do with the error. Common patterns include (A) return an annotated error, (B) log it and continue, (C) retry, (D) aggregate the errors in some way. If you believe the only way to handle an error is (A), then Go's design makes no sense.

You seem to believe options B to D are not available to programmers in languages with exceptions. The real value comes from making option E much more unlikely: ignoring both the result and error value altogether, because you relied on the side effect of the function you called.

I'm talking about unchecked exceptions. It's not about what is possible it's about what patterns a language encourages. It feels like we've lost the thread of discussion.

  - You say there is no difference between unchecked exceptions and Go's errors
  - I say yes there is since Go forces users to handle errors explicitly
  - You say that's not technically true in all cases.
OK. Yes. I should have said "nudges users" instead of force. It's a shortcoming of the language. It is still really hard for me to see unchecked exceptions and value-based error handling as the same thing. One of them encourages doing nothing and hoping that bubbling up is the right answer. Very often, especially in a multi-threaded context, it is not.

Re: Avoid exception throwing in performance-sensitive code

#138

Earlier quoted context omitted.

You might like how Rust does error handling. Rather than a function returning a triple of (val, error) where the error can be ignored, functions return Result . If you want to get the T, you must write code that handles both possibilities. If your function instead wants the T and propagate the E upwards if it exists, you can do that with one character - “?”

I indeed like Rust's approach a lot more than Go's. What I like less still is that it gives the impression that it's even possible to define functions that cannot fail. This is not true. One just has to look at how runtimes deal with stack overflow errors to see how the good old Java RuntimeException creeps in in various forms (e.g. panics) because checked exceptions and it's recent incarnation as error values are a…

> it gives the impression that it's even possible to define functions that cannot fail

Do you mean that e.g. an out-of-bounds error will panic? If that's the case, you can always access arrays/slices with some checked access, that will return a Result/Option and cannot panic. But it would be a PITA if you couldn't skip that.

Re: Avoid exception throwing in performance-sensitive code

#139
post #8

Different languages have different exception handing optimizations. A Java version of the example can run very slow or very fast, depending on how clever you are. When a new RuntimeException is thrown half the time, the example runs about 650 times slower when compared to a function which adds up the integers without using exceptions. If I define an exception subclass which doesn't fill in the stack trace, then it ru…

> converted the immediate throw-catch into a simple goto.

This is very interesting. Both GCC and clang do not do that, as they represent exceptions as abnormal edges out of a basic block and don't optimize further. I guess in Java exceptions are common enough that it is worth the additional effort of transforming some of these cases into normal jumps when the destination is seen, while in C++ it is more of a vicious circle of exceptions not being optimized because they are uncommon and being used sparingly because they are optimized.

It is of course possible that Java exceptions semantics are such that they might be easier to optimize (lots of observable side effects in C++ unfortunately).

Re: Avoid exception throwing in performance-sensitive code

#140

Earlier quoted context omitted.

I indeed like Rust's approach a lot more than Go's. What I like less still is that it gives the impression that it's even possible to define functions that cannot fail. This is not true. One just has to look at how runtimes deal with stack overflow errors to see how the good old Java RuntimeException creeps in in various forms (e.g. panics) because checked exceptions and it's recent incarnation as error values are a…

Rust makes a distinction between recoverable and unrecoverable errors. Recoverable errors are the E in Result . You can take action and recover, depending on what kind of E it is. Unrecoverable errors are things like stack overflows or out of bounds array access. There is no reasonable way to soldier on after this, so the program should just end. Trying to continue the program in such situations only leads to pain. L…

> Unrecoverable errors are things like stack overflows or out of bounds array access. There is no reasonable way to soldier on after this, so the program should just end

No, I wholeheartedly disagree with this. It's the equivalent of exit(1) some way down the stack. Whats recoverable or not depends on the use case and is a decision to be made by the caller of a function, not the implementor.

Post reply on HN