2

我正在使用OProfile在树莓派 3B+ 上分析以下函数。(我在树莓派上使用 gcc 10.2 版(不进行交叉编译)和编译器的以下标志:-O1 -mfpu-neon -mneon-for-64bits. 最后包含生成的汇编代码。)

void do_stuff_u32(const uint32_t* a, const uint32_t* b, uint32_t* c, size_t array_size)
{
  for (int i = 0; i < array_size; i++)
  {

    uint32_t tmp1 = b[i];
    uint32_t tmp2 = a[i];
    c[i] = tmp1 * tmp2;
  }
}

我正在查看L1D_CACHE_REFILLPREFETCH_LINEFILL两个 cpu 事件。查看文档PREFETCH_LINEFILL计算由于预取而L1D_CACHE_REFILL导致的缓存行填充次数,并计算由于缓存未命中而导致的缓存行重新填充次数。对于上述循环,我得到了以下结果:

数组大小 array_size / L1D_CACHE_REFILL array_size / PREFETCH_LINEFILL
16777216 18.24 8.366

我想上面的循环是内存绑定的,这在某种程度上由值 8.366 确认:每个循环实例需要 3 x uint32_t,即 12B。8.366 个循环实例需要约 100B 的内存数据。但是预取器每 8.366 个循环实例只能将 1 个缓存行填充到 L1,根据 Cortex-A53 的手册,它有 64B。因此,其余的缓存访问将导致缓存未命中,即 18.24。如果将这两个数字结合起来,您将得到 ~5.7,这意味着每 5.7 个循环实例从预取或缓存未命中重新填充中填充 1 个缓存行。而 5.7 循环实例需要 5.7 x 3 x 4 = 68B,或多或少与缓存行大小一致。

然后我在循环中添加了更多东西,然后变成以下内容:

void do_more_stuff_u32(const uint32_t* a, const uint32_t* b, uint32_t* c, size_t array_size)
{
  for (int i = 0; i < array_size; i++)
  {

    uint32_t tmp1 = b[i];
    uint32_t tmp2 = a[i];
    tmp1 = tmp1 * 17;
    tmp1 = tmp1 + 59;
    tmp1 = tmp1 /2;
    tmp2 = tmp2 *27;
    tmp2 = tmp2 + 41;
    tmp2 = tmp2 /11;
    tmp2 = tmp2 + tmp2;
    c[i] = tmp1 * tmp2;
  }
}

而 cpu 事件的分析数据是我不明白的:

数组大小 array_size / L1D_CACHE_REFILL array_size / PREFETCH_LINEFILL
16777216 11.24 7.034

由于循环需要更长的时间来执行,预取器现在只需要 7.034 个循环实例来填充 1 个缓存行。但我不明白的是为什么缓存丢失也更频繁地发生,反映在数字 11.24 上,与之前的 18.24 相比?有人可以阐明如何将所有这些放在一起吗?


更新以包含生成的程序集

循环1:

    cbz x3, .L178
    lsl x6, x3, 2
    mov x3, 0
.L180:
    ldr w4, [x1, x3]
    ldr w5, [x0, x3]
    mul w4, w4, w5
    lsl w4, w4, 1
    str w4, [x2, x3]
    add x3, x3, 4
    cmp x3, x6
    bne .L180
.L178:

循环2:

    cbz x3, .L178
    lsl x6, x3, 2
    mov x5, 0
    mov w8, 27
    mov w7, 35747
    movk    w7, 0xba2e, lsl 16
.L180:
    ldr w3, [x1, x5]
    ldr w4, [x0, x5]
    add w3, w3, w3, lsl 4
    add w3, w3, 59
    mul w4, w4, w8
    add w4, w4, 41
    lsr w3, w3, 1
    umull   x4, w4, w7
    lsr x4, x4, 35
    mul w3, w3, w4
    lsl w3, w3, 1
    str w3, [x2, x5]
    add x5, x5, 4
    cmp x5, x6
    bne .L180
.L178:
4

1 回答 1

1

我将尝试根据与@artlessnoise 的更多测量和讨论来回答我自己的问题。

我进一步测量了上述 2 个循环的 READ_ALLOC_ENTER 事件,并获得了以下数据:

循环 1

数组大小 READ_ALLOC_ENTER
16777216 12494

循环 2

数组大小 READ_ALLOC_ENTER
16777216 1933年

因此,显然小循环(第一个)比大循环(第二个)进入读取分配模式更多,这可能是由于 CPU 能够更容易地检测到连续写入模式。在读取分配模式下,存储直接进入 L2(如果 L1 没有命中)。这就是为什么 L1D_CACHE_REFILL 对于第一个循环来说更少,因为它涉及的 L1 更少。对于第二个循环,由于它需要让 L1c[]比第一个循环更频繁地更新,因此由于缓存未命中而导致的重新填充可能会更多。此外,对于第二种情况,由于 L1 经常被更多的缓存行占用c[],它会影响 和 的缓存命中率a[]b[]从而更多的 L1D_CACHE_REFILL。

于 2021-01-22T16:52:18.620 回答