さて、iOS向けお小遣い帳アプリを作っていてCoreData内に保存したデータの合計金額を算出しなければならない場面に遭遇したため、その方法についてメモしておきます。
素直に考えると、CoreData内に保存したデータの集計を行う方法には2通りあることがわかります。一つ目は、CoreDataがバックストアに使用しているSQLiteの集計関数を活用する方法。二つ目が、一旦集計を行うオブジェクトをすべてフェッチして、Swift側で集計を行う方法です。
単純に考えて、一つ目の方法の方が速度は速いでしょう。しかし、バックストアにSQLiteを使用していることから、金銭を扱うアプリでは必須とも言える10進数型での集計は行えなさそうです。今回は軽くベンチマークも取ってみたので、その辺のトレードオフも含めて、SQLite組み込みの浮動小数点型とiOS組み込みの10進数型での合計値算出方法を比較してみたいと思います。
func calculateSumUsingCoreData() { let context = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext let request = NSFetchRequest() request.entity = NSEntityDescription.entityForName("Event", inManagedObjectContext: context) let expr_description = NSExpressionDescription() expr_description.name = "sum" expr_description.expression = NSExpression(forFunction: "sum:", arguments: [NSExpression(forKeyPath: "price")]) expr_description.expressionResultType = .DecimalAttributeType //ここで.DecimalAttributeTypeを指定していても、結果はdoubleかfloatでしか取れない request.propertiesToFetch = [expr_description] request.resultType = .DictionaryResultType do { let results = try context.executeFetchRequest(request) if results.count > 0 { let extracted_results = results.map {(elem: AnyObject) -> Double in let dict = elem as! [String: Double] let func_reuslt = dict["sum"] return func_reuslt! } return extracted_results[0] } else { return 0.0 } } catch { let ns_error = error as NSError print("\(ns_error), \(ns_error.userInfo)") return Double.NaN } }
func calculateSumWithReduce() { let context = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext let request = NSFetchRequest() request.entity = NSEntityDescription.entityForName("Event", inManagedObjectContext: context) do { let entities = try context.executeFetchRequest(request) if entities.count > 0 { let events = entities as! [Event] let result = events.reduce(NSDecimalNumber(integer: 0)) { (currentTotal, event) in let price = event.price! return currentTotal.decimalNumberByAdding(price) } return result } else { return NSDecimalNumber(integer: 0) } } catch { let ns_error = error as NSError print("\(ns_error), \(ns_error.userInfo)") return NSDecimalNumber(integer: 0) } }
合計値を算出するコードは、それぞれ以上の通りです。これを新規プロジェクトウィザードのMaster-Detail Applicationテンプレートに当て込んでベンチマークを行います。ちなみに計測を行う部分はこんな感じです。
let RepeatTimes = 10_000 func viewDidLoad() { if let label = self.detailDescriptionLabel { let start = NSDate() for _ in 0..<RepeatTimes { _ = calculateSumUsingCoreData() } let end = NSDate() let elapsed_time = (end.timeIntervalSinceReferenceDate - start.timeIntervalSinceReferenceDate) * 1000.0 let sum = calculateSumUsingCoreData() label.text = "pure core data: \(sum.description), time elapsed: \(elapsed_time)" } if let label2 = self.detailDescriptionLabel2 { let start2 = NSDate() for _ in 0..<RepeatTimes { _ = calculateSumWithReduce() } let end2 = NSDate() let elapsed_time2 = (end2.timeIntervalSinceReferenceDate - start2.timeIntervalSinceReferenceDate) * 1000.0 let sum2 = calculateSumUsingFor() label2.text = "using reduce: \(sum2.stringValue), time elapsed: \(elapsed_time2)" } }
集計対象のデータは、0〜100,000の整数乱数を生成して10で割り、0.0〜10,000.0までの浮動小数点数を100個としています。
ベンチマークを行う端末は、去年までメインで使用していたiPhone5Sです。
そして、その結果がこちら。
純粋CoreDataによる集計法の方が、約3.5倍速いという結果になりました。しかし、そのトレードオフとして、純粋CoreDataの方法では結果が浮動小数点数になることを忘れてはなりません。
さらに少し気になってデータ数を200個にした場合も試してみました。その結果は次の通り。
なんと今度は、CoreDataのみの方が約6.5倍も速いという結果に。(ちなみにまだApple Developer Programに登録していないため、どちらもDebugビルドでの結果になります。正式にアプリをApp Storeに公開したら、再計測してみたいと思います)
さらにさらに、データ数400個の場合。
今度は約11.2倍。結構リニアに差が開いていきます。Swiftで集計する方はメモリーの消費量もかなり大きく、シチュエーションを選びそうです。
う〜ん、よっぽどデータに偏りがない限り、CoreDataの集計関数を使えば誤差もなく十分な気がしなくもないような……
0 件のコメント:
コメントを投稿
なにか意見や感想、質問などがあれば、ご自由にお書きください。