Сравнение производительности NSArray vs C

Недавно я провел небольшой исследовательский проект по обеспечению последовательного или произвольного доступа к NSArray по отношению к массиву C. Большинство тестовых случаев появляются, как я ожидал бы, однако некоторые из них не работают, как я думал, они будут, и я надеюсь, что кто-то может объяснить, почему.

В основном тест состоит из заполнения массива C объектами 50k, итерации по каждому из них и вызова метода (который внутренне просто увеличивает приращение в объекте), вторая часть теста включает создание цикла, который завершает итерации 50k но обращается к случайному объекту в массиве. В основном это довольно просто.

Чтобы выполнить сравнение, я инициализирую NSArray с помощью C-массива. Затем каждый тест выполняется через блок, переданный в метод, который отслеживает время, необходимое для выполнения блока. Код, который я использую, содержится ниже, но я хотел бы охватить результаты и запросы, которые у меня есть.

Эти тесты выполнялись на iPhone 4 и завершались в dispatch_after, чтобы смягчить любые оставшиеся потоковые или неатомные операции в результате запуска приложения. Результаты одного прогона следующие, каждый прогон по существу тот же, что и с небольшими вариациями:

===SEQUENCE===
NSARRAY FAST ENUMERATION: 12ms
NSARRAY FAST ENUMERATION WEAK: 186ms
NSARRAY BLOCK ENUMERATION: 31ms (258.3%)
C ARRAY DIRECT: 7ms (58.3%)
C ARRAY VARIABLE ASSIGN: 33ms (275.0%)
C ARRAY VARIABLE ASSIGN WEAK: 200ms (1666.7%)

===RANDOM===
NSARRAY RANDOM: 102ms (850.0%) *Relative to fast enumeration
C ARRAY DIRECT RANDOM: 39ms (38.2%) *Relative to NSArray Random
C ARRAY VARIABLE ASSIGN RANDOM: 82ms (80.4%)

Самый быстрый подход, по-видимому, напрямую обращается к элементам в массиве C с использованием "* (carray + idx)", что является самым озадачивающим, хотя это назначение указателя из массива C объективной переменной c "id object = * (carry + idx)" приводит к огромному результату.

Сначала я понял, что возможно, что-то делать с подсчетом ссылок, поскольку переменная была сильной, поэтому на этом этапе я изменил ее на слабые, ожидая, что производительность увеличится "__weak id object = * (carry + idx)". К моему удивлению, это было на самом деле намного медленнее.

Результаты случайного доступа прошли довольно хорошо, как я ожидал, основываясь на результатах последовательности, так что сюрпризов там не достаточно.

В результате этого возникает ряд вопросов:

  • Почему присвоение переменной занимает так много времени?
  • Почему присвоение слабой переменной занимает еще больше времени? (Может быть, я не понимаю, что здесь происходит)
  • Учитывая вышеизложенное, как Apple получила стандартное быстрое перечисление, чтобы так хорошо работать?

И для полноты здесь есть код. Поэтому я создаю массивы следующим образом:

__block id __strong *cArrayData = (id __strong *)malloc(sizeof(id) * ITEM_COUNT);

for (NSUInteger idx = 0; idx < ITEM_COUNT; idx ++) {
    NSTestObject *object = [[NSTestObject alloc] init];
    cArrayData[idx] = object;
}

__block NSArray *arrayData = [NSArray arrayWithObjects:cArrayData count:ITEM_COUNT];

И NSTestObject определяется следующим образом:

@interface NSTestObject : NSObject

- (void)doSomething;

@end

@implementation NSTestObject
{
    float f;
}

- (void)doSomething
{
    f++;
}

И метод, используемый для профилирования кода:

int machTimeToMS(uint64_t machTime)
{
    const int64_t kOneMillion = 1000 * 1000;
    static mach_timebase_info_data_t s_timebase_info;

    if (s_timebase_info.denom == 0) {
        (void) mach_timebase_info(&s_timebase_info);
    }
    return (int)((machTime * s_timebase_info.numer) / (kOneMillion * s_timebase_info.denom));
}

