C ++,对缓存局部性改进进行基准测试的方法?

时间:2009-06-16 21:13:53

标签: c++ performance pointers caching benchmarking

我有一个类X的实现,它有两个指向两条信息的指针。我编写了一个新的实现,类Y,它只有一个指向结构的指针,该结构包含两条信息作为相邻的成员。 X和Y的方法通常只需要操作其中一条信息,但提供一个get()方法返回指向第二块的指针(在这种情况下,类X只返回它指向该块的指针,类Y返回地址结构的第二个成员)。在正常使用中,调用X和Y的方法会发生在调用get()并对返回的第二部分进行处理时间穿插。

我希望在现实生活中,应该有一个性能提升,现在这两个信息在Y类实现中在内存中彼此相邻(因为它们是结构的相邻成员),但是我我没有看到我写过的基准测试有任何不同(散布调用X和Y的方法,在大循环中对他们的第二部分进行工作)。我怀疑这是因为在我的测试中,所有内容都适合缓存。我不想在我的真实应用程序中尝试这个,因为X和Y的语义在与此优化无关的其他微妙方式上有所不同,并且移植使用应用程序将是一些工作,并且这些基准应该有助于证明这样做首先工作。

由于更好​​的缓存局部性,观察性能差异的最佳方法是什么?如果我在一个数组上做一堆虚拟工作,等于调用之间的缓存大小就足够了吗?或者我是否想要在稍微小于缓存大小的数组上进行工作,以便在我的类实例上工作会导致事情进入和退出缓存?我不确定如何编写针对编译器优化和不同高速缓存大小的强大代码。

3 个答案:

答案 0 :(得分:8)

如果您使用的是Linux,那么将CachegrindKCacheGrind结合使用可能会更深入地了解缓存的行为方式。

答案 1 :(得分:2)

您可以专门设计一个基准来破坏缓存。例如,分配指向的数据块,使得它们都保证在不同的高速缓存行上(例如,通过使用将分配填充到至少几百个字节的自定义存储器分配器)。然后反复遍历一些太大的对象,以适应甚至L2缓存中的所有内容(非常依赖于平台,因为它取决于缓存中的行数,但是100万将覆盖大多数体系结构,只需要几百兆RAM总)。

这将为您提供从X到Y的更改所带来的性能增益的上限。但它通过将X的性能降低到任何可能的实际使用率以下来实现。为了证明你的情况,你需要一个下限估计,而不是一个上限估计。所以我不确定你会取得多大成就,除非你发现即使是最糟糕的情况仍然没有显着差异,你也不必费心去做优化。

即使你没有针对X的理论最坏情况性能,任何设计超出缓存的基准都只是选择X的不良性能的任意点,并期待看Y是否更好。为了让Y看起来不错,与基准相比并不遥远。你的代码如何在狡猾的基准测试中表现无关紧要,除了营销谎言文学的目的。

观察现实世界性能差异的最佳方法是衡量班级的真实世界客户。你说“X和Y的语义在与这种优化无关的其他微妙方式上有所区别”,在这种情况下我只能建议你写一个Z类,它不同于X 只有这个优化,并在您的应用程序中用作比较。

一旦您的测试试图表现出最糟​​糕的实际用途,那么如果您没有看到任何性能差异,那么可能无法获得性能提升。

所有这一切,如果它具有逻辑意义(也就是说,它不会使代码更令人惊讶),那么我会主张最小化C ++中的堆分配数量,这只是一个经验法则。它不会使速度或总内存使用率变差,并且它确实倾向于简化您的资源处理。当然,经验法则不能证明重写工作代码是合理的。

答案 2 :(得分:0)

如果我正确理解你的情况(如果没有,请纠正我),那么这是六个中的一个,或另外六个。

在X类中,您需要一个指针查找任何一条信息。在Y类中,您需要对第一个进行一次查找,对第二次进行两次(获取第一次然后偏移)。那是为了另一个内存访问而牺牲“地点”。不幸的是,编译器仍然非常擅长浪费公共汽车时间在RAM中查找单词。

如果可能的话,你可以通过直接在相关类中保存两条目标信息(即每个它自己的类成员)来获得最佳结果,而不是使用那些指针进行不必要的间接寻址。没有看到任何代码,这就是我所能说的全部。

无论如何,在研究应用程序的算法复杂性时,您将获得很多更多的性能,而不是在类定义中微优化两个变量。另外一个好主意是使用分析工具来(客观地)查看瓶颈所在的位置(gprof在* nix系统上很常见)。您是否有明确的理由想要特别增加位置缓存?