Me gusta Rust desde hace mucho tiempo; sus ventajas son evidentes y sus desventajas también lo son. Ofrece seguridad de memoria sin GC, 零成本抽象, 并发安全, probablemente la mejor 工具链 y 配套外围建设 disponible actualmente, una cobertura enorme (desde servidores Web en la nube hasta microcontroladores), y su diseño de manejo de errores es excelente. En cuanto a sus desventajas: ¿una curva de aprendizaje pronunciada? Dificultad para comprender Ownership, Borrow y restricciones de modelo similares; y escribir código resulta lento.
Durante mucho tiempo hemos considerado esto como una desventaja de Rust, pero en realidad los dos primeros problemas se pueden resolver entrenando el pensamiento y la cognición. En cambio, la lentitud de la compilación ha estado frenando continuamente mi flujo de trabajo. Hace algunos años parecía estar bien; antes de que la IA se apoderara de todo, tenía paciencia y a menudo escribía un algoritmo o una función hasta altas horas de la madrugada. ¿Y ahora? Me cuesta decir que no he cambiado; he perdido toda la paciencia, no quiero pensar cómo escribir código Rust y no puedo esperar la larga barra de progreso de Cargo, sobre todo cuando la fase ld no muestra progreso y se queda atascada, lo cual es realmente incómodo.
¿Acaso no hay ninguna solución para todo esto? En realidad sí la hay. Hace aproximadamente un mes, probé algunos métodos para acelerar mi proceso de desarrollo en Rust, incluyendo la optimización del flujo de trabajo y cosas por el estilo, pero en realidad el tiempo de espera siempre se quedaba atascado en Cargo. Si no se resuelve esto, no se ataca la raíz del problema, así que aprovecharé esta oportunidad para hablar un poco sobre la optimización de la configuración de Cargo.toml en el desarrollo de Rust y sus configuraciones periféricas complementarias.
Teoría del backend
En el diseño de Rust, LLVM es un poco holgazán, pero la elección es buena; a diferencia de Go, que mantiene su propio proceso de generación de código máquina, delegar en el amplio ecosistema de LLVM para aprovechar optimizaciones maduras funciona muy bien. El flujo completo de compilación de Rust se puede describir aproximadamente de la siguiente manera: el código fuente primero pasa por un análisis léxico y sintáctico que genera un AST, luego se realiza la expansión de macros y la degradación del HIR que lo aplanaana. Después se llevan a cabo las comprobaciones de tipos habituales y una ligera verificación de préstamos, generándose finalmente el MIR; hasta aquí, todo lo maneja el front‑end del compilador Rust. Lo que ocurre después del MIR se entrega a LLVM, por ejemplo, traduciendo MIR a LLVM IR, ejecutando su serie de pases de optimización (desde O0 hasta O3), y finalmente generando el código máquina para la plataforma objetivo, que luego el enlazador combina en un archivo ejecutable BIN.
¿Dónde está exactamente el problema entonces? La respuesta es en realidad muy sencilla: LLVM es una infraestructura muy pesada —y ser pesada es lo correcto en entornos de producción, ya que cambiar tiempo de compilación por tiempo de ejecución es una excelente filosofía—, y sirve de puente para el Rust de nivel superior, así como para un gran número de lenguajes como C, C++ y Swift. Esto significa que su tubería de optimización está diseñada para "generar el código máquina óptimo", y no para "completar la compilación rápidamente". Ejecutar esas docenas de pases de optimización en modo release consume muchísimo tiempo; incluso en modo debug con O0, LLVM todavía tiene que recorrer todo el proceso de generación de IR y generación de código máquina, y este proceso en sí no es nada ligero. A esto se suma que la monomorfización de genéricos en Rust despliega una gran cantidad de código en tiempo de compilación, por lo que el volumen de IR entregado a LLVM es mucho mayor de lo que parece en el código fuente, y la carga de trabajo que LLVM debe procesar aumenta naturalmente en consecuencia.
Cranelift
En esencia, gran parte del tiempo de compilación lenta no se dedica al front‑end propio de Rust sino al back‑end “pesado” de LLVM. Entonces surge la pregunta: ¿es posible eliminarlo? La respuesta es sí, miau. Existe un back‑end llamado Cranelift que se creó precisamente para esta necesidad. Comenzó como Cretonne, lanzado en 2016 y desarrollado por la Bytecode Alliance. Inicialmente se diseñó como el back‑end de generación de código del paquete Wasmtime; posteriormente, el equipo oficial de Rust lo adoptó como un back‑end de generación de código opcional.

