为啥缓存位置对阵列性能很重要?
Posted
技术标签:
【中文标题】为啥缓存位置对阵列性能很重要?【英文标题】:Why does cache locality matter for array performance?为什么缓存位置对阵列性能很重要? 【发布时间】:2012-08-17 10:37:35 【问题描述】:在下面的blog 中有一个关于数组优于链表的声明:
数组具有更好的缓存局部性,可以对性能产生相当大的影响。
这是什么意思?我不明白缓存局部性如何提供巨大的性能优势。
【问题讨论】:
如果您了解cache 的工作原理,那么您还将了解 1)“引用位置”是一件好事,以及 2)从数组中访问数据通常更有可能具有良好的“ locality”而不是从列表中访问相同的数据。 值得注意的是,虽然这是真的,但单链表与连续分配器相结合可能是一项巨大的资产,主要是因为将元素从一个容器转移到另一个容器只涉及指针逻辑。然而,如果你看一下它们的内存布局,它是连续的,看起来像一个数组,只有指向数组中下一个元素的链接,所以它仍然是缓存友好的(至少在列表全部重新组织之前)。跨度> 【参考方案1】:查看我的回答about spatial and temporal locality。
特别是,数组是连续的内存块,因此它们中的大块将在第一次访问时加载到缓存中。这使得访问数组的未来元素相对较快。另一方面,链表不一定位于连续的内存块中,并且可能导致更多的缓存未命中,从而增加了访问它们所需的时间。
考虑以下可能的内存布局,用于数组data
和链表l_data
的大型结构
Address Contents | Address Contents
ffff 0000 data[0] | ffff 1000 l_data
ffff 0040 data[1] | ....
ffff 0080 data[2] | ffff 3460 l_data->next
ffff 00c0 data[3] | ....
ffff 0100 data[4] | ffff 8dc0 l_data->next->next
| ffff 8e00 l_data->next->next->next
| ....
| ffff 8f00 l_data->next->next->next->next
如果我们想遍历这个数组,第一次访问ffff 0000
将需要我们去内存中检索(CPU 周期中的一个非常慢的操作)。但是,在第一次访问之后,数组的其余部分将在缓存中,随后的访问会快得多。使用链表,第一次访问ffff 1000
也需要我们去记忆。不幸的是,处理器将直接缓存该位置周围的内存,一直到ffff 2000
。如您所见,这实际上并没有捕获列表中的任何其他元素,这意味着当我们访问l_data->next
时,我们将再次访问内存。
【讨论】:
请注意,链表的局部性可以通过使用内存池来提高。但是您仍然存在“下一个”指针占用额外空间的问题。 @paddy 提出了一个很好的观点,因为通常这就是链表的实现方式 现在我明白了“链接列表中的缓存未命中”的含义。 那么链接列表的参考位置是空间的还是时间的,或者两者兼有,还是没有?【参考方案2】:通常,在使用数组时,您会访问彼此靠近的项目。顺序访问数组时尤其如此。
当你访问内存时,它的一部分被缓存在不同的级别。 缓存局部性是指连续操作在缓存中的可能性,因此速度更快。在数组中,您可以最大限度地提高顺序元素访问在缓存中的机会。
对于列表,作为反例,不能保证在列表中按顺序出现的项目实际上在内存中彼此靠近排列。这意味着更少的缓存命中率和性能下降。
【讨论】:
然而,这在很大程度上取决于处理器和内存架构。例如,为面向对象编程而设计的 CPU 通常不关心局部性,仅仅是因为“面向对象”的定义无论如何您都无法保证局部性。 @JörgWMittag 那么你的意思是说用 OOP 语言编写的程序不能有效地使用缓存,或者与用过程语言编写的程序相比,在这种情况下缓存未命中率更高? @SaurabhPatil 在 OO 中,您通常有一个对象数组。如果您必须访问每个对象的一个属性,CPU 会加载大量同样属于对象的不相关数据。有关更多说明,请参阅此链接。 en.wikipedia.org/wiki/AoS_and_SoA 嗯,不仅如此,对象通常是动态分配的。即使使用世界上最智能的内存分配器,它可能会导致对象被打包在一起,或者如果您将其分配为数组,因为对象的某些部分必须引用一个类,那么按照设计,您将丢失一些内存到该元数据.在 C++ 中,这是存储在对象内部的虚拟表。其他 OO 语言可能使用不同的方案,但您最终会在内存中分隔对象数据,或者在其他地方存储一些查找表:其中任何一种都会影响局部性。以上是关于为啥缓存位置对阵列性能很重要?的主要内容,如果未能解决你的问题,请参考以下文章