5

一位朋友给我发了一份最新版本的 Delphi 和 Java 之间的比较(如果需要,可以使用源代码)。信不信由你(最好相信它)Java 现在比 Delphi 快得多,因为 Delphi 编译器不会利用现代 CPU 指令!“慢”Java 的重大突破。

我的问题是:我们如何在 Delphi 中使用现代 CPU 指令而不使用 ASM?

FastCode项目是上述问题的部分答案,但现在已被放弃。还有其他类似 FastCode 的项目吗?

这是另一篇文章,显示Java 和 C# 确实比 Delphi 快得多http ://webandlife.blogspot.com/2011/12/c-performance-vs-delphi-performance.html


JAVA

import java.util.Date;

public class j
{
  public static void xxx(int n, int m)
  {
        double t;
        int i, j;
        double d, r;
        t = 0.0;
        for (j = 1; j <= n; j++)
        {
          t = t / 1000.0;
          for (i = 1; i <= m; i++)
          {
                t = t + i / 999999.0;
                d = t * t + i;
                r = (t + d) / (200000.0 * (i + 1));
                t = t - r;
          }
        }
        System.out.println(t);
  }

  public static void main(String [] args)
  {
        Date t1, t2;

        t1 = new Date();
        xxx(1, 999999999);
        t2 = new Date();
        System.out.println((t2.getTime() - t1.getTime())/1000);
        t1 = new Date();
        xxx(1, 999999999);
        t2 = new Date();
        System.out.println((t2.getTime() - t1.getTime())/1000);
  }
}

25 秒

德尔福

program d;
{$APPTYPE CONSOLE}
uses
  System.SysUtils, System.DateUtils;
var
  t1, t2: TDateTime;

procedure xxx (n: integer; m: integer);
var
  t: double;
  i, j: integer;
  d, r: double;
begin
  t:= 0.0;
  for j:= 1 to n do
  begin
        t:= t / 1000.0;
        for i:= 1 to m do
        begin
          t:= t + i / 999999.0;
          d:= t * t + i;
          r:= (t + d) / (200000.0 * (i + 1));
          t:= t - r;
        end;
  end;
  writeln(t);
end;

begin
  t1:= Now;
  xxx(1, 999999999);
  t2:= Now;
  writeln(SecondsBetween(t2,t1));

  t1:= Now;
  xxx(1, 999999999);
  t2:= Now;
  writeln(SecondsBetween(t2,t1));
end.

37 秒


似乎 Delphi 仍处于链的底部:http ://www.tiobe.com/index.php/content/paperinfo/tpci/index.html

我想知道从这个角度来看,Lazarus 如何与 Delphi 进行比较。

4

5 回答 5

12

根据您的代码,32 位 Delphi 编译器的缓慢之处在于浮点算术支持,它远未优化,并且将大量内容复制到 FPU 堆栈上。

在浮点运算方面,不仅 Java JITted 代码会更快。即使是现代的 JavaScript JIT 编译器也可以比 Delphi 好得多!

这篇博客文章只是对此的参考,并提供了关于 Delphi 浮点慢的 asm 级解释:

在此处输入图像描述

但是,如果您使用针对 Win64 平台的 Delphi 编译器,它将发出的不是 x87 而是 SSE2 操作码,而且速度会快得多。我怀疑与 Java JITted 可执行文件相当。

而且,就 Java 而言,任何 Delphi 可执行文件将使用比 JVM少得多的内存,所以在这里,Delphi 可执行文件完美地走上了正轨!

如果您希望您的代码更快,请不要使用 asm 或低级优化技巧,而是更改您的算法。它可能比编译提示快一个数量级。专用过程将通过内联 asm 操作码实现 - 看看这组关于此类低级 hack 的精彩文章。但这并不容易掌握,通常,适当的软件分析和添加一些缓存是提高性能的最佳方式

于 2013-09-04T11:11:17.707 回答
10

继续 Arnaud 的观点 - 我实际上在 delphi 中为 x86 和 x64 编译了这个。

32位编译器:

