c++ - 缓存友好性 std::list 与 std::vector

标签 c++ list c++11 vector visual-studio-2015

随着 CPU 缓存越来越好,std::vector 通常优于 std::list,即使在测试 std::列表。出于这个原因,即使在我需要在容器中间删除/插入的情况下,我通常也会选择 std::vector 但我意识到我从未测试过这个以确保假设是正确的。所以我设置了一些测试代码:

#include <iostream>
#include <chrono>
#include <list>
#include <vector>
#include <random>

void TraversedDeletion()
{
    std::random_device dv;
    std::mt19937 mt{ dv() };
    std::uniform_int_distribution<> dis(0, 100000000);

    std::vector<int> vec;
    for (int i = 0; i < 100000; ++i)
    {
        vec.emplace_back(dis(mt));
    }

    std::list<int> lis;
    for (int i = 0; i < 100000; ++i)
    {
        lis.emplace_back(dis(mt));
    }

    {
        std::cout << "Traversed deletion...\n";
        std::cout << "Starting vector measurement...\n";

        auto now = std::chrono::system_clock::now();
        auto index = vec.size() / 2;
        auto itr = vec.begin() + index;
        for (int i = 0; i < 10000; ++i)
        {
            itr = vec.erase(itr);
        }

        std::cout << "Took " << std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::system_clock::now() - now).count() << " μs\n";
    }

    {
        std::cout << "Starting list measurement...\n";

        auto now = std::chrono::system_clock::now();
        auto index = lis.size() / 2;
        auto itr = lis.begin();
        std::advance(itr, index);
        for (int i = 0; i < 10000; ++i)
        {
            auto it = itr;
            std::advance(itr, 1);
            lis.erase(it);
        }

        std::cout << "Took " << std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::system_clock::now() - now).count() << " μs\n";
    }

}

void RandomAccessDeletion()
{
    std::random_device dv;
    std::mt19937 mt{ dv() };
    std::uniform_int_distribution<> dis(0, 100000000);

    std::vector<int> vec;
    for (int i = 0; i < 100000; ++i)
    {
        vec.emplace_back(dis(mt));
    }

    std::list<int> lis;
    for (int i = 0; i < 100000; ++i)
    {
        lis.emplace_back(dis(mt));
    }

    std::cout << "Random access deletion...\n";
    std::cout << "Starting vector measurement...\n";
    std::uniform_int_distribution<> vect_dist(0, vec.size() - 10000);

    auto now = std::chrono::system_clock::now();

    for (int i = 0; i < 10000; ++i)
    {
        auto rand_index = vect_dist(mt);
        auto itr = vec.begin();
        std::advance(itr, rand_index);
        vec.erase(itr);
    }

    std::cout << "Took " << std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::system_clock::now() - now).count() << " μs\n";

    std::cout << "Starting list measurement...\n";

    now = std::chrono::system_clock::now();

    for (int i = 0; i < 10000; ++i)
    {
        auto rand_index = vect_dist(mt);
        auto itr = lis.begin();
        std::advance(itr, rand_index);
        lis.erase(itr);
    }

    std::cout << "Took " << std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::system_clock::now() - now).count() << " μs\n";
}

int main()
{
    RandomAccessDeletion();
    TraversedDeletion();
    std::cin.get();
}

所有结果都用/02(最大化速度)编译

第一个,RandomAccessDeletion(),生成一个随机索引并将该索引删除 10.000 次。我的假设是正确的, vector 确实比列表快很多:

Random access deletion...

Starting vector measurement...

Took 240299 μs

Starting list measurement...

Took 1368205 μs

vector 比列表快 5.6 倍。尽管我们需要在每次删除时移动 vector 中的元素,但我们很可能要感谢我们的缓存霸主,因为它的影响小于列表的查找时间,正如我们在基准测试中看到的那样。


然后我添加了另一个测试,在 TraversedDeletion() 中可以看到。它不使用随机位置来删除,而是在容器中间选择一个索引并将其用作基本迭代器,然后遍历容器以删除 10.000 次。

我的假设是该列表的性能仅略胜于 vector 或与 vector 一样快。

相同执行的结果:

Traversed deletion...

Starting vector measurement....

Took 195477 μs

Starting list measurement...

Took 581 μs

哇。该列表的速度大约是 336 倍。这与我的预期相差甚远。因此,在列表中出现一些缓存未命中似乎根本不重要,因为缩短列表的查找时间会更重要。


因此,对于角落/异常案例的性能,或者我的测试用例在某些方面存在缺陷,该列表显然仍然具有非常强大的地位?

这是否意味着现在的列表只是遍历时在容器中间进行大量插入/删除的合理选择,还是有其他情况?

有没有办法我可以更改 TraversedDeletion() 中的 vector 访问和删除,使其至少比列表更具竞争力?


回应@BoPersson 的评论:

vec.erase(it, it+10000) would perform a lot better than doing 10000 separate deletes.

变化:

for (int i = 0; i < 10000; ++i)
{
    itr = vec.erase(itr);
}

收件人:

vec.erase(itr, itr + 10000);

给我:

Starting vector measurement...

Took 19 μs

这已经是一个重大的改进了。

最佳答案

TraversedDeletion 中,您实际上是在执行 pop_front,但不是在前面,而是在中间执行。对于链表,这不是问题。删除节点是一个 O(1) 操作。不幸的是,当您在 vector 中执行此操作时,它是一个 O(N) 操作,其中 Nvec.end() - itr。这是因为它必须将每个元素从删除点向前复制一个元素。这就是为什么它在 vector 情况下要贵得多。

另一方面,在 RandomAccessDeletion 中,您不断更改删除点。这意味着您有一个 O(N) 操作来遍历列表以到达要删除的节点,以及一个 O(1) 来删除节点,而 O(1) 遍历来查找元素和一个 O(N) 操作向前复制 vector 中的元素。这不一样的原因是从一个节点到另一个节点的遍历成本具有比复制 vector 中的元素更高的常数。

关于c++ - 缓存友好性 std::list 与 std::vector,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/40489513/

相关文章:

c++ - 使用 Boost 线程监控对象队列

c++ - OpenMP:有人知道堆损坏的原因吗?

c++ - 不抛出或异常?

c# - 无法在C#中为列表添加值

python - 在不创建任何对象副本的情况下展平 Python 列表?

c++ - thread_local 和 std::future 对象 - 对象的生命周期是多少?

c++ - C、C++ 中的内存泄漏;忘记免费了,删除

java - 从列表中删除 java 类

c++ - 函数调用表达式的类型是什么?

c++ - gcc alignas 问题与指向对象的成员指针