Swift e Objective-C

[Swift avançado #6] Layout de memória em Swift: a ordem muda o tamanho

O tamanho de um struct não é a simples soma de suas propriedades. Resumo de medições de size, stride, alignment, padding e Optional com extra inhabitant, além do existential container de 64 bits.

8 min de leitura
Imagem de capa de [Swift avançado #6] Layout de memória em Swift: a ordem muda o tamanho

Se na parte sobre dispatch vimos «como uma chamada é traduzida», agora veremos «como um valor é armazenado».

Este conteúdo continua o artigo anterior Swift avançado #5.

O tamanho de um struct é a soma do tamanho das propriedades? Não.

Você acreditaria que mudar a ordem de declaração das propriedades pode alterar o tamanho de um struct? Esta é a parte sobre layout de memória da série avançada.

Há momentos claros em que esse conhecimento é necessário: ao projetar modelos com centenas de milhares de elementos em um array e quando o gráfico de memória no Instruments está maior que o esperado.

Também serve para responder à pergunta deixada para a parte sobre some·any: «qual é o tamanho real dessa caixa?»

Três medidas — size, stride e alignment

Swift oferece ferramentas padrão para consultar as medidas de memória de um tipo. É MemoryLayout.

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

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

São três medidas, e cada uma tem um significado diferente.

size é a pegada de memória contígua de um único valor. Não inclui o tail padding necessário para o alinhamento no fim do tipo.

alignment (alinhamento) define as regras dos endereços onde o tipo pode ser armazenado. A CPU lê um valor de 8 bytes com mais eficiência a partir de um endereço múltiplo de 8 (e, dependendo da arquitetura, isso pode ser obrigatório).

Por isso, cada tipo tem a restrição de «ser armazenado em um endereço múltiplo de n». Os valores abaixo são das atuais plataformas Apple de 64 bits: o alignment de Double é 8 e o de Bool é 1.

stride (passo) é a distância até o próximo elemento de um array. Como a restrição de alinhamento pode criar espaço vazio (padding) entre os elementos, stride ≥ size.

Este é um exemplo clássico que mostra a relação entre as três medidas.

struct Bad {
    var flag: Bool      // 1bytes
    var value: Double   // 8bytes, deve ser armazenado em um endereço 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

O conteúdo é o mesmo, mas mudar a ordem economiza 33% considerando o array. Em Bad, são necessários 7 bytes de padding para alinhar value depois de flag (Swift ABI Type Layout).

Diagrama de alinhamento: os mesmos três blocos passam de 24 para 16 bytes mudando apenas a ordem
Com o mesmo conteúdo, mudar apenas a ordem reduz 24 para 16

Na prática, agrupar primeiro as propriedades com maior alignment é uma heurística comum para reduzir o padding. Porém, isso não garante o ótimo para todos os tipos; o resultado final deve ser medido com MemoryLayout.

Atualmente, o algoritmo de layout de fragile struct do Swift posiciona as propriedades armazenadas na ordem de declaração. Mas a ordem dos campos não é garantida pela linguagem nem pela ABI (Swift ABI · SE-0260). Código que precisa de offsets exatos não deve depender de observações.

Segundo, essa otimização só importa quando há muitas instâncias. Diferentemente dos 8 bytes de um objeto de configuração, 8 bytes por elemento em um array de um milhão de elementos totalizam 8 MB (Swift ABI Type Layout).

Como sempre, medir vem primeiro.

A magia de enum — quando a tag de Optional é gratuita

O layout de enum mostra bem a inteligência do compilador do Swift. É preciso armazenar um discriminador de caso (tag), então ele fica oculto nos padrões de bits não utilizados (extra inhabitants) do tipo de payload.

O caso clássico é Bool?. Nas atuais plataformas Apple de 64 bits, MemoryLayout<Bool?>.size é 1.

Bool usa apenas dois padrões de seu byte (0 e 1), então um dos padrões restantes é usado para representar nil.

O mesmo vale para optionals de tipos por referência. Os ponteiros de classes Swift não usam null como objeto, então nil é representado por 0. Atualmente, em 64 bits, o size de AnyObject? é 8.

Mas nem todo Optional é gratuito. Int usa os padrões de bits de 8 bytes para representar valores. Portanto, no ambiente atual, o size de Int? é 9 e o stride é 16 (Swift ABI).

A expressão da parte sobre optionals, «segurança é quase gratuita», é precisa para tipos que podem reutilizar representações extras. A sintaxe Optional não garante custo de memória zero para todos os tipos.

Medição da caixa — um existential de protocolo único ocupa 5 words

Na parte sobre some·any, dissemos apenas que «any é uma caixa e tem um custo»; agora podemos medi-lo.

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

Um any Shape de protocolo único sem restrição de classe ocupa 40 bytes, ou 5 words, no ambiente atual de 64 bits. A composição é: buffer de valores de 3 words + ponteiro para os metadados do tipo + um ponteiro para a witness table.

Aqui surge uma divisão importante. Se o size e o alignment do valor armazenado atenderem às condições do buffer inline de 3 words, ele será armazenado diretamente no buffer.

Se ultrapassar as condições, o valor será alocado em um espaço de armazenamento separado, e o buffer conterá um ponteiro. Isso pode adicionar custos de alocação no heap, contagem de referências e acesso indireto (Swift ABI · SE-0335).

Seção do existential de protocolo único: buffer de valores de 3 words, metadados e tabela de witnesses
Um existential de protocolo único sem classe ocupa 5 words em 64 bits; valores que excedem as condições do buffer usam armazenamento separado

O valor de 5 words não é o tamanho fixo de todos os tipos any. Combinações de protocolos podem aumentar os ponteiros para witness tables.

Existentials de protocolos exclusivos de classe usam um layout menor, sem buffer de valores nem ponteiro separado para metadados.

Se o código orientado a protocolos estiver inesperadamente lento, considere remover a caixa usando some·genéricos ou reduzir o tipo armazenado. O efeito real deve ser verificado com Instruments e benchmarks (SE-0335 Existential any).

Vamos abordar também as medidas de instâncias de classe. Nas atuais plataformas Apple de 64 bits, o stride do valor de um tipo de classe é 8: é o tamanho da referência, não do objeto.

Objetos nativos do heap do Swift geralmente têm um header formado por um ponteiro para metadados e informações inline de contagem de referências; em 64 bits, ele ocupa 16 bytes. O tamanho real da alocação pode ser maior devido ao alinhamento das propriedades e ao arredondamento do allocator (Swift HeapObject · Swift RefCount).

O custo oculto dos tipos por referência mencionado na parte sobre ARC inclui esse header e as operações de alocação, liberação e contagem. Se você transformar dezenas de milhares de pequenos fragmentos de dados em classes, o custo de gerenciamento pode superar o dos dados.

Caixa de ferramentas prática — medir novamente, conferir e questionar

Estas são as ferramentas práticas para trabalhar nesse nível.

Validar hipóteses com MemoryLayout. Ao projetar structs de modelos produzidos em grande escala, basta criar o hábito de medir o stride uma vez.

Também é possível incluir um orçamento nos testes unitários, como em #expect(MemoryLayout<Tick>.stride <= 32). Se uma propriedade for adicionada depois e o orçamento for excedido, isso ficará evidente imediatamente.

Verificar o heap com Instruments Allocations. Se «o código foi escrito principalmente com structs, mas há muitas alocações no heap», os suspeitos são conhecidos.

A caixa any, capturas de closures (parte sobre closures), armazenamento CoW (copy-on-write) e instâncias de classe. A agregação por tipo do Allocations mostra qual deles está envolvido.

A ilusão das coleções copy-on-write: Nas atuais plataformas Apple de 64 bits, MemoryLayout<[Int]>.stride é 8. Um valor Array é um struct pequeno que referencia indiretamente o armazenamento no heap, mas esse número não deve ser tratado como ABI fixa.

Esse é o motivo para não concluir que um struct que contém um array é «leve» só porque seu stride é pequeno.

O peso real está no armazenamento do heap, e o Allocations o mostra. O princípio CoW é o descrito no artigo separado.

Por fim, é preciso ter equilíbrio. Esse conhecimento não é usado todos os dias.

Mas o valor de saber «por quê» aparece quando surgem problemas. Quando o gráfico de memória está estranho, você pode começar por uma lista de suspeitos em vez de fazer suposições; essa é a vantagem de quem já desceu uma vez até esse nível.

Resumo

  • As medidas de um tipo são três. size é a pegada sem tail padding, alignment é a regra de alinhamento dos endereços e stride é o intervalo entre elementos contíguos. Em structs produzidos em grande escala, agrupar propriedades com maior alignment costuma reduzir o padding, mas é preciso medir.
  • enum pode ocultar a tag nos extra inhabitants do payload. Bool? e optionals de referências não precisam de espaço adicional, mas tipos como Int? precisam de espaço separado para a tag.
  • Um existential de protocolo único sem classe ocupa 5 words em 64 bits. Se o size ou o alignment do valor exceder as condições do buffer de 3 words, poderão surgir armazenamento separado e custos indiretos.
  • O header básico dos objetos nativos do Swift em 64 bits geralmente ocupa 16 bytes. O tamanho real da alocação pode ser maior conforme o alinhamento das propriedades e do allocator.
  • As ferramentas são MemoryLayout (validação do design) e Instruments Allocations (medição real). Otimizar o layout sem medir é otimização prematura.

A próxima parte trata da magia do tempo de compilação: os mecanismos que escrevem código por trás de #Preview e @Observable, os macros do Swift.

Fontes e critérios de verificação

  • MemoryLayout — Apple Developer Documentation · verificado em 2026-08-19 · base: definições de API de size, alignment e stride
  • Swift ABI Type Layout — Projeto Swift · documentação oficial · verificado em 2026-08-19 · base: layout de struct, enum e existential container
  • SE-0260 Library Evolution — Swift Evolution · verificado em 2026-08-19 · base: escopo das garantias da linguagem e da ABI sobre a ordem dos campos de struct
  • SE-0335 Existential any — Swift Evolution · verificado em 2026-08-19 · base: buffer inline de 3 words do existential e custos de memória dinâmica
  • Swift HeapObject · Swift RefCount — Runtime do Swift · verificado em 2026-08-19 · base: header de objetos nativos do heap e contagem inline de referências

Continue lendo

Série Swift avançado