我试图了解分支预测单元如何在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具有一个小型分支预测器,例如Intel中的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循环(如果有条件的话)。
这就是为什么我需要更好的测量。我已经尝试过Linux perf-event
,但是它会进行ioctl
调用,这是系统调用,我会使用系统调用来污染全局历史记录,因此不是一个很好的衡量标准。
我已经阅读了rdpmc
和rdmsr
指令,并假设由于它们只是指令,所以我不会污染全局历史记录,并且可以一次测量单个分支指令。
但是,我不知道该怎么做。我有AMD 3600 CPU。这些是我在网上找到的链接,但我不知道该怎么做。除此之外,我还缺少什么吗?
答案 0 :(得分:6)
您已经假定PAPI和/或perf_events代码具有相对较小的占用空间。这是不正确的。如果将性能计数器事件更改为“已退休的指令”或“未暂停的CPU周期”之类的内容,则可以查看此操作在软件环境中包含多少开销。详细信息将取决于您的OS版本,但是我希望开销在数百个指令/数千个周期中,因为读取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)
任何细粒度的测量都是具有挑战性的,因为现代处理器的乱序功能以记录不良且难以预期的方式与用户代码交互。 http://sites.utexas.edu/jdm4372/2018/07/23/comments-on-timing-short-code-sections-on-intel-processors/
中有关于此主题的更多注释(也与AMD处理器有关)。答案 1 :(得分:4)
perf_event_open()
documentation描述了如何通过该接口创建的事件正确使用rdpmc
。 @JohnDMcCalpin的答案中描述的方法也可以使用,但是它是基于直接对事件控制寄存器进行编程的。给定一组硬件事件,很难确定如何在可用的硬件性能计数器上安排这些事件。 perf_event
子系统可以为您解决此问题,这是一个主要优势。
从Linux 3.4开始,perf_event
子系统支持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 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
内容,以解决影响该时间的任何更改。
注意:RDPMC
指令周围的开销并不大,但是我正在尝试剥离所有这些内容,看看是否可以直接使用RDPMC
结果,前提是{{ 1}}不变。