《高阶Perl》——3.8 对象方法里的缓存

3.8 对象方法里的缓存

对象方法,它经常不理解地把缓存的值保存在独立的散列里。考虑一个投资银行写的程序里的Investor对象。该对象表现了银行的一个客户:

package Investor;

# Compute total amount currently invested
sub total {
  my $self = shift;
  # ... complex computation performed here ...
  return $total;
}

如果$total不会改变,就可以缓存它,用对象的本身作为缓存散列的键:

# Compute total amount currently invested
{ my %cache;
  sub total {
    my $self = shift;
    return $cache{$self} if exists $cache{$self};
    # ... complex computation performed here ...
    return $cache{$self} = $total;
  }
}

然而,这个技术有一个严重的问题。当用一个对象作为一个散列键时,Perl把它转换成一个字符串。典型的散列键将看上去像Investor=HASH(0x80ef8dc)。十六进制数字是对象的数据实际存放的地址。本质上,这个键对任何两个对象都不一样,即冒着错误的缓存命中的风险,恢复一个对象的总数,却想着它属于另一个不同的对象。在Perl里,这些散列键确实对系统中任何给定的时刻存在的所有对象都不同,但对已回收的对象没有保证。如果一个对象被销毁了而一个新的对象被创建了,那个新的对象恰好存在于旧的对象先前占有的内存地址,那么混淆如下:

# here 90,000 is returned from the cache
$old_total = $old_object->total();
undef $old_object;
$new_object = Investor->new();
$new_total = $new_object->total();

这里要求新的投资者的投资总数。它应该是0,因为投资者是新的。然而,->{total}方法恰好查看缓存,以刚被销毁的$old_object用过的同样的散列键;这个方法看到90000存在那里,并错误地返回它。这个问题可以用一个DESTROY方法解决,它从缓存里删除一个对象的数据,或者在程序里对每个对象关联一个唯一的不再复用的ID数字,并用这个ID数字作为散列键,但是有个更直接的解决方法。

在一个面向对象编程上下文,缓存散列的技术是独特的,因为有个更自然的地方存放缓存的数据:就像数字存在于它的对象自身。一个缓存的总数变为另一个属性,后者可以或不被每个独立的对象携带:

# Compute total amount currently invested
sub total {
  my $self = shift;
  return $self->{cached_total} if exists $self->{cached_total};
  # ... complex computation performed here ...
  return $self->{cached_total} = $total;
}

这里的逻辑和之前的完全一样,唯一的不同是:这个方法为每个对象把总数保存在对象自身,而不是在一个辅助的散列。这避免了辅助散列带来的散列键冲突的问题。

这个技术的另一个优点是,用来保存缓存的总数的存储空间在对象销毁时会自动回收。用辅助的散列,每个缓存的值会一直存在,甚至在其所属的对象都被销毁以后。

最后,把缓存的信息分别存放在每个对象里在对象到期时带来了更灵活的控制。在例子里,total计算某个投资者已经投资的总数。缓存这个总数是合适的,因为投资者不会太频繁地新投入钱。但是永久缓存它可能也不合适。在这个例子里,无论何时一个投资者投资更多的钱,都需要以某种方式告知total函数,缓存的总数不再正确了,必须要丢掉再重新计算。这就称为缓存值的到期(expiring)。

用辅助散列的技术,要是不在缓存散列的作用域里增加一个特殊需求的方法,就无法做到这一点,如下:

# Compute total amount currently invested
{ my %cache;
  sub total {
    my $self = shift;
    return $cache{$self} if exists $cache{$self};
    # ... complex computation performed here ...
    return $cache{$self} = $total;
  }
  sub expire_total {
    my $self = shift;
    delete $cache{$self};
  }
}
sub invest {
  my ($self, $amount, ...) = @_;
  $self->expire_total;
  ...
}

用面向对象技术,则不必增加特别的方法,因为每个方法都可以在它需要的时候直接使缓存的总数到期:

# Compute total amount currently invested
sub total {
  my $self = shift;
  return $self->{cached_total} if exists $self->{cached_total};
  # ... complex computation performed here ...
  return $self->{cached_total} = $total;
}
sub invest {
  my ($self, $amount, ...) = @_;
  delete $self->{cached_total};
  ...
}

对于对象方法,经常需要把每个计算过的值缓存在与它有联系的对象中,而不是一个独立的散列。但之前介绍的momoize函数没有这么做,但是不难建立一个这么做的:

### Code Library: memoize-method
sub memoize_method {
  my ($method, $key) = @_;
  return sub {
    my $self = shift;
    return $self->{$key} if exists $self->{$key};
    return $self->{$key} = $method->($self, @_);
  };
}

$method是一个指向真实方法的引用。$key是缓存的值,将被保存于每个对象的位置的名字。这里返回的存根函数适合用做一个方法。当存根被执行时,它取回其行为被调用的对象,就像别的方法一样,然后它在对象里查找成员数据$key,看是否有值缓存在那里。如果有,存根返回缓存的值;如果没有,它调用真实的方法,把结果缓存入对象,并返回新缓存的结果。
为使用这个,可以写成如下这样:

*Investor::total = memoize_method(\&Investor::total, 'cached_total');
$investor_bob->total;

这把存根安装入符号表以替代原始的方法。或者,可以使用:

$memoized_total = memoize_method(\&Investor::total, 'cached_total');
$investor_bob->$memoized_total;

这两者不完全一样。在前一种情况,所有对->total的调用将使用方法的带记忆的版本,包括来自继承方法的子类的调用。在后一种情况,只有当使用->memoized(...)以明确地要求时,才得到方法的带记忆的版本。

