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

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

Правильно ли это, прежде всего, использовать среднее значение?

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

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

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


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

(редактор 2-й ноты: oops, ответ на вопрос, почему мы получаем отрицательные результаты. Стоит оставить примечание здесь как предупреждение не копировать эту реализацию 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 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;
}

Ответ 1

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

К лучшему или худшему; тогда у процессоров не было слишком много управления питанием, часто CPU работали с фиксированными "циклами в секунду" в любом случае. Некоторые программисты получили неправильную идею и неправильно использовали TSC для измерения времени, а не циклов. Позже (когда использование функций управления питанием стало более распространенным) эти люди злоупотребляли TSC, чтобы измерить время, скулящее по всем проблемам, вызванным их неправильным использованием. Производители процессоров (начиная с AMD) изменили TSC, поэтому он измеряет время, а не циклы (что делает его сломанным для измерения производительности кода, но верно для измерения пройденного времени). Это вызвало путаницу (программному обеспечению было сложно определить, что фактически измеряет TSC), поэтому немного позже AMD добавила флаг CPU TSC к 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) они могут знать, когда "elapsed = TSC - previous_TSC" дает действительный результат. Однако; "Идентификатор процессора", возвращаемый этой инструкцией, является просто значением в MSR, и ОС должна установить это значение для каждого CPU на что-то другое, иначе 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).

Ответ 2

  • не использовать значение avg

    Используйте самый маленький или средний размер меньших значений (чтобы получить avg из-за CACHE), потому что большие из них были прерваны многозадачной работой OS.

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

    enter image description here

  • не нужно измерять служебные данные RDTSC

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

  • для источника с переменной частотой RDTS (например, на ноутбуках)

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

Ответ 3

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

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

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

Окна:

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

Linux:

cpu_set_t cpuset;
CPU_ZERO(&cpuset);
CPU_SET(0, &cpuset);
sched_setaffinity (getpid(), sizeof(cpuset), &cpuset)

Ответ 4

Основная точка моего вопроса заключалась не в точности результата, а в том, что я получаю отрицательные значения время от времени (первый вызов rdstc дает большее значение, чем второй вызов). Сделав больше исследований (и прочитав другие вопросы на этом сайте), я узнал, что для работы с rdtsc можно использовать команду 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;
}

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

Ответ 5

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

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


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

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

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


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

rdtsc (insn ref manual entry) всегда создает две 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 операнда unsigned __int128, например, для множественного результата или для ввода div. Это вроде хак, потому что использование %0 в шаблоне asm только расширяется до низкого регистра, и нет предупреждения, когда "=A" не использует как регистры a и d.

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

Результирующий цикл (в синтаксисе Intel) выглядит так: от компиляции очищенной версии вашего кода %23include+ %23include+ static inline uint64_t get_cycles() {%0A++uint64_t t;%0A++//unsigned __int128 t;%0A++ //"=A%22+means RAX *or* RDX for uint64_t in x86-64, not both.%0A++__asm__ volatile (%22rdtsc++#+Compiler+picked %0%22+: "=A"(t));%0A++return t; } int num_measures = 10; int main() {%0A++ int tot=0%3B++++ %0A++ for(int i=0; iв проводнике компилятора Godbolt для 64-битного gcc и 32-битного clang:

# 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

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

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

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

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

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

Ответ 6

из википедии

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

Ответ 7

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

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

Ответ 8

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

Убедитесь, что вы остаетесь на одном ядре. У каждого ядра есть свой собственный tsc, который имеет свою ценность. Для использования rdtsc убедитесь, что вы либо taskset, либо SetThreadAffinityMask (windows) или pthread_setaffinity_np, чтобы ваш процесс оставался на одном ядре.

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

RDTSC
clock_gettime
спать 1 секунда
clock_gettime
RDTSC

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

Ответ 9

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

Ответ 10

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

В своем коде я делаю следующее:

if(before_t<after_t){ diff_t=before_t + 4294967296 -after_t;}