Как считать тактовые циклы с RDTSC в GCC x86? [дубликат]

этот вопрос уже есть ответ здесь:

С Visual Studio я могу прочитать счетчик тактового цикла от процессора, как показано ниже. Как мне сделать то же самое с GCC?

#ifdef _MSC_VER             // Compiler: Microsoft Visual Studio

    #ifdef _M_IX86                      // Processor: x86

        inline uint64_t clockCycleCount()
        {
            uint64_t c;
            __asm {
                cpuid       // serialize processor
                rdtsc       // read time stamp counter
                mov dword ptr [c + 0], eax
                mov dword ptr [c + 4], edx
            }
            return c;
        }

    #elif defined(_M_X64)               // Processor: x64

        extern "C" unsigned __int64 __rdtsc();
        #pragma intrinsic(__rdtsc)
        inline uint64_t clockCycleCount()
        {
            return __rdtsc();
        }

    #endif

#endif

4 ответов


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

Если вы действительно хотите вызвать RDTSC, вы можете использовать следующую встроенную сборку:

http://www.mcs.anl.gov / ~kazutomo/rdtsc.html

#if defined(__i386__)

static __inline__ unsigned long long rdtsc(void)
{
    unsigned long long int x;
    __asm__ volatile (".byte 0x0f, 0x31" : "=A" (x));
    return x;
}

#elif defined(__x86_64__)

static __inline__ unsigned long long rdtsc(void)
{
    unsigned hi, lo;
    __asm__ __volatile__ ("rdtsc" : "=a"(lo), "=d"(hi));
    return ( (unsigned long long)lo)|( ((unsigned long long)hi)<<32 );
}

#endif

другие ответы работают, но вы можете избежать встроенной сборки с помощью GCC __rdtsc внутренней, доступной, включая x86intrin.h.

определяется по адресу:gcc/config/i386/ia32intrin.h:

/* rdtsc */
extern __inline unsigned long long
__attribute__((__gnu_inline__, __always_inline__, __artificial__))
__rdtsc (void)
{
  return __builtin_ia32_rdtsc ();
}

в Linux с gcc, Я использую следующий:

/* define this somewhere */
#ifdef __i386
__inline__ uint64_t rdtsc() {
  uint64_t x;
  __asm__ volatile ("rdtsc" : "=A" (x));
  return x;
}
#elif __amd64
__inline__ uint64_t rdtsc() {
  uint64_t a, d;
  __asm__ volatile ("rdtsc" : "=a" (a), "=d" (d));
  return (d<<32) | a;
}
#endif

/* now, in your function, do the following */
uint64_t t;
t = rdtsc();
// ... the stuff that you want to time ...
t = rdtsc() - t;
// t now contains the number of cycles elapsed

обновление: репост и обновил этот ответ по более каноническому вопросу. Я, вероятно, удалю это в какой-то момент, как только мы разберемся, какой вопрос использовать в качестве дублирующей цели для закрытия всех подобных rdtsc вопросы.


вам не нужно и не следует использовать встроенный asm для этого. Нет никакой пользы; компиляторы имеют встроенные модули для rdtsc и rdtscp и (по крайней мере в эти дни) все определения __rdtsc intrinsic, если вы включаете правильные заголовки. https://gcc.gnu.org/wiki/DontUseInlineAsm

к сожалению, MSVC не согласен со всеми остальными о том, какой заголовок использовать для встроенных не SIMD. (руководство Intel intriniscs говорит #include <immintrin.h> для этого, но с gcc и clang не-SIMD внутреннеприсущие в основном находятся в x86intrin.h.)

#ifdef _MSC_VER
#include <intrin.h>
#else
#include <x86intrin.h>
#endif

// optional wrapper if you don't want to just use __rdtsc() everywhere
inline
unsigned long long readTSC() {
    // _mm_lfence();  // optionally wait for earlier insns to retire before reading the clock
    return __rdtsc();
    // _mm_lfence();  // optionally block later instructions until rdtsc retires
}

компилируется со всеми 4 основными компиляторами: gcc/clang/ICC / MSVC, для 32 или 64-разрядных. посмотреть результаты в проводнике компилятора Godbolt.

подробнее об использовании lfence для улучшения повторяемости rdtsc, см. ответ @HadiBrais на clflush для аннулирования строки кэша через функцию C.

см. также сериализуется ли lfence на процессорах AMD? (TL: DR да с включенным смягчением спектра, в противном случае ядра оставляют соответствующий MSR unset.)


rdtsc графы ссылка циклы, а не тактовые циклы ядра процессора

он подсчитывает на фиксированной частоте независимо от турбо / энергосбережения, поэтому, если вы хотите анализ uops-per-clock, используйте счетчики производительности. rdtsc точно коррелирует со временем настенных часов (за исключением системных настроек часов, поэтому это в основном steady_clock). Он тикает на номинальной частоте процессора, т. е. рекламируемой частоте наклейки.

если вы используете его для microbenchmarking, сначала включите период прогрева, чтобы убедиться, что ваш процессор уже на максимальной тактовой частоте, прежде чем начать синхронизацию. Или лучше использовать библиотеку, которая дает вам доступ к счетчикам производительности оборудования, или трюк, как perf stat для части программы если ваша временная область достаточно длинная, что вы можете прикрепить perf stat -p PID. Обычно вы все равно хотите избежать частотных сдвигов процессора во время microbenchmark.

также не гарантируется, что TSCs всех ядер синхронизированы. Поэтому, если ваш поток мигрирует в другое ядро процессора между __rdtsc(), может быть дополнительный перекос. (Большинство ОС пытаются синхронизировать TSC всех ядер.) Если вы используете rdtsc напрямую, вы вероятно, вы хотите прикрепить свою программу или поток к ядру, например, с помощью taskset -c 0 ./myprogram на Linux.


насколько хорош asm от использования внутреннего?

это, по крайней мере, так же хорошо, как все, что вы могли бы сделать с встроенным asm.

не встроенная версия компилирует 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 совместить низкие и высокие половины в различный регистр. Я думаю это своего рода консервированная последовательность, которую они не могут оптимизировать.

но писать его в встроенном asm едва ли лучше. Вы лишите компилятор возможности игнорировать высокие 32 бита результата в EDX, если вы синхронизируете такой короткий интервал, что сохраняете только 32-битный результат. Или, если компилятор решит сохранить время начала в памяти, он может просто использовать два 32-битных хранилища вместо shift/или / mov. Если 1 дополнительный uop как часть вашего времени беспокоит вас, вам лучше напишите весь свой microbenchmark в чистом asm.