为什么在 L1 缓存中使用 MFENCE 和存储指令块预取?

Why does using MFENCE with store instruction block prefetching in L1 cache?

我有一个 64 字节大小的对象:

typedef struct _object{
  int value;
  char pad[60];
} object;

在 main 中,我正在初始化对象数组:

volatile object * array;
int arr_size = 1000000;
array = (object *) malloc(arr_size * sizeof(object));

for(int i=0; i < arr_size; i++){
    array[i].value = 1;
    _mm_clflush(&array[i]);
}
_mm_mfence();

然后再次遍历每个元素。这是我正在计算事件的循环:

int tmp;
for(int i=0; i < arr_size-105; i++){
    array[i].value = 2;
    //tmp = array[i].value;
     _mm_mfence();
 }

有 mfence 在这里没有任何意义,但我在绑定其他东西时意外发现,如果我有 store 操作,没有 mfence 我会收到 50 万个 RFO 请求(测量通过 papi L2_RQSTS.ALL_RFO 事件),这意味着另外 50 万是 L1 命中,在需求之前预取。然而 包括 mfence 导致 100 万个 RFO 请求,给出 RFO_HITs,这意味着缓存行仅在 L2 中预取,不再在 L1 缓存中。

除了英特尔文档以某种方式指出的事实之外:"data can be brought into the caches speculatively just before, during, or after the execution of an MFENCE instruction." 我检查了 加载操作。 没有 mfence 我得到了 2000 L1 命中,而有 mfence,我有高达 100 万的 L1 命中率(用 papi MEM_LOAD_RETIRED.L1_HIT 事件测量)。缓存行在 L1 中预取以用于加载指令。

所以不应该包含 mfence 阻塞预取。存储和加载操作几乎花费相同的时间——没有 mfence 5-6 毫秒,有 mfence 20 毫秒。我经历了关于 mfence 的其他问题,但它没有提到预取的预期行为是什么,我没有看到足够好的理由或解释为什么它会阻止仅存储操作的 L1 缓存中的预取。或者我可能缺少 mfence 描述的内容?

我正在 Skylake 微架构上进行测试,但是与 Broadwell 核对并得到了相同的结果。

关于存储操作的情况,我在 Haswell 处理器上有四种不同配置的相同循环:运行:

  • MFENCE + E:存储后有MFENCE指令。启用所有硬件预取器。
  • E : 没有MFENCE。启用所有硬件预取器。
  • MFENCE + D:存储后有MFENCE指令。所有硬件预取器都被禁用。
  • D:没有MFENCE。所有硬件预取器都被禁用。

结果如下所示,已按存储数量标准化(每个存储到不同的缓存行)。它们在多个 运行 中非常确定。

                                 | MFENCE + E |      E     | MFENCE + D |      D     |
    L2_RQSTS.ALL_RFO             |    0.90    |    0.62    |    1.00    |    1.00    |
    L2_RQSTS.RFO_HIT             |    0.80    |    0.12    |    0.00    |    0.00    |
    L2_RQSTS.RFO_MISS            |    0.10    |    0.50    |    1.00    |    1.00    |
    OFFCORE_REQUESTS.DEMAND_RFO  |    0.20    |    0.88    |    1.00    |    1.00    |
    PF_L3_RFO                    |    0.00    |    0.00    |    0.00    |    0.00    |
    PF_RFO                       |    0.80    |    0.16    |    0.00    |    0.00    |
    DMND_RFO                     |    0.19    |    0.84    |    1.00    |    1.00    |

前四个事件是核心事件,后三个事件是off-core响应事件:

  • L2_RQSTS.ALL_RFO:发生在每个对 L2 的 RFO 请求中。这包括来自已退休或其他商店的 RFO 请求,以及来自 PREFETCHW 的 RFO 请求。对于启用硬件预取器的情况,事件计数低于预期,这是一个标准化事件。人们可以想到两个可能的原因:(1) 不知何故,一些 RFO 命中了 L1,以及 (2) 事件被低估了。我们将通过检查其他事件的计数并回顾我们对 L1D 预取器的了解来尝试找出它是哪个。
  • L2_RQSTS.RFO_HITL2_RQSTS.RFO_MISS:分别发生在 L2 中命中或未命中的 RFO。在所有配置中,这些事件的计数总和正好等于 L2_RQSTS.ALL_RFO.
  • OFFCORE_REQUESTS.DEMAND_RFO:这个事件的文档表明它应该与L2_RQSTS.RFO_MISS相同。但是,请注意 OFFCORE_REQUESTS.DEMAND_RFOL2_RQSTS.RFO_HIT 的总和实际上等于 1。因此,L2_RQSTS.RFO_MISS 有可能少计(因此 L2_RQSTS.ALL_RFO 也是如此)。事实上,这是最可能的解释,因为 Intel 优化手册(和其他 Intel 文档)说只有 L2 streamer prefetcher 可以跟踪存储。英特尔性能计数器手册在L2_RQSTS.ALL_RFO的描述中提到了"L1D RFO prefetches"。这些预取可能指的是尚未退休的商店的 RFO(请参阅 答案的最后一部分)。
  • PF_L3_RFO:触发L2 streamer prefetcher的RFO且目标缓存结构仅为L3时发生。此事件的所有计数均为零。
  • PF_RFO:触发L2 streamer prefetcher的RFO,目标缓存结构是L2,可能是L3(如果L3包含,则该行也会被填充到L3)。此事件的计数接近 L2_RQSTS.RFO_HIT。在 MFENCE + E 的情况下,似乎 100% 的 RFO 已按时完成(在需求 RFO 到达 L2 之前)。在 E 案例中,25% 的预取未按时完成或预取了错误的行。与E情况相比,MFENCE + E情况下L2中RFO命中数更大的原因是MFENCE指令延迟了后面的RFO,从而使L2的大部分超级队列条目可用于L2流光预取器。所以 MFENCE 确实使 L2 流媒体预取器的性能更好。没有它,L2会有很多in-flight需求RFO,留下少量的超级队列条目进行预取。
  • DMND_RFO:和OFFCORE_REQUESTS.DEMAND_RFO一样,不过好像少算了一点

