Há alguns dias, eu e o Bruno Consul (@bconsul) iniciamos uma cruzada: começamos a elaborar (e produzir evidentemente) uma série de posts que pretende, através de diversos tipos de testes, levantar informações sobre a performance dos compiladores/interpretadores (como você preferir) das linguagens Java e das linguagens pertencentes a plataforma .NET (C#, VB e F#). No primeiro artigo, expusemos nossos objetivos gerais e específicos com esta série de posts e estabelecemos a timeline de atividades, portanto, para fins de contextualização, recomendo fortemente a leitura do primeiro post antes de prosseguir com este.
No post de hoje, discursaremos sobre o processo de compilação/interpretação de linguagens que não geram código objeto direto para máquina real com base nas informações específicas da arquitetura de software e hardware, isto é, que geram pseudo-códigos para serem “executados” por emuladores em software (em nosso caso específico, Java e C#). Entender o processo de compilação/interpretação é requisito obrigatório para entender os posts futuros com aplicação de testes sobre os compiladores.
Com este post, nosso objetivo não é tratar de todos os pormenores do processo de compilação/interpretação, mas sim, apresentar uma visão geral das plataformas e suas formas de trabalho de forma abrangente. Ao final do post, deixamos a disposição uma relação de link’s que tratam do assunto de forma demorada.
Compilação versus Interpretação
O primeiro possível ponto de discórdia desta série se encontra neste ponto e consiste na resposta a seguinte pergunta: Java e/ou C#, são compiladas, interpretadas ou um modelo híbrido? Olhemos então, primeiro para os conceitos, depois para a estrutura de trabalho das linguagens e, finalmente, o confronto de um com o outro.
1. Compilação
É chamado de “compilação” o processo de transcrição de uma linguagem de alto nível para uma linguagem de baixo nível (também conhecida como linguagem de máquina). Falando em termos práticos, é graças ao processo de compilação que conseguimos escrever códigos orientados a objetos em alto nível (em uma linguagem cujo vocabulário é de fácil compreensão para as pessoas) de forma que, ao submetê-lo para execução pelo computador, este último consegue converter para uma linguagem que ele entende e por coseguinte, consegue executar as ações implementadas. A Figura 1 apresenta o o fluxograma que ilustra o processo de compilação para uma linguagem de alto nível como C, por exemplo.
Figura 1. Fluxo de trabalho da linguagem C (fonte: http://www.codexterity.com/raw-delphi/index.htm)
O fluxograma é simples e dispensa comentários adicionais. Vale observar entretanto que, neste processo, são realizadas três tipos de análises: léxica, sintática e semântica (essas análises estão implícitas no processo apresentado pela Figura 1, portanto, explicitamos aqui textualmente). Algumas observações importantes sobre o processo de compilção são listada abaixo:
- Informações do Sistema Operacional: no processo de compilação, alguns recursos do sistema operacional são utilizados para compor o executável final. O Linker é o responsável por estabeler o roteamento destes recursos e incorporá-lo ao arquivo com código objeto.
- Informações de Hardware: o processo de compilação também colhe informações da estrutura de hardware (por exemplo: qual é a arquitetura do processador para o qual o código final será gerado? Qual o número de instruções do processador? É CISC, RISC, etc.).
- Desempenho: de forma geral, por possuírem uma relação próxima com o hardware, aplicativos compilados tendem a ser mais performáticos em relação a seus semelhantes interpretados (falaremos sobre o processo de interpretação mais adiante neste mesmo post). Quando apresentamos o processo de interpretação, esta afirmação fará mais sentido pra você.
- Dependência de ambiente de execução: com base nos ítens apresentados acima, é possível afirmar que o processo de compilação gera um código objeto completamente dependente do ambiente de execução, assim, por inércia, temos mais desempenho e nenhuma portabilidade.
2. Interpretação
Um processo alternativo ao da compilação é a “interpretação”. A sequência de processamento é mais simplificada e é realizada em três etapas (compilação, ligação e execução) mas com uma diferença fundamental em relação ao processo de compilação: em tempo de execução. Neste processo, não existe qualquer geração de código intermediário ou etapas adicionais. Tudo é realizado de forma linear e em tempo de execução. Assim, podemos dizer que, no processo de interpretação, um comando é lido, verificado, convertido em executável e, finalmente executado e o que é mais importante, tudo isso é realizado antes do próximo comando sofrer o mesmo processo. A Figura 2 apresenta descreve graficamente este procedimento.
Figura 2: O processo de interpretação
3. Modelo Híbrido de Desenvolvimento e o compilador Java
O modelo tradicional de compilação foi por muitos anos o principal schema para geração de código objeto na computação. Algumas das principais linguagens (C, Pascal, Fortran, dentre outras) disponíveis na época (e que serviram de base para as principais linguagens modernas) tinham suas engine’s baseadas neste modelo.
O modelo parecia ideal até o momento em que o mercado começou a pedir interoperabilidade. Sim, a necessidade de sistemas interoperáveis (que pudessem ser executados em qualquer computador independente da estrutura de hardware e de software disponíveis) se fez presente, o que demandava um novo modelo para geração de código objeto.
Com o advento das linguagens modernas (principalmente das orientadas a objetos) um novo modelo de processamento surgiu – o modelo conhecido como “pseudo-compilação”.
A ideia principal envolvida neste modelo é disponibilizar um esquema de geração de código objeto que seja suficientimente capaz de ser executado em qualquer plataforma, isto é, uma vez desenvolvido, funciona em qualquer computador. Estruturalmente, o que este modelo pretendia conseguir, era trazer as vantagens do modelo de compilação tradicional (principalmente performance) associadas com as vantagens do modelo de interpretação (principalmente a multiplataforma).
Para que essa ideia torne-se mais clara, considere a Figura 3.
Figura 3: O processo de “psedo-compilação” da linguagem Java (fonte: http://support.novell.com/techcenter/articles/dnd19960601.html)
A Figura 3 apresenta uma visão geral do processo de pseudo-compilação da linguagem Java. Utilizamos aqui o exemplo da linguagem Java por ser “famosa” por disponibilizar a multiplataforma e assim, já apresentamos os principais conceitos relacionados ao processo de compilação da linguagem Java. A seguir, descrevemos em linhas gerais o processo ilustrado pela Figura 3.
- Código Java é submetido ao compilador Java, sendo que a versão deste último, é determinada pelo processador sobre o qual está executando (na imagem temos 3 exemplos: Pentium, PowerPC e SPARC).
- Como saída deste primeiro processo, temos a geração de um novo arquivo fonte (que possui a extensão *.class), chamado Java ByteCode. Sim, este arquivo é elemento chave do processo de compilação, pois, a teórica “independência de plataforma” encontra-se encapsulada em seu interior. O conteúdo do arquivo class consiste de uma sequência de tokens que são “interpretáveis” pela “Máquina Virtual Java” (JVM).
- O passo seguinte, consiste na submissão do arquivo .class gerado pelo pré-compilador para interpretadores internos a VM para que então a aplicação possa funcionar corretamente e, independente da plataforma.
Neste ponto algumas observações fazem-se necessárias. São elas:
- Máquina Virtual: a virtual machine é uma camada de software posicionada acima das camadas do sistema operacional e de aplicações (Figura 4) que “emula” o comportamento de uma máquina real. Suas principais funções: interpretar/executar o código apresentado pelo bytecode e, como consequência desta, realizar uma comunicação eficiente com o sistema operacional para a correta execução da aplicação.
- Interpretador Java: sim, o bytecode é independente de contexto em função da VM, entretanto, a VM não é independente de contexto. Assim, para que uma aplicação Java “rode”, o interpretador Java precisa ser específico para o sistema operacional e hardware onde se deseja executar a aplicação.
- JIT (Just-In-Time): é um otimizador de instruções de código que funciona com base no processador corrente e que atua em tempo de execução. O desempenho da aplicação está diretamente associado ao resultado do trabalho do JIT.
Figura 4: Camadas de software no ambiente computacional
4. O compilador .NET
O processo de compilação/execução de aplicações da plataforma .NET possui muitas semelhanças com o modelo da plataforma Java, entretanto, as diferenças existentes são definitivas para agregar valor a plataforma da Microsoft.
A plataforma .NET disponibiliza um ambiente de execução, denominado Common Language Runtime (se preferir, CLR). A comparação com o Java é natural, portanto, podemos dizer que a CLR está para .NET framework como a VM está para a plataforma Java, entretanto, existem significativas diferenças entre os dois ambientes. A seguir, apresentamos algumas das principais características relacionadas a CLR e o processo de geração de código objeto da plataforma .NET:
- Independência de linguagens: se você possui o mínimo conhecimento a respeito da plataforma .NET, deve saber que com ela, não se está preso a uma linguagem específica, sendo possível em um mesmo ambiente de desenvolvimento, encontrar trechos de uma aplicação escritas em Visual Basic (por exemplo) e trechos escritos em C# e, ainda assim, a aplicação funcionar perfeitamente bem. Isto somente é possível graças a IL (ou MSIL, falaremos dela mais adiante) e a interpretação dela por parte da CLR. Dois aspectos são fundamentais para que esta interoperabilidade entre as linguagens ocorra na plataforma .NET: a Common Type-System (CTS) e a Common Language Specification (CLS). A primeira é responsável por definir os tipos de dados suportados pela plataforma, enquanto que a segunda, define os requisitos mínimos necessários para que uma linguagem possa funcionar dentro da .NET framework.
- IL (Intermediate Language) ou MSIL ou CIL: no Java temos o bytecode que é a linguagem intermediária gerada após o processo de “pré-compilação”. No .NET, temos a linguagem intermediária (IL) que serve de base para execução da aplicação dentro da CLR. IL é uma linguagem mais “amigável” ao computador e não tanto amigável às pessoas, mas ainda assim, por manipular estruturas de alto nível (objetos, etc.), é de “relativa facilidade compreensão”. Neste post não entraremos em maiores detalhes sobre isso por dois motivos: 1º este não é o foco do texto; 2º o Elemar Junior escreveu uma excepcional série sobre o assunto em seu blog. Se desejar conhecer mais de perto a linguagem intermediária do .NET, recomendo fortemente a leitura da série, que pode ser encontrada clicando AQUI.
- JIT: o JIT é um elemento de fundamental importância no processo de compilação/execução das linguagens disponíveis na .NET framework, pois é o responsável por otimizar o processo de geração de IL. Imagine por exemplo que, alguns objetos foram instanciados no decorrer de seu código mas nunca foram utilizados. Neste caso, p JIT entraria em ação e não geraria código IL para algo que nunca será utilizado. O exemplo é simples mas ajuda ilustrar a importância do recurso.
Evidentemente que muitos detalhes estão sendo desprezados, pois como mencionado anteriormente, este post pretende apresentar uma visão geral do processo de compilação oferecido pelas plataformas Java e .NET.
A Figura 5 apresenta o fluxo de compilação e execução do ambiente .NET.
Figura 5: Pipiline de compilação/execução na plataforma .NET (Fonte: http://www.learnerin.com/2011/05/what-is-common-language-runtime-clr.html)
O fluxo de compilação/execução apresentado pela Figura 5 é simples e dispensa maiores comentários. Evidentemente que a Figura 5 apresenta uma visão geral do processo e muitos detalhes do mesmo encontram-se encapsulados.
Analisando o fluxo de compilação/execução da plataforma .NET, é impossível negar as semelhanças com o processo disponibilizado pela plataforma Java (Figura 3), assim, faz sentido comparar performances entre as duas tecnologias (há equivalência nas abordagens).
Em relação aos conceitos apresentados, para mim (e aqui é minha impressão em relação aos conceitos apresentados neste post) existe uma clara distinção entre “Compilação”, “Interpretação” e o processo ao qual são submetidos os fontes das linguagens Java, C#, VB, etc (que chamo de “Pseudo-compilação”).
Em relação a performance, a impressão imediatista que tenho apenas por visualizar as estruturas de compilação/execução das plataformas a serem testadas, é a de que, por possuírem uma camada acima do sistema operacional, a comunicação com o mesmo é prejudicada (não posso mensurar ainda em que escala) e por inércia o desempenho também.
No próximo post, iniciaremos os testes com as plataformas. Conforme mencionado no primeiro post, iniciaremos os testes com métodos matemáticos, mediremos o desempenho e publicaremos os resultados.
Bom, por hoje é só! Espero que este post possa o ajudar a reafirmar seus conceitos sobre as plataformas Java e .NET e que sirva de base para as análises que faremos no futuro. Não se esqueça de deixar seu feedback sobre o post através dos comentários.
Facebook
Twitter
Instagram
LinkedIn
RSS