先做一个有趣的实验,测试一下 CArray和 vector添加数据的效率:
结果 (VS2005, release,默认优化 O2):
可以看到,当需要添加大量数据时, CArray明显比 vector慢。
测试代码:
- const int TEST_CASE_SIZE = 5;
- long len[TEST_CASE_SIZE];
- long time_used[2][TEST_CASE_SIZE];
- CalcuUsedTime ct;
- { //Init data length
- len[0] = 100;
- for (int i=1; i<TEST_CASE_SIZE; ++i)
- len[i] = len[i-1] * 10;
- }
- { //Test CArray
- for (int i=0; i<TEST_CASE_SIZE; ++i)
- {
- CArray<int, int> arr;
- ct.SetStartTimeSpot();
- for (int j=0; j<len[i]; ++j)
- arr.Add (j);
- time_used[0][i] = ct.LookForTimeUsed();
- }
- }
- { //Test vector
- for (int i=0; i<TEST_CASE_SIZE; ++i)
- {
- vector<int> arr;
- ct.SetStartTimeSpot();
- for (int j=0; j<len[i]; ++j)
- arr.push_back (j);
- time_used[1][i] = ct.LookForTimeUsed();
- }
- }
- { //Output result
- string strResult[3];
- strResult[0] = "Data Length : ";
- strResult[1] = "CArray(ms) : ";
- strResult[2] = "vector(ms) : ";
- const int VALUE_LEN = 10;
- char value[VALUE_LEN];
- for (int i=0; i<TEST_CASE_SIZE; ++i)
- {
- sprintf_s (value, VALUE_LEN, "%7d ", len[i]);
- strResult[0] += value;
- sprintf_s (value, VALUE_LEN, "%7d ", time_used[0][i]);
- strResult[1] += value;
- sprintf_s (value, VALUE_LEN, "%7d ", time_used[1][i]);
- strResult[2] += value;
- }
- cout << strResult[0].c_str() << endl;
- cout << strResult[1].c_str() << endl;
- cout << strResult[2].c_str() << endl;
- }
- #ifdef _WIN32
- #include <mmsystem.h>
- #pragma comment( lib, "winmm" )
- #endif
- //计算时间消耗的类
- class CalcuUsedTime
- {
- public:
- CalcuUsedTime()
- {
- #ifdef _WIN32
- timeBeginPeriod(1);
- #endif
- SetStartTimeSpot();
- };
- ~CalcuUsedTime()
- {
- #ifdef _WIN32
- timeEndPeriod(1);
- #endif
- };
- void SetStartTimeSpot()
- {
- time_spot[0] = GetCurrentTime();
- }
- long LookForTimeUsed()
- {
- time_spot[1] = GetCurrentTime();
- return (time_spot[1] - time_spot[0]);
- }
- private:
- CalcuUsedTime (const CalcuUsedTime&);
- CalcuUsedTime& operator= (const CalcuUsedTime&);
- long GetCurrentTime()
- {
- long curTime;
- #ifdef _WIN32
- curTime = timeGetTime();
- #else
- curTime = clock();
- #endif
- return curTime;
- }
- long time_spot[2];
- };
为什么会这样呢,研究一下 CArray::Add和 vector::push_back的内部实行,可以发现一些有趣的东西。
CArray和 vector都在内部维护一个数组,当添加新的元素时总数据大小超过原来数组长度时,它们都会在开辟一个新的更大的数组,把原来数组内容拷贝过来,再在后面附上新的元素。
在 CArray中,这一操作最终是通过以下函数实现的:
- void CArray<TYPE>::SetSize(int nNewSize, int nGrowBy)
- {
- //......
- if (nGrowBy == 0)
- {
- nGrowBy = m_nSize / 8;
- nGrowBy = (nGrowBy < 4) ? 4 : ((nGrowBy > 1024) ? 1024 : nGrowBy);
- }
- int nNewMax = max (nNewSize, m_nMaxSize + nGrowBy);
- TYPE* pNewData = (TYPE*) new BYTE[(size_t)nNewMax * sizeof(TYPE)];
- // copy new data from old
- ::ATL::Checked::memcpy_s(pNewData, (size_t)nNewMax * sizeof(TYPE),
- m_pData, (size_t)m_nSize * sizeof(TYPE));
- memset((void*)(pNewData + m_nSize), 0, (size_t)(nNewSize-m_nSize) * sizeof(TYPE));
- for( int i = 0; i < nNewSize-m_nSize; i++ )
- #pragma push_macro("new")
- #undef new
- ::new( (void*)( pNewData + m_nSize + i ) ) TYPE;
- #pragma pop_macro("new")
- delete[] (BYTE*)m_pData;
- m_pData = pNewData;
- m_nSize = nNewSize;
- m_nMaxSize = nNewMax;
- //......
- }
从上面可以看到,当数组需要增大时,其增长幅度根据当前数组长度而定,但在 [4, 1024]这个范围内,这样当不断循环 Add数据时,循环较大的话这个开辟新数组的操作次数会相当多,结果就是速度较慢。
而在 vector中,其实现如下:
- void _Insert_n(iterator _Where, size_type _Count, const _Ty& _Val)
- {
- //......
- // not enough room, reallocate
- _Capacity = max_size() - _Capacity / 2 < _Capacity
- ? 0 : _Capacity + _Capacity / 2; // try to grow by 50%
- if (_Capacity < size() + _Count)
- _Capacity = size() + _Count;
- pointer _Newvec = this->_Alval.allocate(_Capacity);
- pointer _Ptr = _Newvec;
- //......
- _Ptr = _Umove(_Myfirst, _VEC_ITER_BASE(_Where), _Newvec); // copy prefix
- _Ptr = _Ufill(_Ptr, _Count, _Tmp); // add new stuff
- _Umove(_VEC_ITER_BASE(_Where), _Mylast, _Ptr); // copy suffix
- //......
- _Destroy(_Myfirst, _Mylast);
- this->_Alval.deallocate(_Myfirst, _Myend - _Myfirst);
- _Myfirst = _Newvec;
- }
可以看出, vector每次增长时,都增长当前长度的一半,典型的以空间换时间!难怪速度这么快!
一般说来,当数组已经很大时,如果还需要往数组中插入数据,可以预期,这种操作还会发生多次,所以一次开辟更大的数组是一个比较好的选择,所以, vector的增长算法更合理些。
另外,在代码中还有些有趣的地方,在增大新数组时, CArray是以以下方式进行的:
- TYPE* pNewData = (TYPE*) new BYTE[(size_t)nNewMax * sizeof(TYPE)];
- ::ATL::Checked::memcpy_s(pNewData, (size_t)nNewMax * sizeof(TYPE),
- m_pData, (size_t)m_nSize * sizeof(TYPE));
- memset((void*)(pNewData + m_nSize), 0, (size_t)(nNewSize-m_nSize) * sizeof(TYPE));
- for( int i = 0; i < nNewSize-m_nSize; i++ )
- ::new( (void*)( pNewData + m_nSize + i ) ) TYPE;
- delete[] (BYTE*)m_pData;
- //......
- m_pData[nIndex] = newElement;
可以看到其实现过程:
- 以 new BYTE[]方式开辟一段内存
- 通过 memcpy_s 拷贝以前的数组内容
- 通过 ::new ( (void *)( pNewData + m_nSize + i ) ) TYPE 的方式对新元素调用默认构造函数
- 通过 delete [] (BYTE*)m_pData 释放以前的数组
- 最后通过 m_pData[nIndex] = newElement 调用拷贝构造函数来给新元素附值
这些操作的组合,避免了在开辟新数组和释放旧数组时不会反复调用构造函数和析构函数,并保证数组内对象元素内部的指针类型成员变量所指向的内存地址保持不变,保证函数调用前后内部的一致性,可谓是用心良苦啊!
那么, vector又是怎么做到这一点的呢?我们先看看代码
- pointer _Newvec = this->_Alval.allocate(_Capacity);
- pointer _Ptr = _Newvec;
- _Ptr = _Umove(_Myfirst, _VEC_ITER_BASE(_Where), _Newvec);//copy prefix
- _Ptr = _Ufill(_Ptr, _Count, _Tmp); // add new stuff
- _Umove(_VEC_ITER_BASE(_Where), _Mylast, _Ptr); // copy suffix
- _Destroy(_Myfirst, _Mylast);
- this->_Alval.deallocate(_Myfirst, _Myend - _Myfirst);
STL的代码真的好难看懂,模板泛型用到了极致,对上面每一个函数跟进去,最终可以得到这样的结论:
- 通过 this ->_Alval.allocate(_Capacity); 开辟新数组内存,而他最终调用 ((_Ty *)::operator new (_Count * sizeof (_Ty))) 实现 ;
- 通过 _Umove(_Myfirst, _VEC_ITER_BASE(_Where), _Newvec) 拷贝原数组内容,底层调用 memmove(dst, src, count) 实现 ;
- 通过 _Ufill(_Ptr, _Count, _Tmp)初始化新元素,底层调用 *_First = _ Val 也就是最终还是调用拷贝构造函数
- 通过 this ->_Alval. deallocate释放旧数组内存,底层调用 ::operator delete (_Ptr) , 其中 _Destroy(_Myfirst, _Mylast) 底层最终为空函数。
通过上面分析可以发现 vector和 CArray的处理过程几乎完全一致。
另外,如果事先知道数组的大小或大概大小,可以调用接口预先分配数组,再给数组元素附值。 CArray提供了 SetSize 接口可以预先分配数组。使用如下
- const int LEN = 1000000;
- CArray<int, int> arr;
- arr.SetSize(LEN);
- for (int i=0; i<LEN; ++i)
- arr[i] = i;
而 vector提供了两种方式处理:
- const int LEN = 1000000;
- vector<int> arr1, arr2;
- arr1.resize (LEN, 0);
- for (int i=0; i<LEN; ++i)
- arr1[i] = i;
- arr2.reserve (LEN);
- for (int i=0; i<LEN; ++i)
- arr2.push_back (i);