为什么在Delphi中循环比C#快?

5

Delphi:


procedure TForm1.Button1Click(Sender: TObject);
var I,Tick:Integer;
begin
  Tick := GetTickCount();
  for I := 0 to 1000000000 do
    begin
    end;
  Button1.Caption := IntToStr(GetTickCount()-Tick)+' ms';
end;

C#:


private void button1_Click(object sender, EventArgs e)
        {
            int tick = System.Environment.TickCount;
            for (int i = 0; i < 1000000000; ++i)
            {
            }
            tick = System.Environment.TickCount - tick;
            button1.Text = tick.ToString()+" ms"; 
        }

Delphi需要大约515毫秒

C#需要大约3775毫秒


7
循环可能在 Delphi 中进行了优化处理。 - SLaks
2
但是515毫秒听起来有点太长了... - Andreas Rejbrand
6
如果你在一个每次迭代需要3ns的项目中工作,我猜你不应该使用C#。 - Ken
10个回答

28
Delphi编译成本地代码,而C#编译为CLR代码,然后在运行时进行翻译。虽然C#使用JIT编译,因此你可能会期望时间更加相似,但这并不是一定的。
如果您能描述一下您运行实验的硬件(CPU,时钟频率),那将非常有用。
我没有访问Delphi来重复您的实验,但使用本地C++与C#和以下代码:
VC ++ 2008
#include <iostream>
#include <windows.h>

int main(void)
{
    int tick = GetTickCount() ;
    for (int i = 0; i < 1000000000; ++i)
    {
    }
    tick = GetTickCount() - tick;
    std::cout << tick << " ms" << std::endl  ; 
}

C#

using System;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            int tick = System.Environment.TickCount;
            for (int i = 0; i < 1000000000; ++i)
            {
            }
            tick = System.Environment.TickCount - tick;
            Console.Write( tick.ToString() + " ms" ) ; 
        }
    }
}

我最初得到的是:

C++  2792ms
C#   2980ms

然后我对C#版本进行了重建,并从命令行直接运行了<project>\bin\release<project>\bin\debug中的可执行文件。结果如下:

C# (release):  720ms
C# (debug):    3105ms

所以我认为真正的区别在于,您是从IDE运行C#代码的调试版本。

如果您认为C ++特别慢,那么我将其作为优化的发布版本运行,并获得:

C++ (Optimised): 0ms

这并不奇怪,因为循环是空的,控制变量在循环外部没有被使用,所以优化器将其完全删除。为了避免这种情况,我将i声明为volatile,结果如下:

C++ (volatile i): 2932ms

我的猜测是C#实现也删除了循环,720ms来自其他地方;这可能解释了第一个测试中计时之间的大部分差异。

我无法告诉您Delphi正在做什么,您可以查看生成的汇编代码。

所有上述测试都在AMD Athlon Dual Core 5000B 2.60GHz上进行,在Windows 7 32位操作系统上。


我的汇编代码运行时间为0.390秒,我认为C#循环正在执行。 - Behrooz
1
@Behrooz:很可能,我选择不再深入分析,并且并不真正相信自己的假设!重点是要表明在进行这种比较时需要非常小心,并且可以从C#中获得可比较的时间。此外,作为基准测试,它很糟糕。知道做一些无意义的事情需要多长时间并不能告诉你太多!真正的工作通常需要至少访问内存,而此循环可以完全在CPU内部的寄存器级别执行。 - Clifford
1
将 i 声明为 volatile 会使编译器每次想要增加或检查其值时都从内存中重新获取 i。这将非常缓慢... - Goz
@Goz:是的,我考虑过这个问题,但循环要么执行某些操作,要么就什么都不做;而没有使用 volatile 关键字的话,它等价于什么也没做,因此被优化为什么也没做。一种解决办法是将 i 的作用域移出循环,并在循环后访问它;但我怀疑编译器优化器会将其简化为一个赋值语句。然而,修复无意义的测试并不值得尝试。 - Clifford
1
但它并不什么都不做。将一个寄存器递增并与之比较1,000,000,000次,与从缓存中获取值、递增它、写回到缓存中、从缓存中获取它并进行比较是非常不同的。一个是对寄存器进行2个操作...另一个是对寄存器进行2个操作、2个读取和1个写入。2个读取和1个写入将比递增和比较都慢得多。 - Goz