时间: 2024-09-13 04:13:07

《高阶Perl》——3.8 对象方法里的缓存的相关文章

《高阶Perl》——3.2 内联缓存

3.2 内联缓存 给一个函数添加缓存的最直接的方式就是给函数一个私有的散列.在这个例子里,可以使用一个数组代替散列,因为fib()的参数总是一个非负整数.但是一般需要使用一个散列,那么将会看到: ### Code Library: fib-cached # Compute the number of pairs of rabbits alive in month n { my %cache; sub fib { my ($month) = @_; unless (exists $cache{$m

《高阶Perl》——3.9 持续的缓存

3.9 持续的缓存 在离开自动的记忆术的主题之前,将浏览一些外围的技术.将看到一个函数如何被带记忆的版本替代,后者在缓存里存储了返回值,缓存就仅是一个散列变量. 在Perl里,可以使用tie操作符把一个散列变量关联到一个磁盘上的数据库,那么存储在散列的数据会自动写到磁盘上,从散列取回数据实际上是从磁盘取回.把这个功能增加到memoize函数是简单的: use DB_File; sub memoize { my ($func, $keygen, $file) = @_; my %cache; if

《高阶Perl》——导读

前 言 在编程圈子里有一句著名的俗语,一个优秀的Fortran程序员可以用任何语言写Fortran程序.然而,让人悲哀的是,不管他们是否愿意,Fortran程序员用任何语言写Fortran程序.类似地,作为Perl程序员,我们也在用Perl写C程序,不管我们是否愿意.这让人羞愧,因为Perl是一门比C更富有表现力的语言.我们本可以做得更好,以C程序员梦想不到的方式使用Perl,但是我们没有那样做. 怎么会这样呢?Perl的设计初衷是一方面作为C的替代品,另一方面作为UNIX脚本语言(如Bourn

《高阶Perl》——3.7 键的生成

3.7 键的生成 先前的记忆器至少有一个严重的问题.它需要把函数的参数转变成一个散列键,它的做法是使用join: my $key = join ',', @_; 这对只带一个参数的函数有效,也对参数不含逗号的函数有效,包括所有的参数是数字的函数.但是如果函数的参数可能包含逗号,它可能失效,因为以下两个调用却计算出了相同的键: func("x,", "y"); func("x", ",y"); 第一次调用,返回的值将以键&quo

《高阶Perl》——3.4 记忆术

3.4 记忆术 给函数添加缓存技术代码不是非常困难的.且已经看到,需要的改变对任何函数几乎一样.那么,为什么不让计算机做这些呢?若告诉Perl想要使一个函数具有缓存行为.Perl应该能自动地执行所需的转换.这样的给函数添加缓存行为的自动的转换就称为记忆术(memoization),函数则称为带记忆的(memoized). 标准的Memoize模块就是做这个的.如果Memoize模块可用,就完全不必重写fib代码.可以简单地在程序的顶部添加两行: ### Code Library: fib-aut

《高阶Perl》——1.4 层次化数据

1.4 层次化数据 已经介绍的例子显示了递归过程的风采,但是它们遗漏了重要的一点.在介绍汉诺塔问题时,我说过当你想解决的问题可以简化成同样问题的更简形式时,递归是很有用的.但是这样的问题是普遍的这一点可能不清楚. 多数递归的函数是为处理递归的数据结构的建立.一个递归的数据结构就像一个列表.一棵树或者一个堆,它们根据相同数据结构的更小的实例定义而成.最常见的例子可能是文件系统目录结构.文件可以是: 文件可以是一个目录,含有一列文件,其中的一些可以是目录,再含有另一些文件,以此类推.处理这样的结构最

《高阶Perl》——3.6 CAVEATS

3.6 CAVEATS (这是拉丁文的"警告".) 显然,记忆术不适合所有的性能问题.它甚至不适合所有的函数.有几类函数就不应该带记忆. 3.6.1 返回值不依赖参数的函数 记忆术最适合那些返回值只依赖它们的参数的函数.想象一下使时间函数带记忆的愚蠢:第一次你调用它,你将得到时刻,随后的调用将会返回一样的时刻.类似地,想象一个带记忆的随机数生成器是多么固执. 或者想象一个返回值是指示某类成功或失败的函数.你不会希望这类函数是带记忆的,每次被调用都返回同一个值. 然而,记忆术适合一些这样

《高阶Perl》——3.5 MEMOIZE模块

3.5 MEMOIZE模块 本书不是关于Perl模块的内部细节的,而是关于一些Memoize模块内部使用的并和稍后要做的事情有直接联系的技术,所以现在简短地介绍一下. Memoize得到一个函数名(或引用)作为它的参数.它制造一个新的函数,后者维护一个缓存并在其中查找它的参数.如果新的函数在缓存里找到了参数,就返回缓存的值:如果没找到,就调用原始函数,把返回的值保存入缓存,并把它返回给原始的主调者. 制造完这个新的函数,Memoize就把它装入Perl的符号表以代替原始的函数,那样当你认为你在调

《高阶Perl》——3.10 可供选择的记忆术

3.10 可供选择的记忆术 大多数纯函数提供一个缓存的机会.尽管乍一看纯函数很少,它们只以一定频率出现.纯函数特别普遍的地方是在排序中用做比较器函数. Perl内置的sort操作符是通用的,它可以把一列任何种类的数据以程序要求的任何次序排序.默认状态下,它把一列字符串以字母表次序排序,但是程序员可以任意提供一个比较器函数(comparator function),告诉Perl怎样重排sort的参数列表.比较器函数被反复调用,每次带有待排序列表中的两个不同元素,如果两个元素次序正确,就必须返回一个