Joao Brietzke Blog

← voltar

Como uma CPU funciona, do transístor ao sistema completo

Este post foi construído como uma escada. Cada camada começa pela pergunta natural que você faz ao tentar entender uma CPU, e vai sendo refinada e aprofundada até chegar na ideia de que um computador inteiro é só uma pilha de contratos simples. No fim, a "inteligência" da máquina deixa de ser mágica e passa a ser algo que você consegue rastrear, do elétron ao programa.


Ponto de partida: o que é, afinal, uma CPU

Você provavelmente imagina a CPU como o cérebro do computador, algo que recebe informações e as processa. A primeira parte está certa, mas a palavra "processar" precisa ser aberta, porque junta duas coisas diferentes. A CPU não pensa sobre as informações. Ela executa instruções, uma de cada vez, sem entender a intenção por trás delas. E o lado "lógico" é bem literal: tudo se reduz a operações sobre 0s e 1s, como somar, comparar, mover dados de um lugar para outro e desviar (decidir "se isto for verdade, pule para tal instrução"). Operações simples assim, repetidas aos bilhões e na ordem certa, é o que constrói desde uma planilha até um jogo 3D. A inteligência que parece existir está em como o programa organiza as instruções, não em nenhuma instrução isolada.

Por dentro, a CPU tem quatro peças principais. A Unidade de Controle coordena o trabalho e decide o que acontece a cada momento. A ULA (Unidade Lógica e Aritmética) faz as contas e as comparações. Os registradores são uma memória ultrarrápida, pequena, que fica ao lado da ULA. E o cache é uma memória intermediária para o que se usa com frequência. O programa em si mora na memória RAM, e a CPU o busca de lá dentro de um ciclo que nunca para.

Componentes da CPU

Uma dúvida que aparece cedo é se a CPU recebe um binário e, a partir dele, realiza uma ação direta, como mover ou deletar um arquivo. A resposta tem uma camada no meio que muda tudo: a CPU nunca sabe o que é um arquivo. "Deletar arquivo" é uma ordem de alto nível, que vai sendo traduzida camada por camada até virar instruções binárias mínimas. A CPU só enxerga ordens do tipo "leia o byte do endereço X", "compare com Y", "escreva Z em W". Ela não faz ideia de que aquilo, somado, significa apagar alguma coisa.

Da ação ao binário

Cada instrução binária tem duas partes: o opcode (o que fazer) e os operandos (sobre o quê). Um detalhe curioso fecha essa ideia: "deletar" raramente apaga os dados na hora. Na maioria dos casos o sistema só marca o espaço como reusável, e por isso programas de recuperação às vezes conseguem trazer arquivos de volta.

Talvez você esteja pensando que a CPU é então um executor que recebe instruções pontuais, como quando você roda comandos no terminal. O espírito está certo, mas falta uma distinção de nível. Um comando de terminal, como rm arquivo.txt, ainda é de alto nível: ele não é uma instrução da CPU, e por baixo é traduzido em milhares de instruções de máquina minúsculas. O vocabulário real da CPU é muito mais primitivo (coisas como "carregue o endereço X", "compare", "pule se igual"), e ela puxa essas instruções sozinha da memória, aos bilhões por segundo, sem ninguém digitando nada. A comparação certa é esta: o terminal está para você assim como o conjunto de instruções de máquina está para a CPU. Os dois são executores literais, mas com vocabulários de granularidade muito diferente. A partir daqui, descemos pela escada, do interruptor físico até o sistema inteiro.


Camada 1: transístores e portas lógicas

No nível mais baixo, a CPU são bilhões de transístores, que funcionam como interruptores controlados por voltagem: ligado é 1, desligado é 0. Sozinhos, eles não fazem nada inteligente. A capacidade aparece quando você os arruma em certos padrões, e esses arranjos passam a se comportar como portas lógicas (AND, OR, NOT, XOR). Vale corrigir um ponto de vocabulário aqui, porque ele abre as próximas camadas: o transístor e a porta lógica não são coisas separadas que se conectam. A porta lógica é feita de transístores.

