Измерение задержки памяти с помощью счетчика меток времени

Я написал следующий код, который сначала сбрасывает два элемента массива, а затем пытается прочитать элементы, чтобы измерить задержки попадания / пропуска.

#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] доступ к HW prefetcher приносит array[70], Итак, это должно быть хитом. Тем не менее, TSC гораздо больше, чем хит. Вы можете убедиться, что значение TSC составляет около 20, когда я пытаюсь прочитать array[30] во второй раз.

Даже если array[70] предварительно не выбран, TSC должен быть похож на промах кэша.

Есть ли причина для этого?

Update1:

Чтобы прочитать массив, я попытался (void) *((int*)array+i) по предложению Петра и Хади.

В выводе я вижу много отрицательных результатов. Я имею в виду, что накладные расходы больше (void) *((int*)array+i)

UPDATE2:

Я забыл добавить volatile, Результаты теперь значимы.

2 ответа

Во-первых, обратите внимание, что два вызова printf после измерения diff1 а также 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), После внесения этого изменения мои эксперименты показали, что большинство зарегистрированных измерений для diff3-ov это ровно 8 циклов (что говорит о том, что погрешность измерения составляет около 4 циклов), и сообщаются только другие значения, равные 0, 4 и 12. Поэтому подход Питера настоятельно рекомендуется.

В общем, задержка доступа к основной памяти зависит от многих факторов, включая состояние кэшей MMU и влияние обходчиков таблиц страниц на кэши данных, частоту ядра, частоту некорда, состояние и конфигурацию контроллера памяти и микросхемы памяти по отношению к целевому физическому адресу, неосновной конкуренции и конкуренции на ядре из-за гиперпоточности. array[70] может быть на другой виртуальной странице (и физической странице), чем array[30] и их IP-адреса команд загрузки и адреса целевых областей памяти могут взаимодействовать с устройствами предварительной выборки сложным образом. Поэтому может быть много причин, почему cache miss1 отличается от cache miss2, Тщательное расследование возможно, но это потребует много усилий, как вы можете себе представить. Как правило, если частота вашего ядра превышает 1,5 ГГц (что меньше частоты TSC на высокопроизводительных процессорах Intel), то потеря загрузки 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. Я постараюсь добавить объяснение позже.

Некоторые идеи:

  • Возможно, [70] был предварительно загружен в какой-то уровень кэша помимо L1?
  • Возможно, некоторая оптимизация в DRAM делает этот доступ быстрым, например, может быть, буфер доступа остается открытым после доступа к [30].

Вы должны исследовать другой доступ, кроме [30] и [70], чтобы увидеть, если вы получаете разные номера. Например, получаете ли вы те же моменты времени для попадания в [30], за которым следует [31] (который должен быть выбран в той же строке, что и [30], если вы используете align_alloc с выравниванием по 64 байта). И дают ли другие элементы, такие как [69] и [71], то же время, что и [70]?

Другие вопросы по тегам