HN público: notas sobre Go Plan9
(pehringer.info)Aprovechando la concurrencia y el paralelismo en Go
- Introducción a un proyecto que busca mejorar la capacidad de cálculo numérico aprovechando la concurrencia y el paralelismo de Go.
- Es posible realizar cálculos paralelos a nivel de hardware usando instrucciones SIMD (Same Instruction Multiple Data).
- El compilador de Go no aprovecha SIMD, y al no encontrar un paquete SIMD de propósito general adecuado, se decidió desarrollar uno propio.
Lenguaje ensamblador Plan9
- Go usa su propio lenguaje ensamblador llamado Plan9, que utiliza instrucciones y registros de plataformas específicas con ligeras modificaciones.
- x86 Plan9 y ARM Plan9 son diferentes entre sí.
- Se explica el uso básico mediante un ejemplo sencillo de Plan9.
Ejemplo de Plan9
- A través de los archivos
AddInts_amd64.symain.gose explica la forma básica de declarar y usar funciones en Plan9. - Se describe cómo almacenar los argumentos de función y los valores de retorno en la pila según la convención de llamadas de Go.
Plan de diseño del paquete
- Se diseña un paquete que proporciona una capa delgada de abstracción para operaciones SIMD aritméticas y de bits.
- Se crea un paquete interno que incluye implementaciones de Plan9 por arquitectura y se configura mediante funciones de inicialización.
Ejemplo de SIMD
- Se explica cómo usar SIMD mediante un ejemplo de una función Plan9 con SIMD para x86.
- A través de los archivos
Supported_amd64.syAddFloat32_amd64.sse muestra cómo verificar el soporte de SSE y cómo realizar una operación de suma defloat32.
Rendimiento y futuro
- Un gráfico que muestra la diferencia de rendimiento entre la implementación de software en Go y la implementación SIMD con Plan9 confirma una mejora de velocidad aproximada de 200-450%.
- Se espera que estas notas inspiren proyectos que usen Plan9 y SIMD.
# Resumen de GN⁺
- Este artículo presenta una forma de maximizar el rendimiento aprovechando la concurrencia y el paralelismo de Go.
- Explica cómo realizar cálculos paralelos a nivel de hardware usando el lenguaje ensamblador Plan9 e instrucciones SIMD.
- El texto muestra a los desarrolladores de Go las posibilidades de usar Plan9 y SIMD, y puede ser útil para explorar nuevos enfoques de mejora de rendimiento.
- Como proyectos con funciones similares, se recomiendan bibliotecas de soporte SIMD de Rust o bibliotecas relacionadas con SIMD en C++.
1 comentarios
Comentarios de Hacker News
Para señalar un par de cosas sobre el ensamblador de Go, en amd64 esos
inten realidad son de 64 bitsSi usas
int32, entonces sí quedan alineados por palabra en la lista de parámetros, pero hay una trampa. En sistemas de 64 bits, los valores de retorno siempre empiezan en un desplazamiento alineado a doble palabraNOSPLITestá definido entextflag.h, que el compilador de Go proporciona automáticamente. Pero, por lo que he leído, parece queNOSPLITsolo se respeta en funcionesruntime.XX, así que aquí no hace nada y tampoco es necesarioNOSPLITsignifica que no se inserte el código con el que el compilador verifica si la pila podría desbordarse y necesitar dividirse. Si una función no necesita espacio de pila, técnicamente no hace falta, y básicamente existe para evitar que ese código de verificación se inyecte dentro de la propia función que revisa la división de pila4representa “NOSPLIT”, y como dice que por alguna razón es necesario, agrego esto para quien tenga curiosidad: normalmente, después del tamaño del frame (el parámetro después deNOSPLIT) viene el tamaño de los argumentos, y ambos se separan con un signo menosNo es una resta, solo una sintaxis peculiar. Un tamaño de frame
$24-8significa que la función tiene un frame de 24 bytes y fue llamada con 8 bytes de argumentos en el frame del llamadorSi no especificas
NOSPLITenTEXT, entonces debes proporcionar el tamaño de los argumentos. En el caso de funciones en ensamblador con un prototipo Go,go vetcomprueba que el tamaño de los argumentos sea correctoFuente: https://go.dev/doc/asm
Dice que “Go usa un lenguaje de ensamblador interno propio llamado Plan9”, ¿de verdad se llama así?
La sintaxis proviene de Plan 9, pero lo llamamos ensamblador de Go
Ver https://go.dev/doc/asm
Pero mientras más busco, más me parece una alucinación de LLM
La documentación del formato ensamblador no le pone un nombre formal; simplemente lo llama
go assemblerEl origen probable de esta alucinación está en el primer párrafo: “El ensamblador se basa en el estilo de entrada del ensamblador de Plan 9… este documento resume esa sintaxis, sus diferencias y las particularidades de escribir código ensamblador que interactúe con Go”
Es como decir que “GNU Compiler Collection usa un lenguaje de ensamblador interno propio llamado Unix”
Si te preguntas por qué el equipo de Go eligió este formato de ensamblador dedicado, Rob Pike habló sobre el diseño del ensamblador de Go en una presentación de 2016 [1][2]
La idea central parece haber sido observar que la mayoría de los lenguajes ensamblador son más o menos parecidos, así que se podía crear un lenguaje ensamblador común que te permitiera “hablar con el nivel más bajo de la máquina sin tener que aprender una sintaxis nueva”
Además, eso también permite generar automáticamente un ensamblador que funcione tomando como entrada los PDF de manuales de instrucciones de nuevas arquitecturas
[1]: https://www.youtube.com/watch?v=KINIAgRpkDA
[2]: https://go.dev/talks/2016/asm.slide#1
Decía que “se necesitaban funciones para realizar operaciones SIMD sobre slices”, pero yo quería ver cómo se usa eso realmente
Busqué en todo el artículo y no encontré en ninguna parte una operación aplicada a slices
Edición: lo encontré en la documentación enlazada: https://pkg.go.dev/github.com/pehringer/simd#pkg-index
Básicamente, si tienes 2 slices que quieres sumar, en lugar de un bucle
forpuedes procesarlas en paralelo con SIMD y usar algo comosimd.AddInt32(slice1, slice2, result)https://github.com/pehringer/simd/blob/main/simd_test.go
Respecto a los procesadores que soporta Go, esta sección es la relevante (1). El soporte base de x64 incluye SSE y SSE2
Aunque no sé si el compilador de Go realmente genera eso. A diferencia de compiladores muy complejos como gcc, que priorizan el rendimiento por encima de todo, el compilador de Go prefiere compiladores simples y rápidos al estilo de Wirth (2)
(1) https://go.dev/wiki/MinimumRequirements#amd64
(2) https://irreal.org/blog/?p=7075
https://smartgo.blog/2024/01/06/niklaus-wirth/
Creo que el autor se confundió en esta parte, así que dejo el enlace de referencia: https://en.wikipedia.org/wiki/Plan_9_from_Bell_Labs
Yo también estaba por decir que esto parece el resultado de que un LLM interpretó mal el código
Cuesta imaginar de otra forma que alguien conozca el término Plan 9, se meta a fondo en ensamblador y aun así no sepa en qué aguas se está metiendo. Viendo que otros pensaron lo mismo, me di cuenta de ello
Si eso es correcto, ojalá el autor no sienta vergüenza ni que “lo descubrieron”, y sea honesto. Así también podemos aprender. Me gustaría ganar confianza para identificar este tipo de “exposición de LLM”, pero por más evidente que parezca, casi nunca veo que la gente lo admita
Claro, aquí no es algo evidente, y no deja de ser una conjetura muy apresurada y prejuiciosa
Este tipo de cosas de verdad me enojan
Se me hizo un poco más avanzado que mi nivel, pero disfruté la forma en que el texto lleva al lector de la mano
¿No deberían existir ya intentos así? Debe de haber literalmente decenas de Gophers con ganas de SIMD. ¿El patrón más común será usar CGO?
Llamar funciones en ensamblador desde Go sale mucho más barato
En https://pkg.go.dev/github.com/grailbio/base/simd tengo algunas cosas en las que trabajé en esa dirección
Si mal no recuerdo, en esa época varias instrucciones AVX ni siquiera se podían codificar en el ensamblador Plan 9 de Go, así que había que codificarlas directamente como bytes [0]
La biblioteca más pulida que he visto, aunque no la he usado, hace un hack elegante para usar CGO de forma parcial y evitar esa sobrecarga [1]
[0]: https://github.com/slimsag/rand/blob/f1e8d464c0021a391d5cd64...
[1]: https://github.com/alivanz/go-simd/
Si vas a escribir programas en ensamblador para Go, te recomiendo revisar Avo(https://github.com/mmcloughlin/avo)
Ofrece seguridad de tipos y hace algunas comprobaciones para verificar que genere ensamblador válido. Puede asignar registros dinámicamente y también evita que tengas que calcular a mano cosas como la pila y el tamaño del frame
También puede encargarse de los detalles de la convención de llamadas, así que es fácil cargar argumentos en el registro o la ubicación que quieras
Hace poco se porteó a Avo todo el ensamblador amd64 de la biblioteca crypto de Go, y es una biblioteca muy útil para este tipo de trabajo