为什么ARC的objc_autoreleaseReturnValue的实现对x86_64和ARM有所不同?

时间:2014-05-20 15:36:18

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

在阅读了Mike Ash "Friday Q&A 2014-05-09: When an Autorelease Isn't"关于ARC的优秀博客文章之后,我决定查看ARC应用于加速保留/发布过程的优化细节。 我所指的技巧称为“快速自动释放”,其中调用者和被调用者合作以将返回的对象保持在自动释放池之外。这在以下情况下效果最佳:

- (id) myMethod {
    id obj = [MYClass new];
    return [obj autorelease];
}

- (void) mainMethod {
   obj = [[self myMethod] retain];
   // Do something with obj
   [obj release];
}

可以通过完全跳过自动释放池来优化:

- (id) myMethod {
    id obj = [MYClass new];
    return obj;
}

- (void) mainMethod {
   obj = [self myMethod];
   // Do something with obj
   [obj release];
}

实施此优化的方式非常有趣。我引用Mike的帖子:

  

“在Objective-C运行时自动释放的实现中有一些非常奇特和令人费解的代码。在实际发送自动释放消息之前,它首先检查调用者的代码。如果它看到调用者将立即调用objc_retainAutoreleasedReturnValue它完全跳过了发送的消息。它实际上根本不进行自动释放。相反,它只是将对象存放在已知位置,这表示它根本没有发送自动释放。“

到目前为止一切顺利。 NSObject.mm上的x86_64实现非常简单。代码分析位于objc_autoreleaseReturnValue返回地址之后的汇编程序,以查看是否存在对objc_retainAutoreleasedReturnValue的调用。

static bool callerAcceptsFastAutorelease(const void * const ra0)
{
    const uint8_t *ra1 = (const uint8_t *)ra0;
    const uint16_t *ra2;
    const uint32_t *ra4 = (const uint32_t *)ra1;
    const void **sym;

    //1. Navigate the DYLD stubs to get to the real pointer of the function to be called
    // 48 89 c7    movq  %rax,%rdi
    // e8          callq symbol
    if (*ra4 != 0xe8c78948) {
        return false;
    }

    ra1 += (long)*(const int32_t *)(ra1 + 4) + 8l;
    ra2 = (const uint16_t *)ra1;
    // ff 25       jmpq *symbol@DYLDMAGIC(%rip)
    if (*ra2 != 0x25ff) {
        return false;
    }

    ra1 += 6l + (long)*(const int32_t *)(ra1 + 2);
    sym = (const void **)ra1;

    //2. Check that the code to be called belongs to objc_retainAutoreleasedReturnValue
    if (*sym != objc_retainAutoreleasedReturnValue)
    {
        return false;
    }

    return true;
}

但是当谈到ARM时,我无法理解它是如何工作的。代码看起来像这样(我简化了一点):

static bool callerAcceptsFastAutorelease(const void *ra)
{
    // 07 70 a0 e1    mov r7, r7
    if (*(uint32_t *)ra == 0xe1a07007) {
        return true;
    }
    return false;
}

看起来代码正在识别objc_retainAutoreleasedReturnValue的存在,而不是通过查找是否存在对该特定函数的调用,而是查找特殊的无操作操作mov r7, r7

深入了解LLVM源代码我发现了以下解释:

  

“objc_autoreleaseReturnValue的实现在其返回地址之后嗅探指令流,以确定它是否是对objc_retainAutoreleasedReturnValue的调用。这可能过于昂贵,取决于重定位模型,等等一些目标它反过来嗅探特定的指令序列这个函数在内联汇编中返回该指令序列,如果不需要,它将为空。“

我想知道为什么在ARM上会如此?

让编译器放置一个标记,以便库的特定实现可以发现它听起来像编译器和库代码之间的强耦合。为什么不能像在x86_64平台上那样实现“嗅探”?

1 个答案:

答案 0 :(得分:19)

IIRC(自从我编写ARM程序集以来已经有一段时间了),ARM的寻址模式实际上并不允许在整个地址空间中进行直接寻址。用于执行寻址的指令 - 加载,存储等... - 不支持直接访问整个地址空间,因为它们的位宽有限。

因此,任何类型的都会转到这个任意地址并检查该值,然后使用该值来查看那里在ARM上会明显变慢,因为你必须使用间接寻址数学和...数学吃CPU周期。

通过让编译器发出可以轻松检查的NO-OP指令,它不需要通过DYLD存根进行间接寻址。

至少,我很确定这是怎么回事。有两种方法可以肯定;获取这两个函数的代码并使用-Os为x86_64与ARM编译它,看看结果指令流的样子(即每个架构上的两个函数)或等到Greg Parker出现以纠正这个答案。