Performance Case Study on Swift 1.1, Swift 1.2, and Objective-C

In this study, rather than making a statement about which language is holistically faster, I attempt to embrace the differences between Objective-C and Swift to choose which language performs faster for the job at hand.

For my analysis, I will be benchmarking performance in regards to shuffling an array of objects. I felt this was a nice case to test because of it’s simplicity. At the heart of the test, it involves iterating through a large collection of objects and randomly swapping them with other objects. Nothing fancy going on. I like to think of it as a sorting test without the uncertainty of how a sort is implemented between the two languages.

Note: All tests are ran on an iPhone 6 with iOS 8.1. Swift’s code generation optimization level is set to “Fastest [-O]”.

Round 1

Take the following NSMutableArray:

NSMutableArray *array = [NSMutableArray new];
for (NSUInteger i = 0; i < 1000000; i++) {
[array addObject:@(i)];
}

We populate the array with a million NSNumber objects. Once that’s done, we want to setup an Objective-C method and a Swift function to shuffle the above array so that we can measure performance. Below you will see that I am using XCTestCase’s measureBlock method to capture the results:

- (void)testShuffleNumberObjC
{
NSMutableArray *objects = [self createNSNumberArray];
    [self measureBlock:^{
[ObjectiveCUtils shuffleObjects:objects];
}];
}
(void)testShuffleNumberSwift
{
NSMutableArray *objects = [self createNSNumberArray];

[self measureBlock:^{
[SwiftUtils shuffleObjects:objects];
}];
}

Now let’s get to the implementation of the shuffleObject method/function:

ObjectiveCUtils.m

+ (void)shuffleObjects:(NSMutableArray *)array {
for (NSInteger i = 0; i < [array count]; i++) {
id currentObject = array[i];
NSUInteger randomIndex = arc4random() % array.count;
id randomObject = array[randomIndex];

array[i] = randomObject;
array[randomIndex] = currentObject;
}
}

SwiftUtils.swift

class func shuffleIntObjects(inout array: [Int]) {
for (var i = 0; i < array.count; i++) {
let currentObject: Int = array[i]
let randomIndex = Int(arc4random()) % array.count
let randomObject: Int = array[randomIndex]

array[i] = randomObject;
array[randomIndex] = currentObject
}
}

You may have noticed something. Given our test case, the above Swift method will not compile. That’s because “inout” in the parameter will not bridge to Objective-C. Let’s rewrite this method so we can use it in our test case.

SwiftUtils.swift

@objc class func shuffleObjects(array: NSMutableArray) {
for (var i = 0; i < array.count; i++) {
let currentObject: AnyObject = array[i]
let randomIndex = Int(arc4random()) % array.count
let randomObject: AnyObject = array[randomIndex]

array[i] = randomObject;
array[randomIndex] = currentObject
}
}

Okay. That’s better. Let’s run this test and see what we get.

  • Objective-C: 0.296 seconds (4% STDEV)
  • Swift 1.1: 0.376 seconds (1% STDEV)
  • Swift 1.2: 0.328 seconds (3% STDEV)

We can see that Objective-C got the best of Swift. Swift 1.2 comes very close to Objective-C but doesn’t quite beat it. At the end of Round 1, the winner here is Objective-C.

Round 2

Looking at the results from Round 1, I am actually pretty disappointed. I really wanted Swift to win. Is it really possible that Swift can’t outperform Objective-C? Well, let’s try another test.

Remember that Swift function we couldn’t use in Round 1? Well let’s bring that function back.

SwiftUtils.swift

class func shuffleIntObjects(inout array: [Int]) {
for (var i = 0; i < array.count; i++) {
let currentObject: Int = array[i]
let randomIndex = Int(arc4random()) % array.count
let randomObject: Int = array[randomIndex]

array[i] = randomObject;
array[randomIndex] = currentObject
}
}

In order for us to bring this function back, we’re also going to have to rewrite how we capture our times. That means not bridging to Objective-C so this code can compile. Let’s convert the tests from Round 1 into Swift.

func testShuffleIntObjC() {
let mutableArrayObjects = NSMutableArray()
for (var i = 0; i < 1000000; i++) {
mutableArrayObjects.addObject(i)
}

self.measureBlock() {
ObjectiveCUtils.shuffleObjects(mutableArrayObjects)
}
}

Let’s also write the following test for a Swift Array of Int.

func testShuffleIntSwift() {
var objects = [Int]()
for (var i = 0; i < 1000000; i++) {
objects.append(i)
}

self.measureBlock() {
SwiftUtils.shuffleIntObjects(&objects)
}
}

Let’s ⌘U these tests and see what we get.

  • Objective-C: 0.292 seconds (3% STDEV)
  • Swift 1.1: 0.181 seconds (2% STDEV)
  • Swift 1.2: 0.192 seconds (2% STDEV)

