在追求编写高性能代码的道路上,我们拥有许多技巧可以使用,其中之一就是记忆化。记忆化简单来说就是存储昂贵函数调用的结果,以便在需要时避免重复调用。
本文将带你了解在实现一个 Ruby 记忆化 gem 的过程中获得的所有经验教训。
**局部变量**
最简单的记忆值方法是使用局部变量的概念。在下面的示例中,`calc_base_price` 函数被调用了两次:
```ruby
def total_price
vat = calc_base_price * VAT_RATE
calc_base_price + vat
end
```
如果 `calc_base_price` 函数很昂贵,例如由于数据库查询,那么将结果存储在局部变量中会很有意义。在下面的代码片段中,基础价格存储在一个名为 `base_price` 的局部变量中:
```ruby
def total_price
base_price = calc_base_price
vat = base_price * VAT_RATE
base_price + vat
end
```
**记忆化操作符**
Ruby 提供了一个操作符 `||=`,有时称为记忆化操作符。以下是在 Ruby 中使用的示例:
```ruby
def base_price
@base_price ||= calc_base_price
end
```
当执行此方法时,Ruby 会检查实例变量 `@base_price` 是否已定义,并且其值是否为真值(即不为 nil 且不为 false)。如果是,则返回 `@base_price` 的值。
另一方面,如果 `@base_price` 未定义或设置为 nil 或 false,则它将调用 `calc_base_price` 方法,将其返回值存储在 `@base_price` 实例变量中,并将该值用作整个表达式的值(因此也用作 `base_price` 的返回值)。
**注意 false 和 nil**
`false` 和 `nil` 值可能会导致使用 `||=` 操作符进行记忆化时出现意外情况。这是因为当记忆化变量未定义或记忆化值为假值时,`||=` 操作符会计算右侧表达式。
**参数感知记忆化**
任何关于记忆化主题的文献都不可避免地会将斐波那契数列作为记忆化特别有用的示例。这是一个返回斐波那契数列中给定元素的 Ruby 实现:
```ruby
def fib(n)
case n
when 0
0
when 1
1
else
fib(n - 1) + fib(n - 2)
end
end
```
**记忆化 DSL**
Ruby 的一项重要优势是其元编程能力。一个很好的用例是通过在方法定义后附加调用 `memoize` 来自动化记忆化:
```ruby
def fib(n)
# [snip]
end
memoize :fib
```
**记忆化要求**
在继续之前,我想解决以下问题:在什么情况下可以安全地应用记忆化?记忆化并不是一种可以喷涂到代码上使其变快的技术,需要考虑一些限制才能使记忆化代码正常工作。
**在冻结对象上记忆化**
此实现有一个特别的问题。尝试在冻结对象上使用记忆化会导致失败。
**内存高效记忆化**
记忆化实现永远不会释放内存。这意味着内存使用量可能会持续增长。
**弱引用**
Ruby 默认提供了一种潜在的解决方案:弱引用。解释弱引用是什么方法是通过将它们与常规引用(也称为强引用)进行比较来完成的。
**软引用**
存在一种比强引用弱但比弱引用强的引用类型。这些被称为软引用。
**重新审视冻结**
处理冻结对象有一种更好的方法。它是记忆化 DSL 初始实现的一个轻微变体。弱引用和软引用都不再使用了。
**支持关键字参数**
从 3.0 版本开始,Ruby 支持关键字参数。这些参数不同于常规参数(也称为位置参数)。
**支持块**
在 Ruby 中,方法可以接受三种类型的内容。我们已经解决了位置参数和关键字参数,但还有一种类型 Ruby 方法可以接受:块。
**线程安全记忆化**
在多线程环境中,记忆化的当前实现可能存在一些问题。
**指标**
记忆化函数必须与其非记忆化对应函数具有完全相同的行为,除了执行速度的差异。这是使用记忆化的契约。但如果记忆化方法的行为与非记忆化对应函数完全相同,那么如何检查记忆化是否有用呢?
**结论**
希望前面几千字能够说明记忆化实现起来很棘手。
请使用经过实战检验的库,而不是自己编写记忆化实现。我推荐使用 memo_wise Ruby gem。这是我所知道的最好的 Ruby 记忆化库。