Прискорення коду за допомогою GNU-профайлера, Linux, Операційні системи, статті

Зміст



Введення


Вимоги до продуктивності програмного забезпечення дуже різні, але ймовірно не секрет, що багато програм мають дуже жорсткі вимоги по швидкості виконання. Відеопрогравачі є хорошим прикладом: відеопрогравачем не дуже то можна користуватися, якщо він програє відеофільм на 75% від необхідної швидкості.


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


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


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


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


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


Рятівний gprof


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


  1. Коректність роботи програмного забезпечення. Це завжди головна мета розробки. Взагалі немає сенсу писати дуже швидке додаток, якщо воно не робить те, що повинно робити! Очевидно, що коректність – це щось кшталт “сірої зони”; відеопрогравач, який працює з 99% ваших файлів, або програє відео з випадковими збоями, все-таки можна використовувати, але взагалі-то, коректність більш важлива, ніж швидкість.
  2. Зручність в обслуговуванні програмного забезпечення. Це, фактично, підцілі першої мети. У загальному випадку, якщо програма не написано для зручного обслуговування, то навіть якщо воно спочатку працює, рано чи пізно ви (або хто-небудь ще) залишите спроби виправити помилки або додати нові функції.
  3. Швидкість роботи програмного забезпечення. Ось тут і потрібен профайлер. Якщо програмний додаток працює коректно, почніть профілювання для прискорення його роботи.

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


gprof може профілювати програми, написані на мовах C, C + +, Pascal і Fortran 77. Наведені тут приклади використовують C.


Лістинг 1. Приклад довго виконується програми




#include <stdio.h>
int a(void) {
int i=0,g=0;
while(i++<100000)
{
g+=i;
}
return g;
}
int b(void) {
int i=0,g=0;
while(i++<400000)
{
g+=i;
}
return g;
}
int main(int argc, char** argv)
{
int iterations;
if(argc != 2)
{
printf(“Usage %s <No of Iterations>
“, argv[0]);
exit(-1);
}
else
iterations = atoi(argv[1]);
printf(“No of iterations = %d
“, iterations);
while(iterations–)
{
a();
b();
}
}

Як видно з цього коду, це дуже просте додаток містить дві функції, a і b, Обидві з яких виконують тривалі цикли, витрачаючи процесорний час. Функція main просто містить цикл, в якому викликає по черзі ці функції. У другій функції, b, Цикл працює в чотири рази довше, ніж в a, Тому при профілюванні коду ми очікуємо, що 20% часу програма буде витрачати на функцію a, І 80% на b. Давайте дозволимо профілювання і подивимося, чи підтверджуються ці очікування.


Для дозволу профілювання просто додайте параметр -pg при виклику компілятора gcc. Виконайте компіляція наступним чином:


gcc example1.c -pg -o example1 -O2 -lc


Після компонування програми запустіть його звичним способом:


./example1 50000


Після завершення роботи програми ви повинні побачити файл gmon.out, створений в поточному каталозі.


Використання результатів


Перш за все, перегляньте “простий профіль” (flat profile), який ви отримуєте при виконанні команди gprof з наступними параметрами: сам виконуваний файл і файл gmon.out:


gprof example1 gmon.out -p


Результати роботи такі:


Лістинг 2. Простий профіль




Flat profile:
Each sample counts as 0.01 seconds.
% cumulative self self total
time seconds seconds calls ms/call ms/call name
80.24 63.85 63.85 50000 1.28 1.28 b
20.26 79.97 16.12 50000 0.32 0.32 a

З отриманої інформації ви можете побачити, що, як і очікувалося, приблизно в чотири рази більше часу витрачається на функцію b, Ніж на функцію a. Реальні цифри не дуже корисні, вони можуть бути дуже не точними через помилки округлення.


Проникливі читачі помітять, що багато виклики функцій (наприклад printf) Не включені в результат. Це відбувається тому, що вони знаходяться в бібліотеці, виконавчі C (libc.so), яка (в даному випадку) не була откомпилирован з параметром -pg і, отже, інформація профілювання не збирається ні для якої функції з цієї бібліотеки. Я повернуся до цієї теми пізніше.


Потім ви, можливо, захочете побачити “граф викликів” (call graph), який можна отримати наступним чином:


gprof example1 gmon.out -q


Результати роботи такі:


Лістинг 3. Граф викликів




                     Call graph (explanation follows)
granularity: each sample hit covers 2 byte(s) for 0.01% of 79.97 seconds
index % time self children called name
<spontaneous>
[1] 100.0 0.00 79.97 main [1]
63.85 0.00 50000/50000 b [2]
16.12 0.00 50000/50000 a [3]
———————————————–
63.85 0.00 50000/50000 main [1]
[2] 79.8 63.85 0.00 50000 b [2]
———————————————–
16.12 0.00 50000/50000 main [1]
[3] 20.2 16.12 0.00 50000 a [3]
———————————————–

