2 puntos por GN⁺ 2026-01-08 | 1 comentarios | Compartir por WhatsApp
  • Verifica con experimentos el desequilibrio entre el rendimiento de E/S y la velocidad de procesamiento de la CPU discutido recientemente, y muestra que en la práctica la CPU sigue siendo la principal limitación
  • La velocidad de lectura secuencial alcanza 1.6 GB/s con caché fría y 12.8 GB/s con caché caliente, pero el cálculo de frecuencia de palabras en un solo hilo se queda en unos 278 MB/s
  • La estructura de ramificaciones (branches) del código dificulta la vectorización (vectorization), y aun con una simple optimización de conversión a minúsculas solo mejora hasta alrededor de 330 MB/s
  • Incluso el comando wc -w apenas llega a 245 MB/s, confirmando que el cuello de botella está en la CPU y el procesamiento de ramas, no en el disco
  • Con vectorización manual basada en AVX2 se elevó hasta 1.45 GB/s, pero aun así sigue siendo aprox. el 11% de la velocidad de lectura secuencial, lo que demuestra que el cuello de botella no es la E/S sino la CPU

Comparación entre la velocidad de E/S y el rendimiento de la CPU

  • Siguiendo la afirmación de Ben Hoyt, se probó si la reciente mejora en la velocidad de lectura secuencial ya superó el estancamiento de la velocidad de la CPU
    • Midiendo del mismo modo, se obtuvieron 1.6 GB/s con caché fría y 12.8 GB/s con caché caliente
  • Sin embargo, al ejecutar el cálculo de frecuencia de palabras en un solo hilo, el rendimiento fue de apenas 278 MB/s
    • Eso equivale a cerca de 1/5 de la velocidad de lectura del disco incluso con la caché caliente

Experimento de conteo de frecuencia de palabras en C

  • Tras compilar optimized.c con GCC 12 usando las opciones -O3 -march=native, se ejecutó sobre un archivo de entrada de 425 MB
    • Resultado: 1.525 segundos, con una velocidad de procesamiento de 278 MB/s
  • Las múltiples ramas y salidas tempranas en el código dificultan la optimización de vectorización del compilador
    • Al mover la lógica de conversión a minúsculas fuera del bucle, mejoró a 330 MB/s
    • Con Clang, la vectorización se realiza mejor

Comparación con el conteo simple de palabras (wc -w)

  • En lugar del cálculo de frecuencia, se ejecutó el comando wc -w para contar solo el número de palabras
    • Resultado: 245.2 MB/s, más lento de lo esperado
  • wc procesa varios caracteres en blanco y caracteres según la configuración regional, como ' ', '\n', '\t'
    • Requiere más cómputo que un código que separa solo por espacios simples

Intento de vectorización basada en AVX2

  • Aprovechando funciones modernas del CPU, se implementó vectorización con el conjunto de instrucciones AVX2
    • Uso de registros de 256 bits y alineación de datos de 32 bits
    • Para comparar caracteres en blanco se usó la instrucción VPCMPEQB
  • Se detectaron límites de palabras con la máscara de bits (PMOVMSKB) y la instrucción Find First Set (ffs)
    • Inspirado en la implementación de strlen de Cosmopolitan libc

Resultados de rendimiento y conclusión

  • El código vectorizado manualmente (wc-avx2) alcanzó una velocidad de procesamiento de 1.45 GB/s
    • Se verificó que produce el mismo resultado que wc -w (82,113,300 palabras)
  • Incluso con caché fría, el tiempo de cómputo en modo usuario sigue dominando
    • Se confirma que el cuello de botella está en el cómputo de CPU, no en la E/S de disco
  • En general, la velocidad del disco es suficientemente rápida, pero operaciones de CPU como el procesamiento de ramas y el cálculo de hashes siguen siendo el factor limitante
  • El código y los resultados del experimento están publicados en GitHub (haampie/wc-avx2)

