Profiling com gprof e perf

1. Introdução ao Profiling em C

Profiling é o processo de análise dinâmica de um programa para identificar onde o tempo de execução é gasto. Em Linguagem C, essa prática é especialmente crítica devido ao controle manual de memória, acesso direto a hardware e operações de baixo nível que podem esconder gargalos sutis. Diferente de linguagens gerenciadas, em C cada instrução conta — um loop mal otimizado ou uma função de alocação frequente pode degradar drasticamente o desempenho.

Existem duas abordagens principais de profiling:

  • Instrumentação (gprof): O compilador insere hooks (código extra) em cada chamada de função para contar execuções e medir tempo gasto. Gera relatórios precisos de contagem de chamadas, mas adiciona overhead significativo.
  • Amostragem (perf): O sistema operacional interrompe o programa em intervalos regulares (ou em eventos de hardware) e registra o contador de programa atual. Menos intrusivo, ideal para análise estatística de hot spots.

Use gprof quando precisar de um mapa completo de chamadas de funções em programas single-thread. Use perf para micro-otimizações, análise de cache, branch prediction e quando o overhead do gprof distorcer os resultados.

2. gprof: Profiling por Instrumentação

Para usar gprof, o programa deve ser compilado com a flag -pg:

gcc -pg -o meu_programa main.c funcoes.c

O compilador insere instruções de prólogo em cada função que registram o endereço de retorno e contabilizam o tempo. Ao executar o programa normalmente:

./meu_programa

Um arquivo gmon.out é gerado no diretório de trabalho. Para gerar o relatório legível:

gprof meu_programa gmon.out > relatorio.txt

O relatório contém:

  • Flat profile: Lista funções ordenadas por tempo próprio (self seconds) — tempo gasto dentro da função excluindo chamadas internas.
  • Call graph: Mostra quem chamou quem, número de chamadas e tempo acumulado (incluindo funções filhas).
  • Index: Mapeia funções para números no gráfico de chamadas.

Exemplo de saída do flat profile:

Each sample counts as 0.01 seconds.
  %   cumulative   self              self     total
 time   seconds   seconds    calls  ms/call  ms/call  name
 45.2      2.34     2.34   100000    0.023    0.051  bubble_sort
 30.1      3.89     1.55    50000    0.031    0.031  swap
 15.3      4.68     0.79   100000    0.008    0.008  compare
  9.4      5.17     0.49        1  490.00  517.00  main

3. Limitações e Boas Práticas com gprof

O gprof tem limitações importantes:

  • Overhead: A instrumentação pode aumentar o tempo de execução em 30-200%. Para programas com muitas funções pequenas, os resultados podem ser distorcidos.
  • Multi-thread: gprof não suporta threads POSIX. Apenas a thread principal é monitorada; threads filhas não geram dados.
  • Otimizações do compilador: Flags como -O2 ou -O3 podem inlinar funções, removendo os hooks do gprof. Compile com -O0 -pg para dados precisos, ou use -fno-inline com otimizações.
  • Bibliotecas estáticas: Se uma biblioteca foi compilada sem -pg, suas funções não serão instrumentadas. Bibliotecas dinâmicas (.so) não são suportadas — gprof só funciona com código estaticamente linkado.

Boas práticas:
- Use um conjunto de dados representativo, mas reduzido, para diminuir o tempo de execução com overhead.
- Compare perfis com e sem -pg para avaliar o impacto da instrumentação.
- Para bibliotecas, recompile-as com -pg ou use profiling amostral (perf).

4. perf: Profiling Amostral no Linux

O subsistema perf_events do Linux permite coletar amostras baseadas em eventos de hardware e software. Para usar perf, o programa deve ser compilado com informações de depuração (-g):

gcc -g -O2 -o meu_programa main.c

Coleta de amostras

perf record -F 100 -g ./meu_programa

Onde:
- -F 100: taxa de amostragem de 100 Hz (padrão é 4000 Hz; valores menores reduzem overhead).
- -g: habilita gravação do call graph (pilha de chamadas).
- Eventos padrão: cycles (ciclos de CPU). Para outros: perf record -e cache-misses,instructions -F 1000.

Análise dos resultados

perf report

Exibe uma interface interativa com as funções que mais consomem CPU. Use perf annotate para ver o código assembly e fonte anotado com contagem de amostras:

perf annotate -s nome_da_funcao

