+1

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 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_dateend_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

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí