使用指针或引用访问向量然后遍历它缓存不友好吗?
Is using a pointer or reference to access a vector and then iterating through it cache unfriendly?
我有一个指向存储在其他对象中的向量的指针。
vector<Thing>* m_pThings;
然后当我想遍历这个向量时,我使用下面的 for
循环:
for (auto& aThing : *m_pThings){
aThing.DoSomething();
}
假设 Thing::DoSomething()
存在。
我的问题是:此代码在取消引用 m_pThings
时是否会导致太多缓存未命中?我想知道这一点,因为我特意制作了一个向量,以便所有 Thing
都连续保存在内存中并避免缓存未命中,并且想知道像这样取消引用指针是否会导致缓存未命中而毁了我的努力。
对于 for (auto& aThing : *m_pThings)
,使用 vector<Thing>*
v/s vector<Thing>
几乎不会改变性能,因为在这两种方式中你指的是只包含指针的向量(假设这些是实际数据的开始、结束和 end_of_allocation),主要性能影响将由向量数据的布局引起,而不是由向量原始指针本身引起。
但是为了便于阅读,您应该更喜欢使用 vector<Thing>
我只想说,将一个值传递给 for-each 语句和传递一个取消引用的指针之间的区别最多只是几个时钟周期的一次性惩罚。 for-each 语句通过调用您传入的对象的 begin()
函数来工作,其中 returns 是一个迭代器。然后它仅使用该迭代器完成其余工作,这在两种情况下都是相同的。没有必须取消引用指针的重复开销。
在现代处理器上,几乎所有你能做的操作都不会对缓存太不友好,除非你用一些奇怪的 n 次多项式的模式来寻址内存。即使您的程序在几个不同的地址之间快速跳转,缓存通常也很容易处理此类行为,尤其是当某些地址是静态地址时,例如您的 m_pThings 向量。
最后,除非您设计的模式将在整个庞大的程序中使用,否则像这样的选择并不需要预先进行深入检查。过早的优化是邪恶的,应该不惜一切代价避免!如果它被证明是一个问题,你的探查器会提醒你这个特定的设计正在吞噬周期,应该重新检查。
如果你真的想知道,基准测试是你最好的选择。当涉及到这些本质上非常依赖于特定硬件实现的问题时,再多的代码分析也无法击败简单的基准测试。不过,并非所有缓存的行为方式都相同,因此虽然一个 CPU 可以毫不费力地处理算法,但另一个 CPU 可能会自行燃烧。
所以我的回答基本上归结为:做你认为最清楚and/or方便的事情,因为它可能实际上并不重要。如果它被证明是一个问题,您最好重新考虑算法本身,而不是提高缓存性能。缓存应该被认为是一种奖励,而不是必需品。如果您的代码仅在某些特定的缓存行为下运行良好,那您就错了*。
*我想做的一个例外是,如果您正在为单个特定目标(例如游戏机或其他专有嵌入式系统)编程。
它会在您第一次检查它时导致缓存未命中,如果在下次重用同一向量之间有太多时间 - 它可能会被从缓存中清除。
连续存储的好处主要来自两点 - 如果元素小于高速缓存行,第一次提取也会同时带来接下来的几个项目,所以你节省了内存带宽(这意味着如果您的内存受到许多请求的压力并且您 运行 缓冲区不足,则会延迟。
第二个好处是来自硬件预取器,它可以 运行 领先于连续的内存范围并减少任何后续需求的访问延迟。
您没有指定 Thing
的大小、该数组的重用距离,或者它是否足够小以适合缓存(如果是的话 - 什么级别) - 这些参数可能确定上述哪些好处适用。
无论哪种方式,您通过指针引用数组这一事实确实不会影响它的连续放置 - 硬件最终会看到一个接一个访问的地址,并能够在缓存或预取时使用该事实它。但是,请注意它仍然不能保护您免受缓存抖动。
我有一个指向存储在其他对象中的向量的指针。
vector<Thing>* m_pThings;
然后当我想遍历这个向量时,我使用下面的 for
循环:
for (auto& aThing : *m_pThings){
aThing.DoSomething();
}
假设 Thing::DoSomething()
存在。
我的问题是:此代码在取消引用 m_pThings
时是否会导致太多缓存未命中?我想知道这一点,因为我特意制作了一个向量,以便所有 Thing
都连续保存在内存中并避免缓存未命中,并且想知道像这样取消引用指针是否会导致缓存未命中而毁了我的努力。
对于 for (auto& aThing : *m_pThings)
,使用 vector<Thing>*
v/s vector<Thing>
几乎不会改变性能,因为在这两种方式中你指的是只包含指针的向量(假设这些是实际数据的开始、结束和 end_of_allocation),主要性能影响将由向量数据的布局引起,而不是由向量原始指针本身引起。
但是为了便于阅读,您应该更喜欢使用 vector<Thing>
我只想说,将一个值传递给 for-each 语句和传递一个取消引用的指针之间的区别最多只是几个时钟周期的一次性惩罚。 for-each 语句通过调用您传入的对象的 begin()
函数来工作,其中 returns 是一个迭代器。然后它仅使用该迭代器完成其余工作,这在两种情况下都是相同的。没有必须取消引用指针的重复开销。
在现代处理器上,几乎所有你能做的操作都不会对缓存太不友好,除非你用一些奇怪的 n 次多项式的模式来寻址内存。即使您的程序在几个不同的地址之间快速跳转,缓存通常也很容易处理此类行为,尤其是当某些地址是静态地址时,例如您的 m_pThings 向量。
最后,除非您设计的模式将在整个庞大的程序中使用,否则像这样的选择并不需要预先进行深入检查。过早的优化是邪恶的,应该不惜一切代价避免!如果它被证明是一个问题,你的探查器会提醒你这个特定的设计正在吞噬周期,应该重新检查。
如果你真的想知道,基准测试是你最好的选择。当涉及到这些本质上非常依赖于特定硬件实现的问题时,再多的代码分析也无法击败简单的基准测试。不过,并非所有缓存的行为方式都相同,因此虽然一个 CPU 可以毫不费力地处理算法,但另一个 CPU 可能会自行燃烧。
所以我的回答基本上归结为:做你认为最清楚and/or方便的事情,因为它可能实际上并不重要。如果它被证明是一个问题,您最好重新考虑算法本身,而不是提高缓存性能。缓存应该被认为是一种奖励,而不是必需品。如果您的代码仅在某些特定的缓存行为下运行良好,那您就错了*。
*我想做的一个例外是,如果您正在为单个特定目标(例如游戏机或其他专有嵌入式系统)编程。
它会在您第一次检查它时导致缓存未命中,如果在下次重用同一向量之间有太多时间 - 它可能会被从缓存中清除。
连续存储的好处主要来自两点 - 如果元素小于高速缓存行,第一次提取也会同时带来接下来的几个项目,所以你节省了内存带宽(这意味着如果您的内存受到许多请求的压力并且您 运行 缓冲区不足,则会延迟。
第二个好处是来自硬件预取器,它可以 运行 领先于连续的内存范围并减少任何后续需求的访问延迟。
您没有指定 Thing
的大小、该数组的重用距离,或者它是否足够小以适合缓存(如果是的话 - 什么级别) - 这些参数可能确定上述哪些好处适用。
无论哪种方式,您通过指针引用数组这一事实确实不会影响它的连续放置 - 硬件最终会看到一个接一个访问的地址,并能够在缓存或预取时使用该事实它。但是,请注意它仍然不能保护您免受缓存抖动。