понеділок, 24 липня 2017 р.

Вимірювання часу роботи коду

Взято тут.
Для одного дослідження виникла потреба заміряти час виконання шматків програми. При чому -- різних програм, включаючи багатопоточні, які виконуватимуться на різних архітектурах, із різними процесорами.

Задача ця виявилася далеко не тривіальною. Сучасний процесор -- дуже складний пристрій. Який працює із складно організованою пам'яттю -- кеші, NUMA, інші жахіття. А на ньому виконується жахливо складна операційна система.

Однак, задачу таки треба вирішувати. Що важливо -- профайлери, на жаль, не дуже підходять, через необхідність дослідити час виконання певного шматка коду -- достатньо великого, але не цілої програми. Точніше -- десятків програм на різних мовах. Добування потрібної інформації із профілів -- чи не складніша задача, за самостійне вимірювання.

Спробую тут дати короткий огляд підходів. Він не претендує на вичерпність -- це надміру складна та велика сфера, хоча принаймні згадати - де дивитися для глибшого ознайомлення, старатимуся.

Отож, задача: є багатопоточна програма, слід виміряти час виконання її фрагменту, тривалість роботи якого складатиме між секундами і хвилинами.

Найпростіший варіант

 

clock()


Здавалося б -- що складного. Беремо та міряємо. Стандартна бібліотека С та С++ містить функцію clock():


std::clock_t clock(); 

Повертає значення в умовних одиницях (жартома називаю їх "папугаями",  відсилаючись до відомого мультфільму). Щоб перетворити цей час в секунди, слід поділити його на константу CLOCKS_PER_SEC. В стандарті POSIX вона рівна 1 000 000, для Windows -- 1000.

Точка відліку, ймовірно, знаходиться десь в минулому, тому слід дивитися лише на різницю між двома викликами clock():

1
2
3
4
5
6
7
8
9
    std::clock_t start = std::clock();

    /* Begin of code for testing */
......................................
    /* End of code for testing */

    double duration = ( std::clock() - start ) / static_cast<double>(CLOCKS_PER_SEC);

    cout << "Time: " << duration << " Seconds " << endl;

Рішення кросплатформове і просте. Однак, має багато недоліків.
  • На POSIX системах повертає так-званий процесорний час -- те ж, що і функція times(), про яку буде далі. Тому грануляція -- порядку тривалості кванту, виділеного диспетчером ОС. Зазвичай це 10-50 мс. У нашому випадку -- не проблема, бо часи помітно більші.
  • Може переповнюватися, скажімо -- на 72-й хвилині для 32-бітних систем. При чому -- на 72-й хвилині після невизначеного моменту в минулому. Але стандарт С99+ вимагає, щоб ця функція при переповненні завжди повертала максимальне значення -- (clock_t)(-1).
  • Однак, наступна серйозніша -- процесорний час включає час виконання всіх потоків процесу
  • А на деяких системах --  може включати час виконання дочірніх процесі. (На Linux -- не включає.) З цим для нашої задачі можна жити, але сюрприз.
  • Зовсім погано -- на Windows ці функція повертає wall clock time -- час по годиннику, а не час процесу, як POSIX системи. Зате переповнюється лише через 24.8 доби. 
Нагадаємо, що і Windows і POSIX-системи -- багатозадачні. Тому процеси можуть періодично призупинятися диспетчером. При тому, "настінний годинник" -- wall time, продовжує йти, а от процесорний час процесу -- зупиняється. 
Відпадає.

Нативні таймери високої роздільної здатності


Сучасні операційні системи, зазвичай, обладнані таймерами високої роздільної здатності.

З-під Windows вони доступні за допомогою функції  QueryPerformanceCounter(), а QueryPerformanceFrequency() дозволяє взнати роздільну здатність:

1
2
3
4
5
6
7
BOOL WINAPI QueryPerformanceFrequency(
  _Out_ LARGE_INTEGER *lpFrequency
);

BOOL WINAPI QueryPerformanceCounter(
  _Out_ LARGE_INTEGER *lpPerformanceCount
);

Використати їх можна якось так:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
LARGE_INTEGER tick1, tick2;
LARGE_INTEGER Frequency;

QueryPerformanceCounter(&tick1);

