Julia 的表现远远优于 Delphi。Delphi 编译器的 asm 代码已过时?

WeG*_*ars 5 delphi julia

我在 Delphi 中编写了一个简单的 for 循环。\n相同的程序在 Julia 1.6 中快了 7.6 倍。

\n
procedure TfrmTester.btnForLoopClick(Sender: TObject);\nVAR\n   i, Total, Big, Small: Integer;\n   s: string;\nbegin   \n  TimerStart;\n\n   Total:= 0;\n   Big  := 0;\n   Small:= 0;\n   for i:= 1 to 1000000000 DO    //1 billion\n    begin\n      Total:= Total+1;\n      if Total > 500000\n      then Big:= Big+1\n      else Small:= Small+1;\n    end;\n\n s:= TimerElapsedS;\n //here code to show Big/Small on the screen\nend;\n
Run Code Online (Sandbox Code Playgroud)\n

ASM 代码对我来说似乎不错:

\n
TesterForm.pas.111: TimerStart;\n007BB91D E8DE7CF9FF       call TimerStart\nTesterForm.pas.113: Total:= 0;\n007BB922 33C0             xor eax,eax\n007BB924 8945F4           mov [ebp-$0c],eax\nTesterForm.pas.114: Big  := 0;\n007BB927 33C0             xor eax,eax\n007BB929 8945F0           mov [ebp-$10],eax\nTesterForm.pas.115: Small:= 0;\n007BB92C 33C0             xor eax,eax\n007BB92E 8945EC           mov [ebp-$14],eax\nTesterForm.pas.**116**: for i:= 1 to 1000000000 DO    //1 billion\n007BB931 C745F801000000   mov [ebp-$08],$00000001\nTesterForm.pas.118: Total:= Total+1;\n007BB938 FF45F4           inc dword ptr [ebp-$0c]\nTesterForm.pas.119: if Total > 500000\n007BB93B 817DF420A10700   cmp [ebp-$0c],$0007a120\n007BB942 7E05             jle $007bb949\nTesterForm.pas.120: then Big:= Big+1\n007BB944 FF45F0           inc dword ptr [ebp-$10]\n007BB947 EB03             jmp $007bb94c\nTesterForm.pas.121: else Small:= Small+1;\n007BB949 FF45EC           inc dword ptr [ebp-$14]\nTesterForm.pas.122: end;\n007BB94C FF45F8           inc dword ptr [ebp-$08]\nTesterForm.pas.**116**: for i:= 1 to 1000000000 DO    //1 billion\n007BB94F 817DF801CA9A3B   cmp [ebp-$08],$3b9aca01\n007BB956 75E0             jnz $007bb938\nTesterForm.pas.124: s:= TimerElapsedS;\n007BB958 8D45E8           lea eax,[ebp-$18]\n
Run Code Online (Sandbox Code Playgroud)\n

与 Julia 相比,Delphi 的分数怎么会如此可怜?\n我可以做些什么来改进编译器生成的代码吗?

\n

信息

\n

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

\n

在此输入图像描述

\n

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

\n

更多讨论

\n
    \n
  1. 我不得不提一下,当你第一次在 Julia 中运行代码时,它的时间长得离谱,因为 Julia 是 JIT,所以它必须先编译代码。编译时间(因为它是“一次性”)未包含在测量中。

    \n
  2. \n
  3. 此外,正如 AmigoJack 评论的那样,Delphi 代码几乎可以在任何地方运行,而 Julia 代码可能只能在具有现代 CPU 来支持所有这些新/奇特指令的计算机中运行。我确实有 2004 年生产的小工具,至今仍在运行。

    \n
  4. \n
  5. 无论 Julia 生成什么代码,除非安装了 Julia,否则都无法交付给“客户”。

    \n
  6. \n
\n

无论如何,综上所述,令人遗憾的是 Delphi 编译器如此过时。

\n
    \n
  1. 我进行了其他测试,发现在字符串列表中查找最短和最长的字符串在 Delphi 中比 Julia 快 10 倍。分配小内存块(10000x10000x4 字节)具有相同的速度。

    \n
  2. \n
  3. 正如 AhnLab 提到的,我进行了相当“干”的测试。我想需要编写一个执行更复杂/现实任务的完整程序,并在程序结束时查看 Julia 是否仍然优于 Delphi 7x。

    \n
  4. \n
\n
\n

更新

\n

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

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

ahn*_*abb 4

首先让我们注意到优化编译器没有理由实际执行循环,目前 Delphi 和 Julia 输出类似的汇编器,实际运行循环,但编译器将来可以跳过循环并分配值。微基准测试很棘手。

不同之处似乎在于 Julia 使用SIMD 指令,这对于此类循环非常有意义(~8 倍加速非常有意义,具体取决于您的 CPU)。

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


尽管这不是答案的要点,但我将稍微扩展一下完全删除循环的可能性。我不确定 Delphi 规范怎么说,但在许多编译语言中,包括 Julia(“提前”),编译器可以简单地找出循环后变量的状态并替换循环与那个状态。看一下下面的 C++ 代码(编译器资源管理器):

#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);
}
Run Code Online (Sandbox Code Playgroud)

这是汇编程序 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"
Run Code Online (Sandbox Code Playgroud)

如您所见,没有循环,只有结果。对于较长的循环,clang 会停止进行此优化,但这只是编译器的限制,其他编译器可以以不同的方式执行此操作,并且我确信有一个高度优化的编译器可以处理更复杂的情况。

  • @ServerOverflow:是的,但是这些值不在循环之外使用,因此在这方面计算实际上是“无操作”。 (2认同)
  • @ServerOverflow,即使在循环之后使用计算,智能编译器也可以完成计算并完全消除循环。这是因为所有变量在循环之前都有已知值。 (2认同)