Hôm nay tôi muốn nói về một trong những kỹ thuật yêu thích của tôi để cải thiện hiệu suất. Đó là một nguồn tạo ra những chiến thắng hiệu suất nhỏ dễ dàng mà cuối cùng sẽ cộng lại và chỉ đôi khi làm giảm ứng dụng của bạn thành một đống gạch vụn âm ỉ. Chỉ thỉnh thoảng thôi.
Kỹ thuật này được gọi là "ghi nhớ". Mặc dù từ khoa học máy tính $ 10, nó chỉ đơn giản có nghĩa là, thay vì thực hiện cùng một công việc mỗi khi bạn gọi một phương thức, bạn lưu giá trị trả về vào một biến và sử dụng thay thế đó.
Đây là những gì nó trông giống như trong mã giả:
def my_method
@memo = <work> if @memo is undefined
return @memo
end
Và đây là cách bạn làm điều đó trong Ruby. Đây là cách tiếp cận mạnh mẽ nhất, nhưng nó khá dài dòng. Có những cách tiếp cận khác, ngắn gọn hơn mà chúng ta sẽ thảo luận sau.
class MyClass
def my_method
unless defined?(@my_method)
@my_method = begin
# Do your calculation, database query
# or other long-running thing here.
end
end
@my_method
end
end
Đoạn mã trên thực hiện ba điều:
- Kiểm tra xem có biến phiên bản nào có tên
@my_method
không . - Nếu có, nó hoạt động một số và lưu kết quả trong
@my_method
. - Nó trả về
@my_method
Đừng nhầm lẫn bởi thực tế là chúng ta có cả một phương thức và một biến cá thể có tên là my_method
. Tôi có thể đặt tên cho biến của mình là bất kỳ thứ gì, nhưng quy ước là đặt tên nó theo phương thức đang được ghi nhớ.
Một phiên bản tốc ký
Một vấn đề với đoạn mã trên là nó hơi cồng kềnh. Do đó, nhiều khả năng bạn sẽ thấy một phiên bản tốc ký gần như thực hiện điều tương tự:
class MyClass
def my_method1
@my_method1 ||= some_long_calculation
end
def my_method2
@my_method2 ||= begin
# The begin-end block lets you easily
# use multiple lines of code here.
end
end
end
Cả hai đều sử dụng a ||= b
của Ruby toán tử, viết tắt của a || (a = b)
, bản thân nó ít nhiều là viết tắt của:
# You wouldn't use return like this in real life.
# I'm just using it to express to beginners the idea
# that the conditional evaluates to whatever winds up in `a`.
if a
return a
else
a = b
return a
end
Nguồn lỗi
Nếu để ý kỹ, bạn có thể nhận thấy rằng các phiên bản tốc ký đánh giá "tính xác thực" của biến ghi nhớ thay vì kiểm tra sự tồn tại của nó. Đây là nguồn gốc của một trong những hạn chế lớn của phiên bản tốc ký:nó không bao giờ ghi nhớ nil
hoặc false
.
Có rất nhiều trường hợp sử dụng mà điều này không quan trọng. Nhưng đó là một trong những sự thật khó chịu mà bạn phải ghi nhớ trong đầu mỗi khi ghi nhớ.
Phương thức ghi nhớ có đối số
Cho đến nay, chúng tôi chỉ giải quyết việc ghi nhớ các giá trị đơn lẻ. Nhưng không nhiều hàm trả về cùng một kết quả mọi lúc. Chúng ta hãy xem xét một cuộc phỏng vấn kỹ thuật yêu thích cũ:dãy Fibonacci.
Bạn có thể tính toán đệ quy dãy Fibonacci trong Ruby như sau:
class Fibonacci
def self.calculate(n)
return n if n == 0 || n == 1
calculate(n - 1) + calculate(n - 2)
end
end
Fibonacci.calculate(10) # => 55
Vấn đề với việc triển khai này là nó không hiệu quả. Để chứng minh điều này, hãy thêm một print
câu lệnh để xem giá trị của n
.
class Fibonacci
def self.calculate(n)
print "#{ n } "
return n if n == 0 || n == 1
calculate(n - 1) + calculate(n - 2)
end
end
Fibonacci.calculate(4)
# Outputs: 4 3 2 1 0 1 2 1 0
Như bạn có thể thấy, calculate
đang được gọi lặp lại với nhiều giá trị giống nhau của n
. Trên thực tế, số lượng cuộc gọi đến calculate
sẽ phát triển theo cấp số nhân với n
.
Một cách giải quyết vấn đề này là ghi nhớ kết quả của calculate
. Làm như vậy không khác nhiều so với các ví dụ ghi nhớ khác mà chúng tôi đã đề cập.
class Fibonacci
def self.calculate(n)
@calculate ||= {}
@calculate[n] ||= begin
print "#{ n } "
if n == 0 || n == 1
n
else
calculate(n - 1) + calculate(n - 2)
end
end
end
end
Fibonacci.calculate(4)
# Outputs: 4 3 2 1 0
Bây giờ chúng ta đã ghi nhớ calculate
, số lượng cuộc gọi không còn tăng theo cấp số nhân với n
.
Fibonacci.calculate(20)
# Outputs: 20 19 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
Vô hiệu
Ghi nhớ rất giống với bộ nhớ đệm, ngoại trừ việc các kết quả được ghi nhớ không tự động hết hạn và thường không có cách dễ dàng để xóa chúng sau khi thiết lập.
Đối với các trường hợp sử dụng như trình tạo chuỗi Fibonacci, điều này hầu như không quan trọng. Fibonacci.calculate(10)
sẽ luôn trả về cùng một kết quả. Nhưng trong các trường hợp sử dụng khác, nó có vấn đề.
Ví dụ:bạn có thể thấy mã như thế này:
# Not the best idea
class User
def full_name
@full_name ||= [first_name, last_name].join(" ")
end
end
Cá nhân tôi sẽ không sử dụng ghi nhớ ở đây vì nếu họ hoặc tên được thay đổi, tên đầy đủ có thể không được cập nhật.
Một nơi mà bạn có thể lỏng lẻo hơn một chút là bên trong bộ điều khiển Rails. Rất thường thấy mã như thế này:
class ApplicationController
def current_user
@current_user ||= User.find(...)
end
end
Điều này không sao, vì phiên bản controller bị hủy sau mỗi lần yêu cầu web. Không chắc rằng người dùng hiện đang đăng nhập sẽ thay đổi trong bất kỳ yêu cầu bình thường nào.
Khi xử lý các kết nối phát trực tuyến như ActionCable, bạn có thể cần phải cẩn thận hơn. Tôi không biết. Tôi chưa bao giờ sử dụng nó.
Lạm dụng
Cuối cùng, tôi cảm thấy rằng tôi nên chỉ ra rằng giống như bất cứ điều gì, có thể đưa việc ghi nhớ đi quá xa. Đó là một kỹ thuật thực sự chỉ nên áp dụng cho các hoạt động đắt tiền mà sẽ không bao giờ thay đổi trong suốt thời gian tồn tại của biến ghi nhớ.