Измерение времени выполнения программы

Последнее обновление: 28.06.2025

В языке программирования 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() (или __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 процессора. В многопоточных приложениях или при переключении контекста это может быть полезно для отслеживания, на каком ядре выполнялся код.

Помощь сайту
Юмани:
410011174743222
Номер карты:
4048415020898850