17°
Portada del artículo: Wild le gana a mold enlazando Rust en 20 de 20, y en release el enlazador ya no es el cuello de botella
RustEnlazadoresBenchmarksRendimiento

Wild le gana a mold enlazando Rust en 20 de 20, y en release el enlazador ya no es el cuello de botella

Medí Wild 0.10.0, mold 2.42.1, rust-lld y GNU ld enlazando ripgrep y cargo. Wild ganó las 20 repeticiones de cada proyecto, por 1,9 ms y unos 8 ms. Con cualquiera de los dos el enlace es cerca del 1 % del rebuild. Y por el camino casi publico números falsos dos veces.

Efrain Garay 18 de septiembre de 2026

Reproduciendo el resumen

El 18 de septiembre David Lattimore, el autor de Wild, publicó su propio banco de Wild contra mold. Ese mismo día yo tenía los dos enlazadores descargados y dos proyectos de Rust de verdad para enlazar: ripgrep y el propio cargo. Quería saber cuál me conviene en mi máquina, y de paso cuánto me estoy perdiendo por seguir con el enlazador que trae rustc.

Wild fue más rápido que mold en las 20 repeticiones de cada proyecto. La ventaja es de 1,9 ms en ripgrep y de unos 8 ms en cargo, en un ciclo donde el resto del rebuild, sin el enlace, tarda entre 2,2 y 2,6 s. Con cualquiera de los dos, el enlace es cerca del 1 % del rebuild. Y antes de llegar a esos números estuve a punto de publicar otros, falsos, dos veces.

En 71 segundos y narrado: Wild contra mold enlazando ripgrep y cargo, con la mediana de 20 repeticiones. Wild ganó las 20 de cada proyecto, por 1,9 ms en ripgrep y unos 8 ms en cargo. Con rust-lld como enlazador por defecto, el enlace es de 2 a 5 % de un rebuild en release de 2,2 a 2,6 s, y cerca de 1 % con mold o Wild. Incluye el fallo de mi banco: un enlazador falso pasado con -B nunca se ejecutó. Sin sonido por defecto: actívalo en los controles.Verlo en el visualizador de reels →

Qué medí

Un enlazador toma los objetos que produce el compilador y los junta en un ejecutable. En Rust ese paso corre al final de cada build del binario, y como las dependencias se enlazan estáticamente, el archivo que escribe es grande: los dos de este post pesan 30 y 39 MB. mold es el enlazador del autor original de LLD. Wild apunta a ser muy rápido en el desarrollo iterativo.

Cuatro enlazadores sobre el mismo código:

  • GNU ld (bfd) 2.45.1, el histórico.
  • rust-lld, que es LLD 22.1.8, el que viene en el sysroot de rustc. Es el enlazador por defecto de rustc 1.98 en el target x86_64-unknown-linux-gnu, que es el mío. Lo verifiqué: sin ningún flag, el binario sale firmado Linker: LLD 22.1.8. El blog de Rust anunció el cambio para ese target; no revisé otros, como musl.
  • mold 2.42.1.
  • Wild 0.10.0.

Dos proyectos. ripgrep 15.2.0 en el commit 3fce3b5, con el perfil release de upstream, que lleva debug = 1: sale un binario de 30 MB con DWARF. Y cargo en el commit 8814ead, release sin DWARF, 39 MB. Ojo con leerlos como el mismo caso en dos tamaños: uno carga información de depuración y el otro no.

La máquina: Fedora 43 con kernel 7.2.4, un AMD Ryzen 7 7800X3D de 8 núcleos y 16 hilos, NVMe con ext4. rustc y cargo 1.98.0, clang 21.1.8. El banco corre con 8 núcleos físicos visibles y un tope de memoria:

systemd-run --user --scope -p MemoryMax=12G taskset -c 0-7 ./bench3.sh

Comprobé que nproc diera 8 ahí dentro. AllowedCPUs no sirve en mi sesión: systemd lo acepta y lo ignora, porque el controlador cpuset no está delegado al usuario. El governor quedó en powersave (amd-pstate-epp, balance_performance). No pude cambiarlo sin sudo.

Instalación, y la primera trampa

