Static Dispatch Pitfalls


(Fabian Ehrentraud) #1

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.

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

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. :slight_smile:

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

···

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:

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:


(Austin Zheng) #2

I almost want to propose forbidding methods in protocol extensions unless they're also a requirement in the protocol itself, but I don't think that would fly.

Austin

···

On May 20, 2016, at 5:56 AM, Fabian Ehrentraud via swift-evolution <swift-evolution@swift.org> wrote:

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. :slight_smile:

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
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Chris Lattner) #3

I don’t think that would fly :-). That said, has anyone considered requiring a keyword on the method in the protocol extension that makes it explicit that the dispatch is non-dynamic?

-Chris

···

On May 20, 2016, at 9:17 AM, Austin Zheng <austinzheng@gmail.com> wrote:

I almost want to propose forbidding methods in protocol extensions unless they're also a requirement in the protocol itself, but I don't think that would fly.


(David Waite) #4

I almost want to propose forbidding methods in protocol extensions unless they're also a requirement in the protocol itself, but I don't think that would fly.

Austin

Thats one option.

Think two separate developers - only who implements the protocol Foo and has an independent bar() method on their type, and one who writes an extension to Foo to add a bar method on it.

bar() is not part of the protocol specification. the person implementing the type has no idea that others are expecting his bar() to meet particular requirements, just because it was declared in a protocol extension.

So silently using the type implementor’s bar() method when using Foo is unacceptable. You don’t know if it meets the requirements, because Foo never specified requirements for a bar() method.

BTW, it would also be dangerous to let an extension make a protocol implement another protocol for similar reasons.

So, options to solve:
- Having the extension to protocol Foo only apply when dealing in terms of Foo type and not the implementors type. This sounds less useful than what we have today.
- You could have a warning if the compiler sees a Foo extension and a Foo implementation both with bar(). Hopefully the application developer has control over either the protocol definition, the extension, or the implementation of the protocol to try and resolve it.
- forbid protocol extensions adding methods - they can only implement existing methods
- If protocol implementations used “override” (or perhaps a better named keyword for covering this instance as well like “implement”), you could consider protocol extensions to extend the protocol definition more safely. You would also catch the case where the Protocol definition changed to include the same foo() method the implementing type defined independently, so the implementor can make sure their version of foo() meets the requirements the protocol gives.

-DW

···

On May 20, 2016, at 10:17 AM, Austin Zheng via swift-evolution <swift-evolution@swift.org> wrote:

On May 20, 2016, at 5:56 AM, Fabian Ehrentraud via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

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. :slight_smile:

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
swift-evolution@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Dave Abrahams) #5

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.

Being in the run-up to WWDC, I don't really have the time to participate
in this discussion right now, although I think it is important and
interesting. However, this last sentence caught my eye and I thought I
should clarify something: protocols unify static and dynamic dispatch,
and one is not inherently better than the other. They have different
strengths. Protocol-oriented programming is about leveraging those
strengths appropriately, not about a mandate to choose one or the other
form of dispatch.

···

on Fri May 20 2016, Fabian Ehrentraud <swift-evolution@swift.org> wrote:

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. :slight_smile:

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
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

--
-Dave


(David Waite) #6

Assuming you have protocol extenders and protocol implementors as different people, they are competing in the same problem space. The odds of an unintentional name collision is higher than normal, in which case the person implementing a protocol just unknowingly changed behavior for everyone relying on that extension.

The implementors can’t look at the protocol to find out the list of requirements on the protocol anymore - they have to also find every extension.

-DW

···

On May 20, 2016, at 10:43 AM, Brandon Knope via swift-evolution <swift-evolution@swift.org> wrote:

From the compilers/runtimes perspective why couldn't the declaration automatically be included in the main protocol definition as if it was defined there?


(Timothy J. Wood) #7

This would be fabulous. Of course, if you could mark it dynamic too to get that behavior that would be great, but preventing the error in the first place seems great.

`static` is taken already. `nondynamic`, `direct`, …? `nondynamic` seems the least surprising term to me.

-tim

···

On May 20, 2016, at 10:35 AM, Chris Lattner via swift-evolution <swift-evolution@swift.org> wrote:

I don’t think that would fly :-). That said, has anyone considered requiring a keyword on the method in the protocol extension that makes it explicit that the dispatch is non-dynamic?


(Matthew Johnson) #8

I almost want to propose forbidding methods in protocol extensions unless they're also a requirement in the protocol itself, but I don't think that would fly.