A lógica nasce do arranjo. Imagine dois interruptores controlando uma lâmpada. Ligados em série, a corrente só passa se os dois estiverem ligados, e isso é uma porta E (AND). Ligados em paralelo, a corrente passa se qualquer um estiver ligado, e isso é uma porta OU (OR). O mesmo material físico calcula regras diferentes só de mudar como está ligado.

Chaves viram lógica

Em transístores reais isso é automático, já que o sinal de um transístor liga ou desliga o outro. E aquela ideia de "tomar decisões e seguir caminhos diferentes" já começa aqui. Existe um arranjo de portas chamado multiplexador que seleciona entre duas entradas conforme um bit de controle: se o controle é 0 sai um caminho, se é 1 sai o outro. É o "se isto, então aquilo" surgindo puramente de portas lógicas. Guarde esse fio, porque ele reaparece como as flags na Camada 2 e como os saltos condicionais na Camada 4.


Camada 2: a ULA, ou lógica organizada virando aritmética

Transístores e portas sozinhos são burros. Arrumados em blocos, fazem contas de verdade, e é aí que entra a ULA. O exemplo mínimo que prova isso é o somador: com apenas duas portas você já soma dois bits. A porta XOR dá o bit da soma, e a porta AND dá o "vai-um".

Meio-somador

A matemática por trás das operações

Tudo se apoia em duas bases: notação posicional em base 2 e álgebra booleana. Quando você junta as duas, a aritmética cai como consequência.

Comece pela representação. Um número de n bits vale a soma das potências de 2 ligadas: N = Σ bᵢ·2ⁱ. Por exemplo, 1011₂ = 8 + 2 + 1 = 11. Cada casa vale o dobro da anterior, e é dessa regra que saem soma, deslocamento e comparação.

As portas são funções booleanas, e duas delas coincidem com aritmética: AND é a multiplicação de bits (a·b), XOR é a soma módulo 2 ((a+b) mod 2) e NOT a = 1−a. Essa coincidência é a ponte entre lógica e aritmética.

Daí sai a identidade do somador. Somar dois bits dá 0, 1 ou 2, então o resultado precisa de dois bits de saída: a + b = 2c + s, com s = a⊕b e c = a∧b. Para somar mais de um bit, você encadeia somadores levando o vai-um adiante:

sᵢ      = aᵢ ⊕ bᵢ ⊕ cᵢ
cᵢ₊₁    = (aᵢ ∧ bᵢ) ∨ (cᵢ ∧ (aᵢ ⊕ bᵢ))      com c₀ = 0

Funciona por causa da notação posicional: o vai-um da casa i vale 2^(i+1), então passá-lo para a casa seguinte preserva o valor total.

A subtração reaproveita o mesmo somador, usando o complemento de dois. Em n bits, a aritmética dá a volta módulo 2ⁿ. Define-se o negativo de B como 2ⁿ − B, e acontece que 2ⁿ − B = (NOT B) + 1, porque subtrair B de "tudo 1" inverte cada bit. Logo:

A − B = A + (NOT B) + 1   (mod 2ⁿ)

Por isso não há circuito separado de subtração: a ULA inverte B, joga o +1 no carry inicial c₀ e usa o somador que já existe. Na leitura com sinal, o bit mais à esquerda passa a ter peso negativo: N = −b₍ₙ₋₁₎·2^(n−1) + Σ bᵢ·2ⁱ.

O deslocamento sai direto da base posicional. Empurrar todos os bits uma casa para a esquerda multiplica por 2, e para a direita divide: x << k = x·2ᵏ e x >> k = ⌊x/2ᵏ⌋. É a forma mais barata de multiplicar ou dividir por potências de 2, porque nem aciona o somador.

