Подтвердить что ты не робот

Почему реализация ARC objc_autoreleaseReturnValue отличается для x86_64 и ARM?

После прочтения превосходного сообщения в блоге Майка Эша "Friday Q & A 2014-05-09: Когда Autorelease не" на 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];
}

То, как эта оптимизация реализована, очень интересна. Я цитирую сообщение Майка:

"В реализации времени автономной работы Objective-C есть очень необычный и разумный код для выполнения. Перед отправкой сообщения об автоопределении он сначала проверяет код вызывающего абонента. Если он видит, что вызывающий абонент немедленно вызовет objc_retainAutoreleasedReturnValue, он полностью пропускает отправку сообщения. На самом деле он вообще не выполняет авторекламу, а просто закрывает объект в известном месте, что означает, что он вообще не отправил авторекламу.

Пока все хорошо. Реализация для x86_64 на NSObject.mm довольно проста. Код анализирует ассемблер, расположенный после обратного адреса 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 *[email protected](%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 не путем поиска присутствия вызова этой конкретной функции, а путем поиска вместо него специальной операции no-op mov r7, r7.

Погрузившись в исходный код LLVM, я нашел следующее объяснение:

"Реализация objc_autoreleaseReturnValue обнюхивает поток команд, следуя его обратному адресу, чтобы решить, является ли это вызовом objc_retainAutoreleasedReturnValue. Это может быть чрезмерно дорого, в зависимости от модели перемещения, и так далее на некоторых целевых объектах вместо этого он обнюхивает для определенной последовательности команд Эти функции возвращают эту последовательность команд в встроенной сборке, которая будет пустой, если не требуется."

Мне было интересно, почему это так на ARM?

Наличие компилятора помещает туда определенный маркер, так что конкретная реализация библиотеки может найти это, как сильная связь между компилятором и кодом библиотеки. Почему нельзя "обнюхивать" так же, как на платформе x86_64?

4b9b3361

Ответ 1

IIRC (прошло некоторое время с тех пор, как я написал сборку ARM), режимы адресации ARM на самом деле не позволяют прямое обращение по всему адресному пространству. Инструкции, используемые для адресации - загрузки, сохранения и т.д. - не поддерживают прямой доступ к полному адресному пространству, поскольку они ограничены в ширине бита.

Таким образом, любой вид идет на этот произвольный адрес и проверяет это значение, а затем использовать это значение для просмотра, там будет значительно медленнее ARM, поскольку вам нужно использовать косвенную адресацию, которая включает в себя математику и... математику Циклы CPU.

Благодаря тому, что компилятор испускает команду NO-OP, которая может быть легко проверена, она устраняет необходимость косвенности через заглушки DYLD.

По крайней мере, я уверен, что это то, что происходит. Два способа узнать наверняка; возьмите код для этих двух функций и скомпилируйте его с -Os для x86_64 по сравнению с ARM и посмотрите, как выглядят результирующие потоки команд (т.е. обе функции в каждой архитектуре) или подождите, пока не появится Greg Parker, чтобы исправить этот ответ.