【问题标题】:Memory latency measurement with time stamp counter使用时间戳计数器测量内存延迟
【发布时间】:2019-02-04 14:22:41
【问题描述】:

我编写了以下代码,它首先刷新两个数组元素,然后尝试读取元素以测量命中/未命中延迟。

#include <stdio.h>
#include <stdint.h>
#include <x86intrin.h>
#include <time.h>
int main()
{
    /* create array */
    int array[ 100 ];
    int i;
    for ( i = 0; i < 100; i++ )
        array[ i ] = i;   // bring array to the cache

    uint64_t t1, t2, ov, diff1, diff2, diff3;

    /* flush the first cache line */
    _mm_lfence();
    _mm_clflush( &array[ 30 ] );
    _mm_clflush( &array[ 70 ] );
    _mm_lfence();

    /* READ MISS 1 */
    _mm_lfence();           // fence to keep load order
    t1 = __rdtsc();         // set start time
    _mm_lfence();
    int tmp = array[ 30 ];   // read the first elemet => cache miss
    _mm_lfence();
    t2 = __rdtsc();         // set stop time
    _mm_lfence();

    diff1 = t2 - t1;        // two fence statements are overhead
    printf( "tmp is %d\ndiff1 is %lu\n", tmp, diff1 );

    /* READ MISS 2 */
    _mm_lfence();           // fence to keep load order
    t1 = __rdtsc();         // set start time
    _mm_lfence();
    tmp = array[ 70 ];      // read the second elemet => cache miss (or hit due to prefetching?!)
    _mm_lfence();
    t2 = __rdtsc();         // set stop time
    _mm_lfence();

    diff2 = t2 - t1;        // two fence statements are overhead
    printf( "tmp is %d\ndiff2 is %lu\n", tmp, diff2 );


    /* READ HIT*/
    _mm_lfence();           // fence to keep load order
    t1 = __rdtsc();         // set start time
    _mm_lfence();
    tmp = array[ 30 ];   // read the first elemet => cache hit
    _mm_lfence();
    t2 = __rdtsc();         // set stop time
    _mm_lfence();

    diff3 = t2 - t1;        // two fence statements are overhead
    printf( "tmp is %d\ndiff3 is %lu\n", tmp, diff3 );


    /* measuring fence overhead */
    _mm_lfence();
    t1 = __rdtsc();
    _mm_lfence();
    _mm_lfence();
    t2 = __rdtsc();
    _mm_lfence();
    ov = t2 - t1;

    printf( "lfence overhead is %lu\n", ov );
    printf( "cache miss1 TSC is %lu\n", diff1-ov );
    printf( "cache miss2 (or hit due to prefetching) TSC is %lu\n", diff2-ov );
    printf( "cache hit TSC is %lu\n", diff3-ov );


    return 0;
}

输出是

# gcc -O3 -o simple_flush simple_flush.c
# taskset -c 0 ./simple_flush
tmp is 30
diff1 is 529
tmp is 70
diff2 is 222
tmp is 30
diff3 is 46
lfence overhead is 32
cache miss1 TSC is 497
cache miss2 (or hit due to prefetching) TSC is 190
cache hit TSC is 14
# taskset -c 0 ./simple_flush
tmp is 30
diff1 is 486
tmp is 70
diff2 is 276
tmp is 30
diff3 is 46
lfence overhead is 32
cache miss1 TSC is 454
cache miss2 (or hit due to prefetching) TSC is 244
cache hit TSC is 14
# taskset -c 0 ./simple_flush
tmp is 30
diff1 is 848
tmp is 70
diff2 is 222
tmp is 30
diff3 is 46
lfence overhead is 34
cache miss1 TSC is 814
cache miss2 (or hit due to prefetching) TSC is 188
cache hit TSC is 12

读取array[70] 的输出存在一些问题。 TSC 既没有被击中也没有被击中。我已经刷新了类似于array[30] 的那个项目。一种可能性是,当访问array[40] 时,硬件预取器会带来array[70]。所以,这应该是一个打击。然而,TSC 远不止是一击。当我第二次尝试读取array[30] 时,您可以验证命中 TSC 约为 20。

即使array[70] 没有被预取,TSC 也应该类似于缓存未命中。

有什么原因吗?

更新1:

为了读取数组,我按照 Peter 和 Hadi 的建议尝试了 (void) *((int*)array+i)

在输出中,我看到了许多负面结果。我的意思是开销似乎大于(void) *((int*)array+i)

更新2:

我忘记添加volatile。结果现在很有意义。

【问题讨论】:

  • 编译器可能不会打扰从数组中读取,因为它不是 volatile 并且没有使用该值(优化器会/应该完全忽略它);并且lfence 的成本取决于周围的代码(例如,当时有多少负载在飞行),并且无法在一组条件下测量,并且在不同的一组条件下假设是相同的。
  • 是的。我忘了加volatile。谢谢。

标签: c performance x86 cpu-architecture tsc


【解决方案1】:

