QVector的內存分配策略
阿新 • • 發佈:2017-05-04
結果 註意 才幹 sse qtp cati 新的 cpp location
我們都知道 STL std::vector 作為動態數組在所分配的內存被填滿時。假設繼續加入數據,std::vector 會另外申請一個大小當前容量兩倍的區域(假設 n > size 則申請 n+當前容量 的空間)。然後把當前內容復制到新的內存,以達到動態擴容的效果:
最直觀的方式是寫個客戶程序看看:
輸出例如以下。capacity 每次擴張為之前容量的兩倍:
相似的,Qt在其 QTL 中也實現了相似的QVector,為了更方便地服務為 Qt 應用服務。它提供了隱式共享。寫時復制等機制。並同一時候提供了 Java Style 和 C++ Style 的接口,相同功能的接口也就是換了個名字而已:
那麽,在 QVector 所分配的內存被填滿時。它的內存又是以何種方式擴充的呢?我們能夠在源代碼中一探到底:先看看 QVector::append():
isDetached()調用一個引用計數,用來推斷該QVector是否獨立(未被隱式共享)。假設該 QVector 是被共享的。那麽我們此時想要在這個已被我們“復制”的 QVector 上調用 append() 時,當然須要真正分配一段新的內存並在該內存上進行加入元素的操作。也就是所謂的“寫時復制”。
isTooSmall 則告訴我們當前szie加 1 之後是否超出了當前容量(d->alloc),假設是相同須要調用 reallocData 開始申請內存。
QVector::reallocData()函數調用了QTypedArrayData::allocate(),前者運行了begin(),end()等指針的又一次指向,原內存釋放等工作。後者實際調用了 QArrayData::allocate(),其函數原型為:
這裏的 Q_ALIGNOF(AlignmentDummy) 十分關鍵。AlignmentDummy是以下這種一個class:
QArrayData 是 Qt 全部連續型容器實際存放數據的地方。包括以下幾個數據成員,也就是說。在32位機器上(以下以此為默認環境),sizeof(QArrayData) 通常是16個字節長度:
而 Q_ALIGNOF 在 gcc 下是 __alignof__ 的別名。而在MSVC下則為 __alignof。用來獲得 AlignmentDummy 的內存對齊大小。由上面的數據成員能夠知道 Q_ALIGNOF(QArrayData) 的值為4。當 Q_ALIGNOF(AlignmentDummy) 大於4 時。意味著該 QArrayData 的成員變量所占內存空間與實際 T 型數據間因為內存對齊將會存在間隙(padding),因此我們須要額外多申請 padding 的空間才幹保證全部數據都能夠被正確安放。
理解這一點後,我們就能夠來看看QArrayData::allocate()
qAllocMore() 實如今 qbyteArray.cpp 文件裏,這個函數返回一個整型數,返回數據內容所需的字節數:
函數開頭告訴我們假設申請字節不能超過 2^30 - extra。註意這裏的 extra 就是我們在上面求到的 sizeof(QArrayData) + sizeof(padding)。
撥開雲霧見青天的時候最終要到了,回到我們最初的問題:QVector 在滿容量之後繼續插入,其內存增長策略怎樣?依照我們前面所示。大家心裏或許有了答案:QVector的所申請內存大小依照 2^n 增長,也就是 2, 4, 8, 16, 32...OK,寫測試代碼的時候到了:
輸入例如以下:
似乎有些奇怪。容量(占用內存為 capacity * sizeof(int))並非 2 的 n 次方?還記得QArrayData類中的數據成員所占用的 sizeof(QArrayData) = 16 嗎,正是這 16 個字節占用了我們這個QVector<int>的 4 個容量。也就是說。這個QVector<int>實際的容量應該為:
如今我們再考慮帶有 padding 的情況,當我們創建一個 QVector<quint64> 時,因為內存對齊的關系,QArrayData的數據成員與實際存儲數據之間應該存在間隙,導致不可用的空間超過 16 字節:能夠看到,實際空間占用比容量大了 3*8 = 24bytes,當中 16bytes 為 headerSize,余下 8bytes 則為間隙了。這樣應該非常清晰了吧(●‘?‘●)
那麽,這個分配策略和 STL std::vector 的差異主要在哪呢,不也是每次翻倍嗎?使用int作為數組數據類型,直接給個輸出結果哈:
相同向 100 個容量的滿數組中加入一個數據,QVector擴容將申請 128*4 (124*4 數據容量 + 4*4個字節的headerSize) 個字節,而 std::vector 將申請 200*4 個字節。能夠預見,下次增長QVector將申請256*4個字節。而std::vector將申請400*4個字節。至於優劣。大家仁者見仁。智者見智咯。就先到這裏吧~
size_type _M_check_len(size_type __n, const char* __s) const { if (max_size() - size() < __n) __throw_length_error(__N(__s)); const size_type __len = size() + std::max(size(), __n); return (__len < size() || __len > max_size()) ?max_size() : __len; }
最直觀的方式是寫個客戶程序看看:
vector<int> ve(4, 8); cout << "size : " << ve.size() << " capacity : " << ve.capacity() << endl; for ( int i = 0; i < 14; ++i ) { ve.push_back(9); ve.push_back(0); cout << "size : " << ve.size() << " capacity : " << ve.capacity() << endl; }
輸出例如以下。capacity 每次擴張為之前容量的兩倍:
相似的,Qt在其 QTL 中也實現了相似的QVector,為了更方便地服務為 Qt 應用服務。它提供了隱式共享。寫時復制等機制。並同一時候提供了 Java Style 和 C++ Style 的接口,相同功能的接口也就是換了個名字而已:
inline void push_back(const T &t) { append(t); }
那麽,在 QVector 所分配的內存被填滿時。它的內存又是以何種方式擴充的呢?我們能夠在源代碼中一探到底:先看看 QVector::append():
const bool isTooSmall = uint(d->size + 1) > d->alloc; if (!isDetached() || isTooSmall) { QArrayData::AllocationOptions opt(isTooSmall ?QArrayData::Grow : QArrayData::Default); reallocData(d->size, isTooSmall ? d->size + 1 : d->alloc, opt); }
isDetached()調用一個引用計數,用來推斷該QVector是否獨立(未被隱式共享)。假設該 QVector 是被共享的。那麽我們此時想要在這個已被我們“復制”的 QVector 上調用 append() 時,當然須要真正分配一段新的內存並在該內存上進行加入元素的操作。也就是所謂的“寫時復制”。
isTooSmall 則告訴我們當前szie加 1 之後是否超出了當前容量(d->alloc),假設是相同須要調用 reallocData 開始申請內存。
因為內存分配可能是由寫時復制策略調用,因此依據 isTooSmall 參數的不同。reallocData()的參數也不同。
QVector::reallocData()函數調用了QTypedArrayData::allocate(),前者運行了begin(),end()等指針的又一次指向,原內存釋放等工作。後者實際調用了 QArrayData::allocate(),其函數原型為:
static QTypedArrayData *allocate(size_t capacity, AllocationOptions options = Default) Q_REQUIRED_RESULT { Q_STATIC_ASSERT(sizeof(QTypedArrayData) == sizeof(QArrayData)); return static_cast<QTypedArrayData *>(QArrayData::allocate(sizeof(T), Q_ALIGNOF(AlignmentDummy), capacity, options)); }
這裏的 Q_ALIGNOF(AlignmentDummy) 十分關鍵。AlignmentDummy是以下這種一個class:
class AlignmentDummy { QArrayData header; T data; };
QArrayData 是 Qt 全部連續型容器實際存放數據的地方。包括以下幾個數據成員,也就是說。在32位機器上(以下以此為默認環境),sizeof(QArrayData) 通常是16個字節長度:
QtPrivate::RefCount ref; int size; uint alloc : 31; uint capacityReserved : 1; qptrdiff offset; // in bytes from beginning of header
而 Q_ALIGNOF 在 gcc 下是 __alignof__ 的別名。而在MSVC下則為 __alignof。用來獲得 AlignmentDummy 的內存對齊大小。由上面的數據成員能夠知道 Q_ALIGNOF(QArrayData) 的值為4。當 Q_ALIGNOF(AlignmentDummy) 大於4 時。意味著該 QArrayData 的成員變量所占內存空間與實際 T 型數據間因為內存對齊將會存在間隙(padding),因此我們須要額外多申請 padding 的空間才幹保證全部數據都能夠被正確安放。
理解這一點後,我們就能夠來看看QArrayData::allocate()
QArrayData *QArrayData::allocate(size_t objectSize, size_t alignment, size_t capacity, AllocationOptions options) { // 檢測aligment是否為2的階數倍 Q_ASSERT(alignment >= Q_ALIGNOF(QArrayData) && !(alignment & (alignment - 1))); ... // 獲取 QArrayData 類為空時的大小 size_t headerSize = sizeof(QArrayData); // 申請額外的 alignment-Q_ALIGNOF(QArrayData)大小的 padding 字節數 // 這樣就能將數據放在合適的位置上 if (!(options & RawData)) headerSize += (alignment - Q_ALIGNOF(QArrayData)); // 假設數組長度超出容量則申請新的內存 if (options & Grow) capacity = qAllocMore(int(objectSize * capacity), int(headerSize)) / int(objectSize); //一共須要申請的字節數 size_t allocSize = headerSize + objectSize * capacity; QArrayData *header = static_cast<QArrayData *>(::malloc(allocSize)); if (header) { ... } return header; }
qAllocMore() 實如今 qbyteArray.cpp 文件裏,這個函數返回一個整型數,返回數據內容所需的字節數:
int qAllocMore(int alloc, int extra) { Q_ASSERT(alloc >= 0 && extra >= 0); Q_ASSERT_X(alloc < (1 << 30) - extra, "qAllocMore", "Requested size is too large!"); unsigned nalloc = alloc + extra; // Round up to next power of 2 // Assuming container is growing, always overshoot //--nalloc; nalloc |= nalloc >> 1; nalloc |= nalloc >> 2; nalloc |= nalloc >> 4; nalloc |= nalloc >> 8; nalloc |= nalloc >> 16; ++nalloc; Q_ASSERT(nalloc > unsigned(alloc + extra)); return nalloc - extra; }
函數開頭告訴我們假設申請字節不能超過 2^30 - extra。註意這裏的 extra 就是我們在上面求到的 sizeof(QArrayData) + sizeof(padding)。
alloc是我們存放實際數據區域的大小,nalloc即為我們總共須要的新內存容量。
以下的幾排移位算法假設大家眼熟的話應該知道得到的 nalloc 的新值為比其原值大的一個近期的 2 的階乘數。比方輸入20。經過最後一步 ++nalloc 操作後,nalloc將變成 32。撥開雲霧見青天的時候最終要到了,回到我們最初的問題:QVector 在滿容量之後繼續插入,其內存增長策略怎樣?依照我們前面所示。大家心裏或許有了答案:QVector的所申請內存大小依照 2^n 增長,也就是 2, 4, 8, 16, 32...OK,寫測試代碼的時候到了:
QVector<int> ve(2, 8); qDebug() << "size : " << ve.size() << " capacity : " << ve.capacity(); for ( int i = 0; i < 20; ++i ) { ve.append(9); qDebug() << "size : " << ve.size() << " capacity : " << ve.capacity(); }
輸入例如以下:
似乎有些奇怪。容量(占用內存為 capacity * sizeof(int))並非 2 的 n 次方?還記得QArrayData類中的數據成員所占用的 sizeof(QArrayData) = 16 嗎,正是這 16 個字節占用了我們這個QVector<int>的 4 個容量。也就是說。這個QVector<int>實際的容量應該為:
如今我們再考慮帶有 padding 的情況,當我們創建一個 QVector<quint64> 時,因為內存對齊的關系,QArrayData的數據成員與實際存儲數據之間應該存在間隙,導致不可用的空間超過 16 字節:能夠看到,實際空間占用比容量大了 3*8 = 24bytes,當中 16bytes 為 headerSize,余下 8bytes 則為間隙了。這樣應該非常清晰了吧(●‘?‘●)
那麽,這個分配策略和 STL std::vector 的差異主要在哪呢,不也是每次翻倍嗎?使用int作為數組數據類型,直接給個輸出結果哈:
相同向 100 個容量的滿數組中加入一個數據,QVector擴容將申請 128*4 (124*4 數據容量 + 4*4個字節的headerSize) 個字節,而 std::vector 將申請 200*4 個字節。能夠預見,下次增長QVector將申請256*4個字節。而std::vector將申請400*4個字節。至於優劣。大家仁者見仁。智者見智咯。就先到這裏吧~
QVector的內存分配策略