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

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

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

計測環境

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

https://github.com/rikusouda/CollectionFunctionPerformance

結論

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

計測内容の説明

計測に使ったデータ

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


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の処理の重さがあまり影響しないように何も変換しないmapuserIdが偶数だけにするfilterをするようにしました。

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

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

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

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


// 下記のようなクラス内で計測します
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合計を求めるケース

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


// 下記のようなクラス内で計測します
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などの終端処理で終わる場合はほとんど遜色ありませんでした。

思ったこと

filtermapなど、今回のようなケースだと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の要素を実際に取得しないようになっています。実際に要素にアクセスされたときに、その要素を計算するための関数が実行されていくようなイメージだと思います。


『 Swift 』Article List
Category List

Eye Catch Image
Read More

Androidに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

AWSに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Bitcoinに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

CentOSに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

dockerに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

GitHubに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Goに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Javaに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

JavaScriptに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Laravelに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Pythonに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Rubyに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Scalaに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Swiftに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Unityに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Vue.jsに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Wordpressに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

機械学習に関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。