............Timed.code..............
QueryPerformanceCounter(&tick2);

QueryPerformanceFrequency(&Frequency); 

cout << (tick2.QuadPart - tick1.QuadPart)/Frequency.QuadPart <<endl;

LARGE_INTEGER -- такий збочений спосіб від Microsoft мати 64-бітне ціле число на платформах, що їх не мають.

Подробиці можна почитати тут: "Acquiring high-resolution time stamps", зокрема див. розділ про підтримку (та спосіб реалізації) для різних версій Windows. Наприклад, хоча лічильник базується на підрахункові тактів процесора, обіцяють, крім рідких випадків, що результат залишається коректним, навіть якщо задача блукала між процесорами -- диспетчер запускав її на різних ядрах.

Обіцяють роздільну здатність порядку часток мікросекунди (в документації згадується тривалість тіка порядку 100 нс, на моїй системі -- 427 нс.)

В POSIX-системах аналогічний інструмент -- пара функцій clock_gettime() і clock_getres(). В Лінукс вони присутні з версії ядра 2.6. Подробиці див., наприклад, тут. В порівнянні із обіцянками Мікрософт (в реалізацію я таки не дивився -- мало що вони там наобіцяли), гарантії щодо багатопроцесорних систем скромніші, але нічого особливо страшного.

Прототипи цих функцій наступні:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#include <time.h>
struct timespec {
 time_t   tv_sec;        /* seconds */
 long     tv_nsec;       /* nanoseconds */
};

int clock_getres(clockid_t clk_id, struct timespec *res);

int clock_gettime(clockid_t clk_id, struct timespec *tp);

int clock_settime(clockid_t clk_id, const struct timespec *tp);

Де clockid_t визначає, що ж за таймер нам потрібен. В розглянутому контексті нас може цікавити його значення, рівне CLOCK_MONOTONIC чи CLOCK_MONOTONIC_RAW. В майбутньому ще стикатимемося із таймерами CLOCK_PROCESS_CPUTIME_ID і  CLOCK_THREAD_CPUTIME_ID.

Користуємося якось так (результат в мікросекундах):

1
2
3
4
5
6
7
8
9
clock_gettime(CLOCK_MONOTONIC, &tick1);

............Timed.code................. 

clock_gettime(CLOCK_MONOTONIC, &tick2);

double interval = (tick2.tv_sec - tick1.tv_sec) * pow(10, 9) 
                  + (tick2.tv_nsec - tick1.tv_nsec);
interval /= 1000;

Ремарка: на деяких системах  clock_getres() може повертати відверті дурниці для CLOCK_MONOTONIC_RAW. Одного разу отримав (CentOS, ядро 2.6.32-642.6.2.el6.x86_64):

CLOCK_MONOTONIC_RAW resolution:  -3 seconds, 870049760 nanoseconds
CLOCK_MONOTONIC            resolution:  0 seconds, 1 nanoseconds

При тому, clock_gettime() також повертає безглузді результати -- просто якісь, менш-більш стабільні великі числа.  
Що це взагалі відбувається? Не знаю. Варіант із переповненням цілочисельних типів, некоректним специфікатором виводу і т.д. ніби відпадає, але див. код.

Тому, поки зупинився на CLOCK_MONOTONIC. Він відрізняється від CLOCK_MONOTONIC_RAW тим, що його швидкість скоректована згідно вказівок від сервера часу (ntp), а _RAW просто покладається на локальний лічильник.

Time stamp counters


Сучасні архітектури процесорів містять апаратні лічильники кількості тактів (разом із іншими performance counter-ами, про які трохи пізніше). В принципі, таймери високої роздільної здатності часто покладаються на них.

Для архітектури x86 першим згадується регістр Time Stamp Counter (TSC), 64-бітний регістр, що містить кількість тактів, яка минула із перевантаження процесора. Його вміст доступний за командою RDTSC, котра кладе його вміст в пару регістрів EDX:EAX, для 64-бітного режиму зануляючи верхні частини RDX i RAX.

ARM-и обладнані аналогічними лічильниками, крім того, можуть містити апаратні таймери із схожими властивостями -- див. "Мікросекундні затримки та відлік мікросекунд для STM32", про DWT->CYCCNT і далі.

