一种精巧的模式,可在Swift中制作高效缓存功能

Swift是在移动设备上很好地使用的一种语言,其中一些在(非常)有限的计算能力下运行—例如,以较旧的iPhone或Apple Watch为例。

在这种情况下,每个CPU周期都非常重要,精心设计的应用程序将充分利用它们。 减少应用程序占用空间的一种已知方法是通过缓存。

缓存通常被认为是一项棘手的业务。 当将诸如网页之类的外部资源放入缓存时,这是正确的,但是当我们仅考虑缓存纯函数的结果时,事情就容易得多。

如果某个函数始终为给定的输入返回相同的值,而不会产生任何副作用,则认为该函数是纯函数。

缓存纯非递归函数的结果

对于此类功能,功能编程是透明地提高其缓存效率的指定工具。

考虑以下代码:

cached函数将一个函数作为参数,并返回一个新函数,该函数将计算与原始函数完全相同的结果。 但是,一旦第一次计算出一个值,它将被存储在缓存中。

因此,使用相同输入进行的任何后续调用将不再需要执行任何计算以得出正确的结果。

考虑到您正在编写2D游戏的代码,现在可以轻松使用标准三角函数的缓存有效版本:

如果您的游戏经常对相同的值进行三角函数计算,那么现在怀疑这种方法是否会对性能产生重大影响?

递归函数的问题

上面讨论的技术是如此易于使用,以至于人们可能认为它有一个陷阱! 确实有一个限制,或者说是一个限制。

当您使用非递归函数时,一切都很好,并且已cached函数就像一个符咒一样工作。

但是,如果您尝试将其传递给递归函数,例如:

您不会注意到性能的提高,并且实际上对于大于20的n值,该函数仍将花费大量时间返回-请记住,此函数在不缓存中间结果的情况下具有指数级的时间复杂度。

为什么会这样? 嗯, fib是一个递归函数,因此它在自己的体内进行调用。 确实,当您将fib放入cached函数中并调用生成的函数时,将进行缓存查找。 但是之后,对fib的递归调用仍将指向原始fib ,因此将不再发生缓存查找。

为了允许相同的缓存机制与递归函数一起使用,将需要其他工作。

编写高效的缓存递归函数

首先,我们将无法创建现有功能的缓存版本。 但是我们可以提供一个框架来轻松编写具有缓存效率的递归函数。

我们需要解决的关键问题是递归调用。 我们需要一种透明地将一些代码包装在递归调用周围的方法,以便在实际执行缓存查找之前执行它。

解决方案在于以下类型签名: ((In) -> Out, In) -> OutInOut是泛型类型。 此签名描述了一个带有两个参数的函数:

  • 递归函数
  • 递归函数的输入

通过这种类型,我们既可以使用第一个参数进行递归调用,又可以通过第二个参数访问函数的输入。

从那里,我们只需要编写一个内部函数,该函数实际上将在执行递归调用之前执行高速缓存查找。

尽管此实现确实有点复杂,但很棒的事情是使用非常容易,因为将其他语法保持在最低限度:

从那里,您现在可以使用缓存的功能,并看到它运行得更快!

从那里去哪里?

我上面描述的包含缓存中间结果的过程称为“记忆化”。 它的原理很简单:它将内存换为CPU周期。

尽管可以进行任何形式的折衷,但是它可以带来显着的计算加速,但这并不是万灵丹。 因此,在应用每个特定情况之前,必须考虑到每个特定情况:

  • 在某些情况下,您可能需要限制高速缓存的大小,并实施某些高速缓存替换策略以管理高速缓存。
  • 当使用浮点数时,缓存可能显示效率低下,因为输入之间的差异通常很小。 在这种情况下,在计算之前四舍五入输入可能会有所帮助。
  • 等等

感谢您阅读本文,如果您喜欢它的内容,请确保检查我的GitHub存储库,该存储库专门提供Swift语言的提示!