October 19, 2012

A Massive Speed Difference In iPhone CoreData

It was brought to my attention last night by my good friend, Collin
Donnel
, that using a fetch request to grab
objects based on their objectID’s was much slower than simply using a
for loop and the NSManagedObjectContext method
-existingObjectWithID:error: to grab each object one by one. My initial
response was supreme disbelief. How could this be true? I needed a break
today from my pile of work to do, so I thought I would investigate and
see what’s up.

I started with a dummy project to create a scenario I could measure. You
can find that project here. I just created
a single-view app with 2 buttons, one to trigger a fetch with a
predicate and one to trigger finding objects with a for loop. When the
app launched, it creates 1000 objects to have some test data. Nothing
fancy, a simple object with a few properties. The code for these two
fetch methods are fairly straightforward as you can see here:

+ (NSArray *)fetchRequestobjectsWithIDs:(NSArray *)objectIDs inContext:(NSManagedObjectContext *)context
{
    NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:[self entityName]];
    [request setPredicate:[NSPredicate predicateWithFormat:@"SELF in %@", objectIDs]];
    
    NSError *fetchError = nil;
    NSArray *results = [context executeFetchRequest:request error:&fetchError];
    if(!results){
        NSLog(@"Error fetching: %@", fetchError);
    }
    return results;
}

+ (NSArray *)forLoopObjectsWithIDs:(NSArray *)objectIDs inContext:(NSManagedObjectContext *)context
{
    NSMutableArray *objects = [NSMutableArray arrayWithCapacity:[objectIDs count]];
    [objectIDs enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
        NSError *findError = nil;
        NSManagedObject *object = [context existingObjectWithID:obj error:&findError];
        if(object){
            [objects addObject:object];
        }else{
            NSLog(@"Error finding object %@", findError);
        }
    }];
    return objects;
}

I ran this test project on my iPhone 4 (I know, so old) and was quite
surprised by the results. The trace it generated looks like this:

The Speed TestTrace

The first bit of activity there is the app launching, and the 1000
objects getting created. Not interesting for the purpose of this
article. That big spike there is the fetch request. In between those 2
spikes, the tiny bit you can barely see, is the for loop version.
Amazing right? Which do you want in your app, that activity spike to
fetch 1000 objects? That tiny bit of CPU usage you can hardly see on the
timeline?

Now go forth and enjoy your faster app! Also, be sure to buy a copy of
Collin’s app,
Pinbook
to say thanks for the tip.