- En la rama master de Zig se integraron mejoras en el manejo de enteros no ABI del backend de LLVM y una nueva semántica de
@bitCast, resolviendo a la vez problemas de optimización y discrepancias en el comportamiento del lenguaje - Los enteros de ancho de bits arbitrario como
u4,i13yu40ahora se manejan como bit-int en valores SSA, pero al almacenarse en memoria se expanden a enteros de tamaño ABI - El
@bitCastanterior se parecía más a una reinterpretación de bytes en memoria, pero la nueva definición lo interpreta según el arreglo lógico de bits del tipo, reduciendo la dependencia del endian - El cambio se extendió a los backends de LLVM y C, así como a la ejecución en
comptime, y también se revisaron los usos relacionados en la biblioteca estándar, el compilador ycompiler_rt - Al recuperarse optimizaciones que antes se perdían, se observó una mejora de rendimiento de ~5% en el propio compilador de Zig, y se esperan algunas mejoras de rendimiento en tiempo de ejecución en la versión 0.17.0
Cambios en el manejo de enteros de ancho de bits arbitrario en el backend de LLVM
- Zig antes hacía lowering directo de tipos enteros de ancho de bits arbitrario como
u4,i13yu40a los tipos bit-int de LLVM IR:i4,i13,i40 - Ese enfoque hacía que la semántica de representación en memoria de LLVM impusiera restricciones innecesarias al optimizador, y como Clang no genera este tipo de LLVM IR, esas rutas internas de LLVM tampoco estaban suficientemente probadas
- En los últimos años se observaron casos reales de optimizaciones omitidas y miscompilation
- El nuevo enfoque mantiene los tipos bit-int para manipular valores SSA, pero al guardarlos en memoria los hace zero-extend o sign-extend a tipos de tamaño ABI como
i8,i16oi32 - Este lowering coincide con la forma en que Clang hace lowering de
_BitInt(N)en C, por lo que se espera que use una ruta mejor soportada dentro de LLVM
Limitaciones del @bitCast anterior
- Conceptualmente, el
@bitCastanterior se parecía al siguiente comportamiento- obtener un puntero al valor operando
- hacer cast de ese puntero a un puntero del tipo destino
- cargar el valor desde ese puntero
- Es decir, la definición anterior se parecía más a una reinterpretación de bytes en memoria que a la estructura lógica del tipo
- Con el tiempo, el comportamiento real se fue apartando de esa definición, y aunque en la mayoría de los targets
@sizeOf(u24)es mayor que@sizeOf([3]u8), aun así se permitía hacer@bitCastde[3]u8au24 - El backend de LLVM estaba implementando una semántica de
@bitCastinsuficientemente especificada, y al cambiar la forma de almacenar en memoria los tipos enteros aparecieron Illegal Behavior y crashes en la suite de pruebas del compilador - En vez de agregar lógica al backend de LLVM para imitar el comportamiento anterior, se eligió implementar de forma general la nueva definición de
@bitCast
Nueva semántica de @bitCast
- La nueva semántica se basa en la propuesta del lenguaje #19755, presentada y aceptada en 2024
- Esta semántica ya estaba implementada en el backend self-hosted x86_64, y con este cambio se extendió también a los backends de LLVM y C, además de la ejecución en
comptime - El nuevo
@bitCastopera no sobre bytes en memoria, sino sobre el orden de bits que representa lógicamente al tipou5se compone de 5 bits lógicos, desde el least-significant bit hasta el most-significant bit[2]u5se compone de 10 bits lógicos: primero los 5 bits del primer elemento y luego los 5 bits del segundo
- En conversiones simples entre enteros, como cambiar
u8ai8del mismo tamaño, los bits se conservan tal cual y el bit más alto se interpreta como bit de signo - También se conserva la semántica de
@bitCastentre tipos enteros ypacked structopacked union
Cambios de comportamiento en arreglos y vectores
- El punto donde la nueva semántica difiere de la anterior es cuando intervienen tipos agregados como arreglos y vectores
- Por ejemplo, al hacer
@bitCastde[2]u8au16, con la semántica anterior el resultado dependía del endian del target- en targets big-endian, el primer elemento del arreglo se convertía en los 8 bits altos
- en targets little-endian, el primer elemento del arreglo se convertía en los 8 bits bajos
- La nueva semántica solo considera la representación lógica de bits, así que es independiente del endian, y en todos los targets el primer elemento del arreglo pasa a ser los 8 bits bajos
- En general, se parece más al comportamiento anterior en targets little-endian
- También permite conversiones menos convencionales, como transformar
[2]u3a@Vector(3, u2)- se concatenan los bits lógicos del arreglo y luego se leen en unidades de 2 bits para formar los elementos del vector
- también puede usarse para descomponer un entero en un vector de bits individuales con
@bitCasta@Vector(n, u1)
Propuestas incluidas y migración
- Durante este trabajo también se implementaron pequeñas propuestas aceptadas relacionadas con
@bitCast - Como la nueva semántica difiere de forma significativa de la anterior, se revisaron los usos de
@bitCasten librerías de soporte como la biblioteca estándar, el compilador ycompiler_rt - El PR relacionado es codeberg.org/ziglang/zig/pulls/35711, y al integrarse en master también cerró varios issues
- La semántica modificada y el procedimiento de migración recomendado se documentarán en las notas de lanzamiento de Zig 0.17.0
Efecto esperado en rendimiento para 0.17.0
- El cambio en el lowering de enteros no ABI del backend de LLVM, que era el objetivo original, sí logró recuperar optimizaciones que se estaban perdiendo
- El resultado puede verse en demonstrably successful
- Aunque el propio compilador de Zig no usa internamente tantos enteros de ancho de bits arbitrario, mostró una mejora de rendimiento de ~5% gracias a una mejor optimización
- En 0.17.0 podría haber pequeñas mejoras de rendimiento en tiempo de ejecución para parte del código
1 comentarios
Opiniones en Lobste.rs
El artículo dice que la representación lógica de bits es independiente del endianness, pero la explicación real parece un enfoque claramente little-endian que no soporta ni orden de bits big-endian ni orden de bytes big-endian
En el nuevo registro de desarrollo con fecha del 25 de junio de 2026, se menciona que la nueva semántica de
@bitCasty las mejoras del backend de LLVM se fusionaron en un pull request recienteEs interesante, pero me pregunto si código escrito como el de abajo podría romperse de repente en targets big-endian que rara vez se prueban
Escrito en pseudocódigo no-Zig:
En realidad no parece un gran problema; de los miles de
@bitCasten el repositorio de Zig, da la impresión de que muchos menos de 100 se vieron afectados por este cambioSinceramente, tampoco creo que la mayoría de los usuarios de Zig supieran con precisión cómo funcionaba
@bitCastal convertir entre arreglos/vectores y escalares. También es probable que bastante código que antes solo se probaba en el sistema del autor y por eso solo funcionaba en little-endian, ahora pase a funcionar en cualquier parteComo exprogramador de C, recuerdo que los bit fields de C no eran muy populares porque su comportamiento no era portable entre arquitecturas
La nueva semántica de
@bitCasten Zig me parece justo la dirección necesaria: una semántica abstracta portable que da el mismo resultado en distintas arquitecturasÚltimamente he estado diseñando bit fields y bit casts en mi propio lenguaje, así que pienso revisar con más detalle la documentación de diseño e implementación de Zig para dejar claro cómo debería comportarse mi código
packed structypacked union, y ambos están definidos para encajar bien con la nueva definición de@bitCastpacked structfunciona rellenando los bits de los campos dentro de un “entero subyacente”. Por ejemplo, si los campos sonbool,u6,i9y el entero subyacente esu16, entonces el bit menos significativo deu16serábool, los siguientes 6 bits seránu6, y los 9 bits restantes seráni9. Es decir, el packed struct de Zig se parece más a azúcar sintáctica sobre varios shifts y maskspacked uniontambién tiene un entero subyacente, pero todos los campos deben usar exactamente la misma cantidad de bits que ese entero subyacente. Por eso, guardar en un campo y leer desde otro es casi idéntico a@bitCastbajo la nueva semántica. Eso sí, los campos depacked union/packed structno pueden tener tipos arreglo ni vectorPersonalmente, me parece que estas herramientas encajan muy bien para expresar “estructuras relacionadas con bits”. Puedes empaquetar varios valores en un
packed structpara usarlo como los bit fields de C, y como es azúcar sintáctica sobre operaciones de bits, también permite expresar limpiamente bit flags que en C se resolvían con un montón de macros sin seguridad de tiposPor ejemplo, las banderas de acceso RWX en C podrían recibirse como macros
ACCESS_READ,ACCESS_WRITE,ACCESS_EXECy una API conuint8_t, pero en Zig puedes definirAccess = packed struct(u8)con camposread,write,exec,reserved, y hacer que la API recibaAccessCon
packed structypacked uniontambién se pueden expresar distribuciones de bits bastante extrañas. La entrada de tabla de símbolos del formato de objeto Mach-O tiene, al parecer por razones históricas, un campon_typepeculiar, y eso se puede modelar comobits: packed struct(u8)ystab: enum(u8)dentro de unpacked union(u8)Al manejar este valor
n_type, no hace falta hacer shifts o masking manualmente. Basta con comprobarn_type.bits.is_stab != 0y, si se cumple, hacerswitchsobren_type.stab; si no, mirar los otros campos den_type.bits. A la inversa, también puedes crear valores como.{ .stab = .gsym }o.{ .bits = .{ .ext = false, .type = .undf, .pext = false, .is_stab = 0 } }Me extendí un poco con una función del lenguaje distinta del tema principal del artículo, pero si estás buscando algo que pueda servir de referencia para el diseño de un lenguaje nuevo, valdría la pena probar directamente
packed structypacked unionde Zig. Me parecen herramientas simples, pero bastante buenas