我试图了解分支预测单元是如何在CPU中工作的。
我使用了papi
和linux的perf-events
,但它们都没有给出准确的结果(对于我的情况)。
这是我的代码:
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
或类似的模式。
试用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位饱和计数器,还有一个大的全局分支预测器。当我在环外测量时,我会在测量中引入较少的噪声。我所说的较少噪音,是指papi
呼叫。
想想看:环外测量
全球历史是:papi_start, branch_outcome1, branch_outcome2, branch_outcome3, ..., papi_end, papi_start (2nd loop of main iteration), branch_outcome1, ...
因此,分支预测器不知何故在同一分支中找到了模式。
但是,如果我尝试测量单个分支指令,则全局历史记录为:papi_start, branchoutcome1, papiend, papistart, branchoutcome2, papiend...
因此,我正在介绍越来越多的全球历史分支。我假设全局历史不能容纳许多分支条目,因此,它在所需的if语句(分支)中找不到任何相关性/模式。
结果
我需要测量单个分支的预测结果。我知道,如果我不过多地介绍papi,CPU可以学习200模式。我看过papi调用,我看到了很多for循环,如果条件允许的话。
这就是为什么我需要更好的测量。我尝试过linuxperf-event
,但它进行ioctl
调用,这是一个系统调用,我用系统调用污染了全局历史,因此,这不是一个好的度量。
我已经阅读了rdpmc
和rdmsr
指令,我认为由于它们只是指令,我不会污染全局历史,并且我可以一次测量单个分支指令。
然而,我不知道该怎么做。我有AMD 3600 CPU。这些是我在网上找到的链接,但我不知道该怎么做。除此之外,我还错过了什么吗?
英特尔rdpmc
AMD性能手册
您已经假设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.utexas.edu/jdm4372/2018/07/23/comments-on-timing-short-code-sections-on-intel-processors/
perf_event_open()
文档描述了如何将rdpmc
正确用于通过该接口创建的事件。@JohndMcAlpin的回答中描述的方法也有效,但它是基于直接对事件控制寄存器进行编程。给定一组硬件事件,很难弄清楚如何在可用的硬件性能计数器上安排这些事件。perf_event
子系统为您处理这个问题,这是一个主要优势。
perf_event
子系统从Linux 3.4开始支持rdpmc
。
从<linux/perf_event.h>
开始,以下工作:
-
执行
perf_event_open()
以准备读取type = PERF_TYPE_HARDWARE
config = PERF_COUNT_HW_BRANCH_MISSES
的计数器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 rdpmcn" : "=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 ;
如果线程在"开始"one_answers"结束"读取之间没有中断,那么我认为我们可以假设
perf_mm
内容不会改变。但是,如果它被中断,那么内核可以更新perf_mm
内容,以考虑影响这个时间的任何更改。 -
注意:
RDPMC
指令的开销并不大,但我正在尝试剥离所有这些,看看如果perf_mm->lock
不变,我是否可以直接使用RDPMC
结果。