Compilação: o que é e qual a função dos compiladores

Quando criamos uma aplicação, normalmente prestamos atenção apenas no nosso código ou nas bibliotecas, e não nos preocupamos em como esse código será executado, se ele será compilado ou interpretado.

Na imagem vemos uma tela com códigos hexadecimais divididos em 4 colunas, cada uma com um grupo de 8 caracteres.
Ambas as opções têm suas vantagens e desvantagens, principalmente em relação a distribuição e desempenho do nosso código. Um código compilado é feito para apenas um sistema, então se formos disponibilizá-lo para mais de um sistema, teremos que manter um código binário para cada. Por outro lado, um código interpretado pode ser facilmente distribuído entre vários sistemas, mas não tem o mesmo desempenho de um código compilado.
As linguagens compiladas como C, C++, Fortran, Delphi e Pascal geralmente têm um melhor desempenho, enquanto linguagens interpretadas como JavaScript, Python, PHP e Ruby tendem a ter uma portabilidade mais fácil, permitindo a execução em múltiplos sistemas.
O que é um compilador e quais suas principais tarefas
O compilador é um programa que lê e analisa o código fonte da aplicação, ou seja, o código que nós escrevemos, e gera a partir dele um código binário que pode ser executado.
Normalmente os compiladores são programas com uma lógica complexa, já que se utilizam de muitas técnicas para acelerar a aplicação e reduzir os requisitos do sistema onde essa aplicação irá ser executada.
As principais tarefas de um compilador no processo de compilação são:
Pré-processamento
Essa é a primeira etapa para um compilador, onde o código fonte é incluído, analisado por erros de sintaxe, e macros ou definições são substituídos e processados. Essa etapa ocorre muito em linguagens como C e C++.
Compilação
Nesta etapa, o código fonte é convertido em código assembly, que se aproxima bastante do código de máquina. No entanto, esse código ainda pode conter referências a arquivos externos e, por isso, não pode ser executado diretamente.
Assembler
Com o código assembly pronto, ele passa por um conversor, chamado assembler, para se tornar um código binário feito exclusivamente para um único sistema, finalizando mais uma etapa do processo de compilação.
Linker
Essa é a última etapa do compilador. Durante esse momento do processo de compilação, as bibliotecas, já compiladas, são adicionadas ao nosso código binário, permitindo a criação de um arquivo executável final.

