NSEG13で軽く紹介したのですが、MmapScannerというRubyライブラリを作ってみました。
簡単にいうと StringScanner のようなことを mmap(2) 領域に対して行えるようにしたものです。
インストール方法
% gem install mmapscanner
次のようにすると、ファイルを mmap(2) して MmapScanner オブジェクトを返します。
file = File.open('filename') ms = MmapScanner.new(file)
あとは StringScanner と同じように、scan, skip 等が使えます。
StringScanner と異なり、MmapScanner は取り出した値も MmapScanner で、元の MmapScanner とメモリ領域を共有してます。MmapScanner#to_s で String を取り出した時に初めて String 用メモリを獲得します。
# ファイルの中身が "0123:abcd" の場合 ms2 = ms.scan(/\w+/) #=> MmapScanner ms2.to_s #=> "0123"
大きなファイルの中身をパースしたい時などに、あまりメモリのことを気にせずに使えるかもしれません。
注意点:
- Encoding は常に ASCII-8BIT。
- 明に munmap(2) できない。GCまかせ。
CSVパーサー
MmapScannerを使って簡単なCSVパーサーを作ってみました。
require 'mmapscanner' class MmapCSV include Enumerable def initialize(filename) File.open(filename) do |f| @ms = MmapScanner.new(f) end end def each rec = [] until @ms.eos? if @ms.skip(/"((?:""|[^"])*)"(,|\r\n|\n|\z)/n) rec.push @ms.matched_str(1).gsub(/""/,'"') elsif @ms.skip(/([^,"\r\n]*)(,|\r\n|\n|\z)/n) rec.push @ms.matched_str(1) else raise "invalid format: #{@ms.rest.slice(0,20).to_s}" end unless @ms.matched_str(2) == ',' yield rec rec.clear end end end end
時間を測ってみます。CSVファイルは http://www.post.japanpost.jp/zipcode/dl/kogaki.html の ken_all を使いました。
% time ruby -r ./mmapcsv -e 'MmapCSV.new("/tmp/ken_all.csv").each{}' ... 4.88s user 0.02s system 100% cpu 4.904 total
Ruby標準添付のCSVライブラリはこんな感じだったので、それよりは速くできたみたいです。
% time ruby -r csv -e 'CSV.new(File.open("/tmp/ken_all.csv","r:binary")).each{}' ... 6.88s user 0.05s system 100% cpu 6.929 total