A comparação é uma subtração seguida da leitura das flags. Você calcula A − B e olha o estado, não o valor. A = B quando o resultado é 0, o que liga a flag Z. Sem sinal, A ≥ B quando houve vai-um final, a flag C. A flag N é o bit mais à esquerda do resultado, ou seja, o sinal. A flag V (overflow) liga quando o resultado com sinal não cabe, e é detectada por V = c₍ₙ₋₁₎ ⊕ cₙ, isto é, quando o vai-um que entra no último bit difere do que sai dele. Com sinal, A < B quando N ⊕ V = 1. No fundo, a "decisão" do computador é um XOR entre dois bits de carry, e aquele fio do multiplexador da Camada 1 reaparece aqui como as flags.

A ULA reúne todos esses circuitos especializados (soma e subtração, lógica, deslocamento, comparação) e os faz calcular ao mesmo tempo. Um multiplexador escolhe qual resultado sai, conforme o opcode. Ela é rápida justamente porque não decide primeiro para depois calcular: calcula tudo e só seleciona no fim. Junto do resultado, ela emite as flags Z, C, N, V.

Estrutura interna da ULA

A ULA não decide o que calcular. Ela executa o que o opcode mandar, e quem manda é a camada de cima.


Camada 3: microarquitetura, ou o que faz a ULA o que fazer

Falta dizer à ULA o que fazer, com quais números, e onde guardar o resultado. Quem dá essa ordem é a Unidade de Controle, e os números vêm dos registradores e, mais atrás, da memória. A peça que faltava no seu modelo é separar dois tipos de fluxo. De um lado estão os dados (os números e as instruções). Do outro estão os sinais de controle (as ordens). A Unidade de Controle não carrega números, ela apenas comanda.

Caminho de dados

A Unidade de Controle decodifica a instrução (separa opcode e operandos), gera os sinais de controle (manda o opcode para a ULA, diz quais registradores ler e onde escrever, escolhe a próxima instrução) e lê as flags para decidir os saltos. Ela não calcula nada, é pura coordenação. Em muitas CPUs, esses sinais vêm de uma tabela interna, o microcódigo.

Sobre por que existem registradores e também memória, a razão é velocidade. A ULA é rapidíssima, mas só lê dados que estejam ao lado dela, nos registradores, com acesso em um único ciclo de clock. Acontece que registradores são pouquíssimos e caros. A RAM é enorme e barata, mas muito mais lenta, custando dezenas a centenas de ciclos. A solução é uma hierarquia, que troca velocidade por tamanho.

Hierarquia de memória

O que mora em cada nível ajuda a fixar a ideia. Os registradores guardam o que a CPU toca neste exato instante. Há os de uso geral (os operandos da ULA, o resultado recém-saído, contadores de laço, endereços) e os especiais (o contador de programa, que aponta o endereço da próxima instrução; o registrador de instrução, que guarda a instrução em decodificação; o registrador de flags; o ponteiro de pilha). O cache guarda cópias do que foi usado há pouco na RAM e dos vizinhos, e o hardware decide isso sozinho, explorando dois padrões: a localidade temporal (o que foi usado tende a ser usado de novo) e a localidade espacial (dados próximos tendem a ser usados juntos). Quando o dado pedido está no cache é um acerto (hit), rápido; quando não está é uma falha (miss), e o dado precisa ser buscado na RAM. A RAM guarda o programa inteiro em execução e seus dados (variáveis, pilha de chamadas, heap), além do sistema operacional e dos outros programas abertos, mas é volátil, ou seja, perde tudo quando falta energia. O disco ou SSD é não volátil e guarda arquivos, programas instalados e o sistema em repouso. A CPU não executa nada direto do disco: o programa é primeiro copiado para a RAM e só então roda.

O percurso de um dado fecha o raciocínio. Ele nasce parado no disco, sobe para a RAM quando o programa abre, vai para o cache quando vai ser usado, e, no momento do cálculo, para um registrador, que é o único lugar de onde a ULA lê. As operações load (memória para registrador) e store (registrador para memória), comandadas pela Unidade de Controle, movem os dados entre os níveis. A ULA nunca toca a RAM diretamente.


