rdpmc: удивительное поведение

Я пытаюсь понять инструкцию rdpmc. Таким образом, у меня есть следующий код asm:

segment .text
global _start

_start:
    xor eax, eax
    mov ebx, 10
.loop:
    dec ebx
    jnz .loop

    mov ecx, 1<<30
    ; calling rdpmc with ecx = (1<<30) gives number of retired instructions
    rdpmc
    ; but only if you do a bizarre incantation: (Why u do dis Intel?)
    shl rdx, 32
    or  rax, rdx

    mov rdi, rax ; return number of instructions retired.
    mov eax, 60
    syscall

(Реализация является переводом rdpmc_instructions () .) Я считаю, что этот код должен выполнить 2 * ebx + 3 инструкции перед выполнением команды rdpmc , поэтому я ожидаю (в этом случае), что я должен получить состояние возврата 23.

Если я запускаю perf stat -e instruction:u ./a.out в этом двоичном perf stat -e instruction:u ./a.out , perf говорит мне, что я выполнил 30 инструкций, что выглядит правильно. Но если я выполняю двоичный файл, я получаю статус возврата 58 или 0, не детерминированный.

Что я здесь не так сделал?

Всего 2 ответа


Фиксированные счетчики не учитываются постоянно, только если программное обеспечение их разрешило. Обычно (сторона ядра) perf делает это вместе со сбросом их в ноль перед запуском программы.

Фиксированные счетчики (например, программируемые счетчики) имеют биты, которые управляют счетом пользователя, ядра или пользователя + ядра (т.е. всегда). Я предполагаю, что код ядра Linux perf оставляет их установленными для подсчета ни когда они не используются.

Если вы хотите использовать raw RDPMC самостоятельно, вам нужно либо запрограммировать / включить счетчики (путем установки соответствующих битов в IA32_PERF_GLOBAL_CTRL и IA32_FIXED_CTR_CTRL), либо получить perf, чтобы сделать это для вас, продолжая запускать программу под perf . например, perf stat ./a.out

Если вы используете perf stat -e instructions:u ./perf ; echo $? perf stat -e instructions:u ./perf ; echo $? фиксированный счетчик будет фактически обнуляться перед вводом вашего кода, поэтому вы получите непротиворечивые результаты при использовании rdpmc один раз. В противном случае, например, с помощью -e instructions по умолчанию (не: u) вы не знаете начальное значение счетчика. Это можно исправить, взяв дельту, прочитав счетчик один раз при запуске, а затем один раз после цикла.

Ширина выхода составляет всего 8 бит, так что этот небольшой взлом, чтобы избежать printf или write() работает только для очень маленьких подсчетов.

Это также означает, что бессмысленно создавать полный 64-битный результат rdpmc : старшие 32 бита входов не влияют на младшие 8 бит sub результата, потому что перенос распространяется только от низкого до высокого. В общем, если вы не рассчитываете на счет> 2 ^ 32, просто используйте результат EAX. Даже если необработанный 64-битный счетчик обернут в течение измеренного вами интервала, результат вычитания все равно будет правильным маленьким целым числом в 32-битном регистре.


Упрощено даже больше, чем в твоем вопросе. Также обратите внимание на отступы операндов, чтобы они могли оставаться в последовательном столбце даже для мнемоник длиннее 3 букв.

segment .text
global _start

_start:
    mov   ecx, 1<<30      ; fixed counter: instructions
    rdpmc
    mov   edi, eax        ; start

    mov   edx, 10
.loop:
    dec   edx
    jnz   .loop

    rdpmc               ; ecx = same counter as before

    sub   eax, edi       ; end - start

    mov   edi, eax
    mov   eax, 231
    syscall             ; sys_exit_group(rdpmc).  sys_exit isn't wrong, but glibc uses exit_group.

perf stat ./a.out это в соответствии с perf stat ./a.out или perf stat -e instructions:u ./a.out , мы всегда получаем 23 из echo $? ( instructions:u показывает 30, что на 1 больше, чем фактическое количество инструкций, выполняемых этой программой, включая syscall )

23 инструкции - это точно количество команд строго после первого rdpmc , но включая 2-й rdpmc .

Если мы закомментируем первый rdpmc и запустим его в соответствии с perf stat -e instructions:u , мы последовательно получим 26 качестве состояния выхода и 29 из perf . rdpmc - 24-я инструкция, которую нужно выполнить. (И RAX начинает инициализироваться нулем, потому что это статический исполняемый файл Linux, поэтому динамический компоновщик не запускался до _start ). Интересно, sysret ли sysret в ядре как «пользовательская» инструкция?

Но с первым rdpmc выполнение по perf stat -e instructions (не: u) дает произвольные значения, поскольку начальное значение счетчика не фиксировано. Таким образом, мы просто принимаем (некоторую произвольную начальную точку + 26) мод 256 в качестве состояния выхода.

