Performance and Principle of iOS Objective-C Array Traversal

Keywords: less Mobile

// Contact person: Shihu QQ: 1224614774 nickname: buzzing


Links to the original text: http://www.jianshu.com/p/66f8410c6bbc
Array traversal, this topic seems to have nothing to explore, how to traverse on how to traverse! But if you want to answer these questions: What are the most efficient ways to traverse OC arrays? Why? What are the internal implementations of all kinds of traversals? What is the internal structure of NS(Mutable)Array? I think it still needs to be explored. Membrane method share

Array traversal, this topic seems to have nothing to explore, how to traverse on how to traverse! But if you want to answer these questions:
What are the traversal modes of OC arrays?
Which is the most efficient way? Why?
What are the internal implementations of various traversals?
What is the internal structure of NS(Mutable)Array?
I think we still need to explore it.

I. Class System of OC Arrays

When we create an NSArray object, we actually get a subclass of NSArray _NSArray I object. Similarly, we create an NSMutableArray object and get its subclass _NSArray M object. Interestingly, when we create a NSArray object with only one object, we get an object of _NSSingleObjectArray I class.
_ NSArrayI and _NSArrayM, _NSSingleObjectArrayI are hidden classes of the framework.

The class system of OC arrays is as follows:



Through the NSArray and NSMutableArray interfaces, it returns subclass objects. How do you do that?
We first introduce another private class, _NSPlaceholder Array, and two global variables of this class, _immutablePlaceholder Array, _mutablePlaceholder Array. _ NSPlaceholder Array is only used to occupy space from the point of view of class naming. The specific method of occupying space will be discussed later. An important feature is that _NSPlaceholder Array implements the same initialization methods as NSArray and NSMutable Array, such as initWithObjects:count:,initWithCapacity, etc.

After introducing _NSPlaceholder Array, this mechanism can be summarized as the following two major steps:
NSArray rewrote +(id) allocWithZone:(struct_NSZone) *) In the zone method, if the calling class is NSArray, the global variable _immutablePlaceholder Array is returned directly, and if the calling class is NSMUtableArray, the global variable _mutablePlaceholder Array is returned directly.
That is to call [NSArray alloc] or [NSMUtableArray] alloc] gets only two placeholder pointers of type _NSPlaceholder Array.
(2) On the basis of calling alloc, either NSArray or NSMutable Array must continue to call an initXXX method, while the actual invocation is _NSPlaceholder Array's initXXX. Within this initXXX method, if self ==_ The immutablePlaceholder Array is reconstructed and returns the _NSArray I object if self ==_ The mutablePlaceholder Array is reconstructed and returns the _NSArray M object.

In conclusion, for NSArray and NSMutable Array, alloc only gets a placeholder object, and init only gets the real subclass object.

Next, count several traversals:

Two. Several ways of traversing OC arrays

1.for loop

for (NSUInteger i = 0;  i < array.count; ++i) {
        object = array[i];
  }

array[i] is converted by the compiler into pairs- (ObjectType) Call of objectAtIndexedSubscript:(NSUInteger) index, which is called internally- (ObjectType)objectAtIndex:(NSUInteger)index method.
2.for in

for (id obj in array) {
        xxx
  }

The article will discuss the internal implementation of for in later.
3.enumerateObjectsUsingBlock
Traverse through the block callback sequence:

[array enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
       xxx
  }];

4.enumerateObjectsWithOptions:usingBlock:
Through block callbacks, the callback order of the object is disorderly, and the calling thread waits for the completion of the traversal process:

[array enumerateObjectsWithOptions:NSEnumerationConcurrent usingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        xxx
  }];

Through block callbacks, traverse in reverse order in the main thread:

[array enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        xxx
  }];

5.objectEnumerator/reverseObjectEnumerator
Through Enumerator sequential traversal:

NSEnumerator *enumerator = array.objectEnumerator;
while((object = enumerator.nextObject)) {
    xxx
}

Through Reverse Enumerator reverse traversal:

NSEnumerator *enumerator = array.reverseObjectEnumerator;
while((object = enumerator.nextObject)) {
    xxx
}

6.enumerateObjectsAtIndexes:options:usingBlock:
By block callback, the specified IndexSet is traversed in the sub-thread. The callback order of the object is disorderly, and the calling thread waits for the traversal process to complete:

[array enumerateObjectsAtIndexes:[NSIndexSet xxx] options:NSEnumerationConcurrent usingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        xxx
 }];

By block callback, the specified IndexSet is traversed in reverse order in the main thread:

[array enumerateObjectsAtIndexes:[NSIndexSet xxx] options:NSEnumerationReverse usingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        xxx
 }];

III. Performance comparison

NSArray with a step size of 100 and a number of objects between 0 and 1 million is constructed. The traversal method mentioned above is used to traverse and time (unit us), and in each traversal, only the object is obtained, without any other interference operations such as input and output, calculation and so on. Each traversal method collects 10,000 sets of data and obtains the following performance comparison results:



The horizontal axis is the number of traversing objects, and the vertical axis is the time-consuming unit us.

As can be seen from the figure, when the number of objects is very small, the performance differences of various methods are very small. As the number of objects increases, performance differences become apparent.
for in has always been the lowest time-consuming, when the number of objects is as high as 1 million, for The time consumption of in is not more than 5 ms.
Secondly, for cycle consumes less time.
Instead, intuitively, multithreaded traversal should be very fast:

[array enumerateObjectsWithOptions:NSEnumerationConcurrent usingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        xxx
  }];

But the performance is the worst.
Enumerate Objects UsingBlock: Very similar to the traversal performance of reverse Object Enumerator.
Why does this result occur? The article will analyze the reasons later from the internal implementations of various traversals.

IV. Internal structure of OC arrays

Neither NSArray nor NSMutable Array defines instance variables, but defines and implements interfaces, and interfaces for internal data operations are implemented in various subclasses. So what we really need to know is the structure of subclasses. Understanding _NSArray I is equivalent to understanding NSArray, and understanding _NSArray M is equivalent to understanding NSMutable Array.
1. __NSArrayI
_ The structure of NSArray I is defined as:

@interface __NSArrayI : NSArray
{
    NSUInteger _used;
    id _list[0];
}
@end

_ use is the number of elements in an array, calling [array] When count] returns the value of _use.
Id_list [0] is an array of actual stored objects inside an array, but why is it defined as zero length? Here's an article about 0-length arrays: http://blog.csdn.net/zhaqiwen/article/details/7904515
Here we can take id_list [0] as ID * list is used to store buff for id objects.
Because _NSArrayI is immutable, once the list is allocated, there will be no mobile deletion operation until it is released. Only one operation is to get the object. Therefore, the implementation of _NSArrayI is not complicated.
2. __NSSingleObjectArrayI
_ The structure of NSSingleObjectArray I is defined as:

@interface __NSSingleObjectArrayI : NSArray
{
    id object;
}
@end

Because the _NSSingleObjectArrayI object is only obtained when an immutable array containing only one object is created, its internal structure is simpler, and an object is enough.
3. __NSArrayM
_ The structure of NSArray M is defined as:

@interface __NSArrayM : NSMutableArray
{
    NSUInteger _used;
    NSUInteger _offset;
    int _size:28;
    int _unused:4;
    uint32_t _mutations;
    id *_list;
}
@end

_ NSArray M is slightly more complex, but similarly, its internal object array is also a continuous memory id.* _ list, as the id of _NSArrayI _ The list[0] is the same
_ used: Number of current objects
_ offset: The starting offset of the actual object array, the use of this field will be discussed later.
_ size: The allocated _list size (the number of objects that can be stored, not bytes)
_ mutations: Modify the tag. Each modification to _NSArrayM adds 1 to mutations, "**"* Collection <_NSArray M: 0x1002076b0 > was mutated while being enumerated. "This exception is triggered by the recognition of _mutations.

id *_list is a circular array. It is dynamically reallocated when adding or deleting operations to meet current storage requirements. For example, a _list with an initial five objects and a total size of _size of 6:
_offset = 0,_used = 5,_size=6



After adding three objects to the end:
_offset = 0,_used = 8,_size=8
_ list has been reassigned



Delete object A:
_offset = 1,_used = 7,_size=8



Delete object E:
_offset = 2,_used = 6,_size=8
B,C moved back and E was filled.



Two objects are added at the end:
_offset = 2,_used = 8,_size=8
_ List is sufficient to store two newly added objects, so instead of reallocating, two new objects are stored at the start of _list.



Therefore, the _list of _NSArrayM is a circular array, whose beginning is identified by _offset.

5. Internal implementation of various Traversals

1. Quick Enumeration
Quick enumeration has not been mentioned before. How come it suddenly pops up here? In fact, for in is based on fast enumeration. But without discussing for in, we first recognize a protocol: NSFast Enumeration, which is defined in the Foundation Framework's NSFast Enumeration. h header file:

@protocol NSFastEnumeration

- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(id __unsafe_unretained _Nullable [_Nonnull])buffer count:(NSUInteger)len;

@end

NSFast Enumeration State definition:

typedef struct {
    unsigned long state;
    id __unsafe_unretained _Nullable * _Nullable itemsPtr;
    unsigned long * _Nullable mutationsPtr;
    unsigned long extra[5];
} NSFastEnumerationState;

Looking at these definitions and the Apple Documents, I don't know how to use this method. How can it be called quick enumeration? Unless we know its implementation details, we're confused too much. So let's look at its implementation details first, no matter how we use it.
_ NSArray I, _NSArray M, _NSSingle Object Array I all implement the NSFast Enumeration protocol.
(1) Implementation of _NSArray I:
According to assembly rewriting, we can get:

- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(id __unsafe_unretained _Nullable [_Nonnull])buffer count:(NSUInteger)len {
    if (!buffer && len > 0) {
        CFStringRef errorString = CFStringCreateWithFormat(kCFAllocatorSystemDefault, NULL, CFSTR("*** %s: pointer to objects array is NULL but length is %lu"), "-[__NSArrayI countByEnumeratingWithState:objects:count:]",(unsigned long)len);
        CFAutorelease(errorString);
        [[NSException exceptionWithName:NSInvalidArgumentException reason:(__bridge NSString *)errorString userInfo:nil] raise];
    }

    if (len >= 0x40000000) {
        CFStringRef errorString = CFStringCreateWithFormat(kCFAllocatorSystemDefault, NULL, CFSTR("*** %s: count (%lu) of objects array is ridiculous"), "-[__NSArrayI countByEnumeratingWithState:objects:count:]",(unsigned long)len);
        CFAutorelease(errorString);
        [[NSException exceptionWithName:NSInvalidArgumentException reason:(__bridge NSString *)errorString userInfo:nil] raise];
    }

    static const unsigned long mu = 0x01000000;

    if (state->state == 0) {
        state->mutationsPtr = μ
        state->state = ~0;
        state->itemsPtr = _list;
        return _used;
    }
    return 0;
}

It can be seen that in the implementation of this method by _NSArrayI, the main thing is to assign _NSArrayI's internal array_list to state-> itemsPtr and return _use, that is, array size. State-> mutations Ptr points to a local static variable, and state-> state seems to be a flag. If the method is called with the same state again, it will return directly to 0.
As for the incoming buffer,len is only used to determine the reasonableness of the parameters.
It seems that the meaning of fast enumeration is somewhat clear. This will get all the objects, and in a c array, the objects that need to be located after that can be quickly addressed. The caller accesses the array through state - > itemsPtr, and determines the number of objects in the array by the return value.
For example, traversing a NSArray can do this:

    NSFastEnumerationState state = {0};
    NSArray *array = @[@1,@2,@3];
    id buffer[2];
//buffer is not actually used internally, but it has to be passed on. 2To express my expectation2The number of objects actually returned is the total number of objects3
    NSUInteger n = [array countByEnumeratingWithState:&state objects:buffer count:2];
    for (NSUInteger i=0; i<n; ++i) {
        NSLog(@"%@", (__bridge NSNumber *)state.itemsPtr[i]);
    }

It seems that the reason why fast traversal is called is that it takes objects directly from the c array without calling other methods, so it is fast.

_ The implementation of NSSingle Object Array I has also been guessed, so there is no code to paste here. Let's see how _NSArray M implements this protocol.
(2) Realization of _NSArrayM:

- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(id __unsafe_unretained _Nullable [_Nonnull])buffer count:(NSUInteger)len {
    if (!buffer && len > 0) {
        CFStringRef errorString = CFStringCreateWithFormat(kCFAllocatorSystemDefault, NULL, CFSTR("*** %s: pointer to objects array is NULL but length is %lu"), "-[__NSArrayI countByEnumeratingWithState:objects:count:]",(unsigned long)len);
        CFAutorelease(errorString);
        [[NSException exceptionWithName:NSInvalidArgumentException reason:(__bridge NSString *)errorString userInfo:nil] raise];
    }

    if (len >= 0x40000000) {
        CFStringRef errorString = CFStringCreateWithFormat(kCFAllocatorSystemDefault, NULL, CFSTR("*** %s: count (%lu) of objects array is ridiculous"), "-[__NSArrayI countByEnumeratingWithState:objects:count:]",(unsigned long)len);
        CFAutorelease(errorString);
        [[NSException exceptionWithName:NSInvalidArgumentException reason:(__bridge NSString *)errorString userInfo:nil] raise];
    }

    if (state->state != ~0) {
        if (state->state == 0) {
            state->mutationsPtr = &_mutations;
            //Find the starting position of the element in _list
            state->itemsPtr = _list + _offset;
            if (_offset + _used <= _size) {
                //There must be no remaining elements.
                //Indicate traversal completion
                state->state = ~0;
                return _used;
            }
            else {
                //There are residual elements (_list is a circular array, and the remaining elements are stored in _list from the beginning)
                //State - > State stores the number of remaining elements
                state->state = _offset + _used - _size;
                //Returns the number of elements obtained this time (total - residual)
                return _used - state->state;
            }
        }
        else {
            //Get the remaining element pointer
            state->itemsPtr = _list;
            unsigned long left = state->state;
            //Markup traversal completed
            state->state = ~0;
            return left;
        }
    }
    return 0;
}

