Enrico Nicoletto
liverig@gmail.com 2012
Rafael Fontenelle
rafaelff@gnome.org 2013-2019

# Utilizando Massif para perfilar utilização de memória em software do GNOME

Este artigo descreve como utilizar o perfilador (profiler) de heap Massif com aplicativos do GNOME. Nós descrevemos como invocar, interpretar e agir sobre a saída do Massif. O jogo Swell Foop é utilizado como exemplo.

# Introdução

Massif é um membro da suíte de ferramentas de perfilamento de memória valgrind. Seu propósito é fornecer uma visão detalhada da utilização de memória dinâmica durante o tempo de vida do programa. Especificamente, ele grava a utilização de heap e de stack (pilha) da memória.

O heap é a região da memória que é alocada com funções como malloc. Ele cresce por demanda e geralmente é a maior região de memória em um programa. A pilha (stack) é onde são armazenados todos os dados locais para as funções. Isto inclui as variáveis “automáticas” em C e os endereços de retorno para as sub-rotinas. A pilha é tipicamente bem menor e muito mais ativa que o heap. Nós não vamos considerar explicitamente como pilha já que o Massif o trata como se fosse apenas outra parte do heap. Massif também fornece informações sobre quanta memória é utilizada para gerenciar o heap.

O Massif produz dois arquivos de saída: um resumo gráfico em um arquivo postscript e uma discriminação detalhada em um arquivo de texto.

# Utilizando o Massif com o GNOME

Massif possui muitas poucas opções e para muitos programas elas não são necessárias. Entretanto para aplicativos GNOME, onde a alocação de memória pode estar enterrada nas profundezas do glib ou GTK, os descendentes do Massif precisam ser aumentados a medida que o número de níveis da pilha de chamadas abaixam. Isto é alcançado usando o parâmetro --depth. Por padrão, este valor é 3; aumentando ele para 5 irá garantir que a pilha de chamadas atinja o seu código. Mais um ou dois níveis podem ser convenientes para fornecer algum contexto ao seu código. Uma vez que o nível de detalhamento se torna rapidamente sobrecarregado, é melhor iniciar com o parâmetro de menor profundidade (depth) e apenas aumentá-lo quando se torna aparente que isto não é suficiente.

Também é útil avisar o Massif quais funções alocam memória no glib. Isto remove uma camada desnecessária de chamadas de função dos relatórios e oferece a você uma ideia mais clara de qual código está alocando memória. As funções de alocação do glib são: g_malloc, g_malloc0, g_realloc, g_try_malloc e g_mem_chunk_alloc. Você utiliza a opção --alloc-fn para contar ao Masiff sobre elas.

A partir de agora, sua linha de comando deve parecer como algo assim:

valgrind --tool=massif --depth=5 --alloc-fn=g_malloc --alloc-fn=g_realloc --alloc-fn=g_try_malloc \ --alloc-fn=g_malloc0 --alloc-fn=g_mem_chunk_alloc swell-foop

Swell Foop é o programa que estaremos utilizando como exemplo. Esteja avisado de quê, uma vez que valgrind emula a CPU, ele irá rodar muito lentamente. Você precisará também de muita memória.

# Interpretando os resultados

A saída gráfica do Massif é amplamente auto-explicativa. Cada faixa representa a memória alocada para uma função em relação ao tempo. Uma vez que você identifique quais faixas estão usando a maior parte da memória, geralmente a mais espessa ao topo, você terá de consultar o arquivo de texto para os detalhes.

O arquivo de texto é organizado em hierarquia de seções, ao topo há uma lista dos piores usuários de memória dispostos em espaço-tempo decrescente. Abaixo disso há mais seções, cada qual decompondo os resultados em detalhes mais sutis a medida que você prossegue para a porção inferior da pilha de chamadas. Para esclarecer isto, nós utilizaremos a saída do comando supra-citado.

## Saída do Massif para a versão não otimizada do programa Swell Foop.

A imagem acima mostra uma saída postscript típica do Massif. Este é o resultado que você obteria ao jogar um único jogo do Swell Foop (versão 2.8.0) e depois saindo. O arquivo postscript terá um nome como massif.12345.ps e o arquivo de texto será chamado massif.12345.txt. O número ao meio é o ID do processo do programa que foi examinado. Se você realmente tentar realizar este exemplo, você encontrará duas versões para cada arquivo com números levemente diferentes, devido o Swell Foop iniciar um segundo processo e o Massif o seguir também. Nós iremos ignorar este segundo processo, ele consome muito pouca memória.

