Оптимизация C/C++ приложений с помощью GProf

ArticleCategory:

Software Development

AuthorImage:

Arnout Engelen

TranslationInfo:

original in en Arnout Engelen 

en to ru Pukhlyakov Kirill

AboutTheAuthor:

Arnout Engelen учится в Университете Неймегена (Нидерланды) и работает в компании TUNIX специализирующейся на безопасности в интернет. В свободное время он занимается бегом и играет на саксофоне.

Abstract:

Если вы решили оптимизировать приложение сначала подумайте есть ли смысл проводить часы оптимизируя код, который выполняется в течение 0.04 секунды.

GProf предоставляет простой путь профилирования ваших C/C++ приложений и определения интересных участков кода. В процессе изучения будет показано как использовался GProf для уменьшения времени выполнения реального приложения с 3-х минут до 5-ти секунд, с помощью идентификации и оптимизации 2-х структур данных.

История приложения берет свое начало в 1982 году, когда оно было представлено на SIGPLAN Symposium on Compiler Construction. В настоящее время это стандартный инструмент практически для всех разновидностей UNIX.

ArticleIllustration:

Profiling with GProf

ArticleBody:

Кратко о профилировании

Концепция профилирования достаточно проста: фиксирование момента входа программы в функцию и выхода из нее, что позволяет проанализировать какой участок кода выполняется дольше всего. Может создаться впечатление, что сбор подобной информации достаточно трудоемкий процесс - но это не так! Всего навсего необходимо использовать ключ -pg компилятора gcc ( для сбора этих данных ) и затем запустить 'gprof' для анализа получившегося файла.

Использование Pathalizer

В качестве примера возьмем реальное приложение, входящее в pathalizer: приложение event2dot преобразует pathalizer 'events' файл в графический 'dot' файл.

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

Синхронизация приложения

Сначала приложение, которое мы хотим оптимизировать запускаем без профилирования и фиксируем время выполнения. Используемые примеры и файлы с данными ( 55000 строк ) предоставляются.

На моем компьютере event2dot работает около 3-х минут, обрабатывая эти данные.

real    3m36.316s
user    0m55.590s
sys     0m1.070s

Профилирование

Профилирование включается опцией '-pg' во время компиляции. Перекомпилируем приложение:
g++ -pg dotgen.cpp readfile.cpp main.cpp graph.cpp config.cpp -o event2dot

Опять запустим event2dot. Во время исполнения требуемая информация будет помещена в файл 'gmon.out'. Можно посмотреть полученные данные следующей командой 'gprof event2dot | less'.

gprof показывает, что следующие функции представляют для нас интерес:

 % cumulative  self              self     total           
 time seconds  seconds  calls s/call s/call name    
43.32   46.03  46.03 339952989  0.00  0.00 CompareNodes(Node *,Node *)
25.06   72.66  26.63    55000   0.00  0.00 getNode(char *,NodeListNode *&)
16.80   90.51  17.85 339433374  0.00  0.00 CompareEdges(Edge *,AnnotatedEdge *)
12.70  104.01  13.50    51987   0.00  0.00 addAnnotatedEdge(AnnotatedGraph *,Edge *)
 1.98  106.11   2.10    51987   0.00  0.00 addEdge(Graph *,Node *,Node *)
 0.07  106.18   0.07        1   0.07  0.07 FindTreshold(AnnotatedEdge *,int)
 0.06  106.24   0.06        1   0.06 28.79 getGraphFromFile(char *,NodeListNode *&,Config *)
 0.02  106.26   0.02        1   0.02 77.40 summarize(GraphListNode *,Config *)
 0.00  106.26   0.00    55000   0.00  0.00 FixName(char *)
Наиболее интересная для нас колонка - первая: она отображает количество времени, которое приложение выполняло ту или иную функцию в процентном соотношении.

Оптимизация

Анализируя полученные данные мы видим, что практически половина времени уходит на выполнение функции CompareNodes. Эта функция вызывается только функцией CompareEdges, которая в свою очередь вызывается только addAnnotatedEdge - обе кстати тоже присутствуют в списке. Подумаем об оптимизации.

В функции addAnnotatedEdge просматривается связанный список ( linked list ). Несмотря на простоту использования, это не лучший способ хранения данных. Меняем g->edges на бинарное дерево ( binary tree ), что обеспечит более быстрый поиск.

Результат

Как видно время исполнения уменьшилось:
real    2m19.314s
user    0m36.370s
sys     0m0.940s

Вторая попытка

Запускаем опять gprof и смотрим:
%   cumulative self           self    total           
 time   seconds seconds calls  s/call  s/call name    
87.01     25.25  25.25  55000    0.00    0.00 getNode(char *,NodeListNode *&)
10.65     28.34   3.09  51987    0.00    0.00 addEdge(Graph *,Node *,Node *)
Видно, что функции, которые занимали более половины времени выполнения приложения, теперь выполняются гораздо быстрее! Попробуем еще - заменим использование NodeList на NodeHashTable.

Это хорошее улучшение кода:

real    0m3.269s
user    0m0.830s
sys     0m0.090s

Другие C/C++ профайлеры

Кроме рассмотренного нами существует еще несколько профайлеров использующих данные gprof, например KProf ( скриншот ) и cgprof. Несмотря на то, что графические приложения выглядят более привлекательно, я считаю более удобным gprof, работающий из командной строки.

Профилирование других языков

Мы рассмотрели профилирование C/C++ приложений с помощью gprof, также подобные вещи можно делать и для других языков программирования : для Perl используйте модуль Devel::DProf. Запустите ваше приложение следубщим образом :perl -d:DProf mycode.pl и смотрите результаты с помощью dprofpp. Если вы скомпилируете свое Java приложение с помощью gcj вы сможете использовать gprof, но заметим что в настоящее время поддерживается только однопотоковый Java код.

Вывод

Используя профилирование несложно найти участки приложения, подлежащие оптимизации. В рассмотренном нами примере мы уменьшили время работы приложения с 3m36 до 5 секунд.

Ссылки