1 puntos por GN⁺ 5 시간 전 | 1 comentarios | Compartir por WhatsApp
  • 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, i13 y u40 ahora se manejan como bit-int en valores SSA, pero al almacenarse en memoria se expanden a enteros de tamaño ABI
  • El @bitCast anterior 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 y compiler_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, i13 y u40 a 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, i16 o i32
  • 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 @bitCast anterior 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 @bitCast de [3]u8 a u24
  • El backend de LLVM estaba implementando una semántica de @bitCast insuficientemente 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 @bitCast opera no sobre bytes en memoria, sino sobre el orden de bits que representa lógicamente al tipo
    • u5 se compone de 5 bits lógicos, desde el least-significant bit hasta el most-significant bit
    • [2]u5 se 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 u8 a i8 del 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 @bitCast entre tipos enteros y packed struct o packed 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 @bitCast de [2]u8 a u16, 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]u3 a @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 @bitCast a @Vector(n, u1)

Propuestas incluidas y migración

  • Durante este trabajo también se implementaron pequeñas propuestas aceptadas relacionadas con @bitCast
    • prohibición de @bitCast con vectores de punteros: #18936
    • permitir @bitCast sobre enum: parte de #35602
  • Como la nueva semántica difiere de forma significativa de la anterior, se revisaron los usos de @bitCast en librerías de soporte como la biblioteca estándar, el compilador y compiler_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

 
GN⁺ 5 시간 전
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

    • Aquí, independiente del endianness parece significar que el comportamiento no cambia entre arquitecturas little-endian y big-endian
  • En el nuevo registro de desarrollo con fecha del 25 de junio de 2026, se menciona que la nueva semántica de @bitCast y las mejoras del backend de LLVM se fusionaron en un pull request reciente

  • Es 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:

    if target_is_little_endian {  
        my_int = @bitCast(my_array);  
    } else {  
        my_int = @bitCast([my_array[1], my_array[0]]);  
    }  
    
    • Yo pensé lo mismo, pero al final creo que posponer un cambio inevitable solo hace el problema más grande
      En realidad no parece un gran problema; de los miles de @bitCast en el repositorio de Zig, da la impresión de que muchos menos de 100 se vieron afectados por este cambio
      Sinceramente, tampoco creo que la mayoría de los usuarios de Zig supieran con precisión cómo funcionaba @bitCast al 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 parte
  • Como 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 @bitCast en 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

    • La principal alternativa de Zig a los bit fields de C probablemente sea packed struct y packed union, y ambos están definidos para encajar bien con la nueva definición de @bitCast
      packed struct funciona rellenando los bits de los campos dentro de un “entero subyacente”. Por ejemplo, si los campos son bool, u6, i9 y el entero subyacente es u16, entonces el bit menos significativo de u16 será bool, los siguientes 6 bits serán u6, y los 9 bits restantes serán i9. Es decir, el packed struct de Zig se parece más a azúcar sintáctica sobre varios shifts y masks
      packed union tambié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 @bitCast bajo la nueva semántica. Eso sí, los campos de packed union/packed struct no pueden tener tipos arreglo ni vector
      Personalmente, me parece que estas herramientas encajan muy bien para expresar “estructuras relacionadas con bits”. Puedes empaquetar varios valores en un packed struct para 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 tipos
      Por ejemplo, las banderas de acceso RWX en C podrían recibirse como macros ACCESS_READ, ACCESS_WRITE, ACCESS_EXEC y una API con uint8_t, pero en Zig puedes definir Access = packed struct(u8) con campos read, write, exec, reserved, y hacer que la API reciba Access
      Con packed struct y packed union tambié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 campo n_type peculiar, y eso se puede modelar como bits: packed struct(u8) y stab: enum(u8) dentro de un packed union(u8)
      Al manejar este valor n_type, no hace falta hacer shifts o masking manualmente. Basta con comprobar n_type.bits.is_stab != 0 y, si se cumple, hacer switch sobre n_type.stab; si no, mirar los otros campos de n_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 struct y packed union de Zig. Me parecen herramientas simples, pero bastante buenas