Обычно при выполнении приложения задействованы код приложения, библиотечные функции и службы ядра. В неоптимизированных программах большая часть времени CPU тратится на выполнение нескольких операторов или подпрограмм. Обычно программист не учитывает большую нагрузку, создаваемую такими критическими участками. Часто это приводит к снижению производительности. Такие критические участки можно найти с помощью команды tprof (более подробная информация об этой команде приведена в разделе Команда tprof). Команда tprof применяется для анализа программ, созданных компиляторами C, C++ и FORTRAN.
Для того чтобы узнать, установлена ли команда tprof, вызовите следующую команду:
# lslpp -lI perfagent.tools
Исходные данные для программы tprof предоставляются функцией трассировки (см. Глава 12. Анализ производительности с помощью функции трассировки). Во время анализа программы профайлером активизируется функция трассировки, которая считывает в точке трассировки с ИД 234 содержимое регистра адреса команды при получении сигнала прерывания от системных часов (100 раз в секунду на каждом процессоре). Кроме того, активизируются некоторые другие точки трассировки, позволяющие tprof отслеживать работу процессов и передавать управление. Записи трассировки не сохраняются в файле на диске, а передаются в конвейер, из которого считываются специальной программой. Эта программа создает таблицу, в которой содержатся все уникальные адреса и число обращений по каждому из них. После анализа рабочей схемы эта таблица записывается на диск. Затем компонент tprof, предназначенный для сокращения объема данных, сопоставляет найденные адреса команд с диапазонами адресов, выделенными различным программам, и сообщает о распределении обращений по адресам (тактах) в программах, включенных в рабочую схему.
Распределение тактов примерно пропорционально времени CPU, которое затрачивается на выполнение программы (10 миллисекунд на один такт). Определив программы, для выполнения которых требуется много ресурсов, программист может попытаться избавиться от критических участков или уменьшить их число.
В следующей программе на C каждому байту массива целых чисел присваивается значение 0x01, каждый элемент увеличивается на произвольную константу, после чего печатается произвольно выбранный элемент. Эта программа является примером обработки больших массивов чисел.
/* Увеличение массива -- Версия 1 */ #include <stdlib.h> #define Asize 1024 #define RowDim InnerIndex #define ColDim OuterIndex main() { int Increment; int OuterIndex; int InnerIndex; int big [Asize][Asize]; /* присвоение каждому байту массива значения 0x01 */ for(OuterIndex=0; OuterIndex<Asize; OuterIndex++) { for (InnerIndex=0; InnerIndex<Asize; InnerIndex++) big[RowDim][ColDim] = 0x01010101; } Increment = rand(); /* увеличение каждого элемента массива */ for(OuterIndex=0; OuterIndex<Asize; OuterIndex++) { for (InnerIndex=0; InnerIndex<Asize; InnerIndex++) { big[RowDim][ColDim] += Increment; if (big[RowDim][ColDim] < 0) printf("Отрицательное число. %d\n",big[RowDim][ColDim]); } } printf("Версия 1, контрольное число: %d\n", big[rand()%Asize][rand()%Asize]); return(0); }
Для компиляции программы была вызвана следующая команда:
# xlc -g version1.c -o version1
Параметр -g означает, что компилятор C создаст объектный модуль с символьной информацией отладки, предназначенной для программы tprof. Несмотря на то, что программа tprof может работать с оптимизированными модулями, в этом примере не был указан параметр -O, для того чтобы номера строк, указанные программой tprof, были более точными. При оптимизации программы компилятор C часто изменяет порядок строк кода так, что вывод программы tprof становится менее наглядным. В тестовой системе эта программа выполняется за 5.97 секунд, из которых более 5.9 секунд - это пользовательское время CPU. Очевидно, что эту программу нужно переписать так, чтобы для ее выполнения требовалось меньше процессорного времени.
Эту программу можно проанализировать с помощью следующей команды (в операционных системах AIX версии выше 4.3.3 укажите опцию -m):
# tprof -p version1 -x version1
Будет создан файл __version1.all. В нем указано, сколько тактов CPU заняло выполнение каждой программы.
Процесс PID TID Всего Ядро Пользоват. Общие Другое ======= === === ===== ====== ==== ====== ===== version1 30480 30481 793 30 763 0 0 ksh 32582 32583 8 8 0 0 0 /etc/init 1 459 6 0 6 0 0 /etc/syncd 3854 4631 5 5 0 0 0 tprof 5038 5019 4 2 2 0 0 rlogind 11344 15115 2 2 0 0 0 PID.771 770 771 1 1 0 0 0 tprof 11940 11941 1 1 0 0 0 tprof 11950 11951 1 1 0 0 0 tprof 13986 15115 1 1 0 0 0 ksh 16048 7181 1 1 0 0 0 ======= === === ===== ====== ==== ====== ===== Всего 823 52 771 0 0 Процесс FREQ Всего Ядро Пользоват. Общие Другое ======= === ===== ====== ==== ====== ===== version1 1 793 30 763 0 0 ksh 2 9 9 0 0 0 /etc/init 1 6 0 6 0 0 /etc/syncd 1 5 5 0 0 0 tprof 4 7 5 2 0 0 rlogind 1 2 2 0 0 0 PID.771 1 1 1 0 0 0 ======= === ===== ====== ==== ====== ===== Всего 11 823 52 771 0 0 Всего тактов для version1( USER) = 763 Подпрограмма Такты % Источник Адрес Байты ============= ====== ====== ======= ======= ===== .main 763 92.7 version1.c 632 560
В первой части отчета tprof указано число тактов, затраченных на выполнение каждого процесса. Сама программа version1 заняла 763 такта и, кроме того, 30 тактов было затрачено на выполнение функций ядра, используемых в программе version1. Для выполнения version1 в оболочке Bourne было запущено два процесса. Кроме того, еще четыре процесса были выделены для tprof. Процесс init, программа-демон sync, процесс rlogin и еще один процесс заняли 14 тактов.
Обратите внимание, что при каждом вызове exec() запускается новая программа, однако она будет связана с тем же ИД. Если одно приложение с помощью exec() вызывает другое приложение, то в выводе tprof с именами обоих программ будет связан один и тот же ИД процесса.
Во второй части отчета приведены данные о программах, независимо от ИД процесса. Здесь указан номер (FREQ) процесса, который в некоторой точке запустил данную программу.
В третьей части приведено распределение пользовательских тактов, затраченных на выполнение анализируемой программы. Здесь указано число тактов, затраченных на выполнение каждой функции, и их доля от общего числа тактов CPU (823).
До этого момента команде tprof нигде не требовалась специально скомпилированная версия программы. Для получения всех предыдущих сведений не требуется исходный код программы.
Из данного отчета видно, что в основном (на 92.7 процента) CPU используется самой программой, а не ядром или подпрограммами применяемой библиотеки. Значит, нужно более детально проанализировать саму программу.
Так как программа version1.c была скомпилирована с опцией -g, объектный файл содержит информацию о соотношении смещений в тексте программы со строками исходного кода. На основе информации о смещениях и номерах строк в объектном модуле программа tprof создала версию исходного файла version1.c с комментариями и присвоила ему имя __t.version1.c. В первом столбце указан номер строки. Во втором столбце указано, сколько раз во время выполнения команд из данной строки возникало прерывание от таймера, зарегистрированное с помощью точки трассировки.
Отчет о тактах для функции main из файла version1.c Строка Такты Исходный код 14 34 for(OuterIndex=0; OuterIndex<Asize; OuterIndex++) 15 - { 16 40 for (InnerIndex=0; InnerIndex<Asize; InnerIndex++) 17 261 big[RowDim][ColDim] = 0x01010101; 18 - } 19 - Increment = rand(); 20 - 21 - /* увеличивает каждый элемент массива */ 22 70 for(OuterIndex=0; OuterIndex<Asize; OuterIndex++) 23 - { 24 - for (InnerIndex=0; InnerIndex<Asize; InnerIndex++) 25 - { 26 69 big[RowDim][ColDim] += Increment; 27 50 if (big[RowDim][ColDim] < 0) 28 239 printf("Отрицательное число.%d\n", big[RowDim][ColDim]); 29 - } 30 - } 31 - printf("Версия 1, контрольное число: %d\n", 32 - big[rand()%Asize][rand()%Asize]); 33 - return(0); 34 - } Всего тактов для функции main из файла version1.c - 763
Как видно из этого отчета, наибольшее число тактов расходуется на обращение к массиву big, поэтому можно значительно повысить производительность за счет изменения внутренних циклов for. Первый цикл for нерационален, так как на каждом витке цикла инициализируется всего один элемент массива. Для обнуления массива можно было бы воспользоваться функцией bzero(). Однако поскольку в каждый байт массива записывается некоторый символ, вместо первого цикла for можно воспользоваться функцией memset(). (Функции bzero() и memset(), а также str*(), написаны на языке Ассемблер. В них используются аппаратные команды, аналогов которым нет в языке C.)
Для увеличения элементов массива приходится последовательно обращаться к каждому элементу. В этом случае кэш-память используется наиболее эффективно, если элементы последовательно выбираются из памяти. Для этого элементы массива нужно перебирать по строкам, а не по столбцам. Поскольку массивы в языке C представляют собой последовательность строк, за одно обращение к памяти можно заполнить целую строку. Так как каждая строка содержит 1024 целых числа (4096 байт), то каждое последующее обращение происходит к новой странице. Размер массива намного превышает максимальный объем кэша данных и таблицы преобразования адресов (TLB), поэтому при выполнении программы кэш и TLB будут переполнены. Для предотвращения связанных с этим ошибок укажите две директивы #define для перестановки значений RowDim и ColDim.
Для выполнения неоптимизированного варианта итоговой программы (version2.c) требуется 2.7 секунды процессорного времени, а не 7.9 секунд, как это было при выполнении программы version1.
Следующий файл __t.version2.c, получен в результате обработки неоптимизированного варианта программой tprof:
Отчет о тактах для функции main из файла version2.c Строка Такты Исходный код 15 - memset(big,0x01,sizeof(big)); 16 - Increment = rand(); 17 - 18 - /* увеличение адреса памяти */ 19 60 for(OuterIndex=0; OuterIndex<Asize; OuterIndex++) 20 - { 21 - for (InnerIndex=0; InnerIndex<Asize; InnerIndex++) 22 - { 23 67 big[RowDim][ColDim] += Increment; 24 60 if (big[RowDim][ColDim] < 0) 25 43 printf("Отрицательное число. %d\n",big[RowDim][ColDim]); 26 - } 27 - } 28 - printf("Версия 2, контрольное число: %d\n", 29 - big[rand()%Asize][rand()%Asize]); 30 - return(0); 31 - } Всего тактов для функции main из version2.c - 230
После изменения программы время использования CPU сократилось почти втрое. Если выполнить компиляцию программ version1.c и version2.c с оптимизацией и сравнить их производительность, то окажется, что внесенные изменения ускоряют работу программы в 7 раз.
Во многих случаях большая часть времени CPU тратится не на саму программу, а на библиотечные процедуры. Если в программе version2.c удалить проверку условия в строке 24 и функцию printf() в строке 28, то мы получим еще одну версию программы version3.c, которая выглядит следующим образом:
#include <string.h> #include <stdlib.h> #define Asize 256 #define RowDim OuterIndex #define ColDim InnerIndex main() { int Increment; int OuterIndex; int InnerIndex; int big [Asize][Asize]; /* Присваивает каждому байту значение 0x01 */ memset(big,0x01,sizeof(big)); Increment = rand(); /* увеличение адреса памяти */ for(OuterIndex=0; OuterIndex<Asize; OuterIndex++) { for (InnerIndex=0; InnerIndex<Asize; InnerIndex++) { big[RowDim][ColDim] += Increment; printf("RowDim=%d, ColDim=%d, Number=%d\n", RowDim, ColDim, big[RowDim][ColDim]); } } return(0); }
В результате окажется, что значительную часть времени занимает выполнение оператора printf(). Команда
# tprof -v -s -k -p version3 -x version3 >/dev/null
создает файл __version3.all, содержащий результаты анализа для ядра и функций из стандартной библиотеки libc.a (единственной библиотеки, используемой в данной программе):
Процесс PID TID Всего Ядро Пользоват. Общие Другое ======= === === ===== ====== ==== ====== ===== version3 28372 28373 818 30 19 769 0 ksh 27348 27349 5 5 0 0 0 tprof 15986 19785 3 1 2 0 0 tprof 7784 8785 1 1 0 0 0 tprof 12904 13657 1 1 0 0 0 ksh 13940 13755 1 1 0 0 0 ======= === === ===== ====== ==== ====== ===== Всего 829 39 21 769 0 Процесс FREQ Всего Ядро Пользоват. Общие Другое ======= === ===== ====== ==== ====== ===== version3 1 818 30 19 769 0 ksh 2 6 6 0 0 0 tprof 3 5 3 2 0 0 ======= === ===== ====== ==== ====== ===== Всего 6 829 39 21 769 0 Всего тактов для version3( USER) = 19 Подпрограмма Такты % Источник Адрес Байты ============= ====== ====== ======= ======= ===== .main 11 1.3 version3.c 632 320 .printf 8 1.0 glink.s 1112 36 Всего тактов для version3( KERNEL) = 30 Подпрограмма Такты % Источник Адрес Байты ============= ====== ====== ======= ======= ===== .sc_flih 7 0.8 low.s 13832 1244 .i_enable 5 0.6 low.s 21760 256 .vmcopyin 3 0.4 vmmove.c 414280 668 .xix_setattr 2 0.2 xix_sattr.c 819368 672 .isreadonly 2 0.2 disubs.c 689016 60 .lockl 2 0.2 lockl.s 29300 208 .v_pagein 1 0.1 v_getsubs1.c 372288 1044 .curtime 1 0.1 clock.s 27656 76 .trchook 1 0.1 noname 48168 856 .vmvcs 1 0.1 vmvcs.s 29744 2304 .spec_rdwr 1 0.1 spec_vnops.c 629596 240 .rdwr 1 0.1 rdwr.c 658460 492 .imark 1 0.1 isubs.c 672024 184 .nodev 1 0.1 devsw_pin.c 135864 32 .ld_findfp 1 0.1 ld_libld.c 736084 240 Всего тактов для version3( SH-LIBs) = 769 Общий объект Такты % Источник Адрес Байты ============= ====== ====== ======= ======= ===== libc.a/shr.o 769 92.0 /usr/lib 794624 724772 Профиль: /usr/lib/libc.a shr.o Всего тактов для version3(/usr/lib/libc.a) = 769 Подпрограмма Такты % Источник Адрес Байты ============= ====== ====== ======= ======= ===== ._doprnt 476 56.9 doprnt.c 36616 7052 .fwrite 205 24.5 fwrite.c 50748 744 .strchr 41 4.9 strchr.s 31896 196 .printf 18 2.2 printf.c 313796 144 ._moveeq 16 1.9 memcmp.s 36192 184 .strlen 10 1.2 strerror.c 46800 124 .isatty 1 0.1 isatty.c 62932 112 ._xwrite 1 0.1 flsbuf.c 4240 280 .__ioctl 1 0.1 ioctl.c 57576 240
Из этого отчета видно, что большая часть тактов расходуется на выполнение библиотечных функций (в данном случае - функций из библиотеки libc.a). Отчет об анализе библиотеки libc.a говорит о том, что больше всего тактов требуется для выполнения функции _doprnt().
Функция _doprnt() представляет собой рабочий модуль функций printf(), sprintf() и т.д. После добавления функции форматированного вывода время выполнения программы увеличилось с 2.7 до 8.6 секунд, а время использования CPU - на 60 процентов. Следовательно, форматирование следует применять только там, где это действительно необходимо. Время выполнения функции _doprnt() зависит и от выбранной локали. Дополнительная информация приведена в Приложении E: Поддержка национального языка - зависимость производительности от локали. Эти тесты были выполнены в системе с наиболее эффективной локалью - C.
Флаг -iфайл-трассировки предназначен для запуска команды tprof в автономном режиме, в котором она обрабатывает файлы данных трассировки, созданные командой trace. Флаг -n позволяет задать файл gennames, который должен применяться при обработке файла в автономном режиме. Эти флаги позволяют обработать файл трассировки, который был создан на удаленном компьютере или ранее на том же компьютере. В этом случае с опцией -n должен быть задан файл gennames того компьютера, на котором выполнялась трассировка. Эти флаги также могут применяться в системе с высокой нагрузкой, если команда tprof пропустила точки трассировки. Опция автономной обработки позволяет решить эту проблему.
Команда trace собирает информацию о точках трассировки, связанных с командой tprof, которые заданы с флагом trace -j. После этого запускается функция gennames для сбора дополнительной информации для команды tprof. После выполнения команд trace и gennames файл-gennames необходимо обработать файл протокола трассировки с помощью команды trcrpt -r, перенаправив вывод в другой файл. Обработанный файл протокола трассировки и файл-gennames передаются на вход команде tprof.
Например:
# trace -af -T 1000000 -L 10000000 -o trace.out -j 000,001,002,003,005,006,234,106,10C,134,139,00A,465 # workload # trcoff # gennames > gennames.out # trcstop # trcrpt -r trace.out > trace.rpt
После этого запустите команду tprof, указав как минимум флаги -i и -n:
# tprof -i trace.rpt -n gennames.out -s -k -e
В многопроцессорной системе команды trace и trcrpt рекомендуется запускать с флагом -C all (см. Форматирование вывода команды trace -C).