De acordo com as Leis 12.965/2014 e 13.709/2018, que regulam o uso da Internet e o tratamento de dados pessoais no Brasil, ao me inscrever na newsletter do portal DICAS-L, autorizo o envio de notificações por e-mail ou outros meios e declaro estar ciente e concordar com seus Termos de Uso e Política de Privacidade.

Otimização de Desempenho em sistemas GNU/Linux

Colaboração: Rubens Queiroz de Almeida

Data de Publicação: 14 de agosto de 2010

Este artigo foi traduzido e adaptado a partir do original em inglês, de autoria de Swayam Prakasha, do artigo Optimizing Linux System Performance, publicado em 6 de julho de 2007, no site LinuxDevCenter.com

Otimização de desempenho em sistemas GNU/Linux nem sempre significa o que podemos pensar. Não é apenas diretamente uma questão de velocidade; às vezes pode envolver ajustar o sistema para fazer com que consiga caber dentro de um sistema com pouca memória. Sem dúvida, é difícil encontrar um programador que não queira fazer seus programas rodarem mais rápido, independentemente da plataforma. Programadores Linux não são exceção; alguns deles chegam às raias do fanatismo na forma como se dedicam à tarefa de otimizar seu código para obter melhor desempenho. A medida que o hardware se torna mais rápido, barato e abundante, alguns argumentam que otimização de performance não é tão algo tão crítico -- com especial destaque para pessoas envolvidas com desenvolvimento de software e que tentam seguir à risca os prazos estabelecidos. Não é bem assim, mesmo o hardware mais avançado dos dias de hoje, combinado com as últimos avanços na tecnologia de otimização de compiladores, não consegue nem chegar perto dos benefícios de melhoria de desempenho que podem ser alcançados através da correção de alguns pequenos programas, ou mesmo chegando ao ponto de adotar um projeto inteiramente diferente e muito mais rápido.

Muitas idéias podem ser empregadas para fazer programas obterem um desempenho melhor. Tendo estas idéias em mente ao escrever (e revisar) o código, podemos esperar que os programas sejam melhores e mais rápidos. Quando falamos sobre performance, precisamos levar em consideração diversas coisas diferentes. Uma é a quantidade absoluta de tempo necessária para que o software finalize uma dada tarefa. Considere um exemplo em que mesmo se um servidor web atende o pedido de um cliente perfeitamente, pode haver um atraso de alguns segundos antes que o servidor comece a enviar as páginas para o cliente. Em tal caso, o servidor web está falhando em obter um desempenho adequado em termos do tempo total exigido para completar a tarefa.

Outro fator a ser considerado é o tempo de CPU requerido por um programa. Tempo de CPU é uma medida do tempo gasto pelos processadores do computador para executar o código. Muitos programas tendem a gastar a maior parte de seu tempo esperando que algo aconteça -- chegada dos dados de entrada, saída dos dados para serem escritos no disco, etc. Enquanto espera, a CPU irá atender outros pedidos e, portanto, o programa não estará usando tempo de CPU. Entretanto, alguns programas podem ser primariamente dependentes de CPU e, para tais programas, uma economia no tempo de CPU necessário pode resultar em uma economia substancial do tempo absoluto.

É importante observar que se o seu programa usa muito tempo de CPU, ele pode impactar o tempo de execução de todos os processos em seu sistema. O tempo de CPU podem ser separado em tempo de sistema e tempo do usuário. O tempo do sistema é o tempo de CPU usado pelo kernel para atender aos seus pedidos. Isto pode ocorrer quando da invocação de funções tais como open() e fork(). O tempo de usuário (i.e. a quantidade de tempo de CPU usada pelo seu programa) pode ser usada para manipulação de strings e operações aritméticas. Um terceiro fator a ser considerado para desempenho é o tempo gasto realizando I/O. Se considerarmos que alguns programas, tais como servidores de rede, gastam a maior parte do seu tempo gerenciando solicitações de I/O. Outros programas gastam pouco tempo em tarefas relacionadas com I/O. Então, a otimização de operações de I/O pode ser muito crítica em alguns projetos e completamente irrelevantes em outros.

A otimização de performance consiste basicamente dos seguintes passos:

  1. Definir o problema de performance.
  2. Identificar os gargalos e fazer uma análise da causa.
  3. Remover os gargalos utilizando as metodologias apropriadas.
  4. Repetir os passos 2 e 3 até que se chegue a uma solução satisfatória.

