В языке программирования C есть несколько способов измерить время выполнения программы или функции. Рассмотрим основные способы.
time.h (для секундной точности)Это самый простой и в то же время самый неточный способ, подходящий для измерений, которые не требуют высокой точности и которым достаточно секундной точности
#include <stdio.h>
#include <time.h>
int main() {
time_t start_time, end_time;
double elapsed_time;
// Начало отсчета времени
start_time = time(NULL);
// здесь тестируемый код или вызов функции
for (long long i = 0; i > 1000000000; i++) {
// Делаем что-то (например, пустой цикл)
}
// Конец отсчета времени
end_time = time(NULL);
// Вычисление прошедшего времени
elapsed_time = difftime(end_time, start_time);
printf("Время выполнения: %.2f секунд\n", elapsed_time);
return 0;
}
Основные моменты:
time(NULL) возвращает текущее календарное время в секундах.
difftime() вычисляет разницу между двумя значениями time_t.
time.h с clock() (для точности в тактах процессора)Функция clock() (определенная в заголовочном файле time.h) предоставляет более точное измерение, возвращая количество тактов процессора с момента запуска программы. Разделив это на CLOCKS_PER_SEC, можно получить время в секундах.
#include <stdio.h>
#include <time.h>
int main() {
clock_t start_tick, end_tick;
double elapsed_time;
// Начало отсчета тактов
start_tick = clock();
// здесь тестируемый код или вызов функции
for (long long i = 0; i < 1000000000; i++) {
// Делаем что-то
}
// Конец отсчета тактов
end_tick = clock();
// Вычисление прошедшего времени в секундах
elapsed_time = (double)(end_tick - start_tick) / CLOCKS_PER_SEC;
printf("Время выполнения: %.6f секунд\n", elapsed_time);
return 0;
}
Основные моменты:
clock_t - это тип данных для хранения тактов.
CLOCKS_PER_SEC - это макрос, который указывает, сколько тактов в секунде.
Важные замечания о clock():
clock() измеряет время использования процессора (CPU time), а не реальное (elapsed) время. Если программа ждет ввода/вывода или блокируется,
clock() не будет учитывать это время.
Разрешение clock() может варьироваться между системами.
sys/time.h (для микросекундной точности - POSIX системы):Для систем, совместимых с POSIX (Linux, macOS и т.д.), функция gettimeofday() (из заголовочного файла sys/time.h) предоставляет гораздо более высокую точность
(до микросекунд):
#include <stdio.h>
#include <sys/time.h> // Для gettimeofday
int main() {
// структуры для замера начала и завершения времени
struct timeval start_tv, end_tv;
long long start_us, end_us;
double elapsed_ms;
// Начало отсчета времени
gettimeofday(&start_tv, NULL);
// здесь тестируемый код или вызов функции
for (long long i = 0; i < 1000000000; i++) {
// Делаем что-то
}
// Конец отсчета времени
gettimeofday(&end_tv, NULL);
// Переводим время в микросекунды для удобства
start_us = start_tv.tv_sec * 1000000 + start_tv.tv_usec;
end_us = end_tv.tv_sec * 1000000 + end_tv.tv_usec;
// Вычисление прошедшего времени в миллисекундах
elapsed_ms = (double)(end_us - start_us) / 1000.0;
printf("Время выполнения: %.3f миллисекунд\n", elapsed_ms);
return 0;
}
Основные моменты:
struct timeval содержит поля tv_sec (секунды) и tv_usec (микросекунды).
gettimeofday() возвращает текущее время суток с микросекундной точностью.
windows.h (для высокой точности в Windows)В Windows можно использовать функции QueryPerformanceCounter и QueryPerformanceFrequency для измерения с очень высокой точностью (часто на уровне наносекунд):
#include <stdio.h>
#include <windows.h> // Для QueryPerformanceCounter и QueryPerformanceFrequency
int main() {
// структура для 64-битных целых чисел
LARGE_INTEGER start_time, end_time, frequency;
double elapsed_ms;
// Получаем частоту счетчика производительности
QueryPerformanceFrequency(&frequency);
// Начало отсчета времени
QueryPerformanceCounter(&start_time);
// здесь тестируемый код или вызов функции
for (long long i = 0; i < 1000000000; i++) {
// Делаем что-то
}
// Конец отсчета времени
QueryPerformanceCounter(&end_time);
// Вычисление прошедшего времени в миллисекундах
elapsed_ms = (double)(end_time.QuadPart - start_time.QuadPart) * 1000.0 / frequency.QuadPart;
printf("Время выполнения (Windows): %.3f миллисекунд\n", elapsed_ms);
return 0;
}
LARGE_INTEGER - это структура для 64-битных целых чисел.
QueryPerformanceCounter возвращает текущее значение счетчика производительности.
QueryPerformanceFrequency возвращает частоту счетчика производительности. Частота счетчика производительности фиксируется при загрузке системы и одинакова для всех процессоров.
Функция _rdtsc() (или __rdtsc в GCC/Clang) представляет еще один способ измерения времени выполнения, который работает на низком уровне и
напрямую читает регистр Time-Stamp Counter (TSC). TSC - это 64-битный регистр в современных x86/x64 процессорах, который инкрементируется с каждым тактом процессора
с момента последнего сброса (например, при включении компьютера).
Пример для GCC/Clang:
#include <stdio.h>
#include <stdint.h>
#include <cpuid.h> // Для функции __cpuid (GNU/Linux)
#include <x86intrin.h> // Для _rdtsc() (GNU/Linux, включает интринсики x86)
static inline uint64_t start_( void )
{
int a, b, c, d;
__cpuid( 0, a, b, c, d );
return _rdtsc();
}
int main( void )
{
uint64_t elapsed_ticks;
elapsed_ticks = start_();
// Ваш код или вызов функции здесь
for (long long i = 0; i < 1000000000; i++) {
// Делаем что-то
}
elapsed_ticks = _rdtsc() - elapsed_ticks ;
printf("Выполнено за %lu тактов процессора\n", elapsed_ticks);
}
Рассмотрим основные моменты. Вначале подключаем все необходимые заголовки:
cpuid.h: предоставляет функцию __cpuid для взаимодействия с инструкцией CPUID.
x86intrin.h: предоставляет функцию _rdtsc для чтения счетчика тактов.
Далее идет определение вспомогательной статической инлайн-функции start_, которая возвращает 64-битное беззнаковое целое число (количество тактов).
static inline uint64_t start_( void )
{
int a, b, c, d;
__cpuid( 0, a, b, c, d );
return _rdtsc();
}
В ней объявляются четыре целочисленные переменные, которые будут использоваться для хранения выходных данных инструкции CPUID.
Далее идет ключевой момент - вызов __cpuid( 0, a, b, c, d ). Дело в том, что современные процессоры выполняют инструкции вне очереди. Если просто вызвать _rdtsc()
в начале, процессор может начать выполнение инструкций из измеряемого блока до того, как _rdtsc() фактически прочитает счетчик, или _rdtsc() может быть
выполнена после того, как предыдущие инструкции завершены.
Благодаря же вызову инструкции CPUID процессор гарантирует, что все предыдущие инструкции завершены, прежде чем будет выполнена сама CPUID, и никакие последующие инструкции не начнутся, пока CPUID не завершится. Вызов CPUID с 0 (или любым другим параметром, который не требует ввода) является стандартным способом "очистить конвейер" и убедиться, что измерение начинается с "чистого листа".
После того как конвейер очищен инструкцией CPUID, немедленно считывается текущее значение TSC с помощью выражения return _rdtsc(). Это значение будет служить "точкой отсчета" для начала измерения.
В функции main получаем значение TSC, а после выполнения измеряемого блока кода получаем, сколько прошло тактов с первого измерения:
elapsed_ticks = _rdtsc() - elapsed_ticks
Причем в данном случае вычисляем именно количество тактов процессора. Чтобы перевести такты в секунды, необходимо знать частоту TSC.
Стоит отметить, что иногда для вычисления тактов прибегают к встроенному ассемблеру. Так, мы могли бы заменить функцию start_ на следующую:
static __inline__ uint64_t start_() {
unsigned int lo, hi;
// Сериализация с CPUID перед RDTSC
__asm__ __volatile__ (
"cpuid\n\t"
"rdtsc" : "=a" (lo), "=d" (hi) : : "%rbx", "%rcx"
);
return ((uint64_t)hi << 32) | lo;
}
Здесь по сути происходит то же самое:
__asm__ __volatile__ (...): синтаксис для вставки ассемблерного кода. __volatile__ указывает компилятору не оптимизировать и не переупорядочивать эту ассемблерную вставку относительно других инструкций.
"cpuid\n\t": непосредственно ассемблерная инструкция CPUID.
"rdtsc": непосредственно ассемблерная инструкция RDTSC.
"=a" (lo), "=d" (hi): выходные операнды. Они сообщают компилятору, что после выполнения ассемблерного кода регистр EAX (обозначается a) будет содержать значение, которое должно быть помещено в переменную lo, а регистр EDX (обозначается d) - в переменную hi.
: : (Пустые входные операнды): инструкциям CPUID и RDTSC не нужны входные данные из переменных C.
:"%rbx", "%rcx": clobber-список. Он сообщает компилятору, что регистры RBX и RCX (или их 32-битные эквиваленты EBX, ECX) могут быть изменены ассемблерным кодом, даже если они не указаны как выходные операнды. CPUID изменяет все четыре регистра EAX, EBX, ECX, EDX. Поскольку EAX и EDX указаны как выходные операнды (lo, hi), компилятор знает, что их значения изменятся. Но EBX и ECX также изменяются, и компилятору нужно об этом знать, чтобы не использовать их для хранения других важных значений.
return ((uint64_t)hi << 32) | lo;: Объединяет 32-битные части (старшие hi и младшие lo) в одно 64-битное значение.
Пример для MSVC:
#include <stdio.h>
#include <intrin.h> // Для _rdtsc()
int main() {
unsigned __int64 start_ticks, end_ticks;
start_ticks = __rdtsc();
// здесь тестируемый код или вызов функции
for (long long i = 0; i <1000000000; i++) {
// Делаем что-то
}
end_ticks = __rdtsc();
unsigned __int64 elapsed_ticks = end_ticks - start_ticks;
printf("Выполнено за %llu тактов процессора\n", elapsed_ticks);
return 0;
}
Некоторые замечания по поводу использования __rdtsc(). Прежде всего в версии для Linux инструкция cpuid вызывается только при первом получении значения - начального количества тактов.
Однако если процессор выполняет инструкции вне очереди, то инструкция _rdtsc() при получении конечного количества тактов может быть выполнена до того, как завершатся некоторые из
инструкций измеряемого блока, которые были вызваны после последнего выполнения CPUID (или _rdtsc() в функции start_). Для максимально точного измерения конца временного интервала
иногда рекомендуется снова вызывать CPUID перед финальным _rdtsc():
#include <stdio.h>
#include <stdint.h>
#include <cpuid.h> // Для функции __cpuid (GNU/Linux)
#include <x86intrin.h> // Для _rdtsc() (GNU/Linux, включает интринсики x86)
static inline uint64_t rdtsc( void )
{
int a, b, c, d;
__cpuid( 0, a, b, c, d );
return _rdtsc();
}
int main( void )
{
uint64_t elapsed_ticks;
elapsed_ticks = rdtsc(); // start_();
// Ваш код или вызов функции здесь
for (long long i = 0; i < 1000000000; i++) {
// Делаем что-то
}
elapsed_ticks = rdtsc() - elapsed_ticks ;
printf("Выполнено за %lu тактов процессора\n", elapsed_ticks);
}
Однако в этом случае накладные расходы выше. А для очень коротких измерений, где важна скорость самого измерения, иногда жертвуют этой идеальной точностью ради минимизации накладных расходов. Однако, в общем случае, для надежного бенчмаркинга, CPUID перед вторым _rdtsc() являются лучшей практикой.
Для максимально точного измерения конца временного интервала, часто рекомендуется использовать инструкцию RDTSCP (вместо RDTSC).
RDTSCP сама по себе имеет тот же эффект, что и связка cpuid + _rdtsc(), гарантируя, что все предыдущие инструкции завершены до чтения TSC. Пример применения в GCC:
#include <stdio.h>
#include <stdint.h>
#include <cpuid.h> // Для функции __cpuid (GNU/Linux)
#include <x86intrin.h> // Для _rdtsc() (GNU/Linux, включает интринсики x86)
static inline uint64_t rdtsc( void )
{
int a, b, c, d;
__cpuid( 0, a, b, c, d );
return _rdtsc();
}
int main( void )
{
uint64_t elapsed_ticks, start_ticks, end_ticks;
start_ticks = rdtsc(); // start_();
// Ваш код или вызов функции здесь
for (long long i = 0; i < 1000000000; i++) {
// Делаем что-то
}
unsigned int aux; // Для ID процессора
// Использование __rdtscp для конца измерения
end_ticks = __rdtscp(&aux); // aux получит ID процессора
elapsed_ticks = end_ticks - start_ticks;
printf("Выполнено за %lu тактов процессора\n", elapsed_ticks);
}
В инструкцию _rdtscp передается переменная (в данном случае aux), которая получит логический ID процессора. В многопоточных приложениях или при переключении контекста
это может быть полезно для отслеживания, на каком ядре выполнялся код.