Computer >> Máy Tính >  >> Lập trình >> Ruby

Tăng tốc Rails với Memoization

Khi phát triển các ứng dụng chúng ta thường có các phương pháp chạy chậm. Có lẽ họ cần truy vấn cơ sở dữ liệu hoặc truy cập vào một dịch vụ bên ngoài, cả hai đều có thể khiến họ chạy chậm lại. Chúng tôi có thể gọi phương thức bất cứ khi nào chúng tôi cần dữ liệu đó và chỉ cần chấp nhận chi phí, nhưng nếu hiệu suất là mối quan tâm, chúng tôi có một số tùy chọn.

Đối với một, chúng ta có thể gán dữ liệu cho một biến và sử dụng lại nó, điều này sẽ đẩy nhanh quá trình. Mặc dù là một giải pháp khả thi, nhưng việc quản lý biến đó theo cách thủ công có thể nhanh chóng trở nên tẻ nhạt.

Nhưng, điều gì sẽ xảy ra nếu thay vào đó, phương thức thực hiện "công việc chậm" này có thể xử lý biến đó cho chúng ta? Điều này sẽ cho phép chúng ta gọi phương thức theo cùng một cách, nhưng phương thức này sẽ lưu và sử dụng lại dữ liệu. Đây chính xác là những gì ghi nhớ thực hiện.

Nói một cách đơn giản, ghi nhớ là lưu giá trị trả về của một phương thức để nó không phải tính lại mỗi lần. Như với tất cả bộ nhớ đệm, bạn đang sử dụng bộ nhớ theo thời gian một cách hiệu quả (tức là bạn loại bỏ bộ nhớ cần thiết để lưu trữ giá trị, nhưng bạn tiết kiệm thời gian cần thiết để xử lý phương pháp).

Cách ghi nhớ giá trị

Ruby cung cấp một thành ngữ rất rõ ràng để ghi nhớ các giá trị bằng toán tử hoặc-bằng:||= . Điều này sử dụng một OR logic (|| ) giữa các giá trị bên trái và bên phải, sau đó gán kết quả cho biến ở bên trái. Thực tế:

value ||= expensive_method(123)

#logically equivalent to:
value = (value || expensive_method(123))

Tính năng Ghi nhớ hoạt động như thế nào

Để hiểu cách này hoạt động, bạn cần nắm được hai khái niệm:giá trị "falsey" và đánh giá lười biếng. Trước tiên, chúng ta sẽ bắt đầu với sự thật-giả.

Truthy và Falsey

Ruby (giống như hầu hết các ngôn ngữ khác) có các từ khóa tích hợp cho boolean truefalse các giá trị. Chúng hoạt động chính xác như bạn mong đợi:

if true
  #we always run this
end

if false
  # this will never run
end

Tuy nhiên, Ruby (và nhiều ngôn ngữ khác) cũng có khái niệm về giá trị "true" và "falsey". Điều này có nghĩa là các giá trị có thể được coi là "như thể" chúng là true hoặc false . Trong Ruby only nilfalse là giả dối. Tất cả các giá trị khác (bao gồm cả giá trị 0) được coi là true (lưu ý:các ngôn ngữ khác đưa ra các lựa chọn khác nhau. Ví dụ C coi số 0 là false ). Sử dụng lại ví dụ của chúng tôi ở trên, chúng tôi cũng có thể viết:

value = "abc123" # a string
if value
  # we always run this
end

value = nil
if value
  # this will never run
end

Đánh giá Lười biếng

Đánh giá lười biếng là một hình thức tối ưu hóa rất phổ biến trong các ngôn ngữ lập trình. Nó cho phép chương trình bỏ qua các thao tác không cần thiết.

Toán tử OR logic (|| ) trả về true nếu bên trái hoặc bên phải là true. Điều này có nghĩa là nếu đối số bên trái là đúng thì không có lý do gì để đánh giá bên phải vì chúng ta đã biết kết quả sẽ đúng.>

def logical_or (lhs, rhs)
  return lhs if lhs

  rhs
end

