RubyのFloat(arg, exception: true)をbuiltinで再実装してみた

はじめに

この記事は以前書いた以下の記事で紹介したbuiltinを使ってFloat(arg, exception: true)というメソッドを再定義してみた記事になります。

gamelinks007.hatenablog.com

やっていることは前回の記事とおおよそ同じです。前回と異なる点は、特定のクラスのメソッドではなく、どこでも呼び出すことができるメソッドをbuiltinで再定義してみたという点です。

前回の記事を書いた後、raiseFloatなどのメソッドをbuiltin対応ができるのか気になり試してみた感じですね。

結論から言うと、そういったグローバルなメソッドもbuiltinで対応することができるようです(意図した挙動かは確認とっていないので、推奨されたものではないかもしれませんが……)

builtinって?

builtinとはRuby(とC)でRuby自体を実装するというものです。詳しくは前回の記事を読んでいただければと思います。

gamelinks007.hatenablog.com

環境構築

前回の記事の環境をそのまま使用しています。特に大きな変更点などもありません。

gamelinks007.hatenablog.com

やってみた

Floatメソッドの実装場所を探す

まずはFloatメソッドが実装されているソースを探します。Rubyのメソッドは大体こんなかんじで定義されています。

rb_define_method(rb_mKernel, "to_s", rb_any_to_s, 0);

第一引数がメソッドが定義されているクラス/モジュールの変数になります。大体、rb_cArrayなどのようにクラス名やモジュール名で作成されています。第二引数の"to_s"Ruby側で呼び出されるメソッド名になっています。第三引数はメソッドが呼び出された際に実行するCの関数で、最後の引数がメソッドが受け取る引数の数を指定しています。

今回、再実装したいのはFloatメソッドですのでRubyソースコード内からgit grep \"Float\"などで検索してみましょう。 するとobject.cで以下のように実装されているようです。

rb_define_global_function("Float", rb_f_float, -1);

それでは再実装していきます。

Floatメソッドの再実装

前回の記事ではHash#deleteを再実装していました。またbuiltin対応のため、common.mkなどの修正していました。 ですが、現在のRubyのmasterのコードではobject.cで実装されていたKernel#cloneをbuiltinに対応させているので、それらの修正は必要ありません。

修正が必要なソースはobject.ckernel.rbの二つになります。

object.cの修正

object.cではまずFloatメソッドを定義している以下のコードを削除します。

void
InitVM_Object(void)
{
    Init_class_hierarchy();

    // 省略

-    rb_define_global_function("Float", rb_f_float, -1);

    // 省略
}

次に、Floatメソッドの処理を定義しているrb_f_floatを以下のように修正します。

- /*  
-  *  call-seq:    
-  *     Float(arg, exception: true)    -> float or nil 
-  *   
-  *  Returns <i>arg</i> converted to a float. Numeric types are   
-  *  converted directly, and with exception to String and 
-  *  <code>nil</code> the rest are converted using    
-  *  <i>arg</i><code>.to_f</code>.  Converting a String with invalid  
-  *  characters will result in a ArgumentError.  Converting   
-  *  <code>nil</code> generates a TypeError.  Exceptions can be   
-  *  suppressed by passing <code>exception: false</code>. 
-  *   
-  *     Float(1)                 #=> 1.0   
-  *     Float("123.456")         #=> 123.456 
-  *     Float("123.0_badstring") #=> ArgumentError: invalid value for Float(): "123.0_badstring"   
-  *     Float(nil)               #=> TypeError: can't convert nil into Float   
-  *     Float("123.0_badstring", exception: false)  #=> nil  
-  */  

static VALUE    static VALUE
- rb_f_float(int argc, VALUE *argv, VALUE obj)
+ rb_f_float(rb_execution_context_t *ec, VALUE main, VALUE arg, VALUE opts)
{
-    VALUE arg = Qnil, opts = Qnil;    
-
-    rb_scan_args(argc, argv, "1:", &arg, &opts);    
-    return rb_convert_to_float(arg, opts_exception_p(opts));      
+   return rb_convert_to_float(arg, opts_exception_p(opts));
}

builtinを使い、キーワード引数を受け取るのでint argc, VALUE *argvではなくVALUE main, VALUE arg, VALUE optsとしています。またVALUE mainはグローバルなメソッドが暗黙の裡に受け取っている引数を代わりに受け取るために追加しています。

