在本书的第一部分,我讲述了C#语言的许多基本方面——包括整型、字符型和浮点型之间的转换,循环、迭代算法,以及字符串比较、拼接和 特定符号统计 。正如我所说,对一种新的语言与它的类似语言进行全面、公平的性能比较,范围将会很庞大,所以本书沿用第一个例子并只对有限的重要的部分特性做测试。这种比较,以先前的 C#及它的库与C、C++、D和Java的特性对比为基础,主要在以下几个领域:
·类机制(封装与模板)
·异常
·内存— 重点
·大型文件的随机访问
·资源获取初始化
正如我在第一部分提到的,这里所做的测试只是针对程序性能的一小部分,硬件及操作系统的配置是测试语言 /编译器/库函数的机器的典型配置:2-GHZ,512-MB,Pentium IV,Windows XP Professional。 这里我们看到的任何结果揭示了在其它环境下可能的性能,这不说明可以假设这些结果就是正确的典型例子。 最后要说明的是,当我提到“C#比Java好”等结论的时候,一定是指“在特定的测试环境的条件下”,这一点请你铭记在心。
性能评价
所有的代码还是用和第一部分一样的编译器编译。 C#代码在.NET框架 1.0.3705(可从http://microsoft.com/netframework/免费下载)下,由Visual C#.NET 编译器v7.00.9466编译。D代码由Digital Mars D 编译器 Alpha v0.62 (可从http://www.digitalmars.com/免费下载) 编译。Java代码由J2DKSE1.4.1-02 ( 可免费下载 http://java.sun.com/ ) 编译。 C和C++代码 ( 单线程 , 包括 raii , 和静态链接库 ) 由使用 STLPort v4.5的Digital Mars C/C++ v8.33 ( 可从 http://www.digitalmars.com/免费下载 ) 和使用 Visual C++ 6.0的头文件和库文件的Intel C/C++ v7.0来编译。所有这些编译器均使用最优化处理 ( set for speed ) ; Intel 用-QaxW flag [2]提供两套编码路径:一个是针对有SSE2的Pentium IV处理器,另一个是针对其他所有的处理器——在运行时选择合适的一个。(因为Intel编译器只适用于Intel处理器构造,特别是在这个测试中的Pentium IV,这编译器的编译结果必须在此基础上评价,不能在其他的处理器上进行同样的过程,比如Athlon,甚至是旧版本的Intel处理器也不行。)
后面将要描述我们测试的假想的五种例子中的性能表现。他们都用2-GHZ,512-MB,Pentium IV,Windows XP Professional的配置,并沿用在第一部分用的性能分析系统。每一种语言所编程序的调试都执行7次,同时没有其他占线过程,去掉最高速的和最低速的,纪录平均值。所有的时间信息,通过仅区分相应时间区间的高性能计时器保存下来。(C用户调用Win32 API QueryPerformanceCounter() ;C++ 用 Win-STL ' s performance_counter [4], 可在http://winstl.org/找到 ; C# 用 SynSoft.Performance.PerformanceCounter ,可在 http://synsoft.org/dotnet.html找到; D 用 synsoft.win32.perf.PerformanceCounter , 可在http://synsoft.org/d.html找到;Java 用 System.currentTimeMillis() .)每一个测试程序都包含一个启动迭代的程序,这样可以将启动或缓冲时的影响最小化。
在每一个例子中的C#,C++和D的特征: C在所有 保存 封装和 raii( 类)的地方出现,因为它没有这两种例子必需的语言特征。 raii 没有出现Java,因为Java甚至连 Resource AcquisitionIs Initialization (RAII [1])的机制也没有,所以它仅仅作为在GC-kicking的一个练习。简单的说,我不会展示所有的语言的每一个例子的所有特性,但是所有这些都包含于在线记录中(连同产生他们的Digital Mars make-compatible make-file)。
下面就对测试性能的例子进行评价 :
封装 。这个例子( Listing1)的目的是比较不同语言(除了C)在类机制上的代价,这些语言并不完全支持相对于它们所做的所有类型的一般处理。尤其是C#和Java一般不能存储和使用它们的内置类型实例,比如int,long,bool等等,同样,对于从这些实例派生的类型也不行。相反C++和D都支持模板,它可以给任何类型的一般性操作带来方便。在以后的文章里,我将详细地讲解容器。但在这个章节将不会用到它,如果提及它反而会分散对于封装意义的理解。C#和Java中未封装的变量传递ints给异或函数,封装变量传递整形值在Int32(C#)和Interger(Java)的实例中。注意Java不支持模糊的封装(这被证明是好的因为编译器将对于这种封装带来的低效给予警告),所以实例必须明确地被创建(在列表1中标记“Java ” 处可以看到)。 C++和D的运行是简单的使用模板,这就是它们的泛型机制。(在这里我不对“一切都是对象”的模板的好处作深入探讨,但是我将在这期的姊妹篇Stepanov camp中对它做详细讲解)。
异常处理 。异常的语义在各种语言之间有很大的区别。 在 C++,异常处理和调用析构函数一起沿着堆栈操作下来,然而在有垃圾回收的语言中,这些对象被丢弃,直到将来的某一时刻被收集。 这样,就无法对各种语言进行有意义的比较。所以在这个例子中,只能通过将一段 using/aware代码与另一段没有异常处理支持的相同代码进行比较。这个例子有三种测试,它们的目的就是去衡量使用和抛出异常处理的代价。第一(Listing 2)是比较使用异常处理和通过函数返回值来传递信息给调用者这两者的代价。第二(Listing 3)是评估在try-catch模块中执行的代价(尽管没有异常处理被抛出)。最后(Listing 4)就是评估遍历执行try-catch模块的代价(同样,这没有异常处理被抛出)。
Mstress。 这一个例子的目的是,在假设分配比回收优越的基础上,看看垃圾回收语言是否在高的内存利用率上有重要的非线形关系。这里存在四种测试,每一个都牵涉到分配与释放( C 和 C++)或者丢弃(C#, D, 和 Java)在一个时间段内的内存块。这些块大小是通过相同的伪随机数算法生成的(详见文章的第一部分),这能使块的大小处于1至1000之间,从而在确保每种语言在相同的环境下测试时仿效现实世界中的分配。第一种测试(Listing 5)通过一个不断循环的变量分配了1000个内存块,目的是看看频繁地分配会不会“留下”更多的碎片,这些碎片会阻碍分配的进行直到突然产生明显的GC收集。第二种测试(Listing 5)增加了1000次循环中分配的次数,以测试由不断增加的内存负担引起的性能的 损失 。 第三和第四种测试(没列出来)跟前两个相似,它们增加了另外的内存块和分配/回收的交叉活动,目的是为了模拟不断增加的内存碎片。 C使用malloc()/free(),而C++,C#,D和JAVA用new[]。
rafile 。这一个例子( Listing 6)评估各种语言从大文件中随机读取的能力。它利用某种伪随机算法(详见文章第一部分)来模拟在大文件中的查找/读取。这里用到了两个文件,一个4.2MB,一个21.1MB。在每一个查找点,四个字节被读取,并且和一个运行值进行异或运算(这样有利于证明所有的语言以相同的方式查找和读取)。有意思的是,原始的运行程序读了32位的整数,但不幸的是,JAVA用了network byte-order而不是host byte-order,这与C, C++, 和D的machine-order reading是不相容的。
Raii(类) : 这最后的例子目的是为了评估没有内建支持这个非常重要的语言特征(很不幸 C#和Java都没有)而付出的代价。此例子表达了通过资源句柄XUser 类型从XFactory资源管理器分配X资源到客户端代码(见代码段7)。在C++和D语言中,XUser 提供一个析构函数,在析构函数里分配的资源自动地回收到资源库,资源在这种情况下是个信号量,.NET和D的执行代码可从 http://synsoft.org/dotnet.html 和 http://synsoft.org/d.html 下载。C#、D和Java 提供它们自己抽象的同步机制,但我需要一个信号量去引起交叉线程的竞争,去保证同样的同步机制以便所有语言的进行生动的比较。此信号量在每种语言执行代码中都有最初可获得的五个键值。
尽管 C#的XUser 类有一个析构函数,但此析构函数仅在实例进行垃圾碎片收集时被调用。(C#使用析构函数的语法,但编译器把它翻译成 CLR (公共语言运行时刻)的 Finalize() 方法。)这种情况跟 Java一样。C# 还尽量更多地支持习惯用语(虽然它支持的还不够),这是通过使用引用声明来实现的,正如表7所示。(为了方便,“using-ed”类型必须实现 IDisposable 接口,否则声明无效),一个明显的问题是类的使用者必须承担确保语义的正确的负担,而不是类的开发者。这是奇怪的二分法:C#是面向对象的,但它依靠类的使用者对类的内部执行代码的足够理解以决定是否需要此类的直接析构。C++和D语言过程明确,由类自己决定:C++通过析构函数的执行,D提供析构或终止函数,类声明验证关键字 AUTO (没有这些,它按C#和Java的方式运行正常的垃圾碎片收集程序)。
C#语言使用 USING 声明(和 IDisposable 接口)的一个可替代方法是依靠垃圾回收。在形成这个情况之前,我假设当内存使用率达到触发阀值或者主线程空闲/等待的时候启动垃圾碎片回收。(我不知道这种误解是怎么形成的,或许只是异想天开)。当我在执行 Raii2 例子的最初版本的时候(变量没有使用 USING 声明),处理进程马上挂起。很明显,在其他线程堵塞时垃圾碎片收集没有触动,只有在内存消耗到一定程度才触动。因此,为了程序执行,我们被迫在另一个工作线程中自己剔除垃圾碎片收集。当然,这在实际编程中是非常可笑的执行策略,执行结果证明了这一点。
结论
大部分的结果是以绝对运行时间数(全部循环或每循环)或者 C#的执行时间和C,C++,D,Java分别的百分比来表达的。在后一种方式,围绕着百分刻度的结果表示C#的性能比率,高的值表示C#的性能优越,低的值表示相对低下。 Raii 的情况是个例外,在这里结果是以C++(Digital Mars)时间的百分比表示的,由于垃圾碎片收集代码的消耗,调用时间值是以对数刻度表示的。这说明在对数比例上归根于非常高的垃圾处理花费.
BOX . 从图 1中我们能看出,在合理范围里(C#语言的97%-271%)对于所有编译语言解除封装操作(就简单整形数据)成本的差别:最差的是C++(Digital Mars),成本是C#的271%,最好的是C++(Intel) 成本是C#的97%.假设执行相对比较简单的
异或 运算, C#几乎可以像 Intel那样出色,你对这个并不会感到很惊奇,但你相信C#要比C++(Digital Mars)和D模板实例快两倍.我有些惊讶C++(Digital Mars)这个相对弱的性能,特别是和D比起来它要慢得多,然而他们出自同一开发商。
显然这个封装花费是有意义的 ,相当于C++模板花费4-10倍.这个测试仅仅包括整数,相对来说可能不能完全反映特殊类型.虽然如此,我们能有把握断言这个模板轻易赢得了性能战.有趣的是就他们各自的成熟度而言,C#要比java稍好,
差距并不大 (小于4%).(我想可能由于.NET处理目标JIT3。【3】)。更有趣的是,事实上在未封装的形态下,c#明显比java快.
除了图 1以外,有人认为,不抛出的异常variants都是是比抛出的异常副本要快的(如图2). 用 c和c++,Intel完成任务要比所有其它不抛出的异常variants快.c#紧随其后。似乎c,c++,D(不考虑编译器)在异常处理上花费大致相等,符合win32异常处理机制的预期限制因素.我们能看到三种语言在抛出异常variants上的差别是比较小的,很可能的原因是由于有相关的额外的管理消耗关系 (c++规定执行破坏功能的堆栈是从捕获点到抛出点的来回).
我发现有趣的是在抛出异常variants方面c#和java的执行是有关系的.
因为这两种语言语义在异常抛出处理要胜于返回当前错误值 ,也胜于其它语言,
尤其 c语言所呈现的关系执行调用.我非常赞同Brian Kernighan和Rob Pike
(<>,Addison Wesley,1999)所说的例外仅仅在例外条件的使用,并不是必然会发生的.然而人们能容易看到为何服务器在处理巨大数量的数据的时候可能出现的例外,而经常担心性能的问题.正是如此,java以小于其它语言25%的消耗给人深刻的印象.由此我们限定win32,采用两个有代表性的差不多的c编译器来构造例外处理,虽然没有定论,但是我认为java使用了不同的机制.
那么c#的异常抛出性能如何呢?在相关条件下,相对其他语言而言,使用异常处理语句是不使用异常处理语句的21-77倍,而c#则是191倍;鉴于Java的出色表现,.net工具应该引起注意了。
Except-2. 这个图表是为了弄清楚在一个 try-catch范围内异常处理语句的执行是否耗时,如图3,大多数语言的表现同预期的一样:c、c++和D对于Digital Mars是类似的;Intel c 和c++比其它都好.c#要比java快.相比较就性能而言,
c#是很优势的,令人感兴趣的是,c#在进行异常处理是耗时比不进行时还要少.产生这种结果肯定是有原因的(这个结果是经过了多次验证的), 但由于对于c#异常处理机制我没有很深的认识,我不能做出合理的解释.同样,虽然并没有定论,对于D来说,是否进行异常处理对结果似乎并无影响!.
我认为这是因为它不用清除栈内数据, java也有大致相同的自由度,并且表现出的差异也并不大。
except-3 如图表 4所示,测试结果与预期大体相符。除了D编译器的结果中微不足道的差异,所有的语言在遍历异常处理语句的时候都比不进行是耗时要长,尽管如此,我仍然对在这两种情况下差异如此之小感到吃惊。我们通常认为使用异常处理机制会耗费时间,并且我认为遍历异常处理语句会在启动和完成上耗费大量时间;事实上,我一开始就预期两者的差异将会非常地显著。然而,在这个测试当中,两者的差异相当不明显。
但是这个结果并不难理解,导致差异如此小的原因首先在于:这次测试太过于纯粹。这次测试的所有异常处理(类型)都是 int型的。尽管并没有异常被抛出,这样做的目的在于避免在异常处理体的构造(和析构)上耗费时间和消除各编译器处理机制不同所造成的影响。其次,本次测试中没有基于框架的结构体,因为在C++中,尽管析构函数并不会真正被调用,析构函数调用方面的准备也是必须进行的,这样也会造成时间的耗费。(注意析构函数无论异常是否被抛出都会被调用。)这些耗费都会造成测试的不公平,所以我们选择了int型。注意到程序不论是因为异常抛出而中止还是正常退出,析构函数都是要被调用的。所以它仅仅做为勾子而被添加进这些额外的函数调用中。然而,我认为所有的这些因素并不是很充足,它们仅仅使我们可以从那张表里知道当不使用的时候异常的开销是非常小的。很自然,这也正确的表明了一个人对所有语言性能(perform)的期望,当然我们要给它足够的信任.
metress-1 这个测试很清楚的表明了在不使用内存分配机制的展示 (exhibit)中不断增长的对固定数量的内存快(随机大小)分配/释放的循环的反应是非线性的。事实上,在很大程度上它们从没有线性(增长)的趋势:从性能上来说并没有什么变化(见图5),除了在一些低循环(low interations)上有所不同之外.(有趣的是,那些低循环的非线性是有意义的--占到了全部命令的50%还多--当然这仅仅是对c#和java而言).不管内存分配机制是否在每次特循环结束后都立即恢复对1000个内存块的释放,或者仅是简单的把它们交给GC在以后的某个时间处理,语言/运行库总是看上去几乎完全不受这些循环执行的影响.
在这组性能相对的测试中,我们可以清楚的看到一些它们之间的不同。在使用 Digital Mars 分配方式的语言中,C和C++的表现是最好的。Visual C++的运行库比使用Digital Mars的语言低大概2.5-3倍,对于使用Digital Mars 的语言和Visaul C++运行库来说,C和C++基本上是相同的。最后,很明显,Java 比C#慢了3-4倍,而比C慢了差不多7倍.
mestress -2
就像我们从图表中看到的,在一定数目的分配释放内存的循环中内存块(随机大小)的不断增长的反应是变量的引用是非线性表现的。这正是我们所希望的,因为在每次循环中分配的内存总量是按指数方式增长.每次循环所分配的内存的大小都低于10,000块.使用Digital Mars分配方式的C 和C++的效率依然是极为优秀的。只是效率表现上低于平均值,而这里也不得不提到java,它的表现同样不好.Visual C++的运行库(C和C++都适用)相对于C#,D 和Java来说,有一个不错的开始,但它很快就降到了一个很低的水平上。
Java在每次循环是使用10,000内存块之前的表现非常有竞争力,并在这一点急剧上升。D在整个测试中都几乎有着一致表现.几乎一直都是非线性的,这与C#很接近。
如果可能的话,我都希望看到一条总够长的X轴,C#在内存溢出的情况下仍然保持每次循环不超出10,000内存块的水平并预防这种情况的出现。(D和Java似乎也做的到,但那也只是类似C#的行为一旦被发现就中止测试。)
mstress-3 和 4 从前两个测试(variants)看,除了时间的的增长,反复的交叉存取和块变量在执行上的表现并没有什么不同。曲线的走势或者说不同的语言/运行库的相对表现在这一点上没有什么明显的改变。
我认为C和C++,在使用外部free/delete的条件下,重新使用的新近释放的碎片。相反的,我很难想像出C#,D和Java 是如何使用垃圾收集机制周期性寻找每次循环所分配的内存从而尽可能的减少由内存碎片所引起的负面效应的,或者在这个跟本就没有产生碎片.除去这些不同,这两种方式的表现还是很相似的。
这只是一种理想的我们所希望的分配机制的表现 ---毕竟,那是一个很极端情况,所有内存分配都可以在给定的周期内完全返回---虽然程序的执行都达到的目的。
rafile. 这次测试中我所希望是那些语言实际 (效率)上并没有什么不同.除了C++的执行也许比其它的低上几个百分点.
图表 7中可以看出,C#的在文件的随机存取上比C(使用Digital Mars)要好,但低于C++(使用Intel和VC6的连接库),和D与Java现表现基本持平。但是C++运行库表现出令印象深刻的性能。
从所有这一系列测试来看,Intel 似乎已经能够产生性能不错的代码了。但是它的连接的运行库头文件却是Visual C++ 6.0的,这个(大概)不是由Intel编译器产生的。因比它几乎是以压倒性的性能优势超过了DMC,这主要是由于各自的Digital Mars 和微软Visual C++运行库的性能。(我承认有点吃惊,对于些测试结果正好可以反对两个卖家的名声--应该或是不应该得到的)。这也表明一个人的偏见是非常没有理由的。
另一件值的注意的事就是对不同大小文件访问的开销都非常的小。这也表明所有的语言都利用操作系统的优势很轻易的达到了相同的程度。
raII . 从图 8中我们能看出使用statement的C#的表现只有C++析构函数效果的一半。D的性能与之相当,虽然产生的代码中有错误(或者是D的Phobos运行库),当进程创建的对像超过32,000左右的时候会使该进程挂起,从而防止过多(一个以上的)通讯中的数据点被确定的使用.在这组单独的测试中我们可以看到RAII对C#和D的支持是很完善的,但并不如想象中那样优秀。如有你要做很多scoping--并且你想,也许要足够的robustaness的帮助---你最好还是选择(stay with)C++。当依赖于.NET的垃圾回收机制时,因为处理过程只是简单地被挂起,所以C#的性能比较难以测算。可是我们不希望.NET经常调用它的垃圾回收线程,但我找不到理由解释为什么回收线程不以较低优先级在内核中等待,当有垃圾对象需要被回收和主线程空闲时再进行有效的回收。当然,我在垃圾回收方面不是专家,并且可能有一种合理的理论原因说明这为什么不是一个好的处理方法。
GC(垃圾回收)即使响应的更灵敏,我们可以在它的性能结果上看出效果并不与之匹配。对应第二组数据指针的方法表明了GC每隔1ms被触发一次。
GC(垃圾回收)的方法比使用使用声明慢了近千倍,所以这不仅仅是一个技术问题的范围。自然,这跟关于这个例子的假设有关,同时跟几乎每个人都用垃圾回收机制去管理频繁争用的资源的释放这个不辩的事实有关,但我没预计到这个变量的性能是如此之差。同时这也给了读过Java/.NET书籍的C++程序员对于书籍建议(或许说依靠)使用终止函数来清除资源而感觉疑惑的一个解释。
结论
在第一篇文章,我对于不同的环节得出不同的结论,这些环节或是语言本身造成的,或是库或者二者造成的,在这里我也这么区分,因为语言特征而产生的影响的部分通常涉及:异常,封装 /模板和RAII的实现。文件存取部分可以看作直接对库函数的操作。(没有任何一种语言阻止写替换操作,虽然这样做并不是很容易)。内存管理部分受语言和内存的影响――虽然我不清楚可以用哪一种管理机制去替代C#和JAVA的缺省的内存管理机制,但是对于其他语言这是很简单的。
封装。当我们比较封装和模板时,我们可以看出模板明显比封装出色很多。我相信这不会让大家感到很惊奇,但是事实上封装消耗的系统资源是模板的十倍。自然,作为模板类库的作者( http://stlsoft.org/ ),我得出这个结论也许带有偏见,但是这些数据在表示更广泛的情况时,他们自身就说明这一点。在后面,我将说到容器和算法,我们将看到比预计更多的这样的结果。
至于有关异常情况,我想讲四点:
1.在所有的程序语言中,使用异常处理从调用的函数过程中返回,而不是保留他们作为异常事件的指示,将导致执行花费巨大的成本。
2.C#的异常处理机制相对于其他被测试的语言效率是极低的.
3.在有异常处理的上下文环境下运行(比如用try-catch的情况)对于性能没有多大影响,除了C#,它为了提高性能(实际上,我对于这种结果很不理解,并且怀疑这是.NET运行期的一个人为结果而不是C#/.NET的一般特征。)
4.交叉于异常上下文的执行(比如说进入try-catch和/或 离开try-catch)对于系统性能的影响基本上是非常小的。
Raii. C#支持的RAII例子比C++在性能方面差,虽然不是很多,但与D相比差别无几,基本一致。(这种一致指出了在处理堆分配对象的确定性析构时的基本限制)然而,从理论观点,或从易用性和/或稳健性的实践观点来看,这里还是有很大差距的。C语言缺乏机制可以解释为年代已久,并且它是一种程序语言。很遗憾,java缺乏这种机制,但是它只是可以解释为忽视了。(至今为止我们已经用java8年左右了,所以“忽视”可能也有些牵强。)C#在这方面的努力还不成熟(因为它更多地依赖类的用户而不是类的作者),很奇怪的是C#/.NET很多优点都是在Java中被看作瑕疵/遗漏的地方,比如属性,输出参数,和无符型。
Mstress. 这个内存测试的目的是证明如果频繁的内存分配加上垃圾回收机制是否会导致—— C#, D, 和Java这些包含垃圾回收的语言严重的非线形,这明显没有。这个结果可以看出所有的语言/库表现的非常合理的。这里我们可得出几个有趣的结论:
1.C语言和C++语言,提供正确的库支持,他们内存分配空间最快。毫无疑问,部分原因由于它们没有初始化字符数组的内容。根据结果,我重新对C语言进行测试,用calloc()替代malloc(),测试结果很接近C#,虽然仍然会高出5个百分点。
2.内存碎片(只要轮流存取第三和第四个变量)不会在很大程度上影响内存分配的性能,不会增加总体负担。
3.如果垃圾回收器安排在没有使用的内存区之后不执行(我假设这是可以的),这将不会对性能有太大的影响。我假设,这说明了C#是第一个会内存枯竭的语言,所以我们可以假定它们通过使用大量的内存空间在内存分配性能方便取得平衡,并且相信在现实的环境中有这种机会运行垃圾回收。
4.通常更希望体面地降低性能―就象C(Digital Mars)的方式――而不是在各个方面都有很强大的性能,然后在某些未知的阀值化为乌有:在服务器环境下,某一时刻提供一个慢点的服务比导致崩溃可能要好。由于对Digital Mars和Visual C++,C和C++的运行实际上是相同的,我们可以假定因为与通过new操作符的调用的交流而增加的成本可以忽略,并且在C和C++之间没有本质的区别。
5.C#内存分配时间会比Java快上3~4倍。
总的来说,这个结果并不象我想象的那样:我期望C和C++在中小应用比 C#,D,和Java稍微落后,但在大型应用中远远优于。真是学无止境。
Rafile . 对于文件的随机存储结果可以看出一些重点。我认为最重要的一点是仔细的选择库。之所以C++的运行效果看上去要比C和其他的语言好很多,就是因为库的原因。当把C++的性能(Intel 和VC6库)和其他语言/编译器进行比较时,用其他任何一种都很给人留下深刻的印象。自然,这边的例子是故意的,但这么大的性能差别的事实——比C#和 Java快23倍,我们可以期待在现实情况中性能的有意义的差别。(这里,再次证明了偏见是错误的:我很厌恶所有的io流,从很多方面很容易能看出它的效率很低,当然也不能说全部这样。我对于这样的性能印象很深刻)。
详细的总结了C#的性能以后,我想就我们在其他语言方面的研究发现做一个简短的介绍。
1.C处理异常事件是不错的,特别是在存储和文件(提供正确的库连接)有非常好的表现;它能提供所有我们能预期的效果,但是与后来的高级语言相比,吸引不了更多的学习者。
2.C++处理异常事件也不错,特别是在存储和文件(提供正确的库连接)有非常好的表现,而且包含模块的概念和有效的RAII;它始终得到我的钟爱,我认为它在未来的很长一段时间内都是编程的最佳选择。
3.D在异常事件处理上不是很太好,在文件处理上处于一般水平,在存储方面有比较合理的相关性能和非常不错的线性度,有模块和RAII(虽然语言本身有很多的bug产生,使得很多的处理过程被挂起);当这语言经过它的初级阶段,期望会得到不错的效果。
4.Java在封装和内存管理的性能上表现最差,不过它有好的异常事件处理,但没有一点模块和RAII的概念;它不指望会达到真正的美好,我认为C#/.NET可以衡量出它的高低,至少运行在Windows平台上 。
摘要
从第一部分开始,就给出全文的延伸的意思,从性能方面,如何能更好的用C#/.NET写出很好的软件。在第一部分里,一些结果虽然不是很值得注意,但是有很多的地方还是令人叫奇的(至少我是这样的!)。
这些研究结果展示了C#可以提供非常好的性能效果——至少在我们测试的范围内是这样——同时不能把它如同象过去把Visual Basic和一些扩展版本的Java当作一种性能差的语言来对待。对我而言,我比较注意语言运行的性能,当然,他们的表现也会超出我的预期效果。我所提出的严肃评论是从语言特征的观点和对多个编程范例的支持方面得出的。
作为一个最初使用C++的程序员,我从特定的观点得出这些比较结果。这可以从RAII和封装测试以及它们的语言解释上看的出来。在某种意义上来说,这就好比比较苹果和橙子,对于不同背景的人们可能会对我为什么如此强调模板和确定性析构感到疑惑,没有这些他们也进行的很好。
毫无疑问,模板给C++带来了非凡的革命,它能支持群组合作,或者独立运行,范例是无可比拟的。Java由于缺少模板和强制要求任何东西都是对象,而被批评了很长时间。.NET框架在这方面做法一样也很让人失望;可能由于Java缺少模板而他们可以在.NET环境下得到(他们确实达到)让更广的开发团体感到信赖并接受了它。
缺少对RAII的支持对GUIs(通用用户接口)是非常好的,这是在严格的框架内操作的软件——即使象J2EE这样精密复杂和高吞吐量的语言,和非关键的程序。但复杂的软件不必要因为它而复杂。在最好的情况下,到处都是最终模块,只要在Finalize()里函数里添加Close()方法就可以了。在最坏的情况下,滞缓或者随机的资源泄漏都会导致系统的崩溃。更甚于以上的,如果OO(面向对象)——就象C#和Java所有都是对象——是你的目的那更让我不安,第一个要失去的就是对象自己清除自己的责任,我没办法理解这个。(我知道一个非常出名的C++程序员——当然我不能告诉是谁,他告诉我当他在课程中突出RAII和模板的重点时,那些非使用C++的人们露出的滑稽表情,让他感觉好象他丢失了什么东西)
D是使用垃圾回收的语言,默认是非确定性析构,有很多地方与C#和Java相似,但是尽管如此,它还是兼容支持RAII的习惯同时具有模板。并且它是一个人写的!我不明白为什么我们对Java或者C#(.NET其它语言也是一样)印象如此深刻,即使有它们支持RAII和模板的缺点,而我们又能比较这些。有可能C#/.NET成功的原因和Java一样,有大量的,有用的库文件(C和C++应该从中学习,D应该去发展),和有一个强有力的后台。
最后,我想说对于所有的性能结果的比较分析,你必须明智地使用这些结果。我努力使自己公平,选择我相信是公平和有意义的测试。但你一定要判断的出这些例子仅代表了广大可能性的的一小部分(不是无限意义上的),它们不是实际的程序,仅仅是测试而已并且把它简化了,其中不可避免的带有我个人的某种偏见在里面。通过逐步的方法,我希望降低这些因素。但你在阅读和使用这些结果的时候要保持留意。
感谢
我要感谢 Walter Bright提供给我最新的Dv0.62版本,能够完全的测试异常事件。感谢Intel的David Hackson给我提供了C/C++编译器。还要感谢Scott Patterson帮我选择了切合实际的测试方法(他总是不断的在我烦躁、偏题、愚蠢的时候提醒我)。还要感谢Eugene Gershnik 对于我的一些断言给了我严厉的反驳,帮助我适当地注意一些防止误解的说明。
Notes and References
[1] The C++ Programming Language, Bjarne Stroustrup, Addison Wesley, 1997
[2] The Software Optimization Cookbook, Richard Gerber, Intel Press, 2002
[3] Applied Microsoft .NET Framework Programming 1 st Edition, Jeffrey Richter, Microsoft Press, 2002
[4] Described in “ Win32 Performance Measurement Options, ” Matthew Wilson, Windows Developer Network, Volume 2 Number 5, May 2003.
在本书的第一部分,我讲述了C#语言的许多基本方面——包括整型、字符型和浮点型之间的转换,循环、迭代算法,以及字符串比较、拼接和 特定符号统计 。正如我所说,对一种新的语言与它的类似语言进行全面、公平的性能比较,范围将会很庞大,所以本书沿用第一个例子并只对有限的重要的部分特性做测试。这种比较,以先前的 C#及它的库与C、C++、D和Java的特性对比为基础,主要在以下几个领域:
·类机制(封装与模板)
·异常
·内存— 重点
·大型文件的随机访问
·资源获取初始化
正如我在第一部分提到的,这里所做的测试只是针对程序性能的一小部分,硬件及操作系统的配置是测试语言 /编译器/库函数的机器的典型配置:2-GHZ,512-MB,Pentium IV,Windows XP Professional。 这里我们看到的任何结果揭示了在其它环境下可能的性能,这不说明可以假设这些结果就是正确的典型例子。 最后要说明的是,当我提到“C#比Java好”等结论的时候,一定是指“在特定的测试环境的条件下”,这一点请你铭记在心。
性能评价
所有的代码还是用和第一部分一样的编译器编译。 C#代码在.NET框架 1.0.3705(可从http://microsoft.com/netframework/免费下载)下,由Visual C#.NET 编译器v7.00.9466编译。D代码由Digital Mars D 编译器 Alpha v0.62 (可从http://www.digitalmars.com/免费下载) 编译。Java代码由J2DKSE1.4.1-02 ( 可免费下载 http://java.sun.com/ ) 编译。 C和C++代码 ( 单线程 , 包括 raii , 和静态链接库 ) 由使用 STLPort v4.5的Digital Mars C/C++ v8.33 ( 可从 http://www.digitalmars.com/免费下载 ) 和使用 Visual C++ 6.0的头文件和库文件的Intel C/C++ v7.0来编译。所有这些编译器均使用最优化处理 ( set for speed ) ; Intel 用-QaxW flag [2]提供两套编码路径:一个是针对有SSE2的Pentium IV处理器,另一个是针对其他所有的处理器——在运行时选择合适的一个。(因为Intel编译器只适用于Intel处理器构造,特别是在这个测试中的Pentium IV,这编译器的编译结果必须在此基础上评价,不能在其他的处理器上进行同样的过程,比如Athlon,甚至是旧版本的Intel处理器也不行。)
后面将要描述我们测试的假想的五种例子中的性能表现。他们都用2-GHZ,512-MB,Pentium IV,Windows XP Professional的配置,并沿用在第一部分用的性能分析系统。每一种语言所编程序的调试都执行7次,同时没有其他占线过程,去掉最高速的和最低速的,纪录平均值。所有的时间信息,通过仅区分相应时间区间的高性能计时器保存下来。(C用户调用Win32 API QueryPerformanceCounter() ;C++ 用 Win-STL ' s performance_counter [4], 可在http://winstl.org/找到 ; C# 用 SynSoft.Performance.PerformanceCounter ,可在 http://synsoft.org/dotnet.html找到; D 用 synsoft.win32.perf.PerformanceCounter , 可在http://synsoft.org/d.html找到;Java 用 System.currentTimeMillis() .)每一个测试程序都包含一个启动迭代的程序,这样可以将启动或缓冲时的影响最小化。
在每一个例子中的C#,C++和D的特征: C在所有 保存 封装和 raii( 类)的地方出现,因为它没有这两种例子必需的语言特征。 raii 没有出现Java,因为Java甚至连 Resource AcquisitionIs Initialization (RAII [1])的机制也没有,所以它仅仅作为在GC-kicking的一个练习。简单的说,我不会展示所有的语言的每一个例子的所有特性,但是所有这些都包含于在线记录中(连同产生他们的Digital Mars make-compatible make-file)。
下面就对测试性能的例子进行评价 :
封装 。这个例子( Listing1)的目的是比较不同语言(除了C)在类机制上的代价,这些语言并不完全支持相对于它们所做的所有类型的一般处理。尤其是C#和Java一般不能存储和使用它们的内置类型实例,比如int,long,bool等等,同样,对于从这些实例派生的类型也不行。相反C++和D都支持模板,它可以给任何类型的一般性操作带来方便。在以后的文章里,我将详细地讲解容器。但在这个章节将不会用到它,如果提及它反而会分散对于封装意义的理解。C#和Java中未封装的变量传递ints给异或函数,封装变量传递整形值在Int32(C#)和Interger(Java)的实例中。注意Java不支持模糊的封装(这被证明是好的因为编译器将对于这种封装带来的低效给予警告),所以实例必须明确地被创建(在列表1中标记“Java ” 处可以看到)。 C++和D的运行是简单的使用模板,这就是它们的泛型机制。(在这里我不对“一切都是对象”的模板的好处作深入探讨,但是我将在这期的姊妹篇Stepanov camp中对它做详细讲解)。
异常处理 。异常的语义在各种语言之间有很大的区别。 在 C++,异常处理和调用析构函数一起沿着堆栈操作下来,然而在有垃圾回收的语言中,这些对象被丢弃,直到将来的某一时刻被收集。 这样,就无法对各种语言进行有意义的比较。所以在这个例子中,只能通过将一段 using/aware代码与另一段没有异常处理支持的相同代码进行比较。这个例子有三种测试,它们的目的就是去衡量使用和抛出异常处理的代价。第一(Listing 2)是比较使用异常处理和通过函数返回值来传递信息给调用者这两者的代价。第二(Listing 3)是评估在try-catch模块中执行的代价(尽管没有异常处理被抛出)。最后(Listing 4)就是评估遍历执行try-catch模块的代价(同样,这没有异常处理被抛出)。
Mstress。 这一个例子的目的是,在假设分配比回收优越的基础上,看看垃圾回收语言是否在高的内存利用率上有重要的非线形关系。这里存在四种测试,每一个都牵涉到分配与释放( C 和 C++)或者丢弃(C#, D, 和 Java)在一个时间段内的内存块。这些块大小是通过相同的伪随机数算法生成的(详见文章的第一部分),这能使块的大小处于1至1000之间,从而在确保每种语言在相同的环境下测试时仿效现实世界中的分配。第一种测试(Listing 5)通过一个不断循环的变量分配了1000个内存块,目的是看看频繁地分配会不会“留下”更多的碎片,这些碎片会阻碍分配的进行直到突然产生明显的GC收集。第二种测试(Listing 5)增加了1000次循环中分配的次数,以测试由不断增加的内存负担引起的性能的 损失 。 第三和第四种测试(没列出来)跟前两个相似,它们增加了另外的内存块和分配/回收的交叉活动,目的是为了模拟不断增加的内存碎片。 C使用malloc()/free(),而C++,C#,D和JAVA用new[]。
rafile 。这一个例子( Listing 6)评估各种语言从大文件中随机读取的能力。它利用某种伪随机算法(详见文章第一部分)来模拟在大文件中的查找/读取。这里用到了两个文件,一个4.2MB,一个21.1MB。在每一个查找点,四个字节被读取,并且和一个运行值进行异或运算(这样有利于证明所有的语言以相同的方式查找和读取)。有意思的是,原始的运行程序读了32位的整数,但不幸的是,JAVA用了network byte-order而不是host byte-order,这与C, C++, 和D的machine-order reading是不相容的。
Raii(类) : 这最后的例子目的是为了评估没有内建支持这个非常重要的语言特征(很不幸 C#和Java都没有)而付出的代价。此例子表达了通过资源句柄XUser 类型从XFactory资源管理器分配X资源到客户端代码(见代码段7)。在C++和D语言中,XUser 提供一个析构函数,在析构函数里分配的资源自动地回收到资源库,资源在这种情况下是个信号量,.NET和D的执行代码可从 http://synsoft.org/dotnet.html 和 http://synsoft.org/d.html 下载。C#、D和Java 提供它们自己抽象的同步机制,但我需要一个信号量去引起交叉线程的竞争,去保证同样的同步机制以便所有语言的进行生动的比较。此信号量在每种语言执行代码中都有最初可获得的五个键值。
尽管 C#的XUser 类有一个析构函数,但此析构函数仅在实例进行垃圾碎片收集时被调用。(C#使用析构函数的语法,但编译器把它翻译成 CLR (公共语言运行时刻)的 Finalize() 方法。)这种情况跟 Java一样。C# 还尽量更多地支持习惯用语(虽然它支持的还不够),这是通过使用引用声明来实现的,正如表7所示。(为了方便,“using-ed”类型必须实现 IDisposable 接口,否则声明无效),一个明显的问题是类的使用者必须承担确保语义的正确的负担,而不是类的开发者。这是奇怪的二分法:C#是面向对象的,但它依靠类的使用者对类的内部执行代码的足够理解以决定是否需要此类的直接析构。C++和D语言过程明确,由类自己决定:C++通过析构函数的执行,D提供析构或终止函数,类声明验证关键字 AUTO (没有这些,它按C#和Java的方式运行正常的垃圾碎片收集程序)。
C#语言使用 USING 声明(和 IDisposable 接口)的一个可替代方法是依靠垃圾回收。在形成这个情况之前,我假设当内存使用率达到触发阀值或者主线程空闲/等待的时候启动垃圾碎片回收。(我不知道这种误解是怎么形成的,或许只是异想天开)。当我在执行 Raii2 例子的最初版本的时候(变量没有使用 USING 声明),处理进程马上挂起。很明显,在其他线程堵塞时垃圾碎片收集没有触动,只有在内存消耗到一定程度才触动。因此,为了程序执行,我们被迫在另一个工作线程中自己剔除垃圾碎片收集。当然,这在实际编程中是非常可笑的执行策略,执行结果证明了这一点。
结论
大部分的结果是以绝对运行时间数(全部循环或每循环)或者 C#的执行时间和C,C++,D,Java分别的百分比来表达的。在后一种方式,围绕着百分刻度的结果表示C#的性能比率,高的值表示C#的性能优越,低的值表示相对低下。 Raii 的情况是个例外,在这里结果是以C++(Digital Mars)时间的百分比表示的,由于垃圾碎片收集代码的消耗,调用时间值是以对数刻度表示的。这说明在对数比例上归根于非常高的垃圾处理花费.
BOX . 从图 1中我们能看出,在合理范围里(C#语言的97%-271%)对于所有编译语言解除封装操作(就简单整形数据)成本的差别:最差的是C++(Digital Mars),成本是C#的271%,最好的是C++(Intel) 成本是C#的97%.假设执行相对比较简单的
异或 运算, C#几乎可以像 Intel那样出色,你对这个并不会感到很惊奇,但你相信C#要比C++(Digital Mars)和D模板实例快两倍.我有些惊讶C++(Digital Mars)这个相对弱的性能,特别是和D比起来它要慢得多,然而他们出自同一开发商。
显然这个封装花费是有意义的 ,相当于C++模板花费4-10倍.这个测试仅仅包括整数,相对来说可能不能完全反映特殊类型.虽然如此,我们能有把握断言这个模板轻易赢得了性能战.有趣的是就他们各自的成熟度而言,C#要比java稍好,
差距并不大 (小于4%).(我想可能由于.NET处理目标JIT3。【3】)。更有趣的是,事实上在未封装的形态下,c#明显比java快.
除了图 1以外,有人认为,不抛出的异常variants都是是比抛出的异常副本要快的(如图2). 用 c和c++,Intel完成任务要比所有其它不抛出的异常variants快.c#紧随其后。似乎c,c++,D(不考虑编译器)在异常处理上花费大致相等,符合win32异常处理机制的预期限制因素.我们能看到三种语言在抛出异常variants上的差别是比较小的,很可能的原因是由于有相关的额外的管理消耗关系 (c++规定执行破坏功能的堆栈是从捕获点到抛出点的来回).
我发现有趣的是在抛出异常variants方面c#和java的执行是有关系的.
因为这两种语言语义在异常抛出处理要胜于返回当前错误值 ,也胜于其它语言,
尤其 c语言所呈现的关系执行调用.我非常赞同Brian Kernighan和Rob Pike
(<>,Addison Wesley,1999)所说的例外仅仅在例外条件的使用,并不是必然会发生的.然而人们能容易看到为何服务器在处理巨大数量的数据的时候可能出现的例外,而经常担心性能的问题.正是如此,java以小于其它语言25%的消耗给人深刻的印象.由此我们限定win32,采用两个有代表性的差不多的c编译器来构造例外处理,虽然没有定论,但是我认为java使用了不同的机制.
那么c#的异常抛出性能如何呢?在相关条件下,相对其他语言而言,使用异常处理语句是不使用异常处理语句的21-77倍,而c#则是191倍;鉴于Java的出色表现,.net工具应该引起注意了。
Except-2. 这个图表是为了弄清楚在一个 try-catch范围内异常处理语句的执行是否耗时,如图3,大多数语言的表现同预期的一样:c、c++和D对于Digital Mars是类似的;Intel c 和c++比其它都好.c#要比java快.相比较就性能而言,
c#是很优势的,令人感兴趣的是,c#在进行异常处理是耗时比不进行时还要少.产生这种结果肯定是有原因的(这个结果是经过了多次验证的), 但由于对于c#异常处理机制我没有很深的认识,我不能做出合理的解释.同样,虽然并没有定论,对于D来说,是否进行异常处理对结果似乎并无影响!.
我认为这是因为它不用清除栈内数据, java也有大致相同的自由度,并且表现出的差异也并不大。
except-3 如图表 4所示,测试结果与预期大体相符。除了D编译器的结果中微不足道的差异,所有的语言在遍历异常处理语句的时候都比不进行是耗时要长,尽管如此,我仍然对在这两种情况下差异如此之小感到吃惊。我们通常认为使用异常处理机制会耗费时间,并且我认为遍历异常处理语句会在启动和完成上耗费大量时间;事实上,我一开始就预期两者的差异将会非常地显著。然而,在这个测试当中,两者的差异相当不明显。
但是这个结果并不难理解,导致差异如此小的原因首先在于:这次测试太过于纯粹。这次测试的所有异常处理(类型)都是 int型的。尽管并没有异常被抛出,这样做的目的在于避免在异常处理体的构造(和析构)上耗费时间和消除各编译器处理机制不同所造成的影响。其次,本次测试中没有基于框架的结构体,因为在C++中,尽管析构函数并不会真正被调用,析构函数调用方面的准备也是必须进行的,这样也会造成时间的耗费。(注意析构函数无论异常是否被抛出都会被调用。)这些耗费都会造成测试的不公平,所以我们选择了int型。注意到程序不论是因为异常抛出而中止还是正常退出,析构函数都是要被调用的。所以它仅仅做为勾子而被添加进这些额外的函数调用中。然而,我认为所有的这些因素并不是很充足,它们仅仅使我们可以从那张表里知道当不使用的时候异常的开销是非常小的。很自然,这也正确的表明了一个人对所有语言性能(perform)的期望,当然我们要给它足够的信任.
metress-1 这个测试很清楚的表明了在不使用内存分配机制的展示 (exhibit)中不断增长的对固定数量的内存快(随机大小)分配/释放的循环的反应是非线性的。事实上,在很大程度上它们从没有线性(增长)的趋势:从性能上来说并没有什么变化(见图5),除了在一些低循环(low interations)上有所不同之外.(有趣的是,那些低循环的非线性是有意义的--占到了全部命令的50%还多--当然这仅仅是对c#和java而言).不管内存分配机制是否在每次特循环结束后都立即恢复对1000个内存块的释放,或者仅是简单的把它们交给GC在以后的某个时间处理,语言/运行库总是看上去几乎完全不受这些循环执行的影响.
在这组性能相对的测试中,我们可以清楚的看到一些它们之间的不同。在使用 Digital Mars 分配方式的语言中,C和C++的表现是最好的。Visual C++的运行库比使用Digital Mars的语言低大概2.5-3倍,对于使用Digital Mars 的语言和Visaul C++运行库来说,C和C++基本上是相同的。最后,很明显,Java 比C#慢了3-4倍,而比C慢了差不多7倍.
mestress -2
就像我们从图表中看到的,在一定数目的分配释放内存的循环中内存块(随机大小)的不断增长的反应是变量的引用是非线性表现的。这正是我们所希望的,因为在每次循环中分配的内存总量是按指数方式增长.每次循环所分配的内存的大小都低于10,000块.使用Digital Mars分配方式的C 和C++的效率依然是极为优秀的。只是效率表现上低于平均值,而这里也不得不提到java,它的表现同样不好.Visual C++的运行库(C和C++都适用)相对于C#,D 和Java来说,有一个不错的开始,但它很快就降到了一个很低的水平上。
Java在每次循环是使用10,000内存块之前的表现非常有竞争力,并在这一点急剧上升。D在整个测试中都几乎有着一致表现.几乎一直都是非线性的,这与C#很接近。
如果可能的话,我都希望看到一条总够长的X轴,C#在内存溢出的情况下仍然保持每次循环不超出10,000内存块的水平并预防这种情况的出现。(D和Java似乎也做的到,但那也只是类似C#的行为一旦被发现就中止测试。)
mstress-3 和 4 从前两个测试(variants)看,除了时间的的增长,反复的交叉存取和块变量在执行上的表现并没有什么不同。曲线的走势或者说不同的语言/运行库的相对表现在这一点上没有什么明显的改变。
我认为C和C++,在使用外部free/delete的条件下,重新使用的新近释放的碎片。相反的,我很难想像出C#,D和Java 是如何使用垃圾收集机制周期性寻找每次循环所分配的内存从而尽可能的减少由内存碎片所引起的负面效应的,或者在这个跟本就没有产生碎片.除去这些不同,这两种方式的表现还是很相似的。
这只是一种理想的我们所希望的分配机制的表现 ---毕竟,那是一个很极端情况,所有内存分配都可以在给定的周期内完全返回---虽然程序的执行都达到的目的。
rafile. 这次测试中我所希望是那些语言实际 (效率)上并没有什么不同.除了C++的执行也许比其它的低上几个百分点.
图表 7中可以看出,C#的在文件的随机存取上比C(使用Digital Mars)要好,但低于C++(使用Intel和VC6的连接库),和D与Java现表现基本持平。但是C++运行库表现出令印象深刻的性能。
从所有这一系列测试来看,Intel 似乎已经能够产生性能不错的代码了。但是它的连接的运行库头文件却是Visual C++ 6.0的,这个(大概)不是由Intel编译器产生的。因比它几乎是以压倒性的性能优势超过了DMC,这主要是由于各自的Digital Mars 和微软Visual C++运行库的性能。(我承认有点吃惊,对于些测试结果正好可以反对两个卖家的名声--应该或是不应该得到的)。这也表明一个人的偏见是非常没有理由的。
另一件值的注意的事就是对不同大小文件访问的开销都非常的小。这也表明所有的语言都利用操作系统的优势很轻易的达到了相同的程度。
raII . 从图 8中我们能看出使用statement的C#的表现只有C++析构函数效果的一半。D的性能与之相当,虽然产生的代码中有错误(或者是D的Phobos运行库),当进程创建的对像超过32,000左右的时候会使该进程挂起,从而防止过多(一个以上的)通讯中的数据点被确定的使用.在这组单独的测试中我们可以看到RAII对C#和D的支持是很完善的,但并不如想象中那样优秀。如有你要做很多scoping--并且你想,也许要足够的robustaness的帮助---你最好还是选择(stay with)C++。当依赖于.NET的垃圾回收机制时,因为处理过程只是简单地被挂起,所以C#的性能比较难以测算。可是我们不希望.NET经常调用它的垃圾回收线程,但我找不到理由解释为什么回收线程不以较低优先级在内核中等待,当有垃圾对象需要被回收和主线程空闲时再进行有效的回收。当然,我在垃圾回收方面不是专家,并且可能有一种合理的理论原因说明这为什么不是一个好的处理方法。
GC(垃圾回收)即使响应的更灵敏,我们可以在它的性能结果上看出效果并不与之匹配。对应第二组数据指针的方法表明了GC每隔1ms被触发一次。
GC(垃圾回收)的方法比使用使用声明慢了近千倍,所以这不仅仅是一个技术问题的范围。自然,这跟关于这个例子的假设有关,同时跟几乎每个人都用垃圾回收机制去管理频繁争用的资源的释放这个不辩的事实有关,但我没预计到这个变量的性能是如此之差。同时这也给了读过Java/.NET书籍的C++程序员对于书籍建议(或许说依靠)使用终止函数来清除资源而感觉疑惑的一个解释。
结论
在第一篇文章,我对于不同的环节得出不同的结论,这些环节或是语言本身造成的,或是库或者二者造成的,在这里我也这么区分,因为语言特征而产生的影响的部分通常涉及:异常,封装 /模板和RAII的实现。文件存取部分可以看作直接对库函数的操作。(没有任何一种语言阻止写替换操作,虽然这样做并不是很容易)。内存管理部分受语言和内存的影响――虽然我不清楚可以用哪一种管理机制去替代C#和JAVA的缺省的内存管理机制,但是对于其他语言这是很简单的。
封装。当我们比较封装和模板时,我们可以看出模板明显比封装出色很多。我相信这不会让大家感到很惊奇,但是事实上封装消耗的系统资源是模板的十倍。自然,作为模板类库的作者( http://stlsoft.org/ ),我得出这个结论也许带有偏见,但是这些数据在表示更广泛的情况时,他们自身就说明这一点。在后面,我将说到容器和算法,我们将看到比预计更多的这样的结果。
至于有关异常情况,我想讲四点:
1.在所有的程序语言中,使用异常处理从调用的函数过程中返回,而不是保留他们作为异常事件的指示,将导致执行花费巨大的成本。
2.C#的异常处理机制相对于其他被测试的语言效率是极低的.
3.在有异常处理的上下文环境下运行(比如用try-catch的情况)对于性能没有多大影响,除了C#,它为了提高性能(实际上,我对于这种结果很不理解,并且怀疑这是.NET运行期的一个人为结果而不是C#/.NET的一般特征。)
4.交叉于异常上下文的执行(比如说进入try-catch和/或 离开try-catch)对于系统性能的影响基本上是非常小的。
Raii. C#支持的RAII例子比C++在性能方面差,虽然不是很多,但与D相比差别无几,基本一致。(这种一致指出了在处理堆分配对象的确定性析构时的基本限制)然而,从理论观点,或从易用性和/或稳健性的实践观点来看,这里还是有很大差距的。C语言缺乏机制可以解释为年代已久,并且它是一种程序语言。很遗憾,java缺乏这种机制,但是它只是可以解释为忽视了。(至今为止我们已经用java8年左右了,所以“忽视”可能也有些牵强。)C#在这方面的努力还不成熟(因为它更多地依赖类的用户而不是类的作者),很奇怪的是C#/.NET很多优点都是在Java中被看作瑕疵/遗漏的地方,比如属性,输出参数,和无符型。
Mstress. 这个内存测试的目的是证明如果频繁的内存分配加上垃圾回收机制是否会导致—— C#, D, 和Java这些包含垃圾回收的语言严重的非线形,这明显没有。这个结果可以看出所有的语言/库表现的非常合理的。这里我们可得出几个有趣的结论:
1.C语言和C++语言,提供正确的库支持,他们内存分配空间最快。毫无疑问,部分原因由于它们没有初始化字符数组的内容。根据结果,我重新对C语言进行测试,用calloc()替代malloc(),测试结果很接近C#,虽然仍然会高出5个百分点。
2.内存碎片(只要轮流存取第三和第四个变量)不会在很大程度上影响内存分配的性能,不会增加总体负担。
3.如果垃圾回收器安排在没有使用的内存区之后不执行(我假设这是可以的),这将不会对性能有太大的影响。我假设,这说明了C#是第一个会内存枯竭的语言,所以我们可以假定它们通过使用大量的内存空间在内存分配性能方便取得平衡,并且相信在现实的环境中有这种机会运行垃圾回收。
4.通常更希望体面地降低性能―就象C(Digital Mars)的方式――而不是在各个方面都有很强大的性能,然后在某些未知的阀值化为乌有:在服务器环境下,某一时刻提供一个慢点的服务比导致崩溃可能要好。由于对Digital Mars和Visual C++,C和C++的运行实际上是相同的,我们可以假定因为与通过new操作符的调用的交流而增加的成本可以忽略,并且在C和C++之间没有本质的区别。
5.C#内存分配时间会比Java快上3~4倍。
总的来说,这个结果并不象我想象的那样:我期望C和C++在中小应用比 C#,D,和Java稍微落后,但在大型应用中远远优于。真是学无止境。
Rafile . 对于文件的随机存储结果可以看出一些重点。我认为最重要的一点是仔细的选择库。之所以C++的运行效果看上去要比C和其他的语言好很多,就是因为库的原因。当把C++的性能(Intel 和VC6库)和其他语言/编译器进行比较时,用其他任何一种都很给人留下深刻的印象。自然,这边的例子是故意的,但这么大的性能差别的事实——比C#和 Java快23倍,我们可以期待在现实情况中性能的有意义的差别。(这里,再次证明了偏见是错误的:我很厌恶所有的io流,从很多方面很容易能看出它的效率很低,当然也不能说全部这样。我对于这样的性能印象很深刻)。
详细的总结了C#的性能以后,我想就我们在其他语言方面的研究发现做一个简短的介绍。
1.C处理异常事件是不错的,特别是在存储和文件(提供正确的库连接)有非常好的表现;它能提供所有我们能预期的效果,但是与后来的高级语言相比,吸引不了更多的学习者。
2.C++处理异常事件也不错,特别是在存储和文件(提供正确的库连接)有非常好的表现,而且包含模块的概念和有效的RAII;它始终得到我的钟爱,我认为它在未来的很长一段时间内都是编程的最佳选择。
3.D在异常事件处理上不是很太好,在文件处理上处于一般水平,在存储方面有比较合理的相关性能和非常不错的线性度,有模块和RAII(虽然语言本身有很多的bug产生,使得很多的处理过程被挂起);当这语言经过它的初级阶段,期望会得到不错的效果。
4.Java在封装和内存管理的性能上表现最差,不过它有好的异常事件处理,但没有一点模块和RAII的概念;它不指望会达到真正的美好,我认为C#/.NET可以衡量出它的高低,至少运行在Windows平台上 。
摘要
从第一部分开始,就给出全文的延伸的意思,从性能方面,如何能更好的用C#/.NET写出很好的软件。在第一部分里,一些结果虽然不是很值得注意,但是有很多的地方还是令人叫奇的(至少我是这样的!)。
这些研究结果展示了C#可以提供非常好的性能效果——至少在我们测试的范围内是这样——同时不能把它如同象过去把Visual Basic和一些扩展版本的Java当作一种性能差的语言来对待。对我而言,我比较注意语言运行的性能,当然,他们的表现也会超出我的预期效果。我所提出的严肃评论是从语言特征的观点和对多个编程范例的支持方面得出的。
作为一个最初使用C++的程序员,我从特定的观点得出这些比较结果。这可以从RAII和封装测试以及它们的语言解释上看的出来。在某种意义上来说,这就好比比较苹果和橙子,对于不同背景的人们可能会对我为什么如此强调模板和确定性析构感到疑惑,没有这些他们也进行的很好。
毫无疑问,模板给C++带来了非凡的革命,它能支持群组合作,或者独立运行,范例是无可比拟的。Java由于缺少模板和强制要求任何东西都是对象,而被批评了很长时间。.NET框架在这方面做法一样也很让人失望;可能由于Java缺少模板而他们可以在.NET环境下得到(他们确实达到)让更广的开发团体感到信赖并接受了它。
缺少对RAII的支持对GUIs(通用用户接口)是非常好的,这是在严格的框架内操作的软件——即使象J2EE这样精密复杂和高吞吐量的语言,和非关键的程序。但复杂的软件不必要因为它而复杂。在最好的情况下,到处都是最终模块,只要在Finalize()里函数里添加Close()方法就可以了。在最坏的情况下,滞缓或者随机的资源泄漏都会导致系统的崩溃。更甚于以上的,如果OO(面向对象)——就象C#和Java所有都是对象——是你的目的那更让我不安,第一个要失去的就是对象自己清除自己的责任,我没办法理解这个。(我知道一个非常出名的C++程序员——当然我不能告诉是谁,他告诉我当他在课程中突出RAII和模板的重点时,那些非使用C++的人们露出的滑稽表情,让他感觉好象他丢失了什么东西)
D是使用垃圾回收的语言,默认是非确定性析构,有很多地方与C#和Java相似,但是尽管如此,它还是兼容支持RAII的习惯同时具有模板。并且它是一个人写的!我不明白为什么我们对Java或者C#(.NET其它语言也是一样)印象如此深刻,即使有它们支持RAII和模板的缺点,而我们又能比较这些。有可能C#/.NET成功的原因和Java一样,有大量的,有用的库文件(C和C++应该从中学习,D应该去发展),和有一个强有力的后台。
最后,我想说对于所有的性能结果的比较分析,你必须明智地使用这些结果。我努力使自己公平,选择我相信是公平和有意义的测试。但你一定要判断的出这些例子仅代表了广大可能性的的一小部分(不是无限意义上的),它们不是实际的程序,仅仅是测试而已并且把它简化了,其中不可避免的带有我个人的某种偏见在里面。通过逐步的方法,我希望降低这些因素。但你在阅读和使用这些结果的时候要保持留意。
感谢
我要感谢 Walter Bright提供给我最新的Dv0.62版本,能够完全的测试异常事件。感谢Intel的David Hackson给我提供了C/C++编译器。还要感谢Scott Patterson帮我选择了切合实际的测试方法(他总是不断的在我烦躁、偏题、愚蠢的时候提醒我)。还要感谢Eugene Gershnik 对于我的一些断言给了我严厉的反驳,帮助我适当地注意一些防止误解的说明。
Notes and References
[1] The C++ Programming Language, Bjarne Stroustrup, Addison Wesley, 1997
[2] The Software Optimization Cookbook, Richard Gerber, Intel Press, 2002
[3] Applied Microsoft .NET Framework Programming 1 st Edition, Jeffrey Richter, Microsoft Press, 2002
[4] Described in “ Win32 Performance Measurement Options, ” Matthew Wilson, Windows Developer Network, Volume 2 Number 5, May 2003.
]]>
posted on 2005-09-15 10:24
Sung 阅读(3579)
评论(2) 编辑 收藏 所属分类:
software Development