в чем проблема с использованием пошагового доступа к потоку для измерения максимальной пропускной способности памяти

#memory #benchmarking #cpu-architecture #microbenchmark

#память #сравнительный анализ #cpu-архитектура #microbenchmark

Вопрос:

Используя Skylake в качестве примера, его строка кэша равна 64B.

Я попытался написать простую программу, чтобы узнать, какова максимальная пропускная способность памяти, которую я могу использовать. В приведенном ниже коде я намеренно делаю шаг 64B, чтобы каждая загрузка получала другую строку кэша (64B). Я собираю время, затраченное на завершение загрузки 10M, а затем вычисляю загруженную память, умножая количество загрузок на 64B.

Затем я запускаю потоки, которые синхронизируют и параллельно выполняют приведенный ниже код. Итак, когда все потоки завершаются, общая загруженная память равна total * NUM_OF_THREADS * 64B. Затем я делю его на (end_time-start_time).

Полученная мной пропускная способность намного выше теоретической максимальной пропускной способности памяти для Skylake. Так что это неверно. Но я не знаю, что не так с моими расчетами.

Единственное предположение, которое я могу сделать, это то, что, возможно, объем памяти не насыщен, процессор выполняет предварительную выборку следующих строк кэша, так что многие нагрузки фактически загружаются из кэша. Но поскольку моя встроенная сборка представляет собой плотную последовательность загрузок памяти, я не уверен, как подтвердить свою догадку.

Есть комментарии? Спасибо.

    st = start_timing()
        do {
          for (i=0; i< 10; i  ) {
            asm volatile("movl 0x0(%[P]),%[sum]nt"
                         "movl 0x40(%[P]),%[sum]nt"
                         "movl 0x80(%[P]),%[sum]nt"
                         "movl 0xc0(%[P]),%[sum]nt"
                         "movl 0x100(%[P]),%[sum]nt"
                         "movl 0x140(%[P]),%[sum]nt"
                         "movl 0x180(%[P]),%[sum]nt"
                         "movl 0x1c0(%[P]),%[sum]nt"
                         "movl 0x200(%[P]),%[sum]nt"
                         "movl 0x240(%[P]),%[sum]nt"
                         "movl 0x280(%[P]),%[sum]nt"
                         "movl 0x2c0(%[P]),%[sum]nt"
                         "movl 0x300(%[P]),%[sum]nt"
                         "movl 0x340(%[P]),%[sum]nt"
                         "movl 0x380(%[P]),%[sum]nt"
                         "movl 0x3c0(%[P]),%[sum]nt"
                         "movl 0x400(%[P]),%[sum]nt"
                         "movl 0x440(%[P]),%[sum]nt"
                         "movl 0x480(%[P]),%[sum]nt"
                         "movl 0x4c0(%[P]),%[sum]nt"
                             : [P]" r"(p), [sum]" r"(sum)
                             : );
          }   
          total  = 200;
          p = q  ((total00000)<<6);

        } while (total < 10000000);
    et = end_timing()

    bw = (total * 64)/(et-st)
 

Комментарии:

1. Какие фактические цифры вы получаете? Что вы видите perf stat для подсчета промахов в кэше?

2. Я использую perf для измерения фактического потребления пропускной способности памяти, которое составляет около 95 ГБ / с. Но мои расчеты, упомянутые в первоначальном посте, дают более 600 ГБ / с. 🙂

3. Вы можете измерить количество обращений к кэшу на каждом уровне , используя события производительности MEM_LOAD_RETIRED.L1_HIT , MEM_LOAD_RETIRED.L2_HIT , и MEM_LOAD_RETIRED.L3_HIT . Я предполагаю, что вы получаете много обращений к L1, но трудно сказать, почему, не видя всего кода. Что вы имели в виду, говоря «я использую perf для измерения фактического потребления пропускной способности памяти, которое составляет около 95 ГБ / с»? Например, как?

Ответ №1:

Да, загрузка dword из каждой строки кэша — хороший способ сравнить пропускную способность кэша / памяти для кэшей, отличных от L1d. (Если данные остаются горячими в L1d, вам необходимо измерить узкое место их получения через модули выполнения загрузки в регистры; если у вас нет AVX512, для чтения всей строки кэша требуется несколько инструкций.)

Вероятно, вы получаете попадания в кэш L1d или L2. Если вы никогда не записывали память, все это будет копироваться при записи, сопоставленной с той же физической нулевой страницей, если она находится в BSS или выделена с помощью malloc.

Или просто то, что разные ядра имеют свои собственные частные кеши L1d. Посмотрите, как кэш может быть таким быстрым?снова на electronics.SE . Однако, если вы на самом деле используете 10 МБ физической оперативной памяти, это больше, чем у четырехъядерного рабочего стола SKL. Если у вас Skylake Xeon с большим объемом кэша L3, то да, совокупная пропускная способность, конечно, может быть значительно выше, чем у ОЗУ.

Кроме того, http://blog.stuffedcow.net/2013/01/ivb-cache-replacement / показывает, что замена L3 не является строго псевдо-LRU; он адаптивен в последних версиях Intel, поэтому он может быть более устойчивым, чем вы ожидаете, к вытеснению из цикла через ОЗУ. 10 МБ может быть достаточно мало, чтобы получить несколько попаданий в L3 при общем объеме L3 8 МБ на четырехъядерном i7.


asm volatile это остановит его оптимизацию, и " r"(pointer) входные данные должны быть в порядке, чтобы видеть обновления вашего указателя. Компилятор «не знает», что asm считывает указанную память (потому что вы не сказали ему об этом, и нет никаких "memory" сбоев), поэтому любые более ранние хранилища в буфере могут быть оптимизированы как мертвые хранилища.

Комментарии:

1. Я дважды проверил окончательную сборку, и моя встроенная сборка осталась нетронутой. На самом деле я не инициализирую (или не сохраняю) массив (‘q’), поскольку вы можете видеть, что я просто просматриваю его.

2. Разве я не думаю, что я касаюсь 10M * 64B = 640BM памяти из-за пошагового доступа?

3. @yeeha: о, я недостаточно внимательно прочитал вопрос. Я пропустил, что это была загрузка 10 м, а не 10 МБ размера буфера. Но я думаю, что не инициализация вашего массива объясняет ваши наблюдения. Вы получите промахи TLB, но попадание в кэш данных, если все страницы сопоставлены COW с одними и теми же физическими страницами.