Skip to content

Efficient Scalar Attributes in Core Data

2010-10-31

I spent some time today on improving performance in my iOS development project, and I came up with some results that may be of interest to others working with Core Data. Note that the hacks demonstrated below are based on Time Profiler measurements taken on my (egregiously unoptimized) app under development. The underlying “performance issue” only occurs when you’re using standard Core Data scalar accessors unusually frequently, like my app did. It is overwhelmingly likely that your application does not exhibit the same behavior; therefore, please use Instruments to verify that read accessors are actually slowing down your app before applying any of these tweaks.

Standard accessors

Implementing accessors for scalar attributes in Core Data is somewhat inconvenient, as the framework only generates primitive accessors for scalars, and you’re supposed to write the normal accessors yourself, complete with correct access and change notifications.

The Core Data Programming Guide recommends (in the Scalar Values section) that you implement scalar accessors like this:

@interface MyManagedObject : NSManagedObject {
}
@property (nonatomic, assign) float x;
@property (nonatomic, assign) float primitiveX;
@end

@implementation
@dynamic primitiveX;

- (float)x {
    [self willAccessValueForKey:@"x"];
    float value = self.primitiveX;
    [self didAccessValueForKey:@"x"];
    return value;
}

- (void)setX:(float)value {
    [self willChangeValueForKey:@"x"];
    self.primitiveX = value;
    [self didChangeValueForKey:@"x"];
}
@end

This works correctly, but if you’re using your attributes frequently, you may find that these accessors are surprisingly expensive. For example, I have an application that stores images in a Core Data database. The images consist of sets of bezier curves that are represented by a handful of model classes, each having several scalar attributes for the positions of their bezier control points. To debug performance issues, I wrote a little test case that repeatedly renders a complex image on a Core Graphics bitmap context. I knew that calling all those notifications can’t be very fast, but I was still surprised when I found that when I used accessors that follow the pattern above, those innocent-looking willAccessValueForKey: calls took close to a third of my app’s entire running time, more than all the graphics operations:

Apparently willAccessValueForKey: is somewhat slow because it needs to look up its key parameter in a table, and this lookup isn’t very efficient. (Judging by the topmost method name above, it looks like Core Data may support other (perhaps more efficient) mapping strategies. However, I don’t think there is a way to override the strategy that the framework chooses by default.)

Now obviously my app is somewhat of a pathological case, since (in its current unoptimized state) it enumerates all attributes of all objects multiple times during rendering, resulting in tens of thousands of willAccessValueForKey: calls to draw just a few hundred Bezier paths. But still, it is worth to find out what we can do to eliminate this overhead.

Replace access notifications with explicit fetches

As far as I know, willAccessValueForKey: is only used to fire faults when the object is not yet fully loaded into memory, and didAccessValueForKey: is simply a no-op. Thus, if you can be sure that your objects aren’t faults, then you can safely elide all willAccessValueForKey:/didAccessValueForKey: calls. One way of ensuring that there are no faults is to prefetch all objects before using them. (Prefetching also greatly improves performance when you’re iterating over a large set of items, like my drawing app does. With no prefetching, the profiler results above would have been swamped by Core Data using separate SQLite roundtrips to fetch each individual object.) Unfortunately it’s relatively easy to mistakenly prefetch less than you need, and accessing attribute values without firing the fault will result in bogus values that can be hard to debug.

For a more robust solution than simply removing access notifications, replace them by explicit fault checks. When a fault is discovered, trigger it before touching any attributes. To trigger a fault, simply call willAccessValueForKey: with a nil key. (Note that this only applies to read accessors; write accessors must keep using the original template above. You definitely don’t want to disable change notifications, because KVO and Core Data’s critical change tracking infrastructure rely on them.)

@interface MyManagedObject : NSManagedObject {
}
@property (nonatomic, assign) float x;
@property (nonatomic, assign) float primitiveX;
- (void)fetch;
@end

@implementation
@dynamic primitiveX;

- (void)fetch {
    // Fire the fault.
    [self willAccessValueForKey:nil];
    [self didAccessValueForKey:nil];
}

- (float)x {
    if ([self isFault])
        [self fetch];
    return self.primitiveX;
}

- (void)setX:(float)value {
    [self willChangeValueForKey:@"x"];
    self.primitiveX = value;
    [self didChangeValueForKey:@"x"];
}
@end

Use instance variables

While we are tweaking accessors, it may be worthwhile to represent performance-critical scalar attributes as instance variables, and access them directly whenever possible. To do this, simply declare the backing ivars, and override the primitive accessors so that they get and set attribute values using them. In the normal accessors, you can inline the primitive accessors instead of calling them to shave off a few extra nanoseconds spent in objc_msgsend.

The final code sample below uses ivars and also keeps track of the fault state in a separate boolean instance variable to let the read accessors work without any additional method calls in the usual case.

@interface MyManagedObject : NSManagedObject {
    float _x;
    BOOL _fetched;
}
@property (nonatomic, assign) float x;
@property (nonatomic, assign) float primitiveX;
- (void)fetch;
@end

@implementation
- (void)fetch {
    // Fire the fault.
    [self willAccessValueForKey:nil];
    [self didAccessValueForKey:nil];
    _fetched = YES;
}

- (void)didTurnIntoFault {
    _fetched = NO;
    [super didTurnIntoFault];
}

- (float)primitiveX {
    return _x;
}

- (void)setPrimitiveX:(float)value {
    _x = value;
} 

- (float)x {
    if (!_fetched)
        [self fetch];
    return _x;
}

- (void)setX:(float)value {
    [self willChangeValueForKey:@"x"];
    self.primitiveX = value;
    [self didChangeValueForKey:@"x"];
}
@end

Results

I implemented my Bezier path accessors to follow this last pattern, and additionally I changed my drawing methods to use ivars directly when possible (after a _fetched test) instead of calling accessors at all. At the end, I got this rather healthier looking Time Profiler output:

The app’s running time is now dominated by bounding box calculations (rcu) and drawing operations, as expected. I can push down willAccessValueForKey: even further later by converting a few more attributes.

It is generally a bad idea to ignore the official way of doing things on iOS, but in my particular case the official approach turned out to be so slow that I had to find alternatives. As far as I can see, these hacks will continue to work fine until and unless Apple introduces new Core Data features that rely on access notifications. However, applying them in your project is a still a possible maintenance headache that is not worth risking unless you are 100% certain that standard read accessors have a measurable adverse effect on your overall performance. Don’t waste time and effort optimizing code that isn’t a performance bottleneck, especially not on the advice of random guys on the intertubes! If you do decide to use some of the above tweaks, use Instruments to measure performance before and after implementing all optimizations, and don’t be afraid to roll back your changes when (not if) they turn out to have no measurable effect.

About these ads

From → Uncategorized

One Comment
  1. Daniel Doubleday permalink

    Thanks! That helped

Comments are closed.

Follow

Get every new post delivered to your Inbox.

%d bloggers like this: