RubyのLTSVパースのベンチマーク

激しく流行に乗り遅れた感がありますが、LTSVを調べてみました。

* 文字列分割
* 正規表現マッチ
* StringScanner

のそれぞれでLTSVをパースした時のベンチマークを取ってみました。
というか、なんで文字列分割してるの、StringScannerが速いんじゃないの、という思いがあったのでした。
比較が大事だろうということでマシン性能とかは省略。
Ruby 1.9.3です。

```ruby
require 'benchmark'

logline = 'time:10/Feb/2013:00:22:43 +0900	host:127.0.0.1	request:GET / HTTP/1.1	status:200	size:133	referer:-	ua:Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:18.0) Gecko/20100101 Firefox/18.0	reqtime:0.000	upsttime:-'
logfile = 'log.ltsv'
open(logfile, 'w') do |file|
  100000.times do
    file.puts logline
  end
end
TAB = "\t"
COLON = ':'
NL = "\n"

def parse_line_by_split(line)
  pairs = line.split(TAB).map {|pair| pair.split(COLON, 2)}
end

def parse_line_by_regexp(line)
  matched = (/((?<key>[^#{COLON}]*):(?<value>[#{TAB}]*)\t*)*/o).match(line)
end

def parse_line_by_strscan(line)
  require 'strscan'
  scanner = StringScanner.new(line)
  while key = scanner.scan(/[^#{COLON}]*/o)
    scanner.scan(/#{COLON}/o)
    value = scanner.scan(/[^#{TAB}]*/o)
    scanner.scan(/#{TAB}/o)
    return if scanner.eos?
  end
end

Benchmark.bmbm do |x|
  [:parse_line_by_split, :parse_line_by_regexp, :parse_line_by_strscan].each do |method|
    x.report method do
      open logfile do |file|
        file.each_line do |line|
          send method, line
        end
      end
    end
  end
end
```

```
$ ruby bench.rb
Rehearsal ---------------------------------------------------------
parse_line_by_split     0.750000   0.010000   0.760000 (  0.751666)
parse_line_by_regexp    0.800000   0.010000   0.810000 (  0.811203)
parse_line_by_strscan   5.990000   2.300000   8.290000 (  8.633933)
------------------------------------------------ total: 9.860000sec

                            user     system      total        real
parse_line_by_split     0.760000   0.010000   0.770000 (  0.768746)
parse_line_by_regexp    0.820000   0.010000   0.830000 (  0.832008)
parse_line_by_strscan   6.170000   2.100000   8.270000 (  8.602963)
```

ありゃ。
まあでも、上のスクリプトは一行一行をメソッドに送り込んで、その度に`StringScanner`オブジェクトを作っているからオーバーヘッドでかいよね。
リアルタイムでログを解析する分にはこういう風になるんだと思うけど、既存のログを解析する時には`StringScanner`活躍するんじゃない？

```ruby
def parse_by_split(filename)
  result = File.read(filename).split(NL).map {|line|
    line.split(TAB).map {|pair| pair.split(COLON, 2)}
  }
end

def parse_by_regexp(filename)
  matched = (/(^((?<key>[^#{COLON}]*):(?<value>[^#{TAB}]*)#{TAB}*)*$)*/mo).match(File.read(filename))
end

def parse_by_strscan(filename)
  require 'strscan'
  scanner = StringScanner.new(File.read(filename))
  while key = scanner.scan(/[^#{COLON}]*/o)
    # p key
    scanner.scan(/#{COLON}/o)
    value = scanner.scan(/[^#{TAB}]*/o)
    # p value
    scanner.scan(/[#{TAB}$]/)
    return if scanner.eos?
  end
end

Benchmark.bmbm do |x|
  [:parse_by_split, :parse_by_regexp, :parse_by_strscan].each do |method|
    x.report method do
      send method, logfile
    end
  end
end
```

```
Rehearsal ----------------------------------------------------
parse_by_split     0.820000   0.060000   0.880000 (  0.874224)
parse_by_regexp    0.780000   0.260000   1.040000 (  1.059403)
parse_by_strscan   4.960000   0.030000   4.990000 (  5.001664)
------------------------------------------- total: 6.910000sec

                       user     system      total        real
parse_by_split     0.750000   0.000000   0.750000 (  0.755708)
parse_by_regexp    0.620000   0.250000   0.870000 (  0.890210)
parse_by_strscan   5.220000   0.030000   5.250000 (  5.245181)
```
ええっ、そうなの……。