【发布时间】:2015-03-12 20:09:42
【问题描述】:
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 带来的逐字节的麻烦?
起初我以为逐字节加载是为了从大端交换字节顺序,但事实并非如此,它实际上只是加载一个 32 位数字和 4 个单字节加载。* 它可能是加载完整的 32 位而不执行未对齐的字大小的内存加载。 (是否应该避免这是另一回事,这会暗示整个事情都是编译器错误)*
让我们看看这个简单的函数:
function ReadInteger(APInteger : PInteger) : Integer;
begin
Result := APInteger^;
end;
即使开启了优化,带有更新包 1 的 Delphi XE7 以及 XE6 也会为该函数生成以下 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
【问题讨论】:
-
顺便说一句,Google+ discussion 关于这一点,Sam Shaw 指出 C++ 在调试版本中显示了长格式代码,在发布中显示了优化代码。德尔福在哪里都做到了。因此,这很可能是他们发送 LLVM 的标志中的一个简单错误,如果是这样一个错误报告非常值得提交,它可能很快就会得到修复。
-
哦,好吧,我看错了。然后,正如 Notlikethat 所说,听起来它假设指针加载将是未对齐的(或不能保证对齐),并且较旧的 ARM 平台不一定可以进行未对齐的加载。确保它的构建目标是
armeabi-v7a而不是armeabi(不确定此编译器中是否有这样的选项),因为自 ARMv6 起应支持未对齐的加载(而armeabi假定为 ARMv5)。 (显示的反汇编看起来不像读取大端值,它只是一次读取一个字节的小端值。) -
我发现RSP-9922 似乎是同样的错误。
-
在 embarcadero.public.delphi.platformspecific.ios 新闻组中,有人询问 XE4 和 XE5 之间的优化是否被破坏,“ARM 编译器优化被破坏?” devsuperpage.com/search/…
-
@Johan:它是什么可执行文件?我的印象是它以某种方式在 Delphi 的编译器可执行文件中烘焙。试一试,让我们知道结果。
标签: android delphi android-ndk arm llvm