論Qt容器與STL
https://zhuanlan.zhihu.com/p/24035468
編輯於 2017-02-27
相關閱讀
推薦一篇比較全面的介紹QTL的文章:Understand the Qt containers
@渡世白玉 對其做了大致的翻譯,鏈接如下:
[翻譯]理解Qt容器:STL VS QTL(一)--特性總覽
[翻譯]理解Qt容器:STL VS QTL(二)--迭代器
[翻譯]理解Qt容器:STL VS QTL(三)--類型系統 和其他處理
============================
定性分析
Qt的容器類具體分析可見官方文檔:Container Classes
里面有關於時間復雜度、迭代器等各方面的概述和表格對比。
各個容器的具體實現,可以看代碼,或者看容器類文檔開頭的詳細介紹。
QTL比起STL的話,最大的特點是統一用了寫時復制技術。缺點是不支持用戶自定allocator。
在這里先簡單類比下吧,具體數據可以看后面的benchmark
- QLinkedList —— std::list 兩者都是雙向鏈表,兩者可以直接互轉。
- QVector —— std::vector 兩者都是動態數組,都是根據sizeof(T)進行連續分配,保證成員內存連續,能夠用data()直接取出指針作為c數組使用,兩者可以直接互轉。
- QMap —— std::map 兩者都是紅黑樹算法,但不能互轉,因為數據成員實現方式不同。std::map的數據成員用的是std::pair,而QMap用的是自己封裝的Node,當然還是鍵值對.
- QMultiMap —— std::multimap 同上。
- QList —— 暫無。QList其實不是鏈表,是優化過的vector,官方的形容是array list。它的存儲方式是分配連續的node,每個node的數據成員不大於一個指針大小,所以對於int、char等基礎類型,它是直接存儲,對於Class、Struct等類型,它是存儲對象指針。
std::deque很相似,但有少許區別。據有的知友提出,QList更像是boost::deque。
QList的實現模式,優點主要在於快速插入。因為其元素大小不會超過sizeof(void*),所以插入時只需要移動指針,而非如vector那樣移動對象。並且,由於QList存儲的是void*,所以可以簡單粗暴的直接用realloc()來擴容。
另外,QList的增長策略是雙向增長,所以對prepend支持比vector好得多,使用靈活性高於Vector和LinkedList。缺點是每次存入對象時,需要構造Node對象,帶來額外的堆開銷。
QList還規避了模板最大的問題——代碼量膨脹。由於QList其實是用void*存儲對象,所以它的絕大部分代碼是封裝在了操作void*的cpp里,頭文件只暴露了對其的封裝。
然而Qt對QList的支持還是很充足,用戶甚至可以用宏為自己要放入list的對象進行屬性指定(POD?Class但可以直接memcpy?只能用拷貝構造?)來輔助編譯優化。(此項輔助優化對絕大部分QTL容器都有效。如將類型定義為POD后,QVector便會通過realloc()擴容,而std::vector只會對基礎類型這么做)
對了,QList雖然是個特殊的Vector,但提供的接口仍然是和std::list的互轉,挺奇葩的……
在Qt里,QList是官方最常用的容器類,也是官方最推薦的,原文是這么說的——“在大多數情況下,都推薦使用QList,它提供更加快速的插入,並且(編譯后)可以展開為更少的代碼量。”
實際QList的內存浪費很嚴重——當元素小於等於sizeof(void*)時,會直接存儲元素,但按照void*對齊的話,過小的數據類型會浪費內存。當元素大於sizeof(void*)時,存在分配指針的開銷。但是,當元素是moveable類型(有構造函數但可以直接memcpy),且大小等於sizeof(void*)時,QList在內存開銷和性能兩者上都達到了完美——而Qt的許多常用數據類型剛好滿足這個條件(Qt內建類型習慣用QXxxPrivate指針存儲對象數據),包括但不限於QString、QByteArray、QIcon、QFileInfo、QPen、QUrl……
因此,當且僅當你的數據類型大小等於sizeof(void*),並且是moveable或者是POD時,通過宏指定類型輔助優化,這時用QList更好。其他情況都應用QVector。當然,如果你需要push_front()/prepend(),那必須得用QList。
- QBitArray —— std::bitset 功能相同,實現相似,都是構造一個array,用位操作來存取數據。不同的是,QBitArray數據的基礎元素是unsigned char,而bitset是unsigned long。所以QBitArray可能在空間消耗上會省一點。至於效率上么,二者查詢都是一次尋址提取加一次移位操作,算法層面應該沒有區別。
不過二者最大的差別是,std::bitset是定長,數據元素分配在棧上。QBitArray是變長,數據元素分配在堆上。這個肯定有性能差別。
- QHash —— std::unordered_map都是各自實現了自己的hashTable,然后查詢上都是用node->next的方式逐一對比,不支持互轉,性能上更多的應該是看hash算法。QHash為常用的qt數據類型都提供好了qHash()函數,用戶自定類型也能通過自己實現qHash()來存入QHash容器。
- QSet —— std::unordered_set二者不能互轉,實現方式本質相同,都是改造過的QHash,用key來存數據,value置空。另外STL提供了使用紅黑樹的std::set,可以看作是std::map的改造版。std::unordered_set效率上一般應該是和QSet沒區別,std::set效率較低,但不用擔心撞車。
- QVarLengthArray —— std::array std::array是用class封裝后的定長數組,數據分配在棧上。QVarLengthArray類似,默認也是定長數組,棧分配。但用戶依舊可以添加超出大小的內容,此時它會退化為Vector,改用堆分配。
- 可靠性——二者都有長期在大型系統級商業應用上使用的經歷,並且除了c++11版本特性引入外,代碼實現上基本沒有大的變動,所以可靠性均無問題。當然,為了保證效率,兩者都不提供thread safe,最多提供reentrant
- 安全性——Qt變量存STL不存在安全隱患,畢竟都是class,只要是支持copy constructor和assignment operator的對象,都可以放心存STL。而且由於Qt對象廣泛使用了寫時復制機制,所以存儲時時空開銷非常小。
當然還是推薦用QTL來存,因為QTL會對這些隱式共享類型做特殊優化,這方面可以看看QList源碼。
唯一的特例是QWidget類型及其子類,這些類型絕對不允許直接保存,只能用指針保存,哪怕用QTL也是這樣。
但是,不推薦用STL保存Qt對象,因為代碼風格不一致。不過QTL同時提供了Qt style API和STL style API,如果有和第三方庫混合編程的需求,推薦用STL style API,這樣可以隨時替換類型。另外,QTL還提供了Java style iterator,對於一些習慣Java語法的用戶會很方便。
總結
QTL比起STL,性能差別不明顯,主要差異在:
- QTL不支持allocator;
- QTL沒有shirnk接口(最新的幾個版本里有了,不過不叫shirnk);
- QTL沒有rbegin()/rend()(同上,最近幾個版本有了,相同API名稱);
- QTL對c++11特性的支持較晚(同上,Qt5.6開始才全面支持新特性),在這之前的版本,比起支持比如右值引用的STL版本,性能要略差。
對於采用相同算法的容器,比如QVector和std::vector,各項操作的時間復雜度應該是相同的,差異只會在實現的語法細節。當然如果stl寫了內存池用allocator的話,肯定會快上許多。
============================
Benchmark算法
設計數據元素均為int,無論key還是value(懶得構造隨機string了)。這個benchmark寫的較早,后來維護文章時把std::hash改為了std::unordered_map,把std::set改為了std::unordered_set,但benchmakr相關用例沒有更新,還望見諒。
- List:一百萬次push_back()隨機值,一百萬次push_front()隨機值。成員查詢都是靠遍歷,就沒必要專門測了。
- ListWithInsert:相比上面的testcase,多了十萬次insert操作,插入隨機值到隨機位置。感覺這個略蛋疼,沒必要做
- Vector:一百萬次push_back()隨機值,一百萬次隨機查詢並求和(求和結果沒去用,只是怕不操作返回值的話,編譯器直接把讀操作優化沒了)(vector的insert效率太低,就不測了。)
- Map:一百萬次隨機插入(key和value都是隨機值),一百萬次隨機查詢並求和。
- Hash:同Map
- Set:同Map
- Bitset:初始化定長一百萬,初值false。然后進行一百萬次隨機位置取反操作
- Array:初始化定長十萬(一百萬試了下,爆棧了),初值隨機值。然后進行一百萬次隨機查詢求和操作
測試平台
- SurfaceBook I5 獨顯版
- CPU:I5-6300U 2.4GHz 四線程
- 內存:8GB
- 編譯器:MinGW 5.3.0 32bit MSVC 2015 (14.0 amd64)
- 框架:Qt 5.7.0的Qt Test模塊(提供代碼段進行benchmark,若耗時過少會自動重復多次,統計平均值)
Benchmark結果