That would prevent us from adding extension methods to protocols declared in modules we depend on. This is way to useful to throw away.

···

On May 20, 2016, at 11:17 AM, Austin Zheng via swift-evolution <swift-evolution@swift.org> wrote:

Austin

On May 20, 2016, at 5:56 AM, Fabian Ehrentraud via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

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. :slight_smile:

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
swift-evolution@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Matthew Johnson) #9

From the compilers/runtimes perspective why couldn't the declaration automatically be included in the main protocol definition as if it was defined there?

That has been discussed as a possible solution but introduces implementation complexity. I believe core team members have commented on the idea in the past. I don’t recall the details but you should be able to find the discussions in the archives if you’re interested.

···

On May 20, 2016, at 11:43 AM, Brandon Knope <bknope@me.com> wrote:

Brandon

On May 20, 2016, at 12:21 PM, Matthew Johnson via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

On May 20, 2016, at 11:17 AM, Austin Zheng via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

I almost want to propose forbidding methods in protocol extensions unless they're also a requirement in the protocol itself, but I don't think that would fly.

That would prevent us from adding extension methods to protocols declared in modules we depend on. This is way to useful to throw away.

Austin

On May 20, 2016, at 5:56 AM, Fabian Ehrentraud via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

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. :slight_smile:

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
swift-evolution@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution


(Matthew Johnson) #10

From the compilers/runtimes perspective why couldn't the declaration automatically be included in the main protocol definition as if it was defined there?

Assuming you have protocol extenders and protocol implementors as different people, they are competing in the same problem space. The odds of an unintentional name collision is higher than normal, in which case the person implementing a protocol just unknowingly changed behavior for everyone relying on that extension.

The implementors can’t look at the protocol to find out the list of requirements on the protocol anymore - they have to also find every extension.

Thanks. These are some of the issues I mentioned that were discussed quite a bit in earlier threads. I suggest anyone who is interested in this problem catch up on those threads before moving any further in this discussion.

···

On May 20, 2016, at 11:52 AM, David Waite <david@alkaline-solutions.com> wrote:

On May 20, 2016, at 10:43 AM, Brandon Knope via swift-evolution <swift-evolution@swift.org> wrote:

-DW


(Brandon Knope) #11

From the compilers/runtimes perspective why couldn't the declaration automatically be included in the main protocol definition as if it was defined there?

Brandon

···

On May 20, 2016, at 12:21 PM, Matthew Johnson via swift-evolution <swift-evolution@swift.org> wrote:

On May 20, 2016, at 11:17 AM, Austin Zheng via swift-evolution <swift-evolution@swift.org> wrote:

I almost want to propose forbidding methods in protocol extensions unless they're also a requirement in the protocol itself, but I don't think that would fly.

That would prevent us from adding extension methods to protocols declared in modules we depend on. This is way to useful to throw away.

Austin

On May 20, 2016, at 5:56 AM, Fabian Ehrentraud via swift-evolution <swift-evolution@swift.org> wrote:

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> 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> 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. :slight_smile:

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
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Charles Srstka) #12

Implementers have to find every extension *already*, since there’s no other way to know if a method on a protocol actually needs to be implemented, or whether it already has a default implementation. Failure to do so results in a lot of reinventing the wheel.

Charles

···

On May 20, 2016, at 11:52 AM, David Waite via swift-evolution <swift-evolution@swift.org> wrote:

On May 20, 2016, at 10:43 AM, Brandon Knope via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

From the compilers/runtimes perspective why couldn't the declaration automatically be included in the main protocol definition as if it was defined there?

Assuming you have protocol extenders and protocol implementors as different people, they are competing in the same problem space. The odds of an unintentional name collision is higher than normal, in which case the person implementing a protocol just unknowingly changed behavior for everyone relying on that extension.

The implementors can’t look at the protocol to find out the list of requirements on the protocol anymore - they have to also find every extension.


(Matthew Johnson) #13

I almost want to propose forbidding methods in protocol extensions unless they're also a requirement in the protocol itself, but I don't think that would fly.

I don’t think that would fly :-). That said, has anyone considered requiring a keyword on the method in the protocol extension that makes it explicit that the dispatch is non-dynamic?

I believe `final` has been discussed previously for this purpose. IIRC the intent was to *disallow* the type to have its own method with the same signature. However, that interacts poorly with retroactive conformance which means `final` is probably not best choice.

Before we introduce a keyword I think we should be sure the current semantics are what we plan to stick with. I haven’t seen any good alternatives proposed, but people do keep bringing up this topic. The desire is usually to change the semantics, not just clarify the existing semantics.

