← Voltar para o blog

**Go Acelera: Alocação na Stack para um Desempenho Imbatível**

O artigo do blog oficial do Go discute otimizações recentes no compilador Go para reduzir alocações na heap, substituindo-as por alocações na stack....

O artigo do blog oficial do Go discute otimizações recentes no compilador Go para reduzir alocações na heap, substituindo-as por alocações na stack. Alocações na stack são significativamente mais rápidas e não sobrecarregam o garbage collector, resultando em programas mais eficientes em termos de tempo de execução e uso de memória. O artigo explora diferentes cenários e as melhorias introduzidas em versões recentes do Go (1.25 e 1.26) para otimizar a alocação de slices.

O Problema das Alocações na Heap

Alocar memória na heap é uma operação relativamente custosa. Cada alocação envolve a execução de um código considerável e impõe uma carga adicional ao garbage collector (GC). Mesmo com otimizações recentes no GC, como o Green Tea, o overhead ainda é significativo. Alocações na stack, por outro lado, são muito mais baratas (às vezes até “gratuitas”), não afetam o GC e permitem um reuso rápido da memória, o que é benéfico para o cache.

Alocação na Stack de Slices com Tamanho Constante

Considere o seguinte exemplo de código que processa tarefas recebidas de um channel:

func process(c chan task) {
    var tasks []task
    for t := range c {
        tasks = append(tasks, t)
    }
    processAll(tasks)
}

Nesse código, a cada iteração do loop, a função append adiciona uma nova tarefa ao slice tasks. Inicialmente, o slice não possui um backing store (a área de memória onde os elementos do slice são armazenados). Assim, a função append precisa alocar um. Como não sabe o tamanho final do slice, ela começa com um tamanho pequeno (1). Nas iterações seguintes, se o backing store estiver cheio, append aloca um novo com o dobro do tamanho anterior, descartando o antigo. Esse processo gera várias alocações na heap e lixo para o GC, especialmente no início, quando o slice é pequeno.

Uma otimização comum é pré-alocar o slice com um tamanho estimado:

func process2(c chan task) {
    tasks := make([]task, 0, 10) // provavelmente no máximo 10 tarefas
    for t := range c {
        tasks = append(tasks, t)
    }
    processAll(tasks)
}

Essa abordagem reduz o número de alocações, mas o compilador Go pode ir além. Se o tamanho máximo do backing store for conhecido em tempo de compilação (neste caso, 10 vezes o tamanho de task), ele pode alocar o backing store na stack do frame da função process2, eliminando completamente a necessidade de alocação na heap. Essa otimização depende do backing store não “escapar” para a heap dentro de processAll.

Alocação na Stack de Slices com Tamanho Variável

E se o tamanho do slice não for conhecido em tempo de compilação? Considere o seguinte:

func process3(c chan task, lengthGuess int) {
    tasks := make([]task, 0, lengthGuess)
    for t := range c {
        tasks = append(tasks, t)
    }
    processAll(tasks)
}

Em versões anteriores ao Go 1.25, o tamanho variável do backing store impedia a alocação na stack. No entanto, o Go 1.25 introduziu uma otimização inteligente: para certas alocações de slice, o compilador aloca automaticamente um pequeno backing store (atualmente, 32 bytes) na stack. Se o tamanho solicitado para o slice for pequeno o suficiente para caber nesse backing store, ele é usado. Caso contrário, a alocação ocorre na heap como de costume.

Essa otimização permite que process3 execute sem alocações na heap se lengthGuess for pequeno o suficiente e corresponder ao número real de elementos no channel c.

Alocação na Stack de Slices Alocados por Append

O Go 1.26 vai ainda mais longe, permitindo a alocação na stack mesmo quando o tamanho do slice é desconhecido e o append é usado sem pré-alocação.

func process(c chan task) {
    var tasks []task
    for t := range c {
        tasks = append(tasks, t)
    }
    processAll(tasks)
}

Nesse caso, o compilador aloca um pequeno backing store especulativo na stack. Na primeira iteração do loop, append usa esse backing store. Se o backing store da stack for grande o suficiente para acomodar os elementos adicionados, as primeiras iterações não exigirão alocações na heap. Quando o backing store da stack estiver cheio, append alocará um novo na heap, mas as alocações iniciais na heap (de tamanhos 1, 2, 4, etc.) são evitadas, reduzindo o overhead e o lixo para o GC.

Slices que “Escapam”

Se um slice “escapar” da função (por exemplo, sendo retornado), seu backing store não pode ser alocado na stack, pois o frame da stack da função desaparece quando ela retorna.

func extract(c chan task) []task {
    var tasks []task
    for t := range c {
        tasks = append(tasks, t)
    }
    return tasks
}

Nesse caso, o backing store do slice retornado por extract deve ser alocado na heap. No entanto, o compilador ainda pode otimizar a alocação dos slices intermediários usados dentro do loop, alocando-os na stack. Uma abordagem comum é criar uma copia do slice dentro da função:

func extract2(c chan task) []task {
    var tasks []task
    for t := range c {
        tasks = append(tasks, t)
    }
    tasks2 := make([]task, len(tasks))
    copy(tasks2, tasks)
    return tasks2
}

Implicações Práticas

As otimizações de alocação na stack introduzidas nas versões recentes do Go têm um impacto significativo no desempenho e na eficiência de memória dos programas Go. Ao reduzir o número de alocações na heap e a carga sobre o garbage collector, essas otimizações resultam em programas mais rápidos e com menor consumo de memória.

Os desenvolvedores podem se beneficiar dessas otimizações simplesmente atualizando para as versões mais recentes do Go. Em muitos casos, nenhuma alteração no código é necessária para aproveitar as melhorias. Em outros casos, pequenas modificações, como pré-alocar slices com um tamanho estimado, podem aumentar ainda mais o desempenho.

As otimizações de alocação na stack são um exemplo do compromisso contínuo da equipe Go em melhorar o desempenho da linguagem e fornecer aos desenvolvedores ferramentas para escrever programas eficientes e escaláveis.


Artigo Original

Este e um resumo em português do artigo original publicado no blog oficial do Go.

Titulo original: Allocating on the Stack

Leia o artigo completo em ingles no Go Blog

Autor original: Keith Randall