Swift の配列の subscript
では Int
型だけでなく、Range<Int>
型も対応しています。つまり
let array = [Int](0 ..< 10000)
let number = array[1000] // number = 1000
だけでなく、
let array = [Int](0 ..< 10000)
let slice = array[1000 ..< 2000] // slice = [1000, 1001, 1002, ..., 1999]
も使えるのです。ただしこの場合、slice
は ArraySlice
型であり、Array
型ではありません。何が違うかというと slice
の subscript
では array
のものに対応し、slice[1000] == 1000
になります(ちなみに slice[0]
は存在しないのでアクセスしようとするとランタイムエラーになります)。ですので Array
型に変換するにはもう少し手間をかける必要があり、
let array = [Int](0 ..< 10000)
let newArray = Array(array[1000 ..< 2000]) // newArray = [1000, 1001, 1002, ..., 1999]
になります。こうすることでちゃんと newArray[0] == 1000
になります。
さて、おさらいはここまでにして、何が言いたいかというと、配列の一部範囲から要素を取り出して新しい配列を作るのに主には 2 通りの方法があります(もし他にありましたら教えてください m(_ _)m)。一つは map
を使う方法です:
let array = [Int](0 ..< 10000)
let range = 4000 ..< 5000
let newArray = range.map { (i) -> Int in
return array[i]
}
そしてもう一つの方法は先ほどおさらいした通り直接 subscript
に Range<Int>
を代入する方法です:
let array = [Int](0 ..< 10000)
let range = 4000 ..< 5000
let newArray = Array(array[range])
では、この 2 通りの方法のパフォーマンスはどういったものでしょうか?とりあえず確認して見たいと思います。適当に swift ソースファイルを作ってコンパイルして実行してみましょう:
var baseArray = [Int](0 ..< 10000)
let childArray1Range = 3000 ..< 3050
let childArray2Range = 7000 ..< 7100
baseArray = baseArray.map { (i) -> Int in
return i * i
}
let start1 = NSDate()
for _ in 0 ..< 10000 {
let childArray1 = childArray1Range.map { (i) -> Int in
return baseArray[i]
}
let childArray2 = childArray2Range.map { (i) -> Int in
return baseArray[i]
}
}
print(-start1.timeIntervalSinceNow)
let start2 = NSDate()
for _ in 0 ..< 10000 {
let childArray1 = Array(baseArray[childArray1Range])
let childArray2 = Array(baseArray[childArray2Range])
}
print(-start2.timeIntervalSinceNow)
上記の方法でとりあえずベースとなる配列 baseArray
を作って、それぞれ map
と slice
の方法で生成してみて、それを 10000 回ループして実行タイムを計ってみました。そして肝心な結果は自分のパソコン(2013 モデルの MacBook Pro 15" Retina)ではこのようになります:
0.170238018035889
0.19194096326828
はい、ちょっと予想外の結果かもしれませんが、なんとループを回す map
の方がわずかに早かったのです。つまり非常にパフォーマンス重視のシナリオ(例えば SpriteKit の毎秒 60 回呼び出される update(currentTime: NSTimeInterval)
メソッドとか)では基本的には map
で配列を作った方が早い、ということになります。まあもっとも、10000 回実行しても 0.02 秒の差しかないから別に言うほどパフォーマンスに影響はなさそうですけどね。
ところがこれはマルチスレッドを全く考えていないパターンですが、実際のアプリ制作にマルチスレッドを考慮しないことはあまりないじゃないかと思います。特にゲームの場合何でもかんでもメインスレッドに処理させてはいけないのでほぼ確実にマルチスレッド処理が必要です。そうすると、データの処理を一つの専用スレッド(というより正確にはキューですが)に任せるパターンもあるじゃないかと思います。何も考えずにどのスレッドでもデータの操作出来るようにしちゃうと処理が前後してしまって正しいデータが取れなくなってしまうこともありますから。というわけで、上記の処理に、さらにデータ処理スレッドを入れてみたパターンを追加してみましょう:
let queue = dispatch_queue_create("data", DISPATCH_QUEUE_SERIAL)
var baseArray = [Int](0 ..< 10000)
let childArray1Range = 3000 ..< 3050
let childArray2Range = 7000 ..< 7100
dispatch_sync(queue) { () -> Void in
baseArray = baseArray.map { (i) -> Int in
return i * i
}
}
func getElement(i: Int) -> Int {
var result = 0
dispatch_sync(queue) { () -> Void in
result = baseArray[i]
}
return result
}
func getElements(range: Range<Int>) -> [Int] {
var result = [Int](range)
dispatch_sync(queue) { () -> Void in
result = Array(baseArray[range])
}
return result
}
let start1 = NSDate()
for _ in 0 ..< 10000 {
let childArray1 = childArray1Range.map { (i) -> Int in
return baseArray[i]
}
let childArray2 = childArray2Range.map { (i) -> Int in
return baseArray[i]
}
}
print(-start1.timeIntervalSinceNow)
let start2 = NSDate()
for _ in 0 ..< 10000 {
let childArray1 = Array(baseArray[childArray1Range])
let childArray2 = Array(baseArray[childArray2Range])
}
print(-start2.timeIntervalSinceNow)
let start3 = NSDate()
for _ in 0 ..< 10000 {
let childArray1 = childArray1Range.map { (i) -> Int in
return getElement(i)
}
let childArray2 = childArray2Range.map { (i) -> Int in
return getElement(i)
}
}
print(-start3.timeIntervalSinceNow)
let start4 = NSDate()
for _ in 0 ..< 10000 {
let childArray1 = getElements(childArray1Range)
let childArray2 = getElements(childArray2Range)
}
print(-start4.timeIntervalSinceNow)
これで、一つのデータ処理専用直列キュー queue
が一つ作られました。そしてさらにデータの取得にも専用の関数 getElement(i: Int)
と getElements(range: Range<Int>)
も用意しておきました。これで割と通常のアプリ制作に近いイメージになるのではないかと思います。本当にアクセスしたい baseArray
を private
にすれば、外部から直接アクセスする方法がなくなるので、必ず getElement
などのメソッドでデータ処理キューを介してアクセスするしかなくなります。これで、
-
map
で直接データを取る -
slice
で直接データを取る -
map
でデータキューを介してデータを取る -
slice
でデータキューを介してデータを取る
の 4 パターンが作られました。これでもう一回実行してみましょう。すると自分の環境では下記の実行タイムデータが取れました:
0.160254001617432
0.191264986991882
0.994471967220306
0.305835008621216
1 番と 2 番は先ほどのものと同じように、やはり map
の方がほんのわずか早い結果となりました。ところが気になる 3 番と 4 番では、なんと 4 番の方が 3 番よりはるかに早い結果となりました。スピードで言うとシャア専用じゃないのに 3 倍以上です。まあそれでもやはり dispatch
をかませている以上直接データを取るようにかなり遅い結果となりましたが。
なぜこの結果になったかというと、まあ説明するまでもなく dispatch
のせいですね。やはり dispatch
自体はかなり重い作業のようですので、map
は要素の数だけ dispatch
があるのに対し、slice
は一回の dispatch
だけで済むわけですから、範囲が広ければ広いほど、slice
の優位性がより目立つでしょう。
というわけで、適当に配列の一部範囲を取り出して新しい配列を生成する方法のパフォーマンスを比較してみました。皆さんの何か役に立てると嬉しいです。