- 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
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
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
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é
Parece que el autor del post original interpretó mal la salida del comando
timeEl tiempo
systemes el tiempo de CPU que el kernel usó en nombre del procesoSi en el ejemplo
reales 0.395 s,user0.196 s ysys0.117 s, entonces la CPU solo trabajó un total de 313 ms, y los 82 ms restantes estuvo ociosaEs 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
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
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 quemalloc(), se podría incluso indicar la memoria del programa con nombres de archivo y dejar la persistencia en manos del OSMucho diseño de software todavía sigue atado a las limitaciones de la era del disco duro
fsync()sigue siendo lentoPara una persistencia real, se necesita otro enfoque sin importar si se trata de discos giratorios o no
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/writeEn 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
Es ineficiente como estación de trabajo remota para desarrolladores
Telegram o FB Messenger son rápidos, pero Teams o Skype no
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
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
Como la velocidad de lectura secuencial no se podía mejorar con código, la clave era optimizar los accesos no secuenciales