[swift-evolution] Proposal: Universal dynamic dispatch for method calls

Chris Lattner clattner at apple.com
Sat Dec 12 01:45:18 CST 2015

> On Dec 11, 2015, at 8:56 PM, Kevin Ballard via swift-evolution <swift-evolution at swift.org> 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/20151211/bca201a4/attachment.html>

More information about the swift-evolution mailing list