【问题标题】:Reduce Peak Memory Usage With @autoreleasepool使用 @autoreleasepool 减少峰值内存使用量
【发布时间】:2012-03-29 08:11:58
【问题描述】:

我在一个 iPad 应用程序上工作,它有一个同步过程,它在一个紧密的循环中使用 Web 服务和 Core Data。为了根据Apple's Recomendation 减少内存占用,我会定期分配和耗尽NSAutoreleasePool。这目前效果很好,并且当前应用程序没有内存问题。但是,我计划转移到 NSAutoreleasePool 不再有效的 ARC,并希望保持这种相同的性能。我创建了一些示例并对它们进行了计时,我想知道使用 ARC 实现相同性能并保持代码可读性的最佳方法是什么

出于测试目的,我提出了 3 个场景,每个场景都使用 1 到 10,000,000 之间的数字创建一个字符串。我将每个示例运行了 3 次,以确定它们使用带有 Apple LLVM 3.0 编译器(不带 gdb -O0)和 XCode 4.2 的 Mac 64 位应用程序需要多长时间。我还通过仪器运行了每个示例,以大致了解内存峰值。

以下每个示例都包含在以下代码块中:

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 [Original Pre-ARC](峰值内存:~116 KB)

    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 MB)

    @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.2KB)

    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(内存峰值:~115KB)

    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 的正常退出,如documentation 中所述,不会泄漏内存。

进入时,会推送一个自动释放池。在正常退出时(休息, return、goto、fall-through 等)自动释放池被弹出。 为了与现有代码兼容,如果退出是由于异常, 自动释放池没有弹出。

【问题讨论】:

  • 使用优化器。这对 ARC 代码相当重要。
  • 这样goto肯定不会,不知道,导致内存泄漏?其他一切都是有道理的:排水越少越快。无论如何,我只能评论可读性:你在任何地方都可以。那个 goto 需要一张黄色便签。
  • goto 似乎没有泄漏任何内存。看起来范围像我预期的那样耗尽了自动释放池,但我不是 ARC 专家(还)并且不想依赖 UB。
  • 你不能通过反转代码并将自动释放池放在检查批量大小的for 中来做同样的事情吗?显然count 必须从上次停止的地方开始......
  • @Yar 谢谢,睡眠不足让我又把事情复杂化了。

标签: objective-c ios memory-management automatic-ref-counting nsautoreleasepool


【解决方案1】:

以下内容应该与没有gotogoto 答案相同:

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];
        }
    }
}

【讨论】:

  • 谢谢,漫长的一天,足够简单的答案:)。
  • 这应该得到更多的投票,因为它正是使用旧 NSAutoreleasePool 获得与手动排水相同行为的解决方案!
【解决方案2】:

请注意,ARC 启用了 -O0 未启用的重要优化。如果您要在 ARC 下测量性能,您必须在启用优化的情况下进行测试。否则,您将根据 ARC 的“幼稚模式”来衡量您手动调整的保留/释放位置。

通过优化再次运行您的测试,看看会发生什么。

更新:我很好奇,所以我自己运行了它。这些是发布模式 (-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 次分配,因为 Instruments 一直在占用):

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

这些结果让我有点吃惊。好吧,内存峰值结果没有;这正是您所期望的。但是innerwithGoto 之间的运行时间差异,即使启用了优化,也比我预期的要高。

当然,这在某种程度上是一种病态的微测试,它不太可能模拟任何应用程序的实际性能。这里的要点是 ARC 可能确实会产生一些开销,但您应该始终在做出假设之前衡量您的实际应用程序。

(另外,我使用嵌套的 for 循环测试了 @ipmcc 的答案;它的行为几乎与 goto 版本完全相同。)

【讨论】:

  • 谢谢,感谢您的提示。不知道采取了额外的步骤,但从那以后就绝对了。有没有类似gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html 的优化指南?不期望找到确切的标志,而是更多地沿着可能发生的优化类型。我发现了一些有用的属性项here,但它看起来并不依赖于编译器优化。
  • 我添加了一些启用优化的实际测试。
  • 感谢您协助检查优化统计信息。这对于决定将来出于各种目的使用哪种方法很有用(由于 ipmcc 的回答,不包括 goto)。希望更多的人会通过并投票。
猜你喜欢
  • 2019-03-12
  • 1970-01-01
  • 2020-01-01
  • 2012-01-20
  • 2013-05-21
  • 1970-01-01
  • 2018-07-12
  • 2021-12-12
  • 1970-01-01
相关资源
最近更新 更多