Exemplo de saída do perf report:

Samples: 1K of event 'cycles:u', Event count (approx.): 850000000
Overhead  Command      Shared Object     Symbol
  35.2%  meu_programa  meu_programa      [.] bubble_sort
  22.8%  meu_programa  meu_programa      [.] swap
  15.1%  meu_programa  libc-2.31.so      [.] __memcpy_avx_unaligned
  10.3%  meu_programa  meu_programa      [.] compare

5. Técnicas Avançadas com perf

Eventos personalizados com perf stat

Para medir métricas específicas sem instrumentar o código:

perf stat -e cycles,instructions,cache-misses,branch-misses ./meu_programa

Saída típica:

Performance counter stats for './meu_programa':
        850,000,000      cycles
      1,200,000,000      instructions
           50,000,000      cache-misses
           10,000,000      branch-misses
       0.523456789 seconds time elapsed

Geração de Flame Graphs

Flame graphs visualizam a pilha de chamadas ao longo do tempo. Primeiro, gere dados no formato adequado:

perf script -f > out.perf

Depois, use scripts do repositório FlameGraph (https://github.com/brendangregg/FlameGraph):

./stackcollapse-perf.pl out.perf > out.folded
./flamegraph.pl out.folded > graph.svg

Comparação de versões com perf diff

Para identificar regressões de desempenho entre duas versões:

perf record -o perf_old.data ./programa_antigo
perf record -o perf_new.data ./programa_novo
perf diff perf_old.data perf_new.data

Mostra diferenças percentuais no consumo de CPU por símbolo.

6. Casos Práticos em C: Otimizando um Programa Real

Considere um programa que ordena um array de 100.000 inteiros usando bubble sort. Identificamos gargalo com gprof:

// main.c
#include <stdio.h>
#include <stdlib.h>

void swap(int *a, int *b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}

void bubble_sort(int arr[], int n) {
    for (int i = 0; i < n-1; i++)
        for (int j = 0; j < n-i-1; j++)
            if (arr[j] > arr[j+1])
                swap(&arr[j], &arr[j+1]);
}

int main() {
    int n = 100000;
    int *arr = malloc(n * sizeof(int));
    for (int i = 0; i < n; i++) arr[i] = rand() % 1000;
    bubble_sort(arr, n);
    free(arr);
    return 0;
}

Passo a passo com gprof:

  1. Compilar com -pg -g -O0:
gcc -pg -g -O0 -o bubble main.c
  1. Executar com dados reduzidos (n=10000 para não demorar horas):
./bubble
gprof bubble gmon.out > profile.txt
  1. Analisar: bubble_sort consome 45% do tempo, swap 30%. Identificamos que swap é chamada muitas vezes.

  2. Refatorar: substituir bubble sort por qsort da libc:

int cmp(const void *a, const void *b) {
    return (*(int*)a - *(int*)b);
}
qsort(arr, n, sizeof(int), cmp);

Passo a passo com perf:

  1. Compilar versão otimizada com -g -O2:
gcc -g -O2 -o bubble_opt main.c
  1. Coletar amostras:
perf record -F 1000 -g ./bubble_opt
perf report
  1. Identificar hot spots: se qsort ainda aparece como gargalo, use perf annotate para ver onde o tempo é gasto dentro da função de comparação.

  2. Otimizar: inline a função de comparação ou usar um algoritmo mais adequado (radix sort para inteiros).

7. Integração e Ferramentas Complementares

Combinar gprof e perf oferece uma visão completa:

  • gprof para entendimento macro: quem chama quem, contagem de chamadas.
  • perf para micro-análise: onde exatamente o tempo é gasto, eventos de cache, branch prediction.

Ferramentas complementares:

  • gprof2dot (https://github.com/jrfonseca/gprof2dot): Converte saída do gprof em gráficos DOT para visualização.
  • KCachegrind (https://kcachegrind.github.io/): Visualizador de dados de profiling que aceita saída do Valgrind e perf.
  • Valgrind --tool=callgrind (https://valgrind.org/docs/manual/cl-manual.html): Profiling por instrumentação detalhada, incluindo simulação de cache. Mais lento que perf, mas mais preciso para análise de cache.

Para flame graphs, use os scripts de Brendan Gregg (https://github.com/brendangregg/FlameGraph), que convertem dados do perf script em visualizações interativas.

Referências