35 puntos por GN⁺ 2024-03-04 | 6 comentarios | Compartir por WhatsApp
  • 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

 
cosine20 2024-03-07

Me da curiosidad qué trabajo concreto se hizo en el paso 7. Ahí fue donde hubo una mejora de rendimiento enorme jajaja

 
sddsdd94 2024-03-06

Es interesante cómo separaron cada paso y mostraron cuánto mejoró el rendimiento en cada uno jaja

 
galadbran 2024-03-05

Con wc también toma 1 minuto... al final, lo mejor es no escribir código... jeje

 
jhbaek 2024-03-05

Gracias 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)

 
misolab 2024-03-05

¡¡Código optimizado para la organización!!

 
GN⁺ 2024-03-04
Comentarios de Hacker News
  • El primer usuario mencionó que, como no tenía experiencia optimizando código para manipulación de datos, le resultó especialmente interesante la primera sección en la que se obtienen métricas base usando cat, wc, etc. Considera que este método es una forma sencilla de obtener un rango "razonable".
  • El segundo usuario comentó que el tiempo de procesamiento usando la biblioteca Polars fue de 33 segundos y expresó interés por la solución más simple que se acerca a la solución optimizada manualmente más rápida.
  • El tercer usuario señaló que los informes de perfilado de rendimiento en Go pueden ser confusos y explicó que, cuando el tiempo de ejecución de una línea específica de código no parece intuitivo, puede deberse a que los datos son difíciles de predecir y el predictor de ramas puede equivocarse.
  • El cuarto usuario compartió el resultado de haber hecho el 1BRC (1 Billion Row Challenge) en Go y mencionó que aprendió técnicas de optimización propias del lenguaje. Por ejemplo, lecturas de memoria sin verificación de límites usando unsafe.Pointer, el hecho de que funciones de los paquetes bytes y bits de 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.
  • El quinto usuario afirmó que un desarrollador de shell scripts probablemente ya habría terminado de procesar esos mil millones de filas mientras los desarrolladores de otros lenguajes todavía se preparaban.
  • El sexto usuario sostuvo que las bases de datos son más rápidas, menos complejas y más robustas ante actualizaciones de datos que el código de aplicación, y enfatizó que se debería hacer más trabajo dentro de la base de datos.
  • El séptimo usuario compartió que en 2010 desarrolló una app web usando PostgreSQL para consultar 270 millones de filas de datos climáticos de Environment Canada, y que ese software incluso ganó premios. La app estaba optimizada para generar informes en menos de un minuto.
  • El octavo usuario comentó que le parece genial que el código paralelo en Go siga manteniendo el estilo idiomático del lenguaje.
  • El noveno usuario mencionó que, al trabajar con archivos de texto grandes desde la CLI, si se omite el parseo de Unicode, awk, grep y otros van a otro nivel de velocidad, y afirmó que agregar LC_ALL=C a la solución con awk puede reducir el tiempo de procesamiento a menos de un minuto.
  • El último usuario comentó que le parece interesante que la versión más rápida en Java sea más veloz que la versión más rápida en Go, y evaluó que el rendimiento de la Java Virtual Machine (JVM) es bastante bueno.