Я видел этот пост на SO, который содержит код C, чтобы получить последний счетчик циклов ЦП:
Профилирование на основе подсчета циклов процессора в C / C ++ Linux x86_64
Есть ли способ, которым я могу использовать этот код в C ++ (приветствуются решения для Windows и Linux)? Хотя написано на C (а C является подмножеством C ++), я не слишком уверен, будет ли этот код работать в проекте C ++, а если нет, как его перевести?
Я использую x86-64
EDIT2:
Нашел эту функцию, но не может заставить VS2010 распознать ассемблер. Нужно ли что-нибудь включать? (Я считаю, что я должен поменяться uint64_t
в long long
для windows ….?)
static inline uint64_t get_cycles()
{
uint64_t t;
__asm volatile ("rdtsc" : "=A"(t));
return t;
}
EDIT3:
Из приведенного выше кода я получаю ошибку:
«ошибка C2400: синтаксическая ошибка встроенного ассемблера в ‘код операции’; найдены» данные
тип'»
Может ли кто-нибудь помочь, пожалуйста?
Начиная с GCC 4.5 и позже, __rdtsc()
intrinsic теперь поддерживается как MSVC, так и GCC.
Но включение, которое необходимо, отличается:
#ifdef _WIN32
#include <intrin.h>
#else
#include <x86intrin.h>
#endif
Вот оригинальный ответ перед GCC 4.5.
Вытащил прямо из одного из моих проектов:
#include <stdint.h>
// Windows
#ifdef _WIN32
#include <intrin.h>
uint64_t rdtsc(){
return __rdtsc();
}
// Linux/GCC
#else
uint64_t rdtsc(){
unsigned int lo,hi;
__asm__ __volatile__ ("rdtsc" : "=a" (lo), "=d" (hi));
return ((uint64_t)hi << 32) | lo;
}
#endif
VC ++ использует совершенно другой синтаксис для встроенной сборки — но только в 32-битных версиях. 64-битный компилятор вообще не поддерживает встроенную сборку.
В этом случае это, вероятно, также хорошо — rdtsc
имеет (по крайней мере) две основные проблемы, когда речь идет о временных последовательностях кода. Во-первых (как и большинство инструкций) он может быть выполнен не по порядку, поэтому, если вы пытаетесь определить время короткой последовательности кода, rdtsc
до и после того, как этот код может выполняться как до него, так и после него, или что у вас есть (я уверен, что эти два всегда будут выполняться по порядку относительно друг друга, так что, по крайней мере, разница никогда не будет отрицательной) ,
Во-вторых, в многоядерной (или многопроцессорной) системе один rdtsc может выполняться на одном ядре / процессоре, а другой — на другом ядре / процессоре. В таком случае отрицательный результат является вполне возможно.
Вообще говоря, если вы хотите точный таймер под Windows, вам будет лучше использовать QueryPerformanceCounter
,
Если вы действительно настаиваете на использовании rdtsc
Я считаю, что вам придется сделать это в отдельном модуле, полностью написанном на языке ассемблера (или использовать встроенный компилятор), а затем связать его с вашим C или C ++. Я никогда не писал этот код для 64-битного режима, но в 32-битном режиме он выглядит примерно так:
xor eax, eax
cpuid
xor eax, eax
cpuid
xor eax, eax
cpuid
rdtsc
; save eax, edx
; code you're going to time goes here
xor eax, eax
cpuid
rdtsc
Я знаю, это выглядит странно, но на самом деле это правильно. Вы выполняете CPUID, потому что это инструкция сериализации (не может быть выполнена не по порядку) и доступна в пользовательском режиме. Вы выполняете его три раза, прежде чем начать отсчет времени, потому что Intel документирует тот факт, что первое выполнение может / будет выполняться с другой скоростью, чем второе (и они рекомендуют три, так что три это).
Затем вы выполняете тестируемый код, еще один cpuid для принудительной сериализации и последний rdtsc, чтобы получить время после завершения кода.
Наряду с этим вы хотите использовать любые средства, которые поставляет ваша ОС, чтобы заставить все это работать на одном процессе / ядре. В большинстве случаев также требуется принудительное выравнивание кода — изменения в выравнивании могут привести к довольно существенным различиям в скорости выполнения.
Наконец, вы хотите выполнить его несколько раз — и всегда возможно, что он будет прерван в середине процесса (например, переключение задач), поэтому вам нужно быть готовым к тому, что выполнение может занять совсем немного времени. дольше, чем остальные — например, 5 запусков, которые занимают ~ 40-43 тактовых цикла, а шестой — 10000+ тактов. Понятно, что в последнем случае вы просто выбрасываете выброс — это не из вашего кода.
Резюме: управление выполнением самой инструкции rdtsc — это (почти) ваше наименьшее беспокойство. Там немного больше вас необходимость сделать, прежде чем вы сможете получить результаты от rdtsc
это на самом деле будет означать что угодно.
Вам не нужен встроенный ассемблер для этого. Там нет никакой выгоды; компиляторы имеют встроенные модули для rdtsc
а также rdtscp
и (по крайней мере, в наши дни) все определяют __rdtsc
свойственный, если вы включите правильные заголовки. Но в отличие от почти всех других случаев (https://gcc.gnu.org/wiki/DontUseInlineAsm), нет никаких серьезных недостатков в asm, до тех пор, пока вы используете хорошую и безопасную реализацию, такую как @ Mysticial’s, не один с сломан "=A"
ограничение.
К сожалению, MSVC не согласен со всеми остальными в отношении того, какой заголовок использовать для не-SIMD-функций.
Руководство Intel по внутренним технологиям говорит _rdtsc
(с одним подчеркиванием) находится в <immintrin.h>
, но это не работает на gcc и clang. Они определяют SIMD только в <immintrin.h>
так что мы застряли с <intrin.h>
(MSVC) против <x86intrin.h>
(все остальное, в том числе недавний МУС). Для совместимости с MSVC и документацией Intel gcc и clang определяют версии функции с одним или двумя подчеркиваниями.
Интересный факт: версия с двойным подчеркиванием возвращает 64-разрядное целое число без знака, а документы Intel _rdtsc()
как возвращающийся (подписанный) __int64
,
// valid C99 and C++
#include <stdint.h> // <cstdint> is preferred in C++, but stdint.h works.
#ifdef _MSC_VER
# include <intrin.h>
#else
# include <x86intrin.h>
#endif
// optional wrapper if you don't want to just use __rdtsc() everywhere
inline
uint64_t readTSC() {
// _mm_lfence(); // optionally wait for earlier insns to retire before reading the clock
uint64_t tsc = __rdtsc();
// _mm_lfence(); // optionally block later instructions until rdtsc retires
return tsc;
}
// requires a Nehalem or newer CPU. Not Core2 or earlier. IDK when AMD added it.
inline
uint64_t readTSCp() {
unsigned dummy;
return __rdtscp(&dummy); // waits for earlier insns to retire, but allows later to start
}
Компилируется со всеми 4 основными компиляторами: gcc / clang / ICC / MSVC, для 32 или 64-битных. Увидеть результаты в проводнике компилятора Godbolt, в том числе пара тестовых звонков.
Эти свойства были новыми в gcc4.5 (с 2010 года) и clang3.5 (с 2014 года). gcc4.4 и clang 3.4 на Godbolt не компилируют это, но gcc4.5.3 (апрель 2011) делает. Вы можете увидеть встроенный asm в старом коде, но вы можете и должны заменить его на __rdtsc()
, Компиляторы старше десяти лет обычно делают код медленнее, чем gcc6, gcc7 или gcc8, и имеют менее полезные сообщения об ошибках.
Встроенный MSVC (я думаю) просуществовал гораздо дольше, потому что MSVC никогда не поддерживал встроенный asm для x86-64. ICC13 имеет __rdtsc
в immintrin.h
, но не имеет x86intrin.h
совсем. Более поздние МУС имеют x86intrin.h
По крайней мере, то, как Godbolt устанавливает их для Linux, они делают.
Вы можете определить их как подписанные long long
, особенно если вы хотите вычесть их и преобразовать в число с плавающей точкой. int64_t
-> float / double более эффективен, чем uint64_t
на x86 без AVX512. Кроме того, небольшие отрицательные результаты могут быть возможны из-за миграций ЦП, если TSC не синхронизированы идеально, и это, вероятно, имеет больше смысла, чем огромные числа без знака.
Кстати, Clang также имеет портативный __builtin_readcyclecounter()
который работает на любой архитектуре. (Всегда возвращает ноль на архитектурах без счетчика цикла.) См. документы по расширению языка clang / LLVM
Подробнее о с помощью lfence
(или же cpuid
) улучшить повторяемость rdtsc
и контролировать, какие именно инструкции находятся / не находятся во временном интервале, блокируя неправильное выполнение, смотрите ответ @HadiBrais на clflush для аннулирования строки кэша через функцию C и комментарии для примера различий, которые это делает.
Смотрите также Работает ли LFENCE на процессорах AMD? (TL: DR да с включенным смягчением Спектра, в противном случае ядра оставляют соответствующий MSR не установленным, поэтому вам следует cpuid
для сериализации.) Это всегда определялось как частично-сериализация на Intel.
Как сравнить время выполнения кода на Intel® IA-32 и IA-64
Архитектура набора команд, технический документ Intel от 2010 года.
rdtsc
счетчики ссылка циклы, а не тактовые частоты ядра процессораОн рассчитывает на фиксированную частоту независимо от турбонагнетателя / энергосбережения, поэтому, если вы хотите выполнить анализ числа операций в такт, используйте счетчики производительности. rdtsc
точно соотносится со временем настенных часов (за исключением настроек системных часов, так что это идеальный источник времени для steady_clock
). Это соответствует номинальной частоте процессора, то есть объявленной частоте наклеек. (Или же около тот. например 2592 МГц на i7-6700HQ 2,6 ГГц Skylake.)
Если вы используете его для микробенчмаркинга, сначала включите период прогрева, чтобы убедиться, что ваш процессор уже работает на максимальной тактовой частоте, прежде чем начинать синхронизацию. (И дополнительно отключите turbo и скажите, чтобы ваша ОС предпочитала максимальную тактовую частоту, чтобы избежать сдвигов частоты процессора во время вашего микробенчмарка). Или лучше использовать библиотеку, которая дает вам доступ к аппаратным счетчикам производительности, или трюк, подобный показатель производительности для части программы если ваш временной регион достаточно длинный, чтобы вы могли прикрепить perf stat -p PID
,
Тем не менее, обычно вам все еще нужно фиксировать тактовую частоту ЦП для микробенчмарков, если только вы не хотите увидеть, как различные нагрузки заставят Skylake замедлять работу при ограничении памяти или что-то еще. (Обратите внимание, что пропускная способность / задержка памяти в основном фиксированы, с использованием тактовых импульсов, отличных от ядер. На тактовой частоте простоя, потеря кэша L2 или L3 занимает намного меньше тактовых циклов ядра.)
constant_tsc
), который не останавливается, когда часы останавливаются (nonstop_tsc
). Также несколько советов, например, не берите среднее время, берите медиану (будут очень высокие выбросы).cli
) и виртуализация rdtsc
под ВМ. И, конечно, возможны базовые вещи, такие как регулярные прерывания, поэтому повторяйте время много раз и отбрасывайте выбросы.Определить частоту TSC в Linux. Программно запрашивать частоту TSC сложно и, возможно, невозможно, особенно в пространстве пользователя, или может дать худший результат, чем калибровка. Калибровка с использованием другого известного источника времени требует времени. См. Этот вопрос, чтобы узнать, насколько сложно преобразовать TSC в наносекунды (и было бы неплохо, если бы вы спросили ОС, каков коэффициент преобразования, потому что ОС уже сделала это при загрузке).
Если вы используете микробенчмаркинг с RDTSC для настройки, лучше всего использовать тики и пропустить даже попытки конвертировать в наносекунды. В противном случае используйте функцию времени библиотеки высокого разрешения, такую как std::chrono
или же clock_gettime
, Увидеть более быстрый эквивалент gettimeofday для некоторого обсуждения / сравнения функций временной метки или чтения общей временной метки из памяти, чтобы избежать rdtsc
полностью, если ваши требования к точности достаточно низки для прерывания таймера или потока для его обновления.
Смотрите также Рассчитать системное время, используя rdtsc о поиске частоты кристалла и множителя.
Также не гарантируется, что TSC всех ядер синхронизированы. Так что если ваш поток мигрирует на другое ядро процессора между __rdtsc()
, может быть дополнительный перекос. (Однако большинство ОС пытаются синхронизировать TSC всех ядер, поэтому обычно они будут очень близки.) Если вы используете rdtsc
непосредственно, вы, вероятно, хотите прикрепить вашу программу или поток к ядру, например, с taskset -c 0 ./myprogram
в линуксе
Операция выборки TSC процессора, особенно в многоядерной и многопроцессорной среде Говорит, что Nehalem и новее имеют синхронизированный TSC и заблокированы вместе для всех ядер в пакете (то есть инвариант TSC). Но много сокетные системы все еще могут быть проблемой. Даже в более старых системах (как до Core2 2007 года) может быть TSC, который останавливается при остановке тактовой частоты ядра или привязан к фактической тактовой частоте ядра вместо эталонных циклов. (Более новые процессоры всегда имеют постоянный TSC и нон-стоп-TSC.) Более подробную информацию смотрите в ответе @ amdn на этот вопрос.
Это примерно так же хорошо, как вы могли бы получить из встроенного ассемблера GNU C @ Mysticial, или лучше, потому что он знает, что старшие биты RAX обнуляются. Основная причина, по которой вы хотите сохранить встроенный asm, заключается в том, что вы работаете со старыми компиляторами.
Не встроенная версия readTSC
Сама функция компилируется с MSVC для x86-64 следующим образом:
unsigned __int64 readTSC(void) PROC ; readTSC
rdtsc
shl rdx, 32 ; 00000020H
or rax, rdx
ret 0
; return in RAX
Для 32-битных соглашений о вызовах, которые возвращают 64-битные целые числа в edx:eax
, это просто rdtsc
/ret
, Не то чтобы это важно, вы всегда хотите, чтобы это было встроено.
В тестовом вызове, который использует его дважды и вычитает интервал времени:
uint64_t time_something() {
uint64_t start = readTSC();
// even when empty, back-to-back __rdtsc() don't optimize away
return readTSC() - start;
}
Все 4 компилятора делают довольно похожий код. Это 32-битный вывод GCC:
# gcc8.2 -O3 -m32
time_something():
push ebx # save a call-preserved reg: 32-bit only has 3 scratch regs
rdtsc
mov ecx, eax
mov ebx, edx # start in ebx:ecx
# timed region (empty)
rdtsc
sub eax, ecx
sbb edx, ebx # edx:eax -= ebx:ecx
pop ebx
ret # return value in edx:eax
Это вывод MSVC x86-64 (с примененным разделением имен). gcc / clang / ICC все испускают идентичный код.
# MSVC 19 2017 -Ox
unsigned __int64 time_something(void) PROC ; time_something
rdtsc
shl rdx, 32 ; high <<= 32
or rax, rdx
mov rcx, rax ; missed optimization: lea rcx, [rdx+rax]
; rcx = start
;; timed region (empty)
rdtsc
shl rdx, 32
or rax, rdx ; rax = end
sub rax, rcx ; end -= start
ret 0
unsigned __int64 time_something(void) ENDP ; time_something
Все 4 компилятора используют or
+mov
вместо lea
объединить низкие и высокие половины в другой регистр. Я предполагаю, что это своего рода последовательность, которую они не могут оптимизировать.
Но написать сдвиг / ле в inline asm самостоятельно вряд ли лучше. Вы лишите компилятор возможности игнорировать старшие 32 бита результата в EDX, если вы рассчитываете такой короткий интервал, что сохраняете только 32-битный результат. Или, если компилятор решит сохранить время запуска в памяти, он может просто использовать два 32-битных хранилища вместо shift / или / mov. Если 1 лишний моп как часть вашего времени беспокоит вас, вам лучше написать весь ваш микробенчмарк в чистом асме.
Тем не менее, мы можем получить лучшее из обоих миров с помощью модифицированной версии кода @ Mysticial:
// More efficient than __rdtsc() in some case, but maybe worse in others
uint64_t rdtsc(){
// long and uintptr_t are 32-bit on the x32 ABI (32-bit pointers in 64-bit mode), so #ifdef would be better if we care about this trick there.
unsigned long lo,hi; // let the compiler know that zero-extension to 64 bits isn't required
__asm__ __volatile__ ("rdtsc" : "=a" (lo), "=d" (hi));
return ((uint64_t)hi << 32) + lo;
// + allows LEA or ADD instead of OR
}
На Годболте, это иногда дает лучше АСМ, чем __rdtsc()
для gcc / clang / ICC, но в других случаях это заставляет компиляторы использовать дополнительный регистр для сохранения lo и hi отдельно, поэтому clang может оптимизировать ((end_hi-start_hi)<<32) + (end_lo-start_lo)
, Надеемся, что если будет реальное давление в реестре, компиляторы объединятся раньше. (gcc и ICC все еще сохраняют lo / hi отдельно, но не оптимизируют также.)
Но 32-битный gcc8 делает беспорядок, компилируя даже rdtsc()
функционировать с фактическим add/adc
с нулями вместо того, чтобы просто возвращать результат в edx: eax, как это делает clang. (GCC6 и ранее делать хорошо с |
вместо +
, но определенно предпочитаю __rdtsc()
присуще, если вам небезразлично 32-битное кодирование от gcc).
Для Windows Visual Studio предоставляет удобную «встроенную функцию компилятора» (то есть специальную функцию, которую понимает компилятор), которая выполняет инструкцию RDTSC для вас и возвращает результат:
unsigned __int64 __rdtsc(void);