Julia在性能上远远超过Delphi。Delphi编译器的过时汇编代码?

5
我在Delphi中编写了一个简单的for循环。 在Julia 1.6中,同样的程序运行速度快了7.6倍。
procedure TfrmTester.btnForLoopClick(Sender: TObject);
VAR
   i, Total, Big, Small: Integer;
   s: string;
begin   
  TimerStart;

   Total:= 0;
   Big  := 0;
   Small:= 0;
   for i:= 1 to 1000000000 DO    //1 billion
    begin
      Total:= Total+1;
      if Total > 500000
      then Big:= Big+1
      else Small:= Small+1;
    end;

 s:= TimerElapsedS;
 //here code to show Big/Small on the screen
end;

这段汇编代码在我看来还不错:

TesterForm.pas.111: TimerStart;
007BB91D E8DE7CF9FF       call TimerStart
TesterForm.pas.113: Total:= 0;
007BB922 33C0             xor eax,eax
007BB924 8945F4           mov [ebp-$0c],eax
TesterForm.pas.114: Big  := 0;
007BB927 33C0             xor eax,eax
007BB929 8945F0           mov [ebp-$10],eax
TesterForm.pas.115: Small:= 0;
007BB92C 33C0             xor eax,eax
007BB92E 8945EC           mov [ebp-$14],eax
TesterForm.pas.**116**: for i:= 1 to 1000000000 DO    //1 billion
007BB931 C745F801000000   mov [ebp-$08],$00000001
TesterForm.pas.118: Total:= Total+1;
007BB938 FF45F4           inc dword ptr [ebp-$0c]
TesterForm.pas.119: if Total > 500000
007BB93B 817DF420A10700   cmp [ebp-$0c],$0007a120
007BB942 7E05             jle $007bb949
TesterForm.pas.120: then Big:= Big+1
007BB944 FF45F0           inc dword ptr [ebp-$10]
007BB947 EB03             jmp $007bb94c
TesterForm.pas.121: else Small:= Small+1;
007BB949 FF45EC           inc dword ptr [ebp-$14]
TesterForm.pas.122: end;
007BB94C FF45F8           inc dword ptr [ebp-$08]
TesterForm.pas.**116**: for i:= 1 to 1000000000 DO    //1 billion
007BB94F 817DF801CA9A3B   cmp [ebp-$08],$3b9aca01
007BB956 75E0             jnz $007bb938
TesterForm.pas.124: s:= TimerElapsedS;
007BB958 8D45E8           lea eax,[ebp-$18]

为什么Delphi的得分与Julia相比如此糟糕?我能做些什么来改善编译器生成的代码吗?

信息

我的Delphi 10.4.2程序是Win32位。当然,我在“发布”模式下运行 :)
在此输入图片描述

enter image description here

但是上面的ASM代码是针对“Debug”版本的,因为我不知道如何在运行优化的EXE文件时暂停程序的执行。但是发布版和调试版exe之间的差异非常小(1.8 vs 1.5秒)。Julia只需要195毫秒。

更多讨论

  1. 我必须提到,当您第一次在Julia中运行代码时,它的时间非常长,因为Julia是JIT,所以它必须首先编译代码。编译时间(因为它是“一次性”的)未包含在测量中。

  2. 此外,正如AmigoJack所评论的那样,Delphi代码几乎可以在任何地方运行,而Julia代码可能仅在具有支持所有这些新/花哨指令的现代CPU的计算机上运行。我有一些小工具,它们是在2004年制作的,今天仍在运行。

  3. 无论Julia生成什么代码,除非客户端安装了Julia,否则无法交付给“客户”。

总之,所有这些都表明Delphi编译器非常过时,这很令人遗憾。

4. 我进行了其他测试,发现在一个字符串列表中找到最短和最长的字符串,在Delphi中比Julia快10倍。分配小块内存(10000x10000x4字节)的速度相同。
5. 正如AhnLab所提到的,我进行了相当"干燥"的测试。我猜想需要编写一个执行更复杂/真实任务的完整程序,并在程序结束时查看Julia是否仍然比Delphi 7倍性能更好。

更新

好的,这段 Julia 代码对我来说完全陌生。似乎使用了更现代化的操作符:

; ┌ @ Julia_vs_Delphi.jl:4 within `for_fun`
        pushq   %rbp
        movq    %rsp, %rbp
        subq    $96, %rsp
        vmovdqa %xmm11, -16(%rbp)
        vmovdqa %xmm10, -32(%rbp)
        vmovdqa %xmm9, -48(%rbp)
        vmovdqa %xmm8, -64(%rbp)
        vmovdqa %xmm7, -80(%rbp)
        vmovdqa %xmm6, -96(%rbp)
        movq    %rcx, %rax