Instalar fue lo fácil. mold y Wild publican binarios, e instalar cada uno tomó menos de 2 s. Lo difícil fue lograr que rustc los usara de verdad.

Probé tres formas de pedirle otro enlazador. Para saber cuál mandaba usé un truco que ya me ha salvado antes: un ld falso que lo único que hace es salir con código 7. Si el build pasa con ese enlazador puesto, es que nadie lo llamó.

El sabotaje: un enlazador falso que sale con 7, por tres entradasSi el build sobrevive a un enlazador que siempre falla, a ese enlazador nunca lo llamaron. Elige una pestaña; cada una se repite en bucle.
RUSTFLAGS-C link-arg=-fuse-ld=<ruta absoluta al ld falso>
ccerror: unrecognized command-line option '-fuse-ld=/…/shim/falso/ld'
Falla y avisa. Molesta, pero es honesto.
RUSTFLAGS-C link-arg=-B<dir con un ld falso que sale con 7>
rustc → cc-B<sysroot>/…/gcc-ld -fuse-ld=lld -B<mi dir>
cc buscald.lld: el dir de rustc se busca primero (con un ld.lld falso en el mío: igual)
cargoFinished `release` profile [optimized + debuginfo] · exit=0
.commentLinker: LLD 22.1.8
El build pasa y el falso nunca corrió, con ld o con ld.lld. Lo que hubiera cronometrado aquí era rust-lld.
RUSTFLAGS-Clinker=clang -Clink-arg=--ld-path=shim/crono-sabotaje/ld
clangshim → cronómetro → ld falso
stderrENLAZADOR FALSO INVOCADO
logcódigo de salida 7
cargoerror: linking with `clang` failed: exit status: 7
El build se rompe. Este camino sí ejecuta lo que le paso, y con este medí.

Líneas tomadas de sabotaje_B_fuse.txt y sabotaje3.txt, salvo la fila «clang», que es mi lectura. La prueba del ld.lld falso y el orden de búsqueda son las secciones 4 y 5 de sabotaje_B_fuse.txt.

-C link-arg=-fuse-ld=/ruta/absoluta falla de inmediato: cc responde “unrecognized command-line option”. Molesta, pero avisa.

-C link-arg=-B<dir> es la peligrosa. El build pasa y el enlazador alternativo nunca se ejecuta. Lo probé de dos maneras. Con un ld falso dentro de ese directorio el build terminó bien y el binario salió firmado LLD 22.1.8. Podía ser por el nombre: con -fuse-ld=lld, cc busca un programa llamado ld.lld, no ld. Así que puse en el directorio un falso llamado ld.lld. Mismo resultado: código de salida 0 y el falso nunca se ejecutó.

cc sí respeta -B. Preguntándole con cc -print-prog-name=ld.lld, encuentra el falso si mi directorio va solo o va primero. Lo que lo hunde es el orden: rustc le pasa a cc su propio -B<sysroot>/.../gcc-ld antes de todo lo que uno agregue, y gana el primer directorio que tenga un ld.lld. El mío queda segundo.

El README de Wild lista -B <dir> entre sus opciones de uso general, y cc lo respeta. Agregado por RUSTFLAGS con rustc 1.98 y el driver por defecto, queda en segundo lugar.

Si hubiera medido así, cada serie habría sido rust-lld con otra etiqueta encima.

Lo que sí manda es -Clinker=clang -Clink-arg=--ld-path=<ruta>, que es lo que documenta el README de Wild. Hay que instalar clang, 65 MB. Con esa forma, el mismo ld falso rompe el build: “linking with clang failed: exit status: 7”.

Hay una cuarta forma que no probé: -fuse-ld=mold por nombre. Es lo que documenta mold para GCC 12.1.0 o posterior y Wild para GCC 16.1 o posterior, y exige tener el enlazador en el PATH. Si la usas, el mismo ld falso te dice enseguida si manda.

