14
14

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

配列の一部範囲を取り出して新しい配列を生成する方法のパフォーマンス比較

Posted at

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]

も使えるのです。ただしこの場合、sliceArraySlice 型であり、Array 型ではありません。何が違うかというと slicesubscript では 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]
}

そしてもう一つの方法は先ほどおさらいした通り直接 subscriptRange<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 を作って、それぞれ mapslice の方法で生成してみて、それを 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>) も用意しておきました。これで割と通常のアプリ制作に近いイメージになるのではないかと思います。本当にアクセスしたい baseArrayprivate にすれば、外部から直接アクセスする方法がなくなるので、必ず getElement などのメソッドでデータ処理キューを介してアクセスするしかなくなります。これで、

  1. map で直接データを取る
  2. slice で直接データを取る
  3. map でデータキューを介してデータを取る
  4. 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 の優位性がより目立つでしょう。

というわけで、適当に配列の一部範囲を取り出して新しい配列を生成する方法のパフォーマンスを比較してみました。皆さんの何か役に立てると嬉しいです。

14
14
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
14

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?