Aggiornamento 2017-05-17. Non lavoro più per l'azienda da cui è nata questa domanda e non ho accesso a Delphi XEx. Mentre ero lì, il problema è stato risolto migrando verso FPC + GCC misti (Pascal + C), con intrinseci NEON per alcune routine in cui ha fatto la differenza. (FPC + GCC è altamente raccomandato anche perché consente di utilizzare strumenti standard, in particolare Valgrind.) Se qualcuno può dimostrare, con esempi credibili, come sono effettivamente in grado di produrre codice ARM ottimizzato da Delphi XEx, sono felice di accettare la risposta .
I compilatori Delphi di Embarcadero utilizzano un back-end LLVM per produrre codice ARM nativo per dispositivi Android. Ho una grande quantità di codice Pascal che devo compilare in applicazioni Android e vorrei sapere come fare in modo che Delphi generi codice più efficiente. In questo momento, non sto nemmeno parlando di funzionalità avanzate come le ottimizzazioni SIMD automatiche, ma solo di produrre codice ragionevole. Sicuramente ci deve essere un modo per passare i parametri al lato LLVM o in qualche modo influenzare il risultato? Di solito, qualsiasi compilatore avrà molte opzioni per influenzare la compilazione e l'ottimizzazione del codice, ma gli obiettivi ARM di Delphi sembrano essere solo "ottimizzazione on / off" e basta.
Si suppone che LLVM sia in grado di produrre codice ragionevolmente stretto e ragionevole, ma sembra che Delphi stia usando le sue strutture in un modo strano. Delphi vuole usare lo stack molto pesantemente e generalmente utilizza solo i registri del processore r0-r3 come variabili temporanee. Forse il più folle di tutti, sembra caricare normali numeri interi a 32 bit come quattro operazioni di caricamento da 1 byte. Come fare in modo che Delphi produca un codice ARM migliore e senza la seccatura byte per byte che sta causando per Android?
Inizialmente pensavo che il caricamento byte per byte servisse a scambiare l'ordine dei byte da big-endian, ma non era così, in realtà stava solo caricando un numero a 32 bit con 4 carichi a byte singolo. * Potrebbe essere il caricamento i 32 bit completi senza eseguire un caricamento della memoria di dimensioni non allineate di parole. (se DOVREBBE evitare che sia un'altra cosa, che suggerirebbe che si tratta di un bug del compilatore) *
Diamo un'occhiata a questa semplice funzione:
function ReadInteger(APInteger : PInteger) : Integer;
begin
Result := APInteger^;
end;
Anche con le ottimizzazioni attivate, Delphi XE7 con il pacchetto di aggiornamento 1 e XE6 producono il seguente codice assembly ARM per quella funzione:
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}
Basta contare il numero di istruzioni e gli accessi alla memoria necessari a Delphi. E costruendo un numero intero a 32 bit da 4 carichi a byte singolo ... Se cambio un po 'la funzione e utilizzo un parametro var anziché un puntatore, è leggermente meno contorto:
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}
Non includerò lo smontaggio qui, ma per iOS, Delphi produce un codice identico per le versioni dei puntatori e dei parametri var e sono quasi ma non esattamente gli stessi della versione dei parametri var di Android. Modifica: per chiarire, il caricamento byte per byte è solo su Android. E solo su Android, le versioni del puntatore e del parametro var differiscono l'una dall'altra. Su iOS entrambe le versioni generano esattamente lo stesso codice.
Per fare un confronto, ecco cosa pensa FPC 2.7.1 (versione trunk SVN da marzo 2014) della funzione con livello di ottimizzazione -O2. Le versioni del puntatore e del parametro var sono esattamente le stesse.
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
Ho anche testato una funzione C equivalente con il compilatore C fornito con Android NDK.
int ReadInteger(int *APInteger)
{
return *APInteger;
}
E questo si compone essenzialmente della stessa cosa fatta da FPC:
Disassembly of section .text._Z11ReadIntegerPi:
00000000 <_Z11ReadIntegerPi>:
0: 6800 ldr r0, [r0, #0]
2: 4770 bx lr
armeabi-v7a
invece di armeabi
(non sono sicuro se ci sono tali opzioni in questo compilatore), poiché i carichi non allineati dovrebbero essere supportati da ARMv6 (mentre armeabi
presuppone ARMv5). (Lo smontaggio mostrato non sembra leggere un valore bigendiano, legge solo un piccolo valore endian un byte alla volta.)