Swift использует стратегию оптимизации, называемую Copy-on-Write (COW), для типов значений, которые оборачивают хранилище, выделенное в куче. Вместо того чтобы сразу выполнять глубокое копирование при присваивании, язык откладывает дублирование до тех пор, пока экземпляр фактически не будет изменен. Это достигается за счет того, что тип значения внутренне ссылается на общий экземпляр class, используя функцию времени выполнения isKnownUniquelyReferenced для определения, когда количество ссылок равно одному. Когда происходит мутация и ссылка уникальна, буфер изменяется на месте; в противном случае создается копия перед записью, что сохраняет семантику значений без штрафов по производительности от «неторопливого» копирования.
Наша команда разрабатывала высокопроизводительный конвейер обработки изображений, где мы определили пользовательский Image struct, оборачивающий большой CVPixelBuffer. Проблема возникла во время профилирования: применение каждого фильтра создавало три промежуточные копии 4K изображений, вызывая выделение 300 МБ памяти на кадр и выдавая предупреждения о нехватке памяти на устройствах iPad.
Мы рассмотрели три различных подхода к решению этой проблемы. Первый подход заключался в преобразовании Image из struct в class. Это полностью устранило копии, используя семантику ссылок, но привело к серьезным ошибкам потоковой безопасности, когда несколько цепочек обработки случайно делили и изменяли одни и те же данные пикселей одновременно, что вызывало визуальные артефакты и состояния гонки, которые было трудно отладить.
Второй подход сохранил обозначение struct, но реализовал ручное глубокое копирование с использованием UnsafeMutablePointer и memcpy оптимизаций. Это обеспечивало безопасность за счет строгой семантики значений, но профилирование показало, что он потреблял на 800% больше времени ЦП, чем наша цель, так как каждый аргумент функции вызывал выделение памяти объемом 12 МБ и битовую операцию копирования.
Третий подход реализовал семантику Copy-on-Write вручную. Мы создали приватный ImageBuffer class для хранения фактического CVPixelBuffer, сделали Image struct хранителем ссылки на этот класс и реализовали все мутирующие методы, чтобы проверить isKnownUniquelyReferenced перед изменением:
final class ImageBuffer { var pixels: CVPixelBuffer init(_ buffer: CVPixelBuffer) { self.pixels = buffer } } struct Image { private var buffer: ImageBuffer mutating func applyFilter(_ filter: Filter) { if !isKnownUniquelyReferenced(&buffer) { buffer = ImageBuffer(buffer.pixels.deepCopy()) } filter.process(buffer.pixels) } }
Если ссылка не была уникальной, мы сначала дублировали буфер. Мы выбрали это решение, потому что оно сохранило безопасность семантики значений Swift, устраняя ненужные копии во время операций только для чтения.
Результат снизил нагрузку на память на 94% и улучшил время обработки кадров с 120 мс до 18 мс на изображение, позволяя приложению обрабатывать видеопотоки в реальном времени без термального троттлинга на устаревшем оборудовании.
Почему мы не можем вручную проверять количество ссылок вместо использования isKnownUniquelyReferenced?
Многие кандидаты предлагают отслеживать количество ссылок вручную или сравнивать адреса памяти. Однако isKnownUniquelyReferenced — это не просто проверка счетчиков; он включает вставленные компилятором барьеры, предотвращающие тактику оптимизации, которая может перепорядочивать операции с памятью. Без этой внутренней особенности компилятор может оптимизировать проверку уникальности, или время выполнения может возвращать ложные положительные результаты из-за взаимодействия времени выполнения Objective-C или преобразований с поддержкой дополнительных неназначенных ссылок, которые невидимы для стандартного подсчета ARC.
Как COW взаимодействует с соблюдением эксклюзивности в Swift?
Кандидаты часто считают, что COW работает автоматически для всех типов значений, содержащих классы. Они упускают из виду, что правила эксклюзивности Swift требуют, чтобы мутации имели исключительный доступ. При реализации пользовательского COW проверка isKnownUniquelyReferenced должна проходить до начала мутации, а замена буфера должна происходить атомарно относительно проверки. Нарушение этого правила, удерживая несколько ссылок во время проверки, может привести к нарушениям эксклюзивности во время выполнения или вызвать ложные отрицательные результаты в обнаружении уникальности.
Когда COW не предотвращает копирование в условиях параллельности?
С моделью конкуренции Swift 5.5 кандидаты предполагают, что COW обеспечивает потокобезопасную мутацию. Однако COW обеспечивает безопасность только в рамках одного потока. Когда значения передаются через границы актеров или помечаются как Sendable, компилятор может вынудить «неторопливое» копирование, чтобы поддерживать изоляцию. Кроме того, если в базовом классе находятся объекты Objective-C, isKnownUniquelyReferenced может осторожно возвращать ложные значения из-за реализации слабых ссылок в Objective-C, вызывая ненужные копии, требующие структурирования модели владения для оптимизации.