SZ数组 | MD数组 | |
定义 | 一维的,以0为下限的数组 | 多维的,或者下限不为0的数组 |
C#语法 | Object[] Object[][] (交错数组) | Object[,] ---二维数组 |
是否兼容CLS | 兼容(交错数组除外) | 不兼容 |
IL优化 | 使用专用的IL指令来操作这些数组,比如:ldlen,stelem等等 | 在1.0版本,没有专用的IL指令,对数组的所有操作都是通过方法调用来实现 |
方法优化 | 基元类型数组有专用的方法,这些方法在操作一些值类型数组时不用反复的装箱,所以具有较高的性能 | 在1.0版本,引用类型和值类型数组使用同样的方法。值类型在方法调用时被反复地装箱和拆箱,造成了极大的性能冲击 |
基本长度(不包括8字节的方法表指针和对象头) | 值类型数组 — 4字节 引用类型数组 — 8字节 | 值类型数组 — 4+8*rank(维度数) 引用类型数组 — 8+8*rank(维度数) |
JIT优化 | JIT编译器消除了范围检查 | JIT编译器没有对它进行优化。CLR将会执行额外的代码对每一维进行范围检查 |
数组内部字段
SZ数组和MD数组都包含有下面2个内部字段。 变量 | 类型 | 描述 |
Array Length | int | 数组中实际的元素个数 |
Element Type | Type | 从源代码看,这一字段只在数组包含“指针”的情况下才被使用。这里,“指针”指的是对象的引用,不是非托管代码中的指针 |
变量 | 类型 | 描述 |
Bounds[rank] | int[] | 数组某一维的元素个数 |
LowerBound[rank] | int[] | 数组某一维的下限。合法的索引应该满足条件:lowerBounds[i] <= index[i] < lowerBounds[i] + bounds[i] |
对普通数组的访问必须检查几个内部成员,这会对性能造成一定的影响。一般地,我们有两种办法来优化普通数组的性能:一种是使用交错数组;另一种是使用非安全代码访问。
数组类型与分类
如果两个数组有着相同的维度数和相同的元素类型,我们认为这两个数组具有相同的类型,与C/C++不同,这里每一维的上限和下限不予考虑,下面的代码说明 了这点。一些方法(比如Array.Copy)在操作多维数组时,它们在内部将多维数组看作一个一维数组(数组长度是各维长度的总和)。 Array A=Array.CreateInstance(typeof(int),new int[2]{2,2},new int[2]{-1,-1}); Array B=Array.CreateInstance(typeof(int),new int[2]{3,3},new int[2]{-10,-2}); if (A.GetType().Equals(B.GetType())) Console.WriteLine("数组A与B属于同一类型"); 具有不同维度数的交错数组属于不同的类型,比如: int[][] A=new int[2][]; int[][][] B=new int[2][][]; A 与B是不同的类型。道理比较显然,我们可以认为交错数组的元素是数组,A与B的元素类型是不一样的,所以A与B属于不同的类型。比较有意思的是,基类 Array类型调用Type.IsArray()方法返回值是false,调用Type.GetElementType()方法返回值是null。 除了基本长度外,数组还包含了一些数据,如图1所示。值类型数组包含的是未装箱的结构(连续排列),引用类型数组则包含了指向引用对象的指针(连续排 列)。另外,引用类型数组在指针数据块之前还有一个元素类型字段(ElementType)。读者也许会认为:通过数组的方法表可以获得有关元素类型的信 息,这个字段显得有点多余了。其实不然,通过这个字段,可以迅速地获得类型信息,另外,这对于数组的其他特性,比如数组变异(Array Covariance),是非常重要的(后面会详细讲述这点)。 如果数据是值类型,那么元素的长度与相应的值类型一样,引用类型则占用IntPtr.Size个字节。IntPtr.Size在Win32系统中是4个字 节,在64位系统中是8个字节。依据微软的文档记录,IntPtr.Size与Void *指针的本地字节数相同,但是在非Win32的Rotor包(比如Mac和Unix),不管CPU是什么,IntPtr.Size总是8个字节。 类型 | 元素的字节长度 |
bool | 1 |
byte | 1 |
short | 2 |
int | 4 |
long | 8 |
float | 4 |
double | 8 |
decimal | 16 |
string | IntPtr.Size |
object | IntPtr.Size |
interface | IntPtr.Size |
变量 | 类型 | 描述 |
_items | Object[] | 内部数组 |
_size | int | ArrayList实例实际包含的元素数 |
_version | int | 在每次对ArrayList进行修改后,_version都会递增。 |
_defaultCapacity | int | 常量字段,表示默认容量 |
| Array | ArrayList |
内存占用 | 值类型数组中的数据没有装箱,每个元素的长度等于相应的值类型的长度。 引用类型数组的每个元素的长度等于IntPtr.Size | 内部数组是引用类型数组。值类型数组会带来每元素12字节的开销(4字节用于对象引用,8字节是元素装箱时引入的对象头) |
性能 | 有专用的IL指令;消除了范围检查 |
|
| 长度固定 | 长度可变 |
访问 |
| 对某一索引的元素的访问的前提是该索引之前的所有元素都已经添加。比如下面的代码会发出异常: ArrayList al=new ArrayList(); al[0]=1; |
| 语法 |
将Ilist转换成Array | ArrayList.Adapter(iList).ToArray() |
反转Ilist | ArrayList.Adapter(iList).Reverse() |
取得子集 | ArrayList.Adapter(iList).GetRange(start, count) |
使用对分检索算法查找 | ArrayList.Adapter(iList).BinarySearch() |
排序 | ArrayList.Adapter(iList).Sort() |
object [] data = new Animal[2]; // Animal[]被隐式地转换为object[]
Animal [] animals1 = data; // Error: 从object[]到Animal[]需要显式转换 Animal [] animals2 = (Animal[]) data; // object[] 被显式转换成 Animal [] string [] strings1 = (string[]) animals2; // 编译失败,因为string[]与Animal[]之间不能相互转换 string [] strings2 = (string[]) data; // 编译成功,但是运行时会发生异常,因为Animal[]不是从string[]继承而来 object [] data2 = new object[1]; data2[0] = new Animal(); Animal [] animals3 = (Animal[]) data2; // 编译成功,但是运行时会发生异常。运行时检查将会验证元素类型(ElementType)与目标类型Animal的兼容性。Animal[] animal4=new Animal[1];
object[] data3=(object[])animal4; Animal[] animal5=(Animal[])data3;//编译成功,运行时也不会发生异常。运行时检查将检测到data3的元素类型(ElementType)与目标类型Animal的兼容。 能够将一种类型的数组重新解释为其它类型,这种方式无论在内存使用还是时间上都大大提高了程序的效率。如果从一种类型数组转换为另一种类型数组需要重新构造一个数组的话,显然,程序的性能会受到极大的冲击。 public void Test() { string[] data = new string [] { "a", "b", "c", "d", "e" }; SetRange(data, 1, 3, "x" ); } public void SetRange(object [] array, int start, int count, object value) { for (int i=0; i < count; i++) array[i+start] = value; } 上面的例子,新的字符串数组是{ "a", "x", "x", "x", "e" },如果将一个整数作为参数value,将会引起运行时异常,因为引用类型数组在分配数组元素时执行类型检查,所以在array[i+start]= value这一句会发生异常。 下面的例子显示了值类型数组和引用类型数组的一些区别。 Write( new int [] { 1, 2, 3 } ); // 显示 "System.Int32 []" Write( new string[] {"a", "b", "c" } ); // 显示 "a", "b", "c" void Write(params object [] args) { for (int i=0; i<args.Length; i++) Console.WriteLine(args[i]); } 数组变异(array covariance)的不良影响是每次给数组元素分配对象都必须执行类型检查。 你可以使用object[]或者Array来创建一个"通用"数组。多数情况,object[]在性能上要优于Array,因为object[]属于SZ 数组,有专门的IL指令来设置或读取数组元素。但是,object[]不适用于值类型数组和多维数组,而Array则可以。数组元素转换(Array.Copy) Array.Copy方法可以在复制时转换数组元素。方法可以执行以下转换: 1 将值类型元素装箱到引用类型,比如:将int[]复制到object[] 2 将引用类型元素取消装箱,比如:将上面得到的object[]转换为int[] 3 拓宽转换,比如,可以将int[]复制到double[],但不能将double[]复制到int[]。 复制引用类型数组时,先进行类型检查,然后执行浅表复制,如果类型不兼容,抛出ArrayTypeMismatchException异常。 public Array Convert(Array array, Type type) { Array newArray = Array.CreateInstance(type, array.Length); Array.Copy(array,0, newArray,0, array.Length); return newArray; }通过反射机制访问内部成员 你可以通过反射机制来访问、调用或者修改ArrayList类的内部成员,不管它们被声明为private、protected或者internal。 下面代码取得ArrayList的内部成员_items: object[] abc=new Object[5]; ArrayList al=new ArrayList(5); al.Add("abc"); abc=(object[])al.GetType().GetField("_items",BindingFlags.NonPublic|BindingFlags.Instance).GetValue(al); Console.WriteLine(abc[0]); 你可以阅读微软公布的Rotor包来了解类的内部成员,也可以使用IL反编译器,比如Reflector或者Anakrino。数组性能 对数组进行索引一般都要进行范围检查。根据微软的说法,编译器进行了一些特殊的优化来改善遍历数组或者字符串的性能。我们先来比较一个下面三种遍历数组的方案,看看那一种更快。 a是一维int类型数组 1) int hash = 0; for (int i=0; i< a.Length; i++) { hash += a[i]; } 2) int hash = 0; int length = a.length; for (int i=0; i< length; i++) { hash += a[i]; } 3) foreach (int i in a) { hash += i; } 令人惊讶的是,在目前这个的JIT编译器下,第一个例子是最快的,第三个例子是最慢的。在下一版本JIT编译器,第三个例子将会跟第一个例子具有相同的速度。 为 什么第一个例子比第二个例子快呢?这是因为编译器认识 for (int i=0; i<s.length; i++) 这种模式(仅限于字符串和数组)。编译器将会存储数组的长度,这样在每一次遍历时不用调用任何方法(因为JIT编译器可以自动嵌入只包含简单的流程控制的 非虚拟方法和不超过32个字节的IL指令,在这里,编译器将数组长度的引用嵌入)。 另外,编译器还消除了每一次循环对s[i]的范围检查,因为i 在for条件中已经被限制在0和数组长度之间。在第二个例子中,由于使用一个整数代替了数组长度,编译器就不会认为它是 for (int i=0; i<a.length; i++) 模式(不会假设i在0和数组长度之间),所以每一次循环都会执行一次范围检查。这就是为什么第二个例子要比第一个例子慢。一些使用数组应该注意的问题大数组问题 大数组对性能有很大的影响。超出85K的对象被称为大对象,它们被分配在大对象堆。几乎所有的大对象都是数组,有一些是字符串。显然,很少的类包含有那么 多的成员使得占用内存超过85K。大对象不能被压缩,同时,它只能在全垃圾回收(包含第2代的垃圾回收)中才能被回收。如果大对象包含了析构函数,那么至 少要两次全垃圾回收才能回收它们。全垃圾回收发生的次数一般的是0代垃圾回收的1/100,显然,回收大垃圾对象占用的内存需要经过一段较长的时间。从内 存分配角度看,在程序中频繁的分配临时使用的大对象是一个很糟糕的设计,甚至是最差的设计。构造ArrayList或其他集合类时,为它们指定足够的容量,避免扩大容量时数组复制造成的性能损失
与多维数组相比较,交错数组具有更好的性能,尽量使用交错数组
尽量使用强类型数组,因为强类型数组可以避免装箱,转换,方法调用等带来的性能损失。