C++ 缓存友好性标准::列表与标准::向量

C++ 缓存友好性标准::列表与标准::向量,c++,list,c++11,vector,visual-studio-2015,C++,List,C++11,Vector,Visual Studio 2015,随着CPU缓存变得越来越好,即使在测试std::list的性能时,std::vector也通常优于std::list。由于这个原因,即使在我需要删除/插入容器中间的情况下,我通常会选择 STD::vector < /代码>,但我意识到我从未测试过这一点,以确保假设是正确的。所以我设置了一些测试代码: #include <iostream> #include <chrono> #include <list> #include <vector> #in

随着CPU缓存变得越来越好,即使在测试
std::list
的性能时,std::vector也通常优于
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();
}
致:

给我:

开始矢量测量

耗时19μs


这已经是一项重大改进。

随机删除中
列表
的持续时间较长,这是由于从列表开始到随机选择的元素所需的时间,即O(N)操作

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

关于向量的“快速”部分是“到达”需要访问的元素(遍历)。在删除过程中,实际上不需要遍历向量,只需要访问第一个元素。(我想说,一个人的进步并不能使测量变得明智)

然后,由于内存中的元素发生了变化,删除需要相当长的时间(O(n),因此当单独删除每个元素时,它是O(n²))。因为删除会改变删除元素后位置上的内存,所以预取也不能使向量如此快速


我不确定删除会使缓存失效的程度,因为迭代器之外的内存发生了变化,但这也会对性能产生很大影响 TraveDebug 中,你本质上是在做一个<代码> POPFrADON/<代码>,但不是在前面,而是在中间做。对于链接列表,这不是问题。删除节点是一个O(1)操作。不幸的是,在向量中执行此操作时,它是一个O(N)操作,其中
N
vec.end()-itr
。这是因为它必须将每个元素从删除点向前复制一个元素。这就是为什么在矢量情况下它要昂贵得多


另一方面,在
randomAccessDelete
中,您不断更改删除点。这意味着您有一个O(N)操作来遍历列表,以到达要删除的节点,有一个O(1)操作来删除节点,而有一个O(1)操作来查找元素,有一个O(N)操作来向前复制向量中的元素。但原因不同,因为从一个节点到另一个节点的遍历成本的常数高于复制向量中的元素所需的常数。

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

在第二个测试中,列表遍历一次,然后重复删除。所用的时间仍在穿越中;删除很便宜。但现在我们不再重复穿越

对于向量,遍历是自由的。删除需要时间。随机删除一个元素所花费的时间比列表遍历到该随机元素所花费的时间要少,因此vector在第一种情况下获胜

在第二种情况下,向量所做的艰苦工作比列表所做的艰苦工作多得多

但是,问题不是如何遍历和删除向量。对于列表来说,这是一种可以接受的方式

为向量编写此函数的方法是
std::remove\u if
,然后是
擦除
。或者只是一次擦除:

  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());
auto index=vec.size()/2;
自动itr=vec.begin()+索引;
整数计数=10000;
auto last=std::remove_if(itr,vec.end(),
[&计数](自动&&){

如果(count我想指出这个问题中还没有提到的东西:


在STD::vector中,当删除中间的元素时,由于新的移动语义,元素会被移动。这是第一个测试采用这个速度的原因之一,因为您甚至不在删除迭代器之后复制元素。r列表(在比较中)的性能更好。

向量擦除测试显示未定义的行为。
vec.erase(it)
使
itr
无效。
itr=vec.erase(itr);
取决于“擦除”对向量的作用。它很可能将内存复制到新的内存位置(除去的元素除外)所以你在向量擦除上没有缓存友好性。还有很多“复制”@NathanOliver不,他没有。我不知道你在说什么。@IgorTandetnik噢,我看错了。
vec.erase(它)
ivalidate
itr
吗,他在随后的每次迭代中都会使用它。我在看
RandomAccessDelete
而不是
TraversedDelete
。擦除一个范围意味着元素只需要移动一次,所以大约40000个副本。逐个擦除意味着相同的40000-50000个元素需要移动10000次,例如总共约450000000份。
TraversedDelete
还包括
std::advance(itr,index);
到测量中。我想知道与实际删除相比,这段时间花在了哪一部分。@IgorTandetnik Traverse删除提前1。随机删除提前N,其中N在0到10000之间(平均5000)。要在列表中前进,您需要遍历列表元素N次,因此随机删除需要做更多的工作才能到达要删除的元素。大多数情况下,它前进1次-但是有一个一次性的设置调用,将迭代器移动到列表的中间,并且它包含在度量中。@IgorTandetnik,但这是一次,不是10,0但它需要遍历50000个节点,而不是10000个节点
  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());