RDTSCP по сравнению с RDTSC + CPUID

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

Затем я попытался:

  • RDTSC + CPUID (в обратном порядке, здесь), чтобы очистить конвейер, а - до 60x служебных (!) на виртуальной машине (моей рабочей среде) из-за гиперкалибровки и что "нет. Это происходит с поддержкой виртуализации HW и без нее.

  • Совсем недавно я столкнулся с инструкцией RDTSCP *, которая, похоже, делает то, что делает RDTSC + CPUID, но более эффективно, поскольку это более новая инструкция - только накладные расходы 1.5x-2x относительно.

Мой вопрос: действительно ли RDTSCP как точка измерения, и является ли это "правильным" способом выполнения времени?

Также, чтобы быть более ясным, мое время по существу похоже на это:

  • Сохранить значение счетчика текущего цикла
  • Выполните один тип тестов (например, диск, сеть)
  • Добавьте дельта текущего и предыдущего счетчиков циклов к значению аккумулятора и увеличьте счетчик на индивидуальное прерывание
  • В конце разделите дельта/аккумулятор на количество прерываний, чтобы получить среднюю стоимость цикла за прерывание.

* http://www.intel.de/content/dam/www/public/us/en/documents/white-papers/ia-32-ia-64-benchmark-code-execution-paper.pdf страница 27

Ответ 1

Полное обсуждение накладных расходов, которые вы видите из инструкции cpuid, доступно в fooobar.com/questions/18827/.... При использовании rdtsc вам нужно использовать cpuid, чтобы гарантировать отсутствие дополнительных инструкций в конвейере выполнения. Команда rdtscp сглаживает конвейер по существу. (Связанная нить SO также обсуждает эти основные моменты, но я обращался к ним здесь, потому что они тоже являются частью вашего вопроса).

Вам нужно "использовать" только cpuid + rdtsc, если ваш процессор не поддерживает rdtscp. В противном случае, rdtscp - это то, что вы хотите, и точно сообщите вам информацию, которая вам нужна.

Обе инструкции предоставляют вам 64-битный, монотонно увеличивающий счетчик, который представляет количество циклов на процессоре. Если это ваш шаблон:

uint64_t s, e;
s = rdtscp();
do_interrupt();
e = rdtscp();

atomic_add(e - s, &acc);
atomic_add(1, &counter);

В вашем среднем измерении все равно может быть один за другим, в зависимости от того, где происходит ваше чтение. Например:

   T1                              T2
t0 atomic_add(e - s, &acc);
t1                                 a = atomic_read(&acc);
t2                                 c = atomic_read(&counter);
t3 atomic_add(1, &counter);
t4                                 avg = a / c;

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

Побочных точки:

  • Если вы используете cpuid + rdtsc, вам нужно вычесть стоимость инструкции cpuid, которая может быть затруднена, если вы находитесь в виртуальной машине (в зависимости от того, как VM реализует эту инструкцию). Вот почему вы должны придерживаться rdtscp.
  • Выполнение rdtscp внутри цикла обычно плохое. Я несколько часто вижу микрообъективы, которые делают такие вещи, как

-

for (int i = 0; i < SOME_LARGEISH_NUMBER; i++) {
   s = rdtscp();
   loop_body();
   e = rdtscp();
   acc += e - s;
}

printf("%"PRIu64"\n", (acc / SOME_LARGEISH_NUMBER / CLOCK_SPEED));

Хотя это даст вам достойное представление об общей производительности в циклах всего, что находится в loop_body(), оно побеждает оптимизацию процессора, такую ​​как конвейерная обработка. В микрообъектах процессор будет очень хорошо работать с предсказанием ветвления в цикле, поэтому измерение накладных расходов на петлю прекрасное. Выполнение этого способа, показанного выше, также плохо, потому что вы получаете 2 конвейерных стойла на итерацию цикла. Таким образом:

s = rdtscp();
for (int i = 0; i < SOME_LARGEISH_NUMBER; i++) {
   loop_body();
}
e = rdtscp();
printf("%"PRIu64"\n", ((e-s) / SOME_LARGEISH_NUMBER / CLOCK_SPEED));

Будет более эффективным и, вероятно, более точным с точки зрения того, что вы увидите в реальной жизни, в сравнении с предыдущим этапом.

Ответ 2

Действительно ли RDTSCP является точным в качестве точки измерения, и является ли это "правильным" способом выполнения времени?

