O que é Compilação e qual o papel 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.
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 C#, 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 dos compiladores 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
Nessa etapa, o código fonte é transformado em um código assembly, um código muito próximo do de máquina, ou código binário. Contudo, ainda contém referências a arquivos externos, então não pode ser utilizado.
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.
Linker
Essa é a última etapa do compilador, onde as bibliotecas, já compiladas, são adicionadas em nosso código binário, permitindo a criação de um arquivo binário executável.
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 processo diferentes e acabam gerando códigos binários diferentes, como é o caso da linguagem C e C++ que tem mais de 30 compiladores diferentes.
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 pensar nas instruções como se fossem funções predefinidas que aceitam alguns argumentos, e o trabalho do compilador é fazer com que todo o código seja executado por essas funções.
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.
Ultilizando C para escrevermos um código fonte, 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
1100001101011101
Esse é 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 programa
A 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 aproximadamente 3700 instruções, enquanto a ARM conta com aproximadamente 500 instruções e o RISC-V com 47 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 NodeJS 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 um motivo de privacidade ou de segurança.
Também é 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 Java Virtual Machine (JVM), ou máquina virtual do Java, ele pode ser considerado uma linguagem interpretada. Ao mesmo tempo, a JVM não utiliza o código fonte, mas sim uma versão compilada para ela.
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 gastamos tempo em cada compilação, o que diminui a velocidade de desenvolvimento.
Conclusão
O compilador tem um trabalho muito importante tanto em linguagens que são compiladas quanto nas interpretadas, já que todos os comandos devem ser transformados em código binário para poderem ser processados. 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.
Para aprender mais sobre o assunto, você pode conferir os links a seguir: