NSArray vs. C Array performance comparison Part II - makeObjectsPerformSelector

In Part I, I compared the random access performance of a C float* to an NSMutableArray of NSNumbers, and on average the float* performed more than 400 times faster than the NSMutableArray. While those figures are accurate, they don't represent the optimal way of using NSArrays. NSNumbers can also be slow and there are faster linear enumeration methods than objectAtIndex. So I thought I'd modify the example using custom objects and sending messages to all objects, using floats in both cases. Note: these tests are conducted on an iPhone 3G as that's the target platform I was interested in.

Firstly, there are a few possible ways of iterating through an NSArray. The most obvious is the method I used in the previous post:

for(int i=0; i<count; i++) {
   // do whatever you like
   [[myArray objectAtIndex:i] doSomething];
   // do some other stuff
}

Using objectAtIndex is analogous to using traditional arrays, and allows you to access any element in any order (random access). For linear enumerations however, it is not the most efficient.

You can also use NSEnumerators:

NSEnumerator *it = [myArray objectEnumerator];
id element;
while ((element = [it nextObject])) {
   // do whatever you like
    [element doSomething];
   // do some other stuff
}

The latter was the Objective C 1.0 preferred method for readability and OO. Its performance however, is not necessarily much better than using objectAtIndex. I've read some posts which say it is worse than objectAtIndex, and other posts that say it is better. But it doesn't really matter too much anyway, because as of Objective C 2.0, there is a faster - and the new preferred method - called fast enumeration:

for(id element in myArray) {
   // do whatever you like
   [element doSomething];
   // do some other stuff
}

If however, all you need to do in the for loop, is just send the same message to all elements of the NSArray, you can go one step further and use:

[myArray makeObjectsPerformSelector:@selector(doSomething)];
// the above can be used instead of:
for(id element in myArray) [element doSomething];

There are limitations to using this, you cannot modify the array (add or remove elements) during the iteration (i.e. in doSomething). If you do not need to add or delete elements during the iteration and do not need to pass any parameters, this is the optimal way to run a particular method on all objects of an NSArray. (If you did need to send a parameter to all elements, you could also use makeObjectsPerformSelector:(SEL)aSelector withObject:(id)anObject in which the same anObject is sent to all elements).