Unit1.pas.36: t:= t / 1000.0;
0051274D DD45F0           fld qword ptr [ebp-$10]
00512750 D835E4275100     fdiv dword ptr [$005127e4]
00512756 DD5DF0           fstp qword ptr [ebp-$10]
00512759 9B               wait 
Unit1.pas.37: for i:= 1 to m do
0051275A 8B45F8           mov eax,[ebp-$08]
0051275D 85C0             test eax,eax
0051275F 7E57             jle $005127b8
00512761 8945D0           mov [ebp-$30],eax
00512764 C745EC01000000   mov [ebp-$14],$00000001
Unit1.pas.39: t:= t + i / 999999.0;
0051276B DB45EC           fild dword ptr [ebp-$14]
0051276E D835E8275100     fdiv dword ptr [$005127e8]
00512774 DC45F0           fadd qword ptr [ebp-$10]
00512777 DD5DF0           fstp qword ptr [ebp-$10]
0051277A 9B               wait 
Unit1.pas.40: d:= t * t + i;
0051277B DD45F0           fld qword ptr [ebp-$10]
0051277E DC4DF0           fmul qword ptr [ebp-$10]
00512781 DB45EC           fild dword ptr [ebp-$14]
00512784 DEC1             faddp st(1)
00512786 DD5DE0           fstp qword ptr [ebp-$20]
00512789 9B               wait 
Unit1.pas.41: r:= (t + d) / (200000.0 * (i + 1));
0051278A DD45F0           fld qword ptr [ebp-$10]
0051278D DC45E0           fadd qword ptr [ebp-$20]
00512790 8B45EC           mov eax,[ebp-$14]
00512793 40               inc eax
00512794 8945CC           mov [ebp-$34],eax
00512797 DB45CC           fild dword ptr [ebp-$34]
0051279A D80DEC275100     fmul dword ptr [$005127ec]
005127A0 DEF9             fdivp st(1)
005127A2 DD5DD8           fstp qword ptr [ebp-$28]
005127A5 9B               wait 
Unit1.pas.42: t:= t - r;
005127A6 DD45F0           fld qword ptr [ebp-$10]
005127A9 DC65D8           fsub qword ptr [ebp-$28]
005127AC DD5DF0           fstp qword ptr [ebp-$10]
005127AF 9B               wait 
Unit1.pas.43: end;
005127B0 FF45EC           inc dword ptr [ebp-$14]
Unit1.pas.37: for i:= 1 to m do
005127B3 FF4DD0           dec dword ptr [ebp-$30]
005127B6 75B3             jnz $0051276b
Unit1.pas.44: end;
005127B8 FF45E8           inc dword ptr [ebp-$18]

64 位编译器

