Ruby 2.4 で CGI.unescape が高速化されていた

久しぶりに ruby をいじる機会があったので、備忘的に。

URL encode された文字列を decode したいのだけど、Ruby 2.1 の URI.decode だと遅すぎる!

というのをなんとか解決できないかと調べていました。

ベンチマークコード

似たようなことをやってくれるメソッドが複数あったので、とりあえずベンチとってみる。

require 'benchmark'
require 'uri'
require 'cgi'

encoded = '%7B%22hoge%22%3A%7B%22foo%22%3A%22bar%22%2C%22hoo%22%3A%22baz%22%7D%7D'
# => {"hoge":{"foo":"bar","hoo":"baz"}}

def manytimes
  100000.times { yield } if block_given?
end

Benchmark.bmbm do |bm|
  bm.report("URI.decode") {
    manytimes { URI.decode encoded }
  }
  bm.report("URI.decode_www_form_component") {
    manytimes { URI.decode_www_form_component encoded }
  }
  bm.report("CGI.unescape") {
    manytimes { CGI.unescape encoded }
  }
end
Ruby 2.1
$ ruby --version
ruby 2.1.10p492 (2016-04-01 revision 54464) [x86_64-darwin15.0]

$ ruby decode_bench.rb
Rehearsal -----------------------------------------------------------------
URI.decode                      2.360000   0.020000   2.380000 (  2.404751)
URI.decode_www_form_component   1.270000   0.010000   1.280000 (  1.467851)
CGI.unescape                    1.480000   0.020000   1.500000 (  1.578028)
-------------------------------------------------------- total: 5.160000sec

                                    user     system      total        real
URI.decode                      2.570000   0.030000   2.600000 (  2.729236)
URI.decode_www_form_component   1.170000   0.010000   1.180000 (  1.189314)
CGI.unescape                    1.510000   0.010000   1.520000 (  1.577981)

遅いし obsolete だし、URI.decode はやめておいた方がよさそうだ。 singleton method URI.decode (Ruby 2.4.0)

URI.decode_www_form_component を使うのがいいのかな?

Ruby 2.4

試しに Ruby 2.4 でベンチとってみたら、思わぬ結果になった。

$ ruby --version
ruby 2.4.1p111 (2017-03-22 revision 58053) [x86_64-darwin15]

$ ruby decode_bench.rb
Rehearsal -----------------------------------------------------------------
URI.decode                      2.290000   0.010000   2.300000 (  2.334818)
URI.decode_www_form_component   1.280000   0.020000   1.300000 (  1.330829)
CGI.unescape                    0.120000   0.000000   0.120000 (  0.133207)
-------------------------------------------------------- total: 3.720000sec

                                    user     system      total        real
URI.decode                      2.560000   0.030000   2.590000 (  2.859363)
URI.decode_www_form_component   1.270000   0.010000   1.280000 (  1.329829)
CGI.unescape                    0.120000   0.000000   0.120000 (  0.127875)

CGI.unescape が10倍以上速くなっている。

Ruby 2.4 から CGI モジュールのいくつかのメソッドが C拡張として実装されたらしい。

github.com

escape/unescape, escapeHTML/unescapeHTML 辺りが対象かな?

ruby/escape.c at trunk · ruby/ruby · GitHub

というわけで

URL decode したい時は Ruby 2.4 にして CGI.unescape を使うと速いよ!ということでした。

Project Euler - Problem 10

もろもろの合間にちょっとした体操。

http://projecteuler.net/problem=10
200万までの素数の合計を求める。
単に素数リストを求めて合計すればいいだけ。

組み込みクラス使ってもいいけど、自前で実装する。

#!/usr/bin/env ruby

class Prime
  def list(max)
    # Eratosthenes
    list = (3..max).step(2).to_a
    result = [2]
    while(list.last > result.last ** 2) do
      result << list.shift
      list.reject! {|n| n % result.last == 0}
    end
    result + list
  end
end

# sum of primes
max = ARGV[0].to_i # 2000000
p Prime.new.list(max).reduce(&:+)

$ ruby -v
ruby 1.9.3p194 (2012-04-20 revision 35410) [x86_64-linux]

$ time ruby p10.rb 2000000
ruby p10.rb 2000000 469.85s user 1.62s system 99% cpu 7:53.67 total

reject の部分で素数の倍数を落としているが、件数が多すぎて時間がかかってしまう。
リストにして一気に落とした方が早いか。

#!/usr/bin/env ruby

class Prime
  def list(max)
    # Eratosthenes
    list = (3..max).step(2).to_a
    result = [2]
    while(list.last > result.last ** 2) do
      result << list.shift
      list -= (result.last..list.last).step(result.last).to_a
    end
    result + list
  end
end

# sum of primes
max = ARGV[0].to_i # 2000000
p Prime.new.list(max).reduce(&:+)

$ time ruby p10.rb 2000000
ruby p10.rb 2000000 12.65s user 0.23s system 99% cpu 12.914 total

