6
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Posted at

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

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

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

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活躍するんじゃない?

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)

ええっ、そうなの……。

6
5
1

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
6
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?