【发布时间】:2017-03-22 05:35:24
【问题描述】:
随着 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 微秒
开始列表测量...
耗时 1368205 μs
向量比列表快 5.6 倍。我们很可能要感谢缓存霸主的这种性能优势,即使我们需要在每次删除时移动向量中的元素,它的影响小于列表的查找时间,正如我们在基准测试中看到的那样。
然后我添加了另一个测试,在TraversedDeletion() 中看到。它不使用随机位置来删除,而是在容器中间选择一个索引并将其用作基本迭代器,然后遍历容器以擦除 10.000 次。
我的假设是该列表的性能仅略胜于向量或与向量一样快。
相同执行的结果:
遍历删除...
开始矢量测量....
耗时 195477 微秒
开始列表测量...
耗时 581 微秒
哇。该列表的速度大约是 336 倍。这与我的预期相差甚远。因此,在列表中出现一些缓存未命中似乎根本不重要,因为减少列表的查找时间会更重要。
因此,当涉及到角落/不寻常案例的性能时,该列表显然仍然具有非常强大的地位,或者我的测试用例在某些方面存在缺陷?
这是否意味着现在的列表只是遍历时在容器中间进行大量插入/删除的合理选择,还是有其他情况?
有没有办法可以更改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 微秒
这已经是一个重大的改进了。
【问题讨论】:
-
矢量擦除测试表现出未定义的行为。
vec.erase(it)使itr无效。你想要itr = vec.erase(itr); -
取决于“擦除”对向量的作用。它很可能将内存复制到新的内存位置(删除的元素除外),因此您在矢量擦除中没有缓存友好性。还有很多“抄袭”
-
@NathanOliver 不,他没有。我不确定你在说什么。
-
@IgorTandetnik 哎呀。我看错了。
vec.erase(it)确实 ivalidateitr并且他在每次后续迭代中都使用它。我在看RandomAccessDeletion而不是TraversedDeletion。 -
擦除范围意味着元素只需要移动一次,因此大约需要 40,000 个副本。一个一个地擦除意味着相同的 40,000-50,000 个元素每个需要移动 10,000 次,总共大约 450,000,000 个副本。
标签: c++ list c++11 vector visual-studio-2015