Refactoring some loops to fast enumeration. #879

merged 1 commit into from Apr 6, 2013

7 participants


No description provided.


@OliverLetterer ,however I think block based enumeration is better.


Any reason for the change? I think (not sure though) that the block based implantation is just as fast. Is that not the case?


I remember reading an article where somebody benchmarked the different enumeration styles, and fast-enumeration was the fastest. I can't (be bothered to) verify this, but I do remember reading it.

I think the only time block-based enumeration can be faster is if it's run concurrently (using NSEnumerationConcurrent), which can't be done in all circumstances, at least not without adding some complexity.


I too remember that I read an article one or two weeks ago from a guy measuring the performance for each different NSArray iteration method. Unfortunately, I can't find this article anymore :( Fast enumeration was an order of magnitude faster. Although it wasn't this article, here is one similar:

Since fast enumeration is easier to read, faster and doesn't need to create a heap copy for local variables we want to write in, I changed all enumerateObjectsUsingBlock: loops to fast enumeration where the idx and stop arguments weren't used.

  • You won't get variable capture here as you haven't copied the block.
  • Blocks are as easy to read as fast enumeration; this is just teaching yourself to read them. (i.e. that which is new is not automatically harder.)

That said, I see no strong reason to use one or the other here. I just don't buy the reasons I've seen so far for switching. Even performance, if true, doesn't seem a big deal here.


Personally, I would land the patch just for the last of the loops patched here. The __block integer strikes me as unsafe if enumeration is ever made parallel.


Finally (and I do realize I've gone on a bit) that article is an awful way to measure performance as it doesn't take into account different storage rules for different size arrays. Performance over 1,000,000 array items does not predict performance over 100 array items, as NSArray is definitely using different structures internally to store 1,000,000 vs 100 items. I would expect it to be radically different.


I just did some performance testing myself for arrays with few number of items:

NSMutableArray *array = [NSMutableArray arrayWithCapacity:100];
for (int i = 0; i < 100; i++) {
    [array addObject:@(i)];
NSArray *numbersToEnumerate = [NSArray arrayWithArray:array];

NSMutableArray *recordedTimes = [NSMutableArray arrayWithCapacity:100];

for (int j = 0; j < 100; j++) {
    uint64_t startTime = mach_absolute_time();

    for (int i = 0; i < 1000; i++) {
        for (id obj in numbersToEnumerate) {


//        [numbersToEnumerate enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
//        }];

    uint64_t usedTime = mach_absolute_time() - startTime;
    mach_timebase_info_data_t info;

    usedTime *= info.numer;
    usedTime /= info.denom;

    CFTimeInterval seconds = (CFTimeInterval)usedTime * 1e-9;
    [recordedTimes addObject:@(seconds)];

NSLog(@"%@", [recordedTimes valueForKeyPath:@"@avg.self"]);

Block based enumeration: ~0.0377s
Fast enumeration: ~0.0019s

Even for lower sizes arrays fast enumeration is an order of magnitude faster. You might be right that this will be no big difference here, but still it is a free win and the code is not cluttered with __unused statements for the block based approach since idx and stop aren't used.

@tewha I don't understand what you mean by You won't get variable capture here as you haven't copied the block..


That's more useful information, thanks. I don't really see that as significant, but as you said it's free. (I also like that you did the arithmetic here. It's always worth doing the arithmetic! And it's an even bigger win than you thought.)

As to what I meant, sorry, I should have been more exact. Blocks capture variables in a very cheap way if they're not copied. It's only when the block is copied that they become expensive. It's harder with ARC to predict when blocks will be copied (with MRR, you either did it manually or you didn't), but none of these examples will force a copy.

As I said, I'm in favour of landing this patch. Sorry if it seemed otherwise. I just wanted clarity. :)

To be clear, here's what we get:

  • Less clutter in the form of __unused.
  • More familiar syntax.
  • Better performance.
  • No potential parallelism issue in the future.

Ok now I get what you mean by this :). But since we are passing the block to method, isn't ARC explicitly copying the block for us and therefore no cheap capturing is done?


I believe the block is copied lazily, at the time the compiler detects it would otherwise go out of scope.

Gosh. Now I need to figure out a way to test this tomorrow. :)


I just did some tests because I was not sure either:

@implementation NSArray (HOOKED)

+ (void)load
    class_swizzleSelector(self, @selector(enumerateObjectsUsingBlock:), @selector(__HOOKEDEnumerateObjectsUsingBlock:));

- (void)__HOOKEDEnumerateObjectsUsingBlock:(void (^)(id, NSUInteger, BOOL *))block
    NSLog(@"%@", block);

    [self __HOOKEDEnumerateObjectsUsingBlock:block];

// case 1)
[@[ ] enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {


// case 2)
NSDictionary *dictionary = [NSDictionary dictionary];
[@[ ] enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
    NSLog(@"%@", dictionary);

// case 3)
void(^block)(void) = ^{
    NSLog(@"%@", dictionary);
NSLog(@"%@", block);

// case 4)
NSLog(@"%@", ^{
    NSLog(@"%@", dictionary);

results in the output:

2013-03-28 09:22:58.157 wuff[12766:907] <__NSGlobalBlock__: 0xac5b0>
2013-03-28 09:22:58.159 wuff[12766:907] <__NSStackBlock__: 0x2fd567bc>
2013-03-28 09:22:58.161 wuff[12766:907] <__NSMallocBlock__: 0x1d058150>
2013-03-28 09:22:58.162 wuff[12766:907] <__NSStackBlock__: 0x2fd56784>
  • In the first case, the block is not capturing any variables and therefor is and will stay a global block.
  • In the second and fourth case, the block will stay a stack block an will not be copied.
  • The block in the third case is beeing copied because ARC copies all blocks which are assigned to variables. To verify this, I changed the hooked implementation to
- (void)__HOOKEDEnumerateObjectsUsingBlock:(void (^)(id, NSUInteger, BOOL *))block
    void(^block2)(id, NSUInteger, BOOL *) = block;
    NSLog(@"%@", block2);

    [self __HOOKEDEnumerateObjectsUsingBlock:block];

with the resulting output:

2013-03-28 09:26:18.160 wuff[12783:907] <__NSGlobalBlock__: 0xfa5b0>
2013-03-28 09:26:18.163 wuff[12783:907] <__NSMallocBlock__: 0x1d546550>
2013-03-28 09:26:18.165 wuff[12783:907] <__NSMallocBlock__: 0x1e82a450>
2013-03-28 09:26:18.166 wuff[12783:907] <__NSStackBlock__: 0x2fd08784>

(Notice that case 2 changed)


That's great info, thanks!


Very good info!


I prefer this kind of style

    NSInteger numberOfFinishedOperations = [[operations indexesOfObjectsPassingTest:^BOOL(NSOperation *op, NSUInteger idx, BOOL *stop) {
        return [op isCancelled];
    }] count];
@mattt mattt merged commit c943d8a into AFNetworking:master Apr 6, 2013

This thread turned into quite an interesting read! Thanks for the patch, @OliverLetterer.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment