如何导致指令缓存未命中?

时间:2012-05-14 17:57:19

标签: c linux performance microprocessors

我的任务是生成一定数量的数据缓存未命中和指令缓存未命中。我已经能够毫无问题地处理数据缓存部分。

所以我离开了生成指令缓存未命中。我不知道是什么导致这些。有人可以建议一种生成它们的方法吗?

我在Linux中使用GCC。

5 个答案:

答案 0 :(得分:19)

正如人们所解释的那样,指令缓存未命中在概念上与数据缓存未命中相同 - 指令不在缓存中。这是因为处理器的程序计数器(PC)已跳转到尚未加载到缓存中的位置,或者由于缓存已填满而已刷新,并且该缓存行是为驱逐而选择的缓存行(通常是最近一次)使用)。

手动生成足够的代码来强制指令未命中比强制数据高速缓存未命中要困难得多。

获取大量代码的一种方法是编写一个生成源代码的程序。

例如编写一个程序来生成一个带有巨大switch语句的函数(在C中)[警告,未经测试]:

printf("void bigswitch(int n) {\n    switch (n) {");
for (int i=1; i<100000; ++i) {
    printf("        case %d: n += %d;\n", n, n+i/2);
}
printf("    }\n    return n;}\n");

然后你可以从另一个函数调用它,你可以控制它沿着缓存行跳转的大小。

switch语句的属性是可以强制代码向后执行,或者通过选择参数以模式执行。因此,您可以使用预取和预测机制,或尝试对付它们。

同样的技术也可用于生成大量函数,以确保缓存可以随意“破坏”。所以你可能有bigswitch001,bigswitch002等。你可以使用你也生成的开关来调用它。

如果你可以使每个函数(大约)有一些大小的i-cache行,并且还生成比缓存中更多的函数,那么生成指令缓存未命中的问题就变得更容易控制了。

通过转储汇编程序(使用gcc -S)或objdump .o文件,您可以确切地看到函数,整个switch语句或switch语句的每个分支有多大。因此,您可以通过调整case:语句的数量来“调整”函数的大小。您还可以通过明智地选择bigswitchNNN()参数来选择命中的缓存行数。

答案 1 :(得分:9)

除了这里提到的所有其他方法之外,强制指令缓存未命中的另一种非常可靠的方法是使用自修改代码。

如果您写入内存中的代码页(假设您将操作系统配置为允许此操作),那么当然相应的指令行缓存会立即变为无效,并且处理器将被强制重新获取它。

顺便说一下,不是分支预测会导致icache错过,而只是分支。每当处理器尝试运行最近未运行的指令时,您都会错过指令缓存。现代x86足够智能,可以按顺序预取指令,因此您不可能仅仅通过从一条指令到另一条指令的普通步行而错过icache。但是任何分支(有条件的或其他的)都会不按顺序跳转到新地址。如果新指令地址最近没有运行,并且不在您已运行的代码附近,则可能是超出缓存,并且处理器必须停止并等待指令从主RAM进入。这与数据缓存完全一样。

一些非常现代的处理器(最近的i7)能够在代码中查看即将到来的分支,并启动icache预取可能的目标,但许多不能(视频游戏控制台)。从主RAM获取数据到icache与管道的“指令获取”阶段完全不同,这是分支预测的关键。

“指令获取”是CPU执行流水线的一部分,指的是将来自icache的操作码带入CPU的执行单元,在那里它可以开始解码和工作。这与“指令高速缓存”提取不同,“指令高速缓存”提取必须在许多周期之前发生,并且涉及高速缓存电路向主存储器单元发出请求以通过总线发送一些字节。第一个是CPU管道的两个阶段之间的交互。第二个是管道与内存缓存和主RAM之间的交互,这是一个更复杂的电路。这些名字容易混淆,但它们完全是分开的。

因此,导致指令缓存未命中的另一种方法是编写(或生成)许多非常大的函数,这样您的代码段就会很大。然后从一个函数疯狂地调用到另一个函数,这样从CPU的角度来看,你在整个内存中都在做疯狂的GOTO。

答案 2 :(得分:3)

您的项目需要了解目标系统的缓存硬件,包括但不限于其缓存大小(缓存的总体大小),缓存行大小(最小可缓存实体),关联性和写入&amp;替换政策。设计用于测试缓存性能的任何非常好的算法必须考虑所有这些,因为没有单一的通用算法可以有效地测试所有缓存配置,尽管您可以设计一个有效的参数化测试例程生成器,它可能会生成给出足够的关于给定目标的缓存架构的细节的合适的测试例程。尽管如此,我认为下面的建议是一个非常好的一般案例测试,但首先我想提一下:

你提到你有一个工作数据缓存测试使用“大整数数组a [100] .... [访问]元素,使得两个元素之间的距离大于缓存-line size(在我的情况下为32字节)。“我很好奇你是如何确定你的测试算法是如何工作的以及你如何确定算法导致的数据缓存未命中数,而不是其他因素造成的错误刺激。实际上,使用100 * sizeof(int)的测试数组,您的测试数据区域在当今大多数通用平台上只有400字节长(如果您使用的是64位平台,则可能为800字节,如果您使用的话,则为200字节; '使用16位平台)。对于绝大多数缓存体系结构,整个测试数组将多次适应缓存,这意味着对数组的随机访问将使整个数组在大约(400 / cache_line_size)* 2访问的某个位置进入缓存,并且每个访问除非您弹出一些硬件或操作系统滴答计时器中断并刷新部分或全部缓存数据,否则在此之后访问将是缓存命中,无论您如何订购访问权限。

关于指令缓存:其他人建议使用一个大的switch() - case语句或函数调用不同位置的函数,如果没有仔细(并且我的意思是小心)设计大小的话,这些函数都无法预测有效。相应案例分支或位置中的代码&amp;不同位置功能的大小。这样做的原因是整个存储器中的字节以完全可预测的模式“折叠”(在技术上,“另一个”别名“)。如果你仔细控制switch() - case语句的每个分支中的指令数量,你可能能够在某个地方进行测试,但如果你只是在每个分区中抛出大量不加区分的指令,你就不知道如何它们将折叠到缓存中,并且switch() - case语句的哪些情况相互别名,以便使用它们从缓存中逐出。

我猜你不是对汇编代码过于熟悉,但你必须在这里相信我,这个项目正在为此而尖叫。相信我,我不是一个不使用汇编代码的人,我更喜欢用OO C ++编程,使用STL&amp;尽可能多态的ADT层次结构。但在你的情况下,实际上没有其他万无一失的方法可以做到这一点,并且汇编将使你能够绝对控制你真正需要的代码块大小,以便能够有效地生成指定的缓存命中率。你不必成为装配专家,你可能甚至不需要学习说明书。实现C语言序言所需的结构结语(Google用于“C-callable assembly function”)。你为你的汇编函数编写了一些extern“C”函数原型,然后就开始了。如果您确实需要学习一些装配,那么您在装配功能中放置的测试逻辑越多,您对测试施加的“海森堡效应”就越少,因为您可以仔细控制测试控制指令的位置(从而它们对指令缓存的影响)。但是对于大部分测试代码,你可以使用一堆“nop”指令(指令缓存并不真正关心它包含的指令),并且可能只是让你的处理器&#34;&#34;返回& #34;每个代码块底部的指令。

现在假设您的指令缓存为32K(按照今天的标准来说相当小,但在许多嵌入式系统中可能仍然很常见)。如果你的缓存是4向关联的,你可以创建八个独立的内容相同的8K汇编函数(你希望它们注意到的是64K的代码,是缓存大小的两倍),其中大部分只是一堆NOP指令。你让它们在内存中一个接一个地落下(通常只是在源文件中简单地定义每个一个接一个)。然后使用仔细计算的序列从测试控制函数中调用它们,以生成您想要的任何缓存命中率(由于函数每个都是一个完整的8K长,因此具有相当的过程粒度)。如果你一个接一个地调用第一个,第二个,第三个和第四个函数,你知道你已经用这些测试函数的代码填充了整个缓存。此时再次调用其中任何一个都不会导致指令缓存未命中(除了由测试控制函数自己的指令驱逐的行),而是调用其他任何一个(第5,6,7或8个;让我们只是选择第5个)将驱逐其中一个(虽然哪个被驱逐取决于你的缓存的替换政策)。在这一点上,唯一一个你可以打电话并且知道你不会驱逐另一个的是你刚刚打电话的那个(第五个),而你唯一可以打电话并且知道你的人将驱逐另一个是你还没有叫(第6,7或8号)。为了使这更容易,只需维护一个与您拥有的测试函数数量相同的静态数组。要触发逐出,请调用阵列末尾的函数&amp;将其指针移动到数组的顶部,将其他指针向下移动。要不触发驱逐,请调用您最近调用的那个(阵列顶部的那个;在这种情况下一定不要将其他人调低!)。如果您需要更精细的粒度,请对此进行一些修改(可能会生成16个独立的4K装配函数)。当然,所有这些都取决于测试控制逻辑大小与高速缓存的每个关联“路径”的大小相比是无关紧要的;为了获得更好的控制,你可以将测试控制逻辑放在测试函数中,但是为了完美控制,你必须完全没有内部分支设计控制逻辑(只在每个汇编函数的末尾分支),但我认为我会在这里停下来,因为这可能使事情变得过于复杂。

袖口&amp;未经测试,x86的一个汇编函数的整体可能如下所示:

myAsmFunc1:
   nop
   nop
   nop  # ...exactly enough NOPs to fill one "way" of the cache
   nop  # minus however many bytes a "ret" instruction is (1?)
   .
   .
   .
   nop
   ret  # return to the caller

对于PowerPC,它可能看起来像这样(也是未经测试的):

myAsmFunc1:
   nop
   nop
   nop   # ...exactly enough NOPs to fill one "way" of the cache
   .     # minus 4 bytes for the "blr" instruction.  Note that
   .     # on PPC, all instructions (including NOP) are 4 bytes.
   .
   nop
   blr   # return to the caller

在这两种情况下,用于调用这些函数的C ++和C原型将是:

extern "C" void myAsmFunc1();    // Prototype for calling from C++ code
void myAsmFunc1(void);           /* Prototype for calling from C code */

根据您的编译器,您可能需要在汇编代码本身的函数名前面加下划线(但不能在C ++ / C函数原型中)。

答案 3 :(得分:0)

对于指令缓存未命中,您需要执行相距很远的代码段。在多个函数调用之间拆分逻辑将是一种方法。

答案 4 :(得分:-1)

在不可预测条件下的if else链(例如输入或随机生成的数据),在if情况下和在大小比缓存行大的else情况下具有指令量。