Alguns compiladores contêm mais tarefas, como otimização, e detecção de erros comuns, entre outras opções, ficando a cargo do criador do compilador definir todas elas, logo podemos ter vários compiladores para uma mesma linguagem, que passam por processos diferentes e acabam gerando códigos binários diferentes, como é o caso da linguagem C e C++ que possuem diversos compiladores importantes.
Código binário
Os processadores não entendem palavras, eles contêm apenas circuitos que podem realizar ações, como operações matemáticas, e ler e gravar na memória.
Então, para usarmos essas operações, precisamos escolher os circuitos, também chamados de instruções, e em seguida passar para eles os parâmetros em que queremos atuar.
Podemos imaginar as instruções como funções predefinidas que recebem argumentos. O trabalho do compilador consiste em converter todo o código fonte para uma sequência dessas funções (instruções) que o processador entende.
Os circuitos são acionados, utilizados e desligados durante a execução de um programa porque o arquivo binário deste programa contém as operações que queremos realizar, sempre utilizando números binários, 0 e 1.
Vamos utilizar um exemplo para facilitar um pouco a visualização dos códigos binários e a sua execução nos circuitos do processador.
Utilizando a linguagem C para escrever um código fonte simples, temos:
int main(){ // função main com um retorno tipo numérico
int x; // criação da variável x
x = 3; // define o valor de x
}Ao passarmos por um compilador, teremos um arquivo binário parecido com esse:
11100101100010010100100001010101
00000011111111000100010111000111
10111000000000000000000000000000
00000000000000000000000000000000
1100001101011101Esse é o conteúdo de um arquivo em binário. Para visualizarmos ou editarmos este conteúdo, precisamos de um editor especial, já que editores de texto tentam colocar esse arquivo em alguma codificação, como UTF-8 ou ASCII, para nos mostrar caracteres.
Podemos ver o que esses números significam, traduzindo o arquivo binário para assembly:
pushq %rbp //inicia o programa com operação de 64-bit
movq %rsp, %rbp //transfere o valor presente em rsp para o início do programa
movl $3, -4(%rbp) //criação da variável e atribuição do valor 3
movl $3, $eax //transfere o valor 3 para a posição eax
popq %eax //copia o valor 3 para o final o programa
ret // encerra o programaA primeira palavra de cada linha é a instrução que deve ser acionada dentro do processador, em seguida temos os parâmetros.
Como é muito mais trabalhoso escrevermos em assembly ou em código binário, geralmente utilizamos uma linguagem de mais alto nível, como C, C++.
Além disso, o código binário varia de um sistema para outro, então códigos binários feitos para windows não funcionam em linux, pois o sistema operacional não o reconhece como sendo um executável.
Também temos diferentes arquiteturas dos processadores, como x86-x64, que é utilizada na maioria dos computadores, ARM, que é utilizada em celulares e recentemente em alguns servidores, e RISC-V que está ganhando espaço em aplicações IoT e está tentando entrar nos outros mercados.
Cada uma dessas arquiteturas têm códigos binários diferentes, já que contêm circuitos diferentes, a x86-x64 contém de cerca de 1000 a mais de 3000 instruções, dependendo de como são contadas (incluindo diferentes modos de endereçamento, tamanhos de operandos e extensões), enquanto ARM tem um conjunto de instruções mais enxuto, variando conforme a versão da arquitetura e extensões.
Por fim, o RISC-V é uma arquitetura aberta que vem ganhando espaço principalmente em dispositivos IoT (Internet das Coisas), com um conjunto ainda mais simples, em torno de 47-50 instruções.
Linguagens interpretadas
Apesar da compilação geralmente tornar os programas eficientes em termos do consumo de recursos, ela torna mais difícil portar o código entre vários sistemas. Pensando em como resolver isso, foram criadas linguagens que não são compiladas no desenvolvimento, mas sim durante a execução, as linguagens interpretadas.
As linguagens interpretadas não precisam passar pelo processo de compilação, o que acelera bastante a velocidade de desenvolvimento, já que programas grandes e complexos podem demorar mais de 30 minutos para completá-lo e deve-se recompilar o programa todo a cada alteração no código.
O processo de interpretação da linguagem consiste de várias etapas e muitas são executadas dentro de máquinas virtuais, como é o caso do Python.
Porém, ainda é necessário utilizar apenas as instruções presentes na máquina física e por esse motivo os interpretadores acabam compilando o código, mas fazem isso durante a execução.
Algumas linguagens interpretadas são o PHP, o JavaScript e o Python, cada uma com o desenvolvimento focado, mas não exclusivo, a um público alvo, como o JavaScript no Front-end com os navegadores e no Back-end com o Node.JS ou Deno.
As linguagens interpretadas são mais fáceis de se portar de um sistema para outro, mas precisam de alguma camada intermediária para traduzir os comandos de programa para código binário, como o Interpretador Python para o Python e o navegador para o JavaScript. Esse processo cria uma barreira em relação ao desempenho dessas linguagens.
Em alguns casos, algumas bibliotecas conseguem um desempenho melhor que a linguagem nativa porque são escritas em uma linguagem compilada, como é o caso da biblioteca NumPy e SciPy para o Python.
De forma geral, escolher uma linguagem interpretada permite que o seu código possa ser executado em diversos computadores, com diferentes sistemas ou configurações, e também acelera muito no período de desenvolvimento, mas elas tendem a ser mais lentas e consumir mais da máquina em que estão sendo executadas.
Então, para algumas aplicações, linguagens interpretadas são preferenciais, como é o caso do JavaScript para execução de código no navegador, já que uma linguagem compilada precisaria ter várias versões, dependendo do sistema do cliente. Assim, poderia haver incompatibilidades caso não fossem repassadas as configurações do cliente, por motivos de privacidade ou de segurança.
É recomendável iniciar os estudos em programação fazendo uso de linguagens interpretadas, já que essas não perdem tempo com a compilação para cada mudança no código, tornando a experiência mais rápida e agradável.
Java
O Java é uma linguagem diferente da maioria no quesito compilada ou interpretada, ele é interpretado e compilado ao mesmo tempo. Isso permite que o Java possa ser executado em vários dispositivos diferentes com um único executável e uma única compilação, e também não tenha uma penalidade tão grande no momento da execução.
Como o Java é executado dentro da Máquina Virtual Java (JVM), pode ser considerado interpretado. No entanto, a JVM executa uma versão intermediária do código (o bytecode) que resulta de um processo de compilação.
Com essa técnica, é possível executar o código em qualquer sistema que tenha suporte para a JVM e, ao mesmo tempo, não ter uma penalidade muito grande de desempenho na hora da execução, como há outras linguagens interpretadas, já que o trabalho de otimização do código fonte já foi realizado e o arquivo gerado está mais próximo da linguagem da máquina.
Ao mesmo tempo, precisamos do interpretador que as linguagens compiladas não precisam, e também gastamos tempo em cada compilação, o que pode impactar a velocidade de desenvolvimento.
Conclusão
O compilador exerce um papel fundamental tanto em linguagens compiladas quanto em interpretadas, já que todas, em algum momento, precisam ter seus comandos transformados em código binário para serem processados pelo computador.
Os primeiros compiladores, que foram escritos em assembly, possibilitaram a criação de novos programas e compiladores melhores e mais complexos, acelerando o desenvolvimento e automatizando a tarefa da análise e tradução da lógica de programa para as instruções da máquina.
Como aprender mais sobre o tema?
Compreender como o código é transformado em instruções que o processador executa nos dá uma visão muito mais profunda sobre desempenho, portabilidade e escolhas de tecnologia. Seja utilizando linguagens compiladas ou interpretadas, cada abordagem atende melhor a diferentes contextos e necessidades.
Dominar esses conceitos não só melhora a forma como você programa, mas também amplia sua capacidade de tomar decisões mais estratégicas no desenvolvimento.
Se você quer evoluir nesse nível de entendimento e aplicar isso na prática, vale a pena se aprofundar com formações como os cursos de C e C++, a carreira de desenvolvimento back-end com Python ou a trilha de back-end com Java, que exploram esses conceitos de forma aplicada no dia a dia.






