Ruby界隈において複数のコレクションを並列処理する場合、一般的には外部イテレータが役立つと解説される事が多い。しかしRubyは内部イテレータが非常に強力な言語であるため内部イテレータを使って並列処理を実装するのも比較的簡単にできる。それを考えれば外部イテレータに優位性があるかというと実際問題としては微妙なところだろう。
特にEnumeratorを用いた外部イテレータ機能はFiberで実装されているため、普通のイテレータに比べて大幅に遅い点も注意が必要だ。
下記のサンプルを元に様々なイテレータ例を列挙する。
VAL_NAMES = ("a".."sf").to_a # 変数名配列500要素(仮)
VALS_DATA = (1..500).to_a # 変数配列500要素(仮)
# 二つの配列から[index, name, value]を表示する場合
Enumeratorを外部イテレータとして使った実装例
names = VAL_NAMES.each.with_index # 変数名配列のEnumerator.with_indexを得る
vals = VALS_DATA.each # 変数配列のEnumeratorを得る
loop do # 無限ループ
n, i = names.next # nextで要素を取り出し、無ければloopを抜ける
v = vals.next # nextで要素を取り出し、無ければloopを抜ける
p [i, n, v] # [index, name, value]を出力
end
while文による(Enumeratorを使わない)外部イテレータの実装例
vsize = VALS_DATA.size # 変数配列のsizeを得る
i = -1
while (i = i.succ) < vsize # while文でループ(indexをインクリメント)
n = VAL_NAMES[i] # 名前を取り出す
v = VALS_DATA[i] # 変数の値を取り出す
p [i, n, v] # [index, name, value]を出力
end
for文を使った外部イテレータの実装例
forは内部でeachを呼んでいる
i = 0
for v in VALS_DATA # for文で変数の値を取り出す
n = VAL_NAMES[i] # 名前を取り出す
p [i, n, v] # [index, name, value]を出力
i = i.succ # indexをインクリメント
end
内部イテレータを使った実装例
VALS_DATA.each_with_index do |v,i| # 内部イテレータ(each_with_index)で変数の値とindexを取り出す
n = VAL_NAMES[i] # 名前を取り出す
p [i, n, v] # [index, name, value]を出力
end
内部イテレータ(zip)を使った実装例
二つの配列を合成した二次元配列を作るので配列が大きくなると効率が悪くなる
VALS_DATA.zip(VAL_NAMES).each_with_index{|(v,n),i| p [i, n, v] } # 1行で書ける
並列処理parallelイテレータを新しく定義する例
class Array
def parallel(ary)
return to_enum(:parallel, ary) unless block_given?
vsize = size
i = -1
yield(self[i], ary[i], i) while (i = i.succ) < vsize
self
end
end
parallelイテレータを使った実装例
VALS_DATA.parallel(VAL_NAMES){|v,n,i| p [i, n, v] } # シンプルに書ける
parallelイテレータのEnumeratorを外部イテレータとして使った実装例
data = VALS_DATA.parallel(VAL_NAMES) # 変数配列と変数名配列からEnumeratorを得る
loop do # 無限ループ
v, n, i = data.next # nextで要素を取り出す 要素がなくなるとloopを抜ける
p [i, n, v] # [index, name, value]を出力
end
速度比較 500要素配列1000回ループ8回平均
出力部分(p)のみコメントアウトして計測。
| code | time | notes |
|---|---|---|
| Enumerator | 4.7927s | 内部でFiberを使用しているので圧倒的に遅い |
| Enumerator:parallel | 2.4226s | Fiberが一つで済む分、Enumerator式よりマシ |
| while | 0.0236s | Fiberもブロックも使用していないので速いが冗長 |
| for | 0.0445s | 内部でeachを使っているのでwhileよりは遅い |
| each_with_index | 0.0517s | ブロックを使っている分やや遅い |
| zip | 0.0828s | 二次元配列を作るため要素が増えるほど非効率 |
| parallel | 0.0465s | zip以上の簡潔さでeach_with_indexよりも速い 個人的には並列処理のベストプラクティス |
parallelイテレータは配列の数が3つ以上になると使えなくなるのが欠点だが実用レベルでは十分だし、拡張するのも簡単。というか3つ以上ならfor文でいいかも。
※ i += 1 よりも i = i.succ の方が誤差レベルだがわずかに速い