Отрицательные измерения тактового цикла с последовательным rdtsc?

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

Правильно ли использовать в первую очередь среднее значение?

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

Это также влияет на последовательный подсчет количества циклов процессора, необходимого для операции sem_wait(), который иногда также оказывается отрицательным. Если то, что я написал, непонятно, вот часть кода, над которым я работаю.

Почему я получаю такие отрицательные значения?


(примечание редактора: см. Получить количество циклов ЦП? для правильного и переносимого способа получения полного 64-битная временная метка. Ассемблерное ограничение "=A" получит только младшие или старшие 32 бита при компиляции для x86-64, в зависимости от того, происходит ли выделение регистров для выбора RAX или RDX для вывода uint64_t. Оно не выберет edx:eax.)

(2-е примечание редактора: упс, это ответ на вопрос, почему мы получаем отрицательные результаты. Тем не менее стоит оставить здесь примечание в качестве предупреждения не копировать эту реализацию rdtsc.)


#include <semaphore.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <inttypes.h>

static inline uint64_t get_cycles()
{
  uint64_t t;
           // editor's note: "=A" is unsafe for this in x86-64
  __asm volatile ("rdtsc" : "=A"(t));
  return t;
}

int num_measures = 10;

int main ()
{
   int i, value, res1, res2;
   uint64_t c1, c2;
   int tsccost, tot, a;

   tot=0;    

   for(i=0; i<num_measures; i++)
   {    
      c1 = get_cycles();
      c2 = get_cycles();

      tsccost=(int)(c2-c1);


      if(tsccost<0)
      {
         printf("####  ERROR!!!   ");
         printf("rdtsc took %d clock cycles\n", tsccost);
         return 1;
      }   
      tot = tot+tsccost;
   }

   tsccost=tot/num_measures;
   printf("rdtsc takes on average: %d clock cycles\n", tsccost);      

   return EXIT_SUCCESS;
}

person Discipulus    schedule 12.11.2013    source источник
comment
_1_ проблематичен (или удивителен?) в GCC (gcc.gnu.org/bugzilla /show_bug.cgi?id=21249). Ограничение _2_ означает _3_ в x86_64, а не _4_. SHL _5_ на 32 и ИЛИ на _6_ или SHLD _7_ влево при сдвиге битов _8_ справа.   -  person nos    schedule 13.11.2013
comment
Брендан, отличный ответ. Можете ли вы добавить некоторые ссылки на него?   -  person Iwillnotexist Idonotexist    schedule 18.11.2013


Ответы (9)


Когда Intel впервые изобрела TSC, она измеряла циклы процессора. Из-за различных функций управления питанием «количество циклов в секунду» не является постоянным; поэтому TSC изначально был хорош для измерения производительности кода (и плохо для измерения прошедшего времени).

Для лучшего или худшего; в то время у процессоров не было слишком много управления питанием, часто процессоры все равно работали с фиксированным «циклом в секунду». Некоторые программисты ошиблись и неправильно использовали TSC для измерения времени, а не циклов. Позже (когда использование функций управления питанием стало более распространенным) эти люди, злоупотребляющие TSC для измерения времени, жаловались на все проблемы, вызванные их неправильным использованием. Производители процессоров (начиная с AMD) изменили TSC, чтобы он измерял время, а не циклы (что сделало его неработающим для измерения производительности кода, но правильным для измерения прошедшего времени). Это вызвало путаницу (программному обеспечению было трудно определить, что на самом деле измерял TSC), поэтому немного позже AMD добавила флаг «TSC Invariant» в CPUID, чтобы, если этот флаг установлен, программисты знали, что TSC не работает (для измерения циклы) или фиксированные (для измерения времени).

Intel последовала за AMD и изменила поведение своего TSC, чтобы также измерять время, а также приняла флаг AMD «TSC Invariant».

Это дает 4 разных случая:

  • TSC измеряет как время, так и производительность (количество циклов в секунду является постоянным)

  • TSC измеряет производительность, а не время

  • TSC измеряет время, а не производительность, но не использует для этого флаг «TSC Invariant».

  • TSC измеряет время, а не производительность, и для этого использует флаг «TSC Invariant» (большинство современных процессоров).

