QVector的內存分配策略

咱們都知道 STL std::vector 做爲動態數組在所分配的內存被填滿時。假設繼續加入數據,std::vector 會另外申請一個大小當前容量兩倍的區域(假設 n > size 則申請 n+當前容量 的空間)。而後把當前內容複製到新的內存,以達到動態擴容的效果:
   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()的參數也不一樣。app


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個字節。至於優劣。你們仁者見仁。智者見智咯。

就先到這裏吧~
相關文章
相關標籤/搜索