Qiita更新しました
数多くの考察コメント、本当にありがとうございました!
考察コメントを参考に更新していきたいと思います。
結果から
まず、結果としてappendもmapも処理速度に大差ない
という事です。
@YOCKOW さんがGoogleのswift-benchmarkを使って
検証して下さったのですがコメント欄にもあるように以下の結果となりました。
name time std iterations
----------------------------------------------------------------
append 151697.500 ns ± 46.06 % 6718
append with initial capacity 145791.500 ns ± 45.14 % 6828
map 140519.000 ns ± 41.81 % 8355
計測対象としましては、
・append
・append(事前にcapacityを確保)
・map
の3つです。
結果を見て分かるように、ほとんど違いがない
ですね。
そもそも私のQiitaに書いてあった配列に要素を一個追加するたびにメモリを確保し直すということ
はしないそうでArrayのバッファ(データを一時的に蓄えておく記憶装置や記憶領域)
は拡張が必要な場合、capacityを元の2倍以上で設定する
そうです。
swift/ArrayShared.swift | GitHub
では何故、私の方では差が大きく生まれてしまったのでしょうか?
原因①
まず、主な原因としましてはPlayground
を使用していた点です。
この原因の詳細も有り難いことに、つよつよエンジニアの方がコメントして下さっているのですが
Playgroundではかなりの時間がエディタ右側の表示を行うために使用される
そうなんですね。
私のQiitaで書いてあるappendの処理の場合は、1000回の表示
が行われるのに対して
mapの処理では1度だけの表示
で済みます。
なので、その時間も含まれてしまって大きな差が生まれてしまったという訳です。
原因②
純粋なappend
とmap
の比較になっていないという点です。
for v in 1...1000 {}
let _ = Array(1...1000).map({ $0.description })
この2つの処理は用いられているイテレータが異なる上にmapには余計な変換処理が含まれている
ので
純粋な比較になっていないそうなんですね。
つまり、別物を比較しているので
そもそも検証は成立しないということです。
※詳しい説明は下の方で解説して下さっているので確認して下さい。
結論
@takehito-koshimizu さんがコメントで仰っていたように
私も適材適所でmap
もappend
も使っていこうという結論に至りました。
もはや皆さんのお言葉を借りてばかりの更新内容となってしまいましたね。
また何か間違いや気づきがあればコメントして下さると有り難いです🙇♂️
はじめに
コードレビューにて、メンターにappendは重いからmapを使おう
と
指摘して頂いたのがことの始まり。
appendの場合
ある配列を取得したい場合、大体がこんな感じのコードになると思います。
var a: [Int] = []
var b = [1, 2, 3, 4, 5]
for int in b {
a.append(int)
}
print(a) // [1, 2, 3, 4, 5]
ただ、これはめちゃくちゃ重い処理
になってしまうんですね。
appendの場合、コードを見て分かるようにaの配列は空
ですので
要素数が決まっていません。
var a: [Int] = []
もちろん、この時点ではどのぐらい要素を代入するかというのは決まっていません。
ただの空の配列が定義されているだけです。
実際に要素数を決めていくのは、この部分ですね。
for int in b {
// 要素数があるだけ処理を繰り返す
a.append(b)
}
上記の処理を端末のメモリ上で表すと、こんな感じになります。
|a|配列をメモリ内に確保したよ|b|c|d|
↓ append
|a|配列をメモリ内に確保したよ|b|c|d| // 配列を代入したいのに空いていないから一個ずらすね
|a|配列をメモリ内に確保したよ|appendされた値1だよ|b|c|d|
↓ append
|a|配列をメモリ内に確保したよ|appendされた値1だよ|b|c|d| // 配列を代入したいのに空いていないから、また一個ずらすね
|a|配列をメモリ内に確保したよ|appendされた値1だよ|appendされた値2だよ|b|c|d|
// 以下ループ
.
.
.
.
このような処理が、要素の数だけ繰り返されます。
代入する要素が少なければ、まだ動くかもしれませんが
APIから返ってきた値などをappendするとなると、かなりの負荷になりそうですね。
mapの場合
一個ずつ値を足していくappend
に対してmap
はどうなんでしょうか?
冒頭で出てきた処理を書き直してみましょう。
// appendの場合
var a: [Int] = []
var b = [1, 2, 3, 4, 5]
for int in b {
a.append(int)
}
print(a) // [1, 2, 3, 4, 5]
// mapの場合
let c = b.map({ $0 })
print(c) // [1, 2, 3, 4, 5]
空配列に対して一個ずつ足していくappend
よりも
map
は予めクロージャー内を確認し、一気に代入するので処理が軽い
です。
無駄に空の配列を用意しなくてもいいですし
コード量も抑えることが出来ます。
メモリで表すとこんな感じです。
// let c = b.map({ $0 })の処理が走ったら...
|a|1?|2?|3?|4?|5?|b|c|d| // 要素数が5つの配列が入るらしいから空けとくよ
↓
|a|1|2|3|4|5|b|c|d| // 事前に入る要素数が決まっていたから、ずらす必要がなかったね
こんな感じで、map
は効率よく新しい配列を作ってくれます。
実際に計測してみた
MacBookPro 2016
のplayground
で計測していきます。
計測用メソッドはこちらの記事を参考にしました↓
SwiftやiOSの便利だけど忘れそうな小ネタ集
まずは計測用のクラスを作って、計測の処理をしてくれるメソッドを作っていきます。
final class Benchmark {
static func measure(_ targetStr: String = "", block: () -> Void) {
// 処理始まりの時間を取得
let startTime = Date()
// 計測対象のクロージャー
block()
// 処理終わりの時間を取得
let elapsed = Date().timeIntervalSince(startTime) as Double
// 計測した時間を見やすい文字列に変える
let formatedElapsed = String(format: "%.3f", elapsed)
// 出力する
print("Benchmark: \(targetStr), 実行時間: \(formatedElapsed)(s)")
}
}
このようなメソッドを呼び出して、blockクロージャ内で
append
とmap
の処理を記述していきましょう。
処理内容としましては、数値→文字列
に変換して
その変換された値が入った配列を作るといったものです。
それを1000回
繰り返します。
Benchmark.measure("append") {
var appendList: [String] = []
for v in 1...1000 {
appendList.append(v.description)
}
}
append
の場合、結果は以下のようになりました。
Benchmark: append, 実行時間: 0.715(s)
それでは、map
も計測していきましょう。
Benchmark.measure("map") {
let _ = Array(1...1000).map({ $0.description })
}
このような結果になりました。
Benchmark: map, 実行時間: 0.008(s)
計測結果は並べてみると、違いは歴然です!
これでappend
よりもmap
の方が
処理が早い
ということが分かりました。
Benchmark: append, 実行時間: 0.715(s)
Benchmark: map, 実行時間: 0.008(s)
おわりに
配列操作関数とか言われるぐらいだから
配列に何らかの処理をしたい時はバンバン使おうと思いました。