Al igual que muchos desarrolladores, he estado interesado en Rust durante bastante tiempo. No solo porque aparece en muchos titulares de noticias de Hackers, o por el enfoque novedoso que el lenguaje adopta para la seguridad y el rendimiento, sino también porque la gente parece hablar de ello con un sentido particular de amor y admiración. Además de eso, Rust es de particular interés para mí porque comparte algunos de los mismos objetivos y características de mi idioma favorito: Swift. Dado que recientemente me he tomado el tiempo de probar Rust en algunos pequeños proyectos personales, quería tomarme un poco de tiempo para documentar mis impresiones del lenguaje, especialmente en su comparación con Swift.

El panorama general

Rust y Swift tienen muchas cosas en común: ambos son lenguajes compilados con sistemas de tipos potentes y modernos y se centran en la seguridad. Características como los tipos algebraicos y el manejo de primera clase de valores opcionales ayudan a mover muchas clases de errores del tiempo de ejecución al tiempo de compilación en ambos lenguajes.

Entonces, ¿en qué se diferencian estos idiomas? La mejor manera de caracterizar la diferencia es:

Swift facilita la escritura de código seguro.
Rust hace difícil escribir código inseguro.

Esas dos afirmaciones pueden sonar equivalentes, pero hay una distinción importante. Ambos lenguajes tienen herramientas para lograr la seguridad, pero hacen diferentes concesiones para lograrlo: Swift prioriza la ergonomía a expensas del rendimiento, mientras que Rust prioriza el rendimiento a expensas de la ergonomía.

La compensación: Rendimiento vs Ergonomía

La mayor manera en que se demuestra esta diferencia de prioridad es en el enfoque que estos lenguajes tienen para la gestión de la memoria. Comenzaré con Rust porque el enfoque del lenguaje para la gestión de la memoria es uno de sus puntos de venta únicos.

En Rust, la memoria se administra principalmente de forma estática (sí, hay otros modos de administración de memoria, como el recuento de referencias, pero los ignoraremos por ahora). Lo que esto significa es que el compilador de Rust analiza su programa y, de acuerdo con un conjunto de reglas, decide cuándo se debe asignar y liberar la memoria.

Para ofrecer seguridad, Rust utiliza una estrategia novedosa llamada verificación de préstamos. La forma en que esto funciona en la práctica es que, como programador, cada vez que pasa una variable (es decir, una referencia a una ubicación de memoria), debe especificar si la referencia es mutable o inmutable. El compilador utiliza un conjunto de reglas para asegurarse de que no se puede mutar una sola pieza de memoria en dos lugares a la vez, lo que hace que sea demostrable que su programa no tiene carreras de datos.

Este enfoque tiene algunas propiedades muy beneficiosas con respecto al uso de memoria y el rendimiento. La comprobación de préstamos puede ser muy parsimoniosa con la memoria, ya que generalmente evita copiar valores. También evita la sobrecarga de rendimiento de una solución como la recolección de basura, ya que el trabajo se realiza en tiempo de compilación en lugar de en tiempo de ejecución.

Sin embargo, viene con algunos inconvenientes en cuanto a facilidad de uso. Debido a la naturaleza de la propiedad en óxido, hay algunos patrones de diseño que simplemente no funcionan en óxido. Por ejemplo, no es trivial implementar algo como una lista doblemente vinculada o una variable global. Esto probablemente se vuelve más intuitivo con el tiempo, y hay soluciones para estos problemas, pero Rust ciertamente impone limitaciones al programador que no están presentes en otros lenguajes.

Si bien no se habla tan a menudo como Rust, Swift también tiene una historia interesante cuando se trata de administración de memoria.

