Измерение времени выполнения кода в C с использованием инструкции RDTSC
Я написал простую программу для измерения времени выполнения кода с помощью инструкции RDTSC. Но я не знаю, является ли мой результат правильным и что-то не так с моим кодом... Я понятия не имею, как это проверить.
#include <stdio.h>
#include <assert.h>
#include <stdint.h>
#include <stdlib.h>
#define N (1024*4)
unsigned cycles_low, cycles_high, cycles_low1, cycles_high1;
static __inline__ unsigned long long rdtsc(void)
{
__asm__ __volatile__ ("RDTSC\n\t"
"mov %%edx, %0\n\t"
"mov %%eax, %1\n\t": "=r" (cycles_high), "=r" (cycles_low)::
"%rax", "rbx", "rcx", "rdx");
}
static __inline__ unsigned long long rdtsc1(void)
{
__asm__ __volatile__ ("RDTSC\n\t"
"mov %%edx, %0\n\t"
"mov %%eax, %1\n\t": "=r" (cycles_high1), "=r" (cycles_low1)::
"%rax", "rbx", "rcx", "rdx");
}
int main(int argc, char* argv[])
{
uint64_t start, end;
rdtsc();
malloc(N);
rdtsc1();
start = ( ((uint64_t)cycles_high << 32) | cycles_low );
end = ( ((uint64_t)cycles_high1 << 32) | cycles_low1 );
printf("cycles spent in allocating %d bytes of memory: %llu\n",N, end - start);
return 0;
}
4 ответа
Проблемы, которые могут повлиять на ваши результаты:
на большинстве современных процессоров 80x86 TSC измеряет тактовую частоту фиксированной частоты, а не циклы, и, следовательно, один и тот же фрагмент кода может иметь совершенно разные "циклы" в зависимости от управления питанием, нагрузки на другие логические процессоры в том же ядре (гиперпоточность), нагрузка на другие ядра (турбонаддув), температура процессора (термическое регулирование) и т. д.
ничто не мешает планировщику ОС опережать ваш поток сразу после первого
rdtsc();
заставляя результирующие "циклы, потраченные на распределение" включать время, затраченное ЦП на выполнение любого количества совершенно разных процессов.на некоторых компьютерах TSC на разных процессорах не синхронизируется; и ничто не мешает ОС прервать ваш поток сразу после первого
rdtsc();
а затем запустить ваш поток на совершенно другом процессоре (с совершенно другим TSC). В этом случае это возможно дляend - start
быть отрицательным (как время идет назад).ничто не мешает IRQ (от оборудования) прерывать ваш код сразу после первого
rdtsc();
в результате чего "циклы, потраченные на выделение", включают время, затраченное ОС на обработку любого количества IRQ.невозможно предотвратить SMI ("Прерывание управления системой"), заставляющее ЦП войти в SMM ("Режим управления системой") и выполнить скрытый код прошивки после первого
rdtsc();
заставляя результирующие "циклы, потраченные на распределение" включать время, потраченное ЦП на выполнение кода прошивки.некоторые (старые) процессоры имеют ошибку, при которой
rdtsc
дает хитрые результаты, когда переполнение младших 32 битов (например, когда TSC переходит от 0x00000000FFFFFFFF к 0x0000000100000000, вы можете использоватьrdtsc
в точно неверное время и получите 0x0000000000000000).ничто не мешает "вышедшему из строя" современному ЦП изменить порядок, в котором выполняется большинство инструкций, включая ваш
rdtsc
инструкции.Ваше измерение включает в себя накладные расходы измерения (например, если
rdtsc
занимает 5 циклов и вашmalloc()
стоит 20 циклов, тогда вы сообщаете 25 циклов, а не 20 циклов).с или без виртуальной машины; возможно, что
rdtsc
инструкция виртуализирована (например, ничто, кроме здравого смысла, не мешает ядру создаватьrdtsc
сообщить, сколько свободного дискового пространства есть или что-либо еще, что ему нравится). в идеалеrdtsc
следует виртуализировать, чтобы предотвратить большинство проблем, упомянутых выше, и / или предотвратить побочные каналы синхронизации (но это почти никогда не происходит).на очень старых процессорах (80486 и старше) TSC и
rdtsc
Инструкция не существует.
Примечание: я не эксперт по встроенной сборке GCC; но я сильно подозреваю, что ваши макросы содержат ошибки и что компилятор может сгенерировать что-то вроде этого:
rdtsc
mov %edx, %eax ;Oops, trashed the low 32 bits
mov %eax, %ebx
Должна быть возможность сообщить GCC, что значение / значения возвращаются в EDX:EAX, и избавиться от обоих mov
инструкции полностью.
Есть некоторые (неочевидные) проблемы, которые вы должны учитывать при использовании RDTSC для определения времени:
Частота тактовых импульсов, которые он считает, может быть непредсказуемой. На старом оборудовании частота может фактически изменяться между двумя командами RDTSC, и даже на более новом оборудовании, где оно зафиксировано, может быть трудно определить, на какой частоте он работает.
Поскольку RDTSC не имеет входов, сам ЦП может переупорядочить инструкцию RDTSC, чтобы она предшествовала коду, который вы пытаетесь измерить. Обратите внимание, что это проблема, отличная от перекомпоновки компилятора кода, которую вы избежали с помощью __volatile__. Чтобы эффективно избежать этого, вы должны выполнить команду сериализации, которая является инструкцией, которая будет препятствовать тому, чтобы ЦП перемещал инструкцию перед ней. Вы можете использовать либо CPUID, либо RDTSCP (это просто форма сериализации RDTSC)
Мое предложение: просто используйте API высокочастотного таймера, который есть в вашей ОС. В Windows это QueryPerformanceCounter, а в Unix у вас есть gettimeofday или clock_gettime.
Помимо этого, ваш код RDTSC имеет несколько структурных проблем. Тип возвращаемого значения - "unsigned long long", но на самом деле ничего не возвращается. Если вы исправите это, вы сможете избежать сохранения результата в глобальных переменных и избежать написания нескольких версий.
Примечание: когда я писал это, я придумал более простой и понятный способ калибровки TSC
фактор общения. Итак, продолжайте читать...
Если вы хотите, под Linux [некоторые другие ОС имеют аналогичные - например, BSD реализует часть Linux /proc], в /proc/cpuinfo
вы увидите такие поля:
bogomips : 5306.71
flags : blah blah2 constant_tsc
processor : blah
Если вы читаете этот файл, bogomips
общая частота процессора в МГц [вид], вычисленная во время загрузки системы. Предпочитаю это cpu Mhz
если ваша машина имеет шаг скорости.
Использовать bogomips
посчитать количество processor
линии и разделить bogomips
по этому. Примечание убрать "." и относиться к нему как кхз и использовать целочисленную математику.
Если у вас есть constant_tsc
, TSC
всегда будет работать на этой [максимальной] частоте и никогда не будет изменяться, независимо от того, замедляется ли конкретное ядро из-за шага скорости.
Если читаешь /proc/cpuinfo
брезгливым, есть альтернативный способ калибровки / определения TSC
частота.
Сделайте следующее:
tsc1 = rdtsc
clk1 = clock_gettime
// delay for a while
for (i = 1; i < 1000000; ++i)
asm volatile ("" ::: "memory");
clk2 = clock_gettime
tsc2 = rdtsc
С этими значениями вы можете вычислить TSC
частота. Сделайте вышеперечисленное несколько тысяч раз. Возьмите минимальную дельту - это защищает от тех измерений, где время ОС вас отрезало.
Используйте наибольшее значение для значения счетчика циклов, которое не вызывает временной интервал. На самом деле, вы можете заменить цикл на nanosleep
с tv_sec = 0, tv_nsec = 500000
(500 нас). nanosleep
намного лучше, чем эквивалент usleep
, На самом деле, вы могли бы nanosleep
на 2-3 секунды, если хотите.
clk2 - clk2
значение [преобразуется] в доли секунды, дает вам калибровку для tsc2 - tsc1
и преобразование в / из TSC
тики и секунды.
Для 32-битных платформ есть "= A". Это создает 64-битный результат из eax и edx. К сожалению, на 64-битных платформах это просто означает регистр rax, который не поможет.
Вместо этого, что гораздо лучше, вы можете использовать встроенную функцию "__builtin_ia32_rdtsc()", которая напрямую возвращает 64-разрядное целое число без знака. Аналогично для rdtscp (который также возвращает текущее ядро). Смотрите руководство по gcc. Они генерируют немного лучший код, чем делают это вручную с помощью встроенного asm и переносимы между 32 и 64 битами.
Если в флагах /proc/cpuinfo установлено значение "constant_tsc", TSC работает с постоянной скоростью независимо от масштабирования частоты процессора. Если установлено "nonstop_tsc", TSC продолжает работать в состояниях C (сна). Если оба установлены, счетчики "должны" также синхронизироваться между ядрами (по крайней мере, на последних процессорах, Core i7 или новее). Я не слишком уверен насчет последнего, возможно, кто-то может меня поправить?