Currently, Swift adds a hidden byref error parameter to propagate thrown errors:

> public func foo() throws {
>       throw FooError.error
> }
> 
> define void @_TF4test3fooFzT_T_(%swift.refcounted* nocapture readnone, 
> %swift.error** nocapture) #0 {
> entry:
>   %2 = tail call { %swift.error*, %swift.opaque* } @swift_allocError(/* snip 
> */)
>   %3 = extractvalue { %swift.error*, %swift.opaque* } %2, 0
>   store %swift.error* %3, %swift.error** %1, align 8
>   ret void
> }

This means that call sites for throwing functions must always check if an 
exception occurred. This makes it essentially equivalent to returning an error 
code in addition to the function's actual return type.

On the other hand, there are exception handling mechanisms where the execution 
time cost in the success case is zero, and the error case is expensive. When 
you throw, the runtime walks through the return addresses on the stack to find 
out if there's an associated catch block that can handle the current exception. 
Apple uses this mechanism 
<https://github.com/apple/swift/blob/master/docs/ErrorHandlingRationale.rst#id29>
 (with the Itanium C++ ABI) for C++ and Objective-C exceptions, at least on 
x86_64.

Other compiler engineers, like Microsoft's Joe Duffy 
<http://joeduffyblog.com/2016/02/07/the-error-model/>, have determined that 
there actually is a non-trivial cost associated to branching for error codes. 
In exchange for faster error cases, you get slower success cases. This is 
mildly unfortunate for throwing functions that overwhelmingly succeed.

As Fernando Rodríguez reports in another thread, you have many options to 
signal errors right now (I took the liberty to add mechanisms that he didn't 
cover):

trapping
returning nil
returning an enum that contains a success case and a bunch of error cases 
(which is really just a generalization of "returning nil")
throwing

With the current implementation, it seems to me that the main difference 
between throwing and returning an enum is that catch works even when you don't 
know what you're catching (but I really hope that we can get typed throws for 
Swift 4, because unless you actually don't know what you're catching, this 
feels like an anti-feature). However, if throwing and returning an enum had 
different-enough performance characteristics, the guidance could become:

return an enum value if you expect that the function will fail often or if 
recovery is expected to be cheap;
throw if you expect that the function will rarely fail or if recovery is 
expected to be expensive for at least one failure reason (for example, if you'd 
have to re-establish a connection after some network error, or if you'd have to 
start over some UI process because the user picked a file that was deleted 
before it could be opened).

Additionally, using the native ABI to throw means that you can throw across 
language boundaries, which might be useful in the possible but distant future 
in which Swift interops with C++. Even though catching from the other language 
will probably be tedious, that would already be useful in language sandwiches 
to unwind correctly (like in Swift that calls C++ that calls Swift, where the 
topmost Swift code throws).

I don't really know what to expect in terms of discussion, especially since it 
may boil down to "we're experts in this fields and you're just peasants" or 
"the cost of changing this isn't worth the benefit". Still, I'd like some more 
insight into why Swift exceptions don't use the same mechanism as C++ 
exceptions and Objective-C exceptions. The error handling rationale document is 
very terse 
<https://github.com/apple/swift/blob/master/docs/ErrorHandlingRationale.rst#id62>
 on the implementation design, especially given the length of the rest of the 
document:

> Error propagation for the kinds of explicit, typed errors that I've been 
> focusing on should be handled by implicit manual propagation. It would be 
> good to bias the implementation somewhat towards the non-error path, perhaps 
> by moving error paths to the ends of functions and so on, and perhaps even by 
> processing cleanups with an interpretive approach instead of directly 
> inlining that code, but we should not bias so heavily as to seriously 
> compromise performance. In other words, we should not use table-based 
> unwinding.

I find the rationale somewhat lacking. I can't pretend that I've measured the 
impact or frequency of retuning a error objects in Objective-C or Swift, and 
given my access to source code, I probably couldn't do a comprehensive study. 
However, as linked above, someone did for Microsoft platforms (for 
Microsoft-platform-style errors) and found that there is an impact. The way 
it's phrased here, it feels like this was chosen as a rule of thumb.

Throwing and unwind tables are all over the place in a lot of languages that 
have exceptions (C++, Java, C#). They throw for a lot of the same reasons that 
Objective-C frameworks returns errors, and people usually seem content with the 
performance. Since Swift is co-opting the exception terminology, I think that 
developers reasonably expect that exceptions will have about the same 
performance cost as in these other languages.

For binary size concerns, since Swift functions have to annotate whether they 
throw or not, unless I'm mistaken, there only needs to be exception handler 
lookup tables for functions that call functions that throw. Java and C# 
compilers can't really decide that because it's assumed that any call could 
throw. (C++ has `noexcept` and could do this for the subset of functions that 
only call `noexcept` functions, but the design requires you to be conscious of 
what can't throw instead of what can.)

Finally, that error handling rationale doesn't really give any strong reason to 
use one of the two more verbose error handling solutions (throwing vs returning 
a complex enum value) over the other.

Félix

_______________________________________________
swift-evolution mailing list
[email protected]
https://lists.swift.org/mailman/listinfo/swift-evolution

Reply via email to