···

On May 20, 2016, at 12:35 PM, Chris Lattner via swift-evolution <swift-evolution@swift.org> wrote:

On May 20, 2016, at 9:17 AM, Austin Zheng <austinzheng@gmail.com> wrote:

-Chris
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Brandon Knope) #14

I almost want to propose forbidding methods in protocol extensions unless they're also a requirement in the protocol itself, but I don't think that would fly.

Austin

Thats one option.

Think two separate developers - only who implements the protocol Foo and has an independent bar() method on their type, and one who writes an extension to Foo to add a bar method on it.

bar() is not part of the protocol specification. the person implementing the type has no idea that others are expecting his bar() to meet particular requirements, just because it was declared in a protocol extension.

How is this the case? Don't you have to expect that others can use your protocol extension since it is visible to them?

I think the static behavior should be an opt in feature so the common case is what everyone expects to happen.

···

On May 20, 2016, at 12:46 PM, David Waite via swift-evolution <swift-evolution@swift.org> wrote:

On May 20, 2016, at 10:17 AM, Austin Zheng via swift-evolution <swift-evolution@swift.org> wrote:

So silently using the type implementor’s bar() method when using Foo is unacceptable. You don’t know if it meets the requirements, because Foo never specified requirements for a bar() method.

BTW, it would also be dangerous to let an extension make a protocol implement another protocol for similar reasons.

So, options to solve:
- Having the extension to protocol Foo only apply when dealing in terms of Foo type and not the implementors type. This sounds less useful than what we have today.
- You could have a warning if the compiler sees a Foo extension and a Foo implementation both with bar(). Hopefully the application developer has control over either the protocol definition, the extension, or the implementation of the protocol to try and resolve it.
- forbid protocol extensions adding methods - they can only implement existing methods
- If protocol implementations used “override” (or perhaps a better named keyword for covering this instance as well like “implement”), you could consider protocol extensions to extend the protocol definition more safely. You would also catch the case where the Protocol definition changed to include the same foo() method the implementing type defined independently, so the implementor can make sure their version of foo() meets the requirements the protocol gives.

-DW

On May 20, 2016, at 5:56 AM, Fabian Ehrentraud via swift-evolution <swift-evolution@swift.org> wrote:

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> 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> 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. :slight_smile:

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
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Brandon Knope) #15

Oh right! Doh!!

I looked at it too simply :frowning:

Brandon

···

On May 20, 2016, at 12:52 PM, David Waite <david@alkaline-solutions.com> wrote:

On May 20, 2016, at 10:43 AM, Brandon Knope via swift-evolution <swift-evolution@swift.org> wrote:

From the compilers/runtimes perspective why couldn't the declaration automatically be included in the main protocol definition as if it was defined there?

Assuming you have protocol extenders and protocol implementors as different people, they are competing in the same problem space. The odds of an unintentional name collision is higher than normal, in which case the person implementing a protocol just unknowingly changed behavior for everyone relying on that extension.

The implementors can’t look at the protocol to find out the list of requirements on the protocol anymore - they have to also find every extension.

-DW


(Brandon Knope) #16

I was kind of alluding to this when I said it should be opt in...or at least I thought I mentioned it :joy:.

What keyword name could clearly convey this meaning?

Brandon

···

On May 20, 2016, at 1:35 PM, Chris Lattner via swift-evolution <swift-evolution@swift.org> wrote:

On May 20, 2016, at 9:17 AM, Austin Zheng <austinzheng@gmail.com> wrote:

I almost want to propose forbidding methods in protocol extensions unless they're also a requirement in the protocol itself, but I don't think that would fly.

I don’t think that would fly :-). That said, has anyone considered requiring a keyword on the method in the protocol extension that makes it explicit that the dispatch is non-dynamic?

-Chris
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(L Mihalkovic) #17

Regards
(From mobile)

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

>> 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
>

>
> 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.

Cpp templates were the promise that by trading longer compiler time, we would be able to produce c-like performing code while coding with the flexibility of virtual dispatch. There are some great debates on whether or not the compilers actually deliver it.

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.

Java the following calling conventions
Invokespecial: constructors
Invokestatic: static methods. Need to resolve (heuristic is static)
Invokevirtual: obj type methods. Need to resolve (static heuristic)
Invokeinterface: iface type methods. Needs to resolve (static heuristic)
Invokedynamic: indy is special because the resolution heuristic is user customizable and even reversible at runtime (user code can rewrite the behavior of a callsite) all the while being highly optimized inside the jit compiler (lambdas use it & jdk9 will optimize string concats with it). this is a huge improvement in jvms.

