Linguagem Compilada vs Interpretada | Qual é melhor?

Linguagem Compilada vs Interpretada | Qual é melhor?

Introdução sobre linguagens compiladas e interpretadas

Nesta seção, Fabio Akita discute a diferença entre linguagens compiladas e interpretadas, desmistificando alguns conceitos errôneos.

O que significa ser compilado ou interpretado?

  • Muitas vezes, os programadores argumentam que uma linguagem é melhor do que outra com base no fato de ser compilada ou interpretada.
  • No entanto, esses argumentos não levam em conta outros fatores importantes.
  • O objetivo desta discussão não é determinar qual é melhor, mas sim entender os conceitos corretamente.

Exemplos de compilação e interpretação

Nesta seção, são apresentados exemplos práticos de compilação e interpretação usando as linguagens C e Java.

Compilação em C

  • Um exemplo de código Hello World em C é mostrado.
  • O código em C é compilado usando o comando "cc" no terminal Linux para gerar um arquivo binário executável compatível com Linux (ELF).

Interpretação em Java

  • Um exemplo de código Hello World em Java é mostrado.
  • O código Java é compilado usando o comando "javac" para gerar um arquivo ".class".
  • Em seguida, o arquivo ".class" é interpretado pelo programa "java", que traduz o código para o formato binário executável entendido pelo sistema operacional.

Definições de compilação e interpretação

Nesta seção, são fornecidas definições claras de compilação e interpretação, esclarecendo a natureza das linguagens C e Java.

Compilação

  • A compilação envolve o processo de transformar código fonte em um binário executável diretamente entendido pelo sistema operacional.
  • No caso do Java, o arquivo ".class" não é um executável no formato ELF, portanto, não pode ser executado diretamente.

Interpretação

  • A interpretação envolve a tradução do código fonte ou uma representação intermediária para o formato binário executável entendido pelo sistema operacional.
  • Portanto, por definição, o Java é considerado uma linguagem interpretada.

Diferença entre compilação e interpretação

Nesta seção, é explicada a diferença entre compilação e interpretação, fornecendo uma visão geral do processo de compilação.

Processo de Compilação

  • O processo de compilação envolve vários passos complexos.
  • É importante ter um modelo mental claro sobre esse processo para entender as principais diferenças entre linguagens compiladas e interpretadas.

Linguagens regulares e expressões regulares

Nesta seção final, são abordados conceitos relacionados às linguagens regulares e expressões regulares.

Linguagens Regulares

  • As linguagens de programação como C, Java e Python são consideradas linguagens regulares na teoria formal das linguagens.
  • Uma linguagem regular pode ser definida por meio de expressões regulares que podem ser reconhecidas por autômatos finitos.

Expressões Regulares

  • As expressões regulares são usadas para validar formatos de dados, como e-mails ou CPFs.
  • Elas podem ser definidas como linguagens que podem ser reconhecidas por autômatos finitos.

Understanding Words and Grammar

In this section, the speaker discusses the concept of words and grammar in language. They explain that words are collections of letters separated by spaces or punctuation marks. However, simply combining letters does not always result in meaningful words or sentences. The speaker emphasizes the importance of following grammar rules to form coherent phrases.

Words and their Meaning

  • Words are collections of letters that exist in a dictionary.
  • Combining certain letters can form meaningful words.
  • Removing or changing a single letter can alter the meaning of a word.

Grammar and Sentence Formation

  • Sentences need to follow grammatical rules to make sense.
  • Elements such as subjects, predicates, adjectives, pronouns, and verb tenses contribute to sentence structure.
  • Punctuation marks play a crucial role in conveying different meanings within a sentence.

Tokenizing Text for Analysis

This section focuses on tokenization, which involves breaking down text into meaningful units called tokens. The speaker explains how tokens can be defined based on lexemes (dictionary-like definitions) and how lexical analysis tools like "flex" can tokenize code written in languages like C.

Tokenization Process

  • Tokenization involves breaking down text into tokens.
  • Tokens are meaningful units such as keywords or identifiers.
  • Lexical analysis tools like "flex" can perform tokenization based on predefined lexemes.

Defining Lexemes

  • Lexemes define different categories of characters (e.g., digits, non-digits, punctuation).
  • Characters may belong to multiple lexeme categories (e.g., parentheses as both operators and symbols).

Creating a Simple Programming Language

In this section, the speaker demonstrates the creation of a simple programming language that can perform basic addition and subtraction operations. They explain the process of tokenizing code written in this language and executing the corresponding operations.

Designing a Simple Language

  • The speaker creates a minimalistic programming language that supports only addition and subtraction expressions.
  • The language does not require a separate syntax analyzer, as it uses a straightforward switch case statement to determine the operation based on the second token in the token list.

Limitations and Future Considerations

  • The simplicity of this language raises questions about supporting more complex expressions or additional features like variables and functions.
  • Separating syntax analysis from execution is an important concept in designing programming languages.

Challenges in Language Design

This section explores potential challenges in designing programming languages, such as supporting multiple numbers in an expression, handling operator precedence, and incorporating variables and functions.

Handling Multiple Numbers and Operator Precedence

  • Supporting expressions with more than two numbers requires considering operator precedence.
  • Multiplication takes precedence over addition/subtraction, requiring careful evaluation order.

Extending Language Features

  • Adding support for variables or functions introduces additional complexity to language design.
  • These challenges have already been addressed by existing solutions in the field of programming languages.

Traditional Tools for Language Definition

The speaker discusses traditional tools used for language definition, such as lex or flex for lexical analysis and bison or yacc for grammar definition. These tools allow the definition of a language's grammar in a relatively concise manner.