It can be seen from the implementation that for _NSArrayM, all the elements can be obtained at most twice by fast enumeration. If _list has not yet formed a loop, all elements are obtained for the first time, just like _NSArray I. But if _list constitutes a loop, it takes two times, the first time to get the elements from _offset to the end of _list, and the second time to get the remaining elements stored at the beginning of _list.

2. Implementation of for in
As mentioned in the previous performance comparison section, the performance of for in is the best. It can be assumed that for in is based on the fast enumeration that should just be discussed.
The following code:

    NSArray *arr = @[@1,@2,@3];
    for (id obj in arr) {
        NSLog(@"obj = %@",obj);
    }

Through clang-rewrite-objc The main.m command looks at what the compiler turns for in into:

//NSArray *arr = @[@1,@2,@3];
NSArray *arr = ((NSArray *(*)(Class, SEL, const ObjectType *, NSUInteger))(void *)objc_msgSend)(objc_getClass("NSArray"), sel_registerName("arrayWithObjects:count:"), (const id *)__NSContainer_literal(3U, ((NSNumber *(*)(Class, SEL, int))(void *)objc_msgSend)(objc_getClass("NSNumber"), sel_registerName("numberWithInt:"), 1), ((NSNumber *(*)(Class, SEL, int))(void *)objc_msgSend)(objc_getClass("NSNumber"), sel_registerName("numberWithInt:"), 2), ((NSNumber *(*)(Class, SEL, int))(void *)objc_msgSend)(objc_getClass("NSNumber"), sel_registerName("numberWithInt:"), 3)).arr, 3U);
    {
//Definition of for (id obj in arr) obj
    id obj;
//NSFastEnumerationState
    struct __objcFastEnumerationState enumState = { 0 };
//buffer
    id __rw_items[16];
    id l_collection = (id) arr;
//The first traversal calls countByEnumeratingWithState:objects:count: fast enumeration method
    _WIN_NSUInteger limit =
        ((_WIN_NSUInteger (*) (id, SEL, struct __objcFastEnumerationState *, id *, _WIN_NSUInteger))(void *)objc_msgSend)
        ((id)l_collection,
        sel_registerName("countByEnumeratingWithState:objects:count:"),
        &enumState, (id *)__rw_items, (_WIN_NSUInteger)16);
    if (limit) {
//Save the initial enumState. mutations Ptr value
    unsigned long startMutations = *enumState.mutationsPtr;
    do {
        unsigned long counter = 0;
        do {
//Before retrieving each element in enumState.itemsPtr, check whether the flag indicated by enumState. mutations Ptr changes and throw an exception if it changes.
//For _NSArray I, enumState. mutations Ptr points to a static local variable and never throws an exception
//For _NSArrayM, enumState. mutations Ptr points to the _mutations variable. After each addition and deletion operation, _mutations will + 1
            if (startMutations != *enumState.mutationsPtr)
                objc_enumerationMutation(l_collection);
//Get each obj
            obj = (id)enumState.itemsPtr[counter++]; {
//NSLog(@"obj = %@",obj);
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_rg_wm9xjmyn1kz01_pph_34xcqc0000gn_T_main_c95c5d_mi_8,obj);
    };
    __continue_label_2: ;
        } while (counter < limit);
//Traverse again to get the remaining elements
    } while ((limit = ((_WIN_NSUInteger (*) (id, SEL, struct __objcFastEnumerationState *, id *, _WIN_NSUInteger))(void *)objc_msgSend)
        ((id)l_collection,
        sel_registerName("countByEnumeratingWithState:objects:count:"),
        &enumState, (id *)__rw_items, (_WIN_NSUInteger)16)));
//Traversal completion
    obj = ((id)0);
    __break_label_2: ;
    }
//No element, empty array
    else
        obj = ((id)0);
    }

