4 pontos por GN⁺ 2024-10-19 | 1 comentários | Compartilhar no WhatsApp

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.s e main.go mostram 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.s e AddFloat32_amd64.s mostram como verificar suporte a SSE e como realizar soma de float32.

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

 
GN⁺ 2024-10-19
Comentários do Hacker News
  • Alguns pontos sobre assembly em Go: no amd64, esses int são na verdade 64 bits
    Se 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 dupla
    NOSPLIT está definido em textflag.h, fornecido automaticamente pelo compilador Go. Mas, pelo que li, NOSPLIT parece só ser respeitado em funções runtime.XX, então aqui ele não faz nada e nem é necessário
    NOSPLIT significa 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 pilha

  • 4 representa “NOSPLIT” e, para quem estiver curioso sobre por que dizem que isso é necessário: normalmente, depois do tamanho do frame (o parâmetro após NOSPLIT) vem o tamanho dos argumentos, e os dois são separados por um sinal de menos
    Isso não é subtração, é só uma sintaxe peculiar. Um tamanho de frame $24-8 significa que a função tem um frame de 24 bytes e é chamada com 8 bytes de argumentos no frame do chamador
    Se você não especificar NOSPLIT em TEXT, precisa fornecer o tamanho dos argumentos. No caso de funções assembly com protótipo Go, o go vet verifica se o tamanho dos argumentos está correto
    Fonte: https://go.dev/doc/asm

  • Dizem que “Go usa uma linguagem assembly interna própria chamada Plan9”, mas ela é realmente chamada assim?

    • Não. É apenas assembly de Go
      A sintaxe vem do Plan 9, mas chamamos isso de assembly de Go
      Veja https://go.dev/doc/asm
    • É uma pergunta válida. No começo eu também simplesmente assumi que estava certo. Parecia improvável que alguém pesquisando esse tema errasse algo assim e, se você conhece um pouco do projeto, esse nome até soa plausível
      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 assembler
      A 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”
    • Não há um nome separado. Plan 9 é um sistema operacional, e esse estilo de sintaxe assembly vem do assembler usado nele
      É 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

    • Na prática, isso funcionou. Go ajudou a criar a expectativa de que uma linguagem de programação nova deveria ter compilação cruzada como capacidade básica, e na época quase nenhuma linguagem fazia isso bem
    • Como referência, o SpiderMonkey também vem fazendo quase a mesma coisa há cerca de 25 anos, e imagino que outras máquinas virtuais de JavaScript também façam isso
  • 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 for você pode usar SIMD para processar em paralelo, algo como simd.AddInt32(slice1, slice2, result)

  • 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/

    • Na verdade, acho que todos os chips desde mais ou menos 2008 suportam SSE4.1
  • 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

    • Sinceramente? Não. Essa arrogância de achar que isso era um atalho para não fazer o trabalho que o LLM deveria fazer direito é algo de que a pessoa deveria se envergonhar
      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?

    • O problema do cgo é que o overhead de chamada de função é alto. É melhor usá-lo só para blocos de trabalho relativamente grandes
      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
    • Acho certo que as pessoas já vêm tentando isso há muito tempo. Lembro de conversar ao telefone (era no Skype?) quando meu irmão estava tentando criar uma biblioteca SIMD em Go, quase 10 anos atrás
      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/
    • Uma proposta para adicionar isso à biblioteca padrão foi rejeitada, mas ajuda a entender o contexto: https://github.com/golang/go/issues/53171
  • 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