迅捷的弱参考比强参考慢得多 [英] Swift Weak Reference Much Slower than Strong Reference

查看:111
本文介绍了迅捷的弱参考比强参考慢得多的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

I'm building a physics engine in Swift. After making some recent additions to the engine and running the benchmarking tests I noticed the performance was drastically slower. For example, in the screenshots below you can see how the FPS dropped from 60 to 3 FPS (FPS is in the bottom-right corner). Eventually, I traced the problem down to just a single line of code:

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

在添加的某些时候,我从Shape类向Body类添加了一个弱引用.这是为了防止强引用循环,因为Body也具有对Shape的强引用.

At some point in my additions I added a weak reference from the Shape class to the Body class. This is to prevent a strong reference cycle, as Body also has a strong reference to Shape.

不幸的是,弱引用似乎有很大的开销(我想将其无效的额外步骤).我决定通过在下面构建一个物理引擎的简化版本并对不同的参考类型进行基准测试来对此进行进一步的研究.

Unfortunately, it appears weak references have a significant overhead (I suppose the extra steps in nulling it out). I decided to investigate this further by building a massively simplified version of the physics engine below and benchmarking different reference types.

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构建的.


Results

Below are the benchmark times for each reference type. In each test, the body reference on the Shape class was changed. The code was built using release mode [-O] with Swift 5.1 targeting macOS 10.15.

weak var body: Body!:0.1886 s

weak var body: Body!: 0.1886 s

var body: Body!:0.0167 s

var body: Body!: 0.0167 s

unowned body: Body!:0.0942 s

unowned body: Body!: 0.0942 s

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

You can see using a strong reference in the computation above instead of a weak reference results in over 10x faster performance. Using unowned helps, but unfortunately it is still 5x slower. When running the code through the profiler, there appear to be additional runtime checks being performed resulting in much overhead.

所以问题是,在不产生ARC开销的情况下,具有简单的指向Body的后向指针我有哪些选择?而且,为什么这种开销看起来如此极端?我想我可以保留强大的参考周期并手动中断它.但是我想知道是否还有更好的选择?

So the question is, what are my options for having a simple back pointer to Body without incurring this ARC overhead. And furthermore why does this overhead seem so extreme? I suppose I could keep the strong reference cycle and break it manually. But I'm wondering if there is a better alternative?

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

Update: Based on the answer, here is the result for
unowned(unsafe) var body: Body!: 0.0160 s

Update2: 从Swift 5.2(Xcode 11.4)开始,我注意到unown(unsafe)的开销要大得多.这是现在的结果 unowned(unsafe) var body: Body!:0.0804 s

Update2: As of Swift 5.2 (Xcode 11.4), I have noticed that unowned(unsafe) has much more overhead. Here is the result now for unowned(unsafe) var body: Body!: 0.0804 s

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

Note: This is still true as of Xcode 12/Swift 5.3

推荐答案

在撰写/研究此问题时,我终于找到了解决方案.要使用简单的后向指针而不进行weakunowned的开销检查,可以将主体声明为:

As I was writing up/investigating this issue, I eventually found a solution. To have a simple back pointer without the overhead checks of weak or unowned you can declare body as:

unowned(unsafe) var body: Body!

根据Swift文档:

在需要的情况下,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.

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

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

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

So it is clear these runtime checks can produce serious overhead in performance-critical code.

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

Update: As of Swift 5.2 (Xcode 11.4), I have noticed that unowned(unsafe) has much more overhead. I now simply use strong references and break retain cycles manually, or try to avoid them entirely in performance-critical code.

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

Note: This is still true as of Xcode 12/Swift 5.3

这篇关于迅捷的弱参考比强参考慢得多的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