É importante observar que gargalos ocorrem em diversos pontos em um sistema. Determinar os gargalos é um procedimento passo a passo para se chegar às causas primárias. Otimização de performance é um processo relativamente complexo que requer a correlação de muitos tipos de informação com o código fonte, para localizar e analisar gargalos em problemas de performance.

Ao focar em otimização de performance, um administrador de sistemas precisa de certas ferramentas para medir e monitorar as situações e para identificar os gargalos. Em sistemas GNU/Linux, várias ferramentas estão disponíveis para esta finalidade. top é um pequeno programa e fornece informação em tempo real em nível de sistema. O top é interativo e oferece um instantâneo do sistema no momento em que está sendo executado. mtop é semelhante ao top, mas faz a monitoração de bancos de dados MySQL. Ele exibe as consultas que são lentas ou travas (locks) que estão ativos no momento, mostrando o código SQL que está sendo executado. Sysstat, procfs, sysctl e sysfs são ferramentas valiosas e comandos de configuração que podem ser usados para medir a performance de sistemas GNU/Linux e ajustes de desempenho. Outra ferramenta, sar, pode ser usada para coletar uma grande variedade de informações sobre a atividade do sistema (como utilização de CPU, uso de memória, rede, uso de buffers, etc.) e pode ser usado para identificar gargalos potenciais.

Problemas com laços

Primeiramente, vamos entender os problemas de desempenho causados por laços (loop). Laços amplificam os efeitos de problemas de desempenho que, em outras condições, impactam pouco o sistema. Isto ocorre devido ao fato do código dentro do laço ser executado muitas vezes. Sempre se certifique de mover para fora do laço todo o código que não precise ser executado repetidamente.

