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