QTL与STL对比

作者:诸葛不亮
链接:https://www.zhihu.com/question/50156390/answer/119663575
来源:知乎
著作权归作者所有,转载请联系作者获得授权。

Qt的容器类具体分析可见官方文档: doc.qt.io/qt-5.7/contai
里面有关于时间复杂度、迭代器等各方面的概述和表格对比
各个容器的具体实现,可以看代码,或者看容器类文档开头的详细介绍

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 —— stl没有对应类。QList其实不是链表,是优化过的vector,官方的形容是array list,据说它更类似于boost::ptr_deque,不过我没用过后者。它的存储方式是分配连续的node,每个node的数据成员不大于一个指针大小,所以对于int、char等基础类型,它是直接存储,对于Class、Struct等类型,它是存储对象指针。
  QList的实现模式,优点主要在于快速插入。因为其元素大小不会超过sizeof(void*),所以插入时元素移动比vector要快得多。当然可以用QVecotr<void*>来模拟QList,就是用起来不太方便。
  另外,QList的增长策略是双向增长,所以对prepend支持比vector好得多,使用灵活性高于Vector和LinkedList。缺点是每次存入对象时,需要构造Node对象,带来额外的堆开销。
  然而Qt对QList的支持还是很充足,用户甚至可以用宏为自己要放入list的对象进行属性指定(POD?Class但可以直接memcpy?只能用拷贝构造?)来辅助编译优化。
  对了,QList虽然是个特殊的Vector,但提供的接口仍然是和std::list的互转,挺奇葩的……
  • QBitArray —— std::bitset 功能相同,实现相似,都是构造一个array,用位操作来存取数据。不同的是,QBitArray数据的基础元素是unsigned char,而bitset是unsigned long。所以QBitArray可能在空间消耗上会省一点。至于效率上么……得让懂编译的人来解答了,我是觉得,32位cpu上,char的位操作和int的位操作应该是一样的开销。
  不过二者最大的差别是,std::bitset是定长,数据元素分配在栈上。QBitArray是变长,数据元素分配在堆上。这个肯定有性能差别
  • QHash —— std::hash_map 都是各自实现了自己的hashTable,然后查询上都是用node->next的方式逐一对比,不支持互转,性能上更多的应该是看hash算法。QHash为常用的qt数据类型都提供好了qHash()函数,用户自定类型也能通过自己实现qHash()来存入QHash容器。
  (补充——写benchmark时,hash_map被编译器提示为deprecated,查了下换成unordered_map了,具体差异我并不清楚- -!)
  • QSet —— std::set 二者不能互转,实现方式有本质的区别。QSet其实是改造过的QHash,用key来存数据,value置空。而std::set用的是红黑树。所以,效率上一般应该是QSet高,但数据大了可能会有撞车问题
  (补充——评论里有人指出,STL中有使用hashtable实现的std::unordered_set,那么和QSet理论上性能没有本质差距了,差异只在hash算法上。然而两者都支持用户自定hash算法,所以打平)
  • 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接口

    对于采用相同算法的容器,比如QVector和std::vector,各项操作的时间复杂度应该是相同的,差异只会在实现的语法细节。当然如果stl写了内存池用allocator的话,肯定会快上许多。


上图是Qt Document里Container Classes章节对各容器类性能的统计。从这里也可以看出,QList其实是个特殊的QVector,QSet其实是个特殊的QHash

============================


