更新于2017年5月17日。我已经不在原问题所在的公司工作,也无法访问Delphi XEx。当我在那里工作时,该问题通过迁移到混合FPC+GCC(Pascal+C)来解决,其中一些例程使用NEON内部函数进行优化。(FPC+GCC强烈建议使用,因为它还启用了使用标准工具,特别是Valgrind。)如果有人能够演示如何从Delphi XEx生成优化的ARM代码,并提供可靠的示例,我很乐意接受答案。
Embarcadero的Delphi编译器使用LLVM后端为Android设备生成本地ARM代码。我有大量的Pascal代码需要编译成Android应用程序,并且我想知道如何使Delphi生成更有效的代码。现在,我甚至没有谈论自动SIMD优化等高级功能,只是关于生成合理的代码。肯定有一种方法可以将参数传递给LLVM方面,或以某种方式影响结果?通常,任何编译器都会有许多选项来影响代码编译和优化,但是Delphi的ARM目标似乎只有"开/关优化"这一个选项。
LLVM应该能够生成相当紧凑和明智的代码,但看起来Delphi以一种奇怪的方式使用了它的功能。Delphi希望非常频繁地使用堆栈,并且通常只使用处理器的寄存器r0-r3作为临时变量。也许最疯狂的是,它似乎将普通的32位整数加载为四个1字节加载操作。如何让Delphi生成更好的ARM代码,而不需要在Android平台上进行字节级操作?
起初,我认为字节级加载是用于从大端交换字节顺序,但事实并非如此,它实际上只是进行了4次单字节加载以加载32位数字。可能是为了加载完整的32位而不进行未对齐的字大小内存加载。(是否应该避免这样做是另一回事,这暗示了整个编译器存在错误)*
让我们来看看这个简单的函数:
function ReadInteger(APInteger : PInteger) : Integer;
begin
Result := APInteger^;
end;
即使开启了优化,在Delphi XE7和XE6中,使用更新包1编译的代码仍会生成以下ARM汇编代码: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}
只需计算Delphi所需的指令和内存访问次数。将4个单字节加载构造成32位整数......如果我稍微改变函数并使用var参数而非指针,则会稍微简化:
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}
在这里我不会包含反汇编,但对于iOS而言,Delphi为指针和var参数版本生成了相同的代码,它们几乎完全相同,但与Android var参数版本略有不同。 编辑:澄清一下,在Android上只有逐字节加载。而且仅在Android上,指针和var参数版本才彼此不同。在iOS上,两个版本产生完全相同的代码。
为了比较,以下是FPC 2.7.1(来自2014年3月的SVN主干版本)对具有优化级别-O2的函数的看法。指针和var参数版本完全相同。
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
我还使用Android NDK附带的C编译器测试了一个等价的C函数。
int ReadInteger(int *APInteger)
{
return *APInteger;
}
而且这编译成的东西基本上与FPC制作的相同:
Disassembly of section .text._Z11ReadIntegerPi:
00000000 <_Z11ReadIntegerPi>:
0: 6800 ldr r0, [r0, #0]
2: 4770 bx lr
armeabi-v7a
而不是armeabi
(不确定此编译器是否提供此类选项),因为自ARMv6以来应支持未对齐的加载(而armeabi
则假定ARMv5)。 (所显示的反汇编看起来并不像在读取大端值,而只是一次一个字节地读取小端值。) - mstorsjo