2011-12-23 56 views
18

首先,我知道extendinclude是如何工作的,以及它們通常用於什麼等等。這是否是一個好主意並不是我的問題的一部分。紅寶石中的'擴展'代價如何?

我的問題是:extend多貴?這是擴展實例和單例對象的常用Javascript技術。人們可以在Ruby中做類似的事情,但是如果在很多對象上使用它會很慢嗎?

回答

22

讓我們來看看在Ruby中1.9.3-P0會發生什麼,如果你的對象上調用extend

/* eval.c, line 879 */ 
void 
rb_extend_object(VALUE obj, VALUE module) 
{ 
    rb_include_module(rb_singleton_class(obj), module); 
} 

所以模塊混合到單獨的類的對象。獲取單身人士課程的成本如何?那麼,rb_singleton_class_of(obj)依次調用singleton_class_of(obj)class.c:1253)。如果之前訪問過單個類(並因此已經存在),那麼該單元立即返回。如果不是這樣,一個新的類被make_singleton_class創建這是不是太昂貴,以及:

/* class.c, line 341 */ 
static inline VALUE 
make_singleton_class(VALUE obj) 
{ 
    VALUE orig_class = RBASIC(obj)->klass; 
    VALUE klass = rb_class_boot(orig_class); 

    FL_SET(klass, FL_SINGLETON); 
    RBASIC(obj)->klass = klass; 
    rb_singleton_class_attached(klass, obj); 

    METACLASS_OF(klass) = METACLASS_OF(rb_class_real(orig_class)); 
    return klass; 
} 

這是所有O(1)。 之後,rb_include_moduleclass.c:660)被調用,它是關於單個類已包含的模塊數的O(n),因爲它需要檢查模塊是否已經存在(通常不會有很多包含的模塊在單身類,所以這沒關係)。

結論:extend是不是一個非常昂貴的操作,所以你可以經常使用它,如果你想。我可以想象的唯一的事情是extend之後的實例的方法調用的解析可能稍微複雜一點,因爲需要檢查一個額外的模塊層。如果您知道單例類已經存在,那麼兩者都不是問題。在這種情況下,extend幾乎沒有引入額外的複雜性。 但是,如果應用程序過於廣泛,動態擴展實例會導致代碼非常不可讀,請注意。

這個小基準證明有關業績情況:

require 'benchmark' 

module DynamicMixin 
    def debug_me 
    puts "Hi, I'm %s" % name 
    end 
end 

Person = Struct.new(:name) 

def create_people 
    100000.times.map { |i| Person.new(i.to_s) } 
end 

if $0 == __FILE__ 
    debug_me = Proc.new { puts "Hi, I'm %s" % name } 

    Benchmark.bm do |x| 
    people = create_people 
    case ARGV[0] 
    when "extend1" 
     x.report "Object#extend" do 
     people.each { |person| 
      person.extend DynamicMixin 
     } 
     end 
    when "extend2" 
     # force creation of singleton class 
     people.map { |x| class << x; self; end } 
     x.report "Object#extend (existing singleton class)" do 
     people.each { |person| 
      person.extend DynamicMixin 
     } 
     end 
    when "include" 
     x.report "Module#include" do 
     people.each { |person| 
      class << person 
      include DynamicMixin 
      end 
     } 
     end 
    when "method" 
     x.report "Object#define_singleton_method" do 
     people.each { |person| 
      person.define_singleton_method("debug_me", &debug_me) 
     } 
     end 
    when "object1" 
     x.report "create object without extending" do 
     100000.times { |i| 
      person = Person.new(i.to_s) 
     } 
     end 
    when "object2" 
     x.report "create object with extending" do 
     100000.times { |i| 
      person = Person.new(i.to_s) 
      person.extend DynamicMixin 
     } 
     end 
    when "object3" 
     class TmpPerson < Person 
     include DynamicMixin 
     end 

     x.report "create object with temp class" do 
     100000.times { |i| 
      person = TmpPerson.new(i.to_s) 
     } 
     end 
    end 
    end 
end 

結果

  user  system  total  real 
