我正在生成一个合成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")
代码所做的只是生成一个 C程序,它包含许多随机命名的虚函数,这些函数又在main()
中以随机顺序调用。我正在使用-O0
在CentOS 7下使用gcc 4.8.5编译生成的代码。该代码在配备2x Intel Xeon E5-2630v3(Haswell架构)的双插槽机器上运行。
我感兴趣的是在分析从C代码编译的二进制文件时(而不是Python脚本,仅用于自动生成代码),了解perf报告的与指令相关的计数器。特别是,我正在使用perf stat
观察以下计数器:
我首先在BIOS中禁用了所有硬件预取程序的代码,即
结果如下(进程被固定到第二个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
考虑到上面的数字,我无法解释L2中如此大量的指令获取失误。我已经禁用了所有预取程序,并且 L2_RQSTS.ALL_PF 确认了这一点。但为什么我看到L2中的指令获取次数比L1i多两倍?在我的(简单)心理处理器模型中,如果在L2中查找指令,则必须先在L1i中查找。显然我错了,我错过了什么?
然后,我尝试在启用所有硬件预取程序的情况下运行相同的代码,即
结果如下:
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
现在, 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 计数如此之高的原因是什么?
答案 0 :(得分: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字节的缓存行(here是仅具有两个功能的版本),因此代码大小比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未命中次数的两倍之内。这与您的电话号码一致。
在我的两个函数version中,我计算每个调用的函数有24条指令,因此我希望退休的指令总数约为240亿,但您有250亿。我不知道如何计数,或者由于某种原因每个功能有25条指令。