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

时间:2010-02-18 02:16:06

标签: c# .net c++ benchmarking fibonacci

确保您在IDE之外运行。这是关键。

-edit-我喜欢SLaks评论。 “这些答案中的错误信息量是惊人的。” :d

冷静下来。几乎所有人都错了。我做了优化。 事实证明,我所做的任何优化都不够好。 我使用gettimeofday在GCC中运行代码(我将在下面粘贴代码)并使用g++ -O2 file.cpp并获得比C#略快的结果。 也许MS没有创建这个特定情况下所需的优化,但在下载并安装mingw后,我进行了测试,发现速度几乎相同。 Justicle似乎是对的。我可以发誓我在我的电脑上使用时钟并使用它来计算并发现它速度较慢但问题已解决。在MS编译器中,C ++速度几乎不会慢两倍。

当我的朋友告诉我这件事时我无法相信。所以我拿了他的代码并把一些定时器放在上面。

而不是Boo我使用了C#。我不断在C#中获得更快的结果。为什么?无论我使用什么数字,.NET版本几乎都有一半的时间。

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;
             }
         }
    }
}

GCC版本:

#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;
}

13 个答案:

答案 0 :(得分:16)

编辑:TL / DR版本:CLR JIT将内联一级递归,MSVC 8 SP1不会没有#pragma inline_recursion(on)。您应该在调试器之外运行C#版本以获得完全优化的JIT。

我在使用VS 2008 SP1的C#与C ++上使用VS 2008 SP1获得了类似的结果,运行Vista的Core2 Duo笔记本电脑插入了“高性能”电源设置(~1600 ms vs.~3800 ms)。看到优化的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)。对于call重的函数,减少函数调用次数是提高速度的关键。用以下内容替换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

答案 1 :(得分:8)

编辑: 虽然最初的C ++时序是错误的(比较周期到毫秒),但更好的时序确实表明C#在使用vanilla编译器设置时更快。

好的,足够的随机猜测,时间用于某些科学。在使用现有的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的答案。

答案 2 :(得分:4)

不了解垃圾收集或控制台缓冲的答案。

可能是C ++中的计时器机制本质上存在缺陷。

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

引用:

  

虽然这会让时间更长   一致,它可以扭曲基准,   其中有一定量的旋转时间   以前用于较低的时钟频率   操作系统将处理器切换到   更高的比率。这有效果   让事情看起来像是他们需要的   比它们更多的处理器周期   通常会。

答案 3 :(得分:4)

我认为问题在于您在C ++中的计时代码。

来自__rdtsc的MS文档:

  

生成rdtsc指令,返回处理器时间戳。   处理器时间戳记录自上次复位后时钟周期的数量。

也许尝试GetTickCount()

答案 4 :(得分:3)

不是说这是问题,但您可能需要阅读How to: Use the High-Resolution Timer

另见...... http://en.wikipedia.org/wiki/Comparison_of_Java_and_C%2B%2B#Performance

  

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

它是关于Java但是开始解决C运行时和JITed运行时之间的性能问题。

答案 5 :(得分:2)

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

答案 6 :(得分: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;
};

答案 7 :(得分:0)

可能是这些方法在运行测试之前在运行时进行预先处理...或者当C ++的cout代码被缓冲时,Console是API的输出到控制台的包装器..我猜..

希望这有帮助, 最好的祝福, 汤姆。

答案 8 :(得分:0)

你在c#代码中调用静态函数,它将被内联,而在c ++中你使用非静态函数。我有~1.4秒的c ++。使用g ++ -O3,你可以拥有1.21秒。

你无法将c#与c ++与错误翻译的代码进行比较

答案 9 :(得分:-2)

如果该代码确实是执行时间的1/2,那么可能的原因是:

  • 如果上述代码中的任何地方发生了垃圾收集,那么垃圾收集会加速C#代码在C ++代码上的执行。
  • 可以缓冲写入控制台的C#(C ++可能没有,或者可能效率不高)

答案 10 :(得分:-2)

我知道.NET编译器具有Intel优化功能。

答案 11 :(得分:-2)

猜测1

垃圾收集程序可能会发挥作用。

在C ++版本中,所有内存管理都会在程序运行时内联进行,这将计入最后一次。

在.NET中,公共语言运行时(CLR)的垃圾收集器(GC)是一个不同线程上的独立进程,并且通常在程序完成后清理它。因此,您的程序将完成,在释放内存之前将打印出时间。特别是对于通常在完成之前通常不会被清理的小程序。

这完全取决于垃圾收集实现的细节(如果它以与堆相同的方式优化堆栈),但我认为这在速度增益中起着部分作用。如果C ++版本也被优化为在完成之前不释放/清理内存(或者在程序完成之后推送该步骤),那么我相信你会看到C ++的速度提升。

测试GC:要查看“延迟的”.NET GC行为,请在您的某些对象的析构函数/终结器方法中放置一个断点。程序完成后调试器将变为活动状态并触发这些断点(是的,在Main完成后)。

猜测2

否则,程序员将C#源代码编译到IL代码(Microsoft字节代码指令),并在运行时由CLR的Just-In-Time编译器编译成一个特定于处理器的指令集(与经典编译程序一样)因此,一旦.NET程序开始运行并且第一次运行,它就没有理由变慢。

答案 12 :(得分:-2)

我认为这里的每个人都错过了“秘密成分”,这就完全不同了:JIT编译器确切地知道目标体系结构是什么,而静态编译器却不知道。不同的x86处理器具有非常不同的体系结构和流水线,因此在一个CPU上尽可能快的指令序列在另一个CPU上可能相对较慢。

在这种情况下,Microsoft C ++编译器的优化策略针对的是与CPU acidzombie24实际使用的处理器不同的处理器,但gcc选择了更适合其CPU的指令。在较新的,较旧的或不同的制造商CPU上,Microsoft C ++可能比gcc更快。

JIT拥有最好的潜力:因为它确切地知道什么是CPU,所以它能够在每种情况下生成最好的代码。 因此,对于此类代码,C#本质上(从长远来看)可能比C ++更快。

话虽如此,我猜想CLR的JIT选择比Microsoft C ++更好的指令序列这一事实更多的是运气而不是了解架构。事实证明,在Justicle的CPU上,Microsoft C ++编译器选择了比CLR JIT编译器更好的指令序列。

关于_rdtsc与QueryPerformanceCounter的说明:是_rdtsc已损坏,但当您说3-4秒操作并多次运行以验证一致的时序时,任何导致_rdtsc的情况给出伪造的时间(例如处理器速度变化或处理器变化)会导致测试数据中的偏差值被抛出,所以假设acidzombie24正确地完成了他原来的基准测试,我怀疑_rdtsc与QueryPerformanceCounter问题确实有任何影响。