Quién elige el enlazador: el carril que medí y los dos que mentíanLos tres salen de rustc. Con clang y --ld-path corre mi shim. Con el driver por defecto, el -B de rustc va primero y el mío pierde. Con clang y sin --ld-path, enlaza GNU ld.
Quién elige el enlazador: el carril que medí y los dos que mentíanmedido: manda --ld-pathtrampa: mi -B queda segundocasi publicado como LLD: clang sin --ld-pathrustc 1.98.0cargo build --releaseuna línea a main.rsclang 21.1.8--ld-path=shim/ldshim + cronómetroEPOCHREALTIME, µsregistra código y -oenlazador realGNU ld · rust-lldmold · Wildbinarioreadelf -p .commentcc (por defecto)-B<sysroot>/gcc-ld-fuse-ld=lldld.lld del sysrootrust-lld · LLD 22.1.8el build pasald falso, sale con 7nunca se ejecutaclang 21.1.8-Clinker=clangsin --ld-path/usr/bin/ldGNU ld (bfd) 2.45.1mi CSV decía lldbinariosin firma de LLDmi -B<dir>Quién elige el enlazador: el carril que medí y los dos que mentíanrustc 1.98.0cargo build --releaseuna línea a main.rsmedido-B, segundoclang 21.1.8--ld-path=shim/ldshim + cronómetroEPOCHREALTIME, µsregistra código y -oenlazador realGNU ld · rust-lldmold · Wildbinarioreadelf -p .commentcc (por defecto)-B<sysroot>/gcc-ld-fuse-ld=lldld.lld = rust-lldLLD 22.1.8el build pasald falso, sale con 7nunca se ejecutami -B<dir>casi publicado como LLDclang 21.1.8-Clinker=clangsin --ld-path/usr/bin/ldGNU ld 2.45.1sin firma de LLD

El único carril donde consta que corrió el enlazador alternativo es el primero: ahí un enlazador falso que sale con 7 rompe el build, y en el segundo pasa sin que nadie lo note. En el tercero el build también pasa, y solo lo delata la firma que falta.

El primer banco traía dos errores más

Con --ld-path resuelto armé un primer banco. Traía dos errores, y los dos daban números creíbles.

La línea base estaba mal etiquetada. Para la serie de referencia usé -Clinker=clang sin --ld-path, convencido de que eso era LLD. Con clang como driver y sin --ld-path, clang usa /usr/bin/ld, que es GNU ld. rustc solo agrega su -fuse-ld=lld cuando el driver es el cc por defecto; al pedirle -Clinker=clang, deja de hacerlo. Se comprueba con clang -print-prog-name=ld. Mi CSV decía “lld” y el titular que salía de ahí, mold sacándole más de un segundo a LLD, era falso. Lo descubrí al revisar los binarios con readelf: el de esa serie no traía la firma de LLD.

Y el reloj medía otra cosa. Cronometraba cargo build entero. Eso es un rebuild incremental: rustc recompilando el crate y, al final, el enlace. Los porcentajes que salían de ahí eran porcentajes del rebuild. Ese banco además decía que Wild y mold empataban. Era ruido: un reloj de centésimas de segundo no separa dos enlaces que se llevan 2 ms.

No es la primera vez que me pasa. Cuando medí el SIMD de Go contra NumPy también estuve a punto de publicar dos comparaciones falsas.

Ese primer banco no quedó del todo inútil. Sus diferencias absolutas sirven de validación cruzada, porque salen de un método de cronometraje distinto. No es una réplica independiente: misma máquina, mismos proyectos. Cronometrando cargo build entero, GNU ld menos mold daba 0,34 s en ripgrep y 1,31 s en cargo. El banco definitivo, midiendo solo el enlazador, da 0,33 s y 1,3 s. Con las medianas sin redondear, coinciden dentro de un 2 %.

Entre ese banco y el definitivo hubo uno intermedio. Ya cronometraba solo el enlazador, pero con un reloj más grueso y sin límite de CPU. De él solo cito una salvedad, más abajo.

Cómo quedó el banco definitivo

El enlazador se elige con RUSTFLAGS:

RUSTFLAGS="-Clinker=clang -Clink-arg=--ld-path=$SHIM" cargo build --release

$SHIM es un script mínimo, uno por enlazador. Ejecuta un cronómetro alrededor del enlazador real y registra el argumento de -o, para saber qué archivo se estaba enlazando. Este es el de rust-lld, con la ruta del sysroot abreviada:

#!/usr/bin/env bash
# shim/crono-lld/ld: clang runs this instead of a linker
exec "$CRONO_BIN" <sysroot>/lib/rustlib/x86_64-unknown-linux-gnu/bin/rust-lld -flavor gnu "$@"

rust-lld necesita -flavor gnu para comportarse como ld. El shim de GNU ld apunta a /usr/bin/ld.bfd, y los de mold y Wild, a sus binarios. El cronómetro es este:

REAL="$1"; shift
out=NA; prev=
for a in "$@"; do
  [ "$prev" = "-o" ] && out=$a
  prev=$a
done
# EPOCHREALTIME uses the locale's decimal separator (a comma under es_CL).
INI=${EPOCHREALTIME/[.,]/}
"$REAL" "$@"
COD=$?
FIN=${EPOCHREALTIME/[.,]/}
printf '%s\t%s\t%s\t%s\t%s\t%s\n' "$CRONO_TAG" "$INI" "$((FIN - INI))" "$COD" "$REAL" "$out" >> "$CRONO_LOG"
exit $COD

El reloj es EPOCHREALTIME de bash: microsegundos y sin ningún fork de por medio. Las cuatro series pasan por clang y por el shim, también la de rust-lld. El camino por defecto de rustc, con cc y su propio ld.lld, no lo cronometré.

En cada medida agrego una línea a main.rs (crates/core/main.rs en ripgrep, src/bin/cargo/main.rs en cargo) y reconstruyo en release, igual que cuando medí las cifras de Bun cambiando solo el binario: entre una corrida y otra lo único que cambia es quién enlaza. En cada build medido hubo exactamente una invocación del enlazador, la del ejecutable (deps/rg-<hash>, deps/cargo-<hash>). Fueron 160 builds y ningún build_script se coló en la cuenta.

Son 20 repeticiones por serie y proyecto, con las series intercaladas y el orden rotando en cada repetición. Las 160 terminaron con código 0. La identidad de cada enlace la da la firma del binario, salvo en GNU ld, que no firma: ahí descansa en el log de invocaciones. Los 8 binarios corren (rg --version, cargo --version). Y el sabotaje se repitió detrás del cronómetro nuevo, para comprobar que el shim tampoco se traga un fallo. Los intervalos de confianza que siguen son bootstrap percentil sobre las 20 repeticiones.

Los números

Medianas de 20 repeticiones, a tres cifras significativas:

GNU ldrust-lldmoldWild
ripgrep352 ms42,6 ms18,3 ms16,3 ms
cargo1362 ms121 ms35,7 ms27,9 ms
Los cuatro enlazadores, cada carril al tiempo que medíMediana de 20 repeticiones del enlace del ejecutable. Las pestañas sin GNU ld cambian la escala para que se vea la pelea de abajo.
GNU ld352 ms
rust-lld42,6 ms
mold18,3 ms
Wild16,3 ms

Diez veces más lento que lo medido. Binario de 30 MB con DWARF.

rust-lld42,6 ms
mold18,3 ms
Wild16,3 ms

Cien veces más lento que lo medido.

GNU ld1362 ms
rust-lld121 ms
mold35,7 ms
Wild27,9 ms

Tres veces más lento que lo medido. Binario de 39 MB sin DWARF.

rust-lld121 ms
mold35,7 ms
Wild27,9 ms

Cuarenta veces más lento que lo medido.

Tiempo de pared hasta que el enlazador devuelve el control. mold y Wild terminan su limpieza en un proceso hijo que este reloj no ve.

Cómo leer esto

Wild contra mold. Wild fue más rápido en las 20 repeticiones de cada proyecto. En ripgrep son 1,9 ms menos (IC 95 %: 1,7–2,3 ms), y mold tarda 1,12× lo de Wild (IC 1,10–1,14). En cargo son unos 8 ms menos y 1,28× (IC 1,27–1,31), con distribuciones que no se solapan. La ventaja existe y es consistente. También es chiquita.

