【问题标题】:Swift Weak Reference Much Slower than Strong ReferenceSwift 弱引用比强引用慢得多
【发布时间】:2020-02-26 08:07:33
【问题描述】:

我正在用 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)

结果

以下是每种参考类型的基准时间。在每次测试中,Shape 类上的 body 引用都已更改。该代码是使用发布模式 [-O] 和面向 macOS 10.15 的 Swift 5.1 构建的。

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 s

的结果

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

注意:从 Xcode 12/Swift 5.3 开始,这仍然是正确的

【问题讨论】:

  • 不确定它是否适用于您的情况,但您是否考虑过不使用一个有利于Struct 的类,最好使用自定义的写时复制?
  • @Kamil.S 是的,我有,而且速度更快(虽然不是很多),但是共享状态更方便。不过我可能会回到这个想法。
  • @EpicByte 这是您在那里所做的一些出色的研究!一篇简短的博客文章的好材料 :) 谢谢你!

标签: swift performance weak-references strong-references unowned-references


【解决方案1】:

在我撰写/调查此问题时,我最终找到了解决方案。要拥有一个没有weakunowned 开销检查的简单反向指针,您可以将正文声明为:

unowned(unsafe) var body: Body!

根据 Swift 文档:

Swift 还为您需要的情况提供了不安全的无主引用 禁用运行时安全检查——例如,出于性能原因。 与所有不安全的操作一样,您承担以下责任 检查该代码的安全性。

你通过写 unowned(unsafe) 来表示一个不安全的无主引用。 如果您尝试在实例之后访问不安全的无主引用 它所指的已被释放,您的程序将尝试访问 实例曾经所在的内存位置,这是不安全的 操作

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

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

注意:从 Xcode 12/Swift 5.3 开始,这仍然是正确的

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多