Ruby 2.6 の改善を自慢したい

技術部で Ruby インタプリタの開発をしている笹田です。娘のために、今年はじめて大きなクリスマスツリー(1.8 m)を買いました。

本稿では、私が Ruby 2.6 で取り組んだ中から、次の新しい機能と性能改善について紹介します。どちらのトピックも、普通に Ruby を使っているだけなら気にならない、玄人向きの記事になっていると思います。興味がある人にお読み頂ければ幸いです(居ればいいのですが)。

  • TracePoint の拡張
    • 新しいイベント script_compiled の導入
    • フックを有効にする場所を制限する機能の導入
    • デバッガの実装が、10~100倍くらい速くなる、かもしれない
    • ブレイクポイントの実装を例に解説
  • Transient Heap の導入
    • 短寿命メモリオブジェクトの高速化
    • 世代別コピーGCのアイディアを利用
    • Rails とかには効かないかも...。

そういえば、両方とも "Tra" で始まってますね。寅年はまだ先ですが。

本稿は、Cookpad techlife で三日連続で掲載する Ruby 2.6 紹介記事の最後になります。

タイトルは 昨年の記事 Ruby 2.5 の改善を自慢したい の二番煎じです。来年もやるのかな...。

目次

TracePoint の拡張

まず、簡単なほう、TracePoint の拡張の話から始めます。

TracePoint 基礎

TracePoint は、プログラム中のイベントで起動する Proc を登録するための仕組みです。イベントには、毎行ごとに実行する line イベントや、メソッドの呼び出しと、そのリターンごとに実行する call、return などがあります。

例えば、すべてのメソッド呼び出し、およびそのリターンをログに出力したい、というプログラムは次のように簡単に書くことができます。

     1  def foo; bar; end
     2  def bar; nil; end
     3
     4  TracePoint.trace(:call, :return){|tp| p tp}
     5
     6  foo

このプログラムを実行すると、次のような出力が得られます。

#<TracePoint:call `foo'@t.rb:1>
#<TracePoint:call `bar'@t.rb:2>
#<TracePoint:return `bar'@t.rb:2>
#<TracePoint:return `foo'@t.rb:1>

foo が呼ばれ、bar が呼ばれ、bar から戻り、foo から戻る、ということが見て取れます。が、ちょっと出力が読みづらいですね。呼び出しレベルごとにインデントを付けるようにしてみましょう。

indent = 0
TracePoint.trace(:call, :return){|tp|
  indent -= 2 if tp.event == :return
  print ' ' * indent; p tp
  indent += 2 if tp.event == :call
}

実行結果:

#<TracePoint:call `foo'@t.rb:1>
  #<TracePoint:call `bar'@t.rb:2>
  #<TracePoint:return `bar'@t.rb:2>
#<TracePoint:return `foo'@t.rb:1>

ちょっと読みやすくなりました。もうちょっと整形してみましょう。

indent = 0
TracePoint.trace(:call){|tp|
  puts "#{' ' * indent}-> #{tp.method_id}@#{tp.path}:#{tp.lineno}"
  indent += 2
}
TracePoint.trace(:return){|tp|
  indent -= 2
  puts "#{' ' * indent}<- #{tp.method_id}@#{tp.path}:#{tp.lineno}"
}

TracePoint の設定を、call と return の2つに分けてみました。実行結果です。

-> foo@t.rb:1
  -> bar@t.rb:2
  <- bar@t.rb:2
<- foo@t.rb:1

どうでしょう。それっぽくなったでしょうか。

実は、TracePoint.trace(events){...}TracePoint.new(events){...}.enable(新しい TracePoint を作成し、それを有効にする、という意味)の略なので、今後は TracePoint#enable を使うようにしてみます。

TracePoint の基礎はだいたいこんなものですが、詳細はるりまのドキュメント "class TracePoint (Ruby 2.5.0)" をご覧下さい。

TracePoint の問題

便利に、悪巧みに、色々使えそうな TracePoint ですが、性能の問題があります。

性能が気になる一番顕著な例はブレイクポイントの実装です。あるファイルのある行で実行を止める、ということを考えてみましょう。breakpoint('t.rb', 10) とすると、t.rb:10 で irb が起動するなんてどうでしょうか。

TracePoint を使うと、こんな感じで簡単に作ることができます。

def breakpoint file, line
  TracePoint.new(:line){|tp|
    if tp.path == file && tp.lineno == line
      tp.binding.irb
    end
  }.enable
end

各行で実行するフック(line イベントによるフック)中で、if tp.path == file && tp.lineno == line という if 文で、指定された場所かどうかを判断し、もしそうであれば irb を実行します。Ruby 2.5 から binding.irb という便利なメソッドが追加されたので、もし指定された行を実行したら binding.irb を呼ぶようにしてみました。

では、早速使ってみましょう。

def foo a, b
  p a, b # line 11
end

breakpoint __FILE__, 11

foo 10, 20
foo 30, 40

foo メソッドの1行目の p が11行目だったと思ってください。

実行結果:

From: /home/ko1/src/ruby/trunk/test.rb @ line 11 :

     6:     end
     7:   }.enable
     8: end
     9:
    10: def foo a, b
 => 11:   p a, b # line 11
    12: end
    13:
    14: breakpoint __FILE__, 11
    15:
    16: foo(10, 20)

irb(main):001:0> p [a, b]
[10, 20]
=> [10, 20]
irb(main):002:0> exit
10
20

From: /home/ko1/src/ruby/trunk/test.rb @ line 11 :

     6:     end
     7:   }.enable
     8: end
     9:
    10: def foo a, b
 => 11:   p a, b # line 11
    12: end
    13:
    14: breakpoint __FILE__, 11
    15:
    16: foo(10, 20)

irb(main):001:0> p [a, b]
[30, 40]
=> [30, 40]
irb(main):002:0>
30
40

見事にブレイクポイントを持つデバッガっぽいものが出来ました! たった数行(7行)でデバッガっぽいものが作れる Ruby って凄いですね! ちなみに、byebug などどの Ruby 用デバッガも、基本的にこんな感じで実装されています。

ただ、性能に問題があります。この簡単なブレイクポイントの設置場所「とは関係ない」コードの実行時間を測ってみます。

def fib n
  case n
  when 0, 1; n
  else fib(n-1) + fib(n-2)
  end
end

require 'benchmark'
Benchmark.bm{|x|
  x.report{
    fib(30)
  }
  x.report{
    breakpoint __FILE__, 11  # enable breakpoint here
    fib(30)
  }
}
       user     system      total        real
   0.095480   0.000000   0.095480 (  0.095490)
   0.904503   0.000000   0.904503 (  0.904552)

上が breakpoint なし、下が breakpoint ありです。実に 9.5 倍くらい遅くなっているのがわかります。実行している全ての行で上記(無駄な)チェックを行っているので、しょうがないといえばしょうがないかもしれません。

ちなみに byebug を使って、byebug のブレイクポイント機能を有効にした状態で fib(30) を実行してみましょう。

$ byebug /home/ko1/src/ruby/trunk/test.rb
...
(byebug) b 11
Created breakpoint 1 at /home/ko1/src/ruby/trunk/test.rb:11
(byebug)
       user     system      total        real
   7.406062   0.000000   7.406062 (  7.406415)

さらに遅いです。何もしない場合に比べ、80倍程度遅いようです。きっと、他にもいろいろな処理をしているんでしょうね。

今回はブレイクポイントを例にしましたが、最初に紹介したメソッド呼び出し履歴のロギングでも、例えば gem は除く、といった要望は当然出てくると思います。

TracePoint#enable(target:, target_line:) 拡張

さて、TracePoint では無駄なフックを呼んでしまうことで性能上問題が生じる、ということをご紹介しました。10倍とか100倍遅くなってしまいました。なんとかしたい。

そこで、Ruby 2.6 では、TracePoint を有効にする場所を制限するという拡張を TracePoint#enable メソッドに行うことにしました。

enable(target: code, target_line: line) と、target:target_line: キーワードを追加しました。これらのキーワードを利用することで、指定された code および行番号に、イベント発火を絞ることができます。

なお、target_line:line イベントにだけ有効です。そのため、line イベントと一緒に、他のイベント(call など)が指定されていると、target_line: を指定していても、call などは有効になります。わかりやすいように、target_line: 指定は line イベントのみと一緒に使うことをオススメします。

さて、この拡張を用いて、実際にちゃんと動くブレイクポイントを作ることができるか試してみましょう。まず、breakpoint メソッドの仕様を変更します。

def breakpoint method, line
  TracePoint.new(:line){|tp|
    tp.binding.irb
  }.enable(target: method, targe_line: line)
end

breakpoint メソッドの第一引数がファイルではなく、method とあるのに注意してください。ここでは、メソッドオブジェクトを渡します。

使ってみましょう。

def foo a, b
  p a, b # line 11
end

breakpoint method(:foo), 11
foo(10, 20)
foo(30, 40)

実行結果です。

From: /home/ko1/src/ruby/trunk/test.rb @ line 11 :

     6:     tp.binding.irb
     7:   }.enable(target: method, target_line: line)
     8: end
     9:
    10: def foo a, b
 => 11:   p a, b # line 11
    12: end
    13:
    14: breakpoint method(:foo), 11
    15: foo(10, 20)
    16: foo(30, 40)

irb(main):001:0> p [a, b]
[10, 20]
=> [10, 20]
irb(main):002:0> exit
10
20

From: /home/ko1/src/ruby/trunk/test.rb @ line 11 :

     6:     tp.binding.irb
     7:   }.enable(target: method, target_line: line)
     8: end
     9:
    10: def foo a, b
 => 11:   p a, b # line 11
    12: end
    13:
    14: breakpoint method(:foo), 11
    15: foo(10, 20)
    16: foo(30, 40)

irb(main):001:0> p [a, b]
[30, 40]
=> [30, 40]
irb(main):002:0> exit
30
40

先ほどと同様、ちゃんと動いていることが確認できました。

では、性能はどうなっているでしょうか。

       user     system      total        real
   0.096092   0.000000   0.096092 (  0.096180)
   0.095210   0.000000   0.095210 (  0.095306)

上が breakpoint なし、下が breakpoint ありです。ほぼ、実行時間に変わりが無いことがわかるかと思います。やった!

TracePoint#enable(target: code)code って何?

いやいや、ちょっと待って。いちいちメソッドオブジェクトを渡すのって大変じゃないですか。breakpoint(path, line) を実装したかったらどうすればいいんでしょうか。

そもそも、target: code で指定する code とは、なんでしょうか?

ここには、Ruby で記述したプログラムにおける MethodUnboundMethodProc および RubyVM::InstructionSequence (長いので、以降 ISeq)が指定できます。何やら難しそう...。そもそも、最後の ISeq って何。

Ruby で記述されたプログラムはバイトコードにコンパイルされます。そのバイトコードのことを Ruby (MRI) の文脈では命令列、Instruction Sequence、つまり ISeq と言ってます。

Ruby で記述したメソッドから取り出した Method オブジェクトなどは、たどっていくと ISeq が取り出せます。実は、code で指定しているのは、ISeq なのです。

ちなみに、RubyVM::InstructionSequence.of(code) で、MethodProc オブジェクトなどから ISeq が取り出せます。C で実装されたメソッドの場合、nil が返ります。TracePoint#enable(target: code) では、内部でまさに ISeq.of(code) を呼んで、ISeq を取り出しています。もし、ISeq が取り出せない場合はエラーになります。

p RubyVM::InstructionSequence.of(method(:p)) #=> nil
TracePoint.new{}.enable(target: method(:p))
#=> <internal:prelude>:137:in `__enable': specified target is not supported (ArgumentError)

今回の拡張は、ある ISeq(および、その ISeq から辿ることができるすべての ISeq)に TracePoint を限定する、というのが正しい理解です。

例えば、alias を使うとメソッドを増やすことができますが、指し示す ISeq は同じものです。

def foo; end
alias bar foo

ISeq = RubyVM::InstructionSequence
p ISeq.of(method(:foo)) == ISeq.of(method(:bar)) #=> true

そのため、enable(code: method(:bar)) とすると、foobar で有効な TracePoint になります。

同じように、ある Module を include したクラス C1, C2 も、同じメソッドを共有することになります。

module M; def foo; end; end
class C1; include M; end
class C2; include M; end

ISeq = RubyVM::InstructionSequence
p ISeq.of(C1.new.method(:foo)) == ISeq.of(C2.new.method(:foo)) #=> true

モジュールの例は、もしかしたらはまりやすいかもしれませんね。「そこからたどれるソースコードにイベントで発火する hook を登録する」という機能であることを抑えておくことが大事です。

メソッドを指定するブレイクポイント

メソッドを指定するブレイクポイントを実装する場合を考えます。あるメソッドが起動したとき、というタイミングにブレイクポイントを指定する場合ですね。

前節で実装した breakpoint が、そのまま使えそうです。

def method_breakpoint method, line = nil
  TracePoint.new(:call){|tp|
    tp.binding.irb
  }.enable(target: method)
end

「メソッドが呼ばれるとき」なので、call イベントのみを利用しています。target_line: 指定がないことに気を付けてください。line イベントがないのに target_line: 指定があると、target_line is specified, but line event is not specified (ArgumentError) と怒られます。

簡単ですね。

C で実装されたメソッドの場合は、この方法ではうまくいきません。その点がちょっと残念ですね(これまで通り、c_call イベントをフックするか、Ruby でラッパーメソッドを書く、という方法があります。今だと後者が速いかな)。

場所を指定するブレイクポイント

さて、ファイル(path)と行(line)で場所を指定したいブレイクポイントを実装する場合、対象となる ISeq をどうやって集めてくるか、という問題になります。

実は、今の MRI には、そのための方法がないので、今回は外部の gem を使います。iseq_collector です。この gem が提供する ObjectSpace.each_iseq は、インタプリタ中に存在するすべての ISeq を辿るという API です。

ISeq には ISeq#path というメソッドがあり、そのメソッドが定義されたファイル名を知ることができるので、これで path と比較することで、必要な ISeq を絞ることができます。

次に、line の絞り方です。これには2通りのやり方があります。

まず、ISeq#trace_points を用いて指定の行があるかどうかを調べる方法です。

     1  def foo
     2    p 1
     3  end
     4
     5  ISeq = RubyVM::InstructionSequence      # 長いので
     6  pp ISeq.of(method(:foo)).trace_points
     #=> [[1, :call], [2, :line], [3, :return]]

結果を見るとわかりやすいと思いますが、1行目に call イベント、2行目に line イベント、3 行目に return イベント、計 3 イベントがfooメソッド(を実装する ISeq)に登録可能である、ということを示しています。

場所を指定するので、line イベントを利用します。このメソッドでは、2 行目のみ、場所指定のブレイクポイントをしかけることができる、と捉えることができます(call, return イベントを利用すれば、もうちょっと頑張れますが、ここでは触れません)。

もう一つの絞り方ですが、ちょっと乱暴に、とにかく path で絞って得られた ISeq を用いて enable(target: iseq, target_line: line) を指定してしまう、というものです。もし、対象 ISeq に target_line: で指定した行がなければ、例外を返します。

     1
     2  def foo
     3    p 1
     4  end
     5
     6  TracePoint.new(:line){}.enable(target: method(:foo), target_line: 100)
     #=> <internal:prelude>:137:in `__enable': can not enable any hooks (ArgumentError)

100行目が見つからなかったので、「フックがどこにも指定できなかった」という例外を出しています。これを利用すれば、とにかく TracePoint#enable をしまくってみると、いつかはヒットするかも、という戦略が取れます。