Swift tiene dos tipos fundamentales de variables: tipos de referencia y tipos de valor. En general, los tipos de referencia se asignan a montones y se administran mediante recuento de referencias. Esto significa que en tiempo de ejecución, se realiza un seguimiento del número de referencias a un objeto contado de referencia y el objeto se desasigna cuando el recuento llega a cero. El recuento de referencias en Swift siempre es atómico: esto significa que cada vez que cambia un recuento de referencias, tiene que haber una sincronización entre todos los subprocesos de la CPU. Esto tiene la ventaja de eliminar la posibilidad de que una referencia se libere por error en una aplicación de subprocesos múltiples, pero tiene un costo de rendimiento significativo, ya que la sincronización de CPU es muy costosa.

Rust también tiene herramientas para contar referencias y contar referencias atómicas, pero estas son opcionales en lugar de ser las predeterminadas.Por el contrario, los tipos de valores

se asignan por pila en general y su memoria se administra de forma estática. Sin embargo, el comportamiento de los tipos de valor en Swift es muy diferente a la forma en que Rust maneja la memoria. En Swift, los tipos de valor tienen lo que se denomina comportamiento «copiar al escribir», que se copian de forma predeterminada, lo que significa que cada vez que se escribe un tipo de valor en una nueva variable o se pasa a una función, se realiza una copia.

La copia en escritura por defecto logra algunos de los mismos objetivos de la comprobación de préstamos: como programador, generalmente nunca tiene que preocuparse de que un valor cambie misteriosamente debido a algún efecto secundario inesperado en otro lugar del programa. También requiere un poco menos de carga cognitiva que la comprobación de préstamos, ya que hay clases enteras de errores de tiempo de compilación relacionados con la propiedad en Rust que simplemente no existen en Swift. Sin embargo, tiene un costo: esas copias adicionales requieren un uso de memoria y ciclos de CPU adicionales para completarse.

En Rust también es posible copiar valores como una forma de silenciar los errores de comprobación de préstamos, pero esto agrega ruido visual ya que las copias deben especificarse explícitamente.

Así que aquí tenemos un buen ejemplo de las ventajas y desventajas de estos dos lenguajes: Swift le ofrece algunas suposiciones generales sobre cómo se debe administrar la memoria sin dejar de mantener un nivel de seguridad. Es un poco como cómo un programador de C++ podría manejar la memoria de acuerdo con las mejores prácticas antes de pensar mucho en la optimización. Esto hace que sea muy fácil entrar y escribir código sin pensar mucho en detalles de bajo nivel, y también lograr algunas garantías básicas de seguridad y corrección en tiempo de ejecución que no obtendría en un lenguaje como Python o incluso Golang. Sin embargo, viene con algunos acantilados de rendimiento, que es fácil caerse sin siquiera darse cuenta hasta que ejecute su programa. Es posible escribir código Swift de alto rendimiento, pero esto a menudo requiere un perfil y una optimización cuidadosos para lograrlo.

Rust, por otro lado, le brinda muchas herramientas específicas para especificar cómo se debe administrar la memoria, y luego impone algunas restricciones estrictas sobre cómo las usa para evitar comportamientos inseguros. Esto le brinda características de rendimiento muy agradables desde el primer momento, pero requiere que asuma la sobrecarga cognitiva adicional de garantizar que se sigan todas las reglas.

Mi conclusión de esto ha sido que, si bien estos idiomas tienen algunos objetivos comunes, tienen características fundamentalmente diferentes que se prestan a diferentes casos de uso. Rust, por ejemplo, parece la elección clara para algo como el desarrollo embebido, donde el uso óptimo de la memoria y los ciclos de CPU es extremadamente importante, y donde el bucle de compilación y ejecución de código puede ser más lento, por lo que es valioso detectar todos los problemas posibles en tiempo de compilación. Mientras que Swift podría ser una mejor opción para algo como la ciencia de datos o la lógica sin servidor, donde el rendimiento es una preocupación secundaria, y es valioso trabajar más cerca del dominio del problema sin tener que considerar muchos detalles de bajo nivel.

En cualquier caso, estaré muy interesado en seguir estos dos idiomas en el futuro, y seguiré este post con más observaciones sobre la comparación entre Swift y Rust.

Deja una respuesta

Tu dirección de correo electrónico no será publicada.