Swiftのfilterやmapをチェーンしたときの実行時間とlazy

Swiftの`filter`や`map`を使うとやりたいことを宣言的に書けるのでコードを書くのも楽ですしメンテナンス性もよくなる場面が多いですね。ただ、filterやmapを連続で呼び出して実行時間的に問題がないのかよくわかっていなかったので、実行時間も気にしたベストプラクティスを調べてみました。

`lazy`というやつで遅延評価できるらしいですが、これがどのように実行時間に影響してくるのかも調べてみました。

# 計測環境

私はiOSのアプリ開発のためにSwiftを使うことが多いので、iPhone 6sにて実行速度を計測しました。(もちろんデバッグビルドではなくReleaseビルドで)
計測プログラムは下記のリポジトリで公開しています。

https://github.com/rikusouda/CollectionFunctionPerformance

# 結論

自前で`for`や`forEach`するのが最速です。
でもうまく`lazy`を使えば自前ループに比べても大きな遜色はなさそうです。

# 計測内容の説明

## 計測に使ったデータ

どこかのJSONからユーザーデータをとってきたような雰囲気で、下記のようなデータを`5000件`処理するような前提にしました。

```swift

struct UserData {
    init(userId: Int) {
        self.userId = userId
    }
    var userId: Int
    var userName: String = "rikusouda"
    var userProfileIconURL = "https://pbs.twimg.com/profile_images/842040493827534848/YBx-Bbdy_400x400.jpg"
    var userProfile = "I am iOS application developer"
}

let sourceArray = [Int](0..<5000).map { UserData(userId: $0) }
```

## 行った計測

mapやfilterの処理の重さがあまり影響しないように`何も変換しないmap`と`userIdが偶数だけにするfilter`をするようにしました。

元の配列から下記のようなものを求めるケースで計測しました。

* 結果を新しいArrayにするケース
* 結果のuserId合計を求めるケース

### 結果を新しいArrayにするケース

下記のようなパターンでどれが最速なのか計測しました

```swift

// 下記のようなクラス内で計測します
class PerformenceTester {
    let sourceArray = [Int](0..<5000).map { UserData(userId: $0) }
    
    // mapで使用する関数
    private static func mapFunction(_ data: UserData) -> UserData {
        return data
    }
}

// [ケース1] そのままmapとfilter
let newArray = sourceArray
    .map(PerformenceTester.mapFunction)
    .filter { ($0.userId % 2) == 0 }

        
// [ケース2] lazyにしてからmapとfilter。それをArrayに変換
let newArray = Array(sourceArray
    .lazy
    .filter { ($0.userId % 2) == 0 }
    .map(PerformenceTester.mapFunction)
)

// [ケース3] lazyにしてからmapとfilter。それをreduceでArrayにする
let newArray = sourceArray
    .lazy
    .filter { ($0.userId % 2) == 0 }
    .map(PerformenceTester.mapFunction)
    .reduce(into: [UserData]()) { (result: inout [UserData], data: UserData) in
        result.append(data)
    }

// [ケース4] forでArrayを作る
var newArray = [UserData]()
for value in sourceArray {
    if value.userId % 2 == 0 {
        newArray.append(PerformenceTester.mapFunction(value))
    }
}

```

計測結果

| ケース | 実行時間(ミリ秒) |
|:-----:|:-------------:|
| ケース1 | 1.01 |
| ケース2 | 0.66 |
| ケース3 | 0.75 |
| ケース4 | 0.70 |

もっとチェーンをつないだときの計測のためにmapの回数を3回にして計測してみました。

計測結果(map3回する版)

| ケース | 実行時間(ミリ秒) |
|:-----:|:-------------:|
| ケース1 | 1.73 |
| ケース2 | 3.53 |
| ケース3 | 0.74 |
| ケース4 | 0.70 |

なぜかケース2がめちゃくちゃ遅い。Arrayのイニシャライザでなにか非効率的なことでもやっているのかもしれません。これを勘案すると、Arrayのイニシャライザで新しいArrayを作るのはおすすめできないと思いました。

読みやすさも考慮すると `ケース3` (lazyで処理してreduceでArrayを作る)が一番良さそうです。


### 結果のuserId合計を求めるケース

下記のようなパターンでどれが最速なのか計測しました

```swift

// 下記のようなクラス内で計測します
class PerformenceTester {
    let sourceArray = [Int](0..<5000).map { UserData(userId: $0) }
    
    // mapで使用する関数
    private static func mapFunction(_ data: UserData) -> UserData {
        return data
    }
}

// [ケース1] そのままmapとfilterして合計算出
let result = sourceArray
    .map(PerformenceTester.mapFunction)
    .filter { ($0.userId % 2) == 0 }
    .reduce(0, { (result, val) -> Int in
        return result + val.userId
    })

        
// [ケース2] lazyにしてからmapとfilter。それから合計算出
let result = Array(sourceArray
    .lazy
    .filter { ($0.userId % 2) == 0 }
    .map(PerformenceTester.mapFunction)
    .reduce(0, { (result, val) -> Int in
        return result + val.userId
    })
)

// [ケース3] forで合計算出
var result = 0
for value in sourceArray {
    if value.userId % 2 == 0 {
        result +=
            PerformenceTester.mapFunction(
                PerformenceTester.mapFunction(
                    PerformenceTester.mapFunction(value)
                )
            ).userId
    }
}

```

計測結果

| ケース | 実行時間(ミリ秒) |
|:-----:|:-------------:|
| ケース1 | 1.13 |
| ケース2 | 0.092 |
| ケース3 | 0.079 |

もっとチェーンをつないだときの計測のためにmapの回数を3回にして計測してみました。

計測結果(map3回する版)

| ケース | 実行時間(ミリ秒) |
|:-----:|:-------------:|
| ケース1 | 1.87 |
| ケース2 | 0.092 |
| ケース3 | 0.080 |

こちらも、自前のループが最速でしたがlazyにしてから処理してreduceなどの終端処理で終わる場合はほとんど遜色ありませんでした。

## 思ったこと

`filter`や`map`など、今回のようなケースだと1ミリ秒とかの話なのであまりアプリの使い勝手に影響する場面は少ないのかもしれません。
ですが`lazy`にしてから処理して`reduce`などの終端処理で終わるように書くだけで実行速度は格段に速くなる(自前でforを書いたときに近くなる)ので、少し気にして普段から書くようにするとよいと思います。

余談ですが、Arrayにするときに使っているreduceですがSwift4で追加された機能です。これはresultのArrayがinoutなの要素ごとにコピーすることなくappendで値を追加していくことができるので高速です。

## おまけ: lazyについて調べてみた

lazyが何をしているのか気になって調べてみました。
下記のドキュメントを参考にしました。

https://developer.apple.com/documentation/swift/lazysequenceprotocol

ここを見ると、`scan`という新しいmap的なメソッドをlazy対応する例が載っています。実際に`LazyScanIterator.next()`が呼び出されるまで、元sequenceの要素を実際に取得しないようになっています。実際に要素にアクセスされたときに、その要素を計算するための関数が実行されていくようなイメージだと思います。