今年の6月のWWDCでSwiftが発表された時のアピールポイントの1つとしてパフォーマンス向上がありました。
とはいえ、Objective-Cはメッセージパッシング部分などで多少ボトルネックなどがあるとはいえそこそこ速いし、一般的なGCではなく参照カウンタ形式のメモリ管理なので定期的にもっさりするような挙動もほぼ起こらないので、元々そこをネックに感じていた人は少なかったのではないかと思います。
でも速いに越したことはないですし、実際どのくらい差があるのか気になりますよね。
Swiftの処理速度
いくつか見た中で、良い感じにまとまっているのはこれですかね(8月の記事なので現バージョンでは少し差があるかもですが)。
以下、要点 + 意見です。
- Swiftは、
Optimization Level
をNone [-Onone]
にするとObjective-C
よりかなり遅い- リリースビルドでは
Fastest [-O]
になっているので基本的にこれは問題にならない - ただ、
Fastest [-O]
の場合クラッシュするなどの理由で安易にNone [-Onone]
にしている記事が散見されるのが気になる- Swiftのバグの場合と、書き方が良く無いけどデバッグビルドでは運良く落ちない、の2パターンがあるように見えるけど、ちゃんと書き方を正すなりその部分だけObjective-Cで書いてしのぐなどして、コンパイルオプションは安易に弄らないようにするべき
- 開発時にすごく処理時間がかかっていて色々苦労した結果、実はデバッグビルドで遅かったせい、などありえるのでちゃんと頭には入れておいた方が良さそう
- リリースビルドでは
- リリースビルドでは、SwiftはObjective-Cより10倍程度速い(ソート系のアルゴリズムにおいて)
あと、この記事で触れているmikeash.com: Friday Q&A 2014-07-04: Secrets of Swift's Speedで、もう少し深い議論がなされています。
この記事はソートアルゴリズムについて10倍程度速くなっているとのことでしたが、実際に僕もすごく単純な処理について比較してみました。
自分ですごく単純な処理においてパフォーマンス比較してみた
以下のような配列にaddを100,000回していくだけのすごく単純な処理の場合、SwiftはObjective-Cより2倍弱速い程度でした。
func testCountMany() {
self.measureBlock() {
let toNum = 100000
var list = [Int]()
for i in 0..<toNum {
list.append(i)
}
}
}
- (void)testPerformanceExample {
[self measureBlock:^{
NSUInteger toNum = 100000;
NSMutableArray* list = [NSMutableArray new];
for (NSUInteger i = 0; i < toNum; i++) {
[list addObject:@(i)];
}
}];
}
言語 | 処理時間 |
---|---|
Swift | 0.006 sec |
Objective-C | 0.010 sec |
ただ、実際こういう大量の処理を回すのってあまりないですよね。
通常アプリ作るにあたって、パフォーマンス的に効いてくるのは、画面描画・通信・DB周りが支配的で、細々としたロジック部分は無視できる範囲なことがほとんどですし。
(なので、ロジック部分は基本的にはささいなパフォーマンスなどは無視して可読性・メンテナンス性重視が良いと思っています。)
というわけで、Swiftが速くなったというのは、実質そこまで恩恵大きくないだろうということで、軽快なアプリを作るには、これまで通り「画面描画・通信・DB周り」を開発者ががんばって地道に速くしていくしかないと思っています。
と、ここで終わると残念な感じなので、上に挙げたうち、DB周りの高速化としてiOS8・OSX 10.10からバッチ更新が可能となったとのことで、そのAPIを試してみました。
<<<<<<< HEAD
(どさくさに紛れて自作スタンプを宣伝)
ラヴさん (ラブラドール) - LINE クリエイターズスタンプ
===
=======
LINEスタンプ作ってるので良かったらダウンロードお願いしますヽ(・ω・`)
ラヴさん (ラブラドール) - LINE クリエイターズスタンプ
===
YOUR_EDITION
バッチ更新でCore Dataの更新を大幅に高速化
iOS8・OSX 10.10からNSBatchUpdateRequest
を使えば、Core Dataの大量更新をバッチ処理で行えるようになりました。
(Core Data使わずにSQLiteを生で使っていれば今までも出来たと思いますが)
- 参考: New in Core Data and iOS 8: Batch Updating | Big Nerd Ranch
- 速いだけではなく省メモリっぽいですね
ちょっと長いので、サンプルコードは末尾に載せますが、以下のように100,000件のデータのプロパティを一気に書き換える処理で、既存の処理に比べてNSBatchUpdateRequest
を使った処理では大きな差が出ました。
※速すぎるのでNSBatchUpdateRequest版どこかミスってるかも
言語 | 既存の処理 | NSBatchUpdateRequest版 |
---|---|---|
Swift | 1.863 sec | 0.002 sec |
Objective-C | 1.664 sec | 0.000 sec |
既存の処理の場合、Core Dataの操作がボトルネックになって、言語間の速度差にほとんど反映されないのは予想どおりでしたが、なぜかSwiftの方が微妙に遅いです。
ループ回す処理などの分Swiftの方が微妙に速いことを予想していましたが(´・ω・`)
NSBatchUpdateRequest
の注意点
ただ、もちろんトレードオフがあって、リンク先にもあるように下記の注意が必要です。
-
New in Core Data and iOS 8: Batch Updating | Big Nerd Ranch
- バリデーションが効かない
- 外部キー制約が効かない
- UIへの自動反映がなされない
アプリが操作可能な状態で行うのはけっこう難しそうな感じです。
使えそうなところとしては、アプリアップデート後の初回起動後に、画面ブロックして一気にデータ更新とかですかね。(マッピングファイル使ったCore Dataのマイグレーションは遅いですし)
WWDCのデモでは、メールアプリで大量のメッセージを一気に既読化する処理に適用していました。
確かにそういった場合に、UIがしばらく固まるというのはあるあるですし、それが劇的に改善するのは良いですね。
GitHub検索しても、MagicalRecordなど含めてNSBatchUpdateRequestを使ったライブラリが見当たらないので、是非どなかたプルリクエストなどしてほしいと思ったり。
(あるいは使いたくなったら、自分でやろうかなと思っています。)
何かタイトルとかなり内容が逸れてしまった気がしますが、言語間のパフォーマンスのちょっとした差より、こういった強力なAPIだったり賢いアルゴリズムだったりというのがずっと大きい、という一例として捉えていただけたらと思います。
Core Dataの処理のパフォーマンス検証コード
プロジェクトはこちら: mono0926/IOS-PerformanceCheck
import UIKit
import XCTest
class CoreDataTests: XCTestCase {
override func setUp() {
MagicalRecord.setupCoreDataStack()
let memberCount = 100000
let moc = NSManagedObjectContext.MR_contextForCurrentThread()
for i in 0..<memberCount {
Member.MR_createEntity()
moc.MR_saveToPersistentStoreAndWait()
}
super.setUp()
}
override func tearDown() {
Member.MR_deleteAllMatchingPredicate(nil)
let moc = NSManagedObjectContext.MR_contextForCurrentThread()
moc.MR_saveToPersistentStoreAndWait()
super.tearDown()
}
func testCoreData() {
self.measureBlock() {
let moc = NSManagedObjectContext.MR_contextForCurrentThread()
let members = Member.MR_findAll() as [Member]
for m in members {
m.loveIce = true
}
moc.MR_saveToPersistentStoreAndWait()
}
}
func testCoreDataBatch() {
self.measureBlock() {
let moc = NSManagedObjectContext.MR_contextForCurrentThread()
let entityDescription = NSEntityDescription.entityForName("Member", inManagedObjectContext: moc)!
let batchUpdateRequest = NSBatchUpdateRequest(entity: entityDescription)
batchUpdateRequest.resultType = NSBatchUpdateRequestResultType.UpdatedObjectIDsResultType
batchUpdateRequest.propertiesToUpdate = ["loveIce": true]
// Execute Batch Request
var batchUpdateRequestError: NSError?
moc.executeRequest(batchUpdateRequest, error:&batchUpdateRequestError)
if let e = batchUpdateRequestError {
XCTFail(e.description)
}
}
}
}
@interface CoreDataTests2 : XCTestCase
@end
@implementation CoreDataTests2
- (void)setUp {
[super setUp];
[MagicalRecord setupCoreDataStack];
NSUInteger memberCount = 100000;
NSManagedObjectContext* moc = [NSManagedObjectContext MR_contextForCurrentThread];
for (NSUInteger i = 0; i <memberCount; i++) {
[Member MR_createEntity];
[moc MR_saveToPersistentStoreAndWait];
}
}
- (void)tearDown {
[Member MR_deleteAllMatchingPredicate:nil];
NSManagedObjectContext* moc = [NSManagedObjectContext MR_contextForCurrentThread];
[moc MR_saveToPersistentStoreAndWait];
[super tearDown];
}
- (void)testCoreData {
[self measureBlock:^{
NSManagedObjectContext* moc = [NSManagedObjectContext MR_contextForCurrentThread];
NSArray* members = [Member MR_findAll];
for (Member* m in members) {
m.loveIce = YES;
}
[moc MR_saveToPersistentStoreAndWait];
}];
}
- (void)testCoreDataBatch {
[self measureBlock:^{
NSManagedObjectContext* moc = [NSManagedObjectContext MR_contextForCurrentThread];
NSEntityDescription *entityDescription = [NSEntityDescription entityForName:@"Member" inManagedObjectContext:moc];
NSBatchUpdateRequest *batchUpdateRequest = [[NSBatchUpdateRequest alloc] initWithEntity:entityDescription];
[batchUpdateRequest setResultType:NSUpdatedObjectIDsResultType];
[batchUpdateRequest setPropertiesToUpdate:@{ @"loveIce" : @YES }];
// Execute Batch Request
NSError *batchUpdateRequestError = nil;
[moc executeRequest:batchUpdateRequest error:&batchUpdateRequestError];
if (batchUpdateRequestError) {
XCTFail(@"%@", batchUpdateRequestError);
}
}];
}
@end
実行速度の検証は以下の条件で行いました。
- iPhone 6 Plus実機
- iOS 8.2β
- Xcode 6.2β
- リリースビルド
- 最適化オプションはリリースビルド時のデフォルト