重用CGContext导致奇怪的性能损失

时间:2012-12-28 15:33:32

标签: objective-c cocoa core-graphics cgcontext

我的课程是在屏幕外渲染图像。我想重复使用CGContext而不是一次又一次地为每个图像创建相同的上下文将是一件好事。我设置了一个成员变量_imageContext,所以如果_imageContext为零,我只需要创建一个新的上下文:

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

而不是:

CGContextRef imageContext = [self contextOfSize:imageSize];

当然我不再发布CGContext了。

这是我所做的唯一改变,结果是重用上下文会使渲染速度从大约10ms减慢到60ms。我错过了什么吗?在重新插入之前,我是否必须清除上下文或其他内容?或者它是为每个图像重建上下文的正确方法吗?

修改

找到最奇怪的连接..

当我在应用程序开始渲染图像时,我正在搜索应用程序内存增加的原因时,我发现问题在于我将渲染图像设置为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 - 重用问题发生了什么? 不重复使用上下文现在需要20ms而不是10ms - 当然,绘制到图像所需的时间比设置它要长。 重用上下文也需要20ms而不是60ms。但为什么?我没有看到可能存在任何连接,但我可以通过设置NSImageView的图像而不是绘制图像来重现旧的状态,其中重用需要更多时间。

2 个答案:

答案 0 :(得分:15)

我对此进行了调查,并观察到同样的减速情况。使用设置为内核调用示例的仪器以及用户空调调用显示了罪魁祸首。 @ RyanArtecona的评论是在正确的轨道上。我在两次测试运行(一次重用上下文,另一次每次重新创建一次)中将Instruments聚焦在最底层的用户空间调用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在他的评论中所讨论的行为:

  

在CGBitmapContextCreateImage的Apple文档中,它说的是实际的   直到在更多绘图上完成才会发生位复制操作   原始背景。

在我曾经测试的设计案例中,非重用案例需要3392ms才能执行,重用案例需要4693ms(明显更慢)。考虑到每种情况下单个最重的跟踪,内核跟踪表明我们在第一次访问时花费668.0ms零填充新页面,并且在图像获得引用后在第一次写入时写入写入时复制页面1327.0ms那些页面。这相差659ms。仅这一个差异就占了两个案例之间差距的约50%。

因此,要稍微提炼一下,非重用的上下文会更快,因为当您创建上下文时,它知道页面是空的,并且没有其他人提到这些页面以强制它们被复制时你写信给他们。当您重复使用上下文时,页面会被其他人(您创建的图像)引用,并且必须在第一次写入时复制,以便在上下文状态发生变化时保留图像的状态。

通过在调试器中逐步查看进程的虚拟内存映射,您可以进一步了解此处发生的情况。 vmmap是有用的工具。

实际上,你应该每次只创建一个新的CGContext。

答案 1 :(得分:8)

为了补充@ ipmcc的优秀和彻底的答案,这里是一个教学概述。

Apple docs for CGBitmapContextCreateImage中声明:

  

此函数返回的CGImage对象由副本创建   操作。在某些情况下,副本   操作实际上遵循copy-on-write语义,以便实际   只有当底层数据存在时才会发生位的物理副本   位图图形上下文被修改。

因此,当调用此函数时,可能不会立即复制图像的基础位,而是可能在下次修改位图上下文时等待复制。这种位复制可能很昂贵(取决于上下文的大小和颜色空间),并且可能在Instruments配置文件中将其伪装成在上下文中被调用的任何CGContext...绘制函数的一部分(当位是被迫复制)。这可能是CGContextDrawImage。k

所发生的事情

然而,文档继续这样说:

  

因此,您可能希望使用生成的图像并发布   在您对位图图形执行其他绘制之前   上下文。这样,就可以避免实际的物理副本了   数据

这意味着如果您需要使用内存中创建的图像(即已将其保存到磁盘,通过网络发送等)已完成,那么您需要执行更多操作在上下文中绘制,图像根本不需要进行物理复制!

TL; DR

如果在某些时候你需要从位图上下文中提取CGImage ,则不需要保留对它的任何引用(在您在上下文中进行任何绘制之前,包括将其设置为UIImageView的图像),那么使用CGBitmapContextCreateImage可能是个好主意。如果没有,您的图像将在某个时刻进行物理复制,这可能需要一段时间,而且每次只使用新的上下文可能会更好。