首先,我知道extend
和include
是如何工作的,以及它們通常用於什麼等等。這是否是一個好主意並不是我的問題的一部分。紅寶石中的'擴展'代價如何?
我的問題是:extend
多貴?這是擴展實例和單例對象的常用Javascript技術。人們可以在Ruby中做類似的事情,但是如果在很多對象上使用它會很慢嗎?
首先,我知道extend
和include
是如何工作的,以及它們通常用於什麼等等。這是否是一個好主意並不是我的問題的一部分。紅寶石中的'擴展'代價如何?
我的問題是:extend
多貴?這是擴展實例和單例對象的常用Javascript技術。人們可以在Ruby中做類似的事情,但是如果在很多對象上使用它會很慢嗎?
讓我們來看看在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_module
(class.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的臨時類明顯更有效。
有一點需要注意的是,擴展(和包括)都重置了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)
所以交織電話較慢。我不能確定唯一的原因是方法查找緩存被清除。
您是否碰巧知道緩存失效是發生在對象級還是全局級?我只是擔心這是否會導致與此無關的其他課程無效。 – lulalala 2015-09-18 03:17:18
取決於ruby的版本和實現。在當前版本的MRI中,我認爲它是每類緩存 – 2015-09-18 05:58:21
調用extend
會使Ruby的所有方法緩存無效,包括全局和內聯。這是任何時候你擴展任何類/對象的所有方法緩存刷新和前進任何方法調用將命中冷緩存。
爲什麼這是壞的,什麼是方法緩存使用?
方法緩存用於在運行Ruby程序時節省時間。例如,如果調用value.foo
,則運行時將添加一個內聯緩存,其中包含有關最近類value
以及類層次結構foo
的哪個位置的信息。這有助於加速來自同一個呼叫站點的未來呼叫。
如果您在程序運行時經常擴展類/對象,它將變得相當慢。最好將擴展類/對象限制在程序的開始處。
這同樣適用於定義方法和可能影響方法解析的任何其他更改。
關於這個問題,請由已故的詹姆斯Golick參考這篇文章更infomration,http://jamesgolick.com/2013/4/14/mris-method-caches.html
唉,這不是完整的故事。調用擴展會使Ruby的所有方法緩存無效,包括全局和內聯。如果您在程序運行時經常擴展類/對象,它將變得非常慢。 – akuhn 2016-03-15 09:12:38