在缓存失效:为什么它很难?

2021-04-04 23:52:18

许多人必须听到这句话(菲尔卡尔顿)多次:计算机科学只有两个艰难的东西:缓存失效和命名的东西。两天前,尼克泰尔尼再次在他的帖子“命名事物”中提到了它。由于他说他不确定缓存失效意味着什么,而且在这里有一个微小的体验,我想写这个短篇大论来解释为什么缓存失效来自我的经验。

首先,缓存的主要目的是速度。基本思想很简单:如果你知道你要计算同样的事情,你可能只是加载从上一个运行中保存的结果,并这次跳过计算。这里有两个关键字:“同样的事情”和“已保存的结果”。后者意味着您基本上是交易(更多)存储空间(更少)时间。这是支付缓存支付的价格,也是在使用缓存时要注意的重要事实(即,缓存绝对不是免费的,有时价格可能相当高)​​。

棘手的事情是“同样的事情”。你怎么知道你正在计算同样的事情?这就是所有“缓存失效”就是关于。当事情变得不同时,您必须使缓存失效,并再次执行(大概耗时)计算。

在不考虑失效的情况下实现缓存通常很简单。以下是将正常功能转到支持缓存的函数的一个简单示例:

问候=函数(人){switch(person,nick = {#fast smart sys.sleep(3);'您好!'},yihui = {#慢速和害羞的sys.sleep(15) ;'你好!'})}

我使用sys.sleep()只是假装函数是耗时的。在问候()函数中,尼克很快,会说“你好”,y辉很慢,说中文。如果我们必须多次调用这个函数,如果我们可以保存尼克和yihui说什么,无需等待。我们需要一个数据库来存储他们的单词,以及键来检索结果。

hold_cache = local({#仅限缓存数据库(仅限内存)database = list()函数(person){res = database [person]]#如果在数据库中找到,只需返回缓存结果(人物%在%名称(数据库))返回(res)#未找到?计算和保存它res = break(person)数据库[​​[person]]<<< - res#注意双箭头这里} })

第一次运行此功能时,它将很慢,但第二次,它将是即时的。

玩具示例显示了实现缓存的基本思想:将输入转换为键,如果存在,请使用此键在缓存数据库中检索输出,否则将计算和将输出保存到数据库中的键。

当然,玩具例子通常不能代表现实。如果尼克去日本,他可能会说日语。当yihui在美国时,他应该说英语。我们需要在这些情况下更新缓存数据库(使先前保存的结果无效)。

现在让我谈谈KNITR的缓存中的一个真实例子,这应该听起来与日本的尼克(或美国yihui)的情况相似。对于那些关心技术细节的人,KnitR缓存使用这些代码行并使缓存无效。

Knitr的基本思想是,如果您没有修改代码块(例如,没有修改块选项或代码),则将从上一个运行加载结果。代码块的键是块选项和块内容(代码)的MD5哈希(VIA DIGEST :: DIGEST())。每当修改块选项或代码时,哈希将会更改,缓存将无效。

我听说过不开心的用户诅咒剑的缓存。有些人认为它太敏感了,有些人认为这是愚蠢的。例如,当您在代码块中添加一个注释中的空格时,是否应对缓存无效吗?修改评论肯定不会影响所有计算(但如果您通过Echo = True显示输出中的代码,则可能会更改文本输出),但MD5散列将改变。 1

然后一个例子来解释为什么人们认为knitr的缓存是愚蠢的:如果你在代码块中读取外部CSV文件,knitr不知道您是否已修改数据文件。如果您碰巧更新了数据文件,则如果您没有修改块选项或代码,则knitt不会重新读取它。缓存密钥不依赖于外部文件。在这种情况下,您必须将缓存与外部文件显式关联,例如,2

由于块选项Cache.Extra与CSV文件关联,因此在更改文件时缓存将无效(因为缓存键将不同)。

另一个例子是使用从上一个代码块创建的变量的一个代码块。当变量在上一个块中更新时,此块的缓存也应该无效。这导致代码块的依赖关系的主题,这可能是复杂的,但有一些辅助函数,如knitr :: dep_prev()和knitt :: dep_auto(),使其更容易。

当代码块非常耗时时,knitr应该更保守(除非存在严重变化,否则不要使昂贵的缓存无效)。当代码块仅适度慢慢(例如,10或20秒)时,缓存可能应该更敏感。

棘手的事情是,很难找到平衡。任何一个方向都可以冒犯用户。

我上面说的是,支付缓存的明显价格是存储(在内存或磁盘上)。但是,为了让您对您的缓存工作,存在隐藏的成本。也就是说,了解缓存的成本。这类似于我们日常生活中的情况:我们可能会花费大量的时间和精力来挽救一些钱。我们只能看到我们保存的钱,但忽略了时间和情感的成本。如果您没有分析两种成本,那么您挽救的金额可能并不值得。

如果您不完全了解缓存的工作和无效的条件,缓存可能太敏感或愚蠢,并且可能不适合您。有些用户可能能够快速理解它,有些用户可能不会。如果您想要速度,您可以先更好地了解交通规则,否则可能会被拉出。

KNITR的缓存的完整文档是KNITR书籍“带有R和KNITR(2nd)的动态文件”。如果您没有本书,则KNITR的网站上有一个页面包含更多信息。

当他说这些话时,我不知道是菲尔卡尔顿的思想,但以上是我对缓存的经验。我经常给用户的最终建议是,如果你觉得Knitt的缓存太复杂,它完全可以使用这样更简单的缓存机制:

if(file.exists('结果.rds')){res = readrds('结果.rds')} else {res = compute_it()#耗时的函数saverds( Res,'结果.rds')}

在这种情况下,您清楚地了解您的缓存工作原理。无效缓存的唯一方法是删除结果。它根本不再硬。如果您更喜欢此机制,您可以考虑使用Xfun :: cache_rds()。

实际上,您可以使用chunk选项cache.comments = false,以防止缓存失效,只有在代码块中仅更改注释时。 echo = false时,这可能是有用的。 ↩

如果您希望缓存依赖于CSV文件的修改时间,您还可以使用File.mtime()而不是工具:: md5sum()。 ↩