STL学习笔记1(CArray与vector)

 

先做一个有趣的实验,测试一下 CArray和 vector添加数据的效率:

结果 (VS2005, release,默认优化 O2):

 

 

可以看到,当需要添加大量数据时, CArray明显比 vector慢。

 

测试代码:

  1. const int TEST_CASE_SIZE = 5;  
  2. long len[TEST_CASE_SIZE];  
  3. long time_used[2][TEST_CASE_SIZE];   
  4. CalcuUsedTime ct;  
  5. { //Init data length   
  6.     len[0] = 100;  
  7.     for (int i=1; i<TEST_CASE_SIZE; ++i)  
  8.         len[i] = len[i-1] * 10;  
  9. }  
  10. { //Test CArray   
  11.     for (int i=0; i<TEST_CASE_SIZE; ++i)  
  12.     {  
  13.         CArray<int, int> arr;  
  14.         ct.SetStartTimeSpot();  
  15.         for (int j=0; j<len[i]; ++j)  
  16.             arr.Add (j);  
  17.         time_used[0][i] = ct.LookForTimeUsed();  
  18.     }  
  19. }  
  20. { //Test vector   
  21.     for (int i=0; i<TEST_CASE_SIZE; ++i)  
  22.     {  
  23.         vector<int> arr;  
  24.         ct.SetStartTimeSpot();  
  25.         for (int j=0; j<len[i]; ++j)  
  26.             arr.push_back (j);  
  27.         time_used[1][i] = ct.LookForTimeUsed();  
  28.     }  
  29. }  
  30. { //Output result   
  31.     string strResult[3];  
  32.     strResult[0] = "Data Length : ";  
  33.     strResult[1] = "CArray(ms)  : ";  
  34.     strResult[2] = "vector(ms)  : ";  
  35.     const int VALUE_LEN = 10;  
  36.     char value[VALUE_LEN];  
  37.     for (int i=0; i<TEST_CASE_SIZE; ++i)  
  38.     {  
  39.         sprintf_s (value, VALUE_LEN, "%7d  ", len[i]);  
  40.         strResult[0] += value;  
  41.         sprintf_s (value, VALUE_LEN, "%7d  ", time_used[0][i]);  
  42.         strResult[1] += value;  
  43.         sprintf_s (value, VALUE_LEN, "%7d  ", time_used[1][i]);  
  44.         strResult[2] += value;  
  45.     }  
  46.     cout << strResult[0].c_str() << endl;  
  47.     cout << strResult[1].c_str() << endl;  
  48.     cout << strResult[2].c_str() << endl;  
  49. }  

 

  1. #ifdef _WIN32   
  2. #include <mmsystem.h>   
  3. #pragma comment( lib, "winmm" )   
  4. #endif   
  5. //计算时间消耗的类   
  6. class CalcuUsedTime  
  7. {  
  8. public:  
  9.     CalcuUsedTime()   
  10.     {  
  11. #ifdef _WIN32   
  12.         timeBeginPeriod(1);  
  13. #endif   
  14.         SetStartTimeSpot();  
  15.     };  
  16.     ~CalcuUsedTime()   
  17.     {  
  18. #ifdef _WIN32   
  19.         timeEndPeriod(1);  
  20. #endif   
  21.     };  
  22.     void SetStartTimeSpot()   
  23.     {  
  24.         time_spot[0] = GetCurrentTime();  
  25.     }  
  26.     long LookForTimeUsed()  
  27.     {  
  28.         time_spot[1] = GetCurrentTime();  
  29.         return (time_spot[1] - time_spot[0]);  
  30.     }  
  31. private:  
  32.     CalcuUsedTime (const CalcuUsedTime&);  
  33.     CalcuUsedTime& operator= (const CalcuUsedTime&);  
  34.     long GetCurrentTime()  
  35.     {  
  36.         long curTime;  
  37. #ifdef _WIN32   
  38.         curTime = timeGetTime();  
  39. #else   
  40.         curTime = clock();  
  41. #endif   
  42.         return curTime;  
  43.     }  
  44.     long time_spot[2];  
  45. };  

 