arrayが演算できる言語だからできる手抜き。
Cでやると面倒そうだなあ。

メソッドの呼び出し方

PHPでは、static宣言なしでもメソッドのstatic呼び出しができてしまう。
で、どっちが速いの?という話。
メモリの使用量も比べてみたかったけど、速さのみ。

<?php
require_once 'Benchmark/Timer.php';

class MyTest {
  public function myMethod() {
    return;
  }
  public static function staticMethod() {
    return;
  }
}

$max = 100000;
$timer = new Benchmark_Timer();
$t = new MyTest();

$timer->start();

for($i = 0; $i < $max; $i++) {
  MyTest::staticMethod();
}
$timer->setMarker('static');

for($i = 0; $i < $max; $i++) {
  MyTest::myMethod();
}
$timer->setMarker('static2');

for($i = 0; $i < $max; $i++) {
	$t->myMethod();
}
$timer->setMarker('instance');

$timer->stop();
$timer->display();

/*
------------------------------------------------------
marker    time index            ex time         perct   
------------------------------------------------------
Start     1293934691.81032700   -                0.00%
------------------------------------------------------
static    1293934691.85720200   0.046875        21.74%
------------------------------------------------------
static2   1293934691.99593900   0.138737        64.35%
------------------------------------------------------
instance  1293934692.02592700   0.029988        13.91%
------------------------------------------------------
Stop      1293934692.02593700   0.000010         0.00%
------------------------------------------------------
total     -                     0.215610       100.00%
------------------------------------------------------
*/
$ php --version
PHP 5.3.3 (cli) (built: Aug 22 2010 19:41:55)

static宣言なしのメソッドをstaticに呼ぶのは遅いってことかしら。

ちなみにrubyだとこうなった。
static methodは存在しないので、class method。このへんはうろ覚えなので、ちょっと自信ない・・・

require 'benchmark'

class MyTest
  def myMethod
    1 + 2
  end

  def self.classMethod
    1 + 2
  end
end

module MyModule
  def moduleMethod
    1 + 2
  end
end

module MyModule2
  def moduleClassMethod
    1 + 2
  end
end

max = 100000;

Benchmark.bmbm(10) do |x|
  t = MyTest.new
  x.report("instance: ") {
    max.times do
      t.myMethod
    end
  }

  x.report("class   : ") {
    max.times do
      MyTest::classMethod
    end
  }

  MyTest.class_eval {
    include MyModule
  }
  t = MyTest.new
  x.report("module : ") {
    max.times do
      t.moduleMethod
    end
  }

  MyTest.extend MyModule2
  x.report("module class : ") {
    max.times do
      MyTest::moduleClassMethod
    end
  }
end
Rehearsal ---------------------------------------------------
instance:         0.040000   0.000000   0.040000 (  0.043314)
class   :         0.050000   0.000000   0.050000 (  0.049375)
module :          0.040000   0.000000   0.040000 (  0.042887)
module class :    0.050000   0.000000   0.050000 (  0.045320)
------------------------------------------ total: 0.180000sec

                      user     system      total        real
instance:         0.040000   0.000000   0.040000 (  0.042020)
class   :         0.040000   0.000000   0.040000 (  0.046069)
module :          0.040000   0.000000   0.040000 (  0.041350)
module class :    0.040000   0.000000   0.040000 (  0.045474)
$ ruby --version
ruby 1.8.7 (2010-08-16 patchlevel 302) [i686-darwin10]

マージソート

基本に立ち戻って、マージソートをやってみた。
Ruby, C, Scala, Gaucheの4種類。
最初にRubyで書いて、他に移植という流れ。


ScalaGaucheはほぼ同じ。普通に再帰で書いたけど、Scalaは末尾再帰に最適化されてるそうな。


Cだけは配列をガンガン作ろうとするとmallocが大変なことになるので、1つを使いまわす形に。
tempはmallocしないで静的に定義してもいいかも。
Cはまだまだ慣れない。


Ruby

C

Scala

Gauche

複数のRubyを切り替えられるようにする

Ruby1.9をメインで使いつつ、1.8系も準備しておきたかったので併存させられる環境を作ってみた。


OS: Ubuntu 9.04 Server Edition

Rubyのソースを用意する

Subversionで落としてくる。当然trunk。安定とか知りません。

$ svn co http://svn.ruby-lang.org/repos/ruby/trunk ruby
$ svn co http://svn.ruby-lang.org/repos/ruby/branches/ruby_1_8 ruby1.8
必要なライブラリを入れる

OpenSSLとconfigure作成用のautoconf
bisonがどうとかいうエラーが出るので、それも入れておく

$ sudo apt-get install libssl-dev
$ sudo apt-get install autoconf
$ sudo apt-get install bison
makeする

/local/ruby の下にバージョン番号のディレクトリを作成して、そこに入れる。
プログラムにはバージョン番号を付けておく

$ cd ruby
$ autoconf
$ ./configure --prefix=/local/ruby/<VERSION> --program-suffix=<VERSION> --with-readline-dir=/local/ruby/<VERSION>
$ make
$ make test
$ sudo make install
シンボリックリンクを張る

