缓存友好性std :: list vs std :: vector

时间:2016-11-08 14:31:16

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

随着CPU缓存变得越来越好std::vector通常优于std::list,即使在测试std::list的优势时也是如此。出于这个原因,即使在我需要在容器中间删除/插入的情况下,我通常会选择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 (Maximize speed)编译。

第一个RandomAccessDeletion()生成随机索引并将此索引擦除10.000次。我的假设是正确的,矢量确实比列表快得多:

  

随机访问删除...

     

开始矢量测量......

     

花了240299μs

     

开始列表测量...

     

花了1368205μs

该矢量比列表强5.6倍。我们很可能感谢我们的缓存领域的这种性能优势,即使我们需要在每次删除时移动向量中的元素,它的影响小于列表的查找时间,正如我们在基准测试中看到的那样。

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

我的假设是列表的性能仅略高于矢量或者与向量一样快。

同一执行的结果:

  

遍历删除......

     

开始矢量测量....

     

花了195477μs

     

开始列表测量...

     

花了581μs

哇。该列表大约快336倍。这与我的期望相差甚远。因此,在列表中有一些缓存未命中似乎并不重要,因为减少列表的查找时间会更加重要。

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

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

有没有办法可以改变矢量访问权限&amp;在TraversedDeletion()中删除以使其与列表相比至少有更多的竞争?

回应@ BoPersson的评论:

  

vec.erase(it, it+10000)比10000做得更好   单独删除。

更改:

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

要:

vec.erase(itr, itr + 10000);

给我:

  

开始矢量测量......

     

花了19μs

这已经是主要的改进。

5 个答案:

答案 0 :(得分:4)

listRandomDeletion的持续时间较长是由于从列表开头前进到随机选择的元素O(N)操作所需的时间。

TraverseDeletion只增加一个迭代器,一个O(1)操作。

答案 1 :(得分:4)

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

另一方面,在RandomAccessDeletion中,您不断更改删除点。这意味着您有一个O(N)操作来遍历列表以进入要删除的节点,并且O(1)删除节点而不是O(1)遍历以查找元素和O(N)操作向前复制向量中的元素。之所以不一样,那么从节点到节点遍历的成本比在向量中复制元素所需的常量要高。

答案 2 :(得分:2)

关于向量的“快速”部分是“到达”需要访问的元素(遍历)。您实际上并没有在删除中遍历矢量,而只访问第一个元素。 (我想说,逐个进行并没有多少衡量标准)

删除需要花费很多时间(O(n)所以当由于更改内存中的元素而自行删除每个时,它是O(n²))。因为删除会更改已删除元素后位置的内存,所以也无法从预取中获益,这也是使向量快速生成的因素。

我不确定删除多少会使缓存无效,因为迭代器之外的内存已经改变,但这也会对性能产生很大影响。

答案 3 :(得分:2)

在第一个测试中,列表必须遍历到删除点,然后删除该条目。列表所用的时间是遍历每个删除的

在第二次测试中,列表遍历一次,然后重复删除。所花费的时间仍然在遍历中;删除很便宜。除了现在我们不会反复遍历。

对于向量,遍历是免费的。删除需要时间。随机删除元素需要更少的时间,而不是列表遍历到该随机元素,因此第一种情况下的矢量获胜。

在第二种情况下,向量的努力次数比列表的努力次数多很多次。

但是,问题是你不应该如何从向量中进行遍历和删除。对列表来说这是一种可接受的方式。

您为矢量写这个的方式是std::remove_if,然后是erase。或者只是一次擦除:

  auto index = vec.size() / 2;
  auto itr = vec.begin() + index;
  vec.erase(itr, itr+10000);

或者,模仿一个涉及删除元素的更复杂的决策过程:

  auto index = vec.size() / 2;
  auto itr = vec.begin() + index;
  int count = 10000;
  auto last = std::remove_if( itr, vec.end(),
    [&count](auto&&){
      if (count <= 0) return false;
      --count;
      return true;
    }
  );
  vec.erase(last, vec.end());

list存储到vector时,listvector更快的唯一情况是,并且您定期擦除该迭代器处或附近的 仍在遍历此类擦除操作之间的列表。

根据我的经验,几乎所有其他用例的list使用模式都符合或超过remove_if的效果。

如您所示,代码不能始终按行进行转换。

每次擦除矢量中的元素时,它会移动&#34;尾部&#34;矢量超过1。

如果你删除10,000个元素,它会移动&#34;尾部&#34;矢量超过10000的一步。

如果你 <dependentAssembly> <assemblyIdentity name="System.Web.Helpers" publicKeyToken="31bf3856ad364e35" /> <bindingRedirect oldVersion="1.0.0.0-3.0.0.0" newVersion="3.0.0.0" /> </dependentAssembly> <dependentAssembly> <assemblyIdentity name="System.Web.WebPages" publicKeyToken="31bf3856ad364e35" /> <bindingRedirect oldVersion="1.0.0.0-3.0.0.0" newVersion="3.0.0.0" /> </dependentAssembly> <dependentAssembly> <assemblyIdentity name="System.Web.Mvc" publicKeyToken="31bf3856ad364e35" /> <bindingRedirect oldVersion="0.0.0.0-5.2.3.0" newVersion="5.2.3.0" /> </dependentAssembly> ,它会有效地消除尾部,给你带来浪费的#34;剩下的,然后你可以从矢量中删除废物。

答案 4 :(得分:0)

我希望po指出在这个问题中仍未提及的内容:

在std :: vector中,当你删除中间的元素时,由于新的移动语义,元素会被移动。这是第一次测试采用此速度的原因之一,因为您甚至没有在删除的迭代器之后复制元素。您可以使用矢量和非可复制类型列表重现实验,并查看列表的性能(在比较中)如何更好。