; │ @ Julia_vs_Delphi.jl:8 within `for_fun`
; │┌ @ range.jl:5 within `Colon`
; ││┌ @ range.jl:354 within `UnitRange`
; │││┌ @ range.jl:359 within `unitrange_last`
        testq   %rdx, %rdx
; │└└└
        jle     L80
; │ @ Julia_vs_Delphi.jl within `for_fun`
        movq    %rdx, %rcx
        sarq    $63, %rcx
        andnq   %rdx, %rcx, %r9
; │ @ Julia_vs_Delphi.jl:13 within `for_fun`
        cmpq    $8, %r9
        jae     L93
; │ @ Julia_vs_Delphi.jl within `for_fun`
        movl    $1, %r10d
        xorl    %edx, %edx
        xorl    %r11d, %r11d
        jmp     L346
L80:
        xorl    %edx, %edx
        xorl    %r11d, %r11d
        xorl    %r9d, %r9d
        jmp     L386
L93:    movabsq $9223372036854775800, %r8       # imm = 0x7FFFFFFFFFFFFFF8
; │ @ Julia_vs_Delphi.jl:13 within `for_fun`
        andq    %r9, %r8
        leaq    1(%r8), %r10
        movabsq $.rodata.cst32, %rcx
        vmovdqa (%rcx), %ymm1
        vpxor   %xmm0, %xmm0, %xmm0
        movabsq $.rodata.cst8, %rcx
        vpbroadcastq    (%rcx), %ymm2
        movabsq $1023787240, %rcx               # imm = 0x3D05C0E8
        vpbroadcastq    (%rcx), %ymm3
        movabsq $1023787248, %rcx               # imm = 0x3D05C0F0
        vpbroadcastq    (%rcx), %ymm5
        vpcmpeqd        %ymm6, %ymm6, %ymm6
        movabsq $1023787256, %rcx               # imm = 0x3D05C0F8
        vpbroadcastq    (%rcx), %ymm7
        movq    %r8, %rcx
        vpxor   %xmm4, %xmm4, %xmm4
        vpxor   %xmm8, %xmm8, %xmm8
        vpxor   %xmm9, %xmm9, %xmm9
        nopw    %cs:(%rax,%rax)
; │ @ Julia_vs_Delphi.jl within `for_fun`
L224:
        vpaddq  %ymm2, %ymm1, %ymm10
; │ @ Julia_vs_Delphi.jl:10 within `for_fun`
        vpxor   %ymm3, %ymm1, %ymm11
        vpcmpgtq        %ymm11, %ymm5, %ymm11
        vpxor   %ymm3, %ymm10, %ymm10
        vpcmpgtq        %ymm10, %ymm5, %ymm10
        vpsubq  %ymm11, %ymm0, %ymm0
        vpsubq  %ymm10, %ymm4, %ymm4
        vpaddq  %ymm11, %ymm8, %ymm8
        vpsubq  %ymm6, %ymm8, %ymm8
        vpaddq  %ymm10, %ymm9, %ymm9
        vpsubq  %ymm6, %ymm9, %ymm9
        vpaddq  %ymm7, %ymm1, %ymm1
        addq    $-8, %rcx
        jne     L224
; │ @ Julia_vs_Delphi.jl:13 within `for_fun`
        vpaddq  %ymm8, %ymm9, %ymm1
        vextracti128    $1, %ymm1, %xmm2
        vpaddq  %xmm2, %xmm1, %xmm1
        vpshufd $238, %xmm1, %xmm2              # xmm2 = xmm1[2,3,2,3]
        vpaddq  %xmm2, %xmm1, %xmm1
        vmovq   %xmm1, %r11
        vpaddq  %ymm0, %ymm4, %ymm0
        vextracti128    $1, %ymm0, %xmm1
        vpaddq  %xmm1, %xmm0, %xmm0
        vpshufd $238, %xmm0, %xmm1              # xmm1 = xmm0[2,3,2,3]
        vpaddq  %xmm1, %xmm0, %xmm0
        vmovq   %xmm0, %rdx
        cmpq    %r8, %r9
        je      L386
L346:
        leaq    1(%r9), %r8
        nop
; │ @ Julia_vs_Delphi.jl:10 within `for_fun`
; │┌ @ operators.jl:378 within `>`
; ││┌ @ int.jl:83 within `<`
L352:
        xorl    %ecx, %ecx
        cmpq    $500000, %r10                   # imm = 0x7A120
        seta    %cl
        cmpq    $500001, %r10                   # imm = 0x7A121
; │└└
        adcq    $0, %rdx
        addq    %rcx, %r11
