On the
Cocoa-Dev mailing list, John Nairn asks
why rectangle fills are so much slower in Cocoa than they are in Carbon. Essentially, he has a custom NSView which contains a large number of elements (100 to 100,000) that may need to be drawn. Each element responds to
-stroke: to do its own drawing, which apparently consists of filling a rectangle.
Here's my answer to John, slightly edited compared to what I posted in response on the mailing list:
There are substantial differences in the graphics architectures between QuickDraw and CoreGraphics.
In QuickDraw, you're always working in pixel coordinates. In the worst case, your coordinates have to be translated from port to global coordinates, and then your rectangle can be filled.
In CoreGraphics, you're always working in an abstract "user space." Coordinates in user space are mapped to device space via the concatenation of two translation matrices; they're mapped from user space to ideal space (with 72 points per inch) by the current transformation matrix (CTM) and from ideal space to device space via another matrix. Right now, in CoreGraphics, when drawing to the screen ideal and device space are the same thing. But they don't have to be, and aren't when printing.
OK, so instead of one simple addition per coordinate for your rectangle, you have a matrix multiply. That costs a bit right there, but it gets you a
lot of flexibility.
There's even more to filling a "simple" rectangle in CoreGraphics though. QuickDraw will fill your rectangle with a simple pattern. CoreGraphics patterns are much more complex — there's a lot more they can do. CoreGraphics cares about line joins and end caps and line patterns and mitre limits and a whole lot of other arcane bits. In QuickDraw, you're fundamentally just setting bits; in CoreGraphics, you aren't, you're creating path objects and manipulating them, which results in bits being set. You're working at a higher level of abstraction.
Essentially, an implementation of NSRectFill looks something like this:
void NSRectFill(NSRect rect) {
NSBezierPath *path = [NSBezierPath bezierPathWithRect:rect];
[path fill];
}It's creating a path, filling it, and then tossing it out. That's expensive.
One way to speed up your code would be to create a real NSBezierPath for the rectangle, and keep the path itself around. You can then replace your NSRectFill(ptRect) with something like this:
[cachedPath removeAllPoints];
[cachedPath appendBezierPathWithRect:ptRect];
[cachedPath fill];
Another thing you could do is keep a unit square path around, and then just scale & translate the coordinate system as necessary before filling your unit square path. That's what's happening behind the scenes anyway; you could "cut out the middleman" and only do the work you really need to do by going that route. I'm not sure whether or not that'd be faster, but it might since all you'd be doing is manipulating the CTM.
Another thing you should definitely do to speed up your code is only draw those portions of your view that actually need to be redrawn. Do an intersection test with the rectangle passed to your view's
-drawRect: method, and only draw those that intersect with it. Also, be careful not to use
-setNeedsDisplay: but rather
-setNeedsDisplayInRect: to tell the AppKit that a view needs to be refreshed. That will let the AppKit determine the optimal rectangle to update; it won't try to update everything all the time. This would probably require that your elements keep track of their own radius, rather than asking the sender of
-stroke:; either that, or the sender of
-stroke: should also keep track of the current rectangle passed to
-drawRect: and allow your elements to look at it to determine whether they need to be drawn.
I hope this gives you some idea as to why what you're doing has different performance characteristics with Cocoa/CoreGraphics and QuickDraw, and gives you some strategies for ways you can optimize your drawing.
CoreGraphics does a
lot more than QuickDraw and is quite a nice architecture, but it's not without its costs.