Atualização 2017-05-17. Não trabalho mais na empresa em que essa pergunta se originou e não tenho acesso ao Delphi XEx. Enquanto eu estava lá, o problema foi resolvido migrando para FPC + GCC misto (Pascal + C), com NEON intrínseco para algumas rotinas em que fazia diferença. (FPC + GCC é altamente recomendado também porque permite o uso de ferramentas padrão, particularmente Valgrind.) Se alguém puder demonstrar, com exemplos confiáveis, como é capaz de produzir código ARM otimizado a partir do Delphi XEx, fico feliz em aceitar a resposta .
Os compiladores Delphi da Embarcadero usam um back-end LLVM para produzir código ARM nativo para dispositivos Android. Tenho grandes quantidades de código Pascal que preciso compilar em aplicativos Android e gostaria de saber como fazer o Delphi gerar código mais eficiente. No momento, nem estou falando de recursos avançados, como otimizações automáticas do SIMD, apenas sobre a produção de código razoável. Certamente deve haver uma maneira de passar parâmetros para o lado LLVM, ou de alguma forma afetar o resultado? Normalmente, qualquer compilador terá muitas opções para afetar a compilação e otimização de código, mas os alvos ARM do Delphi parecem ser apenas "otimização on / off" e é isso.
Supõe-se que o LLVM seja capaz de produzir código razoavelmente rígido e sensível, mas parece que o Delphi está usando suas instalações de uma maneira estranha. O Delphi quer usar muito a pilha, e geralmente utiliza apenas os registros do processador r0-r3 como variáveis temporárias. Talvez o mais louco de todos, parece estar carregando inteiros normais de 32 bits como quatro operações de carregamento de 1 byte. Como fazer com que o Delphi produza melhor código ARM, e sem o incômodo de byte a byte que está causando para o Android?
No começo, pensei que o carregamento de byte a byte era para trocar a ordem de bytes do big-endian, mas não era esse o caso, era realmente apenas carregar um número de 32 bits com 4 cargas de um byte. * Pode ser o carregamento os 32 bits completos sem fazer uma carga de memória do tamanho de palavra desalinhada. (se DEVE evitar isso é outra coisa, o que sugere que a coisa toda é um bug do compilador) *
Vamos olhar para esta função simples:
function ReadInteger(APInteger : PInteger) : Integer;
begin
Result := APInteger^;
end;
Mesmo com as otimizações ativadas, o Delphi XE7 com atualização 1, assim como o XE6, produz o seguinte código de montagem do ARM para essa função:
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 contar o número de instruções e os acessos à memória que o Delphi precisa para isso. E construindo um número inteiro de 32 bits a partir de 4 cargas de byte único ... Se eu mudar a função um pouco e usar um parâmetro var em vez de um ponteiro, ele será um pouco 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}
Não incluirei a desmontagem aqui, mas, para iOS, o Delphi produz código idêntico para as versões dos parâmetros apontador e var, e elas são quase mas não exatamente iguais à versão do parâmetro var Android. Editar: para esclarecer, o carregamento de byte a byte é apenas no Android. E apenas no Android, as versões dos parâmetros apontador e var diferem entre si. No iOS, ambas as versões geram exatamente o mesmo código.
Para comparação, veja o que o FPC 2.7.1 (versão de tronco SVN de março de 2014) pensa da função com nível de otimização -O2. As versões dos parâmetros apontador e var são exatamente iguais.
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
Também testei uma função C equivalente com o compilador C que acompanha o Android NDK.
int ReadInteger(int *APInteger)
{
return *APInteger;
}
E isso compila basicamente a mesma coisa que o FPC criou:
Disassembly of section .text._Z11ReadIntegerPi:
00000000 <_Z11ReadIntegerPi>:
0: 6800 ldr r0, [r0, #0]
2: 4770 bx lr
armeabi-v7a
vez de armeabi
(não tenho certeza se existem opções nesse compilador), pois cargas desalinhadas devem ser suportadas desde o ARMv6 (enquanto armeabi
assume o ARMv5). (A mostrado desmontagem não se parece com ele lê um valor bigendian, ele só lê um valor um pouco endian um byte de cada vez.)