L2指令获取错过远高于L1指令获取未命中

Mar*_*rri 5 performance intel cpu-architecture cpu-cache perf

我正在生成一个合成C基准测试,旨在通过以下Python脚本导致大量的指令获取失败:

#!/usr/bin/env python
import tempfile
import random
import sys

if __name__ == '__main__':
    functions = list()

    for i in range(10000):
        func_name = "f_{}".format(next(tempfile._get_candidate_names()))
        sys.stdout.write("void {}() {{\n".format(func_name))
        sys.stdout.write("    double pi = 3.14, r = 50, h = 100, e = 2.7, res;\n")
        sys.stdout.write("    res = pi*r*r*h;\n")
        sys.stdout.write("    res = res/(e*e);\n")
        sys.stdout.write("}\n")
        functions.append(func_name)


    sys.stdout.write("int main() {\n")
    sys.stdout.write("unsigned int i;\n")
    sys.stdout.write("for(i =0 ; i < 100000 ;i ++ ){\n")
    for i in range(10000):
        r = random.randint(0, len(functions)-1)
        sys.stdout.write("{}();\n".format(functions[r]))


    sys.stdout.write("}\n")
    sys.stdout.write("}\n")
Run Code Online (Sandbox Code Playgroud)

代码所做的只是生成一个C程序,该程序由许多随机命名的虚函数组成,这些函数依次以随机顺序调用main().我正在用CentOS 7下的gcc 4.8.5编译生成的代码-O0.该代码在配备2x Intel Xeon E5-2630v3(Haswell架构)的双插槽机器上运行.

我感兴趣的是在分析从C代码编译二进制文件时(而不是Python脚本,仅用于自动生成代码),理解perf报告的与指令相关的计数器.特别是,我正在观察以下计数器perf stat:

  • 说明
  • L1-icache-load- miss(缺少L1的指令提取,Haswell上的r0280)
  • r2424,L2_RQSTS.CODE_RD_MISS(指令提取错过L2)
  • rf824,L2_RQSTS.ALL_PF(所有L2硬件预取器请求,包括代码和数据)

我首先在BIOS中禁用了所有硬件预取器的代码,即

  • MLC Streamer禁用
  • MLC Spatial Prefetcher已禁用
  • DCU数据预取器已禁用
  • DCU指令预取器已禁用

结果如下(进程被固定到第二个CPU的第一个核心和相应的NUMA域,但我想这没有太大的区别):

perf stat -e instructions,L1-icache-load-misses,r2424,rf824 numactl --physcpubind=8 --membind=1 /tmp/code   

 Performance counter stats for 'numactl --physcpubind=8 --membind=1 /tmp/code':    

    25,108,610,204      instructions                                               
     2,613,075,664      L1-icache-load-misses                                       
     5,065,167,059      r2424                                                       
                17      rf824                                                       

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

考虑到上面的数字,我无法解释L2中如此大量的指令获取失误.我已经禁用了所有预取程序,L2_RQSTS.ALL_PF确认了这一点.但为什么我看到L2中的指令获取次数比L1i多两倍?在我的(简单)心理处理器模型中,如果在L2中查找指令,则必须先在L1i中查找.显然我错了,我错过了什么?

然后,我尝试在启用所有硬件预取程序的情况下运行相同的代码,即

  • 启用MLC Streamer
  • 启用MLC空间预取程序
  • DCU数据预取器已启用
  • DCU指令预取器已启用

结果如下:

perf stat -e instructions,L1-icache-load-misses,r2424,rf824 numactl --physcpubind=8 --membind=1 /tmp/code

 Performance counter stats for 'numactl --physcpubind=8 --membind=1 /tmp/code':    

    25,109,877,626      instructions                                               
     2,599,883,072      L1-icache-load-misses                                       
     5,054,883,231      r2424                                                       
           908,494      rf824
Run Code Online (Sandbox Code Playgroud)

现在,L2_RQSTS.ALL_PF似乎表明发生了更多事情,虽然我预计预取器会更加激进,但我认为由于跳跃密集型工作负载和数据预取器,指令预取器严重受到考验与这种工作量没什么关系.但同样,L2_RQSTS.CODE_RD_MISS仍然太高,启用了预取器.

所以,总结一下,我的问题是:

禁用硬件预取程序后,L2_RQSTS.CODE_RD_MISS似乎远高于L1-icache-load-miss.即使启用了硬件预取程序,我仍然无法解释它.与L1-icache-load- miss相比,L2_RQSTS.CODE_RD_MISS计数如此之高的原因是什么?

Had*_*ais 1

指令预取器可以生成的请求不计为对 L1I 高速缓存的访问,但计为较高编号内存级别(例如 L2)的代码提取请求。这对于所有具有指令预取器的英特尔微架构来说通常都是如此。L2_RQSTS.CODE_RD_MISS计算来自 L1I 的请求和预取请求。需求请求由 IFU 中的复用单元生成,该复用单元从流水线中可能改变流的不同单元(例如分支预测单元)中选择目标获取线性地址。如果可能的话,预取请求由 L1I 指令预取器在 L1I 未命中时生成。

一般来说,预取请求的数量几乎与 L1I 未命中的数量成正比。对于从可缓存内存类型的内存区域获取指令,以下公式成立:

ICACHE.MISSES<= L2_RQSTS.CODE_RD_MISS+L2_RQSTS.CODE_RD_HIT

我不确定这个公式是否也适用于不可缓存的获取请求。我没有在那种情况下测试过。我知道这些请求被计为ICACHE.MISSES,但不确定其他事件。

在您的情况下,大多数指令读取都会在 L1I 和 L2 中丢失。您有 10,000 个函数,每个函数几乎完全跨越 2 个 64-btye 缓存线(是一个只有两个函数的版本),因此代码大小比 Haswell 上可用的 256 KiB L2 大得多。这些函数以非连续且不可预测的顺序调用,因此 L1I 和 L2 预取器不会有太大帮助。唯一值得注意的例外是回报,所有这些都将使用 RSB 机制正确预测。

这 10,000 个函数中的每一个都在循环中被调用 100,000 次。大多数获取请求都是针对这些函数占用的行。有用的指令获取请求总数约为每个函数 2 行 * 10,000 个函数 * 100,000 次迭代 = 2,000,000,000 行,其中大部分将在 L1I 和 L2 中丢失(但可能在第一次冷迭代后在 L3 中命中)。数百万个其他请求将针对循环体占用的行。您的测量结果显示,L1I 中丢失的指令获取量大约多了 30%。这是因为分支预测错误,导致对错误行的提取请求,这些错误行甚至可能不在 L1I 和/或 L2 中。每次 L1I 未命中都可能触发预取,因此 L2 指令读取在 L1I 未命中次数的两倍以内是正常的。这与你的数字是一致的。

在我的双函数版本中,我计算出每个调用函数有 24 条指令,因此我预计退役指令的总数约为 240 亿条,但实际结果为 250 亿条。要么我不知道如何计数,要么由于某种原因每个函数有 25 条指令。