6 dicas sobre Go de alto desempenho — Tópicos avançados de Go

Apr 28 2023
O artigo visa discutir 6 dicas que podem ajudar a diagnosticar e corrigir problemas de desempenho em seus aplicativos Go. Benchmarking: escrever benchmarks eficazes em Go é crucial para entender o desempenho do seu código.

O artigo visa discutir 6 dicas que podem ajudar a diagnosticar e corrigir problemas de desempenho em seus aplicativos Go.

Avaliação comparativa:

Escrever benchmarks eficazes em Go é crucial para entender o desempenho do seu código. Benchmarks podem ser criados adicionando o sufixo “_test” a um arquivo Go e usando a função Benchmark do pacote de teste. Aqui está um exemplo:

Neste exemplo, estamos comparando o tempo necessário para calcular o 20º número de Fibonacci. A BenchmarkFibonaccifunção executa os tempos fibonaccide função b.N, que é um valor definido pelo pacote de teste para fornecer um resultado estatisticamente significativo.

Para interpretar os resultados do benchmark, podemos executar go test -bench=. -benchmemno terminal, que executará todos os benchmarks no diretório atual e imprimirá as estatísticas de alocação de memória. O -benchsinalizador é usado para especificar uma expressão regular para correspondência de nomes de benchmark e .corresponderá a todos os benchmarks no diretório atual. O -benchmemsinalizador imprimirá estatísticas de alocação de memória junto com os resultados de tempo.

Criação de perfil:

O Go possui ferramentas de criação de perfil integradas que podem ajudá-lo a obter informações sobre o que seu código está fazendo. A ferramenta de criação de perfil mais comum é o criador de perfil da CPU, que pode ser ativado adicionando o -cpuprofilesinalizador ao go testcomando. Aqui está um exemplo:

A primeira função, “TestFibonacci”, é um teste de unidade simples que verifica se a função fibonacci retorna corretamente o vigésimo número na sequência de fibonacci.

A função “fibonacci” é uma implementação recursiva da sequência de fibonacci que calcula o enésimo número na sequência.

A função “BenchmarkFibonacci” é um benchmark que executa a função “fibonacci” 20 vezes e mede o tempo de execução.

A função “ExampleFibonacci” é um exemplo que imprime o vigésimo número na sequência de fibonacci usando a função “fibonacci” e verifica se é igual ao valor esperado de 6765.

Para habilitar a criação de perfil, usamos o sinalizador “-cpuprofile” com o comando “go test” para enviar os resultados da criação de perfil para um arquivo chamado “prof.out”. O seguinte comando pode ser usado para executar os testes e gerar os dados de criação de perfil:

Depois de executar os testes, podemos usar o comando “go tool pprof” para analisar os dados de criação de perfil. Podemos iniciar a ferramenta pprof com o seguinte comando:

Isso abrirá o shell interativo pprof, onde podemos digitar vários comandos para analisar os dados de criação de perfil. Por exemplo, podemos usar o comando “top” para exibir as funções que consumiram mais tempo de CPU:

Isso exibirá uma lista de funções com o maior uso de tempo de CPU, classificadas por tempo de CPU. Neste caso, devemos ver a função “fibonacci” no topo da lista, já que foi a função que mais consumiu tempo de CPU durante o benchmark.

Também podemos usar o comando “web” para exibir os dados de criação de perfil em um formato gráfico e o comando “list” para exibir o código-fonte anotado com os dados de criação de perfil.

A criação de perfil é uma ferramenta poderosa que pode nos ajudar a identificar gargalos de desempenho em nosso código. Usando o sinalizador “-cpuprofile” e a ferramenta go pprof, podemos facilmente gerar e analisar dados de criação de perfil para nossos testes e aplicativos Go.

Otimizações do compilador:

O compilador Go executa várias otimizações, incluindo inlining, análise de escape e eliminação de código morto. Inlining é o processo de substituir uma chamada de função pelo corpo da função, o que pode melhorar o desempenho reduzindo a sobrecarga da chamada de função. A análise de escape é o processo de determinar se o endereço de uma variável foi obtido, o que pode ajudar o compilador a alocá-lo na pilha em vez do heap. A eliminação de código morto é o processo de remoção de código que nunca é executado.

Incorporação:

No primeiro exemplo, a addfunção é chamada com argumentos 3e 4, o que resulta em uma sobrecarga de chamada de função. No segundo exemplo, a chamada de função é substituída pelo código real da função, resultando em uma execução mais rápida.

Análise de fuga:

Neste exemplo, a avariável é alocada na pilha, pois seu endereço não é obtido. No entanto, a bvariável é alocada no heap, pois seu endereço é obtido com o &operador.

Mais sobre análise de escape:

Na createUserfunção, um novo Useré criado e seu endereço é retornado. Observe que o Uservalor é alocado na pilha desde que seu endereço seja retornado, portanto não escapa para o heap.

Se adicionarmos uma linha que leva o endereço do Uservalor antes de retorná-lo:

Agora, o Userendereço do valor é obtido e armazenado em uma variável que é retornada. Isso faz com que o valor escape para a pilha em vez de ser alocado na pilha.

A análise de escape é importante porque as alocações de heap são mais caras do que as alocações de pilha, portanto, minimizar as alocações de heap pode melhorar o desempenho.

Eliminação de código morto:

Neste exemplo, o código dentro da ifinstrução nunca é executado, portanto, é removido pelo compilador durante a eliminação do código morto.

Compreendendo o Rastreador de Execução:

O rastreador de execução em Go fornece informações detalhadas sobre o que está acontecendo em um programa, incluindo rastreamentos de pilha, bloqueio de goroutine e muito mais. Aqui está um exemplo de como usá-lo:

Neste exemplo, estamos criando um arquivo de rastreamento, iniciando o rastreamento e parando o rastreamento. Quando o programa for executado, os dados de rastreamento serão gravados no trace.outarquivo. Você pode analisar esses dados de rastreamento para entender melhor o que está acontecendo em seu programa.

Gerenciamento de memória e ajuste de GC:

Em Go, a coleta de lixo é automática e gerenciada pelo tempo de execução. No entanto, existem algumas maneiras de ajustar o coletor de lixo para melhorar o desempenho. Aqui está um exemplo de como definir algumas das opções do coletor de lixo:

Neste exemplo, estamos definindo o número máximo de CPUs a serem usadas, o tamanho mínimo de heap e a porcentagem de coleta de lixo. Essas configurações podem ser ajustadas para melhorar o desempenho, dependendo das necessidades do seu programa.

Confira Go Advanced Topics Deep Dive — Garbage Collector para saber mais.

Simultaneidade:

O Go possui suporte integrado para simultaneidade por meio de goroutines e canais. No entanto, é importante usar esses recursos corretamente para evitar problemas como condições de corrida e impasses. Aqui está um exemplo de como usar canais para se comunicar com segurança entre goroutines:

A make(chan int)instrução cria um canal que é usado para comunicar um valor inteiro entre as duas goroutines.

A primeira goroutine é criada com a go func() {...}()instrução, que envia um valor de 1 para o canal chdepois de dormir por 1 segundo. Isso significa que após 1 segundo, o chcanal terá o valor 1.

A segunda goroutine é criada com a selectinstrução, que aguarda a comunicação no chcanal. Se um valor for recebido do canal, a mensagem "Mensagem recebida" será impressa. Se um valor não for recebido em 2 segundos, a mensagem "Timed out" será impressa.

Portanto, embora não haja comunicação explícita entre a selectinstrução e a primeira goroutine, ainda há comunicação acontecendo por meio do canal compartilhado ch.

Afinal:

Se você gostou deste artigo, siga ou assine para receber conteúdo de alta qualidade a tempo. Obrigado pelo seu apoio ;)

Referência:

https://dave.cheney.net/high-performance-go