带时间戳计数器的内存延迟测量

时间:2018-08-29 17:50:58

标签: c performance x86 cpu-architecture tsc

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

#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也应类似于缓存未命中。

有什么理由吗?

UPDATE1:

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

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

UPDATE2:

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

2 个答案:

答案 0 :(得分:3)

首先,请注意,在测量printfdiff1之后,对diff2的两个调用可能会扰乱L1D甚至L2的状态。在我的系统上,使用printf时,diff3-ov的报告值介于4-48个周期之间(我已经配置了系统,使得TSC频率大约等于核心频率)。最常见的值是L2和L3延迟的值。如果报告的值为8,则表明L1D缓存已命中。如果它大于8,则很可能先前对printf的调用已从L1D以及L2(在极少数情况下为L3!)中踢出了目标缓存行,这可以解释测量结果。高于8的延迟。@ PeterCordes使用(void) *((volatile int*)array + i)而不是temp = array[i]; printf(temp)使用suggested。进行此更改后,我的实验表明,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的倍数。我稍后会尝试添加解释。

答案 1 :(得分:1)

一些想法:

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

您应该调查a [30]和a [70]以外的其他访问权限,以查看是否获得不同的号码。例如。是否在击打a [30]以及击打a [31]时得到了相同的时间(如果您使用aligned_alloc与64字节对齐,则应该在与a [30]相同的行中读取)。像a [69]和a [71]这样的其他元素是否具有与a [70]相同的时序?

相关问题