为什么在这种情况下.NET比C++更快?

12

确保在IDE之外运行代码。这很关键。

-编辑- 我喜欢SLaks的评论。"这些答案中的错误信息太多了。" :D

冷静点,伙计们。你们几乎都错了。我做出了优化。 事实证明,无论我做什么样的优化都不够好。 我使用gettimeofday在GCC中运行了代码(我将在下面粘贴代码),并使用g++ -O2 file.cpp比C#获得稍微快一点的结果。 也许MS没有为这种特定情况创建所需的优化,但下载和安装mingw后进行测试,发现速度几乎相同。 Justicle看来是正确的。我发誓我在我的电脑上使用clock计时,并发现它更慢,但问题已解决。在MS编译器中,C++速度不会降低近一倍。

当我的朋友告诉我这个问题时,我简直不敢相信。于是我拿起他的代码并加入了一些定时器。

我用C#代替Boo。无论我使用什么数字,C#的结果总是更快。为什么呢?无论如何,.NET版本几乎始终比C++版本快将近一倍。

C++版本(糟糕的版本):

#include <iostream>
#include <stdio.h>
#include <intrin.h>
#include <windows.h>
using namespace std;

int fib(int n)
{
    if (n < 2) return n;
    return fib(n - 1) + fib(n - 2);
}

int main()
{
    __int64 time = 0xFFFFFFFF;
    while (1)
    {
        int n;
        //cin >> n;
        n = 41;
        if (n < 0) break;
__int64 start = __rdtsc();
        int res = fib(n);
__int64 end = __rdtsc();
        cout << res << endl;
        cout << (float)(end-start)/1000000<<endl;
        break;
    }

    return 0;
}

C++版本(更好的版本):

#include <iostream>
#include <stdio.h>
#include <intrin.h>
#include <windows.h>
using namespace std;

int fib(int n)
{
    if (n < 2) return n;
    return fib(n - 1) + fib(n - 2);
}

int main()
{
    __int64 time = 0xFFFFFFFF;
    while (1)
    {
        int n;
        //cin >> n;
        n = 41;
        if (n < 0) break;
        LARGE_INTEGER start, end, delta, freq;
        ::QueryPerformanceFrequency( &freq );
        ::QueryPerformanceCounter( &start );
        int res = fib(n);
        ::QueryPerformanceCounter( &end );
        delta.QuadPart = end.QuadPart - start.QuadPart;
        cout << res << endl;
        cout << ( delta.QuadPart * 1000 ) / freq.QuadPart <<endl;
break;
    }

    return 0;
}

C# 版本:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using System.Runtime.InteropServices;
using System.ComponentModel;
using System.Threading;
using System.IO;

using System.Diagnostics;

namespace fibCSTest
{
    class Program
    {
         static int fib(int n)
         {
            if (n < 2)return n;
            return fib(n - 1) + fib(n - 2);
         }

         static void Main(string[] args)
         {
             //var sw = new Stopwatch();
             //var timer = new PAB.HiPerfTimer();
             var timer = new Stopwatch();
             while (true)
             {
                 int n;
                 //cin >> n;
                 n = 41;
                 if (n < 0) break;
                 timer.Start();
                 int res = fib(n);
                 timer.Stop();
                 Console.WriteLine(res);
                 Console.WriteLine(timer.ElapsedMilliseconds);
                 break;
             }
         }
    }
}

GNU编译器版本:

#include <iostream>
#include <stdio.h>
#include <sys/time.h>
using namespace std;

int fib(int n)
{
    if (n < 2) return n;
    return fib(n - 1) + fib(n - 2);
}

int main()
{
    timeval start, end;
    while (1)
    {
        int n;
        //cin >> n;
        n = 41;
        if (n < 0) break;
        gettimeofday(&start, 0);
        int res = fib(n);
        gettimeofday(&end, 0);
        int sec = end.tv_sec - start.tv_sec;
        int usec = end.tv_usec - start.tv_usec;
        cout << res << endl;
        cout << sec << " " << usec <<endl;
        break;
    }

    return 0;
}

