Pular para o conteúdo

Arquitetura de Computadores

Ciência da Computação Universitária · Tópico 6

Ver Slides Treinar

Material de apoio

O que este curso é e como usá-lo

Este é o sexto curso de Ciência da Computação Universitária: arquitetura de computadores** — a máquina abaixo do programa: como um conjunto de instruções é especificado, como uma unidade de dado a executa, como um pipeline sobrepõe instruções e como memória e E/S mantêm o núcleo alimentado.

Limite de escopo, declarado uma vez: curso de estudo, não diploma; a máquina didática é um design original de carga e armazenamento de 16 registradores (R0 hardwired para zero), e cada afirmação de temporização neste material é calculada a partir dos parâmetros declarados e re-derivada pela suíte do curso.

  • Unidades 6.1–6.2 são o contrato e a máquina que o honra.
  • As unidades 6.3–6.4 tornam-no rápido: pipeline e hierarquia de memória.
  • Unidades 6.5–6.6 conectam: entrada/saída e aceleração paralela.

O hábito do arquiteto: desempenho é aritmética, não adjetivos — todo ganho de velocidade neste curso é um número que pode ser recomputado.

Arquitetura de conjunto de instruções

Por que importa. A arquitetura de conjunto de instruções (ISA) é o contrato entre compilador e hardware: tudo o que o software pode depender, nada sobre como foi construído. Duas máquinas com uma ISA executam os mesmos binários.

A ideia, com uma imagem. Nossa ISA didática é RISC no espírito: instruções fixas de 32 bits, arquitetura load-store (apenas lw/sw acessam memória), 16 registradores com R0 sempre zero. Dois formatos cobrem o curso:

  • Tipo-R R: opcode(4) | rd(4) | rs(4) | rt(4) | shift(4) | funct(8) — ex.: add rd, rs, rt.
  • Tipo-I I: opcode(4) | rt(4) | rs(4) | pad(4) | immediate(16) — ex.: lw rt, imm(rs); o campo de preenchimento não é usado, mantendo a palavra com 32 bits.
Codificações de instrução
Dois formatos, mesma largura: Tipo-R carrega três registradores e um código de função; Tipo-I troca o terceiro registrador por um imediato de 16 bits

A derivação (o custo do imediato). Um imediato de 16 bits complemento a dois alcança de $-2^{15}$ a $2^{15} - 1$, ou seja, de $-32\,768$ a $32\,767$ — logo addi R2, R0, 40000 não pode codificar e o ensamblador exige duas instruções (metade alta estilo lui, seguida adição). Endereçamento de palavra: com memória endereçável por byte e palavras de 32 bits, offsets de lw são múltiplos de $4$ — um offset de 16 bits atinge $2^{16}$ bytes = $64$ KiB a partir do registro base.

Exemplo resolvido 1 — codificar e decodificar. Campos: opcode add = 1, addi = 2, lw = 3, sw = 4, beq = 5. Codifique add R3, R1, R2: opcode $0001$, rd $0011$, rs $0001$, rt $0010$, deslocamento $0000$, funct $00000001$ — seis campos, 28 de 32 bits usados, os 4 bits superiores da palavra final com preenchimento em zero. Decodifique 0010 0100 0001 0000 000001111111 (opcode 2, rt = 4, rs = 1, padding = 0, imediato $01111111_2 = 127$): addi R4, R1, 127.

Exemplo resolvido 2 — o que R0 compra. addi R0, R0, 0 é o no-op canônico; add R5, R0, R0 zera R5 em um ciclo; comparar com zero (beq R1, R0, loop) não precisa de operando constante. Um registro zero dedicado remove uma classe inteira de instruções de movimento imediato — CISC mantém MOV reg, 0; RISC mantém um fio.

Erros comuns. Ler um imediato de 16-bits como sem sinal (ramos e aritmética de offset são com sinal). Esperar que add acesse memória (carga/armazenamento: ele não pode). Assumir que o opcode sozinho seleciona a operação (R-type também exige o campo funct).

Resumo. ISA = o contrato estável; formatos fixos de largura para R/I; imediatos assinados de 16-bits atingem $\pm 32$ K e 64 KiB de memória; R0 é um fio, não um registro que você escreve.