Ao topo do gráfico, nós vemos uma ampla faixa amarela rotulada gdk_pixbuf_new. Ela parece ser uma candidata ideal para otimização, mas nós iremos precisar utilizar o arquivo de texto para descobrir o que está chamando gdk_pixbuf_new. O topo do arquivo de texto irá se parecer como algo assim:

Comando: ./swell-foop

== 0 =========================== Heap allocation functions accounted for 90.4% of measured spacetime

Called from: 28.8% : 0x6BF83A: gdk_pixbuf_new (in /usr/lib/libgdk_pixbuf-2.0.so.0.400.9)

6.1% : 0x5A32A5: g_strdup (in /usr/lib/libglib-2.0.so.0.400.6)

5.9% : 0x510B3C: (within /usr/lib/libfreetype.so.6.3.7)

3.5% : 0x2A4A6B: __gconv_open (in /lib/tls/libc-2.3.3.so)

A linha com o sinal de '=' indica a que distância abaixo do rastreamento da pilha estamos, neste caso estamos ao topo. Após isto, são listados os usuários de memória mais pesados em ordem decrescente em relação ao tempo. O espaço-tempo é o produto da quantidade de memória utilizada e por quanto tempo ela foi utilizada. Isto corresponde a área das faixas no gráfico. Esta parte do arquivo nos conta o que já sabíamos: a maior parte do espaço-tempo é dedicado ao gdk_pixbuf_new. Para descobrir o que chamou o gdk_pixbuf_new nós precisamos pesquisar, logo abaixo, o arquivo de texto:

== 4 =========================== Context accounted for 28.8% of measured spacetime 0x6BF83A: gdk_pixbuf_new (in /usr/lib/libgdk_pixbuf-2.0.so.0.400.9) 0x3A998998: (within /usr/lib/gtk-2.0/2.4.0/loaders/libpixbufloader-png.so) 0x6C2760: (within /usr/lib/libgdk_pixbuf-2.0.so.0.400.9) 0x6C285E: gdk_pixbuf_new_from_file (in /usr/lib/libgdk_pixbuf-2.0.so.0.400.9)

Called from: 27.8% : 0x804C1A3: load_scenario (swell-foop.c:463)

0.9% : 0x3E8095E: (within /usr/lib/libgnomeui-2.so.0.792.0)

and 1 other insignificant place

A primeira linha nos conta que agora estamos quatro níveis dentro da pilha. Abaixo há uma listagem das chamadas de função que partem daqui até o gdk_pixbuf_new. Finalmente há uma lista de funções que estão no próximo nível inferior e chamam estas funções. Também existem, obviamente, entradas para os níveis 1, 2 e 3, mas este é o primeiro nível a alcançar logo abaixo, através do código GDK, o código do Swell Foop. A partir desta listagem, nós podemos ver instantaneamente que o problema do código é o load_scenario.

Agora que sabemos qual parte do nosso código está utilizando todo o espaço-tempo, nós podemos olhar para ele e descobrir o porquê. Verifica-se que o load_scenario está carregando um pixbuf a partir do arquivo e então nunca liberando aquela memória. Tendo sido identificado o código problemático, nós podemos começar a consertá-lo.

# Atuando sobre os resultados

Reduzir o consumo de espaço-tempo é bom, porém há duas formas de reduzi-lo e elas não são iguais. Você pode tanto reduzir a quantidade de memória alocada ou reduzir o montante de tempo que é alocado para isto. Por um momento, considere um modelo de sistema com apenas dois processos em execução. Ambos os processos utilizam quase toda a memória RAM física e se eles se sobrepõem completamente, então o sistema irá utilizar a memória de troca (swap) e tudo vai ficar lento. Obviamente, se nós reduzimos a utilização de memória de cada processo por um fator de dois, então eles podem coexistir pacificamente sem a necessidade de utilizar a memória de troca (swap). Se ao invés disso, nós reduzimos o tempo o qual a memória é alocada por um fator de dois, então os dois programas podem coexistir, mas apenas enquanto seus períodos de alta utilização de memória não sobreporem-se. Logo, é melhor reduzir o montante de memória alocada.

