Swift数组reduce远慢于unsafeReduce:正确性、问题及慢因问询
Swift原生reduce vs 自定义unsafeReduce的性能疑问解答
先直接给你拆解这三个问题,结合你的代码和测试环境(Xcode 11.5、Swift 5、2015款MacBook Air)来逐一分析:
1. 你的unsafeReduce实现是否正确?
答案是完全正确。
先看你的代码:
extension Array { func unsafeReduce<Result>( _ initialResult: Result, _ nextPartialResult: (Result, Element) throws -> Result ) rethrows -> Result { try self.withUnsafeBufferPointer { buffer -> Result in try buffer.reduce(initialResult, nextPartialResult) } } }
Array.withUnsafeBufferPointer是Swift官方提供的安全API,它会在闭包执行期间锁定数组的底层存储,确保这段时间内数组不会被修改(你用let定义的不可变数组,本身就不可能被修改),生成的UnsafeBufferPointer是完全有效的,指向数组连续的内存空间。而UnsafeBufferPointer的reduce方法,本质就是遍历这段连续内存完成累积计算,逻辑和数组的reduce完全一致,只是绕开了数组的Sequence迭代器抽象层。
2. 使用此版本有哪些潜在问题?
虽然你的实现现在没问题,但还是有几个需要注意的风险:
- 可变数组的存储失效风险:如果调用
unsafeReduce的是var类型的数组,并且在reduce执行过程中(比如闭包内部有异步操作修改数组,或者其他线程修改数组),数组触发了存储扩容(比如append),那UnsafeBufferPointer就会变成野指针,导致未定义行为。不过你的场景里用的是不可变数组,这个风险几乎为零,但如果其他人复用这个方法到可变数组场景,就可能踩坑。 - 调试成本更高:如果在
unsafeReduce的闭包里出现崩溃,调试时会直接定位到底层内存操作的位置,比原生reduce的调试信息更模糊,不好快速定位问题。 - 泛型场景的隐性限制:这个方法是针对
Array的扩展,而原生reduce是Sequence协议的方法,适用于所有序列类型(比如Set、Dictionary的键值对序列等)。如果有人想把这个方法搬到其他序列类型上,直接用会报错,因为不是所有序列都有连续内存的底层存储。
3. 原生reduce性能慢的原因是什么?
我们来对比两种reduce的具体执行流程,就能明白差异所在:
原生Array.reduce的执行流程
Array的reduce是基于Sequence协议的默认实现,流程是:
- 调用
makeIterator()创建一个数组迭代器对象; - 循环调用迭代器的
next()方法获取下一个元素,每次next()都会做边界检查——虽然你开启了-unchecked,但这个选项主要是去掉数组索引的越界检查,而迭代器的next()内部还有遍历状态的检查(比如是否已经遍历到末尾),千万次遍历下来,这些检查的开销会被放大; - 泛型函数的派发开销:
Sequence的reduce是泛型实现,闭包nextPartialResult的调用在某些情况下无法被完全内联,而函数调用的开销在千万级循环里会非常明显; - 迭代器对象的额外开销:每次
next()调用都要操作迭代器的内部状态(比如当前遍历的位置),这比直接操作内存指针的开销要大。
自定义unsafeReduce的执行流程
你的方法直接绕开了Sequence的迭代器抽象,流程是:
- 通过
withUnsafeBufferPointer直接获取数组底层连续内存的指针,没有迭代器的中间层; UnsafeBufferPointer.reduce是针对连续内存优化的实现:直接从baseAddress开始,每次指针偏移MemoryLayout<Element>.stride的大小访问元素,没有next()方法的调用开销;- 编译器更容易做深度优化:你的闭包
{ $0 + $1.data }逻辑非常简单,编译器可以把整个reduce循环完全内联,甚至把$1.data的访问直接转换成内存地址的偏移取值,去掉结构体成员访问的额外开销; -unchecked的优化效果完全发挥:UnsafeBufferPointer的遍历直接使用已知的缓冲区长度,完全去掉了边界检查,进一步减少了循环内的开销。
至于你提到的Python版本比Swift原生reduce快2倍,其实是因为Python的求和逻辑(如果是用内置的sum或者基于C实现的循环),避免了Swift泛型迭代器的额外开销,而你的unsafeReduce因为编译器优化到位,直接操作连续内存,所以比Python快得多。
内容的提问来源于stack exchange,提问作者Louis Lac
相关产品推荐
相关产品推荐

