优化懒惰的集合

时间:2017-09-19 04:02:49

标签: swift optimization lazy-evaluation

这个问题是关于优化惰性集合。我将首先解释问题,然后对可能的解决方案提出一些想法。问题采用粗体

问题

Swift希望Collection上的操作为O(1)。某些操作(尤其是prefixsuffix - 类似的操作会偏离,并且大约为O(n)或更高。

在初始化期间,惰性集合不能遍历基本集合,因为计算应尽可能延迟,直到实际需要该值为止。

那么,我们如何优化懒惰的收藏?当然这引出了一个问题,什么构成了优化的懒惰收藏?

思想

最明显的解决方案是缓存。这意味着对集合方法的第一次调用具有不利的时间复杂度,但是可以在O(1)中计算对相同或其他方法的后续调用。我们将一些空间复杂度交换为O(n)的顺序,以便更快地进行计算。

尝试使用缓存优化struct上的延迟集合是不可能的,因为subscript(_ position:)以及您需要实现以符合LazyProtocolCollection的所有其他方法都是非默认情况下,mutatingstruct是不可变的。这意味着我们必须为每次调用属性或方法重新计算所有操作。

这给我们留下了class es。类是可变的,这意味着所有计算的属性和方法都可以在内部改变状态。当我们使用类来优化惰性集合时,我们有两个选择。首先,如果惰性类型的属性是var iables,那么我们就会把自己带入一个受伤的世界。如果我们更改属性,则可能会使先前缓存的结果无效。我可以想象管理代码路径以使属性变得令人头疼。其次,如果我们使用let,我们会很好;初始化期间设置的状态不能更改,因此不需要更新缓存的结果。请注意,我们只是在谈论使用纯方法的惰性集合而没有副作用。

但是类是引用类型。 为延迟集合使用引用类型有什么缺点? Swift标准库并没有将它们用作初学者。

对不同方法的任何想法或想法?

2 个答案:

答案 0 :(得分:5)

我完全赞同亚历山大。如果你存储了懒惰的集合,你通常会做错事,重复访问的成本会不断给你带来惊喜。

这些集合已经夸大了他们的复杂性要求it's true

  

注意:首先访问startIndex或依赖于startIndex的任何方法的性能取决于在集合开始时满足谓词的元素数量,并且可能无法提供Collection协议给出的通常性能。因此,请注意,LazyDropWhileCollection实例上的常规操作可能没有记录的复杂性。

但缓存不会解决这个问题。它们在第一次访问时仍然是O(n),所以像

这样的循环
for i in 0..<xs.count { print(xs[i]) }

仍为O(n ^ 2)。还记得O(1)和“fast”不是一回事。感觉就像你试图“快速”,但这并没有解决复杂性的承诺(也就是说,懒惰的结构已经在Swift中破坏了它们的复杂性承诺)。

缓存是一个净负值因为它使得延迟数据结构的正常(和预期)使用变慢。使用延迟数据结构的常规方法是使用它们零次或一次。如果您要多次使用它们,则应使用严格的数据结构。缓存你从未使用的东西是浪费时间空间。

当然有一些可以想象的用例,你有一个大型数据结构,这些数据结构将被多次稀疏访问,因此缓存会很有用,但这不是构建用于处理的用例lazy。 / p>

  

尝试使用缓存来优化结构上的延迟集合是不可能的,因为下标(_ position :)和您需要实现以符合LazyProtocolCollection的所有其他方法都是非变异的,并且结构默认是不可变的。这意味着我们必须为每次调用属性或方法重新计算所有操作。

事实并非如此。 struct可以在内部存储引用类型以保存其缓存,这很常见。字符串就是这样做的。它们包含一个StringBuffer,它是一个引用类型(出于与Swift编译器错误相关的原因,StringBuffer实际上是作为一个包装类的结构实现的,但从概念上讲它是一个引用类型)。 Swift中的许多值类型以这种方式存储内部缓冲区类,这允许它们在呈现不可变接口时在内部是可变的。 (这对于CoW以及许多其他与性能和内存相关的原因也很重要。)

请注意,今天添加缓存也会破坏lazy的现有用例:

struct Massive {
    let id: Int
    // Lots of data, but rarely needed.
}

// We have lots of items that we look at occassionally
let ids = 0..<10_000_000

// `massives` is lazy. When we ask for something it creates it, but when we're 
// done with it, it's thrown away. If `lazy` forced caching, then everything 
// we accessed would be forever. Also, if the values in `Massive` change over
// time, I certainly may want it to be rebuilt at this point and not cached.
let massives = ids.lazy.map(Massive.init)
let aMassive = massives[10]

这并不是说缓存数据结构在某些情况下不会有用,但它肯定并不总是一个胜利。它会带来很多成本,并在帮助他人的同时打破一些用途。因此,如果您需要其他用例,则应构建一个提供它们的数据结构。但lazy不是那个工具是合理的。

答案 1 :(得分:3)

Swift的懒惰集合旨在提供对元素的一次性访问。后续访问会导致冗余计算(例如,惰性映射序列会重新计算transform闭包。

如果你想重复访问元素,最好只切片你关心的延迟序列/集合的一部分,并从中创建一个合适的Collection(例如一个数组)。

保持懒惰评估和缓存每个元素的书籍可能比收益更大。