【问题标题】:Does _mm_clflush really flush the cache?_mm_clflush 真的刷新缓存吗?
【发布时间】:2019-03-02 17:07:06
【问题描述】:

我试图通过编写和运行测试程序来了解硬件缓存的工作原理:

#include <stdio.h>
#include <stdint.h>
#include <x86intrin.h>

#define LINE_SIZE   64

#define L1_WAYS     8
#define L1_SETS     64
#define L1_LINES    512

// 32K memory for filling in L1 cache
uint8_t data[L1_LINES*LINE_SIZE];

int main()
{
    volatile uint8_t *addr;
    register uint64_t i;
    int junk = 0;
    register uint64_t t1, t2;

    printf("data: %p\n", data);

    //_mm_clflush(data);
    printf("accessing 16 bytes in a cache line:\n");
    for (i = 0; i < 16; i++) {
        t1 = __rdtscp(&junk);
        addr = &data[i];
        junk = *addr;
        t2 = __rdtscp(&junk) - t1;
        printf("i = %2d, cycles: %ld\n", i, t2);
    }
}

我在不带_mm_clflush 的情况下运行代码,而结果只显示_mm_clflush 第一次内存访问更快。

_mm_clflush:

$ ./l1
data: 0x700c00
accessing 16 bytes in a cache line:
i =  0, cycles: 280
i =  1, cycles: 84
i =  2, cycles: 91
i =  3, cycles: 77
i =  4, cycles: 91

没有_mm_clflush:

$ ./l1
data: 0x700c00
accessing 16 bytes in a cache line:
i =  0, cycles: 3899
i =  1, cycles: 91
i =  2, cycles: 105
i =  3, cycles: 77
i =  4, cycles: 84

刷新缓存行是没有意义的,但实际上变得更快?谁能解释为什么会这样?谢谢

----------------进一步实验-------------------

假设 3899 个周期是由 TLB 未命中引起的。为了证明我对缓存命中/未命中的了解,我稍微修改了这段代码来比较L1 cache hitL1 cache miss 的内存访问时间。

这一次,代码跳过缓存行大小(64 字节)并访问下一个内存地址。

*data = 1;
_mm_clflush(data);
printf("accessing 16 bytes in a cache line:\n");
for (i = 0; i < 16; i++) {
    t1 = __rdtscp(&junk);
    addr = &data[i];
    junk = *addr;
    t2 = __rdtscp(&junk) - t1;
    printf("i = %2d, cycles: %ld\n", i, t2);
}

// Invalidate and flush the cache line that contains p from all levels of the cache hierarchy.
_mm_clflush(data);
printf("accessing 16 bytes in different cache lines:\n");
for (i = 0; i < 16; i++) {
    t1 = __rdtscp(&junk);
    addr = &data[i*LINE_SIZE];
    junk = *addr;
    t2 = __rdtscp(&junk) - t1;
    printf("i = %2d, cycles: %ld\n", i, t2);
}

由于我的电脑有一个8路集的关联L1数据缓存,有64组,总共32KB。如果我每 64 个字节访问一次内存,它应该会导致所有缓存未命中。但似乎有很多缓存行已经缓存了:

$ ./l1
data: 0x700c00
accessing 16 bytes in a cache line:
i =  0, cycles: 273
i =  1, cycles: 70
i =  2, cycles: 70
i =  3, cycles: 70
i =  4, cycles: 70
i =  5, cycles: 70
i =  6, cycles: 70
i =  7, cycles: 70
i =  8, cycles: 70
i =  9, cycles: 70
i = 10, cycles: 77
i = 11, cycles: 70
i = 12, cycles: 70
i = 13, cycles: 70
i = 14, cycles: 70
i = 15, cycles: 140
accessing 16 bytes in different cache lines:
i =  0, cycles: 301
i =  1, cycles: 133
i =  2, cycles: 70
i =  3, cycles: 70
i =  4, cycles: 147
i =  5, cycles: 56
i =  6, cycles: 70
i =  7, cycles: 63
i =  8, cycles: 70
i =  9, cycles: 63
i = 10, cycles: 70
i = 11, cycles: 112
i = 12, cycles: 147
i = 13, cycles: 119
i = 14, cycles: 56
i = 15, cycles: 105

这是由预取引起的吗?还是我的理解有问题?谢谢

