2011-12-23 34 views
18

Trước hết, tôi biết cách hoạt động của extendinclude và chúng thường được sử dụng để làm gì ... Cho dù đó có phải là ý hay hay không không phải là một phần của câu hỏi của tôi.Làm thế nào tốn kém là 'mở rộng' trong ruby?

Câu hỏi của tôi là: đắt đến mức nào là extend? Đó là một kỹ thuật Javascript phổ biến để mở rộng các cá thể và các đối tượng singleton. Người ta có thể làm một cái gì đó tương tự trong Ruby, nhưng nó sẽ được làm chậm nếu được sử dụng trên rất nhiều đối tượng?

Trả lời

22

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.

+0

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

8

Một điều cần lưu ý là mở rộng (và bao gồm) cả hai đặt lại bộ nhớ cache mà ruby ​​sử dụng để tra cứu phương pháp triển khai từ tên.

Tôi nhớ điều này đang được đề cập đến như là một vấn đề hiệu suất tiềm năng tại một phiên tại railsconf một vài năm trước. Tôi không biết tác động hiệu suất thực tế là gì, và đánh tôi như một cái gì đó khó để chuẩn bị trong sự cô lập. Thích ứng chuẩn Niklas', tôi đã làm

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 

Trong trường hợp đầu tiên tôi làm tất cả những mở rộng và sau đó lặp qua tất cả mọi người và gọi phương thức .name. Việc vô hiệu hóa bộ nhớ cache rõ ràng vẫn xảy ra, nhưng khi tôi đã gọi tên trên người đầu tiên, bộ nhớ cache sẽ được làm ấm và không bao giờ bị lạnh

Trong trường hợp thứ hai, tôi đang gọi điện để gia hạn và gọi tới số .name. luôn lạnh khi tôi gọi .name

những con số tôi nhận được

 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) 

Vì vậy, các cuộc gọi xen kẽ là chậm hơn. Tôi không thể chắc chắn rằng lý do duy nhất là bộ nhớ cache tra cứu phương pháp được xóa mặc dù.

+0

Bạn có biết điều này xảy ra ở cấp đối tượng hoặc cấp độ toàn cầu không? Tôi chỉ lo lắng nếu điều này làm mất hiệu lực mọi lớp khác không liên quan đến điều này. – lulalala

+1

Phụ thuộc vào phiên bản Ruby và triển khai. Trong các phiên bản hiện tại của MRI, tôi nghĩ rằng đó là một bộ nhớ cache cho mỗi lớp –

1

Gọi extend 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 dòng. Đó là bất cứ lúc nào bạn mở rộng bất kỳ lớp/đối tượng nào, tất cả các cache của phương thức đều được flushed và chuyển tiếp bất kỳ cuộc gọi phương thức nào sẽ nhấn vào cache lạnh.

Tại sao điều này là xấu và phương pháp lưu trữ được sử dụng để làm gì?

Phương pháp lưu trữ bộ nhớ cache được sử dụng để tiết kiệm thời gian khi chạy chương trình Ruby. Ví dụ: nếu bạn gọi value.foo thời gian chạy sẽ thêm một bộ nhớ cache nội tuyến nhỏ với thông tin về lớp học gần đây nhất của value và nơi trong phân cấp lớp học foo được tìm thấy. Điều này giúp tăng tốc các cuộc gọi trong tương lai từ cùng một cuộc gọi.

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ể. Tốt nhất là giới hạn các lớp/đối tượng mở rộng để bắt đầu chương trình của bạn.

Điều tương tự cũng áp dụng cho phương pháp xác định và bất kỳ thay đổi nào khác có thể ảnh hưởng đến giải pháp phương pháp.

Đối infomration thêm về vấn đề này xin vui lòng tham khảo bài viết này bằng cách cố James Golick, http://jamesgolick.com/2013/4/14/mris-method-caches.html

Các vấn đề liên quan