Introdução
A questão “Zig ou Assembly?” pode parecer estranha à primeira vista — são linguagens de níveis de abstração bastante diferentes. Porém, Zig opera tão próximo do hardware que a pergunta é genuinamente relevante para desenvolvedores de sistemas embarcados, kernels, drivers e código de alta performance.
Este artigo analisa quando Zig é suficiente para trabalho de baixo nível e quando Assembly puro (ou inline assembly em Zig) é a escolha certa. Para comparações em nível mais alto, veja Zig vs C Moderno (C23) e Zig vs Fortran para Computação Científica.
O Que é Assembly e Por Que Ainda Importa
Assembly é a representação textual das instruções de máquina de um processador. Cada instrução Assembly corresponde diretamente a uma instrução que o CPU executa. Não há abstração — você controla registradores, endereços de memória e flags diretamente.
Assembly ainda é usado em cenários específicos:
- Bootloaders e código de inicialização: As primeiras instruções executadas por um CPU precisam ser Assembly
- Instruções específicas de CPU: Instruções de criptografia (AES-NI), virtualização (VMX), ou controle de cache
- Rotinas de interrupção: Handlers de interrupção que precisam salvar/restaurar estado de registradores
- Otimização extrema: Loops críticos onde cada ciclo importa
- Exploração de segurança: Análise de vulnerabilidades e engenharia reversa
O Que Zig Oferece que Assembly Não Oferece
Portabilidade
Código Zig compila para mais de 40 arquiteturas. Um algoritmo escrito em Zig funciona em x86_64, ARM, RISC-V, WebAssembly e mais. Assembly é específico por arquitetura — código x86 não roda em ARM.
// Este código compila para QUALQUER arquitetura suportada
fn somaVetorial(a: []const f32, b: []const f32, resultado: []f32) void {
for (a, b, resultado) |x, y, *r| {
r.* = x + y;
}
}
O compilador LLVM aplica otimizações específicas de cada arquitetura automaticamente, incluindo vetorização e uso de instruções SIMD quando possível. Veja nosso tutorial de cross-compilation em Zig.
Segurança em Tempo de Compilação e Runtime
Zig detecta erros que Assembly ignora silenciosamente:
const std = @import("std");
fn acessoSeguro(dados: []const u8, indice: usize) ?u8 {
if (indice >= dados.len) return null;
return dados[indice];
}
// Em modo debug, bounds checking automático:
fn exemplo(dados: []const u8) void {
_ = dados[999]; // panic em debug se dados.len <= 999
}
Veja Tratamento de Erros em Zig e Padrões Errdefer.
Produtividade
Um algoritmo de ordenação em Zig ocupa 20 linhas. Em Assembly x86_64, o mesmo algoritmo pode facilmente ocupar 200 linhas. Zig oferece structs, enums, error handling, iteradores, e todas as abstrações necessárias sem custo de runtime.
Quando Assembly é Necessário
Código de Boot
O primeiro código executado por um processador precisa ser Assembly. Zig não pode configurar a stack inicial ou entrar em modo protegido sem assembly:
// Zig pode usar inline assembly para boot code
export fn _start() callconv(.naked) noreturn {
asm volatile (
\\mov $0x7C00, %esp
\\call kmain
);
unreachable;
}
Instruções Específicas de CPU
Algumas instruções não têm equivalente em Zig e precisam de inline assembly:
// Ler o timestamp counter do CPU
fn rdtsc() u64 {
var low: u32 = undefined;
var high: u32 = undefined;
asm volatile ("rdtsc"
: "={eax}" (low), "={edx}" (high)
);
return (@as(u64, high) << 32) | low;
}
// Flush de cache line
fn clflush(addr: *const anyopaque) void {
asm volatile ("clflush (%[addr])"
:
: [addr] "r" (addr)
: "memory"
);
}
Otimização de Loops Críticos
Em casos raros, o compilador LLVM pode não gerar o código ideal para um loop crítico. Inline assembly permite controle total:
// Cópia otimizada com REP MOVSB em x86_64
fn copiaRapida(dest: [*]u8, src: [*]const u8, n: usize) void {
asm volatile (
"rep movsb"
:
: [dest] "{rdi}" (dest),
[src] "{rsi}" (src),
[n] "{rcx}" (n)
: "rdi", "rsi", "rcx", "memory"
);
}
Inline Assembly em Zig
Zig oferece suporte robusto a inline assembly, o que significa que raramente é necessário escrever arquivos Assembly separados. A sintaxe de inline assembly de Zig é mais clara que a de C/GCC:
fn multiplicar_e_somar(a: u64, b: u64, c: u64) u64 {
// Usando a instrução MULX + ADCX quando disponível
var resultado: u64 = undefined;
asm volatile (
\\mulx %[b], %[low], %[high]
\\add %[c], %[low]
: [low] "=r" (resultado)
: [a] "{rdx}" (a),
[b] "r" (b),
[c] "r" (c)
: "flags"
);
return resultado;
}
SIMD: Zig vs Assembly Manual
Zig oferece tipos vetoriais nativos que o compilador traduz para instruções SIMD automaticamente:
const Vec4f = @Vector(4, f32);
fn somaSimd(a: Vec4f, b: Vec4f) Vec4f {
return a + b; // Compila para addps/vaddps em x86
}
fn dotProduct(a: Vec4f, b: Vec4f) f32 {
const prod = a * b;
return @reduce(.Add, prod);
}
Em muitos casos, o código SIMD gerado por Zig via LLVM é tão bom quanto Assembly manual. Para os raros casos onde não é, inline assembly está disponível.
Tabela Comparativa
| Aspecto | Zig | Assembly |
|---|---|---|
| Portabilidade | 40+ arquiteturas | Uma arquitetura |
| Produtividade | Alta | Muito baixa |
| Performance | Excelente (LLVM) | Máxima (se bem escrito) |
| Segurança | Bounds checking, type safety | Nenhuma |
| Debug | Stack traces, debugger | Difícil |
| Manutenção | Boa | Muito difícil |
| Curva de aprendizado | Moderada | Muito alta |
| Inline assembly | Suportado | N/A |
Recomendações Práticas
Use Zig para:
- 99% do código de sistemas, incluindo kernels e drivers
- Código de alta performance que precisa ser portável
- Projetos onde manutenibilidade importa
- SIMD via tipos vetoriais nativos
- Qualquer código que será mantido por mais de uma pessoa
Use Assembly (ou inline assembly em Zig) para:
- Código de boot e inicialização de hardware
- Instruções específicas de CPU sem equivalente em Zig
- Os 1% de loops críticos onde o compilador não gera o código ideal
- Handlers de interrupção que exigem controle total de registradores
- Análise de segurança e engenharia reversa
Conclusão
Na prática, Zig elimina a necessidade de Assembly para a grande maioria dos cenários de programação de sistemas. O suporte a inline assembly garante que, quando Assembly é realmente necessário, ele pode ser integrado diretamente no código Zig sem arquivos separados.
A recomendação é escrever em Zig primeiro, medir performance, e recorrer a inline assembly apenas quando medições comprovarem que o compilador não está gerando código suficientemente rápido. Na maioria dos casos, o LLVM faz um trabalho excelente.
Para começar com Zig, visite Introdução ao Zig. Para performance, confira nosso guia de benchmarking e testes unitários.