16
无论如何,这个算法都太糟糕了,不值得优化。 - dsimcha
10
哎呀,这样计算斐波那契数列的方式太糟糕了。 - Jacob Bellamy
14
这些答案中错误信息的数量令人震惊。 - SLaks
7
给所有现在和未来的回答者:目前的C#编译器不支持尾递归优化。请跟我重复:目前的C#编译器不支持尾递归优化。目前的C#编译器不支持尾递归优化。 - SLaks
4
关于尾调用优化的争论并不相关,因为没有尾调用。最后一个操作是加法,所以调用必须返回。 - Rune FS
显示剩余7条评论
13个回答

16

编辑:TL/DR 版本:CLR JIT 将内联一级递归,而 MSVC 8 SP1 不会除非使用 #pragma inline_recursion(on)。你应该在不使用调试器的情况下运行 C# 版本以获取完全优化的 JIT。

我在一台 Core2 Duo 笔记本电脑上,使用 VS 2008 SP1 运行 Vista 的“高性能”电源设置,得到了与 acidzombie24 类似的结果,用 C# 和 C++ 进行了比较(大约为 1600 毫秒和 3800 毫秒)。很难看出经过优化的 JIT 的 C# 代码,但对于 x86,它可以概括为:

00000000 55               push        ebp  
00000001 8B EC            mov         ebp,esp 
00000003 57               push        edi  
00000004 56               push        esi  
00000005 53               push        ebx  
00000006 8B F1            mov         esi,ecx 
00000008 83 FE 02         cmp         esi,2 
0000000b 7D 07            jge         00000014 
0000000d 8B C6            mov         eax,esi 
0000000f 5B               pop         ebx  
00000010 5E               pop         esi  
00000011 5F               pop         edi  
00000012 5D               pop         ebp  
00000013 C3               ret              
            return fib(n - 1) + fib(n - 2);
00000014 8D 7E FF         lea         edi,[esi-1] 
00000017 83 FF 02         cmp         edi,2 
0000001a 7D 04            jge         00000020 
0000001c 8B DF            mov         ebx,edi 
0000001e EB 19            jmp         00000039 
00000020 8D 4F FF         lea         ecx,[edi-1] 
00000023 FF 15 F8 2F 12 00 call        dword ptr ds:[00122FF8h] 
00000029 8B D8            mov         ebx,eax 
0000002b 4F               dec         edi  
0000002c 4F               dec         edi  
0000002d 8B CF            mov         ecx,edi 
0000002f FF 15 F8 2F 12 00 call        dword ptr ds:[00122FF8h] 
00000035 03 C3            add         eax,ebx 
00000037 8B D8            mov         ebx,eax 
00000039 4E               dec         esi  
0000003a 4E               dec         esi  
0000003b 83 FE 02         cmp         esi,2 
0000003e 7D 04            jge         00000044 
00000040 8B D6            mov         edx,esi 
00000042 EB 19            jmp         0000005D 
00000044 8D 4E FF         lea         ecx,[esi-1] 
00000047 FF 15 F8 2F 12 00 call        dword ptr ds:[00122FF8h] 
0000004d 8B F8            mov         edi,eax 
0000004f 4E               dec         esi  
00000050 4E               dec         esi  
00000051 8B CE            mov         ecx,esi 
00000053 FF 15 F8 2F 12 00 call        dword ptr ds:[00122FF8h] 
00000059 03 C7            add         eax,edi 
0000005b 8B D0            mov         edx,eax 
0000005d 03 DA            add         ebx,edx 
0000005f 8B C3            mov         eax,ebx 
00000061 5B               pop         ebx  
00000062 5E               pop         esi  
00000063 5F               pop         edi  
00000064 5D               pop         ebp  
00000065 C3               ret  

与C++生成的代码(/Ox /Ob2 /Oi /Ot /Oy /GL /Gr)相比:

int fib(int n)
{ 
00B31000 56               push        esi  
00B31001 8B F1            mov         esi,ecx 
    if (n < 2) return n; 
00B31003 83 FE 02         cmp         esi,2 
00B31006 7D 04            jge         fib+0Ch (0B3100Ch) 
00B31008 8B C6            mov         eax,esi 
00B3100A 5E               pop         esi  
00B3100B C3               ret              
00B3100C 57               push        edi  
    return fib(n - 1) + fib(n - 2); 
00B3100D 8D 4E FE         lea         ecx,[esi-2] 
00B31010 E8 EB FF FF FF   call        fib (0B31000h) 
00B31015 8D 4E FF         lea         ecx,[esi-1] 
00B31018 8B F8            mov         edi,eax 
00B3101A E8 E1 FF FF FF   call        fib (0B31000h) 
00B3101F 03 C7            add         eax,edi 
00B31021 5F               pop         edi  
00B31022 5E               pop         esi  
} 
00B31023 C3               ret              

C#版本基本上是内联调用了fib(n-1)fib(n-2)。对于一个如此频繁的调用函数,减少函数调用次数是提高速度的关键。将fib替换为以下内容:

int fib(int n);

int fib2(int n) 
{ 
    if (n < 2) return n; 
    return fib(n - 1) + fib(n - 2); 
} 

int fib(int n)
{ 
    if (n < 2) return n; 
    return fib2(n - 1) + fib2(n - 2); 
} 

经过优化后,将时间降低至约1900毫秒。顺便提一下,如果我使用#pragma inline_recursion(on),那么原始的fib也可以获得类似的结果。再展开一层:

int fib(int n);

int fib3(int n) 
{ 
    if (n < 2) return n; 
    return fib(n - 1) + fib(n - 2); 
} 

int fib2(int n) 
{ 
    if (n < 2) return n; 
    return fib3(n - 1) + fib3(n - 2); 
} 

int fib(int n)
{ 
    if (n < 2) return n; 
    return fib2(n - 1) + fib2(n - 2); 
} 

优化后的时间大约为1380毫秒,超过这个时间之后就会逐渐变慢。

因此,在我的机器上,CLR JIT 会将递归调用内联一层,而C++编译器默认情况下不会这样做。

如果所有性能关键代码都像fib一样那该多好啊!


非常准确 - 我在调试器中计时C#,在命令行上运行可以显著提高速度。我已经更新了我的答案并得到了更好的结果。 - Justicle
只是好奇 - 你是怎么得到JIT代码的ASM的?此外,我使用inline_recursion(on)和Ob2没有任何改进,但手动展开确实有改进。我一定错过了编译器设置... - Justicle
2
@justicle,我添加了一个while(true){}并附加到进程上。至于C++版本,我打开了“省略框架布局”并将调用约定更改为__fastcall - MSN
顺便提一下,gcc 生成的代码甚至更加复杂(当我之前尝试时,它执行了尾调用优化)。 - jpalecek
C++代码中的fib函数没有静态关键字,而C#有,这就是时间不准确的原因。代码翻译得很糟糕。 - fazo
@fazo,在fib上指定static在这种情况下最终不会影响链接时代码生成。它将一些代码内联到主函数中,但仅此而已。展开一级递归是更有效的优化。 - MSN

8

编辑: 虽然原始的C++计时是错误的(将周期与毫秒进行比较),但更好的计时显示,使用基本编译器设置,C#更快。

好了,足够的随机猜测,是时候进行一些科学实验了。在使用现有的C++代码出现奇怪结果后,我尝试运行:

int fib(int n)
{
    if (n < 2) return n;
    return fib(n - 1) + fib(n - 2);
}

int main()
{
    __int64 time = 0xFFFFFFFF;
    while (1)
    {
        int n;
        //cin >> n;
        n = 41;
        if (n < 0) break;
        LARGE_INTEGER start, end, delta, freq;
        ::QueryPerformanceFrequency( &freq );
        ::QueryPerformanceCounter( &start );
        int res = fib(n);
        ::QueryPerformanceCounter( &end );
        delta.QuadPart = end.QuadPart - start.QuadPart;
        cout << res << endl;
        cout << ( delta.QuadPart * 1000 ) / freq.QuadPart <<endl;
break;
    }

    return 0;
}

编辑:

MSN指出你应该在调试器外计时C#,所以我重新运行了所有内容:

最佳结果(VC2008,从命令行运行发布版本,未启用任何特殊选项)

  • C++原始代码 - 10239
  • C++ QPF - 3427
  • C# - 2166(在调试器中为4700)。

原始的C ++代码(带有rdtsc)并没有返回毫秒,只是报告的时钟周期的因素,因此直接与StopWatch()结果进行比较是无效的。 原始计时代码是错误的。

请注意,StopWatch()使用QueryPerformance*调用:http://msdn.microsoft.com/en-us/library/system.diagnostics.stopwatch.aspx

因此,在这种情况下,C++比C#更快。它取决于您的编译器设置-请参见MSN的答案。


1
在我的机器上,使用C#版本且未附加调试器时运行速度更快。 - MSN
RDSTC是一个POS,哈哈,你觉得QueryPerformanceCounter是如何实现的?如果你不知道如何在你特定的CPU/型号上使用RDSTC,那么它将是一个POS。 - leppie
1
@leppie。我不知道你怎么想,但实际上QueryPerformanceCounter是实现为使用系统上的任何高性能计数器。如果没有可用的高性能计数器,则它可能不比RDSTC更好http://msdn.microsoft.com/en-us/library/ms644900(VS.85).aspx#high_resolution - MarkJ
1
@leppie。如果检测到结果可靠,它会调用RDSTC。换句话说,它会尽力避免在RDSTC效果差的情况下使用它。http://blogs.msdn.com/oldnewthing/archive/2005/09/02/459952.aspx - MarkJ
@MarkJ: 从那篇非常古老的帖子开始(我注意到每个人都拼错了这个词...),它尝试使用RDTSC作为首选来源。它提到了有缺陷的硬件,但没有提供清单。我可以保证,在我的i7上,它使用了CPU的时间戳计数器。英特尔系统编程指南也说这对基于Nehalem的处理器很好用。我的观点仍然存在。如果您不知道如何为您的CPU使用它,RDTSC只是一个废物。 - leppie
显示剩余2条评论

4

如果你不理解垃圾回收或控制台缓冲区的答案,可能是因为你在C++中的计时机制本质上存在缺陷。

根据http://en.wikipedia.org/wiki/Rdtsc,你可能会得到错误的基准测试结果。

引用:

虽然这使时间保持更加一致,但它可能会扭曲基准测试,在某些时间内花费较少的时钟周期进行自旋,然后操作系统将处理器切换到更高的速率。这会导致事物似乎需要比它们通常需要的更多的处理器周期。


1
沿着这条线,rdtsc()函数不是返回一个通用的“tick”计数吗——并不一定是在代码中假定的1GHz频率? - Eric Pi
@Eric:你可能是对的,但这取决于OP实际拥有的内容。不过似乎不太可能是这样一个好的数字。 - Aryabhatta
3
正确的做法是使用 QueryPerformanceCounter() 和相关函数,RDSTC 是一个有问题的指令。 - Justicle
另一个 RDTSC 的问题是,在某些系统上,它在多个 CPU 核心/插座之间不同步。在这样的系统上,当操作系统将线程迁移到不同的 CPU 时,RDTSC 的值可能会出现向前或向后的“时间扭曲”。亲和力可以用来解决这个问题,但不能解决答案中描述的时钟跳动问题。 - bk1e
顺便提一下,在拥有ACPI多处理器HAL的Windows XP上,QueryPerformanceCounter()仍然使用RDTSC。而在可用时,在Vista/7上则使用HPET(一个更好的定时器)。 - bk1e

4
我认为问题出在你的C++计时代码上。
根据MS文档中对于__rdtsc的描述:
生成rdtsc指令,该指令返回处理器时间戳。处理器时间戳记录了自上次重置以来的时钟周期数
也许可以尝试使用GetTickCount()函数。

3

并不是说这是问题,但您可能想阅读如何使用高分辨率计时器

同时请参见... http://en.wikipedia.org/wiki/Comparison_of_Java_and_C%2B%2B#Performance

