Você provavelmente já ouviu que adicionar final a uma classe a torna mais rápida. Parece quase uma lenda urbana, mas existe um mecanismo preciso por trás disso.
Este artigo dá continuidade ao anterior Swift avançado #4.
A aparentemente óbvia chamada de método é compilada de três formas diferentes. A forma escolhida determina o desempenho e as possibilidades de otimização.
Este capítulo da série avançada é sobre dispatch.
Três faces de uma chamada — direta, por tabela e por mensagem
Um código como object.doSomething() pode ser traduzido para código de máquina de três formas.
Dispatch estático. A função é conhecida em tempo de compilação, então a chamada salta diretamente para seu endereço.
É o mais rápido e, principalmente, o compilador pode fazer inline—eliminar a chamada e inserir o corpo—, abrindo caminho para otimizações posteriores. Métodos de struct, funções globais e métodos final pertencem a esse grupo.
Dispatch por tabela. É a forma usada por métodos de classes que permitem herança e sobrescrita.
Mesmo que o tipo em tempo de compilação de uma variável seja Animal, a instância real pode ser Dog. Só em tempo de execução sabemos qual implementação chamar.
Por isso, cada classe tem uma tabela de funções virtuais (vtable). Na chamada, o índice da função é localizado na tabela da instância e o fluxo salta para lá. O custo do polimorfismo é uma indireção.
Protocolos têm uma equivalente chamada witness table, com a mesma função. É a tabela que vimos ao falar de some e any.
Dispatch por mensagem. É o modelo do runtime do Objective-C, em que as chamadas são resolvidas por nome via objc_msgSend.
É o mais lento dos três, mas oferece flexibilidade extrema, inclusive substituir métodos em tempo de execução. No Swift, métodos marcados com @objc dynamic entram nesse mundo.
O funcionamento interno de objc_msgSend foi tratado em outro artigo; vamos deixá-lo de lado aqui.
No fim, é uma troca entre flexibilidade e velocidade. Quanto mais a decisão fica para o runtime, mais lento; quanto mais é fixada na compilação, mais rápido e otimizado.
Por que final e private melhoram o desempenho
Agora podemos dissecar a lenda urbana. Em que condições o compilador pode rebaixar estaticamente o dispatch por tabela usando devirtualização?
Ele precisa provar que “este método não pode ser sobrescrito”.
final é exatamente essa prova. final class e final func declaram que subclasses não os redefinirão.
O compilador pode remover a consulta à vtable e transformá-la em uma chamada direta, abrindo também caminho para o inline (Swift Optimization Tips).
private produz efeito semelhante. Uma declaração invisível fora do arquivo permite ao compilador ver todos os usos dentro dele e provar que não há sobrescrita.
Com whole-module optimization (WMO, padrão nos builds de release atuais), a prova se estende ao módulo inteiro. Uma classe internal sem subclasses também é tratada automaticamente como final.
A regra prática é simples: marque como final as classes que você não planeja subclassificar.
Isso é correto tanto como declaração de design—o tipo é uma folha da árvore de herança—quanto como prova de otimização para o compilador.
Não espere que “adicionei final e o app ficou mais rápido”. O overhead de chamada é medido em nanossegundos e quase não é percebido fora de loops quentes.
O valor real está nas otimizações encadeadas após o inline, que o compilador faz automaticamente. Nosso trabalho é apenas não bloquear a prova.
A armadilha das extensões de protocolos — requisito ou não?
O conhecimento de dispatch mais brilha na prática—ou, melhor, mais dói quando ignorado—nas implementações padrão de protocolos. Vamos a um quiz.
protocol Greeter {
func hello() // Declarado como requisito
}
extension Greeter {
func hello() { print("Olá") }
func bye() { print("Tchau") } // Não é requisito
}
struct Korean: Greeter {
func hello() { print("Oi") }
func bye() { print("Tchau") }
}
let k: any Greeter = Korean()
k.hello() // ?
k.bye() // ?
A resposta é “Olá” e “Tchau”. hello é um requisito do protocolo, então usa dispatch dinâmico pela witness table, onde a implementação de Korean está registrada.
Já bye é um método exclusivo da extensão, não listado como requisito. Chamado pelo tipo do protocolo, ele salta estaticamente para a implementação da extensão, independentemente do que Korean definir.
Sem essa regra, surge o bug misterioso: “Eu implementei claramente, mas meu código não é chamado”. A solução é simples.
Todo método que os adotantes precisam poder substituir deve ser declarado como requisito no corpo do protocolo. Mantenha a implementação padrão na extensão, mas a declaração no corpo cria uma posição na tabela.
No artigo sobre POP (programação orientada a protocolos), apresentamos extensões de protocolos como alternativa à herança. Esta é a regra de segurança dessa ferramenta.
Verifique com instrumentação — use o profiler, não a intuição
A conclusão sobre dispatch deve sempre trazer o mesmo alerta: isso é micro-otimização, e a ordem importa.
Primeiro, encontre o gargalo real com o Time Profiler do Instruments. A maioria dos problemas de desempenho vem do algoritmo (loops O(n²)), de trabalho desnecessário (recalcular a cada frame) ou de I/O, não do dispatch.
O alerta contra otimização prematura é o mesmo do artigo sobre Knuth.
Se o profiling realmente encontrar dispatch dinâmico dentro de um loop quente, aí entram as opções: tornar o tipo final, trocar any por some ou genéricos para induzir especialização, ou levar o limite do protocolo para fora do loop.
Em outras palavras, o uso normal desse conhecimento é entender o design, não otimizar.
Por que struct é o padrão (favorece dispatch estático), por que some é recomendado em vez de any (permite especialização) e por que o SwiftUI usa views struct.
As grandes decisões da linguagem nascem todas nessa camada; entender dispatch permite ler o design do Swift como uma imagem única.
Resumo
- Chamadas de métodos são compiladas como dispatch estático (direto), por tabela (vtable/witness table) ou por mensagem (objc_msgSend), e flexibilidade e velocidade são inversamente proporcionais.
- final, private e WMO provam “sem sobrescrita”, transformam chamadas dinâmicas em estáticas e abrem caminho para o inline. Em classes sem herança, final é o básico.
- Métodos de extensões de protocolos usam dispatch diferente conforme sejam ou não declarados como requisitos. Métodos substituíveis devem ser declarados no corpo do protocolo.
- A ordem é: profiler primeiro. O valor cotidiano do conhecimento de dispatch não é a técnica de otimização, mas a capacidade de entender o design da linguagem.
No próximo capítulo, descemos mais uma camada: layout de memória—como o tamanho de um struct é definido, por que a ordem das propriedades altera a memória e o tamanho real de uma caixa any.
Continue lendo
Fontes e verificação
- Swift Optimization TipsSwift 프로젝트 · Documentação oficial · Consultado 17 de agosto de 2026Evidência: Dispatch estático e dinâmico, final, whole-module optimization e características de desempenho

![Imagem de capa de [Swift avançado #5] Dispatch em Swift: por que final melhora o desempenho](/assets/images/posts/f974f02c-833c-4ab1-9350-a2e3543e8391/swift-method-dispatch-1.jpg)