Caminho de dados e controle

Por que importa. A ISA são promessas; o caminho de dados é a tubulação que as mantém — e controle é a chave de configurações por instrução.

A ideia, com uma imagem. A máquina de ciclo único: o PC alimenta a memória de instruções (IM); arquivo de registradores (RF) lê duas fontes registradores; a ALU calcula; memória de dados (DM) lê ou grava; o resultado escreve de volta; PC + 4 ou um alvo de desvio carrega o PC. O controle define: RegWrite, MemRead, MemWrite, MemToReg, ALUSrc, Branch.

Caminho de dados de ciclo único
Um clock, uma instrução: busca no PC, leitura de registradores, cálculo na ALU, acesso à memória de dados se solicitado, escrita de volta e escolha do próximo PC

A derivação (tempo de ciclo = caminho mais lento). Todo o trabalho ocorre dentro de um clock, então o clock deve esperar pela instrução mais longa: leitura de memória + leitura de registrador + ALU + memória novamente + escrita de volta — caminho do lw. Com nossos parâmetros (IM 200 ps, leitura do RF 100 ps, ALU 200 ps, DM 250 ps, fiação total de $150$ ps; escritas de registrador sobrepostas ao bordo final do ciclo): o caminho do lw $= 200 + 100 + 200 + 250 + 150 = 900$ ps, logo o ciclo é $900$ ps e um programa de 100 instruções leva $90\,000$ ps. Acelerar add não ajudaria — a máquina opera na velocidade do seu componente mais lento.

Exemplo resolvido 1 — sinais para uma carga. lw R4, 8(R1): RegWrite = 1 (escrever R4), MemRead = 1, MemToReg = 1 (memória de dados para registrador), ALUSrc = 1 (imediato para a soma de endereços), Branch = 0, MemWrite = 0. Seis bits contam toda a história — o mesmo caminho de dados executa cada instrução; apenas as configurações diferem.

Exemplo resolvido 2 — a exceção do store. sw R5, 12(R1): a porta de escrita vai para a memória, não para o arquivo de registradores: RegWrite = 0, MemWrite = 1, MemRead = 0, MemToReg = x (indiferente — nada escreve de volta). Configurações indiferentes são liberdade do projetista: qualquer que seja o bit, a instrução se comporta idênticamente.

Erros comuns. Esquecer PC + 4 acontece até em branches (o alvo é calculado a partir de PC + 4). Misturar polaridade de MemToReg entre desenhos. Acreditar que o controle é sequencial — é pura lógica combinacional sobre o opcode e funct.

Resumo. Um único caminho de dados compartilhado, bits de controle por instrução; o tempo do ciclo é o caminho da instrução mais lenta; lw geralmente o define.

Pipeline

Por que importa. A máquina de ciclo único fica ociosa na maior parte de suas partes na maior parte do tempo. Pipelining sobrepõe instruções como uma linha de montagem — mais instruções concluídas por segundo, com custo de hardware quase o mesmo.

A ideia, com uma imagem. Cinco estágios: IF busca, ID decodificação/leitura de registrador, EX execução, MEM memória, WB escrita back. A instrução $i+1$ é buscada enquanto $i$ é decodificada: após o enchimento do pipeline, uma instrução é concluída por ciclo.

Linha temporal do pipeline
Cinco instruções passam por cinco estágios: 9 ciclos no total, não 25 — o pipeline enche e drena apenas uma vez

A derivação (fórmula de ganho de velocidade). $n$ instruções, $k$ estágios, tempo por estágio $t$; tempo sem pipeline $= n k t$; com pipeline $= (n + k - 1) t$ (fill $k - 1$, depois um por ciclo). Cinco instruções, cinco estágios: $25t$ torna-se $9t$; em $n = 100$: $500t$ torna-se $104t$ — o ganho de velocidade aproxima-se de $k$ à medida que $n$ aumenta, e o CPI ideal é $1$. Nosso tempo por estágio é definido pelo estágio mais lento: com IF 250, ID 150, EX 200, MEM 250, WB 100 ps, cada estágio custa $250$ ps — o pipeline opera a $250$ ps por instrução contra o único ciclo de $900$ ps.

