|
发表于 2004-11-9 10:01:00
|
显示全部楼层
Re:选择C#的理由
结论
大部分的结果是以绝对运行时间数(全部循环或每循环)或者 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 对于我的一些断言给了我严厉的反驳,帮助我适当地注意一些防止误解的说明。
|
|