Апаратні лічильники типу TSC були популярними для дослідження часу виконання невеликих шматків коду, лічених машинних інструкцій. Однак, вже з епохи Pentium із ними почалися проблеми. :-) Процесор може перевпорядковувати команди, тому RDTSC може виконуватися не тоді, коли б нам хотілося. Вирішується командами серіалізації, типу CPUID або розширеним варіантом, RDTSCP, якщо така присутня.

Далі -- більше. Спочатку лічильник збільшувався кожен такт процесора. Однак, на сучасних процесорах, які змінюють свою тактову частоту для економії електроенергії чи збільшення продуктивності окремого ядра, якщо інші не зайняті (TurboBoost), лічильник зазвичай працює із фіксованою (номінальною -- в якомусь сенсі) частотою (invariant TSC), роблячи його позбавленим сенсу для дослідження часу виконання (в процесорних тактах) окремих інструкцій чи фрагментів коду.

Детальніше див.:

Крім того, забігаючи наперед,  процес може мігрувати між процесами, лічильники яких часто не синхронізовані між собою. Тому, перед початком досліджуваного фрагменту можна прочитати один лічильник, після -- інший. Для боротьби із тим можна забороняти міграцію:


На загал, з одного боку, хвости вище перерахованого можна побачити в описі функціювання та гарантій щодо високоточних таймерів Linux та Windows, з іншого -- згадані ускладнення роблять безпосереднє використання TSC-подібних механізмів нераціональним для нашої задачі -- замірів відносно великих інтервалів часу, із мільйонами і більше інструкцій.

Для повноти наведу спосіб використання. Почав-ім шукати свій старий код, але в процесі трапилася симпатична бібліотечка,  "Quick and dirty TSC access for benchmarking", то наведу фрагмент її коду:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
static inline uint64_t bench_start(void)
{
  unsigned  cycles_low, cycles_high;
  asm volatile( "CPUID\n\t" // serialize
                "RDTSC\n\t" // read clock
                "MOV %%edx, %0\n\t"
                "MOV %%eax, %1\n\t"
                : "=r" (cycles_high), "=r" (cycles_low)
                :: "%rax", "%rbx", "%rcx", "%rdx" );
  return ((uint64_t) cycles_high << 32) | cycles_low;
}

Він працюватиме для GCC на будь-якій x86-платформі, де доступ до RDTSC не заборонено відповідними прапорцями процесора (подробиці -- див. список посилань вище). (Все ніяк не напишу детальніше про inline-асемблер GCC...)

MSVC має відповідну вбудовану функцію, (Intrinsic), __rdtsc()/__rdtscp():

1
2
3
unsigned __int64 __rdtsc(); 

unsigned __int64 __rdtscp( unsigned int * Aux );  


std::chrono::high_resolution_clock


