Compilação para bytecode vs código de máquina

13

A compilação que produz um bytecode provisório (como no Java), em vez de ir "até o fim" para o código da máquina, geralmente envolve menos complexidade (e, portanto, provavelmente leva menos tempo)?

Julian A.
fonte

Respostas:

22

Sim, compilar no bytecode Java é mais fácil do que compilar no código da máquina. Isso ocorre parcialmente porque há apenas um formato para segmentar (como menciona Mandrill, embora isso reduz apenas a complexidade do compilador, não o tempo de compilação), em parte porque a JVM é uma máquina muito mais simples e mais conveniente de programar do que as CPUs reais - como foi projetado em Em conjunto com a linguagem Java, a maioria das operações Java é mapeada para exatamente uma operação de bytecode de uma maneira muito simples. Outra razão muito importante é que praticamente nenhumaotimização ocorre. Quase todas as preocupações de eficiência são deixadas para o compilador JIT (ou para a JVM como um todo), portanto, a extremidade intermediária inteira dos compiladores normais desaparece. Basicamente, ele pode percorrer o AST uma vez e gerar sequências de bytecodes prontas para cada nó. Há alguma "sobrecarga administrativa" na geração de tabelas de métodos, conjuntos constantes, etc., mas isso não é nada comparado às complexidades do, digamos, LLVM.

Robert Harvey
fonte
Você escreveu "... final intermediário de ...". Você quis dizer "... do meio ao fim de ..."? Ou talvez "... parte do meio de ..."?
Julian A.
6
@Julian "fim do meio" é um termo real, cunhado em analogia com o "front end" e "back-end" sem ter em conta para a semântica :)
7

Um compilador é simplesmente um programa que pega arquivos de texto 1 legíveis por humanos e os converte em instruções binárias para uma máquina. Se você der um passo para trás e pensar sobre sua pergunta dessa perspectiva teórica, a complexidade é aproximadamente a mesma. No entanto, em um nível mais prático, os compiladores de código de bytes são mais simples.

Que etapas amplas precisam acontecer para compilar um programa?

  1. Digitalização, análise e validação de código fonte.
  2. Convertendo a fonte em uma árvore de sintaxe abstrata.
  3. Opcional: processe e melhore o AST se a especificação do idioma permitir (por exemplo, remover código morto, reordenar operações, outras otimizações)
  4. Convertendo o AST para alguma forma que uma máquina entenda.

Existem apenas duas diferenças reais entre os dois.

  • Em geral, um programa com várias unidades de compilação requer vinculação ao compilar com código de máquina e geralmente não com código de byte. Pode-se dividir a questão de saber se o vínculo faz parte da compilação no contexto desta questão. Nesse caso, a compilação do código de bytes seria um pouco mais simples. No entanto, a complexidade do vínculo é compensada no tempo de execução, quando muitas preocupações de vínculo são tratadas pela VM (veja minha nota abaixo).

  • Os compiladores de código de bytes tendem a não otimizar tanto porque a VM pode fazer isso melhor em tempo real (os compiladores JIT são uma adição bastante padrão às VMs atualmente).

A partir disso, concluo que os compiladores de código de bytes podem omitir a complexidade da maioria das otimizações e de todos os vínculos, adiando ambos para o tempo de execução da VM. Os compiladores de código de byte são mais simples na prática, porque eliminam muitas complexidades na VM que os compiladores de código de máquina assumem.

1 Sem contar as línguas esotéricas


fonte
3
Ignorar otimizações e coisas assim é bobagem. Essas "etapas opcionais" compõem grande parte da base de código, complexidade e tempo de compilação da maioria dos compiladores.
Na prática, isso está correto. Eu estava encerando a academia aqui, atualizei minha resposta.
Existe alguma especificação de linguagem que realmente proíba otimizações? Entendo que algumas línguas dificultam, mas não permitem que nenhuma comece?
Davidmh
@ Davididmh Não conheço nenhuma especificação que os proíba . Meu entendimento é que a maioria diz que o compilador é permitido, mas não entra em detalhes. Cada implementação é diferente porque muitas otimizações dependem de detalhes da CPU, SO e arquitetura de destino em geral. Por esse motivo, é menos provável que um compilador de código de byte otimize e envie isso para a VM que conhece a arquitetura subjacente.
4

Eu diria que simplifica o design do compilador, pois a compilação é sempre Java para código de máquina virtual genérico. Isso também significa que você só precisa compilar o código uma vez e ele será executado em qualquer plataforma (em vez de precisar compilar em cada máquina). Não tenho certeza se o tempo de compilação será menor porque você pode considerar a máquina virtual como uma máquina padronizada.

Por outro lado, cada máquina precisará ter a Java Virtual Machine carregada para que possa interpretar o "código de bytes" (que é o código da máquina virtual resultante da compilação do código java), traduza-o no código de máquina real e execute-o .

Imo, isso é bom para programas muito grandes, mas muito ruim para os pequenos (porque a máquina virtual é um desperdício de memória).

