1 puntos por GN⁺ 2024-01-06 | 1 comentarios | Compartir por WhatsApp

Mejora de velocidad del código: no pases estructuras de más de 16 bytes en AMD64

  • Para mejorar el rendimiento del lenguaje Neat, se cambió la forma de pasar arreglos: en lugar de usar un solo parámetro de estructura, ahora se usan tres parámetros de puntero.
  • La razón por la que los arreglos de Neat eran más lentos que los del lenguaje D es que su tamaño de 24 bytes supera los 16 bytes, por lo que los parámetros se pasan de una manera distinta.
  • Según la especificación SystemV AMD64 ABI, todas las estructuras que superan los 16 bytes se pasan mediante punteros.

Verificación del problema mediante benchmarks

  • Con benchmarks se comprobó la diferencia de rendimiento entre pasar una estructura completa y pasar campos individuales.
  • Al pasar una estructura, es necesario asignarla en la pila y copiarla, mientras que al pasar campos individuales se pueden enviar directamente por registros SSE.
  • Pasar campos individuales ofrece un rendimiento aproximadamente 2 veces mayor que pasar la estructura completa.

La elección del diseñador del lenguaje

  • Al llamar una API de C, se debe seguir la ABI de C, pero los tipos de alto nivel usados internamente no tienen por qué representarse como estructuras.
  • El diseñador del lenguaje puede decidir cómo se pasarán arreglos, tuplas y tipos suma.
  • Pasar como campos individuales los tipos que superan los 16 bytes puede ayudar a mejorar el rendimiento.

La opinión de GN⁺

  • Este artículo es muy útil para desarrolladores interesados en la optimización de software.
  • En especial, muestra que el tamaño de una estructura y su forma de pasarse pueden tener un impacto importante al desarrollar aplicaciones sensibles al rendimiento.
  • Los diseñadores de lenguajes o desarrolladores de APIs pueden aprovechar esta información para encontrar oportunidades de mejora de rendimiento.

1 comentarios

 
GN⁺ 2024-01-06
Opiniones de Hacker News
  • En relación con el problema del ABI SysV amd64, es posible configurar el ABI interno del lenguaje como algo distinto de SysV. Mientras no se exponga a llamadores C de SysV, se puede usar la convención de llamadas que se quiera. La diferencia de NeatLang parece mucho más compleja que simplemente cambiar la convención de llamadas de LLVM, y es posible que el autor también quiera exponer tipos a programas en C con una convención de llamadas consistente.
  • A menudo falta comprensión sobre el costo de pasar argumentos, y lo escrito al respecto resulta útil. Por ejemplo, en Google la práctica de pasar por valor objetos de 24 bytes no aparece en el perfilador, pero genera un costo en todas las funciones.
  • Al migrar a x64, se hizo benchmarking del motor gráfico por la preocupación de que los objetos vec3 (3xfloat) se expandieran de 12 bytes a 16 bytes. Se descubrió que usar 16 bytes era más rápido porque se alinea con lecturas de 8 bytes. Como resultado, vec3 terminó usándose como vec4. Se recomienda siempre hacer benchmarking del sistema completo.
  • Los argumentos precargados en registros rinden mejor que escribir en la pila, y manipular la pila es más rápido que asignar en el heap. Esa es una razón por la que el código complejo con muchas variables globales puede ejecutarse rápido, mientras que funciones recursivas elegantes o argumentos de tuplas/structs/listas pueden ser lentos. Lo primero es más fácil de optimizar en bucles compactos de ensamblador.
  • En MSVC, los structs de más de 8 bytes se pasan en la pila. Ese es un detalle del ABI del que no se debería depender en código portable. Pero en el caso de funciones que no se llaman con frecuencia, no vale la pena estresarse demasiado; y en funciones pequeñas que se llaman mucho, conviene permitir que el compilador haga inline del código para habilitar optimizaciones más útiles que simplemente pasar argumentos en registros.
  • En Windows, al usar la convención de llamadas cdecl predeterminada, los structs de más de 8 bytes no se pasan en registros.
  • En amd64, pasar y devolver por valor structs de más de 16 bytes usando el ABI sysv amd64 es lento, pero a menudo vale la pena para hacer el código más claro. Claro que no aplica en este caso, pero por ejemplo cada compilador de C++, Golang, OCaml y SBCL puede usar un ABI personalizado dentro de su propio lenguaje.
  • En C++, existe la regla práctica de que los tipos no primitivos deberían pasarse por referencia (o, si hace falta, por puntero) salvo que haya una buena razón para no hacerlo. Esto también se debe al ABI y a evitar constructores de copia o de movimiento. Si se busca optimizar rendimiento, este es uno de esos aburridos detalles de bajo nivel a los que hay que prestar atención en C++.
  • El artículo enlaza a un benchmark muy específico en el que Java (JIT) es más rápido que C++ e incluso más rápido que Scala. Eso plantea dudas sobre qué es Julia HO y por qué es tan rápido, por qué hay tanta diferencia de velocidad entre Python y Pypy, si hay razones para no usar Pypy y si debería convertirse en el estándar.
  • En el ejemplo dado, se puede corregir cambiando el tipo de parámetro struct Vector para pasarlo por referencia como const struct Vector &, sin afectar a quien llama. Mucho código C++ con bugs de punteros usó punteros innecesariamente, cuando pasar por referencia habría sido más fácil y seguro de usar.