Sử dụng memoization trong Rails
Memoization là một kỹ thuật tối ưu hóa chủ yếu sử dụng để tặng tốc độ các chương trình máy tính bằng cách gọi chức năng tránh lặp lại việc tính toán các kết qảu cho đầu vào xử lý trước đó. Dưới đây là một ví dụ Đặt vấn đề Hãy tưởng tượng có một hệ thống thanh toán mà một user có nhiều tài ...
Memoization là một kỹ thuật tối ưu hóa chủ yếu sử dụng để tặng tốc độ các chương trình máy tính bằng cách gọi chức năng tránh lặp lại việc tính toán các kết qảu cho đầu vào xử lý trước đó. Dưới đây là một ví dụ
Đặt vấn đề
Hãy tưởng tượng có một hệ thống thanh toán mà một user có nhiều tài khoản, mỗi tài khoản có ngân sách riêng, chúng ta có một phương thức total_budget cho đối tượng user, thực hiện tính tổng ngân sách của tất cả tài khoản có hiệu lực. Tiếp theo chúng ta xem xét model dưới đây
class User < ActiveRecord::Base has_many :available_accounts, class_name: "Account", conditions: "budget > 0" def total_budget self.available_accounts.inject(0) { |sum, a| sum += a.budget } end end
total_budget sẽ được gọi nhiều lần trong models, view và controllers như
<% if current_user.total_budget > 0 %> <%= current_user.total_budget %> <% end %>
Mỗi khi chúng ta sẽ dụng total_budget sẽ là một cầu truy vấn db gửi để lấy tất cả các tài khoản có hiệu lực của user sau đó tính tổng ngân sách của các tài khoản có hiệu lực đó. Vậy làm sao để tránh sao lại cấu truy vấn và sao lại tính toán?
Method 1: Cache với biến instance
Đây là một giải pháp dễ dàng để sử dụng cache với biến instance để tránh thực hiện sao lại.
class User < ActiveRecord::Base has_many :available_accounts, class_name: "Account", conditions: "budget > 0" def total_budget @total_budget ||= self.available_accounts.inject(0) { |sum, a| sum += a.budget } end end
Ở đây khi chúng ta gọi total_budget lần đầu, một cầu truy vấn db sẽ gửi đi và tính tổng của các ngân sách sau đó gán tổng đó cho biến instance @total_budget. Khi chúng ta gọi total_budget lần thứ 2 không có gửi cầu truy vấn db và không thực hiện tính toán chỉ trả về luôn biến @total_budget. nếu như giá trị trả về là non-true như nil hoặc false chúng ta có giải pháp như sau
def has_comment? return @has_comment if defined?(@has_comment) @has_comment = self.comments.size > 0 end
Method 2: Memoizable
Vấn đề với memoization này là chúng ta phải xả rác phương thức thực hiện với caching logic. Memorization phải áp dụng tốt nhất một cách minh bạch. Từ Rails 2.2 có một cách để thực hiện memoization minh bạch là sử dụng memoize kế thừa từ ActiveSupport::Memoizable.
class User < ActiveRecord::Base extend ActiveSupport::Memoizable has_many :available_accounts, class_name: "Account", conditions: "budget > 0" def total_budget self.available_accounts.inject(0) { |sum, a| sum += a.budget } end memoize :total_budget end
Phương thức memoize sẽ giúp chúng ta tự động cache kết quả của phương thức vậy chúng ta không cần đổi sự thực hiện của phương thức nữa mà điều chúng ta cần làm là chỉ khai báo những phương thức nào cần memoization.
Các vấn đề lớn khác với caching biến instance là nó không tiện lợi cho việc cache đối với kết quả khác nhau phụ thuộc vào đầu vào khác nhau. Giờ chúng ta định nghĩa một phương thức mới total_spent.
class User < ActiveRecord::Base extend ActiveSupport::Memoizable has_many :available_accounts, class_name: "Account", conditions: "budget > 0" def total_budget self.available_accounts.inject(0) { |sum, a| sum += a.budget } end def total_spent(start_date, end_date) self.available_accounts.where("created_at >= ? and created_at <= ?", start_date, end_date).inject(0) { |sum, a| sum += a.spent } end memoize :total_budget, :total_spent end
Việc cache kết quả total_spent rất bật tiện bằng cách sử dụng biến instance vì kết quả của total_spent sẽ khác nhau phụ thuộc vào biến đầu vào start_date và end_date. Nhưng memoize có thể làm việc rất hoàn hảo là memoization cho các phương thức mà không cần các đối số, nó sẽ cache các kết quả khác nhau phục thuộc các đầu vào.
Sự phản đối(Deprecation)
Không thể nói là việc sử dụng memoization là phản đối vì module ActiveSupport::Memoize đã phản đối trong Rails 3.2 xem commit của josevalim khuyến khích thay thế sử dụng Ruby nó là cùng giải pháp với caching biến instance như đã nêu lên ở đoạn trên nhưng ActiveSupport::Memoize cùng cấp nhiều tính năng hơn giải pháp @var ||= như sau :
- memoize chính xác giá trị non-true (nil, false, vv..)
- Thu nhập memoization bằng các tham số của phương thức
- Tách biệt giá trị trả về của cache từ biến instance
Kết luận
Đây là một phần để tối ưu hóa hiệu năng của hệ thông và còn có nhiều cách nữa nhưng phải xem xét kỹ trước khi áp dụng hãy nghĩ rằng là bạn có thật sự cần tối ưu hóa chưa?. Nếu bạn muốn tìm hiểu sâu hơn bạn có thể tham khảo gem memoist nó là khai thác trực tiếp từ ActiveSupport::Memoize.
Tham khảo
- User memoization
- memoist
- ActiveSupport::Memoize