# Сборка задач с профилем

- Введение

- Профилирование обычных приложений

- Профилирование демонов

- Профилирование ядра ОС

- Профилирование вызовов по указателю

- Важные особенности работы профилирования

## Введение

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

Профиль бывает реальным (динамическим) и предсказанным (статическим). Динамический профиль получается на основе тествого прогона программы, собранной специальным образом. Данный раздел рассматривает только работу пользователя с динамическим профилем.

## Профилирование обычных приложений

В случаях обычного пользовательского приложения (доступно glibc, стандартное завершение) применение профиля будет проходить по следующей схеме:

1. Сначала программа собирается с опцией `-fprofile-generate`. Данная опция производит инструментирование программы счётчиками, которые инкрементируются каждый раз когда программа проходит по данному маршруту. По умолчанию профильная информация будет сбрасываться в рабочий каталог. Для указания другого каталога можно подать его в опцию: `-fprofile-generate=<путь_к_каталогу>`.

2. Далее необходимо запустить проинструментированную программу на тестовых данных. После завершения каждого запуска инструментированной программы в указанном при сборке каталоге будет создаваться файл `eprof.<N>.sum`, где `<N>` - порядковый номер созданного файла. Т.е. если для сбора тестовых данных необходимо выполнить 3 запуска, то в каталоге будут созданы файлы: `eprof.0.sum`, `eprof.1.sum` и `eprof.2.sum`.

3. После получения файла профиля нужно пересобрать программу с опцией `-fprofile-use`. Данная опция загрузит значения счётчиков из файла профиля `eprof.sum`, который является результатом суммирования счётчиков из файлов `eprof.<N>.sum` и создаётся компилятором автоматически. Можно указать конкретный файл профиля, подав аргумент опции: `-fprofile-use=<путь_к_prof.sum>`.

Пример:

    $ cat > t.c
    #include <stdio.h>

    int main(void)
    {
        printf("Hello\n");
        return 0;
    }

    $ lcc -fprofile-generate=`pwd/` t.c -O2

    $ ./a.out ; ./a.out ; ./a.out
    Hello
    Hello
    Hello

    $ ls
    a.out  eprof.0.sum  eprof.1.sum  eprof.2.sum  t.c

    $ lcc -fprofile-use=`pwd/` t.c -O2

    $ ls
    a.out  eprof.sum  eprof.0.sum  eprof.1.sum  eprof.2.sum  t.c

Профилирование позволяет использовать тонкую настройку имени получаемого файла счётчиков. Переменная среды `MCST_PROF_DIR=<имя_каталога>`, установленная во время запуска компилятора, позволяет задавать необходимый каталог для профиля. Данная переменная имеет приоритет над каталогом, указанным в опции компиляции `-fprofile-generate=<имя_каталога>` и `-fprofile-use=<имя_каталога>`.

Если во время исполнения приложения выставлена переменная окружения `MCST_PROF_DIR_PROG=1`, то файл с профильной информацией будет создан в том же каталоге, в котором находится запущенный исполняемый файл. Данная переменная имеет приоритет над каталогом, указанным в опции компиляции `-fprofile-generate=<имя_каталога>`.

## Профилирование демонов

Если необходимо получить профиль приложения, которое не завершается обычным способом (или просто без завершения его работы), то можно задать сигнал, по которому приложение сбросит профиль. Схема сборки инструментированного приложения остаётся прежней, нужно только записать в переменную среды `ECOMP_PROF_SIG` номер сигнала, по которому приложение должно сбрасывать профиль. После этого командой `pkill` можно послать соответствующий сигнал и получить файл профиля.

Пример:

    $ cat > t.c
    #include <stdlib.h>
    #include <unistd.h>
    #include <signal.h>
    #include <stdio.h>

    static FILE * fd;

    #ifdef SIG
    void handler( int un )
    {
        fprintf( fd, "GOT SIGINT\n" );
        exit( 1 );
    }
    #endif

    int main( int argc, char ** argv )
    {
        if ( fork() ) {
            exit( 0 );
        }

        if ( 2 != argc ) {
            fprintf( stderr, "need file name\n" );
            exit( 2 );
        }

        fd = fopen( argv[1], "w" );

        if ( NULL == fd ) {
            fprintf( stderr, "Couldn't open file\n" );
            exit( 2 );
        }

    #ifdef SIG
        signal( SIGINT, handler );
    #endif

        pause();

        return 0;
    }

    $ lcc -fprofile-generate=`pwd/` -O2 -DSIG t.c

    $ export ECOMP_PROF_SIG="16"

    $ ./a.out signal.txt

    $ pkill -16 a.out

    $ ls
    a.out  eprof.0.sum  signal.txt  t.c

    $ lcc_i -fprofile-use -O2 -DSIG t.c

Данная функциональность была реализована для возможности профилирования приложений типа PostgresQL.

## Профилирование ядра ОС

Компилятор обладает возможностью собирать ядро linux с инструментированием и последующим использованием полученного профиля. Для использования данной возможности необходимо ядро со специальными правками для компилятора lcc. Профилирование ядра является сложным процессом, поэтому конкретные действия могут отличаться от примера ниже.