rust-lld contra los dos. rust-lld tarda 2,3× lo de mold en ripgrep y 3,4× en cargo. Contra Wild, 2,6× y 4,3–4,4×. Suena a mucho hasta que se pasa a milisegundos: cambiar rust-lld por mold o Wild ahorra 24–26 ms en ripgrep y 85–93 ms en cargo.

GNU ld. Tarda 8,3× y 11,2× lo de rust-lld. Aquí la diferencia sí se nota, pero es una referencia histórica. En mi configuración aparece al forzar -Clinker=clang sin --ld-path, que fue justo mi error. Y sigue tocando donde rust-lld no es el default, o si uno lo desactiva con -C linker-features=-lld: ahí rustc usa el enlazador del sistema.

La brecha entre mold y Wild es más ancha en cargo que en ripgrep. No sé por qué, y con un proyecto por régimen no tengo cómo atribuirlo: cambian a la vez el tamaño, el DWARF y el código.

En release, el enlace ya no es el cuello de botella

Para quien compila en x86_64-unknown-linux-gnu con Rust 1.90 o más nuevo y la configuración de enlazador por defecto, la línea base que importa es rust-lld, porque es lo que ya tiene sin tocar nada. Y contra esa línea base la pregunta cambia de “cuál es más rápido” a “cuánto del ciclo es enlace”.

Al tocar main.rs en release, entre el fin de un enlace y el inicio del siguiente pasan entre 2,2 s (ripgrep) y 2,6 s (cargo): medianas de 2,18 s y 2,55 s sobre 79 intervalos cada una. Ahí está rustc recompilando el crate, junto con el arranque de cargo y la contabilidad de mi banco, que no separé. El enlace viene después.

Qué parte del rebuild es enlaceCada barra entera es un rebuild en release tras tocar main.rs. La franja de color, a escala, es lo que se lleva el enlazador.

Barra entera: el resto del rebuild, sin el enlace, 2,2–2,6 s

  1. Enlace con rust-lld, el que viene por defecto2–5 %
  2. Enlace con mold o con Wildcerca del 1 %
  3. Ventaja de Wild sobre mold, en ripgrep0,09 %
  4. Ventaja de Wild sobre mold, en cargo0,3 %Las dos últimas franjas casi no se ven, y están dibujadas con un mínimo de un píxel. Ese es el hallazgo.

Proporciones del banco del 18 de septiembre de 2026, perfil release. En el perfil dev no medí nada.

En un rebuild que dura más de dos segundos, 2 ms son irrelevantes.

El salto grande ya ocurrió, y lo dio rustc al traer rust-lld por defecto: en cargo, de 1362 ms con GNU ld a 121 ms. Lo que queda por ganar cambiando de enlazador son decenas de milisegundos sobre un ciclo que sigue durando más de dos segundos fuera del enlace.

Lo que estos números no dicen

  • Medí tiempo de pared hasta que el enlazador devuelve el control. mold y Wild bifurcan por defecto (--fork) y terminan su limpieza en un proceso hijo que mi reloj no ve. rust-lld y GNU ld no hacen eso. Es el tiempo que espera el usuario. El trabajo total es mayor, y no medí cuánto.
  • Es una máquina, una sesión de 7 minutos, dos proyectos y el perfil release. Nada de esto se traslada al perfil dev, que no medí.
  • La brecha entre mold y Wild depende de las condiciones. El banco intermedio, sin límite de CPU y con condiciones que no registré, dio 1,31× y 1,52× en vez de 1,12× y 1,28×. Mi sospecha son los 16 hilos visibles contra 8 núcleos, pero no lo comprobé. Por eso la tabla y todas las razones de este post salen de un solo banco, el que tiene el entorno anotado.
  • Los 2,2–2,6 s fuera del enlace son el hueco entre dos enlaces seguidos. No son rustc solo: incluyen el arranque de cargo y lo que hace mi script entre un build y otro, y no medí a rustc por separado.
  • El cronómetro suma un costo fijo de unos 0,9–1,0 ms por enlace. Lo medí 4 veces con un enlazador que no hace nada. Eso aplasta las razones hacia 1: las que publico son conservadoras.
  • No digo nada sobre memoria. El primer banco registraba un pico de memoria, pero ese pico era el de rustc.
  • rust-lld en ripgrep muestra dos modos, cerca de 38,5 y de 42,7 ms. No investigué la causa.
  • La rotación de series es cíclica: en 15 de las 20 repeticiones cada serie tiene el mismo predecesor. No detecto efecto de posición (Kruskal-Wallis, p ≥ 0,06), pero con 5 medidas por posición solo puedo descartar efectos grandes.

