[swift-evolution] Static Dispatch Pitfalls

Fabian Ehrentraud Fabian.Ehrentraud at willhaben.at
Fri May 20 07:56:36 CDT 2016


there's been a little discussion about static vs. dynamic dispatch on this mailing list, and there is a good post about the pitfalls when using attributes defined in extensions [1].

Having run into this myself during development, is there a plan on how to reduce the pitfalls in future versions of Swift?

- Fabian

[1] https://developer.ibm.com/swift/2016/01/27/seven-swift-snares-how-to-avoid-them/

Sorry, I understand and appreciate your pragmatism. Right now it feels very much like a fight to the ideological death between POP and OOP and it may get really bad results this way.

Sent from my iPhone

On 4 Mar 2016, at 08:58, Brent Royal-Gordon <brent at architechies.com<https://lists.swift.org/mailman/listinfo/swift-evolution>> wrote:

>> Brent, why is dynamic dispatching for protocol extension default implementations wrong in your mind? Wouldn't you agree that when static dispatching introduces such a side effect that it should not be automatically applied and perhaps a keyword should be added if you really wanted static dispatching nonetheless?
>> I think that code execution should not be affected by type casting, it feels like a very confusing part of the language.
> I don't think dynamic dispatch is wrong; I think it's a large and technically challenging change. So in the spirit of incrementalism, I was trying to make cautious proposals which kept existing semantics intact but made them clearer, in preparation for perhaps eventually introducing dynamic dispatch. (Basically, I suggested that non-overridable protocol extension members should be marked `final` and it should be illegal to shadow them.)
> But the feedback I got indicated that most people wanted a more aggressive proposal which introduced dynamic dispatch immediately. That's much harder to propose because it touches on all sorts of runtime implementation details I know nothing about, so I didn't try to draft a proposal.
> (You are, perhaps inadvertently, currently demonstrating exactly what happened in those previous threads!)
> --
> Brent Royal-Gordon
> Architechies

> On Dec 11, 2015, at 8:56 PM, Kevin Ballard via swift-evolution <swift-evolution at swift.org<https://lists.swift.org/mailman/listinfo/swift-evolution>> wrote:
> You think that Swift prefers virtual dispatch. I think it prefers static.
> I think what's really going on here is that _in most cases_ there's no observable difference between static dispatch and virtual dispatch. If you think of Swift as an OOP language with a powerful value-typed system added on, then you'll probably think Swift prefers virtual dispatch. If you think of Swift as a value-typed language with an OOP layer added, then you'll probably think Swift prefers static dispatch. In reality, Swift is a hybrid language and it uses different types of dispatch in different situations as appropriate.

(emphasis mine)

I know that this is a bit philosophical, but let me suggest a “next level down” way to look at this.  Static and dynamic are *both* great after all, and if you’re looking to type-cast languages, you need to consider them both in light of their semantics, but also factor in their compilation strategy and the programmer model that they all provide.  Let me give you some examples, but keep in mind that this is a narrow view and just MHO:

1. C: Static compilation model, static semantics.  While it does provide indirect function pointers, C does everything possible to punish their use (ever see the non-typedef'd prototype for signal(3/7)?), and is almost always statically compiled.  It provides a very “static centric” programming model.  This is great in terms of predictability - it makes it trivial to “predict” what your code will look like at a machine level.

2. Javascript: Completely dynamic compilation model, completely dynamic semantics.  No one talks about statically compiling javascript, because the result of doing so would be a really really slow executable.  Javascript performance hinges on dynamic profile information to be able to efficiently execute a program.  This provides a very “dynamic centric” programming model, with no ability to understand how your code executes at a machine level.

3. C++: C++ is a step up from C in terms of introducing dynamism into the model with virtual functions.   Sadly, C++ also provides a hostile model for static optimizability - the existence of placement new prevents a lot of interesting devirtualization opportunities, and generally makes the compiler’s life difficult.  OTOH, like C, C++ provides a very predictable model: C++ programmers assume that C constructs are static, but virtual methods will be dynamically dispatched.  This is correct because (except for narrow cases) the compiler has to use dynamic dispatch for C++ virtual methods.   The good news here is that its dynamism is completely opt in, so C++ preserves all of the predictability, performance, and static-compilability of C while providing a higher level programming model.  If virtual methods are ever actually a performance problem, a C++ programmer has ways to deal with that, directly in their code.

4. Java: Java makes nearly "everything" an object (no structs or other non-primitive value types), and all methods default to being “virtual” (in the C++ sense).  Java also introduces interfaces, which offer an added dimension on dynamic dispatch.  To cope with this, Java assumes a JIT compilation model, which can use dynamic behavior to de-virtualize the (almost always) monomorphic calls into checked direct calls.  This works out really well in practice, because JIT compilers are great at telling when a program with apparently very dynamic semantics actually have static semantics in practice (e.g. a dynamic call has a single receiver).  OTOH, since the compilation model assumes a JIT, this means that purely “AOT” static compilers (which have no profile information, no knowledge of class loaders, etc) necessarily produce inferior code.  It also means that Java doesn’t “scale down” well to small embedded systems that can’t support a JIT, like a bootloader.

5) Objective-C: Objective-C provides a hybrid model which favors predictability due to its static compilation model (similar in some ways to C++).  The C-like constructs provide C-like performance, and the “messaging” constructs are never “devirtualized”, so they provide very predictable performance characteristics.  Because it is predictable, if the cost of a message send ever becomes an issue in practice, the programmer has many patterns to deal with it (including "imp caching", and also including the ability to define the problem away by rewriting code in terms of C constructs).  The end result of this is that programmers write code which use C-level features where performance matters and dynamicism doesn’t, but use ObjC features where dynamicism is important or where performance doesn’t matter.

While it would be possible to implement a JIT compiler for ObjC, I’d expect the wins to be low, because the “hot” code which may be hinging on these dynamic features is likely to already be optimized by hand.

6) GoLang: From this narrow discussion and perspective, Go has a hybrid model that has similar characteristics to Objective-C 2013 (which introduced modules, but didn’t yet have generics).  It assumes static compilation and provides a very predictable hybrid programming model.  Its func’s are statically dispatched, but its interfaces are dynamically dispatched.  It doesn’t provide guaranteed dynamic dispatch (or “classes") like ObjC, but it provides even more dynamic feautres in other areas (e.g. it requires a cycle-collecting garbage collector).  Its "interface{}” type is pretty equivalent to “id” (e.g. all uses of it are dynamically dispatched or must be downcasted), and it encourages use of it in the same places that Objective-C does.  Go introduces checked downcasts, which introduce some run-time overhead, but also provide safety compared to Objective-C. Go thankfully introduces a replacement for the imperative constructs in C, which defines away a bunch of C problems that Objective-C inherited, and it certainly is prettier!

… I can go on about other languages, but I have probably already gotten myself into enough trouble. :-)

With this as context, lets talk about Swift:

Swift is another case of a hybrid model: its semantics provide predictability between obviously static (structs, enums, and global funcs) and obviously dynamic (classes, protocols, and closures) constructs.  A focus of Swift (like Java and Javascript) is to provide an apparently simple programming model.  However, Swift also intentionally "cheats" in its global design by mixing in a few tricks to make the dynamic parts of the language optimizable by a static compiler in many common cases, without requiring profiling or other dynamic information..  For example, the Swift compiler can tell if methods in non-public classes are never overridden (and non-public is the default, for a lot of good reasons) - thus treating them as final.  This allows eliminating much of the overhead of dynamic dispatch without requiring a JIT.  Consider an “app”: because it never needs to have non-public classes, this is incredibly powerful - the design of the swift package manager extends this even further (in principle, not done yet) to external libraries. Further, Swift’s generics provide an a static performance model similar to C++ templates in release builds (though I agree we need to do more to really follow through on this) -- while Swift existentials (values of protocol type) provide a balance by giving a highly dynamic model.

The upshot of this is that Swift isn’t squarely in either of the static or dynamic camps: it aims to provide a very predictable performance model (someone writing a bootloader or firmware can stick to using Swift structs and have a simple guarantee of no dynamic overhead or runtime dependence) while also providing an expressive and clean high level programming model - simplifying learning and the common case where programmers don’t care to count cycles.  If anything, I’d say that Swift is an “opportunistic” language, in that it provides a very dynamic “default" programming model, where you don’t have to think about the fact that a static compiler is able to transparently provide great performance - without needing the overhead of a JIT.

Finally, while it is possible that a JIT compiler might be interesting someday in the Swift space, if we do things right, it will never be “worth it” because programmers will have enough ability to reason about performance at their fingertips.  This means that there should be no Java or Javascript-magnitude "performance delta" sitting on the table waiting for a JIT to scoop up.  We’ll see how it works out long term, but I think we’re doing pretty well so far.

TL;DR: What I’m really getting at is that the old static vs dynamic trope is at the very least only half of the story.  You really need to include the compilation model and thus the resultant programmer model into the story, and the programmer model is what really matters, IMHO.


-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20160520/04d686b8/attachment.html>

More information about the swift-evolution mailing list