Traditional Tools for Language Definition

  • Lex and Flex are used for defining lexical analysis.
  • Bison and Yacc are used for defining grammar.
  • GitHub is a good resource to find the grammars of various languages.
  • The C grammar in Yacc is relatively short, contrary to the perception that language definition requires thousands of lines.

Defining Functions in C

The speaker explains how functions are defined in C using different components. They also discuss the meaning of certain terms like "statements" and "expressions" within the context of function definitions.

Defining Functions in C

  • A function in C can be constructed in four different ways.
  • The components include declaration specifiers, declarator, declaration list, and composition of statements.
  • In Portuguese, "statements" may be referred to as expressions, but there is a distinction between expression and statement.
  • The names used in function definitions act as labels representing specific components.

Understanding Compound Statements

The speaker explores the concept of compound statements within the context of function definitions. They explain that compound statements can take on multiple forms based on their composition.

Understanding Compound Statements

  • Compound statements can be represented by an empty block enclosed by curly braces or a list of statements enclosed by curly braces.
  • It can also be a list of declarations enclosed by curly braces followed by a list of statements.
  • There are variations depending on whether it consists only of declarations or both declarations and statements.
  • The speaker emphasizes that this is the official, complete, and unambiguous definition of a function in C.

Statements and Iteration Statements

The speaker delves into the concept of statements within function definitions, specifically focusing on iteration statements. They explain that iteration statements can take different forms such as while, do-while, and for loops.

Statements and Iteration Statements

  • A statement can have various types, including labeled statement, compound statement (as discussed earlier), and iteration statement.
  • Iteration statements in C include while, do-while, and for loops.
  • The for loop itself can be defined in two different ways - with or without an expression at the end.
  • The syntax and semantics of C are defined concisely in relatively short files using lexical definitions and yacc grammar.

Language Definitions in Files

The speaker explains that language definitions are found in specific files rather than blog posts. They mention how JavaScript has a similar definition available on the EcmaScript website.

Language Definitions in Files

  • Language definitions are found in files like lexical definitions and yacc grammar files.
  • These files provide the exact definition of a language without room for discussion or ambiguity.
  • Examples include the definition of JavaScript on the EcmaScript website.
  • These files define various language components such as variable declarations and loops.

Breaking Code into Tokens

The speaker discusses the purpose of breaking source code into tokens using language grammars. They explain that this process allows code to be organized into data structures like parse trees or abstract syntax trees (AST).

Breaking Code into Tokens

  • The goal is to break source code into tokens using language grammars.
  • Tokens are then organized into data structures like parse trees or abstract syntax trees (AST).
  • The resulting structure represents the code in a tree format, allowing for programmatic manipulation.
  • Parse trees and ASTs are important data structures in programming.

Abstract Syntax Trees (AST)

The speaker introduces abstract syntax trees (AST) as an alternative representation of parse trees. They explain that ASTs can be used to represent code in a more concise and meaningful way.

Abstract Syntax Trees (AST)

  • Abstract syntax trees (AST) provide an alternative representation of parse trees.
  • ASTs represent code in a more concise and meaningful manner.
  • The speaker mentions their previous video dedicated to explaining the importance of trees as data structures.
  • Code transformed into an AST provides a clearer understanding of its structure.

Infix Notation vs. Reverse Polish Notation

The speaker compares infix notation with reverse Polish notation (RPN). They explain that while infix notation is more familiar to humans, RPN has advantages and both notations can be represented using syntax trees.

Infix Notation vs. Reverse Polish Notation

  • Infix notation places operators between operands, which is the most common form used by humans.
  • Reverse Polish notation (RPN) places operators after operands, such as seen on HP calculators.
  • RPN is also known as postfix notation, while infix is known as prefix notation.
  • Both notations can be represented using syntax trees.

Advantages of Reverse Polish Notation

The speaker explains the advantages of reverse Polish notation (RPN), highlighting its efficiency and clarity when represented using syntax trees.

Advantages of Reverse Polish Notation

  • RPN offers advantages such as efficiency and clarity.
  • RPN can be represented using syntax trees, providing a clear understanding of its structure.
  • The speaker mentions that what may seem like "common sense" is not always the best approach, and there are often better alternatives.

Conclusion

The speaker concludes by summarizing the importance of language definitions, tokenization, and tree representations in programming languages.

Conclusion

  • Language definitions play a crucial role in programming languages.
  • Tokenization breaks code into tokens for further processing.
  • Tree representations, such as parse trees or abstract syntax trees (AST), provide a structured way to manipulate code programmatically.

A forma de escrever código não é a forma como o computador executa

Neste trecho, o palestrante explica que a forma como escrevemos código em linguagens de programação não é necessariamente a forma como o computador irá executá-lo. Ele ilustra isso com um exemplo e menciona que as linguagens de programação permitem escrever expressões aritméticas na forma infixa, mas internamente o compilador as converte para uma representação préfixa na árvore de sintaxe.

Mudança de símbolos e chamada das funções

  • O palestrante propõe mudar os símbolos matemáticos tradicionais para tornar a expressão mais clara.
  • Exemplo: somar(multiplicar(2, 3), 1)
  • Explica que essa é a maneira como se programa, mesmo que as linguagens permitam escrever na forma infixa.

Compilação do código Java

  • O palestrante mostra um exemplo simples em Java.
  • O programa recebe três parâmetros convertidos em números e imprime a soma da multiplicação desses números.
  • Explica que o compilador transforma o código fonte em instruções binárias de máquina (bytecodes ou assembly).
  • Utiliza a ferramenta javap para mostrar os bytecodes gerados pelo compilador.

