Swift 弱引用比强引用慢得多

Swift Weak Reference Much Slower than Strong Reference

我正在 Swift 中构建一个物理引擎。在对引擎进行了一些最近的添加和 运行 基准测试之后,我注意到性能大大降低了。例如,在下面的屏幕截图中,您可以看到 FPS 如何从 60 FPS 下降到 3 FPS(FPS 在右下角)。最终,我将问题追溯到一行代码:

final class Shape {
    ...
    weak var body: Body! // This guy
    ...
}

在我添加的某些点上,我添加了从 Shape class 到 Body class 的弱引用。这是为了防止强引用循环,因为 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)

结果

以下是每种参考类型的基准时间。在每个测试中,Shape class 上的 body 参考被更改。该代码是使用发布模式 [-O] 构建的,Swift 5.1 针对 macOS 10.15。

weak var body: Body!: 0.1886 秒

var body: Body!: 0.0167 秒

unowned body: Body!: 0.0942 秒

您可以看到在上面的计算中使用强引用而不是弱引用会使性能提高 10 倍以上。使用 unowned 有帮助,但不幸的是它仍然慢了 5 倍。当 运行 代码通过探查器时,似乎会执行额外的运行时检查,从而导致大量开销。

所以问题是,我有什么选择可以让一个简单的反向指针指向 Body 而不会产生这个 ARC 开销。此外,为什么这种开销看起来如此极端?我想我可以保留强引用循环并手动中断它。但我想知道是否有更好的选择?

更新: 根据回答,这里是
的结果 unowned(unsafe) var body: Body!: 0.0160 秒

更新2: 从 Swift 5.2 (Xcode 11.4) 开始,我注意到 unowned(unsafe) 有更多的开销。这是现在的结果 unowned(unsafe) var body: Body!: 0.0804 秒

注意:从 Xcode 12/Swift 5.3

开始仍然如此

当我写 up/investigating 这个问题时,我最终找到了解决方案。要有一个简单的后向指针而无需 weakunowned 的开销检查,您可以将 body 声明为:

unowned(unsafe) var body: Body!

根据 Swift 文档:

Swift also provides unsafe unowned references for cases where you need to disable runtime safety checks—for example, for performance reasons. As with all unsafe operations, you take on the responsibility for checking that code for safety.

You indicate an unsafe unowned reference by writing unowned(unsafe). If you try to access an unsafe unowned reference after the instance that it refers to is deallocated, your program will try to access the memory location where the instance used to be, which is an unsafe operation

很明显,这些运行时检查会在性能关键代码中产生严重的开销。

更新: 从 Swift 5.2 (Xcode 11.4) 开始,我注意到 unowned(unsafe) 有更多的开销。我现在只是简单地使用强引用并手动中断保留循环,或者尝试在性能关键代码中完全避免它们。

注意:从 Xcode 12/Swift 5.3

开始仍然如此