【问题标题】:Using rdmsr/rdpmc for branch prediction accuracy使用 rdmsr/rdpmc 进行分支预测精度
【发布时间】:2020-06-01 12:52:18
【问题描述】:

我试图了解分支预测单元在 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 循环,if 条件。

这就是为什么我需要更好的测量。我试过 linux perf-event,但它会调用 ioctl,这是一个系统调用,我用系统调用污染了全局历史记录,因此不是一个好的衡量标准。

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

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

Intel rdpmc

AMD Performance manual

【问题讨论】:

  • 为什么不试试裸机软件呢?例如在 ARM 微控制器上。由于没有操作系统,行为会更容易预测和更容易调试?
  • 这里有一篇关于测量 ARM cortex 分支预测的好文章:community.arm.com/developer/ip-products/processors/b/…
  • 好吧,我想测量一下 AMD 处理器。我认为您的链接没有为我的问题提供有价值的答案。但我会研究一下,只是为了学习新东西。 @The_Average_Engineer
  • @The_Average_Engineer:x86 CPU 在实模式下启动,并且主板中始终有内置固件,可以加载 UEFI 应用程序或旧版 BIOS 引导扇区。它不像 ARM 板,您基本上是将固件写入闪存。我不认为裸机(甚至在 UEFI 下运行)是一个非常有用的建议。至少 UEFI 应用程序不必为了运行正常的 64 位代码而做一堆 osdev 废话(例如设置 GDT 和页表),并且可以使用 UEFI 函数将结果保存到文件中。但你不会有调试器或任何东西。

标签: c x86 performancecounter branch-prediction papi


【解决方案1】:

您假设 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/

【讨论】:

【解决方案2】:

perf_event_open()documentation 描述了如何正确使用 rdpmc 通过该接口创建的事件。 @JohnDMcCalpin 的答案中描述的方法也有效,但它基于直接对事件控制寄存器进行编程。给定一组硬件事件,弄清楚如何在可用的硬件性能计数器上安排这些事件可能很困难。 perf_event 子系统会为您处理这个问题,这是一个主要优势。

perf_event 子系统自 Linux 3.4 起支持rdpmc

&lt;linux/perf_event.h&gt; 开始,以下工作:

  1. perf_event_open()准备读取type = PERF_TYPE_HARDWAREconfig = 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) ;
    }
    
  2. 将 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。此缓冲区用于存储样本。请参阅文档的“溢出处理”部分。

  3. 要读取计数器,需要将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 的东西来解释影响这个时间的任何变化。

  4. 1234563 .

【讨论】:

  • 有一个 __rdpmc 内在函数,但在 gcc6.5 / 7.4 / 8.3 之前它显然是错误的; before that it wasn't properly volatile。如果你有更新的 GCC,你可以使用它;但我想内联汇编很好。您为 rdpmc 的输出省略了 C 变量。通常你想要"=a"(low_half_result) 或其他东西。省略(var_name) 部分是语法错误。
  • 谢谢。固定为"=a" (a), "=d" (d)
  • @Hadi:感谢您的编辑。是否有必要在读取循环中检查if (pc-&gt;cap_user_rdpmc &amp;&amp; idx)?我提到了time_offset 等,因为文档中的代码示例显示如何使用rdpmc 使用它,但出于这些目的没有必要这样做。您将 page_size 更改为“例如 4096”:您的意思是 4096 可以用于此目的 - 即使用 rdpmc 读取 PERF_TYPE_HARDWARE 计数器?您还指出了“文档”中的“溢出处理”:在这种情况下,这有什么关系?最后:如何判断我何时有“签名事件计数”?
  • @ChrisHall idx 如果事件当前未处于活动状态(例如,由于多路复用),则无效。如果您尝试从无效的idx 访问rdpmc,您将读取不同事件的计数器,否则将发生异常。如果您确定以后没有其他人会出于某种原因禁用用户模式rdpmc,那么在程序开始时只检查一次cap_user_rdpmc 可能就足够了。该缓冲区用于保存事件样本。当缓冲区下降时,内核调用您注册的函数来处理缓冲区。该文档讨论了如何使用缓冲区。
  • @ChrisHall 它们是每个线程的,但是单个线程可以安排比硬件计数器更多的硬件事件,这会触发多路复用。这就是某些事件可以启用但不活动的方式。当然,如果您可以保证用户模式rdpmc 在执行时已启用,您可以删除cap_user_rdpmc。否则代码会崩溃。
猜你喜欢
  • 2015-12-24
  • 1970-01-01
  • 2011-07-20
  • 2015-11-26
  • 2017-09-05
  • 2014-04-25
  • 2014-03-03
  • 2014-07-06
相关资源
最近更新 更多