多项主要针对数字基准测试的研究指出,由于各种原因,Java在某些情况下潜在地比C++更快:[8] [9] 指针使优化变得困难,因为它们可能指向任意数据,尽管许多C++编译器提供了C99关键字restrict来解决此问题。[10] 与C++实现相比,后者对malloc/new标准实现进行无限制使用以进行内存分配,Java垃圾收集的实现可能具有更好的缓存一致性,因为其分配通常是按顺序进行的。 * 运行时编译可以利用运行时可用的其他信息更有效地优化代码,例如知道代码将在哪个处理器上执行。

这是关于Java的,但开始探讨C运行时和JIT运行时之间的性能问题。


2
也许C#能够在递归调用中展开堆栈?我认为这也可以减少计算次数。

3
错了,和大多数其他答案一样都是错的。 - SLaks
3
在这种情况下,@SLaks是正确的。它至少将一个调用嵌入到了fib函数中。 - MSN

1

在比较编程语言时需要记住的一件重要事情是,如果你进行简单的逐行翻译,那么你并没有在进行苹果与苹果的比较。

在一种语言中有意义的东西,在另一种语言中可能会产生可怕的副作用。为了真正比较性能特征,您需要一个C#版本和一个C++版本,这些版本的代码可能非常不同。例如,在C#中,我甚至不会使用相同的函数签名。我会选择类似于这样的东西:

IEnumerable<int> Fibonacci()
{
   int n1 = 0;
   int n2 = 1;

   yield return 1;
   while (true)
   {
      int n = n1 + n2;
      n1 = n2;
      n2 = n;
      yield return n;
   }
}

然后像这样包装:

public static int fib(int n)
{
    return Fibonacci().Skip(n).First();
}

这样做会更好,因为它从底部向上工作,利用最后一个术语中的计算来帮助构建下一个术语,而不是两个独立的递归调用集。

如果你真的想在C++中获得惊人的性能,你可以使用元编程来让编译器像这样预先计算你的结果:

template<int N> struct fibonacci
{
    static const int value = fibonacci<N - 1>::value + fibonacci<N - 2>::value;
};

template<> struct fibonacci<1>
{
    static const int value = 1;
};

template<> struct fibonacci<0>
{
    static const int value = 0;
};

2
编译时的斐波那契数列肯定不是一回事 :-) - Justicle

0

可能是在运行测试之前,方法已经被预编译了...或者控制台是一个包装器,用于输出到控制台的API,当C++的cout代码被缓冲时...我猜测。

希望这可以帮助您, 最好的问候, 汤姆。


5
不,控制台输出并没有被计时。 - SLaks
“可能是在运行测试之前,方法已经被预编译了”这句话是什么意思? - Justicle

0

你正在调用C#代码中的静态函数,该函数将被内联,而在C++中,你使用的是非静态函数。我有1.4秒左右的时间来运行C++。使用g++ -O3可以将其缩短至1.21秒。

你不能拿着翻译不好的代码比较C#和C++。


翻译以下与编程有关的内容从英文到中文。仅返回翻译后的文本:糟糕的翻译?不可能。在这两种情况下,函数可以在文件和类之外看到。您正在使用静态的方式听起来像C#中的内部关键字,但它没有使用(将代码限制为文件不存在于C#中)。因此,翻译相当准确。我不会说C ++中的静态不会提供更好的性能,我知道它可能会。 - user34537
C#版本明显有:静态int fib(int n)……并且是一个类的成员……当然它会被内联。如果你想在C++中拥有相同的代码,你必须创建一个带有成员函数fib的类——默认情况下将像C#一样被内联。 - fazo

-2
如果该代码确实只需要执行时间的一半,那么可能的原因有:
  • 如果在上述代码中发生了垃圾回收,则C#代码的执行速度比C++代码更快。
  • C#写入控制台可能会被缓冲(C++可能没有,或者它可能不够高效)。

4
  1. 没有新的进展,所以我怀疑没有进行垃圾回收的工作。
  2. 计时器不包括控制台写入(console write)/输出(cout)。这并不会使C++的运行时间变慢。
- user34537
澄清一下,递归使用堆栈上的内存,而不是堆上的内存,因此不使用垃圾回收。 - Brendan Long

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