Нарешті, ви можете отримати лістинг “анотований вихідний код” (annotated source), в якому виводиться вихідний код програми з відмітками про кількість викликів кожної функції.


Для використання цієї можливості відкомпілюйте вихідний код з дозволеною налагоджування, для того щоб вихідний код був поміщений у виконуваний файл:


gcc example1.c -g -pg -o example1 -O2 -lc


Знову виконайте додаток:


./example1 50000


Команда gprof:


gprof example1 gmon.out -A


Результати роботи:


Лістинг 4. Анотований вихідний код




*** File /home/martynh/profarticle/example1.c:
#include <stdio.h>
50000 -> int a(void) {
int i=0,g=0;
while(i++<100000)
{
g+=i;
}
return g;
}
50000 -> int b(void) {
int i=0,g=0;
while(i++<400000)
{
g+=i;
}
return g;
}
int main(int argc, char** argv)
##### -> {
int iterations;
if(argc != 2)
{
printf(“Usage %s <No of Iterations>
“, argv[0]);
exit(-1);
}
else
iterations = atoi(argv[1]);
printf(“No of iterations = %d
“, iterations);
while(iterations–)
{
a();
b();
}
}
Top 10 Lines:
Line Count
3 50000
11 50000
Execution Summary:
3 Executable lines in this file
3 Lines executed
100.00 Percent of the file executed
100000 Total number of line executions
33333.33 Average executions per line

Підтримка поділюваних бібліотек


Як я вже згадував раніше, підтримка профілювання додається компілятором, тому, якщо ви хочете отримати інформацію для профілювання з будь-якої вашої бібліотеки, включаючи бібліотеку C (libc.a), ви повинні теж відкомпілювати її з параметром -pg. На щастя, багато дистрибутиви постачаються з уже відкомпілювалися для підтримки профілювання версіями бібліотек C (libc_p.a).


У використовуваному мною дистрибутиві, gentoo, необхідно додати “profile” в прапори USE і перезібрати glibc. Після цього ви повинні побачити, що створилася бібліотека / usr / lib / libc_p.a. Для тих дистрибутивів, які не поставляються з бібліотекою libc_p в стандартному варіанті, ви повинні перевірити, чи можна встановити її окремо, або завантажити вихідний код glibc і скомпонувати її самостійно.


Після отримання файла libc_p.a ви можете просто повторно скомпонувати ваш приклад наступним чином:


gcc example1.c -g -pg -o example1 -O2 -lc_p


Якщо ви знову запустіть програму та отримайте простий профіль або граф викликів, то повинні побачити безліч функцій, виконавчі C, включаючи printf (жодна з яких не є важливою для нашої тестової програми).


Користувацька час або час ядра


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


Однак зараз ви повинні звернути увагу на одне з найбільш важливих обмежень gprof: під час виконання програми він профілює тільки час режиму користувача. Зазвичай програми витрачають деякий кількість часу на користувальницький код і деяка кількість на “системний код”, наприклад, на системні виклики ядра.


Якщо ви трохи змініть лістинг 1, то зможете більш чітко побачити цю проблему:


Лістинг 5. Додавання системних викликів до лістингу 1




#include <stdio.h>
int a(void) {
sleep(1);
return 0;
}
int b(void) {
sleep(4);
return 0;
}
int main(int argc, char** argv)
{
int iterations;
if(argc != 2)
{
printf(“Usage %s <No of Iterations>
“, argv[0]);
exit(-1);
}
else
iterations = atoi(argv[1]);
printf(“No of iterations = %d
“, iterations);
while(iterations–)
{
a();
b();
}
}

Я змінив код таким чином, що замість виконання циклів функції a і b викликають функцію бібліотеки, виконавчі sleep для затримки роботи на 1 і 4 секунди відповідно.


Відкомпілюйте це додаток:


gcc example2.c -g -pg -o example2 -O2 -lc_p


і виконайте його з 30-ю ітераціями:


./example2 30


Ви отримаєте наступний простий профіль:


Лістинг 6. Простий профіль, що показує сіcтемние виклики