Código compilado contém mais instruções do que o código fonte

Nesta parte, o palestrante destaca que o código compilado geralmente contém mais instruções do que o código fonte escrito pelo programador. Ele mostra um exemplo simples em Java e explica como as instruções são geradas para converter os parâmetros de texto em números inteiros.

Geração de instruções

  • O palestrante mostra o trecho do bytecode que converte os argumentos em números inteiros.
  • Destaca que, mesmo com apenas uma linha de código para cada parâmetro, várias instruções são geradas.
  • Explica que o objetivo é otimizar a execução do programa e que os compiladores modernos são capazes de gerar assembly melhor do que se fosse feito manualmente.

Compiladores são mais eficientes do que escrever tudo em assembly

Nesta parte, o palestrante discute a eficiência dos compiladores em comparação com a escrita direta em assembly. Ele menciona que, atualmente, os compiladores evoluíram muito e conseguem gerar assembly melhor do que seria possível fazer manualmente. Além disso, destaca a complexidade dos programas atuais e como seria impraticável escrevê-los completamente em assembly.

Eficiência dos compiladores

  • O palestrante afirma que, na maioria dos casos, nenhum programador é melhor do que um compilador.
  • Destaca a evolução dos compiladores ao longo do tempo e sua capacidade de otimização.
  • Menciona a impossibilidade de escrever programas grandes completamente em assembly.

Pré-cálculo de expressões constantes durante a compilação

Nesta parte, o palestrante aborda o pré-cálculo de expressões constantes durante a compilação. Ele mostra um exemplo em Java onde o compilador identifica que uma expressão sempre terá o mesmo resultado e substitui as operações por um valor constante.

Pré-cálculo de expressões

  • O palestrante mostra um exemplo em Java onde a expressão 1 + 2 * 3 é pré-calculada pelo compilador.
  • Explica que o compilador reconhece que essa conta sempre resultará em 7 e, portanto, substitui as operações por esse valor.
  • Destaca que isso economiza instruções de carregamento e cálculo durante a execução do programa.

Vantagens da compilação em relação à interpretação

Nesta parte, o palestrante discute as vantagens da compilação em relação à interpretação. Ele menciona que a compilação permite otimizações como o pré-cálculo de expressões constantes, enquanto a interpretação requer análise e execução das instruções a cada vez que o programa é executado.

Vantagens da compilação

  • O palestrante destaca que a compilação permite otimizações como pré-cálculo de expressões constantes.
  • Menciona que a interpretação requer análise e execução das instruções toda vez que o programa é executado.
  • Explica que a compilação gera código binário mais eficiente para ser executado diretamente pela máquina.

Otimização de código e sua relevância para o computador

Nesta seção, o palestrante discute a importância da otimização de código e como ela afeta o desempenho do computador.

A irrelevância do código detalhado para o computador

  • O código detalhado, com comentários explicativos antes de cada função, é irrelevante para o computador.
  • Após ser parseado e compilado, restam apenas instruções de máquina.
  • Muitas partes do código serão reescritas pelo compilador para melhorar a eficiência.
  • O objetivo final é que o computador execute as instruções de forma eficiente.

Programando para pessoas, não para o computador

  • O programador não deve escrever código pensando apenas no computador.
  • O objetivo principal é que outras pessoas possam entender e trabalhar com esse código.
  • Boas práticas de programação e organização são importantes para facilitar a compreensão futura.

Exemplo dos truques usados em retrogames

  • Programadores antigos utilizavam truques para extrair o máximo das limitações das máquinas antigas.
  • Comparado aos jogos atuais, os jogos retro ocupavam muito menos espaço em memória.
  • Antigamente, economizar ciclos de CPU e bytes era essencial devido ao alto custo desses recursos.

Mudanças na tecnologia e no custo dos recursos

  • Atualmente, os recursos como ciclos de CPU e memória são muito mais baratos.
  • Desperdiçar alguns gigabytes de RAM não é um problema significativo.
  • No entanto, o tempo do programador se tornou mais valioso.
  • Boas práticas de código limpo economizam tempo e facilitam a manutenção futura.

A importância da análise estática e dos compiladores

  • Os compiladores não apenas otimizam o código, mas também fornecem informações úteis para os programadores.
  • A árvore de sintaxe abstrata (AST) gerada pelo compilador pode ser usada para análise estática.
  • Ferramentas como linters aproveitam a AST para identificar problemas e possíveis bugs no código.

A origem das linguagens de programação

  • A ideia de criar uma gramática livre de contexto para descrever uma linguagem veio do criador do Fortran, John Backus.
  • O ALGOL foi uma das primeiras linguagens a utilizar essa abordagem.
  • Antes do C, o ALGOL era considerado a "linguagem avô" das linguagens de uso genérico.

Limitações dos compiladores e importância do código bem escrito

Nesta seção, o palestrante discute as limitações dos compiladores e destaca a importância do código bem escrito.

Limitações dos compiladores em relação ao código mal escrito

  • Um compilador não pode corrigir um código mal escrito que causa problemas como vazamento de memória.
  • Embora os compiladores façam o melhor possível, há limites para o que podem fazer com um código ruim.

Análise estática e ferramentas auxiliares

  • As ferramentas de análise estática, como linters, ajudam os programadores a escrever um código melhor.
  • Essas ferramentas apontam possíveis problemas e ambiguidades no código, permitindo que o programador os corrija.

A importância do código bem escrito

  • Boas práticas de código limpo economizam tempo de programação.
  • Mesmo trabalhando sozinho, o programador se beneficia de um código organizado e fácil de ler.
  • O futuro "você" agradecerá por ter feito um trabalho bem estruturado em momentos de emergência.

Conhecimento da máquina e comportamento do compilador

  • É importante entender como a máquina se comporta ao escrever código.
  • No entanto, mesmo o melhor compilador não pode melhorar um código ruim que realiza operações desnecessárias.

Análise estática e sua utilidade para os programadores

Nesta seção, o palestrante explora a utilidade da análise estática para os programadores.

Utilização da AST na análise estática

  • A árvore de sintaxe abstrata (AST) é utilizada na análise estática do código.
  • Ferramentas como linters usam a AST para identificar problemas e bugs potenciais no código.

Benefícios da análise estática

  • A análise estática ajuda os programadores a escreverem um código melhor.
  • Ela destaca possíveis erros ou ambiguidades no código antes mesmo da execução.

Origem das linguagens de programação

  • John Backus foi pioneiro na ideia de criar uma linguagem que descreve outra linguagem (meta-linguagem).
  • O ALGOL foi uma das primeiras linguagens a utilizar essa abordagem.
  • Essa ideia influenciou o desenvolvimento de linguagens de programação modernas.

A importância do ALGOL como precursor das linguagens de programação

Nesta seção, o palestrante destaca a importância do ALGOL como precursor das linguagens de programação modernas.

O papel do ALGOL no desenvolvimento das linguagens de programação

  • O ALGOL foi uma das primeiras linguagens de alto nível bem-sucedidas comercialmente.
  • John Backus utilizou a ideia de uma meta-linguagem para descrever outra linguagem ao criar o ALGOL.
  • O C é frequentemente considerado um avanço em relação ao ALGOL, mas este último é considerado o verdadeiro "avô" das linguagens genéricas.

Diferenças entre COBOL e ALGOL

  • Embora algumas pessoas considerem COBOL como o avô das linguagens, ele é mais relacionado às stored procedures em bancos de dados.
  • O verdadeiro avô das linguagens genéricas é o ALGOL, que precedeu o C e influenciou seu desenvolvimento.

Programming Languages and Virtual Machines New Section

Esta seção aborda a história das linguagens de programação e das máquinas virtuais, destacando a influência do Algol e o surgimento da linguagem C. Também explora a importância das metalinguagens e dos geradores de parsers.

História das Linguagens de Programação

  • A linguagem de programação conhecida como "Cambridge Programming Language" ou "ALGOL com os pés no chão" foi proposta, mas não foi bem aceita.
  • O Basic CPL ou BCPL foi uma tentativa de simplificar o CPL, mas também teve limitações.
  • Dennis Ritchie desenvolveu a linguagem C como sucessora do B, buscando ser mais próxima da máquina.
  • As principais funcionalidades das linguagens modernas têm suas raízes no Algol.

Metalinguagens e Geradores de Parsers

  • A notação do Backus para definição de gramáticas se tornou conhecida como Backus-Naur Form (BNF).
  • Ferramentas como flex, bison e yacc simplificam as definições matemáticas por trás das notações de linguagens.
  • O ANTLR é considerado um gerador de parser moderno utilizado para compilar diversas linguagens.
  • Um parser não serve apenas para compilar linguagens de programação, mas também para interpretar arquivos de configuração como YAML e JSON.

Máquinas Virtuais e Compiladores

  • O Document Object Model (DOM) é uma árvore resultante do parsing de HTML por um navegador.
  • Assim como o DOM está para o HTML, a Abstract Syntax Tree (AST) está para uma linguagem de programação.
  • Tanto compiladores quanto interpretadores começam com um lexer e um parser.
  • Bytecodes são instruções em formato binário utilizadas por máquinas virtuais como a JVM do Java.
  • As instruções específicas de uma máquina virtual não existem em forma de hardware físico, mas são interpretadas pela máquina virtual correspondente.

Conclusão

A história das linguagens de programação tem suas raízes no Algol, com a linguagem C se destacando como uma versão que influenciou muitas outras linguagens modernas. Metalinguagens e geradores de parsers simplificam o processo de definição e análise sintática das linguagens. Máquinas virtuais, como a JVM, permitem que programas escritos em diferentes linguagens sejam executados em ambientes compatíveis.

Como funciona um interpretador e uma máquina virtual?

Nesta seção, o palestrante explica como um interpretador e uma máquina virtual funcionam.

Interpretadores e Máquinas Virtuais

  • Um programa em Python passa por um lexer, parser e é transformado em instruções para uma máquina virtual de Python.
  • O AST resultante é serializado e salvo em um arquivo binário ".pyc".
  • O termo "interpretador" é usado para se referir a um caso especial de máquina virtual.
  • Interpretadores geralmente possuem facilidades de desenvolvimento, como um REPL (Read-Eval-Print-Loop).
  • Um REPL permite digitar código que é executado imediatamente, lendo, avaliando, imprimindo o resultado e repetindo até sair.

Linguagens Dinâmicas

  • Interpretadores costumam manter o AST em memória para permitir modificações.
  • Adicionar novos métodos no console do Python adiciona nós na árvore AST.
  • Isso define o conceito de "linguagem dinâmica", onde a estrutura do programa pode ser modificada durante a execução.

