当前位置: 首页 > 知识库问答 >
问题:

使用 rdmsr/rdpmc 提高分支预测准确性

微生阳平
2023-03-14

我试图了解分支预测单元如何在 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 调用,这是一个系统调用,我用系统调用污染了全局历史记录,因此不是一个很好的测量。

我已经阅读了rdpmcrdmsr指令,我认为由于它们只是指令,所以我不会污染全局历史,并且我可以一次测量单个分支指令。

但是,我不知道怎么做。我有AMD 3600 CPU。这些是我在网上找到的链接,但我不知道怎么做。除此之外,我是不是错过了什么?

英特尔rdpmc

AMD 性能手册

共有2个答案

洪知
2023-03-14

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-

  • 宋望
    2023-03-14

    您已经假设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,未采取