【问题标题】:STL list very bad performanceSTL list 性能很差
【发布时间】:2017-02-27 19:58:30
【问题描述】:

假设 STL 列表(作为双链表实现)的“push_back”和“pop_front”方法应该是常数 O(1)。然而,我们在 linux 上运行的应用程序中遇到了 cpu 问题,我们发现“pop_front”方法在使用列表时效率非常低。这是列表实施问题还是预期行为?

这是示例代码:

class A {
public:
    A() { mA = rand(); mB = rand(); mC = rand(); mD = rand(); }
    u32 mA;
    u32 mB;
    u32 mC;
    u32 mD;
};

#define DELTA(t1, t0) ((t1.tv_sec - t0.tv_sec)*1000 + ((t1.tv_usec - t0.tv_usec)/1000))

int main(int argc, char* argv[]) {
    std::list<A> l;
    std::queue<A> q;
    std::deque<A> dq;
    printf("Creating nodes...");
    std::vector<A> v;
    for (int i = 0; i < 100000; ++i) {
        A a;
        v.push_back(a);
    }
    printf("OK\n");
    timeval t0, t1;

    printf("std::deque test: push back...");
    gettimeofday(&t0, NULL);
    for (std::vector<A>::const_iterator iter = v.begin(); iter != v.end(); ++iter) {
        dq.push_back(*iter);
    }
    gettimeofday(&t1, NULL);
    printf("Done in %d ms, size = %d\n", DELTA(t1, t0), dq.size());
    printf("std::deque test: pop front...");
    gettimeofday(&t0, NULL);
    while (dq.size() > 0) {
        A a = dq.front();
        dq.pop_front();
    }
    gettimeofday(&t1, NULL);
    printf("Done in %d ms, size = %d\n", DELTA(t1, t0), dq.size());

    printf("std::queue test: push back...");
    gettimeofday(&t0, NULL);
    for (std::vector<A>::const_iterator iter = v.begin(); iter != v.end(); ++iter) {
        q.push(*iter);
    }
    gettimeofday(&t1, NULL);
    printf("Done in %d ms, size = %d\n", DELTA(t1, t0), q.size());
    printf("std::queue test: pop front...");
    gettimeofday(&t0, NULL);
    while (q.size() > 0) {
        A a = q.front();
        q.pop();
    }
    gettimeofday(&t1, NULL);
    printf("Done in %d ms, size = %d\n", DELTA(t1, t0), q.size());

    printf("std::list test: push back...");
    gettimeofday(&t0, NULL);
    for (std::vector<A>::const_iterator iter = v.begin(); iter != v.end(); ++iter) {
        l.push_back(*iter);
    }
    gettimeofday(&t1, NULL);
    printf("Done in %d ms, size = %d\n", DELTA(t1, t0), l.size());
    printf("std::list test: pop front...");
    gettimeofday(&t0, NULL);
    while (l.size() > 0) {
        A a = l.front();
        l.pop_front();
    }
    gettimeofday(&t1, NULL);
    printf("Done in %d ms, size = %d\n", DELTA(t1, t0), l.size());
    return 0;
}

对于不同数量的节点我们得到:

5000 个节点:

std::deque test: push back...Done in 0 ms, size = 5000
std::deque test: pop front...Done in 0 ms, size = 0
std::queue test: push back...Done in 0 ms, size = 5000
std::queue test: pop front...Done in 0 ms, size = 0
std::list test: push back...Done in 0 ms, size = 5000
std::list test: pop front...Done in 202 ms, size = 0

10000 个节点:

std::deque test: push back...Done in 0 ms, size = 10000
std::deque test: pop front...Done in 0 ms, size = 0
std::queue test: push back...Done in 0 ms, size = 10000
std::queue test: pop front...Done in 0 ms, size = 0
std::list test: push back...Done in 1 ms, size = 10000
std::list test: pop front...Done in 279 ms, size = 0

100000 个节点:

std::deque test: push back...Done in 5 ms, size = 100000
std::deque test: pop front...Done in 4 ms, size = 0
std::queue test: push back...Done in 3 ms, size = 100000
std::queue test: pop front...Done in 4 ms, size = 0
std::list test: push back...Done in 12 ms, size = 100000
std::list test: pop front...Done in 31148 ms, size = 0

