カンマ区切り、shellscript vs ruby

Qiitaのこの記事を読んだ

シェルスクリプトを何万倍も遅くしないためには —— ループせずフィルタしよう - Qiita

「カンマ区切りデータの2列目に含まれる小文字の母音("aeiou")を数える」 というお題で速さを比較してたので、せっかくだからrubyで書いて比べてみた

テストデータ

File.write 'data.dat', ([%(abc,atmark,123), %(def,colon,456)] * 1000_000).join("\n")

ruby vs shellscript

#!/bin/sh
cut -d, -f2 |tr -dc 'aeiou' |wc -c
puts STDIN.each_line.map { |line| line.split(',', 2).last.count('aeiou') }.sum

結果

./v5.sh < data.dat       1.86s user 0.03s system 175% cpu 1.079 total
ruby slow.rb < data.dat  2.10s user 0.05s system  98% cpu 2.188 total

rubyの方が倍くらい遅かった

ruby(リベンジ)

count = 0
target = 'aeiou'
s = ''
STDIN.each_line do |line|
  s << line.split(',', 2).last
  if s.size > 0x10000
    count += s.count target
    s = ''
  end
end
puts count + s.count(target)

String#countは短い文字列に対して何度も呼ぶよりconcatしてまとめて呼ぶ方が良さそう

1000000.times{''.count 'very_long_string'} #=> 0.73秒
1000000.times{''.count 'a'} #=> 0.16秒
('a'*1000000).count 'a' #=> 0.001秒

結果は...

./v5.sh < data.dat    1.86s user 0.03s system 175% cpu 1.079 total
ruby a.rb < data.dat  1.28s user 0.05s system  97% cpu 1.363 total

あとちょっと及ばない
それとsplitしないバージョンも書いてみたけど速度出なかった

ここでshellscriptの171% cpuに注目 rubyでも並列させてみることに

rubyリベンジ2

path = ARGV[0]
filesize = File.size(path)
cores = 4
pipes = Array.new cores do |id|
  per = (filesize + cores - 1) / cores
  from = per * id
  to = [per * (id + 1), filesize].min
  rio, wio = IO.pipe
  fork do
    file = File.open(path, 'r')
    file.seek from
    length = to - from
    length -= file.gets.size if id != 0
    count = 0
    target = 'aeiou'
    s = ''
    file.each_line do |line|
      length -= line.size
      s << line.split(',', 2).last
      if s.size > 0x10000
        count += s.count target
        s = ''
      end
      break if length < 0
    end
    wio.puts count + s.count(target)
  end
  rio
end
puts pipes.map { |io| io.gets.to_i }.sum

(STDINやめてFileにするインチキだけど)
fork→file.seekして4分割それぞれ数える→集約、分割の境界をうまいことどうにかする

./v5.sh < data.dat    1.86s user 0.03s system 175% cpu 1.079 total
ruby b.rb < data.dat  2.51s user 0.07s system 329% cpu 0.782 total

とようやくshellscript超えできた

他の結果

go速い、C速い(-O3つけなかったらgoよりだいぶ遅くなるけど)
goroutineで(cpu327%も使って、ほんの少しだけだけど)Cより速く動いた
SSDかHDDかでどれくらい変わるんだろ

% cc -O3 a.c && time ./a.out < data.dat
% go build a.go && time ./a < data.dat
% go build b.go && time ./b < data.dat
% time ./v5.sh < data.dat
% time ruby a.rb < data.dat
% time ruby b.rb data.dat # これだけSTDINじゃなくfileをコマンドライン引数で渡してる

GO B:   0.13s user 0.01s system 327% cpu 0.043 total 25.1倍
C:      0.04s user 0.01s system  93% cpu 0.053 total 20.4倍
GO A:   0.05s user 0.01s system  93% cpu 0.062 total 17.4倍
RUBY B: 2.51s user 0.07s system 329% cpu 0.782 total 1.38倍
SHELL:  1.86s user 0.03s system 175% cpu 1.079 total 基準
RUBY A: 1.28s user 0.05s system  97% cpu 1.363 total 0.79倍

コードはこちら https://gist.github.com/tompng/2b26c0bf1c1f81b0f8827271d1cb900a
(b.goが特に長ったらしいけどうまいやり方ないかなぁ)