Swift e Objective-C

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

O tamanho de um struct não é simplesmente a soma dos tamanhos de suas propriedades. Reunimos medições de size, stride e alignment, padding, Optional com extra inhabitant e o existential container de 64 bits.

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

Se na parte sobre dispatch vimos “como uma chamada é traduzida”, agora veremos “como um valor é disposto”.

Este conteúdo dá continuidade ao artigo anterior Swift avançado #5.

O tamanho de um struct é a soma dos tamanhos de suas 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 para centenas de milhares de elementos em um array e quando o gráfico de memória no Instruments está mais pesado que o esperado.

E também para responder à pergunta deixada na parte sobre some·any: “qual é o tamanho real dessa caixa?”.

Três medidas — size, stride e alignment

Swift fornece 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. Ela não inclui o tail padding para alinhamento no fim do tipo.

alignment (alinhamento) é a regra dos endereços em que o tipo pode ser colocado. A CPU lê um valor de 8 bytes com mais eficiência em um endereço múltiplo de 8, algo que pode ser obrigatório dependendo da arquitetura.

Por isso, cada tipo tem a restrição de ser colocado em um endereço múltiplo de n. Os números abaixo valem para as plataformas Apple atuais de 64 bits: o alignment de Double é 8 e o de Bool é 1.

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

Este é o 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 colocado 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 trocar a ordem economiza 33% em um array. Bad precisa de 7 bytes de padding para alinhar value depois de flag (Swift ABI Type Layout).

Diagrama de comparação de alinhamento: ao mudar apenas a ordem das mesmas três caixas, 24 bytes viram 16
Com o mesmo conteúdo, mudar apenas a ordem faz 24 virar 16

Na prática, agrupar primeiro as propriedades com maior alignment é uma heurística comum para reduzir o padding. Porém, ela 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 valores observados.

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 sai de graça

O layout de enum revela bem a inteligência do compilador do Swift. É preciso armazenar um discriminador de caso (tag), mas ele é escondido nos padrões de bits não usados (extra inhabitants) do tipo de payload.

O exemplo clássico é Bool?. Nas plataformas Apple atuais 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 Optional de tipos por referência. Os ponteiros de classes do Swift não usam null como objeto, então nil é representado por 0. Em 64 bits, o size de AnyObject? é 8.

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

A expressão da parte sobre Optional, “segurança é quase de graça”, é precisa para tipos que podem reutilizar representações extras. A própria 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 custo”; agora podemos medir.

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

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

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

Se ultrapassar as condições, o valor é alocado em uma área de armazenamento separada e o buffer recebe um ponteiro. Podem ser adicionados custos de alocação no heap, contagem de referências e indireção (Swift ABI · SE-0335).

Seção do buffer de valores de 3 words de um existential de protocolo único e das tabelas de metadados e witness
Um existential de protocolo único não restrito a classe ocupa 5 words em 64 bits; valores que ultrapassam 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 o número de ponteiros para witness tables.

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

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

Vamos abordar também as medidas de instâncias de classe. Nas plataformas Apple atuais 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 Swift no heap geralmente têm um header composto por um ponteiro de metadados e informações inline de contagem de referências; em 64 bits, ele ocupa 16 bytes. O tamanho real alocado 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. Criar centenas de milhares de pequenos fragmentos de dados como classes pode fazer o custo de gerenciamento superar o dos dados.

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

Vamos organizar as ferramentas práticas para lidar com essa camada.

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

Você também pode incluir um orçamento nos testes unitários, como #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 feito 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 plataformas Apple atuais de 64 bits, MemoryLayout<[Int]>.stride é 8. Um valor Array é um struct pequeno que referencia indiretamente o armazenamento no heap, mas não se deve tratar esse número como ABI fixa.

É por isso que um struct que contém um array não deve ser considerado “leve” apenas porque seu stride é pequeno.

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

Por fim, é preciso manter o equilíbrio. O conhecimento desta parte não é usado todos os dias.

Mas o valor de saber “por quê” aparece nos problemas. Quando o gráfico de memória parece estranho, você pode começar por uma lista de suspeitos em vez de adivinhar — essa é a vantagem de quem já desceu uma vez a essa camada.

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 posições contíguas. Em structs produzidos em massa, agrupar propriedades com maior alignment costuma reduzir o padding, mas é obrigatório medir.
  • enum pode esconder a tag nos extra inhabitants do payload. Bool? e Optional 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 ultrapassar as condições do buffer de 3 words, podem surgir armazenamento separado e custos de indireção.
  • O header básico de objetos nativos atuais do Swift em 64 bits geralmente ocupa 16 bytes. O tamanho real alocado pode ser maior conforme o alinhamento das propriedades e do allocator.
  • As ferramentas são MemoryLayout (validação do projeto) e Instruments Allocations (medição real). Otimizar o layout sem medir é otimização prematura.

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

Fontes e critérios de verificação

  • MemoryLayout — Apple Developer Documentation · verificado em 2026-08-19 · base: definições de API de size·alignment·stride
  • Swift ABI Type Layout — projeto Swift · documentação oficial · verificado em 2026-08-19 · base: layout de struct·enum·existential container
  • SE-0260 Library Evolution — Swift Evolution · verificado em 2026-08-19 · base: escopo das garantias de linguagem e 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 de existential e custos de memória dinâmica
  • Swift HeapObject · Swift RefCount — Swift Runtime · verificado em 2026-08-19 · base: header de objetos nativos no heap e contagem inline de referências

Continue lendo