Benchmark算法设计
数据元素均为int,无论key还是value(懒得构造随机string了)
  • List:一百万次push_back()随机值,一百万次push_front()随机值。成员查询都是靠遍历,就没必要专门测了
  • ListWithInsert:相比上面的testcase,多了十万次insert操作,插入随机值到随机位置。感觉这个略蛋疼,没必要做
  • Vector:一百万次push_back()随机值,一百万次随机查询并求和(求和结果没去用,只是怕不操作返回值的话,编译器直接把读操作优化没了)(QVector的insert是用下标索引,std::vector的insert是用迭代器索引,所以就不测insert了,太欺负人)
  • Map:一百万次随机插入(key和value都是随机值),一百万次随机查询并求和
  • Hash:同Map
  • Set:同Map
  • Bitset:初始化定长一百万,初值false。然后进行一百万次随机位置取反操作
  • Array:初始化定长十万(一百万试了下,爆栈了),初值随机值。然后进行一百万次随机查询求和操作

    测试平台
    SurfaceBook I5 独显版
    CPU:I5-6300U
    内存:8GB
    编译器:MinGW 5.3.0 32bit MSVC 2015 (14.0 amd64)
    框架:Qt 5.7.0的Qt Test模块(提供代码段benchmark功能,若耗时过少会自动重复几次,统计平均值)

    Benchmark结果


    QTL与STL对比结论(在不使用allocator的前提下)
  1. QTL和STL性能,同样算法的容器基本在同一数量级
  2. Bitset容器可以看出,堆分配比栈分配有性能损失
  3. Set和Hash两者存在实现差异,所以benchmark结果差距较大。红黑树和hashtable的效率差距太大了……(后来得知STL有使用hashtable的std::unordered_set,不过没继续测了……)
  4. QVector的insert()是用index定位,std::vector()的insert是用iterator定位,明显不公平,所以vector的insert就没测了
  5. QList在拥有vector的操作性能的同时,通过前向扩展空间,模拟出了LinkedList的双向添加O(1)的特点。但存储int这种基础类型时,由于存在Node的构造开销,效率不如vector。但如果存储Struct的话,可能表现会好一点,待会儿我写个testcase试试看
  6. MSVC的Debug真凶残,不造加了多少overhead进去,这运行时间长的……

吐槽
  • Qt的QQueue队列类,是直接继承自QList,只是添加了队列操作的接口。这难道不应该用LinkedList么,残念……如果我enqueue一百万次,dequeue一百万次,没有shrink功能的list,那内存开销……
(补充:刚在一个小程序里尝试了下QQueue,末尾入队,开头出队,在队长不变的情况下,内存开销毫无变动……看来QList的双向内存增长很有意思,很可能是用了个循环内存池放头尾指针,放不下时对其进行扩充,所以才能解释为何QQueue的表现会那么神奇……QList虽然是模板类,但Node的具体操作被封装到cpp里了,回头得翻出来看看才行)
  • QStack栈类,继承自QVector,添加了栈操作接口。和QQueue一样残念……不过这个还算好了,内存应该不会爆掉,虽然我觉得,stack和queue这种无随机访问需求的,应该用LinkedList实现比较好……
  • 出现个诡异的情况,Debug模式下,最后Array的testcase,莫名其妙的越界了,然后assert退出,囧……估计debug的栈分配和realse不同,所以爆栈了

    源代码
// main.cpp
#include "test.h"

QTEST_MAIN(Test)



// test.h
#ifndef TEST_H
#define TEST_H

#include <QtCore>
#include <QtTest>

class Test : public QObject
{
    Q_OBJECT

private slots:
    void testQLinkedList();
    void testStdList();
    void testQList();
    void testQLinkedListWithInsert();
    void testStdListWithInsert();
    void testQListWithInsert();
    void testQVector();
    void testStdVecor();
    void testQMap();
    void testStdMap();
    void testQHash();
    void testStdHash();
    void testQSet();
    void testStdSet();
    void testQBitArray();
    void testStdBitset();
    void testQVarLengthArray();
    void testStdArray();
};

#endif // TEST_H




// test.cpp
#include "Test.h"
#include <list>
#include <vector>
#include <map>
#include <unordered_map>
#include <bitset>
#include <set>
#include <array>

void Test::testQLinkedList()
{
    QBENCHMARK {
        QLinkedList<int> list;
        for(int i=0;i<1000000;++i)
            list.push_back(qrand());
        for(int i=0;i<1000000;++i)
            list.push_front(qrand());
    }
}

