objective c - Reutilizando un CGContext causando extrañas pérdidas de rendimiento
objective-c cocoa (2)
Mi clase está renderizando imágenes fuera de la pantalla. Pensé que reutilizar CGContext
lugar de crear el mismo contexto una y otra vez para cada imagen sería algo bueno. Establecí una variable miembro _imageContext
así que solo tendría que crear un nuevo contexto si _imageContext
es nula así:
if(!_imageContext)
_imageContext = [self contextOfSize:imageSize];
en lugar de:
CGContextRef imageContext = [self contextOfSize:imageSize];
Por supuesto, ya no CGContext
el CGContext
.
Estos son los únicos cambios que realicé, resulta que la reutilización del contexto ralentizó la renderización de aproximadamente 10ms a 60ms. ¿Me he perdido algo? ¿Tengo que borrar el contexto o algo antes de volver a dibujarlo? ¿O es la forma correcta de recrear el contexto para cada imagen?
EDITAR
Encontré la conexión más extraña ..
Mientras buscaba la razón por la que la memoria de la aplicación aumenta increíblemente cuando la aplicación comienza a renderizar las imágenes, descubrí que el problema era donde configuré la imagen renderizada en un NSImageView
.
imageView.image = nil;
imageView.image = [[NSImage alloc] initWithCGImage:_imageRef size:size];
Parece que ARC no está liberando el NSImage
anterior. La primera forma de evitar eso fue dibujar la nueva imagen en la anterior.
[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];
El problema de memoria había desaparecido y ¿qué pasó con el problema de CGContext? No reutilizar el contexto ahora toma 20 ms en lugar de 10 ms; por supuesto, dibujar en una imagen lleva más tiempo que solo configurarlo. Reutilizar el contexto también lleva 20 ms en lugar de 60 ms. ¿Pero por qué? No veo que pueda haber ninguna conexión, pero puedo reproducir el viejo estado donde la reutilización toma más tiempo simplemente configurando la imagen de NSImageView
lugar de dibujarla.
Investigué esto, y observo la misma ralentización. Mirar con los instrumentos configurados para muestrear las llamadas del kernel, así como también las llamadas de usuario local muestra al culpable. El comentario de @ RyanArtecona estaba en el camino correcto. Enfoqué Instrumentos en la parte inferior más userland y llamé a CGSColorMaskCopyARGB8888_sse
en dos ejecuciones de prueba (una reutilizando contextos, la otra haciendo una nueva cada vez), y luego invertí el árbol de llamadas resultante. En el caso donde el contexto no se reutiliza, veo que el rastro más grande del kernel es:
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
Este es el kernel de puesta a cero de las páginas de la memoria que se ha fallado en virtud de CGSColorMaskCopyARGB8888_sse
accediendo a ellas. Lo que esto significa es que el CGContext mapea las páginas VM para respaldar el contexto del mapa de bits, pero el kernel en realidad no hace el trabajo asociado con esa operación hasta que alguien realmente acceda a esa memoria. La asignación / falla real ocurre en el primer acceso.
Ahora veamos el rastro del kernel más pesado cuando HACEMOS reutilizar el contexto:
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
Este es el kernel copiando páginas. Mi dinero consistiría en que este sea el mecanismo subyacente de copiar y escribir que expresa el comportamiento del que habló @RyanArtecona en su comentario:
En los documentos de Apple para CGBitmapContextCreateImage, dice que la operación real de copiado de bits no ocurre hasta que se realiza más dibujo en el contexto original.
En el caso artificial que solía probar, el caso de no reutilización tardó 3392ms en ejecutarse y el caso de reutilización tardó 4693ms (significativamente más lento). Teniendo en cuenta solo el trazo más pesado de cada caso, el rastro del núcleo indica que gastamos 668.0ms en llenar nuevas páginas en el primer acceso, y 1327.0ms escribiendo en las páginas de copia en escritura en la primera escritura después de que la imagen obtiene una referencia a esas páginas. Esta es una diferencia de 659ms. Esta sola diferencia representa ~ 50% de la brecha entre los dos casos.
Entonces, para destilarlo un poco, el contexto no reutilizado es más rápido porque cuando crea el contexto, sabe que las páginas están vacías, y no hay nadie más que haga referencia a esas páginas para obligarlas a copiarse cuando escribe en ellos. Cuando reutiliza el contexto, las páginas son referenciadas por otra persona (la imagen que usted creó) y deben copiarse en la primera escritura, a fin de preservar el estado de la imagen cuando cambia el estado del contexto.
Puede explorar más a fondo lo que está sucediendo aquí mirando el mapa de la memoria virtual del proceso a medida que avanza en el depurador. vmmap
es la herramienta útil para eso.
En términos prácticos, probablemente solo deberías crear un nuevo CGContext cada vez.
Para complementar la excelente y completa respuesta de @ ipmcc, aquí hay una descripción general de la instrucción.
En los documentos de Apple para CGBitmapContextCreateImage
se establece:
El objeto
CGImage
devuelto por esta función se crea mediante una operación de copia. En algunos casos, la operación de copia realmente sigue la semántica de escritura en copia, de modo que la copia física real de los bits ocurre solo si se modifican los datos subyacentes en el contexto de gráficos de mapas de bits.
Entonces, cuando se llama a esta función, los bits subyacentes de la imagen no se pueden copiar de inmediato, y en su lugar pueden esperar a copiarse cuando se modifique el contexto del mapa de bits. Esta copia de bits puede ser costosa (dependiendo del tamaño y del espacio de color del contexto) y puede disfrazarse en un perfil de Instruments como parte de cualquier función de dibujo CGContext...
que se llame a continuación en el contexto (cuando los bits son forzados) copiar). Esto es probablemente lo que está sucediendo aquí con CGContextDrawImage
.k
Sin embargo , los documentos continúan diciendo esto:
Como consecuencia, es posible que desee utilizar la imagen resultante y soltarla antes de realizar un dibujo adicional en el contexto de gráficos de mapa de bits. De esta forma, puede evitar la copia física real de los datos.
Esto implica que si va a terminar de usar la imagen creada en la memoria (es decir, se ha guardado en el disco, enviado a través de la red, etc.) para cuando necesite hacer más dibujos en el contexto, la imagen nunca necesitaría para ser físicamente copiado en absoluto!
TL; DR
Si en algún momento necesita sacar un CGImage
de un contexto de mapa de bits, y no necesitará guardar ninguna referencia al mismo (incluso configurándolo como una imagen de UIImageView
) antes de hacer más dibujos en el contexto, entonces probablemente sea una buena idea usar CGBitmapContextCreateImage
. De lo contrario, su imagen se copiará físicamente en algún momento, lo que puede llevar un tiempo, y puede ser mejor usar un nuevo contexto cada vez.