Mise à jour 2017-05-17. Je ne travaille plus pour l'entreprise à l'origine de cette question et je n'ai pas accès à Delphi XEx. Pendant que j'étais là-bas, le problème a été résolu en migrant vers un FPC + GCC mixte (Pascal + C), avec des intrinsèques NEON pour certaines routines où cela faisait une différence. (FPC + GCC est fortement recommandé également car il permet d'utiliser des outils standard, en particulier Valgrind.) Si quelqu'un peut démontrer, avec des exemples crédibles, comment il est réellement capable de produire du code ARM optimisé à partir de Delphi XEx, je suis heureux d'accepter la réponse .
Les compilateurs Delphi d'Embarcadero utilisent un backend LLVM pour produire du code ARM natif pour les appareils Android. J'ai de grandes quantités de code Pascal que je dois compiler dans des applications Android et je voudrais savoir comment rendre Delphi générer du code plus efficace. Pour le moment, je ne parle même pas de fonctionnalités avancées telles que les optimisations automatiques SIMD, mais simplement de produire un code raisonnable. Il doit sûrement y avoir un moyen de transmettre des paramètres au côté LLVM, ou d'affecter le résultat d'une manière ou d'une autre? Habituellement, n'importe quel compilateur aura de nombreuses options pour affecter la compilation et l'optimisation du code, mais les cibles ARM de Delphi semblent être simplement "optimisation on / off" et c'est tout.
LLVM est censé être capable de produire du code raisonnablement serré et raisonnable, mais il semble que Delphi utilise ses installations de manière étrange. Delphi souhaite utiliser très fortement la pile et n'utilise généralement que les registres r0-r3 du processeur comme variables temporaires. Peut-être le plus fou de tous, il semble charger des entiers 32 bits normaux en quatre opérations de chargement de 1 octet. Comment faire pour que Delphi produise un meilleur code ARM, et sans les tracas octet par octet qu'il crée pour Android?
Au début, je pensais que le chargement octet par octet était destiné à permuter l'ordre des octets de big-endian, mais ce n'était pas le cas, il s'agit simplement de charger un nombre 32 bits avec 4 charges à un octet. * Il pourrait s'agir de charger les 32 bits complets sans effectuer une charge de mémoire de taille de mot non alignée. (si cela DEVRAIT éviter cela est une autre chose, ce qui laisserait penser que le tout est un bogue du compilateur) *
Regardons cette fonction simple:
function ReadInteger(APInteger : PInteger) : Integer;
begin
Result := APInteger^;
end;
Même avec les optimisations activées, Delphi XE7 avec le pack de mise à jour 1, ainsi que XE6, produisent le code d'assemblage ARM suivant pour cette fonction:
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}
Il suffit de compter le nombre d'instructions et d'accès à la mémoire dont Delphi a besoin pour cela. Et construire un entier 32 bits à partir de 4 charges sur un octet ... Si je change un peu la fonction et utilise un paramètre var au lieu d'un pointeur, c'est un peu moins alambiqué:
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}
Je n'inclurai pas le démontage ici, mais pour iOS, Delphi produit un code identique pour les versions de paramètre pointeur et var, et ils sont presque mais pas exactement les mêmes que la version de paramètre var Android. Edit: pour clarifier, le chargement octet par octet est uniquement sur Android. Et uniquement sur Android, les versions des paramètres pointeur et var diffèrent les unes des autres. Sur iOS, les deux versions génèrent exactement le même code.
A titre de comparaison, voici ce que FPC 2.7.1 (version SVN trunk de mars 2014) pense de la fonction avec le niveau d'optimisation -O2. Les versions des paramètres pointeur et var sont exactement les mêmes.
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
J'ai également testé une fonction C équivalente avec le compilateur C fourni avec le NDK Android.
int ReadInteger(int *APInteger)
{
return *APInteger;
}
Et cela compile essentiellement la même chose que FPC a faite:
Disassembly of section .text._Z11ReadIntegerPi:
00000000 <_Z11ReadIntegerPi>:
0: 6800 ldr r0, [r0, #0]
2: 4770 bx lr
armeabi-v7a
place de armeabi
(ne savez pas s'il existe de telles options dans ce compilateur), car les charges non alignées doivent être prises en charge depuis ARMv6 (tout en armeabi
supposant ARMv5). (Le désassemblage montré ne ressemble pas à une valeur bigendienne, il lit juste une petite valeur