【Swift】リングバッファーのQueueでベンチマークテスト
突然ですが、プログラミングでQueueを処理するにはどうしますか?
SwiftでQueueを作ってみました。次の単純なプログラムでサクッと作れました。
var max:Int!
var data:[Any] = []
init(_ max:Int) {
self.max = max
}
func enqueue(_ obj:Any) -> Bool {
if data.count > self.max - 1 {
return false
}
data.append(obj)
return true
}
func dequeue() -> Any? {
if data.count > 0 {
let obj = data[0]
data.remove(at: 0)
return obj
}
return nil
}
しかしこのアルゴリズムは、問題があります。とくにC言語の場合で、致命的な問題になる場合があります。
このプログラムでは配列の先頭を削除してますが、内部では同時に配列全体を前方にシフトする処理を行っているので、配列が大きくなればなるほど速度が遅くなってしまうのです。
何か良い方法があるのではと思いQueueのプログラムを調べていくと、リングバッファと呼ばれるアルゴリズムが見つかりました。
サイズの決まった配列を作り、輪のように見立てて考えます。0番から順番にデータを入れていきます。一周回ったら、また0番に戻ってデータを入れていきます。こうすることで、配列をシフトする処理の必要が無くなります。カウントの管理はheadとnumの値で管理します。enqueueしたらnumカウントを1プラスするようにし、dequeueしたら、headカウンタを1プラスします。enqueueすべきインデックスはhead+num番となります。dequeueすべきインデックスはhead番となります。
リングバッファーの説明およびプログラミングは、こちらのサイトが大変参考になりました。 待ち行列
さて、このリングバッファーのQueueを、Swiftでもやってみたいと思いプログラミングしてみました。
var head:Int = 0
var num:Int = 0
var max:Int = 0
var data:[Any]!
var debug = false
init(_ max:Int) {
self.max = max
self.data = Array<Any>(repeating: -1, count: max)
}
@discardableResult
func enqueue(_ obj:Any) -> Bool { // キューが一杯だったらfalseを返す
if num < max {
self.data[(head + num) % max] = obj
num = num + 1
return true
}
return false
}
@discardableResult
func dequeue() -> Any? {
if num > 0 {
let obj = data[head]
data[head] = -1
num = num - 1
head = (head + 1) % max;
return obj
}
return nil
}
動作確認を行うために、XCTestで次のプログラムを組んで確認してみます。
let max = 5
let loop = 3
var num = 0
func sequenceNo() -> Int {
num = num + 1
return num
}
func testRingQueue() {
let q = RingQueue(max)
for _ in 0..<loop {
q.enqueue(sequenceNo())
q.enqueue(sequenceNo())
q.dequeue()
q.dequeue()
q.dequeue()
q.enqueue(sequenceNo())
q.enqueue(sequenceNo())
q.dequeue()
q.enqueue(sequenceNo())
q.dequeue()
q.enqueue(sequenceNo())
q.enqueue(sequenceNo())
q.enqueue(sequenceNo())
q.dequeue()
q.dequeue()
q.enqueue(sequenceNo())
q.enqueue(sequenceNo())
}
}
動作確認の結果です。1つ1つ丁寧に追っていくと、ちゃんとキューイングできていることがわかります。
さらに、最初に作ったQueue.swiftと、先ほどのRingQueue.swiftで速度比較実験を行ってみました。 次のXCTestCaseを書いて、速度を比較してみます。
let max = 1000
let loop = 100*100*100
var num = 0
func sequenceNo() -> String {
num = num + 1
return "hogehogehogehoge\(num)"
}
func testQueue() {
print("[Queue.swift]\n\n\n")
let q = Queue(max)
for _ in 0..<loop {
q.enqueue(sequenceNo())
q.enqueue(sequenceNo())
q.dequeue()
q.dequeue()
q.dequeue()
q.enqueue(sequenceNo())
q.enqueue(sequenceNo())
q.dequeue()
q.enqueue(sequenceNo())
q.dequeue()
q.enqueue(sequenceNo())
q.enqueue(sequenceNo())
q.enqueue(sequenceNo())
q.dequeue()
q.dequeue()
q.enqueue(sequenceNo())
q.enqueue(sequenceNo())
}
}
func testRingQueue() {
print("[RingQueue.swift]\n\n\n")
let q = RingQueue(max)
for _ in 0..<loop {
q.enqueue(sequenceNo())
q.enqueue(sequenceNo())
q.dequeue()
q.dequeue()
q.dequeue()
q.enqueue(sequenceNo())
q.enqueue(sequenceNo())
q.dequeue()
q.enqueue(sequenceNo())
q.dequeue()
q.enqueue(sequenceNo())
q.enqueue(sequenceNo())
q.enqueue(sequenceNo())
q.dequeue()
q.dequeue()
q.enqueue(sequenceNo())
q.enqueue(sequenceNo())
}
}
QueueのMax sizeを変えて実験してみました。100万回のループ処理における速度結果を次に示します。
Queue max | Queue.swift(seconds) | RingQueue.swift(seconds) |
---|---|---|
10 | 9.954 | 11.201 |
100 | 12.377 | 10.863 |
1000 | 33.233 | 11.789 |
10000 | 232.331 | 10.712 |
上記データをグラフにしてみました。縦軸はQueueのMax size、横軸は100万回処理にかかった秒数を表します。
QueueのMax sizeが小さい場合は、どちらのアルゴリズムでも大した差はありません。しかし、Max sizeが大きくなるほどQueue.swiftでは顕著に速度が遅くなってしまいました。これは最初に説明した通り、C言語と同様にSwiftでもArrayをシフト処理をしているからだと思われます。
一方、RingQueue.swiftの方は、Max sizeを変えても速度変化はみられませんでした。それもそのはず、Arrayそのものは増やしたり、減らしたりせず、参照するインデックスを足したり減らしたりしているだけなので、QueueのMax sizeには依存しないのです。