void Test::testStdList()
{
    QBENCHMARK {
        std::list<int> list;
        for(int i=0;i<1000000;++i)
            list.push_back(qrand());
        for(int i=0;i<1000000;++i)
            list.push_front(qrand());
    }
}

void Test::testQList()
{
    QBENCHMARK {
        QList<int> list;
        for(int i=0;i<1000000;++i)
            list.push_back(qrand());
        for(int i=0;i<1000000;++i)
            list.push_front(qrand());
    }
}

void Test::testQLinkedListWithInsert()
{
    QBENCHMARK {
        QLinkedList<int> list;
        for(int i=0;i<1000000;++i)
            list.push_back(qrand());
        for(int i=0;i<1000000;++i)
            list.push_front(qrand());
        for(int i=0;i<10000;++i)
            list.insert(list.begin() + qrand() % list.size(), qrand());
    }
}

void Test::testStdListWithInsert()
{
    QBENCHMARK {
        std::list<int> list;
        for(int i=0;i<1000000;++i)
            list.push_back(qrand());
        for(int i=0;i<1000000;++i)
            list.push_front(qrand());
        for(int i=0;i<10000;++i)
        {
            auto it = list.begin();
            int index = qrand() % list.size();
            for(int j=0;j<index;++j)
                ++it;
            list.insert(it, qrand());
        }
    }
}

void Test::testQListWithInsert()
{
    QBENCHMARK {
        QList<int> list;
        for(int i=0;i<1000000;++i)
            list.push_back(qrand());
        for(int i=0;i<1000000;++i)
            list.push_front(qrand());
        for(int i=0;i<10000;++i)
        {
            auto it = list.begin();
            int index = qrand() % list.size();
            for(int j=0;j<index;++j)
                ++it;
            list.insert(it, qrand());
        }
    }
}

void Test::testQVector()
{
    QBENCHMARK {
        QVector<int> vector;
        for(int i=0;i<1000000;++i)
            vector.push_back(qrand());
        qint64 sum = 0;
        for(int i=0;i<1000000;++i)
            sum += vector.at(qrand() % vector.size());
    }
}

void Test::testStdVecor()
{
    QBENCHMARK {
        std::vector<int> vector;
        for(int i=0;i<1000000;++i)
            vector.push_back(qrand());
        qint64 sum = 0;
        for(int i=0;i<1000000;++i)
            sum += vector.at(qrand() % vector.size());
    }
}

void Test::testQMap()
{
    QBENCHMARK {
        QMap<int, int> map;
        for(int i=0;i<1000000;++i)
            map.insert(qrand(), qrand());
        qint64 sum = 0;
        for(int i=0;i<1000000;++i)
            sum += *map.find(qrand() % map.size());
    }
}

void Test::testStdMap()
{
    QBENCHMARK {
        std::map<int, int> map;
        for(int i=0;i<1000000;++i)
            map.insert(std::make_pair(qrand(), qrand()));
        qint64 sum = 0;
        for(int i=0;i<1000000;++i)
            sum += map.find(qrand() % map.size())->first;
    }
}

void Test::testQHash()
{
    QBENCHMARK {
        QHash<int, int> hash;
        for(int i=0;i<1000000;++i)
            hash.insert(qrand(), qrand());
        qint64 sum = 0;
        for(int i=0;i<1000000;++i)
            sum += *hash.find(qrand() % hash.size());
    }
}

void Test::testStdHash()
{
    QBENCHMARK {
        std::unordered_map<int, int> hash;
        for(int i=0;i<1000000;++i)
            hash.insert(std::make_pair(qrand(), qrand()));
        qint64 sum = 0;
        for(int i=0;i<1000000;++i)
            sum += hash.find(qrand() % hash.size())->second;
    }
}