Consideremos o seguinte trecho de código:

  main()
  {
       int five;
       int cnt;
       int end;
       int temp;
  
          for (cnt=0; cnt < 2* 100000 * 7/ 15 + 6723; 
                       cnt += (5-2)/2
       {
          temp = cnt / 16430;
          end = cnt;
          five = 5;
       }
       printf("printing values of five=%d and end = %d\n", five,  end);
  }

Se observarmos o código cuidadosamente, podemos ver que muitas coisas podem ser retiradas do laço. O valor de end pode ser calculado apenas uma vez, depois que sairmos do laço. Da mesma forma, a atribuição da variável five é um código morto e faz muito mais sentido movê-lo para fora do laço.

Quando falamos de otimizar a performance, precisamos nos certificar que, a menos que haja uma necessidade inescapável, não devemos nunca tentar usar tipos de dados de ponto flutuante, como float e double. Isto se dá devido ao fato de que estes tipos de dados levam mais tempo para serem calculados do que seus equivalentes inteiros. Da mesma forma, se temos uma função que é chamada frequemente, é melhor declará-la como inline.

Outra maneira de melhorar a performance é aumentar o tamanho do bloco. Como sabemos muitas operações são feitas sobre blocos de dados. Ao aumentar o tamanho do bloco, poderemos transferir mais dados de cada vez. Isto reduzirá a frequencia com que realizamos operações que consomem mais tempo.

Cuidado com chamadas "caras"

É claro que quando estamos interessados em otimizar o código, nós sempre queremos nos livrar de operações "caras" e substituí-las por outras mais "baratas". Chamadas de sistema são operações "caras". Vejamos algumas das chamadas de sistema mais "caras":

  • fork: Um fork é muito útil. Não é lento, mas se os usamos com frequencia, o impacto pode ser significativo. Considere um cenário em que um servidor web realize um fork a cada novo pedido. Não é uma boa prática, e select() pode ser usado para multiplexação.

  • exec: Este é usado imediatamente após um fork. Esta chamada pode ser bastante "cara" pois o novo programa terá que realizar muitas inicializações como carregar bibliotecas, etc.

  • system: Esta chamada invoca uma shell para executar o comando especificado e invocar uma shell pode ser bastante "caro". Portanto, usar a chamada system é definitivamente uma péssima idéia.

Se encontrarmos um trecho de código como system("ls /etc"); nós podemos ver como é "caro". O programa tem que primeiramente executar um fork e então executar a shell. A shell precisa realizar a inicialização, faz um novo fork e então executa o comando ls. Certamente não é o tipo de código que se deva usar com frequencia.

O primeiro passo para ajustar o sistema e deixá-lo mais veloz e confiável é caçar as versões mais recentes dos drivers de dispositivo mais recentes. Outra dica útil é entender quais são os gargalos e como eles podem ser tratados. Nós podemos identificar diversos gargalos executando utilitários de monitoração do sistema, como por exemplo o comando top.

Otimização do acesso a disco

Sempre vale a pena dar atenção ao acesso aos discos. Existem várias técnicas que podem produzir melhoras significativas na performance dos discos.

Primeiramente, leia sobre o comando hdparm e você irá notar que ele define várias diretivas e modos no subsistema IDE do driver disco. Existem duas opções para as quais precisamos dar atenção - a opção -c pode definir 32 bits para suporte de I/O e a opção -d que habilita ou desabilita o uso do DMA (Direct Memory Access - using_dma) para o disco. Na maior parte dos casos, esta diretiva é definida como 1, mas se o seu sistema não estiver assim, então você irá sofrer com problemas de performance. Tente alterar este valor colocando o comando

  hdparm -d 1 /dev/hda

ao final do arquivo /etc/rc.d/rc.local.

De forma similar, adicione a linha

  hdparm -c 1 /dev/hda

ao final do arquivo /etc/rc.d/rc.local/ para definir o suporte de 32 bits a operações de I/O.

GNU profiler (gprof)

Nota da tradução: Em engenharia de software, profiling significa analisar o comportamento de um programa de forma dinâmica, usando informação coletada durante sua execução. O objetivo é determinar quais partes de um programa devem ser otimizadas. Não sei de uma tradução adequada para este termo e, por esta razão, usarei o termo no original em inglês nas próximas duas seções.

Depois de termos tomada as medidas necessárias para otimizar nosso código, o compilar também pode ser útil na otimização. Uma ferramenta que podemos usar para analisar a execução de nosso programa é o GNU profiler (gprof). Com esta ferramenta, nós podemos descobrir onde o programa está gastando a maior parte do seu tempo. Com esta informação nós podemos determinar quais partes do programa são mais lentas do que o esperado. Estas seções são definitivamente boas candidatas a serem reescritas para que o programa seja executado mais rapidamente. O profiler coleta dados durante a execução do programa. Profiling é uma outra forma de conhecer melhor o código fonte.

Relacionamos a seguir os requisitos para analisar um programa usando o comando gprof:

  • Profiling precisa ser habilitado durante a compilação e linkagem do programa.
  • Um arquivo de dados de profiling é gerado quando o programa é executado.
  • Dados de Profiling precisam ser analisados.

Para que você use o utilitário gprof, o pacote precisa ser instalado em seu sistema. Para analisar um programa com gprof, nós precisamos compilá-lo com uma opção especial. Considerando que nós tenhamos um programa chamado sample_2007.c, o seguinte comando pode ser usado para compilá-lo:

  $ gcc -a -p -pg  -o sample_2007 sample_2007.c

Observe que a opção -pg habilita o suporte básico para profiling no gcc. O programa irá executar um pouco mais lentamente quando profiling estiver habilitado. Isto se dá devido ao fato de que ele, além de executar suas instruções, também coleta dados. O suporte ao profiling no programa cria um arquivo chamado gmon.out no diretório corrente. Este arquivo é utilizado mais tarde pelo gprof para analisar o código.

Podemos executar o seguinte comando para obter a saída (que redirecionamos para um arquivo):

  $ gprof sample_2007 gmon.out > output.txt

gprof é útil não apenas para determinar quanto tempo é gasto em diversas rotinas, mas ele também nos diz quais rotinas invocam outras rotinas. Usando gprof, nós podemos identificar quais seções de nosso código estão causando os atrasos maiores. A análise do código fonte com gprof é considerada como uma maneira eficiente de determinar qual função está usando a maior parte do tempo total gasto na execução do programa.

Algumas coisas que devemos saber a respeito do kprof

Kprof é uma ferramenta gráfica que exibe as informações de profiling geradas pelo comando gprof. Kprof é bastante útil pois exibe a informação gerada em formato de lista ou árvore e torna a informação mais fácil de ser entendida.

Kprof tem os seguites recursos:

  • Perfil plano (flat profile) irá exibir todas as funções e métodos bem como suas informações de profiling.
  • Perfil hierárquivo (Hierarchical profile) irá exibir uma árvore para cada função e método com outras funções e métodos que chama como sub-elementos.
  • Visão em gráfico (Graph View) é a representação gráfica da árvore de chamados (call tree).

Referências

Adicionar comentário

* Campos obrigatórios
5000
Powered by Commentics

Comentários

Nenhum comentário ainda. Seja o primeiro!


Veja a relação completa dos artigos de Rubens Queiroz de Almeida