Nếu lhsrhs là các hàm (ví dụ:lamdas) thì bạn có thể thấy rhs sẽ chỉ thực thi nếu lhs là giả dối.

Hoặc-Bằng

Kết hợp hai khái niệm giá trị true-falsey và đánh giá lười biếng này cho chúng ta thấy điều gì ||= nhà điều hành đang thực hiện:

value #defaults to nil
value ||= "test"
value ||= "blah"
puts value
=> test

Chúng tôi bắt đầu với giá trị là nil bởi vì nó không được khởi tạo. Tiếp theo, chúng ta gặp ||= đầu tiên của chúng ta nhà điều hành. value là sai ở giai đoạn này, vì vậy chúng tôi đánh giá phía bên phải ("test" ) và gán kết quả cho giá trị value . Bây giờ chúng tôi nhấn ||= thứ hai toán tử, nhưng lần này là value là trung thực vì nó có giá trị "test" . Chúng tôi bỏ qua phần đánh giá phía bên phải và tiếp tục với giá trị value không đụng chạm.

Quyết định Khi nào Sử dụng Ghi nhớ

Khi sử dụng ghi nhớ, có một số câu hỏi chúng ta cần tự hỏi:Giá trị được truy cập thường xuyên như thế nào? Nguyên nhân nào khiến nó thay đổi? Nó thay đổi bao lâu một lần?

Nếu giá trị chỉ được truy cập một lần thì việc lưu giá trị vào bộ nhớ đệm sẽ không hữu ích lắm, giá trị được truy cập thường xuyên hơn thì chúng ta càng có thể nhận được nhiều lợi ích từ việc lưu vào bộ nhớ đệm.

Khi nói đến nguyên nhân khiến nó thay đổi, chúng ta cần xem xét những giá trị nào được sử dụng trong phương thức. Nó có đối số không? Nếu vậy, việc ghi nhớ có lẽ cần phải tính đến điều này. Cá nhân tôi thích sử dụng viên ngọc ghi nhớ cho việc này vì nó xử lý các đối số cho bạn.

Cuối cùng, chúng ta cần xem xét tần suất thay đổi của giá trị. Có các biến cá thể khiến nó thay đổi không? Chúng ta có cần xóa giá trị được lưu trong bộ nhớ cache khi chúng thay đổi không? Giá trị nên được lưu vào bộ nhớ đệm ở cấp đối tượng hay cấp lớp?

Để trả lời những câu hỏi này, hãy xem một ví dụ đơn giản và thực hiện từng bước để đưa ra quyết định:

class ProfitLossReport
  def initialize(title, expenses, invoices)
    @expenses = expenses
    @invoices = invoices
    @title = title
  end

  def title
    "#{@title} #{Time.current}"
  end

  def cost
    @expenses.sum(:amount)
  end

  def revenue
    @invoices.sum(:amount)
  end

  def profit
    revenue - cost
  end

  def average_profit(months)
    profit / months.to_f
  end
end

Mã gọi điện không được hiển thị ở đây, nhưng có thể đoán được rằng title phương thức có thể chỉ được gọi một lần, nó cũng sử dụng Time.current vì vậy việc ghi nhớ nó có thể có nghĩa là giá trị ngay lập tức trở nên cũ.

revenuecost các phương thức được đánh nhiều lần ngay cả trong lớp này. Cho rằng cả hai đều yêu cầu đánh vào cơ sở dữ liệu, chúng sẽ là những ứng cử viên chính cho việc ghi nhớ nếu hiệu suất trở thành vấn đề. Giả sử chúng tôi ghi nhớ những thứ này, thì profit không cần phải ghi nhớ, nếu không, chúng tôi chỉ thêm bộ nhớ đệm vào bộ nhớ đệm để đạt được lợi nhuận tối thiểu.

Cuối cùng, chúng tôi có average_profit . Giá trị ở đây phụ thuộc vào đối số vì vậy việc ghi nhớ của chúng tôi phải tính đến điều này. Đối với một trường hợp đơn giản như revenue chúng ta chỉ có thể làm điều này:

def revenue
  @revenue ||= @invoices.sum(:amount)
end

