I can’t quite remember how I stumbled across this book, “iOS and macOS Performance Tuning”, a couple of weeks ago (Twitter, basically). But regardless, it is awesome. Written by Marcel Weiher, it has a couple of simple premises, well explained, that really provides a great roadmap for thinking about performance in iOS.
I will actually write up a couple of blogposts out of this book, because I like it so much and want the lessons to take root. This post will focus on something that I had never really explored carefully, which is working with
NSData rather than just initializing Foundation objects with
NSData and working with the resulting objects. The two places where this is a natural thing to do is with strings and with images.
I am going to do a very basic demonstration of his principles at work in reading and parsing a large text file, complete with benchmarks!
One of the primary lessons from this book is that regardless of the continuing speedup of devices and storage, the cost of using objects and their associated methods to process data does add up.1 If you are doing relatively complex transformations on a small number of objects, then using the most abstracted models and methods completely makes sense. But, for operations on large numbers of objects, there are opportunities to achieve profound speedup.
A second lesson is that for most operations and code, Objective-C still is faster than the equivalent Swift code. This is an important point that can get lost. Swift is incredibly powerful, and absolutely lends itself to modern programming practices that can lead to a better, safer, more stable codebase. But when it comes to raw performance, it is still difficult to optimize Swift as thoroughly as Objective-C
While the book has much more thorough exploration of these, ideas, I wanted to see what would happen if I compared the speeds of reading a large text file and splitting it into an array of either using
NSString-based methods or by using
NSData-based methods and parsing the raw memory. Also, I wanted to see how Swift and Objective-C compared using nominally identical code.
This is the code that I used for comparing in Objective-C. You can see that the in the
NSString based method, we use the
NSString initializer to read in the file, and then parse the entire string into an array of strings using
NSString methods. In contrast, the
NSData based method intializes an
NSData object with the file, then runs through the raw bytes, and looks for the locations of ‘\n’ characters, storing those in an array, which can be used later.
For the sake of space, I am pasting both gists at the bottom of this post.
A couple of notes, before looking at the results.
First, Obviously, an array with indices into an
NSData object is more primitive than methods that provide a full set of instantiated objects. But depending on the use case, it is not difficult to work from this point and only perform full-blown object creation on the relevant sections of the data. That is up to the needs of the programmer.
Second, I concede that there may be more nuanced ways to improve the Swift performance. The Swift code is a fairly mechanical translation here. But the point is at least in part that Swift methods of similar sophistication do not perform as well. I performed compilations with either -O3 or -OFast flags. There was not a significant difference.
All that said, we indeed see what we expect when benchmarking.2 Just for reference, the iPod touch is an A8 processor.
|iPhone 7 -O3||0.338||0.184||0.079||0.030|
|iPhone 7 -OFast||0.325||0.181||0.077||0.031|
|iPod Touch -O3||0.458||0.294||0.082||.026|
|iPod Touch -OFast||0.440||0.286||0.080||.026|
Reading and processing bytes is ~8-11x (objc) or ~4-6 (Swift) faster, depending on device, than reading and processing strings:
Objective-C performs ~2x faster on bytes and ~3x faster on strings than Swift.3
So, by taking these results to find the most performant combination, we find the following an 11-17x speedup, depending on the device. This is significant. If the reading is done synchronously, it is the difference on an iPod touch between a user waiting for 1/2 of a second, vs. completing the operation within a single refresh cycle of the UI!
|Best vs. Worst|
|iPhone 7||0.030/0.338 = 8.9%|
|iPod touch||0.026/0.458 = 5.7%|
It is no criticism of Swift that we should remember as iOS and macOS programmers to use the best tool when implementing our programs and that sometimes Swift isn’t that tool. In some cases, it is worth considering using optimized Objective-C to accomplish specific tasks. This code can be wrapped up in an Objective-C class, and then bridged to Swift. It may feel like it “dirties” the codebase, but these results absolutely have me thinking hard about this issue.
- One of the next posts will get into an important point about the design philosophy of Objective-C that has large performance implications ↩︎
- I made 4 copies of a 4.2 MB text file that consists of ~45,000 lines of text. The program read and parsed each file sequentially. I ran the code a couple of times to check for consistency, and I chose the faster runs. ↩︎
- This points to the Swift object creation process being less optimized, or just inherently more complicated than ↩︎