为什么我的滤镜版本的表现与Swifts不同?

Dan*_*ieu 9 filter instruments higher-order-functions swift

作为练习,我重写了一些Swift的高阶函数,一个是.filter.我决定.filter使用乐器测量我对抗Swift的版本,我对结果感到困惑.

这是我的过滤器版本,我承认可能不正确.

extension Array {
    func myFilter(predicate: Element -> Bool) -> [Element] {
        var filteredArray = [Element]()
        for x in self where predicate(x) {
            filteredArray.append(x)
        }

        return filteredArray
    }
}
Run Code Online (Sandbox Code Playgroud)

发生了什么

我的过滤器

  • 整体CPU消耗:85.7%
  • 我的过滤器的消耗量:67.9%

在此输入图像描述 在此输入图像描述

斯威夫特的过滤器

  • 整体CPU消耗:57.7%
  • 我的过滤器的消耗量:70.9%

在此输入图像描述 在此输入图像描述

我的期望

我期待类似的表现.我很困惑为什么我的过滤器函数调用本身会消耗更少的CPU,但我的整体应用程序CPU高出近30%.

我的问题

如果我filter写错了,请帮助我理解我的错误.否则请指出为什么Swift的filterCPU负载比我的减少了30%.

Cri*_*tik 13

好的,所以在阅读了所有发表的评论后,我决定也进行基准测试,这是我的结果.奇怪的是,内置filter似乎比自定义实现更糟糕.

TL; DR; 由于您的函数很短,并且编译器可以访问它的源代码,因此编译器会内联函数调用,从而实现更多优化.

另一个考虑是因为你的myFilter声明没有考虑异常抛出闭包,内置的东西filter.

添加@inline(never),throwsrethrows在您的myFilter声明中,您将获得与内置相似的结果filter

研究结果

我曾经mach_absolute_time()获得准确的时间.我没有将结果转换为秒,因为我只是对比较感兴趣.使用Xcode 7.2在Yosemite 10.10.5上进行测试.

import Darwin

extension Array {
    func myFilter(@noescape predicate: Element -> Bool) -> [Element] {
        var filteredArray = [Element]()
        for x in self where predicate(x) {
            filteredArray.append(x)
        }

        return filteredArray
    }
}

let arr = [Int](1...1000000)

var start = mach_absolute_time()
let _ = arr.filter{ $0 % 2 == 0}
var end = mach_absolute_time()
print("filter:         \(end-start)")

start = mach_absolute_time()
let _ = arr.myFilter{ $0 % 2 == 0}
end = mach_absolute_time()
print("myFilter:       \(end-start)")
Run Code Online (Sandbox Code Playgroud)

debug模式中,filtermyFilter以下更快:

filter:         370930078
myFilter:       479532958
Run Code Online (Sandbox Code Playgroud)

release,但是,myFilter是不是好多了filter:

filter:         15966626
myFilter:       4013645
Run Code Online (Sandbox Code Playgroud)

更奇怪的是,内置的精确副本filter(取自Marc的评论)比内置的更好.

extension Array {
    func originalFilter(
        @noescape includeElement: (Generator.Element) throws -> Bool
        ) rethrows -> [Generator.Element] {

            var result = ContiguousArray<Generator.Element>()

            var generator = generate()

            while let element = generator.next() {
                if try includeElement(element) {
                    result.append(element)
                }
            }

            return Array(result)
    }

}

start = mach_absolute_time()
let _ = arr.originalFilter{ $0 % 2 == 0}
end = mach_absolute_time()
print("originalFilter: \(end-start)")
Run Code Online (Sandbox Code Playgroud)

使用上面的代码,我的基准测试应用程序提供以下输出:

filter:         13255199
myFilter:       3285821
originalFilter: 3309898
Run Code Online (Sandbox Code Playgroud)

回到debug模式,filter给出这个输出的3种口味:

filter:         343038057
myFilter:       429109866
originalFilter: 345482809
Run Code Online (Sandbox Code Playgroud)

filteroriginalFilter给出非常接近的结果.这让我觉得Xcode与Swifts stdlib的调试版本相关联.但是当内置时release,Swifts stdlib的性能比它好3倍debug,这让我很困惑.

所以下一步是分析.我点击Cmd+I,将采样间隔设置为40us,并对应用程序进行了两次配置:一次只filter启用了呼叫,一次myFilter启用了.我删除了打印代码,以使堆栈跟踪尽可能干净.

内置filter分析: 构建过滤时间分析

myFilter: myFilter时间分析

尤里卡!,我找到了答案.没有跟踪myFilter调用,这意味着编译器内联函数调用,从而实现额外的优化,从而提高性能.

我添加了@inline(never)属性myFilter,它的性能降低了.

接下来,为了使它更接近内置过滤器,添加throwsrethrows声明,因为内置过滤器允许传递抛出异常的闭包.

而且(或没有),这是我得到的:

filter: 11489238
myFilter: 6923719
myFilter not inlined: 9275967
my filter not inlined, with throws: 11956755
Run Code Online (Sandbox Code Playgroud)

最后的结论:编译器可以内联函数调用的事实,加上缺乏对异常的支持,这对自定义过滤方法的更好性能负责.

以下代码给出了与内置非常相似的结果filter:

extension Array {
    @inline(never)
    func myFilter(predicate: Element throws -> Bool) rethrows -> [Element] {
        var filteredArray = [Element]()
        for x in self where try predicate(x) {
            filteredArray.append(x)
        }

        return filteredArray
    }
}
Run Code Online (Sandbox Code Playgroud)

原始答案:

Swift filter应该表现更好,因为:

  1. 它可以访问数组的内部状态,并且不会强制通过枚举,这意味着至少减少一个函数调用
  2. 它可能会优化构建结果数组的方式

#1可能没有太大区别,因为函数调用不是很贵

另一方面,#2可能对大型阵列产生很大影响.将新元素附加到数组可能会导致数组需要增加其容量,这意味着分配新内存并复制当前状态的内容.

  • 查看 [默认实现的源代码](https://github.com/apple/swift/blob/master/stdlib/public/core/Sequence.swift),看来 (1) Swift 确实使用了枚举和 ( 2)它没有做任何特殊的事情来优化它如何构建结果数组(即它使用 append() )。但是,它确实使用 ContiguousArray 而不是 Array 来构建结果,这对于 class 和 @objc 类型可能更有效。 (2认同)