Ruby ruby define_method 为什么比直接定义方法要慢?

zuozuo · 2012年10月18日 · 最后由 zw963 回复于 2012年10月22日 · 5344 次阅读

做了一个 benchmark:

class MyClass
  def hello
    "hello"
  end
end
Benchmark.measure { 1000000.times {MyClass.new.sb} }
#=>   0.450000   0.000000   0.450000 (  0.446404)

class MyClass
  send :define_method,"hello" do
    "hello"
  end
end
#=>   0.560000   0.000000   0.560000 (  0.564162)

这是为什么呢? 网上有帖子说是因为 define_method 需要创建 Proc 才能执行,不明白为什么,求解释。 补充:我问的是代码的执行效率,而不是代码的加载效率。

原因应该很复杂吧,调用方法也需要时间

#1 楼 @jjym 两种方法都需要调用方法呀,而且都是只在执行的时候调用一次,因为即使是define_method的话,在代码加载时方法就已经是定义好了的。

是啊,感觉好奇怪,不知道是不是花在方法查找上去了 不过创建对象的时间到时一样的

require 'benchmark'

class Mimmy
  def hi
    'mimmy'
  end
end

class Kitty
  def self.define_hi
    puts 'hi kitty'
    define_method :hi do
      'kitty'
    end
  end

  define_hi
end

3.times do
  puts Benchmark.measure { 1000000.times {Kitty.new} }
  puts Benchmark.measure { 1000000.times {Mimmy.new} }
end

puts "*"*30

3.times do
  puts Benchmark.measure { 1000000.times {Kitty.new.hi} }
  puts Benchmark.measure { 1000000.times {Mimmy.new.hi} }
end

运行结果

hi kitty
  0.580000   0.000000   0.580000 (  0.586829)
  0.480000   0.000000   0.480000 (  0.497991)
  0.490000   0.000000   0.490000 (  0.497683)
  0.490000   0.000000   0.490000 (  0.501469)
  0.490000   0.000000   0.490000 (  0.505061)
  0.500000   0.000000   0.500000 (  0.504393)
******************************
  0.490000   0.000000   0.490000 (  0.497051)
  0.600000   0.000000   0.600000 (  0.657138)
  0.500000   0.000000   0.500000 (  0.522111)
  0.490000   0.000000   0.490000 (  0.510200)
  0.490000   0.000000   0.490000 (  0.512266)
  0.490000   0.000000   0.490000 (  0.506171)
[outman@outman-desktop tmp]$ ruby kitty.rb 
hi kitty
  0.590000   0.000000   0.590000 (  0.628322)
  0.490000   0.000000   0.490000 (  0.508545)
  0.500000   0.000000   0.500000 (  0.507284)
  0.490000   0.000000   0.490000 (  0.505821)
  0.490000   0.000000   0.490000 (  0.519993)
  0.530000   0.000000   0.530000 (  0.571263)
******************************
  1.520000   0.000000   1.520000 (  1.554227)
  0.780000   0.000000   0.780000 (  0.807944)
  1.500000   0.010000   1.510000 (  1.544104)
  0.800000   0.000000   0.800000 (  0.813888)
  1.300000   0.000000   1.300000 (  1.322703)
  0.790000   0.000000   0.790000 (  0.802711)

多调用了一次 define_method 方法和 send 方法。

#4 楼 @sevk 正解 对象创建的时间一样是因为本来这两种方式产生的结果就是基本等价的

#4 楼 @sevk @fsword 我不太明白的地方是:define_method 和 send 方法是在代码加载的时候调用的还是代码执行的时候(调用 hello 方法)调用?

#6 楼 @zzhattzzh .....难道你认为每调用次 hello 就会多个方法吗

#6 楼 @zzhattzzh send :define_method 这个过程是在类载入的时候进行的(类载入的时候为这个类添加一个 hello 的实例方法),后续对 hello 的所有调用和直接写 def 的做法并无差别