Camada 4: instruções e programas

A máquina que você montou nas camadas anteriores fica inerte até alguém dizer quais operações executar e em que ordem. Esse idioma de ordens é o conjunto de instruções (a ISA), e um programa é uma lista dessas instruções guardada na memória. Cada instrução carrega o opcode (o que fazer) e os operandos (sobre o quê), exatamente as duas coisas que você vem rastreando desde o início.

Anatomia de uma instrução

O campo do opcode é exatamente o sinal que a Unidade de Controle da Camada 3 lê e repassa para a ULA, e os outros campos apontam os registradores. As instruções se dividem em três famílias, e juntas elas explicam tudo que um programa faz. As aritméticas e lógicas (ADD, SUB, AND, OR, SHL) dão trabalho à ULA, e são o "calcular". As de movimentação (LOAD, STORE, MOV) sobem e descem dados pela hierarquia, e são o "buscar e guardar". As de controle de fluxo (JMP, e os condicionais JZ, JNZ) mudam qual será a próxima instrução, e os condicionais leem as flags. É aqui que nasce de vez aquele "tomar decisões e seguir caminhos diferentes" que apareceu lá na Camada 1.

As linguagens que as pessoas escrevem são camadas de tradução acima do código de máquina. Veja a mesma soma descendo os níveis:

``` Alto nível: soma = a + b

Assembly: LOAD R1, a ; traz 'a' da memória para R1 LOAD R2, b ; traz 'b' da memória para R2 ADD R3, R1, R2 ; R3 = R1 + R2 (aqui a ULA trabalha) STORE soma, R3 ; guarda R3 na memória, em 'soma'

Máquina: 100000 00011 00001 00010 (a instrução ADD acima, em binário) ```

Três observações fecham a camada. Uma linha de alto nível vira várias instruções de máquina, porque a CPU só sabe fazer coisas minúsculas (lembra da comparação com o terminal?). O assembly é só um apelido legível para o binário, com correspondência de um para um. E um compilador traduz C ou Python até esse nível. O programa é literalmente essa lista ordenada de instruções na RAM, puxada uma a uma. A inteligência mora aqui, na ordem, não em nenhuma instrução isolada.


Camada 5: o ciclo e o clock, o ritmo cerebral

As camadas de 1 a 4 são estrutura parada. Sozinhas, são um circuito congelado. A Camada 5 é a dimensão do tempo: um pulso que se repete sem parar e faz a máquina inteira executar uma instrução a cada batida. As três etapas, e o loop que nunca termina, ficam assim:

Ciclo buscar-decodificar-executar

O compasso vem do clock. Um cristal oscilante gera um pulso elétrico regular bilhões de vezes por segundo, e é daí que vêm os GHz: 3 GHz são 3 bilhões de batidas por segundo. Cada batida empurra a CPU um passo adiante no ciclo. Sem o clock, todo o hardware das camadas anteriores fica parado.

O interessante é que o ciclo costura a escada inteira. Buscar usa a memória e o contador de programa da Camada 3 para pegar o programa da Camada 4. Decodificar é a Unidade de Controle da Camada 3 lendo o opcode da Camada 4. Executar põe os registradores e a ULA das Camadas 2 e 3 para trabalhar, e, no fundo, são as portas e os transístores da Camada 1 que chaveiam. Uma única volta atravessa todos os cinco níveis, de cima a baixo e de volta.


Camada 6: pipeline e múltiplos núcleos

Duas jogadas fazem a CPU render muito mais que "uma instrução por vez". Uma ganha em profundidade, a outra em largura.

O pipeline parte de um desperdício. No ciclo simples, enquanto uma instrução está sendo executada, o circuito que faz a busca fica ocioso. O pipeline divide o ciclo em estágios com hardware separado e mantém todos ocupados ao mesmo tempo, com instruções diferentes em estágios diferentes.

Pipeline