Exemplo resolvido 1 — dependência RAW.

add R1, R2, R3     # R1 written in WB (cycle 6)
sub R4, R1, R1     # needs R1 in ID (cycle 5) — too early

O subtrai lê o arquivo de registradores um ciclo antes do adição escrevê-lo — uma dependência read-after-write. O forwarding roteia a saída da ALU do adição diretamente para as entradas da ALU do subtrai, ocultando o espera completamente — exceto quando o produtor é um load: lw R1, …; add R3, R1, R2 ainda precisa fazer um ciclo de espera, pois os dados existe apenas após MEM.

Exemplo resolvido 2 — CPI com estall. Um programa executa $1000$ instruções em um pipeline de 5 estágios com forwarding; $25\%$ são loads e metade dessas é seguida imediatamente por uso (par load-use), cada um custando um estall: estalls $= 1000 \times 0.25 \times 0.5 = 125$. Ciclos totais $= 1000 + 125 + 4 = 1129$; CPI $= 1129 / 1000 = 1.129$ — o custo de enchimento ($4$ ciclos) diminui conforme $n$ cresce.

Erros comuns. Somar contagens de estágios em vez de usar $n + k - 1$. Acreditar que o forwarding corrige load-use (um bubble permanece). Esquecer que branches alteram PC tarde — um branch tomado já buscou a instrução errada seguinte (nosso design: 1 bubble, resolvido em EX).

Resumo. $n + k - 1$ ciclos para $n$ instruções; CPI ideal 1; o forwarding elimina hazards ALU, load-use custa um estall, branch tomado custa um bubble; o speedup tende ao número de estágios.

Hierarquia de memória

Por que importa. O núcleo quer bytes em nanosegundos; memória barata responde em centenas. A hierarquia de memória empilha pequeno-rápido sobre grande-lento e permite que a localidade pague as contas.

A ideia, com uma figura. Um cache mapeado diretamente coloca cada bloco de memória em exatamente um slot: um endereço de 32 bits divide em etiqueta | índice | deslocamento. Nosso curso cache: $4$ KiB, linhas de $16$ bytes, $\Rightarrow 256$ linhas, $\Rightarrow$ deslocamento de $4$ bits, índice de $8$ bits, etiqueta de $32 - 12 = 20$ bits.

Decomposição do endereço e hierarquia
O endereço de 32 bits divide-se pela geometria do cache; abaixo, a hierarquia em cada nível alimenta

A dedução (AMAT). Tempo médio de acesso à memória $= t_{\text{hit}} + p_{\text{miss}} \times t_{\text{penalty}}$. Com acerto L1 em $1$ ciclo, taxa de erro $2\%$, penalidade de $100$ ciclos: AMAT de $= 1 + 0.02 \times 100 = 3$ ciclos — um atraso de $3\times$ em relação a uma taxa de erro de $2\%$, razão pela qual a hierarquia vive ou morre na localidade. Reduza a taxa de erro para $1\%$ e AMAT ⬇️ $= 2$: a sensibilidade é linear nos erros, não nos acertos.

Exemplo resolvido 1 — para onde vai um endereço? Endereço $0x000004A8$: os 12 bits mais baixos são $0100\,1010\,1000$ — deslocamento $1000_2 = 8$ (byte 8 da sua linha), índice $0100\,1010_2 = 74$ (linha 74 de 256), etiqueta = os 20 bits mais altos. Uma linha candidata por endereço: qualquer outro bloco mapeado para a linha 74 expulsa-o — a velocidade de mapeamento direto tem um comparador, seu preço são as falhas por conflito.

Exemplo resolvido 2 — localidade espacial e temporal. Varredura em escaneamento de um array de integers de $64 \times 64$ com linhas de 16 bytes ($4$ ints por linha): o primeiro acesso falha, os três seguintes têm sucesso — taxa de falhas $1/4 = 25\%$ mais apenas erros compulsórios; acesso com stride-16 (um int por linha, por coluna em uma matriz larga) falha em cada acesso — $100\%$. Mesmo array, mesmo cache, quatro vezes a AMAT: o layout é um recurso de desempenho.