今回は、前者、ISeq#trace_points を利用する方法を用いて、場所指定のブレイクポイントを設定するメソッドである location_breakpoint を作ってみましょう。

     1  require 'iseq_collector'
     2  def location_breakpoint path, line
     3    ObjectSpace.each_iseq{|iseq|
     4      if iseq.path == path &&
     5         iseq.trace_points.find{|(l, ev)| l == line && ev == :line}
     6        TracePoint.new(:line){|tp|
     7          tp.binding.irb
     8        }.enable(target: iseq, target_line: line)
     9      end
    10    }
    11  end
    12
    13  def foo a, b
    14    p a, b # line 14
    15  end
    16
    17  location_breakpoint __FILE__, 14
    18  foo(10, 20)
    19  foo(30, 40)

実際に動かしてみると、きちんと動いていることがわかると思います。やりました!

さて、ご紹介した2つ、method_breakpoint および location_breakpoint ですが、1つ大きな問題があります。それは、「すでに require などでロードしているプログラムにしか適用できない」です。

デバッガの通常の利用方法として、「プログラムのある場所にブレイクポイントを指定する。そして、実行を開始する」というものがあります。一度、すべてのプログラムをロードしたどこかのタイミングでブレイクポイントを(これまで作成してきたメソッドを利用して)設定する、ということをしてもいいですが、Ruby の場合、この「すべてのプログラムをロードしたどこかのタイミング」を特定するのは困難です。dynamic reloading などをしていると、そもそもそういうタイミングがありません。

この問題を解決するのが、Ruby 2.6 から導入された新しいイベントである script_compiled です。

script_compiled によるコンパイル後の処理

Ruby 2.6 から、script_compiled イベントが新しく追加されました。

MRI は Ruby スクリプトを実行するために、

  • (1) スクリプトをパースして AST(構文木)を作成
  • (2) AST を ISeq に変換
  • (3) ISeq を実行

という手順を踏んで実行します。 script_compiled イベントは、(2) 終了時に挿入されるフックです。

この機能を使うと、例えば、どんなファイルが requireload で実行されるか、実行直前で知ることができます。

コンパイルされ、生成された ISeq は TracePoint#instruction_sequence メソッドで取得することができます。

では試しに、net/http ライブラリを require すると、何が起こるのか、観察してみましょう。

TracePoint.new(:script_compiled){|tp|
  pp ["#{tp.path}:#{tp.lineno}", tp.instruction_sequence.path]
}.enable

require 'net/http'

このプログラムでは、Ruby スクリプトを ISeq に変換したとき、「ロードしたソースコードの位置」、「ロードされたファイル名」を表示します。

["/home/ko1/ruby/install/trunk/lib/ruby/2.6.0/rubygems/core_ext/kernel_require.rb:54",
 "/home/ko1/ruby/install/trunk/lib/ruby/2.6.0/net/http.rb"]
["/home/ko1/ruby/install/trunk/lib/ruby/2.6.0/net/http.rb:23",
 "/home/ko1/ruby/install/trunk/lib/ruby/2.6.0/net/protocol.rb"]
["/home/ko1/ruby/install/trunk/lib/ruby/2.6.0/rubygems/core_ext/kernel_require.rb:54",
 "/home/ko1/ruby/install/trunk/lib/ruby/2.6.0/socket.rb"]
["/home/ko1/ruby/install/trunk/lib/ruby/2.6.0/rubygems/core_ext/kernel_require.rb:54",
 "/home/ko1/ruby/install/trunk/lib/ruby/2.6.0/timeout.rb"]
["/home/ko1/ruby/install/trunk/lib/ruby/2.6.0/net/http.rb:1645",
 "/home/ko1/ruby/install/trunk/lib/ruby/2.6.0/net/http/exceptions.rb"]
["/home/ko1/ruby/install/trunk/lib/ruby/2.6.0/net/http.rb:1647",
 "/home/ko1/ruby/install/trunk/lib/ruby/2.6.0/net/http/header.rb"]
["/home/ko1/ruby/install/trunk/lib/ruby/2.6.0/net/http.rb:1649",
 "/home/ko1/ruby/install/trunk/lib/ruby/2.6.0/net/http/generic_request.rb"]
["/home/ko1/ruby/install/trunk/lib/ruby/2.6.0/net/http.rb:1650",
 "/home/ko1/ruby/install/trunk/lib/ruby/2.6.0/net/http/request.rb"]
["/home/ko1/ruby/install/trunk/lib/ruby/2.6.0/net/http.rb:1651",
 "/home/ko1/ruby/install/trunk/lib/ruby/2.6.0/net/http/requests.rb"]
["/home/ko1/ruby/install/trunk/lib/ruby/2.6.0/net/http.rb:1653",
 "/home/ko1/ruby/install/trunk/lib/ruby/2.6.0/net/http/response.rb"]
["/home/ko1/ruby/install/trunk/lib/ruby/2.6.0/net/http.rb:1654",
 "/home/ko1/ruby/install/trunk/lib/ruby/2.6.0/net/http/responses.rb"]
["/home/ko1/ruby/install/trunk/lib/ruby/2.6.0/net/http.rb:1656",
 "/home/ko1/ruby/install/trunk/lib/ruby/2.6.0/net/http/proxy_delta.rb"]
["/home/ko1/ruby/install/trunk/lib/ruby/2.6.0/net/http.rb:1658",
 "/home/ko1/ruby/install/trunk/lib/ruby/2.6.0/net/http/backward.rb"]

このように、多くのファイルがロードされていることがわかります。

また、同じことを fileutils で試してみます。

["/home/ko1/ruby/install/trunk/lib/ruby/2.6.0/rubygems/core_ext/kernel_require.rb:54",
 "/home/ko1/ruby/install/trunk/lib/ruby/2.6.0/fileutils.rb"]
["/home/ko1/ruby/install/trunk/lib/ruby/2.6.0/rubygems/core_ext/kernel_require.rb:54",
 "/home/ko1/ruby/install/trunk/lib/ruby/2.6.0/fileutils/version.rb"]
["/home/ko1/ruby/install/trunk/lib/ruby/2.6.0/fileutils.rb:1670",
 "/home/ko1/ruby/install/trunk/lib/ruby/2.6.0/fileutils.rb"]
...
["/home/ko1/ruby/install/trunk/lib/ruby/2.6.0/fileutils.rb:1670",
 "/home/ko1/ruby/install/trunk/lib/ruby/2.6.0/fileutils.rb"]
["/home/ko1/ruby/install/trunk/lib/ruby/2.6.0/fileutils.rb:1695",
 "/home/ko1/ruby/install/trunk/lib/ruby/2.6.0/fileutils.rb"]
...
["/home/ko1/ruby/install/trunk/lib/ruby/2.6.0/fileutils.rb:1695",
 "/home/ko1/ruby/install/trunk/lib/ruby/2.6.0/fileutils.rb"]
["/home/ko1/ruby/install/trunk/lib/ruby/2.6.0/fileutils.rb:1721",
 "/home/ko1/ruby/install/trunk/lib/ruby/2.6.0/fileutils.rb"]
...
["/home/ko1/ruby/install/trunk/lib/ruby/2.6.0/fileutils.rb:1721",
 "/home/ko1/ruby/install/trunk/lib/ruby/2.6.0/fileutils.rb"]

なぜか、同じ場所で沢山のプログラムがロードされているようです。ちょっとソースコードを確認してみましょう。

  1669      names.each do |name|
  1670        module_eval(<<-EOS, __FILE__, __LINE__ + 1)
  1671          def #{name}(*args, **options)
  1672            super(*args, **options, verbose: true)
  1673          end
  1674        EOS
  1675      end
  1676      private(*names)

module_eavl がある部分です。eval(str)module_eval(str), instance_eval(str) なども同様です)も require などと同様に、スクリプト src を ISeq に変換して実行するので、script_compiled イベントでフックできるのです。

Ruby 2.6 からは、eval に渡した文字列を取得する TracePoint#eval_string というメソッドがあります。これを利用することで、どんな文字列を eval で実行しているかがわかります。なお、require などファイル指定でファイルをロードした場合はこのメソッドは nil を返します。

では、試してみましょう。

TracePoint.new(:script_compiled){|tp|
  pp ["#{tp.path}:#{tp.lineno}", tp.instruction_sequence.path, tp.eval_script]
}.enable

require 'fileutils'

結果はこんな感じです。

["/home/ko1/ruby/install/trunk/lib/ruby/2.6.0/rubygems/core_ext/kernel_require.rb:54",
 "/home/ko1/ruby/install/trunk/lib/ruby/2.6.0/fileutils.rb",
 nil]
["/home/ko1/ruby/install/trunk/lib/ruby/2.6.0/rubygems/core_ext/kernel_require.rb:54",
 "/home/ko1/ruby/install/trunk/lib/ruby/2.6.0/fileutils/version.rb",
 nil]
["/home/ko1/ruby/install/trunk/lib/ruby/2.6.0/fileutils.rb:1670",
 "/home/ko1/ruby/install/trunk/lib/ruby/2.6.0/fileutils.rb",
 "        def cd(*args, **options)\n" +
 "          super(*args, **options, verbose: true)\n" +
 "        end\n"]
["/home/ko1/ruby/install/trunk/lib/ruby/2.6.0/fileutils.rb:1670",
 "/home/ko1/ruby/install/trunk/lib/ruby/2.6.0/fileutils.rb",
 "        def mkpath(*args, **options)\n" +
 "          super(*args, **options, verbose: true)\n" +
 "        end\n"]
...

module_eval によって何が実行されようとしているかがよくわかりますね。

さて、これらの機能を使うと、スクリプトがロードされた瞬間にブレイクポイントを仕込む、ということが可能になります。

では、これまでの知識を利用して、reserve_location_breakpoint path_pattern, line というメソッドを作ってみましょう。パスを指定するパラメータ名が path ではなく path_pattern なのは、パス名は Regexp でパターンマッチ出来た方が便利かなと思ったためです。

require 'iseq_collector'
def location_breakpoint path, line
  ObjectSpace.each_iseq{|iseq|
    if iseq.path == path &&
       iseq.trace_points.find{|(l, ev)| l == line && ev == :line}
      TracePoint.new(:line){|tp|
        tp.binding.irb
      }.enable(target: iseq, target_line: line)
    end
  }
end

def reserve_location_breakpoint path_pattern, line
  TracePoint.new(:script_compiled){|tp|
    compiled_script_path = tp.instruction_sequence.path
    if path_pattern =~ compiled_script_path
      location_breakpoint(compiled_script_path, line)
    end
  }.enable
end

reserve_location_breakpoint(/lib\/ruby\/2.6.0\/fileutils\.rb/, 9)

# ロードする

require 'fileutils'

実行結果です。

From: /home/ko1/ruby/install/trunk/lib/ruby/2.6.0/fileutils.rb @ line 9 :

     4:   require 'rbconfig'
     5: rescue LoadError
     6:   # for make mjit-headers
     7: end
     8:
 =>  9: require "fileutils/version"
    10:
    11: #
    12: # = fileutils.rb
    13: #
    14: # Copyright (c) 2000-2007 Minero Aoki

irb(main):001:0> exit

ちゃんと、これからロードするファイルに対してブレイクポイントを設定できました。

あと少し整備すれば、デバッガなんて簡単に作れそうですね! やってみたくありませんか?

(場所指定ブレイクポイントは、だいたいこれでいいとして(出来た!)、ではメソッド指定ってどうやりますかね。こちらは、実はなかなか難しいけど、さすがに長くなりすぎるので省略します)

TracePoint#enable(target:) の背景と実装

さて、ここまでの記事は、Ruby 2.6 で導入された新しい TracePoint の拡張を用いて、デバッガのブレイクポイントをどうやって作るのか、ということを題材に、使い方をご紹介しました。これくらいの記事なら、ちょっと気の利いた人なら書けそうです。

本稿では差別化を図るために、これをどうやって実装したか、その背景とテクニックをご紹介します。これはさすがに私しか書けないでしょう。「筆者はこのとき何を考えていたか答えなさい」ってやつですね。

この拡張を導入した背景

TracePoint#enable(taget:) なんですが、ずっと導入したかった機能でした。それこそ、TracePoint 導入した Ruby 2.0 から。しかし、なかなか API が決められなかったんですよね。いろいろ考えちゃって、あーでもない、こーでもないと悩んで、時間がかかりました。欲しいって言う人も居ないし。今回、入った原因は2つ、外圧と割り切りでした。

まず、外圧ですが、背景として、RUBY_EVENT_SPECIFIED_LINE という隠し機能が、たしか Ruby 2.1 だかそこらで入れてありました。まさに、ブレイクポイントを実装するための仕組みです。が、Ruby 2.5 で命令書き換えによる trace prefix 命令への置換へ舵を切ったタイミング(詳細は Ruby 2.5 の改善を自慢したい)で、削除したんですよね。同じことは、選択的に命令書き換えすれば出来るだろう、って。

ただ、そのインターフェースを用意していなかった。正確には、用意をしていたんだけど、使おうとするととても使いづらかった。で、RUBY_EVENT_SPECIFIED_LINE なんて隠し機能で誰も知らないだろうと思っていたら、JetBrain でデバッガを作るために使っていたそうなんですよね。で、復活させろー、というリクエストが来まして。ただ、今更戻すのは、いろいろな理由で良くない、ということで、本腰を入れて考え始めました。

割り切りは、ある TracePoint オブジェクトに対して、1 target しか指定できないようにしたことです。いろいろ考えていたときは、複数箇所をどのように対応させるか、それを指定するインターフェースはどうするべきか、というのが悩みどころだったんですが、もう単純に 1 TracePoint object ごとに 1 target、増減はしない! と決めるとすんなり API が決まりました。

この拡張の技術的要点

ISeq ごとに、有効にする hooks を持たせた、というのがキモです。target: で指定された ISeq に、必要な命令を trace_ prefix 命令に変換しておきます。

target_line: 指定があったときは、該当する行の命令のみ、trace_ prefix 命令に置き換えています。ただし、global に有効になる TracePoint と conflict すると、登録していないのに発火する hook を作ることが出来てしまうので、line で内部的にフィルタする仕組みを導入しました。

この辺はソースコードをちゃんと説明しないとわかりづらいので、雰囲気だけ、お伝えしました。

まとめ:TracePoint の拡張

TracePoint の拡張の話をちょっと説明しようと思ったら、Ruby でどうやってデバッガのブレイクポイントを実装するか、という説明になってしまいました。お楽しみ頂けたでしょうか。デバッガ作りたくなってきませんか? 私はなりました。

TracePoint は、デバッガを作る以外にも、いくつか使いようがあります。

例えば、今回導入された script_compiled イベントを用いると、実際にどこでソースコードがロードされたかがわかります。特に、eval の利用を検知することができます。大規模プロジェクトで、なぜか良くわからない挙動をする、「もしかしたらどっかで意図しない eval があるかも?」といったときに、こっそり含まれていた eval を検出する、などといった応用ができるかもしれません。

なお、幸いなことに、script_compiled イベントでは、コンパイル結果の ISeq を差し替えることはできません。平和は保たれました。

用法用量を守って、楽しくお使い頂ければと思います。

Ruby 2.6 で入れようと検討していて、時間切れで入らなかった機能が、沢山あるのですが、その中でも下記の 2 つについては、Ruby 2.7 で、可能なら入れたいと思っています。欲しい人、一緒に検討しませんか?

  • caller/callee での call/return イベント
    • 現状、call/return イベントは、caller で止まるのか callee で止まるのか、決まっていません(call/return は callee、c_call/c_return は caller)。どちらも明確に用意したいと思っています。
    • その際、渡すパラメータ一覧なんかが取れると良さそうだと思っています。
  • method_defined など、メソッド定義等などの変更をフックするイベント

長くなりましたが、TracePoint の拡張については、この辺で終わりにします。

Transient Heap (theap) の導入

もう誰もここまで読んでいない気がしますが、もう少しだけ続けます。

Ruby 2.6 では、Transient Heap (theap) という、メモリ管理のための新しい仕組みを導入しました。Ruby のメモリ管理が、また複雑になりました。