I checked with load operations. without mfence I get up to 2000 L1 hit, whereas with mfence, I have up to 1 million L1 hit (measured with papi MEM_LOAD_RETIRED.L1_HIT event). The cache lines are prefetched in L1 for load instruction.

关于加载操作的情况,根据我的经验,MFENCE(或任何其他防护指令)对硬件预取器的行为没有影响。这里 MEM_LOAD_RETIRED.L1_HIT 事件的真实计数实际上非常小(< 2000)。大多数被统计的事件来自 MFENCE 本身,而不是负载。 MFENCE(和SFENCE)需要一直向内存控制器发送fence request,以确保所有待处理的存储都已到达全局观察点。围栏请求不计为 RFO 事件,但可能计为多个事件,包括 L1_HIT。有关此和类似观察的更多信息,请参阅我的博客 post:An Introduction to the Cache Hit and Miss Performance Monitoring Events.

不是 L1 预取导致您看到计数器值:即使您禁用 L1 预取器,效果仍然存在。事实上,如果禁用除 L2 流媒体之外的所有预取器,效果仍然存在:

wrmsr -a 0x1a4 "$((2#1110))"

如果您 禁用 L2 流媒体,但是,计数如您所料:您看到大约 1,000,000 L2.RFO_MISSL2.RFO_ALL 甚至没有 mfence.

首先,重要的是要注意 L2_RQSTS.RFO_* 事件计数 不计算源自 L2 流光 的 RFO 事件。您可以看到详细信息 here,但基本上每个 0x24 RFO 事件的 umask 是:

name      umask
RFO_MISS   0x22
RFO_HIT    0x42
ALL_RFO    0xE2

请注意,none 的 umask 值具有 0x10 位,表示应跟踪源自 L2 流媒体的事件。

似乎发生的情况是,当 L2 流送器处于活动状态时,您可能希望分配给其中一个事件的许多事件反而 "eaten" 由 L2 预取器事件代替。可能发生的情况是 L2 预取器在请求流之前 运行,并且当请求 RFO 从 L1 传入时,它发现 L2 预取器已经在进行中的请求。这只会再次增加事件的 umask |= 0x10 版本(实际上,当包含该位时我得到了 2,000,000 个总引用),这意味着 RFO_MISSRFO_HITRFO_ALL 将错过它.

这有点类似于 "fb_hit" 场景,其中 L1 加载既没有丢失也没有准确命中,而是命中了 in-progress 加载 - 但这里的复杂情况是加载是由 L2 预取器启动的。

mfence 只是减慢了一切,以至于 L2 预取器 几乎总是 有时间将线路一直带到 L2,给出 RFO_HIT 计数。

我认为这里根本不涉及 L1 预取器(事实表明,如果你关闭它们,它的工作原理是一样的):据我所知,L1 预取器不与存储交互,只负载。

这里有一些有用的 perf 命令,您可以使用它们来查看包含 "L2 streamer origin" 位的区别。这是 w/o L2 流光事件:

perf stat --delay=1000 -e cpu/event=0x24,umask=0xef,name=l2_rqsts_references/,cpu/event=0x24,umask=0xe2,name=l2_rqsts_all_rfo/,cpu/event=0x24,umask=0xc2,name=l2_rqsts_rfo_hit/,cpu/event=0x24,umask=0x22,name=l2_rqsts_rfo_miss/

并包括在内:

perf stat --delay=1000 -e cpu/event=0x24,umask=0xff,name=l2_rqsts_references/,cpu/event=0x24,umask=0xf2,name=l2_rqsts_all_rfo/,cpu/event=0x24,umask=0xd2,name=l2_rqsts_rfo_hit/,cpu/event=0x24,umask=0x32,name=l2_rqsts_rfo_miss/

我 运行 这些针对此代码(sleep(1) 与传递给 perf 的 --delay=1000 命令对齐以排除初始化代码):

#include <time.h>
#include <immintrin.h>
#include <stdio.h>
#include <unistd.h>

typedef struct _object{
  int value;
  char pad[60];
} object;

int main() {
    volatile object * array;
    int arr_size = 1000000;
    array = (object *) malloc(arr_size * sizeof(object));

    for(int i=0; i < arr_size; i++){
        array[i].value = 1;
        _mm_clflush((const void*)&array[i]);
    }
    _mm_mfence();

    sleep(1);
    // printf("Starting main loop after %zu ms\n", (size_t)clock() * 1000u / CLOCKS_PER_SEC);

    int tmp;
    for(int i=0; i < arr_size-105; i++){
        array[i].value = 2;
        //tmp = array[i].value;
        // _mm_mfence();
    }
}