9

如果这是一个基准测试,那么它是一个极其糟糕的测试,因为在这两种情况下,循环都可以被优化掉,所以你需要查看生成的机器代码来了解正在发生的事情。如果您使用 C# 的发布模式,则以下代码

 Stopwatch sw = Stopwatch.StartNew();
 for (int i = 0; i < 1000000000; ++i){ }
 sw.Stop();
 Console.WriteLine(sw.Elapsed);

被JITter转换为以下内容:
 push        ebp 
 mov         ebp,esp 
 push        edi 
 push        esi 
 call        67CDBBB0 
 mov         edi,eax 
 xor         eax,eax               ; i = 0
 inc         eax                   ; ++i
 cmp         eax,3B9ACA00h         ; i == 1000000000?
 jl          0000000E              ; false: jmp
 mov         ecx,edi 
 cmp         dword ptr [ecx],ecx 
 call        67CDBC10 
 mov         ecx,66DDAEDCh 
 call        FFE8FBE0 
 mov         esi,eax 
 mov         ecx,edi 
 call        67CD75A8 
 mov         ecx,eax 
 lea         eax,[esi+4] 
 mov         dword ptr [eax],ecx 
 mov         dword ptr [eax+4],edx 
 call        66A94C90 
 mov         ecx,eax 
 mov         edx,esi 
 mov         eax,dword ptr [ecx] 
 mov         eax,dword ptr [eax+3Ch] 
 call        dword ptr [eax+14h] 
 pop         esi 
 pop         edi 
 pop         ebp 
 ret

7

TickCount 不是一个可靠的定时器;你应该使用 .Net 的 Stopwatch 类(我不知道 Delphi 的等效物是什么)。

另外,你是否在运行 Release 版本?
你是否已经连接了调试器?


虽然GetTickCount没有毫秒分辨率,但它确实可以处理半秒和接近四秒的时间跨度。所以这不应该是问题。 - Andreas Rejbrand
@Andreas:这取决于系统负载和其他因素。 - SLaks
1
Eric Lippert甚至有一篇关于使用StopWatch提高准确性的博客文章http://blogs.msdn.com/ericlippert/archive/2010/04/08/precision-and-accuracy-of-datetime.aspx - juharr
4
刚刚测试了一下,在我的电脑上,发布版本且没有调试器的情况下大约需要500毫秒。切换到调试版本或使用调试器(或两者皆有)会导致需要大约4000毫秒。Stopwatch和TickCount之间的区别微不足道。 - dtb
是的,GetTickCount在大多数系统上精度约为15毫秒。但在这种情况下并不重要。 - Runner
3
如果我们能够获得每当有人在调试模式下“分析”一个 .NET 应用程序并在 Stack Overflow 上发布关于其性能问题的问题时的五美分…… - Aaronaught

4

Delphi编译器尽可能使用向下计数的for循环;以上代码示例被编译为:

Unit1.pas. 42: Tick := GetTickCount();
00489367 E8B802F8FF       call GetTickCount
0048936C 8BF0             mov esi,eax
Unit1.pas.43: for I := 0 to 1000000000 do
0048936E B801CA9A3B       mov eax,$3b9aca01
00489373 48               dec eax
00489374 75FD             jnz $00489373

3
你正在比较本地代码和虚拟机JIT编译的代码,这是不公平的。由于JIT编译器无法像本地编译器那样优化代码,因此本地代码将始终更快。
话虽如此,比较Delphi和C#根本不公平,Delphi二进制文件总是会获胜(更快,更小,没有任何依赖项等)。
顺便说一句,我很遗憾看到这里有多少张贴者不知道这些差异...或者可能只是伤害了一些.NET狂热分子,他们试图捍卫C#对抗任何显示出更好选择的东西。

