如何影响Delphi XEx代码生成Android/ARM目标?

更新2017-05-17。我不再为这个问题产生的公司工作,也没有访问Delphi XEx的权限。当我在那里的时候,这个问题通过迁移到混合FPC+GCC (Pascal+C)解决了,在一些例程中使用了NEON intrinsic。(强烈推荐FPC+GCC,因为它支持使用标准工具,特别是Valgrind。)如果有人可以用可信的例子证明,他们实际上是如何用Delphi XEx生成优化的ARM代码的,我很乐意接受这个答案。


Embarcadero的Delphi编译器使用LLVM后端为Android设备生成原生ARM代码。我有大量的Pascal代码,我需要编译成Android应用程序,我想知道如何使Delphi生成更有效的代码。现在,我还没有讨论自动SIMD优化等高级特性,只讨论生成合理的代码。肯定有一种方法可以将参数传递给LLVM端,或者以某种方式影响结果?通常,任何编译器都会有很多选项来影响代码编译和优化,但Delphi的ARM目标似乎只是“优化开/关”,仅此而已。

LLVM应该能够生成相当紧凑和合理的代码,但似乎Delphi正在以一种奇怪的方式使用它的工具。Delphi非常希望使用堆栈,它通常只使用处理器的寄存器r0-r3作为临时变量。也许最疯狂的是,它似乎将正常的32位整数加载为4个1字节的加载操作。如何让Delphi生成更好的ARM代码,而不像Android那样逐字节地处理代码?

起初,我认为逐字节加载是为了从big-endian交换字节顺序,但事实并非如此,它实际上只是用4个单字节加载加载一个32位数字。*它可能是加载完整的32位,而不做一个未对齐的字大小的内存加载。(它是否应该避免这是另一回事,这将暗示整个事情是一个编译器错误)*

让我们看看这个简单的函数:

function ReadInteger(APInteger : PInteger) : Integer;
begin
Result := APInteger^;
end;

即使打开了优化,Delphi XE7的更新包1,以及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
11153 次浏览

我们正在调查此事。简而言之,它取决于指针引用的Integer的潜在错对(到32边界)。还需要一点时间来得到所有的答案……以及解决这个问题的计划。

Marco Cantù,主持人上Delphi Developers

也引用为什么Delphi的zlib和zip库在64位下这么慢?作为Win64库,在没有优化的情况下构建。


在QP报告中:RSP-9922 坏的ARM代码产生的编译器,$O指令忽略?, Marco增加了如下解释:

.

这里有多个问题:

  • 如上所述,优化设置仅应用于整个单元文件,而不应用于单个函数。简单地说,在同一个文件中打开和关闭优化不会产生任何影响。
  • 此外,只要有“调试信息”;Enabled关闭优化。因此,在调试时,显式地打开优化将不起作用。因此,IDE中的CPU视图将无法显示优化代码的反汇编视图。
  • 第三,加载未对齐的64位数据是不安全的,并且会导致错误,因此在给定的场景中需要单独的4个单字节操作。