Уменьшите пиковое использование памяти с @autoreleasepool - PullRequest
7 голосов
/ 13 марта 2012

Я работаю над приложением для iPad, в котором есть процесс синхронизации, использующий веб-сервисы и Core Data в тесном цикле. Чтобы уменьшить объем памяти в соответствии с Рекомендация Apple , я периодически выделяю и истощаю NSAutoreleasePool. В настоящее время это прекрасно работает, и у текущего приложения нет проблем с памятью. Тем не менее, я планирую перейти на ARC, где NSAutoreleasePool больше не действителен и хотел бы сохранить такой же вид производительности. Я создал несколько примеров и рассчитал их время, и Мне интересно, как лучше всего использовать ARC для достижения такой же производительности и поддержания читабельности кода .

В целях тестирования я предложил 3 сценария, каждый из которых создает строку, используя число от 1 до 10 000 000. Я запускал каждый пример 3 раза, чтобы определить, сколько времени они заняли, используя 64-битное приложение Mac с компилятором Apple LLVM 3.0 (без gdb -O0) и XCode 4.2. Я также проверил каждый пример с помощью инструментов, чтобы примерно увидеть, какой был пик памяти.

Каждый из приведенных ниже примеров содержится в следующем блоке кода:

int main (int argc, const char * argv[])
{
    @autoreleasepool {
        NSDate *now = [NSDate date];

        //Code Example ...

        NSTimeInterval interval = [now timeIntervalSinceNow];
        printf("Duration: %f\n", interval);
    }
}

NSAutoreleasePool Batch [Оригинальный предварительный ARC] (Пиковая память: ~ 116 КБ)

    static const NSUInteger BATCH_SIZE = 1500;
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    for(uint32_t count = 0; count < MAX_ALLOCATIONS; count++)
    {
        NSString *text = [NSString stringWithFormat:@"%u", count + 1U];
        [text class];

        if((count + 1) % BATCH_SIZE == 0)
        {
            [pool drain];
            pool = [[NSAutoreleasePool alloc] init];
        }
    }
    [pool drain];

Время выполнения:
10.928158
10.912849
11.084716


Внешний @autoreleasepool (Пиковая память: ~ 382 МБ)

    @autoreleasepool {
        for(uint32_t count = 0; count < MAX_ALLOCATIONS; count++)
        {
            NSString *text = [NSString stringWithFormat:@"%u", count + 1U];
            [text class];
        }
    }

Время выполнения:
11.489350
11.310462
11.344662


Внутренний @autoreleasepool (Пиковая память: ~ 61,2 КБ)

    for(uint32_t count = 0; count < MAX_ALLOCATIONS; count++)
    {
        @autoreleasepool {
            NSString *text = [NSString stringWithFormat:@"%u", count + 1U];
            [text class];
        }
    }

Время выполнения:
14.031112
14.284014
14.099625


@ autoreleasepool w / goto (Пиковая память: ~ 115 КБ)

    static const NSUInteger BATCH_SIZE = 1500;
    uint32_t count = 0;

    next_batch:
    @autoreleasepool {
        for(;count < MAX_ALLOCATIONS; count++)
        {
            NSString *text = [NSString stringWithFormat:@"%u", count + 1U];
            [text class];
            if((count + 1) % BATCH_SIZE == 0)
            {
                count++; //Increment count manually
                goto next_batch;
            }
        }
    }

Время выполнения:
10.908756
10.960189
11.018382

Оператор goto показал наибольшую производительность, но он использует goto. Есть мысли?

Обновление:

Примечание. Оператор goto является нормальным завершением для @autoreleasepool, как указано в документации , и не приводит к утечке памяти.

При входе пул авто-релиза выдвигается. На нормальном выходе (перерыв, return, goto, fall-through и т. д.) пул авто-релизов выталкивается. Для совместимости с существующим кодом, если выход вызван исключением, пул автозапуска не выталкивается.

Ответы [ 2 ]

9 голосов
/ 13 марта 2012

Следующее должно достигнуть того же, что и ответ goto без goto:

for (NSUInteger count = 0; count < MAX_ALLOCATIONS;)
{
    @autoreleasepool
    {
        for (NSUInteger j = 0; j < BATCH_SIZE && count < MAX_ALLOCATIONS; j++, count++)
        {
            NSString *text = [NSString stringWithFormat:@"%u", count + 1U];
            [text class];
        }
    }
}
2 голосов
/ 13 марта 2012

Обратите внимание, что ARC обеспечивает значительную оптимизацию, которая не включена в -O0. Если вы собираетесь измерять производительность в ARC, вы должны тестировать с включенной оптимизацией. В противном случае вы будете измерять вручную настроенное положение удержания / выпуска против «наивного режима» ARC.

Запустите ваши тесты снова с оптимизацией и посмотрите, что произойдет.

Обновление : Мне было любопытно, поэтому я запустил его сам. Это результаты времени выполнения в режиме Release (-Os) с 7 000 000 выделений.

arc-perf[43645:f803] outer: 8.1259
arc-perf[43645:f803] outer: 8.2089
arc-perf[43645:f803] outer: 9.1104

arc-perf[43645:f803] inner: 8.4817
arc-perf[43645:f803] inner: 8.3687
arc-perf[43645:f803] inner: 8.5470

arc-perf[43645:f803] withGoto: 7.6133
arc-perf[43645:f803] withGoto: 7.7465
arc-perf[43645:f803] withGoto: 7.7007

arc-perf[43645:f803] non-ARC: 7.3443
arc-perf[43645:f803] non-ARC: 7.3188
arc-perf[43645:f803] non-ARC: 7.3098

И пики памяти (работают только с 100 000 выделений, потому что инструменты берутся вечно):

Outer: 2.55 MB
Inner: 723 KB
withGoto: ~747 KB
Non-ARC: ~748 KB

Эти результаты меня немного удивляют. Ну, результаты пика памяти не делают; это именно то, что вы ожидаете. Но разница во времени выполнения между inner и withGoto даже при включенной оптимизации выше, чем я ожидал.

Конечно, это своего рода патологический микротест, который вряд ли будет моделировать реальную производительность любого приложения. Вывод здесь заключается в том, что ARC действительно может иметь некоторые накладные расходы, но вы всегда должны измерять фактическое приложение, прежде чем делать предположения.

(Кроме того, я тестировал ответ @ ipmcc, используя вложенные циклы for; он вел себя почти точно так же, как goto версия.)

...