theap を大雑把に紹介すると、世代別コピーGCのテクニックを使うことで、対応済みのオブジェクトについて、短寿命なオブジェクトのために確保されたメモリを効率よく管理する仕組みを導入した、というものです。Array、Object(ユーザー定義クラス)、Struct、および 8 要素以下の Hash が theap を利用しています。

本章では、この theap について、かいつまんで紹介します。

なお、ここからは C のコードばかりになります。

現在のメモリ管理

Ruby でメモリ管理というとガーベージコレクタ(GC)を思い浮かべると思います。メモリ領域(とか、リソース)は GC によって寿命が管理されているので、やはり GC は花形です。昔の Ruby は遅い、と言われていましたが(今も、利用分野によっては言われていると思います)、GC の性能に問題があったことが、その理由の1つであったかと思います。

今(Ruby 2.2 以降)は、世代別インクリメンタル GC を実装しているので、あまり GC アルゴリズムが性能に問題を与えることは少なくなったのではないかと思います。現在の Ruby の GC の話は、何を見るのが一番いいかわからなかったんですが、とりあえず YARV Maniacs 【第 12 回】 インクリメンタル GC の導入 を参考文献としてあげておきます。

さて、GC はオブジェクトの寿命管理をします。今回は、その話とは(あんまり)関係ありません。寿命管理は GC に任せて、その際に生じるメモリ割り当て・解放の話が今回の主役です。

オブジェクトを 1 つ生成すると、GC 管理の領域から 1 つ、メモリ領域を割り当てられます。このメモリ領域のことを、ここでは RValue と呼んでおきましょう。1 word をポインタのサイズとして、RValue は 5 word の固定長のメモリ領域になります。イマドキの 64 bit CPU では、8 byte * 5 = 40 byte のメモリ領域ですね。GC 対象のすべてのオブジェクトは RValue を最低限割り当てられる、ととらえても良いと思います*1

RValue の 5 words のうち、最初の 2 words には、RBasic というヘッダが含まれています。RBasic は flags という、オブジェクトの管理に必要になる情報、および klass という、オブジェクトのクラス情報が含まれています。すべてのオブジェクトはクラスを持っているので、RValue にクラスの情報がついているわけですね。

さて、Ruby のオブジェクトには型があります。クラスとは違う概念で、この RValue をどのように利用するか、というデータ型です。例えば、文字列を格納するために使うなら T_STRING 型、配列を扱うなら T_ARRAY です。型の種類は、Ruby 2.6 では 15 種類、インターナルで利用するための 4 種類(Ruby プログラムからは見えないが、インタプリタには存在する)の計 19 種類あります。この情報は、RBasic::flags の最初の 5 bit に格納されています。

参考までに、Ruby 2.6 の include/ruby/ruby.h から、どんな型があるか、引用してみます(RUBY_ prefix は無視してもらって構いません)。

enum ruby_value_type {
    RUBY_T_NONE   = 0x00,

    RUBY_T_OBJECT = 0x01,
    RUBY_T_CLASS  = 0x02,
    RUBY_T_MODULE = 0x03,
    RUBY_T_FLOAT  = 0x04,
    RUBY_T_STRING = 0x05,
    RUBY_T_REGEXP = 0x06,
    RUBY_T_ARRAY  = 0x07,
    RUBY_T_HASH   = 0x08,
    RUBY_T_STRUCT = 0x09,
    RUBY_T_BIGNUM = 0x0a,
    RUBY_T_FILE   = 0x0b,
    RUBY_T_DATA   = 0x0c,
    RUBY_T_MATCH  = 0x0d,
    RUBY_T_COMPLEX  = 0x0e,
    RUBY_T_RATIONAL = 0x0f,

    RUBY_T_NIL    = 0x11,
    RUBY_T_TRUE   = 0x12,
    RUBY_T_FALSE  = 0x13,
    RUBY_T_SYMBOL = 0x14,
    RUBY_T_FIXNUM = 0x15,
    RUBY_T_UNDEF  = 0x16,

    RUBY_T_IMEMO  = 0x1a, /*!< @see imemo_type */
    RUBY_T_NODE   = 0x1b,
    RUBY_T_ICLASS = 0x1c,
    RUBY_T_ZOMBIE = 0x1d,

各データ型ごとに、T_STRING なら struct RStringT_ARRAY なら struct RArray のように、 5 word のメモリ領域のレイアウトを示すための構造体が定義されています。それぞれ最初に 2 word の RBasic を持っているので、残り 3 word をどのように利用するか決める、というのが、RString だったり RArray だったりの役目になります。

というところまで紹介しましたが、ちょっと不思議なことがあります。1つのStringオブジェクトが持つ文字列の長さは(メモリのある限り)いくらでも大きくすることが可能です。そのため、3 word に収まるわけがないのです。そこで、MRI はどうしているかというと、例えば RString では、1 word に malloc() で確保したメモリへのポインタ、1 word に確保したメモリのサイズ、1 word に文字列の長さを格納しています*2。この構造でしたら、扱う文字列の長さを RString の大きさに気にすることなく大きくすることが可能です。

String オブジェクトを例にご紹介しましたが、他のデータ型も、3 word で収まらない場合は同じようなテクニックを使います。

このあたりのレイアウトは、RHG が執筆された 2002 年から、ほとんど変っていません。というわけで、詳細は RHG の第2章をご参照ください:第2章 オブジェクト

さて、この malloc() したメモリですが、オブジェクトが GC によって回収されるときに free() されます。つまり、Ruby のオブジェクトがある程度の大きさのメモリを必要とする場合、生成と解放時に malloc()free() を実行することになります。もちろん、必要なメモリ量が変更される場合(例えば String オブジェクトが破壊的に伸張されるような場合)は realloc() などを用いて確保する量を変えたりします。

現在のメモリ管理の問題点

現在の問題点は、malloc()free() による確保・解放を頻繁に行っている、という点が挙げられると思います。細かい話はおいといて、だいたいこれくらいのデメリットがあります。

  • malloc/free の操作が重い(malloc ライブラリの実装によります)
  • メモリの断片化が起こる
  • マルチスレッドプログラミングをするとメモリを余計に食ってしまう(malloc ライブラリの実装、設定によります)

GC を持つフツーの言語処理系は、たいてい GC で管理する領域を Ruby のように固定長ではなく、可変長にして、malloc()free() を用いないで実装されます。そのため、これらのデメリットは MRI ならでは、と言えるかも知れません。

Transient Heap のアイディア

malloc()free() を使うとまずそう、ということがわかりました。では、どうすれば良いか。

Transient Heap (theap)は、これを解決するために導入されました。theap のアイディアを簡単に説明すると、GC と仲良くするメモリ領域です。

malloc()free() で管理する領域の他にメモリ領域を用意して、malloc() で確保していたところを theap からメモリ確保するようにします。解放するときは、何もしません。オブジェクト回収時に free() を呼んでいたところは、何もしないように変えるだけです。GC 終了後、theap をまとめて消してしまうため、それぞれの領域を free() する必要がないんです。なんか、速そうじゃないですか。

また、theap は、単にポインタをずらすだけでメモリを確保します(bump allocation と言います。専門用語っぽくて格好いいですね)。なので、メモリ確保が malloc() より速いです。

theap 良さそうですね!

うまい話には裏がつきもので、今回の裏は「生き残るオブジェクトが余計な仕事をしなければならない」です。生き残るオブジェクトが theap を使っていると、GC 終了時に全部消されてしまうと困ります。そこで、生き残るオブジェクトが theap を使っていた場合、別の領域を割り当てて、そこにデータをコピーすることで対処します。この、別の領域を割り当て、そこにコピーすることを、ここでは 待避(evacuate)と言います。

ちなみに、すぐに(GC タイミングごとに)メモリが解放されてしまうので、「Transient(つかの間の) heap」と名付けました。

表にまとめるとこんな感じです。

malloc/free theap
確保 malloc() theap_alloc() (bump allocation)
生存 N/A evacuate
解放 free() N/A

theap のほうが、確保と解放が速いです。GC での生存時、malloc/free では、オブジェクトの生存時には何もする必要がありませんでしたが、theap では GC が起こる度に待避が必要です。利点と欠点があるんですが、さて、どっちが得でしょうか。

そこで、たびたび世代別GCの解説で紹介される世代別仮説というものを引用します。若いオブジェクトは死にやすく、古いオブジェクトは死ににくい、のではないかという経験則です。Ruby プログラムを見ていると、若いオブジェクトをどんどん作って捨てていく、というプログラムがそこそこありそうですので、この仮説は多分正しいことが多いんじゃないでしょうか。そして、生存する率が低く、待避操作が少ないのであれば、theap はうまく効きそうです。効くんじゃないかな。効いてくれると良いな。

そこで、使えるのが以前 インタプリタ開発者によるRubyの挙動解析への道 ご紹介した debug_counter を見てみます。

discourse benchmark の結果を下記に引用します。

[RUBY_DEBUG_COUNTER]    obj_newobj                       162,910,687
[RUBY_DEBUG_COUNTER]    obj_free                         161,117,628
[RUBY_DEBUG_COUNTER]    obj_promote                        7,289,299

この結果を見ると、promote された、つまり古い世代になったオブジェクトの数は、(7,289,299 / 162,910,687) * 100 = 4.47% と、実に 95% のオブジェクトが新世代のまま、ということがわかります。まぁそんなわけで、多分効くんじゃないかな。効いてくれると良いな。

さて、待避のためには、待避先が必要になります。この領域には2つ候補があります。再度 theap から割り当てる方法、それから従来通り malloc() を利用する方法です。一度 malloc() 領域に移してしまえば、以降 theap を気にすることがありません(待避をこれ以降行う必要はありません)。そこで今回は、オブジェクトが GC における若い世代であれば、待避先に theap の領域を選び、古い世代であれば、待避先を malloc() で確保する、ということにしました。

GC のたびに領域をコピーして待避するので、コピーGC、古い世代の管理を persistent 領域(malloc() 領域)で行うので世代別、なので、世代別コピー GC に似ています(アイディアはそこそこ同じです)。寿命管理は mark&sweep なので、ちょっと特殊ですね。

なお、ものすごく大きいデータを theap に確保してしまうと、いざコピーが必要になったとき大変そうなので、theap で一度に確保できる領域は 2KB に制限しています。このサイズに何か根拠があったわけではなく、当てずっぽうです。もしサイズを超える場合は、最初から malloc() して、theap を利用しないようにします。

Transient Heap の工夫:待避のタイミング

theap 良さそうです。が、いくつか問題があります。一番の問題は、素朴に実装してしまうと C 拡張ライブラリの互換性が壊れてしまう、という問題です。どういうことでしょうか。

theap を使った Array オブジェクトについて考えてみます。Array は theap から確保したメモリへのポインタを持っています。

func(VALUE ary){
  /* ary から配列の実体を格納するポインタを取得 */
  const VALUE *ptr = RARRAY_CONST_PTR(ary);
  ptr[0]; /* (1) */
  func_can_cause_gc();
  ptr[0]; /* (2) */
}

例えば、こんな C のコードがあったとします。ptr が theap から確保したメモリへのポインタですね。

さて、theap を素朴に実装すると、ary が参照している Array オブジェクトは生きているので、GC が起きた直後に ary の実体を待避する、というものになります。

ただ、もしそうだとすると、ソースコードの (2) での ptr アクセスが危険なものになります。というのも、ptr を取得してから (2) までの間に GC が起こってしまった場合、待避が行われてしまうので、ptr は古い領域をさすことになり、おかしなことになります。この問題は、ptr の寿命が、C 言語のソースコードの見た目と直感的に反する、という言い方もできるかもしれません(GC がらみでは、こういう問題がいくつも出てきます)。

これを避けるためには、ptr が必要になったときには、毎回 ptr を Array オブジェクトから取得しなければなりません。

func(VALUE ary){
  /* ary から配列の実体を格納するポインタを取得 */
  const VALUE *ptr = RARRAY_CONST_PTR(ary); 
  ptr[0]; /* (1) */
  func_can_cause_gc();
  ptr = RARRAY_CONST_PTR(ary); 
  ptr[0]; /* (2) */
}

ただ、すでに公開されている C-extension は無数にあります。このようにすべて書き換えてください、というのは、そもそも、それを徹底するのが難しい、それから、実際に正しく書き換えるのが難しい(どこで GC が起こるか、すべてチェックする必要があります)、という問題から、現実的ではありません。

そこで、2つの工夫を行うことにしました。

待避のタイミングをファイナライザタイミングにする

GC は、いろいろなところで起きます。これを予測するのはとても難しい。

そこで、待避が行われるタイミングを、GC の直後ではなく、ファイナライザが起動するタイミングに遅延することにしました。え、なんでファイナライザ?

Ruby では、オブジェクトを解放するときに起動するファイナライザを、任意の Ruby コードで記述することができます。つまり、任意の Ruby プログラムが動き得るタイミングということになります。

任意の Ruby コードは、たとえばオブジェクトの破壊的操作を行うことができるので、C 側で取得したポインタが無効になることがあり得ます。つまり、C 側で取得したポインタは、任意の Ruby コードを実行した後では、それがそのまま利用可能かどうかわからない、ということです。

先ほどの例を見てみましょう。

func(VALUE ary){
  /* ary から配列の実体を格納するポインタを取得 */
  const VALUE *ptr = RARRAY_CONST_PTR(ary);
  ptr[0]; /* (1) */
  func_can_cause_gc();
  ptr[0]; /* (2) */
}

もし func_can_cause_gc() 関数が任意の Ruby コードを実行するとしたら、この C 関数は危険です。なぜなら、そこで ary.replace(other_ary) といった、ptr が無効になる処理が実行されるかもしれないためです。というわけで、任意の Ruby コードを実行すると、事前に取得したポインタは保障できない、という問題は、昔からありました。C-extension 開発者は、この制限を受け入れているはずのですので、「このようなコードは無い」という仮定を置くことは妥当です*3

そこで、任意の Ruby コードが動くタイミングで待避を行うのは妥当と言えると思います。そして、GC の後で任意の Ruby コードが動くタイミングというのが、ファイナライザを動かすタイミングなのです。

ポインタを取り出すとき、theap を無効にする

先ほど利用した RARRAY_CONST_PTR() といった、ポインタを取り出す操作を行うと、malloc() ヒープにコピーすることで、theap から待避することにしました。C-extension が扱うポインタは、これで「待避されるかも...」という不安を払拭することができます。この、theap を無効にする処理を detransient と呼んでいます。rb_ary_detransient(ary) という処理を行っています。

だいぶ保守的な決定です。theap のままなら、性能はもう少し高いままかもしれないのに。ただ、動かなくなるプログラムがでるよりは良いだろう、とこのような仕様にしました。

theap に置いたままポインタを取り出したい、ポインタを使っている間は絶対に待避を起こさない自信がある、という場合は、RARRAY_CONST_PTR_TRANSIENT(ary) を用います。detransient しません。array.c など、私が確認したメソッドの実装については、可能な限り theap のまま扱うようにしています。どっちかなー、ちょっと考えるの面倒くさいなー、というときは、detransient してしまうようにしています。

Array を例に説明しましたが、他のデータ型においても似たような方針で実装しています。

なお、本当は、Ruby のデータ型はの内部は時々変ってしまうので、ポインタを取り出すような操作をしないのが一番です。例えば、Array でしたら RARRYA_AREF(), _ASET() などを用いるのが良いでしょう。

Transient Heap の工夫:Hash の実装

Array, Object(ユーザ定義オブジェクト)、Struct に関しては、素直に theap を用いることが出来ました。しかし、Hash はそうはいきません。なぜでしょうか。

Array の場合は、RArray から、1つの連続したメモリオブジェクトを参照します。しかし、Hash の場合はハッシュテーブルという複雑なデータ構造を用いているため(Towards Faster Ruby Hash Tables)、ぽんっと theap を用いるようにすることが出来ませんでした。具体的なハッシュテーブルの実装は、st.c と言うファイルに収められています。この st.c が提供するハッシュテーブルは、Ruby の Hash オブジェクトだけでなく、インタプリタの中で利用する表などでも利用されています。そのため、Hash オブジェクトのためだけに、ごちゃごちゃ theap 対応を入れることが出来ません。

そこで、Hash オブジェクトの実装を2つにわけることにしました。8 要素以下の場合と、8要素以上の場合です。

8要素以下では、ar_table、8要素より大きい場合は st_table(st.c が提供するテーブル)を用いる、というものです。ar_table は、Hash ではありますが、単なる配列のみで実装されており、線形探索を行います。線形探索だけど、たかだか 8 要素だから、まぁいいかなと。もうちょっと工夫してもいいかもしれませんが。

ar_table は、連続した 8B * 3 words * 8 entries = 192 byte の領域を theap から取得します。

もし、要素の追加などで 8 要素以上になれば、st_table を利用するようにスイッチします。

実は、st_table でも、省メモリ化のために 4 要素以下の場合は同じように線形探索するテーブルに変更するようにしているのですが、その処理を ar_table という形で切り出した、というのが今回の変更になります。

さて、そもそも 8 要素以下の要素数の Hash オブジェクトはどの程度あるんでしょうか。先ほどと同じく、インタプリタ開発者によるRubyの挙動解析への道 から、Hash の値を確認してみます。

[RUBY_DEBUG_COUNTER]    obj_hash_empty                     3,632,018
[RUBY_DEBUG_COUNTER]    obj_hash_under4                    4,204,927
[RUBY_DEBUG_COUNTER]    obj_hash_ge4                       2,453,149
[RUBY_DEBUG_COUNTER]    obj_hash_ge8                         841,866

この例では、空の要素数が 3M個、1~3要素までが4M、4~7要素が 2.5M個、それ以上が 0.8M個と、個数が 8 未満の Hash オブジェクトが支配的です。DB 的に Hash を用いると多くのエントリ数の Hash ができますが、沢山作るのは小さい要素数の Hash のようです。キーワード引数とかで使うからですかね。

ちなみに、ar_table の導入によって、theap を用いなくても(すべて malloc() で確保しても)若干 st_table よりも効率が良くなるケースが出てきました。

なお、Hash の theap 対応は、今年の Google Summer of Code で Evan Zhao さんにプロトタイプを作って頂きました(Tacinight/ruby-gsoc-2018)。

Transient Heap の工夫:その他

他にもいろいろ、ちゃんと動くようにするため、思ったよりも苦労しました。

