是什么导致Cortex-A72上带有-O0而不是-O3的简单紧密循环的周期如此高的变化?


9

我正在围绕获取一段代码的高度一致的运行时进行一些实验。我当前正在计时的代码是一个相当随意的CPU约束工作负载:

int cpu_workload_external_O3(){
    int x = 0;
    for(int ind = 0; ind < 12349560; ind++){
        x = ((x ^ 0x123) + x * 3) % 123456;
    }
    return x;
}

我已经编写了一个内核模块,该模块禁用中断,然后运行上述功能的10次尝试,通过获取前后时钟周期计数器的差值来计时每次尝试。其他注意事项:

  • 该机器是ARM​​ Cortex-A72,具有4个插槽,每个插槽具有4个内核(每个插槽都有自己的L1缓存)
  • 时钟频率缩放关闭
  • 不支持超线程
  • 机器几乎没有运行,除了一些简单的系统进程

换句话说,我相信大多数/所有系统可变性的原因都可以解决,尤其是当通过禁用了中断的内核模块运行时spin_lock_irqsave(),代码在运行时应该获得几乎相同的性能(可能对性能造成很小的影响)在第一次运行时,首先将某些指令拉入缓存,仅此而已)。

的确,当使用编译基准测试代码时-O3,我平均看到了大约135,845,192个周期中的最多200个周期,并且大多数测试花费的时间完全相同。但是,当使用编译时-O0,范围从262,710,916上升至158,386个周期。范围是指最长和最短运行时间之间的差。而且,对于-O0代码而言,哪个试验是最慢/最快的试验并没有太多的一致性-违反直觉,在某些情况下,最快的试验是最先的,而最慢的试验是在后的!

那么:是什么原因导致-O0代码的可变性如此高的上限?看一下程序集,似乎-O3代码将所有内容(?)存储在寄存器中,而-O0代码中有很多引用sp,因此似乎正在访问内存。但是即使那样,我也希望所有内容都可以放入L1缓存中,并且在确定的访问时间内就可以坐在那里。


被基准测试的代码在上面的代码段中。组件在下面。两者都编译时gcc 7.4.0没有标记,除了-O0-O3

-O0