Graal is a upcoming(?) jit implemented in java that supports more agressive per call site optimizations (like same target code inlined differently depending on callsite behavior, including optimistic type specializations per code paths, all reversible). It is a promissing long term solution for the megamorphic callsite performance cliff problem in the jvm.

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!

Scala is another of these interesting animals where the compiler is working super hard to try to produce magic-free runtime code (i.e. no extra lookup other then the jvm's)... not the case yet (the llvm scala backend is interesting though).

···

On May 20, 2016, at 2:56 PM, Fabian Ehrentraud via swift-evolution <swift-evolution@swift.org> wrote:

On 4 Mar 2016, at 08:58, Brent Royal-Gordon <brent at architechies.com> wrote:
> On Dec 11, 2015, at 8:56 PM, Kevin Ballard via swift-evolution <swift-evolution at swift.org> wrote:

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

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
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(L Mihalkovic) #18

Regards
(From mobile)

From the compilers/runtimes perspective why couldn't the declaration automatically be included in the main protocol definition as if it was defined there?

That's just the point, these 2 have very different views of the world. Trying to *fool* the runtime would mean building into it the kind of runtime support that exist in the objc runtime and that disappeared with swift. Part of swift's power is that the compiler is far smarter, making it possible to go back to a dumb (i.e fast) runtime. Method swizzling is fun... but it is slow. Although it is arguable that with the CPUs we now have, who cares if each instruction is actually a complete program, right?... except in the iot,embedded,wearable,mobile world where apple operates!!!
So we DO care that the compiler is orders of magnitude smater and that the runtime does not do vtable lookups on every call, even if we loose a couple things for now.

···

On May 20, 2016, at 6:43 PM, Brandon Knope via swift-evolution <swift-evolution@swift.org> wrote:

Brandon

On May 20, 2016, at 12:21 PM, Matthew Johnson via swift-evolution <swift-evolution@swift.org> wrote:

On May 20, 2016, at 11:17 AM, Austin Zheng via swift-evolution <swift-evolution@swift.org> wrote:

I almost want to propose forbidding methods in protocol extensions unless they're also a requirement in the protocol itself, but I don't think that would fly.

That would prevent us from adding extension methods to protocols declared in modules we depend on. This is way to useful to throw away.

Austin

On May 20, 2016, at 5:56 AM, Fabian Ehrentraud via swift-evolution <swift-evolution@swift.org> wrote:

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> 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> 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. :slight_smile:

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
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(L Mihalkovic) #19

Regards
(From mobile)

From the compilers/runtimes perspective why couldn't the declaration automatically be included in the main protocol definition as if it was defined there?

That has been discussed as a possible solution but introduces implementation complexity. I believe core team members have commented on the idea in the past. I don’t recall the details but you should be able to find the discussions in the archives if you’re interested.

Not to mention the security risk it represents.... yeah!!! an apple provided official way to patch any dylib during load time.

···

On May 20, 2016, at 6:52 PM, Matthew Johnson via swift-evolution <swift-evolution@swift.org> wrote:

On May 20, 2016, at 11:43 AM, Brandon Knope <bknope@me.com> wrote:

Brandon

On May 20, 2016, at 12:21 PM, Matthew Johnson via swift-evolution <swift-evolution@swift.org> wrote:

On May 20, 2016, at 11:17 AM, Austin Zheng via swift-evolution <swift-evolution@swift.org> wrote:

I almost want to propose forbidding methods in protocol extensions unless they're also a requirement in the protocol itself, but I don't think that would fly.

That would prevent us from adding extension methods to protocols declared in modules we depend on. This is way to useful to throw away.

Austin

On May 20, 2016, at 5:56 AM, Fabian Ehrentraud via swift-evolution <swift-evolution@swift.org> wrote:

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> 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> 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. :slight_smile:

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
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Krystof Vasa) #20

How about the other way around, allowing something like this, explicitly specifying the dynamic nature of the function, thus forcing the dynamic dispatch?

protocol MyProtocol { }

extension MyProtocol {
  dynamic func getInt() -> Int {
    return 0
  }
}

Krystof

···

I don’t think that would fly :-). That said, has anyone considered requiring a keyword on the method in the protocol extension that makes it explicit that the dispatch is non-dynamic?

-Chris
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution