我试图了解分支预测单元如何在 CPU 中工作。
我使用了papi
和linux的perf事件
,但这两个都没有给出准确的结果(就我的情况而言)。
这是我的代码:
void func(int* arr, int sequence_len){
for(int i = 0; i < sequence_len; i++){
// region starts
if(arr[i]){
do_sth();
}
// region ends
}
}
我的数组由0和1组成。它有一个大小为sequence_len
的模式。例如,如果我的大小是8,那么它有一个0 1 0 1 0 0 1
或类似的模式。
试验1:
我试图理解CPU是如何预测这些分支的。因此,我使用了papi并为预测失误的分支预测设置了性能计数器(我知道它也计算间接分支)。
int func(){
papi_read(r1);
for(){
//... same as above
}
papi_read(r2);
return r2-r1;
}
int main(){
init_papi();
for(int i = 0; i < 10; i++)
res[i] = func();
print(res[i]);
}
我看到的输出是(序列长度为200)
100 #iter1
40 #iter2
10 #iter3
3
0
0
#...
因此,首先,CPU盲目地预测序列,只有一半的时间成功。在接下来的迭代中,CPU可以预测得越来越好。经过一些迭代,CPU可以完美地猜到这一点。
试验2
我想看看,在哪个数组索引处会导致CPU预测失误。
int* func(){
int* results;
for(){
papi_read(r1);
if(arr[i])
do_sth();
papi_read(r2);
res[i] = r2-r1;
}
return res;
}
int main(){
init_papi();
for(int i = 0; i < 10; i++)
res[i] = func();
print(res[i]);
}
预期结果:
#1st iteration, 0 means no mispred, 1 means mispred
1 0 0 1 1 0 0 0 1 1 0... # total of 200 results
Mispred: 100/200
#2nd iteration
0 0 0 0 1 0 0 0 1 0 0... # total of 200 results
Mispred: 40/200 # it learned from previous iteration
#3rd iteration
0 0 0 0 0 0 0 0 1 0 0... # total of 200 results
Mispred: 10/200 # continues to learn
#...
收到的结果:
#1st iteration
1 0 0 1 1 0 0 0 1 1 0... # total of 200 results
Mispred: 100/200
#2nd iteration
1 0 0 0 1 1 0 1 0 0 0... # total of 200 results
Mispred: 100/200 # it DID NOT learn from previous iteration
#3rd iteration
0 1 0 1 0 1 0 1 1 0 0... # total of 200 results
Mispred: 100/200 # NO LEARNING
#...
我的观察
当我在for循环之外测量错误预测时,我可以看到CPU从它的错误预测中学习。然而,当我试图测量单分支指令错误预测时,CPU要么无法学习,要么我测量错了。
我的解释
我给出的序列长度是200。CPU有一个小的分支预测器,如Intels中的2-3位饱和计数器,以及一个大的全局分支预测器。当我在环路之外测量时,我会减少测量中的噪声。我说的噪音小,是指< code>papi呼叫。
想想这个:在回路测量之外
全局历史是:< code>papi_start,branch_outcome1,branch_outcome2,branch_outcome3,...,papi_end,papi_start(主迭代的第2个循环),branch_outcome1,...
因此,分支预测器以某种方式在同一分支中找到模式。
但是,如果我尝试测量单分支指令,那么全局历史是:papi_start,分支路1,分支路,分支路2,分支路…
因此,我正在向全球历史介绍越来越多的分支。我假设全局历史记录无法容纳许多分支条目,因此,它无法在所需的 if 语句(分支)中找到任何相关性/模式。
结果
我需要测量单个分支的预测结果。我知道CPU不用太多介绍papi也能学会200模式。我看过papi调用,也看过很多for循环,if条件。
这就是为什么我需要更好的测量。我尝试过 linux perf-event
,但它会进行 ioctl
调用,这是一个系统调用,我用系统调用污染了全局历史记录,因此不是一个很好的测量。
我已经阅读了rdpmc
和rdmsr
指令,我认为由于它们只是指令,所以我不会污染全局历史,并且我可以一次测量单个分支指令。
但是,我不知道怎么做。我有AMD 3600 CPU。这些是我在网上找到的链接,但我不知道怎么做。除此之外,我是不是错过了什么?
英特尔rdpmc
AMD 性能手册
perf_event_open()
文档描述了如何对通过该接口创建的事件正确使用rdpmc
。@JohnDMcCalpin的回答中描述的方法也有效,但它是基于直接编程事件控制寄存器。给定一组硬件事件,很难确定如何在可用的硬件性能计数器上安排这些事件。perf_event
子系统为您处理此问题,这是一个主要优势。
从Linux 3.4开始,< code>perf_event子系统支持< code>rdpmc。
以开头
>
执行
perf_event_open()
以准备读取type=perf_type_HARDWARE
的计数器
struct perf_event_attr attr ;
int fd ;
memset(&attr, 0, sizeof(attr)) ;
attr.type = PERF_TYPE_HARDWARE ;
attr.config = PERF_COUNT_HW_BRANCH_MISSES;
attr.size = sizeof(attr) ; // for completeness
attr.exclude_kernel = 1 ; // count user-land events
perf_fd = (int)sys_perf_event_open(&attr, 0, -1, -1, PERF_FLAG_FD_CLOEXEC) ;
// this pid, any cpu, no group_fd
哪里:
static long
sys_perf_event_open(struct perf_event_attr* attr,
pid_t pid, int cpu, int group_fd, ulong flags)
{
return syscall(__NR_perf_event_open, attr, pid, cpu, group_fd, flags) ;
}
将perf_fd与mmap页面关联:
struct perf_event_mmap_page* perf_mm ;
perf_mm = mmap(NULL, page_size, PROT_READ, MAP_SHARED, perf_fd, 0) ;
例如,page_size可以是 4096。此缓冲区用于存储样本。请参阅文档的“溢出处理”部分。
要读取计数器,需要将
perf_mm
中的一些信息与使用RDPMC
指令读取的信息结合起来,因此:
uint64_t offset, count ;
uint32_t lock, check, a, d, idx ;
lock = perf_mm->lock ;
do
{
check = lock ;
__asm__ volatile("":::"memory") ;
idx = perf_mm->index - 1 ;
// Check that you're allowed to execute rdpmc. You can do this check once.
// Check also that the event is currently active.
// Starting with Linux 3.12, use cap_user_rdpmc.
if (perf_mm->cap_user_rdpmc && idx) {
// cap_user_rdpmc cannot change at this point because no code
// that executes here that changes it. So it's safe.
__asm__ volatile("\t rdpmc\n" : "=a" (a), "=d" (d) : "c" (idx)) ;
}
// In case of signed event counts, you have to use also pmc_width.
// See the docs.
offset = perf_mm->offset ;
__asm__ volatile("":::"memory") ;
lock = perf_mm->lock ;
}
while (lock != check) ;
count = ((uint64_t)d << 32) + a ;
if (perf_mm->pmc_width != 64)
{
// need to sign extend the perf_mm->pmc_width bits of count.
} ;
count += offset ;
如果线程在“开始”和“结束”读取之间没有中断,那么我认为我们可以假设
perf_mm
东西不会改变。但是如果它被中断了,那么内核可以更新perf_mm
东西来考虑影响这个时间的任何变化。
注意:< code>RDPMC
指令的开销不是很大,但是我正在试验将所有这些剥离回来,看看是否可以直接使用< code>RDPMC结果,只要< code>perf_mm-
您已经假设PAPI和/或perf_events代码占用的内存相对较少。这是不正确的。如果您将性能计数器事件更改为类似“失效的指令”或“未暂停的CPU周期”,您将能够看到该操作在您的软件环境中包含了多少开销。细节将取决于您的操作系统版本,但我预计开销将在数百条指令/数千个周期内,因为读取perf _ events(PAPI使用)中的计数器需要内核穿越。代码路径肯定会包含它自己的分支。
如果您的内核支持“用户模式 RDPMC”(CR4.PCE=1),您可以使用一条指令读取性能计数器。https://github.com/jdmccalpin/low-overhead-timers 中提供了示例。
即使将测量代码限制为本机RDPMC指令(以及用于保存结果的周围代码),测量也会中断处理器流水线。RDPMC是一条微码指令。在Ryzen内核上,该指令执行20个微操作,每20个周期有一条指令的吞吐量。(参考:https://www.agner.org/optimize/instruction_tables.pdf)
任何细粒度的测量都是具有挑战性的,因为现代处理器的无序能力与用户代码的交互方式缺乏记录,也难以预测。有关此主题(也与AMD处理器相关)的更多说明,请访问http://sites . ut exas . edu/jdm 4372/2018/07/23/comments-on-timing-short-code-sections-on-Intel-processors/
如果语句更多地依赖于分支预测,而v表查找更多地依赖分支目标预测,那么
分支目标预测(BTP)与分支预测(BP)不同。我知道BTP会找到分支将跳转到的位置,而BP只是决定可能采取哪个分支。 BTP依赖BP吗,如果BTP不使用BP来预测哪个分支被采用,它怎么可能知道分支的目标呢? 我不明白为什么会有这么大的差异?一旦分支被预测为被占用,找到目标并不像读取指令中的地址一样简单吗?
编辑:我的困惑出现了,因为通过预测哪个分支,你肯定也在有效地进行目标预测?? 这个问题与我关于这个主题的第一个问题有内在联系: 分支预测与分支目标预测 无限循环 语句 或语句 语句的“then”子句结尾(跳过子句) 非虚函数调用 从函数返回 虚函数调用 函数指针调用 语句(如果编译为跳转表) 语句 语句(如果编译成一系列语句) 循环条件测试 和运算符 三元运算符 null 如果我有以下代码: (B
我的代码经常调用具有多个(不可预测的)分支的函数。当我分析时,我发现这是一个小瓶颈,大部分CPU时间用于条件JMP。 考虑以下两个函数,其中原始函数有多个显式分支。 这是一个新函数,我试图在其中删除导致瓶颈的分支。 然而,当我分析新代码时,性能只提高了大约20%,而且调用本身(对mem_funcs数组中的一个func)花费了很长时间。 第二个变量仅仅是一个更隐含的条件吗,因为CPU仍然无法预测将要
我正在读一本关于计算机体系结构的书,我在这一章讨论分支预测。有一个小练习,我很难把我的头缠绕在它周围。 考虑以下内部for循环 ------>内循环: b)1位分支预测缓冲器会改善性能吗(与a相比)?假设第一个预测是“未采取”,并且没有其他分支映射到该条目。 ----假设第一个预测是“不采取”,如果预测错误,则1位预测器反转该位。所以它将是NT/T/T。这是否使它具有与问题a)相同的性能?有1个未
我有一个与相关预测因子相关的练习,它指出以下几点: 答:贝兹·R1,D … D:贝兹·R1,F … F:不是R1的R1 预测工作如下 > 获取当前指令 如果是分支,则确定预测器的当前状态并预测分支: a.row 由分支地址确定(在本例中为 A 或 D) b. 列由当前全局移位寄存器确定 c.使用单元格中的值确定来自状态机的预测(当前状态保存在单元格中) 执行分支,并确定实际决策(已采取:1,未采取