これでobject.cでの修正は完了です!

kernel.rbの修正

Rubyでは、どこからでも呼び出すことができるグローバルなメソッドはKernelモジュールにて再定義することができます。

たとえば、putsメソッドは以下のようにモンキーパッチすることができます。

module Kernel
   def puts *args
      p "hoge" 
   end
end


puts :ho
#=> "hoge" と表示される

これを使い、kernel.rb内に以下のようにFloatメソッドを追加します。

module Kernel
+ 
+   #
+   #  call-seq:
+   #     Float(arg, exception: true)    -> float or nil
+   #
+   #  Returns <i>arg</i> converted to a float. Numeric types are
+   #  converted directly, and with exception to String and
+   #  <code>nil</code> the rest are converted using
+   #  <i>arg</i><code>.to_f</code>.  Converting a String with invalid
+   #  characters will result in a ArgumentError.  Converting
+   #  <code>nil</code> generates a TypeError.  Exceptions can be
+   #  suppressed by passing <code>exception: false</code>.
+   #
+   #     Float(1)                 #=> 1.0
+   #     Float("123.456")         #=> 123.456
+   #     Float("123.0_badstring") #=> ArgumentError: invalid value for Float(): "123.0_badstring"
+   #     Float(nil)               #=> TypeError: can't convert nil into Float
+   #     Float("123.0_badstring", exception: false)  #=> nil
+   #
+   def Float(arg, exception: true)
+     __builtin_rb_f_float(arg, exception)
+   end
+ 
  #
  #  call-seq:
  #     obj.clone(freeze: nil) -> an_object
  #
  #  Produces a shallow copy of <i>obj</i>---the instance variables of
  #  <i>obj</i> are copied, but not the objects they reference.
  #  #clone copies the frozen value state of <i>obj</i>, unless the
  #  +:freeze+ keyword argument is given with a false or true value.
  #  See also the discussion under Object#dup.
  #
  #     class Klass
  #        attr_accessor :str
  #     end
  #     s1 = Klass.new      #=> #<Klass:0x401b3a38>
  #     s1.str = "Hello"    #=> "Hello"
  #     s2 = s1.clone       #=> #<Klass:0x401b3998 @str="Hello">
  #     s2.str[1,4] = "i"   #=> "i"
  #     s1.inspect          #=> "#<Klass:0x401b3a38 @str=\"Hi\">"
  #     s2.inspect          #=> "#<Klass:0x401b3998 @str=\"Hi\">"
  #
  #  This method may have class-specific behavior.  If so, that
  #  behavior will be documented under the #+initialize_copy+ method of
  #  the class.
  #
  def clone(freeze: nil)
    __builtin_rb_obj_clone2(freeze)
  end
end

先ほどbuiltin対応させたrb_f_float__builtin_rb_f_floatで呼び出しています。これでkernel.rbでの修正はOKです。

最後に

あとはmakemake install を実行してビルドできればbuiltinでの実装は完了です。

おわりに

こんな感じでFloatIntegerなどのメソッドをbuiltin対応できそうです。パッチとして送るかは別として、結構応用できそうだったのが面白かったですね。

ちなみに、以下のようなベンチマークを試してみたところパフォーマンス向上は期待できそうにないかなという感じでした。

benchmark:
  float: "Float(42)"
  float_true: "Float(42, exception: true)"
  float_false: "Float(42, exception: false)"
loop_count: 10000

以下結果

Calculating -------------------------------------
                     compare-ruby  built-ruby
               float      37.495M     15.878M i/s -     10.000k times in 0.000267s 0.000630s
          float_true       1.109M      1.211M i/s -     10.000k times in 0.009015s 0.008261s
         float_false       1.227M      1.191M i/s -     10.000k times in 0.008150s 0.008395s

Comparison:
                            float
        compare-ruby:  37495314.0 i/s
          built-ruby:  15878056.0 i/s - 2.36x  slower

                       float_true
          built-ruby:   1210507.2 i/s
        compare-ruby:   1109250.0 i/s - 1.09x  slower

                      float_false
        compare-ruby:   1226948.7 i/s
          built-ruby:   1191128.5 i/s - 1.03x  slower

仮にFloatなどのメソッドをbuiltin対応する場合はパフォーマンスよりもコードのメンテナンス性などを重視することになるのかなと思いますね。