使用ARC,有什么更好的:alloc或autorelease初始化器?

时间:2011-07-21 13:07:27

标签: objective-c cocoa-touch cocoa memory-management automatic-ref-counting

使用allocautorelease初始化程序是否更好(更快,更高效)。 E.g:

- (NSString *)hello:(NSString *)name {
    return [[NSString alloc] initWithFormat:@"Hello, %@", name];
}

OR

- (NSString *)hello:(NSString *)name {
    return [NSString stringWithFormat:@"Hello, %@", name];
//    return [@"Hello, " stringByAppendingString:name]; // even simpler
}

我知道在大多数情况下,这里的表现无关紧要。但是,我仍然希望养成更好的方式。

如果他们做同样的事情,那么我更喜欢后一种选择,因为它的输入更短,更具可读性。

在Xcode 4.2中,有没有办法看到ARC编译的内容,即它放置retainreleaseautorelease等的位置?切换到ARC时,此功能非常有用。我知道你不应该考虑这些东西,但这有助于我弄清楚这些问题的答案。

6 个答案:

答案 0 :(得分:36)

差异很小,但您应该选择autorelease版本。首先,您的代码更具可读性。其次,在检查优化的装配输出时,autorelease版本稍微更优化。

autorelease版本,

- (NSString *)hello:(NSString *)name {
    return [NSString stringWithFormat:@"Hello, %@", name];
}

转换为

"-[SGCAppDelegate hello:]":
    push    {r7, lr}
    movw    r1, :lower16:(L_OBJC_SELECTOR_REFERENCES_-(LPC0_0+4))
    mov r3, r2
    movt    r1, :upper16:(L_OBJC_SELECTOR_REFERENCES_-(LPC0_0+4))
    movw    r0, :lower16:(L_OBJC_CLASSLIST_REFERENCES_$_-(LPC0_1+4))
    movt    r0, :upper16:(L_OBJC_CLASSLIST_REFERENCES_$_-(LPC0_1+4))
    add r1, pc
    add r0, pc
    mov r7, sp
    ldr r1, [r1]
    ldr r0, [r0]
    movw    r2, :lower16:(L__unnamed_cfstring_-(LPC0_2+4))
    movt    r2, :upper16:(L__unnamed_cfstring_-(LPC0_2+4))
    add r2, pc
    blx _objc_msgSend    ; stringWithFormat:
    pop {r7, pc}

而[[alloc] init]版本如下所示:

"-[SGCAppDelegate hello:]":
    push    {r4, r5, r6, r7, lr}
    movw    r1, :lower16:(L_OBJC_SELECTOR_REFERENCES_2-(LPC1_0+4))
    add r7, sp, #12
    movt    r1, :upper16:(L_OBJC_SELECTOR_REFERENCES_2-(LPC1_0+4))
    movw    r0, :lower16:(L_OBJC_CLASSLIST_REFERENCES_$_-(LPC1_1+4))
    movt    r0, :upper16:(L_OBJC_CLASSLIST_REFERENCES_$_-(LPC1_1+4))
    add r1, pc
    add r0, pc
    ldr r5, [r1]
    ldr r6, [r0]
    mov r0, r2
    blx _objc_retain    ; ARC retains the name string temporarily
    mov r1, r5
    mov r4, r0
    mov r0, r6
    blx _objc_msgSend   ; call to alloc
    movw    r1, :lower16:(L_OBJC_SELECTOR_REFERENCES_4-(LPC1_2+4))
    mov r3, r4
    movt    r1, :upper16:(L_OBJC_SELECTOR_REFERENCES_4-(LPC1_2+4))
    add r1, pc
    ldr r1, [r1]
    movw    r2, :lower16:(L__unnamed_cfstring_-(LPC1_3+4))
    movt    r2, :upper16:(L__unnamed_cfstring_-(LPC1_3+4))
    add r2, pc
    blx _objc_msgSend   ; call to initWithFormat:
    mov r5, r0
    mov r0, r4
    blx _objc_release   ; ARC releases the name string
    mov r0, r5
    pop.w   {r4, r5, r6, r7, lr}
    b.w _objc_autorelease

正如预期的那样,它会更长一些,因为它调用了allocinitWithFormat:方法。特别有趣的是ARC在这里生成次优代码,因为它保留name字符串(通过调用_objc_retain注释),然后在调用initWithFormat:后释放。

如果我们添加__unsafe_unretained所有权限定符,如下例所示,代码将以最佳方式呈现。 __unsafe_unretained表示编译器使用原语(复制指针)assignment semantics

- (NSString *)hello:(__unsafe_unretained NSString *)name {
    return [[NSString alloc] initWithFormat:@"Hello, %@", name];
}

如下:

"-[SGCAppDelegate hello:]":
    push    {r4, r7, lr}
    movw    r1, :lower16:(L_OBJC_SELECTOR_REFERENCES_2-(LPC1_0+4))
    add r7, sp, #4
    movt    r1, :upper16:(L_OBJC_SELECTOR_REFERENCES_2-(LPC1_0+4))
    movw    r0, :lower16:(L_OBJC_CLASSLIST_REFERENCES_$_-(LPC1_1+4))
    movt    r0, :upper16:(L_OBJC_CLASSLIST_REFERENCES_$_-(LPC1_1+4))
    add r1, pc
    add r0, pc
    mov r4, r2
    ldr r1, [r1]
    ldr r0, [r0]
    blx _objc_msgSend
    movw    r1, :lower16:(L_OBJC_SELECTOR_REFERENCES_4-(LPC1_2+4))
    mov r3, r4
    movt    r1, :upper16:(L_OBJC_SELECTOR_REFERENCES_4-(LPC1_2+4))
    add r1, pc
    ldr r1, [r1]
    movw    r2, :lower16:(L__unnamed_cfstring_-(LPC1_3+4))
    movt    r2, :upper16:(L__unnamed_cfstring_-(LPC1_3+4))
    add r2, pc
    blx _objc_msgSend
    .loc    1 31 1
    pop.w   {r4, r7, lr}
    b.w _objc_autorelease

答案 1 :(得分:10)

[NSString stringWithFormat:]代码较少。但请注意,对象可能最终会出现在自动释放池中。即使使用ARC和-Os编译器优化,目前也会发生这种情况。

目前,[[NSString alloc] initWithFormat:]的性能在iOS(使用iOS 5.1.1和Xcode 4.3.3测试)和OS X(使用OS X 10.7.4和Xcode 4.3.3测试)上都更好。我修改了@Pascal的示例代码以包含自动释放池的排放时间并得到以下结果:

  • ARC优化不会阻止对象在自动释放池中结束。
  • 包括清理具有100万个对象的发布池的时间,{4}在iPhone 4S上快{14},在OS X上快8%左右
  • 在循环周围有一个@autoreleasepool会释放循环中的所有对象,这会占用大量内存。

    Instruments showing memory spikes for [NSString stringWithFormat:] and not for [[NSString alloc] initWithFormat:] on iOS 5.1

  • 可以通过在循环内使用@autoreleasepool来防止内存峰值。性能保持大致相同,但内存消耗则持平。

答案 2 :(得分:4)

我不同意其他答案,autorelease版本(你的第二个例子)不一定更好。

autorelease版本的行为与ARC之前的版本相同。它分配和inits然后自动释放,这意味着需要存储指向对象的指针,以便在下次自动释放池耗尽时自动释放。这使用稍多的内存,因为在处理之前需要保持指向该对象的指针。与立即释放的物体相比,物体的粘附时间也更长。如果您在循环中多次调用此方法,则可能会出现问题,因此自动释放池无法排空。这可能会导致内存不足。

第一个示例的行为与ARC之前的行为不同。使用ARC,编译器现在将为您插入“发布”(不像第二个示例那样自动释放)。它在分配内存的块的末尾执行此操作。通常这是在调用它的函数的末尾。在您的示例中,从查看程序集看起来,对象实际上可能是自动释放的。这可能是由于编译器不知道函数返回的位置,因此块的结尾位于何处。在块的末尾由编译器添加版本的大多数情况下,alloc / init方法将导致更好的性能,至少在内存使用方面,就像在ARC之前一样。

答案 3 :(得分:3)

嗯,这很容易测试,实际上看起来方便构造函数“更快” - 除非我在测试代码中犯了一些错误,见下文。

输出(100万次构建的时间)

Alloc/init:   842.549473 millisec
Convenience:  741.611933 millisec
Alloc/init:   799.667462 millisec
Convenience:  741.814478 millisec
Alloc/init:   821.125221 millisec
Convenience:  741.376502 millisec
Alloc/init:   811.214693 millisec
Convenience:  795.786457 millisec

<强>脚本

#import <Foundation/Foundation.h>
#import <mach/mach_time.h>

int main (int argc, const char * argv[])
{

    @autoreleasepool {
        NSUInteger runs = 4;

        mach_timebase_info_data_t timebase;
        mach_timebase_info(&timebase);
        double ticksToNanoseconds = (double)timebase.numer / timebase.denom;

        NSString *format = @"Hello %@";
        NSString *world = @"World";

        NSUInteger t = 0;
        for (; t < 2*runs; t++) {
            uint64_t start = mach_absolute_time();
            NSUInteger i = 0;
            for (; i < 1000000; i++) {
                if (0 == t % 2) {       // alloc/init
                    NSString *string = [[NSString alloc] initWithFormat:format, world];
                }
                else {                  // convenience
                    NSString *string = [NSString stringWithFormat:format, world];
                }
            }
            uint64_t run = mach_absolute_time() - start;
            double runTime = run * ticksToNanoseconds;

            if (0 == t % 2) {
                NSLog(@"Alloc/init:   %.6f millisec", runTime / 1000000);
            }
            else {
                NSLog(@"Convenience:  %.6f millisec", runTime / 1000000);
            }
        }
    }
    return 0;
}

答案 4 :(得分:1)

由于两个原因,比较两者的表现是一个没有实际意义的问题。首先,随着Clang的发展,两者的性能特征可能会发生变化,并且会在编译器中添加新的优化。其次,在这里和那里跳过一些指令的好处充其量是可疑的。应跨方法边界考虑应用程序的性能。解构一种方法可能是骗人的。

答案 5 :(得分:0)

我认为stringWithFormat:实现实际上就像你的第一个版本一样实现,这意味着什么都不应该改变。在任何情况下,如果有任何差异,可能看起来第二个版本不应该慢。最后,在我看来,第二个版本的可读性稍高一些,所以这就是我使用的。