微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

如何导致指令caching未命中?

我一直负责生成一定数量的数据caching未命中和指令caching未命中。 我已经能够处理数据caching部分没有问题。

所以我留下了生成指令caching未命中。 我不知道是什么原因造成的。 有人可以提出一个生成它们的方法吗?

我在Linux中使用GCC。

从软件的angular度来看,截图是如何工作的

AM335x入门套件使用qemu模拟

如何从计算机体系结构的angular度来执行应用程序

无法在Core i7中禁用硬件预取程序

正如人们所解释的,指令缓存未命中在概念上与数据缓存未命中相同 – 指令不在缓存中。 这是因为处理器的程序计数器(PC)已跳到一个尚未加载到高速缓存中的地方,或者由于高速缓存已满而被刷新,并且该高速缓存行被选为驱逐(通常是最近最少用过的)。

手工生成足够的代码难以强制执行指令,而不是强制执行数据高速缓存未命中。

获取大量代码的一种方法是花费很少的精力编写一个生成代码的程序。

例如编写一个程序来生成一个巨大的switch语句的函数(在C中)[Warning,untested]:

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行,并且产生比适合cache的更多的函数,那么产生指令cache-missses的问题变得更容易控制。

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

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

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

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

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

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

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

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

你提到你有一个工作数据高速缓存测试,它使用一个“大整数数组a [100] … [以这种方式访问​​元素,使得两个元素之间的距离大于高速缓存行大小(在我的情况下是32字节)。“我很好奇你是如何确定你的测试算法的工作原理,以及你如何确定有多少数据缓存未命中是你的算法的结果,而不是其他刺激导致的错失。 实际上,对于一个100 * sizeof(int)的测试数组,当前大多数通用平台上的测试数据区域只有400个字节长(如果您在64位平台上,则可能是800字节,如果是200字节我们正在使用一个16位的平台)。 对于绝大多数高速缓存体系结构来说,整个测试数组将会适应高速缓存多次,这意味着对数组的随机访问会将整个数组放入高速缓存中(400 / cache_line_size)* 2访问的某处,并且每个之后的访问将是一个缓存命中,无论您如何命令您的访问,除非某些硬件或操作系统滴答计时器中断弹出并刷新您的部分或全部缓存数据。

关于指令缓存:还有人建议使用大的switch() – case语句或函数调用不同位置的函数,如果不仔细(我的意思是仔细地)设计代码的大小,相应的案例分支或不同位置的功能的位置和大小。 其原因是整个内存中的字节以完全可预测的模式“折叠”(技术上说,“相互别名”)在缓存中。 如果你仔细控制switch()的每个分支中的指令数量 – case语句,你可能能够在你的测试中得到某个地方,但是如果你只是在每个分支中放入大量的指令,你不知道如何他们将折叠到缓存中,switch() – case语句的哪些情况是相互混淆的,以便使用它们将相互驱逐出缓存。

我猜你对汇编代码不太熟悉,但是你必须相信我,这个项目正在尖叫。 相信我,我不是那种不使用汇编代码的地方,我强烈希望在OO C ++中编程,尽可能使用STL和多态ADT层次结构。 但在你的情况下,真的没有其他的万无一失的方法,程序集将给你绝对的代码块大小的控制,你真的需要为了能够有效地生成指定的缓存命中率。 您不必成为汇编专家,而且您甚至可能甚至不需要学习实现C语言序言和结尾所需的说明和结构(Google为“C可调用汇编函数”)。 你为汇编函数编写一些外部“C”函数原型,然后离开你。 如果你真的关心学习一些装配,那么你在装配功能中加入的测试逻辑越多,你在测试中施加的“海森堡效应”就越少,因为你可以仔细控制测试控制指令的位置它们对指令缓存的影响)。 但是对于大部分的测试代码,你可以使用一堆“nop”指令(指令缓存并不关心它包含的指令),并且可能只是把处理器的“return”指令放在每个指令的底部代码块。

现在让我们说你的指令缓存是32K(根据今天的标准来说相当小,但在许多嵌入式系统中可能仍然很常见)。 如果你的缓存是4路关联的,你可以创建8个独立的内容相同的8K汇编函数(你希望注意到的是64K的代码,是缓存的两倍大小),其中大部分只是一堆nop指令。 你让它们在内存中一个一个地下降(通常只需在源文件中定义每一个)。 然后,您可以使用仔细计算的序列从测试控制函数调用它们来生成您所需的任何缓存命中率(由于函数每个都是全长8K,因此具有相当的粒度)。 如果你一个一个调用1,2,3,4个函数,就知道你已经用这些测试函数代码填充了整个缓存。 在这一点再次调用这些函数不会导致指令缓存未命中(除了由测试控制函数自己的指令驱逐的行之外),而是调用其他任何一个(第5,6,7或8行;让我们选择第五个)将驱逐其中一个(尽管哪个被驱逐取决于你的缓存的替换策略)。 在这一点上,唯一一个你可以打电话,并知道你不会驱逐另一个是你刚才所说的(第五个),唯一你可以打电话,知道你会驱逐另一个是你还没有称为(第六,七,八)。 为了方便起见,只需维护一个静态数组,其大小与您所拥有的测试函数数量相同。 要触发驱逐,请在数组末尾调用函数,并将其指针移至数组的顶部,将其他数据向下移动。 为了不触发驱逐,请调用最近调用的驱动(在数组顶部的那个;确保在这种情况下不要移动其他驱动!)。 如果您需要更精细的粒度,可以对此做一些变化(也许会创建16个独立的4K组合函数)。 当然,所有这些都取决于测试控制逻辑的大小与缓存的每个关联“路径”的大小相比是微不足道的; 对于更积极的控制,你可以把测试控制逻辑放在测试函数本身中,但是为了完美控制,你必须完全没有内部分支地设计控制逻辑(在每个汇编函数结束时只分支),但是我认为我会在这里停止,因为这可能是过于复杂的事情。

即使没有经过测试,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函数原型中)。

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

如果在不可预测的条件下(例如输入或随机生成的数据),则在if情况下以及在其他情况下都大于高速缓存行的情况下,存在大量指令。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 [email protected] 举报,一经查实,本站将立刻删除。

相关推荐