QTL與STL對比結論
(在不使用allocator的前提下)
- QTL和STL性能,同樣算法的容器基本在同一數量級。
- Bitset容器可以看出,堆分配比棧分配有性能損失。
- Set和Hash兩者存在實現差異,所以benchmark結果差距較大。紅黑樹和hashtable的效率差距太大了……(后來得知STL有使用hashtable的std::unordered_set,不過沒繼續測了……)。
- vector的insert效率太低,不管是Qt還是STL,時間開銷都在內存移動上,而且不太可能通過除了內存池之外的辦法進行優化,所以就沒測了。
- QList在擁有vector的操作性能的同時,通過前向擴展空間,模擬出了LinkedList的雙向添加O(1)的特點。但存儲int這種基礎類型時,由於存在Node的構造開銷,效率不如vector。
- MSVC的Debug真凶殘,不造加了多少overhead進去,這運行時間長的……
吐槽
- Qt的QQueue隊列類,是直接繼承自QList,只是添加了隊列操作的接口。這難道不應該用LinkedList么,殘念……如果我enqueue一百萬次,dequeue一百萬次,沒有shrink功能的list,那內存開銷……
(PS:剛在一個小程序里嘗試了下QQueue,末尾入隊,開頭出隊,在隊長不變的情況下,內存開銷毫無變動……看來QList的雙向內存增長很有意思,很可能是用了個環形緩沖放頭尾指針,放不下時對其進行擴充,所以才能解釋為何QQueue的表現會那么神奇……QList雖然是模板類,但Node的具體操作被封裝到cpp里了,回頭得翻出來看看才行)
- QStack棧類,繼承自QVector,添加了棧操作接口。和QQueue一樣殘念……不過這個還算好了,內存應該不會爆掉,雖然我覺得,stack和queue這種無隨機訪問需求的,應該用LinkedList實現比較好……
(PSP:使用list/vector可能是性能考慮吧,因為這二者都是內存連續的,對cache友好)
(PSV:STL的queue和stack好像也是分別用deque和vector實現的……)
- 出現個詭異的情況,Debug模式下,最后Array的testcase,莫名其妙的越界了,然后assert退出,囧……估計debug的棧分配和realse不同,所以爆棧了
===============================
QList、QVector對比
QList在提供最大化的易用性的同時,帶來了最小的性能損耗,若不使用prepend的話,可以換用QVecotr。
以下摘自QList幫助文檔:
Qt 4:
- For most purposes, QList is the right class to use. Its index-based API is more convenient than QLinkedList's iterator-based API, and it is usually faster than QVector because of the way it stores its items in memory. It also expands to less code in your executable.
- If you need a real linked list, with guarantees of constant time insertions in the middle of the list and iterators to items rather than indexes, use QLinkedList.
- If you want the items to occupy adjacent memory positions, use QVector.
Qt 5:
QList<T> is one of Qt's generic container classes. It stores items in a list that provides fast index-based access and index-based insertions and removals.
QList<T>, QLinkedList<T>, and QVector<T> provide similar APIs and functionality. They are often interchangeable, but there are performance consequences. Here is an overview of use cases:
- QVector should be your default first choice. QVector<T> will usually give better performance than QList<T>, because QVector<T> always stores its items sequentially in memory, where QList<T> will allocate its items on the heap unless sizeof(T) <= sizeof(void*) and T has been declared to be either a Q_MOVABLE_TYPE or a Q_PRIMITIVE_TYPE using Q_DECLARE_TYPEINFO. See the Pros and Cons of Using QList for an explanation.
- However, QList is used throughout the Qt APIs for passing parameters and for returning values. Use QList to interface with those APIs.
- If you need a real linked list, which guarantees constant time insertions mid-list and uses iterators to items rather than indexes, use QLinkedList.
- Note: QVector and QVarLengthArray both guarantee C-compatible array layout. QList does not. This might be important if your application must interface with a C API.
翻譯:
Qt 4:
- 在絕大多數場合,QList都是正確的選擇。它的基於下標的API比QLinkeList的基於迭代器的API更加方便,並且它通常比QVector更快——基於它在內存中的存儲方式。並且,QList在編譯到二進制時可以擴展為為更少的代碼。(譯者注:此處應該指的是插入比QVector快。訪問應該比不過真·順序存儲的QVector)。
- 如果你需要一個真正的鏈表,來保證在中間插入時的常量級耗時,並且用迭代器而非下標訪問,那么使用QLinkedList。
- 如果你想讓對象在內存中順序存儲,那么使用QVector。
Qt 5:
QList<T>是Qt的通用容器類之一。它用於列表存儲元素,並提供快速的序號查詢、序號插入和刪除。
QList<T>、QLinkedList<T>、QVector<T>提供相似的API和功能。它們經常是可以互相替換的,但存在性能差異。下面是它們的用例介紹:
- QVector應該是你的默認選擇。QVecotr通常提供比QList更好的性能,因為QVector總是將所有元素在內存中順序存儲,而QList則是將各個元素分別分配到堆中,除非sizeof(T) <= sizeof(void*),並且T被通過Q_DECLARE_TYPEINFO()宏定義為Q_MOVABLE_TYPE(可以memcpy的類型)或Q_PRIMITIVE_TYPE(POD類型)。詳見Pros and Cons of Using QList 。
- 然而,QList被Qt API廣泛用於傳遞參數和返回值。在與這些API交互時使用QList。
- 如果你需要一個真正的鏈表,以保證常量級的插入,並且使用迭代器而非索引,那么選擇QLinkedList。
注意:QVector和QVarLengthArray都保證了對C數組的兼容。QList則不提供此兼容性。這在當你與C API交互時可能很重要。
============= End