Swift Weak Reference Намного медленнее, чем Strong Reference
Я создаю физический движок на 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)
Полученные результаты
Ниже приведены эталонные времена для каждого эталонного типа. В каждом тестеbody
ссылка на Shape
класс был изменен. Код был построен с использованием режима выпуска [-O] с Swift 5.1, нацеленным на macOS 10.15.
weak var body: Body!
: 0,1886 с
var body: Body!
: 0,0167 с
unowned body: Body!
: 0,0942 с
Вы можете видеть, что использование сильной ссылки в вычислении выше вместо слабой приводит к более чем 10-кратному увеличению производительности. С помощьюunowned
помогает, но, к сожалению, все равно в 5 раз медленнее. При запуске кода через профилировщик, похоже, выполняются дополнительные проверки во время выполнения, что приводит к большим накладным расходам.
Итак, вопрос в том, какие у меня варианты иметь простой обратный указатель на Body, не неся накладные расходы ARC. Более того, почему эти накладные расходы кажутся такими чрезмерными? Полагаю, я мог бы сохранить цикл сильных ссылок и разорвать его вручную. Но мне интересно, есть ли лучшая альтернатива?
Обновление: на основе ответа вот результат дляunowned(unsafe) var body: Body!
: 0,0160 с
Update2: Начиная с Swift 5.2 (Xcode 11.4), я заметил, что unowned(небезопасный) имеет гораздо больше накладных расходов. Вот результат дляunowned(unsafe) var body: Body!
: 0,0804 с
Примечание: это все еще верно для Xcode 12/Swift 5.3.
1 ответ
Когда я писал / исследовал эту проблему, я в конце концов нашел решение. Чтобы иметь простой обратный указатель без дополнительных проверокweak
или unowned
вы можете объявить тело как:
unowned(unsafe) var body: Body!
Согласно документации Swift:
Swift также предоставляет небезопасные незарегистрированные ссылки для случаев, когда вам нужно отключить проверки безопасности во время выполнения - например, по соображениям производительности. Как и во всех небезопасных операциях, вы берете на себя ответственность за проверку этого кода на предмет безопасности.
Вы указываете небезопасную ссылку без владельца, написав unowned(небезопасно). Если вы попытаетесь получить доступ к небезопасной незарегистрированной ссылке после того, как экземпляр, на который она ссылается, будет освобожден, ваша программа попытается получить доступ к тому месту в памяти, где раньше находился экземпляр, что является небезопасной операцией.
Таким образом, очевидно, что эти проверки во время выполнения могут привести к серьезным накладным расходам в критически важном для производительности коде.
Обновление: Начиная с Swift 5.2 (Xcode 11.4), я заметил, чтоunowned(unsafe)
имеет гораздо больше накладных расходов. Теперь я просто использую строгие ссылки и вручную прерываю циклы сохранения или пытаюсь полностью их избежать в критичном к производительности коде.
Примечание: это все еще верно для Xcode 12/Swift 5.3.