void Test::testQSet()
{
    QBENCHMARK {
        QSet<int> set;
        for(int i=0;i<1000000;++i)
            set.insert(qrand());
        qint64 sum = 0;
        for(int i=0;i<1000000;++i)
        {
            auto it = set.find(qrand());
            if(it != set.cend())
                sum += *it;
        }
    }
}

void Test::testStdSet()
{
    QBENCHMARK {
        std::set<int> set;
        for(int i=0;i<1000000;++i)
            set.insert(qrand());
        qint64 sum = 0;
        for(int i=0;i<1000000;++i)
        {
            auto it = set.find(qrand());
            if(it != set.cend())
                sum += *it;
        }
    }
}

void Test::testQBitArray()
{
    QBENCHMARK {
        QBitArray array(1000000, false);
        for(int i=0;i<1000000;++i)
        {
            int index = qrand();
            array[index] = !array[index];
        }
    }
}

void Test::testStdBitset()
{
    QBENCHMARK {
        std::bitset<1000000> array(false);
        for(int i=0;i<1000000;++i)
        {
            int index = qrand();
            array[index] = !array[index];
        }
    }
}

void Test::testQVarLengthArray()
{
    QBENCHMARK {
        QVarLengthArray<int, 100000> array;
        for(int i=0;i<100000;++i)
            array[i] = qrand();
        qint64 sum = 0;
        for(int i=0;i<1000000;++i)
            sum += array[qrand() % array.size()];
    }
}

void Test::testStdArray()
{
    QBENCHMARK {
        std::array<int, 100000> array;
        for(int i=0;i<100000;++i)
            array[i] = qrand();
        qint64 sum = 0;
        for(int i=0;i<1000000;++i)
            sum += array[qrand() % array.size()];
    }
}

===============================

QList、QVecotr、std::vector存储复杂类型测试

结果如图。QList性能略小于等于QVector,二者性能和std::vector无本质差别。 结果如图。QList性能略小于等于QVector,二者性能和std::vector无本质差别。

总结:QList在提供最大化的易用性的同时,带来了最小的性能损耗,若不使用prepend的话,可以换用QVecotr。
以下摘自QList帮助文档:
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.

翻译:
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类型)。详见wordpress.com 的页面
  • 然而,QList被Qt API广泛用于传递参数和返回值。在与这些API交互时使用QList。
  • 如果你需要一个真正的链表,以保证常量级的插入,并且使用迭代器而非索引,那么选择QLinkedList。
注意:QVector和QVarLengthArray都保证了对C数组的兼容。QList则不提供此兼容性。这在当你与C API交互时可能很重要。


Benchmark代码
struct PODData {
    quint32 data = qrand();
};
Q_DECLARE_TYPEINFO(PODData, Q_PRIMITIVE_TYPE);

class PODClass {
public:
    quint32 data = qrand();

    PODClass() {}
    PODClass(const PODClass& other) {data = other.data;}
    PODClass(PODClass&& other) {data = other.data;}
    PODClass& operator=(const PODClass& other)
    {
        if(this != &other)
            data = other.data;
        return *this;
    }
    PODClass& operator=(PODClass&& other)
    {
        data = other.data;
        return *this;
    }
};
Q_DECLARE_TYPEINFO(PODClass, Q_MOVABLE_TYPE);

class ComplexClass {
public:
    int* data = nullptr;

    ComplexClass() {data = new int;}
    ~ComplexClass() {clear();}
    ComplexClass(const ComplexClass& other) {clear(); data = new int(*other.data);}
    ComplexClass(ComplexClass&& other) {std::swap(this->data, other.data);}
    ComplexClass& operator=(const ComplexClass& other)
    {
        if(this != &other)
        {
            clear();
            data = new int(*other.data);
        }
        return *this;
    }
    ComplexClass& operator=(ComplexClass&& other)
    {
        std::swap(this->data, other.data);
        return *this;
    }
    void clear() {if(data) delete data;}
};
Q_DECLARE_TYPEINFO(ComplexClass, Q_COMPLEX_TYPE);

