MmapScanner

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