So my new test code is below: (I'm testing 3 things: NSArrays iterated with normal for loop using objectAtIndex, NSArrays iterated with makeObjectsPerformSelector, and C Arrays):

// process using NSArray and iterating with normal for loops
-(IBAction) doNSArrayForLoop:(id)sender {
    NSLog(@"doNSArray: %@", sender);
 
    uint64_t startTime = mach_absolute_time();
    for(int i=0; i<numberOfItems; i++) {
        [[nsArray objectAtIndex:i] doSomething];
    }
    [self displayResult:mach_absolute_time() - startTime];
}
 
// process using NSArray and using makeObjectsPerformSelector to send messages to all objects
-(IBAction) doNSArrayPerformSelector:(id)sender {
    NSLog(@"doNSArray: %@", sender);
 
    uint64_t startTime = mach_absolute_time();
    [nsArray makeObjectsPerformSelector:@selector(doSomething)];
    [self displayResult:mach_absolute_time() - startTime];
}
 
// process using C Array
-(IBAction) doCArray:(id)sender {
    NSLog(@"doCArray: %@", sender);
 
    uint64_t start = mach_absolute_time();
    for(int i=0; i<numberOfItems; i++) {
        cArray[i].doSomething();
    }
    [self displayResult:mach_absolute_time() - start];
}

The results and full source are below (the ratio, is the ratio of the fastest NSArray method / the C Array). So its pretty clear that on average C Array is 6-7 times faster than the fastest NSArray method. Note: these tests are conducted on an iPhone 3G as that's the target platform I was interested in.

numberOfItems NSArray for loop (ms) NSArray PerformSelector (ms) C Array (ms) Ratio
100 0.11 0.26 0.019 5.79
166 0.24 0.29 0.030 8.00
396 0.65 0.61 0.07 8.71
574 0.92 0.58 0.092 6.30
4,789 8.9 4.4 0.73 6.03
8,237 14 8.9 1.25 7.12
20,881 36 23.3 3.3 7.06
48,467 83 51 8.2 6.22
100,000 180 104 16 6.50

MyNSObject:

@interface MyNSObject : NSObject {
    float f;
}
 
@property float f;
 
-(void) doSomething;
 
@end
 
 
 
@implementation MyNSObject
 
@synthesize f;
 
-(void) doSomething {
    f++;
}
 
 
@end

MyCppObject:

class MyCppObject {
 
public:
    float f;
 
    void doSomething() {
        f++;
    }
 
};

Full source:

@interface Array_Speed_TestViewController : UIViewController {
    int                     numberOfItems;          // number of items in array
//  float                   *cArray;                // normal c array
    MyCppObject             *cArray;                // normal c array
    NSMutableArray          *nsArray;               // ns array
    double                  machTimerMillisMult;    // multiplier to convert mach_absolute_time() to milliseconds
 
 
    IBOutlet    UISlider    *sliderCount;
    IBOutlet    UILabel     *labelCount;
 
    IBOutlet    UILabel     *labelResults;
}
 
-(IBAction) doNSArrayPerformSelector:(id)sender;
-(IBAction) doNSArrayForLoop:(id)sender;
-(IBAction) doCArray:(id)sender;
 
-(IBAction) sliderChanged:(id)sender;
 
@end
 
 
 
@implementation Array_Speed_TestViewController
 
 
// Implement viewDidLoad to do additional setup after loading the view, typically from a nib.
- (void)viewDidLoad {
    NSLog(@"viewDidLoad");
 
    [super viewDidLoad];
 
    cArray      = NULL;
    nsArray     = NULL;
 
    // read initial slider value setup accordingly
    [self sliderChanged:sliderCount];
 
    // get mach timer unit size and calculater millisecond factor
    mach_timebase_info_data_t info;
    mach_timebase_info(&info);
    machTimerMillisMult = (double)info.numer / ((double)info.denom * 1000000.0);
    NSLog(@"machTimerMillisMult = %f", machTimerMillisMult);
}
 
 
// pass in results of mach_absolute_time()
// this converts to milliseconds and outputs to the label
-(void)displayResult:(uint64_t)duration {
    double millis = duration * machTimerMillisMult;
 
    NSLog(@"displayResult: %f milliseconds", millis);
 
    NSString *str = [[NSString alloc] initWithFormat:@"%f milliseconds", millis];
    [labelResults setText:str];
    [str release];
}
 
 
 
// process using NSArray and iterating with normal for loops
-(IBAction) doNSArrayForLoop:(id)sender {
    NSLog(@"doNSArray: %@", sender);
 
    uint64_t startTime = mach_absolute_time();
    for(int i=0; i<numberOfItems; i++) {
        [[nsArray objectAtIndex:i] doSomething];
    }
    [self displayResult:mach_absolute_time() - startTime];
}
 
// process using NSArray and using makeObjectsPerformSelector to send messages to all objects
-(IBAction) doNSArrayPerformSelector:(id)sender {
    NSLog(@"doNSArray: %@", sender);
 
    uint64_t startTime = mach_absolute_time();
    [nsArray makeObjectsPerformSelector:@selector(doSomething)];
    [self displayResult:mach_absolute_time() - startTime];
}
 
// process using C Array
-(IBAction) doCArray:(id)sender {
    NSLog(@"doCArray: %@", sender);
 
    uint64_t start = mach_absolute_time();
    for(int i=0; i<numberOfItems; i++) {
        cArray[i].doSomething();
    }
    [self displayResult:mach_absolute_time() - start];
}
 
 
// allocate NSArray and C Array 
-(void) allocateArrays {
    NSLog(@"allocateArrays");
 
    // allocate c array
    if(cArray) delete cArray;
    cArray = new MyCppObject[numberOfItems];
 
    // allocate NSArray
    [nsArray release];
    nsArray = [[NSMutableArray alloc] initWithCapacity:numberOfItems];
 
 
    // fill with random values
    for(int i=0; i<numberOfItems; i++) {
        // add number to c array
        cArray[i].f = random() * 1.0f/(RAND_MAX+1);
 
 
        // add number to NSArray
        MyNSObject *myObj = [[MyNSObject alloc] init];
        myObj.f = cArray[i].f;
 
        [nsArray addObject:myObj];
        [myObj release];
    }
 
}
 
 
// callback for when slider is changed
-(IBAction) sliderChanged:(id)sender {
    numberOfItems = sliderCount.value;
    NSLog(@"sliderChanged: %@, %i", sender, numberOfItems);
 
    NSString *str = [[NSString alloc] initWithFormat:@"%i items", numberOfItems];
    [labelCount setText:str];
    [str release];
 
    [self allocateArrays];
}
 
 
//cleanup
- (void)dealloc {
    [nsArray release];
    if(cArray) delete cArray;
 
    [super dealloc];
}
 
@end

NSArraySpeedTest2.PNG
AttachmentSize
Xcode Project + source Part II25.23 KB