我在YouTube上看到了这个视频:https://www.youtube.com/watch?v=YQs6IC-vgmoBjarne说最好使用向量,而不是链表。我无法理解整件事,有人能用外行的话解释一下他在说什么吗?
p.S:我是一名高中生,可以很容易地处理链表,但我很难独自学习矢量。你能推荐一些学习矢量的来源吗?
矢量与链表的优势
与链表相比,向量的主要优点是内存局部性。
通常,每个元素在一个链表中被单独分配。因此,这些元素在内存中可能并不相邻。(内存中元素之间的间隙。)
向量保证连续存储所有包含的元素。(项目相邻,没有间隙;)
注意:可能会出现过度执行…;)
Imo,关于连续存储的数据存储模式相对于非连续存储的优越性能的简化关键点是
1.缓存未命中
现代CPU不会从内存中提取单个字节,而是提取稍大的块。因此,如果数据对象的大小小于这些块的大小,并且存储是连续的,那么一次可以获得多个元素,因为多个元素可能在一个块中。
示例:
一个64字节的块(通常的高速缓存行大小)一次可容纳16个32位整数。因此,从第一个元素进入缓存的那一刻起,在处理完16个元素后,最早会发生缓存未命中(数据尚未在缓存中->需要从主内存加载)。如果使用链表,那么第一个元素很可能是64字节块中唯一的元素。理论上,列表中的每个元素都可能发生缓存未命中。
具体示例:
std::vector<std::uint32_t> v;
// somehow fill 64 values into v
std::uint32_t s{};
for(std::size_t i{0}; i<v.size(); ++i)
{
s += v[i];
}
想象一下v
的内容还没有被缓存。
在处理for循环中的数据过程中会发生什么
1) 检查元素v[0]是否在缓存中。-->无
2) 从主存储器中提取从v[0]地址开始的64个字节到高速缓存行
3) 从缓存加载v[0],并通过将其值添加到s 进行处理
4) 元素v1是否在缓存中?-->是,由于邻近v[0],已加载以前的提取
5) 从缓存加载v1,并通过将其值添加到s 进行处理
6) 元素v[2]是否在缓存中?-->对
7) 从缓存加载v[2],并通过将其值添加到进行处理
等等。
34)元素v[16]是否在缓存中?-->无
35)从主存储器获取从v[16]的地址开始的64个字节到高速缓存行
36)从缓存加载v[16],并通过将其值添加到进行处理
37)元素v[17]是否在缓存中?-->是,由于邻近v[16],已加载以前的提取
等等。。。
将数据从主存中提取到高速缓存比将数据从高速缓存加载到处理器寄存器和执行简单操作花费更多的时间。因此,多个值可能位于单个缓存线上,这一事实可以显著提高性能。
链表不能提供连续的存储保证,您也不能指望获得这种性能提升。这也是为什么对于连续容器,随机迭代(随机访问元素)的性能比正向迭代(按顺序访问元素)差的原因。
2.预取
CPU的"预取器"功能放大了上述效果。
如果已经从主存储器加载了一个块,则预取器准备加载下一个块/已经将其放入缓存,从而显著降低从主存储器的该部分加载内容的惩罚。
当然,只有当您确实需要来自下一个准备好的区块的数据时,这才是有效的。
矢量通常是如何工作的(内部)
请参阅:c++Vector,每当它在堆栈上扩展/重新分配时会发生什么?
Stroustrup写了一篇文章https://isocpp.org/blog/2014/06/stroustrup-lists这说明他被误解了,并不是说要避开链表。
我不能评论向量和链表的C++实现。。但是,你说你理解链表而不是向量。我可以说,在Java中,人们理解向量,但不一定理解链表。C#有一个List数据类型,大多数人并没有真正考虑它是作为链表还是作为向量实现的。这里就用法上的差异进行了很好的讨论。https://www.reddit.com/r/learnprogramming/comments/15mxrt/what_are_the_real_world_applications_of_linked/一条评论说";存储在链表中的数据一旦分配到内存中,就会保留在同一位置。这意味着,当链表的大小发生变化时,任何元素的数据都不会移动(在内存中),因此可以安全地指向它;
Straustrup的文章说";是的,我的建议是默认使用std::vector。更一般地说,除非有充分的理由不使用连续表示。与C一样,C++在默认情况下也是这样设计的。此外,请不要在没有测量的情况下对性能进行陈述"
我在一次采访中看到Stroustrup说,删除一个元素并将其全部上移真的很快,而且比从链表中删除一个元件更快。但我想人们不应该因此得出结论,他说链表没有用例。