В случаях, когда TSC измеряет время, для правильного измерения производительности/циклов необходимо использовать счетчики мониторинга производительности. К сожалению, счетчики мониторинга производительности различаются для разных процессоров (в зависимости от модели) и требуют доступа к MSR (привилегированный код). Это делает непрактичным для приложений измерение «циклов».

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

Вторая проблема заключается в том, что для многопроцессорных систем большинство операционных систем отстой. Правильный способ для ОС обрабатывать TSC — запретить приложениям использовать его напрямую (путем установки флага TSD в CR4, чтобы инструкция RDTSC вызывала исключение). Это предотвращает различные уязвимости безопасности (временные побочные каналы). Это также позволяет ОС эмулировать TSC и гарантировать, что он возвращает правильный результат. Например, когда приложение использует инструкцию RDTSC и вызывает исключение, обработчик исключений ОС может вычислить правильную «глобальную метку времени» для возврата.

Конечно, у разных процессоров есть свои TSC. Это означает, что если приложение использует TSC напрямую, оно получает разные значения на разных процессорах. Чтобы помочь людям обойти неспособность ОС решить проблему (эмулируя RDTSC, как они должны); AMD добавила инструкцию RDTSCP, которая возвращает TSC и «идентификатор процессора» (Intel также приняла инструкцию RDTSCP). Приложение, работающее в сломанной ОС, может использовать «идентификатор процессора», чтобы определить, когда они работают на другом процессоре по сравнению с прошлым разом; и таким образом (используя инструкцию RDTSCP) они могут узнать, когда "прошедшее = TSC - предыдущее_TSC" дает неверный результат. Тем не мение; «Идентификатор процессора», возвращаемый этой инструкцией, является просто значением в MSR, и ОС должна установить это значение на каждом ЦП в какое-либо другое значение, иначе RDTSCP скажет, что «идентификатор процессора» равен нулю на всех ЦП.

В основном; если ЦП поддерживает инструкцию RDTSCP и если ОС правильно установила «идентификатор процессора» (используя MSR); тогда инструкция RDTSCP может помочь приложениям узнать, когда они получили плохой результат «прошедшего времени» (но она никоим образом не обеспечивает исправление или предотвращение плохого результата).

Так; Короче говоря, если вам нужно точное измерение производительности, вы в основном облажались. Лучшее, на что вы можете надеяться, это точное измерение времени; но только в некоторых случаях (например, при работе на машине с одним процессором или "прикреплении" к определенному процессору; или при использовании RDTSCP в ОС, которые настраивают его правильно, пока вы обнаруживаете и отбрасываете недопустимые значения).

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

Наконец, если вы действительно хотите сделать это правильно, вы должны измерить накладные расходы на измерение. Чтобы сделать это, вы должны измерить, сколько времени требуется, чтобы ничего не делать (только одна инструкция RDTSC/RDTSCP, отбрасывая сомнительные измерения); затем вычтите накладные расходы на измерение из результатов «измерения чего-либо». Это дает вам лучшую оценку времени, которое на самом деле занимает «что-то».

Примечание. Если вы сможете откопать копию Руководства по системному программированию Intel, выпущенного с момента первого выпуска Pentium (середина 1990-х годов — не уверен, что он доступен в Интернете — у меня есть архивные копии с 1980-х годов), вы обнаружите, что Intel задокументировала счетчик отметок времени как нечто, что «может использоваться для отслеживания и определения относительного времени возникновения событий процессора». Они гарантировали, что (исключая 64-битный цикл) он будет монотонно увеличиваться (но не что он будет увеличиваться с фиксированной скоростью) и что потребуется минимум 10 лет, прежде чем он завершится. В последней редакции руководства более подробно описан счетчик меток времени, в котором указано, что для более старых процессоров (P6, Pentium M, более старый Pentium 4) счетчик меток времени «увеличивается с каждым внутренним тактовым циклом процессора» и что «Intel(r) Переходы на технологию SpeedStep(r) могут повлиять на тактовую частоту процессора"; и что более новые процессоры (более новые Pentium 4, Core Solo, Core Duo, Core 2, Atom) TSC увеличиваются с постоянной скоростью (и что это «архитектурное поведение в будущем»). По сути, с самого начала это был (переменный) «внутренний счетчик циклов», который использовался для отметки времени (а не счетчик времени, который использовался для отслеживания времени «настенных часов»), и это поведение изменилось вскоре после 2000 год (на основании даты выпуска Pentium 4).

