Aktualizacja 2017-05-17. Nie pracuję już w firmie, z której pochodzi to pytanie, i nie mam dostępu do Delphi XEx. Podczas gdy tam byłem, problem został rozwiązany przez migrację do mieszanego FPC + GCC (Pascal + C), z wewnętrznymi cechami NEON dla niektórych procedur, w których miało to znaczenie. (FPC + GCC jest wysoce zalecane również dlatego, że umożliwia korzystanie ze standardowych narzędzi, zwłaszcza Valgrind.) Jeśli ktoś może wykazać, na wiarygodnych przykładach, w jaki sposób jest w stanie wygenerować zoptymalizowany kod ARM z Delphi XEx, chętnie przyjmuję odpowiedź .
Kompilatory Delphi Embarcadero wykorzystują backend LLVM do tworzenia natywnego kodu ARM dla urządzeń z Androidem. Mam duże ilości kodu Pascal, który muszę skompilować do aplikacji na Androida i chciałbym wiedzieć, jak sprawić, aby Delphi generował bardziej wydajny kod. W tej chwili nie mówię nawet o zaawansowanych funkcjach, takich jak automatyczne optymalizacje SIMD, tylko o tworzeniu rozsądnego kodu. Z pewnością musi istnieć sposób na przekazanie parametrów po stronie LLVM, czy w jakiś sposób wpłynąć na wynik? Zwykle każdy kompilator będzie miał wiele opcji wpływających na kompilację i optymalizację kodu, ale cele ARM Delphi wydają się być po prostu „optymalizacją włączoną / wyłączoną” i to wszystko.
LLVM ma być w stanie produkować dość ciasny i rozsądny kod, ale wydaje się, że Delphi używa swoich możliwości w dziwny sposób. Delphi chce bardzo intensywnie wykorzystywać stos i generalnie wykorzystuje rejestry procesora r0-r3 jako zmienne tymczasowe. Być może najbardziej szalony ze wszystkich, wydaje się, że ładuje normalne 32-bitowe liczby całkowite jako cztery operacje ładowania 1-bajtowego. Jak zmusić Delphi do tworzenia lepszego kodu ARM i bez kłopotów bajt po bajcie robi to dla Androida?
Na początku myślałem, że ładowanie bajt po bajcie służyło do zamiany kolejności bajtów z big-endian, ale tak nie było, tak naprawdę to po prostu ładowanie liczby 32-bitowej z 4 ładowaniami jednobajtowymi. * Może to być ładowanie pełne 32 bity bez wykonywania niewyrównanego ładowania pamięci o wielkości słowa. (czy NALEŻY tego uniknąć, to kolejna rzecz, która sugerowałaby, że całość jest błędem kompilatora) *
Spójrzmy na tę prostą funkcję:
function ReadInteger(APInteger : PInteger) : Integer;
begin
Result := APInteger^;
end;
Nawet przy włączonych optymalizacjach Delphi XE7 z pakietem aktualizacji 1, a także XE6, generują następujący kod zestawu ARM dla tej funkcji:
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}
Wystarczy policzyć liczbę instrukcji i dostęp do pamięci potrzebnych do tego Delphi. I konstruowanie 32-bitowej liczby całkowitej z 4 ładowań jednobajtowych ... Jeśli zmienię nieco funkcję i użyję parametru var zamiast wskaźnika, będzie to nieco mniej skomplikowane:
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}
Nie dołączę tutaj dezasemblacji, ale dla iOS Delphi produkuje identyczny kod dla wersji wskaźnika i wersji parametru i są one prawie, ale nie dokładnie takie same, jak wersja parametru dla Androida. Edycja: aby wyjaśnić, ładowanie bajt po bajcie odbywa się tylko na Androidzie. I tylko w Androidzie wersje wskaźnika i parametru var różnią się od siebie. W systemie iOS obie wersje generują dokładnie ten sam kod.
Dla porównania oto, co myśli FPC 2.7.1 (wersja magistrali SVN z marca 2014 r.) O funkcji z poziomem optymalizacji -O2. Wersje wskaźnika i parametru var są dokładnie takie same.
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
Testowałem również równoważną funkcję C z kompilatorem C dostarczanym z Androidem NDK.
int ReadInteger(int *APInteger)
{
return *APInteger;
}
I to składa się zasadniczo na to samo, co FPC:
Disassembly of section .text._Z11ReadIntegerPi:
00000000 <_Z11ReadIntegerPi>:
0: 6800 ldr r0, [r0, #0]
2: 4770 bx lr
armeabi-v7a
zamiast armeabi
(nie jestem pewien, czy istnieją takie opcje w tym kompilatorze), ponieważ niezrównane obciążenia powinny być obsługiwane od ARMv6 (przy armeabi
założeniu , że ARMv5). (Pokazany demontaż nie wygląda tak, jakby odczytywał wartość bigendian, po prostu odczytuje małą wartość endian jeden bajt na raz.)