Сначала соберём ядро с инструментированием:

    $ make defconfig

    # Включаем CONFIG_EPROF_KERNEL
    $ make xconfig

    $ gmake bootimage PROFILE_GENERATE=1

При желании сборку можно проводить в отдельном каталоге:

    $ gmake PROFILE_GENERATE=1 O=`pwd` -C <путь_к_коду_ядра> bootimage

В этой строке переменная `O` будет хранить путь куда складывать результаты сборки, опция `-C` указывает на каталог собираемого ядра. Обязательно выставлять переменной `PROFILE_GENERATE` занчение `1`, иначе код ядра не будет проинструментирован.

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

    $ cat /proc/kernel_profile > prof.sum

Здесь `/proc/kernel_profile` - специальный файл, чтение которого приводит к выводу профильной информации на экран.

После этого можно пересобирать ядро с полученным профилем:

    $ # Выключаем CONFIG_EPROF_KERNEL
    $ make xconfig

    $ gmake bootimage PROFILE_USE=<путь_к_prof.sum>

Во время сборки ядра с использованием профиля могут возникать проблемы с неприменимостью полученного профиля к отдельным функциям. Обычно такого быть не должно, или таких функций будет немного. В этом случае следует исключать функции из сборки с профилем опцией `-fprofile-skip-proc`

Для ядра версий linux-3.* ** профилирование реализовано в компиляторе **lcc-23, для ядра версий 4.19.* ** профилирование реализовано в компиляторах версии **>=lcc-24.

Профилирование ядра может быть полезно для приложений, которые много времени проводят внутри системных вызовов, например СУБД.

## Профилирование вызовов по указателю

Компилятор умеет собирать статистику наиболее часто вызываемых по указателю функций. Эта статистика позволяет увеличить точность подстановки функций там где объект вызова заранее неизвестен. Включается такое профилирование добавлением опции:

    -fprofile-values

Warning

На данный момент профилирование вызовов по указателю для параллельных программ работает некорректно.

Данный тип профилирования может быть полезен в случаях активного использования виртуальных функций в C++ или неявных вызовов в C. Однако он значительно замедляет работу инструментированного кода.

## Важные особенности работы профилирования

Для профилирования необходимо производить сборку программ два раза - для получения профильной информации и для её использвоания. Оба раза программа должна собираться с одинаковыми опциями. Если опции сборки различаются (например `-O1 -fprofile-generate` и `-O3 -fprofile-use`), то корректность работы профилирования не гарантируется, скорей всего профильная информация просто не применится.

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

При использовании систем автоматической сборки (например make) важно понимать что запуск компилятора с опцией `-fprofile-use` сначала создаёт файл `eprof.sum` на основе файлов `eprof.<N>.sum`, поэтому при запуске нескольких копий компилятора, они все будут писать в этот файл одновременно. Для избегания такой ситуации следует произвести первый одиночный запуск компилятора с опцией `-fprofile-use`, который создаст данный файл, после чего можно запускать параллельно любое количество копий компилятора, т.к. они будут только читать из данного файла.

Использование систем автоматической сборки (например make) также часто сопровождается сменой рабочего каталога. Опции `-fprofile-use` и `-fprofile-generate` без параметра используют текущий рабочий каталог (`-fprofile-use` использует текущий каталог времени компиляции, `-fprofile-generate` - текущий каталог времени исполнения). При сборке приложения может оказаться, что эти каталоги различаются. Поэтому данные опции рекомендуется использовать с указанием абсолютного пути к каталогу, в котором создавать/из которого брать файл с профильной информацией.

Если приложение является многопоточным (использование omp, нитей, потоков, дочерних процессов и т.п.), то при инструментирующей сборке необходимо подавать опцию:

    -fprofile-generate-parallel.

Данная опция укажет компилятору что код необходимо инструментировать не обычными инструкциями `addd`, а специальными `builtin`’ами с атомарным инкрементом. При работе с ядром опция `-fprofile-generate-kernel` использует не компиляторные `builtin`’ы, а функции атомарного сложения, предоставляемые ядром.

Иногда бывает полезно исключить какие-либо модули или отдельные функции из сбора профиля. Для этого существуют следующие опции:

    -fprofile-skip-proc=proc1,proc2,...,procN

Не применяет профилирование к указанным функциям

    -fprofile-skip-module=module1,module2,...,moduleN

Не применяет профилирование к указанным модулям. Особенностью данной опции является то что именем модуля будет строка, которая подаётся на вызов компилятору. Т.е. если вызов компилятора был следующего вида: `lcc -fprofile-generate ./src/t.c`, то именем модуля будет строка `./src/t.c`.

Начиная с версии >=lcc-24 для указания исключаемых или инструментируемых файлов можно применять регулярные выражения:

    -fprofile-filter-files=<regex1;regex2;...>

Указывает файлы, которые следует профилировать. Файлы, не попадающие под шаблон профилироваться не будут.

    -fprofile-exclude-files=<regex1;regex2;...>

Указывает файлы, которые профилироваться не будут.