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

duh*_*bel 21 assembly memory-management objective-c ios automatic-ref-counting

在阅读了Mike Ash的优秀博客文章"星期五Q&A 2014-05-09:当Autorelease不是"时,我决定查看ARC应用于加速保留/释放过程的优化的详细信息.我所指的技巧称为"快速自动释放",其中调用者和被调用者合作以将返回的对象保持在自动释放池之外.这在以下情况下效果最佳:

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

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

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

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

- (void) mainMethod {
   obj = [self myMethod];
   // Do something with obj
   [obj release];
}
Run Code Online (Sandbox Code Playgroud)

实现此优化的方式非常有趣.我引用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;
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

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

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

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

bbu*_*bum 19

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

因此,任何类型的转到这个任意地址并检查该值,然后使用该值来查看将在ARM上显着更慢,因为你必须使用间接寻址,这涉及数学和...数学吃CPU周期.

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

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

  • 另一个区别.解决的dyld存根在英特尔上很简单:它只是分支的一个分支.在ARM上,分支到存根的指令序列和存根的分支可以采用许多不同的形式,具体取决于分支的长度.检查每个组合会很慢. (10认同)
  • 另请注意,两个版本中的编译器和库之间存在"强耦合".例如,在Intel上,编译器优化器不得在调用/ mov/call序列中调度任何其他指令. (5认同)
  • 使用魔术指令更难实现.在我们开始研究ARM版本之前,我们没有考虑过IIRC,此时更改英特尔版本为时已晚. (2认同)