上周我已经读过很多关于缓存局部性和流水线在cpu中的概念。虽然这些概念很容易理解,但我有两个问题。假设可以在对象向量或指向对象的向量之间进行选择(如this question中所示)。
然后使用指针的一个论点是,放弃较大的对象可能会很昂贵。但是,我无法找到何时应该调用大对象。几个字节的对象已经很大了吗?
针对指针的参数是缓存局部性的丢失。如果使用两个向量,第一个包含对象并且不会重新排序,第二个包含指向这些对象的指针,它会有帮助吗?假设我们有一个包含200个对象的向量,并创建一个带有指向这些对象的指针的向量,然后随机地随机移动最后一个向量。如果我们用指针循环向量,缓存局部性是否会丢失?
最后一个场景在我的程序中发生了很多,我有City对象,然后有大约200个指向这些城市的指针向量。为了避免每个城市有200个实例,我使用指针向量而不是城市向量。
答案 0 :(得分:3)
这个问题没有简单的答案。您需要了解系统如何与内存相关联,您对容器执行的操作以及哪些操作“重要”。但通过了解概念和影响内容的因素,您可以更好地理解事物的运作方式。所以这里有一些关于这个主题的“讨论”。
“缓存局部性”主要是“将内容保留在缓存中”。换句话说,如果你看A,然后B,A和A位于B附近,它们可能会一起加载到缓存中。
如果对象足够大以至于它们填充一个或多个缓存行(现代CPU的缓存行为64-128字节,移动缓存行有时更小),“行中的下一个对象”将不在缓存中无论如何[1],所以“向量中的下一个元素”的缓存局部性不太重要。对象越小,您获得的效果就越大 - 假设您按照存储顺序访问对象。如果你选择一个随机数,那么其他因素开始变得重要[2],缓存局部性就不那么重要了。
另一方面,随着对象变大,在向量中移动它们(包括生长,移除,插入以及“随机随机播放”)将会更耗时,因为复制更多数据会变得更加广泛。 / p>
当然,总是需要从指针读取而不是直接在向量中读取元素,因为指针本身需要在读取指针对象中的实际数据之前“读取”。同样,当随机访问事物时,这变得更加重要。
我总是从“最简单的东西”开始(这取决于代码的整体结构,例如,有时创建指针向量更容易,因为你必须首先动态创建对象)。系统中的大多数代码无论如何都不是性能关键,所以为什么要担心它的性能 - 只要让它正常工作,如果它没有出现在你的性能测量中就离开它。
当然,如果你在容器中进行大量的物体移动,也许矢量不是最好的容器。这就是为什么有多个容器变体 - vector
,list
,map
,tree
,deque
- 因为它们在访问和插入方面具有不同的特征/ remove以及线性遍历数据的特征。
哦,在你的例子中,你谈到了200个城市对象 - 好吧,无论如何,它们可能都适合任何现代CPU的缓存。所以把它们放在矢量中。除非一个城市包含居住在城市中的每个人的列表......但这本身可能应该是vector
(或其他容器对象)。
作为一项实验,制作一个程序,在std::vector<int>
和std::vector<int*>
上执行相同的操作[例如填充随机数,然后对元素进行排序],然后创建一个大的对象[在那里粘贴一些整数数组,或者一些整数,使用一个整数,这样你就可以对它进行相同的操作。改变存储对象的大小,并查看其行为方式。在你的系统上,有指针的好处在于有普通对象。当然,也会改变元素的数量,看看它有什么效果。
[1]好吧,现代处理器使用缓存预取,可以推测性地将“下一个数据”加载到缓存中,但我们当然不能依赖它。
[2]这种情况的极端情况是具有大量订户(数百万)的电话交换机。拨打电话时,会在表格中查找呼叫者和被呼叫者。但是,调用者或被调用者在缓存中的几率几乎为零,因为(假设我们正在处理一个大城市,比如伦敦),每秒发出和接收的呼叫数量非常大。因此,缓存变得无用,并且变得更糟,因为处理器还缓存页表条目,并且它们也很可能已经过时。对于这些类型的应用程序,CPU设计人员拥有“大页面”,这意味着内存被分成1GB页面,而不是通常的4K或2MB页面已经存在了一段时间。这减少了“我们到达正确的地方”之前所需的内存读取量。当然,这同样适用于各种其他“大型数据库,不可预测的模式” - 航空公司,facebook,stackoverflow都有这些问题。