4 puntos por GN⁺ 2024-10-19 | 1 comentarios | Compartir por WhatsApp

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.s y main.go se 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.s y AddFloat32_amd64.s se muestra cómo verificar el soporte de SSE y cómo realizar una operación de suma de float32.

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

 
GN⁺ 2024-10-19
Comentarios de Hacker News
  • Para señalar un par de cosas sobre el ensamblador de Go, en amd64 esos int en realidad son de 64 bits
    Si 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 palabra
    NOSPLIT está definido en textflag.h, que el compilador de Go proporciona automáticamente. Pero, por lo que he leído, parece que NOSPLIT solo se respeta en funciones runtime.XX, así que aquí no hace nada y tampoco es necesario
    NOSPLIT significa 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 pila

  • 4 representa “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 de NOSPLIT) viene el tamaño de los argumentos, y ambos se separan con un signo menos
    No es una resta, solo una sintaxis peculiar. Un tamaño de frame $24-8 significa que la función tiene un frame de 24 bytes y fue llamada con 8 bytes de argumentos en el frame del llamador
    Si no especificas NOSPLIT en TEXT, entonces debes proporcionar el tamaño de los argumentos. En el caso de funciones en ensamblador con un prototipo Go, go vet comprueba que el tamaño de los argumentos sea correcto
    Fuente: https://go.dev/doc/asm

  • Dice que “Go usa un lenguaje de ensamblador interno propio llamado Plan9”, ¿de verdad se llama así?

    • No. Simplemente es ensamblador de Go
      La sintaxis proviene de Plan 9, pero lo llamamos ensamblador de Go
      Ver https://go.dev/doc/asm
    • Es una pregunta válida. Al principio yo también asumí que sí. Me pareció que alguien que investigó este tema no podía equivocarse en algo así, y si conoces un poco el proyecto, el nombre incluso suena medio razonable
      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 assembler
      El 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”
    • No tiene un nombre aparte. Plan 9 es un sistema operativo, y este estilo de sintaxis de ensamblador proviene del ensamblador que se usaba en ese sistema operativo
      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

    • De hecho sí funcionó. Go ayudó a establecer la expectativa de que un lenguaje de programación nuevo debe traer la compilación cruzada como capacidad básica, y en ese momento casi ningún lenguaje lo hacía bien
    • Como referencia, SpiderMonkey también ha hecho casi lo mismo durante unos 25 años, y supongo que otras máquinas virtuales de JavaScript también
  • 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 for puedes procesarlas en paralelo con SIMD y usar algo como simd.AddInt32(slice1, slice2, result)

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

    • En la práctica, yo asumiría que todos los chips desde más o menos 2008 soportan SSE4.1
  • 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

    • ¿Honestamente? No. Debería darle vergüenza esa arrogancia de pensar que esto era un atajo para no hacer el trabajo que el LLM debería poder hacer bien
      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?

    • El problema de cgo es que la sobrecarga de llamada de función es grande. Conviene usarlo solo para bloques de trabajo bastante grandes
      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
    • Seguro que la gente ya lo ha intentado desde hace mucho. Recuerdo que hace casi 10 años hablaba por teléfono con mi hermano cuando intentaba hacer una biblioteca SIMD en Go (¿era en Skype?)
      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/
    • Rechazaron una propuesta para añadir algo así a la biblioteca estándar, pero ayuda a entender el contexto: https://github.com/golang/go/issues/53171
  • 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