顺便罗嗦一句,ruby 的很多元编程能力很多只是在 runtime 时变化了类的固有行为逻辑,但是由于逻辑本身是固化的,调用栈的深度没有增加,此时语言的动态性并不会降低性能。所以如果你不是用 method_missing 这一类技巧,那么无需担心运行效率

#7 楼 @jjym #8 楼 @fsword 我也是这么想的,按道理说程序加载的时候 send 和 define_method 已经执行过了,在程序执行的时候 define_method 和直接用 def 的效果应该是一样的,所以就更不明白为什么 defind_method 会慢了。

有可能是 define_method 是用 lambda 来保存的,所以还要调用 call 花掉时间??

等看过 ruby 实现的大牛来解答吧。。

#10 楼 @zzhattzzh 帖子修改过了?一开始好像说的不是这个意思?

如果真要看执行时的差异,可以这样

1.9.3p194 :038 > iseq = ISeq.compile "class MyClass2; def hello; 'hello' end; end"
 => <RubyVM::InstructionSequence:<compiled>@<compiled>> 
1.9.3p194 :039 > puts iseq.disassemble
== disasm: <RubyVM::InstructionSequence:<compiled>@<compiled>>==========
0000 trace            1                                               (   1)
0002 putspecialobject 3
0004 putnil           
0005 defineclass      :MyClass2, <class:MyClass2>, 3
0009 leave            
== disasm: <RubyVM::InstructionSequence:<class:MyClass2>@<compiled>>====
0000 trace            2                                               (   1)
0002 trace            1
0004 putspecialobject 1
0006 putspecialobject 2
0008 putobject        :hello
0010 putiseq          hello
0012 send             :"core#define_method", 3, nil, 0, <ic:0>
0018 trace            4
0020 leave            
== disasm: <RubyVM::InstructionSequence:hello@<compiled>>===============
0000 trace            8                                               (   1)
0002 trace            1
0004 putstring        "hello"
0006 trace            16
0008 leave            
 => nil 
1.9.3p194 :040 > iseq = ISeq.compile "MyClass2.send :define_method, 'hello' do 'hello' end"
 => <RubyVM::InstructionSequence:<compiled>@<compiled>> 
1.9.3p194 :041 > puts iseq.disassemble
== disasm: <RubyVM::InstructionSequence:<compiled>@<compiled>>==========
== catch table
| catch type: break  st: 0002 ed: 0019 sp: 0000 cont: 0019
|------------------------------------------------------------------------
0000 trace            1                                               (   1)
0002 getinlinecache   9, <ic:0>
0005 getconstant      :MyClass2
0007 setinlinecache   <ic:0>
0009 putobject        :define_method
0011 putstring        "hello"
0013 send             :send, 2, block in <compiled>, 0, <ic:1>
0019 leave            
== disasm: <RubyVM::InstructionSequence:block in <compiled>@<compiled>>=
== catch table
| catch type: redo   st: 0000 ed: 0004 sp: 0000 cont: 0000
| catch type: next   st: 0000 ed: 0004 sp: 0000 cont: 0004
|------------------------------------------------------------------------
0000 trace            1                                               (   1)
0002 putstring        "hello"
0004 leave            
 => nil 

似乎是用 define_method 添加的方法会把 block 封装成调用对象,于是每次调用时,调用栈会多一层 之前的说法不是很准确,稍微精确的说法应该是 define_method 等价于下面的代码

class MyClass3
  def hello
    inner_proc
  end

  def inner_proc
    'hello'
  end
end

#12 楼 @fsword 帖子没改过,不过你说的很有道理,受教了。

首先可以肯定,性能的损失来自于调用 define_method 方法 block, 如果没有这个 block 的调用,它们所花的时间一定是一样的。

一个是关键字直接定义方法,方法名作为原义被直接定义。 另一个是方法调用,而且需要通过符号获取方法名,最后内部还要完成类似于 使用关键字定义一个方法 的过程,多了一道流程,速度慢一些,但是允许通过符号动态的生成方法名称,有得有失。

需要 登录 后方可回复, 如果你还没有账号请 注册新账号