1 comentarios

 
GN⁺ 2026-01-08
Comentarios en Hacker News
  • Cree que el límite de rendimiento de las CPU modernas está determinado por la cantidad de datos que puede procesar un solo núcleo, es decir, la velocidad de memcpy()
    La mayoría de los núcleos x86 rondan los 6 GB/s, y la serie Apple M está alrededor de 20 GB/s
    Las cifras como “200 GB/s” que aparecen en la publicidad son solo el ancho de banda agregado de todos los núcleos; un solo núcleo sigue quedándose cerca de 6 GB/s
    Por lo tanto, incluso si se escribe un parser perfecto, no se puede superar ese límite
    Pero si se usa un formato zero-copy, la CPU puede saltarse datos innecesarios y, en teoría, “superar” esos 6 GB/s
    El formato Lite³ que está desarrollando aprovecha este principio y muestra un rendimiento de hasta 120 veces más rápido que simdjson

    • Cree que la cifra propuesta para un solo núcleo es demasiado baja
      Por ejemplo, Zen 1 muestra 25 GB/s en un solo núcleo (enlace de referencia)
      Según los resultados de su microbenchmark, Zen 2 alcanza 17 GB/s sin usar AVX y hasta 35 GB/s con AVX non-temporal
      En un Apple M3 Max, se midieron hasta 125 GB/s con NEON non-temporal
      Por lo tanto, las cifras de 6 GB/s para x86 y 20 GB/s para Apple están muy por debajo de la realidad
    • Pregunta de dónde viene este límite: si se debe a la estructura del bus entre el núcleo y la caché, o entre la caché y el controlador de memoria
    • Le intriga por qué la serie Apple M tiene un ancho de banda por núcleo 3 veces mayor que x86
    • En los chips modernos, es difícil saturar el ancho de banda de memoria usando solo la CPU; hace falta usar la iGPU
      Esto se debe a que la iGPU puede acceder a la memoria unificada
      Por eso, para tareas como copias grandes de memoria, parsing en paralelo o compresión/descompresión, es técnicamente ventajoso usar la iGPU como blitter
      Aun así, el “salto” del que hablan los formatos zero-copy ocurre a nivel de línea de caché
    • Samsung anuncia que su SSD NVMe lee a 14 GB/s, así que resulta interesante la relación con esa cifra si un solo núcleo de CPU se queda en 6 GB/s
  • Parece que el autor del post original interpretó mal la salida del comando time
    El tiempo system es el tiempo de CPU que el kernel usó en nombre del proceso
    Si en el ejemplo real es 0.395 s, user 0.196 s y sys 0.117 s, entonces la CPU solo trabajó un total de 313 ms, y los 82 ms restantes estuvo ociosa
    Es decir, sí operó más rápido que el subsistema de disco, pero la diferencia no es tan grande
    Además, la ruta de I/O está en un estado CPU-bound: aunque el disco y el código fueran infinitamente rápidos, ejecutar el código de I/O del kernel seguiría requiriendo 117 ms

  • Es el autor del post. Hay una segunda parte: I/O is no longer the bottleneck, part 2

    • Antes participó en una competencia de conteo de frecuencia de palabras
      Le parece interesante este artículo de análisis sobre las distintas técnicas de optimización usadas por los participantes
      Según la complejidad del problema o la cantidad de clasificaciones de espacios en blanco, el enfoque cambiaba
    • Si esta prueba se ejecutó en un solo núcleo, entonces el “límite de 6 GB/s” mencionado arriba quedaría refutado experimentalmente
  • El cuello de botella del rendimiento nunca es solo un único factor como “CPU o I/O”, sino el recurso que se satura primero en la carga real de trabajo
    Puede ser la CPU, el ancho de banda de memoria, la caché, el disco, la red, los locks, la latencia, etc.
    Por eso hay que medir, demostrarlo con profiling y volver a medir después de hacer cambios

  • El problema no es la CPU ni la I/O, sino el equilibrio entre latencia y throughput
    La mayoría del software es lento porque ignora la latencia
    Si los datos se colocan linealmente en memoria, o se aplican procesamiento por lotes y paralelismo, puede volverse mucho más rápido

  • Imagina una arquitectura compuesta solo por CPU ↔ caché ↔ almacenamiento no volátil
    Si mmap() tuviera las mismas características de rendimiento que malloc(), se podría incluso indicar la memoria del programa con nombres de archivo y dejar la persistencia en manos del OS
    Mucho diseño de software todavía sigue atado a las limitaciones de la era del disco duro

    • Pero fsync() sigue siendo lento
      Para una persistencia real, se necesita otro enfoque sin importar si se trata de discos giratorios o no
    • En Linux también se puede implementar algo parecido
      De hecho, la mayoría de las solicitudes de memoria se hacen vía mmap()
      Solo que al kernel le cuesta predecir los patrones de acceso, así que puede ser más lento que read/write
  • En la nube, el rendimiento también puede usarse como herramienta de ajuste de precios
    El rendimiento del hardware ha mejorado de forma sorprendente, pero algunos programas (sobre todo Windows o apps de mensajería) incluso se sienten más lentos

    • En realidad, el rendimiento de una instancia en la nube es 5 veces peor que el de una MacBook M1 y mucho más caro
      Es ineficiente como estación de trabajo remota para desarrolladores
    • La razón por la que la mayoría de las apps con GUI siguen siendo lentas es todavía la espera de I/O
      Telegram o FB Messenger son rápidos, pero Teams o Skype no
    • Los monitores CRT mostraban los datos más rápido
      Algunos LCD tienen 500 ms de latencia
  • Cuando aparecieron los SSD NVMe por primera vez, bromeaba con que “ahora es como tener 2 TB de RAM”
    Pero hoy en día los servidores con GPU realmente vienen con 2 TB de RAM: es una ingeniería impresionante

    • Hace tiempo vio una configuración con 2 TB de RAM DDR4 en un servidor Epyc usado por 5 mil dólares
      Se lamenta de no haberlo comprado entonces
  • Según su experiencia optimizando bases de datos OLAP en entornos de alta concurrencia, el cuello de botella casi siempre era la velocidad de la memoria

  • El cuello de botella de I/O originalmente no estaba relacionado con la lectura secuencial, sino con el tiempo de búsqueda (seek)
    Entiende la idea del post, pero quiere señalar ese punto

    • Gracias a tecnologías modernas como CXL/PCIe, ahora incluso la RAM y los controladores de memoria pueden considerarse una especie de dispositivo de I/O
    • En una antigua clase de bases de datos, el rendimiento de I/O se medía por el tiempo de búsqueda del disco duro
      Como la velocidad de lectura secuencial no se podía mejorar con código, la clave era optimizar los accesos no secuenciales