首先,请注意,在测量diff1diff2 之后对printf 的两次调用可能会扰乱L1D 甚至L2 的状态。在我的系统上,使用printfdiff3-ov 的报告值范围在 4-48 个周期之间(我已经配置了我的系统,使 TSC 频率大约等于核心频率)。最常见的值是 L2 和 L3 延迟的值。如果报告的值为 8,那么我们的 L1D 缓存命中。如果它大于 8,那么很可能之前对 printf 的调用已经从 L1D 和可能的 L2(在一些罕见的情况下,L3!)中踢出目标缓存行,这可以解释测量的延迟高于 8。@PeterCordes 有 suggested 使用 (void) *((volatile int*)array + i) 而不是 temp = array[i]; printf(temp)。进行此更改后,我的实验表明,大多数报告的diff3-ov 测量值恰好是 8 个周期(这表明测量误差约为 4 个周期),而报告的唯一其他值是 0、4 和 12。所以强烈推荐 Peter 的方法。

一般来说,主存访问延迟取决于许多因素,包括 MMU 缓存的状态和页表遍历器对数据缓存的影响、核心频率、非核心频率、内存的状态和配置控制器和内存芯片的目标物理地址、非核心竞争和由于超线程导致的核心竞争。 array[70] 可能与 array[30] 位于不同的虚拟页面(和物理页面)中,并且它们的加载指令 IP 和目标内存位置的地址可能以复杂的方式与预取器交互。所以cache miss1cache miss2 不同的原因可能有很多。进行彻底的调查是可能的,但正如您想象的那样,它需要付出很多努力。通常,如果您的核心频率大于 1.5 GHz(小于高性能 Intel 处理器上的TSC frequency),则 L3 加载缺失将至少需要 60 个核心周期。在您的情况下,两个未命中延迟都超过 100 个周期,因此这些很可能是 L3 未命中。但在极少数情况下,cache miss2 似乎接近 L3 或 L2 延迟范围,这可能是由于预取。


我确定以下代码在 Haswell 上提供了更准确的统计测量结果:

t1 = __rdtscp(&dummy);
tmp = *((volatile int*)array + 30);
asm volatile ("add $1, %1\n\t"
              "add $1, %1\n\t"
              "add $1, %1\n\t"
              "add $1, %1\n\t"
              "add $1, %1\n\t"
              "add $1, %1\n\t"
              "add $1, %1\n\t"
              "add $1, %1\n\t"
              "add $1, %1\n\t"
              "add $1, %1\n\t"
              "add $1, %1\n\t"
          : "+r" (tmp));          
t2 = __rdtscp(&dummy);
t2 = __rdtscp(&dummy);
loadlatency = t2 - t1 - 60; // 60 is the overhead

loadlatency 为 4 个周期的概率为 97%。 loadlatency 为 8 个周期的概率为 1.7%。 loadlatency 取其他值的概率是 1.3%。所有其他值都大于 8 和 4 的倍数。我稍后会尝试添加解释。

【讨论】:

  • 抱歉回复晚了。请参阅帖子中的 UPDATE1。我想知道你是如何得到正确的结果的。你能运行我的代码吗?
  • 对不起,我忘了加volatile
  • 假设两个独立的"=r"(tmp)"r"(tmp) 操作数将使用同一个寄存器是不安全的。这些约束将编译器的 asm 黑匣子描述为复制和任何内容。您修改输入操作数并保留输出操作数不写,除非编译器碰巧为两者选择相同的寄存器。您需要一个 "+r" 约束,或者像 "0"(tmp) 这样的输入匹配约束(与 %0 输入的位置相同)。或者你需要使用lea 1(%1), %0add $1, %0 ; ...
  • @PeterCordes 谢谢。我的目标是创建一个带负载的 dep 链,所以我认为 "=r" (tmp) 可以完全删除。 asm 语句是 volatile 的事实阻止了编译器对其进行优化。
  • @HadiBrais:只需使用"+r"(tmp),这正是您想要的。我现在看到 tmp 以后不会在任何地方使用,但是在没有输出的 asm 语句中修改寄存器似乎是一个糟糕的主意。
【解决方案2】:

一些想法:

  • 也许a[70] 被预取到L1 之外的某个级别的缓存中?
  • 也许 DRAM 中的一些优化导致此访问速度很快,例如,可能在访问 a[30] 后行缓冲区保持打开状态。

您应该调查除 a[30] 和 a[70] 之外的其他访问权限,看看您是否得到不同的数字。例如。你在 a[30] 后跟 a[31] 上的命中时间是否相同(如果你使用 64 字节对齐的aligned_alloc,它应该与 a[30] 在同一行中获取)。 a[69] 和 a[71] 等其他元素是否给出与 a[70] 相同的时序?

【讨论】:

  • 是的array[33] 有大约命中延迟,类似于第二次访问array[30]。我将研究更多关于预取到其他级别的信息。感谢您的提示。
猜你喜欢
  • 2010-09-23
  • 1970-01-01
  • 1970-01-01
  • 2023-03-27
  • 2014-02-17
  • 1970-01-01
  • 2017-01-01
  • 1970-01-01
  • 2012-06-19
相关资源
最近更新 更多