Unit1.pas.36: t:= t / 1000.0;
000000000059F94E F20F104548       movsd xmm0,qword ptr [rbp+$48]
000000000059F953 F20F5E05BD000000 divsd xmm0,qword ptr [rel $000000bd]
000000000059F95B F20F114548       movsd qword ptr [rbp+$48],xmm0
000000000059F960 C7C001000000     mov eax,$00000001
000000000059F966 8B5568           mov edx,[rbp+$68]
000000000059F969 894544           mov [rbp+$44],eax
000000000059F96C 395544           cmp [rbp+$44],edx
000000000059F96F 7F73             jnle xxx + $C4
000000000059F971 83C201           add edx,$01
Unit1.pas.39: t:= t + i / 999999.0;
000000000059F974 F20F2A4544       cvtsi2sd xmm0,dword ptr [rbp+$44]
000000000059F979 F20F5E059F000000 divsd xmm0,qword ptr [rel $0000009f]
000000000059F981 F20F104D48       movsd xmm1,qword ptr [rbp+$48]
000000000059F986 F20F58C8         addsd xmm1,xmm0
000000000059F98A F20F114D48       movsd qword ptr [rbp+$48],xmm1
Unit1.pas.40: d:= t * t + i;
000000000059F98F F20F104548       movsd xmm0,qword ptr [rbp+$48]
000000000059F994 F20F594548       mulsd xmm0,qword ptr [rbp+$48]
000000000059F999 F20F2A4D44       cvtsi2sd xmm1,dword ptr [rbp+$44]
000000000059F99E F20F58C1         addsd xmm0,xmm1
000000000059F9A2 F20F114538       movsd qword ptr [rbp+$38],xmm0
Unit1.pas.41: r:= (t + d) / (200000.0 * (i + 1));
000000000059F9A7 F20F104548       movsd xmm0,qword ptr [rbp+$48]
000000000059F9AC F20F584538       addsd xmm0,qword ptr [rbp+$38]
000000000059F9B1 8B4544           mov eax,[rbp+$44]
000000000059F9B4 83C001           add eax,$01
000000000059F9B7 F20F2AC8         cvtsi2sd xmm1,eax
000000000059F9BB F20F590D65000000 mulsd xmm1,qword ptr [rel $00000065]
000000000059F9C3 F20F5EC1         divsd xmm0,xmm1
000000000059F9C7 F20F114530       movsd qword ptr [rbp+$30],xmm0
Unit1.pas.42: t:= t - r;
000000000059F9CC F20F104548       movsd xmm0,qword ptr [rbp+$48]
000000000059F9D1 F20F5C4530       subsd xmm0,qword ptr [rbp+$30]
000000000059F9D6 F20F114548       movsd qword ptr [rbp+$48],xmm0
Unit1.pas.43: end;
000000000059F9DB 83454401         add dword ptr [rbp+$44],$01
000000000059F9DF 395544           cmp [rbp+$44],edx
000000000059F9E2 7590             jnz xxx + $54
000000000059F9E4 90               nop
Unit1.pas.44: end;
000000000059F9E5 83454001         add dword ptr [rbp+$40],$01
000000000059F9E9 394D40           cmp [rbp+$40],ecx
000000000059F9EC 0F855CFFFFFF     jnz xxx + $2E
000000000059F9F2 90               nop
Unit1.pas.45: writeln(t);
000000000059F9F3 488B0D9E150300   mov rcx,[rel $0003159e]

奇怪的是,在这种情况下,x87 fpu 代码实际上快了大约 5%。结论大概就是 Delphi 的 32 位/x87 编译器非常成熟并且优化得相当好,而 64 位编译器在性能上可能还有一些改进的空间。我可以很容易地看到这里可以优化 SSE 代码的几个地方;i例如,可以存储在 XMM 寄存器中并重新使用,而不是每次都重新转换cvtsi2sdd可以将 保存在 XMM 寄存器中用于下一次计算,而不是存储和重新加载等。

MOV进出 XMM 寄存器的未对齐的 s 实际上可能非常昂贵。实际的 SSE 计算速度更快,但过度的数据移动可能会影响分数。也许 Java 在堆栈上强制 16 字节对齐?我知道 MacOS 会这样做,并且 SSE 使用对齐而不是未对齐的移动肯定有好处(当然,代价是消耗更多的堆栈空间)。

例如

  • fild:1 个操作,9 个延迟(x87)
  • cvtsi2sd: 2 op, 12 延迟 (SSE)

或者

  • fld:1 个操作,4 个延迟(x87)
  • movsd[r,m]:2op,4 延迟(SSE)

Delphi 的编译器在发出 SSE 指令的同时,似乎仍然以与 x87 单元类似的方式处理工作流,这不一定是最好的方式。在任何一种情况下,大卫都是正确的——编译器就是这样。你不能做任何事情来改变它。

在我需要快速数学例程的地方,我仍然自己在 ASM 中编写代码——这通常优于任何编译器可以做的任何事情,因为您可以根据您正在执行的精确计算自定义行为。我有旧的 32 位应用程序,带有手动调整的 SSE3 ASM 算法,用于复数算术和矩阵运算。关键是你不需要优化一切——你只需要优化瓶颈。这是需要注意的一个相当重要的点。

于 2013-09-04T11:19:51.790 回答
8

我将在这里回答元问题:“为什么 Delphi 编译器不能使用更现代的 CPU 指令,为什么 Java 可以?”

基本上,有两种编译代码的方法:

  1. 在开发者机器上预编译
  2. 在目标机器上后编译(包括 JITed)

1.的例子包括Delphi、C/C++等
。2.的例子包括Java、.NET、JavaScript等。

预编译环境

