RDTSCP в NASM всегда возвращает одно и то же значение

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

Я прочитал "Как оценить время выполнения кода на архитектурах наборов инструкций Intel IA-32 и IA-64" Габриэле Паолони (Intel, сентябрь 2010 г.) и другие веб-ресурсы (большинство из которых были примерами на C).

Используя приведенный ниже код (перевод с C), я тестирую различные инструкции, но RDTSCP всегда возвращает ноль в RDX и 7 в RAX. Сначала я подумал, что 7 - это количество циклов, но, очевидно, не все инструкции занимают 7 циклов.

rdtsc
cpuid
addsd xmm14,xmm1 ; Instruction to time
rdtscp
cpuid

Это возвращает 7, что неудивительно, потому что на некоторых архитектурах добавление составляет 7 циклов с включенной задержкой. Первые две инструкции могут (по некоторым данным) быть обратными, сначала cpuid, а затем rdtsc, но здесь это не имеет значения.

Когда я меняю инструкцию на инструкцию с 2 циклами:

rdtsc
cpuid
add rcx,rdx ; Instruction to time
rdtscp
cpuid

Это также возвращает 7 в rax и ноль в rdx.

Итак, мои вопросы:

  1. Как получить доступ и интерпретировать значения, возвращенные в RDX:RAX?

  2. Почему RDX всегда возвращает ноль и что он должен возвращать?

ОБНОВИТЬ:

Если я изменю код на это:

cpuid
rdtsc
mov [start_time],rax
addsd xmm14,xmm1 ; INSTRUCTION
rdtscp
mov [end_time],rax
cpuid
mov rax,[end_time]
mov rdx,[start_time]
sub rax,rdx

Я получаю 64 в Rax, но это звучит как слишком много циклов.

1 ответ

Решение

Ваш первый код (ведущий к заглавному вопросу) содержит ошибки, потому что он перезаписывает rdtsc а также rdtscp результаты с cpuid результаты в EAX,EBX,ECX и EDX.

использование lfence вместо cpuid; на Intel с незапамятных времен и AMD с включенным смягчением Spectre, lfence будет сериализовать поток инструкций и таким образом делать то, что вы хотите с rdtsc,


Помните, что RDTSC считает контрольные циклы, а не такты ядра. Получить количество тактов процессора? для этого и больше о RDTSC.

У вас нет cpuid или же lfence внутри вашего интервала измерения. Но у вас есть rdtscp Сам в интервале измерений. Спина к спине rdtscp не быстрый, 64 референсных цикла звучат вполне разумно, если вы работали без прогрева процессора. Частота холостого хода обычно намного ниже, чем у эталонного цикла; 1 эталонный цикл равен или близок к частоте "стикера", например, максимальная частота без поддержки турбо, на процессорах Intel. например, 4008 МГц на 4 ГГц процессоре Skylake.


Это не то, как вы рассчитываете одну инструкцию

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

Тем не менее, вы можете попытаться вычесть накладные расходы на измерения. например, clflush для аннулирования строки кэша с помощью функции C. А также см. Также: Использование счетчика меток времени и clock_gettime для пропуска кэша и измерения задержки памяти со счетчиком меток времени.


Это то, что я обычно использую, чтобы профилировать задержку или пропускную способность (и uops слитый и неиспользованный домен) инструкции короткого блока. Отрегулируйте, как вы используете его, чтобы ограничить задержку, как здесь, или нет, если вы хотите просто проверить пропускную способность. например, с %rep блокировать достаточно разных регистров, чтобы скрыть задержку, или разорвать цепочки зависимостей с помощью pxor xmm3, xmm3 после короткого блока и исполнил exec-порядка exec работать свое волшебство. (До тех пор, пока у вас нет узкого места на переднем конце.)

Возможно, вы захотите использовать пакет smartalign NASM или YASM, чтобы избежать стены однобайтовых инструкций NOP для директивы ALIGN. NASM по умолчанию использует действительно тупые NOP даже в 64-битном режиме, где всегда поддерживается long-NOP.

global _start
_start:
    mov   ecx, 1000000000
