26
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

[配列].appendは重いからmapを使う

Last updated at Posted at 2021-04-04

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度だけの表示で済みます。

なので、その時間も含まれてしまって大きな差が生まれてしまったという訳です。

原因②

純粋なappendmapの比較になっていないという点です。

for v in 1...1000 {}

let _ = Array(1...1000).map({ $0.description })

この2つの処理は用いられているイテレータが異なる上にmapには余計な変換処理が含まれているので
純粋な比較になっていないそうなんですね。

つまり、別物を比較しているので
そもそも検証は成立しないということです。

※詳しい説明は下の方で解説して下さっているので確認して下さい。

結論

@takehito-koshimizu さんがコメントで仰っていたように
私も適材適所でmapappendも使っていこうという結論に至りました。

もはや皆さんのお言葉を借りてばかりの更新内容となってしまいましたね。

また何か間違いや気づきがあればコメントして下さると有り難いです🙇‍♂️

はじめに

コードレビューにて、メンターに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 2016playgroundで計測していきます。

計測用メソッドはこちらの記事を参考にしました↓
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クロージャ内で
appendmapの処理を記述していきましょう。

処理内容としましては、数値→文字列に変換して
その変換された値が入った配列を作るといったものです。

それを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)

おわりに

配列操作関数とか言われるぐらいだから
配列に何らかの処理をしたい時はバンバン使おうと思いました。

26
12
11

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
26
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?