Mandrill
fonte
Entendo. Então, você acha que a complexidade de mapear o bytecode para a máquina padrão (ou seja, a JVM) corresponderia à de mapear o código-fonte para uma máquina física, não deixando motivo para pensar que o bytecode resultaria em menor tempo de compilação?
Julian A.
Isso não é o que eu disse. Eu disse que o mapeamento do código Java para o código de bytes (que é o Virtual Machine Assembler) corresponderia ao mapeamento do código-fonte (Java) para o código físico da máquina.
Mandrill
3

A complexidade da compilação depende em grande parte do intervalo semântico entre o idioma de origem e o idioma de destino e o nível de otimização que você deseja aplicar ao preencher esse intervalo.

Por exemplo, compilar o código-fonte Java para o código de bytes da JVM é relativamente simples, pois existe um subconjunto principal de Java que é mapeado diretamente para um subconjunto do código de bytes da JVM. Existem algumas diferenças: Java possui loops, mas não GOTO, a JVM possui, GOTOmas não possui loops, Java possui genéricos, a JVM não, mas eles podem ser facilmente tratados (a transformação de loops em saltos condicionais é trivial, o apagamento do tipo é um pouco menos portanto, mas ainda gerenciável). Existem outras diferenças, mas menos graves.

A compilação do código-fonte Ruby para o código de bytes da JVM é muito mais envolvida (especialmente antes invokedynamice MethodHandlesfoi introduzida no Java 7, ou mais precisamente na 3ª edição da especificação da JVM). No Ruby, os métodos podem ser substituídos no tempo de execução. Na JVM, a menor unidade de código que pode ser substituída no tempo de execução é uma classe, portanto, os métodos Ruby precisam ser compilados não nos métodos da JVM, mas nas classes da JVM. O envio de método Ruby não corresponde ao envio de método da JVM e invokedynamic, antes , não havia como injetar seu próprio mecanismo de envio de método na JVM. Ruby tem continuações e corotinas, mas a JVM não possui as instalações para implementá-las. (As JVMsGOTO é restrito a pular destinos dentro do método.) O único fluxo de controle primitivo da JVM, que seria poderoso o suficiente para implementar continuações são exceções e para implementar encadeamentos de corotinas, os quais são extremamente pesados, enquanto todo o objetivo das corotinas é seja muito leve.

OTOH, compilar o código-fonte Ruby para o código de bytes Rubinius ou YARV é novamente trivial, pois os dois são explicitamente projetados como um destino de compilação para Ruby (embora o Rubinius também tenha sido usado para outros idiomas, como CoffeeScript e o mais famoso, Fancy) .

Da mesma forma, a compilação do código nativo x86 no código de bytes da JVM não é direta; novamente, há uma lacuna semântica bastante grande.

Haskell é outro bom exemplo: com o Haskell, existem vários compiladores prontos para produção de alto desempenho e força industrial que produzem código de máquina x86 nativo, mas até hoje não há compilador ativo para a JVM ou a CLI, porque a semântica a diferença é tão grande que é muito complexo colmatá-la. Portanto, este é um exemplo em que a compilação no código da máquina nativa é realmente menos complexa do que a compilação no código de byte da JVM ou CIL. Isso ocorre porque o código de máquina nativo possui primitivas de nível muito mais baixo ( GOTO, ponteiros, ...) que podem ser mais facilmente "coagidas" a fazer o que você deseja do que usar primitivas de nível superior, como chamadas de método ou exceções.

Portanto, pode-se dizer que, quanto mais alto o idioma de destino, mais ele deve corresponder à semântica do idioma de origem para reduzir a complexidade do compilador.

Jörg W Mittag
fonte
0

Na prática, a maioria das JVM atualmente são softwares muito complexos, executando a compilação JIT (portanto, o bytecode é convertido dinamicamente em código de máquina pela JVM).

Portanto, embora a compilação do código-fonte Java (ou código-fonte Clojure) para o código de bytes da JVM seja realmente mais simples, a própria JVM está fazendo uma tradução complexa para o código da máquina.

O fato de essa conversão JIT dentro da JVM ser dinâmica permite que a JVM se concentre nas partes mais relevantes do bytecode. Na prática, a maioria das JVM otimiza mais as partes mais quentes (por exemplo, os métodos mais chamados ou os blocos básicos mais executados) do bytecode da JVM.

Não tenho certeza de que a complexidade combinada do JVM + Java para o compilador de bytecode seja significativamente menor que a complexidade dos compiladores antecipados.

Observe também que a maioria dos compiladores tradicionais (como GCC ou Clang / LLVM ) está transformando o código fonte de entrada C (ou C ++, ou Ada, ...) em uma representação interna ( Gimple for GCC, LLVM for Clang), que é bastante semelhante a algum bytecode. Em seguida, eles estão transformando as representações internas (primeiro otimizando-as em si mesmas, ou seja, a maioria das passagens de otimizações do GCC estão usando o Gimple como entrada e produzindo o Gimple como saída; depois emitindo código de montador ou máquina) para o código do objeto.

BTW, com a infraestrutura recente do GCC (notavelmente libgccjit ) e LLVM, você pode usá-los para compilar outra linguagem (ou a sua) em suas representações internas do Gimple ou LLVM e, em seguida, lucrar com as muitas habilidades de otimização do ponto intermediário e traseiro. partes finais desses compiladores.

Basile Starynkevitch
fonte