Sử dụng memoization trong Rails
Bài đăng này đã không được cập nhật trong 7 năm
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
All rights reserved