person Brendan    schedule 13.11.2013
comment
@Brendan: Фактически, в современных процессорах Intel TSC должен рассчитываться с одной и той же частотой независимо от тактовой частоты, состояния питания или используемого ядра. - person osgx; 04.02.2014
comment
Я бы сказал по-другому: AMD и Intel поняли, что высокоточный источник времени с низкими накладными расходами более полезен, чем счетчик циклов. В современных процессорах это могут делать аппаратные счетчики производительности, поэтому для этого вам не нужен _1_. И вы можете измерять события, отличные от циклов, для микробенчмаркинга. Также обратите внимание, что некоторые ранние процессоры TSC с постоянной скоростью останавливали TSC во время выполнения инструкции _2_, что делало его непригодным для использования в качестве источника времени. (В Linux /proc/cpuinfo отображается _3_ для ЦП без этой проблемы и _4_ для функции фиксированной скорости.) - person Nathan Fellman; 03.07.2014
comment
Кроме того, IIRC, rdtsc сообщает вам, с какой скоростью тикает TSC. и/или номинальная номинальная тактовая частота ЦП без турбонаддува, которую Intel использует для своего TSC. - person Peter Cordes; 03.03.2016
comment
Я думаю, люди, использующие его для измерения циклов, заметили, что T в CPUID означает Cycles, верно? - person Peter Cordes; 03.03.2016
comment
@MaximEgorushkin: Я полагаю, они заметили, что TS означает отметку времени (и что TSC не означает счетчик времени). Обратите внимание, что монотонно возрастающая отметка времени может быть реализована в программном обеспечении с чем-то вроде (например) rdtsc без какого-либо отношения к настенным часам. - person Maxim Egorushkin; 22.03.2017
comment
Мнение в ответе неверно, даже если факты верны. Не бесполезно иметь этот таймер. В настоящее время для эмуляторов чрезвычайно важно иметь часы с точностью до наносекунды и очень малой задержкой возврата результатов. PS. Кроме того, ни один процессор с 2013 года больше не делает этого иначе, поэтому нет смысла не считать его надежным методом, если вашей аудитории требуются быстрые процессоры. - person Brendan; 23.03.2017
comment
@Brendan, поэтому, если в cpuid установлен инвариантный флаг TSC, мы можем использовать его как время стены. Какую единицу времени (секунды, нс) мы должны принять в этом случае для времени, возвращаемого rdtsc? - person j riv; 22.09.2017
comment
Терминология: инвариантный TSC означает, что все ядра в пакете синхронизируют свои TSC (операция выборки TSC ЦП, особенно в многоядерной многопроцессорной среде говорит Nehalem и новее), поэтому миграция ЦП не вызывает проблем (за исключением, возможно, нескольких сокетов). Функция, о которой вы говорите, подсчет на опорной частоте независимо от турбо / энергосбережения, называется постоянным TSC. (И непрерывный TSC не останавливается, когда останавливается тактовая частота ядра.) - person abhi; 21.06.2018
comment
Подробнее об этом см. в разделе Операция выборки ЦП TSC, особенно в многоядерной многопроцессорной среде. По-видимому, в последних процессорах Intel синхронизированы TSC на всех ядрах в пакете, но с несколькими сокетами все еще могут быть проблемы. А про АМД не знаю. - person Peter Cordes; 19.08.2018

  1. не использовать среднее значение

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

    Вы также можете запомнить все значения, а затем найти границу детализации процесса ОС и отфильтровать все значения после этой границы (обычно> 1ms, что легко обнаружить)

    введите здесь описание изображения

  2. нет необходимости измерять накладные расходы RDTSC

    Вы просто измеряете со смещением на какое-то время, и одно и то же смещение присутствует в оба раза, а после вычитания оно исчезает.

  3. для переменного источника тактовой частоты RDTS (как на ноутбуках)

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

person Spektre    schedule 05.02.2014

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

Попробуйте установить привязку к процессору, прежде чем начинать измерения.

Из вопроса я не могу понять, работаете ли вы под Windows или Linux, поэтому я отвечу за оба.

Окна:

DWORD affinityMask = 0x00000001L;
SetProcessAffinityMask(GetCurrentProcessId(), affinityMask);

Линукс:

cpu_set_t cpuset;
CPU_ZERO(&cpuset);
CPU_SET(0, &cpuset);
sched_setaffinity (getpid(), sizeof(cpuset), &cpuset)
person Neil    schedule 05.02.2014
comment
Если вы компилируете для x86-64, возможно, первый rdtsc выбрал RAX, а второй rdtsc выбрал RDX в качестве выходных данных, потому что _1_ не делает то, что вы думали. (Таким образом, вы фактически будете сравнивать _2_ или _3_ после обработки регистров компилятора.) - person Peter Cordes; 19.08.2018

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

Другая возможность заключается в том, что вы компилировали это как 32-битный код, но с гораздо большим количеством повторений, и иногда получали отрицательный интервал при миграции ЦП в системе, в которой нет инвариантного TSC (синхронизированные TSC для всех ядер). Либо многосокетная система, либо более старая многоядерная. Операция выборки ЦП TSC, особенно в многоядерной многопроцессорной среде .


Если вы компилировали для x86-64, ваши отрицательные результаты полностью объясняются вашим неправильным выходным ограничением "=A" для asm. См. Получить количество циклов ЦП? для правильных способов использования rdtsc, переносимого на все компиляторы, и 32-битного режима против 64-битного. Или используйте выходы "=a" и "=d" и просто игнорируйте выход старшей половины для коротких интервалов, которые не переполнят 32 бита.)

(Я удивлен, что вы не упомянули, что они также огромны и сильно различаются, а также переполняются tot, чтобы дать отрицательное среднее значение, даже если ни одно из отдельных измерений не было отрицательным. Я вижу средние значения, такие как -63421899, или 69374170, или 115365476.)

Компиляция его с gcc -O3 -m32 заставляет его работать так, как ожидалось, печатая средние значения от 24 до 26 (если запускать в цикле, чтобы ЦП оставался на максимальной скорости, в противном случае, как 125 эталонных циклов для 24 тактовых циклов ядра между rdtsc на Skylake ). https://agner.org/optimize/ для таблиц инструкций.


Принципиальным моментом моего вопроса была не точность результата, а тот факт, что я время от времени получаю отрицательные значения (первый вызов rdstc дает большее значение, чем второй вызов). Проведя дополнительные исследования (и прочитав другие вопросы на этом веб-сайте), я обнаружил, что способ заставить все работать при использовании rdtsc — это поставить перед ним команду cpuid. Эта команда сериализует код. Вот как я сейчас делаю:

rdtsc (ввод вручную insn ref) всегда создает два 32- бит hi:lo составляет половину его 64-битного результата в edx:eax, даже в 64-битном режиме, где мы действительно предпочли бы иметь его в одном 64-битном регистре.

Вы ожидали, что выходное ограничение "=A" выберет edx:eax для uint64_t t. Но это не то, что происходит. Для переменной, которая помещается в один регистр, компилятор выбирает либо RAX, либо RDX и предполагает, что другой не изменен, точно так же, как выбирает ограничение "=r" один регистр и предполагает, что остальные не изменены. Или ограничение "=Q" выбирает одно из a, b, c или d. (См. ограничения x86).

В x86-64 вам обычно нужно только "=A" для операнда unsigned __int128, например, множественный результат или ввод div. Это своего рода хак, потому что использование %0 в ассемблерном шаблоне расширяется только до нижнего регистра, и нет предупреждения, когда "=A" не использует оба регистра a и d.

Чтобы увидеть, как именно это вызывает проблему, я добавил комментарий в ассемблерный шаблон:
__asm__ volatile ("rdtsc # compiler picked %0" : "=A"(t));. Таким образом, мы можем видеть, что ожидает компилятор, основываясь на том, что мы сказали ему операндами.

Когда компилятор вычисляет c2-c1, он фактически вычисляет hi-lo из второго rdtsc, потому что мы солгали компилятору о том, что делает оператор asm. 2-й rdtsc разгромил c1

# the main loop from gcc -O3  targeting x86-64, my comments added
.L6:
    rdtsc  # compiler picked rax     # c1 = rax
    rdtsc  # compiler picked rdx     # c2 = rdx, not realizing that rdtsc clobbers rax(c1)

      # compiler thinks   RAX=c1,               RDX=c2
      # actual situation: RAX=low half of c2,   RDX=high half of c2

    sub     edx, eax                 # tsccost = edx-eax
    js      .L3                      # jump if the sign-bit is set in tsccost
   ... rest of loop back to .L6

Мы сказали ему, что у него есть выбор, в какой регистр получить вывод, поэтому он выбрал один регистр в первый раз, а другой — во второй раз, поэтому ему не нужны никакие mov инструкции.

TSC подсчитывает эталонные циклы с момента последней перезагрузки. Но код не зависит от hi<lo, он зависит только от знака hi-lo. Поскольку lo повторяется каждую секунду или две (2 ^ 32 Гц близко к 4,3 ГГц), запуск программы в любой момент времени имеет примерно 50% шанс увидеть отрицательный результат.

Это не зависит от текущего значения hi; в 2^32 может быть 1 часть смещения в ту или иную сторону, потому что hi меняется на единицу, когда lo зацикливается.

Поскольку hi-lo представляет собой почти равномерно распределенное 32-битное целое число, переполнение среднего значения очень распространено. Ваш код в порядке, если среднее значение обычно невелико. (Но посмотрите другие ответы, почему вам не нужно среднее значение; вы хотите получить медиану или что-то еще, чтобы исключить выбросы.)

Подробности Asm о том, что пошло не так с ограничением "=A"

person Peter Cordes    schedule 19.08.2018

Я все еще получаю НЕГАТИВНУЮ разницу между вторым вызовом и первым вызовом функции get_cycles. ЗАЧЕМ? Я не уверен на 100% в синтаксисе встроенного кода сборки cpuid, это то, что я нашел в Интернете.

static inline uint64_t get_cycles()
{
  uint64_t t;          

   volatile int dont_remove __attribute__((unused));
   unsigned tmp;
     __asm volatile ("cpuid" : "=a"(tmp), "=b"(tmp), "=c"(tmp), "=d"(tmp)
       : "a" (0));

   dont_remove = tmp; 




  __asm volatile ("rdtsc" : "=A"(t));
  return t;
}

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

person Discipulus    schedule 13.11.2013
comment
Вы не отбрасываете выбросы, вы просто берете наименьшее значение из многих тысяч прогонов. Это правильно. - person Peter Cordes; 19.08.2018

Я думаю, что все, что вы делаете, чтобы устранить шум работающей системы, исказит результаты гораздо хуже, чем простое признание того, что вы никогда не сможете надежно предсказать, сколько времени это займет что-нибудь Чтобы завершить эти дни.

rdtsc можно использовать для получения надежного и очень точного прошедшего времени. Если вы используете linux, вы можете увидеть, поддерживает ли ваш процессор tsc с постоянной скоростью, заглянув в /proc/cpuinfo, чтобы узнать, определен ли у вас константный_tsc.

person jthill    schedule 12.11.2013
comment
tsc часто синхронизируются между ядрами одного и того же сокета и обычно могут быть синхронизированы по нескольким сокетам (stackoverflow.com/questions/10921210 На более новых процессорах (i7 Nehalem+ IIRC) TSC синхронизируется между всеми ядрами и работает с постоянной скоростью. ... Intel .. синхронизирует ядра и пакеты на материнской плате с несколькими сокетами). Вероятно, это делается ОС для получения глобального источника тактового сигнала высокого разрешения. - person Johan; 06.03.2016

Убедитесь, что вы остаетесь на том же ядре. Каждое ядро ​​имеет свой собственный tsc, который имеет свое значение. Чтобы использовать rdtsc, убедитесь, что у вас либо набор задач, либо SetThreadAffinityMask (Windows) или pthread_setaffinity_np, чтобы ваш процесс оставался на том же ядре.

Затем вы делите это на свою основную тактовую частоту, которую в Linux можно найти в /proc/cpuinfo, или вы можете сделать это во время выполнения с помощью

rdtsc
clock_gettime
переход в спящий режим на 1 секунду
clock_gettime
rdtsc

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

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

person Michael    schedule 01.12.2013

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

person BitTwiddler    schedule 18.03.2014
comment
Результирующий цикл (в синтаксисе Intel) выглядит следующим образом: компиляция очищенной версии вашего кода в обозревателе компиляторов Godbolt для 64-битного gcc и 32-битного clang: - person osgx; 12.03.2017

Я делаю следующее в своем коде, чтобы исправить это:

См. stackoverflow.com/questions/3388134/.

if(before_t<after_t){ diff_t=before_t + 4294967296 -after_t;}
person Zhu Guoliang    schedule 30.06.2015