Но обратите внимание, что RDPMC не является командой сериализации и может выполняться не по порядку. В общем, вам может понадобиться lfence или (как предлагает Джон МакКэлпин в связанной с вами теме) предоставление ECX ложной зависимости от результатов инструкций, которые вас интересуют. Например, and ecx, 0 / or ecx, 1<<30 работает, потому что в отличие от xor-zeroing, and ecx,0 не является нарушением зависимости.

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


PS для будущих читателей: один из способов включить пользовательское пространство RDPMC в Linux без каких-либо пользовательских модулей, помимо того, что требуется для perf задокументировано в perf_event_open(2) :

echo 2 | sudo tee /sys/devices/cpu/rdpmc    # enable RDPMC always, not just when a perf event is open

Первый шаг - убедиться, что счетчики производительности, которые вы хотите использовать, включены в IA32_PERF_GLOBAL_CTRL MSR IA32_PERF_GLOBAL_CTRL , расположение которого показано на рис. 18-8 тома Intel Manual Volume 3 (январь 2019 г.). Вы можете легко это сделать, загрузив модуль ядра MSR ( sudo modprobe msr ) и выполнив следующую команду:

sudo rdmsr -a 0x38F

Значение 0x38F является адресом IA32_PERF_GLOBAL_CTRL MSR IA32_PERF_GLOBAL_CTRL а опция -a указывает, что инструкция rdmsr должна выполняться на всех логических ядрах. По умолчанию это должно вывести 7000000ff (когда HT отключен) или 70000000f (когда HT включен) для всех логических ядер. Для INST_RETIRED.ANY фиксированной функцией INST_RETIRED.ANY бит в индексе 32 - это тот, который его включает, поэтому он должен быть равен 1. Значение 7000000ff что все три счетчика с фиксированной функцией и все восемь программируемых счетчиков включены ,

Регистр IA32_PERF_GLOBAL_CTRL имеет один бит разрешения для каждого счетчика производительности на логическое ядро. Каждый программируемый счетчик производительности также имеет свой собственный регистр управления и есть регистр управления для всех счетчиков с фиксированной функцией. В частности, управляющим регистром для INST_RETIRED.ANY фиксированных функций IA32_FIXED_CTR_CTRL является IA32_FIXED_CTR_CTRL , схема которого показана на рис. 18-7 тома Intel Manual Volume 3. В регистре 12 определенных битов, первые 4 бита могут быть используется для управления поведением первого счетчика с фиксированной функцией, т. е. INST_RETIRED.ANY (порядок показан в таблице 19-2). Перед изменением регистра вы должны сначала проверить, как он был инициализирован операционной системой, выполнив:

sudo rdmsr -a 0x38D

По умолчанию должно быть напечатано 0xb0. Это указывает на то, что второй счетчик с фиксированной функцией (циклы без остановок) включен и настроен для подсчета как в режиме супервизора, так и в режиме пользователя. Чтобы включить INST_RETIRED.ANY и настроить его на подсчет только событий пользовательского режима, сохраняя при этом счетчик циклов неинсталлированного ядра как есть, выполните следующую команду:

sudo wrmsr -a 0x38D 0xb2

Как только эта команда выполнена, события подсчитываются немедленно. Вы можете проверить это, прочитав первый счетчик фиксированных функций IA32_PERF_FIXED_CTR0 (см. Таблицу 19-2):

sudo rdmsr -a 0x309

Вы можете выполнить эту команду несколько раз и посмотреть, как меняются значения на каждом ядре. К сожалению, это означает, что к моменту запуска вашей программы текущее значение в IA32_PERF_FIXED_CTR0 будет в основном случайным значением. Вы можете попытаться сбросить счетчик, выполнив:

sudo wrmsr -a 0x309 0

Но основная проблема остается; вы не можете мгновенно сбросить счетчик и запустить вашу программу. Как указано в ответе @ Peter, правильный способ использования любого счетчика производительности - rdpmc область интереса между инструкциями rdpmc и принять разницу.

Модуль ядра MSR очень удобен, потому что единственный способ получить доступ к регистрам MSR - это режим ядра. Однако существует альтернатива rdpmc кода между инструкциями rdpmc . Вы можете написать свой собственный модуль ядра и поместить свой код в модуль ядра сразу после инструкции, которая включает счетчик. Вы даже можете отключить прерывания. Как правило, этот уровень точности не стоит усилий.

Вы можете использовать опцию -p вместо -a чтобы указать конкретное логическое ядро. Однако вам нужно убедиться, что программа запущена на том же ядре, что и taskset -c 3 ./a.out для запуска на ядре # 3.