Как получить количество циклов ЦП в x86_64 из C++?

Я видел это сообщение на SO, которое содержит C-код для получения последнего значения CPU Cycle:

Профилирование количества циклов процессора в C/С++ Linux x86_64

Есть ли способ использовать этот код в С++ (приветствуются решения для Windows и Linux)? Хотя написано в C (и C является подмножеством С++), я не слишком уверен, что этот код будет работать в проекте С++, а если нет, то как его перевести?

Я использую x86-64

EDIT2:

Нашел эту функцию, но не может получить VS2010 для распознавания ассемблера. Нужно ли включать что-нибудь? (Я считаю, что мне нужно поменять uint64_t на long long для окон....?)

static inline uint64_t get_cycles()
{
  uint64_t t;
  __asm volatile ("rdtsc" : "=A"(t));
  return t;
}

EDIT3:

Из кода выше я получаю сообщение об ошибке:

"ошибка C2400: ошибка синтаксиса встроенного ассемблера в 'opcode'; найденные данные типа"

Может ли кто-нибудь помочь?

Ответы

Ответ 1

Начиная с GCC 4.5 и выше, __rdtsc() теперь поддерживается и 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

Этот GNU C Extended asm сообщает компилятору:

  • volatile: выходы не являются чистой функцией входов (поэтому приходится каждый раз перезапускать, а не повторно использовать старый результат).
  • "=a"(lo) и "=d"(hi): выходные операнды являются фиксированными регистрами: EAX и EDX. (ограничения машины x86). rdtsc x86 rdtsc помещает свой 64-битный результат в EDX: EAX, так что позволить компилятору выбрать вывод с "=r" не будет работать: нет способа спросить у процессора, чтобы результат ушел куда-либо еще.
  • ((uint64_t)hi << 32) | lo ((uint64_t)hi << 32) | lo - обнулить обе 32-битные половины до 64-битных (потому что lo и hi не unsigned) и логически сдвинуть + ИЛИ их вместе в одну 64-битную переменную C. В 32-битном коде это просто переосмысление; значения все еще остаются в паре 32-битных регистров. В 64-битном коде вы обычно получаете фактические инструкции shift + OR asm, если только старшая половина не оптимизируется.

(примечание редактора: возможно, это было бы более эффективно, если бы вы использовали unsigned long вместо unsigned int. Тогда компилятор знал бы, что lo уже расширен до нуля в RAX. Он не будет знать, что верхняя половина равна нулю, поэтому | и + эквивалентны, если они хотят объединить другим способом. Теоретически, внутреннее должно дать вам лучшее из обоих миров, если позволить оптимизатору сделать хорошую работу.)

https://gcc.gnu.org/wiki/DontUseInlineAsm, если вы можете избежать этого. Но, надеюсь, этот раздел полезен, если вам нужно понять старый код, который использует встроенный asm, чтобы вы могли переписать его с помощью встроенных функций. Смотрите также fooobar.com/questions/tagged/...

Ответ 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, которые на самом деле означают что угодно.

Ответ 4

Для Windows Visual Studio предоставляет удобный "встроенный компилятор" (т.е. специальную функцию, которую понимает компилятор), которая выполняет инструкцию RDTSC для вас и возвращает результат:

unsigned __int64 __rdtsc(void);