Java最新的1.5SE (市场名字是J2SE 5.0,代号Tiger) 和微软的.NET 2.0现在都已经浮出水面了,.NET 2.0虽然离最后的发布还有一些日子,但是它的基本框架我们已经可以感受到了,所以对Java 1.5和.NET 2.0的Beta版进行评测还是可以比较准确地看出这两种技术的总体情况和技术特色的。今天我要讨论的是Java 1.5和.NET 2.0中Generics。
Generics对于Java和.NET来说,都是非常重要的一个新的语言特色。熟悉C++中Template的朋友可能对这个概念不会陌生。从本质上来说,Generics就是使你的类型(Type)具有参数能力,即所谓的参数化类型或参数式多态性。用.NET和C# 设计师Anders Hejlsberg的话来说就是"Generics is essentially the ability to have type parameters on your type. They are also called parameterized types or parametric polymorphism."从理论上说, Generics应该具有这样四个突出的优点。
· 类型安全
举个最简单的例子来说吧。在以前的Java或是.NET中,如果我们往ArrayList里加入一个任何一种对象,ArrayList都将这个对象视为最宽泛的Object类型。在取出对象使用的时候,我们必须要先转化为相应的对象(Casting),这个类型转换操作是一个无法避免的开销,并且也是一个没有类型安全保障的操作。因为你取出的对象理论上可以是任何类型。很多情况下,我们希望某个ArrayList是用来专门存放某一种对象,比如说是String对象的。但是编译器没法保障我们的这个目的。别人可以向这个ArrayList加入任何对象。在编译的时候,编译器认为这都是合法的操作而给与编译通过。但是这样的程序在运行的时候就会出现问题。因为你从这个ArrayList里取出的不一定是String对象。你在转化为String类型的时候就会出现异常(Exception)。
使用了Generics,这个问题就可以避免。比如在Java 1.5中,你可以这样使用ArrayList。
ArrayList myList = new ArrayList ();
这样的话,编译器在编译的时候就会发现一切可能的类型匹配问题。试图向这个ArrayList加入其它非String对象的操作都会被视为非法的。这样保证加入的对象一定是String 类型的。而从这个ArrayList取出的对象也一定是String类型的,你不需要再进行类型转化了。
· 二进制代码重用性
有人可能会说了,对于你上面所说的问题,不用Generics,我一样有办法解决。比如对ArryList进行包装(Wrap-up)或是从ArryList派生出一个新的类,在这个新的类中改变函数的参数类型,从而保证只有String对象可以被加入。
这样做是可以的,但是问题也是非常明显的。为了String类型,你创建了一个新的类型,那么对于Integer,Double,DateTime以及用户自定义的类型呢?你对每一个类型都要创建一个新的专门的类,这是非常繁琐和容易出错的。这些新的类型功能是类似的,功能类似的函数一次又一次的被定义和实现,这是不符合面向对象编程原则的。
使用Generics,这些问题就都可以避免。你的算法可以最大限度的得到重用。如果需要改动的话,那么你也只需要修改一个地方。
· 性能提高
使用Generics,类型安全问题在编译的时候就决定了,而不是在运行时。另外我们还避免了类型转化的开销(Casting Overhead)以及包装和解包装的开销(Boxing and Unboxing Overhead),这使得程序性能有了很大提高。有趣的是,Java和.NET由于实现上的不同,在性能问题上有巨大的区别,后文中会详细讨论。
· 语义清晰
使用Generics,我们的程序变得更清晰和明确。有潜在类型安全的问题的程序将不能被编译通过。在.NET中,我们还可以对Generics加以限制(Constraints),这可以进一步明确程序的意图和目的,减少歧义性。
那么,Java和.NET的Generics实现这些承诺了吗?我们可以通过下面这个简单的程序来看个究竟。
这个程序非常简单,就是事先准备三个数组,这三个数组分别存放整数,双精度浮点数和String,然后我们来将这些东西分别放入使用了Generics的Collection对象和没有使用Generics的对象中,然后再取出,看看他们到底表现如何。
Java 1.5 程序(使用支持Generics的NetBeans 4.0 IDE)
下一篇我们将会介绍.NET 2.0 程序(使用支持Generics的Visual Studio 2005 Beta1 IDE)。(T111)
.NET 2.0 程序(使用支持Generics的Visual Studio 2005 Beta1 IDE)
上面这两段程序几乎完全一样。有几点情况在这里要说明一下。
1. Java和.NET都提供了Generics的LinkedList,所以我们可以直接对比这两者的性能。
2. Java和.NET都提供了非Generics的ArrayList,即传统的ArrayList,所以我们可以直接对比这两者的性能。
3. Java提供了Generics和非Generics的ArrayList,所以我们可以比较使用了Generics后性能的提升。这是我们今天测试的重点之一。
4. .NET提供了非Generics的ArrayList,但是很遗憾没有Generics的ArrayList。按照微软的说法是,我们可以使用相对应的Generics List类,它和传统的ArrayList功能和意图非常相似。对比.NET中非Generics的ArrayList和Generics List性能上的差异是我们今天测试的另一个重点。
5. 由于Java的Hotspot工作特性,所以我对测试有一个外部循环,这样做的目的是使Java的Hotspot有机会对程序进行优化,从而达到最佳性能。
下面是测试的结果。这里给出两组结果,分别是数据量为100000,循环次数为100和数据量为500000,循环次数为20。
从上面这个图表中,我们可以得出以下结论:
1. 在所有的测试项目中,.NET 2.0的性能都要比Java 1.5的性能高出许多。为什么会有这样的结果不是我们今天要讨论的话题。我们关注的是Generics给这两种技术带来的好处。
2. 在我们前面列举的Generics的四个特点中,类型安全,二进制代码重用以及语义清晰这三个特点在Java和.NET中都得到了很好地体现。
3. 使用了Generics,Java并没有得到什么性能提升。最直接的表现就是Generics和非Generics的ArrayList在性能上几乎没有什么差异,无论是基础的数据类型,如整数,浮点数,还是参考型String对象。这一点可能会出乎很多人的意料之外。
4. 使用了Generics,.NET的性能有了提升。尤其是整数,双精度浮点数这些数值类型(Value Type),性能成倍的提高。对于参考类型(Reference Type),性能也有比较明显的提高。
下一篇让我们深入到它们的内部,看看他们内部工作的机理各是什么样的。(T111)
同样是Generics,为什么两者在性能上会有这么大的区别呢?现在让我们深入到它们的内部,看看他们内部工作的机理各是什么样的。
首先,然我们用反编译工具看看两者生成的Code到底是什么样的。(本文使用了DJ Java Decompiler和Lutz Roeder's Reflector.exe,如图所示)
比如对于int和String段的测试,反编译Java class 的 bytecode我们得到这样的程序(double和int类似,为了节省篇幅,这里就不具体列出了)。
反编译.NET可执行文件,我们得到这样的程序。
这些程序反映出什么问题呢?
Java 1.5虽然支持Generics,对于开发人员来说,我们确实感受到了Generics带来的诸如类型安全等等好处。但是对于Java虚拟机来说,bytecode还是以前的bytecode,没有任何的变化。如果是整数,浮点数这些基本数据类型(primary data type),Java还是先将它们转化成了相应的类的实例,即Integer,Double类的对象。在使用的时候,还必须从Object类型转换成相应的类型。也就是说,Java的Generics只是一种"障眼法",在编译的时候,编译器把我们以前手工写的包装/解包装和类型转换代码自动地加进去了。所以对于具体运行的程序而言,和以前没有使用Generics的程序没有任何的变化。我们没有看到任何性能上的提升,原因就在于此。
.NET 2.0是一种全新的Generics设计。我们得到了Generics承诺的所有好处。按照微软的说法,对于整数,浮点数这些数值类型(Value Type),由于Generics避免了包装/解包装操作这个不小的开销,所以性能会有2~3倍的提高;而对于String以及其它的参考类型(Reference Type),由于避免的类型转换,性能会有20%左右的提高。我们今天的测试基本证明了这一说法。
看到这里,你可能不禁会问,同样是Generics,为什么Sun和Microsoft会有截然不同的做法呢?
Sun的Generics是起源于一个叫做"比萨饼(Pizza)"的项目。这个项目的设计原则就是使加了Generics后的Java程序可以在以前的Java虚拟机(Java Virtual Machine)上运行,而无需对Java虚拟机进行任何改动。在这种指导思想下,Generics的工作实际上就落在了编译器的身上。由编译器在编译的时候进行类型安全检查。对于通过语法检查的程序,自动加入包装/解包装操作以及类型转换操作这样的程序代码。这样编译生成的bytecode和以前没有任何不同,Java虚拟机在执行这些bytecode的时候,根本不知道还有Generics曾经发生过。这种做法的好处是显而易见的,那就是简单,易于实现,对以前的Java虚拟机有很好的兼容性。但是其缺点也是非常突兀的,那就是从核心上牺牲了Generics的精髓,我们没能感受到Generics应该带来的性能上的提高。
.NET的Generics是全新设计的,从根本上体现了Generics的精髓。语法上的保障是在编译器层次实现的。但是编译器除了应有的静态检查外和加入一些Meta信息外,并不做更多的工作(从我们反编译的程序中可以看出这一点)。具体的工作是由.NET的公共语言运行时(CLR,Common Language Runtime)来完成的。在你在第一次使用它的时候,比如说是List ,CLR会让JIT(Just-in-time-compiler)动态的生成这样的一个专门的int类List的本地机器代码。这个代码会被保存起来,以后类似的请求(List )就可以重复使用这个已经生成的类代码。出于性能上的考虑,所有程序中申明并且使用的Value Type都会有其相应的动态类生成。比如CLR会为我们的例程生成double和int两个专用List类。为了减小程序在运行时的膨胀(Code expansion)和增加代码重用性,所有的参考类(Reference type,比如我们使用的String)共享一个专门的类,也就是说所有的参考类只有一套本地机器代码。因为参考类的本质就是一个指针,这个共性使它们可以共享同样的代码。但是这些参考类有分开的VTable,这样的做法避免了类型转换的需要。
.NET这种设计是全新的,我们感受到了它的威力。但是这个新增的Generics将不兼容以前的CLR。以前的CLR(即.NET 1.0版和1.1版)无法处理这种Generics,这是一个需要指出的问题。但就我个人感觉而言,牺牲这个兼容性是值得的。
最后要说明的一点就是.NET 2.0在总体上的性能比Java 1.5要高出很多,这点非常出乎我的意料。包括程序中用到的那个非常简单的函数prepareData() 两者都有巨大的性能差距。记得在.NET 1.0Beta的时候,我对比过.NET和当时Java的性能。在那个时候,他们还是非常相近的。当然了,要全面评价.NET 2.0和 Java 1.5性能上的优劣需要更全面,更系统的测试,本文目的不在于此,所以就不多加评判了。
Generics对于Java和.NET来说,都是非常重要的一个新的语言特色。熟悉C++中Template的朋友可能对这个概念不会陌生。从本质上来说,Generics就是使你的类型(Type)具有参数能力,即所谓的参数化类型或参数式多态性。用.NET和C# 设计师Anders Hejlsberg的话来说就是"Generics is essentially the ability to have type parameters on your type. They are also called parameterized types or parametric polymorphism."从理论上说, Generics应该具有这样四个突出的优点。
· 类型安全
举个最简单的例子来说吧。在以前的Java或是.NET中,如果我们往ArrayList里加入一个任何一种对象,ArrayList都将这个对象视为最宽泛的Object类型。在取出对象使用的时候,我们必须要先转化为相应的对象(Casting),这个类型转换操作是一个无法避免的开销,并且也是一个没有类型安全保障的操作。因为你取出的对象理论上可以是任何类型。很多情况下,我们希望某个ArrayList是用来专门存放某一种对象,比如说是String对象的。但是编译器没法保障我们的这个目的。别人可以向这个ArrayList加入任何对象。在编译的时候,编译器认为这都是合法的操作而给与编译通过。但是这样的程序在运行的时候就会出现问题。因为你从这个ArrayList里取出的不一定是String对象。你在转化为String类型的时候就会出现异常(Exception)。
使用了Generics,这个问题就可以避免。比如在Java 1.5中,你可以这样使用ArrayList。
ArrayList myList = new ArrayList ();
这样的话,编译器在编译的时候就会发现一切可能的类型匹配问题。试图向这个ArrayList加入其它非String对象的操作都会被视为非法的。这样保证加入的对象一定是String 类型的。而从这个ArrayList取出的对象也一定是String类型的,你不需要再进行类型转化了。
· 二进制代码重用性
有人可能会说了,对于你上面所说的问题,不用Generics,我一样有办法解决。比如对ArryList进行包装(Wrap-up)或是从ArryList派生出一个新的类,在这个新的类中改变函数的参数类型,从而保证只有String对象可以被加入。
这样做是可以的,但是问题也是非常明显的。为了String类型,你创建了一个新的类型,那么对于Integer,Double,DateTime以及用户自定义的类型呢?你对每一个类型都要创建一个新的专门的类,这是非常繁琐和容易出错的。这些新的类型功能是类似的,功能类似的函数一次又一次的被定义和实现,这是不符合面向对象编程原则的。
使用Generics,这些问题就都可以避免。你的算法可以最大限度的得到重用。如果需要改动的话,那么你也只需要修改一个地方。
· 性能提高
使用Generics,类型安全问题在编译的时候就决定了,而不是在运行时。另外我们还避免了类型转化的开销(Casting Overhead)以及包装和解包装的开销(Boxing and Unboxing Overhead),这使得程序性能有了很大提高。有趣的是,Java和.NET由于实现上的不同,在性能问题上有巨大的区别,后文中会详细讨论。
· 语义清晰
使用Generics,我们的程序变得更清晰和明确。有潜在类型安全的问题的程序将不能被编译通过。在.NET中,我们还可以对Generics加以限制(Constraints),这可以进一步明确程序的意图和目的,减少歧义性。
那么,Java和.NET的Generics实现这些承诺了吗?我们可以通过下面这个简单的程序来看个究竟。
这个程序非常简单,就是事先准备三个数组,这三个数组分别存放整数,双精度浮点数和String,然后我们来将这些东西分别放入使用了Generics的Collection对象和没有使用Generics的对象中,然后再取出,看看他们到底表现如何。
Java 1.5 程序(使用支持Generics的NetBeans 4.0 IDE)
import java.util.LinkedList; import java.util.ArrayList; import java.util.List; import java.util.Vector; /** * @author Jinsong Zhang */ public class JavaGenerics { private int[] m_testInt= null; private double[] m_testDouble = null; private String[] m_testString = null; private int m_dataSize = 0; private int m_loopNum = 0; public JavaGenerics(int dataSize, int loopNum) { this.m_dataSize = dataSize; this.m_loopNum = loopNum; prepareData(m_dataSize); } private void prepareData(int dataSize) { m_testInt = new int[dataSize]; m_testDouble = new double[dataSize]; m_testString = new String[dataSize]; for(int i=0; i<dataSize; i++){ m_testInt[i] = i; m_testDouble[i] = i * 1.0; m_testString[i] = Double.toString(m_testDouble[i]); } } private void testIntA(){ long startTime = System.currentTimeMillis(); long totalInt = 0; for(int num=0;num<this.m_loopNum;num++){ LinkedList<Integer> iList = new LinkedList<Integer>(); for(int i=0;i<m_dataSize; i++) iList.add(m_testInt[i]); for(Integer i:iList) totalInt += i; } long time = System.currentTimeMillis()-startTime; System.out.println("Using Generics LinkedList Takes Time: " + time + " millseconds. Result: " + totalInt); } private void testIntB(){ long startTime = System.currentTimeMillis(); long totalInt = 0; for(int num=0;num<this.m_loopNum;num++){ List<Integer> iList= new ArrayList<Integer>(); for(int i=0;i<m_dataSize; i++) iList.add(m_testInt[i]); for(Integer i:iList) totalInt += i; } long time = System.currentTimeMillis()-startTime; System.out.println("Using Generics ArrayList Takes Time: " + time + " millseconds. Result: " + totalInt); } private void testIntC(){ long startTime = System.currentTimeMillis(); long totalInt = 0; for(int num=0;num<this.m_loopNum;num++){ ArrayList iList = new ArrayList(); for(int i=0;i<m_dataSize; i++) iList.add(m_testInt[i]); for(Object i:iList) totalInt += ((Integer)i).intValue(); } long time = System.currentTimeMillis()-startTime; System.out.println("Using Regular ArrayList Takes Time: " + time + " millseconds. Result: " + totalInt); } private void testDoubleA(){ long startTime = System.currentTimeMillis(); double totalDouble = 0; for(int num=0;num<this.m_loopNum;num++){ LinkedList<Double> dList = new LinkedList<Double>(); for(int i=0;i<m_dataSize;i++) dList.add(m_testDouble[i]); for(Double d :dList) totalDouble += d.doubleValue(); } long time = System.currentTimeMillis()-startTime; System.out.println("Using Generics LinkedList Takes Time: " + time + " millseconds. Result: " + totalDouble); } private void testDoubleB(){ long startTime = System.currentTimeMillis(); double totalDouble = 0; for(int num=0;num<this.m_loopNum;num++){ List<Double> dList = new ArrayList<Double>(); for(int i=0;i<m_dataSize;i++) dList.add(m_testDouble[i]); for(Double d :dList) totalDouble += d.doubleValue(); } long time = System.currentTimeMillis()-startTime; System.out.println("Using Generics ArrayList Takes Time: " + time + " millseconds. Result: " + totalDouble); } private void testDoubleC(){ long startTime = System.currentTimeMillis(); double totalDouble = 0; for(int num=0;num<this.m_loopNum;num++){ ArrayList dList = new ArrayList(); for(int i=0;i<m_dataSize;i++) dList.add(m_testDouble[i]); for(Object d :dList) totalDouble += ((Double)d).doubleValue(); } long time = System.currentTimeMillis()-startTime; System.out.println("Using Regular ArrayList Takes Time: " + time + " millseconds. Result: " + totalDouble); } private void testStringA(){ long startTime = System.currentTimeMillis(); String temp = null; for(int num=0;num<this.m_loopNum;num++){ LinkedList<String> sList = new LinkedList<String>(); for(int i=0;i<m_dataSize; i++){ sList.add(m_testString[i]); } for(String s : sList) temp = s.toUpperCase(); } long time = System.currentTimeMillis()-startTime; System.out.println("Using Generics LinkedList Takes Time: " + time + " millseconds. Result: " + temp); } private void testStringB(){ long startTime = System.currentTimeMillis(); String temp = null; for(int num=0;num<this.m_loopNum;num++){ List<String> sList = new ArrayList<String>(); for(int i=0;i<m_dataSize; i++){ sList.add(m_testString[i]); } for(String s : sList) temp = s.toUpperCase(); } long time = System.currentTimeMillis()-startTime; System.out.println("Using Generics ArrayList Takes Time: " + time + " millseconds. Result: " + temp); } private void testStringC(){ long startTime = System.currentTimeMillis(); String temp = null; for(int num=0;num<this.m_loopNum;num++){ ArrayList sList = new ArrayList(); for(int i=0;i<m_dataSize; i++) sList.add(m_testString[i]); for(Object s : sList) temp = ((String)s).toUpperCase(); } long time = System.currentTimeMillis()-startTime; System.out.println("Using Regular ArrayList Takes Time: " + time + " millseconds. Result: " + temp); } public static void main(String[] args) throws Exception{ if(args.length != 2){ System.out.println("Usage:JavaGenerics dataSize loopNumber "); System.exit(0); } int dataSize = Integer.parseInt(args[0]); int loopNumber = Integer.parseInt(args[1]); JavaGenerics obj = new JavaGenerics(dataSize,loopNumber); System.out.println("/nPrimary type int test..."); obj.testIntA(); obj.testIntB(); obj.testIntC(); System.out.println("/nPrimary type double test..."); obj.testDoubleA(); obj.testDoubleB(); obj.testDoubleC(); System.out.println("/nReference type String test..."); obj.testStringA(); obj.testStringB(); obj.testStringC(); } } |
下一篇我们将会介绍.NET 2.0 程序(使用支持Generics的Visual Studio 2005 Beta1 IDE)。(T111)
.NET 2.0 程序(使用支持Generics的Visual Studio 2005 Beta1 IDE)
#region Using directives using System; using System.Collections; using System.Collections.Generic; using System.Text; #endregion namespace GenericTest { class CSharpGenerics { private int[] m_testInt = null; private double[] m_testDouble = null; private String[] m_testString = null; private int m_dataSize = 0; private int m_loopNum = 0; public CSharpGenerics(int dataSize, int loopNum) { this.m_dataSize = dataSize; this.m_loopNum = loopNum; prepareData(m_dataSize); } private void prepareData(int dataSize) { m_testInt = new int[dataSize]; m_testDouble = new double[dataSize]; m_testString = new String[dataSize]; for (int i = 0; i < dataSize; i++) { m_testInt[i] = i; m_testDouble[i] = i * 1.0d; m_testString[i] = m_testDouble[i].ToString(); } } private void testIntA() { long startTime = System.Environment.TickCount; long totalInt = 0; for (int num = 0; num < this.m_loopNum; num++) { LinkedList<int> iList = new LinkedList<int>(); for (int i = 0; i < m_dataSize; i++) iList.AddTail(m_testInt[i]); foreach (int i in iList) totalInt += i; } long time = System.Environment.TickCount - startTime; Console.WriteLine("Using Generics LinkedList Takes Time: " + time + " millseconds. Result:" + totalInt); } private void testIntB() { long startTime = System.Environment.TickCount; long totalInt = 0; for(int num=0;num<this.m_loopNum;num++) { List<int> iList = new List<int>(); for (int i = 0; i < m_dataSize; i++) iList.Add(m_testInt[i]); foreach (int i in iList) totalInt += i; } long time = System.Environment.TickCount - startTime; Console.WriteLine("Using Generics List Takes Time: " + time + " millseconds. Result:" + totalInt); } private void testIntC() { long startTime = System.Environment.TickCount; long totalInt = 0; for (int num = 0; num < this.m_loopNum; num++) { ArrayList iList = new ArrayList(); for (int i = 0; i < m_dataSize; i++) iList.Add(m_testInt[i]); foreach (Object i in iList) totalInt += Convert.ToInt32(i); } long time = System.Environment.TickCount - startTime; Console.WriteLine("Using Regular ArrayList Takes Time: " + time + " millseconds. Result:" + totalInt); } private void testDoubleA() { long startTime = System.Environment.TickCount; double totalDouble = 0; for (int num = 0; num < this.m_loopNum; num++) { LinkedList<double> dList = new LinkedList<double>(); for (int i = 0; i < m_dataSize; i++) dList.AddTail(m_testDouble[i]); foreach (double i in dList) totalDouble += i; } long time = System.Environment.TickCount - startTime; Console.WriteLine("Using Generics LinkedList Takes Time: " + time + " millseconds. Result:" + totalDouble); } private void testDoubleB() { long startTime = System.Environment.TickCount; double totalDouble = 0; for (int num = 0; num < this.m_loopNum; num++) { List<double> dList = new List<double>(); for (int i = 0; i < m_dataSize; i++) dList.Add(m_testDouble[i]); foreach (double i in dList) totalDouble += i; } long time = System.Environment.TickCount - startTime; Console.WriteLine("Using Generics List Takes Time: " + time + " millseconds. Result:" + totalDouble); } private void testDoubleC() { long startTime = System.Environment.TickCount; double totalDouble = 0; for (int num = 0; num < this.m_loopNum; num++) { ArrayList dList = new ArrayList(); for (int i = 0; i < m_dataSize; i++) dList.Add(m_testDouble[i]); foreach (double i in dList) totalDouble += Convert.ToDouble(i); } long time = System.Environment.TickCount - startTime; Console.WriteLine("Using Regular ArrayList Takes Time: " + time + " millseconds. Result:" + totalDouble); } private void testStringA() { long startTime = System.Environment.TickCount; String temp = null; for (int num = 0; num < this.m_loopNum; num++) { LinkedList<String> sList = new LinkedList<String>(); for (int i = 0; i < m_dataSize; i++) sList.AddTail(m_testString[i]); foreach (String i in sList) temp = i.ToUpper(); } long time = System.Environment.TickCount - startTime; Console.WriteLine("Using Generics LinkedList Takes Time: " + time + " millseconds. Result:" + temp); } private void testStringB() { long startTime = System.Environment.TickCount; String temp = null; for (int num = 0; num < this.m_loopNum; num++) { List<String> sList = new List<String>(); for (int i = 0; i < m_dataSize; i++) sList.Add(m_testString[i]); foreach (String i in sList) temp = i.ToUpper(); } long time = System.Environment.TickCount - startTime; Console.WriteLine("Using Generics List Takes Time: " + time + " millseconds. Result:" + temp); } private void testStringC() { long startTime = System.Environment.TickCount; String temp = null; for (int num = 0; num < this.m_loopNum; num++) { ArrayList sList = new ArrayList(); for (int i = 0; i < m_dataSize; i++) sList.Add(m_testString[i]); foreach (String i in sList) temp = ((String)i).ToUpper(); } long time = System.Environment.TickCount - startTime; Console.WriteLine("Using Regular ArrayList Takes Time: " + time + " millseconds. Result:" + temp); } public static void Main(string[] args) { if (args.Length != 2) { Console.WriteLine("Usage: GenericTest dataSize loopNum"); return; } int dataSize = Convert.ToInt32(args[0]); int loopNumber = Convert.ToInt32(args[1]); CSharpGenerics obj = new CSharpGenerics(dataSize, loopNumber); Console.WriteLine("/nValue type int test..."); obj.testIntA(); obj.testIntB(); obj.testIntC(); Console.WriteLine("/nValue type double test..."); obj.testDoubleA(); obj.testDoubleB(); obj.testDoubleC(); Console.WriteLine("/nReference type String test..."); obj.testStringA(); obj.testStringB(); obj.testStringC(); } } } |
上面这两段程序几乎完全一样。有几点情况在这里要说明一下。
1. Java和.NET都提供了Generics的LinkedList,所以我们可以直接对比这两者的性能。
2. Java和.NET都提供了非Generics的ArrayList,即传统的ArrayList,所以我们可以直接对比这两者的性能。
3. Java提供了Generics和非Generics的ArrayList,所以我们可以比较使用了Generics后性能的提升。这是我们今天测试的重点之一。
4. .NET提供了非Generics的ArrayList,但是很遗憾没有Generics的ArrayList。按照微软的说法是,我们可以使用相对应的Generics List类,它和传统的ArrayList功能和意图非常相似。对比.NET中非Generics的ArrayList和Generics List性能上的差异是我们今天测试的另一个重点。
5. 由于Java的Hotspot工作特性,所以我对测试有一个外部循环,这样做的目的是使Java的Hotspot有机会对程序进行优化,从而达到最佳性能。
下面是测试的结果。这里给出两组结果,分别是数据量为100000,循环次数为100和数据量为500000,循环次数为20。
从上面这个图表中,我们可以得出以下结论:
1. 在所有的测试项目中,.NET 2.0的性能都要比Java 1.5的性能高出许多。为什么会有这样的结果不是我们今天要讨论的话题。我们关注的是Generics给这两种技术带来的好处。
2. 在我们前面列举的Generics的四个特点中,类型安全,二进制代码重用以及语义清晰这三个特点在Java和.NET中都得到了很好地体现。
3. 使用了Generics,Java并没有得到什么性能提升。最直接的表现就是Generics和非Generics的ArrayList在性能上几乎没有什么差异,无论是基础的数据类型,如整数,浮点数,还是参考型String对象。这一点可能会出乎很多人的意料之外。
4. 使用了Generics,.NET的性能有了提升。尤其是整数,双精度浮点数这些数值类型(Value Type),性能成倍的提高。对于参考类型(Reference Type),性能也有比较明显的提高。
下一篇让我们深入到它们的内部,看看他们内部工作的机理各是什么样的。(T111)
同样是Generics,为什么两者在性能上会有这么大的区别呢?现在让我们深入到它们的内部,看看他们内部工作的机理各是什么样的。
首先,然我们用反编译工具看看两者生成的Code到底是什么样的。(本文使用了DJ Java Decompiler和Lutz Roeder's Reflector.exe,如图所示)
比如对于int和String段的测试,反编译Java class 的 bytecode我们得到这样的程序(double和int类似,为了节省篇幅,这里就不具体列出了)。
private void testIntA() { long l = System.currentTimeMillis(); long l1 = 0L; for(int i = 0; i < m_loopNum; i++) { LinkedList linkedlist = new LinkedList(); for(int j = 0; j < m_dataSize; j++) linkedlist.add(Integer.valueOf(m_testInt[j])); for(Iterator iterator = linkedlist.iterator(); iterator.hasNext();) { Integer integer = (Integer)iterator.next(); l1 += integer.intValue(); } } long l2 = System.currentTimeMillis() - l; System.out.println((new StringBuilder()).append("Using Generics LinkedList Takes Time: ").append(l2).append(" millseconds. Result: ").append(l1).toString()); } private void testIntB() { long l = System.currentTimeMillis(); long l1 = 0L; for(int i = 0; i < m_loopNum; i++) { ArrayList arraylist = new ArrayList(); for(int j = 0; j < m_dataSize; j++) arraylist.add(Integer.valueOf(m_testInt[j])); for(Iterator iterator = arraylist.iterator(); iterator.hasNext();) { Integer integer = (Integer)iterator.next(); l1 += integer.intValue(); } } long l2 = System.currentTimeMillis() - l; System.out.println((new StringBuilder()).append("Using Generics ArrayList Takes Time: ").append(l2).append(" millseconds. Result: ").append(l1).toString()); } private void testIntC() { long l = System.currentTimeMillis(); long l1 = 0L; for(int i = 0; i < m_loopNum; i++) { ArrayList arraylist = new ArrayList(); for(int j = 0; j < m_dataSize; j++) arraylist.add(Integer.valueOf(m_testInt[j])); for(Iterator iterator = arraylist.iterator(); iterator.hasNext();) { Object obj = iterator.next(); l1 += ((Integer)obj).intValue(); } } long l2 = System.currentTimeMillis() - l; System.out.println((new StringBuilder()).append("Using Regular ArrayList Takes Time: ").append(l2).append(" millseconds. Result: ").append(l1).toString()); } private void testStringA() { long l = System.currentTimeMillis(); String s = null; for(int i = 0; i < m_loopNum; i++) { LinkedList linkedlist = new LinkedList(); for(int j = 0; j < m_dataSize; j++) linkedlist.add(m_testString[j]); for(Iterator iterator = linkedlist.iterator(); iterator.hasNext();) { String s1 = (String)iterator.next(); s = s1.toUpperCase(); } } long l1 = System.currentTimeMillis() - l; System.out.println((new StringBuilder()).append("Using Generics LinkedList Takes Time: ").append(l1).append(" millseconds. Result: ").append(s).toString()); } private void testStringB() { long l = System.currentTimeMillis(); String s = null; for(int i = 0; i < m_loopNum; i++) { ArrayList arraylist = new ArrayList(); for(int j = 0; j < m_dataSize; j++) arraylist.add(m_testString[j]); for(Iterator iterator = arraylist.iterator(); iterator.hasNext();) { String s1 = (String)iterator.next(); s = s1.toUpperCase(); } } long l1 = System.currentTimeMillis() - l; System.out.println((new StringBuilder()).append("Using Generics ArrayList Takes Time: ").append(l1).append(" millseconds. Result: ").append(s).toString()); } private void testStringC() { long l = System.currentTimeMillis(); String s = null; for(int i = 0; i < m_loopNum; i++) { ArrayList arraylist = new ArrayList(); for(int j = 0; j < m_dataSize; j++) arraylist.add(m_testString[j]); for(Iterator iterator = arraylist.iterator(); iterator.hasNext();) { Object obj = iterator.next(); s = ((String)obj).toUpperCase(); } } long l1 = System.currentTimeMillis() - l; System.out.println((new StringBuilder()).append("Using Regular ArrayList Takes Time: ").append(l1).append(" millseconds. Result: ").append(s).toString()); } |
反编译.NET可执行文件,我们得到这样的程序。
private void testIntA() { long num1 = Environment.TickCount; long num2 = 0; int num3 = 0; while ((num3 < this.m_loopNum)) { LinkedList<int> list1 = new LinkedList<int>(); int num4 = 0; while ((num4 < this.m_dataSize)) { list1.AddTail(((int) this.m_testInt[num4])); ++num4; } LinkedList.Enumerator<int> enumerator1 = list1.GetEnumerator(); try { while (enumerator1.MoveNext()) { int num5 = enumerator1.Current; num2 += num5; } } finally { enumerator1.Dispose(); } ++num3; } long num6 = (Environment.TickCount - num1); object[] objArray1 = new object[4]; objArray1[0] = "Using Generics LinkedList Takes Time: "; objArray1[1] = num6; objArray1[2] = " millseconds. Result:"; objArray1[3] = num2; Console.WriteLine(string.Concat(objArray1)); } private void testIntB() { long num1 = Environment.TickCount; long num2 = 0; int num3 = 0; while ((num3 < this.m_loopNum)) { List<int> list1 = new List<int>(); int num4 = 0; while ((num4 < this.m_dataSize)) { list1.Add(((int) this.m_testInt[num4])); ++num4; } List.Enumerator<int> enumerator1 = list1.GetEnumerator(); try { while (enumerator1.MoveNext()) { int num5 = enumerator1.Current; num2 += num5; } } finally { enumerator1.Dispose(); } ++num3; } long num6 = (Environment.TickCount - num1); object[] objArray1 = new object[4]; objArray1[0] = "Using Generics List Takes Time: "; objArray1[1] = num6; objArray1[2] = " millseconds. Result:"; objArray1[3] = num2; Console.WriteLine(string.Concat(objArray1)); } private void testIntC() { long num1 = Environment.TickCount; long num2 = 0; int num3 = 0; while ((num3 < this.m_loopNum)) { ArrayList list1 = new ArrayList(); int num4 = 0; while ((num4 < this.m_dataSize)) { list1.Add(this.m_testInt[num4]); ++num4; } foreach (object obj1 in list1) { num2 += Convert.ToInt32(obj1); } ++num3; } long num5 = (Environment.TickCount - num1); object[] objArray1 = new object[4]; objArray1[0] = "Using Regular ArrayList Takes Time: "; objArray1[1] = num5; objArray1[2] = " millseconds. Result:"; objArray1[3] = num2; Console.WriteLine(string.Concat(objArray1)); } private void testStringA() { long num1 = Environment.TickCount; string text1 = null; int num2 = 0; while ((num2 < this.m_loopNum)) { LinkedList<string> list1 = new LinkedList<string>(); int num3 = 0; while ((num3 < this.m_dataSize)) { list1.AddTail(((string) this.m_testString[num3])); ++num3; } LinkedList.Enumerator<string> enumerator1 = list1.GetEnumerator(); try { while (enumerator1.MoveNext()) { string text2 = enumerator1.Current; text1 = text2.ToUpper(); } } finally { enumerator1.Dispose(); } ++num2; } long num4 = (Environment.TickCount - num1); object[] objArray1 = new object[4]; objArray1[0] = "Using Generics LinkedList Takes Time: "; objArray1[1] = num4; objArray1[2] = " millseconds. Result:"; objArray1[3] = text1; Console.WriteLine(string.Concat(objArray1)); } private void testStringB() { long num1 = Environment.TickCount; string text1 = null; int num2 = 0; while ((num2 < this.m_loopNum)) { List<string> list1 = new List<string>(); int num3 = 0; while ((num3 < this.m_dataSize)) { list1.Add(((string) this.m_testString[num3])); ++num3; } List.Enumerator<string> enumerator1 = list1.GetEnumerator(); try { while (enumerator1.MoveNext()) { string text2 = enumerator1.Current; text1 = text2.ToUpper(); } } finally { enumerator1.Dispose(); } ++num2; } long num4 = (Environment.TickCount - num1); object[] objArray1 = new object[4]; objArray1[0] = "Using Generics List Takes Time: "; objArray1[1] = num4; objArray1[2] = " millseconds. Result:"; objArray1[3] = text1; Console.WriteLine(string.Concat(objArray1)); } private void testStringC() { long num1 = Environment.TickCount; string text1 = null; int num2 = 0; while ((num2 < this.m_loopNum)) { ArrayList list1 = new ArrayList(); int num3 = 0; while ((num3 < this.m_dataSize)) { list1.Add(this.m_testString[num3]); ++num3; } foreach (string text2 in list1) { text1 = text2.ToUpper(); } ++num2; } long num4 = (Environment.TickCount - num1); object[] objArray1 = new object[4]; objArray1[0] = "Using Regular ArrayList Takes Time: "; objArray1[1] = num4; objArray1[2] = " millseconds. Result:"; objArray1[3] = text1; Console.WriteLine(string.Concat(objArray1)); } |
这些程序反映出什么问题呢?
Java 1.5虽然支持Generics,对于开发人员来说,我们确实感受到了Generics带来的诸如类型安全等等好处。但是对于Java虚拟机来说,bytecode还是以前的bytecode,没有任何的变化。如果是整数,浮点数这些基本数据类型(primary data type),Java还是先将它们转化成了相应的类的实例,即Integer,Double类的对象。在使用的时候,还必须从Object类型转换成相应的类型。也就是说,Java的Generics只是一种"障眼法",在编译的时候,编译器把我们以前手工写的包装/解包装和类型转换代码自动地加进去了。所以对于具体运行的程序而言,和以前没有使用Generics的程序没有任何的变化。我们没有看到任何性能上的提升,原因就在于此。
.NET 2.0是一种全新的Generics设计。我们得到了Generics承诺的所有好处。按照微软的说法,对于整数,浮点数这些数值类型(Value Type),由于Generics避免了包装/解包装操作这个不小的开销,所以性能会有2~3倍的提高;而对于String以及其它的参考类型(Reference Type),由于避免的类型转换,性能会有20%左右的提高。我们今天的测试基本证明了这一说法。
看到这里,你可能不禁会问,同样是Generics,为什么Sun和Microsoft会有截然不同的做法呢?
Sun的Generics是起源于一个叫做"比萨饼(Pizza)"的项目。这个项目的设计原则就是使加了Generics后的Java程序可以在以前的Java虚拟机(Java Virtual Machine)上运行,而无需对Java虚拟机进行任何改动。在这种指导思想下,Generics的工作实际上就落在了编译器的身上。由编译器在编译的时候进行类型安全检查。对于通过语法检查的程序,自动加入包装/解包装操作以及类型转换操作这样的程序代码。这样编译生成的bytecode和以前没有任何不同,Java虚拟机在执行这些bytecode的时候,根本不知道还有Generics曾经发生过。这种做法的好处是显而易见的,那就是简单,易于实现,对以前的Java虚拟机有很好的兼容性。但是其缺点也是非常突兀的,那就是从核心上牺牲了Generics的精髓,我们没能感受到Generics应该带来的性能上的提高。
.NET的Generics是全新设计的,从根本上体现了Generics的精髓。语法上的保障是在编译器层次实现的。但是编译器除了应有的静态检查外和加入一些Meta信息外,并不做更多的工作(从我们反编译的程序中可以看出这一点)。具体的工作是由.NET的公共语言运行时(CLR,Common Language Runtime)来完成的。在你在第一次使用它的时候,比如说是List ,CLR会让JIT(Just-in-time-compiler)动态的生成这样的一个专门的int类List的本地机器代码。这个代码会被保存起来,以后类似的请求(List )就可以重复使用这个已经生成的类代码。出于性能上的考虑,所有程序中申明并且使用的Value Type都会有其相应的动态类生成。比如CLR会为我们的例程生成double和int两个专用List类。为了减小程序在运行时的膨胀(Code expansion)和增加代码重用性,所有的参考类(Reference type,比如我们使用的String)共享一个专门的类,也就是说所有的参考类只有一套本地机器代码。因为参考类的本质就是一个指针,这个共性使它们可以共享同样的代码。但是这些参考类有分开的VTable,这样的做法避免了类型转换的需要。
.NET这种设计是全新的,我们感受到了它的威力。但是这个新增的Generics将不兼容以前的CLR。以前的CLR(即.NET 1.0版和1.1版)无法处理这种Generics,这是一个需要指出的问题。但就我个人感觉而言,牺牲这个兼容性是值得的。
最后要说明的一点就是.NET 2.0在总体上的性能比Java 1.5要高出很多,这点非常出乎我的意料。包括程序中用到的那个非常简单的函数prepareData() 两者都有巨大的性能差距。记得在.NET 1.0Beta的时候,我对比过.NET和当时Java的性能。在那个时候,他们还是非常相近的。当然了,要全面评价.NET 2.0和 Java 1.5性能上的优劣需要更全面,更系统的测试,本文目的不在于此,所以就不多加评判了。