КДПВ
От переводчика:
Большинство моих знакомых для измерения времени в разного вида бенчмарках в С++ используют chrono
или, в особо запущенных случаях, ctime
. Но для бенчмаркинга гораздо полезнее замерять процессорное время. Недавно я наткнулся на статью о кроссплатформенном замере процессорного времени и решил поделиться ею тут, возможно несколько увеличив качество местных бенчмарков.
P.S. Когда в статье написано "сегодня" или "сейчас", имеется ввиду "на момент выхода статьи", то есть, если я не ошибаюсь, март 2012. Ни я, ни автор не гарантируем, что это до сих пор так.
P.P.S. На момент публикации оригинал недоступен, но хранится в кэше Яндекса
Функции API, позволяющие получить процессорное время, использованное процессом, отличаются в разных операционных системах: Windows, Linux, OSX, BSD, Solaris, а также прочих UNIX-подобных ОС. Эта статья предоставляет кросс-платформенную функцию, получающую процессорное время процесса и объясняет, какие функции поддерживает каждая ОС.
Как получить процессорное время
Процессорное время увеличивается, когда процесс работает и потребляет циклы CPU. Во время операций ввода-вывода, блокировок потоков и других операций, которые приостанавливают работу процессора, процессорное время не увеличивается пока процесс снова не начнет использовать CPU.
Разные инструменты, такие как ps
в POSIX, Activity Monitor в OSX и Task Manager в Windows показывают процессорное время, используемое процессами, но часто бывает полезным отслеживать его прямо из самого процесса. Это особенно полезно во время бенчмаркинга алгоритмов или маленькой части сложной программы. Несмотря на то, что все ОС предоставляют API для получения процессорного времени, в каждой из них есть свои тонкости.
Код
Функция getCPUTime( )
, представленная ниже, работает на большинстве ОС (просто скопируйте код или скачайте файл getCPUTime.c). Там, где это нужно, слинкуйтесь с librt, чтобы получить POSIX-таймеры (например, AIX, BSD, Cygwin, HP-UX, Linux и Solaris, но не OSX). В противном случае, достаточно стандартных библиотек.
Далее мы подробно обсудим все функции, тонкости и причины, по которым в коде столько #ifdef
'ов.
/*
* Author: David Robert Nadeau
* Site: http://NadeauSoftware.com/
* License: Creative Commons Attribution 3.0 Unported License
* http://creativecommons.org/licenses/by/3.0/deed.en_US
*/
#if defined(_WIN32)
#include <Windows.h>
#elif defined(__unix__) || defined(__unix) || defined(unix) || (defined(__APPLE__) && defined(__MACH__))
#include <unistd.h>
#include <sys/resource.h>
#include <sys/times.h>
#include <time.h>
#else
#error "Unable to define getCPUTime( ) for an unknown OS."
#endif
/**
* Returns the amount of CPU time used by the current process,
* in seconds, or -1.0 if an error occurred.
*/
double getCPUTime( )
{
#if defined(_WIN32)
/* Windows -------------------------------------------------- */
FILETIME createTime;
FILETIME exitTime;
FILETIME kernelTime;
FILETIME userTime;
if ( GetProcessTimes( GetCurrentProcess( ),
&createTime, &exitTime, &kernelTime, &userTime ) != -1 )
{
SYSTEMTIME userSystemTime;
if ( FileTimeToSystemTime( &userTime, &userSystemTime ) != -1 )
return (double)userSystemTime.wHour * 3600.0 +
(double)userSystemTime.wMinute * 60.0 +
(double)userSystemTime.wSecond +
(double)userSystemTime.wMilliseconds / 1000.0;
}
#elif defined(__unix__) || defined(__unix) || defined(unix) || (defined(__APPLE__) && defined(__MACH__))
/* AIX, BSD, Cygwin, HP-UX, Linux, OSX, and Solaris --------- */
#if defined(_POSIX_TIMERS) && (_POSIX_TIMERS > 0)
/* Prefer high-res POSIX timers, when available. */
{
clockid_t id;
struct timespec ts;
#if _POSIX_CPUTIME > 0
/* Clock ids vary by OS. Query the id, if possible. */
if ( clock_getcpuclockid( 0, &id ) == -1 )
#endif
#if defined(CLOCK_PROCESS_CPUTIME_ID)
/* Use known clock id for AIX, Linux, or Solaris. */
id = CLOCK_PROCESS_CPUTIME_ID;
#elif defined(CLOCK_VIRTUAL)
/* Use known clock id for BSD or HP-UX. */
id = CLOCK_VIRTUAL;
#else
id = (clockid_t)-1;
#endif
if ( id != (clockid_t)-1 && clock_gettime( id, &ts ) != -1 )
return (double)ts.tv_sec +
(double)ts.tv_nsec / 1000000000.0;
}
#endif
#if defined(RUSAGE_SELF)
{
struct rusage rusage;
if ( getrusage( RUSAGE_SELF, &rusage ) != -1 )
return (double)rusage.ru_utime.tv_sec +
(double)rusage.ru_utime.tv_usec / 1000000.0;
}
#endif
#if defined(_SC_CLK_TCK)
{
const double ticks = (double)sysconf( _SC_CLK_TCK );
struct tms tms;
if ( times( &tms ) != (clock_t)-1 )
return (double)tms.tms_utime / ticks;
}
#endif
#if defined(CLOCKS_PER_SEC)
{
clock_t cl = clock( );
if ( cl != (clock_t)-1 )
return (double)cl / (double)CLOCKS_PER_SEC;
}
#endif
#endif
return -1; /* Failed. */
}
Использование
Чтобы замерить процессорное время алгоритма, вызовите getCPUTime( )
до и после запуска алгоритма, и выведите разницу. Не стоит предполагать, что значение, возвращенное при единичном вызове функции, несет какой-то смысл.
double startTime, endTime;
startTime = getCPUTime( );
...
endTime = getCPUTime( );
fprintf( stderr, "CPU time used = %lf\n", (endTime - startTime) );
Обсуждение
Каждая ОС предоставляет один или несколько способов получить процессорное время. Однако некоторые способы точнее остальных.
OS | clock | clock_gettime | GetProcessTimes | getrusage | times |
---|---|---|---|---|---|
AIX | yes | yes | yes | yes | |
BSD | yes | yes | yes | yes | |
HP-UX | yes | yes | yes | yes | |
Linux | yes | yes | yes | yes | |
OSX | yes | yes | yes | ||
Solaris | yes | yes | yes | yes | |
Windows | yes |
Каждый из этих способов подробно освещен ниже.
GetProcessTimes( )
На Windows и Cygwin (UNIX-подобная среда и интерфейс командной строки для Windows), функция GetProcessTimes( ) заполняет структуру FILETIME процессорным временем, использованным процессом, а функция FileTimeToSystemTime( ) конвертирует структуру FILETIME в структуру SYSTEMTIME, содержащую пригодное для использования значение времени.
typedef struct _SYSTEMTIME
{
WORD wYear;
WORD wMonth;
WORD wDayOfWeek;
WORD wDay;
WORD wHour;
WORD wMinute;
WORD wSecond;
WORD wMilliseconds;
} SYSTEMTIME, *PSYSTEMTIME;
Доступность GetProcessTimes( ): Cygwin, Windows XP и более поздние версии.
Получение процессорного времени:
#include <Windows.h>
...
FILETIME createTime;
FILETIME exitTime;
FILETIME kernelTime;
FILETIME userTime;
if ( GetProcessTimes( GetCurrentProcess( ),
&createTime, &exitTime, &kernelTime, &userTime ) != -1 )
{
SYSTEMTIME userSystemTime;
if ( FileTimeToSystemTime( &userTime, &userSystemTime ) != -1 )
return (double)userSystemTime.wHour * 3600.0 +
(double)userSystemTime.wMinute * 60.0 +
(double)userSystemTime.wSecond +
(double)userSystemTime.wMilliseconds / 1000.0;
}
clock_gettme( )
На большинстве POSIX-совместимых ОС, clock_gettime( )
(смотри мануалы к AIX, BSD, HP-UX, Linux и Solaris) предоставляет самое точное значение процессорного времени. Первый аргумент функции выбирает "clock id", а второй это структура timespec
, заполняемая использованным процессорным временем в секундах и наносекундах. Для большинства ОС, программа должна быть слинкована с librt.
Однако, есть несколько тонкостей, затрудняющих использование этой функции в кросс-платформенном коде:
- Функция является опциональной частью стандарта POSIX и доступна только если
_POSIX_TIMERS
определен в<unistd.h>
значением больше 0. На сегодняшний день, AIX, BSD, HP-UX, Linux и Solaris поддерживают эту функцию, но OSX не поддерживает. - Структура
timespec
, заполняемая функциейclock_gettime( )
может хранить время в наносекундах, но точность часов отличается в разных ОС и на разных системах. Функция clock_getres( ) возвращает точность часов, если она вам нужна. Эта функция, опять-таки, является опциональной частью стандарта POSIX, доступной только если_POSIX_TIMERS
больше нуля. На данный момент, AIX, BSD, HP-UX, Linux и Solaris предоставляют эту функцию, но в Solaris она не работает. - стандарт POSIX определяет имена нескольких стандартных значений "clock id", включая
CLOCK_PROCESS_CPUTIME_ID
, чтобы получить процессорное время процесса. Тем не менее, сегодня BSD и HP-UX не имеют этого id, и взамен определяют собственный idCLOCK_VIRTUAL
для процессорного времени. Чтобы запутать все ещё больше, Solaris определяет оба этих, но используетCLOCK_VIRTUAL
для процессорного времени потока, а не процесса.
ОС | Какой id использовать |
---|---|
AIX | CLOCK_PROCESS_CPUTIME_ID |
BSD | CLOCK_VIRTUAL |
HP-UX | CLOCK_VIRTUAL |
Linux | CLOCK_PROCESS_CPUTIME_ID |
Solaris | CLOCK_PROCESS_CPUTIME_ID |
- Вместо того, чтобы использовать одну из констант, объявленных выше, функция clock_getcpuclockid( ) возвращает таймер для выбранного процесса. Использование процесса 0 позволяет получить процессорное время текущего процесса. Однако, это ещё одна опциональная часть стандарта POSIX и доступна только если
_POSIX_CPUTIME
больше 0. На сегодняшний день, только AIX и Linux предоставляют эту функцию, но линуксовские include-файлы не определяют_POSIX_CPUTIME
и функция возвращает ненадёжные и несовместимые с POSIX результаты. - Функция
clock_gettime( )
может быть реализована с помощью регистра времени процессора. На многопроцессорных системах, у отдельных процессоров может быть несколько разное восприятие времени, из-за чего функция может возвращать неверные значения, если процесс передавался от процессора процессору. На Linux, и только на Linux, это может быть обнаружено, еслиclock_getcpuclockid( )
возвращает не-POSIX ошибку и устанавливаетerrno
вENOENT
. Однако, как замечено выше, на Linuxclock_getcpuclockid( )
ненадежен.
На практике из-за всех этих тонкостей, использование clock_gettime( )
требует много проверок с помощью #ifdef
и возможность переключиться на другую функцию, если она не срабатывает.
Доступность clock_gettime( ): AIX, BSD, Cygwin, HP-UX, Linux и Solaris. Но clock id на BSD и HP-UX нестандартные.
Доступность clock_getres( ): AIX, BSD, Cygwin, HP-UX и Linux, но не работает Solaris.
Доступность clock_getcpuclockid( ): AIX и Cygwin, не недостоверна на Linux.
Получение процессорного времени:
#include <unistd.h>
#include <time.h>
...
#if defined(_POSIX_TIMERS) && (_POSIX_TIMERS > 0)
clockid_t id;
struct timespec ts;
#if _POSIX_CPUTIME > 0
/* Clock ids vary by OS. Query the id, if possible. */
if ( clock_getcpuclockid( 0, &id ) == -1 )
#endif
#if defined(CLOCK_PROCESS_CPUTIME_ID)
/* Use known clock id for AIX, Linux, or Solaris. */
id = CLOCK_PROCESS_CPUTIME_ID;
#elif defined(CLOCK_VIRTUAL)
/* Use known clock id for BSD or HP-UX. */
id = CLOCK_VIRTUAL;
#else
id = (clockid_t)-1;
#endif
if ( id != (clockid_t)-1 && clock_gettime( id, &ts ) != -1 )
return (double)ts.tv_sec +
(double)ts.tv_nsec / 1000000000.0;
#endif
getrusage( )
На всех UNIX-подобных ОС, функция getrusage( ) это самый надежный способ получить процессорное время, использованное текущим процессом. Функция заполняет структуру rusage временем в секундах и микросекундах. Поле ru_utime
содержит время проведенное в user mode, а поле ru_stime
— в system mode от имени процесса.
Внимание: Некоторые ОС, до широкого распространения поддержки 64-бит, определяли функцию getrusage( )
, возвращающую 32-битное значение, и функцию getrusage64( )
, возвращающую 64-битное значение. Сегодня, getrusage( )
возвращает 64-битное значение, аgetrusage64( )
устарело.
Доступность getrusage( ): AIX, BSD, Cygwin, HP-UX, Linux, OSX, and Solaris.
Получение процессорного времени:
#include <sys/resource.h>
#include <sys/times.h>
...
struct rusage rusage;
if ( getrusage( RUSAGE_SELF, &rusage ) != -1 )
return (double)rusage.ru_utime.tv_sec +
(double)rusage.ru_utime.tv_usec / 1000000.0;
times( )
На всех UNIX-подобных ОС, устаревшая функция times( ) заполняет структуру tms
с процессорным временем в тиках, а функция sysconf( ) возвращает количество тиков в секунду. Поле tms_utime
содержит время, проведенное в user mode, а поле tms_stime
— в system mode от имени процесса.
Внимание: Более старый аргумент функции sysconf( )
CLK_TCK
устарел и может не поддерживаться в некоторых ОС. Если он доступен, функция sysconf( )
обычно не работает при его использовании. Используйте _SC_CLK_TCK
вместо него.
Доступность times( ): AIX, BSD, Cygwin, HP-UX, Linux, OSX и Solaris.
Получение процессорного времени:
#include <unistd.h>
#include <sys/times.h>
...
const double ticks = (double)sysconf( _SC_CLK_TCK );
struct tms tms;
if ( times( &tms ) != (clock_t)-1 )
return (double)tms.tms_utime / ticks;
clock( )
На всех UNIX-подобных ОС, очень старая функция clock( ) возвращает процессорное время процесса в тиках, а макрос CLOCKS_PER_SEC
количество тиков в секунду.
Заметка: Возвращенное процессорное время включает в себя время проведенное в user mode И в system mode от имени процесса.
Внимание: Хотя изначально CLOCKS_PER_SEC
должен был возвращать значение, зависящее от процессора, стандарты C ISO C89 и C99, Single UNIX Specification и стандарт POSIX требуют, чтобы CLOCKS_PER_SEC
имел фиксированное значение 1,000,000, что ограничивает точность функции микросекундами. Большинство ОС соответствует этим стандартам, но FreeBSD, Cygwin и старые версии OSX используют нестандартные значения.
Внимание: На AIX и Solaris, функция clock( )
включает процессорное время текущего процесса И и любого завершенного дочернего процесса для которого родитель выполнил одну из функций wait( )
, system( )
или pclose( )
.
Внимание: В Windows, функция clock( ) поддерживается, но возвращает не процессорное, а реальное время.
Доступность clock( ): AIX, BSD, Cygwin, HP-UX, Linux, OSX и Solaris.
Получение процессорного времени:
#include <time.h>
...
clock_t cl = clock( );
if ( cl != (clock_t)-1 )
return (double)cl / (double)CLOCKS_PER_SEC;
Другие подходы
Существуют и другие ОС-специфичные способы получить процессорное время. На Linux, Solarisи некоторых BSD, можно парсить /proc/[pid]/stat, чтобы получить статистику процесса. На OSX, приватная функция API proc_pidtaskinfo( )
в libproc
возвращает информацию о процессе. Также существуют открытые библиотеки, такие как libproc, procps и Sigar.
На UNIX существует несколько утилит позволяющих отобразить процессорное время процесса, включая ps, top, mpstat и другие. Можно также использовать утилиту time, чтобы отобразить время, потраченное на команду.
На Windows, можно использовать диспетчер задач, чтобы мониторить использование CPU.
На OSX, можно использовать Activity Monitor, чтобы мониторить использование CPU. Утилита для профайлинга Instruments поставляемая в комплекте с Xcode может мониторить использование CPU, а также много других вещей.
Downloads
- getCPUTime.c реализует выше описанную функцию на C. Скомпилируйте её любым компилятором C и слинкуйте с librt, на системах где она доступна. Код лицензирован под Creative Commons Attribution 3.0 Unported License.
Смотри также
Связанные статьи на NadeauSoftware.com
- C/C++ tip: How to measure elapsed real time for benchmarking объясняет как получить реальное время, чтобы измерить прошедшее время для куска кода, включая время, потраченное на I/O или пользовательский ввод.
- C/C++ tip: How to use compiler predefined macros to detect the operating system объясняет как использовать макросы
#ifdef
для ОС-специфичного кода. Часть из этих методов использовано в этой статье, чтобы определить Windows, OSX и варианты UNIX.
Статьи в интернете
- Процессорное время на википедии объясняет, что такое процессорное время.
- CPU Time Inquiry на GNU.org объясняет как использовать древнюю функцию clock( ).
- Determine CPU usage of current process (C++ and C#) предоставляет код и объяснения для получения процессорного времени и другой статистики на Windows.
- Posix Options на Kernel.org объясняет опциональные фичи и константы POSIX, включая _POSIX_TIMERS и _POSIX_CPUTIME.
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Комментарии (69)
jcmvbkbc
24.04.2016 05:19+2Как вы замеряете время в своих бенчмарках?
perf stat
Зачем в виндовом коде следующий фрагмент:
if ( FileTimeToSystemTime( &userTime, &userSystemTime ) != -1 ) return (double)userSystemTime.wHour * 3600.0 + (double)userSystemTime.wMinute * 60.0 + (double)userSystemTime.wSecond + (double)userSystemTime.wMilliseconds / 1000.0;
Почему не просто
ULARGE_INTEGER li = {{userTime.dwLowDateTime, userTime.dwHighDateTime }}; return li.QuadPart / 100000000000.;
Randl
24.04.2016 12:14Так как код не мой, то точно ответить вам не могу. Судя по всему, вы правы и результат будет таким же, разве что делить надо на другое число и взять остаток.
ULARGE_INTEGER li = {{userTime.dwLowDateTime, userTime.dwHighDateTime }}; return (li.QuadPart % 864000000000) / 10000000.;
Zealint
24.04.2016 09:00-1Я использую стороннюю утилиту runexe (работаю в Windows). Она же или аналогичная ей используется на некоторых олимпиадных серверах.
Randl
24.04.2016 12:19+1Отвечу вам, хотя ответ для всех, кто использует сторонние утилиты в бенчмарках.
Почему утилита а не функция? Ведь автоматизировать замеры с функцией обычно гораздо легче, чем с утилитой.
Zealint
24.04.2016 13:03-3Совершенно не легче. Во-первых, подобные утилиты обычно работают везде, где их можно скомпилировать, по идее их должны поддерживать их разработчики (runexe, в принципе, не требует поддержки сейчас — она работает). Что касается таких вот жутких функций, то встраивание их в мой код делает программу сложнее и не даёт гарантий, что она запустится у кого-то другого. Это против моих правил — давать код, который тяжело скомпилировать (одна из причин, по которой я плохо отношусь к опенсорсу — это трудности с компиляцией, возникающие по причине доступа к коду тех людей, которых это мало заботит). Во-вторых, указанная мною утилита может также правильно замерять использованную единовременно память, что тоже важно. В-третьих, включать таймер до цикла тестирования и выключать после — это ничем по сути не отличается от тестирования внешней утилитой. В-четвертых, я не знаю, какие побочные эффекты порождают вот такие отдельные функции и как они влияют на измерение. Короче, я считаю, что в любом эксперименте измерительный прибор должен быть как можно дальше от опытного образца, не только в программировании. Ну и в-пятых, дело привычки.
Поэтому когда я пишу код для других людей, то, к сожалению, приходится замеры делать через chono, в надежде, что когда-нибудь оно начнёт работать везде. Дома использую только runexe.Randl
24.04.2016 13:26+4Что касается таких вот жутких функций, то встраивание их в мой код делает программу сложнее и не даёт гарантий, что она запустится у кого-то другого. Это против моих правил — давать код, который тяжело скомпилировать
Почему бы ей не скомпилироваться, если у нее никаких зависимостей, кроме стандартной билиотеки? Что тяжелого в компиляции, которая сводится к запуску компилятора?
runexe, в принципе, не требует поддержки сейчас
И, в принципе, не поддерживается, судя по сайту на мертвом Google Code и отсутсвию даже минимальной инструкции на этом самом сайте.
включать таймер до цикла тестирования и выключать после — это ничем по сути не отличается от тестирования внешней утилитой.
Отличается простотой автоматизации. Залез в ваш аккаунт и обнаружил, что как раз в вашей статье это всплыло.
Если ставить целью сделать код, который будет измерять процессорное время на любой машине и везде запускаться, то я затрачу уйму времени.
Собственно вот он это код.
Zealint
24.04.2016 14:08-2Вы ответили лишь на половину моих замечаний, при этом недооцениваете мой способ работы дома и то, что лично мне удобнее. Я лишь ответил на ваш тезис "Ведь автоматизировать замеры с функцией обычно гораздо легче, чем с утилитой", ответив, что лично мне не легче. Я с подозрением отношусь к внедрению стороннего кода в свой, в 99% случаев это приводит к пустой потере времени (в силу доминирующей криворукости создателей подобного опенсорса). Это лишь моё мнение. Вопрос практической полезности подобного кода решается ТОЛЬКО через практику.
Что касается моих проблем, которые я мог бы попытаться решить этим готовым кодом — а именно, подготовка для других людей готового к запуску проекта — это нужно пробовать решать в ходе исследования. На Вашем месте я бы так и сделал: состряпал бы программу с тестированием некоторых интересных функций, и попросил бы пользователей БЕЗ КАКИХ ЛИБО ЛИШНИХ УСИЛИЙ скомпилировать из запустиь её, затем показать вывод на консоль. Если это сработает, я буду рад таком подходу… однако это не изменит моих личных привычек и избавить от необходимости измерять иногда память.
Что касается автоматизации, то со сторонними утилитами она решается, но иным способом, например, через cmd-файл.
Если за мои эксперименты проголосует больше 80% участников, я могу попробовать данный подход в тестировании. Только тогда станет ясно, стоит игра свеч или нет. Я НЕ спорю, что это может работать, я лишь ответил, почему не считаю это решение более лёгким. Ответ мой можно опровергнуть только на практике. Тут как бы спорить не имеет смысла даже.Randl
24.04.2016 14:18+1Вы ответили лишь на половину моих замечаний, при этом недооцениваете мой способ работы дома и то, что лично мне удобнее.
Я говорил конкретно об автоматизации бенчмарка — "запустил и пошел спать". Хотя вы и ответили, что автоматизировать со встроенной функцией не легче, но та самая половина замечаний отношения к автоматизации не имеет.
в силу доминирующей криворукости создателей подобного опенсорса
Код включает в себя вызов исключительно системных функций. При этом от криворукости создателя runexe вас никто не спасает, кроме вашей веры в него.
Что касается автоматизации, то со сторонними утилитами она решается, но иным способом, например, через cmd-файл
Если вы хотите сравнить две версии алгоритма, то в лоб еще скриптом не автоматизируешь. Или держать два проекта, или собирать вручную, или использовать аргументы при запуске.
Zealint
24.04.2016 14:24Так а Вы думаете, что у меня подход чем-то отличается от «запустил и пошёл спать»?: )
Код включает в себя вызов исключительно системных функций. При этом от криворукости создателя runexe вас никто не спасает, кроме вашей веры в него.
С этим кодом я и не спорю (пока), я выразил мнение о том, чем закачивается 99% попыток внедрить в свой код что-то другое. По поводу runexe — у меня пока не было нареканий к нему, так что это дело не веры, а проверки на сотнях тысячах запусках. А предложенные Вами код я пока не проверял. Когда проверю, тогда будет видно.
Если вы хотите сравнить две версии алгоритма, то в лоб еще скриптом не автоматизируешь. Или держать два проекта, или собирать вручную, или использовать аргументы при запуске.
Эта проблема уже давно решена. Поэтому я и говорю, что лично мне не легче. Легче будет, возможно, сделать универсальный код для других пользователей, но это, повторюсь, нужно проверять на практике. Как я уже сказал, что если сработает, буду рад. Но почему-то я сомневаюсь пока.Randl
24.04.2016 14:33Так а Вы думаете, что у меня подход чем-то отличается от «запустил и пошёл спать»? :)
Эта проблема уже давно решена.
Думаю, что или вы пишете для этого дополнительный код (скрипты, аргументы), или делаете часть работы вручную (как в вышеупомянутой статье, перекомпилируете вручную N раз)
С этим кодом я и не спорю (пока), я выразил мнение о том, чем закачивается 99% попыток внедрить в свой код что-то другое.
Ну чужой код надо читать, а не бездумно копировать.
По поводу runexe — у меня пока не было нареканий к нему, так что это дело не веры, а проверки на сотнях тысячах запусках.
А вы каким-то образом его проверяли? Или это "по ощущениям"? Если бы вы могли мерить процессорное время "по ощущениям", то и программа не нужна была наверное ;) Вдруг там лишних 5-10% намеряется в особых случаях.
Zealint
24.04.2016 14:55Мне кажется, Вы напрасно уводите разговор в сторону. То, как я работаю дома, это вряд ли кому-то интересно, но раз я сказал, что у меня нет проблем с автоматизацией, значит их нет. Да, там есть скрипты, да, там есть разные параметры перекомпиляции, это всё, что я могу сообщить.
Чужой код, конечно, нужно читать, когда это возможно. Например, в Вашем случае это невозможно. Потому что этот код вызывает функции системы. Если ту часть кода линукса, которая отвечает за таймер, писал какой-нибудь школьник в рамках своей курсовой работы, то эту функцию тоже нужно детально изучать — это тянет за собой ещё кучу ненужной работы. Так ведь? И потом только мы получим гарантию, функция из Вашей статьи работает правильно. Так или нет?
Вопрос риторический, конечно. Что касается вопроса корректности тестирования с помощью runexe, то здесь всё просто — меня устраивает время, которое она выдаёт при тестировании. Есть ряд традиционных методов, изучаемых на курсах истории и методологии науки, по которым можно считать некое знание достоверным или недостоверным. Все эти методы мне известны и они работают. Абсолютной уверенности в том, что runexe работает лучше или хуже системных функций нет никаких. То же самое можно сказать о системных функциях — они тоже работают «как-то» и нам остаётся верить, что работают правильно. Это философский вопрос… если тянуть его дальше, то возникнет другой вопрос: правильно ли мы вообще живём и какова (и откуда) степень нашей уверенности в этом?
Я уже говорил, что предложенный здесь Вами подход нужно детально изучать на практике. А я по природе своей (и тому есть объяснение) всегда НЕ доверяю ЛЮБЫМ чужим программам, пока не проверю их на практике — отсюда и мой кажущийся скептицизм. Не знаю, что Вы ещё от меня хотите :) Я уже сказал, что попытаюсь всё это проверить массово и сделать выводы. Хотя это могли бы сделать и Вы сами.Randl
24.04.2016 15:24Мне кажется, Вы напрасно уводите разговор в сторону. То, как я работаю дома, это вряд ли кому-то интересно
Ну так и то, что в вашем конкретном случае, после того, как вы проделали некоторую работу, вы смогли автоматизировать runexe никак не значит, что автоматизировать метод из статьи не легче.
И потом только мы получим гарантию, функция из Вашей статьи работает правильно. Так или нет?
Фактически — так. Практически, лично я доверяю системным вызовам и стандартным библиотекам, например.
Абсолютной уверенности в том, что runexe работает лучше или хуже системных функций нет никаких.
Runexe, очевидно, использует системные функции. В частности, версия на которую ссылаетесь вы, использует QueryInformationJobObject, хотя на данный момент, судя по всему, всё уже переписано на Go.
Не знаю, что Вы ещё от меня хотите :)
Собственно от вас ничего больше и не хочу. Весь спор начался из-за вашего необоснованного предположения, что если у вас все работает, значит ваш метод не сложнее.
Zealint
24.04.2016 16:45Не-не-не, Евгений. Вы уж простите, если я как-то не так слова подобрал, но я имел в виду, что У МЕНЯ ЛИЧНО runexe уже хорошо работает, и поэтому мне не сложнее делать так, как уже делается. Кому-то другому, наверное, удобнее взять готовое. И потом я пока ещё не убедился, что предложенный Вами метод действительно будет работать — это нужно проверять. В данном случае практика — критерий истинности.
А в целом по статье всё нормально, я Вам очень благодарен за проделанную работу (перевод и популяризация). Одно замечание: название неверное. «Как измерять процессорное время» — это намёк на методологию самого процесса измерения, Вы же даёте готовое решение, написанное каким-то мужиком. То есть предлагаете инструмент, а не методологию. Но это имхо.
Ivan_83
25.04.2016 01:28А как же погрешность измерений, когда некто может собрать прогу без сишного рантайма и получить выигрыш по времени за счёт этого? (это приминительно к олимпиадным чужим прогам)
BalinTomsk
24.04.2016 09:21+1в windows лучше пользоваться
::QueryPerformanceCounter( &perfCntStart );
::QueryPerformanceFrequency( &proc_freq );
что-то делаем
::QueryPerformanceCounter( &perfCntStop );
float result = float( perfCntStop.QuadPart — perfCntStart.QuadPart );
::QueryPerformanceCounter( &perfCntStop );IRainman
24.04.2016 22:07С тех пор как в процессорах появилось больше одного потока (не важно ядра или HT) с помощью QueryPerformanceCounter считать нельзя, а с тех пор как процессоры стали в ходе работы изменять свою частоту вызов QueryPerformanceFrequency показывает текущую частоту и только одного ядра, а в итоговых вычислениях по такой формуле получается, скажем так, какая то дичь.
Victor_Grigoryev
24.04.2016 10:49OpenMP и omp_get_wtime()/omp_get_tick(). В академических целях для вопросов параллелизма, правда, но почему бы и нет?
Temtaime
24.04.2016 10:49+4Забавно. Способы измерения процессорного времени на C++, а про RDTSC забыли.
Только им и измеряем.Randl
24.04.2016 10:57clock_gettime()
теоретически может использовать RDTSC.
На многоядерных/многопроцессорных системах RDTSC вроде как ненадежен.
vadimr
24.04.2016 10:58+1Для Intel-совместимых процессоров, и если обеспечить работу процесса без перекидывания с одного процессора на другой, RDTSC является самым правильным методом.
vadimr
24.04.2016 11:03Для целей бенчмаркинга это неважно, но, раз уж в заголовок статьи вынесен вопрос об измерении времени, было бы неплохо упомянуть, что тактовые частоты процессора и таймеров в обычном компьютере сами по себе испытывают определённые отклонения от паспортных значений. Поэтому, если вы хотите измерить реальное физическое время, то необходимо как-то привязываться к внешнему источнику синхронизации (калиброванному генератору частоты или интернетовскому / спутниковому источнику единого времени).
В общем, статья скорее освещает тему “как написать переносимый враппер над функциями библиотеки Си для получения времени”, чем “как измерять время”.Randl
24.04.2016 11:22Поэтому, если вы хотите измерить реальное физическое время
Мы хотим измерить процессорное время.
vadimr
24.04.2016 11:32А что Вы понимаете под “процессорным временем”?
Формально, термин “процессорное время” означает реальное время, занятое выполнением задачи на процессоре, то есть сумму предоставленных ей (или в её интересах) квантов времени.
Если Вы говорите о “процессорном времени” в смысле привязки к единицам тактовой частоты процессора вместо реальных секунд, то даже для бенчмаркинга это требует определённых оговорок, как и вообще всякие махинации с единицами измерения. Обычно, пользователя всё-таки интересует, какова производительность системы, приведённой к реальным единицам времени, а не с использованием удлинённых или укороченных секунд наугад взятого процессора с поехавшим тактовым генератором.
Конечно, если вы своим бенчмаркингом сравниваете два процесса на одном и том же процессоре (и даже одной и той же модели процессора), то это неважно. Но если вы хотите как-то распространить результат на другие системы, то единицы процентов, которые при известном стечении обстоятельств можно получить за счёт погрешности генератора, могут оказаться заслуживающими внимания.Randl
24.04.2016 11:46Обычно, пользователя всё-таки интересует, какова производительность системы, приведённой к реальным единицам времени, а не с использованием удлинённых или укороченных секунд наугад взятого процессора с поехавшим тактовым генератором.
Обычно пользователя интересует субъективные ощущения от работы (быстро/медленно, тормозит/не тормозит), а не процессорное время.
Разработчика же интересуют либо величины в сравнении (два алгоритма, текущая версия vs. предыдущая и т.д.), либо порядок величины, т.к. скорость работы будет меняться от системы к системе достаточно сильно.
Но если вы хотите как-то распространить результат на другие системы, то единицы процентов, которые при известном стечении обстоятельств можно получить за счёт погрешности генератора, могут оказаться заслуживающими внимания.
Если я хочу распространить результат на конкретную другую систему, возможно. Может быть, при разработке какой-то супер критичной системы реального времени, где еле-еле получается уложится в выделенное время, это может сыграть злую шутку.
Но если я разрабатываю массовый софт и хочу прикинуть время выполнения на множестве других произвольных систем ("как это будет работать у пользователей?"), то эта погрешность будет незаметна.vadimr
24.04.2016 11:52Если вы хотите просто прикинуть порядок величины на конкретной системе, тогда вообще неважно, что и как вы меряете, а заодно и вся рассмотренная переносимость вам не нужна.
В общем, на мой взгляд, такая важная и сложная тема, как измерение времени на компьютере, раскрыта автором оригинальной статьи весьма поверхностно.Randl
24.04.2016 12:08Лично я обычно меряю время в бенчмарках, то есть сравниваю результаты работы разных алгоритмов, или версий программы, или программ. Меня мало интересует абсолютная точность, даже если эта функция будет ошибаться каждый раз на 10%, это не важно так как относительные результаты будут верными.
Цели же рассмотреть в общем такую важную и сложную тему, как измерение времени на компьютере, автор перед собой не ставил. Он только описал, как решить конкретную проблему: кроссплатформенно замерить процессорное время процесса. Если вы можете предложить лучшее решение этой проблемы, буду рад его услышать.
vadimr
24.04.2016 12:25+1Лучшее решение предложили коллеги в предыдущем комментарии – использовать ассемблерную вставку с RDTSC и не париться (включая приведённую по ссылке статью с методическими рекомендациями Intel, как вообще правильно организовывать работу с таймером). А прежде, чем замахиваться на кроссплатформенность, автору лучше было бы для начала разобраться поглубже хотя бы с одной платформой.
Ещё навскидку темы, которые хотелось бы видеть освещёнными в подобной статье:
– оценка случайной и систематической составляющей погрешности измерения;
– как момент вызова функции и момент возврата из функции соотносятся с моментом фиксации показаний таймера (в том числе, чему равен оверхед от её собственной работы, как разность между этими значениями);
– что происходит, когда ОС в процессе бенчмаркинга синхронизирует время по NTP и начинает подводить таймеры;
– что происходит при переполнении коротких таймеров;
– там автор в коде зачем-то работает с минутами и часами, что вызывает очевидный вопрос, что происходит в его функции с переходом через полночь, и менее очевидный вопрос, что происходит с удлинёнными минутами по 61 секунде (чем дальше, тем они появляются чаще в связи с удлинением астрономических суток).Randl
24.04.2016 12:34использовать ассемблерную вставку с RDTSC и не париться
А если у меня не только х86 или вообще не х86?
как момент вызова функции и момент возврата из функции соотносятся с моментом фиксации показаний таймера (в том числе, чему равен оверхед от её собственной работы, как разность между этими значениями);
Могу лишь предположить, что общий оверхед будет равен ровно одному вызову функции (мы считаем от фиксации до фиксации, в начальном измерении после фиксации будет учтен "конец" функции, в конечном — будет учтено "начало" функции)
что вызывает очевидный вопрос, что происходит в его функции с переходом через полночь, и менее очевидный вопрос, что происходит с удлинёнными минутами по 61 секунде (чем дальше, тем они появляются чаще в связи с удлинением астрономических суток).
Ничего не происходит, читайте внимательнее код.
vadimr
24.04.2016 12:49А если у меня не только х86 или вообще не х86?
Тогда к чему проблематика Windows?
Собственно, счётчик в процессоре есть во многих архитектурах.
Могу лишь предположить, что общий оверхед будет равен ровно одному вызову функции (мы считаем от фиксации до фиксации, в начальном измерении после фиксации будет учтен «конец» функции, в конечном — будет учтено «начало» функции)
Так вот и хочется знать, в частности, чему равен этот вызов функции в микросекундах (учитывая, что он включает неизвестные нам действия внутри libc и ядра).
Ничего не происходит, читайте внимательнее код.
return (double)userSystemTime.wHour * 3600.0 +
(double)userSystemTime.wMinute * 60.0 +
(double)userSystemTime.wSecond +
(double)userSystemTime.wMilliseconds / 1000.0;
Что здесь произойдёт, когда процесс работает более 24 часов?Randl
24.04.2016 12:55Так вот и хочется знать, в частности, чему равен этот вызов функции в микросекундах (учитывая, что он включает неизвестные нам действия внутри libc и ядра).
Так как это зависит от кучи параметров, надо просто сделать замер на той системе, на которой это вас интересует.
Что здесь произойдёт, когда процесс работает более 24 часов?
В худшем случае — вы получите результат, на 86400 отличающийся от реального. Думаю, это не так уж и сложно заметить.
vadimr
24.04.2016 13:01На 86400*n.
Вот я и говорю – проработка материала в статье на уровне сообразительного школьника. Конечно, есть определённое рациональное зерно в том, что все упомянутые функции действительно существуют и их при случае можно использовать. Но так вот бездумно приравнивать друг к другу в условной компиляции разные по своей сути методы получения времени – если и возможно, то, по крайней мере, это требует значительного количества методических комментариев.Randl
24.04.2016 13:17+1На 86400*n.
Это теория. На практике большая часть бенчмарков работает меньше суток, замеряет отрезки времени меньше суток, не требует абсолютной точности и т.д.
Ну а тот, у кого нестандартные задачи и кто гоняет задачи, съедающие сутки процессорного времени и при этом требующие микросекундной точности, должен разбираться сам.
Вот я и говорю – проработка материала в статье на уровне сообразительного школьника.
Так еще раз — никто и не претендует. Функция будет работать на среднем тесткейсе, запустится
на любой платформе (в отличие от вашего варианта, который даже на достаточно популярном ARM не пойдет). Да, наверняка можно придумать кучу кода, на которой она будет давать неточные результаты. Но при этом заморачиваться не надо, скопировал функцию, два раза вызвал — и вот они результаты. Если же нужно что-то супер-точное — пиши свой код, подходящий под конкретный случай.vadimr
24.04.2016 13:24-2Вы спорите ради флейма. Пока я не могу понять даже того, какой вообще смысл имеет приравнивание друг к другу времени исполнения пользовательского процесса в Linux, OS X и Windows, если у них совершенно разное разделение нагрузки между user- и kernel-space. Так что про кроссплатформенность здесь вообще лучше не вспоминать.
alexeibs
24.04.2016 18:38+1Если нужно просто прикинуть время выполнения, то зачем вообще измерять процессорное время? Закрыть по максимуму все программы, запустить тест несколько раз, отбросить тесты с аномальными результатами (если есть) и вычислить среднее значение.
Zealint
24.04.2016 19:01Есть несколько причин. Самая важная: при длительном тестировании у вас нет никаких гарантий, что в середине пути ОС вдруг не захочет что-нибудь сделать, что повлияет на результат (гарантировать на 100%, что этого не произойдёт, нельзя). При этом программа может работать весьма долго и перетестировать её (минимум трижды) довольно трудно. Далее, например, на олимпиаде по программированию один компьютер может запускать сразу несколько программ единовременно, нужно точно учитывать, сколько отработала каждая, с учётом разделения времени с конкурентами. Даже при отключённых лишних программах разница на несложных тестах может оказаться равной секунде или двум (в виндоусе). Так что как ни крути, процессорное время чище.
alexeibs
24.04.2016 19:19Я писал о «прикинуть время выполнения». Измерение времени выполнения олимпиадных программ — это совершенно другой сценарий.
Zealint
24.04.2016 19:58Ну, смотря как «прикинуть». Если совсем не точно, то достаточно просто запустить и время от времени смотреть на часы. Если прикидка должна быть довольно точной, а программа работает долго, то можно параллельно работать на компьютере с другими вычислениями, которые могут занимать массу времени. В этом случае процессорное время необходимо, иначе получите, что программа работает час, а астрономическое время будет 2-3 часа. Так что при параллельной работе всё равно это важно.
vadimr
24.04.2016 20:10+2Если программа выполняет много системных вызовов, то значительная часть времени её выполнения приходится на работу ядра. В этом случае пользовательское время даст вам заниженный результат даже по процессору. Тем более это справедливо в отношении ожидания ввода-вывода.
Реального способа выделить работу в интересах одного приложения из общей нагрузки на PC/Mac нет.
DistortNeo
24.04.2016 13:27Одна из основных проблем — это не точность часов, не измерение процессорного времени вместо реального, а переменная производительность современных процессоров, связанная со сбросом частоты в целях снижения энергопотребления.
Для оценки времени выполнения коротких вычислительных задач (до 1 секунды) на процессорах Intel/AMD ни один из описанных в статье методов при применении «в лоб» не даст адекватных результатов, если не выключен, например, EIST для процессоров Intel, т.к. разброс будет двух-, а то и трёхкратным.
В этом случае поможет только грамотное использование RDTSC. Несмотря на то, что на выходе будет количество тактов (реальное время, а не время выполнения процесса), точность этого способа будет выше.Randl
24.04.2016 13:28Так если RDTSC дает количество тактов, то, при условии что частота менялось по ходу выполнения процесса, переводить в секунды их тоже нельзя. Разве не так?
vadimr
24.04.2016 13:38Получите приведённую к фиксированной частоте производительность, разумеется. То есть номинальные секунды.
DistortNeo
24.04.2016 13:40+1Зависит от конкретной задачи. Если я оптимизирую по скорости составляющую часть вычислительного алгоритма, то для меня будет иметь значение именно количество тактов, а не время выполнения. Потому что алгоритм, требующий меньшего числа операций, будет выполняться быстрее.
Zealint
24.04.2016 20:18Вынужден Вас поправить, это не всегда так. Иногда добавление лишних команд не только не приводит к замедлению, но даже может сделать программу быстрее. Есть минимум две причины, почему так может произойти: добавление лишней команды меняет состояние кэша, за счёт чего другие команды (дальше по коду), исполняются быстрее; «разбавление» кода командами может улучшить работу конвейера и сделать всё выполнение более быстрым. К сожалению, примеров я Вам не дам сходу, но лично наблюдал оба варианта. Не буду спорить, что второй случай специфический, но первый мне попадается часто. А что касается числа тактов, то это число не имеет никакого смысла на практике, так как есть конвейер (имхо). Число тактов не отражает скорость выполнения достаточно точно.
Randl
24.04.2016 13:44К тому же, по крайней мере на Linux
clock_gettime( )
использует RDTSC или аналоги там, где это возможно.
encyclopedist
25.04.2016 00:50+3Вы катастрофически ошибаетесь. На современных процессорах, RDTSC считает не такты, а с постоянной скоростью независимо от текущей тактовой частоты.
Цитата из интеловского мануала Intel® 64 and IA-32 Architectures Developer's Manual:
Processor families increment the time-stamp counter differently:
• For Pentium M processors (family [06H], models [09H, 0DH]); for Pentium 4 processors, Intel Xeon processors (family [0FH], models [00H, 01H, or 02H]); and for P6 family processors: the time-stamp counter increments with every internal processor clock cycle. The internal processor clock cycle is etermined by the current core-clock to bus-clock ratio. Intel® SpeedStep® technology transitions may also impact the processor clock.
• For Pentium 4 processors, Intel Xeon processors (family [0FH], models [03H and higher]); for Intel Core Solo and Intel Core Duo processors (family [06H], model [0EH]); for the Intel Xeon processor 5100 series and Intel Core 2 Duo processors (family [06H], model [0FH]); for Intel Core 2 and Intel Xeon processors (family [06H], DisplayModel [17H]); for Intel Atom processors (family [06H], DisplayModel [1CH]): the time-stamp counter increments at a constant rate. That rate may be set by the maximum core-clock to bus-clock ratio of the processor or may be set by the maximum resolved frequency at
which the processor is booted. The maximum resolved frequency may differ from the processor base frequency, see Section 18.16.5 for more detail. On certain processors, the TSC frequency may not be the same as the frequency in the brand string.
The specific processor configuration determines the behavior. Constant TSC behavior ensures that the duration of each clock tick is uniform and supports the use of the TSC as a wall clock timer even if the processor core changes frequency. This is the architectural behavior moving forward.
tangro
24.04.2016 13:47Как вы замеряете время в своих бенчмарках?
Профайлером, а что? Или надо по коду через каждую строку вот это getCPUTime писать?
CodeRush
24.04.2016 14:10+4Добавлю про RDTSC — эта инструкция может быть перехвачена гипервизором и возвращать что угодно. Более того, если снят бит TSD в регистре CR4, то для её использования нужны повышенные привелегии, и потому ваша программа, которая эту инструкцию использует, не будет работать правильно в QNX, которая ставит этот бит по умолчанию.
В качестве независимого от платформы и ОС источника времени (как точного, так и интервалов) могу посоветовать UEFI RT-сервис GetTime, но как вы до него из вашей конкретной ОС доберетесь — другой вопрос.vadimr
24.04.2016 14:23А какая у него реальная разрешающая способность?
CodeRush
24.04.2016 14:36До наносекунд, но конкретная реализация может зависеть от точности аппаратных часов платформы.
vadimr
24.04.2016 14:41Так я про конкретные реализации и спрашиваю. Судя по тому, что в материале это увязывается с RT-CMOS, там как бы не 1 секунда может оказаться на самом деле.
CodeRush
24.04.2016 14:46+1Разницу в миллисекунды на современных системах (Broadwell, Skylake, Merlin Falcon) я им измерял успешно, про наносекунды точно не скажу.
Temtaime
24.04.2016 18:41В теории много какие инструкции могут быть перехвачены, так что же, их вводили просто так?
Де-факто на всех современных OS rdtsc работает корректно и не является привилегированной инструкцией.
arielf
25.04.2016 02:37На Mac OS X можно напрямую использовать Mach сервисы — stackoverflow.com/questions/5167269/clock-gettime-alternative-in-mac-os-x
master1312
25.04.2016 11:18Дабы минимизировать временные затраты на саму функцию получения времени, вызываю два раза clock_gettime с CLOCK_PROCESS_CPUTIME_ID в начале и в конце, пересчет в секунды делаю уже потом. Если есть возможность зафиксировать частоту процессора, использую rdtsc инструкцию.
Gumanoid
Регулярное тестирование замеряет с помощью программы time. Если выявляется регрессия, то собираются счётчики CPU_CLK_UNHALTED.THREAD.