At the end of Round 2, we begin to see what we hoped for from Swift. Both versions of Swift beat Objective-C by executing at about 1.5x to 1.6x faster. It’s also worth mentioning that, in case anyone was skeptical about converting the Objective-C test to Swift, the Round 2 Objective-C time is strikingly similar to the Round 1 Objective-C time.

Alright at this point some of you may be thinking, “well you’re comparing apples and oranges here.” Well…you’re right. We are comparing apples and oranges here. That’s because Swift’s Int is a struct and not your typical object. If you know a thing or two about structs, it’s that they’re passed by value. In contrast to NSNumber which is passed by reference.

So what does this mean? Well it’s looking like using Swift’s struct feature is making a great case for being the right language for the job. But let’s solidify that notion further by leveling the playing field a bit.

Round 3

In this round we are going to make sure that the Swift and Objective-C tests are both using objects that are passed by reference. This means instead of passing structs around, we will be passing objects defined from a class. Let’s create our Swift array and our NSMutableArray with the following code:

class MyObject : NSObject {}
// Swift Array
var myObjects = [MyObject]()
for (var i = 0; i < 1000000; i++) {
myObjects.append(MyObject())
}
// NSMutableArray
let myObjects = NSMutableArray()
for (var i = 0; i < 1000000; i++) {
myObjects.addObject(MyObject())
}

We define a class called MyObject and add an instance of it to each array one million times. We will still capture our times similarly to Round 2 except that we will change Swift’s shuffle method to explicitly take a MyObject array:

SwiftUtils.swift

class func shuffleObjects(inout array: [MyObject]) {
for (var i = 0; i < array.count; i++) {
let currentObject: MyObject = array[i]
let randomIndex = Int(arc4random()) % array.count
let randomObject: MyObject = array[randomIndex]

array[i] = randomObject;
array[randomIndex] = currentObject
}
}

⌘U and the results are in:

  • Objective-C: 0.799 seconds (2% STDEV)
  • Swift 1.1: 2.527 seconds (1% STDEV)
  • Swift 1.2: 1.154 seconds (2% STDEV)

Interestingly enough, using our custom object makes Swift perform rather poorly against Objective-C. Swift 1.2 is a vast improvement over it’s predecessor but it’s still about 0.300 seconds behind the Objective-C mark. The story between both of these languages is becoming much clearer. It’s very apparent that not all things will be faster in one language and one language only.

The purpose of Round 3 was essentially to level the playing field for Objective-C. However, that’s not very fair to Swift. Let’s give Swift another chance to show us what it can do.

Bonus Round

In this test, instead of using a custom class object, we’re going to create a custom struct object. Unfortunately, however, Objective-C will have to sit out this round. This is because 1) NSArray cannot hold a struct object and 2) structs cannot bridge to Objective-C. But wait a second, didn’t we put Int struct objects in an NSArray in Round 2? Yes we did. But fortunately for Int, Swift knows how to bridge it to an NSNumber. As far as I know, I’m not sure of a way to tell Swift to bridge my custom struct objects to an Objective-C object. So given that, we will have to continue the show without Objective-C. Let’s create a million custom structs like this:

struct MyStructObject {}
// Swift Array
var myStructs = [MyStructObject]()
for (var i = 0; i < 1000000; i++) {
myStructs.append(MyStructObject())
}

Let’s also change our Swift shuffle function by passing in an array of MyStructObject.

SwiftUtils.swift

class func shuffleObjects(inout array: [MyStructObject]) {
for (var i = 0; i < array.count; i++) {
let currentObject: MyStructObject = array[i]
let randomIndex = Int(arc4random()) % array.count
let randomObject: MyStructObject = array[randomIndex]

array[i] = randomObject;
array[randomIndex] = currentObject
}
}

On your mark, get set, ⌘U!

  • Objective-C: N/A
  • Swift 1.1: 0.070 seconds (1% STDEV)
  • Swift 1.2: 0.065 seconds (2% STDEV)

Since we don’t have an Objective-C test to compare against, let’s compare this benchmark against previous tests.

You’ll notice that using our custom struct in Swift outperforms benchmarks from it’s Round 3 equivalent. This really speaks to the power of using Swift’s structs as it provided a 17x to 36x improvement over using objects defined with class.

Conclusion

As I mentioned before, not all things will be faster in just one language. The purpose of these benchmarks are to illustrate the differences between the languages given a similar task and to help developers evaluate what parts of their code they should be writing in Swift or Objective-C. In this case, performing operations on a large set of data performs best on structs rather than objects defined using class.

I also realize that not everyone is going to write code that shuffles a million objects. However, it’s still worth noting that regardless of how many objects we choose to use for these tests, the underlying themes stay the same. I welcome anyone reading this article to checkout the code here for more details and more benchmarks not mentioned in this article. I’ll be updating the repository as new versions of Swift release and for any new ideas to measure performance on. Thanks for reading! Follow me on Twitter @fielgood.