【问题讨论】:

  • 您无法使用 rdtsc(甚至是 rdtscp 风格)测量几个周期粒度,您可能对性能的影响比负载本身更大。您需要分多行摊销。
  • 是的,clflush 确实会刷新缓存行,如果它存在于任何缓存中。有关测量缓存命中与 L3 未命中延迟的工作程序,请参阅 clflush to invalidate cache line via C function
  • @Leeor 你的意思是因为 rdtscp 函数调用使用的周期,这个测量不准确吗?实际上,我正在研究缓存侧通道。我想这可能是区分 L1 命中、L2 命中和缓存未命中的显着结果。
  • 谢谢,@PeterCordes。我更新了我的问题以查看缓存行命中和缓存行未命中之间的区别。只是有点难以解释结果。你能回答吗?
  • 对于缓存读取侧通道,请参阅:How can I create a spectre gadget in practice?。如果我稍后解决它,我会更详细地阅读你的整个问题。我现在没时间。

标签: linux x86-64 cpu-architecture cpu-cache cache-locality


【解决方案1】:

我修改了代码,在_mm_clflush(data) 之前添加了一个写操作,它显示 clflush 确实刷新了缓存行。修改后的代码:

#include <stdio.h>
#include <stdint.h>
#include <x86intrin.h>

#define LINE_SIZE   64
#define L1_LINES    512

// 32K memory for filling in L1 cache
uint8_t data[L1_LINES*LINE_SIZE];

int main()
{
    volatile uint8_t *addr;
    register uint64_t i;
    unsigned int junk = 0;
    register uint64_t t1, t2;

    data[0] = 1; //write before cflush
    //_mm_clflush(data);

    printf("accessing 16 bytes in a cache line:\n");
    for (i = 0; i < 16; i++) {
        t1 = __rdtscp(&junk);
        addr = &data[i];
        junk = *addr;
        t2 = __rdtscp(&junk) - t1;
        printf("i = %2d, cycles: %ld\n", i, t2);
    }
}

我在我的电脑(Intel(R) Core(TM) i5-8500 CPU)上运行了修改后的代码,结果如下。根据多次尝试,第一次访问之前刷新到内存的数据的延迟明显高于没有刷新的数据。

没有 clflush:

data: 0000000000407980
accessing 16 bytes in a cache line:
i =  0, cycles: 64
i =  1, cycles: 46
i =  2, cycles: 49
i =  3, cycles: 48
i =  4, cycles: 46

使用 clflush:

data: 0000000000407980
accessing 16 bytes in a cache line:
i =  0, cycles: 214
i =  1, cycles: 41
i =  2, cycles: 40
i =  3, cycles: 42
i =  4, cycles: 40

【讨论】:

  • 您可以使用alignas(64) volatile uint8_t data[64] 或其他东西;您不需要单独的指向易失性的指针。此外,缓存命中的 ~47 与 ~41 个周期(加上 rdtscp 开销)可能是由于没有将 CPU 预热到最大涡轮。无论初始访问是否丢失,缓存命中应该大致相同。而你 printf 在循环之前的存储之后,所以存储转发不会发生。
【解决方案2】:

我猜这可能是由于一开始的 TLB 未命中造成的? _mm_clflush 实际上将这个虚拟地址缓存到 TLB 中,我可能是对的吗?怎么证明?

【讨论】:

  • 是的,如果需要找到正确的物理地址,clflush 将导致页面遍历。这不是像预取那样的提示; CPU 不允许在 TLB 未命中时将其丢弃,就像一些用于 SW 预取的旧 CPU。
【解决方案3】:

如果没有clflush,第一次加载大约需要 3899 个周期,这大约是处理次要页面错误所需的时间。 rdtscp 序列化加载操作,从而确保所有后续加载到 L1 缓存中的同一行命中。现在,当您在循环之前添加clflush 时,会触发页面错误并在循环外进行处理。当页面错误处理程序返回并重新执行clflush 时,目标缓存行将被刷新。在 Intel 处理器上,rdtscp 确保在发出循环中的第一个负载之前刷新该行。因此,现金层次结构中的第一次加载未命中及其延迟将与内存访问差不多。就像前面的情况一样,后面的加载是由rdtscp 序列化的,所以它们都在 L1D 中命中。

尽管我们考虑了rdtscp 的开销,但测得的 L1D 命中延迟太高了。你用-O3编译了吗?

当静态分配高速缓存行时,我无法在 Linux 4.4.0-154 上使用 gcc 5.5.0 重现您的结果(即次要页面错误),但仅限于我使用 mmap 时。如果你告诉我你的编译器版本和内核版本,也许我可以进一步调查。

关于您的第二个问题,您测量负载延迟的方式无法区分 L1D 命中和 L2 命中,因为测量误差可能与延迟差异一样大。您可以使用MEM_LOAD_UOPS_RETIRED.L1_HITMEM_LOAD_UOPS_RETIRED.L2_HIT 性能计数器进行检查。顺序访问模式很容易被 L1 和 L2 硬件预取器检测到,因此如果您不关闭预取器,获得命中也就不足为奇了。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多