Swiftで、valueがArrayとなっているDictionaryに対して、そのvalueにappendしたときのパフォーマンスがとても悪かった。
…どういうことかというと、例えば、
1 |
var scores = [String: [Int]]() |
という、keyはStringで、valueにIntの配列を持つscoresに対して、
1 |
scores["someKey"].append(10) |
といった感じで、処理を行っているコードがとても遅かった。
調べてみると、どうやら都度Copy on Writeを行っているかららしい。(Copy on Writeをしていたかどうかは確認していない)
で、リンク先にあるように書き換えて対応した。
Dictionaryのvalueを、removeValueでポップして、ポップしたvalueにappendして、再代入…と、やれば良いみたい。
removeValueでポップしてるから、変更しても、Copy on Writeは走らないってことなのかな。
結果、速度は、桁違いに早くなった。
実験したコードがあるので、貼ります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 |
import UIKit //例えば、何かの得点をあらわすモデル。 struct Score { //日付の年月 var ym = "" //なんらかの値 var value = 0 } class ViewController: UIViewController { //仕分け前のデータ var scores = [Score]() override func viewDidLoad() { super.viewDidLoad() //テストデータ作成。 self.scores = [ Score(ym: "2017-07", value: 1), Score(ym: "2017-06", value: 1), Score(ym: "2017-05", value: 1), Score(ym: "2017-04", value: 1), ] //データを増幅させる。 for _ in 0...14 { self.scores.append(contentsOf: self.scores) } } override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() } @IBAction func startButtonTapped(_ sender: UIButton) { print("全件数は[\(self.scores.count)]件です。") var start = CACurrentMediaTime() var result = self.case1() var end = CACurrentMediaTime() print("case1 実行時間:[\(end - start)]秒") start = CACurrentMediaTime() result = self.case2() end = CACurrentMediaTime() print("case2 実行時間:[\(end - start)]秒") } //実行時間が遅いコード func case1() -> [String: [Score]] { var group = [String: [Score]]() //self.scoresを、年月(yyyy-MM)ごとに仕分けしたい。 for score in self.scores { if let _ = group[score.ym] { //ここが時間かかる。 group[score.ym]!.append(score) } else { group[score.ym] = [score] } } return group } //実行時間改善 func case2() -> [String: [Score]] { var group = [String: [Score]]() //self.scoresを、年月(yyyy-MM)ごとに仕分けしたい。 for score in self.scores { //ここで要素をポップすることで、複製の発生をなくしている。 var temp = group.removeValue(forKey: score.ym) ?? [] temp.append(score) group[score.ym] = temp } return group } } |
コードの解説
Score構造体を、年月の文字列で、仕分けするのが目的のプログラムです。
(そもそも実際に現場で書いていたプログラムでは、Scoreは、Date型のプロパティを持っていて、”yyyyMM”の形式のString型に変換して仕分けしていました。今回は実験のため、最初からString型で年月を定義しています。)
UIButtonを置いて、タップしたら、startButtonTappedで検証開始していました。
で、case1は遅い方、case2が改善した早いコードです。
1 |
var temp = group.removeValue(forKey: score.ym) ?? [] |
の部分は、丁寧に書くと、
1 2 3 4 |
var temp = group.removeValue(forKey: score.ym) if temp == nil { temp = [Score]() } |
と、なります。
データの数が数千個だけでも、結構違うぞ!
結果
俺の環境では、データ約13万件に対して、
case1のコードで、約24秒。
case2のコードで、約0.15秒!
でした。
100倍違う!はんぱねぇ!
コメント