さて、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 件のコメント:
コメントを投稿
なにか意見や感想、質問などがあれば、ご自由にお書きください。