Diferença entre Interpretadores e Máquinas Virtuais

  • Interpretadores não tentam esconder nada da máquina host.
  • Acesso ao disco ou à rede é feito diretamente pela máquina real.
  • Exemplo: quando um programa Python pede acesso ao disco, ele obtém acesso ao disco real.
  • Máquinas virtuais, como VirtualBox, tentam esconder detalhes da máquina host para criar uma ilusão de estar rodando em uma máquina real.

Compiladores e Linguagem Intermediária

  • Compiladores modernos geralmente trabalham com uma representação intermediária.
  • Exemplos de linguagens intermediárias são IL (Intermediate Language), IR (Intermediate Representation) e RTL (Register Transfer Language).
  • A fase de otimização do compilador pode realizar várias otimizações, como pré-cálculo de expressões e eliminação de trechos não utilizados.

Conclusão

  • Interpretadores e máquinas virtuais transformam a árvore abstrata de sintaxe em instruções para uma arquitetura específica.
  • Compiladores também utilizam linguagens intermediárias para otimizar o código antes da geração das instruções finais.

O que é uma linguagem intermediária?

Nesta seção, o palestrante explica o conceito de linguagem intermediária e sua importância no processo de compilação.

Linguagem Intermediária

  • Uma linguagem intermediária é uma representação do código-fonte após a fase de parsing.
  • Compiladores modernos trabalham com uma representação intermediária para realizar otimizações e gerar as instruções finais.
  • Exemplos de linguagens intermediárias incluem IL (Intermediate Language), IR (Intermediate Representation) e RTL (Register Transfer Language).
  • A linguagem intermediária permite que o compilador realize diversas otimizações, como simplificação de expressões e remoção de trechos não utilizados.

Importância da Linguagem Intermediária

  • A linguagem intermediária facilita a implementação das fases subsequentes do compilador, como análise semântica, geração de código e otimização.
  • Ela fornece uma representação mais estruturada e abstrata do código-fonte, permitindo que o compilador realize transformações e otimizações de forma mais eficiente.
  • A utilização da linguagem intermediária ajuda a separar as preocupações do compilador em etapas bem definidas, tornando o processo de compilação mais modular e flexível.

Conclusão

  • A linguagem intermediária é uma representação do código-fonte após a fase de parsing.
  • Ela desempenha um papel fundamental no processo de compilação, permitindo otimizações e facilitando a implementação das fases subsequentes do compilador.

Otimização de Código

Nesta seção, o palestrante discute a importância da otimização de código em um compilador e como isso é feito através do uso de linguagens intermediárias.

Otimização de Código em Compiladores

  • A etapa de otimização de código é onde um compilador gasta mais tempo e recursos.
  • Um gerador de código varre o código escrito várias vezes para reescrevê-lo da melhor forma possível, sem quebrar ou gerar bugs.
  • Reescrever todas as estratégias de otimização para cada nova linguagem seria um trabalho absurdo e propenso a erros.
  • Por isso, é comum converter uma nova linguagem em uma linguagem intermediária após o parsing.
  • A linguagem intermediária geralmente é semelhante a um assembly para uma máquina virtual, permitindo que as estratégias de otimização sejam executadas nessa etapa.

Linguagem Intermediária

Nesta seção, o palestrante explica como as linguagens são convertidas em uma linguagem intermediária antes da aplicação das estratégias de otimização.

Conversão para Linguagem Intermediária

  • Após o parsing, uma nova linguagem é convertida em uma linguagem intermediária.
  • Essa conversão permite reaproveitar as estratégias de otimização já existentes.
  • A preocupação inicial ao criar uma nova linguagem é apenas fazer um parser que converta a nova linguagem em IR (Representação Intermediária).
  • Ferramentas como ANTLR podem ser usadas para esse propósito.

Front-end, Middle-end e Back-end

Nesta seção, o palestrante explica as três principais funções de um compilador: front-end, middle-end e back-end.

Funções do Compilador

  • Um compilador como GCC ou derivados de LLVM é dividido em três grandes funções: front-end, middle-end e back-end.
  • O front-end é responsável por fazer o lexing, parsing e gerar a árvore de sintaxe da linguagem.
  • Ele produz instruções intermediárias que representam o código em uma máquina virtual.
  • O middle-end lê essas instruções intermediárias e aplica várias otimizações para melhorar o código.
  • O resultado é um novo conjunto de instruções intermediárias totalmente otimizadas.
  • O back-end pega as instruções intermediárias otimizadas e as traduz para as instruções da máquina real (como x64 ou arm64).
  • O binário final resultante pode ser executado na máquina alvo.

Middle-End

Nesta seção, o palestrante explora a função do "middle-end" no processo de compilação.

Função do "Middle-End"

  • O "middle-end" é responsável por ler as instruções intermediárias não otimizadas e aplicar várias transformações e otimizações.
  • Cada uma dessas transformações pode ser um tema complexo que requer estudo avançado.
  • Após a aplicação das transformações, o resultado é um conjunto de instruções intermediárias totalmente otimizadas.

Back-End

Nesta seção, o palestrante discute a função do "back-end" no processo de compilação.

Função do "Back-End"

  • O "back-end" pega as instruções intermediárias otimizadas e as traduz para as instruções da máquina real.
  • Isso envolve converter as instruções assembly da linguagem intermediária para as instruções assembly específicas da arquitetura alvo (como x64 ou arm64).
  • O resultado é o binário final que pode ser executado na máquina alvo.

Criando uma Nova Linguagem

Nesta seção, o palestrante explora como criar uma nova linguagem usando partes existentes de um compilador.

