为什么修改指令导致巨大的i-cache和i-TLB在x86上未命中?

use*_*476 5 performance x86 x86-64 performancecounter perf

以下代码片段仅使用一条RET指令创建一个函数(fun).循环重复调用该函数并在返回后覆盖RET指令的内容.

#include <sys/mman.h>
#include<stdlib.h>
#include<unistd.h>
#include <string.h>

typedef void (*foo)();
#define RET (0xC3)

int main(){
     // Allocate an executable page
    char * ins = (char *) mmap(0, 4096, PROT_EXEC|PROT_READ|PROT_WRITE, MAP_PRIVATE| MAP_ANONYMOUS, 0, 0);
    // Just write a RET instruction
    *ins = RET;
    // make fun point to the function with just RET instruction
    foo fun = (foo)(ins);
    // Repeat 0xfffffff times
    for(long i = 0; i < 0xfffffff; i++){
        fun();
        *ins = RET;
    }
    return 0;
}
Run Code Online (Sandbox Code Playgroud)

X86 Broadwell机器上的Linux perf具有以下icache和iTLB统计信息:

perf stat -e L1-icache-load-miss -e iTLB-load-miss ./a.out

"./a.out"的效果统计信息统计信息:

   805,516,067      L1-icache-load-misses                                       
         4,857      iTLB-load-misses                                            

  32.052301220 seconds time elapsed
Run Code Online (Sandbox Code Playgroud)

现在,查看相同的代码而不覆盖RET指令.

#include <sys/mman.h>
#include<stdlib.h>
#include<unistd.h>
#include <string.h>

typedef void (*foo)();
#define RET (0xC3)

int main(){
    // Allocate an executable page
    char * ins = (char *) mmap(0, 4096, PROT_EXEC|PROT_READ|PROT_WRITE, MAP_PRIVATE| MAP_ANONYMOUS, 0, 0);
    // Just write a RET instruction
    *ins = RET;
    // make fun point to the function with just RET instruction
    foo fun = (foo)(ins);
    // Repeat 0xfffffff times
    for(long i = 0; i < 0xfffffff; i++){
        fun();
        // Commented *ins = RET;
    }
    return 0;
}
Run Code Online (Sandbox Code Playgroud)

这是同一台机器上的性能统计数据.

perf stat -e L1-icache-load-miss -e iTLB-load-miss ./a.out

"./a.out"的效果统计信息统计信息:

        11,738      L1-icache-load-misses                                       
           425      iTLB-load-misses                                            

   0.773433500 seconds time elapsed
Run Code Online (Sandbox Code Playgroud)

请注意,覆盖该指令会导致L1-icache-load-miss从11,738增加到805,516,067 - 这是一个多方面的增长.另外请注意,iTLB负载未命中率从425增加到4,857 - 与L1-icache-load-miss相比相当增长但却更少.运行时间从0.773433500秒增加到32.052301220秒 - 增长41倍!

目前还不清楚,如果指令占用空间很小,CPU应该导致i-cache未命中.两个示例中唯一的区别是指令被修改.当然,L1 iCache和dCache是​​分开的,是不是有办法将代码安装到iCache中,这样可以避免缓存i-cache未命中?

此外,为什么iTLB未命中增长10倍?

Zul*_*lan 2

\n

假设一级 iCache 和 dCache 是分开的,是否有办法将代码安装到 iCache 中,以避免缓存 i-cache 未命中?

\n
\n\n

不。

\n\n

如果您想修改代码 - 唯一可以采用的路径如下:

\n\n
    \n
  1. 存储日期执行引擎
  2. \n
  3. 存储缓冲区和转发
  4. \n
  5. 一级数据缓存
  6. \n
  7. 统一二级缓存
  8. \n
  9. L1指令缓存
  10. \n
\n\n

请注意,您还错过了 \xce\xbcOP 缓存。

\n\n

1对此进行了说明,我认为该图足够准确。

\n\n

我怀疑 iTLB 未命中可能是由于定期 TLB 刷新造成的。如果没有修改,您不会受到 iTLB 未命中的影响,因为您的指令实际上来自 \xce\xbcOP 缓存。

\n\n

如果他们不这样做,我不太确定。我认为 L1 指令缓存是虚拟寻址的,因此如果命中则无需访问 TLB。

\n\n

1:不幸的是,该图像具有非常严格的版权,因此我避免突出显示路径/内联图像。

\n

  • L1i 与 L1d 一样是 VIPT,但 *uop* 缓存是虚拟寻址的。“invlpg”也必须刷新相关的 uop 缓存行,但我猜想只是定期 LRU 逐出 iTLB 条目就可以让 uop 缓存保持独立。(即 uop 缓存可以被视为 CPU 中转换缓存的一部分。)所以,是的,随着 uop 缓存命中,iTLB 访问将会减少。 (2认同)