A distinção fina é entre latência e vazão. Cada instrução ainda leva 3 batidas para terminar, então a latência não cai. Mas, depois que a linha enche, a CPU completa uma instrução a cada batida, em vez de uma a cada três, e é a vazão que sobe, perto de 3 vezes nesse exemplo. CPUs reais usam pipelines de 14 a 20 estágios.

O preço aparece quando o programa muda de ideia, nos saltos condicionais. A CPU vai enchendo o pipeline supondo que não haverá desvio. Se o salto acontece, as instruções já buscadas estavam erradas e precisam ser descartadas, num processo chamado flush, e a linha recomeça. Para reduzir isso, existe a previsão de desvio: um circuito que tenta adivinhar o caminho do salto com base no histórico e acerta na maioria das vezes; quando erra, paga a penalidade do flush. Foi explorando justamente essa especulação que surgiu a falha de segurança Spectre. Há ainda os conflitos de dados, quando uma instrução precisa do resultado da anterior, que ainda está no pipeline. Eles são resolvidos por adiantamento (entregar o resultado direto de um estágio ao outro, sem esperar que ele volte ao registrador) ou, no pior caso, por uma pausa de um ciclo.

Os múltiplos núcleos resolvem outra coisa. O pipeline acelera um único fluxo de instruções; os vários núcleos rodam vários fluxos genuinamente ao mesmo tempo. Não é revezamento rápido, é simultaneidade real. Um núcleo é uma CPU completa, com pipeline, ULA, registradores e cache L1 próprios, e um chip moderno coloca vários lado a lado.

CPU com vários núcleos

Isso traz duas dificuldades novas, ambas ligadas ao fato de os núcleos compartilharem o cache L3 e a RAM. A primeira é a coerência de cache. Dois núcleos podem ter copiado o mesmo endereço de memória no L1 de cada um. Se o núcleo 1 altera esse valor, o núcleo 2 fica com a versão antiga. Protocolos como o MESI resolvem isso fazendo os caches avisarem uns aos outros: quando um escreve, os demais invalidam a cópia velha. A segunda é a sincronização. Quando dois núcleos mexem no mesmo dado, pode haver corrida, então o software precisa de travas (locks) e operações atômicas para coordenar quem altera e quando. Existe também um limite teórico, a lei de Amdahl: se uma parte do programa é forçosamente sequencial, ela vira o gargalo, e por isso 8 núcleos raramente entregam 8 vezes de desempenho. Vale citar um primo do paralelismo, o SMT ou Hyper-Threading, em que um único núcleo finge ser dois, intercalando duas threads para preencher os intervalos ociosos do pipeline (por exemplo, enquanto uma thread espera um dado lento da RAM, a outra usa a ULA). Em resumo, o pipeline tira o máximo de um núcleo, em profundidade, e os vários núcleos multiplicam isso, em largura. CPUs modernas usam as duas coisas ao mesmo tempo.


Camada 7: arquitetura e projeto de sistema

Aqui você dá o zoom para fora. A CPU deixa de ser o universo e vira uma peça de um sistema completo, conectada por barramentos à memória, ao armazenamento, à GPU e aos dispositivos. Esta camada é menos sobre uma peça nova e mais sobre enxergar o todo e os princípios que se repetem em tudo que veio antes.

Arquitetura do sistema completo

O barramento carrega três coisas ao mesmo tempo: o endereço (onde), o dado (o quê) e o controle (ler ou escrever). Dois truques de sistema evitam que a CPU vire gargalo. As interrupções permitem que um dispositivo chame a atenção da CPU quando precisa, em vez de a CPU ficar perguntando o tempo todo. E o DMA permite que o disco ou a rede movam dados direto para a RAM sem passar pela CPU, que fica livre para calcular. Na prática, a memória costuma estar num caminho mais curto e rápido, enquanto o resto vai por barramentos como o PCIe.

