Подтвердить что ты не робот

Повторное использование CGContext, вызывающее нечетные потери производительности

Мой класс отображает изображения вне экрана. Я думал, что повторно использовать CGContext вместо того, чтобы создавать один и тот же контекст снова и снова для каждого изображения, было бы хорошо. Я установил переменную-член _imageContext, поэтому мне нужно было бы создать новый контекст, если _imageContext равен нулю:

if(!_imageContext)
    _imageContext = [self contextOfSize:imageSize];

вместо:

CGContextRef imageContext = [self contextOfSize:imageSize];

Конечно, я больше не выпускаю CGContext.

Это единственные изменения, которые я сделал, получается, что повторное использование контекста замедляло рендеринг от примерно 10 мс до 60 мс. Я что-то пропустил? Должен ли я очистить контекст или что-то еще, прежде чем в него вставлять? Или это правильный способ воссоздать контекст для каждого изображения?

ИЗМЕНИТЬ

Найдено самое странное соединение..

В то время как я искал причину, по которой приложение быстро растет, когда приложение начинает рендеринг изображений, я обнаружил, что проблема заключается в том, где я устанавливаю отображаемое изображение на NSImageView.

imageView.image = nil;
imageView.image = [[NSImage alloc] initWithCGImage:_imageRef size:size];

Похоже, что ARC не выпускает предыдущий NSImage. Первый способ избежать этого - нарисовать новое изображение в старом.

[imageView.image lockFocus];
[[[NSImage alloc] initWithCGImage:_imageRef size:size] drawInRect:NSMakeRect(0, 0, size.width, size.height) fromRect:NSZeroRect operation:NSCompositeSourceOver fraction:1.0];
[imageView.image unlockFocus];
[imageView setNeedsDisplay];

Проблема с памятью исчезла и что случилось с проблемой CGContext -reuse? Не повторное использование контекста теперь занимает 20 мс вместо 10 мс - конечно, рисование в изображение занимает больше времени, чем просто его установка. Повторное использование контекста также занимает 20 мс вместо 60 мс. Но почему? Я не вижу, что может быть какая-либо связь, но я могу воспроизвести старое состояние, в котором повторное использование занимает больше времени, просто установив NSImageView изображение вместо его рисования.

4b9b3361

Ответ 1

Я исследовал это, и наблюдаю такое же замедление. Глядя на инструменты, установленные для выборки вызовов ядра, а также вызовы пользовательской правки, вы видите виновника. Комментарий @RyanArtecona был на правильном пути. Я сфокусировал инструменты на нижнем большинстве пользовательских вызовов CGSColorMaskCopyARGB8888_sse в двух тестовых прогонах (один раз повторное использование контекстов, а другой - новый каждый раз), а затем инвертировал результирующее дерево вызовов. В случае, когда контекст не используется повторно, я вижу, что самая тяжелая трассировка ядра:

Running Time    Self            Symbol Name
668.0ms   32.3% 668.0           __bzero
668.0ms   32.3% 0.0              vm_fault
668.0ms   32.3% 0.0               user_trap
668.0ms   32.3% 0.0                CGSColorMaskCopyARGB8888_sse

Это ядро, обнуляющее страницы памяти, которые сбиваются с помощью CGSColorMaskCopyARGB8888_sse доступа к ним. Это означает, что CGContext сопоставляет страницы VM для поддержки растрового контекста, но ядро ​​фактически не выполняет работу, связанную с этой операцией, пока кто-то фактически не обратится к этой памяти. Фактическое отображение/ошибка происходит при первом доступе.

Теперь рассмотрим самую тяжелую ядерную трассировку, когда мы повторно используем контекст:

Running Time            Self            Symbol Name
1327.0ms   35.0%        1327.0          bcopy
1327.0ms   35.0%        0.0              user_trap
1327.0ms   35.0%        0.0               CGSColorMaskCopyARGB8888_sse

Это страницы копирования ядра. Мои деньги были бы на этом, являющемся основным механизмом копирования на запись, который обеспечивает поведение @RyanArtecona, о котором говорится в его комментарии:

В документах Apple для CGBitmapContextCreateImage говорится, что бит-копирование не выполняется, пока больше исходный контекст.

В надуманном случае, который я использовал для тестирования, случай повторного использования занял 3392 мс для выполнения, а случай повторного использования занял 4693 мс (значительно медленнее). Учитывая только самую тяжелую трассировку из каждого случая, трассировка ядра показывает, что мы тратим 668,0 млн. Нулей, заполняя новые страницы при первом доступе, и 1327,0 мс записывают на страницы копирования на запись при первой записи после того, как изображение получает ссылку на эти страницы. Это разница в 659 мс. Только одна разница составляет ~ 50% разрыва между этими двумя случаями.

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

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

Практически, вы должны, вероятно, просто создавать новый CGContext каждый раз.

Ответ 2

Чтобы дополнить @ipmcc превосходный и тщательный ответ, вот обзор инструкций.

В Apple docs для CGBitmapContextCreateImage сказано:

Объект CGImage, возвращаемый этой функцией, создается копией операция. В некоторых случаях копия операция действительно следует семантике copy-on-write, так что фактическая физическая копия бит происходит только в том случае, если базовые данные в растровый графический контекст.

Итак, когда эта функция вызывается, базовые биты изображения не могут быть скопированы сразу и могут вместо этого ждать копирования, когда следующий битмап-контекст будет изменен. Это битовое копирование может быть дорогостоящим (в зависимости от размера и цветового пространства контекста) и может замаскироваться в профиле инструментов как часть любой функции рисования CGContext..., которая вызывается следующим в контексте (когда биты вынуждены копия). Вероятно, это происходит здесь с CGContextDrawImage.k

Тем не менее, документы продолжаются, чтобы сказать следующее:

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

Это означает, что если вы закончите использовать созданное в памяти изображение (то есть оно было сохранено на диске, отправлено по сети и т.д.) к моменту, когда вам понадобится сделать больше рисования в контексте, изображение никогда не нужно было бы физически копировать вообще!

TL; DR

Если в какой-то момент вам нужно вывести CGImage из контекста растрового изображения, и, вам не нужно будет ссылаться на него (включая настройку его как изображения UIImageView), прежде чем вы сделаете больше рисования в контексте, то, вероятно, неплохо использовать CGBitmapContextCreateImage. Если нет, ваше изображение будет физически скопировано в какой-то момент, что может занять некоторое время, и может быть лучше просто использовать новый контекст каждый раз.