Отрицательные измерения тактового цикла с последовательным rdtsc?
Я пишу код C для измерения количества тактов, необходимых для получения семафора. Я использую rdtsc, и перед выполнением измерения на семафоре я вызываю rdtsc два раза подряд, чтобы измерить издержки. Я повторяю это много раз в цикле for, а затем я использую среднее значение в качестве издержек rdtsc.
Правильно ли, прежде всего, использовать среднее значение?
Тем не менее, большая проблема здесь заключается в том, что иногда я получаю отрицательные значения для служебной информации (не обязательно усредненной, но по крайней мере частичной в цикле for).
Это также влияет на последовательный расчет числа циклов процессора, необходимых для sem_wait()
операция, которая иногда также оказывается отрицательной. Если то, что я написал, неясно, здесь есть часть кода, над которой я работаю.
Почему я получаю такие отрицательные значения?
(Примечание редактора: см. Получить счетчик циклов ЦП? для правильного и переносимого способа получения полной 64-битной метки времени. "=A"
Ограничение asm будет получать только младшие или старшие 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;
}
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
инструкция) они могут знать, когда "elapsed = TSC - previous_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).
не используйте среднее значение
Вместо этого используйте наименьшее или среднее значение из меньших значений (чтобы получить среднее значение из-за CACHE), потому что большие значения были прерваны многозадачностью ОС.
Вы также можете запомнить все значения, а затем найти границу гранулярности процесса ОС и отфильтровать все значения после этой границы (обычно>
1ms
что легко обнаружить)нет необходимости измерять накладные расходы
RDTSC
Вы просто измеряете смещение за некоторое время, и одинаковое смещение присутствует в обоих случаях, и после вычитания оно исчезает.
для переменного тактового источника
RDTS
(как на ноутбуках)Вы должны изменить скорость процессора до максимальной с помощью некоторого устойчивого интенсивного цикла вычислений, обычно достаточно нескольких секунд. Вы должны непрерывно измерять частоту процессора и начинать измерять свою вещь только тогда, когда она достаточно стабильна.
Другие ответы великолепны (прочитайте их), но предположите, что rdtsc
читается правильно. Этот ответ направлен на устранение ошибки inline-asm, которая приводит к совершенно фиктивным результатам, включая отрицательные.
Другая возможность состоит в том, что вы компилировали это как 32-битный код, но с большим количеством повторений, и иногда получали отрицательный интервал при миграции ЦП в системе, которая не имеет invariant-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
на скайлэйке). https://agner.org/optimize/ для таблиц инструкций.
Asm детали того, что пошло не так с "=A"
ограничение
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
в шаблоне asm расширяется только до нижнего регистра, и когда "=A"
не использует оба a
а также d
регистры.
Чтобы точно увидеть, как это вызывает проблемы, я добавил комментарий в шаблон asm: __asm__ volatile ("rdtsc # compiler picked %0" : "=A"(t));
, Таким образом, мы можем увидеть, что ожидает компилятор, основываясь на том, что мы сказали с операндами.
Результирующий цикл (в синтаксисе Intel) выглядит следующим образом: от компиляции очищенной версии вашего кода в проводнике компилятора 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
со 2-го rdtsc
потому что мы соврали компилятору о том, что делает оператор asm. 2-й rdtsc
затерт c1
Мы сказали ему, что у него есть выбор, какой из регистров получить для вывода, поэтому он выбрал один регистр в первый раз, а второй - во второй, так что он не понадобится. mov
инструкции.
TSC считает контрольные циклы с момента последней перезагрузки. Но код не зависит от hi<lo
, это просто зависит от знака hi-lo
, поскольку lo
переворачивается каждую секунду или две (2^32 Гц близко к 4,3 ГГц), при запуске программы в любой момент времени вероятность получения отрицательного результата составляет примерно 50%.
Это не зависит от текущей стоимости hi
; там может быть 1 часть 2^32
смещение в одном или другом направлении, потому что hi
изменяется на единицу, когда lo
оборачивается.
поскольку hi-lo
является почти равномерно распределенным 32-разрядным целым числом, переполнение среднего очень распространено. Ваш код в порядке, если среднее значение обычно мало. (Но посмотрите другие ответы, почему вам не нужно среднее значение; вы хотите получить медиану или что-то, чтобы исключить выбросы.)
Если ваш код запускается на одном процессоре, а затем переходит на другой, разница в метках времени может быть отрицательной из-за неактивности процессоров и т. Д.
Попробуйте установить сродство процессора, прежде чем начинать измерения.
Я не могу видеть, работаете ли вы под 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)
Принципиальным моментом моего вопроса была не точность результата, а тот факт, что я время от времени получаю отрицательные значения (первый вызов 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, это то, что я нашел в Интернете.
Перед лицом теплового и холостого дросселирования, движения мыши и сетевого трафика, независимо от того, что он делает с графическим процессором, и всех других накладных расходов, которые современная многоядерная система может поглотить без особой заботы, я думаю, что ваш единственный разумный путь для этого собрать несколько тысяч отдельных выборок и просто выбросить выбросы, прежде чем брать медиану или среднее (не статистик, но я рискну, что здесь не будет большой разницы).
Я думаю, что все, что вы сделаете, чтобы устранить шум работающей системы, будет искажать результаты гораздо хуже, чем просто признать, что вы никогда не сможете надежно предсказать, сколько времени потребуется, чтобы что-то завершить в эти дни.
Я проверил ваш код на моей машине и понял, что во время работы RDTSC разумно использовать только uint32_t.
Я делаю следующее в своем коде, чтобы исправить это:
if(before_t<after_t){ diff_t=before_t + 4294967296 -after_t;}
Если поток, выполняющий ваш код, перемещается между ядрами, возможно, возвращаемое значение rdtsc меньше значения, считанного в другом ядре. Ядро не все устанавливает счетчик на 0 в одно и то же время, когда пакет включается. Поэтому убедитесь, что вы установили привязку потоков к конкретному ядру при запуске теста.
rdtsc может использоваться для получения надежного и очень точного истекшего времени. Если вы используете Linux, вы можете увидеть, поддерживает ли ваш процессор tsc с постоянной скоростью, заглянув в /proc/cpuinfo, чтобы определить, определен ли у вас constant_tsc.
Убедитесь, что вы остаетесь на том же ядре. Каждое ядро имеет свою собственную TSC, которая имеет свою ценность. Чтобы использовать rdtsc, убедитесь, что вы используете либо taskset, либо SetThreadAffinityMask (windows), либо pthread_setaffinity_np, чтобы убедиться, что ваш процесс остается на том же ядре.
Затем вы делите это на основную тактовую частоту, которая в linux находится в /proc/cpuinfo, или вы можете сделать это во время выполнения
RDTSC
clock_gettime
спать на 1 секунду
clock_gettime
RDTSC
затем посмотрите, сколько тиков в секунду, и затем вы можете разделить любую разницу в тиках, чтобы узнать, сколько времени прошло.