Измерение задержки памяти со счетчиком меток времени - PullRequest
0 голосов
/ 29 августа 2018

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

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

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

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

Update1:

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

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

UPDATE2:

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

Ответы [ 2 ]

0 голосов
/ 30 августа 2018

Во-первых, обратите внимание, что два вызова 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. Я постараюсь добавить объяснение позже.

0 голосов
/ 29 августа 2018

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

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...