Розглянуті вище таймери -- системозалежні. С++11 надає бібліотеку chrono, яка містить різні таймери, зокрема chrono::high_resolution_clock, щодо якого обіцяють (як завжди в С++ -- якщо автори реалізації будуть достатньо люб'язні), що це таймер із найвищою роздільною здатністю.

Його статичний метод now() повертає поточний момент часу, у вигляді std::chrono::time_point. Маючи два моменти часу, можна отримати їх різницю -- об'єкт типу std::chrono::duration, (це С++, дитинко...), який, у свою чергу, можна перетворити у кількість секунд, мілісекунд і т.д.

Для представлення часток секунди використовується std::ratio,  для якого описано синоніми, mill це std::ratio<1, 1000> -- мілісекунда, micro    -- std::ratio<1, 1000000>, тощо. Тоді std::chrono::microseconds   це duration< **знаковий цілий тип**, std::micro>  і т.д. (C++14 додав ще й відповідні літерали, 5s, 10ns, тощо :-)

Перевірити чи таймер є steady -- чи рахує він послідовно, чи для нього можливі стрибки (наприклад, коли змінено системний час -- внаслідок втручання користувача, переходу на літній час і т.д.), можна за допомогою статичного поля is_steady.

Використання виглядає якось так:

1
2
3
4
5
6
7
auto tick1 = std::chrono::high_resolution_clock::now();

..............Timed.code............

auto tick2 = std::chrono::high_resolution_clock::now();

std::chrono::duration_cast<std::chrono::microseconds>(tick2 - tick1).count();

Детальніше див. документацію на cppreference та відповідну главу книги Джосаттіса, "The C++ Standard Library - A Tutorial and Reference, 2nd Edition".

Проблеми із wall-clock


Всі розглянуті вище підходи та таймери мають одну додаткову складність у використанні і одну фундаментальну ваду.

Складність у тому, що як компілятор так і процесор (у значно менших масштабах, правда) мають право переставляти команди, оптимізуючи. І може виникнути ситуація, коли момент виміру зсунуто надміру назад чи вперед відносно початку досліджуваного коду.

Не вдаючись зараз в теорію (пост і так величезний виходить), заборонити їм це можна, вставивши відповідні бар'єри. Звичайно, при тому ми заважаємо компілятору оптимізувати наш код, та й самі бар'єри займають якийсь час. Однак, для нашої задачі це не мало б бути проблемою. С++11 включає бар'єр пам'яті atomic_thread_fence(). Звичайно, він призначений не зовсім для цього, але дозволяє досягнути потрібного нам результату -- заборонити перевпорядкування.

Тоді, якщо бути параноїком і змиритися із накладними витратами, код виміру моменту часу може виглядати так:

1
2
3
4
5
6
7
8
inline std::chrono::high_resolution_clock::time_point get_current_time_fenced() {
    assert(std::chrono::high_resolution_clock::is_steady &&
           "Timer should be steady (monotonic).");
    std::atomic_thread_fence(std::memory_order_seq_cst);
    auto res_time = std::chrono::high_resolution_clock::now();
    std::atomic_thread_fence(std::memory_order_seq_cst);
    return res_time;
}

Друга проблема значно фундаментальніша. Наш процес можуть переривати інші процеси -- він виконується під багатозадачною операційною системою. Тому будь-які виміри часу, крім власне часу його виконання, включатимуть час виконання інших процесів. Це різко знижує точність, вводячи неконтрольовану систематичну похибку.

На щастя, сучасні ОС здатні надати інформацію і про те, скільки ж реально працював процес. На жаль -- різними, складними і плутаними механізмами.

Часи процесів та потоків


Кожен процес та потік може виконуватися в користувацькому режимі -- виконуючи свій код та в режимі ядра, коли виконуються системні виклики від його імені. І Windows і POSIX надають доступ до часу, проведеного у кожному із режимів. Правда, грануляція -- грубша ніж у більшості таймерів, розглянутих раніше -- вона порядку розміру кванту часу, 10-50 мс. (Крім того, є ще нюанси -- див. далі).

Час процесу включає час всіх його потоків -- тому заміряний таким чином час може бути більшим за фізичний час, що програма виконувалася. Час потоку включає лише час його виконання.

POSIX/Linux


times()


Перша функція, про яку слід згадати -- times():

#include <sys/times.h>

struct tms {
 clock_t tms_utime;  /* user time */
 clock_t tms_stime;  /* system time */
 clock_t tms_cutime; /* user time of children */
 clock_t tms_cstime; /* system time of children */
};

clock_t times(struct tms *buf);

Вона зберігає, за переданим вказівником, час виконання поточного процесу в користувацькому режимі, час -- в режимі ядра та час виконання дочірніх процесів, який додається після повернення із wait() чи waitpid(). Для нашої задачі він не цікавий, тому поки про нього забудемо. Час власне процесу включає час всіх його потоків. Роздільна здатність -- порядку величини кванту часу.

Час повертається у певних "тіках", кількість яких на секунду можна взнати викликом sysconf(_SC_CLK_TCK).

Для отримання часу в мікросекундах можна зробити так (зверніть увагу, що мільйон -- типу double!):

tms proc_times;
times(&proc_times); // Process times

proc_time_sys  = 1000000.0 * proc_times.tms_stime / ticks_per_second;
proc_time_user  = 1000000.0 * proc_times.tms_utime / ticks_per_second;
proc_time_total = proc_time_sys + proc_time_user;

Отримати часи конкретного потоку трохи важче.

getrusage()


POSIX включає корисну функцію, getrusage(), get-resources-usage. Правда, формат результату не дуже стандартизується -- лише деякі поля, такі як ru_utime i ru_stime.

#include <sys/time.h>
#include <sys/resource.h>

int getrusage(int who, struct rusage *usage);
    
struct rusage {
    struct timeval ru_utime; /* user CPU time used */
    struct timeval ru_stime; /* system CPU time used */
    long   ru_maxrss;        /* maximum resident set size */
    long   ru_ixrss;         /* integral shared memory size */
    long   ru_idrss;         /* integral unshared data size */
    long   ru_isrss;         /* integral unshared stack size */
    long   ru_minflt;        /* page reclaims (soft page faults) */
    long   ru_majflt;        /* page faults (hard page faults) */
    long   ru_nswap;         /* swaps */
    long   ru_inblock;       /* block input operations */
    long   ru_oublock;       /* block output operations */
    long   ru_msgsnd;        /* IPC messages sent */
    long   ru_msgrcv;        /* IPC messages received */
    long   ru_nsignals;      /* signals received */
    long   ru_nvcsw;         /* voluntary context switches */
    long   ru_nivcsw;        /* involuntary context switches */
};    

Аргумент who може набувати значення RUSAGE_SELF, даючи інформацію про процес, RUSAGE_CHILDREN -- про дочірні процеси (в тому ж сенсі, що times). Linux має ще один варіант, RUSAGE_THREAD -- для поточного потоку. На жаль, cygwin його не реалізовує.
Для того, щоб користуватися нестандартними засобами типу згаданого
RUSAGE_THREAD слід, перед включенням будь-яких заголовків, оголосити символ _GNU_SOURCE.
Часи зберігаються в структурі:

struct timeval {
    time_t      tv_sec;     /* seconds */
    suseconds_t tv_usec;    /* microseconds */
};

Щоб не було нудно -- вже зараз знаємо три способи зберігати час в POSIX:
  • struct timespec - секунди, наносекунди;
  • struct timeval - секунди, мікросекунди
  • цілий тип clock_t - умовні одиниці, шкала зберігається окремо. 
Крім того, структура rusage містить багато цікавого, зокрема:
  • ru_maxrss -- піковий розмір робочої області (грубо -- фізично, на противагу -- віртуально, використаного програмою RAM), в кілобайтах.
  • ru_minflt -- soft page faults, кількість відсутностей сторінок, задоволених із сторінок, що чекали на повторне використання (без вводу-виводу).
  • ru_majflt -- hard page faults, події відсутності сторінки, що привели до вводу-виводу -- читання із swap-простору чи виконавчого файлу.
  • ru_nvcsw, ru_nivcsw -- кількість добровільних (наприклад, системні виклики) та недобровільних (диспетчер -- витісняюча багатозадачність) переключень контексту.
Скористатися, для поточних процесу та потоку (якщо останнє підтримується), можна так:

uint64_t to_us(const timeval& t){
 return t.tv_sec * 1000000 + t.tv_usec;
}
..............................

 rusage usage_proc; 
 times(&proc_times); // Process times
 getrusage(RUSAGE_SELF, &usage_proc); 
#ifdef  RUSAGE_THREAD
 rusage usage_thread;
 getrusage(RUSAGE_THREAD, &usage_thread); // Thread times -- needs _GNU_SOURCE 
#endif  

 proc_time_user = to_us(usage_proc.ru_utime);
 proc_time_sys  = to_us(usage_proc.ru_stime);
 proc_time_total= proc_time_user + proc_time_sys;
 
#ifdef  RUSAGE_THREAD
 thread_time_user = to_us(usage_thread.ru_utime);
 thread_time_sys  = to_us(usage_thread.ru_stime);
 thread_time_total= thread_time_user + thread_time_sys;
#endif

clock_gettime()


Якщо RUSAGE_THREAD не підтримується -- є більш портабельний, хоч і менш інформативний -- без додаткової інформації від getrusage() чи поділу на час ядра і користувацький час, спосіб отримати часи виконання потоку -- вже розглянута вище clock_gettime(). Щоб отримати інформацію про час виконання потоку, слід їй передати id його таймера, який можна отримати за допомогою pthread_getcpuclockid() та pthread_self():

uint64_t to_us(const timespec & t){
 return t.tv_sec * 1000000 + t.tv_nsec / 1000;
}
 
........................................... 
 clockid_t cid;
 timespec thread_times;
 err = pthread_getcpuclockid(pthread_self(), &cid);
 if(err == -1){
  std::cerr << "Error obtaining thread clock id" << std::endl;
 }
 err = clock_gettime(cid, &thread_times);

 thread_time_total = to_us(thread_times);


MS Windows

 

GetProcessTimes() та GetThreadTimes()


В принципі, це -- майже прямий аналог того, що повертає times() для процесів та getrusage()/clock_gettime() для потоків. Час ділиться на час ядра і "користувацький" час, для процесів підсумовується час всіх потоків.

BOOL WINAPI GetProcessTimes(
  _In_  HANDLE     hProcess,
  _Out_ LPFILETIME lpCreationTime,
  _Out_ LPFILETIME lpExitTime,
  _Out_ LPFILETIME lpKernelTime,
  _Out_ LPFILETIME lpUserTime
);

BOOL WINAPI GetThreadTimes(
  _In_  HANDLE     hThread,
  _Out_ LPFILETIME lpCreationTime,
  _Out_ LPFILETIME lpExitTime,
  _Out_ LPFILETIME lpKernelTime,
  _Out_ LPFILETIME lpUserTime
);

typedef struct _FILETIME {
  DWORD dwLowDateTime;
  DWORD dwHighDateTime;
} FILETIME, *PFILETIME;

lpKernelTime і lpUserTime -- якраз те, що нам потрібно. lpCreationTime і lpExitTime міститимуть часи запуску та завершення процесу чи потоку, відраховані від 1 січня 1601 року. FILETIME містить час, в одиницях 100 нс = 0.1 мкс. Для конвертації у більш зручні величини існують відповідні функції (для нашої задачі не потрібні).

Детальніше див.: "FILETIME", GetProcessTimes(), GetThreadTimes().
Знає хтось, чого в FILETIME така дивна епоха? Ніби ніколи не траплялася відповідна інформація...
 Приклад використання:

uint64_t to_us(const FILETIME & t){ //! Unit -- 100ns
 return ( (static_cast<uint64_t>(t.dwHighDateTime) << 32) + t.dwLowDateTime)/10;
}

.....................

 int err_proc = GetProcessTimes(GetCurrentProcess(), 
        &creation_time, &exit_time,
                                &kernel_time, &user_time);

 int err_thread = GetThreadTimes(GetCurrentThread(), 
        &thread_creation_time, &thread_exit_time,
                                &thread_kernel_time, &thread_user_time);

 .....................
 proc_time_user = to_us(user_time);
 proc_time_sys  = to_us(kernel_time);
 proc_time_total= proc_time_user + proc_time_sys;        

 thread_time_user = to_us(thread_user_time);
 thread_time_sys  = to_us(thread_kernel_time);
 thread_time_total= thread_time_user + thread_time_sys;  

Однак, див. "Why GetThreadTimes is wrong" -- автор стверджує, що ці функції рахують лише повністю використані кванти часу, тому, зокрема, якщо код не використовує повністю жодного кванту, можуть взагалі повернути нульовий час незалежно від тривалості виконання.

GetProcessMemoryInfo() 


Інформацію про пам'ять процесу можна отримати за допомогою функції GetProcessMemoryInfo(), якій передається структура типу PROCESS_MEMORY_COUNTERS чи PROCESS_MEMORY_COUNTERS_EX:

BOOL WINAPI GetProcessMemoryInfo(
  _In_  HANDLE                   Process,
  _Out_ PPROCESS_MEMORY_COUNTERS ppsmemCounters,
  _In_  DWORD                    cb
);

typedef struct _PROCESS_MEMORY_COUNTERS_EX {
  DWORD  cb;
  DWORD  PageFaultCount;
  SIZE_T PeakWorkingSetSize;
  SIZE_T WorkingSetSize;
  SIZE_T QuotaPeakPagedPoolUsage;
  SIZE_T QuotaPagedPoolUsage;
  SIZE_T QuotaPeakNonPagedPoolUsage;
  SIZE_T QuotaNonPagedPoolUsage;
  SIZE_T PagefileUsage;
  SIZE_T PeakPagefileUsage;
  SIZE_T PrivateUsage;
} PROCESS_MEMORY_COUNTERS_EX, *PPROCESS_MEMORY_COUNTERS_EX;

Назви полів структури говорять самі за себе. Зокрема, PeakWorkingSetSize -- приблизний аналог rusage.ru_maxrss. Приклад використання:

 PROCESS_MEMORY_COUNTERS meminfo;
 GetProcessMemoryInfo( GetCurrentProcess( ), &meminfo, sizeof(meminfo) );
 process_max_resident_size = meminfo.PeakWorkingSetSize; 

Додатково щодо інформації про пам'ять процесів -- див. "C/C++ tip: How to get the process resident set size (physical memory use)".


Windows Performance Counters


WinAPI включає спеціальний інтерфейс доступу до всіляких таких лічильників. На жаль, документація щодо них специфічна: "Performance Counters". Зокрема, я поки так і не зміг знайти зручного способу знайти список назв лічильників (які представляються специфічними шляхами).

MSDN містить приклад: "Browsing Performance Counters", який виводить діалогове вікно, котре дозволяє вибрати (один) лічильник і вивести його значення. (Для них у Windows навіть спеціальне діалогове вікно існує...)



Використовувати їх можна якось так:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <windows.h>
#include <psapi.h>
#include <pdh.h>

.................................................... 
    HQUERY Query = NULL;
    PDH_STATUS Status = PdhOpenQuery(NULL, 0, &Query);
    HCOUNTER Counter;
 
    CHAR CounterPathBuffer[PDH_MAX_COUNTER_PATH] = "\\Thread(_Total/_Total)\\Context Switches/sec";
    if (Status != ERROR_SUCCESS) ...ERROR...    
    
 Status = PdhAddCounter(Query, CounterPathBuffer, 0, &Counter);
 if (Status != ERROR_SUCCESS) ...ERROR...     
 
 Status = PdhCollectQueryData(Query);
    if (Status != ERROR_SUCCESS) ...ERROR...    

 ....................Timed.code................
 
    Status = PdhCollectQueryData(Query);
    if (Status != ERROR_SUCCESS) ...ERROR...    
 
    DWORD CounterType;
    PDH_FMT_COUNTERVALUE DisplayValue;
    Status = PdhGetFormattedCounterValue(Counter,
                                         PDH_FMT_DOUBLE,
                                         &CounterType,
                                         &DisplayValue);
    cout << "Counter: " << DisplayValue.doubleValue << endl;
 
    if (Query){
       PdhCloseQuery(Query);
    } 

  • Готуємо API до роботи в рядку 7. 
  • В рядку 10 задається шлях до лічильника (зверніть увагу на escape-послідовність '\\' -- в кінцевій стрічці це буде один слеш).
  • Рядок 13 -- додаємо лічильник. Може бути декілька. 
  • В рядку 21 отримуємо результати, перетворюємо їх (рядок 26) в зрозумілу форму і виводимо.
Лічильник  "\\Thread(_Total/_Total)\\Context Switches/sec" стосується всіх потоків всіх процесів. Для конкретного процесу доступні якісь такі лічильники (не питайтеся, звідки я їх взяв, не знаючи адекватного способу їх отримати...):

\Process(armsvc)\% Privileged Time
\Process(armsvc)\% Processor Time
\Process(armsvc)\% User Time
\Process(armsvc)\Creating Process ID
\Process(armsvc)\Elapsed Time
\Process(armsvc)\Handle Count
\Process(armsvc)\ID Process
\Process(armsvc)\IO Data Bytes/sec
\Process(armsvc)\IO Data Operations/sec
\Process(armsvc)\IO Other Bytes/sec
\Process(armsvc)\IO Other Operations/sec
\Process(armsvc)\IO Read Bytes/sec
\Process(armsvc)\IO Read Operations/sec
\Process(armsvc)\IO Write Bytes/sec
\Process(armsvc)\IO Write Operations/sec
\Process(armsvc)\Page Faults/sec
\Process(armsvc)\Page File Bytes
\Process(armsvc)\Page File Bytes Peak
\Process(armsvc)\Pool Nonpaged Bytes
\Process(armsvc)\Pool Paged Bytes
\Process(armsvc)\Priority Base
\Process(armsvc)\Private Bytes
\Process(armsvc)\Thread Count
\Process(armsvc)\Virtual Bytes
\Process(armsvc)\Virtual Bytes Peak
\Process(armsvc)\Working Set
\Process(armsvc)\Working Set - Private
\Process(armsvc)\Working Set Peak
\Thread(armsvc/0)\Context Switches/sec
\Thread(armsvc/1)\Context Switches/sec
\Thread(armsvc/2)\Context Switches/sec
\Thread(armsvc/3)\Context Switches/sec
Де armsvc -- назва конкретного процесу (див., скажімо, ф-цію GetModuleBaseName()), armsvc/1 -- перший потік цього процесу, і т.д.
Поки вирішив не застосовувати -- основну інформацію можна отримати і простіше, а детальну -- занадто багато мороки.

QueryProcessCycleTime() і QueryThreadCycleTime() 


WinAPI містить ще пару цікавих функцій, аналогів яких (поки?) в POSIX не знайшов -- QueryProcessCycleTime() і QueryThreadCycleTime(), які повертають кількість тактів процесора, які виконувався код процесу (включаючи кількість тактів всіх потоків) чи потоку. Правда, покладаються на щось типу RDTSC, тому можуть повертати як такти, реально виконані, так і номінальні. Плюс, прив'язка до потоку чи процесу, все ж, не ідеальна.

1
2
3
4
5
6
7
8
9
BOOL WINAPI QueryProcessCycleTime(
  _In_  HANDLE   ProcessHandle,
  _Out_ PULONG64 CycleTime
);

BOOL WINAPI QueryThreadCycleTime(
  _In_  HANDLE   ThreadHandle,
  _Out_ PULONG64 CycleTime
);

Приклад використання:

1
2
    uint64_t process_cycles_finish;
    QueryProcessCycleTime(GetCurrentProcess(), &process_cycles_finish);


Boost::chrono


Хоча std::chrono й моделювався на базі boost::chrono, трохи цікавих засобів не було включено в стандарт. Приклад коду звідси: "The Boost C++ Libraries: Chapter 37. Boost.Chrono"

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <boost/chrono.hpp>
#include <iostream>

using namespace boost::chrono;

int main()
{
  std::cout << system_clock::now() << '\n';
#ifdef BOOST_CHRONO_HAS_CLOCK_STEADY
  std::cout << steady_clock::now() << '\n';
#endif
  std::cout << high_resolution_clock::now() << '\n';

#ifdef BOOST_CHRONO_HAS_PROCESS_CLOCKS
  std::cout << process_real_cpu_clock::now() << '\n';
  std::cout << process_user_cpu_clock::now() << '\n';
  std::cout << process_system_cpu_clock::now() << '\n';
  std::cout << process_cpu_clock::now() << '\n';
#endif

#ifdef BOOST_CHRONO_HAS_THREAD_CLOCK
  std::cout << thread_clock::now() << '\n';
#endif
}

Вивід, правда, буде дещо дивним:

$ ./boost_clocks.exe
14996457470044000 [1/10000000]seconds since Jan 1, 1970
782657084906896 nanoseconds since boot
782657084913722 nanoseconds since boot
404000000 nanoseconds since process start-up
0 nanoseconds since process start-up
31200200 nanoseconds since process start-up
{404000000;0;31200200} nanoseconds since process start-up
31200200 nanoseconds since thread start-up
Можливо, це найбільш портабельний спосіб визначити часи процесу/потоку, однак, потребує наявності доволі монструозного boost, не надаючи інших переваг над викликами, розглянутими раніше.

Завершуючи


На цьому будемо закінчувати. В наступних постах розглянемо існуючі засоби профілювання та трасування багатопоточних програм, та засоби для роботи із внутрішніми лічильниками процесорів, такими як PAPI чи PCM.

Коди використаних, тут та в наступних постах, прикладів, доступний на github: blog_samples_timing_2017.

А на разі --

Дякую за увагу!


P.S. Згадалася ще одна стаття на ту ж тему: "C/C++: как измерять процессорное время".


Немає коментарів:

Дописати коментар