Respuesta a la pregunta.
Rust emplea una estrategia de optimización de diseño conocida como llenado de valores nicho para eliminar la sobrecarga de almacenamiento de los discriminantes de enum cuando las variantes contienen tipos con patrones de bits inválidos. El compilador identifica valores "nicho" dentro del rango representable de un tipo, como el valor cero para NonZeroU32 o el puntero nulo para referencias, y reasigna estos patrones de bits para codificar otras variantes de enum como None. Esta transformación se basa en que el tipo de carga útil posea un rango de validez restringido definido por sus propiedades intrínsecas o atributos internos de rustc_layout. Para que un tipo sirva como transportador nicho válido, debe presentar al menos un patrón de bits que constituya comportamiento indefinido para construir o leer, permitiendo así al compilador reservar ese patrón para las variantes alternativas del enum sin asignar espacio adicional para el discriminante.
Situación de la vida real
Al desarrollar un motor de trading de alta frecuencia, nuestro equipo encontró una severa presión en la caché al almacenar millones de marcas de tiempo de órdenes en un Vec<Option<u64>>. Cada marca de tiempo opcional consumía 16 bytes debido a la alineación y la sobrecarga del discriminante, a pesar de que las marcas de tiempo en sí mismos eran estrictamente valores Unix positivos. Necesitábamos urgentemente reducir la huella de memoria sin sacrificar la seguridad ni recurrir a punteros crudos que complicarían las garantías de Send y Sync requeridas para el procesamiento entre hilos.
Una de las aproximaciones consideradas fue el empacado manual de bits usando valores crudos u64 y valores centinela cero con funciones de conversión inseguras. Esta solución prometía una máxima eficiencia de memoria, pero introducía riesgos catastróficos: un error lógico podría construir un NonZeroU64 inválido o desreferenciar un puntero nulo disfrazado como cero, violando las invariantes de seguridad de memoria de Rust. Además, requeriría extensas auditorías y bloques unsafe que el equipo buscaba evitar.
Otro candidato involucraba el uso de Optionstd::num::NonZeroU64 directamente, aprovechando la optimización nicho garantizada por la biblioteca estándar. Este enfoque mantenía la seguridad total del tipo y expresiones match ergonómicas, asegurando que el Option ocupase exactamente 8 bytes en lugar de 16. La principal restricción era que teníamos que garantizar que las marcas de tiempo nunca fueran cero, lo cual era cierto para nuestra lógica de dominio, ya que todas las marcas de tiempo eran posteriores a 1970.
Seleccionamos la segunda solución, refactorizando nuestro nuevo tipo Timestamp para envolver NonZeroU64 y validar las entradas en el límite del sistema. El resultado fue una reducción del 50% en el uso de memoria para nuestra caché principal del libro de órdenes. Esta optimización eliminó el thrashing de cache y mejoró la latencia de búsqueda en un 30%, todo logrado sin una sola línea de código unsafe.
Lo que a menudo omiten los candidatos
¿Por qué Option<u32> consume 8 bytes mientras que Option<NonZeroU32> consume solo 4, y cómo se comporta esta optimización con tipos anidados como Option<Option<NonZeroU32>>?
El tipo u32 admite todos los 2^32 patrones de bits como válidos, sin dejar ningún patrón de bits "sobrante" para que el compilador lo reutilice como variante None. Como resultado, el compilador debe agregar un byte de discriminante (rellenado a 4 bytes para alineación), dando un total de 8 bytes. En cambio, NonZeroU32 declara explícitamente que el patrón de bits 0x00000000 es inválido, creando un nicho que Rust utiliza para codificar None, permitiendo que el Option resultante ocupe exactamente 4 bytes.
Para estructuras anidadas, la optimización se encadena de manera efectiva: Option<Option<NonZeroU32>> sigue ocupando 4 bytes porque el Option externo utiliza un patrón de bits inválido diferente (por ejemplo, 0x00000001) del espacio de nicho disponible de NonZeroU32. Esta optimización recursiva continúa siempre que el tipo transportador posea suficientes patrones de bits inválidos para acomodar todos los valores de discriminante de enum.
¿Cómo interactúan los atributos de diseño explícitos como #[repr(C)] o #[repr(u8)] con la optimización nicho, y por qué importa esta interacción en los límites de FFI?
Al aplicar #[repr(C)] o #[repr(u8)], el programador dicta un diseño de memoria fijo donde el discriminante ocupa un desplazamiento específico con un tamaño definido. Esta representación explícita desactiva efectivamente la optimización nicho, asegurando la compatibilidad de ABI con estructuras C que esperan etiquetas explícitas, pero obligando al enum a consumir espacio adicional para el discriminante.
En contextos de FFI, esta distinción resulta crítica, ya que el código C espera el discriminante en un desplazamiento predecible y estable. Pasar un enum de Rust optimizado por nicho que carece de atributos repr explícitos a través de la frontera resulta en comportamiento indefinido, mientras que #[repr(C)] garantiza la estabilidad del diseño al costo necesario de la eficiencia de memoria.
¿Qué impide que MaybeUninit<T> sirva como transportador nicho para la optimización de enum incluso cuando T posee patrones de bits inválidos, como en Option<MaybeUninit<NonZeroU32>>?
MaybeUninit<T> está diseñado arquitectónicamente para contener cualquier patrón de bits sin invocar comportamiento indefinido, ya que su propósito es representar memoria potencialmente no inicializada. En consecuencia, el compilador trata a MaybeUninit<T> como si no tuviera patrones de bits inválidos, lo que significa que su rango de validez abarca todas las combinaciones de bits posibles de 2^(8*sizeof(T)). Esta validez total elimina cualquier nicho disponible que podría ser reutilizado para la optimización de enum, independientemente de las propiedades de T.
Por lo tanto, Option<MaybeUninit<NonZeroU32>> ocupa 8 bytes—el tamaño de MaybeUninit<u32> más el relleno del discriminante—aunque el subyacente NonZeroU32 tenga una validez restringida. Este comportamiento ilustra que la optimización nicho opera estrictamente en las restricciones de validez del tipo inmediato y no en las propiedades transitivas de sus contenidos potenciales.