Object#extend        0.200000 0.060000 0.260000 ( 0.272779) 
Object#extend (existing singleton class) 0.130000 0.000000 0.130000 ( 0.130711) 
Module#include       0.280000 0.040000 0.320000 ( 0.332719) 
Object#define_singleton_method   0.350000 0.040000 0.390000 ( 0.396296) 
create object without extending   0.060000 0.010000 0.070000 ( 0.071103) 
create object with extending    0.340000 0.000000 0.340000 ( 0.341622) 
create object with temp class    0.080000 0.000000 0.080000 ( 0.076526) 

有趣的是,Module#include在元類實際上比Object#extend慢,儘管它完全一樣的東西(因爲我們需要特殊的Ruby語法來訪問元類)。如果單例類已經存在,則Object#extend的速度是其速度的兩倍以上。 Object#define_singleton_method是最慢的(儘管如果您只想動態添加單個方法,它可以更清潔)。

最有趣的結果是底部的兩個,但是:創建一個對象然後擴展它的速度幾乎是創建對象的4倍!因此,如果您在循環中創建大量一次性對象,則可能會對性能產生重大影響,如果擴展其中的每一個。這裏創建一個包含mixin的臨時類明顯更有效。

+0

唉,這不是完整的故事。調用擴展會使Ruby的所有方法緩存無效,包括全局和內聯。如果您在程序運行時經常擴展類/對象,它將變得非常慢。 – akuhn 2016-03-15 09:12:38

8

有一點需要注意的是,擴展(和包括)都重置了ruby用來從名稱查找方法實現的緩存。

我記得幾年前在railsconf會議上提到這個問題是一個潛在的性能問題。我不知道實際的性能影響是什麼,並且讓我覺得很難孤立地進行測試。適應尼克拉斯標杆,我做了

require 'benchmark' 

module DynamicMixin 
    def debug_me 
    puts "Hi, I'm %s" % name 
    end 
end 

Person = Struct.new(:name) 

def create_people 
    100000.times.map { |i| Person.new(i.to_s) } 
end 

if $0 == __FILE__ 
    debug_me = Proc.new { puts "Hi, I'm %s" % name } 

    Benchmark.bm do |x| 
    people = create_people 

    x.report "separate loop" do 
     people.each { |person| 
     person.extend DynamicMixin 
     } 
     people.each {|p| p.name} 
    end 

    people = create_people 

    x.report "interleaved calls to name" do 
     people.each { |person| 
     person.extend DynamicMixin 
     person.name 
     } 

    end 

    end 
end 

在第一種情況下,我做所有的擴展,然後遍歷所有的人,並調用.name方法。緩存失效顯然仍然會發生,但是一旦我對第一個人稱呼名稱,緩存將被加熱並永不變冷

在第二種情況下,我交替調用來擴展並調用.name,所以緩存是總是冷的時候我打電話。名稱

我得到的數字是

 user  system  total  real 
separate loop 0.210000 0.030000 0.240000 ( 0.230208) 
interleaved calls to name 0.260000 0.030000 0.290000 ( 0.290910) 

所以交織電話較慢。我不能確定唯一的原因是方法查找緩存被清除。

+0

您是否碰巧知道緩存失效是發生在對象級還是全局級?我只是擔心這是否會導致與此無關的其他課程無效。 – lulalala 2015-09-18 03:17:18

+1

取決於ruby的版本和實現。在當前版本的MRI中,我認爲它是每類緩存 – 2015-09-18 05:58:21

1

調用extend會使Ruby的所有方法緩存無效,包括全局和內聯。這是任何時候你擴展任何類/對象的所有方法緩存刷新和前進任何方法調用將命中冷緩存。

爲什麼這是壞的,什麼是方法緩存使用?

方法緩存用於在運行Ruby程序時節省時間。例如,如果調用value.foo,則運行時將添加一個內聯緩存,其中包含有關最近類value以及類層次結構foo的哪個位置的信息。這有助於加速來自同一個呼叫站點的未來呼叫。

如果您在程序運行時經常擴展類/對象,它將變得相當慢。最好將擴展類/對象限制在程序的開始處。

這同樣適用於定義方法和可能影響方法解析的任何其他更改。

關於這個問題,請由已故的詹姆斯Golick參考這篇文章更infomration,http://jamesgolick.com/2013/4/14/mris-method-caches.html