Swift弱引用比强引用慢很多

10

我正在使用Swift构建物理引擎。在对引擎进行一些最近的添加并运行基准测试后,我注意到性能显着变慢。例如,在下面的截图中,您可以看到FPS从60下降到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)

结果

以下是每种引用类型的基准时间。在每个测试中,更改了Shape类上的body引用。使用Swift 5.1以目标macOS 10.15为目标,在发布模式[-O]下构建代码。

weak var body: Body!: 0.1886秒

var body: Body!: 0.0167秒

unowned body: Body!: 0.0942秒

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

所以问题是,在不产生这种ARC开销的情况下,有哪些选项可用于拥有一个简单的回指针到Body?此外,为什么这种开销看起来如此极端?我想我可以保留强引用循环并手动打破它。但我想知道是否有更好的选择?

更新: 根据答案,以下是结果
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,这仍然是正确的。

不确定在您的情况下是否适用,但您是否考虑过不使用类,而是使用一个Struct,最好是带有自定义的写时复制? - Kamil.S
@Kamil.S 是的,我有使用过,并且它更快(虽然不是很明显),但共享状态更方便。不过我可能会回到这个想法。 - Epic Byte
@EpicByte,你做了一些非常出色的研究!这是一个很好的素材,可以写成一篇简短的博客文章:) 谢谢你! - m_katsifarakis
1个回答

5

当我撰写/调查此问题时,我最终找到了解决方案。为了拥有一个简单的回溯指针,而无需进行weakunowned的开销检查,您可以将其声明为:

unowned(unsafe) var body: Body!

根据Swift文档:
Swift还为需要禁用运行时安全检查(例如出于性能原因)的情况提供了不安全的unowned引用。与所有不安全操作一样,您承担了确保代码安全的责任。
您通过写入unowned(unsafe)来指示不安全的unowned引用。如果在所引用的实例被释放之后尝试访问不安全的unowned引用,则程序将尝试访问实例曾经占据的内存位置,这是一项不安全的操作。
因此,在性能关键代码中,这些运行时检查可能会产生严重的开销。
更新: 截至Swift 5.2(Xcode 11.4),我注意到unowned(unsafe)有更多的开销。我现在只使用强引用并手动打破保留循环,或者尝试在性能关键代码中尽量避免它们。
注意:截至Xcode 12/Swift 5.3,这仍然是正确的。

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