0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Ruby】並列処理イテレータのベストプラクティス

0
Last updated at Posted at 2026-07-21

 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 の方が誤差レベルだがわずかに速い

0
1
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
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?