对.net了解更多点【习惯对List初始化的时候指定容量】
.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性能的杀手。
访问Beetlex的Github