为什么会这样呢,研究一下 CArray::Add和 vector::push_back的内部实行,可以发现一些有趣的东西。

CArray和 vector都在内部维护一个数组,当添加新的元素时总数据大小超过原来数组长度时,它们都会在开辟一个新的更大的数组,把原来数组内容拷贝过来,再在后面附上新的元素。

 

在 CArray中,这一操作最终是通过以下函数实现的:

[c-sharp] view plain copy print ?
  1. void CArray<TYPE>::SetSize(int nNewSize, int nGrowBy)  
  2. {  
  3.     //......   
  4.     if (nGrowBy == 0)  
  5.     {  
  6.         nGrowBy = m_nSize / 8;  
  7.         nGrowBy = (nGrowBy < 4) ? 4 : ((nGrowBy > 1024) ? 1024 : nGrowBy);  
  8.     }  
  9.     int nNewMax = max (nNewSize, m_nMaxSize + nGrowBy);  
  10.     TYPE* pNewData = (TYPE*) new BYTE[(size_t)nNewMax * sizeof(TYPE)];  
  11.     // copy new data from old   
  12.     ::ATL::Checked::memcpy_s(pNewData, (size_t)nNewMax * sizeof(TYPE),  
  13.         m_pData, (size_t)m_nSize * sizeof(TYPE));  
  14.     memset((void*)(pNewData + m_nSize), 0, (size_t)(nNewSize-m_nSize) * sizeof(TYPE));  
  15.     for( int i = 0; i < nNewSize-m_nSize; i++ )  
  16. #pragma push_macro("new")  
  17. #undef new   
  18.         ::new( (void*)( pNewData + m_nSize + i ) ) TYPE;  
  19. #pragma pop_macro("new")   
  20.     delete[] (BYTE*)m_pData;  
  21.     m_pData = pNewData;  
  22.     m_nSize = nNewSize;  
  23.     m_nMaxSize = nNewMax;  
  24.     //......   
  25. }  

 

从上面可以看到,当数组需要增大时,其增长幅度根据当前数组长度而定,但在 [4, 1024]这个范围内,这样当不断循环 Add数据时,循环较大的话这个开辟新数组的操作次数会相当多,结果就是速度较慢。

 

而在 vector中,其实现如下:

[c-sharp] view plain copy print ?
  1. void _Insert_n(iterator _Where, size_type _Count, const _Ty& _Val)  
  2. {  
  3.     //......   
  4.     // not enough room, reallocate   
  5.     _Capacity = max_size() - _Capacity / 2 < _Capacity  
  6.         ? 0 : _Capacity + _Capacity / 2;    // try to grow by 50%   
  7.     if (_Capacity < size() + _Count)  
  8.         _Capacity = size() + _Count;  
  9.     pointer _Newvec = this->_Alval.allocate(_Capacity);  
  10.     pointer _Ptr = _Newvec;  
  11.     //......   
  12.     _Ptr = _Umove(_Myfirst, _VEC_ITER_BASE(_Where), _Newvec); // copy prefix   
  13.     _Ptr = _Ufill(_Ptr, _Count, _Tmp);  // add new stuff   
  14.     _Umove(_VEC_ITER_BASE(_Where), _Mylast, _Ptr);  // copy suffix   
  15.     //......   
  16.     _Destroy(_Myfirst, _Mylast);  
  17.     this->_Alval.deallocate(_Myfirst, _Myend - _Myfirst);  
  18.     _Myfirst = _Newvec;  
  19. }  

 

可以看出, vector每次增长时,都增长当前长度的一半,典型的以空间换时间!难怪速度这么快!

 

一般说来,当数组已经很大时,如果还需要往数组中插入数据,可以预期,这种操作还会发生多次,所以一次开辟更大的数组是一个比较好的选择,所以, vector的增长算法更合理些。

 

 