谢谢!

维森特

【问题讨论】:

  • 这样的事情非常依赖于实现。您使用的是哪个编译器?它的哪个版本?您使用的是哪个标准库(某些编译器允许您选择),以及哪个版本?你是在优化还是不优化的情况下进行构建(基准测试应该始终通过优化来完成)?对于“pop front”测试,您还需要对 A 的复制或移动构造函数以及 front 函数进行测试和基准测试。最后,您是在 C++11(或更高版本)模式下构建的吗?还是使用 C++03(或更早版本)?
  • 你应该默认使用std::vector,并且在做详细的性能分析之后才考虑std::list。 Bjarne Stroustrup 在stroustrup.com/bs_faq.html#liststroustrup.com/bs_faq.html#list
  • @hungptit:考虑到队列行为(push_back/pop_front),我们知道向量非常糟糕。

标签: c++ list stl


【解决方案1】:

如果要检查容器是否为非空,应使用!c.empty(),而不是c.size() &gt; 0

这对std::list 尤其重要,因为在某些实现中,size线性时间 操作,而不是恒定时间 操作。

(尽管正如 vsoftco 在 cmets 中指出的那样,C++11 加强了对 size 的要求,即它确实是恒定的——如果你有一个兼容的编译器/库,你可以尝试启用为该标准编译的选项或以后)

【讨论】:

  • 好答案!从 C++11 开始,it is guaranteedstd::list::size 是常数时间。
  • 但是,我已经为 C++11 重新编译(添加到 g++ -std=c++0x 或 c++11)并且我得到了相同的结果...有吗还有什么应该更改以使用 C++11 实现?
  • @VicenteSirvent 您使用的是哪个版本的 GCC?较旧的版本(尤其是 4.9 之前的版本)没有完全的 C++11 兼容性。完整的 C++11 实现仅在版本 5 或更高版本中得到保证。
  • @Joachim 感谢您的回复。这发生在 Fedora 20 发行版中,明天将检查 gcc 版本。
【解决方案2】:

所以,这里有一些实用的答案:你的测试完全是错误的。

首先,您的代码是非常 C++03 风格编写的,并且在内部利用了邪恶的 C 函数。您应该使用 C++11 随机生成器、计时函数和 C++11 样式范围循环。只有这样 C++ 开发人员才能真正谈论您的代码。

其次,5000 个元素太小了,无法得出任何结论。尝试更大的数字,例如 1'000'000 并在循环内多次执行相同的测试。只有这样你才能真正看到不同容器之间的区别。

第三,我怀疑gettimeofday实际上是否足够准确来衡量这种基准,你绝对应该使用一些C++11 chrono函数或者至少在linux上使用rdtsc命令。

第四,您需要隔离您的测试。让一个测试测试所有您的容器集是错误的。一个测试可能会导致缓存被热数据填满,而随后的测试只是使用这些热数据而具有错误的性能提升。对不同的容器使用不同的测试。

最后,我同意一般来说,链表并不是有史以来最快的容器。在实际加速您的代码时,复杂性有点复杂。复杂性是一个数学极限。它没有考虑真正的 CPU 架构,只是假设一切都是基本步骤,这是错误的。

在典型的 C++ 应用程序中,主要的两个性能因素是:

  1. 缓存友好内存(链表在这方面很糟糕)
  2. 内存分配/释放次数(链表在这方面很糟糕)

由于这两个原因,列表的性能很差。您的数据以非密集方式传播,这会导致许多缓存错误,并且还会迫使应用程序分配许多小内存,这是 C++ 的弱点之一。

【讨论】:

  • 嗯,解决方案只是“list::size() 方法的复杂度在某些 STL 实现中可能为 O(n),这使得测试中的列表迭代变慢”。测试可以很好地显示问题,这只是一个示例。关于缓存/优化我更喜欢避免优化以尝试实现代码的最坏情况而不依赖于编译器。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-02-28
  • 2018-09-04
相关资源
最近更新 更多