ios objective-c memory-management assembly automatic-ref-counting

ios - ¿Por qué la implementación de objc_autoreleaseReturnValue de ARC difiere para x86_64 y ARM?



objective-c memory-management (1)

IIRC (hace un tiempo que no escribí el ensamblado de ARM), los modos de direccionamiento de ARM realmente no permiten el direccionamiento directo en todo el espacio de direcciones. Las instrucciones utilizadas para hacer direccionamiento (cargas, tiendas, etc.) no admiten el acceso directo al espacio de direcciones completo ya que tienen un ancho de bits limitado.

Por lo tanto, cualquier tipo de ir a esta dirección arbitraria y verificar ese valor, luego usar ese valor para ir a mirar allí será significativamente más lento en ARM ya que tiene que usar el direccionamiento indirecto que implica matemáticas y ... la matemática consume ciclos de CPU.

Al hacer que un compilador emita una instrucción NO-OP que se pueda verificar fácilmente, elimina la necesidad de indirección a través de los stubs DYLD.

Al menos, estoy bastante seguro de que eso es lo que está pasando. Dos formas de saberlo con certeza; tomar el código para esas dos funciones y compilarlo con -Os para x86_64 frente a ARM y ver cómo se ven las secuencias de instrucciones resultantes (es decir, ambas funciones en cada arquitectura) o esperar hasta que Greg Parker aparezca para corregir esta respuesta.

Después de leer la excelente publicación de blog de Mike Ash "Viernes Q & A 2014-05-09: Cuando no hay Autorización" en ARC, decidí verificar los detalles de las optimizaciones que aplica ARC para acelerar el proceso de retención / liberación. El truco al que me refiero se llama "Autorrelease rápido" en el que la persona que llama y el que llama cooperan para mantener el objeto devuelto fuera del grupo de autorrelease. Esto funciona mejor en situaciones como las siguientes:

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

que se puede optimizar omitiendo completamente el grupo de autorrelease:

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

La forma en que se implementa esta optimización es muy interesante. Cito de la publicación de Mike:

"Hay un código extremadamente sofisticado y alucinante en la implementación de liberación automática del tiempo de ejecución del Objective-C. Antes de enviar un mensaje de liberación automática, primero inspecciona el código de la persona que llama. Si ve que la persona que llama va a llamar inmediatamente a objc_retainAutoreleasedReturnValue, completamente se salta el mensaje enviado. En realidad, no hace una liberación automática en absoluto. En cambio, simplemente guarda el objeto en una ubicación conocida, lo que indica que no ha enviado ningún reenvío automático ".

Hasta aquí todo bien. La implementación de x86_64 en NSObject.mm es bastante sencilla. El código analiza el ensamblador ubicado después de la dirección de retorno de objc_autoreleaseReturnValue para la presencia de una llamada a 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; }

Pero cuando se trata de ARM, simplemente no puedo entender cómo funciona. El código se ve así (simplifiqué un poco):

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

Parece que el código identifica la presencia de objc_retainAutoreleasedReturnValue no buscando la presencia de una llamada a esa función específica, sino buscando en cambio una operación no operativa especial mov r7, r7 .

Al sumergirme en el código fuente de LLVM, encontré la siguiente explicación:

"La implementación de objc_autoreleaseReturnValue olfatea la secuencia de instrucciones que sigue a su dirección de retorno para decidir si se trata de una llamada a objc_retainAutoreleasedReturnValue. Esto puede ser prohibitivamente costoso, dependiendo del modelo de reubicación, y así en algunos objetivos huele para una secuencia de instrucción particular. devuelve esa secuencia de instrucciones en ensamblaje en línea, que estará vacía si no se requiere ninguna ".

Me preguntaba por qué es eso así en ARM?

Hacer que el compilador coloque allí un marcador determinado para que una implementación específica de una biblioteca pueda encontrarlo suena como un fuerte acoplamiento entre el compilador y el código de la biblioteca. ¿Por qué no se puede implementar el "sniffing" de la misma manera que en la plataforma x86_64?