Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Swift Weak Reference Much Slower than Strong Reference

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
    ...
}

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)

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

var body: Body!: 0.0167 s

unowned body: Body!: 0.0942 s

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.

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?

Update: Based on the answer, here is the result for
unowned(unsafe) var body: Body!: 0.0160 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

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

like image 613
Epic Byte Avatar asked Oct 31 '19 00:10

Epic Byte


People also ask

Why unowned is faster than weak?

"The only difference between unowned and weak, is performance. Since unowned has no checking, it is faster.

What is difference between weak and strong in Swift?

For example, a strong reference keeps a firm hold on instances and doesn't allow deallocation by ARC. Similarly, a weak reference cannot protect the instances from being deallocated by ARC. Before you learn about strong and weak reference, make sure to understand how classes and objects work in Swift.

What is the difference between strong and weak references?

The key difference between a strong and a weak or unowned reference is that a strong reference prevents the class instance it points to from being deallocated. That is very important to understand and remember. ARC keeps track of the number of strong references to a class instance.

Why do we use weak in Swift?

The only reason you need to use weak in Swift coding is to avoid strong reference cycles. In short, a strong reference cycle or “retain cycle” is 2 instances holding onto each other. They cannot be removed from memory, which causes a memory leak, which could crash your app, which is a bad user experience.


1 Answers

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!

According to the Swift documentation:

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

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

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.

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

like image 156
Epic Byte Avatar answered Sep 28 '22 06:09

Epic Byte