在Swift 3中,joined()或flatMap(_ :)的表现更好吗?

时间:2016-08-24 05:38:08

标签: arrays swift functional-programming swift3

我很好奇joined().flatMap(_:)在展平多维数组时的性能特征:

let array = [[1,2,3],[4,5,6],[7,8,9]]
let j = Array(array.joined())
let f = array.flatMap{$0}

他们都将嵌套的array展平为[1, 2, 3, 4, 5, 6, 7, 8, 9]。我是否应该更喜欢一个而不是性能?此外,是否有更可读的方式来编写呼叫?

2 个答案:

答案 0 :(得分:18)

TL; DR

当仅仅展平2D数组(没有应用任何转换或分隔符时,请参阅@dfri's answer以获取有关该方面的更多信息),array.flatMap{$0}Array(array.joined())在概念上都是相同的,有类似的表现。

flatMap(_:)joined()之间的主要区别(请注意,这不是一种新方法,它有just been renamed from flatten()),joined()总是被懒惰地应用(对于数组,它返回一个特殊的FlattenBidirectionalCollection<Base>)。

因此,就性能而言,在您只想迭代部分扁平序列(不应用任何转换)的情况下,使用joined()而不是flatMap(_:)是有意义的。例如:

let array2D = [[2, 3], [8, 10], [9, 5], [4, 8]]

if array2D.joined().contains(8) {
    print("contains 8")
} else {
    print("doesn't contain 8")
}

因为joined()被懒惰地应用了&amp; contains(_:)将在找到匹配时停止迭代,只有前两个内部数组必须“展平”才能从2D数组中找到元素8。虽然,@dfri correctly notes below,您也可以通过使用flatMap(_:) / LazySequence懒惰地应用LazyCollection - 可以通过lazy属性创建。这对于懒惰地应用转换和放大是理想的。展平给定的2D序列。

如果完全迭代joined(),则概念上与使用flatMap{$0}没有什么不同。因此,这些都是平坦化2D阵列的有效(和概念相同)方式:

array2D.joined().map{$0}

Array(array2D.joined())

array2D.flatMap{$0}

就性能而言,flatMap(_:) is documented的时间复杂度为:

  

O(m + n),其中m是此序列的长度,n是结果的长度

这是因为its implementation只是:

  public func flatMap<SegmentOfResult : Sequence>(
    _ transform: (${GElement}) throws -> SegmentOfResult
  ) rethrows -> [SegmentOfResult.${GElement}] {
    var result: [SegmentOfResult.${GElement}] = []
    for element in self {
      result.append(contentsOf: try transform(element))
    }
    return result
  }
}

由于append(contentsOf:)的时间复杂度为O(n),其中n是要追加的序列的长度,我们得到O(m + n)的总时间复杂度,其中m将是总的附加的所有序列的长度,n是2D序列的长度。

当谈到joined()时,没有记录时间复杂性,因为它被懒惰地应用。但是,要考虑的源代码的主要部分是the implementation of FlattenIterator,它用于迭代2D序列的展平内容(使用map(_:)Array(_:)初始化程序时会发生这种情况。 joined())。

  public mutating func next() -> Base.Element.Iterator.Element? {
    repeat {
      if _fastPath(_inner != nil) {
        let ret = _inner!.next()
        if _fastPath(ret != nil) {
          return ret
        }
      }
      let s = _base.next()
      if _slowPath(s == nil) {
        return nil
      }
      _inner = s!.makeIterator()
    }
    while true
  } 

此处_base是基本2D序列,_inner是来自其中一个内部序列的当前迭代器,_fastPath&amp; _slowPath是编译器的提示,用于辅助分支预测。

假设我正确地解释了这段代码&amp;迭代完整序列,这也具有O(m + n)的时间复杂度,其中m是序列的长度,n是结果的长度。这是因为它遍历每个外部迭代器和每个内部迭代器以获得扁平元素。

因此,性能方面,Array(array.joined())array.flatMap{$0}都具有相同的时间复杂度。