Criando uma Nova Linguagem

  • Ao criar uma nova linguagem, é possível aproveitar partes existentes de um compilador.
  • A preocupação inicial é fazer apenas o front-end da nova linguagem, convertendo-a em IR.
  • Ferramentas como ANTLR podem ser úteis nesse processo.
  • Uma vez tendo a representação intermediária (IR), o restante do processo é igual para todas as linguagens.

Investimento em Compiladores

Nesta seção, o palestrante destaca a importância do investimento em tecnologias de compiladores e como isso afeta a migração para novas arquiteturas.

Investimento em Compiladores

  • Empresas como a Apple investiram quase duas décadas melhorando suas tecnologias de compiladores em cima do LLVM.
  • Isso permitiu que eles migrassem rapidamente de PowerPC para Intel e, posteriormente, de Intel para ARM.
  • O investimento contínuo em compiladores permite resolver problemas que não podem ser resolvidos apenas com otimizações de código.
  • A capacidade de projetar soluções no próprio chip contribui para o desempenho excepcional alcançado.

Diferença entre C, Java e Javascript

Nesta seção, o palestrante discute as diferenças entre as estratégias de compilação utilizadas em C, Java e Javascript.

Estratégias de Compilação

  • C é compilado usando a estratégia "ahead of time" (AOT), gerando um binário nativo da máquina.
  • Java e Javascript usam a estratégia do compilador "Just in Time" (JIT).
  • O JIT também possui várias estratégias diferentes para otimização.
  • Essas estratégias são adaptadas para resolver problemas específicos em cada linguagem.

The transcript continues beyond this point.

Disassembly and Optimization

In this section, the speaker discusses disassembly using javap and optimization in Java code.

Disassembly of Code

  • The speaker demonstrates disassembling code using javap.
  • They point out the instruction ldc2_w, which loads a constant from the constant pool to the stack.
  • The comment generated by javap indicates that the constant is a pre-calculated double value.
  • The speaker mentions that this optimization is similar to the previous example of 1 + 2 * 3.

Improving Code Readability

  • The speaker acknowledges that the calculation in the code may look ugly, even though it is simple.
  • They suggest writing a new class instead of directly calculating the circumference.
  • Instead of defining a constant for PI, they mention that Java already has a static double constant for PI in the Math class.
  • They emphasize that when dealing with trivial things like constants, it's best to search online or refer to official documentation.

Writing a New Class

  • The speaker proposes writing a new class with a static method called circunferencia that calculates the circumference based on a given radius.
  • They explain how to calculate 2 times Math.PI times the radius within this method.
  • In the main method, they use System.out.print to display the result of calling circunferencia with a radius value of 10.

Comparison and Clarity

  • After compiling and executing both versions (the original and improved), they compare them.
  • Although the improved version is longer, its intention is much clearer than before.
  • The speaker highlights that if they were to edit this code in one month, it would be immediately clear what it does.

Disassembly of Improved Code

  • The speaker performs disassembly again using javap on the improved code.
  • They notice the instruction ldc2_w loading the constant 10 (the radius) to the stack.
  • Another ldc2_w instruction loads the constant of 2 * PI to the stack, showing that it has been optimized.
  • The dmul instruction performs double multiplication on the last two values in the stack.

Limitations of Optimization

  • The speaker questions why the compiler didn't optimize further and eliminate the circunferencia method since 2 * pi * 10 is a constant value.
  • They explain that Java treats every function as an interface, and removing methods could break other classes that depend on them.
  • Java's dynamic nature and ability to alter code within the JVM prevent aggressive optimizations that could potentially break other parts of a program.

Interface Stability and Reflection

  • The speaker mentions that even if they had declared the method as private, it still couldn't be removed due to Java's reflection system.
  • Any optimization that breaks interface stability cannot be performed in Java.
  • They briefly explain how functions are called using addresses in binary systems and how compilers need to know where to jump when calling a function.

Understanding Interfaces

  • The speaker speculates about how JVM knows which address to jump to when calling a function without explicitly passing class parameters.
  • They mention that when JVM loads classes, it likely annotates functions with their respective addresses for jumps during compilation.

Class Reusability and Compilation

In this section, the speaker discusses class reusability, compilation, and class loading in Java.

Reusable Classes

  • The speaker emphasizes that Java classes are designed to be reusable rather than existing in isolation.
  • They demonstrate creating a new file with another class called Calc5 in the same directory as Calc3.
  • The Calc5 class calls the circunferencia function from Calc3 without explicitly mentioning Calc3 during compilation or execution.

Class Loading and Compilation

  • The speaker explains that when executing Calc5, the JVM automatically loads the Calc3 class using the Class Loader.
  • They highlight that there is no need to recompile Calc3 separately in this scenario.
  • If they were to delete the Calc3.class file and try to execute Calc5, it would result in a NoClassDefFound error.
  • Java's compilation process ensures stability between classes by maintaining their interface even if methods are not used.

Reflection and Private Methods

  • The speaker mentions that even if they had declared the method as private, it still couldn't be removed due to Java's reflection system.
  • They briefly explain how reflection allows manipulation of private methods in Java.

Understanding Interfaces and Binary Execution

In this section, the speaker discusses interfaces, binary execution, and function calls in machine code.

Binary Execution

  • The speaker explains that in binary systems, there is no concept of abstraction; everything consists of instructions and addresses.
  • When calling a function, the machine stores the address on the stack, performs a jump to execute it, and returns to continue from where it left off.

Annotating Function Addresses

  • The speaker speculates that during compilation of Calc3, the compiler likely annotates that the circunferencia function will be available at a specific address (e.g., 90000000).
  • When compiling Calc5, the compiler knows to write a jump instruction to that specific address (e.g., 90000000) without explicitly passing class parameters.

