【问题标题】:x86 Assembly Force Cache Storex86 汇编强制缓存存储
【发布时间】:2013-09-02 17:37:59
【问题描述】:

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

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

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

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

非常感谢!

【问题讨论】:

  • 也许这个类似的问题可能会有所帮助:stackoverflow.com/a/12697439/358328
  • 虽然这个问题是相关的,但它并不完全针对测量延迟,这是我唯一关心的事情。不过谢谢你的帖子!我正在尝试从那里开始工作。

标签: c caching assembly x86


【解决方案1】:

完全没有理由为此作业使用汇编。您的解决方案不需要汇编 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 缓存而言,指令更容易。根据您的操作系统和管理内存的方式,启用数据缓存可能会也可能不会简单。

【讨论】:

  • 非常感谢您。这绝对是一个很好的答案。使用这种想法,我成功地实现了它。
【解决方案2】:

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

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

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

【讨论】:

  • 感谢您的回复。但是,我对您在这里所说的话感到非常困惑。我最终只是想测量每一级缓存的延迟。我不熟悉缓存对齐,并且不确定查看编译器的汇编程序输出比一次查看所有程序集有什么相关性。
  • 您需要查看处理器及其缓存选项的文档。然后,用调试器查看代码在内存中的位置,看看它们是如何与缓存行对齐的。然后,您可以计算处理器访问每行的次数,以查看您有多少缓存命中。根据处理器和工具,您可能可以获得实际的计数器,但此建议无需额外的处理器支持即可工作。
  • 您可以从处理器架构文档中确定缓存大小。然后对齐将取决于您的操作系统如何将虚拟地址空间映射到物理内存。或者,可能有一个 API 可以访问此信息。在 Windows 中,您可以使用WMI Win32_Processor class 来确定这些值。查看汇编程序的目的是让您在运行基准测试时计算出各个代码元素在加载后的地址。
【解决方案3】:

您实际上并不需要在行粒度的基础上查看它,维护起来相当复杂(正如 dwelch 在他的非常好的回答中指出的那样),除非重复足够多的时间(这反过来可以保持正确的条件以强制命中某个缓存级别变得复杂)。

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

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

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

【讨论】:

    猜你喜欢
    • 2012-10-25
    • 1970-01-01
    • 2018-05-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-11-19
    • 1970-01-01
    • 2022-01-11
    相关资源
    最近更新 更多