Hãy xem điều gì xảy ra trong Ruby 1.9.3-p0 nếu bạn gọi extend
vào một đối tượng:
/* eval.c, line 879 */
void
rb_extend_object(VALUE obj, VALUE module)
{
rb_include_module(rb_singleton_class(obj), module);
}
Vì vậy, các mô-đun được trộn vào lớp singleton của đối tượng. Làm thế nào tốn kém là nó để lấy các lớp singleton? Vâng, rb_singleton_class_of(obj)
lần lượt gọi singleton_class_of(obj)
(lớp.c: 1253). Điều đó trở lại ngay lập tức nếu lớp singleton được truy cập trước đó (và do đó đã tồn tại). Nếu không, một lớp mới được tạo ra bởi make_singleton_class
mà không phải là quá đắt cũng như:
/* 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;
}
Đây là tất cả O(1)
. Sau đó, rb_include_module
(lớp.c: 660) được gọi là O(n)
về số lượng các mô-đun đã được bao gồm bởi lớp singleton vì nó cần phải kiểm tra xem mô-đun đã có (thường không có nhiều mô-đun đi kèm) trong lớp học singleton, vì vậy điều này là không sao).
Kết luận:extend
không phải là một hoạt động rất tốn kém để bạn có thể sử dụng nó thường xuyên nếu bạn muốn. Điều duy nhất tôi có thể tưởng tượng là độ phân giải của phương thức gọi tới cá thể sauextend
có thể phức tạp hơn một chút, vì cần phải kiểm tra thêm một lớp mô-đun. Cả hai đều ít vấn đề hơn nếu bạn biết rằng lớp singleton đã tồn tại. Trong trường hợp đó, extend
giới thiệu hầu như không có thêm sự phức tạp nào. Tuy nhiên, các trường hợp mở rộng động có thể dẫn đến mã rất khó đọc nếu được áp dụng quá rộng, vì vậy hãy cẩn thận.
benchmark nhỏ này cho thấy tình hình liên quan đến hiệu suất:
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
Kết quả
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)
Điều thú vị là, Module#include
trên metaclass thực sự là chậm hơn so với Object#extend
, mặc dù nó không chính xác những điều tương tự (vì chúng ta cần cú pháp đặc biệt của Ruby để truy cập metaclass). Object#extend
nhanh gấp hai lần nếu lớp singleton đã tồn tại. Object#define_singleton_method
là chậm nhất (mặc dù nó có thể sạch hơn nếu bạn chỉ muốn tự động thêm một phương thức duy nhất).
Kết quả thú vị nhất là hai kết quả dưới cùng, tuy nhiên: Tạo đối tượng và sau đó mở rộng nó gần gấp 4 lần khi chỉ tạo đối tượng! Vì vậy, nếu bạn tạo ra rất nhiều đối tượng throwaway trong một vòng lặp, ví dụ, nó có thể có một tác động đáng kể đến hiệu năng nếu bạn mở rộng từng cái một. Ở đây hiệu quả hơn nhiều khi tạo một lớp tạm thời bao gồm mixin một cách rõ ràng.
Alas đây không phải là toàn bộ câu chuyện. Việc gọi mở rộng vô hiệu hóa tất cả các phương thức lưu trữ của Ruby, cả trên toàn cầu và nội tuyến. Nếu bạn thường xuyên mở rộng các lớp/đối tượng trong khi chương trình của bạn chạy nó sẽ trở nên chậm hơn đáng kể. – akuhn