HN Open: Memo sobre Go Plan9
(pehringer.info)Uso de concorrência e paralelismo no Go
- Introdução a um projeto que busca melhorar a capacidade de cálculo numérico aproveitando a concorrência e o paralelismo do Go.
- É possível realizar computação paralela em nível de hardware usando instruções SIMD (Same Instruction Multiple Data).
- Como o compilador de Go não utiliza SIMD e não foi encontrado um pacote SIMD de propósito geral adequado, decidiu-se desenvolver um pacote próprio.
Linguagem assembly Plan9
- O Go usa sua própria linguagem assembly chamada Plan9, que emprega instruções e registradores de plataformas específicas com pequenas modificações.
- x86 Plan9 e ARM Plan9 são diferentes entre si.
- Os conceitos básicos de uso são explicados por meio de um exemplo simples de Plan9.
Exemplo de Plan9
- Os arquivos
AddInts_amd64.semain.gomostram a forma básica de declarar e usar funções em Plan9. - Também é explicado como armazenar argumentos e valores de retorno na pilha de acordo com a convenção de chamada do Go.
Plano de design do pacote
- Foi projetado um pacote que fornece uma camada fina de abstração para operações SIMD aritméticas e bit a bit.
- Também foi criado um pacote interno com implementações Plan9 específicas por arquitetura, configuradas por meio de uma função de inicialização.
Exemplo de SIMD
- O uso de SIMD é explicado por meio de exemplos de funções Plan9 SIMD em x86.
- Os arquivos
Supported_amd64.seAddFloat32_amd64.smostram como verificar suporte a SSE e como realizar soma defloat32.
Desempenho e futuro
- Um gráfico mostrando a diferença de desempenho entre a implementação em software em Go e a implementação SIMD em Plan9 confirma um ganho de velocidade de cerca de 200% a 450%.
- O autor espera que esta nota inspire projetos que usem Plan9 e SIMD.
# Resumo do GN⁺
- Este texto apresenta formas de maximizar desempenho aproveitando concorrência e paralelismo no Go.
- Também explica como usar a linguagem assembly Plan9 e instruções SIMD para realizar computação paralela em nível de hardware.
- Para desenvolvedores Go, o texto mostra o potencial de uso de Plan9 e SIMD e pode ser útil para explorar novas abordagens de otimização de desempenho.
- Como projetos com funcionalidades semelhantes, são recomendadas bibliotecas de suporte a SIMD em Rust ou bibliotecas relacionadas a SIMD em C++.
1 comentários
Comentários do Hacker News
Alguns pontos sobre assembly em Go: no amd64, esses
intsão na verdade 64 bitsSe você usar
int32, os parâmetros ficam alinhados por palavra na lista de parâmetros, mas há uma pegadinha. Em sistemas de 64 bits, o valor de retorno sempre começa em um deslocamento alinhado a palavra duplaNOSPLITestá definido emtextflag.h, fornecido automaticamente pelo compilador Go. Mas, pelo que li,NOSPLITparece só ser respeitado em funçõesruntime.XX, então aqui ele não faz nada e nem é necessárioNOSPLITsignifica não inserir o código que o compilador adiciona para verificar se a pilha pode estourar e precisa ser dividida. Se a função não precisa de espaço de pilha, tecnicamente isso é desnecessário e, em essência, existe para impedir que esse código de verificação seja injetado dentro da própria função que verifica a divisão da pilha4representa “NOSPLIT” e, para quem estiver curioso sobre por que dizem que isso é necessário: normalmente, depois do tamanho do frame (o parâmetro apósNOSPLIT) vem o tamanho dos argumentos, e os dois são separados por um sinal de menosIsso não é subtração, é só uma sintaxe peculiar. Um tamanho de frame
$24-8significa que a função tem um frame de 24 bytes e é chamada com 8 bytes de argumentos no frame do chamadorSe você não especificar
NOSPLITemTEXT, precisa fornecer o tamanho dos argumentos. No caso de funções assembly com protótipo Go, ogo vetverifica se o tamanho dos argumentos está corretoFonte: https://go.dev/doc/asm
Dizem que “Go usa uma linguagem assembly interna própria chamada Plan9”, mas ela é realmente chamada assim?
A sintaxe vem do Plan 9, mas chamamos isso de assembly de Go
Veja https://go.dev/doc/asm
Mas, quanto mais eu procurava, mais isso me parecia uma alucinação de LLM
A documentação do formato assembly não dá um nome propriamente dito e só chama de
go assemblerA origem dessa alucinação provavelmente é o primeiro parágrafo: “O assembler é baseado no estilo de entrada do assembler do Plan 9... o documento atual descreve um resumo da sintaxe, suas diferenças e as peculiaridades de escrever código assembly que interage com Go”
É parecido com dizer que “o GNU Compiler Collection usa uma linguagem assembly interna própria chamada Unix”
Se você quer saber por que a equipe do Go escolheu esse formato de assembly dedicado, Rob Pike falou sobre o design do assembler do Go em uma apresentação de 2016 [1][2]
A ideia central parece ter sido a observação de que a maioria das linguagens assembly é mais ou menos parecida, então seria possível criar uma linguagem assembly comum que permitisse “conversar com o nível mais baixo da máquina sem precisar aprender uma nova sintaxe”
Isso também permite gerar automaticamente um assembler que funciona a partir do PDF do manual de instruções de uma nova arquitetura
[1]: https://www.youtube.com/watch?v=KINIAgRpkDA
[2]: https://go.dev/talks/2016/asm.slide#1
Quando ele disse “eu precisava de funções para executar operações SIMD em slices”, fiquei me perguntando como isso era usado na prática
Procurei no texto inteiro e não encontrei em lugar nenhum a operação sendo feita nos slices
Edit: encontrei no documento vinculado: https://pkg.go.dev/github.com/pehringer/simd#pkg-index
Basicamente, se você tiver 2 slices que quer somar, em vez de um loop
forvocê pode usar SIMD para processar em paralelo, algo comosimd.AddInt32(slice1, slice2, result)https://github.com/pehringer/simd/blob/main/simd_test.go
Sobre os processadores que o Go suporta, esta seção é relevante (1). O suporte básico a x64 inclui SSE e SSE2
Mas não sei se o compilador Go realmente gera isso. Ao contrário de compiladores muito complexos como o gcc, que priorizam desempenho acima de tudo, o compilador Go prefere compiladores no estilo de Wirth(2), simples e rápidos
(1) https://go.dev/wiki/MinimumRequirements#amd64
(2) https://irreal.org/blog/?p=7075
https://smartgo.blog/2024/01/06/niklaus-wirth/
Acho que o autor confundiu essa parte, então deixo um link de referência: https://en.wikipedia.org/wiki/Plan_9_from_Bell_Labs
Eu também estava prestes a dizer que isso parecia o resultado de uma interpretação errada do código por um LLM
É difícil imaginar de outra forma alguém conhecer o termo Plan 9, se aprofundar em assembly, e ainda assim não perceber em que terreno está pisando. Acabei percebendo isso ao ver que outras pessoas pensaram o mesmo
Se for esse o caso, espero que o autor não sinta vergonha nem que foi “pego”, e seja honesto. Assim nós também podemos aprender. Quero ganhar mais confiança para identificar esse tipo de “exposição de LLM”, mas por mais óbvio que pareça, quase ninguém parece admitir
Claro, aqui não é algo óbvio, é só uma suposição muito precipitada e julgadora
Isso realmente me irrita
Ficou um pouco acima do meu nível, mas gostei da forma como o texto conduz o leitor junto
Não deve ser a primeira vez que alguém tenta isso, certo? Imagino que haja literalmente dezenas de Gophers sedentos por SIMD. O padrão mais comum seria usar CGO?
Chamar uma função em assembly a partir de Go é bem mais barato
Tenho algumas coisas nessa linha em https://pkg.go.dev/github.com/grailbio/base/simd
Se estou lembrando direito, naquela época várias instruções AVX nem sequer eram codificadas pelo assembler Plan 9 do Go, então era preciso codificá-las manualmente em bytes [0]
A biblioteca mais completa que já vi, embora eu nunca a tenha usado, faz um hack elegante usando CGO parcialmente para evitar esse overhead [1]
[0]: https://github.com/slimsag/rand/blob/f1e8d464c0021a391d5cd64...
[1]: https://github.com/alivanz/go-simd/
Se você quiser escrever programas em assembly para Go, recomendo dar uma olhada no Avo(https://github.com/mmcloughlin/avo)
Ele oferece segurança de tipos e faz algumas verificações para garantir que gera assembly válido. Também pode alocar registradores dinamicamente e evita que você tenha de calcular manualmente coisas como pilha e tamanho do frame
Também consegue lidar com detalhes da convenção de chamada, então é fácil carregar argumentos nos registradores ou posições desejadas
Recentemente portei todo o assembly amd64 da biblioteca crypto do Go para Avo, e é uma biblioteca muito útil para esse tipo de trabalho