LRU缓存奇怪

  • 本文关键字:缓存 LRU python hash
  • 更新时间 :
  • 英文 :


我遇到了一些无法解释的行为。

我有一些昂贵的函数被反复调用;我用@lru_cache(None)装饰了它们,以帮助加快速度。在那之后,我的跑步时间仍然很慢,所以我有点困惑。

然后我意识到其中一些函数有自定义对象作为参数。我的理解是,默认情况下,任何自定义对象的哈希都是基于它的ID。因此,我的理论是,尽管这些参数包含相同的数据,但其中一些昂贵的函数正在重新评估。我的对象只用于对不可变的数据进行分组,所以我很乐意在这些对象中的数据相同的地方查找缓存值。

因此,基于我对lru_cache函数的理解,我在我的对象中添加了一个__hash__方法,只是对初学者来说做了一些非常粗糙的事情:

def __hash__(self):
return hash(str(self.__dict__))

所以我的理论是,我的程序现在应该更快了,因为缓存现在将在一些昂贵的函数上进行,而以前没有。

令我沮丧的是,我的程序慢得多;它可能被卡在了某个地方,因为我甚至没有耐心让它结束。对于上下文,在没有自定义__hash__方法的情况下,测试用例在大约16秒内运行;在添加CCD_ 5方法之后,相同的测试用例在大约10分钟后仍在运行。

我对lru_cache的工作原理没有深入的了解,但我已经看过源代码,据我所知,当它遇到这些对象作为参数时,它只会使用我的__hash__函数。基于运行时间的急剧增加,我目前的理论是,这在某种程度上会导致程序卡在某个地方,而不是由于某种原因,缓存查找实际上需要那么长时间。但我看不出为什么会发生这种情况。

这对我来说有点像是在追逐,但我无法想象我是第一个尝试这个的人。有人知道为什么会发生这种事吗?

感谢

编辑:

我运行了一个更小的测试用例来检查程序是否真的正在终止;是的。较小的测试用例在没有自定义__hash__函数的情况下运行了2.5秒,而有了它们则需要40秒。

我必须强调的是,在这两次跑步之间没有任何其他变化。唯一的区别是,我将上面描述的__hash__函数添加到三个类中,这三个类围绕我的代码进行了一段旅程。因此,我认为唯一可能的结论是,我的__hash__函数在某种程度上比lru_cache使用的默认函数慢得多。也就是说,除非实现自定义__hash__函数有其他我不知道的(不可见的)成本。

我仍然无法解释这一点。这些对象相当大,包含大量数据,因此str(self.__dict__)将是一个相当长的字符串(可能有数千个字符)。然而,我不认为哈希对于一个更长的字符串来说应该花费更长的时间。也许Python在不同的地方在后台进行了大量的哈希运算,而这一微小的差异可以加起来吗?这对我来说似乎很牵强,但似乎没有太多选择——我能看到的唯一选择是与lru_cache逻辑的一些奇怪交互,这导致了速度的大幅下降。我会继续做实验,但希望有人知道答案!

编辑2:

我遵循了Samwise的建议,并以__hash__函数为基准,它似乎确实慢了很多,考虑到调用的数量,我可以相信这就是我问题的全部原因。我猜self.__dict__部分是瓶颈,但我对此的直觉并不是迄今为止最好的记录。

这仍然给我留下了试图加快代码速度的问题,但至少我知道现在发生了什么。

编辑3:

对于将来遇到这个问题的其他人,我决定在对象的初始化程序中预先计算一个哈希值,并在__hash__函数中返回,这大大加快了速度。此解决方案取决于创建后未发生突变的对象。

这个问题的答案很简单——str(self.__dict__)实际上在每个函数调用上运行都很慢。我不知道为什么我一开始就没有想到这一点。

最终,我决定在类中添加一个属性_hash,并在初始化新对象结束时将其设置为str(self.__dict__)。然后,在我的自定义__hash__方法中,我只需要获取_hash的值,这样现在lru_cache将适用于具有参数的函数,这些参数假定了我的对象的类型,而不必为每个函数调用调用str(self.__dict__)

我应该明确一点,这只有在假设对象在初始化时定义了整个状态,并且在其生命周期内不会发生变化的情况下才有效——如果发生了变化,那么哈希就会过期——你最终会从缓存中得到不合适的命中。

最新更新