为什么在L1缓存中将MFENCE与存储指令块预取一起使用?

Ana*_*ani 6 performance x86 intel prefetch memory-barriers

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

typedef struct _object{
  int value;
  char pad[60];
} object;
Run Code Online (Sandbox Code Playgroud)

在主要我正在初始化对象数组:

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();
Run Code Online (Sandbox Code Playgroud)

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

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

拥有mfence在这里没有任何意义,但是我在捆绑其他东西,无意间发现,如果我有存储操作,而没有mfence,我将收到50万次RFO请求(以papi L2_RQSTS.ALL_RFO事件衡量),这意味着另外50万L1命中,在需求之前预取。但是,包含mfence会导致一百万个RFO请求,从而产生RFO_HIT,这意味着仅在L2中预取了缓存行,不再在L1缓存中预取了。

除了英特尔文档以某种方式另有说明的事实之外:“可以在执行MFENCE指令之前,之中或之后将数据推测性地带入缓存”。我检查了加载操作。如果没有mfence,我最多可获得2000 L1命中率,而如果具有mfence,则我最多可获得100万L1命中率(以papi MEM_LOAD_RETIRED.L1_HIT事件衡量)。高速缓存行在L1中预取以用于加载指令。

因此,不应该包含mfence块预取。存储和加载操作几乎都需要花费相同的时间-不需5-6毫秒,而需20毫秒。我经历了有关mfence的其他问题,但未提及预取对它的预期行为,我没有看到足够好的理由或解释,为什么它仅使用存储操作会阻止L1缓存中的预取。还是我可能缺少某些功能描述?

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

Bee*_*ope 4

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

wrmsr -a 0x1a4 "$((2#1110))"
Run Code Online (Sandbox Code Playgroud)

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

首先,需要注意的是,L2_RQSTS.RFO_*事件计数不包括源自 L2 流媒体的 RFO 事件您可以在此处查看详细信息,但基本上每个 0x24 RFO 事件的 umask 是:

name      umask
RFO_MISS   0x22
RFO_HIT    0x42
ALL_RFO    0xE2
Run Code Online (Sandbox Code Playgroud)

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

似乎发生的情况是,当 L2 流送器处于活动状态时,您可能期望分配给这些事件之一的许多事件反而被 L2 预取器事件“吃掉”。可能发生的情况是,L2 预取器在请求流之前运行,并且当需求 RFO 从 L1 传入时,它发现 L2 预取器已经在处理请求。这只会再次增加事件的版本(事实上,umask |= 0x10当包括该位时,我得到了 2,000,000 总引用),这意味着RFO_MISSRFO_HITRFO_ALL错过它。

这有点类似于“fb_hit”场景,其中 L1 加载既没有错过也没有准确命中,而是命中了正在进行的加载 - 但这里的复杂之处在于加载是由 L2 预取器启动的。

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

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

您可以使用以下一些有用的perf命令来查看包含“L2 Streamer origin”位的差异。这是没有 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/
Run Code Online (Sandbox Code Playgroud)

其中包括:

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/
Run Code Online (Sandbox Code Playgroud)

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

#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();
    }
}
Run Code Online (Sandbox Code Playgroud)