Swift は Downcast の仕方でパフォーマンスが大きく変わる (Xcode 6.1 / iOS 8.1 対応)

JSON を読み込む場合や, Objective-C のコードからデータを渡す時など, Swift で Downcast を行う機会は少なからずあります. この際に Downcast (ダウンキャスト) (型変換) を行う方法によってパフォーマンスに大きな差が出ることがあります.

パフォーマンスが変化する条件について

[[String: Int]] の型で表現できる AnyObject を Downcast する場合, AnyObject? -> [[String: Int]] と直接 Downcast するのと, AnyObject? -> [AnyObject] と Downcast してからさらに AnyObject -> [String: Int] に Downcast するのでは後者の方が数倍速いという現象です.

テスト内容について

ここでは JSON ファイルを読み込んで, その結果を Downcast して Swift で利用することを考えます. JSON の読み込みには NSJSONSerialization を用います. ファイルの内容を NSData に読み込み, その NSData を NSJSONSerialization.JSONObjectWithData で読み込みます. NSJSONSerialization.JSONObjectWithData は AnyObject? のオブジェクトを返します.

テストに利用するのは以下のオブジェクトの配列を表す JSON ファイルです. それぞれのオブジェクトは26個の要素を持ち, このオブジェクトが 1,000 個入った配列になっています. Swift の型で表現するなら [[String: Int]] です. このサンプルファイルは Python 3 で作成しています.

[
    {"a": 1, "o": 15, "t": 20, "y": 25, "k": 11, "e": 5, "l": 12, "b": 2, "z": 26, "c": 3, "v": 22, "r": 18, "u": 21, "d": 4, "p": 16, "j": 10, "w": 23, "n": 14, "i": 9, "f": 6, "h": 8, "x": 24, "g": 7, "q": 17, "s": 19, "m": 13},
    {"a": 1, "o": 15, "t": 20, "y": 25, "k": 11, "e": 5, "l": 12, "b": 2, "z": 26, "c": 3, "v": 22, "r": 18, "u": 21, "d": 4, "p": 16, "j": 10, "w": 23, "n": 14, "i": 9, "f": 6, "h": 8, "x": 24, "g": 7, "q": 17, "s": 19, "m": 13},
    {"a": 1, "o": 15, "t": 20, "y": 25, "k": 11, "e": 5, "l": 12, "b": 2, "z": 26, "c": 3, "v": 22, "r": 18, "u": 21, "d": 4, "p": 16, "j": 10, "w": 23, "n": 14, "i": 9, "f": 6, "h": 8, "x": 24, "g": 7, "q": 17, "s": 19, "m": 13},

    〜〜〜 中略 〜〜〜

    {"a": 1, "o": 15, "t": 20, "y": 25, "k": 11, "e": 5, "l": 12, "b": 2, "z": 26, "c": 3, "v": 22, "r": 18, "u": 21, "d": 4, "p": 16, "j": 10, "w": 23, "n": 14, "i": 9, "f": 6, "h": 8, "x": 24, "g": 7, "q": 17, "s": 19, "m": 13}
]

ここでは上記のファイルのそれぞれのオブジェクトの数値の和を求めます. (つまり 26 * 1,000 = 26,000 の数値の和を求める)

1つ目の方法は NSJSONSerialization.JSONObjectWithData で返される AnyObject? を直接 [[String: Int]] に Downcast する方法です. 和は Swift らしく reduce で求めます.

let list1 = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.AllowFragments, error: &error) as [[String: Int]]
sum = list1.reduce(0, combine: { $0 + reduce($1.values, 0, +) })

2つ目の方法では NSJSONSerialization.JSONObjectWithData で返される AnyObject? を一旦 AnyObject の配列 [AnyObject] に Downcast して, その AnyObject を利用する際に再度 AnyObject -> [String: Int] の Downcast をする方法です.

let list2 = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.AllowFragments, error: &error) as [AnyObject]
sum = list2.reduce(0, combine: { $0 + reduce(($1 as [String: Int]).values, 0, +) })

これらのコードは GitHub で公開しています.

コンパイルと実行

上記の処理を iOS アプリケーション上の UIButton をタップすると実行するようにしているので, それで実行します. 環境は OS X Yosemite (14A329f) / Xcode 6.0.1 (6A317) / iOS 8 (12A365) で iPhone 5s 実機と iPhone 5s Simulator で実行します. 最適化オプションも Onone, O, Ounchecked の3種類で試します.

テスト結果

テスト結果は以下のようになりました. 方法1は直接 Downcast する方法 (AnyObject? -> [[String: Int]]), 方法2は2段階に分けて Downcast する方法 (AnyObject? -> [AnyObject], AnyObject -> [String: Int]) です.

iPhone 5s Simulator

Onone

 
方法1: 1.5  [sec]
方法2: 0.65 [sec]

O

 
方法1: 1.2  [sec]
方法2: 0.32 [sec]

Ounchecked

 
方法1: 1.2  [sec]
方法2: 0.31 [sec]

iPhone 5s

Onone

 
方法1: 5.5 [sec]
方法2: 2.2 [sec]

O

 
方法1: 4.4 [sec]
方法2: 1.1 [sec]

Ounchecked

 
方法1: 4.2 [sec]
方法2: 1.0 [sec]

まとめ

Simulator (x64) と iPhone 5s (arm64) で同じ傾向が出ています. 方法2の方が方法1に比べて速い結果になりました. 最適化なしの場合は方法2が2倍程度速く, 最適化ありの場合は方法2が4倍程度速い結果になります.

AnyObject からの Downcast はしばしば利用されるため, パフォーマンスが重要な部分やサイズの大きい AnyObject を Downcast する場合は注意してコードを書くのが良いかもしれません.

追記: Xcode 6.1 / iOS 8.1 でのテスト

Xcode 6.1 と iOS 8.1 が正式リリースとなったため, 改めてテストを行いました. 環境は OS X Yosemite (14A388a) / Xcode 6.1 (6A1052d) / iOS 8.1 (12B411) で iPhone 5s 実機と iPhone 5s Simulator で実行します. 最適化オプションも Onone, O, Ounchecked の3種類で試します.

iPhone 5s Simulator

Onone

 
方法1: 1.92 [sec]
方法2: 0.78 [sec]

O

 
方法1: 1.51 [sec]
方法2: 0.37 [sec]

Ounchecked

 
方法1: 1.40 [sec]
方法2: 0.35 [sec]

iPhone 5s

Onone

 
方法1: 6.16 [sec]
方法2: 2.33 [sec]

O

 
方法1: 4.97 [sec]
方法2: 1.14 [sec]

Ounchecked

 
方法1: 4.93 [sec]
方法2: 1.13 [sec]

追記: Xcode 6.1 / iOS 8.1 まとめ

方法2の方が方法1より速い傾向は, Xcode 6 / iOS 8 から特に変わっていません. Xcode 6 / iOS 8 の時に比べて, Xcode 6.1 / iOS 8.1 の方がこのベンチマークでは少しパフォーマンスが低下しているようにも見えます.