【问题标题】:How can I optimize Objective-C singletons using ARC?如何使用 ARC 优化 Objective-C 单例?
【发布时间】:2014-03-17 19:10:50
【问题描述】:

使用 Objective-C 中单例的标准模式,ARC 仍然会在每次使用单例时自动生成保留和释放调用,即使我们知道对象永远不会被释放。在对性能敏感的代码中,这些 ARC 生成的调用会导致大量额外开销。有没有办法告诉编译器不要为单例生成保留/释放代码?

就我而言,我正在编写一个对性能敏感的日语文本解析器。作为解析的一部分,我经常需要访问一个 NSCharacterSet 汉字字符,我在 NSCharacterSet 的一个类别中定义了它。

+ (id)kanjiCharacterSet
{
    static NSCharacterSet* kanjiCharacterSet = nil;
    static dispatch_once_t onceToken;

    dispatch_once( &onceToken, ^
    {
        NSRange range = { .location = 0x4e00, .length = 0x9faf - 0x4e00 };
        kanjiCharacterSet = [NSCharacterSet characterSetWithRange:range];
    } );

    return kanjiCharacterSet;
}

我访问此字符集的最常见位置之一是检查字符串中是否存在汉字字符。此代码位于 NSString 上的一个类别中。

- (BOOL)containsKanji
{
    return [self rangeOfCharacterFromSet:[NSCharacterSet kanjiCharacterSet]].location != NSNotFound;
}

当我通过分析器运行此程序时,在 [NSString containsKanji] 中花费的全部时间中约有 40% 用于保留/释放代码。至于[NSCharacterSet kanjiCharacterSet],不包括我们实际生成字符集的第一次调用,每次调用大约有80%的时间花在了保留/释放代码上。

如果我在objc_retainobj_releaseobjc_autorelease 中设置断点,我可以看到[NSCharacterSet kanjiCharacterSet] 的return 语句中添加了一个retain 和一个autorelease,@987654329 中添加了一个retain @当它接收到从[NSCharacterSet kanjiCharacterSet]返回的值时,当我们从[NSString containsKanji]返回时正在添加一个释放。似乎所有这些调用对于单身人士来说都是不必要的。有没有办法告诉编译器不要生成这些调用?

我找到了一种解决方法,即向[NSString containsKanji] 添加一个静态变量来存储字符集,但我很想找到一个更通用的解决方案。

【问题讨论】:

  • 您是否分析调试配置或发布?在发布构建中启用优化后,编译器可以优化这些发布/自动释放调用。您也可以尝试在禁用 arc 的情况下编译文件 - 可能会减少开销
  • 我正在分析一个发布版本。感谢您关于禁用 ARC 的建议。不幸的是,如果我在包含单例的文件上禁用 ARC,调用代码仍然会生成一个释放调用,就好像单例在返回之前被保留一样,这迫使我无论如何都要将保留写入单例代码. =/
  • 另外,+ (id)kanjiCharacterSet;应该是 + (instancetype)kanjiCharacterSet;
  • 我差点让它返回 instancetype,但我注意到 Apple 在 NSCharacterSet 返回 id 中包含的字符集单例,所以我想我会遵循他们的约定。我想我认为他们没有将它们转换为实例类型一定是有原因的。
  • 您是否使用过 Core Foundation 的 CFStringCFCharacterSet

标签: objective-c performance singleton automatic-ref-counting


【解决方案1】:

在您反复调用 kanjiCharacterSet 的方法中,缓存一个副本。显然,只有在性能至关重要时才应该做这种事情。

- (BOOL)containsKanji
{
    static NSCharacterSet* cachedKanji = nil;
    static dispatch_once_t onceToken;

    dispatch_once (&onceToken, ^{
        cachedKanji = [NSCharacterSet kanjiCharacterSet];
    });

    return [self rangeOfCharacterFromSet: cachedKanji].location != NSNotFound;
}

【讨论】:

  • 所以你分派了一个方法,该方法在对象创建时分派。我怀疑这里有很多性能可以获得。
  • 这实际上是我现在正在做的。这是一个显着的性能改进,因为当您访问 cachedKanji 时,没有 ARC 生成的 retainrelease 调用。
  • @vikingosegundo:你错过了代码的巧妙之处。缓存的实例将永远不会再被保留或释放。
【解决方案2】:

我通过在方法声明中添加__attribute__((ns_returns_retained)) 找到了部分解决方案:

+ (id)kanjiCharacterSet __attribute__((ns_returns_retained));

根据documentation,这将强制编译器以与处理以alloccopyinitmutableCopynew 开头的方法相同的方式处理该方法。它只生成两个,而不是四个与保留/释放相关的调用:[NSCharacterSet kanjiCharacterSet] 中的一个保留和调用代码中的一个释放。

【讨论】:

  • 看起来NS_RETURNS_RETAINED 可以用来代替不太漂亮的__attribute__((ns_returns_retained)。但是,NS_RETURNS_RETAINED 声明的注释警告当方法符合 Cocoa 命名规则时不要使用任何一种技术(尽管您可能正确地将关键方法的性能视为创建此选项的“例外情况”之一) .
【解决方案3】:

您没有说明 -containsKanji 占总时间的百分比,这是您是否应该优化该例程的唯一相关指标。如果它是你总时间的 1%,那么它是否将所有时间都花在保持、旋转或洗碗上都没关系。

如果它实际上是你的大时间同步,你应该停止在那个方法中使用对象,有点......这是Objective-C的一大优势,我们可以编写低 -高级代码中间的级代码。

在你的情况下,由于汉字是从 0x4e00 到 0x9faf,我只写这样的:

- (BOOL)containsKanji;
{
    const NSUInteger bufferSize = 256;
    unichar buffer[bufferSize];

    for (NSUInteger characterIndex = 0; characterIndex < self.length; characterIndex += bufferSize) {
        const NSUInteger charactersToFetch = MIN(bufferSize, self.length - characterIndex);
        [self getCharacters:buffer range:(NSRange){characterIndex, charactersToFetch}];

        for (NSUInteger checkCharacterIndex = 0; checkCharacterIndex < charactersToFetch; checkCharacterIndex++) {
            unichar characterToCheck = buffer[characterIndex + checkCharacterIndex];
            if (characterToCheck >= 0x4e00 && characterToCheck <= 0x9faf)
                return YES;
        }
    }
    return NO;
}

注意我还没有编译这段代码,所以你可能需要修正错别字等。

【讨论】:

  • 这大约占我总时间的 7%,因此似乎值得尝试改进一下。如果我不必这样做,我有点不愿意走这条路,只是因为使用像rangeOfCharacterFromSet 这样的方法保证了可靠性,而不是自己滚动。但我想这就是权衡,不是吗?
  • 就提高效率而言,从 9 行代码增加到 11 行代码还不错。而且,老实说,我基本上已经放弃了在这样的类别中传播代码,除非它在很多地方使用——当出现错误时,很难找到实际代码。如果仅在一个地方调用此方法,请将代码内联。不要太早概括。
  • 谢谢威尔。我同意你所说的一切,这可能是我最终要走的方向。我想我只是被字符编码的怪事咬了足够多的时间,以至于对为这种事情滚动我自己的代码保持警惕。另外,我仍然很好奇是否有更通用的解决方案来提高在紧密的内部循环中访问单例的性能。
猜你喜欢
  • 2011-11-26
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-11-22
  • 2018-11-02
相关资源
最近更新 更多