[swift-evolution] ABI of throwing

John McCall rjmccall at apple.com
Tue Aug 9 18:22:08 CDT 2016

> On Aug 9, 2016, at 8:19 AM, Félix Cloutier via swift-evolution <swift-evolution at swift.org> wrote:
>> “Zero cost” EH is also *extremely* expensive in the case where an error is actually throw in normal use cases.  This makes it completely inappropriate for use in APIs where errors are expected in edge cases (e.g. file not found errors).
> Anecdote: I work with a web service that gets several million hits a day. Management loves to use the percentage of succeeding web requests as a measure of overall health. The problem with that metric is that when a web request fails, clients fall in an unhealthy state and stop issuing requests for a while. Therefore, one failing request prevents maybe twenty more that would all have failed if the client hadn't bailed out, but these don't show in statistics. This makes us look much better than we actually are.
> If I had any amount of experience with DTrace, I'd write a script that logs syscall errors to try and see how the programs that I use react to failures. I'm almost certain that when one thing stops working, most programs backs out of a much bigger process and don't retry right away. When a program fails to open a file, it's also failing to read/write to it, or whatever else people normally do after they open files. These things are also expensive, and they're rarely the type of things that you need to (or even just can) retry in a tight loop. My perception is that the immediate cost of failing, even with expensive throwing, is generally dwarfed by the immediate cost of succeeding, so we're not necessarily losing out on much.
> And if that isn't the case, there are alternatives to throwing that people are already embracing, to the point where error handling practices seem fractured.
>>> 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”
>> I’m not sure why you think the Swift team would say something that derogatory.  I hope there is no specific action that has led to this belief. If there is, then please let me know.
> Of course not. All of you have been very nice and patient with us peasants, at least as far as "us" includes me. :) This was meant as a light-hearted reflection on discussing intimate parts of the language, where my best perspective is probably well-understood desktop/server development, whereas the core team has to see that but also needs a high focus on other things that don't even cross my mind (or at least, that's the heroic picture I have of you guys).
> For instance, my "expensive" stops at "takes a while". Your "expensive" might mean "takes a while and drains the very finite energy reserves that we have on this tiny device" or something still more expansive. These differences are not always immediately obvious.
>>> However, as linked above, someone did for Microsoft platforms (for Microsoft-platform-style errors) and found that there is an impact. 
>> C++ and Swift are completely different languages in this respect, so the analysis doesn’t translate over.
> The analysis was (probably?) done over C++ and HRESULTs but with the intention of applying it to another language (Midori), and it most likely validated the approach of other languages (essentially everything .NET-based). Several findings of the Midori team are being exported to Microsoft's new APIs, notably the async everywhere and exceptions everywhere paradigms, and these APIs are callable from both so-called managed programs (GCed) and unmanaged programs (ref-counted).
> Swift operations don't tend to throw very much, which is a net positive, but it seems to me that comparing the impact of Swift throws with another language's throws is relatively fair. C# isn't shy of FileNotFoundExceptions, for instance.

I think you may be missing Chris's point here.

Exception ABIs trade off between two different cases: when the callee throws and when it doesn't.  (There are multiple dimensions of trade-off here, but let's just talk about cycle-count performance.)  Suppose that a compiler can implement a call to have cost C if it just "un-implements" exceptions, the way that a C++ compiler does when they're disabled.  If we hand-wave a bit, we can pretend that all the costs are local and just say that any particular ABI will add cost N to calls that don't throw and cost Y to calls that do.  Therefore, if calls throw with probability P, ABI 1 will be faster than ABI 2 if:
   Y_1 * P +  N_1 * (1 - P) < Y_2 * P +  N_2 * (1 - P)

So what is P?  Well, there's a really important difference between programming languages.

In C++ or C#, you have to compute P as a proportion of every call made by the program.  (Technically, C++ has a way to annotate that a function doesn't throw, and it's possible under very specific circumstances for a C++ or C# implementation to prove that even without an annotation; but for the most part, every call must be assumed to be able to throw.)  Even if exceptions were idiomatically used in C++ for error reporting the way they are in Java and C#, the number of calls to such "failable" functions would still be completely negligible compared to the number of calls to functions that literally cannot throw unless (maybe!) the system runs out of memory.  Therefore, P is tiny — maybe one in a trillion, or one in million in C# if the programmer hasn't yet discovered the non-throwing APIs for testing file existence.  At that kind of ratio, it becomes imperative to do basically anything you can to move costs out of N.

But in Swift, arbitrary functions can't throw.  When computing P, the denominator only contains calls to functions that really can report some sort of ordinary semantic failure.  (Unless you're in something like a rethrows function, but most of those are pretty easy to specialize for non-throwing argument functions.)  So P is a lot higher just to begin with.

Furthermore, there are knock-on effects here.  Error-handling is a really nice way to solve certain kinds of language problem.  (Aside: I keep running into people writing things like JSON deserializers who for some reason insist on making their lives unnecessarily difficult by manually messing around with Optional/Either results or writing their own monad + combinator libraries or what not.  Folks, there's an error monad built into the language, and it is designed exactly for this kind of error-propagation problem; please just use it.)  But we know from experience that the expense (and other problems) of exception-handling in other languages drives people towards other, much more awkward mechanisms when they expect P to be higher, even if "higher" is still just 1 in 100 or so.  That's awful; to us, that's a total language failure.

So the shorter summary of the longer performance argument is that (1) we think that our language choices already make P high enough that the zero-cost trade-offs are questionable and (2) those trade-offs, while completely correct for other languages, are known to severely distort the ways that programmers use exceptions in those languages, leading to worse code and more bugs.  So that's why we aren't using zero-cost exceptions in Swift.

-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20160809/7874c455/attachment.html>

More information about the swift-evolution mailing list