[ Страница назад | Страница вперед | Содержание | Индекс | Библиотека | Юридическая информация | Поиск ]

Руководство по настройке производительности


Анализ интенсивности использования процессора с помощью программы tprof

Обычно при выполнении приложения задействованы код приложения, библиотечные функции и службы ядра. В неоптимизированных программах большая часть времени CPU тратится на выполнение нескольких операторов или подпрограмм. Обычно программист не учитывает большую нагрузку, создаваемую такими критическими участками. Часто это приводит к снижению производительности. Такие критические участки можно найти с помощью команды tprof (более подробная информация об этой команде приведена в разделе Команда tprof). Команда tprof применяется для анализа программ, созданных компиляторами C, C++ и FORTRAN.

Для того чтобы узнать, установлена ли команда tprof, вызовите следующую команду:

# lslpp -lI perfagent.tools

Исходные данные для программы tprof предоставляются функцией трассировки (см. Глава 12. Анализ производительности с помощью функции трассировки). Во время анализа программы профайлером активизируется функция трассировки, которая считывает в точке трассировки с ИД 234 содержимое регистра адреса команды при получении сигнала прерывания от системных часов (100 раз в секунду на каждом процессоре). Кроме того, активизируются некоторые другие точки трассировки, позволяющие tprof отслеживать работу процессов и передавать управление. Записи трассировки не сохраняются в файле на диске, а передаются в конвейер, из которого считываются специальной программой. Эта программа создает таблицу, в которой содержатся все уникальные адреса и число обращений по каждому из них. После анализа рабочей схемы эта таблица записывается на диск. Затем компонент tprof, предназначенный для сокращения объема данных, сопоставляет найденные адреса команд с диапазонами адресов, выделенными различным программам, и сообщает о распределении обращений по адресам (тактах) в программах, включенных в рабочую схему.

Распределение тактов примерно пропорционально времени CPU, которое затрачивается на выполнение программы (10 миллисекунд на один такт). Определив программы, для выполнения которых требуется много ресурсов, программист может попытаться избавиться от критических участков или уменьшить их число.

Пример применения команды tprof

В следующей программе на 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.

Автономная обработка данных с помощью команды tprof

Флаг -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).


[ Страница назад | Страница вперед | Содержание | Индекс | Библиотека | Юридическая информация | Поиск ]