UnsafeMutableBuffer
を使った記事を書いていて、ふと疑問に思った事があります。
UnsafeMutableBuffer
を使ったコードと、[]
演算子を使ったコードはパフォーマンスがどの程度変わるのか?です。
この記事ではUnsafeMutableBuffer
と[]
演算子のパフォーマンスを比較しました。
どのように比較するか
シンプルにUInt8
の配列を確保し、全要素に順次アクセスで値を設定するという処理を、UnsafeMutableBuffer
を使った実装と[]
演算子を使った実装で時間を計測します。
確保する容量を変更して、どの程度の容量でどの程度、時間が変わるのかを観測します。
テストコード
テストコードは次のようなコードです。
import Foundation func fillSequentialNumber(buffer: UnsafeMutableBufferPointer<UInt8>, length: Int) { var cur = buffer.startIndex let end = buffer.startIndex.advanced(by: length) while cur < end { buffer[cur] = UInt8((cur - buffer.startIndex) % 255) cur = cur.advanced(by: 1) } } func fillSequalNumber(array: inout [UInt8]) { for i in 0 ..< array.count { array[i] = UInt8(i % 255) } } func measureTime(proc: ()->()) -> TimeInterval { let startDate = Date() proc() let time = Date().timeIntervalSince(startDate) return time } func sequentialWriteTest(count: Int, label: String) { var buf = [UInt8](repeating: 0, count: count) let unsafeTime = measureTime { buf.withUnsafeMutableBufferPointer { (ptr) -> Void in fillSequentialNumber(buffer: ptr, length: count) } } let arrayTime = measureTime { fillSequalNumber(array: &buf) } print("\(label)") print(" unsafe: \(unsafeTime) s") print(" array: \(arrayTime) s") } sequentialWriteTest(count: 1024, label: "1KB") sequentialWriteTest(count: 1024 * 1024, label: "1MB") sequentialWriteTest(count: 1024 * 1024 * 10, label: "10MB") sequentialWriteTest(count: 1024 * 1024 * 100, label: "100MB")
実行結果
実行環境
次のような動作環境で実行しました。
- Machine : MacBook Pro 2020 M1
- OS : macOS Big Sur 11.2.3
出力内容
コンソールに次のように出力されました。
1KB unsafe: 0.00014102458953857422 s array: 0.0006769895553588867 s 1MB unsafe: 0.08228003978729248 s array: 0.4330480098724365 s 10MB unsafe: 0.8178319931030273 s array: 4.373885989189148 s 100MB unsafe: 8.186002969741821 s array: 43.434290051460266 s
考察
1KBのときは、差はあれど、0.1msと0.6msなので無視できるレベルです。
1MBになると、82msと433msなので、ユーザー体験としては差を感じるレベルになります。
それ以上の容量になると、もはやプログレスバーインジケーターが必要になるレベルの差があります。
プログラムの中のちょっとした配列、要素数が数千個くらいまでならどちらでも書いても大丈夫ですが、それを超えてくると明らかにUnsafeMutableBuffer
を使った方がパフォーマンス的に有利です。この差はユーザー体験でもはっきりと現れてくると思います。
画像処理などをSwiftで実装する場合には、[]
演算子を使うのは論外と言えます。可能ならば、Swiftで実装するよりもC/C++で実装してSwiftから呼び出すラッパーをObjective-Cでクラス化するか、ラッパーをC関数で実装して、Swiftでラッパークラスやユーティリティクラスを作るのが現実的だと思います。