Erros comuns. Calcular a largura do índice a partir do tamanho do cache sem dividir pelo tamanho da linha (conta linhas, não bytes). Esperar que um cache maior corrija um passo ruim (a largura de banda ainda transfere uma linha de cada vez). Confundir taxa de falhas com razão de falhas (dependendo se a formulação é por acesso ou por instrução, variando conforme o número de acessos).

Resumo. Dividir o endereço pela geometria (tag | índice | deslocamento); AMAT $= 1 + m \times p$; mapeamento direto troca conflitos por velocidade; localidade — reutilização e adjacência — é a moeda.

Entrada/saída

Por que importa. Uma máquina que não pode comunicar é apenas um aquecedor. E/S é onde microssegundos encontram milissegundos, e os três estilos de transferência trocam atenção da CPU por vazão. A ideia, com uma imagem. E/S programada I/O

(polling): o núcleo verifica continuamente um registro de status. **Impulsionado por interrupções (polling): o núcleo gira em um registro de status. **Interrupções E/S: o dispositivo sinaliza, o núcleo salva estado e executa um manipulador. DMA: um motor DMA transfere o bloco e interrompe apenas uma vez no final.

Três caminhos de E/S
Polling consome o núcleo; interrupções cobram por evento; DMA cobra uma vez por bloco

A dedução (o custo do polling). Um flag de dispositivo atualiza a cada $2$ µs; uma leitura custa $50$ ciclos a $250$ ps por ciclo $= 12.5$ ns. Latência de pior caso exige polling contínuo: $2\,\mathrm{µs} / 12.5\,\mathrm{ns} = 160$ leituras por evento, todas exceto uma desperdiçadas — o núcleo gasta $160 \times 50 = 8\,000$ ciclos por palavra de atenção. Interrupções reduzem isso ao custo do manipulador: $200$ ciclos de salvamento, leitura, restauração por palavra. O DMA move $1024$ palavras com uma configuração de $500$-ciclo mais uma interrupção de conclusão de $200$-ciclo: $0.68$ ciclos por palavra contra $200$ — três ordens de magnitude de atenção da CPU economizada.

Exemplo resolvido 1 — escolha o estilo. Um teclado (um humano digita $\approx 5$ caracteres por segundo): o polling nessa taxa desperdiça o núcleo inteiro; interrupções encaixam — um evento por tecla pressionada, $200$ ciclos cada, desprezível. Um bloco de disco de $4$ MiB a $2$ GB/s: mesmo interrupção por palavra sobrecarrega o núcleo; o DMA assume o controle. Um sensor amostrado exatamente a cada $2$ µs: o polling em loop apertado é honesto e simples — a taxa de amostragem é a taxa de consulta.

Exemplo resolvido 2 — o custo das interrupções. Cada interrupção custa $200$ ciclos de sobrecarga (salvamento de estado, vetor, restauração). Receber $1024$ palavras com uma interrupção por palavra: $204\,800$ ciclos de pura sobrecarga; o mesmo bloco via DMA: $700$ ciclos no total — os dados são movidos de forma idêntica, mas o custo da CPU difere em $292\times$. O loteamento não é uma otimização; é a diferença entre arquiteturas.

Erros comuns. Consultar dentro de um manipulador de interrupção (invertendo os dois estilos). Esquecer que o DMA ainda exige cuidado com coerência de cache (o motor DMA e o núcleo veem a memória). Confundir latência de interrupção (tempo para iniciar o manipulador) com throughput (palavras por segundo).

Resumo. Consulta: simples, hostil ao núcleo. Intercupções: custeadas por evento, sobrecarga do manipulador por palavra. DMA: custeado por bloco, uma interrupção por bloco — e a atenção da CPU por palavra cai em ordens de grandeza.

Paralelismo e desempenho

Por que importa. Os clocks pararam de ficar mais rápidos; a máquina tornou-se mais larga e com mais núcleos em vez disso. Ler sobre ganho de velocidade agora significa ler Amdahl.