另外,在代码中还有些有趣的地方,在增大新数组时, CArray是以以下方式进行的:

 

  1. TYPE* pNewData = (TYPE*) new BYTE[(size_t)nNewMax * sizeof(TYPE)];  
  2. ::ATL::Checked::memcpy_s(pNewData, (size_t)nNewMax * sizeof(TYPE),  
  3. m_pData, (size_t)m_nSize * sizeof(TYPE));  
  4. memset((void*)(pNewData + m_nSize), 0, (size_t)(nNewSize-m_nSize) * sizeof(TYPE));  
  5. for( int i = 0; i < nNewSize-m_nSize; i++ )  
  6. ::new( (void*)( pNewData + m_nSize + i ) ) TYPE;      
  7. delete[] (BYTE*)m_pData;  
  8. //......   
  9. m_pData[nIndex] = newElement;  

 

可以看到其实现过程:

  1. 以 new BYTE[]方式开辟一段内存
  2. 通过 memcpy_s 拷贝以前的数组内容
  3. 通过 ::new ( (void *)( pNewData + m_nSize + i ) ) TYPE 的方式对新元素调用默认构造函数
  4. 通过 delete [] (BYTE*)m_pData 释放以前的数组
  5. 最后通过 m_pData[nIndex] = newElement 调用拷贝构造函数来给新元素附值

这些操作的组合,避免了在开辟新数组和释放旧数组时不会反复调用构造函数和析构函数,并保证数组内对象元素内部的指针类型成员变量所指向的内存地址保持不变,保证函数调用前后内部的一致性,可谓是用心良苦啊!

 

那么, vector又是怎么做到这一点的呢?我们先看看代码

 

  1. pointer _Newvec = this->_Alval.allocate(_Capacity);  
  2. pointer _Ptr = _Newvec;  
  3. _Ptr = _Umove(_Myfirst, _VEC_ITER_BASE(_Where), _Newvec);//copy prefix   
  4. _Ptr = _Ufill(_Ptr, _Count, _Tmp);  // add new stuff   
  5. _Umove(_VEC_ITER_BASE(_Where), _Mylast, _Ptr);  // copy suffix   
  6. _Destroy(_Myfirst, _Mylast);  
  7. this->_Alval.deallocate(_Myfirst, _Myend - _Myfirst);  

 

STL的代码真的好难看懂,模板泛型用到了极致,对上面每一个函数跟进去,最终可以得到这样的结论:

  1. 通过 this ->_Alval.allocate(_Capacity); 开辟新数组内存,而他最终调用 ((_Ty *)::operator new (_Count * sizeof (_Ty))) 实现 ;
  2. 通过 _Umove(_Myfirst, _VEC_ITER_BASE(_Where), _Newvec) 拷贝原数组内容,底层调用 memmove(dst, src, count) 实现 ;
  3. 通过 _Ufill(_Ptr, _Count, _Tmp)初始化新元素,底层调用 *_First = _ Val 也就是最终还是调用拷贝构造函数
  4. 通过 this ->_Alval. deallocate释放旧数组内存,底层调用 ::operator delete (_Ptr) , 其中 _Destroy(_Myfirst, _Mylast) 底层最终为空函数。

 

通过上面分析可以发现 vector和 CArray的处理过程几乎完全一致。

 

另外,如果事先知道数组的大小或大概大小,可以调用接口预先分配数组,再给数组元素附值。 CArray提供了 SetSize 接口可以预先分配数组。使用如下

 

  1. const int LEN = 1000000;  
  2. CArray<int, int> arr;  
  3. arr.SetSize(LEN);  
  4. for (int i=0; i<LEN; ++i)  
  5.     arr[i] = i;  

 

而 vector提供了两种方式处理:

 

  1. const int LEN = 1000000;  
  2. vector<int> arr1, arr2;  
  3. arr1.resize (LEN, 0);     
  4. for (int i=0; i<LEN; ++i)  
  5.     arr1[i] = i;  
  6. arr2.reserve (LEN);  
  7. for (int i=0; i<LEN; ++i)  
  8.     arr2.push_back (i);  

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值