预编译环境让您编译一次代码,然后在目标计算机上运行它。编译程序不能在使用比编译程序使用的旧指令集的机器上运行。最低要求是编译器可以做得最好的最低要求,也是所有目标机器的最低架构。如果你不知道你的目标机器,它会受到编译器的限制。

后编译环境

后编译环境在目标机器上编译。您不必知道它运行什么架构:在其上运行的编译器需要知道它支持什么才能充分利用它。最低要求是编译器可以做得最好的最低要求,以及目标机器的体系结构。

原因是在后编译、JIT 或解释的语言环境中,编译器实际上是在目标机器上运行的。这意味着编译器可以使用该目标架构的所有功能。它甚至可以考虑物理内存、缓存大小或磁盘速度等方面,并测量当前运行时性能以对运行代码进行编译后优化。

Delphi 等工具

至于 Windows 32 位 Delphi 编译器,我认为最低要求仍然是 486 或 Pentium(鉴于Pentium-Safe FDIV 选项)。因此,它使用 x87 作为 CPU 代码。Windows 64 位 Delphi 编译器对用于 FPU 代码的 SSE 指令有最低
要求。 我还没有检查其他编译器平台的最低要求。

Delphi 的最低要求与强调向后兼容性有关。

其他一些环境(大多数 C/++ 编译器,可能还有其他)允许您指定最小指令集。德尔福没有。我认为主要原因是开发和测试的复杂性。可能性矩阵(如果它确实是一个二维问题)很快就会变大。

JIT 编译器通常不能全面支持最新的硬件架构优势,因为这样做非常昂贵。

JIT 编译器通常支持某些处理器系列的优化(例如在复制内存区域时)。

我知道 Java 和 .NET 在过去十年中在这一领域取得了相当大的进步。有一篇非常好的 2005 年关于.NET JIT 使用 CPU 功能的文章。

于 2013-09-04T13:22:01.860 回答
4

背景。

问题是如何优化 Delphi 代码以使其与 Java 相媲美。并且在不使用 asm 编码的情况下这样做。

分析。

在给定的示例中,该算法使用浮点计算。编译器的性能和弱点已在其他答案中进行了调查。从理论上讲,x64 位编译器可以执行得更好,因为 SSE2 操作码和寄存器可以提供更好的优化。所以编译器将成为这里的瓶颈。

也有人建议更好的算法可以提高性能。让我们再多看一点。

改进算法。

在算法循环中,循环索引 i 在计算中被用作变量 3 次。由于这会强制整数每次进行浮点转换(在加载到 fpu 或 SSE2 寄存器时),因此会对性能产生很大影响。让我们研究一下我们是否可以帮助编译器优化掉这些转换。

procedure xxx (n: integer; m: integer);
var
  t,ii: double;
  i, j: integer;
  d, r: double;
begin
  t:= 0.0;
  for j:= 1 to n do
  begin
    t:= t / 1000.0;
    ii:= 1.0;
    for i:= 1 to m do
    begin
      t:= t + ii / 999999.0;
      d:= t * t + ii;
      ii:= ii + 1.0;
      r:= (t + d) / (200000.0 * ii);
      t:= t - r;
    end;
  end;
  writeln(t);
end;

现在我们有了一个只使用浮点值的干净算法。这里的参考是java代码:

public static void xxy(int n, int m)
{
    double t;
    int i, j;
    double d, r, ii;
    t = 0.0;
    for (j = 1; j <= n; j++)
    {
      t = t / 1000.0;
      ii = 1.0;
      for (i = 1; i <= m; i++)
      {
            t = t + ii / 999999.0;
            d = t * t + ii;
            ii = ii + 1.0;
            r = (t + d) / (200000.0 * ii);
            t = t - r;
      }
    }
    System.out.println(t);
}

基准。

使用 XE2 编译器。

                     x32      x64     Java(x64) 
                     --------------------------          
Original algorithm   23417ms  22293ms 22045ms
Updated algorithm    22362ms  14059ms 15507ms

x64 代码的反汇编如下所示:

Project19.dpr.11: begin
000000000046ABC0 55               push rbp
000000000046ABC1 4883EC20         sub rsp,$20
000000000046ABC5 488BEC           mov rbp,rsp
Project19.dpr.12: t:= 0.0;
000000000046ABC8 F20F1005B0000000 movsd xmm0,qword ptr [rel $000000b0]
000000000046ABD0 C7C001000000     mov eax,$00000001
000000000046ABD6 4189C8           mov r8d,ecx
000000000046ABD9 89C1             mov ecx,eax
000000000046ABDB 413BC8           cmp ecx,r8d
000000000046ABDE 7F7B             jnle xxx + $9B
000000000046ABE0 4183C001         add r8d,$01
Project19.dpr.15: t:= t / 1000.0;
000000000046ABE4 F20F5E059C000000 divsd xmm0,qword ptr [rel $0000009c]
Project19.dpr.16: ii := 1.0;
000000000046ABEC F20F100D9C000000 movsd xmm1,qword ptr [rel $0000009c]
000000000046ABF4 C7C001000000     mov eax,$00000001
000000000046ABFA 4189D1           mov r9d,edx
000000000046ABFD 413BC1           cmp eax,r9d
000000000046AC00 7F50             jnle xxx + $92
000000000046AC02 4183C101         add r9d,$01
Project19.dpr.19: t:= t + ii / 999999.0;
000000000046AC06 660F28D1         movapd xmm2,xmm1
000000000046AC0A F20F5E1586000000 divsd xmm2,qword ptr [rel $00000086]
000000000046AC12 F20F58C2         addsd xmm0,xmm2
Project19.dpr.20: d:= t * t + ii;
000000000046AC16 660F28D0         movapd xmm2,xmm0
000000000046AC1A F20F59D0         mulsd xmm2,xmm0
000000000046AC1E F20F58D1         addsd xmm2,xmm1
Project19.dpr.21: ii := ii + 1.0;
000000000046AC22 F20F580D66000000 addsd xmm1,qword ptr [rel $00000066]
Project19.dpr.22: r:= (t + d) / (200000.0 * ii);
000000000046AC2A 660F28D8         movapd xmm3,xmm0
000000000046AC2E F20F58DA         addsd xmm3,xmm2
000000000046AC32 660F28D1         movapd xmm2,xmm1
000000000046AC36 F20F591562000000 mulsd xmm2,qword ptr [rel $00000062]
000000000046AC3E F20F5EDA         divsd xmm3,xmm2
000000000046AC42 660F29DA         movapd xmm2,xmm3
Project19.dpr.23: t:= t - r;
000000000046AC46 F20F5CC2         subsd xmm0,xmm2
Project19.dpr.24: end;
000000000046AC4A 83C001           add eax,$01
000000000046AC4D 413BC1           cmp eax,r9d
000000000046AC50 75B4             jnz xxx + $46
000000000046AC52 90               nop
Project19.dpr.25: end;
000000000046AC53 83C101           add ecx,$01
000000000046AC56 413BC8           cmp ecx,r8d
000000000046AC59 7589             jnz xxx + $24
000000000046AC5B 90               nop
Project19.dpr.26: WriteLn(t);
000000000046AC5C 488B0DC5100100   mov rcx,[rel $000110c5]
000000000046AC63 660F29C1         movapd xmm1,xmm0
000000000046AC67 E874D7F9FF       call @Write0Ext
000000000046AC6C 4889C1           mov rcx,rax
000000000046AC6F E88CD7F9FF       call @WriteLn
000000000046AC74 E877AFF9FF       call @_IOTest
Project19.dpr.27: end;
000000000046AC79 488D6520         lea rsp,[rbp+$20]

多余的整数到​​浮点数的转换消失了,寄存器得到了更好的使用。

额外优化

对于 x32 位编译器,将999999.0;and替换200000.0为倒数常量(const cA : Double = 1.0/999999.0; cB : Double = 1.0/200000.0;)并改用乘法也可以提高性能。

于 2015-09-17T14:13:16.293 回答
2

我们如何在 Delphi 中使用现代 CPU 指令(不求助于 ASM)?

如果编译器不会发出您希望使用的 CPU 指令,那么除了自己生成所需的指令之外别无选择,例如使用内联汇编器。

于 2013-09-04T11:03:27.857 回答