Swift Performance: Too Slow For Production
Much and more has been written about Apple’s new programming language, Swift. I even did a talk about it at Yow: Connected 2014. If you saw me there, you’d know that I’m not the biggest fan of Swift. I think the rollout of it has been an absolute disaster: the compiler is a shambles, the language design is inconsistent in places1 and it’s stupidly difficult to get working in libraries, particularly if you want to support iOS 7. Surely though, at the very least its performance would be all right. After all, it’s designed to be more static and do more checking at compile-time and is meant to be made with the LLVM optimiser in mind…
Let’s talk about performance in Swift, then.
Like many people I was lulled into a false sense of security about Swift’s performance by a combination of factors:
- This post. It shows Swift as approaching the performance of C when it comes to sorting arrays. Very impressive. Surely the rest of Swift is this fast too, right?
- Apple’s own marketing about it, which claims an impressive “2.6x” faster than Objective-C and “8.4x” faster than Python 2.7.
- The name. Come on, they called it Swift. They wouldn’t call it that if it was slow.
I got about 1,000 lines into my project before thinking to myself that I should really performance test what I’ve written on the off-chance it wasn’t fast enough. At the time, the project was entirely Swift, and it made use of the pretty nice JSONHelper library to make the deserialisers look nicer. An example of what my Swift-like solution looked like:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import Foundation
public class User : ModelObject, UpdatableFromJSON {
public var name: String?
public var handle: String?
public required init(data: [String : AnyObject]) {
super.init(data: data)
updateWithJSON(data)
}
public override func updateWithJSON(data: [String : AnyObject]) {
super.updateWithJSON(data)
name <<< data["name"]
handle <<< data["handle"]
}
}
So far, so nice.
At this point I devised a test, which would parse about 500kb of JSON, which
is pretty far into “worst-case scenario” territory for our app, but isn’t
outside the realm of possibility. It’s indicative JSON, very much like
something the API would really return. You can find a copy of the sample JSON
here. It shows a user who has a membership in 1,000
conversations. The model parser should read this JSON, create Membership
objects which point directly to the corresponding instance of User
and
Convo
, and create (or update) a heap of Convo
objects based on the contents
of the convos
key. In the the Convo
s, the creator
key should point to
the corresponding instance of User
. Nothing here’s unusual for a relatively
sophisticated production iOS app.
I wrote a performance test using XCTest’s new performance testing feature. The test itself looked like this:
1
2
3
4
5
6
7
8
9
func testUserConvosSwiftParsingPerformance() {
let filePath = NSBundle(forClass: PerformanceTests.self).pathForResource("convos", ofType: "json")
let jsonData = NSData(contentsOfFile: filePath!)
var error: NSError?
let jsonObject = NSJSONSerialization.JSONObjectWithData(jsonData!, options: nil, error: &error)! as [String : AnyObject]
self.measureBlock() {
let resp = ChatspryClient.UserConvosResponse(data: jsonObject)
}
}
Making sure I had -O
turned on in the build settings, I pulled out my iPod
Touch 5th generation, which is realistically the slowest device we’ll be
supporting, since it runs iOS 8 and has the same dual-core A5 as the iPhone
4S (which I don’t have). This made for a reasonable worst-case scenario: huge
JSON payload, slow device.
Want to know how Swift did?
…
Guess.
…
Nope, higher. Try again.
…
Nope, higher, try again!
…
Oh fine I’ll just tell you! It took 1.42 seconds. One-point-four-two _seconds_. That can’t be right! Horrified by this, I decided to quickly whip up an Objective-C version of the same performance test. It was just the bare minimum code to implement the test, written in a distinctly Objective-C style, just like the Swift code was written in a distinctly Swift style.
An example of what the Objective-C looked like:
1
2
3
4
5
6
7
8
9
10
11
12
@interface CSUser : CSModelObject
@property (nonatomic, strong) NSString *name;
@property (nonatomic, strong) NSString *handle;
@end
@implementation CSUser
- (void) updateWithJSON:(NSDictionary *)json
{
[super updateWithJSON: json];
self.name = json[@"name"];
self.handle = json[@"handle"];
}
@end
There were also implementations for CSMembership
and CSConvo
among others,
and superclasses too. It was a pretty representative parser. So I whipped out
my trusty XCTest case and created a new performance test which used the
Objective-C classes instead, again making sure that -Os
was enabled in the
build settings.
You’ll never guess. This version runs on my device in just 0.09 seconds. Which makes the Objective-C version about 15x faster than the Swift version. Remember, the LLVM optimiser is turned on for both of these cases.
Ridiculous! I’d wasted most of my weekend at this point, so I figured why not
waste some more. I wasn’t sure if Swift was slow or if it was just JSONHelper
that was slow. The only way to rule it out for sure would be to write a version
of the Swift code that is basically a line-for-line translation of the
Objective-C so I could do a true Apples-to-Apples test. This is absolutely
not how you’d write Swift, since it was full of NSDictionary
references
everywhere instead of Swift dictionaries, to name an example.
An example of what this looked like:
1
2
3
4
5
6
7
8
9
10
public class CSSwiftUser : CSSwiftModelObject {
public var name: String?
public var handle: String?
public override func updateWithJSON(json: NSDictionary) {
super.updateWithJSON(json)
name = json["name"] as String?
handle = json["handle"] as String?
}
}
Now, one caveat about the results here. I simply couldn’t get the Swift to run
with -O
turned on without segfaulting. So, I figured to be fair, I’d turn off
the optimiser for Objective-C this time around too. Here’s the result:
- Objective-C: 0.06 seconds
- Objective-C-like Swift: 0.29 seconds
I have no idea why the Objective-C runs faster with the optimiser turned off, but OK, sure. This proves that Swift is only acceptably fast if you basically write it exactly like you’d write Objective-C, but if you do, you’ll probably get segfaults that can’t be fixed, and it’ll still be about 5x slower anyway.
For one last test just for fun, I decided to rewrite the Objective-C test in
Ruby by using RubyMotion. If you haven’t heard of RubyMotion, it allows you
to write iOS and Android apps using Ruby, which compiles down into the same
native code that you’d get when compiling Swift or Objective-C. It’s meant to
be pretty fast, but I’ve never really benchmarked it with real production-like
code before. I’ve always suspected it was significantly slower than Objective-C
since it can’t do the crazy objc_msgSend
tricks that the Objective-C compiler
can do thanks to it doing all sorts of weird static analysis on method
dispatches to see if it can be sped up. This is just because Ruby is a really
dynamic language. Anyway…
Here’s an example of what that Ruby looked like:
1
2
3
4
5
6
7
8
9
class CSUser < CSModelObject
attr_accessor :name, :handle
def updateWithJSON(json)
super
self.name = json[:name]
self.handle = json[:handle]
end
end
RubyMotion doesn’t seem to have any options for the optimiser. It seems to just default to having optmisation on if you build for device, which would be in keeping with the convention-over-configuration mentality.
Here are the final results:
Style | Optimisation | Time |
---|---|---|
Objective-C | -Os |
0.09 |
Objective-C-like Swift | -Onone |
0.29 |
Swift | -O |
1.42 |
RubyMotion | ??? | 0.21 |
So RubyMotion is faster than Swift, then? The same Ruby, a language notorious in the programmer community for being quite slow2 is faster than a language called Swift which is marketed as being a fast language?
If your language is slower than a language that isn’t even trying to be fast, I’m sorry, but you’re just too far up shit creek. At this point, I wave goodbye to Swift. The Objective-C models I’ve written make for a pretty good starting point, so for now I’m just going to build that out.
I would love to provide all the source code used in this post, but unfortunately our product isn’t open source, and it will in fact be our production code someday. Feel free to leave a comment if you want to know more about what the code did, and I’ll tell you what I can.
UPDATE: I should point out that all these tests were run on Xcode 6.1.1 (6A2006), with whatever Swift compiler comes with that. RubyMotion was at 2.38.
-
Like subscripting, which uses
get
andset
syntax just like a property, but they are declared with a function style syntax? There’s no good reason for this that I can see… ↩ -
This isn’t a criticism. I understand that it’s just an extension of the idea that computer time is less valuable than human time, and Ruby lets you write programs faster to save human time. I appreciate, and even respect that world-view. ↩