为啥 ARC 的 objc_autoreleaseReturnValue 的实现对于 x86_64 和 ARM 不同?

Posted

技术标签:

【中文标题】为啥 ARC 的 objc_autoreleaseReturnValue 的实现对于 x86_64 和 ARM 不同?【英文标题】:Why the implementation of ARC's objc_autoreleaseReturnValue differs for x86_64 and ARM?为什么 ARC 的 objc_autoreleaseReturnValue 的实现对于 x86_64 和 ARM 不同? 【发布时间】:2014-07-08 23:29:50 【问题描述】:

在阅读了 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运行时的autorelease实现中有一些非常花哨和令人费解的代码。在实际发送autorelease消息之前,它首先检查调用者的代码。如果它看到调用者将立即调用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】:

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

因此,任何类型的到该任意地址并检查该值,然后使用该值去查看那里在 ARM 上都会显着变慢,因为您必须使用间接寻址,其中涉及数学和...数学会占用 CPU 周期。

通过让编译器发出一个易于检查的 NO-OP 指令,它消除了通过 DYLD 存根进行间接寻址的需要。

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

【讨论】:

另一个区别。解析的 dyld 存根在 Intel 上很简单:它只是一个分支到一个分支。在 ARM 上,分支到存根和从存根分支的指令序列可以采用多种不同的形式,具体取决于分支的长度。检查每个组合会很慢。 另请注意,两个版本的编译器和库之间存在“强耦合”。例如,在 Intel 上,编译器优化器不得在 call/mov/call 序列中调度任何其他指令。 @GregParker - 啊!这很有意义。但是为什么不简化 callerAcceptsFastAutorelease 的代码并在 x86_64 上也使用 ARM 标记呢? 使用魔法指令更难实现。 IIRC 直到我们开始研究 ARM 版本时我们才考虑它,此时更改 Intel 版本为时已晚。

以上是关于为啥 ARC 的 objc_autoreleaseReturnValue 的实现对于 x86_64 和 ARM 不同?的主要内容,如果未能解决你的问题,请参考以下文章

为啥 Clippy 建议传递 Arc 作为参考?

为啥 ARC 在 popViewController 之后不释放内存

为啥在启用 ARC 的项目中不需要维护保留计数

为啥 Mutex 被设计为需要 Rust 中的 Arc

为啥我不能在 ARC 中使用自定义颜色创建 CAGradientLayer?

为啥迁移到 ARC 后我的应用程序充满了内存泄漏?