  • ファイナライザタイミングまで遅延することと、旧世代オブジェクトとの食い合わせが悪かったので、いろいろ頑張った
  • 省メモリにするため、いろいろとケチケチするテクニックを使った
  • デバッグが大変だったので、書き込み禁止メモリを用いるオプションを作った
  • それでも後から後から、「時々」出現するバグが見つかるので、テストを沢山実行した

他にもあったような気がしますが、思い出せない。

面倒なので、詳細は割愛します。

Transient Heap の評価

実際、どの程度速くなるんでしょうか。少し評価をご紹介します。

どれくらい theap を使っているか

debug_counter に theap を使っているかどうかを見るカウンタを新設したので、それでチェックしてみましょう。

実行するのが簡単なので、rdoc ベンチマークの結果を見てみます。

# Object
[RUBY_DEBUG_COUNTER]    obj_obj_embed                           11,360
[RUBY_DEBUG_COUNTER]    obj_obj_transient                      541,445
[RUBY_DEBUG_COUNTER]    obj_obj_ptr                             47,567
# Array
[RUBY_DEBUG_COUNTER]    obj_ary_embed                        8,261,454
[RUBY_DEBUG_COUNTER]    obj_ary_transient                    2,818,104
[RUBY_DEBUG_COUNTER]    obj_ary_ptr                            558,292
# Hash
[RUBY_DEBUG_COUNTER]    obj_hash_empty                         523,811
[RUBY_DEBUG_COUNTER]    obj_hash_under4                        541,901
[RUBY_DEBUG_COUNTER]    obj_hash_ge4                             1,598
[RUBY_DEBUG_COUNTER]    obj_hash_ge8                             2,432
[RUBY_DEBUG_COUNTER]    obj_hash_ar                          1,066,943
[RUBY_DEBUG_COUNTER]    obj_hash_st                              2,799
[RUBY_DEBUG_COUNTER]    obj_hash_transient                     955,418
# Struct
[RUBY_DEBUG_COUNTER]    obj_struct_embed                       791,055
[RUBY_DEBUG_COUNTER]    obj_struct_transient                 1,351,924
[RUBY_DEBUG_COUNTER]    obj_struct_ptr                         543,895

Object は 90%、24%(ただし、埋め込み Array を排除すると83%)、Hash は 89%、Struct は50%(埋め込み Struct を除くと 71%)と、結構支配的です。きちんと、theap が活用されていることがわかります。

[RUBY_DEBUG_COUNTER]    heap_xmalloc                         8,244,862
[RUBY_DEBUG_COUNTER]    heap_xrealloc                          318,127
[RUBY_DEBUG_COUNTER]    heap_xfree                           7,905,729
[RUBY_DEBUG_COUNTER]    theap_alloc                          7,332,681
[RUBY_DEBUG_COUNTER]    theap_alloc_fail                       583,852
[RUBY_DEBUG_COUNTER]    theap_evacuate                       1,257,952

こちらでは、malloc した回数と、theap から確保した回数(theap_alloc)が見て取れます。malloc 回数と同程度の回数、theap で割り当てていることがわかります。原因をさがしたら、もうちょっと theap の率を増やすことができるかもしれません。

待避(theap_evacuate)したのは、17% 程度みたいですね。そこそこ少なそうです。

theap_alloc_fail は、theap から確保しようとしたが、何らかの理由で失敗した数です。もうちょっと減らせそうですね。

マイクロベンチマーク

次のグラフは、対応している Array オブジェクトの要素数を変えながら、沢山作るのを繰り返す、というものを、 theap ありとなしで比べた結果です。

f:id:koichi-sasada:20181226145007p:plain

このマイクロベンチマークでは、だいたい、1.5 倍程度の性能向上が得られているのがわかると思います。

f:id:koichi-sasada:20181226171946p:plain

Hash もだいたい 1.5~2倍程度の性能向上を実現しています。ただ、9要素になると theap を使わないため、以前と同程度の性能になります。

実アプリケーション

rdoc

USE_TRANSIENT_HEAP というマクロを 0 にすると theap を無効にできるので、オンとオフ、それぞれを調べてみます。

# without theap
      user     system      total        real
 23.757590   0.363964  24.121554 ( 24.124386)
VmHWM: 423932 kB

# with theap
      user     system      total        real
 22.334208   0.355940  22.690148 ( 22.693046)
VmHWM: 358584 kB

時間は 24.12/22.69 = 6% ほど速くなっています。良かった良かった。

VmHWM は、Linux だと取れる、必要になった実メモリのサイズですが、なぜか 18% ほど削減しています。あまりちゃんと調べていないし、他のアプリではどうか、という調査はしていないのですが、とりあえず「良かったね」と捉えておきます。

sinatra-benchmark

https://github.com/benchmark-driver/sinatra を用いてみました。

Calculating -------------------------------------
                     without-theap  with-theap    ruby_2_5
             sinatra       10.293k     10.493k     10.263k i/s -    100.000k times in 9.714985s 9.530253s 9.743398s

Comparison:
                          sinatra
          with-theap:     10492.9 i/s
       without-theap:     10293.4 i/s - 1.02x  slower
            ruby_2_5:     10263.4 i/s - 1.02x  slower

うーん、2% 速いらしい。微妙ですね...。

discourse

discourse rails benchmark で試したんですが、ちょっと手元に結果がないのですが、たしかさっぱり変らなかった気がします。

この辺は、そもそも malloc/free 自体があまりオーバーヘッドになってないってところでしょう。まぁ、そうですよね。でも、もうちょっと効くと思ったんだけどなあ。

速くなった! という話があれば、お寄せ下さい。

こんな話もあるみたいです。

まとめ:Transient Heap

いろいろ複雑な工夫を入れた割に、Rails とかで効かないとか、イマイチ感動の少ない工夫ではありますが、効くところではもしかしたら効くかもな、という感じです。

一番 theap が効きそうな String ですが、対応していません。C レベルで、すぐにポインタを取り出す処理をしてしまうので、あまり効果がないためです。効果を出すためには、絶対に待避されない、ということを保障する必要がありますが、それを行うのが大変なのですよね。というわけで、将来のバージョンでは対応するかもしれないし、そもそも効果が無いから theap 自体が削除されるかもしれません。効くアプリには効きそうなんですけどねぇ。

もう一つ。現在 theap 用のメモリ領域は、最初に 32MB 固定長を確保しています。これを可変に、必要なときに多く、不用なときには少なく、みたいにするのも手かもしれません。現状は、その辺の調整が難しくて(例えば、OS に返す、要求する、を繰り返すと、とても遅くなります)、手を入れられていません。

おわりに

本稿では、私が Ruby 2.6 に導入した次の2つのトピックについてご紹介しました。

  • TracePoint の拡張
    • 新しいイベント script_compiled の導入
    • フックを有効にする場所を制限する機能の導入
    • デバッガの実装が、10~100倍くらい速くなる、かもしれない
    • ブレイクポイントの実装を例に解説
  • Transient Heap の導入
    • 短寿命メモリオブジェクトの高速化
    • 世代別コピーGCのアイディアを利用
    • Rails とかには効かないかも...。

三日連続で Ruby 2.6 の新機能をお伝えしました。いつになく長い記事になりましたが、冬休みにでもお楽しみ頂ければ幸いです。

では、良いお年をお迎えください。

*1:GC 対象でない Ruby オブジェクトもあります。

*2:本当はもっと複雑ですが、ここでは単純化しておきます。

*3:いや、たまたま動いてたから、と言うケースはそこそこありそうですが...。

Ruby 2.6 新機能:本番環境での利用を目指したコードカバレッジ計測機能

技術部の遠藤(mame)です。1 ヶ月くらい風邪が直らず、苦しみながらこれを書いています。

昨日は Ruby 2.6 の NEWS を裏話付きで解説する記事を書きました(プロと読み解く Ruby 2.6 NEWS ファイル)。今日と明日は、その中でクックパッドのフルタイムRubyコミッタが主に担当したところを少し詳しく紹介します。

今日は、遠藤が作った "oneshot coverage" と言う 2.6 の新機能を紹介します。

背景:Ruby では不要コードの発見・削除が難しい

クックパッドのサービスの多くは、cookpad_all という 1 リポジトリからなる、巨大な Rails アプリケーションとして実現されていました。しかし、このやり方ではメンテナンスが限界になってきたので、「お台場プロジェクト」という大整理プロジェクトが行われてきました。この辺の詳細は次の 2 つの記事が詳しいです。

お台場プロジェクトの活動のひとつに、「不要になったコードを削除する」というものがあります。

クックパッドに入るまで考えたことがなかったのですが、この「不要コードを発見・削除する」が、なかなかむずかしいのでした。他人の書いたコードが不要になったかどうかの判定はむずかしいですし、不要と思って消したら予想外のところで使われていたということもあります。特に Ruby は、インタプリタ言語なのでコンパイルによるチェックがなく*1、その上リフレクションが多用される文化なので、検証がとてもむずかしいです。

そこでクックパッドでは現在、Ruby 本体にパッチを入れて、各コード断片が初めて実行されたときに記録をつけながら本番運用をしています。これにより、長期間運用していても全く使われていないコード断片を効率的に発見できます。また、実際に使われていないコードなので、わりと安心して削除できます。この詳細は次の記事をご覧ください。

これを同じようなことを、Ruby にパッチをあてずに実現するのが、oneshot coverage です。

コードカバレッジとは

コードカバレッジを知っている人はこの節をスキップしてください。

コードカバレッジとは、どのコード断片が実行されたかを記録したものです。

-: # test.rb
1: def foo(n)
2:   if n <= 10
2:     p "n < 10"
-:   else
0:     p "n >= 10" # テストされていない
-:   end
-: end
-:
1: foo(1)
1: foo(2)

左端の数字がコードカバレッジです。各行が何回実行されたかを表しています。空行など意味のない行は - になっています。0 になっているところは、1 度も実行されなかったことを意味しています。通常、コードカバレッジはテスト時に使われ、テスト不足のコード(端的に言うと実行回数が 0 の行)を探すために使われます。

Ruby では、coverage ライブラリを使うことでコードカバレッジを計測できます。

require "coverage"

# コードカバレッジ測定開始
Coverage.start(lines: true)

# 測定対象のプログラムを読み込む
load "test.rb"

# 結果の取得
p Coverage.result
#=> {"test.rb"=>{:lines=>[nil, 1, 2, 2, nil, 0, nil, nil, nil, 1, 1]}}

配列の数字が各行の実行回数に対応しています。

Ruby のコードカバレッジ測定について詳しく知りたい方は RubyKaigi 2017 の遠藤の発表資料をご覧ください。

このコードカバレッジ測定を本番環境で使えば、いつまでたっても実行されない行を発見することができます。しかし、通常のコードカバレッジ測定では、各行を実行するたびにカウントアップのフックを実行することになり、オーバーヘッドが問題になります。また、Covearge.result は、全ソースファイル行数の長さの配列を作るので、こちらもオーバーヘッドが気になるところです。

この問題を解決するのが oneshot coverage です。

oneshot coverage とは

oneshot coverage とは、各行の実行回数ではなく、各行が 1 回でも実行されたかどうかを計測するコードカバレッジです。コードカバレッジ測定ツールは伝統的に、行ごとの実行回数を数えるものが多いですが、実際の用途としては、未テストの行(実行回数が 0 の行)を探すというのがふつうです。なので、実行回数が取れなくなることにデメリットはほとんどないと思います。

oneshot coverage の計測モードでは、各行について最初の 1 回だけカウントアップのフックを実行します。1 度実行されたら、その行のフックのフラグを消し去るので、あとはカバレッジ計測のない状態と同じになります。*2

以下、具体的な使い方を説明していきます。

1. oneshot モードにする方法

oneshot coverage を有効にするには、カバレッジ測定開始メソッドを Coverage.start(oneshot_lines: true) というように呼び出します。

require "coverage"

# カバレッジ測定開始 (oneshot_lines モードにする)
Coverage.start(oneshot_lines: true)

# 測定対象のプログラムを読み込む
load "test.rb"

#  1: # test.rb
#  2: def foo(n)
#  3:   if n <= 10
#  4:     p "n < 10"
#  5:   else
#  6:     p "n >= 10"
#  7:   end
#  8: end
#  9:
# 10: foo(1)
# 11: foo(2)

# 結果の取得
p Coverage.result
{"test.rb"=>{:oneshot_lines=>[2, 10, 3, 4, 11]}}

Coverage.result の返り値が「実行された行番号」の列に変わりました。

2. インクリメンタルに計測する方法

oneshot coverage は運用環境で使うことを想定しているので、たとえば 100リクエストごとや 10 分ごとなど、定期的に Coverage.result を呼んで測定結果を記録していきたくなると思われます。しかし Coverage.result を無引数で呼ぶと、カバレッジの測定を停止してしまいます*3

現在までに実行した行番号は知りたいが、その後もカバレッジ測定は継続して欲しい、というときのために、Coverage.resultstop というキーワード引数を追加しました。次のように使えます。

require "coverage"

# カバレッジ測定開始 (oneshot_lines モードにする)
Coverage.start(oneshot_lines: true)

# 測定対象のプログラムを読み込む
load "test.rb"

# 結果の取得
p Coverage.result(stop: false)
{"test.rb"=>{:oneshot_lines=>[2, 10, 3, 4, 11]}}

# 実行されていなかった test.rb の 6 行目を実行する
foo(100)

# 新たな結果の取得:6 行目が追加された
p Coverage.result(stop: false)
{"test.rb"=>{:oneshot_lines=>[2, 10, 3, 4, 11, 6]}}

1 回目の Coverage.result の呼び出し結果と 2 回目の結果を比べると、6 行目が追加で実行されたことがわかります。

また、clear キーワードを使うと、1 度見た行をクリアできます。

require "coverage"

# カバレッジ測定開始 (oneshot_lines モードにする)
Coverage.start(oneshot_lines: true)

# 測定対象のプログラムを読み込む
load "test.rb"

# 結果の取得
p Coverage.result(clear: true, stop: false)
{"test.rb"=>{:oneshot_lines=>[2, 10, 3, 4, 11]}}

# 実行されていなかった test.rb の 6 行目を実行する
foo(100)

# 新たな結果の取得
p Coverage.result(clear: true, stop: false)
{"test.rb"=>{:oneshot_lines=>[6]}}

# もう未到達の行は存在しない
foo(0)
foo(100)

# 新たな結果の取得(新たに実行された行はない)
p Coverage.result(clear: true, stop: false)
{"test.rb"=>{:oneshot_lines=>[]}}

このように、oneshot coverage では Coverage.result(clear: true, stop: false) を使うのが基本です。

oneshot coverage の結果は「実行された行番号の列」なので、Coverage.result を呼ぶたびに全ソースファイル行数分の配列ができるのを避けることができます。

追記(2019/01/09):clear キーワードを間違えて reset と書いてました。すみません。

3. 実行されなかった行を調べる

「実行された行番号」の列が取れるようになりましたが、実際に興味があるのは「実行されなかった行番号」です。ただし、空行とかコメント行とかのように、実行という概念がない行は無視する必要があります。

そのために、Coverage.line_stub という補助関数を用意しました。この関数を使うと、行カバレッジの配列のスタブを作れます。

require "coverage"

# 行カバレッジのスタブ配列を作る
ary = Coverage.line_stub("test.rb")
p ary #=> [nil, 0, 0, 0, nil, 0, nil, nil, nil, 0, 0]

nil になっているのは空行やコメント行など、0 になっているのは行カバレッジの測定対象(実行という概念がある行)です。

「実行された行番号」番目を 1 にすることで、見慣れた行カバレッジの形式に変換できます。

require "coverage"

# カバレッジ測定開始 (oneshot_lines モードにする)
Coverage.start(oneshot_lines: true)

# 測定対象のプログラムを読み込む
load "test.rb"

# 行カバレッジのスタブ配列を作る
ary = Coverage.line_stub("test.rb")

# 実行された行番号の要素を 1 にしていく
Coverage.result["test.rb"][:oneshot_lines].each do |i|
  ary[i - 1] = 1
end

# test.rb の行カバレッジ
p ary #=> [nil, 1, 1, 1, nil, 0, nil, nil, nil, 1, 1]

このように変換すれば、既存のカバレッジ可視化ツールに渡したり、自分で可視化したりがやりやすくなると思います。

評価実験

次の 3 つの条件で、プログラムの実行にかかる時間を測定してみました。

