Introdução
Olá a todos. Bem-vindos a novembro. Espero que estejam bem.
Tivemos um outubro super ocupado. Acabámos de lançar uma grande melhoria nas soluções da comunidade. Agora removemos as duplicações, por isso as soluções semelhantes só aparecem uma vez, adicionámos novas opções de ordenação, a possibilidade de pesquisar por código e, se fores ver o C#, vais ver que também adicionámos a possibilidade de filtrar por diferentes conceitos de programação. Assim, podes procurar soluções que usem deslocamento de bits, recursão ou qualquer outra coisa que te apeteça. Vamos alargar isso a outros percursos durante a semana da comunidade.
Mas, por agora, vamos concentrar-nos no #12in23. Outubro foi um mês interessante, a explorar linguagens orientadas a objetos, mas este mês vamos a fundo. Vamo-nos concentrar nas linguagens assembly e, mais concretamente, em MIPS Assembly, x86-64 Assembly e WebAssembly. Como sempre, o Erik vai explicar-nos o que torna estas linguagens interessantes e únicas.
Os distintivos
Como sempre, podes ganhar o distintivo Novembro Nibbly completando 5 exercícios em qualquer uma destas linguagens. Temos também o distintivo de Ano Inteiro, que sei que muitos de vocês estão a tentar alcançar. Para isso, temos 5 exercícios em destaque para completarem. São estes:
- Contagem de Bits: conta os bits 1 de um número
- Grãos: calcula o número de grãos num tabuleiro de xadrez com casas que duplicam
- Cor do Resistor: converte a cor de uma banda de resistência na sua representação numérica
- Cifra Rotacional: implementa a cifra rotacional (também conhecida como cifra de César)
- Contagem de Nucleótidos: calcula quantas vezes cada nucleótido aparece numa string de ADN
Contexto
Porque é que este mês se chama Novembro Nibbly?
Bem, como provavelmente sabes, um byte são 8 bits. E um nibble são 4 bits. O nome começa a fazer sentido se trocares o y de byte por um i, o que dá bite. Um nibble é, então, uma pequena dentada.
O que é uma linguagem assembly?
Bem, vamos começar pelo básico. Uma CPU executa instruções, como «somar dois números» ou «deslocar bits para a esquerda», etc. Estas instruções são conhecidas como instruções de código máquina e não passam de sequências específicas de bits. Executar um programa é «simplesmente» (entre aspas) a CPU a processar e a executar estas sequências de bits.
Como escrever diretamente as instruções como sequências de bits é trabalhoso e propenso a erros, Kathleen e Andrew Donald Booth criaram uma linguagem mais amigável para os humanos, para representar instruções de código máquina, já lá em 1947. Uma linguagem deste tipo, que representa instruções de código máquina, chama-se linguagem assembly. Esta linguagem assembly é depois convertida em instruções de código máquina através de um «assembler».
Um aspeto interessante das linguagens assembly é que estão diretamente ligadas a uma arquitetura de CPU, embora sejam normalmente independentes do sistema operativo.
Já agora, se te lembras dos cartões perfurados, aquelas grandes peças de cartão usadas nos primeiros computadores para os fazer executar programas, também eram uma linguagem assembly!
Como é que a linguagem assembly é diferente das linguagens em que programamos hoje?
A principal diferença é que as linguagens assembly são de muito baixo nível. Vais sentir falta de muitas abstrações a que te habituaste. Não encontras classes nem objetos em lado nenhum. Ciclos? Vais ter de os escrever manualmente, através de saltos. Funções? Nem pensar! Achei que foi uma grande lição de humildade escrever código assembly, porque depois percebes o quanto as linguagens modernas te facilitam a vida. Mas também é incrivelmente útil escrever código assembly, porque ganhas uma compreensão muito melhor de como as coisas funcionam realmente.
Curiosidade: 99% do código-fonte do RollerCoaster Tycoon era código assembly escrito à mão! É um feito espantoso, que vais apreciar ainda mais depois de terem escrito um pouco de assembly.
As pessoas ainda escrevem em código assembly?
Bem, menos do que antigamente. Antigamente, o assembly escrito à mão superava muitas vezes o código máquina gerado pelo compilador (uma das razões pelas quais a linguagem C++ permite incorporar diretamente código assembly), mas os compiladores tornaram-se tão bons a gerar código máquina que isso já raramente acontece. Dito isto, continuas a encontrar linguagem assembly a ser usada em ambientes críticos para o desempenho ou com recursos limitados.
Visão geral
MIPS
- MIPS (Microprocessor without Interlocked Pipelined Stages) é uma família de arquiteturas de conjunto de instruções de computador de conjunto reduzido de instruções (RISC)
- Desenvolvida pela MIPS Computer Systems e lançada pela primeira vez em 1985
- Várias versões: MIPS I, II, III, IV, V e MIPS32/64. As duas primeiras versões eram apenas de 32 bits, mas o MIPS III introduziu o suporte de 64 bits.
- Várias extensões opcionais, como instruções SIMD e compressão
- Muito influente nas arquiteturas RISC posteriores
- A MIPS anunciou em 2021 que a arquitetura MIPS já não estava a ser desenvolvida e migrou para o RISC-V (uma arquitetura de código aberto e livre de royalties)
- Usado sobretudo em sistemas embebidos (por exemplo, routers) e servidores (os computadores da Silicon Graphics usavam-no, e ficaram famosos pelos efeitos especiais em filmes), no supercomputador NEC Cenju-4, no carro Model S da Tesla, na sonda New Horizons da NASA, mas também para ensinar assembly nas universidades e em várias consolas de jogos (por exemplo, a PlayStation original, a PlayStation Portable e a Nintendo 64)
x86-64 Assembly
- Concebida pela AMD e lançada em 1999 como a arquitetura AMD64
- É uma versão de 64 bits do conjunto de instruções x86, que remonta a 1978, quando a Intel lançou o seu microprocessador 8086. Era um processador de 16 bits, mas mais tarde o 80386 acrescentou instruções de 32 bits, e esse conjunto de instruções tornou-se sinónimo de x86.
- O principal que os 64 bits permitiram foi endereçar mais memória (o endereçamento de 32 bits está limitado a 4 GB), que se tinha tornado um estrangulamento. Em teoria, os 64 bits podem endereçar 16 exabytes, mas atualmente só são usados 48 bits, o que permite endereçar 256 TB (pode ser alargado mais tarde, quando for preciso)
- O AMD64 estende o conjunto de instruções x86 e foi concebido para ser totalmente compatível com as aplicações existentes de 16 e 32 bits, através de um modo de compatibilidade
- A Intel tinha concebido o IA-64 sem a participação da AMD. Era um conjunto de instruções de 64 bits novo, muito diferente e incompatível com versões anteriores. No fim, o AMD64 venceu e a Intel implementou a sua própria versão dele (apenas com diferenças semânticas menores)
- Usado em todo o lado. De estações de trabalho a servidores (incluindo supercomputadores), de sistemas embebidos a consolas de jogos (por exemplo, a PS5 e a Xbox Series X).
WebAssembly
- Concebido pelo W3C, um organismo de normalização para tecnologias web
- Os objetivos de conceção são:
- Rápido, seguro e portátil
- Representação eficiente e portátil
- Antigamente, a execução rápida na web era normalmente obtida através de plugins especializados do navegador, como o Flash e o Silverlight, uma vez que o próprio JavaScript não se presta muito bem à computação de alto desempenho. As principais desvantagens destes plugins eram que normalmente tinham muitos problemas de segurança e não eram normalizados.
- A Mozilla concebeu o asm.js, um subconjunto de JavaScript que pretendia permitir executar código no navegador com ótimo desempenho, o que conseguia através da consistência de tipos (sem alteração dinâmica de tipos) e sem recolha de lixo. As linguagens podiam então compilar para asm.js e ainda assim ter bom desempenho na web. Mas continuava a ser JS, por isso era limitado no que se podia alcançar. Daí a proposta de uma nova linguagem: o WASM.
- É uma linguagem do tipo assembly, na medida em que fornece um conjunto de instruções a executar. Crucialmente, não está ligada a uma CPU específica, pelo que é independente da plataforma e exige uma implementação para cada plataforma (uma máquina virtual). Isto significa que o WebAssembly é, na verdade, bytecode, e não código máquina
- Tipagem estática (uma diferença crucial em relação ao JS)
- Usa normalmente compilação ahead-of-time ou just-in-time (mas pode ser interpretado)
- É uma norma aberta e define duas coisas:
- Um formato binário
- Um formato de texto (que é compilado para o formato binário)
- Existem implementações em todos os principais navegadores
- É usado em muitas páginas web que exigem alto desempenho, como o Google Earth, o Figma, o Unity e o Autocad. Tem também vindo a ganhar terreno no lado do servidor, por exemplo para executar microsserviços, correr em plataformas SaaS (como os CloudFlare workers) ou no Docker
E do ponto de vista da programação, em que é que diferem?
MIPS
- Usa uma arquitetura load-store (também conhecida como registo-registo), em que as instruções ou fazem acesso à memória, ou aritmética, mas operam exclusivamente sobre dados nos registos
x86-64 Assembly
- Usa uma arquitetura registo-memória, que permite realizar operações sobre (ou a partir da) memória, bem como sobre registos
WebAssembly
- Usa programação baseada em pilha (sem registos), com a opção de ler e escrever dados na memória
O que é que torna estas linguagens tão boas?
MIPS
- Pequena. O conjunto de instruções do MIPS cabe todo numa única página
- Convenções de chamada bem estabelecidas ajudam a perceber como usar os registos disponíveis, por exemplo, quais usar para passar argumentos e quais para devolver resultados.
- Estável. A última versão foi lançada em 2014
- Amplamente documentada, sobretudo em textos académicos
- Muita utilização no mundo real. Milhares de milhões de dispositivos
x86-64 Assembly
- Embora seja uma extensão do x64, foram acrescentadas muitas funcionalidades novas, incluindo:
- Suporte para inteiros de 64 bits
- Registos adicionais
- Instruções SSE (instruções vetoriais)
- Acesso relativo aos dados (mais eficiente com bibliotecas partilhadas)
- Bit No-Execute (funcionalidade de segurança que impede a execução de código em certas páginas de memória)
- Familiar. Como estende o conjunto de instruções x86, será relativamente fácil de aprender para quem já está familiarizado com o conjunto de instruções x86 Documentação extensa e detalhada
- Estável. Embora sejam acrescentadas novas versões regularmente, o núcleo mantém-se extremamente estável e compatível com versões anteriores
WebAssembly
- A máquina virtual do WebAssembly, baseada em pilha, é leve e simples em comparação com as linguagens assembly dos processadores físicos (incluindo o RISC). Isso torna-a relativamente fácil de compilar.
- O formato de texto do WebAssembly usa «açúcar» sintático de S-expressions para obter um estilo imperativo familiar, que é transformado em código baseado em pilha. A forma com S-expressions chama-se «forma açucarada» e é «desaçucarada» para a outra forma, equivalente ao que está no binário. As S-expressions serão familiares para quem já trabalhou com uma LISP
- Forte interoperabilidade com o JavaScript. É simples passar dados de e para o JavaScript. Um aviso importante: o WASM ainda não permite interagir com o DOM
- Está em constante melhoramento. Não só as máquinas virtuais do WASM estão a ser melhoradas, como a própria norma está em desenvolvimento ativo. Há imensas funcionalidades novas a ser concebidas e desenvolvidas, como instruções relacionadas com SIMD, recolha de lixo, threads, otimizações de chamadas de cauda e muito mais
- Seguro. O código é validado e executado num ambiente isolado, o que proporciona um grau de validação estática superior ao do JavaScript ou das linguagens assembly nativas. A semântica está bem definida, o que a torna fácil de verificar e de analisar
Funcionalidades de destaque
MIPS
- Eficiência. Os processadores MIPS são muito eficientes, o que os torna ótimos para sistemas embebidos.
- Desempenho. Excelente desempenho, daí o MIPS ter sido usado em supercomputadores
- Fácil de aprender. Com poucas instruções e cada instrução a fazer apenas uma coisa simples, é fácil de aprender. Ótima para fins de ensino.
x86-64 Assembly
- Poderosa. O x86-64 foi afinado ao longo de décadas, com imensas instruções que ajudam no desempenho. Um exemplo disso é o SIMD (Single Instruction, Multiple Data), que são instruções que, bem, permitem que uma única instrução seja executada em paralelo sobre vários dados.
- Omnipresente. Os dispositivos que correm x86-64 estão em todo o lado. As CPUs da Intel e da AMD implementam-no. É o padrão de facto há bastante tempo
- Atualizada regularmente. Por exemplo, novas instruções vetoriais, com SSE3-5, AVX, AVX-512 e mais
WebAssembly
- Eficiente. O formato binário é compacto e pode ser descodificado, validado e compilado numa única passagem rápida. Também suporta streaming, o que permite que a descodificação, a validação e a compilação comecem o mais cedo possível, antes de todos os dados terem sido vistos. E também é paralelizável. Isto torna-o perfeito para aplicações web de alto desempenho.
- Excelente alvo de compilação. Permite que código escrito em muitas linguagens seja executado na web. A maioria das grandes linguagens suporta a compilação para binários WebAssembly, o que significa que o teu código pode correr na web sem precisares de escrever JavaScript. Algumas linguagens não compilam o teu código para WebAssembly; em vez disso, compilam o runtime para WebAssembly, que pode depois executar bytecode inalterado.
- Fácil de publicar e executar. Só precisas de uma máquina virtual capaz de interpretar o bytecode, e todos os principais navegadores incluem uma.
- Não está preso à web, também pode correr no lado do servidor. A WebAssembly System Interface (WASI) é uma interface (ABI e API) concebida para ser portátil para qualquer plataforma. É semelhante ao POSIX (interfaces padrão para sistemas Unix) e fornece coisas como I/O. A segurança é uma parte fundamental da sua conceção, que inclui o isolamento e a orientação a capacidades (tens de pedir explicitamente permissões para coisas como ficheiros ou sockets). O WASI tem também o potencial de permitir uma fácil interligação entre linguagens. Solomon Hykes, cofundador da Docker, escreveu em 2019: «Se o WASM+WASI tivesse existido em 2008, não teríamos precisado de criar o Docker»
O que escolher
- Se nunca trabalhaste com uma linguagem assembly, o WebAssembly é provavelmente a linguagem mais fácil para começar. Dito isto, se queres aprender uma linguagem assembly que compile para código máquina, experimenta o MIPS Assembly
- Se estás a trabalhar numa máquina x86-64 (muito provável), experimenta o x86-64 Assembly
- Se estás familiarizado com LISP, vais gostar de saber que o WebAssembly usa S-expressions
- Se trabalhas com aplicações web, o WebAssembly é a opção mais lógica
- Se te preocupas com o desempenho, o x86-64 e o MIPS são ótimas opções. Ou, se te preocupas com o desempenho na web, experimenta o WebAssembly