C# 性能能赶上编译型的 C/C++/D 和中间代码运行时解释的 java 吗?
微软发布了 .net 平台和 .net 平台支持的多种语言,包括 C#,vb.net, 受管 (managed extensions) 的 C++. 它们构成的体系能同时满足开发出高效的服务器架构,和开发部门对强壮的多功能的接口的需求.
和其他众多程序员一样,拿新开发语言与日常用到的语言作比较和选择的时候,我对新玩意总有莫名的偏见和怀疑.本篇文章将会讨论 .net 平台的优点,特别是相关的 C#. 我们拿 C# 和其他开发语言在高性能软件开发方面,作定量的性能比较.
因为 c# 程序编译运行分为两部分:先编译成中间代码,然后是中间代码的运行时解释执行,所以我们不仅会把他跟纯编译型的 C/C++/D 语言相比,也会把他与典型的 ( 中间代码运行时解释 ) 的 Java 语言作比较. Java 无疑是一个成熟的语言,应用在各个计算机领域. Java 程序先被编译成中间代码,然后运行时通过 java 虚拟机解释成本地机器码执行.由此看来, .net 平台,特别是为首的 c# 语言,借鉴了 java 的很多东西,包括语法和运行时支持.
对新语言(或者说时语言 + 对应的库 + 该语言的运行时支持)作完整公平的性能比较算是一个庞大考验.本文对 c# 与 C/C++/D 就开发中都涉及到的基本共通点作初步的性能比较,同时还包括各个语言所带的库的性能比较.包括:
1. 运行一次完整的程序(转载执行一个程序的时间)
2. 算法和递归(计算圆周率和 Eratorshnes's sieve )
3. 类型转换( int->float,float->int,int->string.string->int )
4. 字符串比较
5. 字符串连接
6.( 字符串特定符号统计 )
通过上面的一系列比较,将会揭示 c# 编译器效力的一些有意思的地方和 .net 运行库的性能
背景 :
虽然作比较的语言都可以算是 C 语言系列,虽然这五种语言在语法上只有或多或少的不同,但在其他方面,它们的差别是巨大的.(见表一, http://www.digitalmars.com/d/comparison.html ) . 我假设读者已经熟悉 C/C++/Java ,以及一些基本的 C# .对于 D 语言,不如其他语言那么有名,但你可以找到它的介绍
D 语言介绍
D 语言是 Walter Bright 正在开发的一种语言,安装和学习这种语言的开销很小。实质上, D 语言的目标是更成功的 C/C++ 语言,改进包括可变长度的数组,函数参数的 [in][out] 属性,自动内联,版本控制,局部函数,内建单元测试,严格的类型检查等等,包括 Java/C# 中实现的忽略指针和引用的差别 , 这在 C++ 中只有通过模板实现。它同时支持对现有 C 函数和 API 的直接支持。
它支持许多跟 Java/C# 类似的特性 , 比如垃圾收集。 D 语言跟 C#/Java 的本质区别是 D 语言运行时不需要运行时的虚拟机支持。 D 语言所支持的所有特性都是在编译连接时完成的,所以不需要运行时的额外支持。 D 语言的开发目前已经到了 Alpha 版本,将来的成熟版本在绝大多数情况下将会比 Java/C# 高效得多,除非 JIT 优化非常明显.所以我们才把 D 语言加到我们的测试当中。实际上, Walter 告诉我们, D 完全有能力产生跟 C 代码运行得一样快的代码,同时,由于自动内联,内建数组,字符串内建优化,非 \0 的字符串结束标记等特性,他相信在 D 语言正式发布的时候,它的性能将会超过 C 。
在真正的测试开始以前,我假定性能按照从高到低排序,应该是 C/C++,D,C#,Java .我跟其随后证实的其他好多人一样,一开始就怀疑 C# 产生高性能代码的能力.
性能比较
我们会在下面接着介绍的十一种不同情况下测试性能.测试平台是 2G 512M 的 pentium4 ,操作系统是 Windows XP Pro. 测试是在一套我自己设计的工具下进行的,为了符合这篇文章,我理所当然地用 C# 写了这个工具。在没有其他干扰进程的情况下,对每个测试者运行七次,分别去掉最快的一次和最慢的一次后得到的品均值,就是测试者的成绩。除了下面说到的 noop 测试,所有的测试通过高精确度定时器计算完成所有内循环所需时间完成。(译者注:比如 int->float 测量中,会通过循环运行多个 int->float 的转换,然后测量完成整个循环需要的时间)。如 Listing1 所示的代码,就是用 c# 完成的 f2i 测试。 (C 通过调用 win32 api 函数 QueryPerformanceCounter() ; C++ 通过 WinSTL 的 performance_counter;C# 通过 SynSoft.Performance.PerformanceCounter;D 通过 syncsoft.win32.perf.PerfomanceCounte ; Java 通过 System.currentTimeMillis()) 。每个程序都包含进入实质测试代码前一段 ” 热身代码 ” 来减少程序初始化 ,catch 等因素带来的不利影响。
编译器版本号如下:
C#:VC#.net v7.00.9466,.NET Framework v1.0.3705;
D: Digital Mars D compiler Alpha v0.61
Java: J2KDSE 1.4.1
C/C++ 在两个编译器下完成,分别是 Digital Mars C/C++ v8.33,STL Port v4.5 和 Inter C/C++ v7.0 ,头文件和库用 VC++ 6.0
用 Digital Mars 编译器的原因是:首先它能快速生成高质量代码,而且免费。其次是为了和 D 语言作比较,因为 D 语言用到相同的 C/C++ 连接器和很多 C 库。
为了作彻底的性能比较,所有的编译器都用了速度最优的优化方案。 Inter 编译选项是 -QaxW ,它提供两种代码路径,一种是专门为 Pentium4+SIMD 优化,一种是为其他 cpu 优化。具体的代码运行路径在运行时自动选择。 ( 通过对不同 C/C++ 编译器性能的分析,我认为 Inter 编译器针对他自己芯片的优化非常好,而对于 Digital Mars 的编译器编译出来的代码性能,可以代表普遍的 C/C++ 性能 )
对这几种语言作方方面面的比较是不太现实的,因为某些特定的功能并没有在这几种语言中都得到实现。比如 C 内建库里没有(字符串特定符号统计)函数。不过既然这篇文章的主题是讨论 C#, 所做的测试都是 C# 支持的,如果其他语言没有提供对应的功能,我们会提供一个自定义的实现。同时,如果某种语言对某种功能的内建支持非常差的话,我会考虑提供一个相对好一点的自定义支持。 ( 特别是对于 C/C++ ,至少有一两个地方看得出效率不高的内建库掩盖了语言本身的真实性能 )
简而言之,我不会列出每种测试在不同语言的具体实现代码,不过在这里,我会详细介绍下面几种测试情况 :
noop.noop 测试的是程序的装载时间 : 一个空的 main/Main 的执行时间。跟其它测试不一样,这个测试是在 ptime 工具下完成的 .( 可以从 http://synesis.com.au/r_systools.html) 获得,它提供了了 unix 环境下 time 工具类似的功能,同时它还能多次运行目标程序来获得平均值 ( 在我们的测试中, C/C++/D 运行 200 次 ,C# 运行 50 次, java 运行 30 次 ). 为了除去某些特定情况造成的影响,我们会多次测量 ( 译者注:指多次运行 ptime 程序, ptime 程序自身又会多次运行目标程序 ) ,并且取去掉最高值和最低值后的平均值。
f2i 这个测试把 1 个长度为 10,000 的双精度浮点值转换为 32bit 的整数。从 Listing 1 可以看出,使用伪随机数产生算法可以保证各语言比较相同的浮点数。这带来了两个有趣的情况,我们将在查看结果的时候予以讨论。
I2f 跟前一个相反,这个测试对针对具体某个函数进行的测试。程序生成 10000 个确定的伪随机整数,保证在不同语言中被转换的整数是相同的。
I2str 把整型转换成成字符串型。整型变量从 0 循环增加到 5000000 ,每次做一个整型到字符串型的转换 ( 见 listing2).C#/Java 用内建函数完成 ---i.ToString() 和 Integer.Tostring(i),C/D 用传统的 sprintf() 完成。 C++ 分为两次完成。第一次用 iostream 的 strstream 类,和预期的一样,这种情况下的性能非常差。为了提高效率,我采取了一些措施,所以导致了第二种方案,见 i2str2
Str2i. 把字符串转换成整型。建立一个字符串数组,内容是 i2str 转换过来的内容,然后再通过语言本身的支持或者库函数把字符串转换回去,计算转换回去的时间(见 listing3 ) .C/D 用 atoi 完成, c++ 用 iostream 的 strstream 完成, c# 用 int32.parse 完成, java 用 integer.parseint() 完成。
picalc 迭代和浮点。通过 listing 4 所示的迭代算法计算圆周率。每种语言的实现其迭代次数是 10,000,000 。
Picalcr. 递归。通过 listing5 所示的算法计算圆周率。对每种语言,我们会做 10000 次运算,考虑到 java 堆栈在深层地归时很容易被耗尽,所以每次运算限制在 4500 次递归以内。
Sieve. 叠代。通过 Listing 6 所示的迭代算法( Eratosthenes's sieve ( http://www.math.utah.edu/~alfeld/Eratorthenes.html ))得到质数。每种语言的实现其迭代次数是 10,000,000 。
Strcat. 字符串连接 --- 通常是大量运行时开销的根源,也是导致程序低效的潜在领域。(我以前做过一个对性能敏感的项目,通过改善效率底下的字符串连接,我获得了 10% 的整体性能提升) . 测试通过构造四种不同类型的变量 :short,int, double 和一个 string ,然后把它们连接成一个 string 变量完成,连接时添加用分号分割.在连接的时候采用四种不同的连接方
法 :1) 默认连接方法 2) 故意构造出来的导致效率低下的连接方法 3)C 语言种典型的 printf/Format 格式,需要重新分配空间 4) 为提高性能而进行的硬编码 . 代码摘录见 Listing7
Strswtch. 通过级联 switch 进行字符串比较 . 通过语言支持的 switch 语句进行比较或者通过一些列的 if-else 语句模拟( Listing 8 ).这项测试是为了检验 c# 的字符串处理功能是否如他所声称的那样高效.测试分成两部分 : 首先是进行字符串之间的赋值,也就是同一身份的比较(译者注:也就是字符串内部的指针直接复制,不涉及到字符串内容的拷贝).其次是通过字符串的拷贝后进行比较,这样就避免了同一身份比较而是进行进行字符串之间的字面比较.每种语言上都进行 850000 次叠代,因为超过这个次数后会导致 java vm 内存耗尽.
strtok 字符串分段。字符串的运行效率也极低,有时甚至引发严重的运行开销。这个测试( Listing 9)读取一个文本文件的全部内容至一个字符串,然后拆分成段。第一个变量用字符分隔符:‘;'分隔字符串,并忽略所有的空白。第二个变量也用分号分隔,但保留了空白。第三个和第四个变量用字符串分隔符:<->,并相应的忽略和保留了空白处。C里仅第二个变量用了strtok(),C++四个变量都用STLSoft的string_tokeniser模板,但各自正确给参数赋值。C#第一个变量用System.String.Split(),2至4变量用SynSoft.Text.Tokeniser.Tokenise();Java用java.util.StringTokenizer,仅支持变量2和4(注:我选用STLSoft的string_tokeniser模板不是我要为STLSoft作宣传,而是测试表明它比Boost的快2.5倍,而Boost是大多数开发人员在查找库时的首选)。
结果
在 noop测试中,表2列出了以微秒(us)为单位装载一个空操作的平均开销。显然C,C++,D的差别不合逻辑,每一个大约是5-6毫秒,而Intel确低至2ms。我所想到的唯一可能是这个编译器优化了所有的C实时运行库的初始化工作。如果是这样,这个性能在其他非试验性即实际应用中不能体现出来,因为它至少需要一些C … ,这有待深入研究。
C#用了大约70ms,Java约0.2秒。C#和Java的装载清晰的表明其实时开销在于它们的实时运行的结构基础(VM和支持DLLs)。但是除了大规模的命令行的商业化批处理,对极大型系统(上百万条代码)或CGI的基础构建,启动开销在大多数情况下不重要。重荷服务器允许在数秒内启动,所以语言间200ms的差异就微乎其微了。
其余的结果分三部分显示。图 2和3分别展示了字符串连接和分段的结果。图1包含了其余全部的测试结果,我们现在先来看看它。
图 1中,对各测试项,C,C++,D,Java的结果均以其相应测试项C#运行时间的百分比显示。因此,在100%线上下波动的结果说明与C#性能相当。高百分比说明C#性能较优,而低百分比表明C#性能相对较差。
f2i 抛开Intel不谈,可以看出C#在浮点到整型的转换中效率远远高于其余四个,仅是C,C++, 和D开销的2/3,是Java的2/5。在我让它们使用相同的随机数发生器前,不同语言的性能差异极大,这说明转换速度非常依赖具体的浮点数值,当然这是后见之明了。为了确保每个语言做相同的转换,程序计算转换值的总和,这也有助于任何过度的优化去除整个循环。可以得知这种转化在语言间正确度不同。每种语言的计算总和迭代10,000次的结果在表3中列处。它们表明(虽然无法证实)C,C++,和D是最正确的,Java其次,C#最差。C#牺牲正确性和降低处理器浮点精度(参见 引用)换取优越性能是令人信服的,但事实上做此判断前我还需要更多的证据。至于Intel,说它的性能没有什么更卓越的也是公平的。
5个语言这里效率相当。另4种性能比C#低5%以内。唯一叫人感兴趣的是这四个语言都是性能比C#略好。这表明C#确实效率低,虽然在这方面只低一点。这与从整型到浮点型转换的结果相反。同样,值得我们注意的是,虽然Java在浮点到整型的转换中性能很低,但在整型到浮点的转换中却是最好的,比C#高10%。对f2i,Intel表现极佳,其C和C++分别只用了C#时间的10%和23%。
i2str 整型到字符型的转换。 C#比C和D的printf略好。比C++的以iostream为基础的转换效率要高。它是C#运行时间的4倍(对Intel 和VC6 STL/CRT是5倍)。但Java性能出色,用了C#2/5的时间。因为C++的效率低下,第二个变量使用STLSoft的 integer_to_string<> 模板函数。它在很多编译器上比C和C++库的所有可利用的转换机制要快。在这种情况下,性能比C#高10%,比C和D高约20%。但是因为Intel编译器的优化影响, integer_to_string<> 似乎找到了其完美搭档:其时间比C#低30%,是第三个速度超过Java的,比Iostream快17倍以上。认为它很可能已经接近了转换效率的上限也是合理的。
str2i 字符型到整型的转化结果与前面大相径庭。 C和D用atoi比C#快约5-8倍,比Java快3倍,Java比C#效率高得多。但是这四个比C++以iostream为基础的转换都快。C++用 Digital Mars/STLPor运行时间t 是 C#的2.5倍,用 Intel和VC6 CRT/STL 是 10倍。这是iostream的另一个致命弱点。但因此说C++效率低下未免有失公允(实际上,我正在为 STLSoft 库写字符至整型的转换,以和它 integer_to_string<> 的杰出转换性能相匹配。最初的结果表明它将优于 C的atoi,所以这个转换中C++也是优于C的。我希望能在本文刊发前完成此转换。请参阅 http://stlsoft.org/conversion_library.html 。 )
picalc 在这里各语言性能表现相近。 C#和Java几乎相同,比C,C++效率高上10%。C#的性能可以这样解释:它的浮点数操作效率高,这可以从f2I看出。但这不能理解Java的表现。我们可以认为C#和Java都能较好的优化循环,它涉及函数调用,但这有赖于深入研究。既然性能相差10%以内,我们只能得出这样的结论:各语言在循环结构性能方面差异不显著。有趣的是,Intel的优化这里没有实际效果,可能是因为Pi值计算太简单了吧。
Picalcr : 这里的结果可以更加确定一点, C , C++ , C# 和 D 语言的性能偏差在 2% 之内,我们可以公平地说它们对于递归执行的性能是一样的。 Java 却是花费比其他四种语言多 20% 的时间,加上 JVM ( Java 虚拟机)在超过 4500 次递归时堆栈耗竭,可以明显地从速度和内存消耗方面得出 Java 处理递归不是很好。 Intel 的优化能力在这里有明显的作用(让人印象深刻的 10% ),但我在这里并没有详细分析这一点。
Sieve: 相对简单的计算质数的算法(仅包含迭代和数组访问)说明 C , C++ , C# 和 D 语言在这方面事实上是一样的(仅 0.5% 的偏差),但 Java 的性能低了 6% 。我认为这中间大有文章, C , C++ 代码不进行数组边界安全检查,而 C# 和 Java 进行数组边界检查。我对这种结果的解释是: C# 和 D 语言在 FOR 循环内能根据对数组边界的条件测试对边界检查进行优化,而 Java 不行。由于 C# 是新兴语言, D 语言还没发行,这就让人不那么失望了。即使这不是为 Java 辩解,也不会让人印象深刻了。再说, Intel 优化起了明显的作用——增长 5% 性能,正如 picalcr 例子,这相对于其他语言更让人印象深刻。
Strswtch : 这个测试对于 C# 语言不是很好。即使是 C++ 低效的字符串类操作符 = = 都比 C# 的性能快 2.5 倍 (Digital Mars) 到 5 倍( Intel VC6 STL/CRT )。 .NET 环境下字符串是内部处理的,这表示字符串存储在全局离散表中,副本可以从内存中消除,等值测试是以一致性检查的方式进行的 ( 只要两个参数都是在内部 ) 。这明显表示内部修复机制的效率严重低下或者一致性检查没有建立 C# 的 ”string-swatching” 机制性。 C# 的这种严重低效率更让人倾向于后者,因为想不到个好的理由解释为什么会这样。正如例 8 所示,变量 1 的字符串实例是文字的,都在内部(事实上,这里有个限制没有在例 8 中表示出来,在变量 1 的预循环中用来验证参数是真正在内部的)。
当在变量 2 中假装进行一致性检查,发现仅 Java 的性能有明显的提高,从 2% 到令人印象深刻的 30% 。很明显 Java 在使用一致性检查所取的性能是不切实际的,因为在真正的程序中不能限制字符串都是文字的。但这可用来说明支持内部处理和提供可编程实现访问这种机制的语言的一种性能。讽刺的是, C# 作为这五种语言之一,它的性能是最差的。
Strcat : 表 2 以微秒( μs )显示了五种语言每一种在这个例子中的四个变量的花费时间。本文没有足够的篇幅来细节描述 17 执行代码,但尽量保持例子 7 的四个变量的真实面貌。变量 1 和 2 在所有语言执行代码中涉及以堆栈为基础的字符串内存,同时 C , D 语言在第三个变量中、 C , C++ , D 语言在第四个变量中用结构内存,这在某种程度说明他们的性能优于其他变量和语言的执行代码。本例子的 C , C++ 的讨论适合于 Digital Mars 版本,因为很明显的 Intel 所带来的性能提高被 Visual C++ 运行时刻库的效率(非期望)所抵消,事实上 Intel 的每种测试的性能都比 Digital Mars 的差。
第一种测试是以默认的方式执行的,很显然 C# 和 D 语言的性能都好于其它语言。很奇怪的是 Java 的性能最差,因为 Java 被认为有能力在一条语句中把字符串序列解释成字符串构件格式(在变量 4 中手工转换)。
第二种测试,众所周知它的格式不好,透露了很多东西。它的劣性根据语言而不同,对于 C# , D 和 Java ,包含在连续的表达式中连接每一项,而不是象第一个变量在一个表达式中。对于 C ,它简单地省略了在开始多项连接重新分配之前的的内存请求。对于 C++ ,它使用 strstream 和插入操作符而不是使用 str::string 和 + ()操作符。结果显示所有语言不同程度地降低性能。 C 内存分配器的附加轮询看起来没有起多大作用,可能因为没有竞争线程造成内存碎片,所以消耗同样的内存块。 C++ 改变成 iostreams 使代码更紧凑,但没有转变数字部分,它们仅被插入到流当中,所预计的执行结果验证了这一点。 D 因为这个改变降低性能许多( 32% ), Java 更多( 60% )。很奇怪的是 C# 性能在这里仅降低很小,少于 7% 。这让人印象非常深刻,意味着可能在 .NET 的开发环境下因为 Java 字符串连接而形成的代码回查的警觉本能会衰退。
第三种测试使用 printf()/Format() 的变量不足为奇。通过使用部分 / 全部的帧内存, C , C++ 和 D 达到很好的性能。奇怪的是 C# 的性能仅是 22% ,简直不值一提。 Java 根本没有这个便利功能。(以个人的观点也是不值一提的) C#和D语言都能够在封闭的循环中,依照数组边界测试情况,优化他们的越界检查。
最大的 测试 -硬编码的性能-这个是很有趣的问题,因为 C++还是能提供超级的性能,如果能够依照问题的精确领域来编码,即使别的语言(C#,java)使用默认的或者性能优化的组件或者理念也是如此。在C#和java上使用他们各自的StringBuilders能够提供真实的效果,达到他们在这个集合中的最佳性能。然而,C#能发挥最佳效率的机制,还是不能与C/C++、D语言相比。更糟糕的是,java的最佳性能都比别的语言的最差性能要可怜,这是相当可悲的。
总之,我们说对底层直接操作的语言垂手可得地赢得那些转化率工作得很好的或者完全地在中间代码中,并且很显然地能够提供良好性能的语言用法对java是很重要的而对C#是不太重要的。请注意这一点。
Strtok . Table3显示了五种语言的每一个环节总共的时间(单位是毫秒ms)。同时他们的空间占用在这篇文章中并没有提及,但是所有语言的实现都是非常直接的并且很明显很接近在Listing9中列出的四个问题的本质。与strcat一样,基于同样的原因,使用Intel编译器的结果没有讨论。
第一个问题――分割字符,保留空格――显示C++版本是明显的胜利者,D语言要慢20%,C# 要3倍的时间。显然,这个与库的特征的关系比语言的更明显,
并且我使用 tokenizer比著名的Boost更多一些,那样的话,D语言要比C++快2倍,并且C#只慢20%。虽然如此,STLSoft的tokenizer是免费得到,我想我们应该把这个作为C++的一个胜利。(其实,template在这一点上的使用STL的basic_string作为他的值类型,这个并不因为他的良好性能而闻名,这可能引起争论-我并没有使用一个更有效率的string,就像STLSoft的frame_string.总之,我认为这是个公平比较)。
第二个问题--分割字符,去掉空格--包含所有语言的实现版本。自然地,C语言赢得了比赛,它使用了strtok()函数,这个函数在创建tokens时并不分配内存并且直接写终结符NULL到tokenized string的结尾。尽管有这些不利条件,C++的性能还是很好的,时间是C的221%,比较好的是D语言,432%。
C#和java就很差了,分别是746%和957%。这简直是不敢相信的,C#和java运行的时间是C++的3.4倍和4.3倍,这三种语言都为tokens分配了内存。我相信这个是STL模型在处理iterable sequences时比passing around arrays更有效率的很好的例子。(请注意,我们有高度的信心这是场公平的关于C++,C#和D语言的比较,我写了三种tokerizer的实现,都是公开的,可得到的.)
第三个问题--分割句子,保留空格--显示了两件事情。第一,在这三种语言在实现这个程序的代价比实现单个字符更昂贵。第二,我们发现D语言取代了C++的最佳性能的地位,领先5%左右。C#继续被甩下,大概是另外两种语言的1.8倍时间左右。
第四个问题--分割句子,去掉空格--没有任何令人意外的,C++和D拥有大概一致的性能。C#比较不错(大概2倍的性能差距)java更慢(差不多3倍时间)。对于C++,C#和D来说去掉空格比不去掉空格导致一小点的性能损失。很明显的,在C#中,这么做比其他两个的损失更大一点。很有价值的是,这些功能在C#和D中是已经实现的。由于数组在C#中是不可变的,从tokens中返回一个没有空格的数组将导致重新分配一个数组。可是,D允许指派数组的长度,这个能动态地调整数组的大小这是个很好的例子说明D语言提供更好的效率。
总的来说,考虑到性能和自由度,我们能够说 C++是胜利者,D是很接近的第二名。C拥有最佳的性能,但是只支持一种类型的tokerization。C#表现得很差,java更甚。
总结
我们可以把结果分成3种情况:这些包括语言的特性,包括库的特性,或者两者都相关。这些只与语言相关的特性显示在 f2i, i2f, picalc, picalcr, and sieve scenarios,这些在语言选择上的作用是很小的。c#看起来在总体上是最好的,但是有点让人不能信服的是它在f2i中取得的优异性能并且是因为牺牲了浮点精确度而取得效率。(这一点需要更多大检查,我将在下一篇文章中去做。)
java是明显最差的 。
当讨论intel编译器的浮点计算的深度优化效果时,它很明显地显示,c和c++能够做的比C#更好。我讨论这些并不是说这些语言必须要更好的效率,而是说他们能比其他的语言提供更好的优化和增强的机会。这个大概是与本主题完全不相干的是,C和C++比别的语言有更多的编译器;更多的竞争,更聪明的人拥有不同的策略和技术。编译器的结果的运行目标是处理器,这将导致戏剧性的效果;我们仅仅在表面上探讨了这个庞大的问题,但是我不久将在以后的文章中回到这个主题。
Strtok 是唯一一个能说是library-only的,在这一点上C#干的并不好。虽然比java快,但是比其他的语言慢2倍或更多。同样令人失望的是VisualC/C++的运行库。在这一点上包括语言和库的效果的是i2str,str2i,strcat和strtswtch,描绘的并不是很清楚。C#明显的在string的拼接这一环节上比java好得多,但是明显地比其他的差。关于尊敬的C,C++,D,只是在一些环节上领先,在另外一些环节上相当地差。
很有趣的是定制的C++的库替换加上深度优化的intel的编译器。
总之,不可能做出定量的结论。从语言的表层来看, C# 和 Java 挺简单的,但低效的库降低了速度。我觉得,当相关的库还不如语言本身的时候, C# 的确实有微弱的优势;但反过来,当库逐渐完善,超过语言本身后, C# 明显就不行了。但还是能看到一点点希望: C# 还是有可能实现预期的目标,因为库比语言更容易实现组装。但是,人们认为由于跟别的语言相比, C# 和 Java 所用的库与语言结合的更加紧密,这些库就可以作为这两种语言效率的关键所在,至少对当前的版本来说是这样的。
正如我所作出的示范(从整数到字符)以及相关提到的(从字符到整数),有人可能会批评者并不符合这篇文章的原意。
更深入的探讨
本文主要探讨了一下 C# 对 C++ 和 Java 可能造成的“威胁”。总的来说,这结果虽然不怎么鼓舞人心,却足以让人吃惊。从效率上看, C# 跟 C 以及 C++ 这些过去的同类产品(假如它们是更高效的)相比只能算是一般水平,至少在基本语言特征的比较上是这样的。从某种程度上来说, Java 也是如此。(我承认我无法证明 C 和 C++ 相对于 C# 的绝对优势。在多年前我的毕业论文分析调查期间,我认识到令人乏味的结果正和令人兴奋的结果一样生动形象,但前者花费更小的经济支出。)
上述结论对于多处理器以及(高性能的文件/网络处理系统)等来说却不一定适用。不过,这确实很好地展现出了这些语言的基本效率以及更复杂的运算所依赖的最基本的库。
就 D 发展状况来看,现在就对它进行这样的归类似乎为时过早,不过它现在的表现确实不错。 D 的效率至少是 C# 的 167% ,其实大多数情况下还不止。有些人指出,目前仅仅还处于字符时代,只需几个人就能完成一个编译器及其附带的库。我猜测, D 有可能发展成一门强大的技术。
就我个人而言,作为一个对 C# 持有偏见的人,偶然还是会对它的性能感到惊讶。我热切地期盼着能将性能的对比分析延伸到更多领域:复杂而高效的内存的应用,多线程,进程通信,特殊文件处理,手写代码开销,这些技术都能让我们对语言进行更深入的研究。能看到 Inter 公司怎样通过定制的 C Library (比如 CRunTiny ;参考 http://cruntiny.org/ )和 STL (比如 STLPort )。
鸣谢
感谢 Walter Bright 为我提供了一份言简意赅的关于 D 的介绍,并及时地指出了一些当时被我忽略的优点,使我能对测试工程进行改进。同时也要感谢 Sun Microsystems 公司的 Gary Pennington ,他为我提供了关于 Java 方面的资料。还有 Scott Patterson ,他对本文的草稿进行了大量的精简,并对全篇文章进行了细致的检查。
C# 性能能赶上编译型的 C/C++/D 和中间代码运行时解释的 java 吗?
微软发布了 .net 平台和 .net 平台支持的多种语言,包括 C#,vb.net, 受管 (managed extensions) 的 C++. 它们构成的体系能同时满足开发出高效的服务器架构,和开发部门对强壮的多功能的接口的需求.
和其他众多程序员一样,拿新开发语言与日常用到的语言作比较和选择的时候,我对新玩意总有莫名的偏见和怀疑.本篇文章将会讨论 .net 平台的优点,特别是相关的 C#. 我们拿 C# 和其他开发语言在高性能软件开发方面,作定量的性能比较.
因为 c# 程序编译运行分为两部分:先编译成中间代码,然后是中间代码的运行时解释执行,所以我们不仅会把他跟纯编译型的 C/C++/D 语言相比,也会把他与典型的 ( 中间代码运行时解释 ) 的 Java 语言作比较. Java 无疑是一个成熟的语言,应用在各个计算机领域. Java 程序先被编译成中间代码,然后运行时通过 java 虚拟机解释成本地机器码执行.由此看来, .net 平台,特别是为首的 c# 语言,借鉴了 java 的很多东西,包括语法和运行时支持.
对新语言(或者说时语言 + 对应的库 + 该语言的运行时支持)作完整公平的性能比较算是一个庞大考验.本文对 c# 与 C/C++/D 就开发中都涉及到的基本共通点作初步的性能比较,同时还包括各个语言所带的库的性能比较.包括:
1. 运行一次完整的程序(转载执行一个程序的时间)
2. 算法和递归(计算圆周率和 Eratorshnes's sieve )
3. 类型转换( int->float,float->int,int->string.string->int )
4. 字符串比较
5. 字符串连接
6.( 字符串特定符号统计 )
通过上面的一系列比较,将会揭示 c# 编译器效力的一些有意思的地方和 .net 运行库的性能
背景 :
虽然作比较的语言都可以算是 C 语言系列,虽然这五种语言在语法上只有或多或少的不同,但在其他方面,它们的差别是巨大的.(见表一, http://www.digitalmars.com/d/comparison.html ) . 我假设读者已经熟悉 C/C++/Java ,以及一些基本的 C# .对于 D 语言,不如其他语言那么有名,但你可以找到它的介绍
D 语言介绍
D 语言是 Walter Bright 正在开发的一种语言,安装和学习这种语言的开销很小。实质上, D 语言的目标是更成功的 C/C++ 语言,改进包括可变长度的数组,函数参数的 [in][out] 属性,自动内联,版本控制,局部函数,内建单元测试,严格的类型检查等等,包括 Java/C# 中实现的忽略指针和引用的差别 , 这在 C++ 中只有通过模板实现。它同时支持对现有 C 函数和 API 的直接支持。
它支持许多跟 Java/C# 类似的特性 , 比如垃圾收集。 D 语言跟 C#/Java 的本质区别是 D 语言运行时不需要运行时的虚拟机支持。 D 语言所支持的所有特性都是在编译连接时完成的,所以不需要运行时的额外支持。 D 语言的开发目前已经到了 Alpha 版本,将来的成熟版本在绝大多数情况下将会比 Java/C# 高效得多,除非 JIT 优化非常明显.所以我们才把 D 语言加到我们的测试当中。实际上, Walter 告诉我们, D 完全有能力产生跟 C 代码运行得一样快的代码,同时,由于自动内联,内建数组,字符串内建优化,非 \0 的字符串结束标记等特性,他相信在 D 语言正式发布的时候,它的性能将会超过 C 。
在真正的测试开始以前,我假定性能按照从高到低排序,应该是 C/C++,D,C#,Java .我跟其随后证实的其他好多人一样,一开始就怀疑 C# 产生高性能代码的能力.
性能比较
我们会在下面接着介绍的十一种不同情况下测试性能.测试平台是 2G 512M 的 pentium4 ,操作系统是 Windows XP Pro. 测试是在一套我自己设计的工具下进行的,为了符合这篇文章,我理所当然地用 C# 写了这个工具。在没有其他干扰进程的情况下,对每个测试者运行七次,分别去掉最快的一次和最慢的一次后得到的品均值,就是测试者的成绩。除了下面说到的 noop 测试,所有的测试通过高精确度定时器计算完成所有内循环所需时间完成。(译者注:比如 int->float 测量中,会通过循环运行多个 int->float 的转换,然后测量完成整个循环需要的时间)。如 Listing1 所示的代码,就是用 c# 完成的 f2i 测试。 (C 通过调用 win32 api 函数 QueryPerformanceCounter() ; C++ 通过 WinSTL 的 performance_counter;C# 通过 SynSoft.Performance.PerformanceCounter;D 通过 syncsoft.win32.perf.PerfomanceCounte ; Java 通过 System.currentTimeMillis()) 。每个程序都包含进入实质测试代码前一段 ” 热身代码 ” 来减少程序初始化 ,catch 等因素带来的不利影响。
编译器版本号如下:
C#:VC#.net v7.00.9466,.NET Framework v1.0.3705;
D: Digital Mars D compiler Alpha v0.61
Java: J2KDSE 1.4.1
C/C++ 在两个编译器下完成,分别是 Digital Mars C/C++ v8.33,STL Port v4.5 和 Inter C/C++ v7.0 ,头文件和库用 VC++ 6.0
用 Digital Mars 编译器的原因是:首先它能快速生成高质量代码,而且免费。其次是为了和 D 语言作比较,因为 D 语言用到相同的 C/C++ 连接器和很多 C 库。
为了作彻底的性能比较,所有的编译器都用了速度最优的优化方案。 Inter 编译选项是 -QaxW ,它提供两种代码路径,一种是专门为 Pentium4+SIMD 优化,一种是为其他 cpu 优化。具体的代码运行路径在运行时自动选择。 ( 通过对不同 C/C++ 编译器性能的分析,我认为 Inter 编译器针对他自己芯片的优化非常好,而对于 Digital Mars 的编译器编译出来的代码性能,可以代表普遍的 C/C++ 性能 )
对这几种语言作方方面面的比较是不太现实的,因为某些特定的功能并没有在这几种语言中都得到实现。比如 C 内建库里没有(字符串特定符号统计)函数。不过既然这篇文章的主题是讨论 C#, 所做的测试都是 C# 支持的,如果其他语言没有提供对应的功能,我们会提供一个自定义的实现。同时,如果某种语言对某种功能的内建支持非常差的话,我会考虑提供一个相对好一点的自定义支持。 ( 特别是对于 C/C++ ,至少有一两个地方看得出效率不高的内建库掩盖了语言本身的真实性能 )
简而言之,我不会列出每种测试在不同语言的具体实现代码,不过在这里,我会详细介绍下面几种测试情况 :
noop.noop 测试的是程序的装载时间 : 一个空的 main/Main 的执行时间。跟其它测试不一样,这个测试是在 ptime 工具下完成的 .( 可以从 http://synesis.com.au/r_systools.html) 获得,它提供了了 unix 环境下 time 工具类似的功能,同时它还能多次运行目标程序来获得平均值 ( 在我们的测试中, C/C++/D 运行 200 次 ,C# 运行 50 次, java 运行 30 次 ). 为了除去某些特定情况造成的影响,我们会多次测量 ( 译者注:指多次运行 ptime 程序, ptime 程序自身又会多次运行目标程序 ) ,并且取去掉最高值和最低值后的平均值。
f2i 这个测试把 1 个长度为 10,000 的双精度浮点值转换为 32bit 的整数。从 Listing 1 可以看出,使用伪随机数产生算法可以保证各语言比较相同的浮点数。这带来了两个有趣的情况,我们将在查看结果的时候予以讨论。
I2f 跟前一个相反,这个测试对针对具体某个函数进行的测试。程序生成 10000 个确定的伪随机整数,保证在不同语言中被转换的整数是相同的。
I2str 把整型转换成成字符串型。整型变量从 0 循环增加到 5000000 ,每次做一个整型到字符串型的转换 ( 见 listing2).C#/Java 用内建函数完成 ---i.ToString() 和 Integer.Tostring(i),C/D 用传统的 sprintf() 完成。 C++ 分为两次完成。第一次用 iostream 的 strstream 类,和预期的一样,这种情况下的性能非常差。为了提高效率,我采取了一些措施,所以导致了第二种方案,见 i2str2
Str2i. 把字符串转换成整型。建立一个字符串数组,内容是 i2str 转换过来的内容,然后再通过语言本身的支持或者库函数把字符串转换回去,计算转换回去的时间(见 listing3 ) .C/D 用 atoi 完成, c++ 用 iostream 的 strstream 完成, c# 用 int32.parse 完成, java 用 integer.parseint() 完成。
picalc 迭代和浮点。通过 listing 4 所示的迭代算法计算圆周率。每种语言的实现其迭代次数是 10,000,000 。
Picalcr. 递归。通过 listing5 所示的算法计算圆周率。对每种语言,我们会做 10000 次运算,考虑到 java 堆栈在深层地归时很容易被耗尽,所以每次运算限制在 4500 次递归以内。
Sieve. 叠代。通过 Listing 6 所示的迭代算法( Eratosthenes's sieve ( http://www.math.utah.edu/~alfeld/Eratorthenes.html ))得到质数。每种语言的实现其迭代次数是 10,000,000 。
Strcat. 字符串连接 --- 通常是大量运行时开销的根源,也是导致程序低效的潜在领域。(我以前做过一个对性能敏感的项目,通过改善效率底下的字符串连接,我获得了 10% 的整体性能提升) . 测试通过构造四种不同类型的变量 :short,int, double 和一个 string ,然后把它们连接成一个 string 变量完成,连接时添加用分号分割.在连接的时候采用四种不同的连接方
法 :1) 默认连接方法 2) 故意构造出来的导致效率低下的连接方法 3)C 语言种典型的 printf/Format 格式,需要重新分配空间 4) 为提高性能而进行的硬编码 . 代码摘录见 Listing7
Strswtch. 通过级联 switch 进行字符串比较 . 通过语言支持的 switch 语句进行比较或者通过一些列的 if-else 语句模拟( Listing 8 ).这项测试是为了检验 c# 的字符串处理功能是否如他所声称的那样高效.测试分成两部分 : 首先是进行字符串之间的赋值,也就是同一身份的比较(译者注:也就是字符串内部的指针直接复制,不涉及到字符串内容的拷贝).其次是通过字符串的拷贝后进行比较,这样就避免了同一身份比较而是进行进行字符串之间的字面比较.每种语言上都进行 850000 次叠代,因为超过这个次数后会导致 java vm 内存耗尽.
strtok 字符串分段。字符串的运行效率也极低,有时甚至引发严重的运行开销。这个测试( Listing 9)读取一个文本文件的全部内容至一个字符串,然后拆分成段。第一个变量用字符分隔符:‘;'分隔字符串,并忽略所有的空白。第二个变量也用分号分隔,但保留了空白。第三个和第四个变量用字符串分隔符:<->,并相应的忽略和保留了空白处。C里仅第二个变量用了strtok(),C++四个变量都用STLSoft的string_tokeniser模板,但各自正确给参数赋值。C#第一个变量用System.String.Split(),2至4变量用SynSoft.Text.Tokeniser.Tokenise();Java用java.util.StringTokenizer,仅支持变量2和4(注:我选用STLSoft的string_tokeniser模板不是我要为STLSoft作宣传,而是测试表明它比Boost的快2.5倍,而Boost是大多数开发人员在查找库时的首选)。
结果
在 noop测试中,表2列出了以微秒(us)为单位装载一个空操作的平均开销。显然C,C++,D的差别不合逻辑,每一个大约是5-6毫秒,而Intel确低至2ms。我所想到的唯一可能是这个编译器优化了所有的C实时运行库的初始化工作。如果是这样,这个性能在其他非试验性即实际应用中不能体现出来,因为它至少需要一些C … ,这有待深入研究。
C#用了大约70ms,Java约0.2秒。C#和Java的装载清晰的表明其实时开销在于它们的实时运行的结构基础(VM和支持DLLs)。但是除了大规模的命令行的商业化批处理,对极大型系统(上百万条代码)或CGI的基础构建,启动开销在大多数情况下不重要。重荷服务器允许在数秒内启动,所以语言间200ms的差异就微乎其微了。
其余的结果分三部分显示。图 2和3分别展示了字符串连接和分段的结果。图1包含了其余全部的测试结果,我们现在先来看看它。
图 1中,对各测试项,C,C++,D,Java的结果均以其相应测试项C#运行时间的百分比显示。因此,在100%线上下波动的结果说明与C#性能相当。高百分比说明C#性能较优,而低百分比表明C#性能相对较差。
f2i 抛开Intel不谈,可以看出C#在浮点到整型的转换中效率远远高于其余四个,仅是C,C++, 和D开销的2/3,是Java的2/5。在我让它们使用相同的随机数发生器前,不同语言的性能差异极大,这说明转换速度非常依赖具体的浮点数值,当然这是后见之明了。为了确保每个语言做相同的转换,程序计算转换值的总和,这也有助于任何过度的优化去除整个循环。可以得知这种转化在语言间正确度不同。每种语言的计算总和迭代10,000次的结果在表3中列处。它们表明(虽然无法证实)C,C++,和D是最正确的,Java其次,C#最差。C#牺牲正确性和降低处理器浮点精度(参见 引用)换取优越性能是令人信服的,但事实上做此判断前我还需要更多的证据。至于Intel,说它的性能没有什么更卓越的也是公平的。
5个语言这里效率相当。另4种性能比C#低5%以内。唯一叫人感兴趣的是这四个语言都是性能比C#略好。这表明C#确实效率低,虽然在这方面只低一点。这与从整型到浮点型转换的结果相反。同样,值得我们注意的是,虽然Java在浮点到整型的转换中性能很低,但在整型到浮点的转换中却是最好的,比C#高10%。对f2i,Intel表现极佳,其C和C++分别只用了C#时间的10%和23%。
i2str 整型到字符型的转换。 C#比C和D的printf略好。比C++的以iostream为基础的转换效率要高。它是C#运行时间的4倍(对Intel 和VC6 STL/CRT是5倍)。但Java性能出色,用了C#2/5的时间。因为C++的效率低下,第二个变量使用STLSoft的 integer_to_string<> 模板函数。它在很多编译器上比C和C++库的所有可利用的转换机制要快。在这种情况下,性能比C#高10%,比C和D高约20%。但是因为Intel编译器的优化影响, integer_to_string<> 似乎找到了其完美搭档:其时间比C#低30%,是第三个速度超过Java的,比Iostream快17倍以上。认为它很可能已经接近了转换效率的上限也是合理的。
str2i 字符型到整型的转化结果与前面大相径庭。 C和D用atoi比C#快约5-8倍,比Java快3倍,Java比C#效率高得多。但是这四个比C++以iostream为基础的转换都快。C++用 Digital Mars/STLPor运行时间t 是 C#的2.5倍,用 Intel和VC6 CRT/STL 是 10倍。这是iostream的另一个致命弱点。但因此说C++效率低下未免有失公允(实际上,我正在为 STLSoft 库写字符至整型的转换,以和它 integer_to_string<> 的杰出转换性能相匹配。最初的结果表明它将优于 C的atoi,所以这个转换中C++也是优于C的。我希望能在本文刊发前完成此转换。请参阅 http://stlsoft.org/conversion_library.html 。 )
picalc 在这里各语言性能表现相近。 C#和Java几乎相同,比C,C++效率高上10%。C#的性能可以这样解释:它的浮点数操作效率高,这可以从f2I看出。但这不能理解Java的表现。我们可以认为C#和Java都能较好的优化循环,它涉及函数调用,但这有赖于深入研究。既然性能相差10%以内,我们只能得出这样的结论:各语言在循环结构性能方面差异不显著。有趣的是,Intel的优化这里没有实际效果,可能是因为Pi值计算太简单了吧。
Picalcr : 这里的结果可以更加确定一点, C , C++ , C# 和 D 语言的性能偏差在 2% 之内,我们可以公平地说它们对于递归执行的性能是一样的。 Java 却是花费比其他四种语言多 20% 的时间,加上 JVM ( Java 虚拟机)在超过 4500 次递归时堆栈耗竭,可以明显地从速度和内存消耗方面得出 Java 处理递归不是很好。 Intel 的优化能力在这里有明显的作用(让人印象深刻的 10% ),但我在这里并没有详细分析这一点。
Sieve: 相对简单的计算质数的算法(仅包含迭代和数组访问)说明 C , C++ , C# 和 D 语言在这方面事实上是一样的(仅 0.5% 的偏差),但 Java 的性能低了 6% 。我认为这中间大有文章, C , C++ 代码不进行数组边界安全检查,而 C# 和 Java 进行数组边界检查。我对这种结果的解释是: C# 和 D 语言在 FOR 循环内能根据对数组边界的条件测试对边界检查进行优化,而 Java 不行。由于 C# 是新兴语言, D 语言还没发行,这就让人不那么失望了。即使这不是为 Java 辩解,也不会让人印象深刻了。再说, Intel 优化起了明显的作用——增长 5% 性能,正如 picalcr 例子,这相对于其他语言更让人印象深刻。
Strswtch : 这个测试对于 C# 语言不是很好。即使是 C++ 低效的字符串类操作符 = = 都比 C# 的性能快 2.5 倍 (Digital Mars) 到 5 倍( Intel VC6 STL/CRT )。 .NET 环境下字符串是内部处理的,这表示字符串存储在全局离散表中,副本可以从内存中消除,等值测试是以一致性检查的方式进行的 ( 只要两个参数都是在内部 ) 。这明显表示内部修复机制的效率严重低下或者一致性检查没有建立 C# 的 ”string-swatching” 机制性。 C# 的这种严重低效率更让人倾向于后者,因为想不到个好的理由解释为什么会这样。正如例 8 所示,变量 1 的字符串实例是文字的,都在内部(事实上,这里有个限制没有在例 8 中表示出来,在变量 1 的预循环中用来验证参数是真正在内部的)。
当在变量 2 中假装进行一致性检查,发现仅 Java 的性能有明显的提高,从 2% 到令人印象深刻的 30% 。很明显 Java 在使用一致性检查所取的性能是不切实际的,因为在真正的程序中不能限制字符串都是文字的。但这可用来说明支持内部处理和提供可编程实现访问这种机制的语言的一种性能。讽刺的是, C# 作为这五种语言之一,它的性能是最差的。
Strcat : 表 2 以微秒( μs )显示了五种语言每一种在这个例子中的四个变量的花费时间。本文没有足够的篇幅来细节描述 17 执行代码,但尽量保持例子 7 的四个变量的真实面貌。变量 1 和 2 在所有语言执行代码中涉及以堆栈为基础的字符串内存,同时 C , D 语言在第三个变量中、 C , C++ , D 语言在第四个变量中用结构内存,这在某种程度说明他们的性能优于其他变量和语言的执行代码。本例子的 C , C++ 的讨论适合于 Digital Mars 版本,因为很明显的 Intel 所带来的性能提高被 Visual C++ 运行时刻库的效率(非期望)所抵消,事实上 Intel 的每种测试的性能都比 Digital Mars 的差。
第一种测试是以默认的方式执行的,很显然 C# 和 D 语言的性能都好于其它语言。很奇怪的是 Java 的性能最差,因为 Java 被认为有能力在一条语句中把字符串序列解释成字符串构件格式(在变量 4 中手工转换)。
第二种测试,众所周知它的格式不好,透露了很多东西。它的劣性根据语言而不同,对于 C# , D 和 Java ,包含在连续的表达式中连接每一项,而不是象第一个变量在一个表达式中。对于 C ,它简单地省略了在开始多项连接重新分配之前的的内存请求。对于 C++ ,它使用 strstream 和插入操作符而不是使用 str::string 和 + ()操作符。结果显示所有语言不同程度地降低性能。 C 内存分配器的附加轮询看起来没有起多大作用,可能因为没有竞争线程造成内存碎片,所以消耗同样的内存块。 C++ 改变成 iostreams 使代码更紧凑,但没有转变数字部分,它们仅被插入到流当中,所预计的执行结果验证了这一点。 D 因为这个改变降低性能许多( 32% ), Java 更多( 60% )。很奇怪的是 C# 性能在这里仅降低很小,少于 7% 。这让人印象非常深刻,意味着可能在 .NET 的开发环境下因为 Java 字符串连接而形成的代码回查的警觉本能会衰退。
第三种测试使用 printf()/Format() 的变量不足为奇。通过使用部分 / 全部的帧内存, C , C++ 和 D 达到很好的性能。奇怪的是 C# 的性能仅是 22% ,简直不值一提。 Java 根本没有这个便利功能。(以个人的观点也是不值一提的) C#和D语言都能够在封闭的循环中,依照数组边界测试情况,优化他们的越界检查。
最大的 测试 -硬编码的性能-这个是很有趣的问题,因为 C++还是能提供超级的性能,如果能够依照问题的精确领域来编码,即使别的语言(C#,java)使用默认的或者性能优化的组件或者理念也是如此。在C#和java上使用他们各自的StringBuilders能够提供真实的效果,达到他们在这个集合中的最佳性能。然而,C#能发挥最佳效率的机制,还是不能与C/C++、D语言相比。更糟糕的是,java的最佳性能都比别的语言的最差性能要可怜,这是相当可悲的。
总之,我们说对底层直接操作的语言垂手可得地赢得那些转化率工作得很好的或者完全地在中间代码中,并且很显然地能够提供良好性能的语言用法对java是很重要的而对C#是不太重要的。请注意这一点。
Strtok . Table3显示了五种语言的每一个环节总共的时间(单位是毫秒ms)。同时他们的空间占用在这篇文章中并没有提及,但是所有语言的实现都是非常直接的并且很明显很接近在Listing9中列出的四个问题的本质。与strcat一样,基于同样的原因,使用Intel编译器的结果没有讨论。
第一个问题――分割字符,保留空格――显示C++版本是明显的胜利者,D语言要慢20%,C# 要3倍的时间。显然,这个与库的特征的关系比语言的更明显,
并且我使用 tokenizer比著名的Boost更多一些,那样的话,D语言要比C++快2倍,并且C#只慢20%。虽然如此,STLSoft的tokenizer是免费得到,我想我们应该把这个作为C++的一个胜利。(其实,template在这一点上的使用STL的basic_string作为他的值类型,这个并不因为他的良好性能而闻名,这可能引起争论-我并没有使用一个更有效率的string,就像STLSoft的frame_string.总之,我认为这是个公平比较)。
第二个问题--分割字符,去掉空格--包含所有语言的实现版本。自然地,C语言赢得了比赛,它使用了strtok()函数,这个函数在创建tokens时并不分配内存并且直接写终结符NULL到tokenized string的结尾。尽管有这些不利条件,C++的性能还是很好的,时间是C的221%,比较好的是D语言,432%。
C#和java就很差了,分别是746%和957%。这简直是不敢相信的,C#和java运行的时间是C++的3.4倍和4.3倍,这三种语言都为tokens分配了内存。我相信这个是STL模型在处理iterable sequences时比passing around arrays更有效率的很好的例子。(请注意,我们有高度的信心这是场公平的关于C++,C#和D语言的比较,我写了三种tokerizer的实现,都是公开的,可得到的.)
第三个问题--分割句子,保留空格--显示了两件事情。第一,在这三种语言在实现这个程序的代价比实现单个字符更昂贵。第二,我们发现D语言取代了C++的最佳性能的地位,领先5%左右。C#继续被甩下,大概是另外两种语言的1.8倍时间左右。
第四个问题--分割句子,去掉空格--没有任何令人意外的,C++和D拥有大概一致的性能。C#比较不错(大概2倍的性能差距)java更慢(差不多3倍时间)。对于C++,C#和D来说去掉空格比不去掉空格导致一小点的性能损失。很明显的,在C#中,这么做比其他两个的损失更大一点。很有价值的是,这些功能在C#和D中是已经实现的。由于数组在C#中是不可变的,从tokens中返回一个没有空格的数组将导致重新分配一个数组。可是,D允许指派数组的长度,这个能动态地调整数组的大小这是个很好的例子说明D语言提供更好的效率。
总的来说,考虑到性能和自由度,我们能够说 C++是胜利者,D是很接近的第二名。C拥有最佳的性能,但是只支持一种类型的tokerization。C#表现得很差,java更甚。
总结
我们可以把结果分成3种情况:这些包括语言的特性,包括库的特性,或者两者都相关。这些只与语言相关的特性显示在 f2i, i2f, picalc, picalcr, and sieve scenarios,这些在语言选择上的作用是很小的。c#看起来在总体上是最好的,但是有点让人不能信服的是它在f2i中取得的优异性能并且是因为牺牲了浮点精确度而取得效率。(这一点需要更多大检查,我将在下一篇文章中去做。)
java是明显最差的 。
当讨论intel编译器的浮点计算的深度优化效果时,它很明显地显示,c和c++能够做的比C#更好。我讨论这些并不是说这些语言必须要更好的效率,而是说他们能比其他的语言提供更好的优化和增强的机会。这个大概是与本主题完全不相干的是,C和C++比别的语言有更多的编译器;更多的竞争,更聪明的人拥有不同的策略和技术。编译器的结果的运行目标是处理器,这将导致戏剧性的效果;我们仅仅在表面上探讨了这个庞大的问题,但是我不久将在以后的文章中回到这个主题。
Strtok 是唯一一个能说是library-only的,在这一点上C#干的并不好。虽然比java快,但是比其他的语言慢2倍或更多。同样令人失望的是VisualC/C++的运行库。在这一点上包括语言和库的效果的是i2str,str2i,strcat和strtswtch,描绘的并不是很清楚。C#明显的在string的拼接这一环节上比java好得多,但是明显地比其他的差。关于尊敬的C,C++,D,只是在一些环节上领先,在另外一些环节上相当地差。
很有趣的是定制的C++的库替换加上深度优化的intel的编译器。
总之,不可能做出定量的结论。从语言的表层来看, C# 和 Java 挺简单的,但低效的库降低了速度。我觉得,当相关的库还不如语言本身的时候, C# 的确实有微弱的优势;但反过来,当库逐渐完善,超过语言本身后, C# 明显就不行了。但还是能看到一点点希望: C# 还是有可能实现预期的目标,因为库比语言更容易实现组装。但是,人们认为由于跟别的语言相比, C# 和 Java 所用的库与语言结合的更加紧密,这些库就可以作为这两种语言效率的关键所在,至少对当前的版本来说是这样的。
正如我所作出的示范(从整数到字符)以及相关提到的(从字符到整数),有人可能会批评者并不符合这篇文章的原意。
更深入的探讨
本文主要探讨了一下 C# 对 C++ 和 Java 可能造成的“威胁”。总的来说,这结果虽然不怎么鼓舞人心,却足以让人吃惊。从效率上看, C# 跟 C 以及 C++ 这些过去的同类产品(假如它们是更高效的)相比只能算是一般水平,至少在基本语言特征的比较上是这样的。从某种程度上来说, Java 也是如此。(我承认我无法证明 C 和 C++ 相对于 C# 的绝对优势。在多年前我的毕业论文分析调查期间,我认识到令人乏味的结果正和令人兴奋的结果一样生动形象,但前者花费更小的经济支出。)
上述结论对于多处理器以及(高性能的文件/网络处理系统)等来说却不一定适用。不过,这确实很好地展现出了这些语言的基本效率以及更复杂的运算所依赖的最基本的库。
就 D 发展状况来看,现在就对它进行这样的归类似乎为时过早,不过它现在的表现确实不错。 D 的效率至少是 C# 的 167% ,其实大多数情况下还不止。有些人指出,目前仅仅还处于字符时代,只需几个人就能完成一个编译器及其附带的库。我猜测, D 有可能发展成一门强大的技术。
就我个人而言,作为一个对 C# 持有偏见的人,偶然还是会对它的性能感到惊讶。我热切地期盼着能将性能的对比分析延伸到更多领域:复杂而高效的内存的应用,多线程,进程通信,特殊文件处理,手写代码开销,这些技术都能让我们对语言进行更深入的研究。能看到 Inter 公司怎样通过定制的 C Library (比如 CRunTiny ;参考 http://cruntiny.org/ )和 STL (比如 STLPort )。
鸣谢
感谢 Walter Bright 为我提供了一份言简意赅的关于 D 的介绍,并及时地指出了一些当时被我忽略的优点,使我能对测试工程进行改进。同时也要感谢 Sun Microsystems 公司的 Gary Pennington ,他为我提供了关于 Java 方面的资料。还有 Scott Patterson ,他对本文的草稿进行了大量的精简,并对全篇文章进行了细致的检查。
]]>
posted on 2005-09-15 10:23
Sung 阅读(6601)
评论(3) 编辑 收藏 所属分类:
software Development