Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is this an inefficient way of using fast enumeration?

I don't entirely understand the details of how fast enumeration works, but compare the following two cases:

for(NSObject *object in self.myParent.parentsParents.granfathersMother.cousin.unclesNephew.array) {
    // do something
}

vs.

NSArray *array = self.myParent.parentsParents.granfathersMother.cousin.unclesNephew.array;
for(NSObject *object in array) {
     // do something
 }

In the first example, will it go through that entire chain every iteration to get the array? Should I be using the second way?

like image 740
Snowman Avatar asked Dec 26 '22 18:12

Snowman


2 Answers

I was at WWDC when Apple introduced Fast Enumeration, and (I recall) we were told then that the right hand object is moved into a temp. In addition, it must be since this works:

for(id foo in [myCollection reverseObjectEnumerator])

You can see that collections that perform fast enumeration adopt the "Fast Enumeration Protocol" (NSFastEnumeration), which has one method:

– countByEnumeratingWithState:objects:count:

That method returns a C Array of objects that lets the enumeration go very quickly, again supporting the one time use of the right side.


Now, having said all that, currently Apple advises developers (at WWDC) to use the block enumeration, which they claim is both faster and generates less code:

[myCollection enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop)
{
... your code
} ];

What I am fond of doing is not using "id obj", but the actual type (to avoid a cast in the block):

[myCollection enumerateObjectsUsingBlock:^(NSDictionary *dict, NSUInteger idx, BOOL *stop)
{
... your code
} ];

Neither the compiler nor the analyzer (4.4) complains when I do this.

If you need to set a variable outside this method, then you have to make it a block variable:

__block int foo = 0;
[myCollection enumerateObjectsUsingBlock:^(NSNumber *num, NSUInteger idx, BOOL *stop)
{
   foo = MAX(foo, [num integerValue]);
} ];

EDIT: as a clarification, the direct answer to your question is 'no', the statement 'self.myParent.parentsParents.granfathersMother.cousin.unclesNephew.array' is evaluated once, and the final object stored as a temp on the stack. Also, you can use the same technique with block enumeations - the statement is evaluated once and the final returned object used for the enumeration.

__block int foo = 0;
[self.myParent.parentsParents.granfathersMother.cousin.unclesNephew.array enumerateObjectsUsingBlock:^(NSNumber *num, NSUInteger idx, BOOL *stop)
{
   foo = MAX(foo, [num integerValue]);
} ];

EDIT2: I found another thread on SO where this same topic was discussed. The one point I missed regarding block enumeration is that you can specify that they should be run concurrently (or in reverse) using the slightly more complex method:

enumerateObjectsWithOptions:usingBlock:

As iOS devices get more and more core's this could potentially be a big win depending on what you're doing.

@bbum's response to the question (and others too) are here.

like image 57
David H Avatar answered Jan 11 '23 18:01

David H


That's probably compiler-specific (i.e. undefined). If you are that bothered then add some timing code and find out yourself:

#import <sys/time.h>

static unsigned getTickCount()
{
    struct timeval tv;
    gettimeofday(&tv, 0);
    return (unsigned)((tv.tv_sec * 1000) + (tv.tv_usec / 1000));
}

...

unsigned startTime = getTickCount();
for(NSObject *object in self.myParent.parentsParents.granfathersMother.cousin.unclesNephew.array) {
    // do something
}
unsigned endTime = getTickCount();
NSLog(@"That took %umS", endTime - startTime);

You will have to have a pretty big array however in order to register anything above 0.

like image 30
trojanfoe Avatar answered Jan 11 '23 18:01

trojanfoe