Swift Optional Chaining Performance
Optional chaining in Swift offers a convenient mechanism for testing an optional value embedded in a statement without having to bother with messy binding or dangerous implicit unwrapping. It’s basically just a bit of syntactical sugar that internally converts something like this:
foo?.bar = 42
into this:
if let unwrappedFoo = foo {
unwrappedFoo = 42
}
This is fine when used in moderation. However I occasionally run across code like this:
foo?.bar = 42
foo?.baz = 3.14
foo?.doThatThing()
This makes me a little itchy. My worry has been that the compiler then generates code as if it encountered the following:
if let unwrappedFoo = foo {
unwrappedFoo.bar = 42
}
if let unwrappedFoo = foo {
unwrappedFoo.baz = 3.14
}
if let unwrappedFoo = foo {
unwrappedFoo.doThatThing()
}
You would (hopefully) never write something like this but that’s how the compiler is going to interpret all those chained optionals… Or is it? Maybe the compiler is smart enough to figure out what is going on here and I should just relax and let it do its thing?
Nah, I need to know what’s going on. So I cobbled together a few contrived examples in Xcode and ask it to generate some assembly for me… Except Xcode can’t yet show you the assembly for a Swift file. Sigh. Okay well Google can probably tell me how to look at the assembly and sure enough I find this lovely article (which, by the way, also introduced me to Hopper which is pretty awesome).
Armed with Hopper and a bit of knowledge I set about examining the assembly produced with a variety of techniques and optimization levels with my sample code.
My first test was an unoptimized test of a function using optional binding versus the equivalent using optional chaining (letTest vs chainTest in the sample code) and which yielded assembly with the following lengths*.
Unoptimized | Opcode Count |
---|---|
Optional Binding | 138 |
Optional Chaining | 248 |
As I suspected, the optional chaining was much less efficient. Not really surprising, until I examined the same functions with optimizations turned on.
Optimized | Opcode Count |
---|---|
Optional Binding | 87 |
Optional Chaining | 82 |
Wait, what? The compiler was somehow smart enough to figure out what I was doing and doesn’t just match the optional binding approach, it beats it. Looking over the assembly, it appears the optional binding approach included an extra retain / release.
After the first batch of results the relaxed approach is starting to look better. Maybe I just hammer on the keyboard and the compiler somehow just figures everything out for me. But first another test. This sample is identical except these are methods instead of global functions. First the unoptimized results.
Unoptimized | Opcode Count |
---|---|
Optional Binding | 147 |
Optional Chaining | 195 |
Actually a bit more respectable here than the global counterparts, but optional binding is still much more efficient. And the optimized results…
Optimized | Opcode Count |
---|---|
Optional Binding | 102 |
Optional Chaining | 132 |
Interesting. This is what I expected originally. But why the difference between a method and the function? I imagine because the variables could have setter functions or observers which could alter the value of tObj, therefore the compiler can’t be confident that it does not have to test the value of tObj for each assignment.
In the end, using a series of optionally chained statements is not horrible and in at least one case actually faster than optional binding, but personally I’m going to continue to do what I can to provide those additional clues to compiler and future maintainers of my code (including myself) as to my intent where practical.
Of course this just goes for a series of optionally chained statements. If I’m only evaluating that optional once (maybe even twice if I’m feeling naughty) then optional chains are perfect. Any more than that though and it’s getting wrapped in an optional binding.
*Using the length of the generated assembly as a measurement of efficiency is not always the best idea. The compiler could be unrolling loops or any number of optimization techniques that don’t end up generating less code. However this example is pretty simple and serves as a decent yardstick here.