Depende de la configuración, y lo dice el propio autor de Wild

Lo que sigue lo leí en el post de Lattimore. Yo no corrí esos proyectos. Ninguno es de Rust, su máquina es otra, y él midió builds propios de los dos enlazadores al 28 de agosto de 2026, que no son necesariamente las versiones publicadas que medí yo.

Su conclusión es que el resultado depende de la configuración. Con salida nueva en ext4 y --no-fork, Wild tarda 2,23 s contra 1,79 s de mold en blender-debug, y 1,11 s contra 0,89 s en godot-debug: mold gana por 1,2×. En clang-release pasan de empate (Wild 0,21 s, mold 0,20 s) a Wild tardando 0,6× lo de mold (Wild 0,11 s, mold 0,19 s) cuando se enlaza en tmpfs, sobre una salida que ya existe y con fork. También dice que a la versión de Wild que midió le faltan fallocate y hugepages para crear archivos rápido fuera de tmpfs, y que las dos cosas ya están hechas y vienen en la próxima versión.

Mi caso es ext4, fork por defecto en los dos enlazadores y binarios de Rust de 30–39 MB. Es un solo punto de ese mapa. Que en mi punto gane Wild y en los suyos de depuración gane mold no es una contradicción: son proyectos de otro tamaño, con otra forma de escribir la salida.

Mi opinión

Lo bueno:

  • Wild 0.10.0 le gana a mold 2.42.1 en todo lo que medí, con intervalos que no tocan el 1.
  • Instalar cualquiera de los dos tomó menos de 2 s, y los dos firman el binario. Eso permite verificar, en vez de confiar.
  • rust-lld por defecto ya entrega casi toda la mejora sin configurar nada.

Lo que no:

  • Elegir el enlazador desde Rust tiene una forma que falla en silencio. -B deja pasar el build y enlaza con otro, porque rustc pone su propio -B primero. Sin el sabotaje no me habría enterado.
  • La única forma que comprobé, --ld-path, pide instalar clang y cambiar el driver de enlace de todo el proyecto.
  • La ganancia sobre rust-lld, en mis dos proyectos y en release, es pasar de un enlace que ocupa el 2–5 % del ciclo a uno que ocupa cerca del 1 %.

Conclusión: si tu rustc ya enlaza con rust-lld, en release y con proyectos como estos dos, el tiempo que te queda por recuperar está fuera del enlazador.

Cuándo cambiaría de enlazador y cuándo no

  • Cambiaría si mis binarios siguieran saliendo de GNU ld. Se comprueba en una línea con readelf -p .comment: si no aparece ninguna firma de enlazador, conviene revisar.
  • Cambiaría si el enlace dominara mi ciclo. No es mi caso con estos dos proyectos, y no medí ninguno donde lo sea.
  • Probaría Wild antes que mold en una máquina parecida a la mía, con ext4 y fork por defecto. Con binarios de depuración grandes miraría primero la tabla de Lattimore, y repetiría la medida cuando salga la versión de Wild con fallocate.
  • No cambiaría con rustc 1.98 en x86_64 Linux y un rebuild que pasa más de dos segundos fuera del enlace. Ahí me quedo con rust-lld: cero configuración, y la diferencia es de decenas de milisegundos.
  • Y en cualquier caso, antes de creerle a un banco de enlazadores, le pondría un enlazador falso que salga con 7 y miraría la firma del binario. A mí lo primero me evitó medir cuatro series que habrían sido todas rust-lld, y lo segundo, un titular falso. Ninguna de las dos cosas me avisó de que el reloj medía el rebuild entero.

Fuentes

Comentarios

Todavía no hay comentarios. El primero es tuyo.

Se revisa antes de publicarse. El correo no se guarda ni aparece en ninguna parte.