艾伟_转载:把Array说透

简介:   1. 数组大局观   数组是一个引用类型,也就是意味着数组的内存分配在托管堆上,并且我们在栈上维护的是他的指针而并非真正的数组。接下来我们分析下数组的元素,其中的元素无外乎是引用类型和值类型。

  1. 数组大局观

  数组是一个引用类型,也就是意味着数组的内存分配在托管堆上,并且我们在栈上维护的是他的指针而并非真正的数组。接下来我们分析下数组的元素,其中的元素无外乎是引用类型和值类型。当数组中的元素是值类型时,不同于int i;这样的代码。数组会根据数组的大小自动把元素的值初始化为他的默认值。例如:

static void Main(string[] args)
{
    int[] intArray = new int[3];
    foreach(int i in intArray)
    {
        Console.WriteLine(i);
    }
    DateTime[] dtArray = new DateTime[3];
    foreach (DateTime i in dtArray)
    {
        Console.WriteLine(i);
    }
}

  结果如下:

image

  当数组中的元素是引用类型时,实际上数组中的元素是一个指向对象实际内存空间的指针,占用4Bytes的空间。

  2. 谈谈零基数组

  从学C语言时起,相信老师就会对我们讲,数组的第一个索引是0,而不是1。但是在C#中,我们可以去构造一个非零基数组,在这一节,我们就来把这个说透。

  在常规意义上,我们初始化一个数组,都默认是零基数组,这也使得数组成为了字符串后再一个初始化时特殊的类型。正如我们知道的一样,初始化一个字符串时,对应的IL指令是newstr,同样,初始化一个零基数组对应的IL指令是newarr。

  当我们希望构造一个非零基数组时,我们可以以下的语句来做到:

static void Main(string[] args)
{
    Array intArr = Array.CreateInstance(typeof(Int32), new int[] { 5 }, new int[] { 1 });
    Console.WriteLine(intArr.GetValue(1).ToString());
    Console.WriteLine(intArr.GetValue(0).ToString());
}

  得到的测试结果便如下:

image

  于是便证明,我们初始化了一个非零基数组。此外,延伸一下,我们还应该通过这个记住以下两个方法:

static void Main(string[] args)
{
    Array intArr = Array.CreateInstance(typeof(Int32), new int[] { 5 }, new int[] { 1 });
    Console.WriteLine(intArr.GetLowerBound(0));
    Console.WriteLine(intArr.GetUpperBound(0));
}

  得到的测试结果如下:

image

  3. 谈谈效率问题

  相信会有好多阴谋论者说,C#是个类型安全的语言,也就是意味着我循环时每次访问一次数组的元素,那么就要检查一次该索引是否会造成数组越界,于是就造成了一定的性能损失。那么在这里,我们就把这个问题说透。

  我们在这里把数组分成零基数组,非零基数组,多维数组,交错数组四种情况来分别讨论这个问题。

  零基数组是.NET中提倡使用的类型,并且初始化时提供了特殊的IL指令newarr则充分说明了他在.NET中的特殊性,自然.NET Framework也会为其提供很大的优化待遇。在循环访问数组时,如这样的代码:

static void Main(string[] args)
{
    int[] intArr = new int[5];
    for (int i = 0; i < 4; i++)
    { 
        //Some Method
    }
}

  JIT编译器只会在循环开始之前检查一次4和intArr.GetUpperBound的大小关系,之后便不会对其进行干预。也就是说JIT编译器只对其检查一次安全,因此带来的性能损失是非常小的。

  而对于非零基数组,我们来比较这样两段代码:

static void Main(string[] args)
{
    Array intArr = Array.CreateInstance(typeof(Int32), new int[] { 5 }, new int[] { 1 });
    Console.WriteLine(intArr.GetValue(1).ToString());
    Console.WriteLine(intArr.Length);
    //
    int[] intArr1 = new int[5];
    Console.WriteLine(intArr1[1]);
    Console.WriteLine(intArr1.Length);
}

  其实两者创建的几乎是相同的数组,调用的也几乎是一样的方法,但是我们看下IL却会发现两者有着惊人的不同,首先是非零基数组的IL:

image

  接下来是零基数组的:

image

  我们可以发现,对于非零基数组中的大部分操作,.NET Framework都提供了对应的IL指令,我们也可以理解为.NET Framework为其提供了特殊的优化。

  当然,实际上,正如CLR via C#所说的一样:.NET Framework对应非零基数组没有任何方面的优化,每次访问都需要检查其上限和下限与索引之间的关系。效率的损耗是必然的。

  事实上,当我们测试这样一段代码时,也会发现其实零基数组和非零基数组的区别是很大的:

static void Main(string[] args)
{
    Array intArr = Array.CreateInstance(typeof(Int32), new int[] { 5 }, new int[] { 1 });
    Console.WriteLine("intArr的?类à型í是?:o{0}", intArr.GetType()); 
    //
    int[] intArr1 = new int[5];
    Console.WriteLine("intArr1的?类à型í是?:o{0}", intArr1.GetType()); 
}

  得到的结果如下:

image

  接下来我们再来简单地说下多维数组和交错数组。

  多维数组和非零基数组一样,都没有受到.NET Framework的特殊优待。

  而交错数组,其实就是数组中的数组,因此效率实际上取决于数组中的数组是零基数组还是非零基数组。

  那接下来的一节,我们来具体探讨一下交错数组和多维数组的区别和应用。

  4. 多维数组和交错数组

  考虑到两个词的翻译问题,在这里给出两个词的英文:

  多维数组:Multi-dimensional Array。

  交错数组:Jagged Array。

  好,下面步入正题。

  首先从二者的内存分布说起。

  多维数组是一个整体的数组,因此他在内存中占据一个整体的托管堆内存块。

  而交错数组实际上是数组中的数组,因此我们用二维交错数组来举例,其内存如图所示:

image

  也就是说,如果是一个3*100的数组,也就是说需要初始化101次数组,当数组的元素更加多的时候,那创建和垃圾回收将带来巨大的效率损失。

  因此,也就是说:交错数组的效率瓶颈在于创建和销毁上,而并非类型安全检查上。

  于是,我们就可以得出这样的结论:

  当一次创建,多次访问时,我们应该创建交错数组。

  当一次创建,一次访问时,我们应该创建多维数组。

  5. 用代码改善效率

  上面说到了,访问非零基数组和多维数组的效率是比较低的,对于非零基数组,我们的应用比较少,但是多维数组,相信每个人都或多或少有着一定的应用,那么面对其性能问题,我们该怎么办呢?

  我们先来想想,多维数组的访问,性能瓶颈在安全检查上。在C语言中,为什么没有这样的问题,对,因为C语言不会做这样的检查。于是,相信聪明的大家都会想到不安全代码。

  改善多维数组以及非零基数组的效率问题,我们就用不安全代码。

static unsafe void Main(string[] args)
{
    int[,] intArr = new int[3, 3];
    for (int i = 0; i < 3; i++)
    {
        for (int j = 0; j < 3; j++)
        {
            intArr[i, j] = i * 3 + j;
        }
    }
    fixed (int* p = &intArr[0, 0])
    {
        for (int i = 0; i < 3; i++)
        {
            int baseOffset = i * 3;
            for (int j = 0; j < 3; j++)
            {
                Console.WriteLine(baseOffset + j);
            }
        }
    }
}

  这里,我们又见到了C语言中熟悉的指针,相信不需要多加介绍了。这里唯一需要注意的就是fixed,由于在垃圾回收时采用的是代机制+压缩机制,因此其内存地址很可能发生改变,因此我们应该讲数组的内存地址锁住,防止我们访问到其他的内存地址而造成我们读取数据的错误。

  6. 对零基数组的精益求精

  当然,即使是零基数组,我们依然在托管堆上为其分配了内存空间。如果对性能要求极高,我们知道创建一个对象也是有着一定的时间损耗,其中包括分配内存空间,同步块索引,以及指向下一块内存空间的指针等一系列复杂的操作。那么我们就放弃掉托管堆这个东东,而直接在栈中来创建这个数组,这样又省去了很多时间,从而达到了和C语言相同的效果,代码如下:

static unsafe void Main(string[] args)
{
    int* intArr=stackalloc int[10];
    for (int i = 0; i < 10; i++)
    {
        intArr[i] = i;
    }
    for (int i = 0; i < 10; i++)
    {
        Console.WriteLine(intArr[i]);
    }
}

 

  这样,效率就进一步提高了,对于二维数组,我们一样可以如此创建。其代码与C语言完全等同。我在这里就不继续演示了。

  7. 总结

  在全文中,主要是对数组的各个方面做一个比较简略地介绍,其中包括数组的基础知识,分类,以及效率性能问题,最后就是用不安全代码来访问创建数组来提高性能。

  不过最后说一句,在实际工作中,如果对性能没有特别高的要求,则没必要用不安全代码来操作数组,因为其很可能因为你的一些失误而带来其他的一些安全问题,并且对代码的可读性也是个比较大的伤害,这就有些得不偿失了。

目录
相关文章
|
存储 算法 C++
【C++从0到王者】第三十一站:map与set(上)
【C++从0到王者】第三十一站:map与set
59 0
|
6月前
|
前端开发 开发工具 图形学
【你问我答】unity实现一个刮刮乐效果
【你问我答】unity实现一个刮刮乐效果
140 0
|
6月前
|
网络协议 前端开发 JavaScript
程序与技术分享:ALIENTEK战舰ENC28J60LWIP和UIP补充例程(LWIPWEB有惊喜)
程序与技术分享:ALIENTEK战舰ENC28J60LWIP和UIP补充例程(LWIPWEB有惊喜)
|
6月前
|
前端开发 Java 程序员
牛皮的程序猿后端返回值怎么定义
在后端接口封装中,通常会统一返回数据格式,确保稳定性和可预测性。常见的模式包括状态码(如`code`或`ret`)、状态信息(`message`或`msg`)、核心数据(`data`)。`success`字段提供了一种直观判断接口是否成功的标志。例如:
37 0
|
存储 编译器 C++
【C++从0到王者】第三十一站:map与set(下)
【C++从0到王者】第三十一站:map与set
56 0
新增节添加代码【滴水逆向三期46笔记】
新增节添加代码【滴水逆向三期46笔记】
|
存储 安全 算法
从“Back to Basic”到伙伴优先,阿里云的组合拳总算整明白了
阿里巴巴最近又活跃了起来——不是在天猫,也不是在支付宝,而是在技术端。 5月26日,阿里云发布了2022财年财报,营收首次超过千亿达到1001.8亿元,同时首次实现年度盈利(11.46亿元); 6月13日,阿里云智能总裁张建锋在2022年阿里云峰会上发布年度策略“Back to Basic”,发布了云数据中心专用处理器CIPU,提出要在技术长征路上不懈努力赢取新的突破;
349 0
|
C++
爱上c++的第十天:共用数据的保护(const)
最近事情还是比较多,我的心态也出现了一些小问题,目前也在积极的调整,想学的东西太多,但是又不是那么容易学,不可避免的产生了一些消极的态度,老师也是对我很好,帮助我调整状态,很快我也会慢慢的持续变好,我自己也相信自己。好了,开始我们今天的学习。
98 0
PancakeBot ,把薄饼做成任何你想要的样子 | 酷玩精选
也许你已经厌倦了传统的薄饼的单一样式,那是时候做出改变了,正巧 PancakeBot 可以帮你改变这一切。
158 0
PancakeBot ,把薄饼做成任何你想要的样子 | 酷玩精选
|
前端开发
前端百题斩【020】——竟然有五种方式实现flat方法
前端百题斩【020】——竟然有五种方式实现flat方法