Hi,

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.

-Chris

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

Reply via email to