A ideia central desta camada é a ISA como contrato. Existe uma fronteira entre dois mundos. De um lado está a arquitetura, ou seja, a ISA: o que o software enxerga, com o conjunto de instruções e os registradores visíveis. Isso é um contrato. Do outro lado está a microarquitetura: como aquilo é construído por dentro, com a profundidade do pipeline, o tamanho dos caches, a previsão de desvio e o número de núcleos. Isso é a implementação. A genialidade está na separação. Como o contrato não muda, um programa compilado hoje roda num chip de daqui a 10 anos, construído de um jeito completamente diferente. As duas filosofias clássicas refletem esse jogo. O RISC tem instruções poucas, simples e uniformes, fáceis de colocar em pipeline, e é o caso do ARM do seu celular. O CISC tem instruções complexas e variadas, e é o caso do x86 do PC. O detalhe revelador é que os x86 modernos traduzem, por dentro, suas instruções complexas em micro-operações no estilo RISC. O contrato continua CISC, mas a implementação virou RISC.

Projetar sistemas também é casar cada tarefa com o motor certo, porque existe um trade-off duro entre generalidade e eficiência. A CPU é a generalista, ótima em tarefas sequenciais e cheias de decisões. A GPU tem milhares de núcleos simples para o mesmo cálculo repetido em montanhas de dados em paralelo, o que serve para gráficos e IA. As NPUs e TPUs são especializadas em redes neurais, e os ASICs fazem uma única função com eficiência máxima. Quanto mais específico o circuito, mais eficiente e menos flexível ele é. A computação moderna é heterogênea: junta todos esses motores e manda cada tarefa para quem a faz melhor.

A verdadeira maestria é perceber que os mesmos poucos princípios governam cada degrau da escada. O primeiro é a abstração em camadas: cada nível esconde a complexidade do de baixo e oferece uma interface limpa para o de cima. O segundo é a localidade: programas reusam as mesmas coisas e as vizinhas, e por isso a hierarquia de memória funciona. O terceiro é o paralelismo em toda escala: nos bits (a largura da ULA), nas instruções (o pipeline), nas threads (os núcleos) e nos dados (a GPU). O quarto são os trade-offs constantes: velocidade contra energia, contra custo, contra área de silício e contra generalidade. Não existe almoço grátis, e a própria hierarquia de memória existe porque a CPU ficou rápida demais para a RAM acompanhar, o chamado muro da memória.


Síntese: a torre inteira

Vista de cima, a escada conta uma única história, da matéria ao sistema vivo.

  1. Transístores e portas: matéria virando lógica.
  2. ULA e blocos: lógica virando aritmética.
  3. Microarquitetura: aritmética virando uma máquina organizada.
  4. Instruções e programas: a máquina ganhando idioma e propósito.
  5. Ciclo e clock: o tempo dando vida a tudo, batida por batida.
  6. Pipeline e múltiplos núcleos: fazendo essa vida render, em profundidade e em largura.
  7. Arquitetura e sistema: a peça encaixada no todo, regida por contratos e trade-offs.

O insight que amarra os sete é que um computador é uma torre de contratos. Cada camada faz o mesmo movimento: pega as capacidades confusas da camada de baixo, embrulha numa interface confiável e some com os detalhes, para a camada de cima poder esquecê-los e construir de novo. O transístor não sabe o que é uma porta, a porta não sabe o que é um número, e a ULA não sabe o que é um arquivo. Ninguém ali embaixo faz ideia de que, no topo, tudo isso virou o vídeo que você assiste ou o texto que você lê.

Voltando à pergunta do começo, sobre se a CPU é um executor que recebe instruções pontuais, a resposta agora fica clara, e você pode ver o porquê. A inteligência nunca esteve em nenhuma peça isolada. Ela emerge de bilhões de passos simples e obedientes por segundo, na ordem certa, marcados por um relógio que nunca para. Um executor simples, rápido o bastante para parecer mágica.


Conteúdos que me ajudaram a estudar

Esses são conteúdos que eu usei como base para o estudo e para o documento. Coloquei aqui pois são vídeos magníficos e de alto valor, onde não tem como eu não deixar a recomendação e a referência.