0000000000000000 <cpu_workload_external_O0>:
   0:   d10043ff        sub     sp, sp, #0x10
   4:   b9000bff        str     wzr, [sp, #8]
   8:   b9000fff        str     wzr, [sp, #12]
   c:   14000018        b       6c <cpu_workload_external_O0+0x6c>
  10:   b9400be1        ldr     w1, [sp, #8]
  14:   52802460        mov     w0, #0x123                      // #291
  18:   4a000022        eor     w2, w1, w0
  1c:   b9400be1        ldr     w1, [sp, #8]
  20:   2a0103e0        mov     w0, w1
  24:   531f7800        lsl     w0, w0, #1
  28:   0b010000        add     w0, w0, w1
  2c:   0b000040        add     w0, w2, w0
  30:   528aea61        mov     w1, #0x5753                     // #22355
  34:   72a10fc1        movk    w1, #0x87e, lsl #16
  38:   9b217c01        smull   x1, w0, w1
  3c:   d360fc21        lsr     x1, x1, #32
  40:   130c7c22        asr     w2, w1, #12
  44:   131f7c01        asr     w1, w0, #31
  48:   4b010042        sub     w2, w2, w1
  4c:   529c4801        mov     w1, #0xe240                     // #57920
  50:   72a00021        movk    w1, #0x1, lsl #16
  54:   1b017c41        mul     w1, w2, w1
  58:   4b010000        sub     w0, w0, w1
  5c:   b9000be0        str     w0, [sp, #8]
  60:   b9400fe0        ldr     w0, [sp, #12]
  64:   11000400        add     w0, w0, #0x1
  68:   b9000fe0        str     w0, [sp, #12]
  6c:   b9400fe1        ldr     w1, [sp, #12]
  70:   528e0ee0        mov     w0, #0x7077                     // #28791
  74:   72a01780        movk    w0, #0xbc, lsl #16
  78:   6b00003f        cmp     w1, w0
  7c:   54fffcad        b.le    10 <cpu_workload_external_O0+0x10>
  80:   b9400be0        ldr     w0, [sp, #8]
  84:   910043ff        add     sp, sp, #0x10
  88:   d65f03c0        ret

-O3

0000000000000000 <cpu_workload_external_O3>:
   0:   528e0f02        mov     w2, #0x7078                     // #28792
   4:   5292baa4        mov     w4, #0x95d5                     // #38357
   8:   529c4803        mov     w3, #0xe240                     // #57920
   c:   72a01782        movk    w2, #0xbc, lsl #16
  10:   52800000        mov     w0, #0x0                        // #0
  14:   52802465        mov     w5, #0x123                      // #291
  18:   72a043e4        movk    w4, #0x21f, lsl #16
  1c:   72a00023        movk    w3, #0x1, lsl #16
  20:   4a050001        eor     w1, w0, w5
  24:   0b000400        add     w0, w0, w0, lsl #1
  28:   0b000021        add     w1, w1, w0
  2c:   71000442        subs    w2, w2, #0x1
  30:   53067c20        lsr     w0, w1, #6
  34:   9ba47c00        umull   x0, w0, w4
  38:   d364fc00        lsr     x0, x0, #36
  3c:   1b038400        msub    w0, w0, w3, w1
  40:   54ffff01        b.ne    20 <cpu_workload_external_O3+0x20>  // b.any
  44:   d65f03c0        ret

内核模块

运行试验的代码如下。它PMCCNTR_EL0在每次迭代之前/之后读取,将差异存储在数组中,并在所有试验的最后打印出最小/最大时间。函数cpu_workload_external_O0cpu_workload_external_O3在单独编译然后链接入的外部目标文件中。

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>

#include "cpu.h"

static DEFINE_SPINLOCK(lock);

void runBenchmark(int (*benchmarkFunc)(void)){
    // Enable perf counters.
    u32 pmcr;
    asm volatile("mrs %0, pmcr_el0" : "=r" (pmcr));
    asm volatile("msr pmcr_el0, %0" : : "r" (pmcr|(1)));

    // Run trials, storing the time of each in `clockDiffs`.
    u32 result = 0;
    #define numtrials 10
    u32 clockDiffs[numtrials] = {0};
    u32 clockStart, clockEnd;
    for(int trial = 0; trial < numtrials; trial++){
        asm volatile("isb; mrs %0, PMCCNTR_EL0" : "=r" (clockStart));
        result += benchmarkFunc();
        asm volatile("isb; mrs %0, PMCCNTR_EL0" : "=r" (clockEnd));

        // Reset PMCCNTR_EL0.
        asm volatile("mrs %0, pmcr_el0" : "=r" (pmcr));
        asm volatile("msr pmcr_el0, %0" : : "r" (pmcr|(((uint32_t)1) << 2)));

        clockDiffs[trial] = clockEnd - clockStart;
    }

    // Compute the min and max times across all trials.
    u32 minTime = clockDiffs[0];
    u32 maxTime = clockDiffs[0];
    for(int ind = 1; ind < numtrials; ind++){
        u32 time = clockDiffs[ind];
        if(time < minTime){
            minTime = time;
        } else if(time > maxTime){
            maxTime = time;
        }
    }

    // Print the result so the benchmark function doesn't get optimized out.
    printk("result: %d\n", result);

    printk("diff: max %d - min %d = %d cycles\n", maxTime, minTime, maxTime - minTime);
}

int init_module(void) {
    printk("enter\n");
    unsigned long flags;
    spin_lock_irqsave(&lock, flags);

    printk("-O0\n");
    runBenchmark(cpu_workload_external_O0);

    printk("-O3\n");
    runBenchmark(cpu_workload_external_O3);

    spin_unlock_irqrestore(&lock, flags);
    return 0;
}

void cleanup_module(void) {
    printk("exit\n");
}

硬件

$ lscpu
Architecture:        aarch64
Byte Order:          Little Endian
CPU(s):              16
On-line CPU(s) list: 0-15
Thread(s) per core:  1
Core(s) per socket:  4
Socket(s):           4
NUMA node(s):        1
Vendor ID:           ARM
Model:               3
Model name:          Cortex-A72
Stepping:            r0p3
BogoMIPS:            166.66
L1d cache:           32K
L1i cache:           48K
L2 cache:            2048K
NUMA node0 CPU(s):   0-15
Flags:               fp asimd evtstrm aes pmull sha1 sha2 crc32 cpuid
$ lscpu --extended
CPU NODE SOCKET CORE L1d:L1i:L2 ONLINE
0   0    0      0    0:0:0      yes
1   0    0      1    1:1:0      yes
2   0    0      2    2:2:0      yes
3   0    0      3    3:3:0      yes
4   0    1      4    4:4:1      yes
5   0    1      5    5:5:1      yes
6   0    1      6    6:6:1      yes
7   0    1      7    7:7:1      yes
8   0    2      8    8:8:2      yes
9   0    2      9    9:9:2      yes
10  0    2      10   10:10:2    yes
11  0    2      11   11:11:2    yes
12  0    3      12   12:12:3    yes
13  0    3      13   13:13:3    yes
14  0    3      14   14:14:3    yes
15  0    3      15   15:15:3    yes
$ numactl --hardware
available: 1 nodes (0)
node 0 cpus: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
node 0 size: 32159 MB
node 0 free: 30661 MB
node distances:
node   0
  0:  10

样品测量

以下是内核模块一次执行的一些输出:

[902574.112692] kernel-module: running on cpu 15                                                                                                                                      
[902576.403537] kernel-module: trial 00: 309983568 74097394 98796602 <-- max
[902576.403539] kernel-module: trial 01: 309983562 74097397 98796597                                                                                                                  
[902576.403540] kernel-module: trial 02: 309983562 74097397 98796597                                                                                                                  
[902576.403541] kernel-module: trial 03: 309983562 74097397 98796597
[902576.403543] kernel-module: trial 04: 309983562 74097397 98796597
[902576.403544] kernel-module: trial 05: 309983562 74097397 98796597                                                                                                                  
[902576.403545] kernel-module: trial 06: 309983562 74097397 98796597
[902576.403547] kernel-module: trial 07: 309983562 74097397 98796597
[902576.403548] kernel-module: trial 08: 309983562 74097397 98796597
[902576.403550] kernel-module: trial 09: 309983562 74097397 98796597                                                                                                                  
[902576.403551] kernel-module: trial 10: 309983562 74097397 98796597
[902576.403552] kernel-module: trial 11: 309983562 74097397 98796597
[902576.403554] kernel-module: trial 12: 309983562 74097397 98796597                                                                                                                  
[902576.403555] kernel-module: trial 13: 309849076 74097403 98796630 <-- min
[902576.403557] kernel-module: trial 14: 309983562 74097397 98796597                                                                                                                  
[902576.403558] kernel-module: min time: 309849076
[902576.403559] kernel-module: max time: 309983568                                                                                                                                    
[902576.403560] kernel-module: diff: 134492

对于每个试验,报告的值为:周期数(0x11),L1D访问次数(0x04),L1I访问次数(0x14)。我正在使用此ARM PMU参考的 11.8节。


2
还有其他线程在运行吗?它们的内存访问会导致总线带宽和缓存空间竞争,从而产生影响。
PRL

可能。我还没有隔离任何内核,即使那样,内核线程也可能会安排在套接字上的其他一个内核上。但是,如果我理解lscpu --extended正确,那么每个内核都有自己的L1数据和指令缓存,然后每个套接字都为其4个内核拥有一个共享的L2缓存,因此,只要一切都在L1缓存中完成,我希望代码会很漂亮完全“拥有”它的总线(因为它是唯一在其核心上运行直到完成的东西)。我对这个级别的硬件了解不多。
sevko

1
是的,它显然被报告为4个插槽,但这可能只是互连如何在16核SoC内布线的问题。但是您拥有物理机器,对吗?您有品牌和型号吗?如果盖子掉了,大概您也可以确认是否确实有4个单独的插座。我不明白为什么这会很重要,除了主板的供应商/型号。您的基准测试纯粹是单核,应该在缓存中保持最新状态,因此重要的是A72核心本身及其存储缓冲区+存储转发。
彼得·科德斯

1
我更改了内核模块以跟踪三个计数器,并添加了一些示例输出。有趣的是,大多数运行都是一致的,但是随机运行会更快。在这种情况下,看起来最快的L1访问实际上实际上要多得多,这可能意味着某个地方的分支预测更加积极。另外,很遗憾,我无法使用该机器。这是一个AWS a1.metal实例(它为您提供了物理硬件的完全所有权,因此表面上不会受到管理程序等的干扰)。
sevko

1
有趣的是,如果我使内核模块通过并在所有CPU上同时运行此代码on_each_cpu(),则每个人在100次试验中几乎都报告没有变化。
sevko

Answers:


4

在最近的Linux内核中,自动NUMA页迁移机制会定期删除TLB条目,以便它可以监视NUMA局部性。即使数据保留在L1DCache中,TLB重装也会减慢O0代码的速度。

页面迁移机制不应在内核页面上激活。

您检查是否启用了自动NUMA页面迁移

$ cat /proc/sys/kernel/numa_balancing

您可以使用禁用它

$ echo 0 > /proc/sys/kernel/numa_balancing

我最近一直在做一些相关的测试。我正在运行一个工作负载,该工作负载可以对L1缓存中的内存缓冲区进行大量随机访问。我背对背进行了一系列试验,运行时间高度一致(实际变化幅度小于0.001%),只是周期性地出现了一个小的向上峰值。在此峰值期间,基准运行时间仅延长了0.014%。这个很小,但是每个尖峰的幅度都完全相同,并且几乎每2秒出现一次尖峰。本机已numa_balancing禁用。也许您有一个主意?
sevko

弄清楚了。我整天盯着性能计数器,但发现根本原因是完全不相关的。我在安静的计算机上的tmux会话中运行这些测试。2秒的间隔恰好与我的tmux状态线的刷新间隔相符,这使得发出网络请求等。禁用它会使尖峰消失。不知道该如何脚本不同的核心集群上运行我的状态行是影响一个分离的核心集群上运行的过程中,仅接触L1数据..
sevko

2

您的差异约为6 * 10 ^ -4。虽然惊人地超过了1.3 * 10 ^ -6,但是一旦程序与缓存进行对话,它就会参与许多同步操作。同步总是意味着浪费时间。

有趣的是,您的-O0,-O3比较如何模仿一般规则,即L1高速缓存命中率约为寄存器引用的2倍。O3的平均运行时间为O0的51.70%。当您应用较低/较高方差时,我们得到(O3-200)/(O0 + 158386),我们看到改善了51.67%。

简而言之,是的,缓存永远不会是确定性的。并且您看到的低方差与通过较慢的设备进行同步所期望的一致。与更具确定性的仅寄存器的机器相比,这只是一个很大的差异。


从L1i缓存中获取指令。我猜您在说,这不会因为无法与相同或其他内核上的数据缓存保持一致而遭受不可预测的速度下降吗?但是无论如何,如果Bandwidth博士的答案是正确的,则差异并不是由于缓存本身,而是内核周期性地使dTLB无效。该解释完全解释了所有观察结果:由于在用户空间中包含任何负载/存储而增加的方差,以及在对内核模块中的循环进行计时时不会发生这种下降的事实。(Linux内核内存不可交换。)
Peter Cordes

当您访问热数据时,缓存通常是确定性的。它们可以是多端口的,以实现一致性流量,而不会干扰核心本身的负载/存储。您认为干扰是由于其他内核造成的,这似乎合理的,但我numa_balancing仅凭TLB无效就可以解释这一点。
彼得·科德斯

任何监听缓存都必须具有不间断的序列,其中任何请求都必须停止。1 vs 2周期操作中10 ^ -4的速度变慢意味着每10 ^ 5操作出现一个时钟时钟。整个问题实际上是一个禁忌,差异很小。
mevets
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.