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
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
orunowned
you can declare body as:According to the Swift documentation:
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