Actualización 2017-05-17. Ya no trabajo para la empresa donde se originó esta pregunta y no tengo acceso a Delphi XEx. Mientras estuve allí, el problema se resolvió migrando a FPC + GCC mixto (Pascal + C), con NEON intrínsecos para algunas rutinas en las que marcó la diferencia. (FPC + GCC es muy recomendable también porque permite el uso de herramientas estándar, particularmente Valgrind.) Si alguien puede demostrar, con ejemplos creíbles, cómo puede realmente producir código ARM optimizado desde Delphi XEx, me complace aceptar la respuesta .
Los compiladores Delphi de Embarcadero utilizan un back-end LLVM para producir código ARM nativo para dispositivos Android. Tengo grandes cantidades de código Pascal que necesito compilar en aplicaciones de Android y me gustaría saber cómo hacer que Delphi genere un código más eficiente. En este momento, ni siquiera estoy hablando de funciones avanzadas como las optimizaciones automáticas SIMD, solo de producir un código razonable. ¿Seguramente debe haber una manera de pasar parámetros al lado LLVM, o de alguna manera afectar el resultado? Por lo general, cualquier compilador tendrá muchas opciones para afectar la compilación y la optimización del código, pero los objetivos ARM de Delphi parecen ser solo "activación / desactivación de optimización" y eso es todo.
Se supone que LLVM es capaz de producir código razonablemente ajustado y sensible, pero parece que Delphi está usando sus instalaciones de una manera extraña. Delphi quiere usar mucho la pila, y generalmente solo utiliza los registros del procesador r0-r3 como variables temporales. Quizás el más loco de todos, parece estar cargando enteros normales de 32 bits como cuatro operaciones de carga de 1 byte. ¿Cómo hacer que Delphi produzca un mejor código ARM, y sin la molestia byte por byte que está haciendo para Android?
Al principio pensé que la carga byte por byte era para intercambiar el orden de bytes de big-endian, pero ese no fue el caso, en realidad solo se trata de cargar un número de 32 bits con 4 cargas de un solo byte. * Podría ser cargar los 32 bits completos sin realizar una carga de memoria de tamaño de palabra no alineada. (si DEBERÍA evitar eso es otra cosa, lo que indicaría que todo es un error del compilador) *
Veamos esta simple función:
function ReadInteger(APInteger : PInteger) : Integer;
begin
Result := APInteger^;
end;
Incluso con las optimizaciones activadas, Delphi XE7 con el paquete de actualización 1, así como XE6, producen el siguiente código de ensamblaje ARM para esa función:
Disassembly of section .text._ZN16Uarmcodetestform11ReadIntegerEPi:
00000000 <_ZN16Uarmcodetestform11ReadIntegerEPi>:
0: b580 push {r7, lr}
2: 466f mov r7, sp
4: b083 sub sp, #12
6: 9002 str r0, [sp, #8]
8: 78c1 ldrb r1, [r0, #3]
a: 7882 ldrb r2, [r0, #2]
c: ea42 2101 orr.w r1, r2, r1, lsl #8
10: 7842 ldrb r2, [r0, #1]
12: 7803 ldrb r3, [r0, #0]
14: ea43 2202 orr.w r2, r3, r2, lsl #8
18: ea42 4101 orr.w r1, r2, r1, lsl #16
1c: 9101 str r1, [sp, #4]
1e: 9000 str r0, [sp, #0]
20: 4608 mov r0, r1
22: b003 add sp, #12
24: bd80 pop {r7, pc}
Solo cuente la cantidad de instrucciones y accesos de memoria que Delphi necesita para eso. Y construir un número entero de 32 bits a partir de 4 cargas de un solo byte ... Si cambio un poco la función y uso un parámetro var en lugar de un puntero, es un poco menos complicado:
Disassembly of section .text._ZN16Uarmcodetestform14ReadIntegerVarERi:
00000000 <_ZN16Uarmcodetestform14ReadIntegerVarERi>:
0: b580 push {r7, lr}
2: 466f mov r7, sp
4: b083 sub sp, #12
6: 9002 str r0, [sp, #8]
8: 6801 ldr r1, [r0, #0]
a: 9101 str r1, [sp, #4]
c: 9000 str r0, [sp, #0]
e: 4608 mov r0, r1
10: b003 add sp, #12
12: bd80 pop {r7, pc}
No incluiré el desmontaje aquí, pero para iOS, Delphi produce un código idéntico para las versiones de puntero y parámetro var, y son casi, pero no exactamente, las mismas que la versión de parámetro var de Android. Editar: para aclarar, la carga byte por byte es solo en Android. Y solo en Android, las versiones de puntero y parámetro var difieren entre sí. En iOS, ambas versiones generan exactamente el mismo código.
A modo de comparación, esto es lo que piensa FPC 2.7.1 (versión troncal SVN de marzo de 2014) de la función con nivel de optimización -O2. Las versiones de los parámetros puntero y var son exactamente iguales.
Disassembly of section .text.n_p$armcodetest_$$_readinteger$pinteger$$longint:
00000000 <P$ARMCODETEST_$$_READINTEGER$PINTEGER$$LONGINT>:
0: 6800 ldr r0, [r0, #0]
2: 46f7 mov pc, lr
También probé una función C equivalente con el compilador C que viene con el NDK de Android.
int ReadInteger(int *APInteger)
{
return *APInteger;
}
Y esto se compila esencialmente en lo mismo que FPC hizo:
Disassembly of section .text._Z11ReadIntegerPi:
00000000 <_Z11ReadIntegerPi>:
0: 6800 ldr r0, [r0, #0]
2: 4770 bx lr
armeabi-v7a
lugar de armeabi
(no estoy seguro de si existen tales opciones en este compilador), ya que las cargas no alineadas deben ser compatibles desde ARMv6 (mientras que armeabi
supone ARMv5). (El desmontaje se muestra no se ve como se lee un valor bigEndian, sólo se lee un poco valor endian un byte a la vez.)