代码之家  ›  专栏  ›  技术社区  ›  mahmood

使用时间戳计数器和时钟获取缓存未命中时间

  •  0
  • mahmood  · 技术社区  · 7 年前

    作为后续行动 topic ,为了计算内存丢失延迟,我使用 _mm_clflush , __rdtsc _mm_lfence (基于此代码) question/answer ).

    正如您在代码中看到的,我首先将数组加载到缓存中。然后刷新一个元素,因此缓存线将从所有缓存级别中逐出。我把 _恩斯 为了在 -O3

    接下来,我使用时间戳计数器计算延迟或读取 array[0] . 正如你在两个时间戳之间所看到的,有三条指令:两条 lfence 还有一个 read . 所以,我必须减去 lfence公司 头顶上。代码的最后一部分计算该开销。

    在代码末尾,将打印开销和未命中延迟。但是,结果是无效的!

    #include <stdio.h>
    #include <stdint.h>
    #include <x86intrin.h>
    int main()
    {
        int array[ 100 ];
        for ( int i = 0; i < 100; i++ )
                array[ i ] = i;
        uint64_t t1, t2, ov, diff;
    
        _mm_lfence();
        _mm_clflush( &array[ 0 ] );
        _mm_lfence();
    
        _mm_lfence();
        t1 = __rdtsc();
        _mm_lfence();
        int tmp = array[ 0 ];
        _mm_lfence();
        t2 = __rdtsc();
        _mm_lfence();
    
        diff = t2 - t1;
        printf( "diff is %lu\n", diff );
    
        _mm_lfence();
        t1 = __rdtsc();
        _mm_lfence();
        _mm_lfence();
        t2 = __rdtsc();
        _mm_lfence();
        ov = t2 - t1;
        printf( "lfence overhead is %lu\n", ov );
        printf( "miss cycles is %lu\n", diff-ov );
    
        return 0;
    }
    

    $ gcc -O3 -o flush1 flush1.c
    $ taskset -c 0 ./flush1
    diff is 161
    lfence overhead is 147
    miss cycles is 14
    $ taskset -c 0 ./flush1
    diff is 161
    lfence overhead is 154
    miss cycles is 7
    $ taskset -c 0 ./flush1
    diff is 147
    lfence overhead is 154
    miss cycles is 18446744073709551609
    

    有什么想法吗?

    接下来,我试着 clock_gettime 函数计算未命中延迟,如下所示

        _mm_lfence();
        _mm_clflush( &array[ 0 ] );
        _mm_lfence();
    
        struct timespec start, end;
        clock_gettime(CLOCK_MONOTONIC, &start);
        _mm_lfence();
        int tmp = array[ 0 ];
        _mm_lfence();
        clock_gettime(CLOCK_MONOTONIC, &end);
        diff = 1000000000 * (end.tv_sec - start.tv_sec) + end.tv_nsec - start.tv_nsec;
        printf("miss elapsed time = %lu nanoseconds\n", diff);
    

    输出是 miss elapsed time = 578 nanoseconds

    更新1:

    多亏了彼得和哈迪,总结到现在为止的回答,我发现

    1-优化阶段省略了未使用的变量,这就是我在输出中看到的奇怪值的原因。多亏了彼得的答复,有办法解决这个问题。

    2个- 时钟时间 不适用于这种分辨率,该函数用于较大的延迟。

    作为解决方法,我尝试将数组引入缓存,然后刷新所有元素,以确保所有元素都从所有缓存级别中逐出。然后我测量了 然后 array[20] . 因为每个元素是4字节,所以距离是80字节。我希望有两个缓存未命中。然而 阵列[20] 类似于缓存命中。一个安全的猜测是缓存线不是80字节。所以,也许 阵列[20]

        for ( int i = 0; i < 100; i++ ) {
                _mm_lfence();
                _mm_clflush( &array[ i ] );
                _mm_lfence();
        }
    
        _mm_lfence();
        t1 = __rdtsc();
        _mm_lfence();
        int tmp = array[ 0 ];
        _mm_lfence();
        t2 = __rdtsc();
        _mm_lfence();
        diff1 = t2 - t1;
        printf( "tmp is %d\ndiff1 is %lu\n", tmp, diff1 );
    
        _mm_lfence();
        t1 = __rdtsc();
        tmp = array[ 20 ];
        _mm_lfence();
        t2 = __rdtsc();
        _mm_lfence();
        diff2 = t2 - t1;
        printf( "tmp is %d\ndiff2 is %lu\n", tmp, diff2 );
    
        _mm_lfence();
        t1 = __rdtsc();
        _mm_lfence();
        _mm_lfence();
        t2 = __rdtsc();
        _mm_lfence();
        ov = t2 - t1;
        printf( "lfence overhead is %lu\n", ov );
        printf( "TSC1 is %lu\n", diff1-ov );
        printf( "TSC2 is %lu\n", diff2-ov );
    

    输出为

    $ ./flush1
    tmp is 0
    diff1 is 371
    tmp is 20
    diff2 is 280
    lfence overhead is 147
    TSC1 is 224
    TSC2 is 133
    $ ./flush1
    tmp is 0
    diff1 is 399
    tmp is 20
    diff2 is 280
    lfence overhead is 154
    TSC1 is 245
    TSC2 is 126
    $ ./flush1
    tmp is 0
    diff1 is 392
    tmp is 20
    diff2 is 840
    lfence overhead is 147
    TSC1 is 245
    TSC2 is 693
    $ ./flush1
    tmp is 0
    diff1 is 364
    tmp is 20
    diff2 is 140
    lfence overhead is 154
    TSC1 is 210
    TSC2 is 18446744073709551602
    

    那么,“HW prefetcher带来其他块”的说法大约正确80%。那是怎么回事?还有更准确的说法吗?

    1 回复  |  直到 7 年前
        1
  •  3
  •   Peter Cordes    7 年前

    你删除了Hadi的密码 tmp 最后,gcc对它进行了优化。

    看看编译器生成的asm,例如。 on the Godbolt compiler explorer . 当你试图将这种低级的东西微型化时,你应该一直这样做,特别是当你的计时结果出乎意料的时候。

        lfence
        clflush [rcx]
        lfence
    
        lfence
        rdtsc                     # start of first timed region
        lfence
           # nothing because tmp=array[0] optimized away.
        lfence
        mov     rcx, rax
        sal     rdx, 32
        or      rcx, rdx
        rdtsc                     # end of first timed region
        mov     edi, OFFSET FLAT:.LC2
        lfence
    
        sal     rdx, 32
        or      rax, rdx
        sub     rax, rcx
        mov     rsi, rax
        mov     rbx, rax
        xor     eax, eax
        call    printf
    

    您将从中获取有关未使用变量的编译器警告 -Wall tmp++ 不会使 tmp公司 volatile 时间区域外的变量。(或使用内联 asm volatile perf 提到一些技巧: https://www.youtube.com/watch?v=nXaxk27zwlk


    在GNU C中(至少在gcc和clang中 -O3 ),您可以通过强制 (volatile int*)

    // int tmp = array[0];           // replace this
    (void) *(volatile int*)array;    // with this
    

    这个 (void) 避免在空上下文中计算表达式时发出警告,如编写 x;

    这看起来像是严格的UB别名,但我的理解是gcc定义了这种行为。Linux内核投射一个指针来添加 不稳定的 its中的限定符 ACCESS_ONCE 宏,所以它用于gcc绝对关心支持的代码库之一。你可以把整个阵列 不稳定的 ;它的初始化是否不能自动矢量化并不重要。

        # gcc8.2 -O3
        lfence
        rdtsc
        lfence
        mov     rcx, rax
        sal     rdx, 32
        mov     eax, DWORD PTR [rsp]    # the load which wasn't there before.
        lfence
        or      rcx, rdx
        rdtsc
        mov     edi, OFFSET FLAT:.LC2
        lfence
    

    那你就不必再纠结于 tmp公司 使用,或担心死区消除、CSE或持续传播。实际上 _mm_mfence() 或者Hadi最初的答案中包含了足够的内存限制,使得gcc实际上为cache miss+cache hit情况重新加载,但是它很容易优化掉其中一个重新加载。


    注意,这可能会导致asm加载到寄存器中,但从不读取它。当前CPU仍在等待结果(特别是如果 lfence ),但是覆盖结果可能会让一个假设的CPU丢弃负载,而不是等待它。(编译器是否在下一个寄存器之前对它做了其他操作取决于它 lfence公司 ,就像 mov rdtsc 结果在那里。)

    对于硬件来说,这是很棘手/不太可能的,因为CPU必须做好异常准备,请参阅 discussion in comments here What is the latency and throughput of the RDRAND instruction on Ivy Bridge? ),但那可能是个特例。

    我自己在Skylake上测试了这个 xor eax,eax 到编译器的asm输出,就在 mov eax, DWORD PTR [rsp] ,以终止缓存未命中加载的结果。但这并不影响时机。

    不稳定的 volatile int sink ,以防将来的CPU开始丢弃产生未读结果的UOP。但仍在使用 不稳定的 以确保他们发生在你想要的地方。


    ,除非你 希望 以空闲时钟速度测量缓存未命中执行时间。看起来您的空计时区域占用了大量的参考周期,所以您的CPU可能被打卡得很慢。


    那么,缓存攻击(如熔毁和幽灵)究竟是如何克服这一问题的呢?基本上,他们必须禁用硬件预取器,因为他们试图测量相邻地址,以找到他们是否击中或错过。

    作为熔毁或幽灵攻击的一部分,缓存读取端通道通常使用足够大的步幅,以至于硬件预取无法检测访问模式。e、 在单独的页面而不是相邻的行上。最早的谷歌点击之一 meltdown cache read prefetch stride https://medium.com/@mattklein123/meltdown-spectre-explained-6bc8634cc0c2