Đối với average_profit tuy nhiên, chúng tôi cần một giá trị khác nhau cho mỗi đối số được chuyển vào. Chúng tôi có thể sử dụng trình ghi nhớ cho việc này, nhưng để rõ ràng hơn, chúng tôi sẽ đưa ra giải pháp của riêng mình tại đây:

def average_profit(months)
  @average_profit ||= {}
  @average_profit[months] ||= profit / months.to_f
end

Ở đây, chúng tôi đang sử dụng hàm băm để theo dõi các giá trị được tính toán của chúng tôi. Trước tiên, chúng tôi đảm bảo @average_profit đã được khởi tạo, sau đó chúng tôi sử dụng đối số được truyền vào là khóa băm.

Ghi nhớ ở Cấp lớp hoặc Cấp phiên bản

Hầu hết thời gian ghi nhớ được thực hiện ở cấp cá thể, nghĩa là chúng ta sử dụng một biến thể hiện để giữ giá trị được tính toán. Điều này cũng có nghĩa là bất cứ khi nào chúng ta tạo một phiên bản mới của đối tượng, nó không được hưởng lợi từ giá trị "được lưu trong bộ nhớ cache". Đây là một minh họa rất đơn giản:

class MemoizedDemo
  def value
    @value ||= computed_value
  end

  def computed_value
    puts "Crunching Numbers"
    rand(100)
  end
end

Sử dụng đối tượng này, chúng ta có thể thấy kết quả:

demo = MemoizedDemo.new
=> #<MemoizedDemo:0x00007f95e5d9d398>

demo.value
Crunching Numbers
=> 19

demo.value
=> 19

MemoizedDemo.new.value
Crunching Numbers
=> 93

Chúng ta có thể thay đổi điều này bằng cách sử dụng một biến cấp lớp (với @@ ) cho giá trị được ghi nhớ của chúng tôi:

  def value
    @@value ||= computed_value
  end

Kết quả sau đó trở thành:

demo = MemoizedDemo.new
=> #<MemoizedDemo:0x00007f95e5d9d398>
demo.value
Crunching Numbers
=> 60
demo.value
=> 60
MemoizedDemo.new.value
=> 60

Bạn có thể không muốn ghi nhớ cấp lớp thường xuyên, nhưng nó là một tùy chọn. Tuy nhiên, nếu bạn cần một giá trị được lưu vào bộ nhớ đệm ở cấp độ này, có lẽ bạn nên xem xét bộ nhớ đệm của giá trị đó bằng một cửa hàng bên ngoài như Redis hoặc memcached.

Các trường hợp sử dụng ghi nhớ phổ biến trong ứng dụng Ruby on Rails

Trong các ứng dụng Rails, trường hợp sử dụng phổ biến nhất mà tôi thấy đối với ghi nhớ là giảm các lệnh gọi cơ sở dữ liệu, đặc biệt khi một giá trị sẽ không thay đổi trong một yêu cầu duy nhất. Phương thức "Finder" để tìm kiếm bản ghi trong bộ điều khiển là một ví dụ điển hình về kiểu gọi cơ sở dữ liệu này, chẳng hạn như:

  def current_user
    @current_user ||= User.find(params[:user_id])
  end

Một nơi phổ biến khác là nếu bạn sử dụng một số loại kiến ​​trúc kiểu trang trí / người trình bày / chế độ xem-mô hình để hiển thị các khung nhìn. Các phương thức trong các đối tượng này thường có ứng cử viên tốt để ghi nhớ vì chúng chỉ tồn tại trong thời gian tồn tại của yêu cầu, dữ liệu thường không bị thay đổi và một số phương thức có thể bị đánh nhiều lần khi hiển thị các khung nhìn.

Gotchas ghi nhớ

Một trong những lỗi lớn nhất là ghi nhớ mọi thứ khi nó không thực sự cần thiết. Những thứ như nội suy chuỗi có thể trông giống như những ứng cử viên dễ dàng cho việc ghi nhớ, nhưng trên thực tế, chúng không có khả năng gây ra bất kỳ tác động đáng chú ý nào đến hiệu suất trang web của bạn (tất nhiên trừ khi bạn đang sử dụng chuỗi đặc biệt lớn hoặc thực hiện một số lượng lớn thao tác chuỗi), ví dụ:

  def title
    # memoization here is not going to have much of an impact on our performance
    @title ||= "#{@object.published_at} - #{@object.title}"
  end

