- 1BRC: un desafío para escribir código que lea mediciones de temperatura desde un archivo de texto con mil millones de filas y calcule la temperatura mínima/promedio/máxima por estación de observación.
- Se llevó a cabo del 1 al 31 de enero de 2024, y el objetivo era aprovechar al máximo las versiones más recientes de Java.
- A partir de eso, la gente empezó a interesarse y a intentarlo con varios lenguajes (Rust, Go, C++, SQL).
- Presentación detallada de 9 soluciones escritas en Go (ordenadas de la más lenta a la más rápida).
Medición base
- Usando el comando
cat, el tiempo para leer los datos de texto de mil millones de filas (13 GB) es de 1.052 segundos.
- El comando
wc, que realmente procesa el archivo, tarda casi 1 minuto (55.710 segundos).
- Resolver el problema con una solución en AWK toma 7 minutos y 35 segundos.
Solución 1: Go simple e idiomático
- La primera solución, usando la biblioteca estándar de Go, tarda 1 minuto y 45 segundos.
- Lee líneas con
bufio.Scanner y las divide por ';' usando strings.Cut.
- Analiza la temperatura con
strconv.ParseFloat y acumula los resultados usando un mapa de Go.
Solución 2: mapa con valores puntero
- Usa
map[string]*stats para evitar hacer hash dos veces en el mapa.
- Al usar valores puntero, reduce el tiempo de 1 minuto 45 segundos a 1 minuto 31 segundos.
Solución 3: evitar strconv.ParseFloat
- Analiza la temperatura con código personalizado en lugar de
strconv.ParseFloat.
- Reduce el tiempo de 1 minuto 31 segundos a 55.8 segundos.
Solución 4: usar enteros de punto fijo
- Representa la temperatura como entero para evitar operaciones de punto flotante.
- Reduce el tiempo de 55.8 segundos a 51.0 segundos.
Solución 5: evitar bytes.Cut
- En lugar de recorrer todo el nombre de la estación para encontrar
';', analiza desde el final.
- Reduce el tiempo de 51.0 segundos a 46.0 segundos.
Solución 6: evitar bufio.Scanner
- Elimina
bufio.Scanner y lee el archivo en chunks grandes.
- Reduce el tiempo de 46.0 segundos a 41.3 segundos.
Solución 7: tabla hash personalizada
- Implementa una tabla hash personalizada en lugar del mapa de Go.
- Reduce el tiempo de 41.3 segundos a 25.8 segundos.
Solución 8: procesamiento paralelo de chunks
- Paraleliza el código simple e idiomático y reduce el tiempo de 1 minuto 45 segundos a 24.3 segundos.
Solución 9: todas las optimizaciones y paralelización
- Combina todas las optimizaciones con procesamiento paralelo y reduce el tiempo de 24.3 segundos a 3.99 segundos.
Tabla de resultados
- Incluye una tabla que compara todas las soluciones en Go junto con las versiones más rápidas en Go y Java.
- La más rápida entre las versiones en Go procesa en 2.90 segundos, y la versión en Java en 0.953 segundos.
- La versión en Java que tarda menos de 1 segundo fue hecha por Thomas Wuerthinger (creador de GraalVM), algo que probablemente solo sea posible porque es un experto en este campo.
Comentario final
- En tareas cotidianas de programación, el código simple e idiomático es un buen punto de partida.
- Si estás construyendo un pipeline de procesamiento de datos, hacer el código 4 o 26 veces más rápido puede mejorar la satisfacción del usuario y ahorrar costos de cómputo.
- Si estás construyendo un runtime o un intérprete, las mejoras de rendimiento son importantes.
Opinión de GN⁺
- Este artículo ofrece un caso de estudio interesante sobre optimización de rendimiento al explorar distintas formas de optimizar el procesamiento de grandes volúmenes de datos con el lenguaje Go.
- Muestra que, durante el proceso de optimización, implementar estructuras de datos como una tabla hash personalizada, más allá de la biblioteca estándar de Go, juega un papel importante.
- Destaca el efecto del procesamiento paralelo y cómo combinar optimización en un solo núcleo con paralelización permite lograr mejoras de rendimiento sorprendentes.
- El artículo ofrece insights útiles para ingenieros de software que desarrollan aplicaciones sensibles al rendimiento.
- Qué tan útiles sean estas optimizaciones en un entorno real de producción puede variar según el caso de uso. No todas las aplicaciones necesitan este nivel de optimización.
6 comentarios
Me da curiosidad qué trabajo concreto se hizo en el paso 7. Ahí fue donde hubo una mejora de rendimiento enorme jajaja
Es interesante cómo separaron cada paso y mostraron cuánto mejoró el rendimiento en cada uno jaja
Con
wctambién toma 1 minuto... al final, lo mejor es no escribir código... jejeGracias por compartir este buen artículo. Me hizo recordar una época en la que estaba obsesionado con la optimización jaja.
Con los años de experiencia en desarrollo, fui acumulando muchas experiencias en las que el código más optimizado posible era difícil de mantener, así que en un entorno organizacional resultaba complicado operarlo; por eso poco a poco me fui alejando del camino de la optimización. (De repente se volvió una reflexión personal)
¡¡Código optimizado para la organización!!
Comentarios de Hacker News
cat,wc, etc. Considera que este método es una forma sencilla de obtener un rango "razonable".unsafe.Pointer, el hecho de que funciones de los paquetesbytesybitsde la biblioteca estándar están escritas en ensamblador, la configuración para desactivar el recolector de basura y cómo fijar goroutines a hilos.awk,grepy otros van a otro nivel de velocidad, y afirmó que agregarLC_ALL=Ca la solución conawkpuede reducir el tiempo de procesamiento a menos de un minuto.