ログファイルの処理をしていて,メモリに載せるのがまずいからEnumeratorを使うことがよくある.けれどeach_sliceを不用意に使ったせいでメモリオーバーしてしまったのでメモ.
起きた問題
ちゃんと意識すれば当たり前なのだが
pry(main)> (1..100000000000000000000000000).each_slice(1000000000000000000){|slice| slice.each{|item| p item} }
NoMemoryError: failed to allocate memory
こうなる.classをチェックすると
pry(main)> (1..100).each_slice(10).class
=> Enumerator
pry(main)> p (1..100).each_slice(10).map(&:class)
=> [Array, Array, Array, Array, Array, Array, Array, Array, Array, Array]
なので,各sliceはメモリ上に展開されることがわかる.メモリに制約がある状況ではEnumeratorのEnumeratorであってほしい.ということで雑だけど以下のコード
解決策
Enumerator::LazyのメソッドとしてEnumeratorのEnumeratorを返すslicesメソッドを作ってみる
注意: 以下の実装は使い方次第で無限ループに入る
class Enumerator::Lazy
def slices(size)
Enumerator.new do |parent_yielder|
begin
while true
parent_yielder << Enumerator.new do |child_yielder|
(0...size).each do |_|
child_yielder << self.next
end
end
end
rescue StopIteration
end
end
end
end
これで大きなサイズのsliceも扱える
pry(main)>(1..100000000000000000000000000).lazy.slices(1000000000000000000).each{|slice| slice.each{|item| p item} }
=>
1
2
3
...
この実装の問題
下位のEnumeratorの評価に上位のEnumeratorに依存するので,上位だけ評価すると無限ループ
(1..100).lazy.slices(10).each{|slice| p slice }
=> おわらない
これは,下位のExceptionを使って止めるんじゃなくて,上位でも独自にサイズをカウントすればまぁ防げる.ただし2重に評価するので2倍時間かかる
下位のEnumerator同士も評価順によって値が変わる
source = (1..100).map.slices(10)
child1 = source.next
child2 = source.next
p child1.next
=> 1
p child1.next
=> 2
p child2.next
=> 3
どうすりゃいいんだろう…
状態を保ったままEnumeratorをdupすることができれば,親側で消費しながら子を作ってくことができるので解決するが,Enumerator.dupは禁止されている.
(0..size).each{|_| self.next()}
child = self.dup
parent << child
# ↑できない