今回も引き続き、お気に入りな技術ブログ「その Swift コード、こう書き換えてみないか」で紹介されている事例を見ていきますね。前回は zip
や indexed
でコレクションの各要素とインデックスとを結び付ける話を徹底的に見て終わらせた心地でしたけれども、ブログ的に主題な計算速度のまわりを後で実際に眺めてみると面白い感じでしたので、今日もさらにもう少し、そんな計算速度のあたりを見ていこうと思います。よろしくお願いしますね。
——————————————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #261
00:00 開始 00:33 前回に見終わった計算量の話 02:57 考え方に触れていくことは大切 04:50 正解よりも、答えを導く考え方に価値あり 05:51 zip, enumerated, indexed のパフォーマンス 06:21 大きな差があることを表記する方法 08:11 あくまでも、現時点では速いという事実 10:22 本当にそこまで速度さは生まれる? 11:15 処理速度を計測するための準備 12:22 計算速度を測るコード 12:36 enumerated の速度を計測するコード 15:05 zip の速度を計測するコード 15:29 indexed の速度を計測するコード 16:25 インデックスを絡めて繰返処理をしたときの速度を実測 17:48 実際にビルドしたコードの方が有意義な速度計測ができそう 19:52 最適化なしの実行速度 20:33 今回のコードは、最初だけ遅く計測される様子 23:33 初回実行の誤差を考慮して再計測 24:03 変数を活かした処理速度の向上 25:40 ビルドを最適化したときの実行速度 28:07 リリースビルドで見たとき、どれが良さそう? 29:36 今回の所感とクロージング ———————————————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #261
では、引き続きSwiftコードの分析を進めていきましょう。今回は、私のお気に入りのブログ記事を参考に進めます。このブログ記事は非常に内容が濃く、これだけでも1ヶ月以上の学びが得られるほどです。
前回の勉強会では、実務での関数の中でインデックスと要素を合わせて操作するのは効率が悪いということで、別の方法を模索しました。その結果、新しい知見を得て、こちらの方法がいいのではないかと評価しました。
ブログの中では、計算量や計算速度についての話もありました。そこで今回は、さらに詳しくそれを測定してみることにしました。実際に測定してみると非常に興味深い結果が得られましたので、今回はその結果をみんなで確認し、理解を深めていきたいと思います。
まず、このブログをおさらいします。ブログでは、インデックスと要素を組み合わせて操作する際に、enumerateではなくzip
やインデックスで直接操作する方法が推奨されています。enumerateを使わない理由や、zip
の選択肢が出てくる背景も説明されています。
ブログの筆者は、非常に詳細に考察を進めており、その過程で多くの気付きを得られます。彼の考え方を追っていくことで、プログラミングの初心者から中上級者までが学びを深められる構造になっています。特にパフォーマンス面において、具体的な数値を示しながら、各方法の違いを説明しています。
以下に示すとおり、enumerateやzip
の使い方についての具体的なコード例が出てきます。サンプルコードを以下に示します。
for (index, element) in array.enumerated() {
// 処理
}
for (index, element) in zip(array.indices, array) {
// 処理
}
このコーナーでは主にenumerateが遅い理由や、それに代わる手法としてzip
を使うべきだという説明がなされています。さらに、enumerateとzip
を用いた場合の計算速度についても測定されています。この測定結果が非常に興味深く、enumerateよりもzip
の方が明らかに速いことがわかります。
次に、実際のパフォーマンス測定を行いました。そのために、自作のモジュールを作成し、計算速度を記録する関数を定義しました。以下のコードで計算速度を測定する方法を紹介します。
import MyPerformanceModule
func processTime() -> Void {
let start = Date()
// 処理
let end = Date()
print("処理時間: \\(end.timeIntervalSince(start)) 秒")
}
processTime()
このコードでは、「MyPerformanceModule」を使って計算速度を記録するためのモジュールを導入しました。具体的な処理対象を組み合わせた計測結果を確認し、enumerateよりもzip
の方が速いことを確かめました。
このような定量的なデータを基に最適な手法を選ぶことが重要です。パフォーマンスの観点から見ても、現在のベストプラクティスを理解し、最適なコードを書くことが求められます。
この後も、引き続き計算速度や実装手法についての考察を進めていきます。それでは、詳細なコード解説と共に、さらなる最適化の方向性を探っていきましょう。 配列があって、リピーティングゼロがあります。カウントがあって、このカウント分配列を要素として用意します。その上で、エナミュレーターで計算するパターンで最適化されると、本当はここでは最適化されないのですが、最適化されると使わないときに最適化で何も処理しなくなってしまいます。計算系を固めているやつです。
そのため、最後に変わっていますが、何をしているかというと、値を保存する場所を用意して、これはループの中で何らかの計算的負荷を用意するために演算を行なっています。その演算が最適化でさっぱり消えないように変数に蓄え、その上で蓄えた変数を判定するという処理です。
まず、エナミュレーターで取って、素直にオフセットでやるパターンです。ただ、これは特に意味を持たせていないオフセットを完全に使っているだけです。オフセットとエレメントの計算はどうでもいいのですが、とにかく得られたオフセットとエレメントを使っているというだけです。計算的負荷が欲しかっただけで、最適化でどこかが適当に捨てられないようにしています。
次に、インデックスをちゃんと取ったやつで、values
のイナミュレーションで取ったオフセットからインデックスをスタートインデックスからオフセットをずらして適切なインデックスにしたという処理です。これが本来の意味で正しい処理です。この速度を測ってみたところ、エナミュレーターを使ったものと、オフセット倍を使ったものとの速度比較もしてみましたが、ちょっと長すぎるのでここを省略します。そして、次はジップについての話をします。 バリューのインディシーズとバリューズのジップを組み合わせると、細かい手間をかけずに効率的な結果を得られるでしょう。インデックスも普通に取得できて、コードが分かりやすいのも良い点ですね。
さて、次に思い浮かぶのは、同等の結果を得るための別の方法です。例えば、in values
で普通にenumerate
してエレメントを取得する方法もあります。先頭から順番にインデックスを取得するなら、これも良い方法です。ジップを使わずに、ループの中でインデックスを更新することで実現できます。
この方法も素晴らしいですね。このように、方法は大きく4つあります。これらを高速計算してみると、どういう結果になるのでしょうか。結果として、enumerate
が0.0268秒、enumerate
プラスオフセット倍が0.2266秒となり、これは少し遅めです。zip
を使った場合は0.02976秒で、少しだけ遅くなりました。インデックスを用いた場合は0.028秒で、zip
よりも速くなりました。
しかし、面白いことに、インデックスアフターを使った場合は34秒もかかる原因不明な結果になりました。この結果は、どう考えても34秒もかかるはずがなく、メモリー領域をそのまま使用しているにも関わらず、非常に遅くなりました。
これをコンパイラーで正しく動かすためのコードも作成しました。enumerate
とインデックスオフセット倍にもう一度注目すると、enumerate
バージョンはインデックスを省いて要素をそのまま利用しています。インデックスオフセット倍の場合は、enumerate
でオフセットを得た後、インデックスに変換して計算負荷をかけるため、こちらの方が時間がかかります。
これらの状況を踏まえて最適化のポイントを押さえたプロジェクトを構築しましょう。プロジェクトのビルドセッティングなども最適化されていますので、何が適切か理解しながら進めるのが良いでしょう。 ターミナルアップのアプリケーション側をしっかり確認すると、オプティマイズのオプティマイゼーションレベルが太字になっていません。これはプロジェクト設定の方なので、こちらでもデフォルトかパステストなのかを確認します。
次に、-O
とかが増えたかどうかを見てみますが、こちらの設定は間違えました。これはコンパイラレベルの設定です。AppleのCランクモードではなく、Swiftコンパイラの設定を見てみましょう。
現状、最適化は無し(-O
無し)で、一般的なデバッグビルドで進めます。これでビルドをして実行しましょう。そうすると、速度が表示されます。例えば、先ほどのフォーループで実行すると、デバッグビルドだと40秒くらいかかりましたが、最適化なしの設定ではそこまでかからないはずです。
この設定で速度がどのように変わるかを確認してみてください。 プレイグラウンドやデバッグビルドで試すとき、時間がかかっているように見えるかもしれません。ただ、どこで時間がかかっているかはっきりとわからないことがあります。
まず、イテラティブな方法で実行した場合とインデックスオフセットを用いた場合を比較しましたが、やはりインデックスオフセットを使う方が速いです。例えば、イテラティブだけのときは0.298秒かかるのに対して、インデックスオフセットを使うと0.290秒でした。ただ、最初に実行する関数の順序によっても計測値が変わることがあるので、何度か実行して確認しました。
次に、for-in
ループでインデックスを外部に持たせた場合、0.1秒という非常に速い結果が出ました。イテラティブよりも速いので、速度を重視する場合はこちらの方法が有効です。ただし、Swiftではできるだけvar
を使わないよう推奨されていますが、関数内の限られたスコープで使う分には問題ありません。このようにvar
を使ってバッファを持たせる方法も一つの手段だと思います。
再帰呼び出し方法の速度も気になりますが、今回は別の要素に注目したいため、そこは後日試すことにします。
最適化ビルドで実行した結果を調べると、圧倒的な速度差が見られました。要素の数を増やして比較したところ、イテラティブは1.5秒、zip
は1.49秒という結果でした。誤差の範囲内かもしれませんが、zip
の方が若干速いこともありました。
最終的に、リリース向けにコードを最適化する場合、インデックスが意味的に明快で使えるなら、それを利用するのが最も効率的です。速度面でも安定的に速いです。しかし、Swift標準ライブラリの外部モジュール(たとえばSwift Algorithms)を使う必要が出てくるため、敷居は少し高いと言えます。これらを踏まえて、ジップやインデックスオフセットを用いる選択肢も有効です。
このように、様々な方法を試し、それぞれの特徴や速度差を理解することが重要です。 気にするほどのパフォーマンスではないので、読みやすさや伝わりやすさを優先しましょう。状況によって、どの選択が最適かは変わってきます。そのため、各状況に応じてどれが良さそうかを判断していくことが重要です。
このように、for
ループやenumerated
、zip
、そしてインデックスも、状況によってどれを使うべきかが異なることがあります。計算速度に関しては、圧倒的な差は見られないことが多いです。それに基づいて、実際にこれらを使う場面において、適切な選択ができるようになると良いですね。
今回の検討は非常に有意義でした。次回は、さらに面白そうな視点からオーインループやプロダクトの使用について見ていこうと思います。
今日はこれで終わりにします。お疲れ様でした。ありがとうございました。