После прочтения превосходного сообщения в блоге Майка Эша "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?