6
如果我必须编写一个空循环,我会记住 Delphi 作为一个选项。但是,就算在理论上,JIT 编译器也可以比本机编译器做出更好的优化。编译器只能访问静态信息来进行优化,而 JIT 编译器还能根据代码实际使用的方式来进行运行时动态优化。真正的优势在于,本机代码执行时间可以更准确地被预测,这使其更适用于实时系统。 - ckarras
5
-1: 看看ckarras,"Always"是一个相当强的词(尤其是以粗体出现时)... 我会尽力将其翻译为:请查看ckarras,并且“always”是一个非常强烈的词(特别是在加粗的情况下)... - Raphaël Saint-Pierre
2
“本地代码将始终更快”:我不这么认为。例如,Sun的Hot Spot编译器甚至可以内联虚拟方法调用。实际上,由于额外的运行时信息,JIT编译的代码可能会更好地优化。 - Frank
2
@Navox,理论上是可以的,但实际上不行。由于C#运行时中其他部分的开销以及所有调用的影响,这种情况(几乎)或者实际上从未发生过。 - Johan

2
这是C#反汇编代码:
DEBUG:
// int i = 0; while (++i != 1000000000) ;//==for(int i ...blah blah blah)
0000004e 33 D2            xor         edx,edx 
00000050 89 55 B8         mov         dword ptr [ebp-48h],edx 
00000053 90               nop              
00000054 EB 00            jmp         00000056 
00000056 FF 45 B8         inc         dword ptr [ebp-48h] 
00000059 81 7D B8 00 CA 9A 3B cmp         dword ptr [ebp-48h],3B9ACA00h 
00000060 0F 95 C0         setne       al   
00000063 0F B6 C0         movzx       eax,al 
00000066 89 45 B4         mov         dword ptr [ebp-4Ch],eax 
00000069 83 7D B4 00      cmp         dword ptr [ebp-4Ch],0 
0000006d 75 E7            jne         00000056 

正如您所看到的,这是浪费CPU资源。

编辑:

发布:

   //unchecked
   //{
   //int i = 0; while (++i != 1000000000) ;//==for(int i ...blah blah blah)
00000032 33 D2            xor         edx,edx 
00000034 89 55 F4         mov         dword ptr [ebp-0Ch],edx 
00000037 FF 45 F4         inc         dword ptr [ebp-0Ch] 
0000003a 81 7D F4 00 CA 9A 3B cmp         dword ptr [ebp-0Ch],3B9ACA00h 
00000041 75 F4            jne         00000037 
   //}

编辑:
这是C++版本:在我的机器上运行速度快了9倍。

    __asm
    {
        PUSH ECX
        PUSH EBX
        XOR  ECX, ECX
        MOV  EBX, 1000000000
NEXT:   INC  ECX
        CMP  ECX, EBX
        JS   NEXT
        POP  EBX
        POP  ECX
    }

这是来自发布版本吗? - SLaks
@SLaks:不,它处于调试模式。 - Behrooz
5
调试版本会增加大量开销。 - ewanm89
@ewanm89:是的,发布版本在2.77中运行,而在调试模式下它在3.14中运行(天啊,这是圆周率)。 - Behrooz

1
你应该附加调试器并查看每个生成的机器代码。

0

Delphi几乎肯定会优化该循环以倒序执行(即DOWNTO零而不是FROM零)-每当它确定“安全”时,Delphi都会这样做,可能是因为减法或针对零的检查比加法或针对非零数字的检查更快。

如果您尝试指定以相反顺序执行循环的两种情况会发生什么?


0
在Delphi中,循环条件只会在循环过程开始前计算一次,而在C#中,循环条件会在每次循环通过时重新计算。
这就是为什么Delphi中的循环比C#快的原因。

-1

"// int i = 0; while (++i != 1000000000) ;"

这很有趣。

while (++i != x) 不同于 for (; i != x; i++)

区别在于 while 循环不会执行 i = 0 的循环。

(试一下:运行类似这样的代码:


int i;

for (i = 0; i < 5; i++)
    Console.WriteLine(i);

i = 0;
while (++i != 5)
    Console.WriteLine(i);

我一直认为++i不等于i++。 - Behrooz

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