Я видел это сообщение на SO, которое содержит C-код для получения последнего значения CPU Cycle:
Есть ли способ использовать этот код в С++ (приветствуются решения для Windows и Linux)? Хотя написано в C (и C является подмножеством С++), я не слишком уверен, что этот код будет работать в проекте С++, а если нет, то как его перевести?
Нашел эту функцию, но не может получить VS2010 для распознавания ассемблера. Нужно ли включать что-нибудь? (Я считаю, что мне нужно поменять uint64_t
на long long
для окон....?)
Ответ 2
Ваш встроенный ассм не работает на x86-64. "=A"
в 64-битном режиме позволяет компилятору выбирать RAX или RDX, а не EDX: EAX. Смотрите этот Q & A для более
Вам не нужен встроенный ассемблер для этого. Там нет никакой пользы; компиляторы имеют встроенные модули для rdtsc
и rdtscp
, и (по крайней мере, в наши дни) все определяют встроенную функцию __rdtsc
если вы включаете правильные заголовки. Но в отличие от почти всех других случаев (https://gcc.gnu.org/wiki/DontUseInlineAsm), у asm нет серьезных недостатков, если вы используете хорошую и безопасную реализацию, такую как @Mysticial's.
К сожалению, MSVC не согласен со всеми остальными в отношении того, какой заголовок использовать для встроенных функций без SIMD.
Руководство Intel по интринисам говорит, что _rdtsc
(с одним подчеркиванием) находится в <immintrin.h>
, но это не работает для gcc и clang. Они определяют встроенные SIMD только в <immintrin.h>
, поэтому мы застряли с <intrin.h>
(MSVC) и <x86intrin.h>
(всем остальным, включая недавний ICC). Для совместимости с 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-битных. Посмотрите also works in+C++, if you prefer that
#ifdef _MSC_VER
%23+include
#else
%23+include
#endif
//optional wrapper if you don!'t want to just use __rdtsc()+everywhere
//+inline
uint64_t readTSC() {%0A++++//+_mm_lfence()%3B++//optionally wait for earlier insns to retire before reading the+Clock%0A++++uint64_t tsc = __rdtsc();%0A++++//+_mm_lfence()%3B++//optionally block later instructions until rdtsc retires%0A++++return tsc;
}
inline
uint64_t readTSCp() {%0A++++unsigned dummy;%0A++++return __rdtscp(&dummy)%3B++//waits for earlier insns to retire, but allows later to start
}
//see if+Compilers+Can optimize when we only need the low 32+bits of the subtraction
//smart+Compilers will not even save the high half and only do a 32-bit subtraction (because+Carry only propagates from low to high)
//even in 64-bit mode. (only+Clang sees that optimization)
uint32_t time32() {%0A++++uint64_t start = readTSC()%3B++//we+Could help the+Compiler+by truncating to 32+bits here, but we aren!'t going to.%0A++++//+empty%0A++++return readTSC()+-+start;
}
uint64_t time_something() {%0A++++uint64_t start = readTSC();%0A++++//even when+empty, back-to-back __rdtsc()+doesn!'t optimize away%0A++++return readTSC()+-+start;
}
'),l:'5',n:'0',o:'C++ source #1',t:'0')),k:41.40171689746253,l:'4',m:100,n:'0',o:'',s:0,t:'0'),(g:!((g:!((h:compiler,i:(compiler:g82,filters:(b:'0',binary:'1',commentOnly:'0',demangle:'0',directives:'0',execute:'1',intel:'0',trim:'1'),lang:c++,libs:!(),options:'-xc -O3+-Wall',source:1),l:'5',n:'0',o:'x86-64 gcc 8.2+(Editor+#1,+Compiler+#1)+C++',t:'0')),k:23.491068847669467,l:'4',m:51.45145145145145,n:'0',o:'',s:0,t:'0'),(g:!((h:compiler,i:(compiler:icc18,filters:(b:'0',binary:'1',commentOnly:'0',demangle:'0',directives:'0',execute:'1',intel:'0',trim:'1'),lang:c++,libs:!(),options:'-std=gnu++14 -O3+-Wall -m32',source:1),l:'5',n:'0',o:'x86-64 icc 18.0.0+(Editor+#1,+Compiler+#2)+C++',t:'0')),l:'4',m:48.54854854854855,n:'0',o:'',s:0,t:'0')),k:28.847568200070576,l:'3',n:'0',o:'',t:'0'),(g:!((g:!((h:compiler,i:(compiler:cl19_64,filters:(b:'0',binary:'1',commentOnly:'0',demangle:'0',directives:'0',execute:'1',intel:'0',trim:'1'),lang:c++,libs:!(),options:'-Ox',source:1),l:'5',n:'0',o:'x86-64 MSVC 19 2017 RTW (Editor+#1,+Compiler+#3)+C++',t:'0')),header:(),k:29.750714902466893,l:'4',m:54.154154154154156,n:'0',o:'',s:0,t:'0'),(g:!((h:compiler,i:(compiler:clang600,filters:(b:'0',binary:'1',commentOnly:'0',demangle:'0',directives:'0',execute:'1',intel:'0',trim:'1'),lang:c++,libs:!(),options:'-O3+-Wall',source:1),l:'5',n:'0',o:'x86-64+Clang 6.0.0+(Editor+#1,+Compiler+#4)+C++',t:'0')),header:(),l:'4',m:45.845845845845844,n:'0',o:'',s:0,t:'0')),k:29.750714902466893,l:'3',n:'0',o:'',t:'0')),l:'2',n:'0',o:'',t:'0')),version:4 rel="nofollow noreferrer">результаты в проводнике компилятора Godbolt, включая пару тестовых вызовов.
Эти свойства были новыми в gcc 4.5 (с 2010 года) и clang 3.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
подписанные, особенно если вы хотите вычесть их и преобразовать в число с плавающей точкой. int64_t
→ float/double более эффективен, чем uint64_t
на x86 без AVX512. Кроме того, небольшие отрицательные результаты могут быть возможны из-за миграций ЦП, если TSC не синхронизированы идеально, и это, вероятно, имеет больше смысла, чем огромные числа без знака.
Кстати, у clang также есть портативный __builtin_readcyclecounter()
который работает на любой архитектуре. (Всегда возвращает ноль на архитектурах без счетчика циклов.) См. Документацию по расширению языка clang/LLVM.
Подробнее об использовании lfence
(или cpuid
) для улучшения повторяемости rdtsc
и контроля, какие именно инструкции находятся/не выполняются во rdtsc
интервале, путем блокировки неупорядоченного выполнения, см. Ответ @HadiBrais на clflush для аннулирования строки кэша через C функция и комментарии для примера различий, которые это делает.
См. Также Сериализация LFENCE на процессорах AMD? (TL: DR да с включенным смягчением Спектра, в противном случае ядра оставляют соответствующий MSR не установленным, поэтому вы должны использовать cpuid
для сериализации.) В Intel это всегда определялось как частичная сериализация.
Как сравнить время выполнения кода на архитектурах наборов инструкций Intel® IA-32 и IA-64, белого цвета Intel -p с 2010 года.
rdtsc
считает опорные циклы, а не тактовые частоты ядра процессора
Он рассчитывает на фиксированную частоту независимо от турбо/энергосбережения, поэтому, если вы хотите выполнить анализ блокировки uops -p или -c, используйте счетчики производительности. rdtsc
точно коррелирует с временем блокировки на стену -c (за исключением настроек системных часов, поэтому он является идеальным источником времени для steady_clock
). Он работает с номинальной частотой процессора, то есть с объявленной частотой наклейки. (Или почти что. Например, 2592 МГц на i7-6700HQ 2,6 ГГц Skylake.)
Если вы используете его для микробенчмаркинга, сначала включите период прогрева, чтобы убедиться, что ваш процессор уже работает на максимальной тактовой частоте, прежде чем начинать синхронизацию. (И дополнительно отключите turbo и скажите, чтобы ваша ОС предпочитала максимальную тактовую частоту, чтобы избежать сдвигов частоты процессора во время микробенчмарка) Или, лучше, использовать библиотеку, которая дает вам доступ к аппаратным счетчикам производительности, или трюк, подобный perf stat для части программы, если ваш синхронизированный регион достаточно длинный, чтобы вы могли прикрепить perf stat -p PID
.
Тем не менее, обычно вам все еще нужно фиксировать тактовую частоту ЦП для микробенчмарков, если только вы не хотите увидеть, как различные нагрузки заставят Skylake замедлять работу при привязке к памяти или что-то еще. (Обратите внимание, что пропускная способность/задержка памяти в основном фиксированы, с использованием тактовых импульсов, отличных от ядер. На тактовой частоте простоя пропуск кеширования L2 или L3 занимает намного меньше тактовых циклов ядра.)
- Отрицательные измерения тактового цикла с обратной связью rdtsc? История RDTSC: изначально процессоры не делали энергосбережения, поэтому TSC работал как в режиме реального времени, так и с тактовой частотой ядра. Затем он эволюционировал через несколько едва полезных шагов в свою текущую форму полезного тайм-источника с низкими издержками, отделенного от тактов ядра (
constant_tsc
), который не останавливается, когда часы останавливаются (nonstop_tsc
). Также некоторые советы, например, не занимайте среднее время, берите медиану (будут очень высокие выбросы). - std :: chrono :: clock, аппаратные часы и счетчик тактов
- Получение циклов процессора с использованием RDTSC - почему значение RDTSC всегда увеличивается?
- Потерянные циклы на Intel? Несоответствие между rdtsc и CPU_CLK_UNHALTED.REF_TSC
- измерение времени выполнения кода в C с использованием инструкций RDTSC приводит список некоторых ошибок, включая SMI (прерывания управления системой), которых нельзя избежать даже в режиме ядра с
cli
), и виртуализацию rdtsc
под виртуальной rdtsc
. И, конечно, возможны базовые вещи, такие как регулярные прерывания, поэтому повторяйте время много раз и отбрасывайте выбросы. -
Определите частоту TSC в Linux. Программно запрашивать частоту TSC сложно и, возможно, невозможно, особенно в пространстве пользователя, или может дать худший результат, чем его калибровка. Калибровка с использованием другого известного источника времени требует времени. См. Этот вопрос, чтобы узнать, насколько сложно преобразовать TSC в наносекунды (и было бы неплохо, если бы вы спросили ОС, каков коэффициент преобразования, потому что ОС уже сделала это при загрузке).
Если вы используете микробенчмаркинг с RDTSC для настройки, лучше всего использовать тики и пропустить, даже пытаясь конвертировать в наносекунды. В противном случае используйте функцию времени библиотеки высокого разрешения, такую как std::chrono
clock_gettime
или clock_gettime
. Смотрите более быстрый эквивалент gettimeofday для некоторого обсуждения/сравнения функций временной метки или считывания общей временной метки из памяти, чтобы полностью избежать rdtsc
если ваше требование к точности достаточно мало для прерывания таймера или потока для его обновления.
См. Также Расчет системного времени с помощью rdtsc для определения частоты кристалла и множителя.
Также не гарантируется, что TSC всех ядер синхронизированы. Таким образом, если ваш поток мигрирует на другое ядро ЦП между __rdtsc()
, может быть дополнительный перекос. (Однако большинство ОС пытаются синхронизировать TSC всех ядер, поэтому обычно они будут очень близки.) Если вы используете rdtsc
напрямую, вы, вероятно, захотите прикрепить свою программу или поток к ядру, например, с помощью taskset -c 0./myprogram
в Linux.
Операция извлечения TSC ЦП, особенно в многоядерно-многопроцессорной среде -p, говорит, что Nehalem и новее имеют синхронизированный TSC и заблокированный вместе для всех ядер в пакете (то есть инвариантный TSC). Но многоразъемные системы все еще могут быть проблемой. Даже в более старых системах (как до Core2 в 2007 году) может быть TSC, который останавливается при остановке тактовой частоты ядра или привязывается к фактической тактовой частоте ядра вместо эталонных циклов. (Более новые процессоры всегда имеют постоянный TSC и нон-стоп-TSC.) Более подробную информацию смотрите в ответе @amdn на этот вопрос.
Насколько хорошо асм от использования встроенного?
Это примерно так же хорошо, как вы можете получить от @Mysticial GNU C inline asm, или лучше, потому что он знает, что старшие биты 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
}
also works in+C++, if you prefer that
#ifdef _MSC_VER
%23+include
#else
%23+include
#endif
#ifdef __GNUC__
uint64_t rdtsc(){%0A++++//long and uintptr_t are 32-bit on x32+(32-bit pointers in long mode), so #ifdef would be better if we+Care about optimal asm there.
%0A++++unsigned long lo,hi%3B++//let the+Compiler+know that zero-extension to 64 bits isn!'t required%0A++++__asm__ __volatile__ ("rdtsc%22+: "=a%22+(lo), "=d%22+(hi));%0A++++return ((uint64_t)hi+<< 32) + lo;%0A++++//+ allows LEA or ADD instead of OR%0A++++//| optimizes much better than + for gcc -m32. (Both suck with gcc7 and later though)
}
#else
#define rdtsc __rdtsc
#endif
//return a+Correctly zero-extended 32-bit result
//just to give the+Compiler+a bit more work to do
uint64_t test32() {%0A++++uint64_t tsc = rdtsc();%0A++++//return rdtsc()+-+tsc;
%0A++++uint32_t low32+= tsc;%0A++++low32+-= rdtsc();%0A++++return low32;
}
void ext(void);
uint64_t test64() {%0A++++uint64_t tsc = rdtsc();%0A++++//+Compilers get+Carried away optimizing, and save/restore two registers%0A++++//instead of+Combining lo and hi+to one reg.%0A++++ext();%0A++++return rdtsc()+-+tsc;
}
'),l:'5',n:'0',o:'C++ source #1',t:'0')),k:41.40171689746253,l:'4',m:100,n:'0',o:'',s:0,t:'0'),(g:!((g:!((h:compiler,i:(compiler:g63,filters:(b:'0',binary:'1',commentOnly:'0',demangle:'0',directives:'0',execute:'1',intel:'0',trim:'1'),lang:c++,libs:!(),options:'-xc -O3+-Wall',source:1),l:'5',n:'0',o:'x86-64 gcc 6.3+(Editor+#1,+Compiler+#1)+C++',t:'0')),k:23.491068847669467,l:'4',m:51.45145145145145,n:'0',o:'',s:0,t:'0'),(g:!((h:compiler,i:(compiler:icc18,filters:(b:'0',binary:'1',commentOnly:'0',demangle:'0',directives:'0',execute:'1',intel:'0',trim:'1'),lang:c++,libs:!(),options:'-std=gnu++14 -O3+-Wall',source:1),l:'5',n:'0',o:'x86-64 icc 18.0.0+(Editor+#1,+Compiler+#2)+C++',t:'0')),l:'4',m:48.54854854854855,n:'0',o:'',s:0,t:'0')),k:28.847568200070576,l:'3',n:'0',o:'',t:'0'),(g:!((g:!((h:compiler,i:(compiler:cl19_64,filters:(b:'0',binary:'1',commentOnly:'0',demangle:'0',directives:'0',execute:'1',intel:'0',trim:'1'),lang:c++,libs:!(),options:'-Ox',source:1),l:'5',n:'0',o:'x86-64 MSVC 19 2017 RTW (Editor+#1,+Compiler+#3)+C++',t:'0')),header:(),k:29.750714902466893,l:'4',m:54.154154154154156,n:'0',o:'',s:0,t:'0'),(g:!((h:compiler,i:(compiler:clang600,filters:(b:'0',binary:'1',commentOnly:'0',demangle:'0',directives:'0',execute:'1',intel:'0',trim:'1'),lang:c++,libs:!(),options:'-O3+-Wall',source:1),l:'5',n:'0',o:'x86-64+Clang 6.0.0+(Editor+#1,+Compiler+#4)+C++',t:'0')),header:(),l:'4',m:45.845845845845844,n:'0',o:'',s:0,t:'0')),k:29.750714902466893,l:'3',n:'0',o:'',t:'0')),l:'2',n:'0',o:'',t:'0')),version:4 rel="nofollow noreferrer">На Godbolt это иногда дает лучшую asm, чем __rdtsc()
для gcc/clang/ICC, но в других случаях он заставляет компиляторы использовать дополнительный регистр для отдельного сохранения lo и hi, поэтому clang может оптимизироваться в ((end_hi-start_hi)<<32) + (end_lo-start_lo)
. Надеемся, что при наличии реального регистра давления, компиляторы будут объединяться раньше. (gcc и ICC все еще сохраняют lo/hi отдельно, но не оптимизируют также.)
Но 32-битный gcc8 делает беспорядок, компилируя даже rdtsc()
функцию rdtsc()
с фактическим add/adc
с нулями вместо того, чтобы просто возвращать результат в edx: eax, как это делает clang. (gcc6 и более ранние версии хорошо работают с |
вместо +
, но определенно предпочитают __rdtsc()
если вы заботитесь о 32-битном коде gen из gcc).
Ответ 3
VС++ использует совершенно другой синтаксис для встроенной сборки - но только в 32-разрядных версиях. 64-разрядный компилятор не поддерживает встроенную сборку вообще.
В этом случае, возможно, так же хорошо - rdtsc
имеет (по крайней мере) две серьезные проблемы, когда речь идет о временных кодовых последовательностях. Сначала (как и большинство инструкций) его можно выполнить не по порядку, поэтому, если вы пытаетесь выполнить короткую последовательность кода, rdtsc
до и после этого кода могут быть выполнены до него или после него или что у вас (я уверен, что они всегда будут выполняться по порядку друг относительно друга, так что по крайней мере разница никогда не будет отрицательной).
Во-вторых, в многоядерной (или многопроцессорной) системе один rdtsc может выполняться на одном ядре/процессоре, а другой - на другом ядре/процессоре. В этом случае вполне возможен отрицательный результат.
Вообще говоря, если вам нужен точный таймер под Windows, вам будет лучше использовать QueryPerformanceCounter
.
Если вы действительно настаиваете на использовании rdtsc
, я считаю, что вам нужно будет сделать это в отдельном модуле, написанном полностью на языке ассемблера (или используя встроенный компилятор), а затем связан с вашим 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 + тактов. Ясно, что в последнем случае вы просто выбросите outlier - это не из вашего кода.
Сводка: управление выполнением самой инструкции rdtsc является (почти) наименьшим из ваших забот. Вам нужно сделать еще немного, прежде чем вы сможете получить результаты от rdtsc
, которые на самом деле означают что угодно.