.NET中数组的隐秘特性
背景知识 Array类是所有数组类型的基类,上一篇文章《.NET中String类的隐秘特性》中提到:数组的长度不是固定不变的,是可变的。 首先了解一些相关的概念: 数组元素:数组包含的值; 数组长度:数组可以包含的元素的个数; 维度数:数组的维度总数; 下限:数组指定的维度的起始索引。多维数组每个维可以有不同的下限。 运行时有两种不同的数组实现--SZ数组和普通数组。SZ数组是以0为下限的一维数组;普通数组指多维的或者下限不为0的数组。有时候我们称呼多维数组为MD数组。由于SZ数组较常用,微软对它的性能进行了极大的优化。下面的表详细列出了SZ数组与MD数组的区别。
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数组,交错数组可以看作数组元素是SZ数组的SZ数组,当然在性能上它要优于MD数组。不过要记住一点,交错数组不兼容CLS,因此它不能在不同的语言编写的代码之间传递。数组的IL优化using System;namespace abc{ class Class1 { [STAThread] static void Main(string[] args) { int[] A=new int[5]; int[,] C=new int[5,5]; A[0]=1; C[0,0]=1; } }}上面代码的IL代码如下:.method private hidebysig static void Main(string[] args) cil managed{ .entrypoint .custom instance void [mscorlib]System.STAThreadAttribute::.ctor() = ( 01 00 00 00 ) // Code size 29 (0x1d) .maxstack 4 .locals ([0] int32[] A, [1] int32[0...,0...] C) IL_0000: ldc.i4.5 IL_0001: newarr [mscorlib]System.Int32 IL_0006: stloc.0 IL_0007: ldc.i4.5 IL_0008: ldc.i4.5 IL_0009: newobj instance void int32[0...,0...]::.ctor(int32, int32) IL_000e: stloc.1 IL_000f: ldloc.0 IL_0010: ldc.i4.0 IL_0011: ldc.i4.1 IL_0012: stelem.i4 IL_0013: ldloc.1 IL_0014: ldc.i4.0 IL_0015: ldc.i4.0 IL_0016: ldc.i4.1 IL_0017: call instance void int32[0...,0...]::Set(int32, int32, int32) IL_001c: ret} // end of method Class1::Main 对比一下给SZ数组和MD数组付值的IL代码:给数组A付值使用stelem.i4 指令,而给多维数组付值则必须调用Set方法。
数组内部字段 SZ数组和MD数组都包含有下面2个内部字段。
变量
类型
描述
Array Length
int
数组中实际的元素个数
Element Type
Type
从源代码看,这一字段只在数组包含“指针”的情况下才被使用。这里,“指针”指的是对象的引用,不是非托管代码中的指针
除了上面两个字段外,MD数组还包含下面两个字段。
变量
类型
描述
Bounds[rank]
int[]
数组某一维的元素个数
LowerBound[rank]
int[]
数组某一维的下限。合法的索引应该满足条件:lowerBounds[i] <= index[i] < lowerBounds[i] + bounds[i]
通过下面的例子和图示,你可以更好的了解这些内部字段。下面的非托管代码的主要目的是确定上述的内部字段在内存中的分布。using System;namespace ABC{ class Class1 { [STAThread] static void Main(string[] args) { int[] A=new int[5]; byte[] a=GetBytes(A,4); //输出数组A的长度(Array Length) Console.WriteLine(BitConverter.ToInt32(a,0)); int[,,] B=new int[2,3,4]; // 4+8*rank(维度数): 4+3*8=28 byte[] b=GetBytes(B,28); //分别输出数组B的长度,第1,2,3维的长度和第1,2,3维的下限 for (int i=0;i<28;i=i+4) Console.WriteLine(BitConverter.ToInt32(b,i)); Console.ReadLine(); } static byte[] GetBytes(int[] array,int count) { unsafe { byte[] b=new byte[count]; byte *pb; fixed (int *p=&array[0]) pb=(byte *)p; pb=pb-count; for (int i=0;i<count;i++) { b[i]=*pb++; } return b; } } static byte[] GetBytes(int[,,] array,int count) { unsafe { byte[] b=new byte[count]; byte *pb; fixed (int *p=&array[0,0,0]) pb=(byte *)p; pb=pb-count; for (int i=0;i<count;i++) { b[i]=*pb++; } return b; } } }} 根据上面代码的运行结果,我们可以确定数组的内部字段在内存中的分布,如图1所示(我在许多条件下测试了数组各内部字段在内存中的分布,发现它们的排列顺序总是如图)。
图1
对普通数组的访问必须检查几个内部成员,这会对性能造成一定的影响。一般地,我们有两种办法来优化普通数组的性能:一种是使用交错数组;另一种是使用非安全代码访问。
数组类型与分类 如果两个数组有着相同的维度数和相同的元素类型,我们认为这两个数组具有相同的类型,与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
你不能通过反射来访问数组的内部字段,那是不是需要使用非安全代码来访问内部字段?在这里,没有这个必要,因为Array的内部字段都通过公共方法和属性公开了。比如:GetLength()方法返回数组中指定维度的元素个数。相关的更详细的内容可以参考MSND。 上面提到了两种数组的分类:SZ数组和MD数组;值类型数组和引用类型数组。在代码中我们该如何判断它们? 下面的代码用来判断数组是否SZ数组:if (array.Rank==1 && array.GetLowerBound(0)==0){} 下面的代码用来判断数组是否值类型数组:if ((elementType = array.GetType().GetElementType()) && elementType.IsSubclassOf(typeof(ValueType)) && elementType != typeof(Enum) && elementType != typeof(ValueType)){} 有意思的是,Enum[]或者ValueType[]都不是值类型数组,它们包含的元素是指向装箱值类型的引用。动态的ArrayList类 ArrayList类是处理动态数组的一个很有用的类,除此之外,它还可以用来封装集合类。 ArrayList类允许创建一个内部数组对象并对数组进行直接的修改。没有显式设置ArrayList容量的情况下,使用默认容量(16),ArrayList创建的数组的长度是16。下面表中列出的是ArrayList类的四个内部成员。变量
类型
描述
_items
Object[]
内部数组
_size
int
ArrayList实例实际包含的元素数
_version
int
在每次对ArrayList进行修改后,_version都会递增。
_defaultCapacity
int
常量字段,表示默认容量
一个ArrayList实例共占用20字节的内存(8字节的对象开销内存+12字节的实例信息),这不包括内部数组(_items)占用的空间。 给ArrayList添加新元素时(比如调用AddRange方法),需要超出ArrayList的初始容量,ArrayList将会自动扩大容量。ArrayList的容量或者加倍或者增加到新的 Count,取二者之中较大者,内部数组(_items)也被重新分配以容纳新元素,并且现有的元素被复制到新数组中。出于优化性能的考虑,如果预先知道长度,应该为ArrayList分配足够的内存一避免不必要的复制。如果所有的数组元素都已经添加进去,并且不再对数组(_items)进行扩充,你应该调用ToArray方法将它装换成类型安全的数组,这样,无论在内存使用还是性能上都得到极大的优化。 我们可以调用TrimToSize方法来截去ArrayList的未使用部分,这个方法实际上是执行一次元素复制。在调用TrimToSize后,要真正的释放数组占用的内存,还要调用Clear方法。要注意的是,在空ArrayList上执行TrimToSize方法是将ArrayList的容量设置为默认容量,而不是零。需要注意的是,创建ArrayList如果将容量设为0,CLR将使用默认值16来创建。 ArrayList类不是Array类的一个完全的替代,我觉得ArrayList比多维数组的性能要好得多,我将在另外一篇文章详细分析二者的区别,特别在性能方面。
Array
ArrayList
</script> <script type="text/javascript" src="http://pagead2.googlesyndication.com/pagead/show_ads.js"> </script> <script type="text/javascript"> </script><script type="text/javascript" src="http://pagead2.googlesyndication.com/pagead/show_ads.js"> </script>