6

我有一个任务,我需要测量访问 L1、L2 和 L3 缓存以及主内存中数据的延迟。这将在 C 中完成。

我花了几个小时研究测量缓存延迟的方法,但结果很少。我已经下载了一些基准测试工具,这些工具给了我缓存访问时间,但是在我自己的代码中实现这一点时,我还没有得到任何结果。我知道缓存发生的事情在 C 中不取决于我。

我的下一个想法是,如果我可以用 x86 程序集(首先想到的)强制填充缓存,那么只需对我刚刚加载的数据执行时钟()、访问()、时钟(),据说时间是准确的(ish)访问时间,因为我知道它应该在缓存中找到,因为我只是用我的内联汇编或类似方法将它放在那里......

如果有人能对我在这里的任务提供见解,那就太好了。无论是告诉我我很想使用 asm 在缓存中加载某些东西,还是向我介绍其他可能对我有帮助的东西。

非常感谢!

4

3 回答 3

8

完全没有理由为此分配使用汇编。您的解决方案不需要汇编 C 也可以。我假设你在操作系统之上运行,所以这会妨碍你的测量,既执行你认为你知道它们在哪里的事情,也测量你认为你正在测量的东西。

就进行这些测量而言,缓存基础知识......可以说有四层内存。L1,最快,但也是最昂贵和最小的。然后是L2。速度较慢,价格不高,可能比 L1 大。L3 更便宜、更慢、更大,然后是主内存最慢、最便宜和最大。

假设我们有四块内存要使用 A、B、C 和 D。L1 一次只能保存一个块。L2 一次两个,L3 四个中的三个,主内存全部四个。

如果我们进行读取,它首先通过 L1,如果有未命中则通过 L2,如果未命中则通过 L3,如果未命中则它将始终在主存储器中。了解虽然这些数据在返回的路上被缓存,所以 L3、L2 和 L1 都将包含刚刚读取的数据,并在必要时驱逐(并非总是如此,但假设这个简单的缓存模型可以理解如何完成您的任务)。因此,如果我们读取块 A,则 L1、L2 和 L3 都将包含块 A。现在在这个假设模型中,如果我们读取块 B,则 L1 将包含 B,驱逐 A。L2 将包含 A,b 和 l3 将包含 A和 B。读取 C 和 L1 将包含 C,驱逐 B,假设 L2 选择驱逐 A,并包含 B 和 C,并且 L3 包含 A、B 和 C。读取 D 和 L1 将包含 C 假设 L2 驱逐B 并包含 C 和 D,假设 L3 驱逐 A 并包含 B、C 和 D。

假设我们并不确切知道每个缓存如何选择驱逐什么以及保留什么。但是假设我们确实知道或可以从主板规格或其他来源中弄清楚每个缓存有多大。如果上面的例子按这个顺序发生,L1 有 D,L2 有 C 和 D,L3 有 B、C 和 D,而 main 有所有四个 a、b、c、d。然后,如果在该状态下我们读取所有块 A 并且时间它在理论上我们是从主内存中读取它,那么这不仅仅是读取该内存的时间,而且如果任何被驱逐的内存已经改变,它必须一直写到上游可能的命中。但理想情况下,如果您主要只是读取,那么您将主要为读取计时。

假设我们发现自己处于块 D 在 l1 中,c 和 d 在 l2 中,b,c,d 在 l3 中的情况下,我们读取了所有块 B 并对其计时。那不是测量访问 L3 的时间吗?在相同的起始条件下,读取 C 将为我们提供 l2 时间。在相同的起始条件下,读取 D 将是 l1 时间对吗?

诀窍是让自己进入这些条件。缓存的大小可能不会使 l2 是 l1 大小的两倍,依此类推,要完全控制 L1 中的内容,您需要读取足够的数据来填充 L1。此外,如果您要读取 L3 大小的数据量,那么理论上 L3 具有所有数据,L2 具有该数据的最后 L2 量,L1 具有该数据的最后 L1 量。

使用数据缓存比指令缓存更容易,但无论哪种方式都可以,您需要在主存储器中至少有 L3 大小的指令量,大量的 nop。执行线性指令块与读取线性内存块没有什么不同。就读取周期而言。就启用和使用 I 缓存而言,指令更容易。根据您的操作系统和管理内存的方式,启用数据缓存可能会也可能不会简单。

于 2013-09-02T21:46:25.173 回答
1

您应该能够通过查看编译器的汇编器输出来了解实际操作来避免汇编器。

即使您获得了高分辨率时钟,在运行基准测试时,您也几乎无法对操作系统进行抢占。您将需要执行多次运行才能获得有意义的结果。

与其试图将指令放入高速缓存,不如让处理器在它们运行时加载它们可能更容易。如果您在过程中放置​​不同数量的填充物,您可能能够使高速缓存行对齐到您想要的内容。

于 2013-09-02T19:56:06.203 回答
1

您实际上不需要在行粒度的基础上查看它,维护起来相当复杂(正如 dwelch 在他的非常好的回答中指出的那样),并且除非重复足够多的时间,否则几乎不可能测量(这反过来又会使维护复杂化强制命中某个缓存级别的正确条件)。

相反,您可以从编写一个位于连续物理空间中的简单数组开始(如果您的操作系统具有复杂的页面分配机制,您可能需要进行一些调整)。将数据填充到该数组中(每个缓存行一次访问就足够了),然后开始重复读取它。如果数组大小足够小以适合 L1(例如,对于 32k,您可以分配 32k 字符或稍微更少,并访问每 64 个元素),经过足够的重复后,您应该获得大部分访问。您可能会遇到其他缓存行干扰的极端情况,例如页面映射条目、堆栈或其他堆变量,但在大多数情况下,您会获得 L1 命中,因此可以很好地平衡。如果您重复足够多次以获得稳定的结果,即使是上下文切换之类的事件(以防您无法控制系统以防止这种情况发生)也会逐渐消失。

然后,开始逐渐增加您的数据集大小。一旦您通过了 L1 大小,您应该能够看到每次访问的时间明显下降(总时间除以 # 次访问)。请注意,缓存与 LRU 一起工作的方式,您以线性顺序访问数组的事实意味着一旦它大到不适合 L1,您不应该获得任何部分好处,因为最后一个元素可能会驱逐第一个及时阻止下一次迭代在那里找到它们(尽管您仍然可以享受现代 CPU 中负载可能出现故障的事实)。更进一步,一旦您或多或少地达到 L2 大小(如果您的系统中的 L2 不是严格包含在内,您可能会从同时拥有 L1+L2 中获得一点好处,但所描述的访问模式应该几乎完全防止这种情况发生)。

请注意,某些硬件功能可能会使事情复杂化——首先是硬件预取器,它几乎可以启动并获取您前面的行。如果可能的话,您应该通过 BIOS 禁用它,否则您可以大步前进(128 甚至 256 而不是 64) - 缓存很可能是直接映射的(具有一定的关联性),因此这将产生只强调每个2-4 组,其余为空,这很好(只要您记得将时间除以新的访问次数)。它还会中断流,足以让您获得实际时间,而不是预取辅助的时间(您可能也有一个基于步幅的预取器,但它通常不如流光强)。

于 2013-09-03T19:42:22.573 回答