In a talk last week at Swift Summit, I spoke a bit about Swift and performance, and about how structs and generic functions in Swift allow you to write high-level code without paying a significant performance penalty. This built on the example I posted a couple of weeks back about sorting nibbles in Swift.
Towards the end, I gave a couple of lines of code that seemingly behave the same, but are actually subtly different:
// given a protocol with no Self or associated type requirements, // such as Printable (one of the few like this in the standard library) // how is this... func f(x: Printable) { } // different from this... func g<T: Printable>(x: T) { }
This article is going to look at the differences between the two. Warning – some of the behaviour below is undocumented, and subject to change. All the code has been tested on Swift 1.2 beta 4, but may behave differently in future versions. Also, all size numbers assume a 64-bit platform.
Functional Differences
So what is the difference between the two? In the first case, the function takes an argument with the type of the protocol Printable
, and can then access the argument value via any of the methods that protocol provides.
In the second case, g
takes an argument of any type T
that conforms to Printable
. This means T
is the type of whatever was passed in as the argument. If you pass in an Int
, then T
is an Int
. If you pass in an array of integers, then T
is an [Int]
.
For most purposes, this distinction doesn’t matter when implementing the body of the function. Even though T
might be an Int
or an [Int]
, you can’t use any properties not guaranteed by Printable
because the function needs to be valid for any kind of type that conforms to Printable
. So you can’t call x.successor()
or x.count
. Only x.description
.
The most important functional difference between the two forms comes when the function returns a value. Suppose you wanted to implement your own version of the standard library’s abs
function. It could look something like this:
func myAbs<T: SignedNumberType>(x: T) -> T { if x < 0 { return -x } else { return x } } // myAbs works for any kind of signed number // (e.g. Int, Int64, Float, Double etc) let i: Int8 = -4 let j = myAbs(i) // j will be of type Int8, with value 4
This function relies on 3 things provided by SignedNumberType
: a negation operator, a less-than operator (via SignedNumberType
conforming to Comparable
), and the ability to create a zero of the same type for comparison (via IntegerLiteralConvertible
). It compares to zero and then returns the original value or its negation. Importantly, it returns the same type that was passed in. If you pass in an Int8
, you get back an Int8
. If you passed in a Double
, you get back a Double
. If this function were implemented using protocols,1 you’d get back the type of the protocol. Not only would that be inconvenient for type inference purposes, you’d also probably need to cast the value back to the type you wanted using as!
in order to make use of it.
Poking at Protocols
So aside from that, how else do the two functions differ? Well, there are still some ways you can tell the difference between the protocol and the T
placeholder. For example, you can look at the size of the value:
func takesProtocol(x: Printable) { // this will print "40" every time: println(sizeofValue(x)) } func takesPlaceholder<T: Printable>(x: T) { // this will print whatever the size of // the argument type is (for example, Int64 // is 8, Int8 is 1, class references are 8) println(sizeofValue(x)) } takesProtocol(1 as Int16) // prints 40 takesPlaceholder(1 as Int16) // prints 2 class MyClass: Printable { var description: String { return "MyClass" } } takesProtocol(MyClass()) // prints 40 takesPlaceholder(MyClass()) // prints 8
So it looks like Printable
is some kind of fixed-sized box that holds any kind of value that is printable. This kind of boxing is quite a common feature in other languages like Java and C#. But here even references to classes are put inside this 40-byte box, which might surprise you if you‘re used to thinking of protocols as like references to pure virtual base classes. This Swift box is geared up to hold both value and reference types.
edit: as @norio_nomura points out, class-only protocols are smaller, at 16 bytes, as they never need to hold a larger-sized payload.
So what’s inside those 40 bytes? Mike Ash had a great series of posts in his Friday Q&A column about poking around inside Swift’s memory layout. Here we’re just going to do a much shorter bit of code to look into contents of the protocol:
// function to dump out the contents of a protocol variable func dumpPrintable(var p: Printable) { // you could also do this with unsafeBitCast withUnsafePointer(&p) { ptr -> Void in let intPtr = UnsafePointer<Int>(ptr) for i in stride(from: 0, to: (sizeof(Printable)/8), by: 1) { println("\(i):\t 0x\(String(intPtr[i], radix: 16))") } } } let i = Int(0xb1ab1ab1a) dumpPrintable(i) // prints out: 0: 0xb1ab1ab1a 1: 0x7fff5ad10f48 2: 0x2000000000 3: 0x10507bfa8 4: 0x105074780
With a single 8-byte integer, the protocol appears to pack the value into the protocol value itself. The next two words look like uninitialized memory (their contents vary on each run), and then the last two are pointers to some metadata about the type that is actually being held. (Mike’s article goes into more detail about this metadata).
If you create a custom struct of size 16 or 24, this is also held within the first 3 words of the protocol. But if you go above this, it switches over to holding a pointer to the referenced value:
struct FourInts: Printable { var a = 0xaaaa var b = 0xbbbb var c = 0xcccc var d = 0xdddd var description: String { return toString(a,b,c,d) } } dumpPrintable(FourInts(), FourInts.self) // prints out: 0: 0x7f8b5840fb90 // this is a pointer to a FourInts value 1: 0xaaaa // uninitialized garbage (?) 2: 0xbbbb // ditto 3: 0x10dde52a8 // metadata 4: 0x10dde51b8 // metadata
How to know that first part is a pointer to a FourInts
type? Well, you can dereference it and see! We need to amend our dumping function slightly to tell it what the real type the protocol is referencing is:
func dumpPrintable<T>(var p: Printable, type: T.Type) { withUnsafePointer(&p) { ptr -> Void in let intPtr = UnsafePointer<Int>(ptr) for i in stride(from: 0, to: (sizeof(Printable)/8), by: 1) { println("\(i):\t 0x\(String(intPtr[i], radix: 16))") } // if the pointed-to value is to big to fit: if sizeof(T) > 24 { // we have an integer, and we want to turn it into a pointer, // so we use the bitPattern: constructor of UnsafePointer<T> let valPtr = UnsafePointer<T>(bitPattern: Int(intPtr.memory)) // and now we can look at the value at that location in memory println("value at pointer: \(valPtr.memory)") } } } dumpPrintable(FourInts(),FourInts.self) // prints out: 0: 0x7f8b5840fb90 1: 0x7fff909c5395 2: 0xaaaa 3: 0x10dde52a8 4: 0x10dde51b8 value at pointer: (43690, 48059, 52428, 56797)
Bingo! Those are the values of the 4 integers.
One final point before we move on. Just because you switch to referring to a value type using a protocol, this does not turn it into a reference type:
protocol Incrementable { var x: Int { get } mutating func inc() } struct S: Incrementable { var x = 0 mutating func inc() { ++x } } var p1: Incrementable = S() var p2 = p1 p1.inc() p1.x // now 1 p2.x // still 0
Performance Implications
OK, so protocols used like this seem to add some level of indirection. Does that cost us anything compared to using the generic placeholder approach? To test this out, we can construct some trivial structs that perform a basic operation, and then attempt to run that operation multiple times via both a protocol and a generic placeholder.
First here’s the protocol:
protocol NumberGeneratorType { mutating func generateNumber() -> Int }
We’re pretty restricted in what can be done without resorting to associated types, so all it does is spit out a number. Here are 3 implementions that do different things, along with two harnesses that iterate multiple times, totalling the returned numbers:
struct RandomGenerator: NumberGeneratorType { func generateNumber() -> Int { return Int(arc4random_uniform(10)) } } struct IncrementingGenerator: NumberGeneratorType { var n: Int init(start: Int) { n = start } mutating func generateNumber() -> Int { n += 1 return n } } struct ConstantGenerator: NumberGeneratorType { let n: Int init(constant: Int) { n = constant } func generateNumber() -> Int { return n } } func generateUsingProtocol(var g: NumberGeneratorType, count: Int) -> Int { return reduce(stride(from: 0, to: count, by: 1), 0) { total, _ in total &+ g.generateNumber() } } func generateUsingGeneric<T: NumberGeneratorType>(var g: T, count: Int) -> Int { return reduce(stride(from: 0, to: count, by: 1), 0) { total, _ in total &+ g.generateNumber() } }
Running the above, compiled with -O
, with a count
of 10k iterations, gives the following timings:
(the full code with the timing can be found here)
Generic rand 261829µs Protocol rand 286625µs Generic incr 5481µs Protocol incr 45094µs Generic const 0µs Protocol const 43666µs
So what does this tell us? Calling arc4random
is quite expensive, so the marginal difference made by the protocol is negligible but noticeable. But in the case of the incrementing generator, it’s a lot more in proportion to the actual operation being performed.
And in the case of the constant generator, the compiler has managed to optimize away all the code of the generic version and turn it into a single multiply operation (number of iterations times the constant)! So it takes a constant tiny amount of time. But the protocol indirection acted as a barrier to this same optimization.
In fact if you recompile with -Ounchecked
, the same happens with the incrementing generator too – it’s only the check for overflow on the increment that was preventing it. The protocol versions remain the same.
For the most part, much of this can probably be put in the “premature optimization” camp. The big wins are really more about the expressiveness generics give you, as seen in the previous article about sorting, and avoiding things like type erasure shown in the earlier section. But if you’re in the habit of writing generic functions, the performance gains are nice too. And when writing re-useable library functions like sort
or reduce
that are going to be called a lot and perform tight loops, they’re critical.
Of course, all this is subject to change – given that the protocols are not being used to get any kind of dynamic behaviour, perhaps the compiler could have optimized them too. But it doesn’t appear to at the moment.
Dynamic Dispatch
Speaking of dynamic behaviour – that might be one occasion when you might prefer to use protocols rather than generics.
Consider the following code:
func f(p: Printable) { println(p) } func g<T: Printable>(t: T) { println(t) } let a = [1,2,3] let i = 1 // this will work fine: either can be converted // to Printable and passed in, then the appropriate // version of `description` is called at runtime f(arc4random()%2 == 0 ? a : i) // this will _not_ compile. T needs to be fixed as // either an array of ints, or an int at compile time, // it can't be both g(arc4random()%2 == 0 ? a : i)
A contrived example obviously, but it shows how even structs in Swift can get dynamic behaviour at runtime via protocols, that you might find a use-case for.
There’s lots more to explore with generics. The mental model of “Swift just writes a version of your function for each different type” isn’t quite how it works in practice. And associated types and constraints between multiple placeholders allow you to create kinds of generic algorithms that would be very difficult to achieve with protocols. But this article is already pretty long so I’ll leave those for another day.
-
In fact you couldn’t implement this function using protocols rather than generics, because
SignedIntegerType
usesSelf
and (viaIntegerLiteralConvertible
) associated types. A topic for a different post. ↩