如果我们在调试版本中运行快速基准测试(Swift 3.1):

import QuartzCore

func benchmark(repeatCount:Int = 1, name:String? = nil, closure:() -> ()) {
    let d = CACurrentMediaTime()
    for _ in 0..<repeatCount {
        closure()
    }
    let d1 = CACurrentMediaTime()-d
    print("Benchmark of \(name ?? "closure") took \(d1) seconds")
}

let arr = [[Int]](repeating: [Int](repeating: 0, count: 1000), count: 1000)

benchmark {
    _ = arr.flatMap{$0} // 0.00744s
}

benchmark {
    _ = Array(arr.joined()) // 0.525s
}

benchmark {
    _ = arr.joined().map{$0} // 1.421s
}

flatMap(_:)似乎是最快的。我怀疑joined()较慢可能是由于FlattenIterator内发生的分支(虽然编译器的提示最小化了这个成本) - 尽管map(_:)为什么这么慢,我我不太确定。当然有兴趣知道是否有其他人知道更多。

但是,在优化的构建中,编译器能够优化这种巨大的性能差异;给所有三个选项提供可比较的速度,尽管flatMap(_:)仍然是最快的一小段时间:

let arr = [[Int]](repeating: [Int](repeating: 0, count: 10000), count: 1000)

benchmark {
    let result = arr.flatMap{$0} // 0.0910s
    print(result.count)
}

benchmark {
    let result = Array(arr.joined()) // 0.118s
    print(result.count)
}

benchmark {
    let result = arr.joined().map{$0} // 0.149s
    print(result.count)
}

(请注意,执行测试的顺序会影响结果 - 上述结果均为执行各种不同订单测试的平均值)

答案 1 :(得分:4)

Swiftdoc.org documentation of Array (Swift 3.0/dev)我们读到[强调我的]:

func flatMap<SegmentOfResult : Sequence>(_: @noescape (Element) throws -> SegmentOfResult)
     

返回一个包含调用的连接结果的数组   给出了与该序列的每个元素的转换。

     

...

     

事实上,s.flatMap(transform) 相当于 Array(s.map(transform).flatten())

我们也可以看看Swift源代码中的两个实际实现(从中生成Swiftdoc ......)

最值得注意的是后一个源文件,其中使用的闭包(flatMap)未产生的transform实现和可选值(如此处的情况)都被描述为

/// Returns the concatenated results of mapping `transform` over
/// `self`.  Equivalent to 
///
///     self.map(transform).joined()

从上面的内容(假设编译器可以是一个简单的自我{ $0 } transform),看起来似乎在性能方面,两个选择应该是等价的,但是{{1做,imo,更好地显示操作的 intent

除了语义意图之外,还有一个明显的用例,joined优先于joined(并不完全可比)flatMapjoined使用init(separator:)使用分隔符连接序列的let array = [[1,2,3],[4,5,6],[7,8,9]] let j = Array(array.joined(separator: [42])) print(j) // [1, 2, 3, 42, 4, 5, 6, 42, 7, 8, 9] 初始值设定项:

flatMap

使用flatMap的相应结果并不是很整洁,因为我们明确需要在 let f = Array(array.flatMap{ $0 + [42] }.dropLast()) print(f) // [1, 2, 3, 42, 4, 5, 6, 42, 7, 8, 9] 操作之后删除最后一个额外的分隔符(两个不同的用例,或没有尾随分隔符)

flatMap

另请参阅Erica Sadun关于flatten()joined()的一篇有些过时的帖子(注意:flatten()在Swift&lt; 3)中被命名为onSubmit(props){ console.log('csrf',CSRF_TOKEN); axios({ method:'POST', url:'/api/review/create/', headers:{ 'X-CSRF-Token':CSRF_TOKEN, //'Access-Control-Allow-Origin':'*', 'Accept': 'application/json', 'Content-Type': 'application/json', }, data:{ review:props.review } }) .then(response => { console.log('success'); }) .catch(error => { throw("Error: ",error); }); }