Summary

In this section of the transcript:

  • The speaker demonstrates disassembling code using javap and highlights optimization in Java code.
  • They discuss the importance of code readability and suggest writing a new class for improved clarity.
  • The limitations of optimization are explained, considering interface stability and Java's reflection system.
  • The speaker explores class reusability, compilation, and class loading in Java.
  • They explain how interfaces are annotated with function addresses during compilation and how binary execution works.

Como os endereços são definidos em tempo de execução

Nesta seção, o palestrante explica como os endereços são definidos em tempo de execução e como isso difere em linguagens como C, C++, Swift, Go, Rust e Crystal.

Definição dos endereços em tempo de execução

  • Os endereços não estão nas classes, mas sim atribuídos quando a JVM carrega as classes.
  • Os endereços finais são definidos quando o módulo é carregado pela JVM.
  • Em linguagens como C, C++, Swift, Go, Rust e Crystal que não possuem um interpretador ou máquina virtual para controlar os endereços em tempo de execução, cada parte do módulo precisa saber antecipadamente o endereço final das coisas.

Como lidar com múltiplos arquivos em um projeto

Nesta seção, o palestrante aborda a complexidade de compilar projetos com vários arquivos e explica como os compiladores dividem essa tarefa em duas etapas.

Compilação de projetos com múltiplos arquivos

  • Compilar projetos com milhares de arquivos seria uma tarefa complexa.
  • O GCC (GNU Compiler Collection), clang (LLVM Compiler Infrastructure) e rustc (Rust Compiler) dividem a compilação em duas etapas.
  • A primeira etapa envolve checar dependências e chamar o compilador para cada arquivo do projeto.
  • Durante essa etapa, são gerados arquivos objetos (.o), que são binários compilados a partir do código fonte de cada arquivo.

O papel do linker na compilação

Nesta seção, o palestrante explica o papel do linker na compilação e como ele lida com a resolução de endereços das funções.

Função do linker na compilação

  • O linker é responsável por ligar os endereços dos binários gerados durante a compilação.
  • Durante a primeira fase da compilação, quando os endereços ainda não são conhecidos, o compilador deixa espaços em branco para os endereços das funções externas.
  • A segunda fase da compilação é realizada pelo linker (como o GNU LD), que resolve esses endereços em tempo de linking.
  • O linker também pode realizar otimizações, como inlining de funções, para melhorar o desempenho do programa final.

Otimizações realizadas pelo compilador e linker

Nesta seção, o palestrante discute as otimizações realizadas pelo compilador e pelo linker para melhorar o desempenho do programa final.

Otimizações realizadas pelo compilador e linker

  • O compilador de C consegue analisar o código e realizar diversas otimizações para reescrevê-lo de forma mais eficiente.
  • Essas otimizações são feitas arquivo por arquivo independentemente.
  • O linker reavalia os binários gerados durante a compilação, liga os endereços onde necessário e realiza mais otimizações.
  • Isso resulta em um programa final menor, que economiza memória e processamento.

Performance Comparison: C vs Java vs JavaScript

This section discusses the performance differences between C, Java, and JavaScript. It explains how Just In Time (JIT) compilers work in Java and JavaScript to optimize code execution.

JIT Compilation in Java

  • When a project is loaded in Java, the JVM (Java Virtual Machine) starts and the class loader loads the binary files (.class) of the project.
  • The JVM creates an internal table that maps memory addresses to classes and functions.
  • The JVM acts as an interpreter or virtual machine, eliminating the need for a static linker.
  • Unlike C, which needs to know the address of everything beforehand, Java's JVM dynamically handles function calls by referring to this internal table.
  • The JVM analyzes frequently executed code segments (hotspots) and optimizes them during runtime using Just In Time (JIT) compilation.
  • JIT caching improves performance by translating bytecode into optimized native machine instructions.

