Delphi循环性能远逊Julia:编译器生成ASM代码是否过时?
Delphi循环性能为何比Julia慢7.6倍?优化方向探讨
测试场景与原始代码
我在Delphi 10.4.2中编写了一个10亿次的简单循环,Win32 Release模式下耗时1.5秒(Debug版1.8秒,差异极小),而相同逻辑在Julia 1.6中仅需195毫秒(不计JIT首次编译的一次性开销)。
Delphi原始代码
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; // 显示Big/Small的代码 end;
Delphi Debug版生成的汇编代码
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]
性能差异的核心原因
对比Julia生成的汇编代码(见下文),关键差异如下:
- 指令集利用:Julia生成了基于AVX2的64位向量代码(使用
ymm寄存器、vpaddq/vpcmpgtq等向量指令),一次可批量处理8个64位整数;而Delphi的Win32代码仅使用32位通用寄存器,每次只能处理一个整数。 - 变量存储:Delphi将
Total/Big/Small存储在栈内存中([ebp-$xx]),每次操作都要读写内存;Julia则将变量放入寄存器甚至向量寄存器,避免了内存访问开销。 - 编译器优化策略:Julia的JIT编译器会针对当前CPU自动做循环向量化、展开等优化;而Delphi的Win32编译器对这类简单循环的自动向量化支持不足,未做批量处理优化。
Julia生成的汇编代码(64位AVX2优化)
; ┌ @ 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)
Delphi可尝试的优化方案
1. 改用Win64目标平台
切换到64位编译后,编译器可使用更多通用寄存器,支持AVX等现代指令集,自动优化能力会显著提升,部分场景下可接近Julia的性能。
2. 手动消除循环内分支
本次测试的分支条件在循环执行500000次后就不再变化,可直接拆分循环,完全消除分支判断:
procedure TfrmTester.btnForLoopClick(Sender: TObject); VAR i, Total, Big, Small: Integer; s: string; begin TimerStart; Total := 0; Big := 0; Small := 0; // 前500000次循环,Small递增 for i := 1 to 500000 do begin Inc(Small); Inc(Total); end; // 剩余999500000次循环,直接赋值Big,无需循环 Big := 999500000; Total := 1000000000; s:= TimerElapsedS; // 显示Big/Small的代码 end;
该优化会让编译器直接把循环替换为常数赋值,耗时几乎为0。
3. 开启编译器最高优化
在项目选项中开启以下设置:
- 勾选Full Optimization
- 关闭Debug Information
- 开启Inline Function Expansion
- 勾选Remove Redundant Code
4. 手动使用寄存器或内嵌汇编
将变量声明为register类型,让编译器优先放入寄存器;或直接编写内嵌汇编,利用SIMD指令批量处理计数。
补充讨论
- Julia的优势集中在数值计算场景,JIT编译可针对当前CPU生成最优向量代码,但需要依赖Julia运行时,且首次启动有编译开销。
- Delphi的优势在于兼容性(可在老系统运行)、无需额外运行时,在字符串操作、内存管理等业务场景中可能表现更优(测试中字符串列表查找Delphi比Julia快10倍)。
- 本次测试为极端简单的循环场景,真实业务场景中性能差异可能大幅缩小。
内容的提问来源于stack exchange,提问作者Error - CPU Not Foud
相关产品推荐
相关产品推荐

