[Objc翻译]看看 ARC 的引擎盖下 - 第 4 集

102 阅读8分钟

本文由 简悦SimpRead 转码, 原文地址 www.galloway.me.uk

我深入 ARC 底层的下一集从 @steipete...... 的这条推特开始

下一集我对 ARC 的深入探讨将从 @steipete 的这条推特开始,在这条推特中,他说:"有了 ARC,我现在发现自己要为愚蠢的模型对象输入 "new"。好还是不好?这引起了我的思考。他说得完全正确,有了 ARC,我们现在可以直接使用"[SomeClass new]",让 ARC 帮我们处理所有的内存管理。以前,我们经常会在 SomeClass 上创建一个方便的类方法,该方法会返回一个自动释放的对象,这样就可以使调用代码简洁明了,并易于理解内存管理。现在有了 ARC,我们就不需要这样做了,我想知道使用 newalloc + init 比使用我们的老朋友方便类方法有什么好处。这篇博文讲述了这个故事。

关于 new 的一些背景

首先,我们来看看 new 的实际作用。根据 Apple 文档,它是这样做的:

分配一个接收类的新实例,向其发送 init 消息,并返回初始化对象。

因此,我们应该把类似[SomeClass new]的调用等同于[[SomeClass alloc] init]。这里的内存管理告诉我们,返回的对象归调用者所有,也就是说,返回的对象的保留数为 +1。因此,在使用完该对象后,我们必须释放它。正如我们所知,ARC 为我们添加了这些功能。

测试内容

我想知道的是,这些方法中哪个更快?

  1. [[SomeClass alloc] init]
  2. [SomeClass new]
  3. [SomeClass giveMeAnObject]
  4. [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);

为了测试这一点,并确保编译器/运行时不会通过使用 NSStringNSNumber 来走捷径,我创建了一个名为 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 下编译了代码。

结果见下表!

以下是运行测试的结果。每列下的值是左侧给出的迭代次数所耗费的时间(以毫秒为单位)。

ABCD
10002. 2642.3492.1992.394
500010. 10210.1499.99311.017
1000019.18020.14819. 50920.036
5000092.35798.177104.36297. 099
100000185.054199.825204.560194.353
500000924. 0901000.5881335.106985.735
10000001863.1101973. 0862885.7191977.487
50000009407.94110245.85723314.4959757. 074
1000000018557.63220841.90556602.49120315.784

图形显示如下

image.png

这说明了什么?基本上,它告诉我们 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 的速度大致相同,这当然也是我们所期望的。这意味着我们最好使用 newalloc + init 或返回保留数为 +1 的对象的方便方法,而不是使用返回自动释放对象的方便方法。请参阅下文,了解方法 C 为何较慢,以及方法 C 如何变得与其他方法一样快。

啊哈,原因就在这里!

经过深入研究,我找到了方法 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 的先前结果中):

ABCDE
10002. 2642.3492.1992.3942.401
500010. 10210.1499.99311.01711.381
1000019.18020.14819. 50920.03622.120
5000092.35798.177104.36297.099106. 966
100000185.054199.825204.560194.353223.045
500000924. 0901000.5881335.106985.7351113.261
10000001863.1101973.0862885. 7191977.4872262.960
50000009407.94110245.85723314.4959757.07411419. 025
1000000018557.63220841.90556602.49120315.78422510.462

因此,这至少解释了为什么方法 C 要慢得多。但是我不知道为什么当 giveMeAnObject 的返回类型是 ClassA*id 时,编译器不会发出同样的提示。

更新:事实证明,这是一个错误

结果这是编译器的Bug,编译器(编译器的优化部分)对返回 idClassA* 的情况以及在方法中分拆出 alloc + init 和在同一行返回的情况做了不同的处理。所有这些都应该编译得一模一样,但在当前版本的 clang 中却并非如此。