Использование rdmsr / rdpm c для точности прогнозирования ветвлений - PullRequest
7 голосов
/ 17 февраля 2020

Я пытаюсь понять, как работает модуль предсказания ветвления в CPU.

Я использовал papi, а также linux * perf-events, но оба они не дают точных результатов (для моего случая).

Это мой код:

void func(int* arr, int sequence_len){
  for(int i = 0; i < sequence_len; i++){
      // region starts
      if(arr[i]){
          do_sth();
      }
      // region ends
  }
}

Мой массив состоит из 0 и 1. Имеет шаблон размером sequence_len. Например, если мой размер равен 8, то он имеет шаблон 0 1 0 1 0 0 1 1 или что-то в этом роде.

Испытание 1:

Я пытаюсь понять, как CPU предсказывает эти ветви. Итак, я использовал papi и настроил счетчик производительности для предсказанных ветвлений прогнозов (я знаю, что он также подсчитывает косвенные ветки).

int func(){
  papi_read(r1);
  for(){
    //... same as above
  }
  papi_read(r2);
  return r2-r1;
}

int main(){
   init_papi();
   for(int i = 0; i < 10; i++)
     res[i] = func();

   print(res[i]);
}

Что я вижу в качестве вывода (для длины последовательности 200)

100 #iter1
40  #iter2
10  #iter3
3
0
0
#...

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

Пробная версия 2

Я хотел бы увидеть, при каком индексе массива ошибочно прогнозируется процессор.

int* func(){
  int* results;
  for(){
    papi_read(r1);
    if(arr[i])
        do_sth();   
    papi_read(r2);
    res[i] = r2-r1;
  }
  return res;
}

int main(){
   init_papi();
   for(int i = 0; i < 10; i++)
     res[i] = func();

   print(res[i]);
}

Ожидаемый результат:

#1st iteration, 0 means no mispred, 1 means mispred
1 0 0 1 1 0 0 0 1 1 0... # total of 200 results
Mispred: 100/200
#2nd iteration
0 0 0 0 1 0 0 0 1 0 0... # total of 200 results
Mispred: 40/200 # it learned from previous iteration
#3rd iteration
0 0 0 0 0 0 0 0 1 0 0... # total of 200 results
Mispred: 10/200 # continues to learn
#...

Полученный результат:

#1st iteration
1 0 0 1 1 0 0 0 1 1 0... # total of 200 results
Mispred: 100/200
#2nd iteration
1 0 0 0 1 1 0 1 0 0 0... # total of 200 results
Mispred: 100/200 # it DID NOT learn from previous iteration
#3rd iteration
0 1 0 1 0 1 0 1 1 0 0... # total of 200 results
Mispred: 100/200 # NO LEARNING
#...

Мои наблюдения

Когда я измеряю неверное прогнозирование кроме как для l oop, я вижу, что процессор учится на своих неправильных предсказаниях. Однако, когда я пытаюсь измерить неправильное прогнозирование инструкций для одной ветви, ЦП либо не может учиться, либо я измеряю это неправильно.

Мое объяснение

Я даю 200 как длина последовательности. Процессор имеет один небольшой предиктор ветвления, такой как 2-3-битный насыщенный счетчик в Intel, и один большой глобальный предиктор ветвления. Когда я измеряю за пределами l oop, я добавляю меньше шума к измерению. Под меньшим шумом я имею в виду papi звонки.

Подумайте об этом: за пределами l oop измерения

глобальная история: papi_start, branch_outcome1, branch_outcome2, branch_outcome3, ..., papi_end, papi_start (2nd loop of main iteration), branch_outcome1, ...

Итак предиктор ветви каким-то образом находит шаблон в той же ветви.

Однако, если я попытаюсь измерить инструкцию с одной ветвью, глобальная история будет выглядеть так: papi_start, branchoutcome1, papiend, papistart, branchoutcome2, papiend...

Итак, я представляю больше и больше веток в мировой истории. Я предполагаю, что глобальная история не может содержать много записей ветвления и, следовательно, не может найти какую-либо корреляцию / шаблон в требуемом операторе if (ветвь).

В результате

Мне нужно измерить результат предсказания одной ветви. Я знаю, что процессор может выучить шаблон 200, если я не буду слишком много вводить папи. Я посмотрел на вызовы папи и видел множество циклов for, если условия.

Вот почему мне нужно лучшее измерение. Я пробовал linux perf-event, но он делает ioctl вызовы, что является системным вызовом, и я загрязняю глобальную историю системными вызовами, и, следовательно, это не очень хорошее измерение.

Я читал, что rdpmc и rdmsr инструкции, и я предполагаю, что, поскольку они являются только инструкциями, я не буду загрязнять глобальную историю, и я могу измерять одну инструкцию за один раз.

Однако я понятия не имею, как я могу это сделать У меня процессор AMD 3600. Это ссылки, которые я нашел в Интернете, но я не мог понять, как это сделать. В дополнение к этому, я что-то упустил?

Intel rdpm c

Руководство по производительности AMD

Ответы [ 2 ]

6 голосов
/ 17 февраля 2020

