Muitos programadores Go estão familiarizados com bytes.Buffer . Uma de suas vantagens é que ele permite que você evite alocar memória no heap da mesma maneira que a " pequena otimização de buffer / tamanho ":
type Buffer struct { bootstrap [64]byte  
Existe apenas um problema. Essa otimização não funciona .
No final deste artigo, você descobrirá por que essa otimização não funciona e o que podemos fazer sobre isso.
Conforme pretendido, "otimização de buffer pequeno"
Vamos apresentar uma definição ligeiramente simplificada de bytes.Buffer .
 const smallBufSize int = 64 type Buffer struct { bootstrap [smallBufSize]byte buf []byte } 
Quando executamos ações no Buffer , por exemplo, chamamos o método Buffer.Write , o registro é sempre feito no buf ; no entanto, antes desse registro, Buffer.grow(n) iniciado dentro de cada método semelhante, o que garante que haja espaço suficiente nessa fatia para próximos n bytes.
Grow pode ser algo como isto:
 func (b *Buffer) grow(n int) {  
Pressupostos usados em nossa implementação do Buffer.grow
Assumimos que len(b.buf) é o tamanho real dos dados no Buffer, o que exigiria que o Write use métodos de adição para adicionar novos bytes à fatia. Esse não é o caso em bytes.Buffer da biblioteca padrão, mas, por exemplo, este é um detalhe de implementação sem importância.
 Se b alocado na pilha, o bootstrap dentro dela será alocado na pilha, o que significa que a fatia b.buf reutilizará a memória dentro de b sem exigir alocação adicional.
Quando o grow revelar que a matriz de bootstrap já é insuficiente, uma nova fatia "real" será criada, onde os elementos do armazenamento passado (do "pequeno buffer") serão copiados. Depois disso, o Buffer.bootstrap perderá sua relevância. Se Buffer.Reset for Buffer.Reset , cap(b.buf) permanecerá o mesmo e não haverá mais necessidade de uma matriz de bootstrap .
Memória fugindo na pilha
Espera-se ainda que o leitor esteja pelo menos superficialmente familiarizado com o que é a análise de escape no Go.
Considere a seguinte situação:
 func f() *Buffer { var b bytes.Buffer  
Aqui b será alocado na pilha. A razão para isso é o ponteiro vazando para b :
 $ go tool compile -m leak.go leak.go:12:9: &b escapes to heap leak.go:11:6: moved to heap: b 
Terminologia
Neste artigo, "vazamento" e "escape" são usados quase como sinônimos.
Há alguma diferença no próprio compilador, por exemplo, o valor "escapa para a pilha", mas os parâmetros de função são "vazamento de parâmetro x".
Um parâmetro vazando significa que o argumento passado para esse parâmetro será alocado no heap. Em outras palavras, o parâmetro vazamento faz com que os argumentos escapem para um heap.
 O acima foi um caso óbvio, mas e quanto a isso:
 func length() int { var b bytes.Buffer b.WriteString("1") return b.Len() } 
Aqui precisamos de apenas 1 byte, tudo se encaixa no bootstrap , o buffer em si é local e não "escapa" da função. Você pode se surpreender, mas o resultado será o mesmo, alocação b na pilha.

Para ter certeza, você pode verificar isso usando a referência:
 BenchmarkLength-8 20000000 90.1 ns/op 112 B/op 1 allocs/op 
Listagem de benchmark
 package p import ( "bytes" "testing" ) func length() int { var b bytes.Buffer b.WriteString("1") return b.Len() } func BenchmarkLength(b *testing.B) { for i := 0; i < bN; i++ { _ = length() } } 
 Explicação 112 B / op
Quando o tempo de execução solicita N bytes ao alocador, não é necessário que exatamente N bytes sejam alocados.
Todos os resultados abaixo são para a combinação de GOOS=linux e GOARCH=AMD64 .
 package benchmark import "testing"  
Se você executar, go test -bench=. -benchmem go test -bench=. -benchmem com este teste:
 BenchmarkAlloc9-8 50000000 33.5 ns/op 16 B/op 1 allocs/op 
9 bytes solicitados, 16 alocados. Agora, de volta aos bytes.Buffer :
 fmt.Println(unsafe.Sizeof(bytes.Buffer{})) => 104 
Vejamos $ GOROOT / src / runtime / sizeclasses.go :
 // class bytes/obj bytes/span objects tail waste max waste // 1 8 8192 1024 0 87.50% // 2 16 8192 512 0 43.75% // 3 32 8192 256 0 46.88% // 4 48 8192 170 32 31.52% // 5 64 8192 128 0 23.44% // 6 80 8192 102 32 19.07% // 7 96 8192 85 32 15.95% // 8 112 8192 73 16 13.56% // ...  
Não cabe em 96 bytes, 112 está selecionado.
 Mas por que isso está acontecendo?
O que está acontecendo e por quê
Alguma análise da situação pode ser encontrada na questão mencionada no início.
Há também um reprodutor simples .
O local do problema está apenas na atribuição b.buf = b.bootstrap[:] . Esse código faz com que a análise de escape assuma que o b.bootstrap "fugindo" e, como é uma matriz, ele é armazenado dentro do próprio objeto, o que significa que todo o b deve ser alocado no heap.
Se o bootstrap fosse uma fatia, não uma matriz, isso não aconteceria, porque existe uma otimização adhoc para atribuir fatias do objeto ao próprio objeto:
 
A resposta por que essa otimização não funciona para matrizes já foi formulada acima, mas aqui está um aperto do próprio esc.go # L835-L866 (todo o código de otimização é destacado por referência):
 
Vale acrescentar aqui que, para o analisador de ponteiros, existem vários níveis de "vazamentos", os principais deles:
- O próprio objeto escapa (b escapa). Nesse caso, o próprio objeto precisa ser alocado no heap.
- Os elementos do objeto (escape de conteúdo b) escapam. Nesse caso, os ponteiros no objeto são considerados escapantes.
O caso da matriz é especial, pois se a matriz vazar, o objeto que a contém também deverá vazar.
A análise de escape exibe uma decisão sobre se é possível colocar um objeto na pilha ou não, baseando-se apenas nas informações disponíveis no corpo da função analisada. O método Buffer.grow recebe b por ponteiro, portanto, ele precisa calcular um local adequado para colocar. Como no caso de uma matriz não podemos distinguir entre "b escape" e "b contents escape" , temos que ser mais pessimistas e chegar à conclusão de que b não b seguro colocar na pilha.
Suponha o contrário, que o padrão de self-assignment resolva o mesmo para matrizes e para fatias:
 package example var sink interface{} type bad struct { array [10]byte slice []byte } func (b *bad) bug() { b.slice = b.array[:]  
A decisão de colocar b na pilha nessa situação levará a um desastre: depois de sair da função na qual b foi criado, a memória à qual o sink se referirá será nada mais que lixo.
Ponteiros de matriz
Imagine que nosso Buffer foi declarado um pouco diferente:
 const smallBufSize int = 64 type Buffer struct { - bootstrap [smallBufSize]byte + bootstrap *[smallBufSize]byte buf []byte } 
Ao contrário de uma matriz regular, um ponteiro para uma matriz não armazena todos os elementos dentro do próprio Buffer . Isso significa que se a alocação de bootstrap no heap não envolver alocação de Buffer no heap. Como a análise de escape pode alocar campos de ponteiro na pilha quando possível, podemos assumir que essa definição de Buffer é mais bem-sucedida.
Mas isso é em teoria. Na prática, um ponteiro para uma matriz não possui muito processamento e se enquadra na mesma categoria que uma fatia de uma matriz regular, o que não está totalmente correto. CL133375: cmd / compile / internal / gc: handle auto-atribuir fatia em esc.go visa corrigir essa situação.
Suponha que essa alteração tenha sido aceita no compilador Go.
Valor zero que perdemos
Infelizmente, a transição de [64]byte para *[64]byte tem um problema: agora não podemos usar o bootstrap sem inicializá-lo explicitamente, um valor zero de Buffer deixa de ser útil, precisamos de um construtor.
 func NewBuffer() Buffer { return Buffer{bootstrap: new(*[smallBufSize]byte)} } 
Retornamos o Buffer , não o *Buffer , para evitar problemas com a análise de ponteiros (é muito conservador no Go) e, levando em consideração o fato de que o NewBuffer sempre NewBuffer embutido no local de uma chamada, não haverá cópia desnecessária.
Após incorporar o corpo NewBuffer no lugar da chamada, a análise de escape pode tentar provar que o new(*[smallBufSize]byte) não excede a vida útil do quadro da função na qual é chamado. Nesse caso, a alocação estará na pilha.
Intel bytebuf
A otimização descrita acima é aplicada no pacote intel-go / bytebuf .
Essa biblioteca exporta o tipo bytebuf.Buffer , que duplica 99,9% de bytes.Buffer . Todas as alterações são reduzidas à introdução de um construtor ( bytebuf.New ) e um ponteiro para uma matriz em vez de uma matriz regular:
 type Buffer struct { buf []byte // contents are the bytes buf[off : len(buf)] off int // read at &buf[off], write at &buf[len(buf)] - bootstrap [64]byte // helps small buffers avoid allocation. + bootstrap *[64]byte // helps small buffers avoid allocation. lastRead readOp // last read operation (for Unread*). } 
Aqui está uma comparação de desempenho com bytes.Buffer :
 name old time/op new time/op delta String/empty-8 138ns ±13% 24ns ± 0% -82.94% (p=0.000 n=10+8) String/5-8 186ns ±11% 60ns ± 1% -67.82% (p=0.000 n=10+10) String/64-8 225ns ±10% 108ns ± 6% -52.26% (p=0.000 n=10+10) String/128-8 474ns ±17% 338ns ±13% -28.57% (p=0.000 n=10+10) String/1024-8 889ns ± 0% 740ns ± 1% -16.78% (p=0.000 n=9+10) name old alloc/op new alloc/op delta String/empty-8 112B ± 0% 0B -100.00% (p=0.000 n=10+10) String/5-8 117B ± 0% 5B ± 0% -95.73% (p=0.000 n=10+10) String/64-8 176B ± 0% 64B ± 0% -63.64% (p=0.000 n=10+10) String/128-8 368B ± 0% 256B ± 0% -30.43% (p=0.000 n=10+10) String/1024-8 2.16kB ± 0% 2.05kB ± 0% -5.19% (p=0.000 n=10+10) name old allocs/op new allocs/op delta String/empty-8 1.00 ± 0% 0.00 -100.00% (p=0.000 n=10+10) String/5-8 2.00 ± 0% 1.00 ± 0% -50.00% (p=0.000 n=10+10) String/64-8 2.00 ± 0% 1.00 ± 0% -50.00% (p=0.000 n=10+10) String/128-8 3.00 ± 0% 2.00 ± 0% -33.33% (p=0.000 n=10+10) String/1024-8 3.00 ± 0% 2.00 ± 0% -33.33% (p=0.000 n=10+10) 
Todas as outras informações estão disponíveis no README .
Devido à incapacidade de usar o valor zero e a ligação à função de construção New , não é possível aplicar essa otimização a bytes.Buffer .
Essa é a única maneira de gerar bytes.Buffer mais bytes.Buffer ? A resposta é não. Mas esse é definitivamente um método que requer mudanças mínimas na implementação.
Planos de Análise de Escape
Em sua forma atual, a análise de escape no Go é bastante fraca. Quase qualquer operação com valores de ponteiro leva a alocações no heap, mesmo que essa não seja uma decisão razoável.
Tentarei direcionar a maior parte do tempo que dedico ao projeto golang / go para resolver esses problemas, portanto, algumas melhorias são possíveis na próxima versão (1.12).
Você pode ler sobre os resultados e detalhes da estrutura interna desta parte do compilador em um dos meus próximos artigos. Tentarei fornecer um conjunto de recomendações que ajudarão em alguns casos a estruturar o código para que ele tenha menos alocações de memória indesejadas.