
Volví a medir Wild contra mold y ganó mold: la culpa no era de las versiones, era de btrfs
En septiembre Wild le ganó a mold enlazando ripgrep. Repetí el banco con Wild 0.11 y mold 3.0 y mold ganó, también con los binarios viejos. Entre medio la máquina pasó de ext4 a btrfs. Con target en ext4 o tmpfs, Wild vuelve a ganar; en btrfs, Wild elige escribir desde un búfer y forzarle mmap le recorta entre 18 y 37 % del tiempo. En debug el enlace pesa de verdad: rust-lld tardó 37 s en btrfs y 1,9 s en ext4 el mismo enlace de bevy.
El 18 de septiembre publiqué que Wild le ganaba a mold enlazando Rust: 16,3 contra 18,3 ms en ripgrep, en las 20 repeticiones. Ese post dejó cosas sin medir: el perfil debug, la memoria, el número de hilos, otra máquina. Y en octubre salieron Wild 0.11.0 y mold 3.0.0, que es mold reescrito en Rust. Así que escribí una ficha con cinco hipótesis y su regla de decisión, la commiteé, y volví a correr el mismo banco.
mold ganó. Le ganó a Wild en 18 de las 20 repeticiones, 22,5 contra 35,3 ms, y también con los mismos binarios de septiembre. Las versiones no explicaban nada. Lo que había cambiado era el disco: entre un post y otro la máquina pasó de ext4 a btrfs. Con target en ext4 o en tmpfs Wild vuelve a ganar, y en btrfs Wild elige un modo de escritura que, en mi banco, lo hace más lento. En debug el enlace deja de ser un detalle: con bevy, rust-lld tardó 37 s en btrfs y 1,9 s en ext4 repitiendo el mismo enlace.
Qué cambió entre septiembre y hoy
La máquina es la misma: un Ryzen 7 7800X3D de 8 núcleos y 16 hilos con NVMe. El sistema no. En septiembre era Fedora 43 con ext4. Desde octubre es NixOS, y el directorio donde viven los proyectos, /data, es un subvolumen btrfs montado con compress=zstd:1. El kernel pasó de 7.2.4 a 6.18.55 y rustc de 1.98.0 a 1.99.1.
Por eso la ficha no compara cifras de septiembre con cifras de hoy. Las versiones viejas de los dos enlazadores, Wild 0.10.0 y mold 2.42.1, se vuelven a medir en el sistema nuevo, junto a las nuevas, en el mismo banco. El banco es el de septiembre sin cambios: un shim con un cronómetro de microsegundos alrededor del enlazador, una línea agregada a main.rs por medida, 20 repeticiones por brazo, brazos intercalados con el orden rotando, 8 núcleos visibles y un tope de 12 GB.
Las cinco hipótesis de la ficha: que Wild 0.11 no es más lento que 0.10 (sus notas avisan que el soporte de COMDAT lo hizo “un poco más lento”), que en debug el enlace pesa más, que la brecha entre mold y Wild cambia de 8 a 16 CPU, que la memoria ordena distinto que el tiempo, y que en un notebook la ventaja se achica. Lo del sistema de archivos no estaba en la lista. Apareció en el primer experimento, y lo que vino después está en cuatro enmiendas, cada una commiteada antes de su medición.
Las versiones nuevas no explican el cambio
Medianas de 20 repeticiones del enlace de ripgrep y cargo en release, con target en btrfs:
| ms | GNU ld | rust-lld | mold 2.42.1 | mold 3.0.0 | Wild 0.10.0 | Wild 0.11.0 |
|---|---|---|---|---|---|---|
| ripgrep | 401 | 45,3 | 22,5 | 22,2 | 35,3 | 34,9 |
| cargo | 1530 | 108 | 41,8 | 62,7 | 54,1 | 54,8 |
Con los binarios de septiembre, mold 2.42.1 tarda 0,64 veces lo de Wild 0.10.0 en ripgrep (IC 95 %: 0,63 a 0,65). En septiembre era al revés.
Wild 0.11 contra 0.10: razón 1,01 en ripgrep (IC 1,00 a 1,03) y 0,99 en cargo (IC 0,97 a 1,00). En cargo el intervalo deja fuera el 1 por poco, con 0.11 un 1,3 % más lento, muy debajo del 5 % que la ficha fijó como efecto mínimo. Sin diferencia relevante.
mold 3.0 contra 2.42.1: empate en ripgrep (razón 1,01, IC 1,00 a 1,04). En cargo, 3.0.0 tardó 62,7 ms contra 41,8 ms. No lo cuento como regresión: el mismo brazo de mold 3.0.0 en cargo release dio 55,3 ms y 49,4 ms en otros dos experimentos del mismo día. Es un dato de un solo experimento, y las notas de mold 3.0 dicen que el rendimiento quedó a la par de 2.42.1.
Ninguna versión mueve la aguja. Wild seguía perdiendo con los binarios con los que había ganado.
El sistema de archivos invierte el orden
La enmienda 1 nació de ese resultado, y lo dice: es una hipótesis post hoc. El experimento cambia una sola cosa, dónde vive target. Cuatro lugares en el mismo NVMe: btrfs con zstd (como está), btrfs con la compresión apagada en ese directorio, tmpfs en RAM, y ext4 en una imagen loop. ripgrep release, 20 repeticiones por brazo y sistema, en dos rondas con el orden de los sistemas invertido en la segunda.
Animación veinte veces más lenta que lo medido. Así está /data en fedora.
Animación veinte veces más lenta que lo medido. Apagar zstd no cambia nada.
Animación veinte veces más lenta que lo medido. En RAM, Wild vuelve adelante.
Animación veinte veces más lenta que lo medido. Como en septiembre.
Fedora con NixOS, kernel 6.18.55, rustc 1.99.1. Las cuatro ubicaciones están en el mismo NVMe; ext4 es una imagen loop.
La regla de la enmienda pedía que la razón mold/Wild quedara bajo 1 en btrfs y sobre 1 en ext4 o tmpfs, con intervalos que no tocaran el 1. Se cumplió con margen: 0,63 en btrfs con zstd, 0,65 sin compresión, 1,20 en ext4 y 1,30 en tmpfs. Y como btrfs sin compresión se parece a btrfs con zstd, no a ext4, la compresión no explica nada.
Wild pasa de 14,6 ms en ext4 a 35,1 ms en btrfs: 2,4 veces. mold pasa de 17,5 a 22,1 ms: 1,26 veces. rust-lld, de 34,3 a 44,3 ms. Los tres pierden en btrfs; Wild pierde mucho más.
Lo comprobé por un segundo camino. Wild sabe guardar los insumos de un enlace con WILD_SAVE_BASE y repetirlo después con cualquier enlazador (run-with), que es el método de su propio BENCHMARKING.md. Repitiendo el mismo enlace de ripgrep 5 veces por brazo, con la copia en btrfs y en ext4: Wild 39,1 contra 18,5 ms, mold 25,8 contra 21,7, rust-lld 43,8 contra 40,6. Mismo orden.
btrfsext4
El notebook confirma el lado ext4 en otra máquina. En lenovo, un i5-8250U de 4 núcleos con NixOS y ext4 sobre un SSD SATA, Wild ganó todo lo que medí: ripgrep release 33,5 contra 59,7 ms de mold, ripgrep debug 72,0 contra 123,3, cargo release 65,7 contra 143,3 y cargo debug 508 contra 718 ms. La hipótesis de la ficha, que en un notebook la ventaja se achicaba, salió al revés: se agrandó.
Por qué btrfs le pega más a Wild
Con perf stat sobre el mismo enlace repetido (media de 5 ejecuciones, contando los procesos hijos) aparece una diferencia que acompaña al tiempo: en btrfs los tres enlazadores tienen más fallos de página. Wild 16.329 contra 8.063 en ext4, mold 12.763 contra 5.388, rust-lld 18.548 contra 14.213. Y strace -c muestra a los hilos de Wild pasando 0,51 s en futex en btrfs contra 0,19 s en ext4. Eso es asociación: dice qué cambia junto con el tiempo, no qué lo causa.
La pista concreta salió del código de Wild. El issue #1499, “BTRFS write performance”, dice que la escritura de la salida en btrfs es mala con varios hilos. La respuesta está en libwild/src/fs.rs: si la salida va a btrfs y el kernel es anterior a 7.2, Wild no escribe con mmap. Arma el archivo entero en memoria y lo escribe al final, y apaga las hugepages. El PR #2509, que entró en 0.11.0, agregó la excepción del kernel: con 7.2 o más nuevo vuelve al mmap. Mi kernel es 6.18. El strace ya lo mostraba: en btrfs, Wild hace 3 llamadas a write que suman 21,7 ms; en ext4, write ni aparece entre sus llamadas principales.
Las razones son el tiempo de Wild con mmap forzado sobre su tiempo por defecto en btrfs, mediana de 20 repeticiones del mismo enlace. Con un kernel 7.2 o más nuevo, el código manda también a btrfs por el carril de mmap; eso no lo medí.
Wild tiene opciones para elegir el modo a mano: --mmap-output-file y --no-mmap-output-file. La enmienda 3 las usó como control positivo, con una regla escrita antes de medir: si forzar el modo de búfer en ext4 hacía a Wild al menos 30 % más lento, ese modo reproducía la penalización; y en btrfs, el modo por defecto y el de búfer tenían que quedar dentro de ±10 % entre sí, para confirmar que era el que Wild elegía.
Animación veinticinco veces más lenta que lo medido. Corrió con otro banco activo en la máquina.
Animación veinticinco veces más lenta que lo medido. btrfs y ext4 en dos imágenes loop del mismo SSD.
Wild 0.11.0, kernel 6.18.55. Por defecto en btrfs con este kernel, Wild escribe desde un búfer; en ext4 usa mmap.
En fedora, el modo de búfer forzado en btrfs quedó a 0,96 veces el de por defecto (IC 0,93 a 0,99): es el mismo modo, como dice el código. En ext4, forzar el búfer hizo a Wild 1,24 veces más lento (IC 1,15 a 1,28): una penalización real, pero bajo el 30 % que pedía la regla.
Ese experimento tiene un problema, y lo declaré en la ficha antes de mirar más: corrió con la semana 2 de otro banco ocupando la máquina. mold, que en el replay sin carga tardaba 22 a 26 ms, aquí dio 68 a 71 ms en los dos sistemas. Las razones entre modos de Wild son comparables, porque los brazos van intercalados, pero las cifras absolutas no. La enmienda 4 lo repitió en lenovo, sin carga, con btrfs y ext4 en dos imágenes loop del mismo SSD para que solo cambiara el sistema de archivos.
En lenovo la confirmación del modo se repite: búfer forzado y por defecto quedan iguales en btrfs (1,00, IC 0,98 a 1,01). El control positivo no: en ext4, forzar el búfer costó apenas 1,04 veces (IC 1,01 a 1,06). En las dos máquinas se cumplió una condición de dos, así que lo que puedo decir es que el modo de escritura es consistente con la penalización, no que la explique. Escribir desde un búfer no es caro en sí; es caro en btrfs.
Lo que no esperaba: forzar mmap en btrfs hizo a Wild más rápido en las dos máquinas. En fedora quedó en 0,63 veces su tiempo por defecto (IC 0,61 a 0,67), y con hugepages en 0,62. En lenovo, 0,82 (IC 0,82 a 0,84), y ahí con mmap Wild en btrfs quedó casi igual que en ext4: 43,8 contra 42,0 ms. En fedora no alcanzó: 32,1 ms en btrfs con mmap contra 24,2 en ext4. El modo que Wild elige para protegerse de btrfs, en este banco y con el kernel 6.18, le cuesta.
Y un matiz que corrige el titular para el notebook: en lenovo, mold no perdió nada en btrfs (79,0 contra 78,9 ms en ext4), y Wild le ganó en los dos sistemas, 1,49 veces en btrfs. Que btrfs invierta el orden entre los dos enlazadores pasó en fedora, con 8 núcleos rápidos y un NVMe, no en el notebook.
En debug, el enlace sí pesa
En septiembre la conclusión fue que en release, con rust-lld por defecto, el enlace ya no era el cuello de botella: 2 a 5 % del rebuild. En debug la historia cambia, porque el enlazador mueve toda la información de depuración. Parte del enlace en el rebuild (mediana del enlace sobre mediana del rebuild, target en btrfs):
| parte del enlace | GNU ld | rust-lld | mold 3.0.0 | Wild 0.11.0 |
|---|---|---|---|---|
| cargo | 83 % | 26 % | 16 % | 26 % |
| rust-analyzer | 94 % | 49 % | 38 % | 44 % |
| bevy (breakout) | 87 % | 96 % | 54 % | 49 % |
En release, con ripgrep y cargo, la misma proporción fue del 1 al 4 % salvo con GNU ld.
Barra entera: el rebuild completo, enlace incluido
- rust-lld, el que viene por defecto96 %
- GNU ld87 %
- mold 3.0.054 %
- Wild 0.11.049 %
Medianas de 20 rebuilds por enlazador, octubre de 2026. El binario de debug pesa unos 2 GB.
El caso extremo es rust-lld con bevy: 108,7 s de mediana dentro del build, contra 5,2 s de mold y 6,9 s de Wild. Lo traté como bug hasta reproducirlo. Repitiendo el enlace guardado, rust-lld tardó 37,4 s con la copia en btrfs y 1,9 s en ext4. En ext4, Wild fue el más rápido: 0,91 s, contra 1,13 de mold. En btrfs, mold 1,42 s y Wild 2,16 s.
La diferencia entre los 108,7 s del build y los 37,4 s del replay en btrfs no la tengo explicada del todo. Dentro del build, la salida vieja de 2 GB existe y se reemplaza, y los 5,5 GB de insumos no siempre están en caché; el replay borra la salida antes y lee insumos calientes. Son dos condiciones distintas, y no aislé cuál pesa.
Si compilas proyectos grandes en debug sobre btrfs con el enlazador por defecto, mira cuánto tarda el enlace. A mí me tomaba casi dos minutos de cada rebuild.
Hilos: el paralelismo explica buena parte
Con un solo hilo (--threads=1), mold tarda 72,8 ms en ripgrep y Wild 85,1; en cargo, 159,3 contra 171,5. Con 8 hilos los dos bajan entre 2 y 3 veces, y la ventaja de cada uno sobre el otro depende en buena parte de cómo reparte el trabajo entre hilos. Con uno solo, mold queda adelante en los dos proyectos.
La sospecha de septiembre era que la brecha cambiaba con 16 CPU visibles en vez de 8. En ripgrep, mold pasó de 35,9 a 26,3 ms y Wild casi no se movió (34,6 a 33,9). En cargo los dos bajaron parejo, de 55 a 49 ms. Hay un problema con ese experimento: mold en ripgrep dio 22 ms en los otros dos experimentos de btrfs y 36 ms aquí con 8 CPU, sin efecto de la posición en la rotación ni del brazo anterior. Es una deriva entre experimentos que no sé explicar, y por eso todas las comparaciones de este post son dentro de un mismo experimento.
Memoria: una medición que falló y otra que sirve
La ficha planeaba medir el pico de memoria con /usr/bin/time -f %M. Para GNU ld y rust-lld funciona. Para mold y Wild dio entre 5 y 11 MiB en todos los proyectos, lo que es imposible: los dos se bifurcan y el trabajo lo hace un proceso hijo que time no mide. Lo publico como medición fallida.
La enmienda 2 la reemplazó por el pico de memoria del cgroup completo, que sí incluye a los hijos, repitiendo cada enlace guardado 3 veces. Ojo: ese pico también cuenta la caché de páginas del archivo de salida, así que no es memoria anónima.
| pico del cgroup, MiB | GNU ld | rust-lld | mold 3.0.0 | Wild 0.11.0 |
|---|---|---|---|---|
| rust-analyzer debug | 1946 | 877 | 925 | 1126 |
| bevy debug | 6246 | 3379 | 3482 | 5018 |
| cargo release | 345 | 142 | 201 | 123 |
| ripgrep release | 115 | 63 | 118 | 86 |
En debug, Wild usa más memoria que mold y rust-lld: en bevy, 5 GB contra 3,4 a 3,5 GB. En release el orden depende del proyecto: Wild es el más bajo en cargo y rust-lld en ripgrep. Estas pasadas fueron en btrfs, donde Wild arma la salida entera en memoria antes de escribirla. Eso es consistente con su pico en debug, pero no medí la memoria en ext4, así que no puedo separar las dos cosas.
Controles
- Los 6 ripgrep enlazados corren y dan la misma búsqueda, con salida idéntica entre brazos, y cada binario lleva la firma de su enlazador en
.comment(GNU ld no firma). - El brazo saboteado, con un enlazador que sale con error, dio fallo en sus 3 filas.
- Descarté 160 de 240 filas de una pasada de debug: el filtro de rust-analyzer y bevy buscaba un archivo con guion y el ejemplo de bevy se había movido de carpeta. Las repetí limpias en otra pasada.
- Ninguna fila de los experimentos cronometrados de la ficha original corrió con el otro banco activo. E8 en fedora sí, y está marcado fila por fila.
Lo que estos números no dicen
- El sistema de archivos no es la única diferencia con septiembre. También cambiaron el kernel, la distribución y rustc. Lo que E6 aísla es el sistema de archivos dentro del sistema nuevo.
- El código de Wild cambia de modo en btrfs según el kernel. Con un kernel 7.2 o más nuevo, Wild usa
mmaptambién en btrfs, y no lo medí: mi kernel es 6.18. - La imagen ext4 es un archivo loop dentro del mismo NVMe, no una partición. En el replay, la copia de btrfs vive en un subvolumen comprimido.
- Los fallos de página y el
futexacompañan al tiempo. No probé que lo causen. - E8 en fedora corrió con carga de otro banco; su réplica en lenovo, sin carga, usa imágenes loop en un SSD SATA y una CPU de 4 núcleos. Las dos coinciden en la dirección, no en el tamaño.
- Las cifras absolutas derivan entre experimentos en btrfs (mold en ripgrep: 22 ms en uno, 36 ms en otro). Las razones dentro de un experimento son lo que publico.
- El tiempo es de pared hasta que el enlazador devuelve el control. mold y Wild terminan su limpieza en un proceso hijo que este reloj no ve, igual que en septiembre.
- Una máquina de escritorio, un notebook, cuatro proyectos.
Mi opinión
Lo bueno:
- La ventaja de Wild sobre mold que medí en septiembre sigue ahí con target en ext4 o tmpfs, y en el notebook es de 1,4 a 2,2 veces.
- Wild documenta su problema con btrfs en el código y deja elegir el modo de escritura con una opción. Eso me permitió probarlo en vez de adivinar.
- En debug, cualquiera de los dos le saca mucho a rust-lld con binarios grandes.
Lo que no:
- El resultado de un banco de enlazadores depende de dónde está
target, y casi nadie lo anota. Yo no lo anoté en septiembre: decía “NVMe con ext4” y nada más. - En mi banco, el modo que Wild elige para btrfs con kernel 6.18 tarda 1,6 veces lo que tarda con
mmap. - rust-lld con un binario de 2 GB en btrfs llegó a casi dos minutos por rebuild, y nada avisa.
Conclusión: antes de elegir enlazador por un benchmark, mira en qué sistema de archivos vive tu target. En ext4 y tmpfs ganó Wild. En btrfs, en mi escritorio, ganó mold en ripgrep, y en cargo el resultado cambió de un experimento a otro; en el notebook siguió ganando Wild.
Cuándo elegiría cada uno
- Wild, con target en ext4 o tmpfs. Fue el más rápido en todo lo que medí ahí, en las dos máquinas.
- mold, con target en btrfs, un kernel anterior a 7.2 y una máquina como mi escritorio, si no quiero tocar opciones. En el notebook ni eso: Wild ganó también en btrfs.
- Wild con
--mmap-output-fileen btrfs con kernel anterior a 7.2. En las dos máquinas le recortó entre 18 y 37 % del tiempo. Lo mediría en la mía antes de dejarlo fijo. - Mover
targetfuera de btrfs (a tmpfs, o a un volumen ext4) antes que cambiar de enlazador, si el enlace domina el ciclo. Es lo que más movió los números de este post. - Cambiar rust-lld por mold o Wild en debug con binarios grandes. En release y con proyectos como ripgrep o cargo, el enlace sigue siendo del 1 al 4 % del rebuild y no cambiaría nada.
Fuentes
- Wild le gana a mold enlazando Rust, el post del 18 de septiembre con el banco que repetí.
- Wild 0.11.0, notas de versión: COMDAT, tope de 32 hilos, fallocate y hugepages en ext4/xfs, mmap en btrfs con kernel nuevo.
- Wild, issue #1499 “BTRFS write performance” y PR #2509, con el cambio de modo según el kernel en
libwild/src/fs.rs. - Wild, BENCHMARKING.md, el método de guardar un enlace y repetirlo con
run-with. - mold 3.0.0, notas de versión: la reescritura en Rust y el rendimiento a la par de 2.42.1.
- Los scripts, la ficha con sus cuatro enmiendas, los CSV y el análisis:
lab/linkers2/en el repositorio y en este gist. - ripgrep, cargo, rust-analyzer y bevy, los proyectos enlazados.
Medido entre el 25 y el 27 de octubre de 2026 en fedora (AMD Ryzen 7 7800X3D, NVMe, NixOS, kernel 6.18.55, /data en btrfs con compress=zstd:1) y en lenovo (Intel i5-8250U, SSD SATA, NixOS, kernel 6.18.55, ext4). rustc 1.99.1, GNU ld 2.46, Wild 0.10.0 y 0.11.0, mold 2.42.1 y 3.0.0, rust-lld de la toolchain. ripgrep 3fce3b5, cargo 8814ead, rust-analyzer 0c4d746, bevy fd9e760.
BOLETÍN · MEDIDO
Una medición nueva, cuando la hay.
Cada post con sus números, sus fallos y los scripts para repetirlo. Entre uno y tres correos al mes. Te das de baja con un clic.
LO ÚLTIMO QUE LLEGÓ
- Unlimited-OCR 2 en 16 GBempate con Chandra 2, sin memoria a las 80 páginas
- Wild vs mold en btrfsel orden se invierte según el disco
- 7 arneses con MiniMax-M329 de 39 con el arnés más simple
Uso Listmonk en mi propio servidor; tu correo no sale de ahí. Confirmas por correo antes de recibir nada.
Comentarios
Todavía no hay comentarios. El primero es tuyo.