  • (1) コードカバレッジ測定なし
  • (2) 従来の行コードカバレッジ測定(Coverage.start(lines: true)
  • (3) oneshot coverage モード(Coverage.start(oneshot_lines: true)

マイクロベンチマーク

次のプログラムは、コードカバレッジの計測オーバーヘッドが最大になりそうな人工的な例です。

# bench.rb
def bench
  x = 1
  x = 2
  x = 3
  x = 4
  x = 5
  x = 6
  x = 7
  x = 8
  x = 9
  x = 10
end

10000000.times { bench }

このプログラムの実行時間を測定した結果はこちら。それぞれ 3 回測定した平均です。

条件 時間 (秒)
(1) カバレッジ測定なし 0.972
(2) 従来の行カバレッジ 4.53
(3) oneshot coverage 0.967

(2) がだんとつで遅く、(1) と (3) がほぼ同じです。oneshot coverage が実質ゼロオーバーヘッドであることがわかります。

optcarrot の例

もう少し現実的な例として、Ruby 3 のデファクトベンチマークである optcarrot も測定しました。

結果はこちら。やはり 3 回ずつ測定した平均です。単位は frame per second で、数字が大きいほうが速いです。

条件 fps
(1) カバレッジ測定なし 39.8
(2) 従来の行カバレッジ 10.6
(3) oneshot coverage 39.4

やはり、(2) が 4 倍くらい遅く、(1) と (3) はほぼ同じです。

先ほどのマイクロベンチマークでも optcarrot も CPU 律速のベンチマークなので、Rails のような IO 律速なアプリでは、オーバーヘッドはさらに小さくなっていくはずです。

余談:MJIT との相性

少しだけ補足です。Ruby 2.6 の一番の目玉機能である、JIT コンパイラ、MJIT との相性について。

coverage(というか TracePoint API)が有効だと MJIT は動かないようになっています。oneshot coverage はバイトコード(フックフラグ)を実行時に書き換えるので、JIT コンパイルしてもフラグが変わったらコンパイルのやりなおしになるためです。optcarrot を --jit オプション付きで測定し直した結果がこちら。

条件 --jit なし fps --jit あり fps
(1) カバレッジ測定なし + --jit 39.8 56.1
(2) 従来の行カバレッジ + --jit 10.6 10.3
(3) oneshot coverage + --jit 39.4 38.8

--jit をつけると、(1) カバレッジ測定なしなら 39.8 → 56.1 fps に大幅スピードアップしていますが、(3) oneshot coverage は 39.4 → 38.8 fps と、MJIT が無効になっていることが確認できます。

しかし、現時点では MJIT はまだ Rails アプリを高速化するには至っていない(k0kubun さんによる進捗報告記事)ので、気にしなくても大丈夫です(?)。MJIT のクオリティが上がっていったら、なんか改善できる(してくれる)のではないかと思います。

まとめ

Ruby 2.6 で入った、(最初の1回のフックの後は)ゼロオーバーヘッドでカバレッジを測定できる oneshot coverage という機能を紹介しました。これを本番環境で使うことで、使われていなさそうな行の情報が得られます。不要コードの発見・削除のお役に立てば幸いです。

なお cookpad_all はまだ Ruby 2.6 で運用されていないので(当たり前)、まだ oneshot coverage は適用できていませんが、使っていく方向で計画が進んでます。これについてはまたの機会に報告できればと思います。

*1:インタプリタ言語だからといって同様の検証ができないとも限りませんが、とりあえず Ruby にはそういう検証ツールがいまのところありません。

*2:もう少し言うと、Rails のような IO ボトルネックなプログラムでは、カバレッジ計測のオーバーヘッドは元々ほとんど問題にならないと思います。少なくとも、cookpad.com のテストをカバレッジ計測あり・なしで走らせても、実行時間に違いはみられませんでした。ただ、本番環境で従来のカバレッジ計測を有効にするのは、オーバーヘッドが問題になるリスクがあるかもしれません。

*3:これは Coverage.result の仕様を最初に決めたときの判断ミスで、とても後悔しています。

プロと読み解く Ruby 2.6 NEWS ファイル

技術部の笹田(ko1)と遠藤(mame)です。クックパッドで Ruby (MRI: Matz Ruby Interpreter、いわゆる ruby コマンド) の開発をしています。お金をもらって Ruby を開発しているのでプロの Ruby コミッタです。

もうすぐ Ruby 2.6 がリリースされますね! Ruby 2.6 の新機能は何だろう、と調べるためには、ソースコードの diff を見ればいいのですが、膨大な変更があるので、一つ一つ見ていくのは大変です。

$ git diff --compact-summary origin/ruby_2_5
...
 6404 files changed, 228441 insertions(+), 97984 deletions(-)

そこで、NEWS ファイルという、主要な変更点をまとめたファイルが用意されています。これを見るだけで、Ruby 2.6 の変更点が把握できます。NEWS ファイルは Ruby 2.6 の tarball などに入っています。

ただ、NEWS ファイルも、あまり詳細は書いていないため、読みづらいかも知れません。淡々と、「このメソッドが追加された」とかが並んでいるだけです。

そこで、本記事では Ruby 2.6 の NEWS ファイルの内容を、プロ Ruby コミッタの笹田と遠藤で解説していきます。解説記事はすでにいくつかあり、さらに出てくるだろうと思うのですが、本稿では、なぜ「そのような変更がおこなわれたか」という背景事情をなるべく書くように心がけています。

なお、NEWS ファイルに追記しているのは人間なので、当然追記忘れなどのミスがあります。そのため、これ以外にも変更があるかもしれませんが、ご容赦下さい。

See also:

(ko1) <- 以降、こんなふうに文責を明示します。

NEWS ファイルの読み方

まず、NEWS ファイルの構成について解説しておきます。

見ての通り、Markdown ではなく、RDoc フォーマットで書いてあります。まぁ、読む分には見た目の雰囲気でわかるんじゃないかと思います。

章立ては次の通りです。

  • Language changes / 言語の変更
  • Core classes updates (outstanding ones only) / 組込クラスのアップデート(主要なもののみ)
  • Stdlib updates (outstanding ones only) / 添付ライブラリのアップデート(主要なもののみ)
  • Compatibility issues (excluding feature bug fixes) / 非互換(バグ修正を除く)
  • Stdlib compatibility issues (excluding feature bug fixes) / 添付ライブラリの非互換(バグ修正を除く)
  • C API updates / C API のアップデート
  • Implementation improvements / 性能向上
  • Miscellaneous changes / その他

読めばわかると思いますが、言語の変更が最初にあって、組込クラス、標準ライブラリの仕様変更とかの話になり、最後に互換性関係ない性能向上とかで話を締めています。やっぱり、これまでの Ruby アプリが動くかどうかと言う、仕様変更が気になりますよね。

文中に出てくる [Feature #12912] といった表記は、https://bugs.ruby-lang.org/projects/ruby-trunk/issues に登録されたチケット番号になります。https://bugs.ruby-lang.org/issues/ の後ろに番号を付ければ、この場合は https://bugs.ruby-lang.org/issues/12912 とすれば、当該チケットを見ることができます。

NEWS は上記の順番で並んでいますが、本稿では、この順番は無視して、我々が重要だと思っている、もしくは興味がある変更を順に紹介していきます。でも、笹田が一番興味のある性能向上については、最後にまとめます(知らなくても使う分には問題ないですから)。

(ko1)

言語機能の改善

少し、文法などの拡張がありました。新しい文法などを使うと、古い Ruby で動かなくなるので、お気を付け下さい。

終端なしの Range が導入された

  • Endless ranges are introduced. You can use a Range that has no end, like (0..) (or similarly (0...)). [Feature #12912]

(1..) のように、終端を省略した Range が書けるようになりました。

遠藤が提案・実装しました。提案時に想定していたユースケースは、次の 3 つです。

ary[1..] #=> ary[1..-1] と同じ
    
(1..).each {|index| ... } # 1 から無限にループ

# each_with_index を 0 でなく 1 から始める
ary.zip(1..) {|elem, index| ... }

どれも、意外とスッキリ書けなくてモヤモヤしていました。終端が省略できればいいのでは、と思って実装してみたら、意外にすんなり実装できて驚きました。

他の用途としては、下限の指定を DSL 的に表現するのにも使えそうです。(この用途としては beginless range も欲しくなりますが、こちらの提案は pending となってます)

users.where(id: 10..)

(1..)(1..nil) の構文糖です。終端を nil にするかどうかは少し議論がありました。従来から書けていた nil..nil が endless になってよいのかとか。Qundef(Ruby ユーザからは見えない未定義を表す値)を使うとか、他にも選択肢はありましたが、一番直観的なものということで nil になりました。 (1..)(1...) の違いも結構議論がありました。(1..) は無限大を含む Range のように見えて数学的にはちょっと不思議、とか。しかし ary[1..]ary[1...] と書かないと行けないのは面倒なので、数学的な直感はおいといて (1..)(1...) の両方が入ることになりました。なお、これらはオブジェクトの等価性としては別(exclude フラグの有無が違う)です(ary[1..]ary[1...] は同じ結果になります)。

なお、次のように書くと syntax error になるのでご注意ください。

case id
when 10..
  puts "id >= 10"
end

これは、10.. で行が終わると、行継続になってしまうためです。when (10..) のようにカッコをつければ動きます。直すこともできたのですが、複数行に渡る Range リテラルの例が発見されたので、互換性重視で現在の挙動になりました。

(mame)

ローカル変数の shadowing 警告を削除

ブロックの引数の名前が、外のスコープのローカル変数と衝突しているとき、警告モードで実行すると警告が出ていました。

x = "str"
1.times {|x| ... }
#=> warning: shadowing outer local variable - x

この警告を取り除くことになりました。これにより、次のようなコードで警告を見なくてよくなりました。

user = users.find {|user| cond(user) }
#=> warning: shadowing outer local variable - user ← 2.6 で消えた

歴史的な話をすると、Ruby 1.8 ではこういう衝突があるとき、外のローカル変数を上書きしていたのですが、1.9 から現在の挙動に変わりました。

x = "str"
(0..10).each {|x| }
p x #=> 10    (in Ruby 1.8)
#=> "str" (in Ruby 1.9+)

この警告はその非互換を伝えるために存在しましたが、さすがにもう要らなそうだし、妥当なコードでも警告されてしまうのが邪魔であるということで、消すことになりました。もしこの警告が欲しい人は、Rubocop の Lint/ShadowingOuterLocalVariable を使うといいんじゃないでしょうか。

(mame)

rescue のない else が禁止

  • else without rescue now causes a syntax error. [EXPERIMENTAL] [Feature #14606]

例外処理の構文 begin ... rescue ... end には、例外が投げられなかったときの処理を書く else 節があります。

begin
  ...
rescue
  # 例外が投げられた場合
else
  # 例外が投げられなかった場合
end

rescue 節は何個書いてもよいのですが、0 個でもよかったのでした。

begin
  ...
else
  ...
end

が、このプログラムは意味がなく、理解もしにくいだろうということで、rescue 節 0 個のときは SyntaxError とすることになりました。同様に、メソッド内での rescue 無し else も禁止になりました。

def foo
  ...
else
  ...
end

個人的には、変なコードが書けなくなったので少しだけ残念な気持ちです。

(mame)

定数名で非 ASCII の大文字も利用可能に

定数名は ASCII の大文字でないとダメでしたが、2.6 で Unicode の大文字も OK となりました。

class Мир
  def приветствовать
    "Привет, Мир!"
  end
end

Мはキリル文字の大文字です。

完全に余談ですが、Unicode の大文字・小文字の話題になると、「Dz」という字の話をするのがお作法です。これは D と z の 2 文字ではなく、D と z が合体した 1 つの文字です。こういう文字を、digraph、二重音字と言います。この文字には、大文字・小文字に加え、タイトルケース(先頭の文字だけが大文字)の 3 種類があります。

# 大文字
p "\u01F1" #=> DZ

# 小文字
p "\u01F3" #=> dz

# タイトルケース
p "\u01F2" #=> Dz

Ruby では、大文字とタイトルケースの両方を定数として使えます。

class DZ # OK
end
class Dz # OK
end
class dz #=> class/module name must be CONSTANT
end

ちなみに、Dz はスロバキア語、ハンガリー語などで使われるそうです(Wikipedia の記事)。digraph は Dz 以外にもいくつかあります(List of Unicode Characters of Category “Titlecase Letter”)。

(mame)

キーワード引数とオプション引数のコーナーケースを禁止

  • Non-Symbol keys in a keyword arguments hash cause an exception.

matz がみつけた、オプション引数とキーワード引数を両方受け取るときの微妙な挙動が禁止になりました *1

def foo(h = {}, key: :default)
  p [h, key]
end

foo(:key => 1, "str" => 2)
  #=> [{"str"=>2}, 1] (2.5 まで)
  #=> non-symbol key in keyword arguments: "str" (ArgumentError) (2.6)

Ruby 2 のキーワード引数にはいろいろ変なところがあり、Ruby 3 での作り直しが検討されています(参考:大江戸 Ruby 会議 07 『Ruby 3のキーワード引数について考える』)。この変更は、それの伏線になっているとかいないとか。

追記(2019/03/12):逆にキーワード引数作り直しの障害になることが判明(Bug #15658)したので、キャンセルされました。2.6.2 で戻る予定です。

(mame)

バックトレースで原因(cause)のバックトレースも出るようになった

  • Print cause of the exception if the exception is not caught and printed its backtraces and error message. [Feature #8257]

例外処理中(rescue 文や ensure 文実行中)に、新しい例外を意図して発生させたり(下記の例の NantokaError)、意図せず発生させちゃったり(rescue 文に typo があったりとか。例外処理のテストは網羅するのが面倒なので、よくありそうな話ですね)することがあります。そのとき、プロセス異常終了時のバックトレースの表示には、最後に発生した例外の情報しかありませんでした。ランタイムとしては、最後に発生した例外オブジェクトで、 Exception#cause というメソッドで取れることは取れるんですが、プロセス終了時のエラー表示には含まれていませんでした。

Ruby 2.6 からは、プロセスが例外で終了したとき、あがってきた例外オブジェクトに cause の情報があれば、その情報も一緒に表示するようになりました。

class NantokaError < RuntimeError
end

def my_open
  open('non_existing_file')
end

begin
  my_open
rescue Errno::ENOENT
  raise NantokaError
end

この例では、openErrno::ENOENT 例外を発生しますが、呼び出し元で NantokaError をさらに発生させています。

Ruby 2.5 では、

Traceback (most recent call last):
        1: from /home/ko1/src/ruby/trunk/test.rb:8:in `<main>'
/home/ko1/src/ruby/trunk/test.rb:11:in `rescue in <main>': NantokaError (NantokaError)

と、最後に発生させた NantokaError だけ表示しています。

Ruby 2.6 では、

Traceback (most recent call last):
        3: from /home/ko1/src/ruby/trunk/test.rb:9:in `<main>'
        2: from /home/ko1/src/ruby/trunk/test.rb:5:in `my_open'
        1: from /home/ko1/src/ruby/trunk/test.rb:5:in `open'
/home/ko1/src/ruby/trunk/test.rb:5:in `initialize': No such file or directory @ rb_sysopen - non_existing_file (Errno::ENOENT)
        1: from /home/ko1/src/ruby/trunk/test.rb:8:in `<main>'
/home/ko1/src/ruby/trunk/test.rb:11:in `rescue in <main>': NantokaError (NantokaError)

と、5行目の open が元々のエラーの原因であることを示すようになりました。

冗長になりますが、原因がわからないよりは便利だろう、ということで、導入されることになりました。Java とかで、すでにそのように表示されるようですね。

ちなみに、cause がたくさん連鎖していると、バックトレースはどんどん長くなります。また、上記例をちょっと変えて、少しメソッド呼び出しの深いところで実行してみると、

class NantokaError < RuntimeError
end

def my_open
  open('non_existing_file')
end

def foo
  bar
end

def bar
  begin
    my_open
  rescue Errno::ENOENT
    raise NantokaError
  end
end

foo

結果:

Traceback (most recent call last):
        5: from /home/ko1/src/ruby/trunk/test.rb:20:in `<main>'
        4: from /home/ko1/src/ruby/trunk/test.rb:9:in `foo'
        3: from /home/ko1/src/ruby/trunk/test.rb:14:in `bar'
        2: from /home/ko1/src/ruby/trunk/test.rb:5:in `my_open'
        1: from /home/ko1/src/ruby/trunk/test.rb:5:in `open'
/home/ko1/src/ruby/trunk/test.rb:5:in `initialize': No such file or directory @ rb_sysopen - non_existing_file (Errno::ENOENT)
        3: from /home/ko1/src/ruby/trunk/test.rb:20:in `<main>'
        2: from /home/ko1/src/ruby/trunk/test.rb:9:in `foo'
        1: from /home/ko1/src/ruby/trunk/test.rb:12:in `bar'
/home/ko1/src/ruby/trunk/test.rb:16:in `rescue in bar': NantokaError (NantokaError)

こんなエラー出るようになりました。これをよく見てみると、

        5: from /home/ko1/src/ruby/trunk/test.rb:20:in `<main>'
        4: from /home/ko1/src/ruby/trunk/test.rb:9:in `foo'
        ...
        3: from /home/ko1/src/ruby/trunk/test.rb:20:in `<main>'
        2: from /home/ko1/src/ruby/trunk/test.rb:9:in `foo'

共通するこれらの行が被っていますね。Java だと共通する行は出力しないように制御するようですので、冗長な表記をやめるように、今後変更されるかもしれません。

結構面白いハックネタだと思うので、Ruby インタプリタをいじってみたい人は、挑戦してみませんか?

(ko1)

フリップフロップ構文が非推奨に

Ruby には、フリップフロップと呼ばれる、知る人ぞ知る機能がありました。条件式に 開始条件 .. 終了条件 と書くと、開始条件が成立してから終了条件が成立するまでずっと真になるという便利機能です。たぶん awk → Perl 経由で Ruby に入ったと思われます。

["a", "b", "c", "d", "e"].each do |str|
  if (str == "b") .. (str == "d")
    p str #=> "b", "c", "d" が順に表示される
  end
end

しかし、この機能は 3.0 で削除される方向になりました。2.6 では "warning: flip-flop is deprecated" という警告が出るようになりました。ちょっと残念ですね。

削除が提案された理由 [Feature #5400] が "Nobody knows them. Nobody uses them.(誰も知らない。誰も使ってない。)" という煽りだったので、若干荒れました。「誰がこんなの使うの」と思うような機能でも、誰かは使ってるんですよね。まあそれはともかく matz が消したいと言ったので、消える方向に。

遠藤が非推奨の警告を入れる作業を行ったのですが、思った以上に標準添付ライブラリやビルドスクリプトの中でフリップフロップは使われていて、消して回る対応が大変でした。実際にやってみるとわかるのですが、フリップフロップを使っているコードをフリップフロップ無しにするのは、思った以上にややこしいです。ということで、本当に消えて大丈夫なんでしょうか。

(mame)

Refinement の拡張

Refinement という、Ruby のメソッドを拡張する仕組みがあるのですが、拡張が効く部分が足りない、ということで拡張されることになりました。

Refinement 自体がすごく難しい機能であまり使うべきでは無いと思っているので(笹田個人の感想です)、ここではあんまり紹介しません。ただ、自分が Refinement を使っていて、「あれ、ここで拡張が効くはずなのになんで効かないんだろう?」ということがあれば、それはもしかしたらバグかもしれないので、ご報告頂ければ幸いです。

(ko1)

クラスやメソッドの追加・改善

いろんな変更がありました。

to_h がブロックを受け取るように

  • Array#to_h now accepts a block that maps elements to new key/value pairs. [Feature #15143]

to_h にブロックを渡すことで、キーと値を指定できるようになりました。

# 従来の to_h の使い方

["Foo", "Bar"].map {|x| [x.upcase, x.downcase] }.to_h
  #=> {"FOO"=>"foo", "BAR"=>"bar"}

# 新しい用法

["Foo", "Bar"].to_h {|x| [x.upcase, x.downcase] }
  #=> {"FOO"=>"foo", "BAR"=>"bar"}

Array の他に Enumerable や Struct なども同じように拡張されました。

この提案は過去にもあった([Feature #10208] Passing block to Enumerable#to_h)のですが、そのときは matz がリジェクトしています。しかし今回はシュッとアクセプトされました。気が変わったそうです。何度も言ってみるものですね。

(mame)

Enumerable#chain

  • Enumerable#chain returns an enumerator object that iterates over the elements of the receiver and then those of each argument in sequence. [Feature #15144]
  • Enumerator#+ returns an enumerator object that iterates over the elements of the receiver and then those of the other operand. [Feature #15144]
  • Enumerator::Chain: This is a new class to represent a chain of enumerables that works as a single enumerator, generated by such methods as Enumerable#chain and Enumerator#+.

Ruby プログラミングをしていると、イテレータをよく使います。イテレータが複数あって、それらをいっぺんに辿りたいときはどうするといいでしょうか。

例えば、配列 a1, a2, a3 があるとします。これらの要素すべてを表示する、という簡単な例を考えてみましょう。

こんな感じでしょうか。イテレータの配列を作っています。多重ループになっちゃうのがイマイチですね。

a1 = %w(1 2)
a2 = %w(3 4)
a3 = %w(5 6)
[a1, a2, a3].each{|ary| ary.each{|e| p e}}

では、配列を全部つなげてみるといいでしょうか。

(a1+a2+a3).each{|e| p e}

ただ、これはイテレータが配列の時にしかうまくいかず、また大きな配列を作ってしまうと性能悪化の懸念が生じます。

Ruby 2.6 からは、Enumerable#chain を使って、イテレータをつなげることができるようになりました。例えば、この例では、次のように書くことができます。

a1.chain(a2, a3).each{|e| p e}

Enumerator#+ を使うことで、Enumerator を同じようにつなげることができます。

(a1.each + a2.each + a3.each).each{|e| p e}

Enumerator#+each を持つメソッドならなんでも受け取るので、例えばこんなふうに書けます。

(a1.each + a2 + a3).each{|e| p e}

each を持ったオブジェクトならなんでも与えられるので、a2 だけ逆順に表示したい、といったときは、Array#reverse_eachEnumerator を返すことを利用して、こんなふうに書くことができます。

(a1.each + a2.reverse_each + a3.each).each{|e| p e}

# or
a1.chain(a2.reverse_each, a3).each{|e| p e}

Enumerator::Chain クラスは、この機能を実装するために導入されたので、まぁこのクラスの存在を気にする必要はないでしょう。

要望自体は以前からあったのですが、なんとなくペンディングになっていたのを、最近話題に取り上げたことで導入されることになりました。時々欲しくなりますよね。

最初は、Enumerator だけを対象に、Enumerator#+ だけでいいんじゃないかな、と思っていたんですが、開発者会議で議論するうちに、Enumeable#chain という、みんなが使う配列とかも影響がありそうなメソッドに発展していきました。

Enumerator#+ を使って i1 + i2 + ... と沢山足していくと、内部的に深い木構造を作ることになるので、効率の心配がありました。まぁ、何十も重ねる人は居ないと思うのですが、心配なら、Enumerable#chain(i1, i2, ...) や、Enumerator::Chain.new(i1, i2, ...) を利用するといいと思います。まぁ、居ないと思うんだけど。*2

(ko1)

Enumerator::ArithmeticSequence の導入

  • This is a new class to represent a generator of an arithmetic sequence, that is a number sequence defined by a common difference. It can be used for representing what is similar to Python's slice. You can get an instance of this class from Numeric#step and Range#step.
  • Added Range#% instance method. [Feature #14697]
  • Range#step now returns an instance of the Enumerator::ArithmeticSequence class rather than one of the Enumerator class.

Arithmetic Sequence、つまり等差数列を扱うクラスが提案されました。なんじゃこの長い名前は、誰が使うんだ、と思うかも知れませんが、生成は簡単です。

as1 = 3.step(by: 2, to: 10)

また、Range#% を使っても作ることができます(Range#step の別名として導入されました)。

as2 = (3..10)%2 # 3.step(by: 2, to: 10) と同じ

どちらも、to_a[3, 5, 7, 9] を取り出すことができます。

さて、なんでこのような等差数列が必要になるかというと、Python における スライス があると、色々と便利だそうで(笹田は、どう便利かはよく知りません)、そのため、Ruby ではどのように導入するか、ということが議論になり、最終的に Enumerator::ArithmeticSequence という形で導入されました。

Python では 3:10:2begin:end:step)のように書くそうです。(3..10)%2 は、Ruby で許される表現の中で、短く書けるのでこれでいいか、といった議論を経て導入されました(新規文法の導入も検討しましたが、そこまですることはないか、となりました)。

実は Ruby では、これを役立てるための仕組みは、まだあまり組み込まれていないため、実際に便利に使えるには、いろいろと揃ってきてからかな、と思います。例えば、配列の要素を取り出すといったことはできません(あ、MRI のハックネタですね)。

p (1..20).to_a[(1..)%3]
#=> no implicit conversion of Enumerator::ArithmeticSequence into Integer (TypeError)

例えば Python だと、こんなふうに使えます。

>>> list(range(1, 21))[1::3]
[2, 5, 8, 11, 14, 17, 20]

ちなみに、Enumerator::ArithmeticSequence には #begin#end#step メソッドがあるので、自分のライブラリをこれに対応することが可能です。

なお、Range#step は Enumerator クラスを返していましたが、この変更で Enumerator::ArithmeticSequence が返るという非互換があります。が、まぁ誰もはまらないよね、多分。

(ko1)

Kernel#yield_self の別名に Kernel#then が導入された

yield_self 便利だけど名前がね... という議論に終止符を打つべく、我らがまつもとゆきひろさんが満を持してコミットした別名then です。まつもとさんの久々のコミットでした。

then という言葉自体は、Promise などで使われていて、それと被るから良くないんじゃないの、という批判があったんですが、Matz が、まぁいいんじゃないの、ということで導入されました。新たな名前論争の種になるかもしれません。

(ko1)

Proc に関数合成オペレータ Proc#>>Proc#<< が追加

一部の方にとっては待望の、関数合成オペレータが追加されました。

Proc の f1 、f2 に対して f1 >> f2 とすると、まず f1 を呼び出し、その返り値を f2 に渡して呼び出す、という Proc を新たに作ります。f1 << f2 は逆で、f2 、f1 の順に呼び出します。次の例を見ると違いがわかると思います。

plus2  = -> x { x + 2 }
times3 = -> x { x * 3 }

times3plus2 = plus2 << times3
p times3plus2(3) #=> 3 * 3 + 2 => 11
p times3plus2(4) #=> 4 * 3 + 2 => 14

plus2times3 = times3 << plus2
p plus2times3(3) #=> (3 + 2) * 3 => 15
p plus2times3(5) #=> (5 + 2) * 3 => 21

提案自体は古くからありましたが、なかなか記号が決まらなくて pending になっていました。数学での関数合成の記号は小さい円(たとえばf ∘ g)なのですが、どちらが先に評価されるかわかりにくいことや、Unicode でないと書けないので * で代用せざるを得ないことなどで議論がまとまらないということが続いていました。

今回、Groovy が <<>> を使っている(6.3. Composition)ということが決め手となり、それにならうことになりました。上の例も Groovy のドキュメントのサンプルを翻訳したものです。

(mame)

exception オプションの導入

  • Kernel#Complex, Kernel#Float, Kernel#Integer, and Kernel#Rational take an :exception option to specify the way of error handling. [Feature #12732]
  • Kernel#system takes an :exception option to raise an exception on failure. [Feature #14386]

予想外の入力に対して、例外を起こすか、nil を返すかは、API デザインにとって難しい問題です。例えば、Array#[] は、範囲外アクセスを行うと nil を返します。Array#fetch(index) では、範囲外では例外を返します。Array#[] のほうが圧倒的に短いため、普通は Array#[] を使うと思いますが、その辺(なにをデフォルトに置くか)は言語デザインの妙なのかなと思います。

さて、Kernel#Integer(obj) は、何か obj を与えると、良い感じに整数に変換してくれるメソッドです。ただ、変換できない場合、例外を発生します。

p Integer('hello')
#=> `Integer': invalid value for Integer(): "hello" (ArgumentError)

JSON などをパースするとき、整数としてパースできるかな、という検査をするとき、このメソッドが使えそうですが、失敗時にいちいち例外が発生してしまうと、ちょっと面倒です(プログラムを書くのも面倒だし、性能が落ちてしまうのもいや)。そこで、exception: false というキーワード引数を指定することで、整数への変換に失敗すると、例外ではなく、単に nil を返すようにしました。

Integer だけでなく、Kernel#Complex, Kernel#Float, Kernel#Rational にも同様についたようですね。

似た話で、system() で実行が失敗したときに例外を発生することができるようになりました。

p system("ruby -e raise") #=> false
p system("ruby -e raise", exception: true)
#=> `system': Command failed with exit 1: ruby -e raise (RuntimeError)

(ko1)

File.read('| ...') が出来なくなった

  • File.read, File.binread, File.write, File.binwrite, File.foreach, and File.readlines do not invoke external commands even if the path starts with the pipe character '|'. [Feature #14245]

File.read('| cmd') のように実行すると、cmd の実行結果を返す、みたいな機能があったんですが、File って言ってるのにコマンド実行しちゃうのは罠だろう、ということで、例外になることになりました。

もし必要なら、 IO.read('| cmd') などを使ってください。

(ko1)

String#crypt が非推奨に

crypt(3) はなんかもう古くて脆弱なので消しましょう、ということで、2.6 では非推奨となりました。まあ、String クラスのメソッドにするのは現代から見たらやりすぎですよね。

互換レイヤとして string-crypt gem がリリースされています。

require "string-crypt"

とすれば String#crypt が利用可能になります。これを書きながら気づいたんですが、この gem は Linux でビルドできませんでした。PR を投げておいたのでお待ちください。(なお、Ruby 2.6 でも組み込みの String#crypt が消えたわけではないので、今すぐ困ることはないはずです)

(mame)

Time オブジェクトのタイムゾーンを指定できるように

  • Time.new and Time#getlocal accept a timezone object as well as a UTC offset string. Time#+, Time#-, and Time#succ also preserve the timezone. [Feature #14850]

タイムゾーンを指定した Time オブジェクトを作る方法が環境変数経由しか無かった(びっくり!)ということで、API が追加されました。

Time.new(2018, 12, 25, 0, 0, 0, tz)

tz として渡すオブジェクトは、local_to_utcutc_to_localutc_offset の 3 つのメソッドを実装している必要があるらしいです。

追記(2018/12/25 15:58):local_to_utcutc_to_local が必須、abbrname があると望ましいらしいです。 (mame)

Array#unionArray#difference

それぞれ、Array#|(和集合)と Array#-(差集合)の別名です。

p [1, 2, 3].union([2, 3, 4])      #=> [1, 2, 3, 4]
p [1, 2, 3].difference([2, 3, 4]) #=> [1]

となると Array#& の別名の Array#intersection もありそうなものですが、こちらは導入されていません。なぜなら要望が来なかったので(貢献チャンスかも?)。

追記(2018/12/25 12:33):単なる別名ではなく、任意個の引数がとれるという微妙な違いがありました。

(mame)

Array#select の別名として Array#filter が追加

select の別名です。

[1, 2, 3, 4, 5].filter {|n| n.odd? } #=> [1, 3, 5]

filter というと、該当するものを残すのか(select と同じ)、それとも消すのか(reject と同じ)、曖昧だという声もありましたが、他の言語では残すのが多そうということで、select と同じということになったようです。

#filter!#select! の別名として追加されています。Array 以外に Enumerable や Hash なども同様に追加されてます。

(mame)

Binding#source_location の追加

Binding が作られたファイル位置を返すメソッドが追加されました。[Feature #14230]

# test.rb
bndg = binding # ここは 2 行目

p bndg.source_location #=> ["test.rb", 2]

これには中々面倒くさい背景があります。

現在、eval 内で例外が発生すると、binding 引数由来のファイル名や行番号を表示してしまいます。

bndg = binding # ここは 1 行目

eval(<<END, bndg)
  def foo  # bndg 基準では 1 行目
    raise  # bndg 基準では 2 行目
  end
END

foo #=> Traceback (most recent call last):
    #       1: from test.rb:9:in `<main>'
    #   test.rb:2:in `foo': unhandled exception

例外のスタックトレースを見てください。2 行目で例外が発生したと言われています。しかし、このファイルの 2 行目を見ると、空行です。びっくり。この問題を避けるために、evalbinding 引数の生成元のファイル名や行番号を利用しないようにしよう、ということになりました。[Bug #4352]

しかしこの変更を実際に試したところ、eval("[__FILE__, __LINE__]", bndg) として Binding の生成元のファイル名や行番号を取り出すというイディオムが pry など一部のプログラムで利用されていることが発覚しました。このイディオムはあまり推奨されるものでもないので、この情報をより明示的に取り出す手段の Binding#source_location を導入し、世の中のプログラムではこちらを使うように変えてもらう期間を置くことにしました。

なお、Ruby 2.6 でこのイディオムを警告ありモードで実行すると、警告が出るようになっています。

$ ./ruby -w
eval("[__FILE__, __LINE__]", binding)
-:1: warning: __FILE__ in eval may not return location in binding; use Binding#source_location instead
-:1: warning: __LINE__ in eval may not return location in binding; use Binding#source_location instead

(mame)

Dir#each_child

Ruby 2.5 で導入された、ディレクトリの中を探る(ただし、., .. は列挙しない) Dir.childrenDir.each_child というクラスメソッドはあるけど、Dir.open で生成する Dir インスタンスで使える Dir#each_childDir#children はないから入れましょう、という提案で、「そうだね」とすんなり入りました。

こういう、「そうだね」という提案ばかりだと楽なんですが。

(ko1)

Exception#full_message に highlight, order キーワード引数がついた

  • Exception#full_message takes :highlight and :order options. [Bug #14324]

まぁ、書いてあるとおりなのですが、引数がつきました。そもそも、Exception#full_message とは、って感じですが、ログとかに出力するため、文字列でバックトレース表記(+エラー原因)を出力するために Ruby 2.5 で導入されたものです。これに、いろいろカスタマイズするオプションがついた感じです。

(ko1)

Hash#merge#merge! が任意個の引数を受け取るようになった

  • Hash#merge, Hash#merge!, and Hash#update now accept multiple arguments. [Feature #15111]

Hash#merge が任意個の引数を受け取るようになりました。h1.merge(h2).merge(h3) と書かなくて良くなります。

h1 = { 1 => 1 }
h2 = { 2 => 2 }
h3 = { 3 => 3 }
h1.merge(h2, h3) #=> { 1=>1, 2=>2, 3=>3 }

クックパッドが開催した、Ruby をハックしてみようというイベント Cookpad Ruby Hack Challenge #5 の参加者の方が提案して、作成したパッチが取り込まれました。めでたいですね。Ruby Hack Challenge は、今後もちょくちょく開催すると思うので、貢献してみたい人はぜひご参加ください。

(mame)

open のモードに修飾子 "x" が追加

  • Added new mode character 'x' to open files for exclusive access. [Feature #11258]

Kernel#open などのモードに "x" という修飾子が追加されました。"w" と組み合わせて使うと、ファイルをうっかり上書きしなくて済むようになります。

open("file", "w")  # file を作って開く(すでに file があったら上書き)
open("file", "wx") # file を作って開く(すでに file があったら例外)

(mame)

KeyError 発生原因の receiverkey を Ruby レベルで指定可能に

  • KeyError.new accepts :receiver and :key options to set receiver and key in Ruby code. [Feature #14313]

Ruby 2.5 から、KeyError が発生したときのレシーバとキーが KeyError インスタンスから参照できるようになっています。

begin
  { 1 => 2 }.fetch(:foo)
rescue KeyError => e
  p e.key      #=> :foo
  p e.receiver #=> {1=>2}
end

このように、組み込みの Hash#fetch は内部的にこれらの情報を設定していたのですが、Ruby レベルで投げる例外にこの情報を設定することができませんでした。2.6 では、次のようにできるようになりました。

raise KeyError.new(receiver: recv, key: key)

NameErrorNoMethodError でも、同様に receiver が指定できるようになりました。

(mame)

Module#method_defined? とかが inherited オプショナル引数を受けるようになった

  • Module#method_defined?, Module#private_method_defined?, and Module#protected_method_defined? now accept the second parameter as optional. If it is +true+ (the default value), it checks ancestor modules/classes, or checks only the class itself. [Feature #14944]

細かい話です。

Module#instance_methods は、inherited オプショナル引数を受け取ることができます。デフォルトは true ですが、false にすると、クラスの継承木を辿らないで、そのクラス・モジュールだけ調査します。

p String.instance_methods(true).size   #=> 183
String.ancestors.each{|c|
  p [c, c.instance_methods(false).size]
}
#=>
# [String, 128]
# [Comparable, 7]
# [Object, 0]
# [Kernel, 50]
# [BasicObject, 8]
# 合計 193 ... あれ、あわないよ?
# というのは、10個ほど、重複するメソッドがあるからです(多分)。
# ちなみに、String の 128 個というのはキリが良いですね。

このように、いくつかのメソッドには、inherited オプショナル引数を取りますが、似たようなメソッドで、それを取らないものがあったので、似たようなもの全部に入れてしまえばいいのでは、という提案があって、イイネイイネと入りました。

(ko1)

Object#=~ が非推奨に

Object#=~ が非推奨になりました。と言うとびっくりするかもしれませんが、String#=~Regexp#=~ は残るので、gets =~ /regexp/ みたいな普通のマッチングは引き続き可能なので、ほとんど影響はないはずです。

Object#=~ は、引数にかかわらず常に nil を返すという、あまり用途のわからないメソッドでした(導入経緯も調べたのですが、古すぎてよくわかりませんでした)。

p(1 =~ 1) #=> nil

用途がわからないだけでなく、運が悪いとバグを隠すことがあった(次のような例)ので、非推奨ということになりました。

s = ["foo"] # 文字列のつもりだったのに、うっかり文字列の配列にしてしまった
if s =~ /foo/
  puts "マッチしない……なぜ……"
end

なお、nil はマッチング対象にしたいことがある(たとえば ENV['non_existing'] =~ /regexp/)ということで、NilClass#=~ は新たに導入されました。

Object#=~ と対になる Object#!~ は、非推奨になっていません。!~=~ を呼び出してその返り値の not をとって返すので、自分のクラスに =~ だけを定義するというプログラムが存在します。そういうプログラムは完全に無実であること、また !~ が残っていても =~ がなければ結局 NoMethodError になるだけで実害はないことから、そのまま残されることになりました。

(mame)

Random.bytes が導入

ちょっとした便利メソッドです。

p Random.bytes(3) #=> "\xCF\xB5\xF4"

提案チケットは 2011 年に登録されていて、非常に長い間放置されていました。遠藤が古いチケットを整理しているときに見つけたので、開発者会議の議題にあげて無事採択されました。つまり遠藤がえらい。

(mame)

String#split がブロックを受け取るように

  • String#split yields each substring to the block if given. [Feature #4780]

String#split にブロックを渡すと、区切られた各断片が yield されてくるようになりました。

"foo/bar/baz".split("/") {|s| p s } #=> "foo", "bar", "baz"

これも 2011 年からほったらかしだったチケットを掘り起こした成果です。えらい。

(mame)

Unicode のバージョンが 10.0.0 から 11.0.0 に

  • Update Unicode version from 10.0.0 to 11.0.0. [Feature #14802] This includes a rewrite of the grapheme cluster (/\X/) algorithm and special-casing for Georgian MTAVRULI on String#downcase.
  • Update Emoji version from 5.0 to 11.0.0 [Feature #14802]

Unicode のバージョンが上がったようです。あと Emoji も。正直よくわからないのですが、たとえばジョージア語の大文字が導入されたそうです。

p "ლალი".upcase #=> "ლალი" in 2.5
                 #=> "ᲚᲐᲚᲘ" in 2.6

(mame)

Ruby の抽象構文木を取り出す実験 API が導入

  • RubyVM::AbstractSyntaxTree class is added.
  • RubyVM::AbstractSyntaxTree.parse parses a given string and returns AST nodes. [experimental]
  • RubyVM::AbstractSyntaxTree.parse_file parses a given file and returns AST nodes. [experimental]

なぜか結構話題の、抽象構文木を取り出す API が実験的に導入されました。

ast = RubyVM::AbstractSyntaxTree.parse("1 + 2 * 3")

pp ast #=>
# (SCOPE@1:0-1:9
#  tbl: []
#  args: nil
#  body:
#    (OPCALL@1:0-1:9 (LIT@1:0-1:1 1) :+
#       (ARRAY@1:4-1:9
#          (OPCALL@1:4-1:9 (LIT@1:4-1:5 2) :*
#             (ARRAY@1:8-1:9 (LIT@1:8-1:9 3) nil)) nil)))

RubyVM::AbstractSyntaxTree#children を使うと、サブツリーを取り出せます。

pp ast.children[2] #=>
# (OPCALL@1:0-1:9 (LIT@1:0-1:1 1) :+
#    (ARRAY@1:4-1:9
#       (OPCALL@1:4-1:9 (LIT@1:4-1:5 2) :* (ARRAY@1:8-1:9 (LIT@1:8-1:9 3) nil))
#       nil))

RubyVM::AbstractSyntaxTree.parse_file なんてのもあります。

さて、この API がどういうときに便利かと言うと、実際のところ、そんなに便利ではないと思います。というのも、この抽象構文木は、MRI の評価器の実装に結びついてて読み解くのは難しいし、もちろんドキュメントは無いし、今後の Ruby のバージョンアップで説明なく非互換な変更が入っていくし、微妙に最適化っぽい変換がされててソースとの対応が取りにくいし、という感じで、一般ユーザがカジュアルに使うものではないです(RubyVM という名前空間にあるものは、そういうプロユースのものです)。想定用途は、Ruby 本体のデバッグやテスト、Ruby のバージョンアップに食いついていく覚悟のあるプログラム(たとえば静的解析器とか)などです。普通に Ruby の抽象構文木で遊びたい人は、たぶん parser gem を使うのがよいと思います。

(mame)

RubyVM::AbstractSyntaxTree.of

  • RubyVM::AbstractSyntaxTree.of returns AST nodes of the given proc or method. [experimental]

メソッドオブジェクトや Proc オブジェクトから抽象構文木オブジェクトを取り出す API です。

def f
  1 + 2 + 3
end

pp RubyVM::AbstractSyntaxTree.of(method(:f))
#=> (SCOPE@2:0-4:3
#    tbl: []
#    args:
#      (ARGS@2:5-2:5
#       pre_num: 0
#       pre_init: nil
#       opt: nil
#       first_post: nil
#       post_num: 0
#       post_init: nil
#       rest: nil
#       kw: nil
#       kwrest: nil
#       block: nil)
#    body:
#      (OPCALL@3:2-3:11
#         (OPCALL@3:2-3:7 (LIT@3:2-3:3 1) :+ (ARRAY@3:6-3:7 (LIT@3:6-3:7 2) nil))
#         :+ (ARRAY@3:10-3:11 (LIT@3:10-3:11 3) nil)))

これはもう完全な闇 API です。というのも Ruby インタプリタは、読み込んだソースコードをバイトコードにコンパイルし終えた後は、ソースコード文字列も抽象構文木も捨ててしまうので、本来この抽象構文木は取り出しようがないはずのものです。どのようにしているかと言うと、

  • パース時に、すべてのノードに番号を振っておく
  • 抽象構文木のコンパイル時に、元ソースファイル名やルートノードのノード番号をバイトコードに書き加えておく
  • RubyVM::AbstractSyntaxTree.of が呼ばれたら、メソッドオブジェクトなどのバイトコードが持つ元ソースファイル名とノード番号を引っ張り出す
  • ソースファイルをもう一度開いて、読み込み、パースし直して、対応するノード番号のサブツリーを特定し、抽象構文木オブジェクトとして返す

というハックになっています。なので、ソースファイルの中身が変わっていたり、ソースコードを標準入力などで流し込んだ場合は、RubyVM::AbstractSyntaxTree.of は使えません。闇ですよね。覚悟なしに使わないでください。

(mame)

RubyVM.resolve_feature_path

  • RubyVM.resolve_feature_path identifies the file that will be loaded by require(feature). [experimental] [Feature #15230]

require は、(1) 読み込むファイルのパスを特定する、(2) そのファイルを読み込んで実行する、の 2 段階を行いますが、RubyVM.resolve_feature_path は (1) だけをやる API です。

静的解析を作るときにほしいなーという気分だったのでとりあえず作ってみました。RubyVM の名前空間にある通り、一般ユーザが使うことは想定されていません。もし何か用途があったら、ちゃんとした API として導入することも検討できると思うので、教えてください。

(mame)

TracePoint の拡張

いろいろ便利だったり悪用できたりする TracePoint ですが、いくつか拡張がありました。そもそも、使う人が居なさそうなのに、さらに複雑な拡張が入ったので、普通の人は気にしないでいいと思います。普通じゃない人には待望の機能です。

まず、TracePoint#enable で、target: キーワード引数が導入され、フックを有効にする場所を指定することができるようになりました。

TracePoint(と、その前身となった set_trace_func)は、フックを登録すると、すべての場所でフックが呼ばれるようになります。例えば、あるファイルのある行を実行したときだけ、フックを実行したい、というケースを考えます。つまりブレイクポイントですね。これまでは、フックの中で場所(ファイル名と行番号)を確認する、ということを行っていました。ちょっと考えるだけでも非効率です。TracePoint#enable(target:) を指定することで、本当にフックが欲しいところだけでチェックできるようになりました。

次に、新イベントscript_compiled に対応しました。スクリプトをバイトコード(MRI用語では ISeq: InstructionSequence、命令列)にコンパイルしたタイミングで呼ばれます(ついでに便利メソッドがいくつか増えています)。

この新イベントと TracePoint#enable(target:) を組み合わせることで、(例えば)ブレイクポイントが便利に実装できることになります。

なお、TracePoint の拡張については、後日改めてまとめます。

追記:まとめました。

techlife.cookpad.com

(ko1)

ライブラリの変更

あまり詳しくないので、わかるところだけピックアップしてご紹介します。

Bundler の同梱

Bundler がついに、Ruby と一緒にインストールされるようになりました。もう、gem i bundler としなくてもよくなります。

今後 rubygems と少しずつ統合が進んでいくそうです。

(ko1)

oneshot coverage の導入

  • A oneshot_lines mode is added. [Feature #15022] This mode checks "whether each line was executed at least once or not", instead of "how many times each line was executed". A hook for each line is fired at most once, and after it is fired the hook flag is removed, i.e., it runs with zero overhead.

コードカバレッジ測定機能に、oneshot coverage という新モードを追加しました。 これは、各行の実行回数ではなく、各行が1回でも実行されたかどうかを記録するものです。

oneshot coverageについては、明日詳説する記事を書く予定です。

追記(2018/12/26 10:46):書きました。

techlife.cookpad.com

(mame)

FileUtils#cp_lr

ディレクトリの中の全ファイルを再帰的にハードリンクしていくメソッドです。cp -r と似ていますが、コピーの代わりにハードリンクをします。

2010 年に提案されていて放置されていたチケット [Feature #4189] をチケット整理で掘り起こした成果です。えらい。

(mame)

Matrix の拡張

Matrix が交代行列かどうかを判定する Matrix#antisymmetric? が追加

  • Matrix#antisymmetric?, Matrix#skew_symmetric?

交代行列とは、転置して符号反転させたら元の行列と一致する行列のことです。日本語で言うとややこしいですが、行列 mm.t == -m を満たすなら交代行列です。

これを判定するメソッド antisymmetric? が追加されました。skew_symmetric? という別名も入っています。

以下、どうでもよい話。このメソッドにはなかなかややこしい経緯がありました。このメソッドはそもそも、反対称関係(antisymmetric relation)の行列表現の判定として提案され、取り込まれました。しかし、反対称関係の行列表現とは別に、反対称行列(antisymmetric matrix、日本語では交代行列)という概念があります。Matrix#antisymmetric? という名前なので、「反対称行列の判定がなにかおかしい」というバグ報告が来て、現在の挙動に変わりました。しかし不幸なことに、この行列は数学の分野ではあまり反対称行列とは言わず、歪対称行列(skew-symmetric matrix)と言います(ただし、物理学の世界では antisymmetric matrix が普通らしい)。ということで Matrix#skew_symmetric? の別名も追加されました。なお、日本語では交代行列ということが多いですが、英語で alternating matrix とはどの分野でもあまり言わないようです(ゼロでもないみたいですが)。名前がこんがらがると不幸が起きるという例でした。

(mame)

Matrix の破壊的更新が可能に

  • Matrix#map!, Matrix#collect! [Feature #14151]
  • Matrix#[]=
  • Vector#map!, Vector#collect!
  • Vector#[]=

Matrix#[]= で要素の破壊的更新ができるようになってしまいました。[Feature #14151]

Matrix#map!#collect!Vector#map!#collect!Vector#[]=なども入っています。

個人的に、Matrix みたいな数の一種が破壊的に更新可能なのはとても違和感があるのですが。Matlab とかの方から来た人は、更新したくなるようです。

(mame)

性能向上

最後に、みんな大好き性能向上の話です。本章の執筆は全て笹田 (ko1) が担当します。

MJIT

  • Introduce an initial implementation of JIT (Just-in-time) compiler. [Feature #14235] [experimental]

MJIT という JIT コンパイラが導入されました。Ruby 2.6 の目玉ですね。

私が何か説明するよりも、国分さんの解説記事(Ruby 2.6のJITで実装か検討を行なった最適化集 - Qiita)や発表資料( https://speakerdeck.com/k0kubun )などを見て貰うのが正確で良いと思います。他の人も沢山解説記事を書いているようですし。

現状では、バイトコード実行することによるオーバヘッドを、MJIT によって削減する、といった程度の効果なので、すごく劇的に性能向上、というレベルではありません。いろいろな理由で、Rails での性能向上も、まだ難しいでしょう(他人の仕事には厳しい)。今後、どこまで速くなるか楽しみですね。

Proc まわりの性能向上

ブロックパラメータで渡された Proc#call で呼んでも速い

  • Speedup block.call where block is passed block parameter. [Feature #14330] Ruby 2.5 improves block passing performance. [Feature #14045] Additionally, Ruby 2.6 improves the performance of passed block calling.

Proc まわりでは、def foo(&b); ... baz(&b) のように、渡されたブロックを、別のメソッドに単に渡すだけなら、Proc オブジェクトをわざわざ作らないので速くなる、というハックを Ruby 2.5 で導入しました(Ruby 2.5 の改善を自慢したい)。Ruby 2.6 では、その続きで、もう少しいろいろしています。

大抵、&b と渡ってきたブロックは、yield じゃなくて b.call って呼びたくなると思うんですが、Ruby 2.5 では、結局ここで(b を参照した時点で)Proc オブジェクトを生成してしまので、遅いという問題がありました。

Ruby 2.6 では、ちょっと工夫して(説明が面倒なので詳細割愛)、Proc を生成しなくても済むようになりました。チケットにはどれくらい高速になったか書いていないのですが、下記のベンチマークで試してみると、

Benchmark.driver{|x|
  x.executable name: 'ruby 2.5', command: %w'/home/ko1/ruby/install/ruby_2_5/bin/ruby'
  x.executable name: 'ruby 2.6', command: %w'/home/ko1/ruby/install/trunk/bin/ruby'

  x.prelude %q{
    def foo(&b); b.call; end
    def bar(); yield; end
  }
  x.report 'b.call', %q{
    foo{}
  }
  x.report 'yield', %q{
    bar{}
  }
}
Warming up --------------------------------------
              b.call     2.719M i/s -      2.815M times in 1.035435s (367.78ns/i)
               yield    13.591M i/s -     13.646M times in 1.004094s (73.58ns/i, 272clocks/i)
Calculating -------------------------------------
                       ruby 2.5    ruby 2.6
              b.call     3.451M     12.293M i/s -      8.157M times in 2.363882s 0.663573s
               yield    15.725M     19.970M i/s -     40.772M times in 2.592803s 2.041643s

Comparison:
                           b.call
            ruby 2.6:  12292769.5 i/s
            ruby 2.5:   3450742.9 i/s - 3.56x  slower

                            yield
            ruby 2.6:  19970380.1 i/s
            ruby 2.5:  15725212.8 i/s - 1.27x  slower

こんな結果になり、この環境だと Ruby 2.5 と比べて 3.56 倍くらい速いようです。 ただ、やっぱりまだ yield よりは遅いですね。もうちょと頑張って欲しい。しかし、なんで Ruby 2.6 で yield こんなに速いんだろう。

$SAFE を一部諦めて Proc#call を高速化

  • Speedup Proc#call because we don't need to care about $SAFE any more. [Feature #14318] With +lc_fizzbuzz+ benchmark which uses Proc#call many times we can measure x1.4 improvements. [Bug #10212]

$SAFE という古の機能があるのですが(何の機能かはググってね)、この仕様の一部をこっそり削って、Proc#call が高速になりました。たくさん Proc#call を実行するベンチマークでは、1.4 倍の速度向上が得られたようです。やったね。

具体的に何をしたかというと、Proc#call を呼び出した時の $SAFE を保存しておいて、Proc#call が終了したとき、必ず保存しておいた状態に戻す、という仕様があったんですが、それを撤廃しました。戻すだけなら簡単じゃん、と思うかもしれませんが、「必ず戻す」というのがくせ者で、例外時などでも戻せるように、いろいろ準備が要るのでした。

ただ、そもそも $SAFE を誰も利用しないので、この値を戻す必要がないことが大半で、無駄な努力でした。と言う事情を開発者会議で説明すると、「じゃあやめよう」とすんなりやめることになりました。そのため、元に戻すための諸々のコードが不要になり、軽量になりました。

VM生成系の一新

  • VM generator script renewal; makes the generated VM more optimized. [GH-1779]

VM 生成系と言われてもよくわからないと思いますので、ちょっと解説します。

現在の MRI の仮想マシンは、直接ソースコードを全部書くわけではなく、insns.def というファイルに、命令定義が記述してあります。このファイルを、ある Ruby スクリプトを通すことで、C のソースコードに変換し、それを使って MRI バイナリが完成します。つまり、Ruby のビルドには Ruby が必要です。この「ある Ruby スクリプト」が VM 生成系です。

で、この VM 生成系なんですが、私が10年以上も前に、1ファイルに汚く書き散らしていたモノを Ruby 2.5 までは使っていました。それを今回卜部さんがモダンな感じにファイルを分割したりして整理してくれました。これで、さらに insns.def ファイルに情報を入れやすくなりました。

ということで、すでに最適化に必要な情報を入れたりして VM の実行がいくらか高速化されています。何もしなくても、Ruby 2.6 にバージョンアップするだけで数%速くなる、かもしれません。

スレッドキャッシュの有効化

  • Thread cache enabled for pthreads platforms (for Thread.new and Thread.start). [Feature #14757]

スレッドのキャッシュが有効化されました。ってだけだとわかんないですよね。

Ruby(MRI)のスレッドはネイティブスレッドと1対1対応なので、Thread.new{...} でスレッドを作成すると、OS などが提供するネイティブスレッドの生成が必要になります。例えば、POSIX Thread が利用できる環境では、Ruby は POSIX Thread を pthread_create() 関数を用いて作るのですが、これが重い(ことが多い。実装方法によります)。例えば、Linux では重い。

そこで、このパッチでは、終了して使わなくなった POSIX Thread は少しの間キャッシュしておいて、また Thread.new{...} でスレッドを生成したら、そのキャッシュされたものを使う、というものになります。ベンチマークによっては 70 倍程度速くなったそうです。スレッドを作っては捨て、と繰り返すような、ちょっと特殊かも知れない処理ですね。

なお、「少しの間」というのは、5秒間のようです。

3rd party library が TLS (Thread Local Storage) を用いて、初期値に依存していたりすると、もしかしたらまずいことが起こるかも知れません(再利用されたときは、初期値は前のスレッドの値が残っているため)。もし、そういう例を知っていたら教えてください。

タイマースレッドを不用に

  • timer thread is eliminated for platforms with POSIX timers [Misc #14937]

スレッドを扱うために、インタプリタ内部で、タイマースレッドというものを利用していました。タイマースレッドのために、Ruby プロセスを起動すると、かならず(メイン処理用のスレッドとあわせて)2 つネイティブスレッドを作るようになっていました。

Ruby 2.6 では、POSIX timer API が使えるなら、それを用いることで、タイマースレッドを生成しなくても良くなりました。起動時間の削減と、メモリ等のリソース消費削減に、少し効果ありそうです。

しかし、昔タイマースレッドを導入したのは私(ko1)なんですが、今回の Eric Wong さんによるこの変更、難しすぎて理解できてないんですよね...。

Fiber の実装向上

  • Native implementations (arm32, arm64, ppc64le, win32, win64, x86, amd64) of coroutines to improve performance of Fiber significantly. [Feature #14739]

NEWS エントリ的には「コルーチンをネイティブで実装した」ってありますが、コルーチンを実装するための API を CPU ごとに書いたって話になります。だいたいアセンブラで書いてあります。

技術的には、Fiber の実装には(POSIXの場合)swapcontext といった(現在では非推奨の)API を使っているのですが、こいつらが Fiber の実装にとって、若干無駄な処理をしていました。多分、一番無駄だったのは signal 関連の処理です。今回は、そういう無駄な処理を除いたコンテキスト切り替えの API を独自に作った、というものです。

Fiber 切り替えを沢山行うマイクロベンチマークでは数倍、環境によっては20倍くらい速くなったそうです。また、聞くところによると、Fiber を利用するウェブアプリで数%の性能向上があったとか。

TransientHeap による効率的なメモリ確保

  • Transient Heap (theap) is supported. [Bug #14858] [Feature #14989] theap is managed heap for short-living memory objects. For example, making small and short-living Hash object is x2 faster. With rdoc benchmark, we measured 6-7% performance improvement.

Transient Heap (theap) という新しいメモリ管理のための仕組みを導入して、短寿命のオブジェクトの生成が2倍くらい速くなりました。ただし、対応しているのは、限られたオブジェクトだけで、一番効きそうな String については未対応です。

これについては、後日改めてまとめます。

追記:まとめました。

techlife.cookpad.com

まとめ

Ruby 2.6 の NEWS ファイルの内容を駆け足でご紹介しました。ご紹介したとおり、変更いっぱいありますが、まぁ普段使ってる分にはあまり変らないと思うので、とりあえずバージョンアップしてみてはいかがでしょうか。

なお、本稿をまとめるにあたり、Ruby コミッタ各位にいろいろ「この説明で良い?」とか、「この変更ってなんでやったの?」とか、「なんでこんな仕様にしたの?」といったことを聞いて回りました。快く答えて頂きました各位に御礼申し上げます。

で、その調査の過程で、いくつかのバグを見つけたので、本稿は Ruby 2.6 のリリースに貢献しています。褒めて欲しい。

では、良い Ruby 2.6 ライフをお送りください。メリークリスマス。

(ko1)

*1:ko1: Matz が最近見つけたと言っても、私が気づいた時にはそういう仕様だったので、そういうもんだと思っていたよ...(Ruby 2.1 当時)。

*2:mame: 純粋関数型データ構造っていう、そんなことばかり考えてる本があります。