- (int)profile:(dispatch_block_t)call name:(NSString *)name benchmark:(int)benchmark
{

    uint64_t startTime, stopTime;
    startTime = mach_absolute_time();

    call();

    stopTime = mach_absolute_time();

    int duration = machTimeToMS(stopTime - startTime);

    if (benchmark > 0) {
        NSLog(@"%@: %i (%0.1f%%)", name, duration, ((float)duration / (float)benchmark) * 100.0f);
    } else {
        NSLog(@"%@: %i", name, duration);
    }

    return duration;

}

Наконец, вот как я выполняю фактические тесты:

int benchmark = [self profile:^ {
    for (NSTestObject *view in arrayData) {
        [view doSomething];
    }
} name:@"NSARRAY FAST ENUMERATION" benchmark:0];

[self profile:^ {
    for (NSTestObject __weak *view in arrayData) {
        [view doSomething];
    }
} name:@"NSARRAY FAST ENUMERATION WEAK" benchmark:0];

[self profile:^ {
    [arrayData enumerateObjectsUsingBlock:^(NSTestObject *view, NSUInteger idx, BOOL *stop) {
        [view doSomething];
    }];
} name:@"NSARRAY BLOCK ENUMERATION" benchmark:benchmark];

[self profile:^ {
    for (NSUInteger idx = 0; idx < ITEM_COUNT; idx ++) {
        [*(cArrayData + idx) doSomething];
    }
} name:@"C ARRAY DIRECT" benchmark:benchmark];

[self profile:^ {
    id object = nil;
    NSUInteger idx = 0;
    while (idx < ITEM_COUNT) {
        object = (id)*(cArrayData + idx);
        [object doSomething];
        object = nil;
        idx++;
    }
} name:@"C ARRAY VARIABLE ASSIGN" benchmark:benchmark];

[self profile:^ {
    __weak id object = nil;
    NSUInteger idx = 0;
    while (idx < ITEM_COUNT) {
        object = (id)*(cArrayData + idx);
        [object doSomething];
        object = nil;
        idx++;
    }
} name:@"C ARRAY VARIABLE ASSIGN WEAK" benchmark:benchmark];

NSLog(@"\n===RANDOM===\n");

benchmark = [self profile:^ {
    id object = nil;
    for (NSUInteger idx = 0; idx < ITEM_COUNT; idx ++) {
        object = arrayData[arc4random()%ITEM_COUNT];
        [object doSomething];
    }
} name:@"NSARRAY RANDOM" benchmark:benchmark];

[self profile:^ {
    NSUInteger idx = 1;
    while (idx < ITEM_COUNT) {
        [*(cArrayData + arc4random()%ITEM_COUNT) doSomething];
        idx++;
    }
} name:@"C ARRAY DIRECT RANDOM" benchmark:benchmark];

[self profile:^ {
    id object = nil;
    NSUInteger idx = 0;
    while (idx < ITEM_COUNT) {
        object = (id)*(cArrayData + arc4random()%ITEM_COUNT);
        [object doSomething];
        idx++;
    }
} name:@"C ARRAY VARIABLE ASSIGN RANDOM" benchmark:benchmark];

Ответ 1

Почему присвоение переменной занимает так много времени?

Ваша догадка верна: ARC вызывает retain при назначении и release при переназначении или когда id выходит за пределы области видимости.

Почему присвоение слабой переменной занимает еще больше времени? (Может быть, я не понимаю, что здесь происходит)

Вспомним, что ARC promises, чтобы очистить вашу слабую ссылку, когда последняя сильная ссылка исчезла. Вот почему слабые ссылки стоят дороже: для nil out __weak id ARC регистрирует адрес id с временем выполнения, чтобы получить уведомление об освобождаемом объекте. Эта регистрация требует записи в хеш-таблицу - гораздо медленнее, чем просто сохранение и освобождение.

Учитывая вышеизложенное, как Apple получила стандартное быстрое перечисление, чтобы так хорошо работать?

Быстрое перечисление использует блоки массива, которые обращаются к NSArray напрямую. По сути, они захватывают блок из 30 элементов или около того и обрабатывают доступ к нему как простой массив C. Затем они захватывают следующий блок, перебирают его, как если бы это был массив C, и так далее. Есть небольшие накладные расходы, но это за блок, а не за элемент, поэтому вы получаете довольно впечатляющую производительность.

Ответ 2

2), потому что у вас нет возможности для оптимизации. Слабые переменные, содержащиеся в статическом фрагменте памяти, и доступ к статическому дольше, чем динамический