; │ @ Julia_vs_Delphi.jl:13 within `for_fun`
; │┌ @ range.jl:837 within `iterate`
        incq    %r10
; ││┌ @ promotion.jl:468 within `==`
        cmpq    %r10, %r8
; │└└
        jne     L352
; │ @ Julia_vs_Delphi.jl:17 within `for_fun`
L386:
        movq    %r9, (%rax)
        movq    %rdx, 8(%rax)
        movq    %r11, 16(%rax)
        vmovaps -96(%rbp), %xmm6
        vmovaps -80(%rbp), %xmm7
        vmovaps -64(%rbp), %xmm8
        vmovaps -48(%rbp), %xmm9
        vmovaps -32(%rbp), %xmm10
        vmovaps -16(%rbp), %xmm11
        addq    $96, %rsp
        popq    %rbp
        vzeroupper
        retq
        nopw    %cs:(%rax,%rax)

一些编译器可能会跳过循环,如果没有可见的结果。尝试在循环后写入“Small,Big或Total”。 - LU RD
@LURD - 现在请打印这些数字,确保它们是正确的。我得到了这个结果:(1000000000, 500000, 999500000) - Gabriel
Julia会造假吗?我得测试一下。Delphi生成的ASM代码非常好。我不知道你能通过多大程度的改进来实现7.6倍的速度提升。那个数字简直太疯狂了! - Gabriel
哪个实际的Delphi编译器版本与哪个实际的Julia编译器版本相对应?这些是重要细节。除非这个问题的目的完全不同于将苹果(任何Delphi编译器)与橙子(任何Julia编译器)进行比较,否则请尝试最近的FPCompiler和旧的Turbo Pascal编译器。 - AmigoJack
1
“_Recent_”是一个相对的术语。当某人在3个月后阅读这些评论/您的问题时,要弄清楚实际版本号将更加困难。请将其编辑到您的问题中,避免换行,因为它们总是被吃掉,以防您忘记注意到。 - AmigoJack
显示剩余11条评论
1个回答

4

让我们先注意到优化编译器实际上没有理由执行循环,目前的 Delphi 和 Julia 输出类似的汇编代码,实际上会通过循环运行,但未来编译器可能会跳过循环并直接赋值。微基准测试是棘手的。

差异似乎在于 Julia 使用了 SIMD 指令,这对于这样的循环非常合理(根据您的 CPU,速度提升了大约 8 倍是合理的)。

您可以查看 这篇博客文章 来了解有关 Delphi 中 SIMD 的想法。


虽然这不是答案的重点,但我想稍微扩展一下完全删除循环的可能性。我不确定Delphi规范说了什么,但在许多编译语言中,包括Julia(“刚好提前”),编译器可以简单地计算出循环后变量的状态,并用该状态替换循环。请看下面的C++代码(compiler explorer):

#include <cstdio>
void loop() {
    long total = 0, big = 0, small = 0;
    for (long i = 0; i < 100; ++i) {
        total++;
        if (total > 50) {
            big++;
        } else {
            small++;
        }
    }
    std::printf("%ld %ld %ld", total, big, small);
}

这是汇编器clang trunk的输出:

loop():                               # @loop()
        lea     rdi, [rip + .L.str]
        mov     esi, 100
        mov     edx, 50
        mov     ecx, 50
        xor     eax, eax
        jmp     printf@PLT                      # TAILCALL
.L.str:
        .asciz  "%ld %ld %ld"

正如你所看到的,没有循环,只有结果。对于更长的循环,clang会停止执行此优化,但这只是编译器的限制,其他编译器可能会以不同的方式处理它,我相信存在一些高度优化的编译器可以处理更复杂的情况。


2
@ServerOverflow:是的,但这些值在循环外部没有被使用,因此在这方面计算实际上是一个“无操作”。 - HeartWare
2
即便是在循环之后使用计算,聪明的编译器也可以完成计算并完全消除循环。这是因为所有变量在循环之前都有已知的值。@ServerOverflow - LU RD
@ahnlabb - 当然,它们是在循环之后使用的:我会在屏幕上展示它们。我在帖子中提到过。无论是在Julia还是Delphi中都是如此。我特别这样做是为了确保编译器不会“超级”优化循环 :) - Gabriel
@ServerOverflow:在你提供的问题代码中没有… - HeartWare
@ahnlabb - 好的,现在我明白你的意思了 :) ... 然而,这并不会原谅 Delphi 编译器的缓慢 :) 如果 Julia 可以进行这样的优化,那么 Delphi 也可以。另一方面,这意味着在更多“真实世界”的情况下,Julia 将无法使用这种技巧,Delphi 和 Julia 之间的速度差异可能会更小。但这可能只是我的一厢情愿 :) - Gabriel
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接