Swift y Objective-C

[Swift avanzado #6] Layout de memoria en Swift: el orden cambia el tamaño

El tamaño de un struct no es la simple suma de sus propiedades. Resumen de mediciones de size, stride, alignment, padding y Optional con extra inhabitant, además del existential container de 64 bits.

8 min de lectura
Imagen de portada de [Swift avanzado #6] Layout de memoria en Swift: el orden cambia el tamaño

Si en la entrega sobre dispatch vimos «cómo se traduce una llamada», ahora veremos «cómo se coloca un valor».

Este contenido continúa el artículo anterior Swift avanzado #5.

¿El tamaño de un struct es la suma del tamaño de sus propiedades? No.

¿Creerías que cambiar el orden de declaración de las propiedades puede cambiar el tamaño de un struct? Esta es la entrega sobre layout de memoria de la serie avanzada.

Hay momentos claros en los que este conocimiento resulta necesario: al diseñar modelos con cientos de miles de elementos en un array y cuando el gráfico de memoria en Instruments se ve más grande de lo esperado.

También sirve para responder a la pregunta pendiente de la entrega sobre some·any: «¿cuánto mide realmente esa caja?»

Tres medidas: size, stride y alignment

Swift ofrece herramientas estándar para consultar las medidas de memoria de un tipo. Es MemoryLayout.

MemoryLayout<Int>.size       // 8
MemoryLayout<Bool>.size      // 1

struct Point { var x: Double; var y: Double }
MemoryLayout<Point>.size     // 16

Son tres medidas, y cada una tiene un significado distinto.

size es la huella de memoria contigua de un único valor. No incluye el tail padding necesario para la alineación al final del tipo.

alignment (alineación) define las reglas de las direcciones en las que puede colocarse el tipo. La CPU lee con mayor eficiencia un valor de 8 bytes desde una dirección múltiplo de 8 (y según la arquitectura, puede ser obligatorio).

Por eso cada tipo tiene la restricción de «colocarse en una dirección múltiplo de n». Los valores siguientes corresponden actualmente a plataformas Apple de 64 bits: la alignment de Double es 8 y la de Bool, 1.

stride (paso) es la distancia hasta el siguiente elemento de un array. Como la restricción de alineación puede dejar espacio vacío (padding) entre elementos, stride ≥ size.

Este es un ejemplo clásico que muestra la relación entre las tres medidas.

struct Bad {
    var flag: Bool      // 1bytes
    var value: Double   // 8bytes, debe colocarse en una dirección múltiplo de — 8
    var count: Int32    // 4bytes
}
MemoryLayout<Bad>.stride   // 24

struct Good {
    var value: Double   // 8
    var count: Int32    // 4
    var flag: Bool      // 1
}
MemoryLayout<Good>.stride  // 16

El contenido es el mismo, pero cambiar el orden ahorra un 33 % según el array. En Bad hacen falta 7 bytes de padding para alinear value después de flag (Swift ABI Type Layout).

Diagrama de alineación: los mismos tres bloques pasan de 24 a 16 bytes al cambiar solo el orden
Con el mismo contenido, cambiar solo el orden reduce 24 a 16

En la práctica, agrupar primero las propiedades con mayor alignment es una heurística habitual para reducir el padding. Sin embargo, no garantiza el óptimo para todos los tipos; el resultado final debe medirse con MemoryLayout.

Actualmente, el algoritmo de layout de fragile struct de Swift coloca las propiedades almacenadas en el orden de declaración. Pero el orden de los campos no está garantizado por el lenguaje ni por la ABI (Swift ABI · SE-0260). El código que necesita offsets exactos no debe depender de observaciones.

En segundo lugar, esta optimización solo importa cuando hay muchas instancias. A diferencia de los 8 bytes de un objeto de configuración, 8 bytes por elemento en un array de un millón de elementos suman 8 MB (Swift ABI Type Layout).

Como siempre, primero hay que medir.

La magia de enum: cuando la etiqueta de Optional es gratis

El layout de enum muestra claramente la inteligencia del compilador de Swift. Debe almacenar un discriminador de casos (etiqueta), así que lo oculta en los patrones de bits no utilizados (extra inhabitants) del tipo de payload.

El caso representativo es Bool?. En las plataformas Apple actuales de 64 bits, MemoryLayout<Bool?>.size es 1.

Bool solo usa dos patrones de su byte (0 y 1), así que uno de los patrones restantes se utiliza para representar nil.

Lo mismo ocurre con los optionals de tipos por referencia. Los punteros de clases Swift no usan null como objeto, así que nil se representa como 0. En 64 bits actualmente, el size de AnyObject? es 8.

Pero no todos los Optional son gratis. Int usa los patrones de bits de 8 bytes para representar valores. Por tanto, en el entorno actual, el size de Int? es 9 y su stride es 16 (Swift ABI).

La expresión de la entrega sobre optionals, «la seguridad es casi gratis», es exacta para los tipos que pueden reutilizar representaciones sobrantes. La sintaxis Optional no garantiza coste de memoria cero para todos los tipos.

Medición de la caja: un existential de protocolo único ocupa 5 words

En la entrega sobre some·any solo dijimos que «any es una caja y tiene un coste»; ahora podemos medirlo.

protocol Shape {}
MemoryLayout<any Shape>.size   // 40 — entorno de 64bits actual

Un any Shape de protocolo único sin restricción de clase ocupa 40 bytes, es decir, 5 words, en el entorno actual de 64 bits. Está compuesto por un buffer de valores de 3 words, un puntero a los metadatos del tipo y un puntero a una witness table.

Aquí aparece una bifurcación importante. Si el size y la alignment del valor almacenado cumplen las condiciones del buffer inline de 3 words, se guarda directamente en el buffer.

Si supera las condiciones, el valor se asigna en un espacio de almacenamiento separado y el buffer contiene un puntero. Esto puede añadir costes de asignación en el heap, conteo de referencias y acceso indirecto (Swift ABI · SE-0335).

Sección del existential de protocolo único: buffer de valores de 3 words, metadatos y tabla de witnesses
Un existential de protocolo único sin clase ocupa 5 words en 64 bits; los valores que superan las condiciones del buffer usan almacenamiento separado

La cifra de 5 words no es el tamaño fijo de todos los tipos any. Las combinaciones de protocolos pueden aumentar los punteros a witness tables.

Los existentials de protocolos exclusivos de clase usan un layout más pequeño, sin buffer de valores ni puntero adicional a metadatos.

Si el código orientado a protocolos resulta inesperadamente lento, puedes considerar eliminar la caja mediante some·genéricos o reducir el tipo almacenado. El efecto real debe comprobarse con Instruments y benchmarks (SE-0335 Existential any).

Veamos también las medidas de las instancias de clase. En las plataformas Apple actuales de 64 bits, el stride del valor de un tipo de clase es 8: es el tamaño de la referencia, no del objeto.

Los objetos nativos del heap de Swift suelen tener un header compuesto por un puntero a metadatos e información de conteo de referencias inline; en 64 bits ocupa 16 bytes. El tamaño real de la asignación puede ser mayor por la alineación de las propiedades y el redondeo del allocator (Swift HeapObject · Swift RefCount).

El coste oculto de los tipos por referencia mencionado en la entrega sobre ARC incluye este header y las operaciones de asignación, liberación y conteo. Si conviertes decenas de miles de pequeños fragmentos de datos en clases, el coste de gestión puede superar al de los datos.

Caja de herramientas práctica: volver a medir, comprobar y cuestionar

Estas son las herramientas prácticas para trabajar con este nivel.

Validar hipótesis con MemoryLayout. Al diseñar structs de modelos producidos en grandes cantidades, basta con adquirir el hábito de medir una vez el stride.

También puedes añadir un presupuesto a las pruebas unitarias, como #expect(MemoryLayout<Tick>.stride <= 32). Si más adelante se añade una propiedad y se supera el presupuesto, quedará claro de inmediato.

Comprobar el heap con Instruments Allocations. Si «el código usa principalmente structs, pero hay muchas asignaciones en el heap», los sospechosos están claros.

La caja any, las capturas de closures (entrega sobre closures), el almacenamiento CoW (copy-on-write) y las instancias de clase. La agregación por tipo de Allocations indica cuál de ellos está implicado.

La ilusión de las colecciones copy-on-write: En las plataformas Apple actuales de 64 bits, MemoryLayout<[Int]>.stride es 8. Un valor Array es un struct pequeño que referencia indirectamente el almacenamiento del heap, pero no debe asumirse esta cifra como ABI fija.

Por eso no debes concluir que un struct que contiene un array es «ligero» solo porque su stride sea pequeño.

El peso real está en el almacenamiento del heap, y eso lo muestra Allocations. El principio CoW es el descrito en el artículo independiente.

Por último, mantengamos la perspectiva. Este conocimiento no se utiliza a diario.

Pero el valor de saber «por qué» aparece cuando surgen problemas. Cuando el gráfico de memoria es extraño, puedes empezar con una lista de sospechosos en lugar de adivinar; esa es la ventaja de quien ha descendido una vez a este nivel.

Resumen

  • Las medidas de un tipo son tres. size es la huella sin tail padding, alignment es la regla de alineación de direcciones y stride es el intervalo entre elementos contiguos. En structs producidos en grandes cantidades, agrupar propiedades con mayor alignment suele reducir el padding, pero siempre hay que medir.
  • enum puede ocultar la etiqueta en los extra inhabitants del payload. Bool? y los optionals de referencias no necesitan espacio adicional, pero tipos como Int? requieren espacio separado para la etiqueta.
  • Un existential de protocolo único sin clase ocupa 5 words en 64 bits. Si el size o la alignment del valor supera las condiciones del buffer de 3 words, pueden aparecer almacenamiento separado y costes indirectos.
  • El header básico de los objetos nativos de Swift en 64 bits ocupa generalmente 16 bytes. El tamaño real de la asignación puede aumentar según la alineación de las propiedades y del allocator.
  • Las herramientas son MemoryLayout (validación del diseño) e Instruments Allocations (medición real). Optimizar el layout sin medir es optimización prematura.

La próxima entrega trata la magia del tiempo de compilación: los mecanismos que escriben código detrás de #Preview y @Observable, los macros de Swift.

Fuentes y criterios de verificación

  • MemoryLayout — Apple Developer Documentation · verificado el 2026-08-19 · fundamento: definiciones de API de size, alignment y stride
  • Swift ABI Type Layout — Proyecto Swift · documentación oficial · verificado el 2026-08-19 · fundamento: layout de struct, enum y existential container
  • SE-0260 Library Evolution — Swift Evolution · verificado el 2026-08-19 · fundamento: alcance de las garantías del lenguaje y la ABI sobre el orden de campos de struct
  • SE-0335 Existential any — Swift Evolution · verificado el 2026-08-19 · fundamento: buffer inline de 3 words del existential y costes de memoria dinámica
  • Swift HeapObject · Swift RefCount — Runtime de Swift · verificado el 2026-08-19 · fundamento: header de objetos nativos del heap y conteo de referencias inline

Seguir leyendo

Serie Swift avanzado