; linux static executables start with XMM0..15 already zeroed
align 32                     ; just for good measure to avoid uop-cache effects
.loop:
    ;; LOOP BODY, put whatever you want to time in here
    times 4   addsd  xmm4, xmm3

    dec   ecx
    jnz   .loop

    mov  eax, 231
    xor  edi, edi
    syscall          ; x86-64 Linux sys_exit_group(0)

Запустите это с чем-то вроде этой строки, которая связывает его в статический исполняемый файл и профилирует его perf stat, который вы можете стрелять вверх и перезапускать каждый раз, когда вы меняете источник:

(Я на самом деле помещаю nasm+ld + необязательный дизассемблер в скрипт asm-link, чтобы сохранить ввод, когда я не профилирую. Разборка гарантирует, что в вашем цикле есть то, что вы хотели профилировать, особенно если у вас есть некоторые %if вещи в вашем коде. И так же, это на вашем терминале прямо перед профилем, если вы хотите прокрутить назад, тестируя теории в своей голове.)

t=testloop; nasm -felf64 -g "$t.asm" && ld "$t.o" -o "$t" &&  objdump -drwC -Mintel "$t" &&
 taskset -c 3 perf stat -etask-clock,context-switches,cpu-migrations,page-faults,cycles,branches,instructions,uops_issued.any,uops_executed.thread -r4 ./"$t"

Результат от i7-6700k при 3,9 ГГц (текущий perf имеет ошибку отображения единиц измерения для вторичного столбца. Это исправлено, но Arch Linux еще не обновился.):

 Performance counter stats for './testloop' (4 runs):

          4,106.09 msec task-clock                #    1.000 CPUs utilized            ( +-  0.01% )
                17      context-switches          #    4.080 M/sec                    ( +-  5.65% )
                 0      cpu-migrations            #    0.000 K/sec                  
                 2      page-faults               #    0.487 M/sec                  
    16,012,778,144      cycles                    # 3900323.504 GHz                   ( +-  0.01% )
     1,001,537,894      branches                  # 243950284.862 M/sec               ( +-  0.00% )
     6,008,071,198      instructions              #    0.38  insn per cycle           ( +-  0.00% )
     5,013,366,769      uops_issued.any           # 1221134275.667 M/sec              ( +-  0.01% )
     5,013,217,655      uops_executed.thread      # 1221097955.182 M/sec              ( +-  0.01% )

          4.106283 +- 0.000536 seconds time elapsed  ( +-  0.01% )

На моем i7-6700k (Skylake), addsd имеет задержку 4 цикла, пропускную способность 0,5 с. (т.е. 2 за час, если задержка не была узким местом). См. https://agner.org/optimize/, https://uops.info/ и http://instlatx64.atw.hu/.

16 циклов на ветвь = 16 циклов на цепочку из 4 addsd = 4 цикла задержки для addsd , воспроизводя измерение Agner Fog 4 циклов с точностью лучше, чем 1 часть на 100, даже для этого теста, который включает в себя незначительные накладные расходы при запуске и служебные сигналы прерывания.

Выберите различные счетчики для записи. Добавление :u, лайк instructions:u в perf даже будут учитываться только инструкции пользовательского пространства, исключая те, которые выполнялись во время обработчиков прерываний. Обычно я этого не делаю, поэтому я могу рассматривать эти накладные расходы как часть объяснения времени настенных часов. Но если ты это сделаешь, cycles:u может очень близко соответствовать instructions:u,

-r4 запускает его 4 раза и усредняет, что может быть полезно, чтобы увидеть, есть ли много вариаций между прогонами вместо того, чтобы просто получить одно среднее из более высокого значения в ECX.

Отрегулируйте исходное значение ECX, чтобы общее время составляло от 0,1 до 1 секунды, что обычно достаточно, особенно если ваш процессор очень быстро разгоняется до макс. Турбо (например, Skylake с аппаратными P-состояниями и довольно агрессивным energy_performance_preference). Или макс нетурбо с турбонаддувом.

Но это учитывается в тактах ядра, а не в эталонных циклах, поэтому он все равно дает один и тот же результат независимо от изменений частоты процессора. (+- некоторый шум от остановки часов во время перехода.)

Другие вопросы по тегам