我正在Swift中建立一个物理引擎。在对引擎进行了一些新的添加并运行了基准测试之后,我注意到性能大大降低了。例如,在下面的屏幕截图中,您可以看到FPS如何从60 FPS下降到3 FPS(FPS在右下角)。最终,我将问题归结为一行代码:
final class Shape {
...
weak var body: Body! // This guy
...
}
[在添加的某些时候,我从Shape
类向Body
类添加了一个弱引用。这是为了防止出现强参考循环,因为Body
也具有对Shape
的强参考。
[不幸的是,弱引用似乎有很大的开销(我想将其无效的额外步骤)。我决定通过在下面构建一个物理引擎的简化版本并对不同参考类型进行基准测试来对此进行进一步研究。
import Foundation
final class Body {
let shape: Shape
var position = CGPoint()
init(shape: Shape) {
self.shape = shape
shape.body = self
}
}
final class Shape {
weak var body: Body! //****** This line is the problem ******
var vertices: [CGPoint] = []
init() {
for _ in 0 ..< 8 {
self.vertices.append( CGPoint(x:CGFloat.random(in: -10...10), y:CGFloat.random(in: -10...10) ))
}
}
}
var bodies: [Body] = []
for _ in 0 ..< 1000 {
bodies.append(Body(shape: Shape()))
}
var pairs: [(Shape,Shape)] = []
for i in 0 ..< bodies.count {
let a = bodies[i]
for j in i + 1 ..< bodies.count {
let b = bodies[j]
pairs.append((a.shape,b.shape))
}
}
/*
Benchmarking some random computation performed on the pairs.
Normally this would be collision detection, impulse resolution, etc.
*/
let startTime = CFAbsoluteTimeGetCurrent()
for (a,b) in pairs {
var t: CGFloat = 0
for v in a.vertices {
t += v.x*v.x + v.y*v.y
}
for v in b.vertices {
t += v.x*v.x + v.y*v.y
}
a.body.position.x += t
a.body.position.y += t
b.body.position.x -= t
b.body.position.y -= t
}
let time = CFAbsoluteTimeGetCurrent() - startTime
print(time)
以下是每种参考类型的基准时间。在每个测试中,body
类上的Shape
参考均已更改。该代码是使用发布模式[-O]和针对MacOS 10.15的Swift 5构建的。
weak var body: Body!
:0.1886 s
var body: Body!
:0.0167 s
unowned body: Body!
:0.0942 s
您可以在上面的计算中看到使用强引用而不是弱引用会导致性能提高10倍以上。使用unowned
有帮助,但不幸的是它仍然慢5倍。通过事件探查器运行代码时,似乎还会执行其他运行时检查,从而导致大量开销。
所以问题是,在不招致ARC开销的情况下,具有简单的指向Body的后向指针我有哪些选择。而且为什么这种开销看起来如此极端?我想我可以保留强参考周期并手动中断它。但是我想知道是否有更好的选择?
[当我编写/调查此问题时,我最终找到了解决方案。要使用没有weak
或unowned
开销检查的简单后向指针,可以将body声明为:
unowned(unsafe) var body: Body!
根据Swift文档:
Swift还会在您需要的情况下提供不安全的无主引用禁用运行时安全检查(例如出于性能原因)。与所有不安全的操作一样,您要承担检查该代码是否安全。
您通过写unown(unsafe)来表示不安全的无人引用。如果您尝试在实例之后访问不安全的无主引用它所指的已被释放,您的程序将尝试访问实例曾经所在的内存位置,这是不安全的操作
因此很明显,这些运行时检查会在性能关键代码中产生严重的开销。