void Test::testQListWithStruct()
{
    QBENCHMARK {
        QList<PODData> vector;
        for(int i=0;i<100000;++i)
        {
            PODData data;
            vector.push_back(data);
        }
        qint64 sum = 0;
        for(int i=0;i<1000000;++i)
        {
            PODData data = vector.at(qrand() % vector.size());
            sum += data.data;
        }
    }
}

void Test::testQVectorWithStruct()
{
    QBENCHMARK {
        QVector<PODData> vector;
        for(int i=0;i<100000;++i)
        {
            PODData data;
            vector.push_back(data);
        }
        qint64 sum = 0;
        for(int i=0;i<1000000;++i)
        {
            PODData data = vector.at(qrand() % vector.size());
            sum += data.data;
        }
    }
}

void Test::testStdVecorWithStruct()
{
    QBENCHMARK {
        std::vector<PODData> vector;
        for(int i=0;i<100000;++i)
        {
            PODData data;
            vector.push_back(data);
        }
        qint64 sum = 0;
        for(int i=0;i<1000000;++i)
        {
            PODData data = vector.at(qrand() % vector.size());
            sum += data.data;
        }
    }
}

void Test::testQListWithPODClass()
{
    QBENCHMARK {
        QList<PODClass> vector;
        for(int i=0;i<100000;++i)
        {
            PODClass data;
            vector.push_back(data);
        }
        qint64 sum = 0;
        for(int i=0;i<1000000;++i)
        {
            PODClass data = vector.at(qrand() % vector.size());
            sum += data.data;
        }
    }
}

void Test::testQVectorWithPODClass()
{
    QBENCHMARK {
        QVector<PODClass> vector;
        for(int i=0;i<100000;++i)
        {
            PODClass data;
            vector.push_back(data);
        }
        qint64 sum = 0;
        for(int i=0;i<1000000;++i)
        {
            PODClass data = vector.at(qrand() % vector.size());
            sum += data.data;
        }
    }
}

void Test::testStdVecorWithPODClass()
{
    QBENCHMARK {
        std::vector<PODClass> vector;
        for(int i=0;i<100000;++i)
        {
            PODClass data;
            vector.push_back(data);
        }
        qint64 sum = 0;
        for(int i=0;i<1000000;++i)
        {
            PODClass data = vector.at(qrand() % vector.size());
            sum += data.data;
        }
    }
}

void Test::testQListWithComplexClass()
{
    QBENCHMARK {
        QList<ComplexClass> vector;
        for(int i=0;i<100000;++i)
        {
            ComplexClass data;
            vector.push_back(data);
        }
        qint64 sum = 0;
        for(int i=0;i<1000000;++i)
        {
            ComplexClass data = vector.at(qrand() % vector.size());
            sum += *data.data;
        }
    }
}

void Test::testQVectorWithComplexClass()
{
    QBENCHMARK {
        QVector<ComplexClass> vector;
        for(int i=0;i<100000;++i)
        {
            ComplexClass data;
            vector.push_back(data);
        }
        qint64 sum = 0;
        for(int i=0;i<1000000;++i)
        {
            ComplexClass data = vector.at(qrand() % vector.size());
            sum += *data.data;
        }
    }
}

void Test::testStdVecorWithComplexClass()
{
    QBENCHMARK {
        std::vector<ComplexClass> vector;
        for(int i=0;i<100000;++i)
        {
            ComplexClass data;
            vector.push_back(data);
        }
        qint64 sum = 0;
        for(int i=0;i<1000000;++i)
        {
            ComplexClass data = vector.at(qrand() % vector.size());
            sum += *data.data;
        }
    }
}
还尝试了取消中间变量,添加时使用push_back(PODStruct()),读取时使用const PODStruct& data,性能会有明显提升,但三者的性能对比不变。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值