It can be seen that for in is based on fast enumeration. The compiler converts for in into a two-tier loop. The outer layer calls a fast enumeration method to get elements in batches. The inner layer gets each element in a batch through a c array. Before each element is acquired, it checks whether the array object has been changed. If so, it throws an exception.
3.enumerateObjectsUsingBlock:
This method is implemented in NSArray, which is the implementation of all subclass object calls.

- (void)enumerateObjectsUsingBlock:(void ( ^)(id obj, NSUInteger idx, BOOL *stop))block {
    if (!block) {
        CFStringRef errorString = CFStringCreateWithFormat(kCFAllocatorSystemDefault, NULL, CFSTR("*** %s: block cannot be nil"), "-[NSArray enumerateObjectsUsingBlock:]");
        CFAutorelease(errorString);
        [[NSException exceptionWithName:NSInvalidArgumentException reason:(__bridge NSString *)errorString userInfo:nil] raise];
    }

    [self enumerateObjectsWithOptions:0 usingBlock:block];
}

Enumerate Objects WithOptions is called internally directly with option = 0: usingBlock:
4. enumerateObjectsWithOptions: usingBlock:
(1)_NSArray I Implementation

- (void)enumerateObjectsWithOptions:(NSEnumerationOptions)opts usingBlock:(void (^)(id _Nonnull, NSUInteger, BOOL * _Nonnull))block {
    if (!block) {
        CFStringRef errorString = CFStringCreateWithFormat(kCFAllocatorSystemDefault, NULL, CFSTR("*** %s: block cannot be nil"), "-[__NSArrayI enumerateObjectsWithOptions:usingBlock:]");
        CFAutorelease(errorString);
        [[NSException exceptionWithName:NSInvalidArgumentException reason:(__bridge NSString *)errorString userInfo:nil] raise];
    }

    __block BOOL stoped = NO;
    void (^enumBlock)(NSUInteger idx) = ^(NSUInteger idx) {
        if(!stoped) {
            @autoreleasepool {
                block(_list[idx],idx,&stoped);
            }
        }
    };

    if (opts == NSEnumerationConcurrent) {
        dispatch_apply(_used, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), enumBlock);
    }
    else if(opts == NSEnumerationReverse) {
        for (NSUInteger idx = _used - 1; idx != (NSUInteger)-1 && !stoped; idx--) {
            enumBlock(idx);
        }
    }
    //opts == 0
    else {
        if(_used > 0) {
            for (NSUInteger idx = 0; idx != _used - 1 && !stoped; idx++) {
                enumBlock(idx);
            }
        }
    }
}

(1)_NSArrayM Implementation
_ The only difference in the implementation of NSArray M is enumBlock

 void (^enumBlock)(NSUInteger idx) = ^(NSUInteger idx) {
        if(!stoped) {
            @autoreleasepool {
                NSUInteger idx_ok = _offset + idx;
                //idx corresponds to the element at the beginning of _list (the loop part)
                if (idx_ok >= _size) {
                    idx_ok -= _size;
                }
                block(_list[idx_ok],idx,&stoped);
            }
        }
    };

5.objectEnumerator/reverseObjectEnumerator
From array.objectEnumerator, you get a _NSFastEnumeration Enumerator private class object, which is called every time on the enumerator object.- (id)nextObject, in fact, every time it calls array's fast enumeration method internally:

- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(id __unsafe_unretained _Nullable [_Nonnull])buffer count:(NSUInteger)len

Only one element is retrieved and returned at a time.
What you get from array.reverseObjectEnumerator is a _NSArrayReverseEnumerator private class object, which is called every time on the enumerator object.- (id) When nextObject, the internal direct call is: objectAtIndex: to return the object.
6.enumerateObjectsAtIndexes:options:usingBlock:
Because of the time relationship, it's posted later.

6. Summary

At this point, we should be able to answer some of the questions mentioned at the beginning of the article.
With regard to performance differences:
The reason why for in is fast is that it is based on fast enumeration. A quick enumeration call to NSArray can obtain a c array containing all elements, and a maximum of two times for NSMUtableArray.
for In is slower, just because of the overhead of function calls, for relies on each call to objectAtIndex:, as opposed to the way for in takes each element directly from the c array.
NSEnumeration Concurrent + Block is the most time-consuming way, I think because it uses multi-threading. In this way, the advantage of multi-threading is not how fast it traverses, but that its callback is in each sub-thread. If there is a traversal + time-consuming computing scenario, this method should be the most suitable, but only to measure the traversal speed here, it starts the distributor light. Processing threads take a lot of time, so performance lags behind.

I hope this article will help you.

Posted by herrin on Mon, 17 Jun 2019 12:55:36 -0700