3.9 持续的缓存
在离开自动的记忆术的主题之前,将浏览一些外围的技术。将看到一个函数如何被带记忆的版本替代,后者在缓存里存储了返回值,缓存就仅是一个散列变量。
在Perl里,可以使用tie操作符把一个散列变量关联到一个磁盘上的数据库,那么存储在散列的数据会自动写到磁盘上,从散列取回数据实际上是从磁盘取回。把这个功能增加到memoize函数是简单的:
use DB_File;
sub memoize {
my ($func, $keygen, $file) = @_;
my %cache;
if (defined $file) {
tie %cache => 'DB_File', $file, O_RDWR|O_CREAT, 0666
or die "Couldn't access cache file $file: $!; aborting";
}
my $stub = sub {
my $key = $keygen ? $keygen->(@_) : join ',', @_;
$cache{$key} = $func->(@_) unless exists $cache{$key};
return $cache{$key};
};
return $stub;
}
此处增加了一个可选的第三个形参,它是将要接受缓存的数据的磁盘上的文件名。如果提供了,就用tie把散列关联到文件上。注意,如果不使用这个特性,就几乎不付出什么代价。在调用memoize()时会有一次单一的defined()测试。
当缓存散列以这种方式被关联到一个磁盘文件时,缓存就变成持续的了。程序一次运行时在缓存中存储的数据在程序退出以后还保留在文件里,而且在程序下次运行时依然对函数有效。程序逐步把函数替换成磁盘上的一个搜索表。程序员的代价几乎是零,因为不必改变原始函数的任何代码。
如果厌倦了等待查询表完全被占据,就可以强制它。也可以写个小程序,它仅是反复地以不同的参数调用带记忆的函数。可以在周五下午启动它然后回家过周末。当周一回来时,持续的缓存里将有函数预先计算出来的值。当运行实际应用时,对带记忆的函数的调用将几乎是立即返回,因为值已经保存在数据库里了。
同样的,这也可能不是胜利。记住,从记忆术获得的加速由公式hfK决定,K是管理缓存的开销。如果K足够大,它将淹没从公式的hf部分赚到的,就像在3.6节的sub { $_[0] * $_[0] }例子中一样。当把缓存数据存储在一个磁盘文件里时,开销K会数倍于一般情况,因为程序将不得不进行一次操作系统请求以查看磁盘数据库。
另一种也是更加灵活的界面是允许memoize()的用户提供他们自己的关联散列:
sub memoize {
my ($func, $keygen, $cache) = @_;
$cache = {} unless defined $cache;
my $stub = sub {
my $key = $keygen ? $keygen->(@_) : join ',', @_;
$cache->{$key} = $func->(@_) unless exists $cache->{$key};
return $cache->{$key};
};
return $stub;
}
这允许用户提供一个以他们喜欢的DBM实现关联到一个磁盘文件的缓存,甚至可以是从没听说过的。他们也可以传递一个普通的散列,那将允许他们在他们希望的时候能清除缓存或让旧的值到期。