Получить количество тактов процессора?

Я видел этот пост на 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: синтаксическая ошибка встроенного ассемблера в 'код операции'; найден 'тип данных'"

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

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

Вам не нужен встроенный ассемблер для этого. Там нет никакой выгоды; компиляторы имеют встроенные модули для rdtsc а также rdtscp и (по крайней мере, в наши дни) все определяют __rdtsc свойственный, если вы включите правильные заголовки. Но в отличие от почти всех других случаев ( https://gcc.gnu.org/wiki/DontUseInlineAsm), у asm нет серьезных недостатков, если вы используете хорошую и безопасную реализацию, такую ​​как @Mysticial, а не сломанную. "=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 для части программы, если ваш синхронизированный регион достаточно длинный, чтобы вы могли прикрепить 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 под ВМ. И, конечно, возможны базовые вещи, такие как регулярные прерывания, поэтому повторяйте время много раз и отбрасывайте выбросы.
  • Определите частоту 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
}

На Godbolt, это иногда дает лучше АСМ, чем __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).

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 это на самом деле будет означать что угодно.

Linux perf_event_open системный вызов с config = PERF_COUNT_HW_CPU_CYCLES

Этот системный вызов Linux представляет собой кросс-архитектурную оболочку для событий производительности.

Этот ответ похож: быстрый способ подсчета количества инструкций, выполненных в программе на C, но с PERF_COUNT_HW_CPU_CYCLES вместо PERF_COUNT_HW_INSTRUCTIONS. Этот ответ будет сосредоточен на PERF_COUNT_HW_CPU_CYCLES подробности, см. этот ответ для получения более общей информации.

perf_event_open.c

#include <asm/unistd.h>
#include <linux/perf_event.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <unistd.h>

#include <inttypes.h>

static long
perf_event_open(struct perf_event_attr *hw_event, pid_t pid,
                int cpu, int group_fd, unsigned long flags)
{
    int ret;

    ret = syscall(__NR_perf_event_open, hw_event, pid, cpu,
                    group_fd, flags);
    return ret;
}

int
main(int argc, char **argv)
{
    struct perf_event_attr pe;
    long long count;
    int fd;

    uint64_t n;
    if (argc > 1) {
        n = strtoll(argv[1], NULL, 0);
    } else {
        n = 10000;
    }

    memset(&pe, 0, sizeof(struct perf_event_attr));
    pe.type = PERF_TYPE_HARDWARE;
    pe.size = sizeof(struct perf_event_attr);
    pe.config = PERF_COUNT_HW_CPU_CYCLES;
    pe.disabled = 1;
    pe.exclude_kernel = 1;
    // Don't count hypervisor events.
    pe.exclude_hv = 1;

    fd = perf_event_open(&pe, 0, -1, -1, 0);
    if (fd == -1) {
        fprintf(stderr, "Error opening leader %llx\n", pe.config);
        exit(EXIT_FAILURE);
    }

    ioctl(fd, PERF_EVENT_IOC_RESET, 0);
    ioctl(fd, PERF_EVENT_IOC_ENABLE, 0);

    /* Loop n times, should be good enough for -O0. */
    __asm__ (
        "1:;\n"
        "sub $1, %[n];\n"
        "jne 1b;\n"
        : [n] "+r" (n)
        :
        :
    );

    ioctl(fd, PERF_EVENT_IOC_DISABLE, 0);
    read(fd, &count, sizeof(long long));

    printf("%lld\n", count);

    close(fd);
}

Результаты кажутся разумными, например, если я печатаю циклы, а затем перекомпилирую для подсчета инструкций, мы получаем примерно 1 цикл на итерацию (2 инструкции, выполняемые за один цикл), возможно, из-за таких эффектов, как суперскалярное выполнение, с немного разными результатами для каждого запуска, предположительно из-за задержкам доступа к произвольной памяти.

Вас также может заинтересовать PERF_COUNT_HW_REF_CPU_CYCLES, который, как документы на странице руководства:

Всего циклов; не зависит от масштабирования частоты процессора.

поэтому это даст что-то более близкое к реальному времени, если у вас включено масштабирование частоты. Они были в 2/3 раза больше, чем PERF_COUNT_HW_INSTRUCTIONS о моих быстрых экспериментах, предположительно потому, что моя машина без нагрузки теперь масштабируется по частоте.

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

unsigned __int64 __rdtsc(void);
Другие вопросы по тегам