ruby1.9.1はまだ鬼門?

仕事中 EventMachine を使った ruby のコードを書いていて、ある部分がかなり遅いことに気付いた。
リクエストを非同期で投げるのに em-http-request を使っている部分。
その場はとりあえずやり過ごして、家に帰ってきてから改めて調べてみた。

その結果、

require 'em-http'

の部分だけやたらと時間がかかっている。
で、調べていくうちに em-http-request が遅いんじゃなくてそれが利用している addressable の読み込みが遅いというところまでわかった。
この addressable モジュールのうち、idna.rb というファイルの中で、

    UNICODE_DATA = {
      0x0041 => [0, 0, nil, nil, nil, 0x0061, nil],
      0x0042 => [0, 0, nil, nil, nil, 0x0062, nil],
      0x0043 => [0, 0, nil, nil, nil, 0x0063, nil],
      0x0044 => [0, 0, nil, nil, nil, 0x0064, nil],
      ....同じような感じで4k行以上

という部分と、この UNICODE_DATA を

    COMPOSITION_TABLE = {}
    for codepoint, data in UNICODE_DATA
      canonical = data[UNICODE_DATA_CANONICAL]
      exclusion = data[UNICODE_DATA_EXCLUSION]

      if canonical && exclusion == 0
        COMPOSITION_TABLE[canonical] = codepoint
      end
    end

としてぶん回している部分に異常に時間を取られている。
ひょっとして 1.9.1 だから?とすぐに思いついてプロファイラで計測してみた。

$ cat test-reqidna.rb
require './idna.rb'

$ ruby1.9.1 -r profile test-reqidna.rb
  %   cumulative   self              self     total
 time   seconds   seconds    calls  ms/call  ms/call  name
 64.50     1.49      1.49        1  1490.00  1760.00  Hash#each
 23.38     2.03      0.54        1   540.00  2310.00  Kernel.require
  6.49     2.18      0.15    16932     0.01     0.01  Array#
  2.16     2.23      0.05     4233     0.01     0.01  Array#length
  1.73     2.27      0.04     5544     0.01     0.01  Fixnum#==
  0.87     2.29      0.02     4233     0.00     0.00  Kernel.kind_of?
  0.43     2.30      0.01        7     1.43     1.43  String#+
  0.43     2.31      0.01      918     0.01     0.01  Hash#=
  0.00     2.31      0.00        1     0.00     0.00  Fixnum#<<
  0.00     2.31      0.00       20     0.00     0.00  BasicObject#singleton_method_added
  0.00     2.31      0.00        2     0.00     0.00  Fixnum#*
  0.00     2.31      0.00       17     0.00     0.00  Module#private
  0.00     2.31      0.00        3     0.00     0.00  Class#inherited
  0.00     2.31      0.00        4     0.00     0.00  IO#set_encoding
  0.00     2.31      0.00        1     0.00  2310.00  #toplevel

$ ruby1.8 -r profile test-reqidna.rb
  %   cumulative   self              self     total
 time   seconds   seconds    calls  ms/call  ms/call  name
 62.34     0.48      0.48        1   480.00   710.00  Hash#each
 27.27     0.69      0.21     8466     0.02     0.02  Array#
  7.79     0.75      0.06        1    60.00   770.00  Kernel.require
  1.30     0.76      0.01      918     0.01     0.01  Hash#=
  1.30     0.77      0.01     1311     0.01     0.01  Fixnum#==
  0.00     0.77      0.00        2     0.00     0.00  Fixnum#*
  0.00     0.77      0.00        3     0.00     0.00  Class#inherited
  0.00     0.77      0.00       20     0.00     0.00  Kernel.singleton_method_added
  0.00     0.77      0.00       17     0.00     0.00  Module#private
  0.00     0.77      0.00        1     0.00     0.00  Fixnum#<<
  0.00     0.77      0.00        7     0.00     0.00  String#+
  0.00     0.77      0.00        1     0.00   770.00  #toplevel

1.8に較べてやっぱりすげー遅くなってる。
gprof でもうちょっと深めに追ってみようかとも思ったんだけど、1.8 と 1.9.1 をコンパイルするのが面倒で trunk をリポジトリから持ってきて神頼み的に計測(結局コンパイル)。

$ ruby-trunk --version
ruby 1.9.3dev (2010-07-08 trunk 28577) [x86_64-linux]

$ ruby-trunk -r profile test-reqidna.rb
  %   cumulative   self              self     total
 time   seconds   seconds    calls  ms/call  ms/call  name
 53.52     0.38      0.38        1   380.00   610.00  Hash#each
 23.94     0.55      0.17    16932     0.01     0.01  Array#
 12.68     0.64      0.09        1    90.00   710.00  Kernel.require
  5.63     0.68      0.04     4233     0.01     0.01  Kernel.kind_of?
  1.41     0.69      0.01        7     1.43     1.43  String#+
  1.41     0.70      0.01     5544     0.00     0.00  Fixnum#==
  1.41     0.71      0.01     4233     0.00     0.00  Array#length
  0.00     0.71      0.00      918     0.00     0.00  Hash#=
  0.00     0.71      0.00       17     0.00     0.00  Module#private
  0.00     0.71      0.00       20     0.00     0.00  BasicObject#singleton_method_added
  0.00     0.71      0.00        8     0.00     0.00  Hash#default
  0.00     0.71      0.00        1     0.00     0.00  Fixnum#<<
  0.00     0.71      0.00        2     0.00     0.00  Fixnum#*
  0.00     0.71      0.00        3     0.00     0.00  Class#inherited
  0.00     0.71      0.00        4     0.00     0.00  IO#set_encoding
  0.00     0.71      0.00        1     0.00   710.00  #toplevel

trunk ではかなり改善されている模様。
次に time コマンドと benchmark モジュールでも比較してみた。

$ time ruby1.9.1 test-reqidna.rb
real    0m1.359s
user    0m1.340s
sys     0m0.010s

$ time ruby1.8 test-reqidna.rb
real    0m0.083s
user    0m0.080s
sys     0m0.000s

$ time ruby-trunk test-reqidna.rb
real    0m0.101s
user    0m0.060s
sys     0m0.040s

$ irb1.9.1
irb(main):001:0> require 'benchmark'
irb(main):002:0> puts Benchmark.measure{require './idna.rb'}
  1.350000   0.000000   1.350000 (  1.354746)

$ irb1.8
irb(main):001:0> require 'benchmark'
irb(main):002:0> puts Benchmark.measure{require './idna.rb'}
  0.070000   0.000000   0.070000 (  0.066070)

$ /usr/local/ruby-trunk/bin/irb
irb(main):001:0> require 'benchmark'
irb(main):002:0> puts Benchmark.measure{require './idna.rb'}
  0.070000   0.010000   0.080000 (  0.079105)

ということで ruby 本体にバリっと patch あてられるほど技術力のない自分が取るべき選択肢は

  • 1.8で運用
  • trunkで運用
  • em-http-request 使わない
  • em-http-request で addressable/uri を使わないようにpatchあてる

のどれかで、ただ 1.8 系を使うにしてもリポジトリから最新のものを取ってこないと NKF の utf 周りに悩まされてしまうはずでいずれにしても手はかかりそう・・・ぐわー。


追記:同じように addressable/uri の読み込みの遅さに苦しめられている人がいるのを発見しました。
http://d.hatena.ne.jp/kitamomonga/20100316/ruby_gem_addressable_howto

追記2:1.9.2 のプロファイル結果
速い!

$ /usr/local/ruby_1_9_2/bin/ruby -r profile test-reqidna.rb
  %   cumulative   self              self     total
 time   seconds   seconds    calls  ms/call  ms/call  name
 56.60     0.30      0.30        1   300.00   460.00  Hash#each
 16.98     0.39      0.09    16932     0.01     0.01  Array#
 11.32     0.45      0.06        1    60.00   530.00  Kernel.require
  3.77     0.47      0.02     4233     0.00     0.00  Kernel.kind_of?
  3.77     0.49      0.02     5544     0.00     0.00  Fixnum#==
  3.77     0.51      0.02     4233     0.00     0.00  Array#length
  1.89     0.52      0.01        3     3.33     3.33  Class#inherited
  1.89     0.53      0.01      918     0.01     0.01  Hash#=
  0.00     0.53      0.00       17     0.00     0.00  Module#private
  0.00     0.53      0.00       20     0.00     0.00  BasicObject#singleton_method_added
  0.00     0.53      0.00      236     0.00     0.00  Hash#default
  0.00     0.53      0.00        1     0.00     0.00  Fixnum#<<
  0.00     0.53      0.00        7     0.00     0.00  String#+
  0.00     0.53      0.00        2     0.00     0.00  Fixnum#*
  0.00     0.53      0.00        4     0.00     0.00  IO#set_encoding
  0.00     0.53      0.00        1     0.00   530.00  #toplevel