Infelizmente, a escolha de otimização também é reprimida pelas necessidades do programa. O tamanho dos dados do pixbuf no Swell Foop é determinado pelo tamanho dos gráficos do jogo e não pode ser reduzido facilmente. Entretanto, a quantidade de tempo que ele gasta sendo carregado para dentro da memória, pode ser drasticamente reduzido. A imagem abaixo mostra a análise feita pelo Massif do Swell Foop após se dispor do pixbufs, uma vez que as imagens foram carregadas dentro do servidor X.

## Saída do Massif para o programa Swell Foop otimizado.

A utilização de espaço-tempo do gdk_pixbuf_new é agora uma faixa fina que apenas sofre picos de forma leve (é a décima-sexta faixa para baixo e sombreada de magenta). Como bônus, o pico de utilização de memória caiu 200 kB, já que o pico ocorre antes de outra memória ser alocada. Se dois processos como este são executados juntos, as chances do pico de utilização de memória coincidirem, e por isso o risco de usar memória de troca (swap), seriam bastante baixos.

Nós podemos fazer melhor? Uma verificação rápida na saída de texto do Massif revela: g_strdup como o novo maior ofensor.

Comando: ./swell-foop

== 0 =========================== Heap allocation functions accounted for 87.6% of measured spacetime

Called from: 7.7% : 0x5A32A5: g_strdup (in /usr/lib/libglib-2.0.so.0.400.6)

7.6% : 0x43BC9F: (within /usr/lib/libgdk-x11-2.0.so.0.400.9)

6.9% : 0x510B3C: (within /usr/lib/libfreetype.so.6.3.7)

5.2% : 0x2A4A6B: __gconv_open (in /lib/tls/libc-2.3.3.so)

Se olhamos mais de perto, observamos que ele é chamado de muitos, muitos, lugares.

== 1 =========================== Context accounted for 7.7% of measured spacetime 0x5A32A5: g_strdup (in /usr/lib/libglib-2.0.so.0.400.6)

Called from: 1.8% : 0x8BF606: gtk_icon_source_copy (in /usr/lib/libgtk-x11-2.0.so.0.400.9)

1.1% : 0x67AF6B: g_param_spec_internal (in /usr/lib/libgobject-2.0.so.0.400.6)

0.9% : 0x91FCFC: (within /usr/lib/libgtk-x11-2.0.so.0.400.9)

0.8% : 0x57EEBF: g_quark_from_string (in /usr/lib/libglib-2.0.so.0.400.6)

and 155 other insignificant places

Agora nós nos deparamos com retornos cada vez menores para os nossos esforços de otimização. O gráfico sugere outra possível abordagem: Tanto as faixas “outros” e “gerenciamento de heap” são bem grandes. Isto nos diz que há um monte de pequenas alocações sendo feitas a partir de uma variedade de lugares. Eliminá-las será difícil porém se elas puderem ser agrupadas, então as alocações individuais poderão ser maiores e a elevada faixa “gerenciamento de heap” poderá ser reduzida.

# Advertências

Existem um par de coisas as quais devemos ficar atentos: Primeiramente, espaço-tempo é apenas informado como uma porcentagem, você deve compará-lo ao tamanho total do programa, para decidir se vale a pena você ir atrás de quantidade de memória. O gráfico, com seu eixo vertical em kilobytes, é bom para isto.

Em segundo lugar, o Massif apenas leva em consideração a memória utilizada pelo seu próprio programa. Recursos como pixmaps são armazenados no servidor X e não são considerados pelo Massif. No exemplo do Swell Foop nós realmente só mudamos o consumo de memória do lado do cliente do pixbufs para o lado do servidor pixmaps. Apesar de termos trapaceado, ocorreram ganhos de performance. Manter os dados de imagem no servidor X, faz com que as rotinas dos gráficos fiquem mais rápidas e remove uma grande quantidade de comunicações entre processos (ipcs). Ademais, os pixmaps serão armazenados em um formato gráfico nativo que geralmente é mais compacto que o formato RGBA de 32-bits utilizado pelo gdk_pixbuf. Para medir o efeito de pixmaps, e outros recursos X, utilize o programa xrestop.