Entonces, ¿dónde debería ser rápido Cranelift exactamente? La mejor etapa es durante la fase de desarrollo. En este punto, tal vez no necesites en absoluto un código máquina casi perfecto y optimizado al límite para la eficiencia de ejecución; tal vez solo necesitemos una retroalimentación rápida, siempre y cuando este código máquina no tan bueno sea semánticamente equivalente al código máquina generado adecuadamente por LLVM. En sus inicios, Cranelift no podía lograr esto realmente debido a la gran cantidad de Edge Cases. Aunque todavía existen algunos, ha mejorado muchísimo, y ahora la probabilidad de que Cranelift funcione pero LLVM falle es prácticamente la misma que la de provocar un rustc ICE al escribir código en Rust. Para generar un código máquina extremadamente optimizado, LLVM ejecuta docenas de pases de optimización, como desenrollado de bucles, vectorización, propagación de constantes, eliminación de código muerto y muchos, muchos más, puliendo capa por capa. La filosofía de diseño de Cranelift es completamente diferente: reduce drásticamente estos pasos de optimización, realizando solo la asignación de registros y la selección de instrucciones más básicas, y completando la generación de código en un solo escaneo lineal sin iteraciones repetidas. Al mismo tiempo, el diseño de su IR es más ligero, pensado específicamente para traducir rápidamente el IR de nivel superior a código máquina, a diferencia del IR de LLVM, que arrastra décadas de carga de generalización.
Así que, tras hablar de las ventajas, ¿cuál es el precio? En primer lugar, lo más evidente es que el código generado por Cranelift tiene un rendimiento en tiempo de ejecución inferior al de LLVM, siendo aproximadamente entre un 10 % y un 30 % más lento según el escenario. Pero si lo piensas fríamente, te darás cuenta de que esto en la fase de desarrollo no importa en absoluto. ¿Para qué necesito ir tan rápido? ¿Estás haciendo un benchmark o un CI release? Yo lo único que quiero es que cargo build vaya más rápido para ver si sale la lógica. Además, apuesto a que la mayoría de los programas que escribes no ocupan más del 80 % de los recursos de cómputo la mayor parte del tiempo; seguro que dependerán del tráfico de negocio que reciban, ¿y qué tan grande puede ser ese tráfico durante el desarrollo? ¡La CPU seguro que se pasa todo el tiempo tocándose las narices! Lo que yo quiero es ver los resultados y validar la lógica. Por tanto, intercambiar tiempo de compilación por eficiencia en tiempo de ejecución, ¿no es una cuenta que en el bucle de desarrollo deberíamos hacer al revés?
Unidades de generación de código
Hablemos también de codegen-units. De hecho, es un parámetro que controla cuántas unidades mínimas divide el compilador a un crate para que el backend las procese en paralelo. Por defecto, en modo debug son 256 unidades y en modo release son 16. Cuanto mayor es el número, mayor es el paralelismo y más rápida la compilación, ya que se pueden usar varios núcleos de CPU simultáneamente para la generación de código del backend. Pero el coste es una menor efectividad de la optimización, porque cada unidad se optimiza de forma independiente; LLVM (o Cranelift) ve un contexto más reducido, lo que reduce las oportunidades de inline y de optimización entre unidades.
Pero en una Release real, normalmente deberías usar codegen-units = 1, una opción extrema; solo así el producto puede lograr la máxima optimización. Después de todo, estás usando Rust, ¿no es lógico intercambiar tiempo de compilación por rendimiento en tiempo de ejecución?
Nivel de optimización
Otra configuración que suele pasarse por alto pero que se puede ajustar es opt-level. Por defecto debería ser "3", pero para Release, lo más habitual es ponerla en "z". Esta es una optimización gratuita del tamaño del artefacto sin modificar el código, así que ¿por qué no activarla? Sin embargo, en modo de desarrollo debe configurarse en "0", ya que solo sin ninguna optimización se consigue la máxima velocidad.
Además, hay un pequeño truco: Cargo.toml en realidad puede establecer diferentes niveles de optimización para las dependencias y tu propio código.
[profile.dev.package."*"]
opt-level = 3De esta manera, puedes compilar las dependencias de terceros con O3, lo que solo aumentará el tiempo de la primera compilación en frío, ya que las siguientes serán incrementales. Dado que las dependencias externas generalmente no se actualizan con frecuencia, usar O3 para ellas y O0 para tu propio código de negocio ofrece una buena experiencia en modo dev que equilibra velocidad y tamaño en circunstancias normales. Sin embargo, en el modo Release, sigo recomendando al máximo O3 o Z sin lugar a dudas.
Optimización en tiempo de enlace
LTO (Link‑Time Optimization) es otra característica que consume mucha memoria y rendimiento de compilación, y la mayoría de los proyectos recomiendan habilitarla en las compilaciones Release. En una compilación normal, cada crate se optimiza de forma independiente y el backend del compilador no ve las relaciones de llamada entre crates, lo que impide ciertos inlinings entre crates y la eliminación de código muerto, una situación muy frecuente. LTO rompe esta barrera proporcionando al optimizador, en tiempo de enlace, el IR de todos los crates para realizar una optimización global. Pero nunca añadas lto = true sin pensar al perfil, ya que eso hará que la compilación de Rust sea tan lenta que llegarás a dudar de la vida; simplemente añádelo cuando uses el perfil Release.
Eliminar símbolos de depuración
Como ocurre con todo lo demás, la debuginfo y los symbols de Rust residen aquí. Activar strip en modo release puede reducir el tamaño de forma considerable, una diferencia brutal como (normalmente 50MB vs 5MB). Eliminar los symbols también es más seguro, al fin y al cabo, la mayoría de las fugas de source code frontend suceden cuando se hace push por accidente de las maps a npm :(
Así que en el perfil de desarrollo no hay mucho que decir; no puedes simplemente strippear debuginfo, porque entonces no podrías usar gdb / lldb para depurar normalmente y la backtrace no mostrará nombres de función útiles. Sin embargo, ten en cuenta que al empaquetar en ArchLinux se hace strip de forma predeterminada, lo que choca un poco con nosotros ya que ya lo hemos strippeado, lo que podría provocar errores más adelante. Si estás creando un paquete AUR, puedes prestar atención a omitir explícitamente este paso.
Renunciar a panic!
En la lógica de negocio normal, panic no debería existir. Cuando el código de negocio normal encuentra un error en runtime, cualquier Err diseñado para la tolerancia a fallos debería devolverse de forma segura en lugar de hacer un panic de manera violenta. Al igual que un ErrorBoundary de React, debería tratarse normalmente como un error; un panic solo debería activarse cuando el código tenga la firme certeza de haber entrado en un estado imposible e irrecuperable. Mi filosofía personal es renunciar al panic, ¿por qué? Porque, en la práctica, el código de negocio se puede dividir en 3 capas de pruebas: la primera capa es el happy path, la segunda capa son las rutas de error (una entre infinitas posibilidades) y la tercera capa son los Edge Cases obtenidos exhaustivamente mediante fuzzing o pruebas de colisión. Desde el punto de vista de la ingeniería, la ruta correcta se puede probar al 100 %, y para las rutas de error basta con tomar 1 opción entre n variantes de una categoría. Las pruebas de colisión son puramente una cuestión de tiempo y la mayoría de las veces no valen la pena; cuando tu volumen de usuarios realmente crezca, sabrás por ti mismo cuándo es necesario hacerlo. Por lo general, yo nunca lo hago.
Para un código de calidad, debes cubrir una prueba del camino feliz y al menos una prueba de camino de error. Con esa única ruta de error basta para probar el caso en que se devuelve un Err, escribir lógica de fallback y, de forma natural, thiserror enumerar o anyhow interceptar y aplanar los registros de impresión. En resumen, tu código te obliga a crear mecanismos para manejar esas situaciones; si has llegado a este nivel, panic ya no tiene utilidad – puede interpretarse como “teóricamente imposible”, aunque también es solo teoría. En el mundo real existen errores del SO, errores de memoria, inversiones de bits por rayos cósmicos, extraños desbordamientos… Nunca podrás cubrir todos esos casos. Entonces, ¿por qué evitar el panic? Porque esas situaciones en su mayoría no son culpa de tu código, alrededor del 90 % no provienen de ti. La esencia de panic es que, al ocurrir, Rust recorre la pila de llamadas, llamando a los destructores (drop) de cada marco para liberar recursos. Este proceso requiere que el compilador genere tablas adicionales unwind, lo que ayuda a localizar errores lógicos en tu lógica de negocio. Si los errores son mayormente externos, esa información tiene poco valor, además aumenta el tamaño del binario y la carga del enlazador – desactivarlo es la solución correcta. Cuando tu código está bien probado y tienes confianza, puedes configurar panic = abort.
Benchmark real
Tantas palabras son inútiles; veamos realmente las mejoras obtenidas con estas combinaciones. Aquí tomaremos como referencia un pequeño proyecto de juguete que hice hace unos meses.
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Language Files Lines Code Comments Blanks
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
CSS 5 152 113 16 23
Go 77 14556 12409 584 1563
JavaScript 23 2506 1999 232 275
JSON 93 1780 1780 0 0
Just 1 408 275 71 62
Makefile 1 15 8 3 4
Shell 16 1440 1075 166 199
SVG 1 9 9 0 0
TOML 20 502 419 3 80
TSX 75 3257 2718 132 407
TypeScript 390 36643 30350 1627 4666
─────────────────────────────────────────────────────────────────────────────────
HTML 2 41 32 7 2
|- CSS 1 37 37 0 0
(Total) 78 69 7 2
─────────────────────────────────────────────────────────────────────────────────
Markdown 95 5344 0 3578 1766
|- BASH 3 8 8 0 0
|- Go 2 21 21 0 0
|- HTML 1 25 15 10 0
|- JSON 6 317 317 0 0
|- Rust 1 10 9 0 1
|- TSX 1 11 9 0 2
(Total) 5736 379 3588 1769
─────────────────────────────────────────────────────────────────────────────────
Rust 181 31654 26951 1032 3671
|- Markdown 86 581 0 564 17
(Total) 32235 26951 1596 3688
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Total 980 99317 78554 8025 12738
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━En este proyecto, la parte de Rust es aproximadamente 32 K SCoL Rust, sin contar el tamaño de las dependencias.
Este repositorio es en realidad un monorepo con muchos subproyectos, pero dentro se pueden identificar dos ejemplos típicos. Primero, al mirar el paquete Skeleton, sus características son evidentes: tiene una gran cantidad de código y muy pocas dependencias, lo que lo convierte en la mayor proporción de generación de código (codegen) del proyecto completo, siendo muy adecuado para que Cranelift se desempeñe. Además, este paquete también es muy limpio, y «limpio» aquí significa que está escrito en Rust puro y seguro.
Por otro lado, el paquete CLI de abajo no es muy adecuado. Tener muchas dependencias no es realmente un problema —en teoría, esto refleja mejor la eficiencia—, pero aquí hay una clásica elección entre dos opciones. Puedes ver en la esquina superior izquierda que la dependencia ring está marcada como opcional; esto se debe a que el proyecto originalmente usaba aws-lc-rs, siendo ambos Backends de algoritmos de cifrado en Rust. Pero ¿por qué hacerla opcional? Es porque ring está escrito en pure Rust, mientras que el otro se incluye mediante FFI de ensamblador Asm, diseñado específicamente para la aceleración por ensamblador en arquitecturas cpu principales como x86 y arm64. Sin embargo, ahí radica también su ruina, ya que la magia de Cranelift se limita estrictamente a pure Rust. Una vez que introduces Unsafe Code, o FFI C, ASM y similares, en la práctica la compatibilidad de Cranelift se vuelve extremadamente deficiente.
Sin embargo, no es insoluble; en realidad puedes usar cfg para seleccionar el backend y, en modo Cranelift, pasar ring al compilador. Después de configurar los perfiles dev y release como se describió anteriormente, basta con ejecutar una comparación de compilación con la CLI. En este caso, el desarrollo es al menos tres veces más rápido que la publicación, incluso con compilación en frío y sin compilación incremental; y usando O0 y desactivando LTO, el perfil dev debería ser decenas de veces más rápido que release en actualizaciones incrementales posteriores. Porque la magia del tipo LTO en realidad se logra aplanando los límites de cada crate; una vez que todo se convierte en una única unidad, cambiar una línea de código obliga a recompilar todo, lo que impide una actualización incremental adecuada.
![Esta es una captura de pantalla de una ventana de terminal titulada "~/C/P/seam" que muestra la parte final de una ejecución de `cargo build --release` para un proyecto llamado "seam". La pantalla desplaza docenas de crates de dependencias compilándose en secuencia — walkdir, rand, tungstenite, sha2, toml, notify, rquickjs, clap y otros — seguidos por los miembros propios del espacio de trabajo del proyecto, como seam-codegen, seam-skeleton, seam-injector-wasm, seam-engine-wasm, seam-server, seam-server-axum, seam-cli y varios backends de ejemplo (demo-server-rust, github-dashboard-axum, markdown-demo-rust, i18n-demo-axum). Las líneas finales confirman el éxito: "Finished `release` profile [optimized] target(s) in 1m 05s", seguido de un prompt de shell inactivo que muestra `canmi@xyy ~/C/P/seam (main)>`. Es evidencia de una compilación limpia y completa en modo release de un monorepo Rust de múltiples crates con componentes WASM y servidor Axum.](https://cdn.ffoni.com/image/e8b530aa153a78f1cdea3d2980f80ecd.jpeg)
![Una captura de pantalla de terminal muestra el final de un registro de construcción de Cargo para un espacio de trabajo Rust llamado “seam”, con la ruta `~/C/P/seam` y el shell ejecutándose bajo el usuario `canmi` en la rama `main`. Decenas de crates de dependencias se compilan en secuencia (console, reqwest, clap, tokio‑tungstenite, notify, indicatif y otros), seguidos por los crates propios del proyecto — seam‑codegen, seam‑server‑axum, seam‑skeleton, seam‑injector‑wasm, seam‑engine‑wasm, seam‑cli, todos en la versión 0.5.38 — y varios back‑ends de ejemplo (github‑dashboard‑axum, i18n‑demo‑axum, markdown‑demo‑rust, demo‑server‑rust). El registro termina con `Finished \`dev\` profile [unoptimized + debuginfo] target(s) in 19.23s`, indicando una compilación de depuración exitosa de todo el espacio de trabajo, incluidas sus aplicaciones de ejemplo.](https://cdn.ffoni.com/image/71aa2631cd4c616d2ecca6e510cf06f4.jpeg)
Analizando los resultados globales, estos dos elementos no son en realidad tan diferentes; no se puede hablar de una revolución, pero sin duda se percibe claramente la diferencia. La razón principal es que Apple Silicon es brutalmente potente: el rendimiento mononúcleo y el ancho de banda de memoria de los chips de la serie M son increíblemente altos, por lo que las tareas con gran carga de cálculo y de E/S, como la compilación, aprovechan al máximo estas ventajas. Por lo general, al ejecutar esto en Linux la diferencia sería aún mayor, pero si se instala algo como Asahi Linux en un MacBook, sin duda ganaría Linux.
Enlazador obsoleto
En Linux, Rust usa por defecto GNU ld (bfd); sí, el más antiguo y lento de todos. Es monohilo y procesa la resolución de símbolos y la relocalización mediante un escaneo secuencial, por lo que frena claramente el rendimiento cuando el proyecto crece. En Linux, puedes probar mold, escrito por @Rui Ueyama. Su configuración es facilísima: solo hay que añadir una línea dentro de .cargo/config.toml.
[target.x86_64-unknown-linux-gnu]
linker = "clang"
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
La mejora sigue siendo muy evidente, pero lamentablemente en macOS solo existe sold, que es un proyecto comercial. La buena noticia es que lld en macOS o el ld64 integrado de Apple ya no es lento de por sí, especialmente el nuevo enlazador a partir de Xcode 15, cuyo aumento de velocidad es súper evidente. Sin embargo, lo único malo de esta cosa es que en un CI desatendido de macOS y tras las actualizaciones, hay que hacer sudo Accept de una cláusula cada vez, lo que provocó que mis tareas programadas de CI fallaran varias veces.
MUSL
Personalmente soy un gran fan de MUSL y detesto GNU glibc, pero para el desarrollo sigo recomendándote usar x86_64-unknown-linux-gnu. ¿Por qué? Porque es rápido. El mayor atractivo de MUSL es que puede generar binarios completamente enlazados estáticamente: sin depender de ninguna biblioteca dinámica del sistema, lo que compilas se puede copiar a cualquier máquina Linux y ejecutar, siendo ideal para contenedores y sistemas embebidos. Sin embargo, su lentitud también se debe al enlazado estático: como el enlazador necesita empaquetarlo todo, la carga de trabajo en la fase de enlazado es bastante mayor que con el enlazado dinámico.
Sin embargo, la buena noticia es que si usas macOS, no tienes de qué preocuparte en absoluto: aarch64-apple-darwin es actualmente la única opción. Por diseño, macOS no admite el enlazado totalmente estático, lo cual, desde la perspectiva de la velocidad de compilación, es en realidad algo bueno. Por lo tanto, el dev profile usa gnulibc en Linux y libSystem en macOS, mientras que el release profile puede usar musl tanto en macOS como en Linux. Debido a problemas con la selección predeterminada de linker y ar en macOS, lo único que conviene tener en cuenta es que quizás sea necesario configurar .cargo/config.toml.
[target.x86_64-unknown-linux-musl]
linker = "x86_64-linux-musl-gcc"
ar = "x86_64-linux-musl-ar"Por cierto, no use zig bajo ningún caso; el zbuild de nightly Rust tiene actualmente serios problemas. Cuando necesita compilación cruzada y el host es Linux x86, recomendamos cross. Compilar con Docker brinda un entorno completo y funciona muy bien. Si prefiere glibc, también sirve perfectamente, ya que glibc solo mantiene compatibilidad hacia atrás. Normalmente se compila contra la versión de glibc de una versión mayor de Debian (por ejemplo, Debian‑2). De lo contrario, una glibc demasiado nueva hará que muchas máquinas no funcionen; no se debe a una nueva característica, sino simplemente a que se verifica la cadena de versión para fastidiarle. Si el host es mac, use rustup nativo target + cargo build --release .
UPX es algo malo
Otro punto que vale la pena discutir es el uso de UPX; sé que decir que me gusta musl y al mismo tiempo buscar un tamaño reducido resulta en realidad bastante contradictorio y parece no tener sentido...
ooooo ooo ooooooooo. ooooooo ooooo
`888' `8' `888 `Y88. `8888 d8'
888 8 888 .d88' Y888..8P
888 8 888ooo88P' `8888'
888 8 888 .8PY888.
`88. .8' 888 d8' `888b
`YbodP' o888o o888o o88888o
The Ultimate Packer for eXecutables
Copyright (c) 1996-2026 Markus Oberhumer, Laszlo Molnar & John Reiser
https://upx.github.ioPero en la práctica, la existencia de UPX soluciona este problema. El principio de UPX consiste en comprimir tu BIN y envolverlo en un stub de descompresión, de modo que al ejecutarse se descomprime primero en memoria y luego se ejecuta. Esto permite reducir el tamaño del binario a alrededor del 30%~50% del original, lo que supone una ventaja de tamaño muy evidente. Sin embargo, precisamente por esto, combinar UPX + enlace dinámico con GNU glibc genera grandes problemas: los binarios de glibc con enlace dinámico contienen secciones especiales y mecanismos de carga dinámica que la compresión de UPX puede dañar, haciendo que no se encuentren las bibliotecas dinámicas en tiempo de ejecución segment fault; en cambio, musl es perfecto para UPX. Es totalmente estático por naturaleza e internamente estable, por lo que la descompresión y compresión con UPX resultan muy limpias. Por lo tanto, añadir un paso de UPX en el CI de release para musl sería sin duda un gran punto a favor.
Por otro lado, tampoco se debe abusar de UPX. UPX solo es adecuado para tareas de larga duración o de baja frecuencia. En el caso de las tareas de larga duración, esto se debe a que UPX necesita descomprimirse en memoria al iniciar; aunque este proceso es totalmente automático, requiere un pequeño tiempo de inicio en frío. Este impacto suele ser insignificante, pero resulta fatal en rutas de alta frecuencia. Por ello, iniciar una tarea de larga duración una vez y dejarla ejecutándose en segundo plano es muy adecuado. Sin embargo, no recomiendo meter UPX a ciegas en imágenes de Docker para servicios de larga duración, ya que en realidad se está cambiando memoria por espacio en disco, lo cual es un negocio pésimo. Además, la compresión de las Layers de los contenedores hace exactamente lo mismo que UPX para reducir el tamaño de descarga durante la distribución. En cambio, el entorno recomendado es para ciertos usos de CLI: ahorra muchísimo espacio en disco y el tiempo de descompresión al iniciar es casi imperceptible. Además, musl es naturally idóneo para crear CLI. De lo contrario, tendría que criticar directamente a los asistentes de AUR: esa cosa escrita en Go pero sin enlazado estático provocó que una vez se rompuera yay en mi ArchLinux al no encontrar las bibliotecas dinámicas. Como era el propio gestor de paquetes del sistema, no hubo más remedio que iniciar desde un LiveISO y hacer un chroot para rescatar el sistema.
Resumen
Aunque estas configuraciones útiles pueden ayudarte a distinguir rápidamente entre los perfiles dev y release, sus beneficios se hacen cada vez más evidentes a medida que el código del proyecto crece. Sin embargo, sigo recomendando añadir una tarea de CI que ejecute el Rust Stable LLVM Build, preferiblemente junto con todas tus pruebas. De este modo no tendrás que cambiar a una versión nightly de Rust localmente, los Edge Cases no te atraparán y cualquier problema será bloqueado por la CI al hacer push. Este es, por ahora, el modo DX más cómodo; eso es todo por ahora, la próxima vez que tenga tiempo volveré a ello.