8

I have an implementation of a class X, that has two pointers to two pieces of information. I have written a new implementation, class Y, that has only one pointer to a struct that contains the two pieces of information together as adjacent members. X's and Y's methods usually only need to manipulate one of the pieces of information, but provide a get() method that returns a pointer to the second piece (in this case class X just returns its pointer to that piece and class Y returns the address of the struct's second member). In normal usage, calls to X's and Y's methods will happen interspersed by calls to get() and doing work on that returned second piece.

I expect that in real life situations there should be a performance improvement, now that the two pieces of information are next to one another in memory in the class Y implementation (because they are adjacent members of a struct), but I'm not seeing any difference in the benchmarks I've written (interspersing calls to X's and Y's methods with doing work on their second pieces in big loops). I suspect this is because everything fits in cache in either case in my tests. I don't want to try this in my real app yet because the semantics of X and Y differ in other subtle ways not related to this optimization and porting the using application will be some work, and these benchmarks are supposed to help justify doing that work in the first place.

What's the best way to observe the difference in performance due to better cache locality? If I do a bunch of dummy work on an array equal to the size of the cache in between calls is that sufficient? Or do I want to do work on an array slightly less than the cache size, so that work on my instances of my class will cause things to fall in and out of cache? I'm not sure how to code something that is robust against compiler optimizations and different cache sizes.

4

3 回答 3

8

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

于 2009-06-16T22:57:44.747 回答
2

您可以专门设计一个基准来破坏缓存。例如,分配指向的数据块,以确保它们都位于不同的缓存行上(例如,通过使用自定义内存分配器将分配填充到至少几百字节)。然后反复迭代一些太大的对象,即使 L2 缓存也无法容纳所有内容(非常依赖于平台,因为它取决于缓存中的行数,但是 100 万将涵盖大多数架构并且只需要几百兆 RAM全部的)。

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

即使您的目标不是 X 的理论上的最坏情况性能,任何旨在超过缓存的基准测试也只是选择 X 性能不佳的任意点,并查看 Y 是否更好。操纵基准以使 Y 看起来不错并不遥远。您的代码在不可靠的基准测试中表现如何并不重要,除非可能是出于营销目的的谎言文学。

观察实际性能差异的最佳方法是衡量您班级的真实客户。您说“X 和 Y 的语义在与此优化无关的其他细微方面有所不同”,在这种情况下,我只能建议您编写一个在此优化方面与 X 不同的类 Z,并将其用于您的应用程序作为比较。

一旦您的测试试图代表最差的实际使用,那么如果您没有看到任何性能差异,则可能没有性能提升。

综上所述,如果它合乎逻辑(也就是说,它不会使代码变得更令人惊讶),那么我会提倡尽量减少 C++ 中的堆分配数量,这只是作为一个经验法则。它不会使速度或总内存使用量变得更糟,而且它确实会简化您的资源处理。当然,经验法则并不能证明重写工作代码是合理的。

于 2009-06-17T00:49:31.837 回答
0

如果我正确地理解了您的情况(如果不是,请纠正我),那么它是一个六分之一,或者六分之一。

在 X 类中,您需要一个指针查找任一信息。在 Y 类中,第一个需要一个查找,第二个需要两个(获取第一个然后偏移)。这是为了另一个内存访问而牺牲“局部性”。不幸的是,编译器仍然非常擅长浪费总线时间在 RAM 中查找单词。

如果可能的话,通过将两条目标信息直接保存在所讨论的类中(即每个都是它自己的类成员),而不是将这些指针用于不必要的间接,您将获得最佳结果。没有看到任何代码,这几乎就是我能说的。

无论如何,通过研究应用程序的算法复杂性,您将获得比在类定义中对两个变量进行微优化所获得的更多性能还有一个好主意是使用分析工具(客观地)查看瓶颈在哪里(gprof 在 *nix 系统上很常见)。您是否有明确的原因要专门增加局部缓存?

于 2009-06-16T22:42:38.223 回答