Advanced Swift: Generics and Protocols, Episode 3: Generic Constraints | Kodeco, the new raywenderlich.com

Let's see how you can use protocol constraints to inform the type system about the capabilities of a type.


This is a companion discussion topic for the original entry at https://www.kodeco.com/1940187-advanced-swift-generics-and-protocols/lessons/3

Hi Team,

I thought I understood the Existential containers, but the last few sentences of the video confused me a bit. Could you help me out?

We ended up with the following code:

@inlinable func add<T: Numeric>(_ a: T, _ b: T) -> T {
  a + b
}

add(3, 4)
add(UInt8(20), UInt8(33))

Then Ray says this:

…The compiler now passes along the witness table for Numeric under the hood.
As long as the witness exists for that type this function will now compile and it can use this witness table to call the plus operation. The Swift compiler can hide away the actual implementation and just use the witness table. You might be wondering if it is slower, and it is…

Here are my questions:

  • Which Witness Table is Ray referring to?

I assume Ray is referring to the Protocol Witness Table, but I thought that we only use PWTs when we use Existential Containers and, thanks to the Generics, we aren’t actually going to be using Existential container here as the compiler will determine the types during the compilation time and therefore can pre-calculate the memory requirements during the compilation. Am I completely off?

You might be wondering if it is slower, and it is…

Can you explain this sentence? What is slower than what? Is Ray referring to compilation time or runtime? This is slower than what alternative?

1 Like

With Swift generics, the compiler generates a witness table based implementation so that it can use any type you pass to it. So suppose you have a method like calculate(input: some Collection<Int>) The compiler will generate a vanilla method that can work will all collections of integers, even exotic collections types that it doesn’t have source code for.

If the compiler does have the source code for the collection type, such as Array or Dictionary, it might make specializations of the calculate method that devirtualize all of the collection methods.

It has been a while since I made that video, but I am guessing that is what I was trying to say. :]

You might check out these slides from a recent talk (Japanese) but the slides are mostly English that describes using the internal directive @_specialized to get a 20X speedup.

1 Like

Hi Ray,

Thank you, I sincerely appreciate you responding! I find this part to be rather tricky as I’m wrapping my head around all of this for the past few days.

It doesn’t help that there seem to be very little documentation on these topics, so the videos you have put together are helping a lot!

Here is how I currently understand things, I’d appreciate you correcting me.

Let’s say we have a protocol:

protocol Drivable {
  func drive() -> String
}

And two structs that implement it:

struct Motorcycle: Drivable {
  func drive() -> String {
    "The motorcycle is driving"
  }
}

struct Car: Drivable {
  func drive() -> String {
    "The car is driving"
  }
}

We have two options passing a object conforming to the protocol as a parameter to a function:

// OPTION A
func useVehicle(_ vehicle: Drivable) {
  print("You have entered a vehicle")
  print(vehicle.drive())
}

// OPTION B
func useVehicle2<D: Drivable>(_ vehicle: D) {
  print("You have entered a vehicle")
  print(vehicle.drive())
}

And then we have the following code that uses it:

let car: Drivable = Car()
let motorcycle: Drivable = Motorcycle()
useVehicle(car)
useVehicle(motorcycle)
useVehicle2(car)
useVehicle2(motorcycle)

With Option A the compiler doesn’t know which exact type we will be using during the compilation, so it will have to wrap the input parameters into existential containers which would contain the Value Witness Table and a Protocol Witness Table and then the program will use dynamic method dispatch to call the drive function using the protocol witness table. This gives us a lot of flexibility as we can pass any object, but it is slower during the runtime.

With Option B the compiler can see which exact type will be used in both cases. It will create two copies of the function, one of the Motorcycle type and the other for the Car type. The compiler will not use existential containers, there are no witness tables, and it will use static dispatch which will be a lot more performant.

Is my understanding wrong? More importantly, is there a way to experiment and validate my understanding? Are there any tools that I can use to see if the compiler is using dynamic vs static dispatch and when the application is using the heap vs the stack to allocate variables? I’ve watched a bunch of videos and read a few articles, but there is so little information that goes deep.

Hi!

I think your mental model is good. Of course, when it comes to optimization there are always tricky nuances that don’t apply all of the time. I have found a good way to test out theories is to use the site https://godbolt.org (make sure to set to Swift and use -O for optimization). I put your code in to look at what Swift 5.7 generates.

Here are a few side thoughts:

Your Option A can also be spelled func useVehicle(_ vehicle: any Drivable). The any underscores that you are using an existential (box) type. Just like you said, this implies that you are going to be using dynamic dispatch. For Swift 6, there is talk of requiring that you use any here. (Currently it is only required if the protocol has an associated type.)

For option B, yes, this is a full generic that the compiler can specialize (and even inline) if it decides to. However, I believe the compiler also makes a version of the method that uses @PLTs (I think this stands for protocol lookup table, but I am not sure). I think they call this being passed as an “opaque existential.” (When I did the video, I don’t think “opaque” types existed yet in Swift. At least, it wasn’t a common term then.)

It creates opaque existential implementations so that if you link to a library (no source code) that has a Spaceship conforming to vehicle, it can work with it.

Maybe you are aware of this but your Option B can now be written useVehicle2(_ vehicle: some Vehicle) {...} which is exactly the same as yours. vehicle is called an opaque Vehicle.

Hi Ray,

Thank you so much! Really appreciate your help. It’s been way too long since I have used assembly, but you are right, I’ve got to pick it up again if I want to see the real low-level stuff. I’ve bookmarked the link.

I am aware of the existence of the Opaque types, but I can’t say that I fully understand the implication of their existence yet. I’ll keep reading the materials.

Thanks again, and thank you for the videos!

1 Like