Update 2017-05-17. Ich arbeite nicht mehr für das Unternehmen, aus dem diese Frage stammt, und habe keinen Zugriff auf Delphi XEx. Während ich dort war, wurde das Problem durch die Migration auf gemischte FPC + GCC (Pascal + C) mit NEON-Eigenheiten für einige Routinen gelöst, bei denen es einen Unterschied machte. (FPC + GCC wird dringend empfohlen, auch weil es die Verwendung von Standardtools, insbesondere von Valgrind, ermöglicht.) Wenn jemand anhand glaubwürdiger Beispiele demonstrieren kann, wie er tatsächlich optimierten ARM-Code aus Delphi XEx erstellen kann, bin ich froh, die Antwort zu akzeptieren .
Die Delphi-Compiler von Embarcadero verwenden ein LLVM-Backend, um nativen ARM-Code für Android-Geräte zu erstellen. Ich habe große Mengen an Pascal-Code, die ich zum Kompilieren in Android-Anwendungen benötigen, und ich möchte wissen, wie Delphi effizienteren Code generieren kann. Im Moment spreche ich nicht einmal über erweiterte Funktionen wie automatische SIMD-Optimierungen, sondern nur über die Erstellung von vernünftigem Code. Sicherlich muss es eine Möglichkeit geben, Parameter an die LLVM-Seite zu übergeben oder das Ergebnis irgendwie zu beeinflussen? Normalerweise hat jeder Compiler viele Optionen, um die Code-Kompilierung und -Optimierung zu beeinflussen, aber Delphis ARM-Ziele scheinen nur "Optimierung ein / aus" zu sein, und das war's.
LLVM soll in der Lage sein, einigermaßen engen und vernünftigen Code zu produzieren, aber es scheint, dass Delphi seine Einrichtungen auf seltsame Weise nutzt. Delphi möchte den Stack sehr stark nutzen und verwendet im Allgemeinen nur die Register r0-r3 des Prozessors als temporäre Variablen. Das vielleicht verrückteste von allen scheint das Laden normaler 32-Bit-Ganzzahlen als vier 1-Byte-Ladeoperationen zu sein. Wie kann man Delphi dazu bringen, besseren ARM-Code zu produzieren, und ohne den byteweisen Aufwand für Android?
Zuerst dachte ich, das Laden von Byte für Byte sei für das Austauschen der Bytereihenfolge von Big-Endian gedacht, aber das war nicht der Fall, es wird wirklich nur eine 32-Bit-Zahl mit 4 Einzelbyte-Ladevorgängen geladen. * Es könnte sein, dass geladen wird die vollen 32 Bits ohne eine nicht ausgerichtete Speicherlast in Wortgröße. (ob es das vermeiden SOLLTE, ist eine andere Sache, die darauf hindeuten würde, dass das Ganze ein Compiler-Fehler ist) *
Schauen wir uns diese einfache Funktion an:
function ReadInteger(APInteger : PInteger) : Integer;
begin
Result := APInteger^;
end;
Selbst bei aktivierten Optimierungen erzeugen Delphi XE7 mit Update Pack 1 sowie XE6 den folgenden ARM-Assemblycode für diese Funktion:
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}
Zählen Sie einfach die Anzahl der Anweisungen und Speicherzugriffe, die Delphi dafür benötigt. Und eine 32-Bit-Ganzzahl aus 4 Einzelbyte-Ladevorgängen erstellen ... Wenn ich die Funktion ein wenig ändere und anstelle eines Zeigers einen var-Parameter verwende, ist dies etwas weniger kompliziert:
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}
Ich werde die Demontage hier nicht einschließen, aber für iOS erzeugt Delphi identischen Code für die Zeiger- und Var-Parameterversionen, und sie sind fast, aber nicht genau identisch mit der Android-Var-Parameterversion. Bearbeiten: Zur Verdeutlichung ist das Laden von Byte für Byte nur unter Android möglich. Und nur unter Android unterscheiden sich die Zeiger- und Var-Parameterversionen voneinander. Unter iOS generieren beide Versionen genau den gleichen Code.
Zum Vergleich ist hier, was FPC 2.7.1 (SVN-Trunk-Version von März 2014) von der Funktion mit Optimierungsstufe -O2 hält. Die Zeiger- und var-Parameterversionen sind genau gleich.
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
Ich habe auch eine äquivalente C-Funktion mit dem C-Compiler getestet, der mit dem Android NDK geliefert wird.
int ReadInteger(int *APInteger)
{
return *APInteger;
}
Und dies ergibt im Wesentlichen dasselbe, was FPC gemacht hat:
Disassembly of section .text._Z11ReadIntegerPi:
00000000 <_Z11ReadIntegerPi>:
0: 6800 ldr r0, [r0, #0]
2: 4770 bx lr
armeabi-v7a
anstelle von erstellen armeabi
(nicht sicher, ob es solche Optionen in diesem Compiler gibt), da nicht ausgerichtete Lasten seit ARMv6 unterstützt werden sollten (während armeabi
ARMv5 vorausgesetzt wird). (Die gezeigte Demontage sieht nicht so aus, als würde sie einen Bigendian-Wert lesen, sondern nur einen kleinen Endian-Wert byteweise.)