本文由 简悦SimpRead 转码, 原文地址 www.galloway.me.uk
我深入 ARC 底层的下一集从 @steipete...... 的这条推特开始
下一集我对 ARC 的深入探讨将从 @steipete 的这条推特开始,在这条推特中,他说:"有了 ARC,我现在发现自己要为愚蠢的模型对象输入 "new"。好还是不好?这引起了我的思考。他说得完全正确,有了 ARC,我们现在可以直接使用"[SomeClass new]",让 ARC 帮我们处理所有的内存管理。以前,我们经常会在 SomeClass 上创建一个方便的类方法,该方法会返回一个自动释放的对象,这样就可以使调用代码简洁明了,并易于理解内存管理。现在有了 ARC,我们就不需要这样做了,我想知道使用 new 和 alloc + init 比使用我们的老朋友方便类方法有什么好处。这篇博文讲述了这个故事。
关于 new 的一些背景
首先,我们来看看 new 的实际作用。根据 Apple 文档,它是这样做的:
分配一个接收类的新实例,向其发送 init 消息,并返回初始化对象。
因此,我们应该把类似[SomeClass new]的调用等同于[[SomeClass alloc] init]。这里的内存管理告诉我们,返回的对象归调用者所有,也就是说,返回的对象的保留数为 +1。因此,在使用完该对象后,我们必须释放它。正如我们所知,ARC 为我们添加了这些功能。
测试内容
我想知道的是,这些方法中哪个更快?
[[SomeClass alloc] init][SomeClass new][SomeClass giveMeAnObject][SomeClass newObject]
其中 giveMeAnObject 是一个返回自动释放对象的方便方法,而 newObject 是一个方便方法,我们希望它与标准的 new相同。
如何测试
为了对每种方法进行基准测试,我决定在内存管理正确的情况下(如果启用了 ARC,我就别无选择了),计时调用每种方法一定次数所需的时间。我使用了这种计时方法,它能给出代码执行所需的纳秒数:
uint64_t start = mach_absolute_time();
// Do something which takes a while
uint64_t end = mach_absolute_time();
mach_timebase_info_data_t timebaseInfo;
mach_timebase_info(&timebaseInfo);
uint64_t timeNanos = (end - start) * timebaseInfo.numer / timebaseInfo.denom;
NSLog(@"time = %"PRIu64, timeNanos);
为了测试这一点,并确保编译器/运行时不会通过使用 NSString 或 NSNumber 来走捷径,我创建了一个名为 ClassA 的简单假类,如下所示:
@interface ClassA : NSObject
+ (ClassA*)giveMeAnObject;
+ (ClassA*)newObject;
@end
@implementation ClassA
+ (ClassA*)giveMeAnObject {
return [[ClassA alloc] init];
}
+ (ClassA*)newObject {
return [[ClassA alloc] init];
}
@end
然后,为了对每种方式进行基准测试,我决定对创建 ClassA 实例的每种方式循环迭代 1000 到 10000000 次不等。每种方式的效果应该完全相同,但我们想知道它们在速度上有何不同。下面是我使用的代码,每次测试时,除了一个 ClassA *x = 的注释外,其余的都注释掉了。
for (unsigned long long i = 0; i < iterations; ++i) {
ClassA *a = [[ClassA alloc] init];
ClassA *b = [ClassA new];
ClassA *c = [ClassA giveMeAnObject];
ClassA *d = [ClassA newObject];
}
在每个测试中,我都使用了运行 iOS 5.0.1 的 iPhone 4(因此是 ARMv7),并在 O3 下编译了代码。
结果见下表!
以下是运行测试的结果。每列下的值是左侧给出的迭代次数所耗费的时间(以毫秒为单位)。
| A | B | C | D | |
|---|---|---|---|---|
| 1000 | 2. 264 | 2.349 | 2.199 | 2.394 |
| 5000 | 10. 102 | 10.149 | 9.993 | 11.017 |
| 10000 | 19.180 | 20.148 | 19. 509 | 20.036 |
| 50000 | 92.357 | 98.177 | 104.362 | 97. 099 |
| 100000 | 185.054 | 199.825 | 204.560 | 194.353 |
| 500000 | 924. 090 | 1000.588 | 1335.106 | 985.735 |
| 1000000 | 1863.110 | 1973. 086 | 2885.719 | 1977.487 |
| 5000000 | 9407.941 | 10245.857 | 23314.495 | 9757. 074 |
| 10000000 | 18557.632 | 20841.905 | 56602.491 | 20315.784 |
图形显示如下
这说明了什么?基本上,它告诉我们 alloc + init 是最快的,而 new 和我们自定义的方便 new 则紧随其后。它还告诉我们,在迭代次数较多的情况下,我们返回自动释放值的方便方法要慢一些。在最大迭代次数时,它比其他方法慢两倍多。
让我们分析一下当时的情况
为了了解这里发生了什么,让我们来看看生成的代码。下面是各种有趣的代码。
ClassA 的 giveMeAnObject(18 条指令)
.align 2
.code 16
.thumb_func "+[ClassA giveMeAnObject]"
"+[ClassA giveMeAnObject]":
push {r7, lr}
movw r1, :lower16:(L_OBJC_SELECTOR_REFERENCES_-(LPC0_0+4))
mov r7, sp
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))
LPC0_0:
add r1, pc
LPC0_1:
add r0, pc
ldr r1, [r1]
ldr r0, [r0]
blx _objc_msgSend
movw r1, :lower16:(L_OBJC_SELECTOR_REFERENCES_2-(LPC0_2+4))
movt r1, :upper16:(L_OBJC_SELECTOR_REFERENCES_2-(LPC0_2+4))
LPC0_2:
add r1, pc
ldr r1, [r1]
blx _objc_msgSend
pop.w {r7, lr}
b.w _objc_autorelease
ClassA's newObject (17 指令)
.align 2
.code 16
.thumb_func "+[ClassA newObject]"
"+[ClassA newObject]":
push {r7, lr}
movw r1, :lower16:(L_OBJC_SELECTOR_REFERENCES_-(LPC4_0+4))
mov r7, sp
movt r1, :upper16:(L_OBJC_SELECTOR_REFERENCES_-(LPC4_0+4))
movw r0, :lower16:(L_OBJC_CLASSLIST_REFERENCES_$_-(LPC4_1+4))
movt r0, :upper16:(L_OBJC_CLASSLIST_REFERENCES_$_-(LPC4_1+4))
LPC4_0:
add r1, pc
LPC4_1:
add r0, pc
ldr r1, [r1]
ldr r0, [r0]
blx _objc_msgSend
movw r1, :lower16:(L_OBJC_SELECTOR_REFERENCES_2-(LPC4_2+4))
movt r1, :upper16:(L_OBJC_SELECTOR_REFERENCES_2-(LPC4_2+4))
LPC4_2:
add r1, pc
ldr r1, [r1]
pop.w {r7, lr}
b.w _objc_msgSend
方法 A 的迭代循环(11 条指令)
LBB2_1:
ldr r1, [r5]
ldr r0, [r6]
blx _objc_msgSend
ldr.w r1, [r8]
blx _objc_msgSend
blx _objc_release
adds.w r11, r11, #1
eor.w r0, r11, r10
adc r4, r4, #0
orrs r0, r4
bne LBB2_1
方法 B 的迭代循环(9 条指令)
LBB2_1:
ldr r1, [r5]
ldr r0, [r6]
blx _objc_msgSend
blx _objc_release
adds.w r10, r10, #1
eor.w r0, r10, r8
adc r4, r4, #0
orrs r0, r4
bne LBB2_1
方法 C 的迭代循环(11 条指令)
LBB2_1:
ldr r1, [r5]
ldr r0, [r6]
blx _objc_msgSend
@ InlineAsm Start
mov r7, r7 @ marker for objc_retainAutoreleaseReturnValue
@ InlineAsm End
blx _objc_retainAutoreleasedReturnValue
blx _objc_release
adds.w r10, r10, #1
eor.w r0, r10, r8
adc r4, r4, #0
orrs r0, r4
bne LBB2_1
方法 D 的迭代循环(9 条指令)
LBB2_1:
ldr r1, [r5]
ldr r0, [r6]
blx _objc_msgSend
blx _objc_release
adds.w r10, r10, #1
eor.w r0, r10, r8
adc r4, r4, #0
orrs r0, r4
bne LBB2_1
看完所有相关代码后,我们可能会对这些代码的不同感到惊讶。它们都会有类似数量的指令。事实上,方法 A 的内循环指令更多,但速度最快。有趣的问题是,为什么方法 C 在大量迭代时比其他方法慢得多?如果我们看一下方法 C 的生成代码,就会发现有一个对 objc_retainAutoreleasedReturnValue 的调用。该方法是一种保留已自动释放返回值的快捷方式。由于所有代码都是使用 ARC 编译的,并在 iOS 5 设备上运行,因此它应该能与我们的代码一起工作。有趣的是,当迭代次数较多时,这种方法所需的时间是原来的两倍。我可以理解这可能会更慢,因为有更多的消息派发在进行,但我没想到会慢这么多,而且有趣的是,随着迭代次数的增加,差异也在增大。
结论
实际上,我不知道该如何解释为什么方法 C 的速度如此之慢。很高兴看到 A、B 和 D 的速度大致相同,这当然也是我们所期望的。这意味着我们最好使用 。请参阅下文,了解方法 C 为何较慢,以及方法 C 如何变得与其他方法一样快。new、alloc + init 或返回保留数为 +1 的对象的方便方法,而不是使用返回自动释放对象的方便方法
啊哈,原因就在这里!
经过深入研究,我找到了方法 C 速度如此之慢的原因。在我写这篇文章的时候,我觉得有点奇怪,在 giveMeAnObject 中,尾部调用的是 objc_autorelease 而不是 objc_autoreleaseReturnValue。我之前提到的 objc_retainAutoreleasedReturnValue 的神奇作用只有在值已通过 objc_autoreleaseReturnValue 返回的情况下才会起作用。关于其内部原理,我们将在以后的博文中讨论,但请相信我,它就是这样工作的。因此,我决定将 giveMeAnObject 的返回类型从 ClassA* 改为 id。我以为这样做应该没有什么区别。我错了。看看吧
+ (id)giveMeAnObject {
return [[ClassA alloc] init];
}
giveMeAnObject 的汇编
.align 2
.code 16
.thumb_func "+[ClassA giveMeAnObject3]"
"+[ClassA giveMeAnObject3]":
push {r7, lr}
movw r1, :lower16:(L_OBJC_SELECTOR_REFERENCES_-(LPC2_0+4))
mov r7, sp
movt r1, :upper16:(L_OBJC_SELECTOR_REFERENCES_-(LPC2_0+4))
movw r0, :lower16:(L_OBJC_CLASSLIST_REFERENCES_$_-(LPC2_1+4))
movt r0, :upper16:(L_OBJC_CLASSLIST_REFERENCES_$_-(LPC2_1+4))
LPC2_0:
add r1, pc
LPC2_1:
add r0, pc
ldr r1, [r1]
ldr r0, [r0]
blx _objc_msgSend
movw r1, :lower16:(L_OBJC_SELECTOR_REFERENCES_2-(LPC2_2+4))
movt r1, :upper16:(L_OBJC_SELECTOR_REFERENCES_2-(LPC2_2+4))
LPC2_2:
add r1, pc
ldr r1, [r1]
blx _objc_msgSend
pop.w {r7, lr}
b.w _objc_autoreleaseReturnValue
这里的唯一区别是调用了 objc_autoreleaseReturnValue 而不是 objc_autorelease。 我仍不太明白为什么编译器在这里做了一些不同的事情,所以我还得把这个问题搞清楚使用此方法的基准测试结果如下(添加到我调用了此新方法 E 的先前结果中):
| A | B | C | D | E | |
|---|---|---|---|---|---|
| 1000 | 2. 264 | 2.349 | 2.199 | 2.394 | 2.401 |
| 5000 | 10. 102 | 10.149 | 9.993 | 11.017 | 11.381 |
| 10000 | 19.180 | 20.148 | 19. 509 | 20.036 | 22.120 |
| 50000 | 92.357 | 98.177 | 104.362 | 97.099 | 106. 966 |
| 100000 | 185.054 | 199.825 | 204.560 | 194.353 | 223.045 |
| 500000 | 924. 090 | 1000.588 | 1335.106 | 985.735 | 1113.261 |
| 1000000 | 1863.110 | 1973.086 | 2885. 719 | 1977.487 | 2262.960 |
| 5000000 | 9407.941 | 10245.857 | 23314.495 | 9757.074 | 11419. 025 |
| 10000000 | 18557.632 | 20841.905 | 56602.491 | 20315.784 | 22510.462 |
因此,这至少解释了为什么方法 C 要慢得多。但是我不知道为什么当 giveMeAnObject 的返回类型是 ClassA* 或 id 时,编译器不会发出同样的提示。
更新:事实证明,这是一个错误
结果这是编译器的Bug,编译器(编译器的优化部分)对返回 id 和 ClassA* 的情况以及在方法中分拆出 alloc + init 和在同一行返回的情况做了不同的处理。所有这些都应该编译得一模一样,但在当前版本的 clang 中却并非如此。