JIT Compilation in JavaScript

  • Browsers like Chrome use Google V8 as their JavaScript engine.
  • V8 compiles JavaScript into intermediate bytecodes specific to its virtual machine.
  • These bytecodes are similar to instructions for a real x64 machine but tailored for a non-existent JavaScript machine.
  • The V8 interpreter called Igniter translates these bytecodes instruction by instruction.
  • Hotspots are identified as users navigate through web pages, and TurboFan (V8's JIT compiler) optimizes these frequently used parts by generating optimized native machine code.

Performance Comparison

  • Both Java and JavaScript can achieve high speeds without aggressive pre-compilation like C due to their JIT compilation capabilities.
  • However, C compilers have matured over time and tend to generate faster native binaries compared to Java or JavaScript engines.

Diferença entre linguagens estáticas e dinâmicas

Nesta seção, o palestrante discute a diferença entre linguagens de programação estáticas e dinâmicas, destacando suas características e propriedades.

Comportamento fixo das linguagens estáticas

  • Linguagens estáticas possuem binários compilados estaticamente, o que significa que seu comportamento é fixo e imutável.
  • É possível alterar o comportamento de uma linguagem estática através de técnicas como injeção de código direto na memória do processo ou patches de binários, mas isso é considerado uma prática não recomendada.

Flexibilidade das linguagens dinâmicas

  • Linguagens dinâmicas, por outro lado, permitem a modificação do bytecode e a alteração do comportamento do programa em tempo real de execução.
  • Essa flexibilidade é possibilitada pelo uso de interpretadores que aceitam código novo sem precisar reiniciar o programa.
  • Linguagens dinâmicas como JavaScript, Ruby, Python, Elixir, Java e C# possuem capacidades poderosas de metaprogramação.

Vantagens e desvantagens

  • As linguagens estáticas tendem a ter um compilador que gera binários estáticos com bytecode menos facilmente modificável.
  • Já as linguagens dinâmicas costumam ter um interpretador para permitir a modificação do bytecode em tempo real.
  • A escolha entre uma abordagem estática ou dinâmica depende do uso específico da linguagem. Não há resposta definitiva sobre qual abordagem é melhor.

Capacidades de metaprogramação das linguagens dinâmicas

Nesta seção, o palestrante explora as capacidades de metaprogramação presentes em linguagens dinâmicas como JavaScript, Ruby, Python, Elixir, Java e C#.

Metaprogramação em linguagens dinâmicas

  • Linguagens dinâmicas possuem recursos poderosos de metaprogramação que permitem escrever código que altera o próprio código em execução.
  • Exemplos desses recursos incluem Reflection em Java, APIs como Reflect ou Proxy em JavaScript ES6, a função type em Python e Refinements em Ruby.

Injeção de bytecodes e JIT

  • Interpretadores permitem a injeção de novos bytecodes sem reiniciar o programa.
  • A maioria dos interpretadores possui um Just-in-Time Compiler (JIT) embutido que gera binários nativos otimizados em tempo real.
  • Essa abordagem combina as vantagens da compilação prévia com as capacidades de modificação do bytecode durante a execução.

Desempenho das linguagens interpretadas

Nesta seção, o palestrante discute o desempenho das linguagens interpretadas e destaca exemplos como Erlang, Java e JavaScript.

Desempenho escalável do Erlang

  • Erlang é uma linguagem interpretada que roda numa máquina virtual com um Just-in-Time Compiler (JIT).
  • Apesar de ser interpretada, Erlang é conhecida por sua escalabilidade e é utilizada no motor de comunicação do WhatsApp.

Desempenho do JavaScript

  • O desempenho impressionante do JavaScript é em grande parte devido ao uso do JIT.
  • O JIT permite que o JavaScript alcance performances absurdas, como rodar jogos dentro do navegador.

Transpiladores e máquinas virtuais

  • Algumas linguagens, como PHP, Python e Ruby, possuem transpiladores ou geram bytecodes para suas próprias máquinas virtuais.
  • Essas abordagens permitem a execução eficiente de código interpretado.

Conclusão

Nesta seção final, o palestrante conclui sua explanação sobre as diferenças entre linguagens estáticas e dinâmicas, destacando a importância de compreender essas distinções na programação.

Importância de entender as diferenças

  • É importante compreender as diferenças entre linguagens estáticas e dinâmicas para evitar argumentos infundados sobre desempenho ou qualidade das linguagens.
  • A escolha entre uma abordagem estática ou dinâmica depende do contexto e dos requisitos específicos do projeto.
Video description

Chegou a hora de finalmente entender qual a diferença de linguagens compiladas e interpretadas, linguagens estáticas e dinâmicas. Java é compilado? Javascript é interpretado? Qual a diferença? Hoje você vai ganhar uma fundação mais sólida pra entender linguagens da maneira correta e é o pré-requisito pros próximos videos onde finalmente vou discutir as linguagens mais famosas da atualidade. == Errata eu falo que linguagens de programação são linguagens regulares, mas na realidade são livres-de-contexto. Regulares são mais restritas. o resumo ficou um pouco resumido demais. maus. == Conteúdo 00:00:00 - Intro 00:00:55 - Pré-Requisitos 00:01:53 - Hello World em C e Java 00:03:03 - ELF vs CAFE 00:03:41 - 1a tentativa: compilador vs interpretador 00:04:45 - Estudo de Linguagens 00:07:37 - Análise Léxica 00:10:59 - Análise Sintática 00:14:39 - Abstract Syntax Tree (AST) 00:15:48 - Notação Polonesa 00:17:58 - Otimização de Bytecode 00:22:55 - Pra que serve um Programador? 00:27:04 - Linters 00:28:00 - Backus, Naur, BNF e História 00:31:57 - Parsers e "DOM" 00:32:58 - Interpretadores e Máquinas Virtuais 00:36:56 - Linguagens Dinâmicas 00:38:58 - Otimização Binária 00:42:52 - As Fases de um Compilador 00:46:23 - Just-In-Time Compiler (JIT) 00:50:33 - Linkers 00:59:24 - JIT de novo 01:02:30 - Google V8 01:05:25 - Por que dinâmico em vez de estático? 01:07:42 - 2a tentativa: compilador vs interpretador? 01:10:45 - Bônus: Bloopers (novidade) == Links * ANSI C Grammar (https://www.lysator.liu.se/c/ANSI-C-grammar-y.html#compound-statement) * EcmaScript 2023 (https://tc39.es/ecma262/#sec-ecmascript-language-statements-and-declarations) * List of Java bytecode instructions (https://en.wikipedia.org/wiki/List_of_Java_bytecode_instructions) * Why the New V8 is so Damn Fast (https://nodesource.com/blog/why-the-new-v8-is-so-damn-fast/) * V8 Bytecode.h (https://github.com/v8/v8/blob/master/src/interpreter/bytecodes.h) * LLVM Analysis and Transform Passes (https://llvm.org/docs/Passes.html) * HHVM (https://hhvm.com/) Podcast: https://anchor.fm/akitando/episodes/Akitando-117---Linguagem-Compilada-vs-Interpretada--Qual--melhor-e1h765n Transcript: https://www.akitaonrails.com/2022/04/15/akitando-117-linguagem-compilada-vs-interpretada-qual-e-melhor