- El prototipo de préstamo regional de Vale logró compilar por primera vez, lo que permite validar en programas reales un enfoque de seguridad de memoria que combina referencias generacionales y regiones
- Los desarrolladores pueden escribir código de una forma cercana a C/C++ y aplicar pure y préstamo regional solo donde sea necesario para reducir el overhead de las comprobaciones generacionales
- El primer programa Vale con cero comprobaciones fue un ejemplo de Cellular Automata para generar niveles de roguelike, y el ensamblador resultante quedó casi al mismo nivel que el modo
unsafe_with_bounds
- En el benchmark,
safe_fastest no mostró ninguna desaceleración observable frente a unsafe_with_bounds, mientras que unsafe_no_bounds fue 1.18 ± 0.01 veces más rápido que ambos modos
- Todavía no es una comparación directa con C/Rust; debido al ruido de optimización de LLVM, la madurez del prototipo y la falta de soporte para inline data, aún quedan pendientes un pre-optimizer específico para Vale y la depuración de las funciones regionales
Combinación de referencias generacionales y préstamo regional
- El enfoque de seguridad de memoria de Vale apunta a no usar reference counting, garbage collection con tracing ni borrow checking
- La estructura básica consiste en que los desarrolladores escriban programas de una forma cercana a C o C++, mientras las generational references de Vale mantienen la seguridad de memoria
- Luego, al aplicar pure y region borrowing, se puede eliminar la mayor parte del overhead de las comprobaciones generacionales
- Sumando el linear style, se considera posible reducir las comprobaciones generacionales a cero en código Vale
- El préstamo regional es completamente opt-in, por lo que se puede escribir primero de forma cómoda y agregarlo después solo en las partes que necesiten optimización
- La idea es una estructura en la que, dentro del mismo programa, algunas partes puedan ser flexibles como Java, otras rápidas como Rust, o elegir puntos intermedios
Trabajo necesario para crear el prototipo
- Durante los últimos años se construyó la base del compilador y se trabajó para soportar conjuntamente un sistema de préstamo basado en regiones y generational references
- El sistema de préstamo en sí es complejo y además requería full generics, más potentes que los templates existentes
- Para que las regiones y las referencias generacionales funcionaran juntas de forma natural, también hizo falta una nueva etapa del compilador
- Internamente, las regiones se reducen a un entero de “pure height”
- Los parámetros genéricos de región se representan con números negativos, la región predeterminada con 0 y cada pure block con números positivos crecientes
- Hace unos meses se completó el prototipo de regiones y, aunque todavía tiene partes ásperas, logró compilar algo con éxito por primera vez
- Como resultado se creó el primer programa Vale con cero comprobaciones
- Con la flag del compilador
--print_mem_overhead true se puede contar la cantidad de comprobaciones generacionales del programa
Primer programa con cero comprobaciones y comparación de ensamblador
- El primer programa fue un ejemplo de Cellular Automata que genera niveles para un juego roguelike
- Incluso un pequeño error en el código del compilador podía agregar instrucciones al ensamblador resultante y crear overhead artificial en el programa final
- Para rastrear el problema, el ensamblador resultante se comparó continuamente con los modos unsafe de Vale
unsafe_no_bounds: desactiva toda la protección de seguridad de memoria, de forma similar a C, y usa raw pointers en lugar de generational references
unsafe_with_bounds: agrega bounds checking en los accesos a arrays, como Rust
- Tras varios meses rastreando diferencias, el ensamblador resultante quedó casi idéntico al modo
unsafe_with_bounds
- La única diferencia esperada era la inserción de un pseudo-random generation number al inicio de cada allocation, que en la práctica no se leía en las comprobaciones generacionales
- Internamente, para mantenerlo rápido, se usa un registro que aumenta de forma monótona
- Cuando se agreguen isolates o unique references, esta diferencia podría eliminarse
Resultados del benchmark y condiciones de medición
- El resumen del benchmark es el siguiente
Summary
'./build_unsafe_no_bounds/main' ran
1.18 ± 0.01 times faster than './build_unsafe_with_bounds/main'
1.18 ± 0.01 times faster than './build_safe_fastest/main'
safe_fastest, el modo normal de Vale, no muestra desaceleración frente al modo que solo tiene bounds checking
- En esta medición, este enfoque no tiene overhead observable
- Para probarlo directamente, se puede compilar la rama regions, revisar los scripts de benchmarking y hacer preguntas en el servidor de Discord
- Las condiciones de medición tienen limitaciones claras
- No es un benchmark que compare directamente con lenguajes como C o Rust
- Esos compiladores tienen años de optimizaciones independientes que podrían enturbiar las variables del experimento
- Para aislar las diferencias del enfoque de seguridad de memoria, se comparó con
unsafe_no_bounds y unsafe_with_bounds
- El entorno de medición fue una Razer Blade 15" 2018 con SSD de 512 GB, ejecutando Ubuntu 22.04
- La herramienta de medición fue hyperfine, ejecutada dentro de cset shield
Ruido de optimización observado en programas grandes
- En programas más grandes se observó bastante optimizer noise
- Es distinto del ruido de benchmark, ya que la configuración de medición mostró tiempos de ejecución muy consistentes, como
± 0.01
- Un cambio pequeño en una zona podía mover la medición hacia un lado
- Hubo un caso en el que, al cambiar el tamaño del generation number, apareció de forma consistente un overhead negativo de
1.13 ± 0.01
- Era un resultado extraño porque el programa no tenía muchos generation numbers
- Es posible que cambios en la register allocation hayan dominado la diferencia de rendimiento debida a diferencias semánticas
- En un programa más grande, un pequeño juego roguelike, el optimizador no logró fusionar dos branches idénticas dentro de una sentencia if, y también pasó por alto otras optimizaciones evidentes
- No está claro qué efecto tiene la presencia de un entero que no se lee, y también podría tratarse de un bug de LLVM
- Este resultado sugiere que podría hacer falta un pre-optimizer específico para Vale, similar al MIR de Rust
- LLVM fue diseñado teniendo más en mente a C
- Se considera posible que, si LLVM interpreta las generational references como un patrón de acceso intencional a memoria liberada, las trate como undefined behavior
Aplicabilidad y próximos pasos
- Cuando se combinan, las referencias generacionales y las regiones pueden crear un enfoque de seguridad de memoria muy rápido
- Los dominios de software en los que este enfoque podría encajar bien tienen las siguientes condiciones
- Quieren una latencia más predecible que la de tracing garbage collection
- Quieren mejor rendimiento y cache friendliness que con reference counting
- Quieren hacer prototipos e iterar con más facilidad que con borrow checking
- Antes de que Vale se compare de frente con C o C++, queda trabajo pendiente
- Debido a problemas de LLVM optimizer para inferir generation e immutability, se necesita un pre-optimizer específico para Vale
- Vale debe soportar inline data en lugar de la solución temporal actual de colocar todos los structs en el heap
- Como el benchmark anterior no usaba structs, la falta de soporte para inline data no afectó ese resultado
- Las funciones regionales aún están en etapa de prototipo, por lo que hay que pulir las partes ásperas, reducir la deuda técnica y luego fusionarlas en la main branch
- Después de la fusión, el plan es hacer que la biblioteca estándar use regiones para que el main program code de los usuarios obtenga beneficios aunque no use regiones directamente
- La medición actual muestra que los programas con cero comprobaciones son posibles y que pueden alcanzar la velocidad esperada
1 comentarios
Opiniones de Hacker News
Descargué Vale para probarlo, y me dio mala impresión que, al ejecutar por primera vez el compilador
valecsin argumentos, imprimiera de inmediato "(panic)"panices una expresión muy fuerte y creo que debería evitarse en situaciones normales de manejo de errores. Que un programa haya entrado en pánico se siente como una situación fuera de control, y deja mal saborLuego intenté ver la ayuda de argumentos de la línea de comandos, pero actualmente prácticamente no existe; después guardé el ejemplo Hello World del sitio web en
hello.vly ejecutévalec hello.vl, y aparecióUnknown subcommandAsí que ejecuté
valec build hello.vl, y salióUnrecognized input: hello.vl, seguido otra vez de(panic);valec helptampoco ayudó, así que al final me rendí. No sé cómo se supone que se use estocatdirectamente alvalec-help-build.txtincluido en la descarga, ahí debería explicarse lo que buscasEl compilador está en un estado bastante áspero ahora mismo. De agosto a mayo estuve 100% enfocado en prototipar regiones (
region), y lo que estás viendo ahora es la deuda técnica acumulada en ese proceso. Eso incluye no tener pruebas de integración para el sistema de ayudaDurante el último mes o dos he estado pagando esa deuda, pero todavía no volvió al nivel que tenía en el lanzamiento 0.2. Si necesitas más ayuda, avísame o entra al servidor de Discord; hay mucha gente que puede ayudarte
Dicho eso, el README no lo deja del todo claro y dice “Try Vale”, así que es ambiguo. Aun así, por ahora parece más bien investigación y desarrollo / prueba de concepto
Incluso mirándolo desde la interfaz de usuario o los bugs, la experiencia de depurar C++ de 40 años con gdb de 35 años compite de sobra con cualquier lenguaje experimental. Por ejemplo, imprimir
funcname()::staticvarnamees una interfaz rara y falla como la mitad de las veces. Ni hablar de los sistemas de build de C++Con tecnología experimental, se puede criticar el concepto, pero creo que una interfaz de usuario áspera es algo que se puede tolerar hasta cierto punto
https://github.com/ValeLang/Vale#building-a-vale-program
Que tenga latencia más predecible que la recolección de basura por trazado, mejor rendimiento y afinidad con la caché que el conteo de referencias, y que facilite más el prototipado y la iteración que un verificador de préstamos me genera más que curiosidad: me interesa
También empecé a suscribirme al feed RSS: https://verdagon.dev/rss.xml
Vale necesita más patrocinadores
https://github.com/sponsors/ValeLang
Mientras este artículo esté en la portada, me gustaría que ayudáramos al proyecto a alcanzar su meta de 3.000 dólares al mes
Quiero ayudar a que Evan pueda dedicarse a esto a tiempo completo. Yo también soy patrocinador. Un lenguaje rápido y seguro, pero en el que además sea agradable prototipar, merece apoyo
“Un preoptimizador específico para Vale, algo parecido a Cranelift en Rust” probablemente se refiere a MIR, es decir, una representación intermedia de nivel medio. Hay un buen post de blog relacionado: https://blog.rust-lang.org/2016/04/19/MIR.html
Cranelift es un backend de compilador centrado principalmente en JIT, pero en teoría también podría reemplazar a LLVM. También hay trabajo en curso en backends alternativos, aunque con limitaciones: https://github.com/bjorn3/rustc_codegen_cranelift
Un enfoque en el que no tengas que preocuparte por la gestión de memoria en la mayor parte del código, pero que te dé la opción de optimizar solo las rutas calientes con abstracciones de costo cero, suena como lo mejor de ambos mundos
Sobre todo si, por conveniencia, lo único que se intercambia es rendimiento y no seguridad
La propiedad compartida es un mal concepto, así que tampoco uso punteros inteligentes
En general, creo que los problemas de gestión de memoria son menores
Sigo preguntándome qué significa que sea seguro en el contexto de las referencias generacionales (generational references)
Si lo entendí bien, ¿quiere decir que evita use-after-free y double-free? Si es así, el programa igual puede fallar al acceder a memoria cuando la generación esperada y la generación real no coinciden
En ese sentido, parece menos seguro que el conteo de referencias, la recolección de basura con trazado o el verificador de préstamos
Si se intenta acceder a memoria liberada mediante una referencia, debería producirse de forma predecible y segura un error de segmentación o una falla de aserción. Con futuras mejoras que vuelvan a mapear el espacio de direcciones virtual, espero que incluso se puedan eliminar los errores de segmentación
Aunque, si se trata de un índice generacional, el runtime también debería poder comprobar si el acceso es válido antes de intentar el acceso real. No sé si eso es posible en Vale
malloc/free, y tiene otras ventajasLa función
checknecesita el número de generación de la asignación, así que accede a la asignación. Es decir, para verificar si la referencia puede acceder a esa asignación, primero tiene que acceder a esa asignaciónClaro que, si la asignación ya fue liberada, acceder a esa asignación y a su número de generación ya es comportamiento indefinido, así que no funciona
Parece tan obvio que no sé si me estoy perdiendo algo grande, o si “seguridad de memoria” aquí significa algo totalmente distinto
Vale no es el mismo lenguaje que V. V recibió una reseña muy crítica en https://mawfig.github.io/2022/06/18/v-lang-in-2022.html, y por la similitud del nombre lo estaba recordando erróneamente como Vale
Lo dejo por si alguien más cometió el mismo error
El contenido del artículo ya no es relevante, pero sigue ahí, y además es la única entrada de ese blog
Ahora estamos en 2023 y V también está en beta (0.4). Además, la persona que creó ese texto usó una cuenta desechable de GitHub para publicar la reseña/ataque, generar polémica y luego desaparecer
La única entrada de ese blog también es un ataque a V y no hay otras reseñas. Las partes que tenían contenido sustancial ya fueron corregidas[1]
Si buscas
mawfig.github, también se ve que se difundió repetidamente en HN y que normalmente se usó para difamar[1]: https://github.com/vlang/v/issues/14803
[1]: https://github.com/vlang/v/issues/14787
[1]: https://github.com/vlang/v/issues/14786
Felicidades a Evan por alcanzar este hito. No tengo experiencia en diseño de lenguajes de programación ni en compiladores, pero disfruto leer los artículos de Vale
Ahora están el Vale de Evan y Val, del Adobe Software Technology Lab, así que creo que va a ser bastante difícil buscar materiales relacionados
https://www.val-lang.dev
“Vale es rápido: Vale se compila AOT con LLVM, usa tipos estáticos, emplea una nueva técnica de referencias generacionales para ofrecer seguridad de memoria con velocidad y flexibilidad, y pronto incorporará verificación de préstamos por regiones para volverse aún más rápido”
https://vale.dev/
Se siente como escuchar de reojo una discusión de dos personas que lleva 5 años
¿Alguien puede explicar qué está pasando aquí? El artículo es demasiado difícil de entender
En resumen, Vale es un lenguaje parecido a un C++ más limpio, y usa referencias generacionales[0], mentalmente similares a ejecutar con ASan[1] activado
Las referencias generacionales tienen algo de sobrecarga, pero puede eliminarse con regiones[2], más específicamente con préstamos de regiones inmutables[3]. Esto acerca a Vale a su objetivo de ser un lenguaje de alto rendimiento manteniendo la seguridad de memoria
[0] https://verdagon.dev/blog/generational-references
[1] https://github.com/google/sanitizers/wiki/AddressSanitizer
[3] https://verdagon.dev/blog/zero-cost-borrowing-regions-overvi...
[4] https://verdagon.dev/blog/zero-cost-borrowing-regions-part-1...