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 s

var body: Body!: 0.0167 s

unowned body: Body!: 0.0942 s

Вы можете видеть, что использование сильной ссылки в расчете выше вместо слабой ссылки приводит к увеличению производительности более чем в 10 раз. Использование unowned помогает, но, к сожалению, все еще в 5 раз медленнее. При запуске кода через профилировщик выполняются дополнительные проверки во время выполнения, что приводит к большим накладным расходам.

Итак, вопрос в том, каковы мои варианты использования простого обратного указателя на Body без дополнительных затрат ARC. И, кроме того, почему эти накладные расходы кажутся такими экстремальными? Я полагаю, что мог бы сохранить сильный эталонный цикл и разбить его вручную. Но мне интересно, есть ли лучшая альтернатива?

Обновление: на основе ответа вот результат для
unowned(unsafe) var body: Body!: 0,0160 с.

Обновление 2: Что касается Swift 5.2 (Xcode 11.4), я заметил, что неуправляемый (небезопасный) имеет гораздо больше накладных расходов. Вот результат для unowned(unsafe) var body: Body!: 0,0804 с.

Примечание. Это по-прежнему верно для Xcode 12/Swift 5.3.


person Epic Byte    schedule 31.10.2019    source источник
comment
Не уверен, что это применимо в вашем случае, но рассматривали ли вы возможность не использовать класс в пользу Struct в идеале с копией при записи, сделанной на заказ?   -  person Kamil.S    schedule 31.10.2019
comment
@Kamil.S Да, у меня есть, и это быстрее (хотя и ненамного), но иметь общее состояние было удобнее. Хотя я мог бы вернуться к этой идее.   -  person Epic Byte    schedule 31.10.2019
comment
@EpicByte, вы провели отличное исследование! Отличный материал для короткого поста в блоге :) спасибо вам за это!   -  person m_katsifarakis    schedule 06.11.2020


Ответы (1)


Когда я писал/исследовал эту проблему, я в конце концов нашел решение. Чтобы иметь простой обратный указатель без дополнительных проверок weak или unowned, вы можете объявить тело как:

unowned(unsafe) var body: Body!

Согласно документации Swift:

Swift также предоставляет небезопасные бесхозные ссылки для случаев, когда вам нужно отключить проверки безопасности во время выполнения, например, по соображениям производительности. Как и в случае со всеми небезопасными операциями, вы берете на себя ответственность за проверку этого кода на предмет безопасности.

Вы указываете небезопасную бесхозную ссылку, написав unowned(unsafe). Если вы попытаетесь получить доступ к небезопасной бесхозной ссылке после того, как экземпляр, на который она ссылается, будет освобожден, ваша программа попытается получить доступ к ячейке памяти, где раньше находился экземпляр, что является небезопасной операцией.

Таким образом, становится ясно, что эти проверки во время выполнения могут привести к серьезным накладным расходам в критически важном для производительности коде.

Обновление. Что касается Swift 5.2 (Xcode 11.4), я заметил, что unowned(unsafe) имеет гораздо больше накладных расходов. Теперь я просто использую сильные ссылки и прерываю циклы сохранения вручную или стараюсь полностью избегать их в коде, критичном для производительности.

Примечание. Это по-прежнему верно для Xcode 12/Swift 5.3.

person Epic Byte    schedule 31.10.2019