.net提供了List對象來提供可擴容數據存儲,但在使用的過程中相信很多人直接通過默認構造函數進行創建。但這樣做會存在一定的風險導致Lis在擴容過程增加CPU的損耗和GC的壓力,對於問題的嚴重性就取決於實際應用的場合,如果在高並發的應用下存在大量這操作那問題就變得嚴重多了。
首先需要了解一下List的存儲機制,在初始化的時候不指定大小的情況是默認分配大小為4的數組,當在添加信息超過該值的情況會進行一個倍分擴容,默認的規則是4,8,16,32...;擴展容的過程中是會構建擴展后大小的數組,並把舊的數據復制過去。ArrayList和List<T>實際的代碼大概如下:
public virtual int Add(object value) { if (this._size == this._items.Length) { this.EnsureCapacity(this._size + 1); } this._items[this._size] = value; this._version++; return this._size++; } private void EnsureCapacity(int min) { if (this._items.Length < min) { int num = (this._items.Length == 0) ? 4 : (this._items.Length * 2); if (num < min) { num = min; } this.Capacity = num; } } public virtual int Capacity { get { return this._items.Length; } set { if (value != this._items.Length) { if (value < this._size) { throw new ArgumentOutOfRangeException("value", Environment.GetResourceString("ArgumentOutOfRange_SmallCapacity")); } if (value > 0) { object[] array = new object[value]; if (this._size > 0) { Array.Copy(this._items, 0, array, 0, this._size); }; this._items = array; return; } this._items = new object[4]; } } }
通過代碼可以明確知道當擴展的時候的確存在創建新數據組和復制,這樣問題就來了舉一個簡單的應用場景數據查詢分頁返回List,假設每頁有20條記錄,當我們默認構建List的時候添加20個對象的情況,這個List就要面對3次擴容操作分別是8,16,32. 為了測試這情況分別列舉出兩中情況的代碼
static void Test1(object state) { for (int i = 0; i < count; i++) { var items = new List<TestObj>(); for (int k = 0; k < 20; k++) { items.Add(new TestObj()); } } } static void Test2(object state) { for (int i = 0; i < count; i++) { var items = new List<TestObj>(20); for (int k = 0; k < 20; k++) { items.Add(new TestObj()); } } }
以上兩個方法操作在計時上差上好倍,測試時間還不包括GC上的損耗,當在高並發的情況那GC的壓力所導致的性能的損失遠遠不止這個數。 其實在.net中可擴容存儲的對象大部分都存在這問題,面對這些問題.net在這些類的使用上也預留一些構造方法來滿足實際應用所面對的情況。
當你在寫.net程序的時候發現性能上的問題,多看一下.net的相關代碼,其實很多情況由於自己的不了解從而導致那樣的結果。以上緊緊是一個計時測試,在實際情況中面對這問題我們還要做更多的測試才能找出原因,內存,GC等往往是.NET性能的殺手。