Почему используется MFENCE с предварительной загрузкой блока инструкций в кеше L1? - PullRequest
6 голосов
/ 13 мая 2019

У меня есть объект размером 64 байта:

typedef struct _object{
  int value;
  char pad[60];
} object;

в основном я инициализирую массив объекта:

volatile object * array;
int arr_size = 1000000;
array = (object *) malloc(arr_size * sizeof(object));

for(int i=0; i < arr_size; i++){
    array[i].value = 1;
    _mm_clflush(&array[i]);
}
_mm_mfence();

Затем снова пройдитесь по каждому элементу. Это цикл, для которого я считаю события:

int tmp;
for(int i=0; i < arr_size-105; i++){
    array[i].value = 2;
    //tmp = array[i].value;
     _mm_mfence();
 }

наличие mfence здесь не имеет никакого смысла, но я связывал что-то еще и случайно обнаружил, что если у меня есть операция сохранения без mfence , я получаю полмиллиона запросов RFO (измеряется по событию papi L2_RQSTS.ALL_RFO ), что означает, что еще полмиллиона было поражено L1, предварительно выбрано до спроса. Однако , включая mfence , приводит к 1 миллиону запросов RFO, что дает RFO_HIT, что означает, что строка кэша предварительно выбирается только в L2, а не в кэше L1.

Помимо того, что документация Intel как-то указывает на иное: «данные можно вводить в кэш спекулятивно непосредственно перед, во время или после выполнения инструкции MFENCE». Я проверил с операциями загрузки. без mfence я получаю до 2000 L1, тогда как с mfence у меня до 1 миллиона L1 (измеряется с помощью события pEMI MEM_LOAD_RETIRED.L1_HIT). Строки кэша предварительно загружены в L1 для инструкции загрузки.

Так что не должно быть случая, когда включая предварительную выборку блоков mfence. Операции хранения и загрузки занимают почти одинаковое время - без 5-6 мс, а с 20 мс. Я перебрал другие вопросы, касающиеся mfence, но там не упоминалось, каково его ожидаемое поведение при предварительной загрузке, и я не вижу достаточно веской причины или объяснения, почему он блокировал бы предварительную выборку в кеше L1 только с операциями хранилища. Или я могу что-то упустить для описания mfence?

Я тестирую на микроархитектуре Skylake, однако проверил с помощью Broadwell и получил тот же результат.

Ответы [ 2 ]

3 голосов
/ 14 мая 2019

Значения счетчика, которые вы видите, не являются предварительной выборкой L1: эффект сохраняется, даже если вы отключите предварительные выборки L1. Фактически, эффект остается, если вы отключите все предварительные сборщики, кроме стримера L2:

wrmsr -a 0x1a4 "$((2#1110))"

Если вы делаете отключение стримера L2, однако, количество будет таким, как вы ожидаете: вы увидите примерно 1 000 000 L2.RFO_MISS и L2.RFO_ALL даже без mfence.

Во-первых, важно отметить, что количество событий L2_RQSTS.RFO_* не учитывает события RFO, исходящие от стримера L2 . Вы можете увидеть подробности здесь , но в основном umask для каждого из событий RFO 0x24:

name      umask
RFO_MISS   0x22
RFO_HIT    0x42
ALL_RFO    0xE2

Обратите внимание, что ни одно из значений umask не имеет бита 0x10, который указывает, что события, которые происходят от стримера L2, должны отслеживаться.

Похоже, что когда стример L2 активен, многие из событий, которые можно ожидать назначить одному из этих событий, вместо этого "съедаются" событиями предварительной выборки L2. Вероятно, случается так, что предварительная выборка L2 выполняется раньше, чем поток запросов, и когда запрос RFO поступает из L1, он находит запрос, уже выполняемый предварительным выборщиком L2. Это только снова увеличивает версию события umask |= 0x10 (на самом деле я получаю 2 000 000 ссылок, включая этот бит), что означает, что RFO_MISS, RFO_HIT и RFO_ALL пропустят его.

Это в некоторой степени аналогично сценарию "fb_hit", где L1 не загружает ни попадания, ни попадания точно, но поражает текущую загрузку - но сложность здесь заключается в том, что загрузка была инициирована средством предварительной выборки L2.

The mfence только замедляет все вниз достаточно, что префетчер L2 почти всегда имеет время, чтобы довести линию вплоть до L2, давая RFO_HIT кол.

Я не думаю, что здесь используются средства предварительной выборки L1 (на это указывает тот факт, что это работает так же, если вы их отключите): насколько я знаю, средства предварительной выборки L1 не взаимодействуют с магазинами, а только загружаются.

Вот несколько полезных perf команд, которые вы можете использовать, чтобы увидеть разницу, в том числе и с битом «L2 streamer origin». Вот без событий стримера L2:

perf stat --delay=1000 -e cpu/event=0x24,umask=0xef,name=l2_rqsts_references/,cpu/event=0x24,umask=0xe2,name=l2_rqsts_all_rfo/,cpu/event=0x24,umask=0xc2,name=l2_rqsts_rfo_hit/,cpu/event=0x24,umask=0x22,name=l2_rqsts_rfo_miss/

и вместе с ними:

perf stat --delay=1000 -e cpu/event=0x24,umask=0xff,name=l2_rqsts_references/,cpu/event=0x24,umask=0xf2,name=l2_rqsts_all_rfo/,cpu/event=0x24,umask=0xd2,name=l2_rqsts_rfo_hit/,cpu/event=0x24,umask=0x32,name=l2_rqsts_rfo_miss/

Я запустил их для этого кода (с выравниванием sleep(1) с командой --delay=1000, переданной perf, чтобы исключить код инициализации):

#include <time.h>
#include <immintrin.h>
#include <stdio.h>
#include <unistd.h>

typedef struct _object{
  int value;
  char pad[60];
} object;

int main() {
    volatile object * array;
    int arr_size = 1000000;
    array = (object *) malloc(arr_size * sizeof(object));

    for(int i=0; i < arr_size; i++){
        array[i].value = 1;
        _mm_clflush((const void*)&array[i]);
    }
    _mm_mfence();

    sleep(1);
    // printf("Starting main loop after %zu ms\n", (size_t)clock() * 1000u / CLOCKS_PER_SEC);

    int tmp;
    for(int i=0; i < arr_size-105; i++){
        array[i].value = 2;
        //tmp = array[i].value;
        // _mm_mfence();
    }
}
1 голос
/ 14 мая 2019

Что касается случая операций с хранилищем, я выполнил один и тот же цикл на процессоре Haswell в четырех различных конфигурациях:

  • MFENCE + E: после хранилища есть инструкция MFENCE.Все аппаратные средства предварительной выборки включены.
  • E: Нет MFENCE.Все аппаратные средства предварительной выборки включены.
  • MFENCE + D: после хранилища есть инструкция MFENCE.Все аппаратные средства предварительной выборки отключены.
  • D: MFENCE нет.Все аппаратные средства предварительной выборки отключены.

Результаты показаны ниже, которые нормированы по количеству хранилищ (каждое хранилище относится к отдельной строке кэша).Они очень детерминированы для нескольких прогонов.

                                 | MFENCE + E |      E     | MFENCE + D |      D     |
    L2_RQSTS.ALL_RFO             |    0.90    |    0.62    |    1.00    |    1.00    |
    L2_RQSTS.RFO_HIT             |    0.80    |    0.12    |    0.00    |    0.00    |
    L2_RQSTS.RFO_MISS            |    0.10    |    0.50    |    1.00    |    1.00    |
    OFFCORE_REQUESTS.DEMAND_RFO  |    0.20    |    0.88    |    1.00    |    1.00    |
    PF_L3_RFO                    |    0.00    |    0.00    |    0.00    |    0.00    |
    PF_RFO                       |    0.80    |    0.16    |    0.00    |    0.00    |
    DMND_RFO                     |    0.19    |    0.84    |    1.00    |    1.00    |

Первые четыре события являются основными событиями, а последние три события являются событиями вне ядра:

  • L2_RQSTS.ALL_RFO: Происходитза каждый запрос RFO к L2.Это включает запросы RFO от магазинов, которые удалились или иным образом, и запросы RFO от PREFETCHW.Для случаев, когда аппаратные средства предварительной выборки включены, число событий меньше ожидаемого, что является нормированным.Можно подумать о двух возможных причинах этого: (1) каким-то образом некоторые из RFO попали в L1, и (2) событие недооценено.Мы попытаемся выяснить, что это, изучив количество других событий и вспомнив, что мы знаем о предварительных выборщиках L1D.
  • L2_RQSTS.RFO_HIT и L2_RQSTS.RFO_MISS: произойдет для RFO, который попадает или пропускаетв L2 соответственно.Во всех конфигурациях сумма счетчиков этих событий в точности равна L2_RQSTS.ALL_RFO.
  • OFFCORE_REQUESTS.DEMAND_RFO: документация по этому событию предполагает, что оно должно совпадать с L2_RQSTS.RFO_MISS.Однако обратите внимание, что сумма OFFCORE_REQUESTS.DEMAND_RFO и L2_RQSTS.RFO_HIT фактически равна единице.Таким образом, вполне возможно, что L2_RQSTS.RFO_MISS недооценивает (и то же самое делает L2_RQSTS.ALL_RFO).Фактически, это наиболее вероятное объяснение, потому что в руководстве по оптимизации Intel (и других документах Intel) говорится, что только предварительный сборщик стримера L2 может отслеживать магазины.Руководство по счетчику производительности Intel упоминает «предварительные выборки L1D RFO» в описании L2_RQSTS.ALL_RFO.Эти предварительные выборки, вероятно, относятся к RFO из хранилищ, которые еще не удалились (см. Последний раздел ответа на . Почему события пропуска хранилища L1 в пользовательском режиме учитываются только при наличии цикла инициализации хранилища? ).
  • PF_L3_RFO: Происходит, когда запускается RFO от устройства предварительной выборки стримера L2, а целевой структурой кэша является только L3.Все значения этого события равны нулю.
  • PF_RFO: Происходит, когда запускается RFO от средства предварительной выборки стримера L2 и целевой структурой кэша является L2 и, возможно, L3 (если L3 включительно, толиния также будет заполнена в L3).Счет этого события близок к L2_RQSTS.RFO_HIT.В случае MFENCE + E кажется, что 100% RFO были выполнены вовремя (до того, как RFO спроса достигло L2).В случае E 25% предварительных выборок не были завершены вовремя или неправильные строки были предварительно выбраны.Причина, по которой число обращений RFO в L2 больше в случае MFENCE + E по сравнению со случаем E, заключается в том, что инструкция MFENCE задерживает более поздние RFO, тем самым сохраняя большинство записей супер-очереди L2 доступными для средства предварительной выборки стримера L2,Так что MFENCE действительно позволяет предварительному сборщику стримера L2 работать лучше.Без него было бы много запросов RFO в полете на L2, оставляя небольшое количество записей супер-очереди для предварительной выборки.
  • DMND_RFO: То же, что и OFFCORE_REQUESTS.DEMAND_RFO, но похоже, что это можетнедоучета немного.

Я проверил с операциями загрузки.без mfence я получаю до 2000 L1, тогда как с mfence у меня до 1 миллиона L1 (измеряется с помощью события MEM_LOAD_RETIRED.L1_HIT).Строки кэша предварительно загружены в L1 для инструкции загрузки.

Что касается случая с операциями загрузки, то, по моему опыту, MFENCE (или любая другая инструкция ограждения) не влияет на поведение аппаратных средств предварительной выборки. Истинное количество событий MEM_LOAD_RETIRED.L1_HIT здесь на самом деле очень мало (<2000). Большинство подсчитываемых событий происходит от самого <code>MFENCE, а не от нагрузок. MFENCESFENCE) требуют отправки запроса на забор до контроллера памяти, чтобы гарантировать, что все ожидающие хранилища достигли глобальной точки наблюдения. Запрос на забор не считается событием RFO, но может учитываться как несколько событий, включая L1_HIT. Для получения дополнительной информации об этом и аналогичных наблюдениях см. Мой пост в блоге: Введение в события мониторинга кэширования и пропадания производительности .

...