2016年2月26日金曜日

CoreDataによる集計とインメモリ集計の比較

どうも、こんにちは。はざまです。今回は珍しく、前回の投稿からあまり日にちが空いてませんね。
さて、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 件のコメント:

コメントを投稿

なにか意見や感想、質問などがあれば、ご自由にお書きください。