Một điều khác cần lưu ý là sự vô hiệu hóa bộ nhớ cache của người bạn cũ của chúng tôi, đặc biệt nếu giá trị được ghi nhớ của bạn phụ thuộc vào trạng thái của đối tượng. Một cách để giúp ngăn chặn điều này là lưu vào bộ nhớ cache ở mức thấp nhất mà bạn có thể. Thay vì lưu vào bộ nhớ đệm, một phương pháp tính toán a + b tốt hơn nên lưu a vào bộ nhớ đệm và b các phương pháp riêng lẻ.

  # Instead of this
  def profit
    # anyone else calling 'revenue' or 'losses' is not benefitting from the caching here
    # and what happens if the 'revenue' or 'losses' value changes, will we remember to update profit?
    @profit ||= (revenue - losses)
  end

  # try this
  def profit
    # no longer cached, but subtraction is a fast calculation
    revenue - losses
  end

  def revenue
    @revenue ||= Invoice.all.sum(:amount)
  end

  def losses
    @losses ||= Purchase.all.sum(:amount)
  end

Điểm mấu chốt cuối cùng là do cách đánh giá lười biếng hoạt động - bạn sẽ phải làm điều gì đó tùy chỉnh hơn một chút nếu bạn cần ghi nhớ một giá trị falsey (tức là nil hoặc false), như ||= idiom sẽ luôn thực thi phía bên phải nếu giá trị đã lưu của bạn là falsey. Theo kinh nghiệm của tôi, thường thì bạn không cần phải lưu các giá trị này vào bộ nhớ cache, nhưng nếu có, bạn có thể cần thêm cờ boolean để cho biết nó đã được tính toán hoặc sử dụng cơ chế lưu vào bộ nhớ đệm khác.

  def last_post
    # if the user has no posts, we will hit the database every time this method is called
    @last_post ||= Post.where(user: current_user).order_by(created_at: :desc).first
  end

  # As a simple workaround we could do something like:
  def last_post
    return @last_post if @last_post_checked

    @last_post_checked = true
    @last_post ||= Post.where(user: current_user).order_by(created_at: :desc).first
  end

Khi Ghi nhớ không đủ

Ghi nhớ có thể là một cách rẻ và hiệu quả để cải thiện hiệu suất trong các phần của ứng dụng của bạn, nhưng không phải là không có nhược điểm của nó. Một trong những điều quan trọng là sự bền bỉ; đối với ghi nhớ cấp cá thể thông thường, giá trị chỉ được lưu cho một đối tượng cụ thể đó. Điều này làm cho tính năng ghi nhớ trở nên tuyệt vời để lưu các giá trị cho vòng đời của một yêu cầu web, nhưng không mang lại cho bạn lợi ích đầy đủ của bộ nhớ đệm nếu bạn có các giá trị giống nhau cho nhiều yêu cầu và đang được tính toán lại mỗi lần.

Ghi nhớ cấp độ lớp có thể giúp được việc này, nhưng việc quản lý việc vô hiệu hóa bộ nhớ cache trở nên khó khăn hơn. Chưa kể rằng nếu máy chủ của bạn khởi động lại, các giá trị đã lưu trong bộ nhớ cache sẽ bị mất và chúng không thể được chia sẻ giữa nhiều máy chủ web.

Trong số tiếp theo của loạt bài về bộ nhớ đệm này, chúng ta sẽ xem xét giải pháp của Rails cho những vấn đề này - bộ nhớ đệm cấp thấp. Cho phép bạn lưu trữ các giá trị vào bộ nhớ cache vào một cửa hàng bên ngoài có thể được chia sẻ giữa các máy chủ và quản lý việc mất hiệu lực bộ nhớ cache với thời gian hết hạn và khóa bộ nhớ cache động.