Измерение временных интервалов в программах на языке Си

 

Для замера времени в программах на языке Си существуют несколько специальных библиотечных функций, таких как gettimeofday(), times(), clock(), : Некоторые из них являются специфичными для конкретных операционных систем, а некоторые относительно переносимы.

С измерением времени работы программы в многозадачной операционной системе, какими является Linux и Windows, связаны определенные трудности. Они обусловлены тем, что процессор всегда выполняет несколько процессов (программ) <одновременно>. Таким образом, если мы просто замерим время работы нашей программы, то в этот интервал попадут и какие-то другие процессы. Схематично это можно представить следующим образом:

 

Подпись: Процесс 1Подпись: Процесс 2Подпись: Процесс 3Подпись: Процесс 4Подпись: Процесс 5Подпись: Процесс 2Подпись: Процесс 3Подпись: Процесс 1Подпись: Процесс 5Подпись: Процесс 4Подпись: Процесс 1Подпись: Процесс 3Подпись: Процесс 2Подпись: Процесс 4Подпись: Процесс 5

 

Пусть на однопроцессорном компьютере активно выполняются 5 процессов (программ). Процессор в некотором порядке выполняет все эти программы - по одной в каждый интервал времени. Наша программа (процесс 2) запомнила значение времени до замеряемого участка кода, а потом после него, и вычислила временной интервал. На рисунке видно, что в этот интервал также попали и другие процессы, время исполнения которых нам совершенно не нужно измерять. Необходимо различать следующие времена, которые можно получить с помощью различных функций:

Рассмотрим несколько способов измерения интервалов времени.

 

1. Использование функции gettimeofday()

Функция gettimeofday() позволяет получить текущее значение системного времени. Достоинством этого способа измерения является относительно большая точность измерения.

Пример реализации в ОС Linux:

#include <sys/time.h>

struct timeval tv1,tv2,dtv;

struct timezone tz;

void time_start() { gettimeofday(&tv1, &tz); }

long time_stop()

{ gettimeofday(&tv2, &tz);

  dtv.tv_sec= tv2.tv_sec -tv1.tv_sec;

  dtv.tv_usec=tv2.tv_usec-tv1.tv_usec;

  if(dtv.tv_usec<0) { dtv.tv_sec--; dtv.tv_usec+=1000000; }

  return dtv.tv_sec*1000+dtv.tv_usec/1000;

}

 

Функция time_stop() возвращает время, прошедшее с запуска time_start(), в миллисекундах. Пример использования:

 

main()

{

     . . .

time_start();

/* какие-то действия */

printf("Time: %ld\n", time_stop());

     . . .

}

 

2. Использование функции times()

Функция times() позволяет получить текущее время процесса. Получаемое время зависит от интервала времени прерываний по таймеру, которые использует планировщик задач, например в IA-32/Linux - 10ms, Alpha/Linux - 1ms. Следовательно, недостатком этой функции является низкая точность на малых интервалах времени.

 

Пример реализации в ОС Linux:

#include <sys/times.h>

#include <time.h>

struct tms tmsBegin,tmsEnd;

void time_start() { times(&tmsBegin); }

long time_stop()

{ times(&tmsEnd);

  return ((tmsEnd.tms_utime-tmsBegin.tms_utime)+

          (tmsEnd.tms_stime-tmsBegin.tms_stime))*1000/CLK_TCK;

}

 

Функция time_stop() возвращает время, прошедшее с запуска time_start, в миллисекундах. Пример использования совпадает с приведенным выше.

 

3. Использование счетчика тактов процессора.

Практически каждый процессор имеет специальный встроенный регистр - счетчик тактов, значение которого можно получить специальной командой процессора. Команда процессора RDTSC (Read Time Stamp Counter) возвращает в регистрах EDX и EAX 64-разрядное беззнаковое целое, равное число тактов с момента запуска процессора. Вызвав эту команду до и после участка программы, для которого требуется вычислить время исполнения, можно вычислить разность показаний счетчика. Это равно числу тактов, затраченных на исполнение замеряемого участка. Для перехода от числа тактов к времени требуется умножить число тактов на время одного такта (величина, обратная тактовой частоте процессора). Для процессора с тактовой частотой 1ГГц время такта - 1 нс.

Достоинством этого способа является максимально возможная точность измерения времени.

Недостатки: команда получения числа тактов зависит от .архитектуры процессора.

 

Пример реализации в ОС Linux:

 

long long TimeValue=0;

unsigned long long time_RDTSC()

{ union ticks

  { unsigned long long tx;

    struct dblword { long tl,th; } dw; // little endian

  } t;

  asm("rdtsc\n": "=a"(t.dw.tl),"=d"(t.dw.th));

  return t.tx;

} // for x86 only!

void time_start() { TimeValue=time_RDTSC(); }

long long time_stop() { return time_RDTSC()-TimeValue; }

 

Функция time_stop возвращает число тактов процессора, прошедших с запуска time_start. Пример использования совпадает с приведенным выше.

 

4. Уменьшения влияния прочих факторов, искажающих измерения

В идеале следует замерять только саму исследуемую процедуру. Код инициализации (например, выделение памяти, заполнение массивов), деинициализации, файлового и консольного ввода и вывода должен обязательно быть вне замеряемого участка.

 

Прочие процессы в системе

Все современные ОС - многозадачные. Готовые задачи конкурируют за процессор. Кроме этого, они используют виртуальную и основную память и загружают кэш. Необходимо минимизировать число исполняемых процессов на машине в момент профилирования программы. Других счетных процессов и процессов, выделяющих много памяти, не должно быть в системе. Некоторые системные фоновые процессы устранить нельзя, они вносят искажения в измерения.

 

Кэш записи

В современных ОС существует механизм отложенной записи на диск. Рекомендуется очистить буфер записи перед запуском исследуемой программы для уменьшения влияния предыстории. В ОС Linux для этого используется команда sync. Ее также можно выполнить прямо в программе на Си:

     system("sync");