- Al reconstruir el mismo código fuente de
jqy cambiar el asignador, el tiempo para procesar un GeoJSON de 500 MB bajó de 4.606 s a 2.428 s, haciéndolo 1.90 veces más rápido que el binario de Ubuntu - El benchmark se realizó en un Ryzen 9 9950X usando el parcel map del Alameda County Assessor, extrayendo
SitusCitycon la condiciónTotalNetValue < 193000 - Solo con una reconstrucción simple ya hubo una mejora de 2 a 4%, y la combinación de
clang-18,-O3,-fltoy-DNDEBUGlogró 1.20 veces el rendimiento del paquete de Ubuntu - En el perfil apareció con fuerza el costo de asignación de memoria, por lo que se compararon
TCMalloc,jemallocymimalloc; en los experimentos conLD_PRELOAD,mimallocfue el más rápido - La compilación final enlazada con
mimalloctambién registró 0.755 s frente a 1.424 s en otro caso de procesamiento de JSON de 2.2 GB, mostrando que, según la carga de trabajo, puede haber una gran diferencia frente a la compilación predeterminada de la distribución
Carga de trabajo base y método de medición
- El objetivo de la prueba es la herramienta de procesamiento JSON
jq, y los datos de entrada son un archivo GeoJSON de 500 MB con el parcel map del Alameda County Assessor - La consulta ejecutada imprime
SitusCityde los elementos de la lista de parcelas que cumplen la condiciónTotalNetValue < 193000.features[] | select(.properties.TotalNetValue < 193000) | .properties.SitusCity
- El
/usr/bin/jqpredeterminado de Ubuntu tardó alrededor de 5 segundos con el archivo en caché, y el benchmark detallado se midió repetidamente conhyperfine - Para reducir la variación durante la ejecución, se fijó al CPU lógico 2 con
taskset -c 2- Es una configuración para evitar el impacto de las interrupciones del sistema que corren en el CPU 0 y de la migración de CPU
Reconstrucción simple del mismo código fuente
- Se tomó el
código fuente de jqque usa Ubuntu y se ejecutaron configure y build sin flags adicionales - Solo con esta reconstrucción simple ya fue aproximadamente 2 a 4% más rápido que el paquete binario de Ubuntu
- Binario reconstruido: promedio de 4.517 s
/usr/bin/jqde Ubuntu: promedio de 4.641 s- En consecuencia, el rendimiento fue de alrededor de 1.03 veces
Aplicación de clang y flags de optimización
- En el siguiente paso se aplicaron juntos
clang-18, un nivel de optimización más alto, LTO y flags relacionados con depuración y profiling - Los flags clave que afectaron el rendimiento fueron
-O3,-fltoy-DNDEBUG-O3usa un nivel de optimización más alto que-O2-fltohabilita la optimización en tiempo de enlace-DNDEBUGreduce el costo de las aserciones, que aparecía grande en el perfil
- Un ejemplo de configure aplicado es el siguiente
CC=clang-18LDFLAGS="-flto -g -Wl,--emit-relocs -Wl,-z,now -Wl,--gc-sections -fuse-ld=lld"CFLAGS="-flto -DNDEBUG -fno-omit-frame-pointer -gmlt -march=native -O3 -mno-omit-leaf-frame-pointer -ffunction-sections -fdata-sections"
- Esta compilación fue 1.20 veces más rápida que el binario de Ubuntu
- Reconstrucción optimizada: promedio de 3.853 s
/usr/bin/jqde Ubuntu: promedio de 4.631 s
Experimentos cambiando el asignador
jqes un programa complejo en C, y en el perfil la asignación de memoria apareció como el mayor costo- Primero se reconstruyó enlazando
TCMalloc, provisto como paquete de Ubuntu- Se agregó
-L/usr/lib/x86_64-linux-gnu -ltcmalloc_minimalaLDFLAGS - Binario reconstruido: promedio de 3.253 s
/usr/bin/jqde Ubuntu: promedio de 4.611 s- El resultado fue 1.42 veces más rápido que el binario de Ubuntu
- Se agregó
- Incluso cambiando solo el asignador mediante
LD_PRELOADen el binario predeterminado de Ubuntu se puede lograr cierta mejora- Predeterminado: promedio de 4.601 s
- TCMalloc precargado: promedio de 4.082 s
- 1.13 veces más rápido que el predeterminado
Precarga dinámica y configuración de THP
- Se compararon mediante
LD_PRELOADjemalloc,mimallocyTCMallocprovistos por Ubuntu - Esta comparación se obtuvo después de configurar las siguientes variables de entorno
MIMALLOC_LARGE_OS_PAGES=1MALLOC_CONF="thp:always,metadata_thp:always"GLIBC_TUNABLES=glibc.malloc.hugetlb=1
- Como resultado, mimalloc fue el más rápido
- glibc predeterminado: promedio de 4.123 s
- TCMalloc precargado: promedio de 4.130 s
- jemalloc precargado: promedio de 3.510 s
- mimalloc precargado: promedio de 3.154 s
- Activar THP beneficia tanto al asignador de glibc como a jemalloc y mimalloc
THP + mimalloces 31% más rápido queTHP + glibcy 48% más rápido que el valor predeterminado de glibc
Resultado final de la compilación enlazada con mimalloc
- Al considerar que la precarga dinámica no es ideal en términos de rendimiento, en la etapa final se reconstruyó
jqenlazándolo conmimalloc - La compilación final fue 1.90 veces más rápida que el paquete binario de Ubuntu
- Reconstrucción con
mimalloc: promedio de 2.428 s /usr/bin/jqde Ubuntu: promedio de 4.606 s- Cada benchmark se basa en 10 ejecuciones
- Reconstrucción con
- La misma compilación también se usó en otra aplicación
- Procesó 2.2 GB de JSON en 13,000 archivos
- Para la paralelización se usó
rush jqreconstruido conmimalloc: 0.755 sjqdel paquete de Ubuntu: 1.424 s
- En este caso separado, la mejora de velocidad también se acerca a casi 2 veces
1 comentarios
Opiniones de Hacker News
Clickbait como “recompilar un paquete de Ubuntu y cambiar el asignador de memoria para hacerlo 90% más rápido” dan ganas de dar un golpe a través de TCP/IP. Era exactamente un paquete, y algunas mejoras ni siquiera se debían a la recompilación
Aun así, alguna vez metí jemalloc en un programa con
LD_PRELOADpara cambiar la implementación demalloc, y los resultados fueron bastante buenos. No medí el rendimiento, pero el uso de memoria de esa aplicación se estabilizó y también se resolvió un problema que parecía una fuga de memoria. En realidad, es muy probable que no fuera un problema de la app en sí, sino fragmentación de memoria delmallocestándarfree(), salvo en situaciones excepcionales la memoria no se libera realmente hacia afueraCuantos más hilos y núcleos de CPU haya, peor se vuelve este problema. Una solución sencilla es establecer la variable de entorno “mágica”
MALLOC_ARENA_MAX=2para limitar la cantidad de cachés. Otra opción es que la aplicación llame periódicamente amalloc_trim()para vaciar la caché, pero eso requiere modificar el código fuentehttps://www.joyfulbikeshedding.com/blog/2019-03-14-what-caus...
En cambio, me alegra que no compile con el potencialmente problemático
-O3. Puede ser bueno para algunas partes donde el rendimiento es importante, pero no quiero que todo el sistema se compile con-O3Hace mucho empecé a compilar Mozilla y mi kernel de Linux a mi gusto, y por lo general obtenía una mejora de rendimiento decente. Todo el propósito de la distribución Gentoo Linux, por ejemplo, es la mejora de rendimiento que se puede obtener al compilar todo desde el código fuente con optimizaciones
jq,grep,ffmpeguocrmypdf, y estas utilidades generales de Unix muchas veces se crean como builds de propósito general, no para una aplicación específicaLa ingeniería es negociación. En el texto, la mayor parte de las ganancias viene de especializar el asignador de memoria. Hay que recordar que algunos proyectos son multihilo, y pueden asignar en un hilo, escribir datos en otro y liberar en un tercer hilo
El asignador tiene que manejar eso, así que una mejora de velocidad en un proyecto puede convertirse en un crash en otro. La estrategia de reasignación también es un problema. Algunos programas preasignan y nunca vuelven a tocar
malloc, pero otros liberan y vuelven a obtener memoria constantemente. También importa qué tan bien maneja la fragmentación y si el tiempo de ejecución es de 10 segundos o de 10 años. A veces, la elección del asignador es la diferencia entre estabilidad a largo plazo y velocidad a corto plazoAl probar video 4K y crear un editor de video que cachea frames, experimenté con varios asignadores. Con 32 MB por frame y 60 fps, es casi 2 GB por segundo por cada pista. Enseguida te topas con los límites del asignador, y te das cuenta de que, al menos, el asignador predeterminado de glibc es el mejor para la estabilidad a largo plazo. Pero en benchmarks cortos es el más lento
Claro que la magnitud de la mejora puede variar, pero los benchmarks en general muestran mejoras generales. Si eso tiene sentido para una aplicación específica es un asunto totalmente distinto. En cuanto a los crashes, todos estos son asignadores multihilo de propósito general, así que no se comportan distinto de glibc, y los bugs podrían existir igual en glibc
DEFAULT_MMAP_THRESHOLD_MAX, que en plataformas de 64 bits es 32 MiB, así que no puedes convencer a glibc de cachearlo como está documentado en el manual demalloptCada vez pide memoria directamente al kernel con
mmapy la devuelve conmunmap. Estas llamadas al sistema son algo lentas y, en mi caso, el costo de generar un fallo de página en cada página de memoria en el primer acceso es lo bastante lento como para no cumplir mis objetivos de rendimiento. La solución es realmente simple: usar una lista libre propia encima de un asignador de propósito general o demmapsolo para los frames de video. Funciona bien porque se repiten de forma muy estable asignaciones de exactamente el mismo tamaño[1] En formato UYVY es un poco menos de 64 MiB, y en formato I420 es un poco menos de 48 MiB
Pero al leer este comentario superior me preocupa haber malinterpretado por completo el texto. Por el tono, suena como si dijera que lo que propone el gist no debe hacerse jamás, y que es una sugerencia terrible que pasa por alto toda esta complejidad. ¿Podrías ayudarme a entender si el gist original es un buen texto, si tiene puntos válidos o si no tiene ningún valor? Hasta ver este comentario pensaba que sí lo tenía, pero me di cuenta de que no soy lo bastante listo para distinguirlo
Si usas el algoritmo de compresión adecuado para los datos, la gente dice por qué eres tonto y deberías haber usado otro algoritmo. Hace poco tuve que comprimir una cadena JSON larga específica para meterla en Dynamo, y tras probar exhaustivamente todos los algoritmos populares, Brotli quedó muy por delante. Aun así, eso no impidió que cada persona que pasaba dijera que zlib era mejor. A veces cansa bastante
Gentoo Linux es, en la práctica, una distribución hecha para este tipo de personas, para que puedan optimizar su equipo Linux según sus propios usos
Después de la configuración inicial, es bastante simple y fácil de usar. Recuerdo haber hecho muchos amigos en el canal de Gentoo Linux en Matrix; fueron tiempos divertidos
https://www.gentoo.org/
Como dato curioso, las primeras versiones de ChromeOS eran básicamente una instalación personalizada de Gentoo Linux. No sé si todavía usan Gentoo Linux internamente
He usado Gentoo durante 20 años, pero nunca lo usé por rendimiento. Gentoo es excelente cuando sabes cómo quieres que se comporte el sistema y te ayuda a llegar ahí
El objetivo de Gentoo es tener un sistema operativo que compile todos los programas desde el código fuente, en lugar de usar paquetes binarios precompilados. Esto permite mejoras avanzadas de velocidad y personalización, pero también significa que incluso los componentes más básicos, como el kernel, deben compilarse desde el código fuente. En la comunidad Linux es conocido como un sistema operativo muy complejo por su proceso de instalación exigente. Una instalación básica de Gentoo arranca directamente en una línea de comandos, y el usuario debe particionar el disco manualmente, descargar y descomprimir un paquete llamado “Stage 3 tarball”, instalar paquetes a mano y construir el sistema. Los usuarios nuevos o con poca experiencia muchas veces no saben qué hacer cuando entran al instalador y no hay interfaz gráfica. Los miembros de /g/ a menudo exageran el valor de Gentoo para engañar a usuarios nuevos y hacer que intenten instalarlo
Puede haber uno o dos tropiezos, pero si tienes suficiente experiencia general con Linux, es probable que puedas meterte en la receta de compilación, arreglarla, hacer que funcione como necesitas y contribuir los cambios upstream. Esto es resultado de un enfoque obsesivo en el minimalismo y de evitar cualquier tipo de sobreingeniería. También es algo que extrañé durante todo el tiempo que usé Gentoo. En Gentoo, siempre terminaba tocando flags USE y máscaras de paquetes de formas que no eran muy útiles para otros usuarios. El sistema de compilación era tan complejo que, durante muchos años, fue muy difícil dominarlo bien, solucionar problemas a nivel de causa raíz y contribuirlos upstream. Void también puede ser una base ideal si no quieres compilar todo el sistema desde el código fuente, pero sí quieres mezclar binarios provistos por la distribución con paquetes compilados directamente desde el código fuente
Después me pasé a ArchLinux, y en general me fue bien. Si usas un procesador bastante estándar, no creo que Gentoo te dé una ventaja tan grande
Si haces esto, te quedas fuera no solo de
jq, sino también de las actualizaciones de seguridad de onigurama, la dependencia de parsing de expresiones regulares. Ya hubo una actualización de seguridad de onigurama antes, y si vuelve a pasar algo así podrías quedar vulnerable.jqse usa con frecuencia para parsear JSON no confiableDecía “Actualización de seguridad: corrige varias desreferencias de punteros inválidos, corrupción de memoria por escritura fuera de límites y desbordamiento de búfer en la pila”, y ese caso correspondía a CVE-2017-9224, CVE-2017-9226, CVE-2017-9227, CVE-2017-9228 y CVE-2017-9229
libonig5, y se actualizará normalmenteNo quiero menospreciar el valor del sistema CVE, pero también es difícil negar que hay grandes diferencias de impacto real entre los hallazgos
Hace tiempo que no trabajo con este tipo de cosas, pero según recuerdo, en cuanto pasas de las flags que usan los desarrolladores upstream, te ganas bugs raros y una enorme indiferencia cuando aparecen. Me refiero a los desarrolladores upstream, no a los empaquetadores de la distribución
Nunca he usado un
mallocque no sea el de libc, pero imagino que aplica el mismo principioPero si todos hacen eso, se convierte en un monocultivo, y los monocultivos son frágiles y malos. El código solo se vuelve un poco más robusto porque se compila en otros contextos: otras plataformas, compiladores, opciones, bibliotecas, etc. Un bug que la mayoría no tocó porque una plataforma o unas flags de compilación pasaron por casualidad justo al lado de la trampa sigue siendo un bug, y encontrarlo y corregirlo es mejor para el código. Como individuos, todos nos beneficiamos cuando el código en general se vuelve más robusto en lugar de frágil
Claro que también pensaba que
-march=nativeera la mejora principal que veía, pero este artículo muestra que no necesariamente es así. También parece posible que las aplicaciones que usan coma flotante tengan más asperezasAunque se parece más a haberlo recompilado con otro asignador que da buenos benchmarks en flujos de trabajo específicos
mallocde glibc. Que las distribuciones sigan usandomallocde glibc en lugar de mimalloc o jemalloc es prácticamente una negligencia profesionalmallocde glibc?Me da curiosidad cómo se compara el rendimiento con este clon de
jqbasado en Rustcargo install --locked jaqPara activar optimizaciones específicas para una familia de CPU, también se puede agregar
RUSTFLAGS="-C target-cpu=native".cargo installes una función subestimada de Rust que encaja perfecto con el tipo de uso descrito en el artículo. Como compila la herramienta desde el código fuente, se pueden elegir funciones o instrucciones específicas de la plataforma que normalmente no se incluirían en los binarios por compatibilidad con CPUs antiguas. No hace falta clonar el repositorio ni averiguar cómo compilarlo: viene incluidojaq[1] yyq[2] son mis opciones cada vez que estoy usandojqy necesito una mejora de rendimiento rápida y sencilla[1] https://github.com/01mf02/jaq
[2] https://github.com/mikefarah/yq
jqygojq, usando como prueba mi solución enjqpara AoC 2022 day 13https://gist.github.com/oguz-ismail/8d0957dfeecc4f816ffee79d...
Todavía queda por detrás de ambos
cargo installtambién tiene la bandera--git, que permite especificar la URL del repositorio. Sirve cuando no hay un paquete público o cuando quieres un commit reciente que todavía no se ha lanzadoLa he usado varias veces antes, sobre todo como una forma sencilla de instalar rápidamente herramientas personales hechas a las apuradas después de subirlas a un repositorio, sin tener que armar un proceso de release ni copiar binarios manualmente a mis equipos personales y rastrear el commit exacto usado para compilar
Si de verdad vas a hacer esto, basta con hacer que Ubuntu descargue el paquete fuente exacto que quiere. En este caso, se puede usar
apt-get source jqLuego entras al paquete y recompilas todo lo que quieras. También puedes volver a empaquetarlo para distribuirlo o archivarlo. Así obtendrás un resultado mucho más cercano al Ubuntu upstream, en lugar de un montón de errores raros e inconsistencias
El título induce a error. Se refiere al 90% del tiempo que se volvió más rápido; en realidad es aproximadamente 45% más rápido
Es algo interesante si te importa cómo usamos el lenguaje. También podríamos decir que hace 90% más trabajo en el mismo tiempo, lo cual coincide con otras unidades de velocidad que usamos comúnmente, como millas por hora, palabras por minuto o bits por segundo. Pero en rendimiento de computadoras existe la convención de medir el tiempo que tarda una cantidad fija de trabajo. Supongo que se debe a que, por lo general, la cantidad de trabajo es fija y lo que cambia es el tiempo de espera. En el caso de este post, también es exactamente así, por eso el tiempo queda en el numerador. El artículo en sí es muy interesante y está bien escrito, pero no es 90% más rápido
Además, si se usara una unidad de tiempo, no usaría la palabra “faster”. “45% less time” y “45% faster” son afirmaciones muy distintas, y ambas tienen sentido dentro y fuera de la programación
Cuando decimos “reduje algo en N%”, normalmente asumimos que ese N% es del propio objeto reducido, no de otro valor
Al leer que un cambio tan simple puede lograr una gran mejora de velocidad, lo primero que se me ocurrió fue que habría que avisarles a los autores de jq. Puede haber trampas a tener en cuenta, o quizá podrían probarlo y hacerlo más rápido para todos
Sea cual sea el resultado, parece útil avisarles brevemente. Pero el artículo ni siquiera parece considerar esa opción, y tampoco la veo en estos comentarios. ¿Me estoy perdiendo algo?
No sé si allí el asignador de glibc también es el estándar
https://en.m.wikipedia.org/wiki/Clear_Linux_OS