Получить счетчик циклов процессора?
Я видел этот пост на SO, который содержит код C, чтобы получить последний счетчик циклов процессора:
профилирование на основе подсчета циклов процессора в C / C++ Linux x86_64
есть ли способ использовать этот код на C++ (приветствуются решения для windows и linux)? Хотя написано на C (а C является подмножеством c++), я не слишком уверен, будет ли этот код работать в проекте C++, а если нет, то как его перевести?
Я использую для x86-64
EDIT2:
нашел эту функцию, но не может заставить VS2010 распознать ассемблер. Нужно ли что-то включать? (Я считаю, что я должен поменять uint64_t
to long long
для windows....?)
static inline uint64_t get_cycles()
{
uint64_t t;
__asm volatile ("rdtsc" : "=A"(t));
return t;
}
EDIT3:
сверху кода я получаю сообщение об ошибке:
"error C2400: синтаксическая ошибка встроенного ассемблера в 'opcode'; найденные ' данные типа'"
может кто-нибудь помочь?
4 ответов
начиная с 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 может выполняться на одном ядре/процессоре, а другой-на другом ядре/процессоре. В таком случае, отрицательный результат is вполне возможно.
вообще говоря, если вы хотите точный таймер под 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, чтобы получить время после завершения кода.
наряду с этим, вы хотите использовать все, что означает вашу ОС поставки, чтобы заставить все это работать на одном процессе / ядре. В большинстве случаев вы также хотите принудительно выровнять код - изменения в выравнивании могут привести к довольно существенным различиям в spee выполнения.
наконец, вы хотите выполнить его несколько раз - и всегда возможно, что он будет прерван в середине вещей( например, переключатель задач), поэтому вам нужно быть готовым к возможности выполнения, занимающего немного больше времени, чем остальные-например, 5 запусков, которые занимают ~40-43 тактов за штуку, и шестой, который берет 10000+ тактов. Ясно, что в последнем случае вы просто выбрасываете выброс-это не из вашего кода.
резюме: управление выполнением инструкции rdtsc само по себе (почти) является наименьшей из ваших забот. Там совсем немного больше вас нужно сделать, прежде чем вы сможете получить результаты от rdtsc
это на самом деле будет означать что угодно.
для Windows Visual Studio предоставляет удобный "встроенный компилятор" (т. е. специальную функцию, которую понимает компилятор), который выполняет инструкцию RDTSC для вас и возвращает результат:
unsigned __int64 __rdtsc(void);
вам не нужен встроенный asm для этого. Нет никакой пользы; компиляторы имеют встроенные модули для rdtsc
и rdtscp
и (по крайней мере в эти дни) все определения __rdtsc
intrinsic, если вы включаете правильные заголовки. Но в отличие от почти всех других случаев (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>
(все остальное, включая недавний ICC). Для compat с 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 compiler explorer, включая испытание пары абоненты.
эти внутренние компоненты были новыми в 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
на всех. Более недавние ICC имеют x86intrin.h
, по крайней мере, так, как Godbolt устанавливает их для Linux.
вы можете определить их как подписанные long long
, особенно если вы хотите вычесть их и преобразовать в float. int64_t
-> float / double более эффективен, чем uint64_t
на x86 без AVX512. Кроме того, небольшие отрицательные результаты могут быть возможны из-за CPU миграции, если TSCs не полностью синхронизированы, и это, вероятно, имеет больше смысла, чем огромные неподписанные числа.
кстати, clang также имеет портативный __builtin_readcyclecounter()
, которая работает на любой архитектуре. (Всегда возвращает ноль на архитектурах без счетчика циклов.) См.язык clang/LLVM-расширение docs
подробнее о используя lfence
(или cpuid
) для улучшения повторяемости rdtsc
и указать, какие именно инструкции are / aren'T в интервале времени, блокируя выполнение вне ордера, см. ответ @HadiBrais на clflush для аннулирования строки кэша через функцию C и комментарии к пример разница.
см. также сериализуется ли lfence на процессорах AMD? (TL: DR да с включенным смягчением спектра, в противном случае ядра оставляют соответствующий MSR unset, поэтому вы должны использовать cpuid
сериализовать.) Это всегда определялось как частично-сериализация на Intel.
Как проверить время выполнения кода на Intel® IA-32 и IA-64 Архитектура Набора Инструкций, белая бумага Intel от 2010.
rdtsc
графы ссылка циклы, а не тактовые циклы ядра процессора
он подсчитывает на фиксированной частоте независимо от турбо / энергосбережения, поэтому, если вы хотите анализ uops-per-clock, используйте счетчики производительности. rdtsc
ровно коррелирует со временем настенных часов (за исключением системных настроек часов, поэтому это идеальный источник времени для steady_clock
). Он тикает на номинальной частоте процессора, т. е. рекламируемой частоте наклейки. (Или почти что. например, 2592 МГц на i7-6700HQ 2.6 GHz Skylake.)
если вы используете его для microbenchmarking, сначала включите период прогрева, чтобы убедиться, что ваш процессор уже на максимальной тактовой частоте перед началом синхронизации. (И при необходимости отключите turbo и сообщите своей ОС предпочитайте максимальную тактовую частоту, чтобы избежать сдвигов частоты процессора во время microbenchmark). Или лучше использовать библиотеку, которая дает вам доступ к счетчикам производительности оборудования, или трюк, как perf stat для части программы если ваша временная область достаточно длинная, что вы можете прикрепить perf stat -p PID
.
вы, как правило, по-прежнему хотите сохранить часы процессора фиксированными для microbenchmarks, хотя, если вы не хотите видеть, как разные нагрузки будут получать Skylake для синхронизации при привязке к памяти или что угодно. (Обратите внимание, что пропускная способность / задержка памяти в основном фиксирована, используя другие часы, чем ядра. При тактовой частоте холостого хода пропускание кэша L2 или L3 занимает намного меньше тактовых циклов ядра.)
-
отрицательные измерения тактового цикла с спиной к спине rdtsc? история RDTSC: первоначально процессоры не делали энергосбережения, поэтому TSC был как в реальном времени, так и в основных часах. Затем он эволюционировал через различные едва полезные шаги в свою текущую форму полезного низкозатратный Источник времени, отделенный от основных тактовых циклов (
constant_tsc
), который не останавливается, когда время останавливается (nonstop_tsc
). Также некоторые советы, например, не берите среднее время, возьмите медиану (будут очень высокие выбросы). - std:: chrono:: часы, аппаратные часы и количество циклов
- получение циклов процессора с помощью RDTSC-почему значение RDTSC всегда увеличивается?
- потерянные циклы на Intel? Непоследовательность между rdtsc и CPU_CLK_UNHALTED.REF_TSC
-
измерение времени выполнения кода в C с помощью инструкции RDTSC перечисляет некоторые gotchas, включая SMI (прерывания системного управления), которых вы не можете избежать даже в режиме ядра с
cli
), и виртуализацияrdtsc
под виртуальной машиной. И, конечно, основные вещи, такие как регулярные прерывания, возможны, поэтому повторите свое время много раз и выбросьте выбросы. -
определить TSC частоты на Linux. программно запрашивать частоту TSC трудно и, возможно, невозможно, особенно в пользовательском пространстве, или может дать худший результат, чем его калибровка. Калибровка с использованием другого известного источника времени требует времени. См. этот вопрос для получения дополнительной информации о том, как трудно преобразовать TSC в наносекунды (и что было бы неплохо, если бы вы могли спросить ОС, что такое коэффициент преобразования, потому что ОС уже сделала это при загрузке).
если вы microbenchmarking с RDTSC для целей настройки лучше всего использовать тики и пропустить даже попытку преобразования в наносекунды. в противном случае используйте функцию времени библиотеки с высоким разрешением, например
std::chrono
илиclock_gettime
. См. более быстрый эквивалент gettimeofday для некоторого обсуждения / сравнения функций метки времени или чтения общей метки времени из памяти, чтобы избежатьrdtsc
полностью, если ваше требование точности достаточно низкое для прерывания таймера или потока для обновления он.см. также вычислить системное время с помощью rdtsc о поиске частоты кристалла и множителя.
также не гарантируется, что TSCs всех ядер синхронизированы. Поэтому, если ваш поток мигрирует в другое ядро процессора между __rdtsc()
, может быть дополнительный перекос. (Большинство ОС пытаются синхронизировать TSC всех ядер, поэтому обычно они будут очень близки.) Если вы используете rdtsc
напрямую, вы, вероятно, хотите прикрепить свою программу или нить к ядру, например, с помощью taskset -c 0 ./myprogram
на Linux.
CPU TSC fetch operation особенно в многоядерной многопроцессорной среде говорит, что Nehalem и новее имеют TSC синхронизированы и заблокированы вместе для всех ядер в пакете (т. е. инвариантный TSC). Но мульти-сокетные системы все еще могут быть проблемой. Даже более старые системы (например, до Core2 в 2007 году) могут иметь TSC, который останавливается, когда часы ядра останавливаются или привязаны к фактическая тактовая частота сердечника вместо циклов справки. (Новые процессоры всегда имеют constant-TSC и non-stop-TSC.) См. ответ @amdn на этот вопрос для получения более подробной информации.
насколько хорош asm от использования внутреннего?
это примерно так же хорошо, как вы получите от встроенного asm GNU C @Mysticial, или лучше, потому что он знает, что верхние биты RAX обнулены. Основная причина, по которой вы хотите сохранить встроенный asm, - это compat с crusty old компиляторы.
не встроенная версия 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
это выход x86-64 MSVC (с примененным именем-demangling). 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
совместить низкие и высокие половины в различный регистр. Я думаю, это своего рода консервированная последовательность, которую они не могут оптимизировать.
но писать shift / lea в inline asm самому вряд ли лучше. Вы лишите компилятор возможности игнорировать высокие 32 бита результата в EDX, если вы синхронизируете такой короткий интервал, что вы сохраняете только 32-битный результат. Или, если компилятор решит сохранить время начала в памяти, он может просто использовать два 32-битных хранилища вместо shift/или / mov. Если 1 дополнительный uop как часть вашего времени беспокоит вас, вам лучше написать весь свой microbenchmark в чистом asm.
тем не менее, мы можем получить лучшее из обоих миров с модифицированной версией @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
}
На Godbolt, это иногда дает лучший asm, чем __rdtsc()
для gcc / clang / ICC, но в других случаях он обманывает компиляторы в использовании дополнительного регистра для сохранения lo и hi отдельно, поэтому clang может оптимизировать в ((end_hi-start_hi)<<32) + (end_lo-start_lo)
. Надеюсь, если будет реальное давление регистра, компиляторы объединятся раньше. (gcc и ICC все еще сохраняют lo/hi отдельно, но не оптимизируют также.)
но 32-бит gcc8 делает беспорядок, компилируя даже просто с add/adc
с нулями вместо того, чтобы просто возвращать результат в edx:eax, как это делает clang. (gcc6 и ранее делать ок с |
вместо +
, но определенно предпочитаю __rdtsc()
intrinsic если вы заботитесь о 32-разрядном коде-gen от gcc).