Flat profile:
Each sample counts as 0.01 seconds.
no time accumulated
% cumulative self self total
time seconds seconds calls Ts/call Ts/call name
0.00 0.00 0.00 120 0.00 0.00 sigprocmask
0.00 0.00 0.00 61 0.00 0.00 __libc_sigaction
0.00 0.00 0.00 61 0.00 0.00 sigaction
0.00 0.00 0.00 60 0.00 0.00 nanosleep
0.00 0.00 0.00 60 0.00 0.00 sleep
0.00 0.00 0.00 30 0.00 0.00 a
0.00 0.00 0.00 30 0.00 0.00 b
0.00 0.00 0.00 21 0.00 0.00 _IO_file_overflow
0.00 0.00 0.00 3 0.00 0.00 _IO_new_file_xsputn
0.00 0.00 0.00 2 0.00 0.00 _IO_new_do_write
0.00 0.00 0.00 2 0.00 0.00 __find_specmb
0.00 0.00 0.00 2 0.00 0.00 __guard_setup
0.00 0.00 0.00 1 0.00 0.00 _IO_default_xsputn
0.00 0.00 0.00 1 0.00 0.00 _IO_doallocbuf
0.00 0.00 0.00 1 0.00 0.00 _IO_file_doallocate
0.00 0.00 0.00 1 0.00 0.00 _IO_file_stat
0.00 0.00 0.00 1 0.00 0.00 _IO_file_write
0.00 0.00 0.00 1 0.00 0.00 _IO_setb
0.00 0.00 0.00 1 0.00 0.00 ____strtol_l_internal
0.00 0.00 0.00 1 0.00 0.00 ___fxstat64
0.00 0.00 0.00 1 0.00 0.00 __cxa_atexit
0.00 0.00 0.00 1 0.00 0.00 __errno_location
0.00 0.00 0.00 1 0.00 0.00 __new_exitfn
0.00 0.00 0.00 1 0.00 0.00 __strtol_internal
0.00 0.00 0.00 1 0.00 0.00 _itoa_word
0.00 0.00 0.00 1 0.00 0.00 _mcleanup
0.00 0.00 0.00 1 0.00 0.00 atexit
0.00 0.00 0.00 1 0.00 0.00 atoi
0.00 0.00 0.00 1 0.00 0.00 exit
0.00 0.00 0.00 1 0.00 0.00 flockfile
0.00 0.00 0.00 1 0.00 0.00 funlockfile
0.00 0.00 0.00 1 0.00 0.00 main
0.00 0.00 0.00 1 0.00 0.00 mmap
0.00 0.00 0.00 1 0.00 0.00 moncontrol
0.00 0.00 0.00 1 0.00 0.00 new_do_write
0.00 0.00 0.00 1 0.00 0.00 printf
0.00 0.00 0.00 1 0.00 0.00 setitimer
0.00 0.00 0.00 1 0.00 0.00 vfprintf
0.00 0.00 0.00 1 0.00 0.00 write

Аналізуючи цей результат, ви можете побачити, що хоча профайлер і зареєстрував правильно кількість викликів кожної функції, час роботи цих функцій (і, в кінцевому рахунку, всіх функцій) дорівнює 0.00. Причина цього полягає в тому, що функція sleep насправді робить виклик в простір ядра для затримки додатки, потім перериває виконання і чекає до тих пір, поки ядро ​​не продовжить роботу програми. Оскільки сумарний час виконання функції в просторі користувача дуже мало в порівнянні з часом очікування в просторі ядра, цей час округлюється до нуля. Програма gprof працює таким чином, що вимірювання ведуться фіксованими інтервалами під час виконання програми. Отже, коли програма не виконується, ніяких вимірів не проводиться.


У цьому є як плюси, так і мінуси. З одного боку, важко оптимізувати програми, що витрачають більшу частину часу в просторі ядра, або працюють повільно через зовнішніх чинників, таких як перевантаження підсистеми вводу / виводу операційної системи. З іншого боку, це означає, що профілювання не піддається впливу всіх інших відбуваються в системі подій (наприклад, якщо інший користувач займає багато часу CPU).


У загальному випадку хорошою оцінкою корисності застосування gprof для оптимізації вашого застосування є його запуск з командою time. Ця команда вимірює тривалість роботи програми і час, проведений в просторі користувача і ядра.


Запустіть програму, наведену в лістингу 2, наступним чином:


time ./example2 30


Ви отримаєте наступний результат:


Лістинг 7. Результат команди time




No of iterations = 30
real 2m30.295s
user 0m0.000s
sys 0m0.004s

З цього видно, що на виконання програми в просторі користувача час майже не витрачається, тому gprof не буде корисна в даному випадку.


Висновок


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


Якщо програма gprof не підходить для профілювання вашого застосування, існують альтернативні програми, що не мають обмежень gprof, включаючи OProfile і Sysprof.


З іншого боку (якщо припустити, що у вас встановлений пакет gcc), одним з головних переваг gprof над альтернативними програмами є те, що, найімовірніше, вона вже встановлена ​​на будь-який використовується вами системі Linux.

Схожі статті:


Сподобалася стаття? Ви можете залишити відгук або підписатися на RSS , щоб автоматично отримувати інформацію про нові статтях.

Коментарів поки що немає.

Ваш отзыв

Поділ на параграфи відбувається автоматично, адреса електронної пошти ніколи не буде опублікований, допустимий HTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*

*