1. 數組大局觀
數組是一個引用類型,也就是意味着數組的記憶體配置設定在托管堆上,并且我們在棧上維護的是他的指針而并非真正的數組。接下來我們分析下數組的元素,其中的元素無外乎是引用類型和值類型。當數組中的元素是值類型時,不同于int i;這樣的代碼。數組會根據數組的大小自動把元素的值初始化為他的預設值。例如:
結果如下:
當數組中的元素是引用類型時,實際上數組中的元素是一個指向對象實際記憶體空間的指針,占用4Bytes的空間。
2. 談談零基數組
從學C語言時起,相信老師就會對我們講,數組的第一個索引是0,而不是1。但是在C#中,我們可以去構造一個非零基數組,在這一節,我們就來把這個說透。
在正常意義上,我們初始化一個數組,都預設是零基數組,這也使得數組成為了字元串後再一個初始化時特殊的類型。正如我們知道的一樣,初始化一個字元串時,對應的IL指令是newstr,同樣,初始化一個零基數組對應的IL指令是newarr。
當我們希望構造一個非零基數組時,我們可以以下的語句來做到:
<a href="http://11011.net/software/vspaste"></a>
得到的測試結果便如下:
<a href="http://images.cnblogs.com/cnblogs_com/kym/WindowsLiveWriter/CLRviaCArray_F3FC/image_4.png"></a>
于是便證明,我們初始化了一個非零基數組。此外,延伸一下,我們還應該通過這個記住以下兩個方法:
得到的測試結果如下:
<a href="http://images.cnblogs.com/cnblogs_com/kym/WindowsLiveWriter/CLRviaCArray_F3FC/image_6.png"></a>
3. 談談效率問題
相信會有好多陰謀論者說,C#是個類型安全的語言,也就是意味着我循環時每次通路一次數組的元素,那麼就要檢查一次該索引是否會造成數組越界,于是就造成了一定的性能損失。那麼在這裡,我們就把這個問題說透。
我們在這裡把數組分成零基數組,非零基數組,多元數組,交錯數組四種情況來分别讨論這個問題。
零基數組是.NET中提倡使用的類型,并且初始化時提供了特殊的IL指令newarr則充分說明了他在.NET中的特殊性,自然.NET Framework也會為其提供很大的優化待遇。在循環通路數組時,如這樣的代碼:
JIT編譯器隻會在循環開始之前檢查一次4和intArr.GetUpperBound的大小關系,之後便不會對其進行幹預。也就是說JIT編譯器隻對其檢查一次安全,是以帶來的性能損失是非常小的。
而對于非零基數組,我們來比較這樣兩段代碼:
其實兩者建立的幾乎是相同的數組,調用的也幾乎是一樣的方法,但是我們看下IL卻會發現兩者有着驚人的不同,首先是非零基數組的IL:
<a href="http://images.cnblogs.com/cnblogs_com/kym/WindowsLiveWriter/CLRviaCArray_F3FC/image_8.png"></a>
接下來是零基數組的:
<a href="http://images.cnblogs.com/cnblogs_com/kym/WindowsLiveWriter/CLRviaCArray_F3FC/image_10.png"></a>
我們可以發現,對于非零基數組中的大部分操作,.NET Framework都提供了對應的IL指令,我們也可以了解為.NET Framework為其提供了特殊的優化。
當然,實際上,正如CLR via C#所說的一樣:.NET Framework對應非零基數組沒有任何方面的優化,每次通路都需要檢查其上限和下限與索引之間的關系。效率的損耗是必然的。
事實上,當我們測試這樣一段代碼時,也會發現其實零基數組和非零基數組的差別是很大的:
得到的結果如下:
<a href="http://images.cnblogs.com/cnblogs_com/kym/WindowsLiveWriter/CLRviaCArray_F3FC/image_12.png"></a>
接下來我們再來簡單地說下多元數組和交錯數組。
多元數組和非零基數組一樣,都沒有受到.NET Framework的特殊優待。
而交錯數組,其實就是數組中的數組,是以效率實際上取決于數組中的數組是零基數組還是非零基數組。
那接下來的一節,我們來具體探讨一下交錯數組和多元數組的差別和應用。
4. 多元數組和交錯數組
考慮到兩個詞的翻譯問題,在這裡給出兩個詞的英文:
多元數組:Multi-dimensional Array。
交錯數組:Jagged Array。
好,下面步入正題。
首先從二者的記憶體分布說起。
多元數組是一個整體的數組,是以他在記憶體中占據一個整體的托管堆記憶體塊。
而交錯數組實際上是數組中的數組,是以我們用二維交錯數組來舉例,其記憶體如圖所示:
<a href="http://images.cnblogs.com/cnblogs_com/kym/WindowsLiveWriter/CLRviaCArray_F3FC/image_14.png"></a>
也就是說,如果是一個3*100的數組,也就是說需要初始化101次數組,當數組的元素更加多的時候,那建立和垃圾回收将帶來巨大的效率損失。
是以,也就是說:交錯數組的效率瓶頸在于建立和銷毀上,而并非類型安全檢查上。
于是,我們就可以得出這樣的結論:
當一次建立,多次通路時,我們應該建立交錯數組。
當一次建立,一次通路時,我們應該建立多元數組。
5. 用代碼改善效率
上面說到了,通路非零基數組和多元數組的效率是比較低的,對于非零基數組,我們的應用比較少,但是多元數組,相信每個人都或多或少有着一定的應用,那麼面對其性能問題,我們該怎麼辦呢?
我們先來想想,多元數組的通路,性能瓶頸在安全檢查上。在C語言中,為什麼沒有這樣的問題,對,因為C語言不會做這樣的檢查。于是,相信聰明的大家都會想到不安全代碼。
改善多元數組以及非零基數組的效率問題,我們就用不安全代碼。
這裡,我們又見到了C語言中熟悉的指針,相信不需要多加介紹了。這裡唯一需要注意的就是fixed,由于在垃圾回收時采用的是代機制+壓縮機制,是以其記憶體位址很可能發生改變,是以我們應該講數組的記憶體位址鎖住,防止我們通路到其他的記憶體位址而造成我們讀取資料的錯誤。
6. 對零基數組的精益求精
當然,即使是零基數組,我們依然在托管堆上為其配置設定了記憶體空間。如果對性能要求極高,我們知道建立一個對象也是有着一定的時間損耗,其中包括配置設定記憶體空間,同步塊索引,以及指向下一塊記憶體空間的指針等一系列複雜的操作。那麼我們就放棄掉托管堆這個東東,而直接在棧中來建立這個數組,這樣又省去了很多時間,進而達到了和C語言相同的效果,代碼如下:
這樣,效率就進一步提高了,對于二維數組,我們一樣可以如此建立。其代碼與C語言完全等同。我在這裡就不繼續示範了。
7. 總結
在全文中,主要是對數組的各個方面做一個比較簡略地介紹,其中包括數組的基礎知識,分類,以及效率性能問題,最後就是用不安全代碼來通路建立數組來提高性能。
不過最後說一句,在實際工作中,如果對性能沒有特别高的要求,則沒必要用不安全代碼來操作數組,因為其很可能因為你的一些失誤而帶來其他的一些安全問題,并且對代碼的可讀性也是個比較大的傷害,這就有些得不償失了。