Современные процессоры x86 могут динамически настраивать частоту, чтобы экономить электроэнергию под тактовым управлением (например, Intel SpeedStep) и повышать производительность при большой нагрузке путем чрезмерной синхронизации (например, Intel Turbo Boost). Счетчик времени на этих современных процессорах, однако, рассчитывается с постоянной скоростью (например, найдите флаг "constant_tsc" в Linux/proc/cpuinfo).

Таким образом, ответ на ваш вопрос зависит от того, что вы действительно хотите знать. Если динамическое масштабирование частоты не отключено (например, в BIOS), счетчик временных меток больше не может рассчитываться для определения количества прошедших циклов. Тем не менее, счетчик временных меток все еще может рассчитываться, чтобы определить время, прошедшее с некоторой осторожностью, но я использую clock_gettime в C - см. Конец моего ответа).

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

Позвольте представить три разных метода для определения количества прошедших циклов.

  • Отключить масштабирование динамической частоты в BIOS и использовать счетчик времени.
  • Для процессоров Intel запросите core clock cycles из счетчика монитора производительности.
  • Измерьте частоту под нагрузкой.

Первый способ является самым надежным, но он требует доступа к BIOS и влияет на производительность всего остального, что вы запускаете (когда я отключил динамическое масштабирование частоты на моем i5-4250U, он работает на постоянной 1,3 ГГц вместо базы 2,6 ГГц). Также неудобно менять BIOS только для бенчмаркинга.

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

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

Вот как я определяю время, прошедшее (в секундах) с C.

#define TIMER_TYPE CLOCK_REALTIME

timespec time1, time2;
clock_gettime(TIMER_TYPE, &time1);
foo();
clock_gettime(TIMER_TYPE, &time2);
double dtime = time_diff(time1,time2);

double time_diff(timespec start, timespec end)
{
    timespec temp;
    if ((end.tv_nsec-start.tv_nsec)<0) {
        temp.tv_sec = end.tv_sec-start.tv_sec-1;
        temp.tv_nsec = 1000000000+end.tv_nsec-start.tv_nsec;
    } else {
        temp.tv_sec = end.tv_sec-start.tv_sec;
        temp.tv_nsec = end.tv_nsec-start.tv_nsec;
    }
    return (double)temp.tv_sec +  (double)temp.tv_nsec*1E-9;
}

Ответ 3

Следующий код гарантирует, что rdstcp срабатывает точно в нужное время. RDTSCP не может выполняться слишком рано, но может выполнять до конца, потому что CPU может перемещать инструкции после RDTSCP для выполнения перед ним.

Чтобы предотвратить это, мы создаем цепочку ложных зависимостей, основанную на том, что rdstcp помещает свой вывод в edx: eax

rdtscp       ;rdstcp is read serialized, it will not execute too early.
;also ensure it does not execute too late
mov r8,rdx   ;rdtscp changes rdx and rax, force dependency chain on rdx
xor r8,rbx   ;push rbx, do not allow push rbx to execute OoO
xor rbx,rdx  ;rbx=r8
xor rbx,r8   ;rbx = 0
push rdx
push rax
mov rax,rbx  ;rax = 0, but in a way that excludes OoO execution.
cpuid
pop rax
pop rdx
mov rbx,r8
xor rbx,rdx  ;restore rbx

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

Ответ 4

Документ Intel 2010 года Как определить время выполнения кода в архитектурах наборов команд Intel® IA-32 и IA-64 можно считать устаревшим, когда речь идет о рекомендациях по объединению RDTSC/RDTSCP с CPUID.

Текущая справочная документация Intel рекомендует инструкции по фехтованию как более эффективные альтернативы CPUID:

Обратите внимание, что инструкции SFENCE, LFENCE и MFENCE обеспечивают более эффективный метод управления памятью. порядок, чем инструкция CPUID.

(Руководство по разработке программного обеспечения для архитектуры Intel® 64 и IA-32: том 3, раздел 8.2.5, сентябрь 2016 г.)

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

(Intel RDTSC)

Таким образом, чтобы получить начальное значение TSC, вы выполните следующую последовательность команд:

mfence
lfence
rdtsc
shl     rdx, 0x20
or      rax, rdx

В конце теста для получения значения остановки TSC:

rdtscp
lfence
shl     rdx, 0x20
or      rax, rdx

Обратите внимание, что в отличие от CPUID, команда lfence не затирает какие-либо регистры, поэтому нет необходимости спасать регистры EDX:EAX перед выполнением команды сериализации.

Соответствующий фрагмент документации:

Если программное обеспечение требует выполнения RDTSCP до выполнения любой последующей инструкции (включая любые обращения к памяти), оно может выполнить LFENCE сразу после RDTSCP (Intel RDTSCP)

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