Вы предположили, что код PAPI и / или perf_events имеет сравнительно небольшую площадь. Это неверно Если вы измените событие счетчика производительности на что-то вроде «инструкции устарели» или «Циклы ЦП не остановлены», вы сможете увидеть, сколько служебных данных содержит эта операция в вашей программной среде. Детали будут зависеть от вашей версии ОС, но я ожидаю, что издержки будут в сотнях команд / тысяч циклов из-за пересечения ядра, необходимого для чтения счетчиков в perf_events (который используется PAPI). Путь кода, безусловно, будет включать свои собственные ветви.

Если ваше ядро ​​поддерживает «Пользовательский режим RDPM C» (CR4.PCE = 1), вы можете прочитать счетчик производительности с помощью одной инструкции. Примеры доступны в https://github.com/jdmccalpin/low-overhead-timers.

Даже при ограничении кода измерения собственной инструкцией RDPM C (и окружающим кодом для сохранения результатов) измерения разрушают конвейер процессора. RDPM C - это микрокодированная инструкция. На ядре Ryzen инструкция выполняет 20 микроопераций и имеет пропускную способность одной инструкции на 20 циклов. (Ссылка: https://www.agner.org/optimize/instruction_tables.pdf)

Любые измерения с высокой степенью детализации сопряжены с трудностями, поскольку возможности современных процессоров в неупорядоченном состоянии взаимодействуют с пользовательским кодом способами, которые плохо документированы и сложны предвкушать. Дополнительные примечания по этой теме c (также относящиеся к процессорам AMD): http://sites.utexas.edu/jdm4372/2018/07/23/comments-on-timing-short-code-sections-on-intel-processors/

4 голосов
/ 17 февраля 2020

Документация perf_event_open() описывает, как правильно использовать rdpmc с событиями, созданными через этот интерфейс. Подход, описанный в ответе @ JohnDMcCalpin, также работает, но он основан на программировании регистров управления событиями напрямую. Учитывая набор аппаратных событий, выяснить, как запланировать эти события на доступных счетчиках производительности оборудования, может быть сложно. Подсистема perf_event решает эту проблему для вас, что является основным преимуществом.

Подсистема perf_event поддерживает rdpmc, начиная с Linux 3.4.

Начиная с <linux/perf_event.h>, следующие работы:

  1. до perf_event_open() для подготовки к считыванию счетчика type = PERF_TYPE_HARDWARE config = PERF_COUNT_HW_BRANCH_MISSES

    struct perf_event_attr attr ;
    int fd ;
    
    memset(&attr, 0, sizeof(attr)) ;
    
    attr.type   = PERF_TYPE_HARDWARE ;
    attr.config = PERF_COUNT_HW_BRANCH_MISSES;
    attr.size = sizeof(attr) ;        // for completeness
    attr.exclude_kernel = 1 ;         // count user-land events
    
    perf_fd = (int)sys_perf_event_open(&attr, 0, -1, -1, PERF_FLAG_FD_CLOEXEC) ;
                                      // this pid, any cpu, no group_fd
    

    где:

    static long
    sys_perf_event_open(struct perf_event_attr* attr,
                                  pid_t pid, int cpu, int group_fd, ulong flags)
    {
      return syscall(__NR_perf_event_open, attr, pid, cpu, group_fd, flags) ;
    }
    
  2. свяжите perf_fd со страницей mmap:

    struct perf_event_mmap_page* perf_mm ;
    
    perf_mm = mmap(NULL, page_size, PROT_READ, MAP_SHARED, perf_fd, 0) ;
    

    page_size может быть 4096, например. Этот буфер используется для хранения образцов. См. Раздел «Обработка переполнения» документации.

  3. , чтобы прочитать счетчик, необходимо объединить некоторую информацию в perf_mm с тем, что вы читаете, используя инструкцию RDPMC, таким образом:

    uint64_t  offset, count ;
    uint32_t  lock, check, a, d, idx ;
    
    lock = perf_mm->lock ;
    do
      {
        check = lock ;
        __asm__ volatile("":::"memory") ;
        idx = perf_mm->index - 1 ;
        // Check that you're allowed to execute rdpmc. You can do this check once.
        // Check also that the event is currently active.
        // Starting with Linux 3.12, use cap_user_rdpmc.
        if (perf_mm->cap_user_rdpmc && idx) {
           // cap_user_rdpmc cannot change at this point because no code
           // that executes here that changes it. So it's safe.
           __asm__ volatile("\t rdpmc\n" : "=a" (a), "=d" (d) : "c" (idx)) ;
        }
        // In case of signed event counts, you have to use also pmc_width.
        // See the docs.
         offset = perf_mm->offset ;
        __asm__ volatile("":::"memory") ;
        lock = perf_mm->lock ;
      }
    while (lock != check) ;
    
    count = ((uint64_t)d << 32) + a ;
    if (perf_mm->pmc_width != 64)
      {
        // need to sign extend the perf_mm->pmc_width bits of count.
      } ;
    count += offset ;
    

    Если поток не прерывается между чтениями "начало" и "конец", то я думаю, мы можем предположить, что материал perf_mm не изменится. Но если оно прервано, ядро ​​может обновить perf_mm материал, чтобы учесть любые изменения, влияющие на это время.

  4. Примечание: накладные расходы на инструкции RDPMC невелики , но я экспериментирую с удалением всего этого и выясняю, могу ли я использовать результаты RDPMC напрямую, при условии, что perf_mm->lock не изменится.

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