使用Swift数组的AnyObject类型执行+操作比使用T类型的数组要快得多

7

假设有以下三个简单函数:

func twice_Array_of_Int(a: [Int]) -> [Int] {
    return a + a
}

func twice_Array_of_T<T>(a: [T]) -> [T] {
    return a + a
}

func twice_Array_of_Any(a: [AnyObject]) -> [AnyObject] {
    return a + a
}

假设使用发布版(-Os),您认为它们的性能会如何比较?
我期望 [Int] -> [Int][AnyObject] -> [AnyObject] 快得多...而且确实是...快了好几个数量级。
然而,我也期望 [T] -> [T] 的表现要比 [AnyObject] -> [AnyObject] 好得多,几乎与 [Int] -> [Int] 一样快...对吗?
结果证明我错了:即使是[AnyObject] -> [AnyObject](包括转换回到[Int]),也比[T] -> [T] 快5倍!这是令人失望的,尤其是因为泛型是 Swift 最有前途的特性之一。
在 Apple 的 WWDC 视频中,工程师们提到他们正在本地实现泛型,即使用它们不会导致代码膨胀。这是否解释了[T] -> [T] 的性能差?如果他们只是在编译时扩展通用函数,那么 [T] -> [T][Int] -> [Int] 的性能应该是相同的,对吗?
以下是测试代码:
func testPerformance_twice_Array_of_Int() {
    let a = Array(1...100_000)
    self.measureBlock {
        let twice_a = twice_Array_of_Int(a)
    }
    // average: 0.000, relative standard deviation: 76.227%
}

func testPerformance_twice_Array_of_T() {
    let a = Array(1...100_000)
    self.measureBlock {
        let twice_a = twice_Array_of_T(a)
    }
    // measured [Time, seconds] average: 0.554, relative standard deviation: 7.846%
}

func testPerformance_twice_Array_of_Any() {
    let a = Array(1...100_000)
    self.measureBlock {
        let twice_a = twice_Array_of_Any(a) as [Int]
    }
    // average: 0.115, relative standard deviation: 8.303%

    // without the cast to [Int] = average: 0.039, relative standard deviation: 2.931%
}

我很想听听你的意见,以及你如何计划将其纳入你的代码设计中。

编辑

我刚刚进行了一项更简单的测量,结果更加惊人:

func ==(lhs: (Int, Int), rhs: (Int, Int)) -> Bool {
    return lhs.0 == rhs.0 && lhs.1 == rhs.1
}

相比之下:

func ==<T: Equatable>(lhs: (T, T), rhs: (T, T)) -> Bool {
    return lhs.0 == rhs.0 && lhs.1 == rhs.1
}

结果:

func testPerformance_Equals_Tuple_Int() {
    let a = (2, 3)
    let b = (3, 2)
    XCTAssertFalse(a == b)
    let i = 1_000_000
    self.measureBlock() {
        for _ in 1...i {
            let c = a == b
        }
        // average: 0.002, relative standard deviation: 9.781%
    }
}

相比之下:

func testPerformance_Equals_Tuple_T() {
    let a = (2, 3)
    let b = (3, 2)
    XCTAssertFalse(a == b)
    let i = 1_000_000
    self.measureBlock() {
        for _ in 1...i {
            let c = a == b
        }
        // average: 2.080, relative standard deviation: 5.118%
    }
}

通用版本的中缀函数要慢1000多倍!
编辑2:
8月21日,奥斯丁·郑在一次Swift语言用户组聚会上发表了题为“Enums、模式匹配和Generics”的演讲(特别嘉宾克里斯·拉特纳)。他说,Swift会为常见类型生成优化代码,但必要时会回退到本地通用版本的函数以满足运行时需要。参考:http://realm.io/news/swift-enums-pattern-matching-generics/ (从32:00开始) 。
编辑3:
Swift 2已经发布,这个问题已经迫切需要更新(只要我有喘息时间就会更新)...

你是怎样编译的?使用了什么优化级别和标志位? - Benjamin Gruenbaum
使用苹果的Cocoa框架模板进行发布构建,即最快、最小[-Os] - Milos
1个回答

6
我很乐意听取您的意见并了解您是如何将其纳入您的代码设计中的。 您不应该将此因素纳入您的代码设计。 Swift编译器正在快速发展,优化器也在持续和彻底地开发中。基于早期优化器的微基准测试来更改编码实践是最糟糕的“过早优化”形式。
为清晰度编写代码。 为正确性编写代码。 当看到性能问题时,请调查它们。没有比崩溃的程序更慢的程序。 [Int] [T] 都比 [AnyObject] 更安全、更清晰、更易于使用(您必须不断进行类型转换和验证)。选择不应该难。当您有一些演示 [T] 与仪器存在问题的活动代码时,那么您应该调查其他选项(尽管我仍会将 [AnyObject] 放在底部;如果真的更快,那么上述代码的明显解决方案是编写处理 [Int] 的特殊情况重载)。

既然您有一个有趣的测试用例展示了通用和本机之间出人意料的差异,那么打开一个radar(bugreport.apple.com)就是合适的。这样,当问题得到解决时,您清晰、正确的代码将获得免费的速度提升。


编辑:我还没有查看汇编输出(你应该查看),但我有几个理论可以解释为什么这可能是真的(如果它确实是真的;我也没有再现它)。在这里,[AnyObject] 可能会被替换为 NSArray,它具有与 Array 截然不同的性能特征。这是你永远不应该根据某些微基准测试认为 "[AnyObject] 更快" 的关键原因,因为那不是你真正代码的表现。a+a 的性能可能与其他操作的性能完全无关。

关于 [Int][T],您可能误解了Swift处理泛型的方式。Swift不为每种类型创建完全不同的函数版本,而是创建一个通用版本。这个版本可能无法像特定类型版本那样对所有优化进行优化。例如,在这种情况下,[T] 版本可能会做一些整形版本没有做的内存管理操作(我猜测)。优化器可以生成优化版本(这就是为什么不应该试图去猜测它),但也可能不会生成(这就是为什么有时候需要使用特殊重载来帮助它)。Swift Yeti有一篇很好的文章进一步解释了这一点
同样地,您绝不能在没有测试与您所关心的相似的实时代码之前就认为自己知道优化器将要执行什么操作(甚至不应考虑此事,除非您有理由相信这是一个性能瓶颈)。编写“因为更快而疯狂”的代码非常容易,但实际上可能会慢得多,但仍然很疯狂。

优化器知识是力量,但如果你不知道何时使用它,这将是一种危险的力量。


2
谢谢您的认真回答(+1)。然而,我不确定建议“不将此纳入您的代码设计中”有多大帮助。从我的问题中可以清楚地看出,“安全性”和“清晰度”是提出这个问题的动机。此外,我明确引用了一位苹果工程师的话,他说他们正在本地实现泛型(即,正如您所说,“Swift不会为每种类型创建完全新的函数版本”),所以我不确定您为什么觉得需要重复这一点。 - Milos
2
然而,我最不确定的是反对“疯狂代码”的普遍论点。什么算作“疯狂”随时间和环境而变化。部分原因是我们倾向于编写“疯狂”代码,这是许多改进的源泉。此外,一个非常好理解的“疯狂快速”例程隐藏在美丽的API后面不会伤害任何人。最后,提出这些问题难道不可能对其他开发人员有所帮助吗?他们可能会为了理解为什么他们的代码性能不如预期而苦苦挣扎。Swift只会从这样的讨论中受益。 - Milos

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接