A ideia, com uma imagem. Um programa tem uma fração serial e um fração paralelizável $f$. Em $p$ processadores: $\text{Speedup} = \dfrac{1}{(1 - f) + f / p}$ — a parte serial nunca diminui.

Curvas de aceleração de Amdahl
Aceleração em relação à quantidade de processadores para frações paralelas 0.5, 0.9 e 0.99: a fração serial limita todas as curvas

A dedução (o teto). À medida que $p \to \infty$, aceleração $\to 1 / (1 - f)$ — the serial fraction alone. With $f = 0.9$: ilimitado hardware compra no máximo $10\times$; em $p = 8$: $1 / (0.1 + 0.1125) = 4.71×$; at $p = 16$: $1 / (0.1 + 0.0563) = 6.40×$ — o segundo conjunto de oito processadores adiciona $1.7\times$ onde os primeiros oito adicionaram $4.7\times$. Com $f = 0.99$, o teto é $100\times$, mas alcançar $50\times$ exige $p = 99$: exatamente $1/(0.01 + 0.99/99) = 50.0$. Rendimentos decrescentes são estruturais, não incidentais.

Exemplo resolvido 1 — avalie um ganho de velocidade. Uma renderização gasta $36$ s compositando (serial) e $64$ s sombreado (paralelo) em um núcleo: $f = 64/100 = 0.64$. On $p = 4$: speedup $= 1/(0.36 + 0.16) = 1.92\times$; tempo de execução $= 100 \times 0.52 = 52.0$ s. Dobrar os núcleos para $8$: $1/(0.36 + 0.08) = 2.27\times$, $44.0$ s — a cauda de composição agora domina. A próxima aquisição é um algoritmo, não uma máquina.

Exemplo resolvido 2 — paralelismo ao nível de instrução. Antes dos núcleos, a própria pipeline é paralelismo: a máquina de 5 estágios sobrepõe cinco instruções (ILP). Adicionar threads de hardware (SMT) permite que um núcleo preencha buracos de pipeline com outra thread de instruções — a matemática de perigo de 6.3 ainda se aplica por thread. O pilha: ILP dentro de um núcleo, threads por núcleo, núcleos por chip — três níveis, uma disciplina de Amdahl.

Erros comuns. Reportar ganho de velocidade máximo em vez de medido (ganho nominal $p$ assume $f = 1$). Tratar uma fração serial de $10\%$ como inofensiva — ela limita toda máquina que você comprará a $10\times$. Comparar ganhos entre diferentes bases (o tempo de um único núcleo deve ser o mesmo programa, mesma máquina, um núcleo ativo).

Resumo. Ganho de velocidade $= 1/((1-f) + f/p)$, teto $1/(1-f)$; fração serial domina em escala; ILP, threads e núcleos são uma escada — e a escada apoia-se em Amdahl.

Vocabulário
English Português
computer architecture arquitetura de computadores
instruction set architecture arquitetura de conjunto de instruções
RISC/rɪsk/ RISC
load-store arquitetura de carga e armazenamento
byte-addressable endereçável por byte
datapath caminho de dados
control/kənˈtrəʊl/ controle
ALU/ˌeɪ el ˈjuː/ unidade lógica aritmética
pipelining/ˈpaɪplaɪnɪŋ/ pipelining
retires finalização
CPI ciclos por instrução
read-after-write hazard dependência read-after-write
forwarding forwarding
memory hierarchy hierarquia de memória
locality localidade
direct-mapped cache cache mapeamento direto
tag/tæɡ/ marcador
index/ˈɪndeks/ índice
block offset deslocamento do bloco
average memory access time tempo médio de acesso à memória
evicts expulsão
programmed I/O E/S programada
interrupt-driven I/O E/S acionada por interrupção
DMA acesso direto à memória
instruction-level parallelism paralelismo a nível de instrução
SMT SMT
R-type R-type
I-type I-type

Aulas interativas sobre este tópico

Passe por ele passo a passo, com exercícios de verificação instantânea.

Mais tópicos em Ciência da Computação Universitária

Entrar ou criar conta

IGCSE, A-Level & AP