aliasを付けてもいいけど、gemとか他のプログラムが /usr/bin/ruby とかを見ている場合があるので、シンボリックリンクを作っておく。
投げやりなシェルスクリプトにしてみる。

$ less switch_ruby.sh
#!/bin/sh
ORG_PATH=/local/ruby/$2/bin
PREFIX=/usr/local/bin

for NAME in ruby gem irb rdoc ri 
do
  ln -s $ORG_PATH/$NAME$1 $PREFIX/$NAME
  echo "/usr/bin/$NAME linked @ $ORG_PATH/$NAME$1"
done

$ ./switch_ruby.sh 1.9 1.9.1
併存させたいバージョンの数だけ繰り返す

1.8系はrubygemsのビルドもあるので注意。今回はめんどくさいのでやってない。


バージョンを切り替えたい場合はシンボリックリンクを入れ替えればよし。
これでtrunk Rubyライフを満喫!!

Mitaka.rb に行ってきた

4/15 に吉祥寺で開かれた、Mitaka.rb設立総会に行ってきました。

一次会

会場がヴィレッジバンガードダイナーだったこともあり、外国ビールが飲み放題!!

Poken共同購入の受け渡しをしてもらったのもあり、Poken充してました。はいふぉーはいふぉー。


その後は、散々に飲み食いしながらおしゃべりを楽しみました。
結構「仕事でRuby/Rails使ってます」という人が多くてびっくり!うらやましい・・・


二次会

カレー屋に移動してさらにしゃべくる。今度販売予定のiPhoneアプリについて話したり遊んでたり、
逆側でid:t-wada さんがBDDやテストツールについて話してたりしてカオス。
iPhoneアプリ開発やってる人が3〜4人いて、またまたびっくり。

お疲れ様でした

幹事のid:onering さん、Pokenの共同購入を主催してくださったid:conceal-rs さん、お話してくださった皆様ありがとうございました!
またよろしくお願いします。

番外編

id:akasata さんに、著書の「実践バグ管理」を頂きました。ありがとうございます!
早速読んで、初書評とかやってみようと思います*1

まだぱらぱらと見た限りですが、バグ管理にとどまらず、システム開発やレポートの書き方などの様々なノウハウが詰まっている感じです。
読むのが楽しみー。

*1:自分でハードルを上げてみる

ifもcaseも使わずに条件分岐

再び、λ楽しいよラムダの時間がやってまいりました。

true=λx.λy.x
false=λx.λy.y

そうすると、変数bがtrueかfalseをとるとして

b(0)(1)

とすると、bがtrueのとき0、falseのとき1を返します。

λで条件分岐ができた!

2009-04-09 - きしだのはてな


というわけで、λで条件分岐ができると聞いて、早速やってみた。
できれば、別関数を呼ばずにすませたい!ということで・・・

true/falseの代わりに関数を返すようにする

数字を比較した結果に応じて、値を返す。

#!/opt/local/bin/ruby1.9
class Fixnum
  @@tr = ->(x){ ->(y){ x }}
  @@fl = ->(x){ ->(y){ y }}
  
  def compare(n, arr)
    arr[self <=> n]
  end
  
  def ==(n)
    compare n, [@@tr, @@fl, @@fl]
  end
  
  def >(n)
    compare n, [@@fl, @@tr, @@fl]
  end

  def >=(n)
    compare n, [@@tr, @@tr, @@fl]
  end

  def <(n)
    compare n, [@@fl, @@fl, @@tr]
  end
  
  def <=(n)
    compare n, [@@tr, @@fl, @@tr]
  end  
end

puts (3 == 5).('foo').('bar') #=> bar
puts (3 == 3).('foo').('bar') #=> foo

puts (3 < 8).('foo').('bar') #=> foo
puts (3 > 8).('foo').('bar') #=> bar

できた。けど・・・
FloatやStringとか色々なところでこれを書くのは面倒。
true/falseを書き換えてしまおう。

true/falseが関数を返すようにする

といっても、true/falseはTrueClass, FalseClassのインスタンス
Classごと書き換えるのはちょっとなーってことで、ちょっと逃げてみた。

class Proc
  def [](n=nil)
    self.call n
  end
end

class TrueClass
  def [](n)
    ->(x){ ->(y){ x }}[n]
  end
end

class FalseClass
  def [](n)
    ->(x){ ->(y){ y }}[n]
  end
end

tr = ->{puts 'this is true.'}
fl = ->{puts 'this is false.'}
puts (('hoge' == 'hogo')[tr][fl]).() #=> this is false.

比較の結果返されたtrue, falseの[]メソッドを呼ぶと、それぞれを表す関数が返される。
呼び出しが()じゃなくなるのが汚いけど、()は定義できないみたい。

結論

ifもcaseも使わずに、条件分岐ができた!
もっと()だけで済ませられる方法とかないかなー。*1

*1:できればruby