CoreDataをRealmに移行し高速化できたのでベンチマークで性能を比較してみた。


**追記:** Fetch結果について補足しました。

---

[Aplos](https://itunes.apple.com/jp/app/id910000043)というTwitterクライアントでCoreDataを使用していたのですが、CoreDataが基本重いに加えてデータベースが肥大化するにつれて指数関数的にどんどん重くなるという問題があり頭を抱えていました。そこで以前から気になっていたRealmを試してみたら実用に耐えられるレベルと判断できたので、思い切ってRealmに移行してみました。
(※まだ全ての実装は終わっていないのですが、このまま問題がなければ[Aplos](https://itunes.apple.com/jp/app/id910000043)のバージョン2.0で完全移行する予定です。)

Realmを採用するかを検討するにあたりベンチマークも取ってみました。Realm公式にも[ベンチマーク比較](http://realm.io/news/introducing-realm/#fast)はありますが、今回はTwitterクライアントで実際に使用している設計とテストデータでベンチマークを取っていますので、より現実的な結果が反映されていると思います。

※この記事はRealmのバージョン0.88.0について記載しています。Relamの開発は活発なので試す場合はバージョンに注意してください。

#Realmの基礎知識

Realm(レルム)はモバイル向けに開発されたデータベースです。コア部分にはTightDBというC++で実装されたストレージエンジンが使用されています。

**公式**

- [ドキュメント(日本語)](http://realm.io/jp/docs/cocoa/0.87.1/) (とてもよくまとめられているので、一通り読むとどういうものかつかめると思います。)
- [Introducing Realm](http://realm.io/news/introducing-realm/) (Realmを作った経緯、ベンチマークとか)

**その他**

- [CoreDataはもう古い？新しいモバイルデバイス向けデータベース「Realm」を使ってみた](http://qiita.com/caesar_cat/items/632d1e85a67f2c65a473) (SQLとの比較でわかりやすい)
- [次世代モバイルデータベース「Realm」のリレーションシップ](http://qiita.com/caesar_cat/items/6ebd92f4a0e700591e6b)
- [Why Realm is great and why are we not using it](http://bsktapp.com/blog/why-is-realm-great-and-why-are-we-not-using-it/) (わかりやすいチュートリアル)
- [Realmについて](http://www.slidesearch.org/slide/realm) (内部実装の解説)

#ベンチマーク

##条件
- テストデータはTwitter REST APIで取得した[Tweet](https://dev.twitter.com/rest/reference/get/statuses/show/%3Aid)を扱います。
- [Tweet]((https://dev.twitter.com/rest/reference/get/statuses/show/%3Aid))と[User](https://dev.twitter.com/rest/reference/get/users/show)はユニークです。

	- CoreDataでは追加毎にフェッチし一意性を保ちます。
	- RealmはPrimaryKey(主キー)がサポートされていますが、様々な理由(次回の記事で補足)によりCoreDataと同じくフェッチを行っています。

- [Aplos](https://itunes.apple.com/jp/app/id910000043)の仕様で特殊なマルチユーザ対応などその他様々な条件も絡みあっているので、単純に[Tweet](https://dev.twitter.com/rest/reference/get/statuses/show/%3Aid)データのみを扱った結果ではありません。(ただそこまで影響度が高いわけでもありません。)

##テーブル定義

###1. CoreData1

TwitterのJSON通り(リレーションも適切に)
![model.png](https://qiita-image-store.s3.amazonaws.com/0/16700/5bbb14e4-4aac-634c-10b3-6c8b2ffe943d.png "model.png")

###2. CoreData2

リレーションを極力排除、カラムも整理し最適化
![optimized-the-model.png](https://qiita-image-store.s3.amazonaws.com/0/16700/1c4e0618-7e6a-7cb6-eaee-eeb6db814f52.png "optimized-the-model.png")

###3. Realm

CoreData1をそのままRealmで定義

##ベンチマーク結果

###Insert
- [Tweet]((https://dev.twitter.com/rest/reference/get/statuses/show/%3Aid))をInsert

####iPhone4s
![aeb4a707c4a75eea529984a06645c9f6.png](https://qiita-image-store.s3.amazonaws.com/0/16700/184ca453-1e8e-0c6c-01e7-887eefd3788f.png "aeb4a707c4a75eea529984a06645c9f6.png")

####その他の結果
||100件(4s)|1000件(4s)||100件(6+)|1000件(6+)||100件(Sim)|1000件(Sim)|1万件(Sim)|
|:---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|
|CoreData1|1.382|33.974||0.309|7.168||0.158|4.057|-|
|CoreData2|0.938|19.842||0.195|3.917||0.118|2.197|281.451|
|Realm|0.576|5.800||0.102|1.031||0.075|0.664|7.431|

###Fetch
- [Tweet]((https://dev.twitter.com/rest/reference/get/statuses/show/%3Aid))をIDでソートしフェッチ

####iPhone4s
![117b8411f878b9b2a2a64cbe65beb19b.png](https://qiita-image-store.s3.amazonaws.com/0/16700/2a8c1df1-b21c-b772-dbc2-0e246b62ba15.png "117b8411f878b9b2a2a64cbe65beb19b.png")

####その他の結果
||1000件(4s)|1万件(4s)||1000件(6+)|1万件(6+)||1000件(Sim)|1万件(Sim)|10万件(Sim)|
|:---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|
|CoreData1|0.003|-||0.001|-||0.000|-|-|
|CoreData2|0.003|-||0.001|-||0.000|-|-|
|Realm|0.064|1.541||0.012|0.155||0.006|0.125|6.677|

*- 単位は秒でXCTestの[パフォーマンス測定用メソッド](http://qiita.com/nomadmonad/items/d2c283f9b9ad33a2b32a#2-2)の平均処理速度を記載。*
*- 4sはiPhone4s、6+はiPhone6 Plus、Simはシミュレータ。'-'は都合により未測定。*

#考察
##Insert
###CoreData
- リレーションを減らすことによってパフォーマンスが向上した。
- データ件数に対する処理時間の増加率はリレーションとは関連なさそう。データ件数が10倍になった場合、10倍以上の処理時間が増加する傾向。

###Realm
- CoreData1の様なリレーションが多い設計でもパフォーマンスはCoreDataより良く、平均して6倍程度速かった。
- データ件数が10倍になったら処理時間も10倍と比例する。

##Fetch
- RealmのフェッチはCoreDataより遅い。 ~~おそらくインデックスが定義できないのでソートのコストが高いと予想。~~

**[訂正]**
**- ベンチマーク結果は変わらないのですが、当初Realmが遅い原因がソートコストと記載していましたが今回の結果には関係なかったです。純粋にオブジェクトの取得部分の比較になります。具体的には以下のコードでベンチマークを取っています。**

**- 前提としてですが、フェッチ条件が複雑な場合を想定しています。複雑なフェッチを一旦バックグラウンドで実行し、メインスレッドでPrimaryKeyの値を利用して再度フェッチするという方法です。フェッチ条件が複雑でなければそのままの条件をメインスレッドでフェッチした方が高速かもしれません。**
**- RealmではCoreDataのObjectIDのような仕組みがまだ提供されていないので、CoreDataと比べるとどうしてもコストが高くなってしまいます。(十分高速ですが)**

```objectivec:Realm
RLMResults *results;
if (!wself.isCancelled && [values count] > 0 && resultClass && primaryKey) {
    results = [resultClass objectsInRealm:[wself realm]
                                    where:@"%K IN %@", primaryKey, values];
}      
```

```objectivec:CoreData
NSMutableArray *fetchResults = [NSMutableArray arrayWithCapacity:[ids count]];
for (NSManagedObjectID *objID in ids) {
    NSError *error = nil;
    NSManagedObject *obj = [strongSelf.mainContext existingObjectWithID:objID error:&error];
    if (obj == nil || error) {
        if (completion) completion(strongSelf, nil, error);
            return;
        }
    [fetchResults addObject:obj];
}
```

#まとめ
- 今回の条件では6倍程度の高速化ができました。Realmが現在の設計(CoreData1)を変更することなくパフォーマンスが向上できたことが、採用に踏み切れた一番の決め手でした。速いは正義です。
- CoreData2の設計にすればより速くなるかもしれませんが、どうしてもコード量が増えてしまうので避けたいところです。
- CoreDataで1000件INSERTするのに100件x10の方が速い場合もあり、[Aplos](https://itunes.apple.com/jp/app/id910000043)ではいろいろ試して良かったものを採用しています。もしかしたらCoreDataの作法としてInsertや一意性の確保などで別のより良い方法があるかもしれませんが、何も考えなくても速いRealmは素晴らしいと思います。
- そんな素敵なRealmもいいところばかりではなく様々な問題があります。長くなったので詳しくは次の記事[CoreDataからRealmに移行してわかったメリット/デメリット](http://qiita.com/yusuga_/items/add6f9e290b92f42104a)をご覧ください。