4ª. E D I Ç Ã O
ORGANIZAÇÃO E PROJETO DE COMPUTADORES
4ª. E D I Ç Ã O
ORGANIZAÇÃO E PROJETO DE COMPUTADORES I N T E R F A C E
David A. Patterson John L. Hennessy
H A R D W A R E / S O F T W A R E
© 2014, Elsevier Editora Ltda. Todos os direitos reservados e protegidos pela Lei n° 9.610, de 19/02/1998. Nenhuma parte deste livro, sem autorização prévia por escrito da editora, poderá ser reproduzida ou transmitida sejam quais forem os meios empregados: eletrônicos, mecânicos, fotográficos, gravação ou quaisquer outros. Copyright © 2009 by Elsevier Inc. All rights reserved Morgan Kaufmann Publishers is an imprint of Elsevier. Copidesque: Adriana Kramer Revisão: Carla de Cássia Camargo e Bruna Baldini Editoração Eletrônica: Thomson Digital Elsevier Editora Ltda. Conhecimento sem Fronteiras Rua Sete de Setembro, 111 – 16° andar 20050-006 – Centro – Rio de Janeiro – RJ – Brasil Rua Quintana, 753 – 8° andar 04569-011 – Brooklin – São Paulo – SP Serviço de Atendimento ao Cliente 0800-0265340
[email protected] ISBN original 978-0-12-374493-7 ISBN 978-85-352-3585-2 ISBN digital 978-85-352-6410-4 Nota: Muito zelo e técnica foram empregados na edição desta obra. No entanto, podem ocorrer erros de digitação, impressão ou dúvida conceitual. Em qualquer das hipóteses, solicitamos a comunicação ao nosso Serviço de Atendimento ao Cliente, para que possamos esclarecer ou encaminhar a questão. Nem a editora nem o autor assumem qualquer responsabilidade por eventuais danos ou perdas a pessoas ou bens, originados do uso desta publicação.
CIP-BRASIL. CATALOGAÇÃO NA PUBLICAÇÃO SINDICATO NACIONAL DOS EDITORES DE LIVROS, RJ P344o 4. ed. Patterson, David A. Organização e projeto de computadores : interface hardware/software / David A. Patterson, John L. Hennessy ; [tradução Daniel Vieira]. - 4. ed. - Rio de Janeiro : Elsevier, 2014. 28 cm. Tradução de: Computer organization and design: the hardware/software interface ISBN 978-85-352-3585-2 1. Organização de computador. 2. Engenharia de computador. 3. Interfaces (Computadores). I. Hennessy, John L. II. Título. 13-05059
CDD: 004.22 CDU: 004.2
Para Linda, que foi, é e sempre será o amor da minha vida.
Prefácio O que podemos experimentar de mais belo é o mistério. Ele é a fonte de toda arte e ciência verdadeira. Albert Einstein, Como vejo o mundo, 1930
Este livro Acreditamos que o aprendizado na Ciência da Computação e na Engenharia deve refletir o estado atual da área, além de apresentar os princípios que estão moldando a computação. Também achamos que os leitores em cada especialidade da computação precisam apreciar os paradigmas organizacionais que determinam as capacidades, o desempenho e, por fim, o sucesso dos sistemas computacionais. A tecnologia computacional moderna exige que os profissionais de cada especialidade da computação entendam tanto o hardware quanto o software. A interação entre hardware e software em diversos níveis também oferece uma estrutura para se entender os fundamentos da computação. Não importa se seu interesse principal é hardware ou software, Ciência da Computação ou Engenharia Elétrica, as ideias centrais na organização e projeto de computadores são as mesmas. Assim, nossa ênfase neste livro é mostrar o relacionamento entre hardware e software e apresentar os conceitos que são a base para os computadores atuais. A passagem recente de processador para microprocessadores multicore confirmou a solidez desse ponto de vista, dado desde a primeira edição. Embora os programadores pudessem ignorar e confiar em arquitetos de computador, escritores de compilador e engenheiros de silício para fazer os seus programas executarem mais rápido, sem mudanças, essa era já terminou. Para os programas executarem mais rápido, eles precisam se tornar paralelos. Embora o objetivo de muitos pesquisadores seja possibilitar que os programadores não precisem saber a natureza paralela subjacente do hardware que eles estão programando, serão necessários muitos anos para se concretizar essa visão. Nossa visão é que, pelo menos na próxima década, a maioria dos programadores terá de entender a interface hardware/software se quiser que os programas executem de modo eficiente em computadores paralelos. Este livro é útil para aqueles com pouca experiência em linguagem assembly ou projeto lógico, que precisam entender a organização básica do computador, e também para leitores com base em linguagem assembly e/ou projeto lógico, que queiram aprender a projetar um computador ou entender como um sistema funciona e por que se comporta de determinada forma.
O outro livro Alguns leitores podem estar familiarizados com Arquitetura de Computadores: Uma abordagem quantitativa, conhecido popularmente como Hennessy e Patterson. (Este livro, por sua vez, é chamado Patterson e Hennessy.) Nossa motivação ao escrever aquele livro foi descrever os princípios da arquitetura de computadores usando fundamentos sólidos de engenharia e a relação custo/benefício. Usamos um enfoque que combinava exemplos e
xii Prefácio
medições, baseado em sistemas comerciais, para criar experiências de projeto realísticas. Nosso objetivo foi demonstrar que arquitetura de computadores poderia ser aprendida por meio de metodologias quantitativas, em vez de por uma técnica descritiva. O livro era voltado para profissionais de computação sérios, que desejam um conhecimento detalhado dos computadores. A maioria dos leitores deste livro não planeja se tornar arquiteto de computador. Contudo, o desempenho dos sistemas de software futuros será drasticamente afetado pela forma como os projetistas de software entendem as técnicas de hardware básicas em funcionamento em um sistema. Assim, aqueles que escrevem compiladores, projetistas de sistema operacional, programadores de banco de dados e a maioria dos outros engenheiros de software precisam de um fundamento sólido sobre os princípios apresentados neste livro. De modo semelhante, os projetistas de hardware precisam entender claramente os efeitos de seu trabalho sobre as aplicações de software. Assim, sabíamos que este livro tinha de ser muito mais do que um subconjunto do material contido em Arquitetura de Computadores, e o material foi bastante revisado para corresponder a esse público-alvo diferente. Ficamos tão satisfeitos com o resultado que as edições seguintes do Arquitetura de Computadores foram revisadas para remover a maior parte do material introdutório; logo, há muito menos repetição hoje do que nas primeiras edições dos dois livros.
Mudanças para a quarta edição Tivemos cinco objetivos principais para esta terceira edição de Organização e Projeto de Computadores: dada a revolução multicore nos microprocessadores, destacar os tópicos de hardware e software paralelo no decorrer do livro; dinamizar o conteúdo existente de modo a dar espaço para tópicos sobre paralelismo; melhorar a pedagogia em geral; atualizar o conteúdo técnico para refletir mudanças ocorridas na área desde a publicação da terceira edição em 2004; e restaurar a utilidade dos exercícios nesta era da Internet. Antes de discutirmos os objetivos com detalhes, vejamos a tabela a seguir. Ela mostra as sequências de hardware e software no decorrer do livro. Os Capítulos 1, 4, 5 e 7 são encontrados nas duas sequências, não importando a experiência ou o foco. O Capítulo 1 é uma nova introdução, que inclui uma discussão sobre a importância da potência e como ela motiva a mudança de microprocessadores de “core” único para “multicore”. Também apresenta conteúdo sobre desempenho e benchmarking, que tinham um capítulo separado na terceira edição. O Capítulo 2 provavelmente será material de revisão para os que são voltados para o hardware, mas é uma leitura essencial para aqueles voltados para o software, especialmente para os leitores interessados em aprender mais sobre os compiladores e as linguagens de programação orientadas a objeto. Ele inclui material do Capítulo 3 da terceira edição, de modo que a arquitetura MIPS completa agora está em um único capítulo, menos as instruções de ponto flutuante. O Capítulo 3 é para os leitores interessados em construir um caminho de dados ou em aprender mais sobre aritmética de ponto flutuante. Alguns pularão o Capítulo 3, ou porque não precisam dele ou porque é uma revisão. O Capítulo 4 combina dois capítulos da terceira edição para explicar os processadores em pipeline. As Seções 4.1, 4.5 e 4.10 oferecem resumos para os que são voltados a software. Porém, aqueles mais interessados em hardware descobrirão que esse capítulo apresenta um material básico; eles também podem, dependendo de sua base, querer ler primeiro o Apêndice C, sobre projeto lógico. O Capítulo 6, que trata de armazenamento, é essencial para os leitores com foco no software, e deve ser lido pelos outros, se houver tempo. O último capítulo sobre multicores, multiprocessadores e clusters é um material basicamente novo, e deve ser lido por todos.
Prefácio xiii
Capítulo ou apêndice
Seções
1. Abstrações e Tecnologias Computacionais
1.1 a 1.9
2. Instruções: A Linguagem de Máquina
2.1 a 2.14
Foco no software
Foco no hardware
1.10 (História)
2.15 (Compiladores & Java) 2.16 a 2.19 2.20 (História)
E. Arquiteturas do conjunto de instruções RISC 3. Aritmética Computacional
E.1 a E.19 3.1 a 3.9 3.10 (História)
C. Fundamentos do Projeto Lógico 4. O Processador
C.1 a C.13 4.1 (Visão Geral) 4.2 (Convenções Lógicas) 4.3 a 4.4 (Implementação Simples) 4.5 (Visão Geral do Pipelining) 4.6 (Caminho de Dados em Pipeline) 4.7 a 4.9 (Hazards, Exceções) 4.10 a 4.11 (Paralelo, Vida Real) 4.12 (Controle de Pipeline Verilog) 4.13 a 4.14 (Falácias) 4.15 (História)
D. Mapeando o Controle no Hardware 5. Grande e Rápida: Explorando a Hierarquia de Memória
D.1 a D.6 5.1 a 5.8 5.9 (Controlador de Cache Verilog) 5.10 a 5.12 5.13 (História)
6. Armazenamento e outros tópicos de E/S
6.1 a 6.10 6.11 (Redes) 6.12 a 6.13 6.14 (História)
7. Multicores, 7.1 a 7.13 multiprocessadores e clusters 7.14 (História) A. Unidades de Processamento Gráfico
A.1 a A.12
B. Montadores, Link-editores e o Simulador SPIM
B.1 a B.12
Leia cuidadosamente
Leia se tiver tempo
Revise ou leia
Leia para ter cultura
Referência
xiv Prefácio
O primeiro objetivo foi tornar o paralelismo um cidadão de primeira classe nesta edição, pois era um capítulo separado no material complementar da edição anterior. O exemplo mais óbvio é o Capítulo 7. Em particular, esse capítulo apresenta o modelo de desempenho Roofline, e mostra seu valor, avaliando quatro arquiteturas multicore recentes em dois kernels. Esse modelo poderia demonstrar ser tão compreensível para multiprocessadores multicore quanto o modelo 3Cs é para os caches. Dada a importância do paralelismo, não seria sensato esperar até o último capítulo para falar a respeito, de modo que existe uma seção sobre paralelismo em cada um dos seis capítulos anteriores: j
[B] Capítulo 1: Paralelismo e Potência. Mostra como os limites de potência forçaram a indústria a passar para o paralelismo, e por que o paralelismo ajuda nesse aspecto.
j
[B] Capítulo 2: Paralelismo e Instruções: Sincronização. Esta seção discute os bloqueios para variáveis compartilhadas, especificamente as instruções Load Linked e Store Conditional do MIPS.
j
[B] Capítulo 3: Paralelismo e Aritmética de Computador: Associatividade de Ponto Flutuante. Essa seção discute os desafios da precisão numérica e cálculos de ponto flutuante.
j
[B] Capítulo 4: Paralelismo e Paralelismo Avançado em Nível de Instrução. Aborda ILP avançado — superescalar, especulação, VLIW, desdobramento de loop e OOO — além do relacionamento entre profundidade de pipeline e consumo de potência.
j
[B] Capítulo 5: Paralelismo e Hierarquias de Memória: Coerência de Cache. Apresenta coerência, consistência e snooping dos protocolos de cache.
j
[B] Capítulo 6: Paralelismo e E/S: Redundant Arrays of Inexpensive Disks. Descreve RAID como um sistema de E/S paralela, além do sistema ICO altamente disponível.
O Capítulo 7 conclui com os motivos para o otimismo pelo qual a incursão no paralelismo deverá ter mais sucesso do que aquelas do passado. Estou particularmente entusiasmado com o acréscimo de um apêndice sobre Unidades de Processamento Gráfico, escrito pelo cientista chefe da NVIDIA, David Kirk, e arquiteto chefe John Nickolls. O Apêndice A é a primeira descrição em profundidade das GPUs, que representam uma investida nova e interessante em arquitetura de computador. O apêndice se baseia em temas paralelos dessa edição para apresentar um estilo de computação que permite que o programador pense em MIMD embora o hardware tente executar no estilo SIMD sempre que for possível. Como as GPUs são baratas e encontradas com facilidade — até mesmo em muitos laptops — e seus ambientes de programação estão disponíveis gratuitamente, elas oferecem uma plataforma de hardware paralela que muitos poderiam experimentar. O segundo objetivo foi enxugar o livro para criar espaço para o material novo sobre paralelismo. O primeiro passo foi simplesmente passar um pente fino em todos os parágrafos acumulados nas três edições anteriores, para ver se ainda eram necessários. As principais mudanças foram a junção de capítulos e o descarte de tópicos. Mark Hill sugeriu a remoção da implementação de processador multiciclo e, em vez disso, acrescentar um controlador de cache multiciclo ao capítulo sobre hierarquia de memória. Isso permitiu que o processador fosse apresentado em um único capítulo, em vez de dois, melhorando o material sobre processador pela omissão. O material sobre desempenho, de um capítulo separado na terceira edição, agora está junto com o primeiro capítulo. O terceiro objetivo foi melhorar a pedagogia do livro. O Capítulo 1 agora está mais recheado, incluindo desempenho, circuitos integrados e potência, e prepara o palco para o
Prefácio xv
restante do livro. Os Capítulos 2 e 3 foram escritos originalmente em um estilo evolutivo, começando com uma arquitetura de “célula única” e terminando com a arquitetura MIPS completa ao final do Capítulo 3. Esse estilo vagaroso não combina com o leitor moderno. Esta edição junta todo o material sobre conjunto de instruções para as instruções de inteiros no Capítulo 2 — tornando o Capítulo 3 opcional para muitos leitores — e cada seção agora tem significado próprio. O leitor não precisa mais ler todas as seções anteriores. Logo, o Capítulo 2 agora é ainda melhor como referência do que nas edições anteriores. O Capítulo 4 funciona melhor porque o processador agora está em um único capítulo, pois a implementação de multiciclos hoje é uma distração. O Capítulo 5 tem uma nova seção sobre a montagem de controladores de cache. O Material Complementar nos permitiu reduzir o custo do livro, economizando páginas e também aprofundando-nos em assuntos que eram de interesse de alguns, mas não de todos os leitores. Cada capítulo agora tem a seção de Perspectivas Históricas no site (www.elsevier.com.br/organizacaoeprojeto), e quatro capítulos também possuem uma seção de conteúdo avançado. Para aqueles que perguntam por que incluímos material complementar além do livro, a resposta é simples: o material complementar apresenta conteúdo que acreditamos que será fácil e imediatamente acessível ao leitor, não importa onde ele esteja. Se você estiver interessado no conteúdo avançado, ou se quiser rever um tutorial sobre VHDL (por exemplo), ele está no site, pronto para você utilizar. Este é um campo de mudanças rápidas, e como sempre acontece para nossas novas edições, um objetivo importante é atualizar o conteúdo técnico. O AMD Opteron X4 modelo 2356 (apelidado de “Barcelona”) serve como um exemplo contínuo no livro, e aparece nos Capítulos 1, 4, 5 e 7. Os Capítulos 1 e 6 acrescentam resultados do novo benchmark de potência do SPEC. O Capítulo 2 acrescenta uma seção sobre arquitetura ARM, que atualmente é a ISA de 32 bits mais comum. O Capítulo 5 acrescenta uma nova seção sobre máquinas virtuais, que estão ressurgindo em importância. O Capítulo 5 possui medições detalhadas de desempenho de cache no multicore Opteron X4 e alguns detalhes sobre seu rival, o Intel Nehalem, que foi anunciado no final de 2008. O Capítulo 6 descreve a memória flash pela primeira vez, além de um servidor incrivelmente compacto da Sun, que comprime 8 núcleos, 16 DIMMs e 8 discos em um único bit 1U. Ele também inclui os resultados recentes sobre falhas de disco a longo prazo. O Capítulo 7 aborda diversos tópicos com relação a paralelismo — incluindo multithreading, SIMD, vetor, GPUs, modelos de desempenho, benchmarks, redes de multiprocessadores — e descreve três multicores mais o Opteron X4: Intel Xeon modelo e5345 (Clovertown), IBM Cell modelo QS20 e o Sun Microsystems T2 modelo 5120 (Niagara 2). O objetivo final foi tentar tornar os exercícios úteis a instrutores nesta era da Internet, pois os trabalhos de casa há muito tempo têm sido um modo importante de aprender sobre o material. Infelizmente, hoje as respostas são postadas on-line assim que o livro aparece. Temos uma técnica em duas partes. Primeiro, colaboradores especialistas trabalharam para desenvolver exercícios inteiramente novos para cada capítulo do livro. Segundo, a maioria dos exercícios possui uma descrição qualitativa com o apoio de uma tabela que oferece vários parâmetros quantitativos alternativos, necessários para responder a essa questão. O simples número mais a flexibilidade em termos de como o instrutor pode escolher para atribuir variações dos exercícios tornará difícil para os alunos encontrarem as soluções correspondentes on-line. Os instrutores também poderão mudar esses parâmetros quantitativos como desejarem, novamente frustrando os alunos que contam com a Internet para buscar soluções para um conjunto de exercícios estático e inalterável. Achamos que essa nova técnica é um novo acréscimo valioso ao livro — por favor, nos informe a sua opinião, seja como aluno ou como instrutor!
xvi Prefácio
Preservamos elementos úteis do livro das edições anteriores. Para fazer com que o livro funcione melhor como referência, ainda colocamos definições de novos termos nas margens, em sua primeira ocorrência. A seção “Entendendo o Desempenho do Programa”, presente em cada capítulo, ajuda os leitores a entenderem o desempenho de seus programas e como melhorá-lo, assim como o elemento “Interface de Hardware/Software” do livro ajudou os leitores a entenderem as escolhas nessa interface. A seção “Colocando em Perspectiva” continua, de modo que o leitor verá a floresta apesar de todas as árvores. As seções “Verifique Você Mesmo” ajudam os leitores a confirmarem sua compreensão do material na primeira vez com as respostas fornecidas ao final de cada capítulo. Esta edição também inclui a placa de referência MIPS verde, que foi inspirada pelo “Green Card” do IBM System/360. O cartão removível foi atualizado e deverá ser uma referência prática na escrita de programas em linguagem assembly MIPS.
Comentários finais Ao ler a seção de agradecimentos a seguir, você verá que trabalhamos bastante para corrigir erros. Como um livro passa por muitas tiragens, temos a oportunidade de fazer ainda mais correções. Se você descobrir quaisquer outros erros, por favor, entre em contato com a editora por correio eletrônico em
[email protected], ou pelo correio low-tech, usando o endereço encontrado na página de copyright. Esta edição marca uma quebra na colaboração duradoura entre Hennessy e Patterson, que começou em 1989. As demandas da condução de uma das maiores universidades do mundo significam que o Presidente Hennessy não poderia mais se comprometer com a escrita de uma nova edição. O outro autor se sentiu como um malabarista que sempre trabalhou com um parceiro e que, de repente, é lançado no palco como um ato solo. Assim, as pessoas nos agradecimentos e os colegas da Berkeley desempenharam um papel ainda maior na formação do conteúdo deste livro. Apesar disso, desta vez só existe um único autor a culpar pelo novo material que você está para ler.
Agradecimentos da quarta edição Gostaria de agradecer a David Kirk, John Nickolls e seus colegas na NVIDIA (Michael Garland, John Montrym, Doug Voorhies, Lars Nyland, Erik Lindholm, Paulius Micikevicius, Massimiliano Fatica, Stuart Oberman e Vasily Volkov) por escreverem o primeiro apêndice detalhado sobre GPUs. Gostaria de expressar novamente meu apreço a Jim Larus, da Microsoft Research, por sua disposição em contribuir com sua experiência em programação na linguagem assembly, além de aceitar que os leitores deste livro usem o simulador que ele desenvolveu e mantém. Também sou agradecido pelas contribuições de muitos especialistas que desenvolveram os novos exercícios para esta nova edição. A escrita de bons exercícios não é uma tarefa fácil, e cada colaborador trabalhou muito para desenvolver problemas que sejam desafiadores e atraentes: j
Capítulo 1: Javier Bruguera (Universidade de Santiago de Compostela)
j
Capítulo 2: John Oliver (Cal Poly, San Luis Obispo), com colaborações de Nicole Kaiyan (University of Adelaide) e Milos Prvulovic (Georgia Tech)
j
Capítulo 3: Matthew Farrens (University of California, Davis)
j
Capítulo 4: Milos Prvulovic (Georgia Tech)
Prefácio xvii
j
Capítulo 5: Jichuan Chang, Jacob Leverich, Kevin Lim e Partha Ranganathan (todos da Hewlett-Packard), com colaborações de Nicole Kaiyan (University of Adelaide)
j
Capítulo 6: Perry Alexander (The University of Kansas)
j
Capítulo 7: David Kaeli (Northeastern University)
Peter Ashenden realizou um esforço gigantesco de edição e avaliação de todos os novos exercícios. Obrigado a David August e Prakash Prabhu, da Princeton University, por seu trabalho nos exames de capítulo que estão disponíveis para os instrutores no site da editora. Contei com meus colegas do Vale do Silício para grande parte do material que este livro utiliza: j
AMD — para os detalhes e medição do Opteron X4 (Barcelona): William Brantley, Vasileios Liaskovitis, Chuck Moore e Brian Waldecker.
j
Intel — para a informação de pré-lançamento sobre o Intel Nehalem: Faye Briggs.
j
Micron — para a base sobre Memória Flash no Capítulo 6: Dean Klein.
j
Sun Microsystems — para as medições dos mixes de instruções nos benchmarks SPEC2006 no Capítulo 2 e detalhes e medições do Sun Server x4150 no Capítulo 6: Yan Fisher, John Fowler, Darryl Gove, Paul Joyce, Shenik Mehta, Pierre Reynes, Dimitry Stuve, Durgam Vahia e David Weaver. U.C. Berkeley — Krste Asanovic (que forneceu a ideia para concorrência de software versus paralelismo do hardware no Capítulo 7), James Demmel e Velvel Kahan (que comentaram sobre paralelismo e cálculos de ponto flutuante), Zhangxi Tan (que projetou o controlador de cache e escreveu o Verilog para ele no Capítulo 5), Sam Williams (que forneceu o modelo Roofline e as medições multicore no Capítulo 7), e o restante dos meus colegas na Par Lab, que me deram muitas sugestões e opiniões sobre os tópicos de paralelismo encontrados no decorrer do livro.
j
Sou grato aos muitos instrutores que responderam às pesquisas da editora, revisaram nossas propostas e participaram de grupos de foco para analisar e responder aos nossos planos para esta edição. Entre eles estão os seguintes: Grupo de foco: Mark Hill (University of Wisconsin, Madison), E. J. Kim (Texas A&M University), Jihong Kim (Seoul National University), Lu Peng (Louisiana State University), Dean Tullsen (UC San Diego), Ken Vollmar (Missouri State University), David Wood (University of Wisconsin, Madison), Ki Hwan Yum (University of Texas, San Antonio); Inspeções e críticas: Mahmoud Abou-Nasr (Wayne State University), Perry Alexander (The University of Kansas), Hakan Aydin (George Mason University), Hussein Badr (State University of New York em Stony Brook), Mac Baker (Virginia Military Institute), Ron Barnes (George Mason University), Douglas Blough (Georgia Institute of Technology), Kevin Bolding (Seattle Pacific University), Miodrag Bolic (University of Ottawa), John Bonomo (Westminster College), Jeff Braun (Montana Tech), Tom Briggs (Shippensburg University), Scott Burgess (Humboldt State University), Fazli Can (Bilkent University), Warren R. Carithers (Rochester Institute of Technology), Bruce Carlton (Mesa Community College), Nicholas Carter (University of Illinois at Urbana-Champaign), Anthony Cocchi (The City University of New York), Don Cooley (Utah State University), Robert D. Cupper (Allegheny College), Edward W. Davis (North Carolina State University), Nathaniel J. Davis (Air Force Institute of Technology), Molisa Derk (Oklahoma City University), Derek Eager (University of Saskatchewan), Ernest Ferguson (Northwest Missouri State University), Rhonda Kay Gaede (The University of Alabama), Etienne M. Gagnon (UQAM), Costa Gerousis
xviii Prefácio
(Christopher Newport University), Paul Gillard (Memorial University of Newfoundland), Michael Goldweber (Xavier University), Georgia Grant (College of San Mateo), Merrill Hall (The Master's College), Tyson Hall (Southern Adventist University), Ed Harcourt (Lawrence University), Justin E. Harlow (University of South Florida), Paul F. Hemler (Hampden-Sydney College), Martin Herbordt (Boston University), Steve J. Hodges (Cabrillo College), Kenneth Hopkinson (Cornell University), Dalton Hunkins (St. Bonaventure University), Baback Izadi (State University of New York — New Paltz), Reza Jafari, Robert W. Johnson (Colorado Technical University), Bharat Joshi (University of North Carolina, Charlotte), Nagarajan Kandasamy (Drexel University), Rajiv Kapadia, Ryan Kastner (University of California, Santa Barbara), Jim Kirk (Union University), Geoffrey S. Knauth (Lycoming College), Manish M. Kochhal (Wayne State), Suzan Koknar-Tezel (Saint Joseph's University), Angkul Kongmunvattana (Columbus State University), April Kontostathis (Ursinus College), Christos Kozyrakis (Stanford University), Danny Krizanc (Wesleyan University), Ashok Kumar, S. Kumar (The University of Texas), Robert N. Lea (University of Houston), Baoxin Li (Arizona State University), Li Liao (University of Delaware), Gary Livingston (University of Massachusetts), Michael Lyle, Douglas W. Lynn (Oregon Institute of Technology), Yashwant K Malaiya (Colorado State University), Bill Mark (University of Texas at Austin), Ananda Mondal (Claflin University), Alvin Moser (Seattle University), Walid Najjar (University of California, Riverside), Danial J. Neebel (Loras College), John Nestor (Lafayette College), Joe Oldham (Centre College), Timour Paltashev, James Parkerson (University of Arkansas), Shaunak Pawagi (SUNY em Stony Brook), Steve Pearce, Ted Pedersen (University of Minnesota), Gregory D Peterson (The University of Tennessee), Dejan Raskovic (University of Alaska, Fairbanks) Brad Richards (University of Puget Sound), Roman Rozanov, Louis Rubinfield (Villanova University), Md Abdus Salam (Southern University), Augustine Samba (Kent State University), Robert Schaefer (Daniel Webster College), Carolyn J. C. Schauble (Colorado State University), Keith Schubert (CSU San Bernardino), William L. Schultz, Kelly Shaw (University of Richmond), Shahram Shirani (McMaster University), Scott Sigman (Drury University), Bruce Smith, David Smith, Jeff W. Smith (University of Georgia, Athens), Philip Snyder (Johns Hopkins University), Alex Sprintson (Texas A&M), Timothy D. Stanley (Brigham Young University), Dean Stevens (Morningside College), Nozar Tabrizi (Kettering University), Yuval Tamir (UCLA), Alexander Taubin (Boston University), Will Thacker (Winthrop University), Mithuna Thottethodi (Purdue University), Manghui Tu (Southern Utah University), Rama Viswanathan (Beloit College), Guoping Wang (Indiana-Purdue University), Patricia Wenner (Bucknell University), Kent Wilken (University of California, Davis), David Wolfe (Gustavus Adolphus College), David Wood (University of Wisconsin, Madison), Mohamed Zahran (City College of New York), Gerald D. Zarnett (Ryerson University), Nian Zhang (South Dakota School of Mines & Technology), Jiling Zhong (Troy University), Huiyang Zhou (The University of Central Florida), Weiyu Zhu (Illinois Wesleyan University). Gostaria de agradecer especialmente ao pessoal da Berkeley, que deu a opinião principal para o Capítulo 7 e o Apêndice A, que foram as partes mais desafiadoras para se escrever nesta edição: Krste Asanovic, Christopher Batten, Rastilav Bodik, Bryan Catanzaro, Jike Chong, Kaushik Data, Greg Giebling, Anik Jain, Jae Lee, Vasily Volkov e Samuel Williams. Um agradecimento especial a Mark Smotherman, que fez uma revisão final cuidadosa, para descobrir problemas técnicos e de escrita, o que melhorou significativamente a qualidade desta edição. Ele desempenhou um papel ainda mais importante neste ato, visto que esta edição foi feita como um ato solo.
Prefácio xix
Gostaríamos de agradecer a toda a família Morgan Kaufmann, que concordou em publicar este livro novamente, sob a liderança capaz de Denise Penrose. Nathaniel McFadden foi revisor de desenvolvimento para esta edição, e trabalhou comigo semanalmente no conteúdo do livro. Kimberlee Honjo coordenou a inspeção de usuários e suas respostas. Dawnmarie Simpson administrou o processo de produção do livro. Também agradecemos aos muitos fornecedores autônomos que contribuíram para este volume, especialmente Alan Rose, da Multiscience Press, e à diacriTech, pela composição. As contribuições de quase 200 pessoas que mencionamos aqui tornaram esta quarta edição nosso melhor livro até agora. Divirta-se! David A. Patterson
1 A civilização avança estendendo o número de operações importantes que podem ser realizadas sem se pensar nelas.
Abstrações e Tecnologias Computacionais 1.1 Introdução 1 1.2
Por trás do programa 6
1.3
Sob as tampas 9
1.4 Desempenho 19 Alfred North Whitehead Uma Introdução à Matemática, 1911
1.5
A barreira da potência 29
1.6
Mudança de mares: Passando de processadores para multiprocessadores 31
1.7
Vida real: Fabricação e benchmarking do AMD Opteron X4 34
1.8
Falácias e armadilhas 39
1.9
Comentários finais 41
1.10
Perspectiva histórica e leitura adicional 43
1.11 Exercícios 43
1.1 Introdução Bem-vindo a este livro! Estamos felizes por ter a oportunidade de compartilhar o entusiasmo do mundo dos sistemas computacionais. Esse não é um campo árido e monótono, no qual o progresso é glacial e as novas ideias se atrofiam pelo esquecimento. Não! Os computadores são o produto da impressionante e vibrante indústria da tecnologia da informação, cujos aspectos são responsáveis por quase 10% do produto interno bruto dos Estados Unidos. Essa área incomum abraça a inovação com uma velocidade surpreendente. Nos últimos 25 anos, surgiram inúmeros novos computadores que prometiam revolucionar a indústria da computação; essas revoluções foram interrompidas porque alguém sempre construía um computador ainda melhor. Essa corrida para inovar levou a um progresso sem precedentes desde o início da computação eletrônica no final da década de 1940. Se o setor de transportes, por exemplo, tivesse tido o mesmo desenvolvimento da indústria da computação, hoje nós poderíamos viajar de Nova York até Londres em aproximadamente um segundo por apenas alguns centavos. Imagine por alguns instantes como esse progresso mudaria a sociedade – morar em Taiti e trabalhar em São Francisco, indo para Moscou no início da noite a fim de assistir a uma apresentação do balé de Bolshoi. Não é difícil imaginar as implicações dessa mudança. Os computadores levaram a humanidade a enfrentar uma terceira revolução, a revolução da informação, que assumiu seu lugar junto das revoluções industrial e agrícola. A multiplicação da força e do alcance intelectual do ser humano naturalmente afetou muito nossas vidas cotidianas, além de ter mudado a maneira como conduzimos a busca de novos conhecimentos. Agora, existe um novo veio de investigação científica, com a ciência da computação unindo os cientistas teóricos e experimentais na exploração de novas fronteiras na astronomia, biologia, química, física etc. A revolução dos computadores continua. Cada vez que o custo da computação melhora por um fator de 10, as oportunidades para os computadores se multiplicam. As aplicações que eram economicamente proibitivas de repente se tornam viáveis. As seguintes aplicações, no passado recente, eram “ficção científica para a computação”: j
Computação em automóveis: até os microprocessadores melhorarem significativamente de preço e desempenho no início dos anos 80, o controle dos carros por computadores era considerado um absurdo. Hoje, os computadores reduzem a poluição e melhoram a eficiência do combustível, usando controles no motor, além de aumentarem a segurança por meio da prevenção de derrapagens perigosas e pela ativação de air-bags para proteger os passageiros em caso de colisão.
2
Capítulo 1 Abstrações e Tecnologias Computacionais
j
Telefones celulares: quem sonharia que os avanços dos sistemas computacionais levariam aos telefones portáteis, permitindo a comunicação pessoa a pessoa em quase todo lugar do mundo?
j
Projeto do genoma humano: o custo do equipamento computacional para mapear e analisar as sequências do DNA humano é de centenas de milhares de dólares. É improvável que alguém teria considerado esse projeto se os custos computacionais fossem 10 a 100 vezes mais altos, como há dez ou 20 anos. Além do mais, os custos continuam a cair; você poderá adquirir seu próprio genoma, permitindo que a assistência médica seja ajustada a você mesmo.
j
World Wide Web: ainda não existente na época da primeira edição deste livro, a World Wide Web transformou nossa sociedade. Para muitos, a Web substituiu as bibliotecas.
j
Máquinas de busca: à medida que o conteúdo da Web crescia em tamanho e em valor, encontrar informações relevantes tornou-se cada vez mais importante. Hoje, muitas pessoas contam com máquinas de busca para tantas coisas em suas vidas que seria muito difícil viver sem elas.
Claramente, os avanços dessa tecnologia hoje afetam quase todos os aspectos da nossa sociedade. Os avanços de hardware permitiram que os programadores criassem softwares maravilhosamente úteis e explicassem por que os computadores são onipresentes. A ficção científica de hoje sugere as aplicações que fazem sucesso amanhã: já a caminho estão os mundos virtuais, reconhecimento de voz prático e assistência médica personalizada.
Classes de aplicações de computador e suas características
computadores desktop Um computador projetado para uso por uma única pessoa, normalmente incorporando um monitor gráfico, um teclado e um mouse.
servidor Um computador usado para executar grandes programas para múltiplos usuários quase sempre de maneira simultânea e normalmente acessado apenas por meio de uma rede.
Embora um conjunto comum de tecnologias de hardware (discutidas nas Seções 1.3 e 1.7) seja usado em computadores variando dos dispositivos domésticos inteligentes e telefones celulares aos maiores supercomputadores, essas diferentes aplicações possuem diferentes necessidades de projeto e empregam os fundamentos das tecnologias de hardware de diversas maneiras. Genericamente falando, os computadores são usados em três diferentes classes de aplicações. Os computadores desktop são possivelmente a forma mais conhecida de computação e caracterizam-se pelo computador pessoal, que a maioria dos leitores deste livro provavelmente já usou extensivamente. Os computadores desktop enfatizam o bom desempenho a um único usuário por um baixo custo e normalmente são usados para executar software independente. A evolução de muitas tecnologias de computação é motivada por essa classe da computação, que só tem cerca de 30 anos! Os servidores são a forma moderna do que, antes, eram os mainframes, minicomputadores e supercomputadores, e, em geral, são acessados apenas por meio de uma rede. Os servidores são projetados para suportar grandes cargas de trabalho, que podem consistir em uma única aplicação complexa, normalmente científica ou de engenharia, ou manipular muitas tarefas pequenas, como ocorreria no caso de um grande servidor Web. Essas aplicações muitas vezes são baseadas em software de outra origem (como um banco de dados ou sistema de simulação), mas frequentemente são modificadas ou personalizadas para uma função específica. Os servidores são construídos a partir da mesma tecnologia básica dos computadores desktop, mas fornecem uma maior capacidade de expansão tanto da capacidade de processamento quanto de entrada/saída. Em geral, os servidores também dão grande ênfase à estabilidade, já que uma falha normalmente é mais prejudicial do que seria em um computador desktop de um único usuário. Os servidores abrangem a faixa mais ampla em termos de custo e capacidade. Na sua forma mais simples, um servidor pode ser pouco mais do que uma máquina desktop sem monitor ou teclado e com um custo de mil dólares. Esses servidores de baixa capacidade normalmente são usados para armazenamento de arquivos, pequenas aplicações comerciais
1.1 Introdução 3
ou serviço Web simples (veja Seção 6.10). No outro extremo, estão os supercomputadores, que, atualmente, consistem em centenas ou milhares de processadores e, em geral, de terabytes de memória e de petabytes de armazenamento, e custam desde milhões até centenas de milhões de dólares. Os supercomputadores normalmente são usados para cálculos científicos e de engenharia de alta capacidade, como previsão do tempo, exploração de petróleo, determinação da estrutura da proteína e outros problemas de grande porte. Embora esses supercomputadores representem o máximo da capacidade de computação, eles são uma fração relativamente pequena dos servidores e do mercado de computadores em termos de receita total. Os computadores embutidos são a maior classe de computadores e abrangem a faixa mais ampla de aplicações e desempenho. Os computadores embutidos incluem os microprocessadores encontrados em seu carro, os computadores em um telefone celular, os computadores em um video game ou televisão digital, e as redes de processadores que controlam um avião moderno ou um navio de carga. Os sistemas de computação embutidos são projetados para executar uma aplicação ou um conjunto de aplicações relacionadas como um único sistema; portanto, apesar do grande número de computadores embutidos, a maioria dos usuários nunca vê realmente que está usando um computador! A Figura 1.1 mostra que, durante os últimos anos, o crescimento em telefones celulares que contam com computadores embutidos foi muito mais rápido do que a taxa de crescimento dos computadores desktop. Observe que os computadores embutidos também são encontrados em TVs digitais e sintonizadores, automóveis, câmeras digitais, players de música, video games e uma série de outros dispositivos do consumidor, o que aumenta ainda mais a lacuna entre o número de computadores embutidos e os computadores de desktop. As aplicações embutidas normalmente possuem necessidades específicas que combinam um desempenho mínimo com limitações rígidas em relação a custo ou potência. Por exemplo, considere um telefone celular: o processador só precisa ser tão rápido quanto o necessário para manipular sua função limitada; além disso, minimizar custo e potência é o objetivo mais importante. Apesar do seu baixo custo, os computadores embutidos frequentemente possuem menor tolerância a falhas, já que os resultados podem variar desde um simples incômodo, quando sua nova televisão falha, até a completa devastação que poderia ocorrer quando o computador em um avião ou em um navio falha. Nas aplicações
FIGURA 1.1 O número de telefones celulares, computadores pessoais e televisores fabricados por ano entre 1997 e 2007. (Temos dados de televisão somente a partir de 2004.) Mais de um bilhão de novos telefones celulares foram entregues em 2006. As vendas de telefones celulares ultrapassaram os PCs por um fator apenas de 1,4 em 1997, mas a taxa cresceu para 4,5 em 2007. O número total em uso em 2004 é estimado como sendo 2,0B de televisores, 1,8B de telefones celulares e 0,8B de PCs. Como a população do mundo era cerca de 6,4B em 2004, havia aproximadamente um PC, 2,2 telefones celulares e 2,5 televisores para cada oito pessoas no planeta. Um estudo de 2006 das famílias dos Estados Unidos descobriu que eles possuíam, na média, 12 aparelhos, incluindo 3 TVs, dois PCs e outros dispositivos, como consoles de jogos, tocadores de MP3 e telefones celulares.
supercomputador Uma classe de computadores com desempenho e custo mais altos; eles são configurados como servidores e normalmente custam milhões de dólares.
terabyte Originalmente, 1.099.511.627.776 (240) bytes, embora alguns sistemas de comunicações e de armazenamento secundário o tenham redefinido como significando 1.000.000.000.000 (1012) bytes.
petabyte Dependendo da situação, 1000 ou 1024 terabytes.
centro de dados Uma sala ou prédio criado para tratar das necessidades de energia, resfriamento e rede de um grande número de servidores. computador embutido Um computador dentro de outro dispositivo, usado para executar uma aplicação predeterminada ou uma coleção de software.
4
Capítulo 1 Abstrações e Tecnologias Computacionais
embutidas orientadas ao consumidor, como um eletrodoméstico digital, a estabilidade é obtida principalmente por meio da simplicidade – a ênfase está em realizar uma função o mais perfeitamente possível. Nos grandes sistemas embutidos, em geral, são empregadas as mesmas técnicas de redundância utilizadas nos servidores (veja Seção 6.9). Embora este livro se concentre nos computadores de uso geral, a maioria dos conceitos se aplica diretamente – ou com ligeiras modificações – aos computadores embutidos. Detalhamento: os detalhamentos são seções curtas usadas em todo o texto para fornecer mais detalhes sobre um determinado assunto, que pode ser de interesse. Os leitores que não possuem um interesse específico no tema podem pular essas seções, já que o material subsequente nunca dependerá do conteúdo desta seção. Muitos processadores embutidos são projetados usando núcleos de processador, uma versão de um processador escrita em uma linguagem de descrição de hardware como Verilog ou VHDL (veja Capítulo 4). O núcleo permite que um projetista integre outro hardware específico de uma aplicação com o núcleo do processador para a fabricação de um único chip.
O que você pode aprender neste livro Os bons programadores sempre se preocuparam com o desempenho de seus programas porque gerar resultados rapidamente para o usuário é uma condição essencial na criação bem-sucedida de software. Nas décadas de 1960 e 1970, uma grande limitação no desempenho dos computadores era o tamanho da memória do computador. Assim, os programadores em geral seguiam um princípio simples: minimizar o espaço ocupado na memória para tornar os programas mais rápidos. Na última década, os avanços em arquitetura de computadores e nas tecnologias de fabricação de memórias reduziram drasticamente a importância do tamanho da memória na maioria das aplicações, com exceção dos sistemas embutidos. Agora, os programadores interessados em desempenho precisam entender os problemas que substituíram o modelo de memória simples dos anos 60: a natureza paralela dos processadores e a natureza hierárquica das memórias. Os programadores que desejam construir versões competitivas de compiladores, sistemas operacionais, bancos de dados e mesmo aplicações precisarão, portanto, aumentar seu conhecimento em organização de computadores. Sentimo-nos honrados com a oportunidade de explicar o que existe dentro da máquina revolucionária, decifrando o software por trás do seu programa e o hardware sob a tampa do seu computador. Ao concluir este livro, acreditamos que você será capaz de responder às seguintes perguntas: j
Como os programas escritos em uma linguagem de alto nível, como C ou Java, são traduzidos para a linguagem de máquina e como o hardware executa os programas resultantes? Compreender esses conceitos forma o alicerce para entender os aspectos do hardware e software que afetam o desempenho dos programas.
j
O que é a interface entre o software e o hardware, e como o software instrui o hardware a realizar as funções necessárias? Esses conceitos são vitais para entender como escrever muitos tipos de software.
j
O que determina o desempenho de um programa e como um programador pode melhorar o desempenho? Como veremos, isso depende do programa original, da tradução desse programa para a linguagem do computador e da eficiência do hardware em executar o programa.
j
Que técnicas podem ser usadas pelos projetistas de hardware para melhorar o desempenho? Este livro apresentará os conceitos básicos do projeto de computador moderno. O leitor interessado encontrará muito mais material sobre esse assunto em nosso livro avançado, Arquitetura de Computadores: Uma abordagem quantitativa.
j
1.1 Introdução 5
Quais são os motivos e as consequências da mudança recente do processamento sequencial para o processamento paralelo? Este livro oferece a motivação, descreve os mecanismos de hardware atuais para dar suporte ao paralelismo e estuda a nova geração de microprocessadores “multicore” (veja Capítulo 7).
Sem entender as respostas a essas perguntas, melhorar o desempenho do seu programa em um computador moderno ou avaliar que recursos podem tornar um computador melhor do que outro para uma determinada aplicação será um complicado processo de tentativa e erro, em vez de um procedimento científico conduzido por consciência e análise. Este primeiro capítulo é a base para o restante do livro. Ele apresenta as ideias e definições básicas, coloca os principais componentes de software e hardware em perspectiva, mostra como avaliar o desempenho e a potência, apresenta os circuitos integrados, a tecnologia que estimula a revolução dos computadores, e explica a mudança para multicores. Neste capítulo e em capítulos seguintes, você provavelmente verá muitas palavras novas ou palavras que já pode ter ouvido, mas não sabe ao certo o que significam. Não entre em pânico! Sim, há muita terminologia especial usada para descrever os computadores modernos, mas ela realmente ajuda, uma vez que nos permite descrever precisamente uma função ou capacidade. Além disso, os projetistas de computador (inclusive estes autores) adoram usar acrônimos, que são fáceis de entender quando se sabe o que as letras significam! Para ajudá-lo a lembrar e localizar termos, incluímos uma definição destacada de cada termo novo na primeira vez que aparece no texto. Após um pequeno período trabalhando com a terminologia, você estará fluente e seus amigos ficarão impressionados quando você usar corretamente palavras como BIOS, CPU, DIMM, DRAM, PCIE, SATA e muitas outras. Para enfatizar como os sistemas de software e hardware usados para executar um programa irão afetar o desempenho, usamos uma seção especial, “Entendendo o desempenho dos programas”, em todo o livro; a primeira aparece a seguir. Esses elementos resumem importantes conceitos quanto ao desempenho do programa.
O desempenho de um programa depende de uma combinação entre a eficácia dos algoritmos usados no programa, os sistemas de software usados para criar e traduzir o programa para instruções de máquina e da eficácia do computador em executar essas instruções, que podem incluir operações de entrada/saída (E/S). A tabela a seguir descreve como o hardware e o software afetam o desempenho. Componente de hardware ou software
Como este componente afeta o desempenho
Onde este assunto é abordado?
Algoritmo
Determina o número de instruções do código-fonte e o número de operações de E/S realizadas
Outros livros!
Linguagem de programação, compilador e arquitetura
Determina o número de instruções de máquina para cada instrução em nível de fonte
Capítulos 2 e 3
Processador e sistema de memória
Determina a velocidade em que as instruções podem ser executadas
Capítulos 4, 5 e 7
Sistema de E/S (hardware e sistema operacional)
Determina a velocidade em que as operações de E/S podem ser executadas
Capítulo 6
As Seções “Verifique você mesmo” se destinam a ajudar os leitores a avaliar se compreenderam os principais conceitos apresentados em um capítulo e se entenderam as implicações desses conceitos. Algumas questões “Verifique você mesmo” possuem respostas simples; outras são para discussão em grupo. As respostas às questões específicas
microprocessador multicore Um microprocessador contendo múltiplos processadores (“cores” ou núcleos) em um único circuito integrado.
Acrônimo Uma palavra construída tomando-se as letras iniciais das palavras. Por exemplo: RAM é um acrônimo para Random Access Memory (memória de acesso aleatório) e CPU é um acrônimo para Central Processing Unit (unidade central de processamento).
Entendendo o desempenho dos programas
Verifique você mesmo
6
Capítulo 1 Abstrações e Tecnologias Computacionais
podem ser encontradas no final do capítulo. As questões “Verifique você mesmo” aparecem apenas no final de uma seção, fazendo com que fique mais fácil pulá-las se você estiver certo de que entendeu o assunto. 1. A Seção 1.1 mostrou que o número de processadores embutidos vendidos a cada ano supera, e muito, o número de processadores para desktops. Você pode confirmar ou negar isso com base em sua própria experiência? Tente contar o número de processadores embutidos na sua casa. Compare esse número com o número de computadores desktop em sua casa. 2. Como mencionado anteriormente, tanto o software quanto o hardware afetam o desempenho de um programa. Você pode pensar em exemplos em que cada um dos fatores a seguir é o responsável pelo gargalo no desempenho?
Em Paris, eles simplesmente olhavam perdidos quando eu falava em francês; nunca consegui fazer aqueles idiotas entenderem sua própria língua. Mark Twain, The Innocents Abroad, 1869
software de sistemas Software que fornece serviços que normalmente são úteis, incluindo sistemas operacionais, compiladores e montadores.
j
O algoritmo escolhido
j
A linguagem de programação ou compilador
j
O sistema operacional
j
O processador
j
O sistema de E/S e os dispositivos
1.2
Por trás do programa
Uma aplicação típica, como um processador de textos ou um grande sistema de banco de dados, pode consistir em milhões de linhas de código e se basear em bibliotecas de software sofisticadas que implementam funções complexas no apoio à aplicação. Como veremos, o hardware em um computador só pode executar instruções de baixo nível extremamente simples. Ir de uma aplicação complexa até as instruções simples envolve várias camadas de software que interpretam ou traduzem operações de alto nível nas instruções simples do computador. A Figura 1.2 mostra que essas camadas de software são organizadas principalmente de maneira hierárquica, na qual as aplicações são o anel mais externo e uma variedade de software de sistemas situa-se entre o hardware e as aplicações.
FIGURA 1.2 Uma visão simplificada do hardware e software como camadas hierárquicas, mostradas como círculos concêntricos, em que o hardware está no centro e as aplicações aparecem externamente. Nas aplicações complexas, muitas vezes existem múltiplas camadas de software de aplicação. Por exemplo, um sistema de banco de dados pode ser executado sobre o software de sistemas hospedando uma aplicação, que, por sua vez, executa sobre o banco de dados.
1.2 Por trás do programa 7
Existem muitos tipos de software de sistemas, mas dois tipos são fundamentais em todos os sistemas computacionais modernos: um sistema operacional e um compilador. Um sistema operacional fornece a interface entre o programa de usuário e o hardware e disponibiliza diversos serviços e funções de supervisão. Entre as funções mais importantes estão: j
Manipular as operações básicas de entrada e saída
j
Alocar armazenamento e memória
j
Possibilitar e controlar o compartilhamento do computador entre as diversas aplicações que o utilizam simultaneamente
Exemplos de sistemas operacionais em uso hoje são Linux, MacOS e Windows. Os compiladores realizam outra função fundamental: a tradução de um programa escrito em uma linguagem de alto nível, como C, C + +, Java ou Visual Basic, em instruções que o hardware possa executar. Em razão da sofisticação das linguagens de programação modernas e das instruções simples executadas pelo hardware, a tradução de um programa de linguagem de alto nível para instruções de hardware é complexa. Voltaremos a um breve resumo do processo aqui e depois entraremos em mais detalhes no Capítulo 2 e no Apêndice B.
Sistema operacional Programa de supervisão que gerencia os recursos de um computador em favor dos programas executados nessa máquina.
compilador Um programa que traduz as instruções de linguagem de alto nível para instruções de linguagem assembly.
De uma linguagem de alto nível para a linguagem do hardware Para poder falar com uma máquina eletrônica, você precisa enviar sinais elétricos. Os sinais mais fáceis de serem entendidos pelas máquinas são ligado e desligado; portanto, o alfabeto da máquina se resume a apenas duas letras. Assim como as 26 letras do alfabeto português não limitam o quanto pode ser escrito, as duas letras do alfabeto do computador não limitam o que os computadores podem fazer. Os dois símbolos para essas duas letras são os números 0 e 1, e normalmente pensamos na linguagem de máquina como números na base 2, ou números binários. Chamamos cada “letra” de um dígito binário ou bit. Os computadores são escravos dos nossos comandos, chamados de instruções. As instruções, que são apenas grupos de bits que o computador entende e obedece, podem ser imaginadas como números. Por exemplo, os bits 1000110010100000
dizem ao computador para somar dois números. O Capítulo 2 explica por que usamos números para instruções e dados; não queremos roubar o brilho desse capítulo, mas usar números para instruções e dados é um dos conceitos básicos da computação. Os primeiros programadores se comunicavam com os computadores em números binários, mas isso era tão maçante que rapidamente inventaram novas notações mais parecidas com a maneira como os humanos pensam. No início, essas notações eram traduzidas para binário manualmente, mas esse processo ainda era cansativo. Usando a própria máquina para ajudar a programá-la, os pioneiros inventaram programas que traduzem da notação simbólica para binário. O primeiro desses programas foi chamado de montador (assembler). Esse programa traduz uma versão simbólica de uma instrução para uma versão binária. Por exemplo, o programador escreveria add A,B
e o montador traduziria essa notação como 1000110010100000
dígito binário Também chamado bit. Um dos dois números na base 2 (0 ou 1) que são os componentes da informação.
Instrução Um comando que o hardware do computador entende e obedece.
montador (assembler) Um programa que traduz uma versão simbólica de instruções para a versão binária.
8
linguagem assembly Uma representação simbólica das instruções de máquina.
linguagem de máquina Uma representação binária das instruções de máquina.
linguagem de programação de alto nível Uma linguagem, como C, C++, Java ou Visual Basic, composta de palavras e notação algébrica, que pode ser traduzida por um compilador para a linguagem assembly.
Capítulo 1 Abstrações e Tecnologias Computacionais
Essa instrução diz ao computador para somar dois números, A e B. O nome criado para essa linguagem simbólica, ainda em uso hoje, é linguagem assembly. Ao contrário, a linguagem binária que a máquina entende é a linguagem de máquina. Embora seja um fantástico avanço, a linguagem assembly ainda está longe da notação que um cientista poderia desejar usar para simular fluxos de fluidos ou que um contador poderia usar para calcular seus saldos de contas. A linguagem assembly requer que o programador escreva uma linha para cada instrução que a máquina seguirá, obrigando o programador a pensar como a máquina. A descoberta de que um programa poderia ser escrito para traduzir uma linguagem mais poderosa em instruções de computador foi um dos mais importantes avanços nos primeiros dias da computação. Os programadores atuais devem sua produtividade – e sua sanidade mental – à criação de linguagens de programação de alto nível e de compiladores que traduzem os programas escritos nessas linguagens em instruções. A Figura 1.3 mostra os relacionamentos entre esses programas e linguagens. Um compilador permite que um programador escreva esta expressão em linguagem de alto nível: A +B
FIGURA 1.3 Programa em C compilado para assembly e depois montado em linguagem de máquina. Embora a tradução de linguagem de alto nível para a linguagem de máquina seja mostrada em duas etapas, alguns compiladores removem a fase intermediária e produzem linguagem de máquina diretamente. Essas linguagens e esse programa são analisados com mais detalhes no Capítulo 2.
1.3 Sob as tampas 9
O compilador compilaria isso na seguinte instrução em assembly: add A,B
Como podemos ver, o montador traduziria essa instrução para a instrução binária, que diz ao computador para somar os dois números, A e B. As linguagens de programação de alto nível oferecem vários benefícios importantes. Primeiro, elas permitem que o programador pense em uma linguagem mais natural, usando palavras em inglês e notação algébrica, resultando em programas que se parecem muito mais com texto do que com tabelas de símbolos enigmáticos (veja a Figura 1.3). Além disso, elas permitem que linguagens sejam projetadas de acordo com o uso pretendido. É por isso que a linguagem Fortran foi projetada para computação científica, Cobol para processamento de dados comerciais, Lisp para manipulação de símbolos e assim por diante. Há também linguagens específicas de domínio para grupos ainda mais estreitos de usuários, como aqueles interessados em simulação de fluidos, por exemplo. A segunda vantagem das linguagens de programação é a maior produtividade do programador. Uma das poucas áreas em que existe consenso no desenvolvimento de software é que é necessário menos tempo para desenvolver programas quando são escritos em linguagens que exigem menos linhas para expressar uma ideia. A concisão é uma clara vantagem das linguagens de alto nível em relação à linguagem assembly. A última vantagem é que as linguagens de programação permitem que os programas sejam independentes do computador no qual elas são desenvolvidas, já que os compiladores e montadores podem traduzir programas de linguagem de alto nível para instruções binárias de qualquer máquina. Essas três vantagens são tão fortes que, atualmente, pouca programação é realizada em assembly.
1.3
Sob as tampas
Agora que olhamos por trás do programa para descobrir como ele funciona, vamos abrir a tampa do computador para aprender sobre o hardware dentro dele. O hardware de qualquer computador realiza as mesmas funções básicas: entrada, saída, processamento e armazenamento de dados. A forma como essas funções são realizadas é o principal tema deste livro, e os capítulos subsequentes lidam com as diferentes partes dessas quatro tarefas. Quando tratamos de um aspecto importante neste livro, tão importante que esperamos que você se lembre dele para sempre, nós o enfatizamos identificando-o como um item “Colocando em perspectiva”. Há aproximadamente 12 desses itens no livro; o primeiro descreve os cinco componentes de um computador que realizam as tarefas de entrada, saída, processamento e armazenamento de dados.
Os cinco componentes de um computador são entrada, saída, memória, caminho de dados e controle; os dois últimos, às vezes, são combinados e chamados de processador. A Figura 1.4 mostra a organização padrão de um computador. Essa organização é independente da tecnologia de hardware: você pode classificar cada parte de cada computador, antigos ou atuais, em uma dessas cinco categorias. Para ajudar a manter tudo isso em perspectiva, os cinco componentes de um computador são mostrados na primeira página dos capítulos seguintes, com a parte relativa ao capítulo destacada.
em
Colocando perspectiva
10
Capítulo 1 Abstrações e Tecnologias Computacionais
FIGURA 1.4 A organização de um computador, mostrando os cinco componentes clássicos. O processador obtém instruções e dados da memória. A entrada escreve dados na memória, e a saída lê os dados desta. O controle envia os sinais que determinam as operações do caminho de dados, da memória, da entrada e da saída.
dispositivo de entrada Um mecanismo por meio do qual o computador é alimentado com informações, como o teclado e o mouse. dispositivo de saída Um mecanismo que transmite o resultado de uma computação para o usuário ou para outro computador.
Tive a ideia de criar o mouse enquanto ouvia uma palestra de uma conferência. O orador era tão chato que comecei a me distrair e conceber a ideia. Doug Engelbart
A Figura 1.5 mostra um computador desktop típico com teclado, mouse sem fio e monitor. Essa fotografia revela dois dos principais componentes dos computadores: os dispositivos de entrada, como o teclado e o mouse, e os dispositivos de saída, como o monitor. Como o nome sugere, a entrada alimenta o computador, e a saída é o resultado da computação, enviado para o usuário. Alguns dispositivos, como redes e discos, fornecem tanto entrada quanto saída para o computador. O Capítulo 6 descreve dispositivos de entrada e saída (E/S) em mais detalhes, mas vamos dar um passeio introdutório pelo hardware do computador, começando com os dispositivos de E/S externos.
Anatomia do mouse Embora muitos usuários agora aceitem o mouse sem questionar, a ideia de um dispositivo apontador como um mouse foi mostrada pela primeira vez por Doug Engelbart usando um protótipo em 1967. A Alto, que foi a inspiração para todas as estações de trabalho, inclusive para o Macintosh, incluiu um mouse como seu dispositivo apontador em 1973. Na década de 1990, todos os computadores desktop tinham esse dispositivo, e as novas interfaces gráficas com o usuário e os mouses se tornaram regra. O mouse original era eletromecânico e usava uma grande esfera que, quando rolada sobre uma superfície, fazia com que um contador x e um y fossem incrementados. A quantidade do aumento de cada contador informava a distância e a direção em que o mouse tinha sido movido. O mouse eletromecânico está sendo substituído pelo novo mouse ótico. O mouse ótico é, na verdade, um processador ótico em miniatura incluindo um LED para fornecer iluminação, uma minúscula câmera em preto e branco e um processador ótico simples. O LED ilumina a superfície abaixo do mouse; a câmera tira 1.500 fotografias em cada segundo
1.3 Sob as tampas 11
FIGURA 1.5 Um computador desktop. O monitor de cristal líquido (LCD) é o principal dispositivo de saída, e o teclado e o mouse são os principais dispositivos de entrada. À direita está um cabo Ethernet que conecta o laptop à rede e à Web. O laptop contém o processador, a memória e os dispositivos de E/S adicionais. Esse sistema é um laptop Macbook Pro 15” conectado a um monitor externo.
sob a iluminação. Os quadros sucessivos são enviados para um processador ótico simples, que compara as imagens e determina se e quanto o mouse foi movido. A substituição do mouse eletromecânico pelo mouse eletro-óptico é uma ilustração de um fenômeno comum, no qual os custos cada vez menores e a segurança cada vez maior fazem uma solução eletrônica substituir a tecnologia eletromecânica mais antiga. Mais tarde, veremos outro exemplo: a memória flash.
Diante do espelho Talvez o dispositivo de E/S mais fascinante seja o monitor gráfico. Todos os computadores laptop e portáteis, calculadoras, telefones celulares e quase todos os computadores desktop agora utilizam monitores de cristal líquido (LCDs) para obterem uma tela fina, com baixa potência. O LCD não é a fonte da luz; em vez disso, ele controla a transmissão da luz. Um LCD típico inclui moléculas em forma de bastão em um líquido que forma uma hélice giratória que encurva a luz que entra na tela, de uma fonte de luz atrás da tela ou, menos frequentemente, da luz refletida. Os bastões se esticam quando uma corrente é aplicada e não encurvam mais a luz. Como o material de cristal líquido está entre duas telas polarizadas a 90 graus, a luz não pode passar a não ser que esteja encurvada. Hoje, a maioria dos monitores LCD utiliza uma matriz ativa que tem uma minúscula chave de transistor em cada pixel para controlar a corrente com precisão e gerar imagens mais nítidas. Uma máscara vermelha-verde-azul associada a cada ponto na tela determina a intensidade dos três componentes de cor na imagem final; em um LCD de matriz ativa colorida, existem três chaves de transistor em cada ponto. A imagem é composta de uma matriz de elementos de imagem, ou pixels, que podem ser representados como uma matriz de bits, chamada mapa de bits, ou bitmap. Dependendo do tamanho da tela e da resolução, o tamanho da matriz de vídeo variava de 640 × 480
Pela tela do computador aterrissei um avião no pátio de uma transportadora, observei uma partícula nuclear colidir com uma fonte potencial, voei em um foguete quase na velocidade da luz e vi um computador revelar seus sistemas mais íntimos. Ivan Sutherland, o “pai” da computação gráfica, Scientific American, 1984
monitor de cristal líquido Uma tecnologia de vídeo usando uma fina camada de polímeros líquidos que podem ser usados para transmitir ou bloquear a luz conforme uma corrente seja ou não aplicada.
monitor de matriz ativa Um monitor de cristal líquido usando um transistor para controlar a transmissão da luz em cada pixel individual.
12
Capítulo 1 Abstrações e Tecnologias Computacionais
pixel O menor elemento da imagem. A tela é composta de centenas de milhares a milhões de pixels, organizados em uma matriz.
a 2560 × 1600 pixels em 2008. Um monitor colorido pode usar 8 bits para cada uma das três cores primárias (vermelho, verde e azul), totalizando 24 bits por pixel, permitindo que milhões de cores diferentes sejam exibidas. O suporte de hardware do computador para a utilização de gráficos consiste principalmente em um buffer de atualização de varredura, ou buffer de quadros, para armazenar o mapa de bits. A imagem a ser representada na tela é armazenada no buffer de quadros, e o padrão de bits de cada pixel é lido para o monitor gráfico a uma certa taxa de atualização. A Figura 1.6 mostra um buffer de quadros com 4 bits por pixel.
FIGURA 1.6 Cada coordenada no buffer de quadros à esquerda determina o tom da coordenada correspondente para o monitor TRC de varredura à direita. O pixel (X0, Y0) contém o padrão de bits 0011, que, na tela, é um tom de cinza mais claro do que o padrão de bits 1101 no pixel (X1, Y1).
placa-mãe Uma placa de plástico contendo pacotes de circuitos integrados ou chips, incluindo processador, cache, memória e conectores para dispositivos de E/S, como redes e discos.
circuito integrado Também chamado chip, é um dispositivo que combina de dezenas a milhões de transistores. memória A área de armazenamento temporária em que os programas são mantidos quando estão sendo executados e que contém os dados necessários para os programas em execução.
RAM dinâmica (DRAM) Memória construída como um circuito integrado para fornecer acesso aleatório a qualquer local.
dual inline memory module (DIMM) Uma pequena placa que contém chips DRAM em ambos os lados. (Os SIMMs possuem DRAMs em apenas um lado.)
unidade central de processamento (CPU) Também chamada de processador. A parte ativa do computador, que contém o caminho de dados e o controle e que soma e testa números e sinaliza dispositivos de E/S para que sejam ativados etc.
O objetivo do mapa de bits é representar fielmente o que está na tela. As dificuldades dos sistemas gráficos surgem porque o olho humano é muito bom em detectar mesmo as mais sutis mudanças na tela.
Abrindo o gabinete Se abrirmos o gabinete de um computador, veremos uma interessante placa de plástico, coberta com dezenas de pequenos retângulos cinzas ou pretos. A Figura 1.7 mostra o conteúdo do computador laptop da Figura 1.5. A placa-mãe é mostrada na parte superior da foto. Duas unidades de disco estão na frente – o disco rígido à esquerda e uma unidade de DVD à direita. O furo no meio é para a bateria do laptop. Os pequenos retângulos na placa-mãe contêm os dispositivos que impulsionam nossa tecnologia avançada, os circuitos integrados, apelidados de chips. A placa é composta de três partes: a parte que se conecta aos dispositivos de E/S mencionados anteriormente, a memória e o processador. A memória é onde os programas são mantidos quando estão sendo executados; ela também contém os dados necessários aos programas em execução. Na Figura 1.8, a memória é encontrada nas duas pequenas placas, e cada pequena placa de memória contém oito circuitos integrados. A memória na Figura 1.8 é construída de chips DRAM. DRAM significa RAM dinâmica (Dynamic Random Access Memory). Várias DRAMs são usadas em conjunto para conter as instruções e os dados de um programa. Ao contrário das memórias de acesso sequencial, como as fitas magnéticas, a parte RAM do termo DRAM significa que os acessos à memória levam o mesmo tempo independente da parte da memória lida. O processador é a parte ativa da placa, que segue rigorosamente as instruções de um programa. Ele soma e testa números, sinaliza dispositivos de E/S para serem ativados e assim por diante. O processador é o quadrado abaixo da ventoinha e coberto por um dissipador de calor à esquerda na Figura 1.7. Ocasionalmente, as pessoas chamam o processador de CPU, que significa o termo pomposo unidade central de processamento. Penetrando ainda mais no hardware, a Figura 1.9 revela detalhes de um microprocessador. O processador contém dois componentes principais: o caminho de dados e o controle, correspondendo, respectivamente, aos músculos e ao cérebro do processador. O caminho
1.3 Sob as tampas 13
FIGURA 1.7 Dentro do computador laptop da Figura 1.5. A pequena caixa com o rótulo branco no canto inferior esquerdo é uma unidade de disco rígido SATA de 100 GB, e a pequena caixa de metal no canto inferior direito é a unidade de DVD. O espaço entre elas é onde ficaria a bateria do laptop. O pequeno furo acima do espaço da bateria é para as memórias DIMMs. A Figura 1.8 é uma imagem ampliada das DIMMs, que são inseridas por baixo nesse laptop. Acima do espaço da bateria e da unidade de DVD existe uma placa de circuito impresso, chamada placa-mãe, que contém a maior parte da eletrônica do computador. Os dois pequenos círculos na metade superior da figura são duas ventoinhas com tampas. O processador é o grande retângulo elevado logo abaixo da ventoinha da esquerda. Foto por cortesia da OtherWorldComputing.com.
de dados realiza as operações aritméticas, e o controle diz ao caminho de dados, à memória e aos dispositivos de E/S o que fazer de acordo com as instruções do programa. O Capítulo 4 explica o caminho de dados e o controle para um projeto de desempenho mais alto. Descer até as profundezas de qualquer componente de hardware revela os interiores da máquina. Dentro do processador, existe outro tipo de memória – a memória cache. A memória cache consiste em uma memória pequena e rápida que age como um buffer para a memória DRAM. (A definição não-técnica de cache é um lugar seguro para esconder as coisas.) A cache é construída usando uma tecnologia de memória diferente, a RAM estática – Static Random Access Memory (SRAM). A SRAM é mais rápida, mas menos densa e, portanto, mais cara do que a DRAM. Você pode ter observado um conceito comum nas descrições de software e de hardware: penetrar nas profundezas do hardware ou software revela mais informações, ou seja, detalhes de nível mais baixo estão ocultos para oferecer um modelo mais simples aos níveis mais altos. O uso dessas camadas, ou abstrações, é uma técnica importante para projetar sistemas computacionais extremamente sofisticados.
caminho de dados O componente do processador que realiza operações aritméticas.
controle O componente do processador que comanda o caminho de dados, a memória e os dispositivos de E/S de acordo com as instruções do programa.
memória cache Uma memória pequena e rápida que age como um buffer para uma memória maior e mais lenta.
Static Random Access Memory (SRAM) Também uma memória montada como um circuito integrado, porém mais rápida e menos densa que a DRAM.
abstração Um modelo que revela detalhes de nível inferior dos sistemas computacionais temporariamente invisíveis, a fim de facilitar o projeto de sistemas sofisticados.
14
Capítulo 1 Abstrações e Tecnologias Computacionais
FIGURA 1.8 Close do fundo do laptop revela a memória. A memória principal está contida em uma ou mais placas pequenas mostradas à esquerda. O furo para a bateria está à direita. Os chips de DRAM são montados nessas placas (chamadas Dual Inline Memory Modules – DIMMs) e, então, ligados aos conectores. Foto por cortesia da OtherWorldComputing.com.
FIGURA 1.9 Dentro do microprocessador AMD Barcelona. O lado esquerdo é uma microfotografia do chip processador AMD Barcelona, e o lado direito mostra os principais blocos no processador. O chip tem quatro processadores ou “cores”. O microprocessador no laptop da Figura 1.8 tem dois cores por chip, chamado Intel Core 2 Duo.
arquitetura do conjunto de instruções Também chamada simplesmente de arquitetura. Uma interface abstrata entre o hardware e o software de nível mais baixo de uma máquina que abrange todas as informações necessárias para escrever um programa em linguagem de máquina que será corretamente executado, incluindo instruções, registradores, acesso à memória, E/S e assim por diante.
Uma das abstrações mais importantes é a interface entre o hardware e o software de nível mais baixo. Em decorrência de sua importância, ela recebe um nome especial: a arquitetura do conjunto de instruções, ou simplesmente arquitetura, de uma máquina. A arquitetura do conjunto de instruções inclui tudo o que os programadores precisam saber para fazer um programa em linguagem de máquina binária funcionar corretamente, incluindo instruções, dispositivos de E/S etc. Em geral, o sistema operacional encapsulará os detalhes da E/S, da alocação de memória e de outras funções de baixo nível do sistema, para que os programadores das aplicações não precisem se preocupar com esses detalhes. A combinação do conjunto de instruções básico e a interface do sistema operacional fornecida para os programadores das aplicações é chamada de interface binária de aplicação (ABI). Uma arquitetura do conjunto de instruções permite aos projetistas de computador falarem sobre funções independente do hardware que as realiza. Por exemplo, podemos falar sobre as funções de um relógio digital (marcar as horas, exibir as horas, definir o
1.3 Sob as tampas 15
alarme) sem falar sobre o hardware do relógio (o cristal de quartzo, os visores de LEDs, os botões plásticos). Os projetistas de computador distinguem entre a arquitetura e uma implementação da arquitetura da mesma maneira: uma implementação é o hardware que obedece à abstração da arquitetura. Esses conceitos nos levam a outra seção “Colocando em perspectiva”.
interface binária de aplicação (ABI) A parte voltada ao usuário do conjunto de instruções mais as interfaces do sistema operacional usadas pelos programadores das aplicações. Define um padrão para a portabilidade binária entre computadores.
implementação Hardware
Tanto o hardware quanto o software consistem em camadas hierárquicas, com cada camada inferior ocultando detalhes do nível acima. Esse princípio de abstração é o modo como os projetistas de hardware e os de software lidam com a complexidade dos sistemas computacionais. Uma interface-chave entre os níveis de abstração é a arquitetura do conjunto de instruções — a interface entre o hardware e o software de baixo nível. Essa interface abstrata permite que muitas implementações com custo e desempenho variáveis executem um software idêntico.
que obedece à abstração de uma arquitetura.
em
Colocando perspectiva
memória volátil Armazenamento, como a DRAM, que conserva os dados apenas enquanto estiver recebendo energia.
Um lugar seguro para os dados Até agora, vimos como os dados são inseridos, processados e exibidos. Entretanto, se houvesse uma interrupção no fornecimento de energia, tudo seria perdido porque a memória dentro do computador é volátil – ou seja, quando perde energia, ela se esquece. Por outro lado, um DVD não se esquece da música gravada quando você desliga o aparelho de DVD e, portanto, é uma tecnologia de memória não volátil. Para distinguir entre a memória usada para armazenar dados e programas enquanto estão sendo executados e essa memória não volátil usada para armazenar programas entre as execuções, o termo memória principal é usado para o primeiro e o termo memória secundária é usado para o último. As DRAMs dominam a memória principal desde 1975; e os discos magnéticos dominam a memória secundária desde 1965. O principal armazenamento não volátil usado em todos os computadores servidores e estações de trabalho é o disco rígido magnético. A memória flash, uma memória semicondutora não volátil, é usada no lugar dos discos em dispositivos móveis, como telefones celulares, e está cada vez mais substituindo os discos em tocadores de música e laptops. Como mostra a Figura 1.10, um disco rígido magnético consiste em uma série de discos, que giram em torno de um eixo a velocidades que variam entre 5.400 e 15.000 rotações por minuto. Os discos de metal são cobertos por um material de gravação magnético em ambos os lados, semelhante ao material encontrado em uma fita cassete ou de vídeo. Para ler e gravar informações em um disco rígido, um braço móvel com um pequena bobina eletromagnética, chamada de cabeça de leitura/gravação, é localizada pouco acima de cada superfície. A unidade inteira é selada permanentemente para controlar o ambiente dentro da unidade, o que, por sua vez, permite que as cabeças do disco estejam muito mais próximas da superfície da unidade. O uso de componentes mecânicos significa que os tempos de acesso para os discos magnéticos são muito maiores do que para as DRAMs: os discos em geral levam de 5 a 20 milissegundos, enquanto as DRAMs levam de 50 a 70 nanossegundos – tornando as DRAMs cerca de 100.000 vezes mais rápidas. Entretanto, os discos possuem custos muito mais baixos do que a DRAM para a mesma capacidade de armazenamento, pois os custos de produção para uma determinada quantidade de armazenamento em disco são menores do que para a mesma quantidade de circuito integrado. Em 2008, o custo por gigabyte de disco era aproximadamente de 30 a 100 vezes menor do que o custo da DRAM.
memória não volátil Uma forma de memória que conserva os dados mesmo na ausência de energia e que é usada para armazenar programas entre execuções. O disco magnético é não volátil. memória principal A memória usada para armazenar os programas enquanto estão sendo executados; normalmente consiste na DRAM nos computadores atuais.
memória secundária Memória não volátil usada para armazenar programas e dados entre execuções; normalmente consiste em discos magnéticos nos computadores atuais. disco magnético (também chamado de disco rígido) Uma forma de memória secundária não volátil composta por discos giratórios cobertos com um material de gravação magnético.
memória flash Uma memória semicondutora não volátil. Ela é mais barata e mais lenta que a DRAM, porém mais cara e mais rápida que os discos magnéticos.
gigabyte Tradicionalmente, 1.073.741.824 (230) bytes, embora alguns sistemas de comunicações e de armazenamento secundário o tenham definido como 1.000.000.000 (109) bytes. De modo semelhante, dependendo do contexto, o megabyte é 220 ou 106 bytes.
16
Capítulo 1 Abstrações e Tecnologias Computacionais
FIGURA 1.10 Uma unidade de disco mostrando 10 discos e as cabeças de leitura/gravação. Os diâmetros dos discos rígidos atuais variam por um fator de mais de 3, de menos de 2,5cm até 9cm, e têm sido reduzidos ao longo dos anos para se adequarem a novos produtos; servidores, estações de trabalho, computadores pessoais, laptops, palmtops e câmeras digitais têm inspirado novos tamanhos de disco. Tradicionalmente, os discos maiores possuem melhor desempenho, os discos menores possuem o menor custo, e o melhor custo por gigabyte varia. Embora a maioria dos discos rígidos apareça dentro dos computadores (como na Figura 1.7), os discos rígidos também podem ser conectados usando-se interfaces externas, como Universal Serial Bus (USB).
Assim, existem três principais diferenças entre os discos magnéticos e a memória principal: os discos não são voláteis porque são magnéticos; possuem um tempo de acesso maior porque são dispositivos mecânicos; e são mais baratos por gigabyte porque possuem capacidade de armazenamento muito alta a um custo razoável. Muitos tentaram inventar uma tecnologia mais barata que a DRAM, porém mais rápida que o disco, para preencher a lacuna, mas fracassaram. Os desafiantes nunca levaram um produto ao mercado no momento certo. Quando um novo produto estava para ser entregue, as DRAMs e os discos continuavam a ter avanços rápidos, os custos caíam de modo correspondente e os produtos desafiantes ficavam imediatamente obsoletos. A memória flash, porém, é um desafiante sério. Essa memória semicondutora é não volátil como os discos e tem aproximadamente a mesma largura de banda, mas a latência é 100 a 1.000 vezes mais rápida que o disco. Flash é popular em câmeras e players de música portáteis, pois vem com capacidades muito menores, é mais reforçado e mais eficiente em termos de potência do que os discos, apesar de o custo por gigabyte em 2008 ser cerca de 6 a 10 vezes maior que o de disco. Diferente dos discos e da DRAM, os bits da memória flash se desgastam após 100.000 a 1.000.000 de escritas. Assim, os sistemas de arquivo precisam registrar o número de escritas e ter uma estratégia para evitar desgastar o armazenamento, por exemplo, movendo dados populares. O Capítulo 6 descreve a memória flash com mais detalhes. Embora os discos rígidos não sejam removíveis, há várias tecnologias de armazenamento em uso que incluem as seguintes: j
Os discos óticos, incluindo os compact disks (CDs) e digital video disks (DVDs), constituem a forma mais comum de armazenamento removível. O padrão de disco ótico Blu-Ray (BD) é o aparente sucessor do DVD.
1.3 Sob as tampas 17
j
As placas de memória removíveis baseadas em FLASH geralmente são conectadas por uma conexão Universal Serial Bus (USB) e muitas vezes são usadas para transferir arquivos.
j
A fita magnética fornece apenas acesso serial lento e tem sido usada para realização de backups de disco – uma função que, hoje, normalmente está sendo substituída por discos rígidos duplicados.
A tecnologia de disco ótico funciona de uma maneira completamente diferente da tecnologia de disco magnético. Em um CD, os dados são gravados em espiral, com os bits individuais gravados queimando-se pequenas cavidades – de aproximadamente 1 micron (10-6 metros) de diâmetro – na superfície do disco. O disco é lido emitindo-se um laser na superfície do CD e determinando-se, pelo exame da luz refletida, se existe uma cavidade ou uma superfície plana (reflexiva). Os DVDs usam o mesmo método de emissão de um feixe de laser em direção a uma série de cavidades e superfícies planas. Além disso, há diversas camadas em que o feixe de laser pode ser focalizado, e o tamanho de cada cavidade é muito menor, o que, em conjunto, representa um significativo aumento na capacidade. Blu-Ray utiliza lasers com menor comprimento de onda, o que reduz o tamanho dos bits e, portanto, aumenta a capacidade. Os gravadores de disco ótico nos computadores pessoais usam um laser para criar as cavidades na camada de gravação na superfície do CD ou DVD. Esse processo de gravação é relativamente lento, levando de minutos (para um CD inteiro) a dezenas de minutos (para um DVD inteiro). Portanto, para grandes quantidades, é usada uma técnica diferente, chamada impressão, que custa apenas alguns centavos por disco ótico. Os CDs e DVDs regraváveis usam uma superfície de gravação diferente que possui um material reflexivo cristalino; as cavidades não reflexivas são formadas de maneira semelhante ao CD ou DVD de gravação única. Para apagar o CD ou DVD, a superfície é aquecida e resfriada lentamente, permitindo um processo de recozimento para restaurar a camada de gravação da superfície à sua estrutura cristalina original. Esses discos regraváveis são mais caros do que os discos de gravação única; para os discos somente de leitura – usados para distribuir software, música ou filmes – os custos do disco e da gravação são muito menores.
Comunicação com outros computadores Explicamos como podemos realizar entrada, processamento, exibição e armazenamento de dados, mas ainda falta um item que é encontrado nos computadores modernos: as redes de computadores. Exatamente como o processador mostrado na Figura 1.4 está conectado à memória e aos dispositivos de E/S, as redes conectam computadores inteiros, permitindo que os usuários estendam a capacidade de computação incluindo a comunicação. As redes se tornaram tão comuns que, hoje, constituem o backbone (espinha dorsal) dos sistemas de computação atuais; uma máquina nova sem uma interface de rede opcional seria ridicularizada. Os computadores em rede possuem diversas vantagens importantes: j
Comunicação: as informações são trocadas entre computadores em altas velocidades.
j
Compartilhamento de recursos: em vez de cada máquina ter seus próprios dispositivos de E/S, os dispositivos podem ser compartilhados pelos computadores que compõem a rede.
j
Acesso remoto: conectando computadores por meio de longas distâncias, os usuários não precisam estar perto do computador que estão usando.
As redes variam em tamanho e desempenho, com o custo da comunicação aumentando de acordo com a velocidade de comunicação e a distância em que as informações viajam. Talvez o tipo de rede mais comum seja a Ethernet. Sua extensão é limitada em aproximadamente um quilômetro, transferindo até 10 gigabits por segundo. Sua extensão e velocidade tornam a Ethernet útil para conectar computadores no mesmo andar de um
18
Capítulo 1 Abstrações e Tecnologias Computacionais
rede local (LAN) Uma rede
prédio; portanto, esse é um exemplo do que é chamado genericamente de rede local (LAN). As redes locais são interconectadas com switches que também podem fornecer serviços de roteamento e segurança. As redes remotas (WAN) atravessam continentes e são a espinha dorsal da Internet, que é o suporte da World Wide Web. Elas costumam ser baseadas em cabos de fibra ótica e são alugadas de empresas de telecomunicações. As redes mudaram a cara da computação nos últimos 25 anos, por se tornarem muito mais comuns e aumentarem dramaticamente o desempenho. Na década de 1970, poucas pessoas tinham acesso ao correio eletrônico (e-mail). A Internet e a Web não existiam, e a remessa física de fitas magnéticas era o meio principal de transferir grandes quantidades de dados entre dois locais. Nessa década, as redes locais eram quase inexistentes e as poucas redes remotas existentes tinham capacidade limitada e acesso restrito. À medida que a tecnologia de redes avançou, ela se tornou muito mais barata e obteve uma capacidade de transmissão muito mais alta. Por exemplo, a primeira tecnologia de rede local a ser padronizada, desenvolvida há cerca de 25 anos, foi uma versão da Ethernet que tinha uma capacidade máxima (também chamada de largura de banda) de 10 milhões de bits por segundo, normalmente compartilhada por dezenas, se não centenas, de computadores. Hoje, a tecnologia de rede local oferece uma capacidade de transmissão de 100 milhões de bits por segundo até 10 gigabits por segundo, em geral compartilhada por, no máximo, alguns computadores. A tecnologia de comunicação ótica permitiu um crescimento semelhante na capacidade das redes remotas de centenas de kilobits até gigabits, e de centenas de computadores conectados a uma rede mundial até milhões de computadores conectados. Essa combinação do dramático aumento no emprego das redes e os aumentos em sua capacidade tornaram a tecnologia de redes o ponto central para a revolução da informação nos últimos 25 anos. Recentemente, outra inovação na tecnologia de redes está reformulando a maneira como os computadores se comunicam. A tecnologia sem fio se tornou amplamente utilizada e a maioria dos laptops hoje incorpora essa tecnologia. A capacidade de criar um rádio com a mesma tecnologia de semicondutor de baixo custo (CMOS) usada para memória e microprocessadores permitiu uma significativa melhoria no preço, levando a uma explosão no consumo. As tecnologias sem fio disponíveis atualmente, conhecidas pelo padrão IEEE 802.11, permitem velocidades de transmissão de 1 a quase 100 milhões de bits por segundo. A tecnologia sem fio é um pouco diferente das redes baseadas em fios, já que todos os usuários em uma área próxima compartilham as ondas aéreas.
projetada para transportar dados dentro de uma área geograficamente restrita, em geral, dentro de um mesmo prédio.
rede remota (WAN) Uma rede estendida por centenas de quilômetros, que pode atravessar continentes.
Verifique você mesmo
j
A DRAM e o armazenamento de disco diferem significativamente. Descreva a principal diferença quanto a cada um dos seguintes aspectos: volatilidade, tempo de acesso e custo.
Tecnologias para construção de processadores e memórias Os processadores e a memória melhoraram em uma velocidade espantosa porque os projetistas de computadores, durante muito tempo, abraçaram o que havia de mais moderno na tecnologia eletrônica a fim de tentar vencer a corrida para projetar um computador melhor. A Figura 1.11 mostra as tecnologias usadas ao longo do tempo, com uma estimativa do desempenho relativo por custo unitário para cada tecnologia. A Seção 1.7 explora a tecnologia que impulsionou a indústria da computação desde 1975 e continuará
FIGURA 1.11 Desempenho relativo por custo unitário das tecnologias usadas nos computadores ao longo do tempo. Fonte: Computer Museum, Boston, com o ano de 2005 estimado pelos autores. Ver Seção 1.10 no site.
1.4 Desempenho 19
FIGURA 1.12 Crescimento da capacidade por chip de DRAM ao longo do tempo. O eixo y é medido em Kbits, em que K = 1024 (210). A indústria de DRAM quadruplicou a capacidade a cada quase três anos, um aumento de 60% por ano, durante 20 anos. Nos últimos anos, essa taxa diminuiu um pouco e está próximo do dobro a cada dois anos.
a impulsioná-la no futuro previsível. Como essa tecnologia esboça o que os computadores serão capazes de fazer e a velocidade com que irão evoluir, acreditamos que todos os profissionais de computação devem estar familiarizados com os fundamentos dos circuitos integrados. Um transistor é simplesmente uma chave liga/desliga controlada por eletricidade. O circuito integrado (CI) combinou dezenas a centenas de transistores em um único chip. Para descrever o incrível aumento no número de transistores de centenas para milhões, o adjetivo escala muito grande é acrescentado ao termo, criando a abreviação VLSI (de Very Large Scale Integrated). Essa taxa de integração crescente tem se mantido notavelmente estável. A Figura 1.12 mostra o crescimento na capacidade da DRAM desde 1977. Durante 20 anos, a indústria quadruplicou consistentemente a capacidade a cada três anos, resultando em um aumento de mais de 16.000 vezes! Esse aumento no número de transistores para um circuito integrado é popularmente conhecido como a Lei de Moore, que diz que a capacidade em transistores dobra a cada 18 a 24 meses. A Lei de Moore resultou de uma previsão desse crescimento na capacidade do circuito integrado feita por Gordon Moore, um dos fundadores da Intel durante a década de 1960. Sustentar essa taxa de progresso por quase 40 anos exigiu incríveis inovações nas técnicas de fabricação. Na Seção 1.7, discutimos como os circuitos integrados são fabricados.
1.4 Desempenho Avaliar o desempenho dos computadores pode ser desafiador. A escala e a complexidade dos sistemas de software modernos, junto com a grande variedade de técnicas de melhoria de desempenho empregadas por projetistas de hardware, tornaram a avaliação do desempenho muito mais difícil. Ao tentar escolher entre diferentes computadores, o desempenho é um atributo importante. Comparar e avaliar com precisão diferentes computadores é crítico para compradores e por consequência, também para os projetistas. O pessoal que vende computadores também sabe disso. Normalmente, os vendedores desejam que você veja seu computador da melhor maneira possível, não importa se isso reflete ou não as necessidades da aplicação do comprador. Logo, ao escolher um computador, é importante entender como medir melhor o desempenho e as limitações das medições de desempenho. O restante desta seção descreve diferentes maneiras como o desempenho pode ser determinado; depois, descrevemos as métricas para avaliar o desempenho do ponto de vista
válvula Um componente eletrônico, predecessor do transistor, que consiste em um tubo de vidro oco de aproximadamente 5 a 10 centímetros de comprimento do qual o máximo de ar foi removido e que usa um feixe de elétrons para transferir dados.
transistor Uma chave liga/desliga controlada por um sinal elétrico.
circuito Very Large Scale Integrated (VLSI) Um dispositivo com centenas de milhares a milhões de transistores.
20
Capítulo 1 Abstrações e Tecnologias Computacionais
de um usuário do computador e um projetista. Também analisamos como essas métricas estão relacionadas e apresentamos a equação clássica de desempenho do processador, que usaremos no decorrer do texto.
Definindo o desempenho Quando dizemos que um computador tem melhor desempenho que outro, o que queremos dizer? Embora essa pergunta possa parecer simples, uma analogia com aviões de passageiros mostra como a questão de desempenho pode ser sutil. A Figura 1.13 mostra alguns aviões de passageiros típicos, juntamente com a velocidade de cruzeiro, alcance e capacidade. Se você quisesse saber qual dos aviões nessa tabela tem o melhor desempenho, primeiro precisaríamos definir o desempenho. Por exemplo, considerando diferentes medidas de desempenho, vemos que o avião com a maior velocidade de cruzeiro é o Concorde, o avião com o maior alcance é o DC-8, e o avião com a maior capacidade é o 747.
FIGURA 1.13 A capacidade, alcance e velocidade de uma série de aviões comerciais. A última coluna mostra a taxa com que o avião transporta passageiros, que é a capacidade vezes a velocidade de voo (ignorando o alcance e os tempos de decolagem e pouso).
tempo de resposta Também chamado tempo de execução. O tempo total exigido para o computador completar uma tarefa, incluindo acessos ao disco, acessos à memória, atividades de E/S, overhead do sistema operacional, tempo de execução de CPU e assim por diante.
throughput Também chamado largura de banda. Outra medida de desempenho, é o número de tarefas completadas por unidade de tempo.
Vamos supor que o desempenho seja definido em termos de velocidade. Isso ainda deixa duas definições possíveis. Você poderia definir o avião mais rápido como aquele com a velocidade de voo mais alta, levando um único passageiro de um ponto para outro com o menor tempo. Porém, se você estivesse interessado em transportar 450 passageiros de um ponto para outro, o 747 certamente seria o mais rápido, como mostra a última coluna da figura. De modo semelhante, podemos definir o desempenho do computador de diferentes maneiras. Se você estivesse rodando um programa em dois computadores desktop diferentes, diria que o mais rápido é o computador que termina o trabalho primeiro. Se estivesse gerenciando um centro de dados com diversos servidores rodando tarefas submetidas por muitos usuários, você diria que o computador mais rápido é aquele que completasse o máximo de tarefas durante um dia. Como um usuário de computador individual, você está interessado em reduzir o tempo de resposta — o tempo entre o início e o término de uma tarefa — também conhecido como tempo de execução. Os gerentes de centro de dados normalmente estão interessados em aumentar o throughput ou largura de banda — a quantidade total de trabalho realizado em determinado tempo. Logo, na maioria dos casos, ainda precisaremos de diferentes métricas de desempenho, além de diferentes conjuntos de aplicações para avaliar computadores embutidos e de desktop, que são mais voltados para o tempo de resposta, contra servidores, que são mais voltados para o throughput.
Throughput e tempo de resposta
EXEMPLO
As mudanças a seguir em um sistema de computador aumentam o throughput, diminuem o tempo de resposta ou ambos? 1. Substituir o processador em um computador por uma versão mais rápida.
1.4 Desempenho 21
2. Acrescentar processadores adicionais a um sistema que utiliza múltiplos processadores para tarefas separadas — por exemplo, busca na World Wide Web. Diminuir o tempo de resposta quase sempre melhora o throughput. Logo, no caso 1, o tempo de resposta e o throughput são melhorados. No caso 2, ninguém realiza o trabalho mais rapidamente, de modo que somente o throughput aumenta. Porém, se a demanda para processamento no segundo caso fosse quase tão grande quanto o throughput, o sistema poderia forçar as solicitações a se enfileirarem. Nesse caso, aumentar o throughput também poderia melhorar o tempo de resposta, pois poderia reduzir o tempo de espera na fila. Assim, em muitos sistemas de computadores reais, mudar o tempo de execução ou o throughput normalmente afeta o outro.
RESPOSTA
Na discussão sobre o desempenho dos computadores, vamos nos preocupar principalmente com o tempo de resposta nos primeiros capítulos. Para maximizar o desempenho, queremos minimizar o tempo de resposta ou o tempo de execução para alguma tarefa. Assim, podemos relacionar desempenho e tempo de execução para o computador X: Desempenho X =
1 Tempo de execução X
Isso significa que, para dois computadores X e Y, se o desempenho de X for maior que o desempenho de Y, temos Desempenho X > Desempenho Y 1 1 > Tempo de execução X Tempo de execução Y Tempo de execução Y > Tempo de execução X Ou seja, o tempo de execução em Y é maior que o de X, se X for mais rápido que Y. Na discussão de um projeto de computador, normalmente queremos relacionar o desempenho de dois computadores diferentes quantitativamente. Usaremos a frase “X é n vezes mais rápido que Y” — ou, de modo equivalente, “X tem n vezes a velocidade de Y” — para indicar Desempenho X =n Desempenho Y Se X for n vezes mais rápido que Y, então o tempo de execução em Y é n vezes maior do que em X: Desempenho X Tempo de execução Y = =n Desempenho Y Tempo de execução X
Desempenho relativo
Se o computador A executa um programa em 10 segundos e o computador B executa o mesmo programa em 15 segundos, o quanto A é mais rápido que B?
EXEMPLO
22
Capítulo 1 Abstrações e Tecnologias Computacionais
RESPOSTA
Sabemos que A é n vezes mais rápido que B se Desempenho A Tempo de execuçãoB = =n DesempenhoB Tempo de execução A Assim, a razão de desempenho é 15 = 1,5 10 e A, portanto, é 1,5 vez mais rápido que B. No exemplo anterior, também poderíamos dizer que o computador B é 1,5 vez mais lento que o computador A, pois Desempenho A = 1,5 DesempenhoB significando que Desempenho A = DesempenhoB 1,5 Para simplificar, normalmente usaremos a terminologia mais rápido que quando tentamos comparar computadores quantitativamente. Como o desempenho e o tempo de execução são recíprocos, aumentar o desempenho requer diminuir o tempo de execução. Para evitar a confusão em potencial entre os termos aumentar e diminuir, normalmente dizemos “melhorar o desempenho” ou “melhorar o tempo de execução” quando queremos dizer “aumentar o desempenho” e “diminuir o tempo de execução”.
Medindo o desempenho
tempo de execução de CPU Também chamado tempo de CPU. O tempo real que a CPU gasta calculando para uma tarefa específica.
tempo de CPU do usuário O tempo de CPU gasto em um programa propriamente dito.
tempo de CPU do sistema O tempo de CPU gasto no sistema operacional realizando tarefas em favor do programa.
O tempo é a medida de desempenho do computador: o computador que realiza a mesma quantidade de trabalho no menor tempo é o mais rápido. O tempo de execução do programa é medido em segundos por programa. Porém, o tempo pode ser definido de diferentes maneiras, dependendo do que contamos. A definição mais clara de tempo é chamada de tempo do relógio, tempo de resposta ou tempo decorrido. Esses termos significam o tempo total para completar uma tarefa, incluindo acessos ao disco, acessos à memória, atividades de entrada/saída (E/S), overhead do sistema operacional — tudo. Contudo, os computadores normalmente são compartilhados e um processador pode trabalhar em vários programas simultaneamente. Nesses casos, o sistema pode tentar otimizar o throughput em vez de tentar minimizar o tempo decorrido para um programa. Logo, normalmente queremos distinguir entre o tempo decorrido e o tempo que o processador está trabalhando em nosso favor. Tempo de execução de CPU, ou simplesmente tempo de CPU, que reconhece essa distinção, é o tempo que a CPU gasta computando para essa tarefa, e não inclui o tempo gasto esperando pela E/S ou pela execução de outros programas. (Lembre-se, porém, de que o tempo de resposta experimentado pelo usuário será o tempo decorrido do programa, e não o tempo de CPU.) O tempo de CPU pode ser dividido ainda mais no tempo de CPU gasto no programa, chamado tempo de CPU do usuário, e o tempo de CPU gasto no sistema operacional, realizando tarefas em favor do programa, chamado tempo de CPU do sistema. A diferenciação entre o tempo de CPU do sistema e do usuário é difícil de se realizar com precisão, pois normalmente é difícil atribuir a responsabilidade pelas atividades do sistema operacional a um programa do usuário em vez do outro, e por causa das diferenças de funcionalidade entre os sistemas operacionais.
1.4 Desempenho 23
Por uma questão de consistência, mantemos uma distinção entre o desempenho baseado no tempo decorrido e baseado no tempo de execução da CPU. Usaremos o termo desempenho do sistema para nos referirmos ao tempo decorrido em um sistema não carregado e desempenho da CPU para nos referirmos ao tempo de CPU do usuário. Vamos focalizar o desempenho da CPU neste capítulo, embora nossas discussões de como resumir o desempenho possam ser aplicadas às medições de tempo decorrido ou tempo de CPU.
Diferentes aplicações são sensíveis a diferentes aspectos do desempenho de um sistema de computador. Muitas aplicações, especialmente aquelas rodando em servidores, dependem muito do desempenho da E/S, que, por sua vez, conta com o hardware e o software. O tempo decorrido total medido por um relógio comum é a medida de interesse. Em alguns ambientes de aplicação, o usuário pode se importar com o throughput, tempo de resposta ou uma combinação complexa dos dois (por exemplo, o throughput máximo com o tempo de resposta no pior caso). Para melhorar o desempenho de um programa, deve-se ter uma definição clara de qual métrica de desempenho interessa e depois prosseguir para procurar gargalos de desempenho medindo a execução do programa e procurando os prováveis gargalos. Nos próximos capítulos, vamos descrever como procurar gargalos e melhorar o desempenho em diversas partes do sistema.
Embora, como usuários de computador, nos importemos com o tempo, quando examinamos os detalhes de um computador, é conveniente pensar sobre o desempenho em outras métricas. Em particular, os projetistas de comunicação podem querer pensar a respeito de um computador usando uma medida que se relaciona à velocidade com que o hardware pode realizar suas funções básicas. Quase todos os computadores são construídos usando-se um clock que determina quando os eventos ocorrem no hardware. Esses intervalos de tempo discretos são chamados de ciclos de clock (ou batidas, batidas de clock, períodos de clock, clocks, ciclos). Os projetistas referem-se à extensão de um período de clock como o tempo para um ciclo de clock completo (por exemplo, 250 picossegundos, ou 250 ps) e como a taxa de clock (por exemplo, 4 gigahertz, ou 4 GHz), que é o inverso do período de clock. Na próxima subseção, formalizaremos o relacionamento entre os ciclos de clock do projetista de hardware e os segundos do usuário do computador. 1. Suponha que saibamos que uma aplicação que usa um cliente de desktop e um servidor remoto seja limitada pelo desempenho da rede. Para as mudanças a seguir, indique se somente o throughput melhora, o tempo de resposta e o throughput melhoram, ou nenhum destes melhora. a. Um canal de rede extra é acrescentado entre o cliente e o servidor, aumentando o throughput total da rede e reduzindo o atraso para obter o acesso à rede (pois agora existem dois canais). b. O software de rede é melhorado, reduzindo assim o atraso na comunicação da rede, mas não aumentando o throughput. c. Mais memória é acrescentada ao computador. 2. O desempenho do computador C é quatro vezes mais rápido que o desempenho do computador B, que executa determinada aplicação em 28 segundos. Quanto tempo o computador C levará para executar essa aplicação?
Desempenho da CPU e seus fatores Usuários e projetistas normalmente examinam o desempenho usando diferentes métricas. Se pudéssemos relacionar essas diferentes métricas, poderíamos determinar o efeito de uma mudança de projeto sobre o desempenho experimentado pelo usuário. Como estamos
Entendendo o desempenho do programa
ciclo de clock Também chamado batida, batida de clock, período de clock, clock, ciclo. O tempo para um período de clock, normalmente do clock do processador, que trabalha a uma taxa constante. período de clock A extensão de cada ciclo de clock.
Verifique você mesmo
24
Capítulo 1 Abstrações e Tecnologias Computacionais
interessados no desempenho da CPU neste ponto, a medida de desempenho final é o tempo de execução da CPU. Uma fórmula simples relaciona as métricas mais básicas (ciclos de clock e tempo do ciclo de clock) ao tempo da CPU: Tempo de execução da CPU Ciclos de clock da CPU = × Tempo do ciclo de clock para um programa para um programa Como alternativa, como a taxa de clock e o tempo do ciclo de clock são inversos, Ciclos de clock da CPU para um programa Tempo de execução da CPU = para um programa Taxa de clock Essa fórmula deixa claro que o projetista de hardware pode melhorar o desempenho reduzindo o número de ciclos de clock exigidos para um programa ou o tamanho do ciclo de clock. Conforme veremos em outros capítulos, os projetistas normalmente têm de escolher entre o número de ciclos de clock necessários para um programa e a extensão de cada ciclo. Muitas técnicas que diminuem o número de ciclos de clock podem também aumentar o tempo do ciclo de clock.
Melhorando o desempenho
EXEMPLO
RESPOSTA
Nosso programa favorito executa em 10 segundos no computador A, que tem um clock de 2 GHz. Estamos tentando ajudar um projetista de computador a montar um computador B, que executará esse programa em 6 segundos. O projetista determinou que é possível haver um aumento substancial na taxa de clock, mas esse aumento afetará o restante do projeto da CPU, fazendo com que o computador B exija 1,2 vez a quantidade de ciclos de clock do computador A para esse programa. Que taxa de clock o projetista deve ter como alvo? Vamos primeiro achar o número de ciclos de clock exigidos para o programa em A: Tempo de CPU A =
10 segundos =
Ciclos de clock de CPU A Taxa de clock A
Ciclos de clock de CPU A ciclos 2 × 109 segundo
Ciclos de clock de CPU A = 10 segundos × 2 × 109
ciclos = 20 × 109 ciclos segundo
O tempo de CPU para B pode ser encontrado por meio desta equação: Tempo de CPU B =
6 segundos =
1, 2 × Ciclos de CPU A Taxa de clock B
1, 2 × 20 × 109 ciclos Taxa de clock B
Taxa de clock B =
1.4 Desempenho 25
1, 2 × 20 × 109 ciclos 0, 2 × 20 × 109 ciclos 4 × 109 ciclos = = = 4GHz 6 segundos segundo segundo
Para executar o programa em 6 segundos, B deverá ter o dobro da taxa de clock de A.
Desempenho da instrução Essas equações de desempenho não incluíram qualquer referência ao número de instruções necessárias para o programa. (Veremos como são as instruções que compõem um programa no próximo capítulo.) Porém, como o compilador claramente gera instruções para executar, e o computador teve de rodá-las para executar o programa, o tempo de execução deverá depender do número de instruções em um programa. Um modo de pensar a respeito do tempo de execução é que ele é igual ao número de instruções executadas multiplicado pelo tempo médio por instrução. Portanto, o número de ciclos de clock exigido para um programa pode ser escrito como de clock médios Ciclos de clock de CPU = Instruções para um programa × Ciclos por instrução O termo ciclos de clock por instrução, que é o número médio de ciclos de clock que cada instrução leva para executar, normalmente é abreviado como CPI. Como diferentes instruções podem exigir diferentes quantidades de tempo, dependendo do que elas fazem, CPI é uma média de todas as instruções executadas no programa. CPI oferece um modo de comparar duas implementações diferentes da mesma arquitetura do conjunto de instruções, pois o número de instruções executadas para um programa, logicamente, será o mesmo.
ciclos de clock por instruções (CPI) Número médio de ciclos de clock por instrução para um programa ou fragmento de programa.
Usando a equação de desempenho
Suponha que tenhamos duas implementações da mesma arquitetura de conjunto de instruções. O computador A tem um tempo de ciclo de clock de 250 ps e um CPI de 2,0 para algum programa, e o computador B tem um tempo de ciclo de clock de 500 ps e um CPI de 1,2 para o mesmo programa. Qual computador é mais rápido para esse programa e por quanto? Sabemos que cada computador executa o mesmo número de instruções para o programa; vamos chamar esse número de I. Primeiro, encontramos o número de ciclos de clock do processador para cada computador: Ciclos de clock de CPU A = I × 2,0 Ciclos de clock de CPU B = I × 1, 2 Agora, podemos calcular o tempo de CPU para cada computador: Tempo de CPU A = Ciclos de clock de CPU A × Tempo de ciclo de clock = I × 2,0 × 250 ps = 500 × I ps
EXEMPLO
RESPOSTA
26
Capítulo 1 Abstrações e Tecnologias Computacionais
De modo semelhante, para B: Tempo de CPU B = I × 1, 2 × 500 ps = 600 × I ps Claramente, o computador A é mais rápido. A quantidade mais rápida é dada pela razão dos tempos de execução: Desempenho da CPU A Tempo de execuçãoB 600 × I ps = 1, 2 = = Desempenho da CPU B Tempo de execução A 500 × I ps Podemos concluir que o computador A é 1,2 vez mais rápido que o computador B para esse programa.
A equação clássica de desempenho da CPU contador de instrução O número de instruções executadas pelo programa.
Agora, podemos escrever essa equação de desempenho básica em termos do contador de instrução (o número de instruções executadas pelo programa), CPI e tempo de ciclo do clock: Tempo de CPU = Contador de instrução × CPI × Tempo de ciclo de clock ou então, como a taxa de clock é o inverso do tempo de ciclo de clock: Tempo de CPU =
Contador de instrução × CPI Taxa de clock
Essas fórmulas são particularmente úteis porque separam os três fatores principais que afetam o desempenho. Podemos usá-las para comparar duas implementações diferentes ou para avaliar uma alternativa de projeto se soubermos seu impacto sobre esses três parâmetros.
Comparando segmentos de código
EXEMPLO
Um projetista de compilador está tentando decidir entre duas sequências de código para determinado computador. Os projetistas de hardware forneceram os seguintes fatos: CPI para cada classe de instrução CPI
A
B
C
1
2
3
Para determinada instrução na linguagem de alto nível, o escritor do compilador está considerando duas sequências de código que exigem as seguintes contagens de instruções: Contagens de instruções para cada classe de instrução Sequência de código
A
B
C
1
2
1
2
2
4
1
1
1.4 Desempenho 27
Qual sequência de código executa mais instruções? Qual será mais rápida? Qual é o CPI para cada sequência? A sequência 1 executa 2 + 1 + 2 = 5 instruções. A sequência 2 executa 4 + 1 + 1 = 6 instruções. Portanto, a sequência 1 executa menos instruções. Podemos usar a equação para ciclos de clock de CPU com base na contagem de instruções e CPI a fim de descobrir o número total de ciclos de clock para cada sequência:
RESPOSTA
n
Ciclos de clock de CPU = ∑(CPIi × C i ) i =1
Isso gera Ciclos de clock de CPU1 = (2 × 1) + (1 × 2) + (2 × 3) = 2 + 2 + 6 = 10 ciclos Ciclos de clock de CPU 2 = (4 × 1) + (1 × 2) + (1 × 3) = 4 + 2 + 3 = 9 ciclos Assim, a sequência de código 2 é mais rápida, embora execute uma instrução extra. Como a sequência de código 2 leva menos ciclos de clock em geral, mas tem mais instruções, ela deverá ter um CPI menor. Os valores de CPI podem ser calculados por CPI =
Ciclos de clock de CPU Contagem de instruções
CPI1 =
Ciclos de clock de CPU1 10 = = 2,0 Contagem de instruções1 5
CPI2 =
Ciclos de clock de CPU 2 9 = = 1,5 Contagem de instruções 2 6
A Figura 1.14 mostra as medições básicas em diferentes níveis no computador e o que está sendo medido em cada caso. Podemos ver como esses fatores são combinados para gerar um tempo de execução medido em segundos por programa: Tempo =
Segundos Instruções Ciclos de clock Segundos = × × Programa Programa Instrução Ciclo de clock
Lembre-se sempre de que a única medida completa e confiável do desempenho do computador é o tempo. Por exemplo, mudar o conjunto de instruções para reduzir a contagem de instruções pode levar a uma organização com um tempo de ciclo de clock menor ou CPI maior, que compensa a melhoria na contagem de instruções. De modo semelhante, como o CPI depende do tipo das instruções executadas, o código que executa o menor número de instruções pode não ser o mais rápido.
em
Colocando perspectiva
28
Capítulo 1 Abstrações e Tecnologias Computacionais
FIGURA 1.14 Os componentes básicos do desempenho e como cada um é medido.
mix de instruções Uma medida da frequência dinâmica das instruções por um ou muitos programas.
Entendendo o desempenho do programa
Como determinar o valor desses fatores na equação de desempenho? Podemos medir o tempo de execução da CPU rodando o programa, e o tempo do ciclo de clock normalmente é publicado como parte da documentação de um computador. A contagem de instruções e o CPI podem ser mais difíceis de se obter. Naturalmente, se soubermos a taxa de clock e o tempo de execução da CPU, só precisamos da contagem de instruções ou do CPI para determinar o outro. Podemos medir a contagem de instruções usando ferramentas de software que determinam o perfil da execução ou usando um simulador da arquitetura. Como alternativa, podemos usar contadores de hardware, que estão incluídos na maioria dos processadores, para registrar uma série de medidas, incluindo o número de instruções executadas, o CPI médio e, frequentemente, as origens da perda de desempenho. Como a contagem de instruções depende da arquitetura, mas não da implementação exata, podemos medir a contagem de instruções sem conhecer todos os detalhes da implementação. Porém, o CPI depende de diversos detalhes de projeto no computador, incluindo o sistema de memória e a estrutura do processador (conforme veremos nos Capítulos 4 e 5), além da mistura de tipos de instruções executados em uma aplicação. Assim, o CPI varia por aplicação, bem como entre implementações com o mesmo conjunto de instruções. O exemplo anterior mostra o perigo de usar apenas um fator (contagem de instruções) para avaliar o desempenho. Ao comparar dois computadores, você precisa examinar todos os três componentes, que se combinam para formar o tempo de execução. Se alguns dos fatores forem idênticos, como a taxa de clock no exemplo anterior, o desempenho pode ser determinado comparando-se todos os fatores não idênticos. Como o CPI varia por mix de instruções, tanto a contagem de instruções quanto o CPI precisam ser comparados, mesmo que as taxas de clock sejam idênticas. Vários exercícios ao final deste capítulo lhe pedem para avaliar uma série de melhorias de computador e compilador, que afetam a taxa de clock, CPI e contagem de instruções. Na Seção 1.8, examinaremos uma medida de desempenho comum, que não incorpora todos os termos e, portanto, pode ser enganosa.
O desempenho de um programa depende do algoritmo, da linguagem, do compilador, da arquitetura e do hardware real. A tabela a seguir resume como esses componentes afetam os fatores na equação de desempenho da CPU. Componente de hardware ou software
Afeta o quê?
Como?
Algoritmo
Contagem de instruções, possivelmente CPI
O algoritmo determina o número de instruções do programa-fonte executadas e, portanto, o número de instruções de processador executadas. O algoritmo também pode afetar o CPI, favorecendo instruções mais lentas ou mais rápidas. Por exemplo, se o algoritmo utiliza mais operações de ponto flutuante, ele tenderá a ter um CPI mais alto.
Linguagem de programação
Contagem de instruções, CPI
A linguagem de programação certamente afeta a contagem de instruções, pois as instruções na linguagem são traduzidas para instruções de processador, o que determina a contagem de instruções. A linguagem também pode afetar o CPI por causa dos seus recursos; por exemplo, uma linguagem com um suporte intenso para abstração de dados (por exemplo, Java) exigirá chamadas indiretas, que usarão instruções de CPI mais alto.
Componente de hardware ou software
1.5 A barreira da potência 29
Afeta o quê?
Como?
Compilador
Contagem de instruções, CPI
A eficiência do compilador afeta a contagem de instruções e os ciclos médios por instruções, pois o compilador determina a tradução das instruções da linguagem-fonte para instruções do computador. O papel do compilador pode ser muito complexo e afetar o CPI de maneiras complexas.
Arquitetura do conjunto de instruções
Contagem de instruções, taxa de clock, CPI
A arquitetura do conjunto de instruções afeta todos os três aspectos do desempenho da CPU, pois afeta as instruções necessárias para uma função, o custo em ciclos de cada instrução e a taxa de clock geral do processador.
Detalhamento: Embora você possa esperar que o CPI mínimo seja 1,0, conforme veremos no Capítulo 4, alguns processadores buscam e executam múltiplas instruções por ciclo de clock. Para refletir essa técnica, alguns projetistas invertem o CPI para falar sobre IPC, ou instruções por ciclo de clock. Se um processador executa em média duas instruções por ciclo de clock, então ele tem um IPC de 2 e, portanto, um CPI de 0,5.
Determinada aplicação escrita em Java roda por 15 segundos em um processador de desktop. Um novo compilador Java é lançado, exigindo apenas 60% das instruções do compilador antigo. Infelizmente, isso aumenta o CPI por 1,1. Com que velocidade podemos esperar que a aplicação rode usando esse novo compilador? Escolha a resposta certa a partir das três opções a seguir: 15 × 0,6 = 8, 2 seg 1,1 b. 15 × 0,6 × 1,1 = 9,9 seg a.
c.
15 × 1,1 = 27,5 seg 0,6
1.5
A barreira da potência
A Figura 1.15 mostra o aumento na taxa de clock e na potência de oito gerações de microprocessadores Intel durante 25 anos. Tanto a taxa de clock quanto a potência aumentaram rapidamente durante décadas e depois se estabilizaram recentemente. O motivo pelo qual elas cresceram juntas é que estão correlacionadas e o motivo para o seu recuo recente é que chegamos ao limite de potência prático para o resfriamento dos microprocessadores comuns. A tecnologia dominante para circuitos integrados é denominada Complementary Metal Oxide Semiconductor (CMOS). Para CMOS, a principal fonte de dissipação de potência é a chamada potência dinâmica — ou seja, a potência que é consumida durante a comutação. A dissipação da potência dinâmica depende da carga capacitiva de cada transistor, da tensão elétrica aplicada e da frequência com que o transistor é comutado: Potência = Carga capacitiva × Tensão elétrica 2 × Frequência comutada A frequência comutada é uma função da taxa de clock. A carga capacitiva por transistor é uma função do número de transistores conectados a uma saída (chamado de fanout) e da tecnologia, que determina a capacitância dos fios e dos transistores.
Verifique você mesmo
30
Capítulo 1 Abstrações e Tecnologias Computacionais
FIGURA 1.15 Taxa de clock e potência para microprocessadores Intel x86 durante oito gerações e 25 anos. O Pentium 4 fez um salto dramático na taxa de clock e potência, porém menor em desempenho. Os problemas térmicos do Prescott levaram ao abandono da linha Pentium 4. A linha Core 2 retorna a uma pipeline mais simples, com menores taxas de clock e múltiplos processadores por chip.
Como as taxas de clock poderiam crescer por um fator de 1.000 enquanto a potência crescia por um fator apenas de 30? A potência pode ser diminuída reduzindo-se a tensão elétrica, o que ocorreu a cada nova geração da tecnologia, e a potência é uma função da tensão elétrica ao quadrado. Normalmente, a tensão elétrica foi reduzida em 15% por geração. Em 20 anos, as tensões passaram de 5V para 1V, motivo pelo qual o aumento na potência é de apenas 30 vezes.
Potência relativa
EXEMPLO
Suponha que tenhamos desenvolvido um novo processador, mais simples, que tem 85% da carga capacitiva do processador mais antigo e mais complexo. Além do mais, considere que ele tenha tensão ajustável, de modo que pode reduzir a tensão em 15% em comparação com o processador B, o que resulta em um encolhimento de 15% na frequência. Qual é o impacto sobre a potência dinâmica? 2
RESPOSTA
Carga capacitiva × 0,85 × Tensão × 0,85 × Frequência comutada × 0,85 Potência nova = Potência antiga Carga capacitiva × Tensão 2 × Frequência comutada
Assim, a razão de potência é 0,854 = 0,52 Logo, o novo processador usa cerca de metade da potência do processador antigo. O problema hoje é que reduzir ainda mais a tensão parece causar muito vazamento nos transistores, como torneiras de água que não conseguem ser completamente fechadas. Até mesmo hoje, cerca de 40% do consumo de potência é decorrente de vazamentos. Se os transistores começassem a vazar mais, o processo inteiro poderia se tornar incontrolável. Para tentar resolver o problema de potência, os projetistas já conectaram grandes dispositivos a fim de aumentar o resfriamento e depois desligaram partes do chip que não são usadas em determinado ciclo de clock. Embora existam muitas maneiras mais dispendiosas de resfriar os chips e, portanto, aumentar a potência para, digamos, 300 watts, essas técnicas são muito caras para computadores de desktop.
1.6 Mudança de mares: Passando de processadores para multiprocessadores 31
Como os projetistas de computador bateram contra a barreira da potência, eles precisaram de uma nova maneira de prosseguir e escolheram um caminho diferente do modo como projetaram microprocessadores nos seus primeiros 30 anos. Detalhamento: Embora a potência dinâmica seja a principal fonte de dissipação de potência na CMOS, a dissipação de potência estática ocorre devido à corrente de vazamento que flui mesmo quando um transistor está desligado. Conforme já mencionamos, o vazamento normalmente é responsável por 40% do consumo de potência em 2008. Assim, aumentar o número de transistores aumenta a dissipação de potência, mesmo que os transistores estejam sempre desligados. Diversas técnicas de projeto e inovações de tecnologia estão sendo implantadas para controlar o vazamento, mas é difícil reduzir mais a tensão.
Mudança de mares: Passando 1.6 de processadores para multiprocessadores O limite de potência forçou uma mudança dramática no projeto dos microprocessadores. A Figura 1.16 mostra a melhoria no tempo de resposta dos programas para microprocessadores de desktop ao longo dos anos. Desde 2002, a taxa reduziu de um fator de 1,5 por ano para um fator de menos de 1,2 por ano. Em vez de continuar diminuindo o tempo de resposta de um único programa executando num único processador, em 2006 todas as empresas de desktop e servidor estavam usando microprocessadores com múltiplos processadores por chip, em que o benefício normalmente está mais no throughput do que no tempo de resposta. Para reduzir a confusão entre as palavras processador e microprocessador, as empresas se referem aos processadores como “cores” (ou núcleos), e esses microprocessadores são chamados genericamente de microprocessadores “multicore” (ou múltiplos núcleos). Logo, um microprocessador “quadcore” é um chip que contém quatro processadores, ou quatro núcleos. A Figura 1.17 mostra o número de processadores (núcleos), potência e taxas de clock de microprocessadores recentes. O plano de registro oficial para muitas empresas é dobrar o número de núcleos por microprocessador por geração de tecnologia de semicondutor, que é aproximadamente a cada dois anos (veja Capítulo 7). No passado, os programadores podiam contar com inovações no hardware, na arquitetura e nos compiladores para dobrar o desempenho de seus programas a cada 18 meses sem ter de mudar uma linha de código. Hoje, para os programadores obterem uma melhoria significativa no tempo de resposta, eles precisam reescrever seus programas de modo que tirem proveito de múltiplos processadores. Além do mais, para obter o benefício histórico de rodar mais rapidamente nos microprocessadores mais novos, os programadores terão de continuar a melhorar o desempenho de seu código à medida que dobra o número de núcleos. Para reforçar como os sistemas de software e hardware trabalham lado a lado, usamos uma seção especial, Interface hardware/software, no livro inteiro, com a primeira aparecendo logo em seguida. Essas seções resumem ideias importantes nessa interface crítica.
O paralelismo sempre foi fundamental para o desempenho na computação, mas normalmente esteve oculto. O Capítulo 4 explicará sobre o pipelining, uma técnica elegante que roda programas mais rapidamente sobrepondo a execução de instruções. Este é um exemplo de paralelismo em nível de instrução, em que a natureza paralela do hardware é retirada de modo que o programador e o compilador possam pensar no hardware como executando instruções sequencialmente. Forçar os programadores a estarem cientes do hardware paralelo e reescrever explicitamente seus programas para serem paralelos foi a “terceira trilha” da arquitetura de
Até agora, a maior parte do software tem sido como música escrita para um solista; com a geração atual de chips, estamos adquirindo alguma experiência com duetos e quartetos e outros pequenos grupos; mas compor um trabalho para grande orquestra e coro é um tipo de desafio diferente. Brian Hayes, Computing in a Parallel Universe, 2007.
Interface de hardware/ software
32
Capítulo 1 Abstrações e Tecnologias Computacionais
computadores, pois empresas no passado, que dependiam dessa mudança no comportamento, fracassaram (veja Seção 7.14 no site). Do ponto de vista histórico, é surpreendente que a indústria inteira de TI tenha apostado seu futuro em que os programadores finalmente passarão com sucesso para a programação explicitamente paralela.
Por que tem sido tão difícil para os programadores escreverem programas explicitamente paralelos? O primeiro motivo é que a programação paralela é, por definição, programação de desempenho, o que aumenta a dificuldade da programação. Não apenas o programa precisa estar correto, solucionar um problema importante e oferecer uma interface útil às pessoas ou outros programas que o chamam, mas ele também precisa ser rápido. Caso contrário, se você não precisasse de desempenho, bastaria escrever um programa sequencial. O segundo motivo é que rápido, para o hardware paralelo, significa que o programador precisa dividir uma aplicação de modo que cada processador tenha aproximadamente a mesma quantidade de coisas a fazer ao mesmo tempo, e que o overhead do escalonamento e coordenação não afasta os benefícios de desempenho em potencial do paralelismo.
FIGURA 1.16 Crescimento do desempenho do processador desde meados da década de 1980. Este gráfico representa o desempenho relativo ao VAX 11/780 medido pelos benchmarks SPECint (veja Seção 1.8). Antes de meados da década de 1980, o crescimento do desempenho do processador foi em grande parte controlado pela tecnologia e teve uma média de 25% por ano. O aumento no crescimento para cerca de 52% desde então é atribuído a ideias arquiteturais e organizacionais mais avançadas. Por volta de 2002, esse crescimento levou a uma diferença no desempenho por um fator de cerca de sete. O desempenho para cálculos orientados a ponto flutuante aumentou ainda mais rapidamente. Desde 2002, os limites de potência, o paralelismo disponível em nível de instrução e a latência de memória longa reduziram o desempenho do processador recentemente, para cerca de 20% por ano.
FIGURA 1.17 Número de núcleos por chip, taxa de clock e potência para os microprocessadores multicore em 2008.
1.6 Mudança de mares: Passando de processadores para multiprocessadores 33
Como uma analogia, suponha que a tarefa fosse escrever um artigo de jornal. Oito repórteres trabalhando no mesmo artigo poderiam potencialmente escrever um artigo oito vezes mais rápido. Para conseguir essa velocidade aumentada, seria preciso desmembrar a tarefa de modo que cada repórter tivesse algo para fazer ao mesmo tempo. Assim, temos de escalonar as subtarefas. Se algo saísse errado e apenas um repórter levasse mais tempo do que os sete outros levaram, então o benefício de ter oito escritores seria diminuído. Assim, temos de balancear a carga por igual para obter o ganho de velocidade desejado. Outro perigo seria se os repórteres tivessem de gastar muito tempo falando uns com os outros para escrever suas seções. Você também se atrasaria se uma parte do artigo, como a conclusão, não pudesse ser escrita até que todas as outras partes fossem concluídas. Assim, deve-se ter o cuidado para reduzir o overhead de comunicação e sincronização. Para essa analogia e para a programação paralela, os desafios incluem escalonamento, balanceamento de carga, tempo para sincronismo e overhead para comunicação entre as partes. Como você poderia imaginar, o desafio é ainda maior com mais repórteres de um artigo de jornal e mais processadores na programação paralela. Para refletir essa mudança de mares no setor, os próximos cinco capítulos desta edição do livro possuem uma seção sobre as implicações da revolução paralela relacionadas a cada capítulo: j
Capítulo 2, Seção 2.11: Paralelismo e instruções: sincronização. Normalmente, tarefas paralelas independentes às vezes precisam ser coordenadas, como dizer quando elas completaram seu trabalho. Esse capítulo explica as instruções usadas por processadores multicore para sincronizar tarefas.
j
Capítulo 3, Seção 3.6: Paralelismo e aritmética de computador: Associatividade. Em geral, os programadores paralelos começam de um programa sequencial funcionando. Uma questão natural para descobrir se sua versão paralela funciona é: “ela tem a mesma resposta?” Se não, uma conclusão lógica é que existem bugs na nova versão. Essa lógica considera que a aritmética do computador é associativa: você recebe a mesma soma quando adiciona um milhão de números, não importando a ordem. Esse capítulo explica que, embora essa lógica se mantenha para números inteiros, não serve para os números de ponto flutuante.
j
Capítulo 4, Seção 4.10: Paralelismo e paralelismo avançado em nível de instrução. Dada a dificuldade da programação explicitamente paralela, um esforço tremendo foi investido na década de 1990 para que o hardware e o compilador revelassem o paralelismo implícito. Esse capítulo descreve algumas dessas técnicas agressivas, incluindo a busca e a execução simultâneas de múltiplas instruções e a estimativa dos resultados das decisões, com a execução especulativa das instruções.
j
Capítulo 5, Seção 5.8: Paralelismo e hierarquias de memória: coerência do cache. Um modo de reduzir o custo da comunicação é fazer com que todos os processadores usem o mesmo espaço de endereço, de modo que qualquer processador possa ler ou gravar quaisquer dados. Visto que todos os processadores atuais utilizam caches para manter uma cópia temporária dos dados na memória mais rápida, mais próxima do processador, é fácil imaginar que a programação paralela seria ainda mais difícil se os caches associados a cada processador tivessem valores inconsistentes dos dados compartilhados. Esse capítulo descreve os mecanismos que mantêm coerentes os dados em todos os caches.
j
Capítulo 6, Seção 6.9: Paralelismo e E/S: Redundant Arrays of Inexpensive Disks. Se você ignorar a entrada e saída nessa revolução paralela, a consequência não intencionada da programação paralela pode ser fazer com que seu programa paralelo gaste a maior parte do seu tempo esperando pela E/S. Esse capítulo descreve RAID, uma técnica para acelerar o desempenho dos acessos ao armazenamento. RAID assinala outro benefício em potencial do paralelismo: tendo muitas cópias dos recursos, o sistema pode continuar a fornecer serviço apesar de uma falha de um recurso. Logo, RAID pode melhorar tanto o desempenho quanto a disponibilidade da E/S.
34
Capítulo 1 Abstrações e Tecnologias Computacionais
Eu acreditava que [os computadores] seriam uma ideia universalmente aplicável, assim como os livros. Só não imaginava que se desenvolveriam tão rapidamente, pois não pensei que fôssemos capazes de colocar tantas peças em um chip quanto finalmente colocamos. O transistor apareceu inesperadamente. Tudo aconteceu muito mais rápido do que esperávamos.
Além dessas seções, existe um capítulo inteiro sobre processamento paralelo. O Capítulo 7 entra em mais detalhes sobre os desafios da programação paralela; apresenta as duas técnicas contrastantes para a comunicação de endereçamento compartilhado e passagem explícita de mensagens; descreve o modelo restrito de paralelismo que é mais fácil de programar; discute a dificuldade do benchmarking de processadores paralelos; apresenta um novo modelo de desempenho simples para microprocessadores multicore e finalmente descreve e avalia quatro exemplos de microprocessadores multicore usando esse modelo. A partir desta edição do livro, o Apêndice A descreve um componente de hardware cada vez mais comum, que está incluído com os computadores de desktop: a unidade de processamento de gráficos (Graphics Processing Unit – GPU). Inventada para acelerar os gráficos, as GPUs estão se tornando plataformas de programação por si sós. Como você poderia esperar, neste momento, as GPUs são altamente paralelas. O Apêndice A descreve a GPU NVIDIA e realça partes de seu ambiente de programação paralelo.
J. Presper Eckert, coinventor do Eniac, falando em 1991
Vida real: Fabricação e benchmarking
1.7 do AMD Opteron X4
Cada capítulo possui uma seção intitulada “Vida Real”, que associa os conceitos no livro com um computador que você pode usar em seu dia a dia. Essas seções abordam a tecnologia na qual se baseiam os computadores modernos. Nesta primeira “Vida Real”, veremos como os circuitos integrados são fabricados e como o desempenho e a potência são medidos com o AMD Opteron X4 como exemplo. Vamos começar do início. A fabricação de um chip começa com o silício, uma substância encontrada na areia. Como não é um bom condutor de eletricidade, é chamado de semicondutor. Com um processo químico especial, é possível acrescentar ao silício materiais que permitem que minúsculas áreas se transformem em um entre três dispositivos:
silício Um elemento natural que é um semicondutor.
semicondutor Uma substância que não conduz bem a eletricidade.
lingote de cristal de silício Uma barra composta de um cristal de silício que possui entre 15 e 30cm de diâmetro e cerca de 30 a 60cm de comprimento.
wafer Uma fatia de um lingote de silício de não mais de 2,5mm de espessura, usada para criar chips.
defeito Uma imperfeição microscópica em um wafer ou nos passos da aplicação dos padrões que pode resultar na falha do die que contém esse defeito.
j
Excelentes condutores de eletricidade (usando fios microscópicos de cobre ou alumínio)
j
Excelentes isolantes de eletricidade (como cobertura plástica ou vidro)
j
Áreas que podem conduzir ou isolar sob condições especiais (como uma chave)
Os transistores se encaixam na última categoria. Um circuito VLSI, então, simplesmente consiste em bilhões de combinações de condutores, isolantes e chaves, fabricados em um único e pequeno pacote. O processo de fabricação dos circuitos integrados é decisivo para o custo dos chips e, consequentemente, fundamental para os projetistas de computador. A Figura 1.18 mostra esse processo. O processo inicia com um lingote de cristal de silício, que se parece com uma salsicha gigante. Hoje, os lingotes possuem de 20 a 30cm de diâmetro e cerca de 30 a 60cm de comprimento. Um lingote é finamente fatiado em wafers de não mais que 0,25cm de espessura. Esses wafers passam por uma série de etapas de processamento, durante as quais são depositados padrões de elementos químicos em cada lâmina, criando os transistores, os condutores e os isolantes discutidos anteriormente. Os circuitos integrados de hoje contêm apenas uma camada de transistores, mas podem ter de dois a oito níveis de condutor de metal, separados por camadas de isolantes. Uma única imperfeição microscópica no wafer propriamente dito ou em uma das dezenas de passos da aplicação dos padrões pode resultar na falha dessa área do wafer. Esses defeitos, como são chamados, tornam praticamente impossível fabricar um wafer perfeito. Para lidar com a imperfeição, várias estratégias têm sido usadas, mas a mais simples é colocar muitos componentes independentes em um único wafer. O wafer com os padrões é, então, cortado em seções individuais desses componentes, chamados dies, mais informalmente conhecidos como chips. A Figura 1.19 é uma fotografia de um wafer com mi-
1.7 Vida real: Fabricação e benchmarking do AMD Opteron X4 35
FIGURA 1.18 Processo de fabricação de um chip. Após ser fatiado de um lingote de silício, os wafers virgens passam por 20 a 40 passos para criar wafers com padrões (veja a Figura 1.19). Esses wafers com padrões são testados com um testador de wafers e é criado um mapa das partes boas. Depois, os wafers são divididos em dies (moldes) (veja a Figura 1.9). Nessa figura, um wafer produziu 20 dies, dos quais 17 passaram no teste. (X significa que o die está ruim.) O aproveitamento de dies bons nesse caso foi de 17/20, ou 85%. Esses dies bons são soldados a encapsulamentos e testados outra vez antes de serem remetidos para os clientes. Um die encapsulado ruim foi encontrado nesse teste final.
croprocessadores antes de serem cortados; anteriormente, a Figura 1.9 mostrou um die individual do microprocessador e seus principais componentes. Cortar os wafers em seções permite descartar apenas aqueles dies que possuem falhas, em vez do wafer inteiro. Esse conceito é quantificado pelo aproveitamento de um processo, definido como a porcentagem de dies bons do número total de dies em um wafer. O custo de um circuito integrado sobe rapidamente conforme aumenta o tamanho do die, em razão do aproveitamento mais baixo e do menor número de dies que pode caber em um wafer. Para reduzir o custo, um die grande normalmente é “encolhido” usando um processo da próxima geração, que incorpora tamanhos menores de transistores e de fios. Isso melhora o aproveitamento e o número de dies por wafer. Tendo dies bons, eles são conectados aos pinos de entrada/saída de um encapsulamento usando um processo chamado soldagem. Essas peças encapsuladas são testadas uma última vez, já que podem ocorrer erros no encapsulamento, e são remetidas aos clientes. Conforme já foi mencionado, outra limitação de projeto cada vez mais importante é o consumo de energia. O consumo é um problema por duas razões. Primeiro, a corrente precisa ser trazida para o chip e distribuída por toda sua área; os microprocessadores modernos usam centenas de pinos apenas para alimentação e aterramento! Da mesma forma, múltiplos níveis de interconexões são usados unicamente para distribuição de corrente e aterramento para as partes do chip. Segundo, a energia é dissipada como calor e precisa ser removida. Um AMD Opteron X4 modelo 2356 a 2,0 GHz produz 120 watts em 2008, que precisam ser removidos de um chip cuja área de superfície é de apenas 1cm2! Detalhamento: O custo de um circuito integrado pode ser expresso em três equações simples: Custo por die = Dies por wafer = Aproveitamento =
Custo por wafer Dies por wafer × aproveitamento Área do wafer Área do die 1 (1 + (Defeitos por área × Área do die / 2))2
dies As seções retangulares individuais cortadas de um wafer, mas informalmente conhecidos como chips. aproveitamento A porcentagem de dies bons do número total de dies em um wafer.
36
Capítulo 1 Abstrações e Tecnologias Computacionais
FIGURA 1.19 Um wafer de 300mm de diâmetro dos chips AMD Opteron X2, o predecessor dos chips Opteron X4 (Cortesia da AMD). O número de dies de Pentium por wafer em 100% de aproveitamento é 117. As várias dezenas de chips parcialmente arredondados nas bordas do wafer são inúteis; são incluídas porque é mais fácil criar as máscaras usadas para imprimir os padrões desejados ao silício. Esse die usa uma tecnologia de 90 nanômetros, o que significa que os menores transistores possuem um tamanho de aproximadamente 90 nm, embora normalmente sejam um pouco menores do que o tamanho real catalogado, que se refere ao tamanho dos transistores como “desenhados” versus o tamanho final fabricado.
A primeira equação é simples de se derivar. A segunda é uma aproximação, pois não subtrai a área perto da borda do wafer arredondado que não pode acomodar os dies retangulares (veja Figura 1.19). A equação final é baseada nas observações empíricas dos aproveitamentos nas fábricas de circuito integrado, com o expoente relacionado ao número de etapas de processamento crítico. Logo, dependendo da taxa de defeito e do tamanho do die e wafer, os custos geralmente não são lineares em relação à área do die.
Benchmark de CPU SPEC carga de trabalho Um conjunto de programas executados em um computador que é a coleção real das aplicações executadas por um usuário ou construídas a partir de programas reais para aproximar tal mistura. Uma carga de trabalho típica especifica o programa e as frequências relativas.
benchmark Um programa selecionado para uso na comparação do desempenho de computadores.
Um usuário de computador que executa os mesmos programas todos os dias seria o candidato perfeito para avaliar um novo computador. O conjunto de programas executados formaria uma carga de trabalho. Para avaliar dois sistemas, um usuário simplesmente compararia o tempo de execução da carga de trabalho nos dois computadores. A maioria dos usuários, porém, não está nessa situação. Em vez disso, eles precisam contar com outros métodos que medem o desempenho de um computador candidato, esperando que os métodos reflitam como o computador funcionará com a carga de trabalho do usuário. Essa alternativa normalmente é seguida pela avaliação do computador usando um conjunto de benchmarks — programas escolhidos especificamente para medir o desempenho. Os benchmarks formam uma carga de trabalho que o usuário acredita que irá prever o desempenho da carga de trabalho real. O System Performance Evaluation Cooperative (SPEC) é um esforço com patrocínio e suporte de uma série de fornecedores de computador a fim de criar conjuntos padrão de benchmarks para sistemas de computador modernos. Em 1989, o SPEC criou originalmente
1.7 Vida real: Fabricação e benchmarking do AMD Opteron X4 37
FIGURA 1.20 Benchmarks SPECINTC2006 executando no AMD Opteron X4 modelo 2356 (Barcelona). Conforme explica a equação na seção “A equação clássica de desempenho da CPU, anteriormente neste capítulo, o tempo de execução é o produto dos três fatores nesta tabela: contagem de instruções em bilhões, clocks por instrução (CPI) e tempo do ciclo de clock em nanossegundos. SPECratio é simplesmente o tempo de referência, que é fornecido pelo SPEC, dividido pelo tempo de execução medido. O único número mencionado como SPECINTC2006 é a média geométrica dos SPECratios. A Figura 5.40 mostra que mcf, libquantum, omnetpp e xalancbmk possuem CPIs relativamente altos, pois possuem taxas de perda de cache altas.
um conjunto de benchmark focalizando o desempenho do processador (agora chamado SPEC89), que evoluiu por cinco gerações. A mais recente é SPEC CPU2006, que consiste em um conjunto de 12 benchmarks de inteiros (CINT2006) e 17 benchmarks de ponto flutuante (CFP2006). Os benchmarks de inteiros variam desde parte de um compilador C até um programa de xadrez e uma simulação de computador quântico. Os benchmarks de ponto flutuante incluem códigos de grade estruturados para modelagem de elemento finito, códigos de método de partículas para dinâmica molecular e códigos de álgebra linear esparsa para dinâmica de fluidos. A Figura 1.20 descreve os benchmarks de inteiros SPEC e seu tempo de execução no Opteron X4, mostrando os fatores que explicam o tempo de execução: contagem de instruções, CPI e tempo do ciclo de clock. Observe que o CPI varia por um fator de 13. Para simplificar o marketing dos computadores, o SPEC decidiu informar um único número para resumir todos os 12 benchmarks de inteiros. As medidas do tempo de execução são primeiro normalizadas dividindo-se o tempo de execução em um processador de referência pelo tempo de execução no computador medido; essa normalização gera uma medida, chamada SPECratio, que tem a vantagem de que resultados numéricos maiores indicam desempenho melhor (ou seja, o SPECratio é o inverso do tempo de execução). Uma medição de resumo CINT2006 ou CFP2006 é obtida apanhando-se a média geométrica dos SPECratios. Detalhamento: Ao comparar dois computadores usando SPECratios, use a média geométrica, de modo que ela informe a mesma resposta relativa não importa o computador utilizado para normalizar os resultados. Se calculássemos a média dos valores de tempo de execução normalizados com uma média aritmética, os resultados variariam dependendo do computador que escolhêssemos como referência. A fórmula para a média geométrica é n
n
∏ Razão do tempo de execução i =1
i
38
Capítulo 1 Abstrações e Tecnologias Computacionais
FIGURA 1.21 SPECpower_ssj2008 executando no AMD Opteron X4 2345 (Barcelona) a 2,3 GHz e soquete dual com 16 GB de DRAM DDR2-667 e um disco de 500 GB.
em que Razão do tempo de execuçãoi é o tempo de execução, normalizado ao computador de referência, para o i° programa de um total de n na carga de trabalho, e n
∏ a significa oproduto a i
1
× a2 × … × an
i =1
Benchmark de potência SPEC Hoje, o SPEC oferece uma dúzia de conjuntos de benchmark diferentes, projetados para testar uma grande variedade de ambientes de computação usando aplicações reais e regras de execução e requisitos de relatório estritamente especificados. O mais recente é o SPECpower. Ele informa o consumo de potência dos servidores em diferentes níveis de carga de trabalho, dividido em incrementos de 10%, por um período de tempo. A Figura 1.21 mostra os resultados para um servidor usando o Barcelona. SPECpower começou com o benchmark SPEC para aplicações comerciais em Java (SPECJBB2005), que exercita processadores, caches e memória principal, além da máquina virtual Java, compilador, coletor de lixo e partes do sistema operacional. O desempenho é medido no throughput e as unidades são operações de negócios por segundo. Mais uma vez, para simplificar o marketing dos computadores, o SPEC resume esses números em um único número, chamado “ssj_ops geral por Watt”. A fórmula para essa única métrica de resumo é 10 10 ssj_ ops geral por Watt = ∑ ssj_ ops i / ∑ potência i i =0 i =0 em que ssj_opsi é o desempenho em cada incremento de 10% e potênciai é a potência consumida em cada nível de desempenho.
Verifique você Um fator fundamental para determinar o custo de um circuito integrado é o volume de mesmo produção. Quais das seguintes afirmativas são razões para um chip fabricado com alto volume de produção custar menos?
1. Com altos volumes de produção, o processo de fabricação pode ser transformado em um projeto particular, aumentando o aproveitamento. 2. É menos trabalhoso projetar uma peça com alto volume de produção do que uma com baixo volume de produção.
1.8 Falácias e armadilhas 39
3. Como as máscaras usadas para fabricar o chip são caras, o custo por chip é menor para volumes de produção mais altos. 4. Os custos de desenvolvimento de engenharia são altos e quase sempre independem do volume de produção; portanto, o custo de desenvolvimento por die é menor com peças de alto volume de produção. 5. Peças de alto volume de produção normalmente possuem dies menores do que as peças de baixo volume de produção e, portanto, têm um aproveitamento mais alto por wafer.
1.8
A ciência deve começar com os mitos e com a análise dos mitos.
Falácias e armadilhas
A finalidade de uma seção de falácias e armadilhas, que será incluída em cada capítulo, é explicar alguns conceitos errôneos comuns que você pode encontrar. Chamamos esses equívocos de falácias. Quando estivermos discutindo uma falácia, tentaremos fornecer um contraexemplo. Também discutiremos armadilhas ou erros facilmente cometidos. Em geral, as armadilhas são generalizações de princípios verdadeiros em um contexto restrito. O propósito dessas seções é ajudar a evitar esses erros nas máquinas que você pode projetar ou usar. Falácias e armadilhas de custo/desempenho têm confundido muitos arquitetos de computador, incluindo nós. Consequentemente, esta seção não poupa exemplos relevantes. Vamos começar com uma armadilha que engana muitos projetistas e revela um relacionamento importante no projeto de computadores.
Sir Karl Popper, The Philosophy of Science, 1957
Armadilha: Esperar que a melhoria de um aspecto de um computador aumente o desempenho geral por uma quantidade proporcional ao tamanho da melhoria. Essa melhoria tem visitado os projetistas de hardware e de software. Um problema de projeto simples ilustra isso muito bem. Suponha que um programa execute em 100 segundos em um computador, com operações de multiplicação responsáveis por 80 segundos desse tempo. Quanto terei de melhorar a velocidade da multiplicação se eu quiser que meu programa execute cinco vezes mais rápido? O tempo de execução do programa depois de fazer a melhoria é dado pela seguinte equação simples, conhecida como lei de Amdahl: Tempo de execução após o aprimoramento = Tempo de execução afetado pelo aprimoramento + Tempo de execução não afetado Quantidade de aprimoramento Para este problema: Tempo de execução após o aprimoramento =
80 seg + (100 − 80 segundos) n
Como queremos que o desempenho seja cinco vezes mais rápido, o novo tempo de execução deverá ser 20 segundos, gerando 20 seg =
80 seg + 20 seg n
0=
80 seg n
lei de Amdahl Uma regra indicando que a melhoria de desempenho possível com determinado aprimoramento é limitada pela quantidade com que o recurso aprimorado é utilizado. Essa é uma versão quantitativa da lei dos retornos decrescentes.
40
Capítulo 1 Abstrações e Tecnologias Computacionais
Ou seja, não existe quantidade pela qual podemos melhorar a multiplicação para conseguir um aumento quíntuplo no desempenho, se a multiplicação é responsável por apenas 80% da carga de trabalho. A melhoria de desempenho possível com determinado aprimoramento é limitada pela quantidade com que o recurso aprimorado é utilizado. Esse conceito também gera o que chamamos de lei dos retornos decrescentes na vida diária. Podemos usar a lei de Amdahl para estimar os aprimoramentos no desempenho quando sabemos o tempo consumido para alguma função e seu ganho de velocidade em potencial. A lei de Amdahl, junto com a equação de desempenho da CPU, é uma ferramenta prática para avaliar melhorias em potencial. A lei de Amdahl é explorada com mais detalhes nos exercícios. Um tema comum no projeto do hardware é um corolário da lei de Amdahl: torne mais rápido o caso comum. Essa orientação simples nos faz lembrar que, em muitos casos, a frequência com que um evento ocorre pode ser muito mais alta do que a frequência de outro evento. A lei de Amdahl nos lembra que a oportunidade para melhoria é afetada por quanto tempo o evento consome. Assim, tornar o caso comum rápido tenderá a melhorar o desempenho mais do que otimizar o caso raro. Ironicamente, o caso comum normalmente é mais simples do que o caso raro, e, portanto, normalmente é mais fácil de melhorar. A lei de Amdahl também é usada para se demonstrar limites práticos do número de processadores paralelos. Examinamos esse argumento na seção de Falácias e Armadilhas do Capítulo 7. Falácia: Os computadores com pouca utilização demandam menos potência. A eficiência de potência importa em baixas utilizações, pois as cargas de trabalho do servidor variam. A utilização de CPU para os servidores no Google, por exemplo, está entre 10% e 50% na maior parte do tempo e em 100% em menos de 1% do tempo. A Figura 1.22 mostra a potência para os servidores com os melhores resultados do SPECpower em 100% de carga, 50% de carga, 10% de carga e ocioso. Até mesmo os servidores com apenas 10% de utilização se queimam com cerca de dois terços de sua potência máxima. Como as cargas de trabalho dos servidores variam, mas utilizam uma grande fração da potência máxima, Luiz Barroso e Urs Hölzle [2007] argumentam que deveríamos reprojetar o hardware para alcançar a “computação proporcional à energia”. Se os servidores futuros usassem, digamos, 10% da potência máxima a 10% de carga de trabalho, poderíamos reduzir a conta de eletricidade dos centros de dados e nos tornarmos bons cidadãos corporativos em uma era de preocupação crescente com as emissões de CO2.
FIGURA 1.22 Resultados do SPECPower para três servidores com o melhor ssj_ops geral por watt no quarto trimestre de 2007. O ssj_ops geral por watt dos três servidores são 698, 682 e 677, respectivamente. A memória para o dois primeiros servidores é de 16 GB e do último é 8 GB.
Armadilha: Usar um subconjunto da equação de desempenho como uma métrica de desempenho. Já mostramos a falácia de prever o desempenho com base simplesmente na taxa de clock, ou na contagem de instruções ou no CPI. Outro erro comum é usar apenas dois dos três fatores para comparar o desempenho. Embora o uso de dois dos três fatores possa ser válido em um contexto limitado, o conceito facilmente também é mal utilizado. Sem dúvida, quase
1.9 Comentários finais 41
todas as alternativas propostas para o uso do tempo como métrica de desempenho por fim levaram a afirmações enganosas, resultados distorcidos ou interpretações incorretas. Uma alternativa ao tempo é o milhões de instruções por segundo (MIPS). Para determinado programa, o MIPS é simplesmente MIPS =
Contagem de instruções Tempo de execução × 106
Como MIPS é uma taxa de execução de instruções, MIPS especifica o desempenho inversamente ao tempo de execução; computadores mais rápidos possuem uma taxa de MIPS mais alta. A boa notícia sobre MIPS é que ele é fácil de entender e computadores mais rápidos significam um MIPS maior, que corresponde à intuição. Existem três problemas com o uso do MIPS como uma medida para comparar computadores. Primeiro, MIPS especifica a taxa de execução de instruções, mas não leva em conta as capacidades das instruções. Não podemos comparar computadores com diferentes conjuntos de instruções usando MIPS, pois as contagens de instruções certamente serão diferentes. Segundo, MIPS varia entre os programas no mesmo computador; assim, um computador não pode ter uma única avaliação MIPS. Por exemplo, substituindo o tempo de execução, vemos o relacionamento entre MIPS, taxa de clock e CPI: MIPS =
milhões de instruções por segundo (MIPS) Uma medida da velocidade de execução do programa baseada no número de milhões de instruções. MIPS é calculado como a contagem de instruções dividida pelo produto do tempo de execução e 106.
Contagem de instruções Taxa de clock = Contagem de instruções × CPI 6 CPI × 106 × 10 Taxa de clock
Lembre-se de que o CPI variou em 13× para SPEC2006 no Opteron X4, de modo que o MIPS também varia. Finalmente, e mais importante, se um novo programa executa mais instruções, mas cada instrução é mais rápida, o MIPS pode variar independentemente do desempenho! Considere as seguintes medidas de desempenho para um programa: Medida
Computador A
Computador B
10 bilhões
8 bilhões
4 GHz
4 GHz
1,0
1,1
Número de instruções Taxa de clock CPI
Verifique você mesmo
a. Que computador tem a avaliação MIPS mais alta? b. Qual computador é mais rápido?
1.9 Comentários finais Embora seja difícil prever exatamente o nível de custo/desempenho que os computadores terão no futuro, é seguro dizer que serão muito melhores do que são hoje. Para participar desses avanços, os projetistas e programadores de computador precisam entender várias questões. Os projetistas de hardware e de software constroem sistemas computacionais em camadas hierárquicas; cada camada inferior oculta seus detalhes do nível acima. Esse princípio de abstração é fundamental para compreender os sistemas computacionais atuais, mas isso não significa que os projetistas podem se limitar a conhecer uma única tecnologia. Talvez o exemplo mais importante de abstração seja a interface entre hardware e software de baixo
Enquanto o Eniac é equipado com 18.000 válvulas e pesa 30 toneladas, os computadores no futuro poderão ter 1.000 válvulas e talvez pesar apenas 1,5 tonelada. Popular Mechanics, março de 1949
42
Capítulo 1 Abstrações e Tecnologias Computacionais
nível, chamada arquitetura do conjunto de instruções. Manter a arquitetura do conjunto de instruções como uma constante permite que muitas implementações dessa arquitetura – provavelmente variando em custo e desempenho – executem software idêntico. No lado negativo, a arquitetura pode impedir a introdução de inovações que exijam a mudança da interface. Existe um método confiável para determinar e informar o desempenho usando o tempo de execução dos programas reais como métrica. Esse tempo de execução está relacionado a outras medições importantes que podemos fazer pela seguinte equação: Segundos Instruções Ciclos de clock Segundos = × × Programa Programa Instrução Ciclo de clock Usaremos essa equação e seus fatores constituintes muitas vezes. Lembre-se, porém, de que individualmente os fatores não determinam o desempenho: somente o produto, que é igual ao tempo de execução, é uma medida confiável do desempenho.
em
Colocando perspectiva
O tempo de execução é a única medida válida e incontestável do desempenho. Muitas outras métricas foram propostas e desapareceram. Às vezes, essas métricas possuem falhas desde o início, não refletindo o tempo de execução; outras vezes, uma métrica que é válida em um contexto limitado é estendida e usada além desse contexto ou sem o esclarecimento adicional necessário para torná-la válida.
As tecnologias vitais para os processadores modernos são os compiladores e o silício. De igual importância para uma compreensão da tecnologia de circuito integrado é o conhecimento das taxas de mudança tecnológica esperadas. Enquanto o silício impulsiona o rápido avanço do hardware, novas ideias na organização dos computadores melhoraram seu custo/desempenho. Duas das principais ideias são a exploração do paralelismo no programa, normalmente por meio de processadores múltiplos, e a exploração da localidade dos acessos a uma hierarquia de memória, em geral por meio de caches. A potência substituiu a área do die como o recurso mais crítico do projeto de microprocessadores. Conservar energia enquanto se tenta aumentar o desempenho tem forçado o setor de hardware a passar para microprocessadores multicore, forçando, assim, o setor de software a passar para a programação do hardware em paralelo. Os projetos de computadores sempre foram medidos pelo custo e desempenho, além de outros fatores importantes, como potência, confiabilidade, custo de proprietário e escalabilidade (ou facilidade de expansão). Embora este capítulo tenha focalizado o custo, o desempenho e a potência, os melhores projetos buscarão o equilíbrio apropriado para determinado mercado entre todos esses fatores.
Mapa para este livro Na base dessas abstrações estão os cinco componentes clássicos de um computador: caminho de dados, controle, memória, entrada e saída (veja novamente a Figura 1.4). Esses cinco componentes também servem de estrutura para os demais capítulos do livro: j
Caminho de dados: Capítulos 3, 5, 7 e Apêndice A
j
Controle: Capítulos 4, 7 e Apêndice A
j
Memória: Capítulo 5
j
Entrada: Capítulo 6
j
Saída: Capítulo 6
1.11 Exercícios 43
Como dissemos, o Capítulo 4 descreve como os processadores exploram o paralelismo implícito; o Capítulo 7 descreve os microprocessadores multicore explicitamente paralelos, que estão no núcleo da revolução paralela; e o Apêndice A descreve o chip de processador gráfico altamente paralelo. O Capítulo 5 descreve como a hierarquia de memória explora a localidade. O Capítulo 2 descreve os conjuntos de instruções – a interface entre os compiladores e a máquina – e destaca o papel dos compiladores e das linguagens de programação ao usar os recursos do conjunto de instruções. O Apêndice B oferece uma referência para o conjunto de instruções do Capítulo 2. O Capítulo 3 descreve como os computadores realizam operações aritméticas. O Apêndice C, no site, apresenta o projeto lógico.
1.10
Perspectiva histórica e leitura adicional
Para cada capítulo, há uma seção dedicada à perspectiva histórica que pode ser encontrada no site que acompanha este livro. Podemos traçar o desenvolvimento de uma ideia por meio de uma série de máquinas ou descrever alguns projetos importantes; e fornecemos referências, caso você esteja interessado em pesquisar mais a fundo. Esta perspectiva histórica desse capítulo fornece uma base para algumas das principais ideias apresentadas neste capítulo de abertura. Sua finalidade é apresentar a história humana por trás dos avanços tecnológicos e colocar as realizações dentro de seu contexto histórico. Entendendo o passado, você pode compreender melhor as forças que formarão a computação no futuro. Cada seção de perspectiva histórica no site termina com sugestões para leitura adicional, que também são coletadas separadamente no site na seção “Further Reading”. O restante da Seção 1.10 está no site.
1 1.11 Exercícios
A maioria dos exercícios nesta edição é projetada de modo que apresente uma descrição qualitativa com o apoio de uma tabela que oferece parâmetros quantitativos alternativos. Esses parâmetros são necessários para solucionar as perguntas que compreendem o exercício. Perguntas individuais podem ser solucionadas usando-se qualquer um ou todos os parâmetros — você decide quantos dos parâmetros deverão ser considerados para qualquer pergunta do exercício. Por exemplo, é possível dizer “complete a Pergunta 4.1.1 usando os parâmetros dados na linha A da tabela”. Como alternativa, os instrutores podem personalizar esses exercícios para criar novas soluções, substituindo os parâmetros indicados pelos seus próprios valores exclusivos. As avaliações do tempo relativo à solução dos exercícios são mostradas entre colchetes após cada número de exercício. Em média, um exercício avaliado em [10] levará o dobro do tempo de um avaliado em [5]. As seções do texto, que devem ser lidas antes de resolver um exercício, serão indicadas entre sinais de maior e menor; por exemplo, <1.3> significa que você deve ler a Seção 1.3, “Sob as tampas”, para ajudar a resolver esse exercício.
Exercício 1.1 Encontre a palavra ou frase da seguinte lista que melhor corresponde à descrição nas questões a seguir. Use os números à esquerda das palavras na resposta. Cada resposta deve ser usada apenas uma vez. 1
Contribuição de Javier Bruguera da Universidade de Santiago de Compostela.
Um campo ativo da ciência é como um imenso formigueiro; a pessoa quase desaparece na massa de mentes afundando umas sobre as outras, carregando informações de um lugar para outro, passando-as adiante na velocidade da luz. Lewis Thomas, “Natural Science”, em The Lives of a Cell, 1974
44
Capítulo 1 Abstrações e Tecnologias Computacionais
1.
mundos virtuais
14.
sistema operacional
2.
computadores desktop
15.
compilador
3.
servidores
16.
bit
4.
servidores inferiores
17.
instrução
5.
supercomputadores
18.
linguagem assembly
6.
terabyte
19.
linguagem de máquina
7.
petabyte
20.
C
8.
centros de dados
21.
assembler
9.
computadores embutidos
22.
linguagem de alto nível
10.
processadores multicore
23.
software do sistema
11.
VHDL
24.
software de aplicação
12.
RAM
25.
cobol
13.
CPU
26.
fortran
1.1.1 [2] <1.1> Computador usado para executar grandes problemas e normalmente acessado por meio de uma rede 1.1.2 [2] <1.1> 1015 ou 250 bytes 1.1.3 [2] <1.1> Computador composto de centenas a milhares de processadores e terabytes de memória 1.1.4 [2] <1.1> Aplicação atual da ficção científica que provavelmente estará disponível no futuro próximo 1.1.5 [2] <1.1> Um tipo de memória chamada memória de acesso aleatório 1.1.6 [2] <1.1> Parte de um computador, chamada unidade central de processamento 1.1.7 [2] <1.1> Milhares de processadores formando um grande cluster 1.1.8 [2] <1.1> Um microprocessador contendo vários processadores no mesmo chip 1.1.9 [2] <1.1> Computador desktop sem a tela ou teclado, normalmente acessado por uma rede 1.1.10 [2] <1.1> Atualmente a maior classe de computador que executa uma aplicação ou um conjunto de aplicações relacionadas 1.1.11 [2] <1.1> Linguagem especial usada para descrever componentes de hardware 1.1.12 [2] <1.2> Computador pessoal que oferece bom desempenho para usuários isolados a um baixo custo 1.1.13 [2] <1.2> Programa que traduz instruções na linguagem de alto nível para a linguagem assembly 1.1.14 [2] <1.2> Programa que traduz instruções simbólicas para instruções binárias 1.1.15 [2] <1.2> Linguagem de alto nível para processamento de dados comerciais 1.1.16 [2] <1.2> Linguagem binária que o processador pode entender
1.11 Exercícios 45
1.1.17 [2] <1.2> Comandos que os processadores entendem 1.1.18 [2] <1.2> Linguagem de alto nível para computação científica 1.1.19 [2] <1.2> Representação simbólica das instruções de máquina 1.1.20 [2] <1.2> Interface entre o programa do usuário e o hardware, oferecendo uma série de serviços e funções de supervisão 1.1.21 [2] <1.2> Software/programas desenvolvidos pelos usuários 1.1.22 [2] <1.2> Dígito binário (valor 0 ou 1) 1.1.23 [2] <1.2> Camada de software entre o software de aplicação e o hardware, que inclui o sistema operacional e os compiladores 1.1.24 [2] <1.2> Linguagem de alto nível usada para escrever software de aplicação e de sistemas 1.1.25 [2] <1.2> Linguagem portável, composta de palavras e expressões algébricas, que precisa ser traduzida para a linguagem assembly antes de ser executada em um computador 1.1.26 [2] <1.2> 1012 ou 240 bytes
Exercício 1.2 Considere as diferentes configurações mostradas na tabela seguinte. a. b.
Configuração
Resolução
Memória Principal
Rede Ethernet
1
640 x 480
2 GB
100 Mbit
2
1280 x 1024
4 GB
1 Gbit
1
1024 x 768
2 GB
100 Mbit
2
2560 x 1600
4 GB
1 Gbit
1.2.1 [10] <1.3> Para uma tela colorida usando 8 bits para cada uma das cores primárias (vermelho, verde, azul) por pixel e com uma resolução de 1280 × 800 pixels, qual deve ser o tamanho (em bytes) do buffer de frame a fim de armazenar um frame? 1.2.2 [5] <1.3> Se um computador tem uma memória principal de 2 GB, quantos frames ele poderia armazenar, supondo que a memória não contém outra informação? 1.2.3 [5] <1.3> Se um arquivo de 256 KB for enviado por uma rede Ethernet, quanto tempo levará para chegar? Para os problemas abaixo, utilize as informações da tabela abaixo para o tempo de acesso para cada tipo de memória. Cache
DRAM
Memória Flash
Disco magnético
a.
5 ns
50 ns
5 ms
5 ms
b.
7 ns
70 ns
15 ms
20 ms
1.2.4 [5] <1.3> Descubra quanto tempo é necessário para ler um arquivo de uma memória DRAM se a memória cache demora 2 microssegundos para isso. 1.2.5 [5] <1.3> Descubra quanto tempo é necessário para ler um arquivo de um disco magnético se a memória cache demora 2 microssegundos para isso.
46
Capítulo 1 Abstrações e Tecnologias Computacionais
1.2.6 [5] <1.3> Descubra quanto tempo é necessário para ler um arquivo de uma memória flash se a memória cache demora 2 microssegundos para isso.
Exercício 1.3 Considere os três diferentes processadores P1, P2 e P3 executando o mesmo conjunto de instruções com as taxas de clock e CPIs dadas na tabela a seguir. Processador
Taxa de clock
CPI
P1
3 GHz
1,5
P2
2,5 GHz
1,0
P3
4 GHz
2,2
P1
2 GHz
1,2
P2
3 GHz
0,8
P3
4 GHz
2,0
a.
b.
1.3.1 [5] <1.4> Qual processador possui o desempenho mais rápido expressado pelas instruções por segundo? 1.3.2 [10] <1.4> Se cada processador executa um programa em 10 segundos, encontre o número de ciclos e o número de instruções. 1.3.3 [10] <1.4> Ao tentar reduzir o tempo em 30%, a CPI aumenta em 20%. Qual a taxa de clock que deve ser utilizada para a redução de tempo? Para os problemas abaixo, utilize as informações da tabela seguinte. Processador
Taxa de clock
Número de instruções
P1
3 GHz
20 x 109
7s
P2
2,5 GHz
30 x 109
10 s
P3
4 GHz
90 x 109
9s
P1
2 GHz
20 x 109
5s
P2
3 GHz
30 x 109
8s
P3
4 GHz
25 x 109
7s
a.
b.
Tempo
1.3.4 [10] <1.4> Ache instruções por ciclos (IPC) para cada processador. 1.3.5 [5] <1.4> Ache a taxa de clock para P2 que reduz o tempo de execução para o P1. 1.3.6 [5] <1.4> Ache o número de instruções para P2 que reduz o tempo de execução para o P3.
Exercício 1.4 Considere duas implementações diferentes da mesma arquitetura do conjunto de instruções. Existem quatro classes de instruções: A, B, C e D. A taxa de clock e o CPI de cada implementação são dados na tabela a seguir. Processador
Taxa de clock
CPI Classe A
CPI Classe B
CPI Classe C
CPI Classe D
P1
1,5 GHz
1
2
3
4
P2
2 GHz
2
2
2
2
1.4.1 [10] <1.4> Dado um programa com 106 instruções divididas em classes das seguintes formas: 10% classe A, 20% classe B, 50% classe C e 20% classe D, que implementação é mais rápida?
1.11 Exercícios 47
1.4.2 [5] <1.4> Qual é o CPI global para cada implementação? 1.4.3 [5] <1.4> Ache os ciclos de clock exigidos nos dois casos. A tabela a seguir mostra o número de instruções para um programa. Aritmética
Store
Load
Desvio
Total
500
50
100
50
700
1.4.4 [5] <1.4> Considerando que as instruções aritméticas levam 1 ciclo, load e store 5 ciclos e desvio 2 ciclos, qual é o tempo de execução do programa em um processador de 2 GHz? 1.4.5 [5] <1.4> Ache o CPI para o programa. 1.4.6 [10] <1.4> Se o número de instruções de carga puder ser reduzido pela metade, qual é o ganho de velocidade e o CPI?
Exercício 1.5 Considere duas implementações diferentes, P1 e P2, do mesmo conjunto de instruções. Existem cinco classes de instruções (A, B, C, D e E) no conjunto de instruções. A taxa de clock e o CPI de cada classe são dados a seguir. Taxa de clock a. b.
CPI Classe A
CPI Classe B
CPI Classe C
CPI Classe D
CPI Classe E
P1
1,0 GHz
1
2
3
4
3
P2
1,5 GHz
2
2
2
4
4
P1
1,0 GHz
1
1
2
3
2
P2
1,5 GHz
1
2
3
4
3
1.5.1 [5] <1.4> Suponha que o desempenho de pico seja definido como a taxa mais rápida que um computador pode executar qualquer sequência de instruções. Quais são os desempenhos de pico de P1 e P2 expressos em instruções por segundo? 1.5.2 [5] <1.4> Se o número de instruções executadas em um certo programa for dividido igualmente entre as classes de instruções exceto, para a classe A, que ocorre com o dobro da frequência das outras, qual computador é o mais rápido? O quanto ele é mais rápido? 1.5.3 [5] <1.4> Se o número de instruções executadas em um certo programa é dividido igualmente entre as classes de instruções, exceto para a classe E, que ocorre com o dobro da frequência das outras, qual computador é o mais rápido? O quanto ele é mais rápido? A tabela a seguir mostra o desmembramento de tipo de instrução para diferentes programas. Usando esses dados, você estará explorando as opções de desempenho com diferentes mudanças feitas em um processador MIPS. Número de Instruções Cálculo
Load
Store
Desvio
Total
a.
Programa 1
1000
400
100
50
1550
b.
Programa 4
1500
300
100
100
2000
1.5.4 [5] <1.4> Supondo que os cálculos usem 1 ciclo, instruções de load e store usem 10 ciclos e desvios usem 3 ciclos, ache o tempo de execução de cada programa em um processador MIPS de 3 GHz.
48
Capítulo 1 Abstrações e Tecnologias Computacionais
1.5.5 [5] <1.4> Supondo que os cálculos usem 1 ciclo, instruções de load e store usem 2 ciclos e desvios usem 3 ciclos, ache o tempo de execução de cada programa em um processador MIPS de 3 GHz. 1.5.6 [5] <1.4> Supondo que os cálculos usem 1 ciclo, instruções de load e store usem 2 ciclos e desvios usem 3 ciclos, qual é o ganho de velocidade de um programa se o número de instruções de cálculo puder ser reduzido pela metade?
Exercício 1.6 Os compiladores podem ter um impacto profundo sobre o desempenho de uma aplicação em determinado processador. Este problema explorará o impacto que os compiladores têm sobre o tempo de execução. a. b.
CPI Classe A
CPI Classe B
CPI Classe C
CPI Classe D
CPI Classe E
P1
1
2
3
4
5
P2
3
3
3
5
5
P1
1
2
3
4
5
P2
2
2
2
2
6
1.6.1 [5] <1.4> Para o mesmo programa, dois compiladores diferentes são utilizados. Essa tabela mostra o tempo de execução dos dois programas compilados diferentes. Ache o CPI médio para cada programa dado que o processador tem um tempo de ciclo de clock de 1 nS. 1.6.2 [5] <1.4> Considere os CPIs médios encontrados em 1.6.1, mas que os programas compilados executem em dois processadores diferentes. Se os tempos de execução nos dois processadores forem os mesmos, o quão mais rápido é o clock do processador rodando o código do compilador A versus o clock do processador rodando o código do compilador B? 1.6.3 [5] <1.4> Um novo compilador é desenvolvido, usando apenas 600 milhões de instruções, e tendo um CPI médio de 1,1. Qual é o ganho de velocidade do uso desse novo compilador versus o uso do Compilador A ou B no processador original de 1.6.1? Considere duas implementações diferentes, P1 e P2, do mesmo conjunto de instruções. Existem cinco classes de instruções (A, B, C, D e E) no conjunto de instruções. P1 tem uma taxa de clock de 4 GHz, e P2 tem uma taxa de clock de 6 GHz. Os números médios de ciclos para cada classe de instruções para P1 e P2 são listados na tabela a seguir.
a.
b.
Classe
CPI em P1
CPI em P2
A
1
2
B
2
2
C
3
2
D
4
4
E
5
4
Classe
CPI em P1
CPI em P2
A
1
2
B
1
2
C
1
2
D
4
4
E
5
4
1.11 Exercícios 49
1.6.4 [5] <1.4> Suponha que o desempenho de pico seja definido como a taxa mais rápida que um computador pode executar qualquer sequência de instruções. Quais são os desempenhos de pico de P1 e P2 expressos em instruções por segundo? 1.6.5 [5] <1.4> Se o número de instruções executadas em um certo programa for dividido igualmente entre as classes de instruções no Problema 2.36.4, exceto para a classe A, que ocorre com o dobro da frequência das outras, o quão mais rápido é P2 em relação a P1? 1.6.6 [5] <1.4> Em que frequência, P2 tem o mesmo desempenho de P1 para o mix de instruções dado em 1.6.5?
Exercício 1.7 A tabela a seguir mostra o aumento na taxa de clock e potência de oito gerações de processadores Intel durante 28 anos. Processador
Taxa de clock
Potência
80286 (1982)
12,5 MHz
3,3 W
80386 (1985)
16 MHz
4,1 W
80486 (1989)
25 MHz
4,9 W 10,1 W
Pentium (1993)
66 MHz
Pentium Pro (1997)
200 MHz
29,1 W
Pentium 4 Willamette (2001)
2 GHz
75,3 W
Pentium 4 Prescott (2004)
3,6 GHz
103 W
Core 2 Ketsfield (2007)
2,667 GHz
95 W
1.7.1 [5] <1.5> Qual é a média geométrica das razões entre as gerações consecutivas para taxa de clock e potência? (A média geométrica é descrita na Seção 1.7.) 1.7.2 [5] <1.5> Qual é a maior mudança relativa na taxa de clock e potência entre as gerações? 1.7.3 [5] <1.5> O quão maior é a taxa de clock e potência da última geração com relação à primeira geração? Considere os valores a seguir para a voltagem em cada geração. Processador
Tensão
80286 (1982)
5
80386 (1985)
5
80486 (1989)
5
Pentium (1993)
5
Pentium Pro (1997)
3,3
Pentium 4 Willamette (2001)
1,75
Pentium 4 Prescott (2004)
1,25
Core 2 Ketsfield (2007)
1,1
1.7.4 [5] <1.5> Ache a média das cargas capacitivas, considerando um consumo de energia estática desprezível. 1.7.5 [5] <1.5> Ache a maior mudança relativa em tensão entre as gerações. 1.7.6 [5] <1.5> Ache a média geométrica das razões de tensão nas gerações desde o Pentium.
50
Capítulo 1 Abstrações e Tecnologias Computacionais
Exercício 1.8 Suponha que tenhamos desenvolvido novas versões de um processador com as características a seguir. Versão
Tensão
Taxa de clock
a.
Versão 1
1,75 V
1,5 GHz
Versão 2
1,2 V
2 GHz
b.
Versão 1
1,1 V
3 GHz
Versão 2
0,8 V
4 GHz
1.8.1 [5] <1.5> Em quanto foi reduzida a carga capacitiva entre as versões se a potência dinâmica foi reduzida em 10%? 1.8.2 [5] <1.5> Em quanto foi reduzida a potência dinâmica se a carga capacitiva não muda? 1.8.3 [5] <1.5> Supondo que a carga capacitiva da versão 2 é 80% da carga capacitiva da versão 1, ache a voltagem para a versão 2 se a potência dinâmica da versão 2 for reduzida em 40% a partir da versão 1. Supondo que as tendências da indústria mostrem que a geração de um novo processo se expanda da seguinte forma: Capacitância
Tensão
Taxa de clock
Área
a.
1
1/21/2
1,15
1/21/2
b.
1
1/21/4
1,2
1/21/4
1.8.4 [5] <1.5> Encontre o fator de expansão para a potência dinâmica. 1.8.5 [5] <1.5> Encontre a expansão da capacitância por área unitária. 1.8.6 [5] <1.5> Assumindo que um processador Core 2 com a taxa de clock de 2,667 GHz, um poder de consumo de 95 W e uma tensão de 1,1 V, encontre a tensão e a taxa de clock do processador para sua próxima geração de processamento.
Exercício 1.9 Embora a potência dinâmica seja a principal fonte de dissipação de energia na CMOS, a corrente de vazamento produz uma dissipação de potência estática V × Ivazamento. Quanto menores as dimensões no chip, mais significativa é a potência estática. Considere os valores mostrados na tabela a seguir para a dissipação de potência estática e dinâmica para várias gerações de processadores. Tecnologia
Potência dinâmica (W)
Potência estática (W)
Tensão (V)
a.
180 nm
50
10
1,2
b.
70 nm
90
60
0,9
1.9.1 [5] <1.5> Ache a porcentagem da potência total dissipada compreendida por potência estática. 1.9.2 [5] <1.5> Se a potência total dissipada for reduzida em 10% enquanto mantém a estática para a taxa de potência total do problema 1.9.1, quanto a tensão deve ser reduzida para que a corrente de vazamento continue igual?
1.11 Exercícios 51
1.9.3 [5] <1.5> Determine a razão entre potência estática e potência dinâmica para cada tecnologia. Considere agora a dissipação de potência dinâmica de diferentes versões de um processador para três diferentes tensões dadas na tabela seguinte. 1,2 V
1,0 V
0,8 V
a.
75 W
60 W
35 W
b.
62 W
50 W
30 W
1.9.4 [5] <1.5> Determine a potência estática para cada versão em 0,8 V, considerando uma razão entre potência estática e dinâmica de 0,6. 1.9.5 [5] <1.5> Determine a dissipação das potências estática e dinâmica utilizando as taxas obtidas no problema 1.9.1. 1.9.6 [10] <1.5> Determine a média geométrica das variações de potência entre as versões.
Exercício 1.10 A tabela a seguir mostra o desmembramento de tipo de instrução de determinada aplicação executada em 1, 2, 4 ou 8 processadores. Usando esses dados, você estará explorando o ganho de velocidade das aplicações em processadores paralelos. Processadores
a.
CPI
Aritmética
Load/ Store
Desvio
Aritmética
Load/ Store
Desvio
1
2560
1280
256
1
4
2
2
1280
640
128
1
4
2
4
640
320
64
1
4
2
8
320
160
32
1
4
2
Processadores
b.
# Instruções por processador
# Instruções por processador
CPI
Aritmética
Load/ Store
Desvio
Aritmética
Load/ Store
Desvio
1
2560
1280
256
1
4
2
2
1350
800
128
1
6
2
4
800
600
64
1
9
2
8
600
500
32
1
13
2
1.10.1 [5] <1.4, 1.6> A tabela apresentada mostra o número de instruções exigido por processador para completar um programa em um multiprocessador com 1, 2, 4 ou 8 processadores. Qual é o número total de instruções executadas por processador? Qual é o número agregado de instruções executadas por todos os processadores? 1.10.2 [5] <1.4, 1.6> Dados os valores de CPI à direita da tabela, ache o tempo de execução total para esse programa em 1, 2, 4 e 8 processadores. Considere que cada processador tem uma frequência de clock de 2 GHz. 1.10.3 [5] <1.4, 1.6> Se o CPI das instruções aritméticas fosse dobrado, qual seria o impacto sobre o tempo de execução do programa em 1, 2, 4 ou 8 processadores? A tabela a seguir mostra o número de instruções por núcleo de processador em um processador multicore, além do CPI médio para executar o programa em 1, 2, 4 ou 8 núcleos.
52
Capítulo 1 Abstrações e Tecnologias Computacionais
Usando esses dados, você estará explorando o ganho de velocidade das aplicações em processadores multicore. Núcleos por processador
Instruções por núcleo
CPI médio
1
1,00E + 10
1,2
2
5,00E + 09
1,3
4
2,50E + 09
1,5
8
1,25E + 09
1,8
Núcleos por processador
Instruções por núcleo
CPI médio
1
1,00E + 10
1,2
2
5,00E + 09
1,2
4
2,50E + 09
1,2
8
1,25E + 09
1,2
a.
b.
1.10.4 [10] <1.4, 1.6> Considerando uma frequência de clock de 3 GHz, qual é o tempo de execução do programa usando 1, 2, 4 ou 8 núcleos? 1.10.5 [10] <1.5, 1.6> Suponha que o consumo de potência do núcleo de um processador possa ser descrito pela equação a seguir Potência =
5mA Tensão2 MHz
em que a tensão de operação do processador é descrita pela equação a seguir 1 Tensão = Frequência + 0, 4 5 com a frequência medida em GHz. Assim, a 5 GHz, a tensão seria 1,4 V. Ache o consumo de potência do programa executando em 1, 2, 4 e 8 núcleos, supondo que cada núcleo esteja operando a uma frequência de clock de 3 GHz. De modo semelhante, ache o consumo de potência do programa executando em 1, 2, 4 ou 8 núcleos supondo que cada núcleo esteja operando a 500 MHz. 1.10.6 [10] <1.5, 1.6> Ao utilizar um único núcleo, encontre o CPI requerido para o núcleo conseguir o tempo de execução igual ao tempo obtido ao utilizar o número de núcleos da tabela acima (vezes de execução do problema 1.10.4). Repare que o número de instruções deve ser o número agregado de instruções executadas entre todos os núcleos.
Exercício 1.11 A tabela a seguir mostra os dados de manufatura para diversos processadores. Diâmetro do wafer
Dies por wafer
Defeitos por área unitária
Custo por wafer
a.
15 cm
84
0,020 defeitos/cm2
12
b.
20 cm
100
0,031 defeitos/cm2
25
1.11.1 [10] <1.7> Ache o aproveitamento. 1.11.2 [5] <1.7> Ache o custo por die. 1.11.3 [10] <1.7> Se o número de dies por wafer for aumentado em 10% e os defeitos por unidade de área aumentar em 15%, ache a área do die e o aproveitamento.
1.11 Exercícios 53
Suponha que, com a evolução da tecnologia de manufatura dos dispositivos eletrônicos, o aproveitamento varie como mostra a tabela a seguir. aproveitamento
T1
T2
T3
T4
0,85
0,89
0,92
0,95
1.11.4 [10] <1.7> Ache o número de defeitos por unidade de área para cada tecnologia, dada uma área de die de 200 mm2. 1.11.5 [5] <1.7> Represente graficamente a variação do aproveitamento junto com a variação dos defeitos por área unitária.
Exercício 1.12 A tabela a seguir mostra os resultados para os programas de benchmark SPE CPU2006 rodando em um AMD Barcelona. Nome
Contagem instruções × 109
Tempo de execução (seg)
Tempo de referência (seg)
a.
bzip2
2389
750
9650
b.
go
1658
700
10.4090
1.12.1 [5] <1.7> Descubra o CPI se o tempo de ciclo de clock for 0,333 ns. 1.12.2 [5] <1.7> Ache a razão SPEC. 1.12.3 [5] <1.7> Para esses dois benchmarks, encontre a média geométrica. A tabela a seguir mostra dados para outros benchmarks. Nome
CPI
Taxa de clock
SPECratio
a.
libquantum
1,61
4 GHz
19,8
b.
astar
1,79
4 GHz
9,1
1.12.4 [5] <1.7> Ache o aumento em tempo de CPU se o número de instruções do benchmark for aumentado em 10% sem afetar o CPI. 1.12.5 [5] <1.7> Encontre o aumento no tempo de CPU se o número de instruções do benchmark aumentar em 10% e o CPI for aumentado em 5%. 1.12.6 [5] <1.7> Ache a mudança na SPECratio para a mudança descrita em 1.12.5.
Exercício 1.13 Suponha que estejamos desenvolvendo uma nova versão do processador AMD Barcelona com uma taxa de clock de 4 GHz. Acrescentamos algumas instruções ao conjunto de instruções, de modo que o número de instruções foi reduzido em 15% a partir dos valores mostrados para cada benchmark no Exercício 1.12. Os tempos de execução obtidos aparecem na tabela a seguir. Nome
Tempo de execução (seg)
Tempo de referência (seg)
SPECratio
a.
bzip2
700
9.650
13,7
b.
go
620
10.490
16,9
54
Capítulo 1 Abstrações e Tecnologias Computacionais
1.13.1 [10] <1.8> Ache o novo CPI. 1.13.2 [10] <1.8> Em geral, esses valores de CPI são maiores que aqueles obtidos nos exercícios anteriores para os mesmos benchmarks. Isso é decorrente principalmente da taxa de clock usada nos dois casos, 3 GHz e 4 GHz. Determine se o aumento no CPI é semelhante ao da taxa de clock. Se eles forem diferentes, por que isso ocorre? 1.13.3 [5] <1.8> Por quanto o tempo de CPU foi reduzido? A tabela a seguir mostra dados para outros benchmarks. Nome
Tempo de execução (seg)
CPI
Taxa de clock
a.
libquantum
960
1,61
3 GHz
b.
astar
690
1,79
3 GHz
1.13.4 [10] <1.8> Se o tempo de execução for reduzido por outros 10% sem afetar o CPI e com uma taxa de clock de 4 GHz, determine o número de instruções. 1.13.5 [10] <1.8> Determine a taxa de clock exigida para gerar uma redução de mais 10% no tempo de CPU enquanto mantém o número de instruções e CPI inalterados. 1.13.6 [10] <1.8> Determine a taxa de clock se o CPI for reduzido em 15% e o tempo de CPU em 20% enquanto o número de instruções for inalterado.
Exercício 1.14 A Seção 1.8 cita como armadilha a utilização de um subconjunto da equação de desempenho como uma métrica de desempenho. Para ilustrar isso, considere os dados a seguir para a execução de determinada sequência de 106 instruções em diferentes processadores. Processador
Taxa de clock
CPI
P1
4 GHz
1,25
P2
3 GHz
0,75
1.14.1 [5] <1.8> Uma falácia comum é considerar o computador com a maior taxa de clock como tendo o maior desempenho. Verifique se isso é verdade para P1 e P2. 1.14.2 [10] <1.8> Outra falácia é considerar que o processador executando o maior número de instruções precisará de um tempo de CPU maior. Considerando que o processador P1 está executando uma sequência de 106 instruções e que o CPI dos processadores P1 e P2 não muda, determine o número de instruções que P2 pode executar ao mesmo tempo em que P1 precisa para executar 106 instruções. 1.14.3 [10] <1.8> Uma falácia comum é usar milhões de instruções por segundo (MIPS) para comparar o desempenho de dois processadores diferentes e considerar que o processador com o maior valor de MIPS tem o maior desempenho. Verifique se isso é verdade para P1 e P2. Outro valor de desempenho comum é milhões de operações de ponto flutuante por segundo (MFLOPS), definido como MFLOPS =
Nº de operações de PF (Tempo de execução × 106 )
1.11 Exercícios 55
mas esse valor tem os mesmos problemas do MIPS. Considere os programas na tabela a seguir, rodando nos dois processadores a seguir. Número de instruções
a. b.
CPI
Taxa de clock
Processador
Cont. instruções
L/S
PF
Desvio
L/S
PF
P1
1 x 106
50%
40%
10%
0,75
1,0
1,5
4 GHz
P2
5 x 106
40%
40%
20%
1,25
0,8
1,25
3 GHz
P1
5 × 106
30%
30%
40%
1,5
1,0
2,0
4 GHz
P2
2 x 106
40%
30%
30%
1,25
1,0
2,5
3 GHz
Desvio
1.14.4 [10] <1.8> Encontre os valores de MFLOPS para os programas. 1.14.5 [10] <1.8> Ache os valores de MIPS para os programas. 1.14.6 [10] <1.8> Ache o desempenho para os programas e compare com MIPS e MFLOPS.
Exercício 1.15 Outra armadilha citada na Seção 1.8 é esperar aprimorar o desempenho geral de um computador melhorando apenas um aspecto do computador. Isso pode ser verdade, mas nem sempre é. Considere um computador rodando programas com os tempos de CPU mostrados na tabela a seguir.
Instruções PF
Instruções INT
Instruções L/S
Instruções desvio
Tempo total
a.
70 s
85 s
55 s
40 s
250 s
b.
40 s
90 s
60 s
20 s
210 s
1.15.1 [5] <1.8> Em quanto é reduzido o tempo total se o tempo para as operações de PF for reduzido em 20%? 1.15.2 [5] <1.8> Em quanto o tempo para operações INT é reduzido se o tempo total for reduzido em 20%? 1.15.3 [5] <1.8> O tempo total pode ser reduzido em 20% reduzindo-se apenas o tempo para as instruções de desvio? A tabela a seguir mostra o desmembramento de tipo de instrução por processador de determinada aplicação executada em diferentes números de processadores.
Processadores
Instruções PF
Instruções INT
Instruções L/S
Instruções desvio
CPI (PF)
CPI (INT)
CPI (L/S)
CPI (Desvio)
a.
2
280 × 106
1000 × 106
640 × 106
128 × 106
1
1
4
2
b.
16
50 × 106
110 × 106
80 × 106
16 × 106
1
1
4
2
Considere que cada processador tenha uma taxa de clock de 2 GHz. 1.15.4 [10] <1.8> Por quanto devemos melhorar o CPI das instruções de PF se quisermos que o programa execute duas vezes mais rápido? 1.15.5 [10] <1.8> Por quanto devemos melhorar o CPI das instruções de L/S se quisermos que o programa execute duas vezes mais rápido?
56
Capítulo 1 Abstrações e Tecnologias Computacionais
1.15.6 [5] <1.8> Por quanto o tempo de execução do programa é melhorado se o CPI das instruções de INT e PF for reduzido em 40% e o CPI das instruções de L/S e desvio for reduzido em 30%?
Exercício 1.16 Outra armadilha, relacionada à execução dos programas em sistemas multiprocessadores, é esperar melhoria no desempenho aprimorando apenas o tempo de execução de parte das rotinas. A tabela a seguir mostra o tempo de execução de cinco rotinas de um programa rodando em diferentes quantidades de processadores.
N° processadores
Rotina A (ms)
Rotina B (ms)
Rotina C (ms)
Rotina D (ms)
Rotina E (ms)
a.
4
12
45
6
36
3
b.
32
2
7
1
6
2
1.16.1 [10] <1.8> Ache o tempo de execução total e em quanto ele é reduzido se o tempo das rotinas A, C e E for melhorado em 15%. 1.16.2 [10] <1.8> Em quanto o tempo total é reduzido se a rotina B for melhorada em 10%? 1.16.3 [10] <1.8> Em quanto o tempo total é reduzido se a rotina D for melhorada em 10%? O tempo de execução em um sistema multiprocessador pode ser dividido em tempo de computação para as rotinas mais tempo de roteamento gasto enviando dados de um processador para outro. Considere o tempo de execução e o tempo de roteamento dados na tabela a seguir. Nesse caso, o tempo de roteamento é um componente importante do tempo total. N° processadores
Rotina A (ms)
Rotina B (ms)
Rotina C (ms)
Rotina D (ms)
Rotina E (ms)
Roteamento (ms)
2
40
78
9
70
4
11
4
29
60
4
36
2
13
8
15
45
3
19
3
17
16
7
35
1
11
2
22
32
4
23
1
6
1
23
64
2
12
0,5
3
1
26
1.16.4 [10] <1.8> Para cada duplicação do número de processadores determine a razão do tempo de computação novo para antigo e a razão do tempo de roteamento novo para antigo. 1.16.5 [5] <1.8> Usando a média geométrica das razões extrapole para descobrir o tempo de computação e o tempo de roteamento em um sistema com 128 processadores. 1.16.6 [10] <1.8> Ache o tempo de computação e o tempo de roteamento para um sistema com um processador.
1.11 Exercícios 57
§1.1: Questões para discussão: muitas respostas são aceitáveis. §1.3: Memória em disco: não volátil, tempo de acesso longo (milissegundos), e custo de US$0,20 a US$2,00/GB. Memória usando semicondutores: volátil, tempo de acesso curto (nanossegundos) e custo de US$20 a US$75/GB. §1.4: 1.a: ambos, b: latência, c: nenhum. 2,7 segundos. §1.4: b. §1.7: 1, 3 e 4 são motivos válidos. A resposta 5 geralmente pode ser verdadeira, pois o alto volume pode tornar o investimento extra para reduzir o tamanho do die em, digamos, 10%, uma boa decisão econômica, mas isso não precisa ser verdadeiro. §1.8: 53: a. Computador A tem a maior avaliação MIPS. b. Computador B é mais rápido.
Respostas das Seções “Verifique você mesmo”
2 Eu falo espanhol com Deus, italiano com as mulheres, francês com os homens e alemão com meu cavalo. Charles V, rei da França 1337-1380
Instruções: A Linguagem de Máquina 2.1 Introdução 60 2.2
Operações do hardware do computador 62
2.3
Operandos do hardware do computador 63
2.4
Números com sinal e sem sinal 69
2.5
Representando instruções no computador 74
2.6
Operações lógicas 80
2.7
Instruções para tomada de decisões 83
2.8
Suporte a procedimentos no hardware do computador 88
2.9
Comunicando-se com as pessoas 97
2.10
Endereçamento no MIPS para operandos imediatos e endereços de 32 bits 101
2.11
Paralelismo e instruções: Sincronização 109
2.12
Traduzindo e iniciando um programa 111
2.13
Um exemplo de ordenação em C para juntar tudo isso 119
2.14 Arrays versus ponteiros 127 2.15
Material avançado: Compilando C e interpretando Java 130
2.16
Vida real: instruções do ARM 130
2.17
Vida real: instruções do x86 134
2.18
Falácias e armadilhas 141
2.19
Comentários finais 143
2.20
Perspectiva histórica e leitura adicional 145
2.21 Exercícios 145
Os cinco componentes clássicos de um computador
60
Capítulo 2 Instruções: A Linguagem de Máquina
2.1 Introdução conjunto de instruções O vocabulário dos comandos entendidos por uma determinada arquitetura.
Para controlar o hardware de um computador é preciso falar sua linguagem. As palavras da linguagem de um computador são chamadas instruções e seu vocabulário é denominado conjunto de instruções. Neste capítulo você verá o conjunto de instruções de um computador real, tanto na forma escrita pelos humanos quanto na forma lida pelo computador. Apresentamos as instruções em um padrão top-down. Começando com uma notação parecida com uma linguagem de programação restrita; nós a refinamos passo a passo, até que você veja a linguagem real de um computador. O Capítulo 3 continua nossa descida, expondo a representação dos números inteiros e de ponto flutuante e o hardware que os opera. Você poderia pensar que as linguagens dos computadores fossem tão diversificadas quanto as dos humanos, mas, na realidade, as linguagens de computador são muito semelhantes, mais parecidas com dialetos regionais do que linguagens independentes. Logo, quando você aprender uma, será fácil entender as outras. Essa semelhança ocorre porque todos os computadores são construídos a partir de tecnologias de hardware baseadas em princípios básicos semelhantes e porque existem algumas operações básicas que todos os computadores precisam oferecer. Além do mais, os projetistas de computador possuem um objetivo comum: encontrar uma linguagem que facilite o projeto do hardware e do compilador enquanto maximiza o desempenho e minimiza o custo. Esse objetivo é antigo; a citação a seguir foi escrita antes que você pudesse comprar um computador e é tão verdadeira hoje quanto era em 1947: É fácil ver, por métodos lógicos formais, que existem certos [conjuntos de instruções] que são adequados para controlar e causar a execução de qualquer sequência de operações... As considerações realmente decisivas, do ponto de vista atual, na seleção de um [conjunto de instruções], são mais de natureza prática: a simplicidade do equipamento exigido pelo [conjunto de instruções] e a clareza de sua aplicação para os problemas realmente importantes, junto com a velocidade com que tratam esses problemas. Burks, Goldstine e von Neumann, 1947
conceito de programa armazenado A ideia de que as instruções e os dados de muitos tipos podem ser armazenados na memória como números, levando ao computador de programa armazenado.
A “simplicidade do equipamento” é uma consideração tão valiosa para os computadores da década iniciada no ano 2000 quanto foi para os da década de 1950. O objetivo deste capítulo é ensinar um conjunto de instruções que siga esse conselho, mostrando como ele é representado no hardware e o relacionamento entre as linguagens de programação de alto nível e essa linguagem mais primitiva. Nossos exemplos estão na linguagem de programação C; a Seção 2.15 no site mostra como esses exemplos mudariam para uma linguagem orientada a objetos, como Java. Aprendendo como representar as instruções, você também descobrirá o segredo da computação: o conceito de programa armazenado. Além disso, exercitará suas habilidades com “linguagem estrangeira”, escrevendo programas na linguagem do computador e executando-os no simulador que acompanha o livro. Você também verá o impacto das linguagens de programação e das otimizações do compilador sobre o desempenho. Concluímos com uma visão da evolução histórica dos conjuntos de instruções e uma visão geral dos outros dialetos do computador. O conjunto de instruções escolhido vem da MIPS Technology, que é um exemplo elegante dos conjuntos de instruções criados desde a década de 1980. Mais adiante, veremos dois outros conjuntos de instruções populares. ARM é muito semelhante ao MIPS e mais de três bilhões de processadores ARM foram entregues em dispositivos embutidos em 2008. O outro exemplo, o Intel x86, está dentro de quase todos os 330 milhões de PCs fabricados em 2008.
2.1 Introdução 61
Revelamos o conjunto de instruções do MIPS aos poucos, mostrando o raciocínio juntamente com as estruturas do computador. Esse tutorial passo a passo entrelaça os componentes com suas explicações, tornando a linguagem de máquina mais fácil de digerir. A Figura 2.1 oferece uma prévia do conjunto de instruções abordado neste capítulo.
FIGURA 2.1 Assembly do MIPS revelado no Capítulo 2. As partes destacadas mostram o que foi introduzido nas Seções 2.8 e 2.9
62
Capítulo 2 Instruções: A Linguagem de Máquina
Certamente é preciso haver instruções para realizar as operações aritméticas fundamentais. Burks, Goldstine e von Neumann, 1947
2.2 Operações do hardware do computador Todo computador precisa ser capaz de realizar aritmética. A notação em assembly do MIPS add a, b, c
instrui um computador a somar as duas variáveis b e c para colocar sua soma em a. Essa notação é rígida no sentido de que cada instrução aritmética do MIPS realiza apenas uma operação e sempre precisa ter exatamente três variáveis. Por exemplo, suponha que queiramos colocar a soma das variáveis b, c, d e e na variável a. (Nesta seção, estamos sendo deliberadamente vagos com relação ao que é uma “variável”; na próxima seção, vamos explicar com detalhes.) Esta sequência de instruções soma as quatro variáveis: add a,b,c
# A soma b + c é colocada em a.
add a,a,d
# A soma b + c + d agora está em a.
add a,a,e
# A soma b + c + d + e agora está em a.
Portanto, são necessárias três instruções para somar quatro variáveis. As palavras à direita do símbolo (#) em cada linha acima são comentários para o leitor humano, e os computadores os ignoram. Note que como não é o caso comum em linguagens de programação, cada linha desta linguagem pode conter, no máximo, uma instrução. Outra diferença para a linguagem C é que comentários sempre terminam no final da linha. O número natural de operandos para uma operação como a adição é três: os dois números sendo somados e um local para colocar a soma. Exigir que cada instrução tenha exatamente três operações, nem mais nem menos, está de acordo com a filosofia de manter o hardware simples: o hardware para um número variável de operandos é mais complicado do que o hardware para um número fixo. Essa situação ilustra o primeiro dos quatro princípios básicos de projeto do hardware: Princípio de Projeto 1: Simplicidade favorece a regularidade. Agora podemos mostrar, nos dois exemplos a seguir, o relacionamento dos programas escritos nas linguagens de programação de mais alto nível com os programas nessa notação mais primitiva.
Compilando duas instruções de atribuição C no MIPS
EXEMPLO
Este segmento de um programa em C contém as cinco variáveis a, b, c, d e e. Como o Java evoluiu a partir da linguagem C, este exemplo e os próximos funcionam para qualquer uma dessas linguagens de programação de alto nível: a = b + c; d = a − e;
A tradução de C para as instruções em linguagem assembly do MIPS é realizada pelo compilador. Mostre o código do MIPS produzido por um compilador.
RESPOSTA
Uma instrução MIPS opera com dois operandos de origem e coloca o resultado em um operando de destino. Logo, as duas instruções simples anteriores são compiladas diretamente nessas duas instruções em assembly do MIPS: add a,b,c sub d,a,e
2.3 Operandos do hardware do computador 63
Compilando uma atribuição C complexa no MIPS
Uma instrução um tanto complexa contém as cinco variáveis f, g, h, i e j: f = (g + h) − (i + j);
EXEMPLO
O que um compilador C poderia produzir? O compilador precisa desmembrar essa instrução em várias instruções assembly, pois somente uma operação é realizada por instrução MIPS. A primeira instrução MIPS calcula a soma de g e h. Temos de colocar o resultado em algum lugar, de modo que o compilador crie uma variável temporária, chamada t0: add t0,g,h
RESPOSTA
# variável temporária t0 contém g + h
Embora a próxima operação seja subtrair, precisamos calcular a soma de i e j antes de podermos subtrair. Assim, a segunda instrução coloca a soma de i e j em outra variável temporária criada pelo compilador, chamada t1: add t1,i,j # variáveltemporária t1 contém i + j
Finalmente, a instrução de subtração subtrai a segunda soma da primeira e coloca a diferença na variável f, completando o código compilado: sub f,t0,t1 # f recebe t0 − t1,que é(g + h) − (i + j)
Para determinada função, que linguagem de programação provavelmente utiliza mais linhas de código? Coloque as três representações a seguir em ordem. 1. Java 2. C 3. Assembly do MIPS Detalhamento: para aumentar a portabilidade, Java foi idealizada originalmente contando com um interpretador de software. O conjunto de instruções desse interpretador é chamado Seção 2.15 no site), que é muito diferente do conjunto de instruções do bytecode Java (veja a MIPS. Para chegar a um desempenho próximo ao programa em C equivalente, os sistemas Java de hoje normalmente compilam os bytecodes Java para os conjuntos de instruções nativos, como MIPS. Como essa compilação em geral é feita muito mais tarde do que para programas C, esses compiladores Java normalmente são denominados compiladores Just-In-Time (JIT – na hora exata). A Seção 2.12 mostra como os JITs são usados mais tarde que os compiladores C no processo de inicialização e a Seção 2.13 mostra as consequências no desempenho de compilar versus interpretar programas Java.
2.3 Operandos do hardware do computador Ao contrário dos programas nas linguagens de alto nível, os operandos das instruções aritméticas são restritos, precisam ser de um grupo limitado de locais especiais, embutidos diretamente no hardware, chamados registradores. Os registradores são primitivas usadas no projeto do hardware que também são visíveis ao programador quando o computador
Verifique você mesmo
64
Capítulo 2 Instruções: A Linguagem de Máquina
palavra (word) A unidade de acesso natural de um computador, normalmente um grupo de 32 bits; corresponde ao tamanho de um registrador na arquitetura MIPS.
é completado. O tamanho de um registrador na arquitetura MIPS é de 32 bits; os grupos de 32 bits ocorrem com tanta frequência que recebem o nome de palavra (word) na arquitetura MIPS. Uma diferença importante entre as variáveis de uma linguagem de programação e os registradores é o número limitado de registradores, normalmente 32 nos computadores atuais, como o MIPS. (Consulte a Seção 2.20 no site para ver a história do número de registradores.) Assim, continuando em nossa evolução passo a passo da representação simbólica da linguagem MIPS, nesta seção, incluímos a restrição de que cada um dos três operandos das instruções aritméticas do MIPS precisa ser escolhido a partir de um dos 32 registradores de 32 bits. O motivo para o limite dos 32 registradores pode ser encontrado no segundo dos quatro princípios de projeto básicos da tecnologia de hardware: Princípio de Projeto 2: Menor significa mais rápido. Uma quantidade muito grande de registradores pode aumentar o tempo do ciclo do clock simplesmente porque os sinais eletrônicos levam mais tempo quando precisam atravessar uma distância maior. Orientações como “menor significa mais rápido” não são absolutas; 31 registradores podem não ser mais rápidos do que 32. Mesmo assim, a verdade por trás dessas observações faz com que os projetistas de computador as levem a sério. Nesse caso, o projetista precisa equilibrar o desejo dos programas por mais registradores com o desejo do projetista de manter o ciclo de clock rápido. Outro motivo para não usar mais de 32 é o número de bits que seria necessário no formato da instrução, como demonstra a Seção 2.5. O Capítulo 4 mostra o papel central que os registradores desempenham na construção do hardware; como veremos neste capítulo, o uso eficaz dos registradores é fundamental para o desempenho do programa. Embora pudéssemos simplesmente escrever instruções usando números para os registradores, de 0 a 31, a convenção do MIPS é usar nomes com um sinal de cifrão seguido por dois caracteres para representar um registrador. A Seção 2.8 explicará os motivos por trás desses nomes. Por enquanto, usaremos $s0, $s1... para os registradores que correspondem às variáveis dos programas em C e Java, e $t0, $t1... para os registradores temporários necessários para compilar o programa nas instruções MIPS.
Compilando uma atribuição em C usando registradores
EXEMPLO
É tarefa do compilador associar variáveis do programa aos registradores. Considere, por exemplo, a instrução de atribuição do nosso exemplo anterior: f = (g + h) − (i + j);
As variáveis f, g, h, i e j são associadas aos registradores $s0, $s1, $s2, $s3 e $s4, respectivamente. Qual é o código MIPS compilado?
RESPOSTA
O programa compilado é muito semelhante ao exemplo anterior, exceto que substituímos as variáveis pelos nomes dos registradores mencionados anteriormente, mais dois registradores temporários, $t0 e $t1, que correspondem às variáveis temporárias de antes: add $t0,$s1,$s2 # registrador $t0 contém g + h add $t1,$s3,$s4 # registrador $t1 contém i + j sub $s0,$t0,$t1 # f recebe $t0 − $t1,que é(g + h) − (i + j)
2.3 Operandos do hardware do computador 65
Operandos em memória As linguagens de programação possuem variáveis simples, que contêm elementos de dados isolados, como nesses exemplos, mas também possuem estruturas de dados mais complexas. Essas estruturas de dados complexas podem conter muito mais elementos de dados do que a quantidade de registradores em um computador. Logo, como um computador pode representar e acessar estruturas tão grandes? Lembre-se dos cinco componentes de um computador, apresentados no Capítulo 1 e desenhados no início deste capítulo. O processador só pode manter uma pequena quantidade de dados nos registradores, mas a memória do computador contém milhões de elementos de dados. Logo, as estruturas de dados (arrays e estruturas) são mantidas na memória. Conforme explicamos, as operações aritméticas só ocorrem com registradores nas instruções MIPS; assim, o MIPS precisa incluir instruções que transferem dados entre a memória e os registradores. Essas instruções são denominadas instruções de transferência de dados. Para acessar uma palavra na memória, a instrução precisa fornecer o endereço de memória. A memória é apenas uma sequência grande e unidimensional, com o endereço atuando como índice para esse array, começando de 0. Por exemplo, na Figura 2.2, o endereço do terceiro elemento de dados é 2 e o valor de Memória[2] é 10.
instrução de transferência de dados Um comando que move dados entre a memória e os registradores.
endereço Um valor usado para delinear o local de um elemento de dados específico dentro de uma sequência da memória.
FIGURA 2.2 Endereços de memória e conteúdo da memória nesses locais. Se esses elementos fossem palavras, esses endereços estariam incorretos, pois o MIPS, na realidade, usa endereços de bytes, com cada palavra representando quatro bytes. A Figura 2.3 mostra o endereçamento para palavras sequenciais na memória.
A instrução de transferência de dados que copia dados da memória para um registrador tradicionalmente é chamada de load. O formato da instrução load é o nome da operação seguido pelo registrador a ser carregado, depois uma constante e o registrador usado para acessar a memória. A soma da parte constante da instrução com o conteúdo do segundo registrador forma o endereço da memória. O nome MIPS real para essa instrução é lw, significando load word (carregar palavra).
Compilando uma atribuição quando um operando está na memória
Vamos supor que A seja uma sequência de 100 palavras e que o compilador tenha associado as variáveis g e h aos registradores $s1 e $s2, como antes. Vamos supor também que o endereço inicial da sequência, ou endereço base, esteja em $s3. Compile esta instrução de atribuição em C: g = h + A[8];
EXEMPLO
66
Capítulo 2 Instruções: A Linguagem de Máquina
RESPOSTA
Embora haja uma única operação nessa instrução de atribuição, um dos operandos está na memória, de modo que primeiro precisamos transferir A[8] para um registrador. O endereço desse elemento da sequência é a soma da base da sequência A, encontrada no registrador $s3, com o número para selecionar o elemento 9. Os dados devem ser colocados em um registrador temporário, para uso na próxima instrução. Com base na Figura 2.2, a primeira instrução compilada é lw $t0, 8($s3) # Registrador temporário $t0 recebe A[8]
(A seguir, faremos um pequeno ajuste nessa instrução, mas usaremos essa versão simplificada por enquanto.) A seguinte instrução pode operar sobre o valor em $t0 (que é igual a A[8]), já que está em um registrador. A instrução precisa somar h (contido em $s2) com A[8] ($t0) e colocar a soma no registrador correspondente a g (associado a $s1): add $s1,$s2,$t0 # g = h + A[8]
A constante na instrução de transferência de dados (8) é chamada de offset e o registrador acrescentado para formar o endereço ($s3) é chamado de registrador base.
Interface hardware/ software
restrição de alinhamento Um requisito de que os dados estejam alinhados na memória em limites naturais.
Além de associar variáveis a registradores, o compilador aloca estruturas de dados, como arrays e estruturas, em locais na memória. O compilador pode, então, colocar o endereço inicial apropriado nas instruções de transferência de dados. Como os bytes de 8 bits são úteis em muitos programas, a maioria das arquiteturas endereça bytes individuais. Portanto, o endereço de uma palavra combina os endereços dos 4 bytes dentro da palavra. Logo, os endereços sequenciais das palavras diferem em quatro vezes. Por exemplo, a Figura 2.3 mostra os endereços MIPS reais para a Figura 2.2; o endereço em bytes da terceira palavra é 8. No MIPS, palavras precisam começar em endereços que sejam múltiplos de 4. Esse requisito é denominado restrição de alinhamento e muitas arquiteturas o têm. (O Capítulo 4 explica por que o alinhamento ocasiona transferências de dados mais rápidas.) Os computadores se dividem naqueles que utilizam o endereço do byte mais à esquerda, ou big end, como endereço da palavra e aqueles que utilizam o byte mais à direita, ou little end. O MIPS está no campo do Big Endian. (O Apêndice A mostra as duas opções para numerar os bytes de uma palavra.) O endereçamento em bytes também afeta o índice do array. Para obter o endereço em bytes apropriado no código anterior, o offset a ser somado ao registrador base $s3 precisa
FIGURA 2.3 Endereços de memória do MIPS e conteúdo da memória para essas palavras. A mudança de endereços está destacada para comparar com a Figura 2.2. Como o MIPS endereça cada byte, endereços de palavras são múltiplos de 4: existem 4 bytes em uma palavra.
2.3 Operandos do hardware do computador 67
ser 4 × 8, ou 32, de modo que o endereço de load selecione A[8], e não A[8/4]. (Veja a armadilha relacionada na Seção 2.18.) A instrução complementar ao load tradicionalmente é chamada de store; ela copia dados de um registrador para a memória. O formato de um store é semelhante ao de um load: o nome da operação, seguido pelo registrador a ser armazenado, depois o offset para selecionar o elemento do array e finalmente o registrador base. Mais uma vez, o endereço MIPS é especificado, em parte, por uma constante e, em parte, pelo conteúdo de um registrador. O nome real no MIPS é SW, significando store word (armazena palavra).
Compilando com load e store
Suponha que a variável h esteja associada ao registrador $s2 e o endereço base do array A esteja em $s3. Qual é o código assembly do MIPS para a instrução de atribuição em C a seguir?
EXEMPLO
A[12]=h+A[8];
Embora haja uma única operação na instrução em C, agora dois dos operandos estão na memória, de modo que precisamos de ainda mais instruções MIPS. As duas primeiras instruções são iguais às do exemplo anterior, exceto que, desta vez, usamos o offset apropriado para o endereçamento do byte na instrução load word a fim de selecionar A[8], e a instrução add coloca a soma em $t0:
RESPOSTA
lw $t0, 32($s3) # Registrador temporário $t0 recebe A[8] add $t0,$s2,$t0 # Registrador temporário $t0 recebe h + A[8]
A instrução final armazena a soma em A[12], usando 48 (4 × 12) como offset e o registrador $s3 como registrador base. sw $t0, 48($s3) # Armazena h + A[8] de volta em A[12]
Load word e store word são as instruções que copiam words entre memória e registradores na arquitetura MIPS. Outras marcas de computadores utilizam outras instruções juntamente com load e store para transferir dados. Uma arquitetura com essas alternativas é a Intel x86, descrita na Seção 2.17.
Muitos programas possuem mais variáveis do que os computadores possuem registradores. Consequentemente, o compilador tenta manter as variáveis usadas com mais frequência nos registradores e coloca o restante na memória, usando loads e stores para mover variáveis entre os registradores e a memória. O processo de colocar as variáveis menos utilizadas (ou aquelas necessárias mais adiante) na memória é chamado de spilled registers (ou registradores derramados). O princípio de hardware relacionando tamanho e velocidade sugere que a memória deve ser mais lenta que os registradores, pois existem menos registradores. Isso realmente acontece; os acessos aos dados são mais rápidos se os dados estiverem nos registradores, ao invés de estarem na memória. Além do mais, os dados são mais úteis quando em um registrador. Uma instrução aritmética MIPS pode ler dois registradores, operar sobre eles e escrever o resultado. Uma
Interface hardware/ software
68
Capítulo 2 Instruções: A Linguagem de Máquina
instrução de transferência de dados MIPS só lê um operando ou escreve um operando, sem operar sobre ele. Assim, os registradores MIPS levam menos tempo para serem acessados e possuem maior vazão do que a memória – uma combinação rara –, tornando os dados nos registradores mais rápidos de acessar e mais simples de usar. Para conseguir o melhor desempenho, os compiladores precisam usar os registradores de modo eficaz.
Constantes ou operandos imediatos Muitas vezes, um programa usará uma constante em uma operação – por exemplo, ao incrementar um índice a fim de apontar para o próximo elemento de um array. Na verdade, mais da metade das instruções aritméticas do MIPS possuem uma constante como operando quando executam os benchmarks SPEC2006. Usando apenas as instruções vistas até aqui, teríamos de ler uma constante da memória para utilizá-la. (As constantes teriam de ser colocadas na memória quando o programa fosse carregado.) Por exemplo, para somar a constante 4 ao registrador $s3, poderíamos usar o código lw $t0, AddrConstant4($s1)
# $t0 = constante 4
add $s3,$s3,$t0
# $s3 = $s3 + $t0($t0 == 4)
supondo que AddrConstant4 seja o endereço de memória da constante 4. Uma alternativa que evita a instrução load é oferecer versões das instruções aritméticas em que o operando seja uma constante. Essa instrução add rápida, com uma constante no lugar do operando, é chamada add imediato, ou addi. Para somar 4 ao registrador $s3, simplesmente escrevemos addi
$s3,$s3, 4
# $s3 = $s3 + 4
As instruções imediatas ilustram o terceiro princípio de projeto do hardware, mencionado inicialmente na Seção “Falácias e armadilhas”, do Capítulo 1: Princípio de Projeto 3: Agilize os casos mais comuns. Os operandos constantes ocorrem com frequência e, incluindo constantes dentro das instruções aritméticas, as operações são muito mais rápidas e usam menos energia do que se as constantes fossem lidas da memória. A constante zero tem outro emprego, que é simplificar o conjunto de instruções por oferecer variações utéis. Por exemplo, a operação mova é apenas uma instrução de soma na qual cada operando é zero. Portanto, o MIPS dedica o registrador $zero para ter o valor zero. (Como você deve esperar, é o registrador zero.)
Verifique você mesmo
Dada a importância dos registradores, qual é a taxa de aumento no número de registradores em um chip com o passar do tempo? 1. Muito rápida: eles aumentam tão rapidamente quanto a Lei de Moore, o que prevê o dobro do número de transistores em um chip a cada 18 meses. 2. Muito lenta: como os programas normalmente são distribuídos em linguagem de máquina, existe uma inércia na arquitetura do conjunto de instruções, e, por isso, o número de registradores aumenta apenas quando novos conjuntos de instruções se tornam viáveis. Detalhamento: Embora os registradores MIPS neste livro tenham 32 bits de largura, existe uma versão de 64 bits do conjunto de instruções MIPS, definido com 32 registradores de 64 bits. Para distingui-los, eles são chamados oficialmente de MIPS-32 e MIPS-64. Neste capítulo, usamos Apêndice E mostra as diferenças entre MIPS-32 e MIPS-64. um subconjunto do MIPS-32. O
2.4 Números com sinal e sem sinal 69
O endereçamento formado pelo registrador-base mais o offset do MIPS é uma combinação excelente para as estruturas e os arrays, pois o registrador pode apontar para o início da estrutura, e o offset pode selecionar o elemento desejado. Veremos esse exemplo na Seção 2.13. O registrador nas instruções de transferência de dados foi criado originalmente para manter o índice do array com o offset utilizado para o endereço inicial do array. Assim, o registrador-base também é chamado registrador índice. As memórias de hoje são muito maiores e o modelo de software para alocação de dados é mais sofisticado, de modo que o endereço-base do array normalmente é passado em um registrador, pois não caberá no offset, conforme veremos. Como o MIPS admite constantes negativas, a subtração imediata não é necessária no MIPS.
2.4 Números com sinal e sem sinal Primeiro, vamos revisar rapidamente como um computador representa números. Os humanos são ensinados a pensar na base 10, mas os números podem ser representados em qualquer base. Por exemplo, 123 base 10 = 1111011 base 2. Os números são mantidos no hardware do computador como uma série de sinais eletrônicos altos e baixos, e por isso são considerados números de base 2. (Assim como os números de base 10 são chamados números decimais, os números de base 2 são chamados números binários.) Um único dígito de um número binário, portanto, é o “átomo” da computação, pois toda a informação é composta de dígitos binários, ou bits. Esse bloco de montagem fundamental pode assumir dois valores, que podem ser imaginados como várias alternativas: alto ou baixo, ligado ou desligado, verdadeiro ou falso, ou 1 ou 0. Generalizando, em qualquer base numérica, o valor do i-ésimo dígito d é d × Basei em que i começa com 0 e aumenta da direita para a esquerda. Isso leva a um modo óbvio de numerar os bits na word: basta usar a potência da base para esse bit. Subscritamos os números decimais com dec e os números binários com bin. Por exemplo, 1011bin
representa (1 × 23) +(0 × 22) +(1 × 21) +(1 × 20)dec = (1 × 8) +(0 × 4)
+(1 × 2) +(1 × 1)dec
= 8
+2
+0
+1dec
= 11dec
Logo, os bits são numerados com 0, 1, 2, 3, ... da direita para a esquerda em uma palavra. O desenho a seguir mostra a numeração dos bits dentro de uma word MIPS e o posicionamento do número 1011bin:
dígito binário Também chamado bit. Um dos dois números na base 2 (0 ou 1), que são os componentes básicos da informação.
70
bit menos significativo O bit mais à direita em uma palavra MIPS.
Capítulo 2 Instruções: A Linguagem de Máquina
Como as palavras são desenhadas vertical e horizontalmente, esquerda e direita podem não ser termos muito claros. Logo, o termo bit menos significativo é usado para se referir ao bit mais à direita (bit 0, no exemplo anterior) e bit mais significativo para o bit mais à esquerda (bit 31). A palavra MIPS possui 32 bits de largura, de modo que podemos representar 232 padrões diferentes de 32 bits. É natural deixar que essas representações mostrem os números de 0 a 232 – 1 (4.294.967.295dec):
Ou seja, os números binários de 32 bits podem ser representados em termos do valor do bit vezes uma potência de 2 (aqui, xi significa o i-ésimo bit de x): (x 31 × 231 ) + (x 30 × 230 ) + (x 29 × 229 ) + ... + (x1 × 21 ) + (x 0 × 20 ) Lembre-se de que os padrões de bits binários que acabamos de mostrar simplesmente representam os números. Os números, na realidade, possuem uma quantidade infinita de dígitos, com quase todos sendo 0, exceto por alguns dos dígitos mais à direita. Só que, normalmente, não mostramos os 0s à esquerda. O hardware pode ser projetado para somar, subtrair, multiplicar e dividir esses padrões de bits. Se o número que é o resultado correto de tais operações não puder ser representado por esses bits de hardware mais à direita, diz-se que houve um overflow. Fica a critério da linguagem de programação, do sistema operacional e do programa determinar o que fazer quando isso ocorre. Os programas de computador calculam números positivos e negativos, de modo que precisamos de uma representação que faça a distinção entre o positivo e o negativo. A solução mais óbvia é acrescentar um sinal separado, que convenientemente possa ser representado em um único bit; o nome dessa representação é sinal e magnitude. Infelizmente, a representação com sinal e magnitude possui várias desvantagens. Primeiro, não é óbvio onde colocar o bit de sinal. À direita? À esquerda? Os primeiros computadores tentaram ambos. Segundo, os somadores de sinal e magnitude podem precisar de uma etapa extra para definir o sinal, pois não podemos saber, com antecedência, qual será o sinal correto. Finalmente, um bit de sinal separado significa que a representação com sinal e magnitude possui um zero positivo e um zero negativo, o que pode ocasionar problemas para os programadores desatentos. Como resultado desses problemas, a representação com sinal e magnitude logo foi abandonada. Em busca de uma alternativa mais atraente, levantou-se a questão com relação a qual seria o resultado, para números sem sinal, se tentássemos subtrair um número grande de um número pequeno. A resposta é que ele tentaria pegar emprestado de uma sequência de 0s à esquerda, de modo que o resultado seria uma sequência de 1s à esquerda. Como não havia uma alternativa melhor óbvia, a solução final foi escolher a representação que tornasse o hardware simples: 0s iniciais significa positivo e 1s iniciais significa negativo. Essa convenção para representar os números binários com sinal é chamada representação por complemento de dois:
2.4 Números com sinal e sem sinal 71
A metade positiva dos números, de 0 a 2.147.483.647dec (231 – 1), utiliza a mesma representação de antes. O padrão de bits seguinte (1000 ... 0000bin) representa o número mais negativo –2.147.483.648dec (–231). Ele é seguido por um conjunto decrescente de números negativos: –2.147.483.647dec (1000 ... 0001bin) até –1dec (1111 ... 1111bin). A representação em complemento de dois possui um número negativo, –2.147.483.648dec, que não possui um número positivo correspondente. Esse desequilíbrio era uma preocupação para o programador desatento, mas a representação com sinal e magnitude gerava problemas para o programador e para o projetista do hardware. Consequentemente, todo computador hoje em dia utiliza a representação de números binários por complemento de dois para os números com sinal. A representação por complemento de dois tem a vantagem de que todos os números negativos possuem 1 no bit mais significativo. Consequentemente, o hardware só precisa testar esse bit para ver se um número é positivo ou negativo (com 0 considerado positivo). Esse bit normalmente é denominado bit de sinal. Reconhecendo o papel do bit de sinal, podemos representar números positivos e negativos de 32 bits em termos do valor do bit vezes uma potência de 2: (x 31 × 231 ) + (x 30 × 230 ) + (x 29 × 229 ) + ... + (x1 × 21 ) + (x 0 × 20 ) O bit de sinal é multiplicado por –231 e o restante dos bits é multiplicado pelas versões positivas de seus respectivos valores de base.
Conversão de binário para decimal
Qual é o valor decimal deste número em complemento de dois com 32 bits? 1111 1111 1111 1111 1111 1111 1111 1100bin
Substituindo os valores dos bits do número na fórmula anterior: (1 × −231 ) + (1 × 230 ) + (1 × 229 ) + ... + (1 × 22 ) + (0 × 21 ) + (0 × 20 ) = −231 + 230 + 229 + ... + 22 + 0 + 0 = −2,147, 483,648dec + 2,147, 483,644 dec = −4 dec Logo, veremos um atalho para simplificar a conversão.
EXEMPLO RESPOSTA
72
Capítulo 2 Instruções: A Linguagem de Máquina
Assim como uma operação com números sem sinal pode ocasionar overflow na capacidade do hardware de representar o resultado, uma operação com números em complemento de dois também pode. O overflow ocorre quando o bit mais à esquerda da representação binária do hardware não é igual ao número infinito de dígitos à esquerda (o bit de sinal está incorreto): 0 à esquerda do padrão de bits quando o número é negativo ou 1 quando o número é positivo.
Interface hardware/ software
Diferente dos números discutidos anteriormente, os endereços de memória começam com 0 e continuam até o maior endereço. Em outras palavras, endereços negativos não fazem sentido. Assim, os programas desejam lidar às vezes com números que podem ser positivos ou negativos e às vezes com números que só podem ser positivos. Algumas linguagens de programação refletem essa distinção. A linguagem C, por exemplo, chama os primeiros de integers, ou inteiros (declarados como int no programa), e os últimos de unsigned integers, ou inteiros sem sinal (unsighned int). Alguns guias de estilo C recomendam ainda declarar os primeiros como sighned int, para deixar a distinção clara.
Vamos examinar alguns atalhos úteis quando trabalhamos com os números em complemento de dois. O primeiro atalho é um modo rápido de negar um número binário no complemento de dois. Basta inverter cada 0 para 1 e cada 1 para 0, depois somar um ao resultado. Esse atalho é baseado na observação de que a soma de um número e sua representação invertida precisa ser 111 ... 111bin, que representa –1. Como x + x = –1, portanto, x + x + 1 = 0, ou x + 1 = –x.
Atalho para negação
EXEMPLO
Negue 2dec e depois verifique o resultado negando –2dec. 2dec = 00000000000000000000000000000010 bin
RESPOSTA
Negando esse número, invertendo os bits e somando um,
Na outra direção, 1111 1111 1111 1111 1111 1111 1111 1110bin
primeiro é invertido e depois incrementado:
2.4 Números com sinal e sem sinal 73
O próximo atalho nos diz como converter um número binário representado em n bits para um número representado com mais de n bits. Por exemplo, o campo imediato nas instruções load, store, branch, add e set on less than contém um número de 16 bits em complemento de dois, representando de –32.768dec (–215) a 32.767dec (215 – 1). Para somar o campo imediato a um registrador de 32 bits, o computador precisa converter esse número de 16 bits para o seu equivalente em 32 bits. O atalho é pegar o bit mais significativo da menor quantidade – o bit de sinal – e replicá-lo para preencher os novos bits na quantidade maior. Os bits antigos são simplesmente copiados para a parte da direita da nova word. Esse atalho normalmente é chamado de extensão de sinal.
Atalho para extensão de sinal
Converta as versões binárias de 16 bits de 2dec e –2dec para números binários de 32 bits. A versão binária de 16 bits do número 2 é 0000 0000 0000 0010bin = 2dec
Ele é convertido para um número de 32 bits criando-se 16 cópias do valor do bit mais significativo (0) e colocando-as na metade esquerda da word. A metade direita recebe o valor antigo: 0000 0000 0000 0000 0000 0000 0000 0010bin = 2dec
Vamos negar a versão de 16 bits de 2 usando o atalho anterior. Assim, 0000 0000 0000 0010bin
torna-se
Criar uma versão de 32 bits do número negativo significa copiar o bit de sinal 16 vezes e colocá-lo à esquerda: 1111 1111 1111 1111 1111 1111 1111 1110bin = –2dec
Esse truque funciona porque os números positivos em complemento de dois realmente possuem uma quantidade infinita de 0s à esquerda e os que são negativos em complemento de dois possuem uma quantidade infinita de 1s. O padrão binário que representa um número esconde os bits iniciais para caber na largura do hardware; a extensão do sinal simplesmente restaura alguns deles.
Resumo O ponto principal desta seção é que precisamos representar inteiros positivos e negativos dentro de uma palavra do computador e, embora existam prós e contras a qualquer opção, a escolha predominante desde 1965 tem sido o complemento de dois.
EXEMPLO RESPOSTA
74
Capítulo 2 Instruções: A Linguagem de Máquina
Verifique você mesmo
Qual é o valor decimal deste número de 64 bits em complemento de dois? 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1000bin
1) −4dec 2) −8dec 3) −16dec 4) 18.446.744.073.709.551.609dec Detalhamento: o complemento de dois recebe esse nome em decorrência da regra de que a
complemento de um Uma notação que representa o valor mais negativo por 10 ... 000bin e o valor mais positivo por 01 ... 11bin, deixando um número igual de negativos e positivos, mas terminando com dois zeros, um positivo (00 ... 00bin) e um negativo (11 ... 11bin). O termo também é usado para significar a inversão de cada bit em um padrão: 0 para 1 e 1 para 0. notação deslocada Uma notação que representa o valor mais negativo por 00 ... 000bin e o valor mais positivo por 11 ... 11bin, com 0 normalmente tendo o valor 10 ... 00bin, deslocando assim o número, de modo que o número mais o deslocamento têm uma representação não negativa.
soma sem sinal de um número de b bits e seu negativo é 2n; logo, o complemento ou a negação de um número em complemento de dois x é 2n – x. Uma terceira representação alternativa é chamada complemento de um. O negativo de um complemento de um é encontrado invertendo-se cada bit, de 0 para 1 e de 1 para 0, o que ajuda a explicar seu nome, pois o complemento de x é 2n – x – 1. Essa também foi uma tentativa de ser uma solução melhor do que a técnica de sinal e magnitude, e vários computadores científicos utilizaram a notação. Essa representação é semelhante ao complemento de dois, exceto que também possui dois 0s: 00...00bin é o 0 positivo, e 11...11bin é o 0 negativo. O maior número negativo 10...000bin representa –2.147.483.647dec e, por isso, os positivos e negativos são balanceados. Os que aderiram ao complemento de um precisaram de uma etapa extra para subtrair um número e, por isso, o complemento de dois domina hoje. Uma notação final, que veremos quando tratarmos de ponto flutuante no Capítulo 3, é representar o valor mais negativo por 00...000bin e o valor mais positivo por 11...11bin, com 0 normalmente tendo o valor 10...00bin. Isso é chamado de notação deslocada (biased notation), pois desloca o número de modo que o número mais o deslocamento tenha uma representação não negativa.
Detalhamento: Para números decimais com sinal, usamos “-” a fim de representar negativo, pois não existem limites para o tamanho de um número decimal. Dado um tamanho de palavra fixo, sequências de bits binárias e hexadecimais (veja Figura 2.4) podem codificar o sinal; logo, normalmente não usamos “ + ” ou “-” com notação binária ou hexadecimal.
2.5 Representando instruções no computador Agora, estamos prontos para explicar a diferença entre o modo como os humanos instruem os computadores e como os computadores veem as instruções. As instruções são mantidas no computador como uma série de sinais eletrônicos altos e baixos e podem ser representadas como números. Na verdade, cada parte da instrução pode ser considerada um número individual e a colocação desses números lado a lado forma a instrução. Como os registradores são referenciados por quase todas as instruções, é preciso haver uma convenção para mapear nomes de registrador em números. No assembly MIPS, os registradores $s0 a $s7 são mapeados nos registradores de 16 a 23 e os registradores $t0 a $t7 são mapeados nos registradores de 8 a 15. Logo, $s0 significa o registrador 16, $s1significa o registrador 17, $s2 significa o registrador 18, ..., $t0 significa o registrador 8, $t1 significa o registrador 9, e assim por diante. Nas próximas seções, descreveremos a convenção para o restante dos 32 registradores.
2.5 Representando instruções no computador 75
Traduzindo uma instrução assembly MIPS para uma instrução de máquina
Realizaremos a próxima etapa no refinamento da linguagem do MIPS como um exemplo. Mostraremos a versão da linguagem real do MIPS para a instrução representada simbolicamente por
EXEMPLO
add$t0, $s1, $s2
primeiro como uma combinação dos números decimais e depois dos números binários. A representação decimal é 0
17
RESPOSTA 18
8
0
32
Cada um desses segmentos de uma instrução é chamado de campo. O primeiro e o último campos (contendo 0 e 32, nesse caso) combinados dizem ao computador MIPS que essa instrução realiza soma. O segundo campo indica o número do registrador que é o primeiro operando de origem da operação de soma (17 = $s1) e o terceiro campo indica o outro operando fonte para a soma (18 = $s2). O quarto campo contém o número do registrador que deverá receber a soma (8 = $t0). O quinto campo não é utilizado nessa instrução, de modo que é definido como 0. Assim, a instrução soma o registrador $s1 ao registrador $s2 e coloca a soma no registrador $t0. Essa instrução também pode ser representada com campos em números binários, em vez de decimal:
Esse leiaute da instrução é chamado formato de instrução. Como você pode ver pela contagem do número de bits, essa instrução MIPS ocupa exatamente 32 bits – o mesmo tamanho da palavra de dados. Acompanhando nosso princípio de projeto, de que a simplicidade favorece a regularidade, todas as instruções MIPS possuem 32 bits de extensão. Para distinguir do assembly, chamamos a versão numérica das instruções de linguagem de máquina, e a sequência dessas instruções é o código de máquina. Pode parecer que agora você estará lendo e escrevendo sequências longas e cansativas de números binários. Evitamos esse tédio usando uma base maior do que a binária, que pode ser convertida com facilidade para binária. Como quase todos os tamanhos de dados no computador são múltiplos de 4, os números hexadecimais (base 16) são muito comuns. Como a base 16 é uma potência de 2, podemos converter de modo trivial substituindo cada grupo de quatro dígitos binários por um único dígito hexadecimal e vice-versa. A Figura 2.4 converte hexadecimal para binário e vice-versa. Visto que frequentemente lidamos com bases numéricas diferentes, para evitar confusão, vamos anexar em subscrito dec aos números decimais, bin aos números binários e hex aos números hexadecimais. (Se não houver um subscrito, a base padrão é 10.) A propósito, C e Java utilizam a notação 0xnnnn para os números hexadecimais.
formato de instrução Uma forma de representação de uma instrução, composta de campos de números binários. linguagem de máquina Representação binária utilizada para a comunicação dentro de um sistema computacional.
hexadecimal Números na base 16.
76
Capítulo 2 Instruções: A Linguagem de Máquina
FIGURA 2.4 A tabela de conversão hexadecimal-binário. Basta substituir um dígito hexadecimal pelos quatro dígitos binários correspondentes e vice-versa. Se o tamanho do número binário não for um múltiplo de 4, prossiga da direita para a esquerda.
Binário para hexadecimal e vice-versa
EXEMPLO
Converta os seguintes números hexadecimais e binários para a outra base: eca8
6420hex
0001
0011
0101
0111
1001
1011
1101
1111bin
Usando a Figura 2.4, temos a solução bastando olhar na tabela em uma direção:
RESPOSTA
E depois na outra direção:
Campos do MIPS Os campos do MIPS recebem nomes para facilitar seu tratamento:
Aqui está o significado de cada nome dos campos nas instruções MIPS: opcode O campo que denota a operação e formato de uma instrução.
j
op: operação básica da instrução, tradicionalmente chamado de opcode.
j
rs: o registrador do primeiro operando fonte.
2.5 Representando instruções no computador 77
j
rt: o registrador do segundo operando fonte.
j
rd: o registrador do operando de destino. Ele recebe o resultado da operação.
j
shamt: “Shift amount” (quantidade de deslocamento). (A Seção 2.6 explica as instruções de shift e esse termo; ele não será usado até lá, e, por isso, o campo contém zero nesta seção.)
j
funct: função. Esse campo seleciona a variante específica da operação no campo op e, às vezes, é chamado de código de função.
Existe um problema quando uma instrução precisa de campos maiores do que aqueles mostrados. Por exemplo, a instrução load word precisa especificar dois registradores e uma constante. Se o endereço tivesse de usar um dos campos de 5 bits no formato anterior, a constante dentro da instrução load word seria limitada a apenas 25, ou 32. Essa constante é utilizada para selecionar elementos dos arrays ou estruturas de dados e normalmente precisa ser muito maior do que 32. Esse campo de 5 bits é muito pequeno para realizar algo útil. Logo, temos um conflito entre o desejo de manter todas as instruções com o mesmo tamanho e o desejo de ter um formato de instrução único. Isso nos leva ao último princípio de projeto de hardware: Princípio de Projeto 4: Um bom projeto exige bons compromissos. O compromisso escolhido pelos projetistas do MIPS é manter todas as instruções com o mesmo tamanho, exigindo, assim, diferentes tipos de formatos para diferentes tipos de instruções. Por exemplo, o formato anterior é chamado de tipo-R (de registrador) ou formato R. Um segundo tipo de formato de instrução é chamado tipo I (de imediato), ou formato I, e é utilizado pelas instruções imediatas e de transferência de dados. Os campos do formato I são
O endereço de 16 bits significa que uma instrução load word pode carregar qualquer palavra dentro de uma região de ±215, ou 32.768 bytes (±213, ou 8.192 words) do endereço no registrador base rs. De modo semelhante, a soma imediata é limitada a constantes que não sejam maiores do que ±215. Vemos que o uso de mais de 32 registradores seria difícil nesse formato, pois os campos rs e rt precisariam cada um de outro bit, tornando mais difícil encaixar tudo em uma palavra. Vejamos a instrução load word da página 83: lw $t0, 32($s3) # Registrador temporário $t0 recebe A[8]
Aqui, 19 (para $s3) é colocado no campo rs, 8 (para $t0) é colocado no campo rt, e 32 é colocado no campo de endereço. Observe que o significado do campo rt mudou para essa instrução: em uma instrução load word, o campo rt especifica o registrador de destino, que recebe o resultado do load. Embora o uso de vários formatos complique o hardware, podemos reduzir a complexidade mantendo os formatos semelhantes. Por exemplo, os três primeiros campos nos formatos de tipo R e tipo I possuem o mesmo tamanho e têm os mesmos nomes; o tamanho do quarto campo no tipo I é igual à soma dos tamanhos dos três últimos campos do tipo R. Caso você esteja curioso, os formatos são diferenciados pelos valores no primeiro campo: cada formato recebe um conjunto distinto de valores no primeiro campo (op), de modo que o hardware sabe se deve tratar a última metade da instrução como três campos (tipo R) ou como um único campo (tipo I). A Figura 2.5 mostra os números utilizados em cada campo para as instruções MIPS descritas aqui.
78
Capítulo 2 Instruções: A Linguagem de Máquina
FIGURA 2.5 Codificação de instruções MIPS. Na tabela, “reg” significa um número de registrador entre 0 e 31, “endereço” significa um endereço de 16 bits, e “n.a.” (não se aplica) significa que esse campo não aparece nesse formato. Observe que as instruções add e sub têm o mesmo valor no campo op; o hardware usa o campo funct para decidir sobre a variante da operação: somar (32) ou subtrair (34).
Traduzindo do assembly MIPS para a linguagem de máquina
EXEMPLO
Agora, já podemos usar um exemplo completo daquilo que o programador escreve até o que o computador executa. Se $t1 possui a base do array A e $s2 corresponde a h, então a instrução de atribuição A [ 3 0 0 ] = h + A[300];
é compilada para lw
$t0,1200($t1) # Reg.temporário $t0 recebe A[300]
add $t0,$s2,$t0 sw
# Reg.temporário $t0 recebe h + A[300]
$t0,1200($t1) # Armazena h + A[300] de volta para A[300]
Qual o código em linguagem de máquina MIPS para essas três instruções?
RESPOSTA
Por conveniência, primeiro vamos representar as instruções em linguagem de máquina usando os números decimais. Pela Figura 2.5, podemos determinar as três instruções em linguagem de máquina: op
rs
rt
35
9
8
0
18
8
43
9
8
rd
endereço/shamt
funct
1200 8
0
32
1200
A instrução lw é identificada por 35 (Figura 2.5) no primeiro campo (op). O registrador base 9 ($t1) é especificado no segundo campo (rs), e o registrador de destino 8 ($t0) é especificado no terceiro campo (rt). O offset para selecionar A[300] (1200 = 300 × 4) aparece no campo final (endereço). A instrução add, que vem em seguida, é especificada com 0 no primeiro campo (op) e 32 no último campo (funct). Os três registradores operandos (18, 8 e 8) aparecem no segundo, no terceiro e no quarto campos e correspondem a $s2, $t0 e $t0. A instrução sw é identificada com 43 no primeiro campo. O restante dessa última instrução é idêntico à instrução lw. Como 1200dec = 0000 0100 1011 0000bin, o equivalente binário ao formato decimal é o seguinte: 100011
01001
01000
000000
10010
01000
101011
01001
01000
0000 0100 1011 0000 01000
00000
100000
0000 0100 1011 0000
Observe a semelhança das representações binárias da primeira e última instruções. A única diferença está no terceiro bit a partir da esquerda, que está destacado aqui.
2.5 Representando instruções no computador 79
A Figura 2.6 resume as partes do assembly do MIPS descritas nesta seção. Como veremos no Capítulo 4, a semelhança das representações binárias de instruções relacionadas simplifica o projeto do hardware. Essas instruções são outro exemplo da regularidade da arquitetura MIPS.
FIGURA 2.6 Arquitetura MIPS revelada até a Seção 2.5. Os dois formatos de instrução MIPS até aqui são R e I. Os 16 primeiros bits são iguais: ambos contêm um campo op, indicando a operação básica; um campo rs, indicando um dos operandos origem; e um campo rt, que especifica o outro operando origem, exceto para load word, em que especifica o registrador destino. O formato R divide os 16 últimos bits em um campo rd, especificando o registrador destino; um campo shamt, explicado na Seção 2.6; e o campo funct, que especifica a operação específica das instruções no formato R. O formato I mantém os 16 bits finais como um único campo de endereço.
Os computadores de hoje são baseados em dois princípios fundamentais: 1. As instruções são representadas como números. 2. Os programas são armazenados na memória para serem lidos ou escritos, assim como os números. Esses princípios levam ao conceito de programa armazenado; sua invenção permite que o “gênio da computação saia de sua garrafa”. A Figura 2.7 mostra o poder do conceito; especificamente, a memória pode conter o código-fonte de um editor de textos, o código de máquina compilado correspondente, o texto que o programa compilado está usando e até mesmo o compilador que gerou o código de máquina. Uma consequência de instruções em forma de números é que os programas normalmente são entregues como arquivos de números binários. A implicação comercial é que os computadores podem herdar softwares já prontos desde que sejam compatíveis com um conjunto de instruções existente. Essa “compatibilidade binária” normalmente alinha o setor em torno de uma quantidade muito pequena de arquiteturas de conjuntos de instruções.
em
Colocando perspectiva
80
Capítulo 2 Instruções: A Linguagem de Máquina
FIGURA 2.7 O conceito de programa armazenado. Os programas armazenados permitem que um computador que realiza contabilidade se torne, em um piscar de olhos, um computador que ajuda um autor a escrever um livro. A troca acontece simplesmente carregando a memória com programas e dados e depois dizendo ao computador para iniciar a execução em determinado local na memória. Tratar as instruções da mesma maneira que os dados simplifica bastante tanto o hardware da memória quanto o software dos sistemas computacionais. Especificamente, a tecnologia de memória necessária para os dados também pode ser usada para programas, e programas como compiladores, por exemplo, podem traduzir o código escrito em uma notação muito mais conveniente para os humanos em código que o computador consiga entender.
Verifique você mesmo
Que instrução MIPS isto representa? Escolha dentre uma das quatro opções a seguir. op
rs
rt
rd
shamt
funct
0
8
9
10
0
34
1. add $s0, $s1, $s2 2. add $s2, $s0, $s1 3. add $s2, $s1, $s0 4. sub $s2, $s0, $s1 “Ao contrário”, continuou Tweedledee, “se foi assim, poderia ser; e se fosse assim, seria; mas como não é, então não é. Isso é lógico.” Lewis Carroll, Alice no país das maravilhas, 1865
2.6 Operações lógicas Embora os primeiros computadores se concentrassem em palavras completas, logo ficou claro que era útil atuar sobre campos de bits dentro de uma palavra ou até mesmo sobre bits individuais. Examinar os caracteres dentro de uma palavra, cada um dos quais armazenados como 8 bits, é um exemplo dessa operação (veja a Seção 2.9). Instruções foram acrescentadas para simplificar, entre outras tarefas, o empacotamento e o desempacotamento dos bits em words. Essas instruções são chamadas operações lógicas. A Figura 2.8 mostra as operações lógicas em C, Java e MIPS.
2.6 Operações lógicas 81
FIGURA 2.8 Operadores lógicos em C e Java e suas instruções MIPS correspondentes. MIPS implementa NOT usando um NOR com um operando sendo zero.
A primeira classe dessas operações é chamada de shifts (deslocamentos). Elas movem todos os bits de uma word para a esquerda ou direita, preenchendo os bits que ficaram vazios com 0s. Por exemplo, se o registrador $s0 tivesse 0000 0000 0000 0000 0000 0000 0000 0000 1001bin = 9dec
e fosse executada a instrução para deslocar 4 bits à esquerda, o novo valor se pareceria com: 0000 0000 0000 0000 0000 0000 0000 1001 0000bin = 144dec
O dual de um shift à esquerda é um shift à direita. Os nomes reais das duas instruções shift no MIPS são shift left logical (sll) e shift right logical (srl). A instrução a seguir realiza essa operação, supondo que o valor original estava no registrador $t0 e o resultado deverá ir para o registrador $t2: sll
$t2,$s0, 4
# reg $t2 = reg $s0 << 4 bits
Adiamos até agora a explicação do campo shamt, do formato R. O nome significa shift amount (quantidade de deslocamento) e é usado nas instruções de deslocamento. Logo, a versão em linguagem de máquina da instrução anterior é
A codificação de sll é 0 nos campos op e funct, rd contém 10 (registrador $t2), rt contém $s0 e shamt contém 4. O campo rs não é utilizado e, por isso, é definido como 0. O deslocamento lógico à esquerda oferece um benefício adicional. O deslocamento à esquerda de i bits gera o mesmo resultado que multiplicar por 2i, assim como o deslocamento de um número decimal por i dígitos é equivalente a multiplicar por 10i. Por exemplo, a instrução sll anterior desloca de 4, o que gera o mesmo resultado que multiplicar por 24, ou 16. O primeiro padrão de bits descrito anteriormente representa 9, e 9 × 16 = 144, o valor do segundo padrão de bits. Outra operação útil que isola os campos é AND. AND é uma operação bit a bit que deixa um 1 no resultado somente se os dois bits dos operandos forem 1. Por exemplo, se o registrador $t2 tiver 0000 0000 0000 0000 0000 1101 0000 0000bin
e o registrador $t1 tiver 0000 0000 0000 0000 0011 1100 0000 0000bin
AND Uma operação lógica bit a bit com dois operandos, que calcula um 1 somente se houver um 1 em ambos os operandos.
82
Capítulo 2 Instruções: A Linguagem de Máquina
então, depois de executar a instrução MIPS and $t0,$t1,$t2
o valor do registrador
# reg $t0 = reg $t1 & reg $t2 $t0
seria
0000 0000 0000 0000 0000 1100 0000 0000bin
OR Uma operação lógica bit a bit com dois operandos, que calcula um 1 se houver um 1 em qualquer um dos operandos.
Como você pode ver, o AND pode aplicar um padrão de bits a um conjunto de bits para forçar 0s onde houver um 0 no padrão de bits. Esse padrão de bits, em conjunto com o AND, tradicionalmente é chamado de máscara, pois a máscara “oculta” alguns bits. Para colocar um valor em um desses 0s, existe o dual do AND, chamado OR. Essa é uma operação bit a bit, que coloca 1 no resultado se qualquer um dos bits do operando for 1. Exemplificando, se os registradores $t1 e $t2 não tiverem sido alterados do exemplo anterior, o resultado da instrução MIPS or $t0,$t1,$t2 # reg $t0 = reg $t1 | reg $t2
é este valor no registrador $t0: 0000 0000 0000 0000 0011 1101 0000 0000bin NOT Uma operação lógica bit a bit com um operando, que inverte os bits; ou seja, ela substitui cada 1 por um 0, e cada 0 por um 1. NOT Uma operação lógica bit a bit com dois operandos, que calcula o NOT do OR dos dois operandos. Ou seja, ela calcula um 1 somente se houver um 0 em ambos os operandos.
A última operação lógica é um contrário. O NOT apanha um operando e coloca um 1 no resultado se um bit do operando for 0, e vice-versa. Acompanhando o formato de dois operandos, os projetistas do MIPS decidiram incluir a instrução NOR (NOT OR) no lugar de NOT. Se um operando for zero, então ele é equivalente a NOT: A NOR 0 = NOT (A OR 0) = NOT (A). Se o registrador $t1 não tiver mudado desde o exemplo anterior e o registrador $t3 tiver o valor 0, o resultado da instrução MIPS nor $t0,$t1,$t3 # reg $t0 = ~ (reg $t1 | reg $t3)
é este valor no registrador $t0: 1111 1111 1111 1111 1100 0011 1111 1111bin
A Figura 2.8 mostrou o relacionamento entre os operadores em C e Java e as instruções MIPS. As constantes são úteis nas operações lógicas AND e OR, assim como nas operações aritméticas, de modo que o MIPS também oferece as instruções and imediato (andi) e or imediato (ori). As constantes são raras para NOR, pois seu uso principal é inverter os bits de um único operando; assim, o hardware não possui uma versão imediata. Detalhamento: O conjunto de instruções MIPS completo também inclui exclusive or (XOR), que define o bit como 1 quando dois bits correspondentes diferem, e como 0 quando eles são iguais. C permite que campos de bit ou campos sejam definidos dentro das palavras, ambos permitindo que os objetos sejam empacotados com uma palavra e combinem com uma interface imposta externamente, como um dispositivo de E/S. Todos os campos precisam caber dentro de uma única palavra. Os campos recebem inteiros que podem ser tão curtos quanto 1 bit. Os compiladores C inserem e extraem campos usando instruções lógicas no MIPS: and, or, sll e srl.
2.7 Instruções para tomada de decisões 83
Que operações podem isolar um campo em uma word? 1. AND 2. Um deslocamento à esquerda seguido por um deslocamento à direita
2.7 Instruções para tomada de decisões O que distingue um computador de uma calculadora simples é a sua capacidade de tomar decisões. Com base nos dados de entrada e nos valores criados durante o cálculo, diferentes instruções são executadas. A tomada de decisão normalmente é representada nas linguagens de programação usando a instrução if, às vezes combinadas com instruções go to e rótulos (labels). O assembly do MIPS inclui duas instruções para tomada de decisões, semelhantes a uma instrução if com um go to. A primeira instrução é beq registrador1, registrador2, L1
Essa instrução significa ir até a instrução chamada L1 se o valor no registrador1 for igual ao valor no registrador2. O mnemônico beq significa branch if equal (desviar se for igual). A segunda instrução é bne registrador1,registrador2,L1
Ela significa ir até a instrução chamada L1 se o valor no registrador1 não for igual ao valor no registrador2. O mnemônico bne significa branch if not equal (desviar se não for igual). Essas duas instruções tradicionalmente são denominadas desvios condicionais.
Verifique você mesmo A utilidade de um computador automático se encontra na possibilidade de usar determinada sequência de instruções repetidamente o número de vezes em que ela é repetida depende dos resultados do cálculo. ...Essa escolha pode depender do sinal de um número (zero é considerado positivo para as finalidades da máquina). Consequentemente, apresentamos uma [instrução] (a [instrução] de transferência condicional) que, dependendo do sinal de determinado número, causa a execução de uma dentre duas rotinas. Burks, Goldstine e von Neumann, 1947
desvio condicional Uma instrução que requer a comparação de dois valores e que leva em conta uma transferência de controle subsequente para um novo endereço no programa, com base no resultado da comparação.
Compilando if-then-else em desvios condicionais
No segmento de código a seguir, f, g, h, i e j são variáveis. Se as cinco variáveis de f a j correspondem aos cinco registradores de $s0 a $s4, qual é o código MIPS compilado para esta instrução if em C?
EXEMPLO
if (i = = j) f = g + h;else f = g − h;
A Figura 2.9 é um fluxograma de como deve ser o código MIPS. A primeira expressão compara a igualdade, de modo que poderíamos querer desviar se os registradores forem a instrução de igual (beq). De modo geral, o código será mais eficiente se testarmos a condição oposta ao desvio no lugar do código que realiza a parte then subsequente do if (o rótulo Else é definido a seguir) e assim usamos o desvio se os registradores forem a instrução de não igual (bne): bne $s3,$s4,Else
# vá para Else se i ≠ j
A próxima instrução de atribuição realiza uma única operação, e se todos os operandos estiverem em registradores, essa é apenas uma instrução:
RESPOSTA
84
Capítulo 2 Instruções: A Linguagem de Máquina
add $s0,$s1,$s2
# f = g + h (ignorada se i ≠ j)
Agora, precisamos ir até o final da instrução if. Este exemplo apresenta outro tipo de desvio, normalmente chamado desvio incondicional. Essa instrução diz que o processador sempre deverá seguir o desvio. Para distinguir entre os desvios condicionais e incondicionais, o nome MIPS para esse tipo de instrução é jump, abreviado como j (o rótulo Exit é definido a seguir). j Exit
# vá para Exit
A instrução de atribuição na parte else da instrução if pode novamente ser compilada para uma única instrução. Só precisamos anexar um rótulo Else a essa instrução. Também mostramos o rótulo Exit que está após essa instrução, mostrando o final do código compilado de if-then-else: Else : sub $s0,$s1,$s2
# f = g − h (ignorada se i = j)
Exit :
FIGURA 2.9 Ilustração das opções na instrução if acima. A caixa da esquerda corresponde à parte then da instrução if, e a caixa da direita corresponde à parte else.
Observe que o montador alivia o compilador e o programador assembly do trabalho de calcular endereços para os desvios, assim como evita o cálculo dos endereços de dados para loads e stores (veja Seção 2.12).
Interface hardware/ software
Os compiladores constantemente criam desvios e rótulos onde não aparecem na linguagem de programação. Evitar o trabalho de escrever rótulos e desvios explícitos é um benefício em linguagens de programação de alto nível e um dos motivos para a codificação ser mais rápida nesse nível.
Loops Decisões são importantes tanto para escolher entre duas alternativas – como encontramos nas instruções if – quanto para repetir um cálculo – como nos loops. As mesmas instruções assembly são os blocos de montagem para os dois casos.
2.7 Instruções para tomada de decisões 85
Compilando um WHILE loop em C
Aqui está um loop tradicional em C:
EXEMPLO
Suponha que i e K correspondam aos registradores $s3 e $s5 e a base do array save esteja em $s6. Qual é o código assembly MIPS correspondente a esse segmento C? O primeiro passo é ler save[i] para um registrador temporário. Antes que possamos ler save[i] para um registrador temporário, precisamos ter seu endereço. Antes que possamos somar i à base do array save para formar o endereço, temos de multiplicar o índice i por 4, em razão do problema do endereçamento em bytes. Felizmente, podemos usar o deslocamento lógico à esquerda, pois o deslocamento à esquerda de 2 bits multiplica por 22 ou 4 (veja a Seção “Operações Lógicas”, anteriormente neste capítulo). Precisamos acrescentar o rótulo Loop para podermos desviar de volta a essa instrução no final do loop: Loop :
sll
$t1,$s3,2
RESPOSTA
# Registrador temporário $t1 = 4 * i
Para obter o endereço de save[i], temos de somar $t1 e a base do array save em $t6: add $t1,$t1,$s6
# $t1 = endereço de save[i]
Agora, podemos usar esse endereço para ler save[i] para um registrador temporário: lw
$t0,0($t1)
# Registro temporário $t0 = save[i]
A próxima instrução realiza o teste do loop, terminando se bne $t0,$s5, Exit
save[i] ≠ k :
# vá para Exit se save[i] ≠ k
A próxima instrução soma 1 a i: add $s3,$s3,1
# i = i+ 1
O final do loop desvia de volta ao teste do while no início do loop. Simplesmente acrescentamos o rótulo Exit depois dele e terminamos:
(Veja nos exercícios uma otimização para essa sequência.)
Essas sequências de instruções que terminam em um desvio são tão fundamentais para a compilação que recebem seu próprio termo: um bloco básico é uma sequência de instruções sem desvios, exceto, possivelmente, no final, e sem destinos de desvio ou rótulos de desvio, exceto, possivelmente, no início. Uma das primeiras fases da compilação é desmembrar o programa em blocos básicos.
Interface hardware/ software bloco básico Uma sequência de
O teste de igualdade ou desigualdade provavelmente é o teste mais comum, mas às vezes é útil ver se uma variável é menor do que outra. Por exemplo, um loop for pode querer testar se a variável de índice é menor do que 0. Essas comparações são realizadas em assembly do MIPS com uma instrução que compara dois registradores e atribui 1 a um terceiro
instruções sem desvios (exceto, possivelmente, no final) e sem destinos de desvio ou rótulos de desvio (exceto, possivelmente, no início).
86
Capítulo 2 Instruções: A Linguagem de Máquina
registrador se o primeiro for menor do que o segundo; caso contrário, é atribuído 0. A instrução MIPS é chamada set on less than (atribuir se menor que), ou slt. Por exemplo, slt
$t0, $s3, $s4
# $t0 = 1 se $s3
<
$s4
significa que é atribuído 1 ao registrador $t0 se o valor no registrador $s3 for menor do que o valor no registrador $s4; caso contrário, é atribuído 0 ao registrador $t0. Operadores constantes são comuns nas comparações, de modo que existe uma versão imediata da instrução set on less than. Para testar se o registrador $s2 é menor do que a constante 10, podemos simplesmente escrever slti
Interface hardware/ software
$t0,$s2,10
# $t0 = 1 se $s2 < 10
Atentando para a advertência de von Neumann quanto à simplicidade do “equipamento”, a arquitetura do MIPS não inclui “desvio se menor que”, pois isso é muito complicado; ou ela esticaria o tempo do ciclo de clock ou exigiria ciclos de clock extras por instrução. Duas instruções mais rápidas são mais úteis. Os compiladores MIPS utilizam as instruções slt, slti, beq, bne e o valor fixo 0 (sempre à disposição com a leitura do registrador $zero) para criar todas as condições relativas: igual, diferente, menor que, menor ou igual, maior que, maior ou igual.
Interface hardware/ software
As instruções de comparação precisam lidar com a dicotomia entre números com sinal e sem sinal. Às vezes, um padrão de bits com 1 no bit mais significativo representa um número negativo e, naturalmente, é menor que qualquer número positivo, que precisa ter um 0 no bit mais significativo. Com inteiros sem sinal, por outro lado, um 1 no bit mais significativo representa um número que é maior que qualquer um que comece com um 0. (Logo tiraremos proveito desse significado dual do bit mais significativo para reduzir o custo da verificação dos limites do array.) MIPS oferece duas versões da comparação set on less than para tratar dessas alternativas. Set on less than (slt) e set on less than immediate (slti) funcionam com inteiros com sinal. Os inteiros sem sinal são comparados por meio de set on less than unsigned (sltu) e set on less than immediate unsigned (sltiu).
Comparação de números com sinal e sem sinal
EXEMPLO
Suponha que o registrador $s0 tenha o número binário 1111 1111 1111 1111 1111 1111 1111 1111bin
e que o registrador $s1 tenha o número binário 0000 0000 0000 0000 0000 0000 0000 0001bin
2.7 Instruções para tomada de decisões 87
Quais são os valores dos registradores $t0 e $t1 após essas duas instruções? slt
$t0, $s0, $s1 # comparação com sinal
sltu
$t1, $s0, $s1 # comparação sem sinal
O valor no registrador $s0 representa -1dec se for um inteiro e 4.294.967.295dec se for um inteiro sem sinal. O valor no registrador $s1 representa 1dec em qualquer caso. O registrador $t0 tem o valor 1, pois -1dec < 1dec, e o registrador $t1 tem o valor 0, pois 4.294.967.295dec > 1dec.
RESPOSTA
Tratar números com sinal como se fossem sem sinal é um modo de baixo custo para verificar se 0 ≤ x < y, que corresponde à verificação de índice fora de limite dos arrays. O principal é que os inteiros negativos na notação de complemento de dois se parecem com números grandes na notação sem sinal; ou seja, o bit mais significativo é um bit de sinal na primeira notação, mas uma grande parte do número na segunda. Assim, uma comparação sem sinal de x < y também verifica se x é negativo, bem como se x é menor que y.
Atalho para verificação de limites
Use este atalho para reduzir uma verificação de índice fora dos limites: salte para IndexOutOfBounds se $s1 ≥ $t2 ou se $s1 é negativo. O código de verificação só usa sltu para realizar as duas verificações: sltu $t0,$s1,$t2 # $t0 = 0 se $s1 >= tamanho ou $s1 < 0 beq
EXEMPLO RESPOSTA
$t0,$zero,IndexOutOfBounds # se erro, goto Error
Instrução Case/Switch A maioria das linguagens de programação possui uma instrução case ou switch, para o programador poder selecionar uma dentre muitas alternativas, dependendo de um único valor. O modo mais simples de implementar switch é por meio de uma sequência de testes condicionais, transformando a instrução switch em uma cadeia de instruções if-then-else. Às vezes, as alternativas podem ser codificadas de forma mais eficiente como uma tabela de endereços de sequências de instruções alternativas, chamada tabela de endereços de desvio ou tabela de desvio, e o programa só precisa indexar na tabela e depois desviar para a sequência apropriada. A tabela de desvios é, então, apenas um array de palavras com endereços que correspondem aos rótulos no código. Para apoiar tais situações, computadores como o MIPS incluem uma instrução jump register (jr), significando um desvio incondicional para o endereço especificado em um registrador. Depois desvia para o endereço apropriado usando essa instrução, que é descrita na próxima seção.
Embora haja muitas instruções para decisões e loops em linguagens de programação como C e Java, a instrução básica que as implementa no nível do conjunto de instruções é o desvio condicional. Detalhamento: se você já ouviu falar em delayed branches, explicados no Capítulo 4, não se preocupe: o montador do MIPS os torna invisíveis ao programador assembly.
tabela de endereços de desvio Também chamada de tabela de desvios. Uma tabela de endereços de sequências de instruções alternativas.
Interface hardware/ software
88
Capítulo 2 Instruções: A Linguagem de Máquina
Verifique você mesmo
I. A linguagem C possui muitas instruções para decisões e loops, enquanto o MIPS possui poucas. Quais dos seguintes itens explicam ou não esse desequilíbrio? Por quê? 1. Mais instruções de decisão tornam o código mais fácil de ler e entender. 2. Menos instruções de decisão simplificam a tarefa da camada inferior responsável pela execução. 3. Mais instruções de decisão significam menos linhas de código, o que geralmente reduz o tempo de codificação. 4. Mais instruções de decisão significam menos linhas de código, o que geralmente resulta na execução de menos operações. II. Por que a linguagem C oferece dois conjuntos de operadores para AND (& e &&) e dois conjuntos de operadores para OR (| e ||), enquanto o MIPS não faz isso? 1. As operações lógicas AND e OR implementam & e |, enquanto os desvios condicionais implementam && e ||. 2. A afirmativa anterior é o contrário: && e || correspondem a operações lógicas, enquanto & e | são mapeados para desvios condicionais. 3. Elas são redundantes e significam a mesma coisa: && e || são simplesmente herdados da linguagem de programação B, a antecessora do C.
Suporte a procedimentos no hardware
2.8 do computador procedimento Uma sub-rotina armazenada que realiza uma tarefa com base nos parâmetros com os quais ela é provida.
Um procedimento ou função é uma ferramenta que os programadores utilizam a fim de estruturar programas, tanto para torná-los mais fáceis de entender quanto para permitir que o código seja reutilizado. Os procedimentos permitem que o programador se concentre em apenas uma parte da tarefa de cada vez, com os parâmetros atuando como uma barreira entre o procedimento e o restante do programa e dos dados, permitindo que sejam passados valores e resultados de retorno. Descrevemos o equivalente aos procedimentos em Java na Seção 2.15 do CD, mas a linguagem Java precisa de tudo de um computador que a linguagem C também necessita. Você pode pensar em um procedimento como um espião que sai com um plano secreto, adquire recursos, realiza a tarefa, cobre seus rastros e depois retorna ao ponto de origem com o resultado desejado. Nada mais deverá ter sido perturbado depois que a missão terminar. Além do mais, um espião opera apenas sobre aquilo que ele “precisa saber”, de modo que não pode fazer suposições sobre seu patrão. De modo semelhante, na execução de um procedimento, o programa precisa seguir estas seis etapas: 1. Colocar parâmetros em um lugar onde o procedimento possa acessá-los. 2. Transferir o controle para o procedimento. 3. Adquirir os recursos de armazenamento necessários para o procedimento. 4. Realizar a tarefa desejada. 5. Colocar o valor de retorno em um local onde o programa que o chamou possa acessá-lo. 6. Retornar o controle para o ponto de origem, pois um procedimento pode ser chamado de vários pontos em um programa.
2.8 Suporte a procedimentos no hardware do computador 89
Como já dissemos, os registradores são o local mais rápido para manter dados em um computador, de modo que queremos usá-los ao máximo possível. O software do MIPS utiliza a seguinte convenção na alocação de seus 32 registradores: j $a0-$a3: quatro j $v0-$v1: dois j $ra: um
registradores de argumento, para passar parâmetros
registradores de valor, para valores de retorno
registrador de endereço de retorno, para retornar ao ponto de origem
Além de alocar esses registradores, o assembly do MIPS inclui uma instrução apenas para os procedimentos: ela desvia para um endereço e simultaneamente salva o endereço da instrução seguinte no registrador $ra. A instrução de jump-and-link (jal) é escrita simplesmente como jal EndereçoProcedimento
A parte do link no nome da instrução significa que um endereço ou link é formado de modo a apontar para o local de chamada, permitindo que o procedimento retorne ao endereço correto. Esse “link”, armazenado no registrador $ra, é denominado endereço de retorno. O endereço de retorno é necessário porque o mesmo procedimento poderia ser chamado de várias partes do programa. Para apoiar tais situações, computadores como o MIPS utilizam uma instrução de jump register (jr), apresentada anteriormente para ajudar com as instruções case, significando um desvio incondicional para o endereço especificado em um registrador:
instrução de jump-and-link Uma instrução que salta para um endereço e simultaneamente salva o endereço da instrução seguinte em um registrador ($ra no MIPS).
endereço de retorno Um link para o local de chamada, permitindo que um procedimento retorne ao endereço correto; no MIPS, ele é armazenado no registrador $ra.
jr $ra
A instrução de jump register pula para o endereço armazenado no registrador $ra – que é exatamente o que queremos. Assim, o programa que chama, ou caller, coloca os valores de parâmetro em $a0-$a3 e utiliza jal X para desviar para o procedimento X (às vezes denominado callee). O callee, então, realiza os cálculos, coloca os resultados em $v0-$v1 e retorna o controle para o caller usando jr $ra. Num programa armazenado é necessário ter um registrador para manter o endereço da instrução atual sendo executada. Por motivos históricos, esse registrador quase sempre é denominado contador de programa, abreviado como PC (Program Counter) na arquitetura MIPS, embora um nome mais sensato teria sido registrador de endereço de instrução. A instrução jal salva o PC + 4 no registrador $ra para o link com a instrução seguinte, a fim de preparar o retorno do procedimento.
caller O programa que instiga um procedimento e oferece os valores de parâmetro necessários. callee Um procedimento que executa uma série de instruções armazenadas com base nos parâmetros fornecidos pelo caller e depois retorna o controle para o caller novamente.
contador de programa (PC) O registrador que contém o endereço da instrução sendo executada no programa.
Usando mais registradores Suponha que um compilador precise de mais registradores para um procedimento do que os quatro registradores para argumentos e os dois para valores de retorno. Como temos de cobrir nossos rastros após o término desta missão, quaisquer registradores necessários ao caller deverão ser restaurados aos valores que possuíam antes de o procedimento ser chamado. Essa situação é um exemplo em que são usados os spilled registers em memória, conforme mencionamos na Seção “Interface hardware/software”. A estrutura de dados ideal para armazenar os spilled registers é uma pilha – uma fila do tipo “último a entrar, primeiro a sair”. Uma pilha precisa de um ponteiro para o endereço alocado mais recentemente na pilha, a fim de mostrar onde o próximo procedimento deverá colocar os spilled registers ou onde os valores antigos dos registradores estão localizados. O stack pointer é ajustado em uma palavra para cada registrador salvo ou restaurado. O software MIPS reserva o registrador 29 para o stack pointer, dando-lhe o nome óbvio $sp. As pilhas são tão comuns que possuem seus próprios termos para transferir dados da pilha e para ela: colocar dados na pilha é denominado push, e remover dados da pilha é denominado pop.
pilha (stack) Uma estrutura de dados utilizada para armazenar os spilled registers, organizada como uma fila do tipo “último a entrar, primeiro a sair”. stack pointer Um valor indicando o endereço alocado mais recentemente em uma pilha, que mostra onde spilled registers devem ser armazenados ou onde os valores antigos dos registradores podem ser localizados. push Acrescentar elemento à pilha.
pop Remover elemento da pilha.
90
Capítulo 2 Instruções: A Linguagem de Máquina
Por motivos históricos, as pilhas “crescem” de endereços maiores para endereços menores. Essa convenção significa que você põe valores na pilha subtraindo do valor do stack pointer. Somar ao stack pointer diminui essa pilha, removendo seus valores.
Compilando um procedimento em C que não chama outro procedimento
EXEMPLO
Vamos transformar o exemplo da Seção 2.2 em um procedimento em C:
Qual é o código assembly do MIPS compilado?
RESPOSTA
As variáveis de parâmetro g, h, i e j correspondem aos registradores de argumento$a0, $a1, $a2 e $a3, e f corresponde a $s0. O programa compilado começa com o rótulo do procedimento: exemplo_folha:
O próximo passo é salvar os registradores usados pelo procedimento. A instrução de atribuição em C no corpo do procedimento é idêntica ao exemplo da Seção 2.2, que usa dois registradores temporários. Assim, precisamos salvar três registradores: $s0, $t0 e $t1. “Empilhamos” os valores antigos, criando espaço para três palavras (12 bytes) na pilha e depois as armazenamos:
A Figura 2.10 mostra a pilha antes, durante e após a chamada do procedimento. As três instruções seguintes correspondem ao corpo do procedimento, que segue o exemplo da Seção 2.2:
Para retornar o valor de f, nós o copiamos para um registrador de valor de retorno: add $v0,$s0,$zero
# retorna f ($v0 = $s0 + 0)
Antes de retornar, restauramos os três valores antigos dos registradores que salvamos, desempilhando-os:
2.8 Suporte a procedimentos no hardware do computador 91
O procedimento termina com um jump register usando o endereço de retorno: jr $ra
# desvia de volta à rotina que chamou
FIGURA 2.10 Os valores do stack pointer e a pilha (a) antes, (b) durante e (c) após a chamada do procedimento. O stack pointer sempre aponta para o “topo” da pilha ou para a última palavra na pilha neste desenho.
No exemplo anterior, usamos registradores temporários e consideramos que seus valores antigos precisam ser salvos e restaurados. Para evitar salvar e restaurar um registrador cujo valor nunca é utilizado, o que poderia acontecer com um registrador temporário, o software do MIPS separa 18 dos registradores em dois grupos: j $t0–$t9: dez registradores temporários que não são preservados pelo procedimento
chamado em uma chamada de procedimento j
(se forem usados, o procedimento chamado os salva e restaura)$s0–$s7: oito registradores salvos que precisam ser preservados em uma chamada de procedimento
Essa convenção simples reduz o register spilling. No exemplo anterior, como o caller não espera que os registradores $t0 e $t1 sejam preservados durante uma chamada de procedimento, podemos descartar dois stores e dois loads do código. Ainda temos de salvar e restaurar $s0, pois o procedimento chamado deve considerar que o caller precisa de seu valor.
Procedimentos aninhados Os procedimentos que não chamam outros são denominados procedimentos folha. A vida seria simples se todos os procedimentos fossem procedimentos folha, mas não são. Assim como um espião poderia empregar outros espiões como parte de uma missão, que, por sua vez, poderiam utilizar ainda mais espiões, os procedimentos também chamam outros procedimentos. Além do mais, os procedimentos recursivos ainda chamam “clones” de si mesmos. Assim como precisamos ter cuidado ao usar registradores nos procedimentos, também precisamos ter mais cuidado ao chamar procedimentos não folha. Por exemplo, suponha que o programa principal chame o procedimento A com um argumento 3, colocando o valor 3 no registro $a0 e depois usando jal A. Depois, suponha que o procedimento A chame o procedimento B por meio de jal B com um argumento 7, também colocado em $a0. Como A ainda não terminou sua tarefa, existe um conflito com
92
Capítulo 2 Instruções: A Linguagem de Máquina
relação ao uso do registrador $a0. De modo semelhante, existe um conflito em relação ao endereço de retorno no registrador $ra, pois ele agora tem o endereço de retorno para B. A menos que tomemos medidas para evitar o problema, esse conflito eliminará a capacidade do procedimento A de retornar para o procedimento que o chamou. Uma solução é empilhar todos os outros registradores que precisam ser preservados, assim como fizemos com os registradores salvos. O caller empilha quaisquer registradores de argumento ($a0–$a3) ou registradores temporários ($t0–$t9) que sejam necessários após a chamada. O callee empilha o registrador do endereço de retorno $ra e quaisquer registradores salvos ($s0–$s7) usados por ele. O stack pointer $sp é ajustado para levar em consideração a quantidade de registradores colocados na pilha. No retorno, os registradores são restaurados da memória e o stack pointer é reajustado.
Compilando um procedimento C recursivo, mostrando a ligação do procedimento aninhado
EXEMPLO
Vamos realizar um procedimento recursivo que calcula o fatorial:
Qual é o código assembly do MIPS?
RESPOSTA
O parâmetro n corresponde ao registrador de argumento $a0. O programa compilado começa com o rótulo do procedimento e depois salva dois registradores na pilha, o endereço de retorno e $a0:
Na primeira vez que fact é chamado, sw salva um endereço do programa que chamou fact. As duas instruções seguintes testam se n é menor do que 1, indo para L1 se n ≥ 1 .
Se n for menor do que 1, fact retorna 1, colocando 1 em um registrador de valor: ele soma 1 a 0 e coloca essa soma em $v0. Depois, ele retira os dois valores salvos da pilha e desvia para o endereço de retorno:
2.8 Suporte a procedimentos no hardware do computador 93
Antes de retirar dois itens da pilha, poderíamos ter restaurado $a0 e $ra. Como $a0 e $ra não mudam quando n é menor do que 1, pulamos essas instruções. Se n não for menor do que 1, o argumento n é decrementado e depois fact é chamado novamente com o valor decrementado.
A próxima instrução é onde fact retorna. Agora, o endereço de retorno antigo e o argumento antigo são restaurados, juntamente com o stack pointer:
Em seguida, o registrador de valor $v0 recebe o produto do argumento antigo $a0 e o valor atual do registrador de valor. Consideramos que exista uma instrução de multiplicação à disposição, embora isso não seja explicado antes do Capítulo 3: mul
$v0,$a0,$v0
# retorna n * fact(n − 1)
Finalmente, fact salta novamente para o endereço de retorno: jr
$ra
# retorna para o procedimento que chamou
Uma variável em C é um local na memória e sua interpretação depende tanto do seu tipo quanto da sua classe de armazenamento. Alguns exemplos são inteiros e caracteres (veja Seção 2.9). A linguagem C possui duas classes de armazenamento: automática e estática. As variáveis automáticas são locais a um procedimento e são descartadas quando o procedimento termina. As variáveis estáticas permanecem durante entradas e saídas de procedimentos. As variáveis C declaradas fora de todos os procedimentos são consideradas estáticas, assim como quaisquer variáveis declaradas por meio da palavra reservada static. As outras são automáticas. Para simplificar o acesso aos dados estáticos, o software do MIPS reserva outro registrador, chamado ponteiro global, ou $gp. A Figura 2.11 resume o que é preservado em uma chamada de procedimento. Observe que vários esquemas preservam a pilha, garantindo que o caller receberá os mesmos dados em um load da pilha que foram armazenados nela. A pilha acima de $sp é preservada simplesmente verificando se o procedimento chamado não escreve acima de $sp; $sp é preservado pelo procedimento chamado somando-se exatamente o mesmo valor que foi subtraído dele, e os outros registradores são preservados por serem salvos na pilha (se forem usados) e restaurados de lá.
FIGURA 2.11 O que é e o que não é preservado durante uma chamada de procedimento. Se o software contar com o registrador de frame pointer ou com o registrador de ponteiro global, discutidos nas próximas seções, eles também são preservados.
Interface hardware/ software
ponteiro global O registrador reservado para apontar para a área estática.
94
Capítulo 2 Instruções: A Linguagem de Máquina
Alocando espaço para novos dados na pilha
frame de procedimento Também chamado registro de ativação. O segmento da pilha contendo os registradores salvos e as variáveis locais de um procedimento.
frame pointer Um valor indicando o local dos registradores salvos e as variáveis locais para um determinado procedimento.
A complexidade final é que a pilha também é utilizada para armazenar variáveis que são locais ao procedimento, que não cabem nos registradores, como arrays ou estruturas locais. O segmento da pilha que contém os registradores salvos e as variáveis locais de um procedimento é chamado frame de procedimento, ou registro de ativação. A Figura 2.12 mostra o estado da pilha antes, durante e após a chamada de um procedimento. Alguns softwares MIPS utilizam o frame pointer ($fp) a fim de apontar para a primeira word do registro de ativação de um procedimento. O stack pointer poderia mudar durante o procedimento, e assim as referências a uma variável local na memória poderiam ter offsets diferentes, dependendo de onde estiverem no procedimento, o que torna o procedimento mais difícil de entender. Como alternativa, um frame pointer oferece um registrador base estável dentro de um procedimento para as referências locais à memória. Observe que um registro de ativação aparece na pilha independente de o frame pointer explícito ser utilizado. Evitamos o $fp impedindo mudanças no $sp dentro de um procedimento: em nossos exemplos, a pilha é ajustada apenas na entrada e na saída do procedimento.
FIGURA 2.12 Ilustração da alocação de pilha (a) antes, (b) durante e (c) após a chamada de um procedimento. O frame pointer ($fp) aponta para a primeira palavra do frame, normalmente um registrador de argumento salvo, e o stack pointer ($sp) aponta para o topo da pilha. A pilha é ajustada de modo a criar espaço para todos os registradores salvos e quaisquer variáveis locais residentes na memória. Como o stack pointer pode mudar durante a execução do programa, é mais fácil para os programadores referenciarem variáveis por meio do frame pointer estável, embora isso também pudesse ser feito por meio do stack pointer e um pouco de aritmética de endereços. Se não houver variáveis locais na pilha dentro de um procedimento, o compilador ganhará tempo não atribuindo um endereço ao frame pointer, e depois, restaurando-o. Quando um frame pointer é usado, ele é inicializado usando o endereço que está no $sp em uma chamada, e o $sp é restaurado usando o valor do $fp. Essa informação também aparece na Coluna 4 do Guia de Instrução Rápida do MIPS, no início deste livro.
Alocando espaço para novos dados no heap
segmento de texto O segmento de um arquivo-objeto Unix que contém o código em linguagem de máquina para as rotinas do arquivo-fonte.
Além das variáveis automáticas que são locais aos procedimentos, os programadores C precisam de espaço na memória para as variáveis globais e para estruturas de dados dinâmicas. A Figura 2.13 mostra a convenção do MIPS para a alocação de memória. A pilha começa na parte alta da memória e cresce para baixo. A primeira parte da extremidade baixa da memória é reservada, seguida pelo código de máquinas do MIPS, tradicionalmente denominado segmento de texto. Acima do código existe o segmento de dados estáticos, que é o local para constantes e outras variáveis estáticas. Embora os arrays costumem ter um tamanho fixo e, portanto, correspondam muito bem ao segmento de dados estático,
2.8 Suporte a procedimentos no hardware do computador 95
estruturas de dados como listas encadeadas costumam crescer e diminuir durante suas vidas. O segmento para tais estruturas de dados é tradicionalmente chamado de heap e fica posicionado logo a seguir na memória. Observe que essa alocação permite que a pilha e o heap cresçam um em direção ao outro, permitindo, assim, o uso eficiente da memória enquanto os dois segmentos aumentam e diminuem.
FIGURA 2.13 A alocação de memória do MIPS para programas e dados. Esses endereços são apenas uma convenção do software e não fazem parte da arquitetura MIPS. De cima para baixo, o stack pointer é inicializado com 7fff fffchexa e cresce para baixo, em direção ao segmento de dados. Na outra extremidade, o código do programa (“texto”) começa em 0040 0000 . Os dados estáticos começam em 1000 0000hexa . Os dados dinâmicos, hexa alocados por malloc em C e por new em Java, vêm em seguida e crescem para cima em direção à pilha, em uma área chamada heap. O ponteiro global, $gp, é definido como um endereço para facilitar o acesso aos dados. Ele é inicializado com 1000 8000 para poder acessar de 1000 0000 até 1000 ffffhexa usando os offsets hexa hexa de 16 bits positivos e negativos a partir do $gp. Essa informação também aparece na Coluna 4 do Guia de Instrução Rápida do MIPS, no início deste livro.
A linguagem C aloca e libera espaço no heap com funções explícitas. malloc() aloca espaço no heap e retorna um ponteiro para ela, e free() libera o espaço no heap para o qual o ponteiro está apontando. A alocação da memória é controlada por programas em C e essa é a fonte de muitos bugs comuns e difíceis de serem encontrados. Esquecer de liberar espaço ocasiona um “vazamento de memória”, que, por fim, pode vir a causar a falha do sistema operacional. Liberar espaço muito cedo ocasiona “ponteiros pendentes”, podendo fazer os ponteiros apontarem para áreas que o programa nunca desejou. Java utiliza alocação de memória e coleta de lixo automáticas justamente para evitar esses problemas. A Figura 2.14 resume as convenções de registrador para o assembly do MIPS.
FIGURA 2.14 Convenções de registradores MIPS. O registrador 1, chamado $at , é reservado para o montador (veja Seção 2.12), e os registradores 26-27, chamados $k0-$k1, são reservados para o sistema operacional. Essa informação também aparece na Coluna 2 do Guia de Instrução Rápida do MIPS, no início deste livro.
96
Capítulo 2 Instruções: A Linguagem de Máquina
Detalhamento: e se houver mais do que quatro parâmetros? A convenção do MIPS é colocar os parâmetros extras na pilha, logo acima do stack pointer. O procedimento, então, espera que os quatro primeiros parâmetros estejam nos registradores de $a0 a $a3, e que o restante esteja na memória, endereçável por meio do frame pointer. Conforme dissemos na legenda da Figura 2.12, o frame pointer é conveniente porque todas as referências a variáveis na pilha dentro de um procedimento terão o mesmo offset. Contudo, o frame pointer não é necessário. O compilador C para MIPS sob licença GNU utiliza um frame pointer, mas não o compilador C do MIPS; ele trata o registrador 30 como outro registrador de valor salvo ($a8).
Detalhamento: Alguns procedimentos recursivos podem ser implementados iterativamente sem o uso de recursão. A iteração pode melhorar significativamente o desempenho, removendo o overhead associado a chamadas de procedimento. Por exemplo, considere um procedimento usado para acumular uma soma:
Considere a chamada de procedimento sum(3,0). Isso resultará em chamadas recursivas a sum(2,3), sum(1,5)e sum(0,6), e depois o resultado 6 será retornado quatro vezes. Essa chamada recursiva de sum é conhecida como tail call e esse exemplo de uso da recursão tail pode ser implementado de modo muito eficiente (suponha que $a0 =n e $a1=acc):
Verifique você mesmo
Quais das seguintes afirmações sobre C e Java geralmente são verdadeiras? 1. Os programadores C gerenciam os dados explicitamente, enquanto isso é automático em Java. 2. A linguagem C leva a mais problemas de ponteiro e vazamento de memória do que Java.
2.9 Comunicando-se com as pessoas 97
2.9 Comunicando-se com as pessoas Os computadores foram inventados para devorar números, mas, assim que se tornaram comercialmente viáveis, eles foram usados para processar textos. A maioria dos computadores hoje utiliza bytes de 8 bits para representar caracteres; o American Standard Code for Information Interchange (ASCII) é a representação que quase todos seguem. A Figura 2.15 resume o código ASCII.
!(@ | = > (wow open tab at bar is great) Quarta linha do poema de teclado “Hatless Atlas”, 1991 (alguns dão nomes aos caracteres ASCII: “!” é “wow”, “(” é open, “|” é bar, e assim por diante)
FIGURA 2.15 Representação dos caracteres no código ASCII. Observe que as letras maiúsculas e minúsculas diferem exatamente em 32; essa observação pode levar a atalhos na verificação ou mudança entre maiúsculas e minúsculas. Os valores não mostrados incluem caracteres de formatação. Por exemplo, 8 representa backspace, 9 representa o caractere de tabulação e 13, um carriage return. Outro valor útil é 0 para null, o valor que a linguagem de programação C utiliza para marcar o final de uma string. Essa informação também aparece na Coluna 3 do Guia de Instrução Rápida do MIPS, no início deste livro.
A base 2 não é natural para os seres humanos; temos 10 dedos e por isso achamos a base 10 natural. Por que os computadores não usam decimal? Na verdade, o primeiro computador comercial oferecia aritmética decimal. O problema foi que o computador ainda usava sinais ligado e desligado, de modo que um dígito decimal era simplesmente representado por vários dígitos binários. O decimal provou ser tão ineficaz que os computadores subsequentes reverteram para tudo binário, convertendo para a base 10 somente os eventos de entrada/saída relativamente infrequentes.
Interface hardware/ software
98
Capítulo 2 Instruções: A Linguagem de Máquina
ASCII versus números binários
EXEMPLO
Poderíamos representar números como strings de dígitos ASCII em vez de inteiros. Em quanto o armazenamento aumenta se o número 1 bilhão for representado em ASCII em vez de inteiro com 32 bits?
RESPOSTA
Um bilhão é 1.000.000.000, de modo que ele precisaria de 10 dígitos ASCII, cada um com 8 bits de extensão. Assim, a expansão no armazenamento seria (10 x 8)/32, ou 2,5. Além da expansão no armazenamento, o hardware para somar, subtrair, multiplicar e dividir esses números decimais é difícil. Essas dificuldades explicam por que os profissionais de computação são levados a crer que o binário é natural e que o computador decimal ocasional é bizarro. Diversas instruções podem extrair um byte de uma palavra, de modo que load word e store word são suficientes para transferir bytes e também palavras. Entretanto, em razão da popularidade do texto em alguns programas, o MIPS oferece instruções para mover bytes. Load byte (lb) lê um byte da memória, colocando-o nos 8 bits mais à direita de um registrador. Store byte (sb) separa o byte mais à direita de um registrador e o escreve na memória. Assim, copiamos um byte com a sequência lb $t0,0($sp)
# Lê byte da origem
sb $t0,0($gp)
# Escreve byte no destino
(Conforme veremos, Java utiliza a primeira opção.) Os caracteres normalmente são combinados em strings, que possuem uma quantidade variável de caracteres. Existem três opções para representar uma string: (1) a primeira posição da string é reservada para indicar o tamanho de uma string, (2) uma variável acompanhante possui o tamanho da string (como em uma estrutura), ou (3) a última posição da string é ocupada por um caractere que serve para marcar o final da string. A linguagem C utiliza a terceira opção, terminando uma string com um byte cujo valor é 0 (denominado null, em ASCII). Assim, a string “Cal” é representada em C pelos 4 bytes a seguir, em forma de números decimais: 67, 97, 108, 0.
Interface hardware/ software
A presença ou ausência de sinal aplica-se a loads e também à aritmética. A função de um load com sinal é copiar o sinal repetidamente para preencher o restante do registrador – chamado extensão de sinal –, mas sua finalidade é colocar uma representação correta do número dentro desse registrador. Os loads sem sinal simplificam o preenchimento com 0s à esquerda dos dados, pois o número representado pelo padrão de bits é sem sinal. Ao carregar uma palavra de 32 bits para um registrador de 32 bits, o ponto é irrelevante; loads com sinal e sem sinal são idênticos. O MIPS oferece dois tipos de loads de byte: load byte (lb) trata o byte como um número com sinal e, portanto, estende o sinal para preencher os 24 bits mais à esquerda do registrador, enquanto load byte unsigned (lbu) funciona com inteiros sem sinal. Como os programas em C quase sempre utilizam bytes para representar caracteres, em vez de considerar bytes como inteiros com sinal muito curtos, lbu é usado de modo praticamente exclusivo para loads de byte.
2.9 Comunicando-se com as pessoas 99
Compilando um procedimento de cópia de string, para demonstrar o uso de strings em C
O procedimento strcpy copia a string y para a string x, usando a convenção de término com byte nulo da linguagem C:
EXEMPLO
Qual é o código assembly correspondente no MIPS? A seguir está o segmento básico em código assembly do MIPS. Considere que os endereços base para os arrays x e y são encontrados em $a0 e $a1, enquanto i está em$s0. strcpy ajusta o stack pointer e depois salva o registrador de valores salvos $s0 na pilha:
Para inicializar i como 0, a próxima instrução define $s0 como 0, somando 0 a 0 e colocando essa soma em $s0: add
$s0,$zero,$zero # i = 0 + 0
Esse é o início do loop. O endereço de y[i] é formado inicialmente pela soma de i a y[] : L1:
add $t1,$s0,$a1 # endereço de y[i] em $t1
Observe que não temos de multiplicar i por 4, pois y é um array de bytes, e não de palavras, como nos exemplos anteriores. Para carregar o caracter em y[i], usamos load byte, que coloca o caractere em $t2: lbu
$t2, 0($t1)
# $t2 = y[i]
Um cálculo de endereço semelhante coloca o endereço de x[i] em $t3, e depois o caractere em $t2 é armazenado nesse endereço.
Em seguida, saímos do loop se o caractere foi 0; ou seja, esse é o último caractere da string: beq
$t2,$zero,L2
# se y[i]
Se não, incrementamos i e voltamos ao loop:
==
0, vai para L2
RESPOSTA
100
Capítulo 2 Instruções: A Linguagem de Máquina
Se não voltamos, então esse foi o último caractere da string; restauramos $s0 e o stack pointer, para depois retornar.
As cópias de string normalmente utilizam ponteiros no lugar de arrays em C, para evitar as operações com i no código anterior. Veja, na Seção 2.14, uma explicação sobre arrays e ponteiros. Como o procedimento strcpy anterior é um procedimento folha, o compilador poderia alocar i a um registrador temporário e evitar as operações de salvar e restaurar $s0. Em virtude disso, em vez de pensar nos registradores $t como sendo apenas para temporários, podemos pensar neles como registradores que o procedimento chamado deve utilizar sempre que for conveniente. Quando um compilador encontra um procedimento folha, ele esgota todos os registradores temporários antes de usar registradores que precisa salvar.
Caracteres e strings em Java Unicode é uma codificação universal dos alfabetos da maior parte das linguagens humanas. A Figura 2.16 é uma lista de alfabetos Unicode; existem tantos alfabetos em Unicode quanto símbolos úteis em ASCII. Para ser mais específico, Java utiliza Unicode para os caracteres. Como padrão, ela utiliza 16 bits a fim de representar um caractere. O conjunto de instruções do MIPS possui instruções explícitas para carregar e armazenar quantidades de 16 bits, chamadas halfwords. Load half (lh) lê uma halfword da memória, colocando-a nos 16 bits mais à direita de um registrador. Assim como load byte, load half ( lh ) trata a halfword como um número com sinal e, portanto, estende o sinal para preencher os 16 bits mais à esquerda do registrador, enquanto load halfword unsigned ( lhu ) trabalha com inteiros sem sinal. Assim, lhu é o mais comum dos dois. Store half ( sh ) separa a halfword correspondente aos 16 bits mais à direita de um registrador e a escreve na memória. Copiamos uma halfword com a sequência
As strings são uma classe padrão do Java, com suporte interno especial e métodos predefinidos para concatenação, comparação e conversão. Ao contrário da linguagem C, Java inclui uma palavra que indica o tamanho da string, semelhante aos arrays Java. Detalhamento: o software do MIPS tenta manter a pilha alinhada em endereços de palavra, permitindo que o programa sempre use lw e sw (que precisam estar alinhados) para acessar a pilha. Essa convenção significa que uma variável char alocada na pilha ocupa 4 bytes, embora precise de menos. Contudo, uma variável string ou um array de bytes em C agrupará 4 bytes por palavra, e uma variável string ou array de shorts em Java agrupará 2 halfwords por palavra.
2.10 Endereçamento no MIPS para operandos imediatos e endereços de 32 bits 101
FIGURA 2.16 Exemplos de alfabetos em Unicode. O Unicode versão 4.0 possui mais de 160 “blocos”, que é o nome para uma coleção de símbolos. Cada bloco é um múltiplo de 16. Por exemplo, Grego começa em 0370hexa e Cirílico em 0400hexa. As três primeiras colunas mostram 48 blocos que correspondem a linguagens humanas em ordem numérica aproximada no Unicode. A última coluna possui 16 blocos que são multilinguais e não estão em ordem. Uma codificação de 16 bits, chamada UTF-16, é o padrão. Uma codificação de tamanho variável, chamada UTF-8, mantém o subconjunto ASCII como 8 bits e utiliza 16-32 bits para os outros caracteres. UTF-32 utiliza 32 bits por caractere. Para saber mais sobre isso, consulte www.unicode.org.
I. Quais das seguintes afirmações sobre caracteres e strings em C e Java são verdadeiras? 1. Uma string em C utiliza cerca da metade da memória da mesma string em Java. 2. Strings são apenas um nome informal para arrays de uma única dimensão de caracteres em C e Java. 3. As strings em C e Java utilizam null (0) para marcar o fim de uma string. 4. As operações sobre strings, como saber seu tamanho, são mais rápidas em C do que em Java. II. Que tipo de variável que pode conter 1.000.000.000dec ocupa mais espaço na memória? 1. int em C 2. string em C 3. string em Java
Endereçamento no MIPS para operandos
2.10 imediatos e endereços de 32 bits
Embora manter todas as instruções MIPS com 32 bits simplifique o hardware, existem ocasiões em que seria conveniente ter uma constante de 32 bits ou endereço de 32 bits. Esta seção começa com a solução geral para constantes grandes e depois apresenta as otimizações para endereços de instruções usados em desvios condicionais e jumps.
Operandos imediatos de 32 bits Embora as constantes normalmente sejam curtas e caibam em um campo de 16 bits, às vezes elas são maiores. O conjunto de instruções MIPS inclui a instrução load upper
Verifique você mesmo
102
Capítulo 2 Instruções: A Linguagem de Máquina
immediate (lui) especificamente para atribuir os 16 bits mais altos de uma constante a um registrador, permitindo que uma instrução subsequente atribua os 16 bits mais baixos da constante. A Figura 2.17 mostra a operação de lui.
FIGURA 2.17 O efeito da instrução lui. A instrução lui transfere o valor do campo de constante imediata de 16 bits para os 16 bits mais à esquerda do registrador, preenchendo os 16 bits de menor ordem (direita) com 0s.
Interface hardware/ software
Tanto o compilador quanto o montador precisam desmembrar constantes grandes em partes e depois remontá-las em um registrador. Como você poderia esperar, a restrição de tamanho do campo imediato pode ser um problema para endereços de memória em loads e stores, e também para constantes em instruções imediatas. Se esse trabalho recair para o montador, como acontece para o software do MIPS, então o montador precisa ter um registrador temporário disponível, onde criará valores longos. Esse é um motivo para o registrador $at, que é reservado para o montador. Logo, a representação simbólica da linguagem de máquina do MIPS não está mais limitada pelo hardware, mas a qualquer coisa que o criador de um montador decidir incluir (veja Seção 2.12). Vamos examinar o hardware de perto para explicar a arquitetura do computador, indicando quando usarmos a linguagem avançada do montador que não se encontra no processador.
Carregando uma constante de 32 bits
EXEMPLO
Qual é o código assembly do MIPS para carregar esta constante de 32 bits no registrador $s0? 0000 0000 0011 1101 0000 1001 0000 0000
RESPOSTA
Primeiro, carregaríamos os 16 bits mais altos, que é 61 em decimal, usando a instrução lui: lui $s0, 61
# 61 decimal = 0000 0000 0011 1101 binário
O valor do registrador $s0 depois disso é 0000 0000 0011 1101 0000 0000 0000 0000
O próximo passo é acrescentar os 16 bits inferiores, cujo valor decimal é 2304: ori $s0, $s0, 2304
# 2304 decimal = 0000 1001 0000 0000
O valor final no registrador $s0 é o valor desejado: 0000 0000 0011 1101 0000 1001 0000 0000
2.10 Endereçamento no MIPS para operandos imediatos e endereços de 32 bits 103
Detalhamento: a criação de constantes de 32 bits requer cuidado. A instrução addi copia o bit mais à esquerda do campo imediato de 16 bits da instrução para todos os 16 bits mais altos de uma palavra. O OR immediate lógico, da Seção 2.6, carrega 0s nos 16 bits superiores e, portanto, é usado pelo montador em conjunto com lui para criar constantes de 32 bits.
Endereçamento em desvios condicionais e jumps As instruções de jump no MIPS possuem o endereçamento mais simples possível. Elas utilizam o último formato de instrução do MIPS, chamado tipo J, que consiste em 6 bits para o campo de operação e o restante dos bits para o campo de endereço. Assim, j 10000 # vai para a posição 10000
poderia ser gerada neste formato (normalmente, isso é um pouco mais complicado, como veremos):
em que o valor do código da operação de jump é 2 e o endereço destino é 10000. Ao contrário da instrução de jump, a instrução de desvio condicional precisa especificar dois operandos além do endereço de desvio. Assim, bne $s0,$s1,Exit
# vai para Exit se $s0 ≠ $s1
é gerada nesta instrução, deixando apenas 16 bits para o endereço de desvio:
Se os endereços do programa tivessem de caber nesse campo de 16 bits, nenhum programa poderia ser maior do que 216, que é muito pequeno para ser uma opção real nos dias atuais. Uma alternativa seria especificar um registrador que sempre seria somado ao endereço de desvio, de modo que uma instrução de desvio pudesse calcular o seguinte: Contador de programa = Registrador + Endereçodedesvio Essa soma permite que o contador de programa tenha até 232 bits e ainda possa usar desvios condicionais, solucionando o problema do tamanho do endereço de desvio. A questão, portanto, é: qual registrador? A resposta vem da observação de como os desvios condicionais são usados. Eles são encontrados em loops e em instruções if, de modo que costumam desviar para uma instrução próxima. Por exemplo, cerca de metade de todos os desvios condicionais nos benchmarks SPEC vão para locais a menos de 16 instruções de distância. Como o contador de programa (PC) contém o endereço da instrução atual, podemos desviar dentro de ±215 palavras da instrução atual se usarmos o PC como o registrador a ser somado ao endereço. Quase todos os loops e as instruções if são muito menores do que 216 palavras, de modo que o PC é a opção ideal.
104
Capítulo 2 Instruções: A Linguagem de Máquina
endereçamento relativo ao PC Um regime de endereçamento em que o endereço é a soma do contador de programa (PC) e uma constante na instrução.
Essa forma de endereçamento de desvio é denominada endereçamento relativo ao PC. Conforme veremos no Capítulo 4, é conveniente que o hardware incremente o PC desde cedo, a fim de que aponte para a próxima instrução. Logo, o endereço MIPS, na realidade, é relativo ao endereço da instrução seguinte (PC + 4), em vez da instrução atual (PC). Como na maioria dos computadores atuais, o MIPS utiliza o endereçamento relativo ao PC para todos os desvios condicionais, pois o destino dessas instruções provavelmente estará próximo do desvio. Por outro lado, instruções de jump-and-link chamam procedimentos que não têm motivo para estarem próximos à chamada e, por isso, normalmente utilizam outras formas de endereçamento. Logo, a arquitetura MIPS oferece endereços longos para chamadas de procedimento, usando o formato do tipo J para instruções de jump e jump-and-link. Como todas as instruções MIPS possuem 4 bytes de extensão, o MIPS aumenta a distância do desvio fazendo com que o endereçamento relativo ao PC se refira ao número de palavras até a próxima instrução, no lugar do número de bytes. Assim, o campo de 16 bits pode se desviar para uma distância quatro vezes maior, interpretando o campo como um endereço relativo à palavra, em vez de um endereço relativo a byte. De modo semelhante, o campo de 26 bits nas instruções de jump também é um endereço de palavra, significando que representa um endereço de 28 bits. Detalhamento: como o contador de programa (PC) utiliza 32 bits, 4 bits precisam vir de outro lugar para os jumps. A instrução de jump do MIPS substitui apenas os 28 bits menos significativos do PC, deixando os 4 bits mais significativos inalterados. O loader e o link-editor (Seção 2.12) precisam ter cuidado para evitar colocar um programa entre um limite de endereços de 256MB (64 milhões de instruções); caso contrário, um jump precisa ser substituído por uma instrução de jump register precedida por outras instruções, a fim de carregar o endereço de 32 bits inteiro em um registrador.
Mostrando o offset do desvio em linguagem de máquina
EXEMPLO
O loop while da Seção 2.7 foi compilado para este código em assembly do MIPS:
RESPOSTA
Se consideramos que o loop inicia na posição 80000 da memória, qual é o código de máquina do MIPS para esse loop? As instruções montadas e seus endereços são:
2.10 Endereçamento no MIPS para operandos imediatos e endereços de 32 bits 105
Lembre-se de que as instruções do MIPS possuem endereços em bytes, de modo que os endereços das palavras sequenciais diferem em 4, a quantidade de bytes em uma palavra. A instrução bne na quarta linha acrescenta 2 palavras ou 8 bytes ao endereço da instrução seguinte (80016), especificando o destino do desvio em relação à instrução seguinte (8 + 80016), e não em relação à instrução de desvio (12 + 80012) ou ao uso do endereço de destino completo (80024). A instrução de salto na última linha utiliza o endereço completo (20000 × 4 = 80000), correspondente ao rótulo Loop.
A maioria dos desvios condicionais é feita para um local nas proximidades, mas, ocasionalmente, eles se desviam para um ponto mais distante do que pode ser representado nos 16 bits da instrução de desvio condicional. O montador vem ao auxílio como fez com endereços ou constantes grandes: ele insere um jump incondicional para o destino do desvio, e inverte a condição de modo que o desvio decida para onde fazer o jump.
Interface hardware/ software
Desviando para um lugar mais distante
Dado um desvio em que o registrador $s0 é igual ao registrador $s1,
EXEMPLO
beq $s0, $s1, L1
substitua-o por um par de instruções que ofereça uma distância de desvio muito maior. Estas instruções substituem o desvio condicional com endereço curto:
RESPOSTA
Resumo dos modos de endereçamento no MIPS Múltiplas formas de endereçamento geralmente são denominadas modos de endereçamento. A Figura 2.18 mostra como os operandos são identificados para cada modo de endereçamento. Os modos de endereçamento do MIPS são os seguintes: 1. Endereçamento imediato, em que o operando é uma constante dentro da própria instrução 2. Endereçamento em registrador, no qual o operando é um registrador 3. Endereçamento de base ou deslocamento, em que o operando está no local de memória cujo endereço é a soma de um registrador e uma constante na instrução 4. Endereçamento relativo ao PC, no qual o endereço de desvio é a soma do PC e uma constante na instrução 5. Endereçamento pseudodireto, em que o endereço de jump são os 26 bits da instrução concatenados com os bits mais altos do PC
modo de endereçamento Um dos diversos regimes de endereçamento delimitados por seu uso variado de operandos e/ou endereços.
106
Capítulo 2 Instruções: A Linguagem de Máquina
FIGURA 2.18 Ilustração dos cinco modos de endereçamento do MIPS. Os operandos estão sombreados na figura. O operando do modo 3 está na memória, enquanto o operando para o modo 2 é um registrador. Observe que as versões de load e store acessam bytes, halfwords ou palavras. Para o modo 1, o operando é formado pelos 16 bits da própria instrução. Os modos 4 e 5 endereçam as instruções na memória, com o modo 4 adicionando um endereço de 16 bits deslocado à esquerda em 2 bits ao PC, e o modo 5 concatenando um endereço de 26 bits deslocado à esquerda em 2 bits com os 4 bits superiores do PC.
Interface hardware/ software
Embora tenhamos mostrado a arquitetura MIPS como tendo endereços de 32 bits, quase todos os microprocessadores (incluindo o MIPS) possuem extensões de endereço de 64 bits (ver o Apêndice E). Essas extensões foram a resposta às necessidades de software para programas maiores. O processo de extensão do conjunto de instruções permite que as arquiteturas se expandam de modo que o software possa prosseguir de forma compatível para a próxima geração da arquitetura. Observe que uma única operação pode usar mais de um modo de endereçamento. Add, por exemplo, utiliza um endereçamento imediato (addi) e por registrador (add).
Decodificando a linguagem de máquina Às vezes, você é forçado a usar engenharia reversa na linguagem de máquina para criar o código assembly original. Um exemplo é quando se examina um dump de memória. A Figura 2.19 mostra a codificação dos campos para a linguagem de máquina do MIPS. Essa figura ajuda na tradução manual entre o assembly e a linguagem de máquina.
2.10 Endereçamento no MIPS para operandos imediatos e endereços de 32 bits 107
FIGURA 2.19 Codificação de instruções do MIPS. Essa notação indica o valor de um campo por linha e por coluna. Por exemplo, a parte superior da figura mostra load word na linha número 4 (100bin para os bits 31-29 da instrução) e a coluna número 3 (011bin para os bits 28-26 da instrução), de modo que o valor correspondente do campo op (bits 31-26) é 100011bin. Formato R na linha 0 e coluna 0 (op = 000000bin) é definido na parte inferior da figura. Logo, subtract na linha 4 e coluna 2 da seção inferior significa que o campo funct (bits 5-0) da instrução é 100010bin e o campo op (bits 31-26) é 000000bin. O valor de ponto flutuante na linha 2, coluna 1, é definido na Figura 3.18, no Capítulo 3. Bltz/gez é o opcode para quatro instruções encontradas no Apêndice B: bltz, bgez, bltzal e bgezal. Este capítulo descreve as instruções indicadas com nome completo usando cor, enquanto o Capítulo 3 descreve as instruções indicadas em mnemônicos usando cor. O Apêndice B abrange todas as instruções.
108
Capítulo 2 Instruções: A Linguagem de Máquina
Decodificando a linguagem de máquina
EXEMPLO
Qual é a instrução em assembly correspondente a esta instrução de máquina? 00af8020hexa
RESPOSTA
O primeiro passo na conversão de hexadecimal para binário é encontrar os campos op:
Examinamos o campo op para determinar a operação. Consultando a Figura 2.19, quando os bits 31-29 são 000 e os bits 28-26 são 000, essa é uma instrução no Formato R. Vamos reformatar a instrução binária para campos no Formato R, listado na Figura 2.20:
A parte inferior da Figura 2.19 determina a operação de uma instrução no Formato R. Nesse caso, os bits 5-3 são 100 e os bits 2-0 são 000, o que significa que esse padrão binário representa uma instrução add. Decodificamos as demais instruções examinando os valores de campo. Os valores decimais são 5 para o campo rs, 15 para rt, 16 para rd (shamt não é usado). A Figura 2.14 diz que esses números representam os registradores $a1, $a7 e $s0. Agora, podemos mostrar a instrução assembly: add $s0, $a1, $t7
A Figura 2.20 mostra todos os formatos de instrução do MIPS. A Figura 2.1 mostra o assembly do MIPS revelado neste capítulo; a outra parte ainda oculta das instruções MIPS trata principalmente de aritmética e números reais, abordados no próximo capítulo.
FIGURA 2.20 Formatos das instruções do MIPS.
Verifique você mesmo
I. Qual é o intervalo de endereços para desvios condicionais no MIPS (K = 1024)? 1. Endereços entre 0 e 64K – 1 2. Endereços entre 0 e 256K – 1 3. Endereços desde cerca de 32K antes do desvio até cerca de 32K depois 4. Endereços desde cerca de 128K antes do desvio até cerca de 128K depois
2.11 Paralelismo e instruções: Sincronização 109
II. Qual é o intervalo de endereços para jump e jump-and-link no MIPS (M = 1024K)? 1. Endereços entre 0 e 64M – 1 2. Endereços entre 0 e 256M – 1 3. Endereços desde cerca de 32M antes do desvio até cerca de 32M depois 4. Endereços desde cerca de 128M antes do desvio até cerca de 128M depois 5. Qualquer lugar dentro de um bloco de 64M endereços, em que o PC fornece os 6 bits mais altos 6. Qualquer lugar dentro de um bloco de 256M endereços, em que o PC fornece os 4 bits mais altos III. Qual é a instrução em assembly do MIPS correspondente à instrução de máquina com o valor 0000 0000hexa? 1. j 2. Formato R 3. addi 4. sll 5. mfc0 6. Opcode indefinido: não existe uma instrução válida que corresponda a 0.
2.11 Paralelismo e instruções: Sincronização A execução paralela é mais fácil quando as tarefas são independentes, mas frequentemente elas precisam cooperar. A cooperação normalmente significa que algumas tarefas estão escrevendo novos valores que outras precisam ler. Para saber quando uma tarefa terminou de escrever, de modo que é seguro que outra tarefa leia, elas precisam de sincronização. Se elas não estiverem sincronizadas, haverá um perigo de data race, em que os resultados do programa podem mudar, dependendo de como os eventos ocorram. Por exemplo, lembre-se da analogia citada no Capítulo 1 dos oito repórteres escrevendo um artigo. Suponha que um repórter precise ler todas as seções anteriores antes de escrever uma conclusão. Logo, temos de saber quando os outros repórteres terminaram suas seções, de modo que ele não se preocupe se será alterado depois disso. Ou seja, é melhor que eles sincronizem a escrita e leitura de cada seção, para que a conclusão seja coerente com o que é impresso nas seções anteriores. Na computação, os mecanismos de sincronização normalmente estão embutidos com as rotinas de software em nível de usuário que contam com as instruções de sincronização fornecidas pelo hardware. Nesta seção, focalizamos a implementação das operações de sincronização lock e unlock. Lock e unlock podem ser usados facilmente para criar regiões nas quais apenas um único processador possa operar, chamada exclusão mútua, além de implementar mecanismos de sincronização mais complexos. A habilidade fundamental que exigimos para implementar a sincronização em um multiprocessador é um conjunto de primitivos de hardware com a capacidade de ler e modificar um local de memória atomicamente. Ou seja, nada mais pode se interpor entre a leitura e a escrita do local da memória. Sem essa capacidade, o custo da montagem de primitivos de sincronização básicos será muito alto e aumentará à medida que crescer a quantidade de processadores.
data race Dois acessos à memória formam uma data race se eles forem de threads diferentes para o mesmo local, pelo menos um é de escrita, e eles ocorrem um após o outro.
110
Capítulo 2 Instruções: A Linguagem de Máquina
Existem diversas formulações alternativas das primitivas de hardware básicas, todas oferecendo a capacidade de ler e modificar um local atomicamente, junto com algum modo de dizer se a leitura e escrita foram realizadas atomicamente. Em geral, os arquitetos não esperam que os usuários empreguem as primitivas de hardware básicas, mas em vez disso esperam que as primitivas sejam usadas pelos programadores de sistemas para montar uma biblioteca de sincronização, um processo que normalmente é complexo e intricado. Vamos começar com uma primitiva de hardware desse tipo, mostrando como ela pode ser usada para montar um primitivo de sincronização básico. Uma operação típica para a montagem de operações de sincronização é a troca atômica ou swap atômico, que troca um valor em um registrador por um valor na memória. Para ver como usar isso a fim de montar uma primitiva de sincronização básica, suponha que queremos montar um bloqueio simples, em que o valor 0 é usado para indicar que o bloqueio está livre e 1 é usado para indicar que o bloqueio está indisponível. Um processador tenta definir o bloqueio realizando uma troca de 1, que está em um registrador, com o endereço de memória correspondendo ao bloqueio. O valor retornado da instrução de troca é 1 se algum outro processador já tiver solicitado acesso, e 0 em caso contrário. No segundo caso, o valor também é trocado para 1, impedindo que qualquer outra troca em outro processador também recupere um 0. Por exemplo, considere dois processadores que tentam cada um realizar a troca simultaneamente; essa race é interrompida, pois exatamente um dos processadores realizará a troca primeiro, retornando 0, e o segundo processador retornará 1 quando realizar a troca. A chave para o uso da primitiva de troca para implementar a sincronização é que a operação seja atômica: a troca é indivisível, e duas trocas simultâneas serão ordenadas pelo hardware. É impossível que dois processadores tentando definir a variável de sincronização dessa maneira pensem que definiram a variável simultaneamente. A implementação de uma única operação de memória atômica apresenta alguns desafios no projeto do processador, pois requer uma leitura e uma escrita na memória em uma única instrução ininterrupta. Uma alternativa é ter um par de instruções em que a segunda instrução retorna um valor mostrando se o par de instruções foi executado como se fosse atômico. O par de instruções é efetivamente atômico se parecer que todas as outras operações executadas por qualquer processador ocorreram antes ou depois do par. Assim, quando um par de instruções é efetivamente atômico, nenhum outro processador pode alterar o valor entre o par de instruções. No MIPS, esse par de instruções inclui um load especial, chamado load vinculado, e um store especial, chamado store condicional. Essas instruções são usadas em sequência: se o conteúdo do local de memória especificado pelo load vinculado for alterado antes que ocorra o store condicional para o mesmo endereço, então o store condicional falha. O store condicional é definido para armazenar o valor de um registrador na memória e alterar o valor desse registrador para 1 se tiver sucesso e para 0 se falhar. Como o load vinculado retorna o valor inicial, e o store condicional retorna 1 somente se tiver sucesso, a sequência a seguir implementa uma troca atômica no local de memória especificado pelo conteúdo de $s1:
Ao final dessa sequência, o conteúdo de $s4 e o local de memória especificado por $s1 foram trocados atomicamente. Sempre que um processador intervém e modifica o valor na memória entre as instruções ll e sc, o script retorna 0 em $t0, fazendo com que a sequência de código tente novamente.
2.12 Traduzindo e iniciando um programa 111
Detalhamento: Embora fosse apresentada para sincronização de multiprocessador, a troca atômica também é útil para o sistema operacional lidar com múltiplos processos em um único processador. A fim de garantir que nada interfira em um único processador, o store condicional também falha se o processador realizar uma troca de contexto entre as duas instruções (veja Capítulo 5). Como o store condicional falhará após outro store tentado ao endereço do load vinculado ou qualquer exceção, deve-se ter o cuidado na escolha de quais instruções são inseridas entre as duas instruções. Em particular, somente instruções registrador-registrador podem ser permitidas com segurança; caso contrário, é possível criar situações de impasse, em que o processador nunca pode completar o sc, em consequência das faltas de página repetidas. Além disso, o número de instruções entre o load vinculado e o store condicional deve ser pequeno, para minimizar a probabilidade de que um evento não relacionado ou um processador concorrente faça com que o store condicional falhe com frequência. Uma vantagem do mecanismo de load vinculado/store condicional é que ele pode ser usado para montar outras primitivas de sincronização, como compare e swap atômicos ou fetch-e-increment atômicos, que são usados em alguns modelos de programação paralela. Estes envolvem mais instruções entre o ll e o sc.
Verifique você mesmo 1. Quando threads em cooperação de um programa paralelo precisarem ser sincro-
Quando você usará primitivas como load vinculado e store condicional?
nizados para obter um comportamento apropriado para leitura e escrita de dados compartilhados 2. Quando processos em cooperação em um processador precisarem ser sincronizados para a leitura e escrita de dados compartilhados
2.12 Traduzindo e iniciando um programa Esta seção descreve as quatro etapas para a transformação de um programa em C, armazenado em um arquivo no disco, para um programa executando em um computador. A Figura 2.21 mostra a hierarquia de tradução. Alguns sistemas combinam essas etapas para reduzir o tempo de tradução, mas elas são as quatro fases lógicas pelas quais os programas passam. Esta seção segue essa hierarquia de tradução.
Compilador O compilador transforma o programa C em um programa em assembly, uma forma simbólica daquilo que a máquina entende. Os programas em linguagem de alto nível usam muito menos linhas de código do que o assembly, de modo que a produtividade do programador é muito mais alta. Em 1975, muitos sistemas operacionais e montadores foram escritos em linguagem assembly, pois as memórias eram pequenas e os compiladores eram ineficientes. O aumento de 500.000 vezes na capacidade de memória em um único chip de DRAM reduz os problemas com tamanho do programa e os compiladores otimizadores de hoje podem produzir programas em assembly quase tão bem quanto um especialista em assembly, e às vezes ainda melhores, para programas grandes.
linguagem assembly Uma linguagem simbólica, que pode ser traduzida para o formato binário.
Montador Como a linguagem assembly é a interface com o software de nível superior, o montador também pode cuidar de variações comuns das instruções em linguagem de máquina como se fossem instruções propriamente ditas. O hardware não precisa implementar essas instruções; porém, seu aparecimento em assembly simplifica a tradução e a programação. Essas instruções são conhecidas como pseudoinstruções.
pseudoinstrução Uma variação comum das instruções em assembly, normalmente tratada como se fosse uma instrução propriamente dita.
112
Capítulo 2 Instruções: A Linguagem de Máquina
FIGURA 2.21 Uma hierarquia de tradução para a linguagem C. Um programa em linguagem de alto nível é inicialmente compilado para um programa em assembly e depois montado em um módulo objeto em linguagem de máquina. O link-editor combina vários módulos com as rotinas de biblioteca para resolver todas as referências. O loader, então, coloca o código de máquina nos locais apropriados da memória, de modo a ser executado pelo processador. Para agilizar o processo de tradução, algumas etapas são puladas ou combinadas. Alguns compiladores produzem módulos-objeto diretamente e alguns sistemas utilizam loaders com link-editores, que realizam as duas últimas etapas. A fim de identificar o tipo de arquivo, o UNIX segue uma convenção de sufixo para os arquivos: os arquivos-fonte em C são chamados x.c, os arquivos em assembly são x.s, os arquivos-objeto são x.o, as rotinas de biblioteca link-editadas estaticamente são x.so, e os arquivos executáveis, como padrão, são chamados a.out. O MS-DOS utiliza os sufixos .C, .ASM, .OBJ, .LIB, .DLL e .EXE para indicar a mesma coisa.
Como já dissemos, o hardware do MIPS garante que o registrador $zero sempre tenha o valor 0. Ou seja, sempre que o registrador $zero é utilizado, ele fornece um 0, e o programador não pode alterar o valor do registrador $zero. O registrador $zero é usado para criar a instrução move em assembly, que copia o conteúdo de um registrador para outro. Assim, o montador do MIPS aceita esta instrução, embora ela não se encontre na arquitetura do MIPS: move $t0, $t1
# registrador $t0 recebe reg. $t1
O montador converte essa instrução em assembly para o equivalente em linguagem de máquina da seguinte instrução: add $t0,$zero,$t1
# registrador $t0 recebe 0 + reg. $t1
O montador do MIPS também converte blt (branch on less than) para as duas instruções slt e bne mencionadas no segundo exemplo da Seção 2.5. Outros exemplos são bgt, bge e ble. Ele também converte desvios a locais distantes para um desvio e um jump. Como já dissemos, o montador do MIPS permite que constantes de 32 bits sejam atribuídas a um registrador, apesar do limite de 16 bits das instruções imediatas. Resumindo, as pseudoinstruções dão ao MIPS um conjunto mais rico de instruções assembly do que é implementado pelo hardware. O único custo é reservar um registrador, $at, para ser usado pelo montador. Se você tiver de escrever programas em assembly, use pseudoinstruções de modo a simplificar seu trabalho. Contudo, para entender a arquitetura do MIPS e ter certeza de que obterá o melhor desempenho, estude as instruções reais do MIPS, encontradas nas Figuras 2.1 e 2.19.
2.12 Traduzindo e iniciando um programa 113
Os montadores também aceitarão números em diversas bases. Além de binário e decimal, eles normalmente aceitam uma base mais sucinta do que o binário, mas que seja convertida facilmente para um padrão de bits. Esses recursos são convenientes, mas a tarefa principal de um montador é a montagem para código de máquina. O montador transforma o programa assembly em um arquivo objeto, que é uma combinação de instruções de linguagem de máquina, dados e informações necessárias a fim de colocar instruções corretamente na memória. Para produzir a versão binária de cada instrução no programa em assembly, o montador precisa determinar os endereços correspondentes a todos os rótulos. Os montadores registram os rótulos utilizados nos desvios e nas instruções de transferência de dados por meio de uma tabela de símbolos. Como você poderia esperar, a tabela contém pares de símbolo e endereço. O arquivo-objeto para os sistemas UNIX normalmente contém seis partes distintas: j
O cabeçalho do arquivo objeto descreve o tamanho e a posição das outras partes do arquivo objeto.
j
O segmento de texto contém o código na linguagem de máquina.
j
O segmento de dados estáticos contém os dados alocados por toda a vida do programa. (O UNIX permite que os programas usem dados estáticos, que são alocados para o programa inteiro, ou dados dinâmicos, que podem crescer ou diminuir conforme a necessidade do programa. Veja a Figura 2.13.)
j
As informações de relocação identificam instruções e palavras de dados que dependem de endereços absolutos quando o programa é carregado na memória.
j
A tabela de símbolos contém os rótulos restantes que não estão definidos, como referências externas.
j
As informações de depuração contêm uma descrição resumida de como os módulos foram compilados, para o depurador poder associar as instruções de máquina aos arquivos-fonte em C e tornar as estruturas de dados legíveis.
tabela de símbolos Uma tabela que combina nomes de rótulos aos endereços das palavras na memória ocupados pelas instruções.
A próxima subseção mostra como juntar rotinas que já foram montadas, como as rotinas de biblioteca.
Link-editor O que apresentamos até aqui sugere que uma única mudança em uma linha de um procedimento exige a compilação e a montagem do programa inteiro. A tradução completa é um desperdício terrível de recursos computacionais. Essa repetição é um desperdício principalmente para rotinas de bibliotecas padrão, pois os programadores estariam compilando e montando rotinas que, por definição, quase nunca mudam. Uma alternativa é compilar e montar cada procedimento de forma independente, de modo que uma mudança em uma linha só exija a compilação e a montagem de um procedimento. Essa alternativa requer um novo programa de sistema, chamado link-editor, ou linker, que apanha todos os programas em linguagem de máquina montados independentes e os “remenda”. Existem três etapas realizadas por um link-editor: 1. Colocar os módulos de código e dados simbolicamente na memória. 2. Determinar os endereços dos rótulos de dados e instruções. 3. Remendar as referências internas e externas. O link-editor utiliza a informação de relocação e a tabela de símbolos em cada módulo objeto para resolver todos os rótulos indefinidos. Essas referências ocorrem em instruções de desvio e endereços de dados, de modo que a tarefa desse programa é muito semelhante à de um editor: ele encontra os endereços antigos e os substitui pelos novos. A edição é a
linker Também chamado link-editor, é um programa de sistema que combina programas em linguagem de máquina montados de maneira independente e traduz todos os rótulos indefinidos para um arquivo executável.
114
Capítulo 2 Instruções: A Linguagem de Máquina
arquivo executável Um programa funcional no formato de um arquivo-objeto, que não contém referências não resolvidas. Ele pode conter tabelas de símbolos e informações de depuração. Um “executável despido” não contém essa informação. As informações de relocação podem ser incluídas para o loader.
origem do nome “link-editor”, ou linker para abreviar. O uso de um link-editor faz sentido porque é muito mais rápido remendar o código do que recompilá-lo e remontá-lo. Se todas as referências externas forem resolvidas, o link-editor em seguida determina os locais da memória que cada módulo ocupará. Lembre-se de que a Figura 2.13 mostra a convenção do MIPS para alocação de programas e dados na memória. Como os arquivos foram montados isoladamente, o montador não poderia saber onde as instruções e os dados do módulo serão colocados em relação a outros módulos. Quando o link-editor coloca um módulo na memória, todas as referências absolutas, ou seja, endereços de memória que não são relativos a um registrador, precisam ser relocadas a fim de refletir seu verdadeiro local. O link-editor produz um arquivo executável que pode ser executado em um computador. Normalmente, esse arquivo possui o mesmo formato de um arquivo-objeto, exceto que não contém referências não resolvidas. É possível ter arquivos parcialmente link-editados, como rotinas de biblioteca, que ainda possuem endereços não resolvidos e, portanto, resultam em arquivos-objeto.
Link-edição de arquivos-objeto
EXEMPLO
Link-edite os dois arquivos-objeto a seguir. Mostre os endereços atualizados das primeiras instruções do arquivo executável gerado. Mostramos as instruções em assembly só para tornar o exemplo compreensível; na realidade, as instruções seriam números. Observe que, nos arquivos-objeto, destacamos os endereços e símbolos que precisam ser atualizados no processo de link-edição: as instruções que se referem a endereços dos procedimentos A e B e as instruções que se referem aos endereços das words de dados X e Y. Cabeçalho do arquivo-objeto Nome
Segmento de texto
Segmento de dados
Informações de relocação
Tabela de símbolos
Procedimento
A
Tamanho do texto
100hexa
Tamanho dos dados
20 hexa
Endereço
Instrução
0
lw $a0, 0($gp)
4
jal 0
…
…
0
(X)
…
…
Endereço
Tipo de instrução
Dependência
0
lw
X B
4
jal
Rótulo
Endereço
X
–
B
–
Nome
Procedimento B
Tamanho do texto
200 hexa
Tamanho dos dados
30 hexa
Cabeçalho do arquivo-objeto
2.12 Traduzindo e iniciando um programa 115
Segmento de texto
Endereço
Instrução
0
sw $al, 0($gp)
4
jal 0
…
…
Segmento de dados
0
(Y)
…
…
Informações de relocação
Endereço
Tipo de instrução
Dependência
0
sw
Y
4
jal
A
Rótulo
Endereço
Y
–
A
–
Tabela de símbolos
O procedimento A precisa encontrar o endereço para a variável cujo rótulo é X a fim de colocá-lo na instrução load e encontrar o endereço do procedimento B para colocá-lo na instrução jal. O procedimento B precisa do endereço da variável cujo rótulo é Y para a instrução store e o endereço do procedimento A para sua instrução jal. Pela Figura 2.13, sabemos que o segmento de texto começa no endereço 40 0000hexa e o segmento de dados em 1000 0000hexa. O texto do procedimento A é colocado no primeiro endereço e seus dados no segundo. O cabeçalho do arquivo-objeto para o procedimento A diz que seu texto possui 100hexa bytes e seus dados possuem 20hexa bytes, de modo que o endereço inicial para o texto do procedimento B é 40 0100hexa e seus dados começam em 1000 0020hexa . Cabeçalho do arquivo executável Tamanho do texto
Segmento de texto
Segmento de dados
300hexa
Tamanho dos dados
50hexa
Endereço
Instrução
0040 0000hexa
lw $a0, 8000hexa($gp)
0040 0004hexa
jal 40 0100hexa
…
…
0040 0100hexa
sw $a1, 8020hexa($gp)
0040 0104hexa
jal 40 0000hexa
…
…
Endereço 1000 0000hexa
(X)
…
…
1000 0020hexa
(Y)
…
…
RESPOSTA
116
Capítulo 2 Instruções: A Linguagem de Máquina
A Figura 2.13 também mostra que o segmento de texto começa no endereço
40 0000hexa e o segmento de dados no 1000 0000hexa . O texto do procedimento A
é colocado no primeiro endereço e seus dados no segundo. O cabeçalho do arquivo objeto para o procedimento A diz que seu texto é 100hexa bytes e seus dados 20hexa bytes, então o começo do endereço do texto do procedimento B é 40 0100hexa , e seus dados começam em 1000 0020hexa . Agora, o link-editor atualiza os campos de endereço das instruções. Ele usa o campo de tipo de instrução para saber o formato do endereço a ser editado. Temos dois tipos aqui: 1. Os jal são fáceis porque utilizam o endereçamento pseudodireto. O jal no 40 0004hexa recebe 40 0100hexa (o endereço do procedimento B ) em seu campo de endereço, e o jal em 40 0104hexa recebe 40 0000hexa (o endereço do procedimento A) em seu campo de endereço. 2. Os endereços de load e store são mais difíceis, pois são relativos a um registrador de base. Este exemplo utiliza o ponteiro global como registrador de base. A Figura 2.13 mostra que $gp é inicializado com 1000 8000hexa . Para obter o endereço 1000 0000hexa (o endereço da palavra X), colocamos 8000hexa no campo de endereço da instrução lw, no endereço 40 0000hexa . De modo semelhante, colocamos 8020hexa no campo de endereço da instrução sw no endereço 40 0100hexa para obter o endereço 1000 0020hexa (o endereço da palavra Y).
Detalhamento: Lembre-se de que as instruções MIPS são alinhadas na palavra, de modo que
jal remove os dois bits da direita para aumentar a faixa de endereços da instrução. Assim, ele usa 26 bits para criar um endereço de byte de 28 bits. Logo, o endereço real nos 26 bits inferiores da instrução jal neste exemplo é 10 0040hexa , em vez de 40 0100hexa .
Loader loader Um programa de sistema
Agora que o arquivo executável está no disco, o sistema operacional o lê para a memória e o inicia. O loader segue estas etapas nos sistemas UNIX:
que coloca o programa-objeto na memória principal, de modo que esteja pronto para ser executado.
1. Lê o cabeçalho do arquivo executável para determinar o tamanho dos segmentos de texto e de dados. 2. Cria um espaço de endereçamento grande o suficiente para o texto e os dados. 3. Copia as instruções e os dados do arquivo executável para a memória. 4. Copia os parâmetros (se houver) do programa principal para a pilha. 5. Inicializa os registradores da máquina e define o stack pointer para o primeiro local livre. 6. Desvia para uma rotina de partida, que copia os parâmetros para os registradores de argumento e chama a rotina principal do programa. Quando a rotina principal retorna, a rotina de partida termina o programa com uma chamada ao sistema exit. As Seções B.3 e B.4 no Apêndice B descrevem os link-editores e os loaders com mais detalhes.
Dinamically Linked Libraries (DLLs) A primeira parte desta seção descreve a técnica tradicional para a link-edição de bibliotecas antes de o programa ser executado. Embora essa técnica estática seja o modo mais rápido de chamar rotinas de biblioteca, ela possui algumas desvantagens:
2.12 Traduzindo e iniciando um programa 117
j
As rotinas de biblioteca se tornam parte do código executável. Se uma nova versão da biblioteca for lançada para reparar os erros ou dar suporte a novos dispositivos de hardware, o programa link-editado estaticamente continua usando a versão antiga.
j
Ela carrega todas as rotinas na biblioteca que são chamadas de qualquer lugar no executável, mesmo que essas chamadas não sejam executadas. A biblioteca pode ser grande em relação ao programa; por exemplo, a biblioteca padrão da linguagem C possui 2,5MB.
Essas desvantagens levaram às Dynamic Linked Libraries (DLLs), nas quais as rotinas da biblioteca não são link-editadas e carregadas até que o programa seja executado. Tanto o programa quanto as rotinas da biblioteca mantêm informações extras sobre a localização dos procedimentos não locais e seus nomes. Na versão inicial das DLLs, o loader executava um link-editor dinâmico, usando as informações extras no arquivo para descobrir as bibliotecas apropriadas e atualizar todas as referências externas. A desvantagem da versão inicial das DLLs era que ela ainda link-editava todas as rotinas da biblioteca que poderiam ser chamadas, quando apenas algumas são chamadas durante a execução do programa. Essa observação levou à versão da link-edição de procedimento tardio das DLLs, no qual cada rotina só é link-editada depois de chamada. Como muitas inovações em nosso campo, esse truque conta com um certo nível de indireção. A Figura 2.22 mostra a técnica. Ela começa com as rotinas não locais chamando um conjunto de rotinas fictícias no final do programa, com uma entrada por rotina não local. Essas entradas fictícias contêm, cada uma, um jump indireto.
FIGURA 2.22 DLL por meio da link-edição de procedimento tardio. (a) Etapas para a primeira vez em que uma chamada é feita à rotina da DLL. (b) As etapas para encontrar a rotina, remapeá-la e link-editá-la são puladas em chamadas subsequentes. Conforme veremos no Capítulo 5, o sistema operacional pode evitar copiar a rotina desejada remapeando-a por meio do gerenciamento de memória virtual.
Dynamically Linked Libraries (DLLs) Rotinas de bit que são vinculadas a um programa durante a execução.
118
Capítulo 2 Instruções: A Linguagem de Máquina
Na primeira vez em que a rotina da biblioteca é chamada, o programa chama a entrada fictícia e segue o jump indireto. Ele aponta para o código que coloca um número em um registrador para identificar a rotina de biblioteca desejada e depois desvia para o loader com link-editor dinâmico. O loader com link-editor encontra a rotina desejada, remapeia essa rotina e altera o endereço do desvio indireto, de modo a apontar para essa rotina. Depois, ele desvia para ela. Quando a rotina termina, ele retorna ao local de chamada original. Depois disso, a chamada para rotina de biblioteca desvia indiretamente para a rotina, sem os desvios extras. Resumindo, as DLLs exigem espaço extra para as informações necessárias à link-edição dinâmica, mas não exigem que as bibliotecas inteiras sejam copiadas ou link-editadas. Elas realizam muito trabalho extra na primeira vez em que uma rotina é chamada, mas executam somente um desvio indireto depois disso. Observe que o retorno da biblioteca não realiza trabalho extra. O Microsoft Windows conta bastante com as DLLs dessa forma e esse também é um modo normal de executar programas nos sistemas UNIX atuais.
Iniciando um programa Java
bytecode Java Instruções que formam programas em Java, escritas em um conjunto de instruções elaborado para ser interpretado.
Java Virtual Machine (JVM) O programa que interpreta os bytecodes Java.
A discussão anterior captura o modelo tradicional de execução de um programa, no qual a ênfase está no tempo de execução rápido para um programa voltado a uma arquitetura específica, ou mesmo para uma implementação específica dessa arquitetura. Na verdade, é possível executar programas Java da mesma forma que programas C. No entanto, Java foi inventada com objetivos diferentes. Um deles era funcionar rapidamente e de forma segura em qualquer computador, mesmo que isso pudesse aumentar o tempo de execução. A Figura 2.23 mostra as etapas típicas de tradução e execução para os programas em Java. Em vez de compilar para assembly de um computador de destino, Java é compilado primeiro para instruções fáceis de interpretar: o conjunto de instruções do bytecode Java (ver Seção 2.15 no site). Esse conjunto de instruções foi criado para ser próximo da linguagem Java, de modo que essa etapa de compilação seja trivial. Praticamente nenhuma otimização é realizada. Assim como o compilador C, o compilador Java verifica os tipos dos dados e produz a operação apropriada a cada tipo. Os programas em Java são distribuídos na versão binária desses bytecodes. Um interpretador Java, chamado Java Virtual Machine (JVM), pode executar os bytecodes Java. Um interpretador é um programa que simula um conjunto de instruções. Por exemplo, o simulador do MIPS usado com este livro é um interpretador. Não é necessária uma etapa de montagem separada, pois ou a tradução é tão simples que o compilador preenche os endereços ou a JVM os encontra durante a execução.
FIGURA 2.23 Uma hierarquia de tradução para Java. Um programa em Java primeiro é compilado para uma versão binária dos bytecodes Java, com todos os endereços definidos pelo compilador. O programa em Java agora está pronto para ser executado no interpretador, chamado Java Virtual Machine (JVM – máquina virtual Java). A JVM link-edita os métodos desejados na biblioteca Java enquanto o programa está sendo executado. Para conseguir melhor desempenho, a JVM pode chamar o compilador Just-In-Time (JIT), que compila os métodos seletivamente para a linguagem nativa da máquina em que está executando.
2.13 Um exemplo de ordenação em C para juntar tudo isso 119
A vantagem da interpretação é a portabilidade. A disponibilidade das máquinas virtuais Java em software significou que muitos puderam escrever e executar programas Java pouco tempo depois que o Java foi anunciado. Hoje, as máquinas virtuais Java são encontradas em centenas de milhões de dispositivos, em tudo desde telefones celulares até navegadores da Internet. A desvantagem da interpretação é o desempenho fraco. Os avanços incríveis no desempenho dos anos 80 e 90 do século passado tornaram a interpretação viável para muitas aplicações importantes, mas um fator de atraso de 10 vezes, em comparação com os programas C compilados tradicionalmente, tornou o Java pouco atraente para algumas aplicações. A fim de preservar a portabilidade e melhorar a velocidade de execução, a fase seguinte do desenvolvimento do Java foram compiladores que traduziam enquanto o programa estava sendo executado. Esses compiladores Just-In-Time (JIT) normalmente traçam o perfil do programa em execução para descobrir onde estão os métodos “quentes”, e depois os compilam para o conjunto de instruções nativo em que a máquina virtual está executando. A parte compilada é salva para a próxima vez em que o programa for executado, de modo que possa ser executado mais rapidamente cada vez que for executado. Esse equilíbrio entre interpretação e compilação evolui com o tempo, de modo que os programas Java executados com frequência sofrem muito pouco com o trabalho extra da interpretação. À medida que os computadores ficam mais rápidos, de modo que os compiladores possam fazer mais, e os pesquisadores inventam melhores meios de compilar Java durante a execução, a lacuna de desempenho entre Java e C ou C + + está se fechando. A Seção 2.15 do CD contém muito mais detalhes sobre a implementação do Java, dos bytecodes Java, da JVM e dos compiladores JIT. Qual das vantagens de um interpretador em relação a um tradutor você acredita que tenha sido mais importante para os criadores do Java? 1. Facilidade de escrita de um interpretador 2. Melhores mensagens de erro 3. Código-objeto menor 4. Independência de máquina
Um exemplo de ordenação em C para juntar
2.13 tudo isso
Um perigo de mostrar o código em assembly em partes é que você não terá ideia de como se parece um programa inteiro em assembly. Nesta seção, deduzimos o código do MIPS a partir de dois procedimentos escritos em C: um para trocar elementos do array e outro para ordená-los.
O procedimento swap Vamos começar com o código para o procedimento swap na Figura 2.24. Esse procedimento simplesmente troca os conteúdos de duas posições de memória. Ao traduzir de C para assembly manualmente, seguimos estas etapas gerais: 1. Alocar registradores a variáveis do programa. 2. Produzir código para o corpo do procedimento. 3. Preservar registradores durante a chamada do procedimento.
compilador Just-In-Time (JIT) O nome normalmente dado a um compilador que opera durante a execução, traduzindo os segmentos de código interpretados para o código nativo do computador.
Verifique você mesmo
120
Capítulo 2 Instruções: A Linguagem de Máquina
FIGURA 2.24 Um procedimento em C que troca o conteúdo de duas posições de memória. Esta subseção utiliza esse procedimento em um exemplo de ordenação.
Esta seção descreve o procedimento swap nessas três partes, concluindo com a junção de todas as partes. Alocação de registradores para swap
Como mencionamos anteriormente na Seção 2.8, a convenção do MIPS sobre passagem de parâmetros é usar os registradores $a0 , $a1 , $a2 e $a3 . Como swap tem apenas dois parâmetros, v e k, eles serão encontrados nos registradores $a0 e $a1. A única outra variável é temp, que associamos com o registrador $t0, pois swap é um procedimento folha (veja seção “Procedimentos aninhados”). Essa alocação de registradores corresponde às declarações de variável na primeira parte do procedimento swap da Figura 2.24. Código do corpo do procedimento swap
As linhas restantes do código em C do swap são temp = v[k]; v[k] = v[k + 1]; v[k + 1] = temp;
Lembre-se de que o endereço de memória para o MIPS refere-se ao endereço em bytes, e, por isso, as palavras, na realidade, estão afastadas por 4 bytes. Logo, precisamos multiplicar o índice k por 4 antes de somá-lo ao endereço. Esquecer que os endereços de palavras sequenciais diferem em 4, em vez de 1, é um erro comum na programação em assembly. Logo, o primeiro passo é obter o endereço de v[k] multiplicando k por 4 por meio de um deslocamento à esquerda por 2:
Agora, lemos v[k] para $t1, e depois v[k+1] somando 4 a $t1:
Agora, armazenamos $t0 e $t2 nos endereços trocados:
2.13 Um exemplo de ordenação em C para juntar tudo isso 121
Até agora, alocamos registradores e escrevemos o código de modo a realizar as operações do procedimento. O que está faltando é o código para preservar os registradores salvos usados dentro do swap. Como não estamos usando registradores salvos nesse procedimento folha, não há nada para preservar. O procedimento swap completo
Agora, estamos prontos para a rotina inteira, que inclui o rótulo do procedimento e o jump de retorno. A fim de facilitar o acompanhamento, identificamos na Figura 2.25 cada bloco de código com sua finalidade no procedimento.
O procedimento sort Para garantir que você apreciará o rigor da programação em assembly, vamos experimentar um segundo exemplo, maior. Nesse caso, montaremos uma rotina que chama o procedimento swap. Esse programa ordena um array de inteiros, usando ordenação por trocas, que é uma das mais simples, mas não a mais rápida. A Figura 2.26 mostra a versão em C do programa. Mais uma vez, apresentamos esse procedimento em várias etapas, concluindo com o procedimento completo.
FIGURA 2.25 Código assembly do MIPS do procedimento swap na Figura 2.24.
FIGURA 2.26 Um procedimento em C que realiza uma ordenação no array v.
Alocação de registradores para sort
Os dois parâmetros do procedimento sort, v e n, estão nos registradores de parâmetro $a0 e $a1, e alocamos o registrador $s0 a i e o registrador $s1 a j. Código para o corpo do procedimento sort
O corpo do procedimento consiste em dois loops for aninhados e uma chamada a swap que inclui parâmetros. Vamos desvendar o código de fora para o meio. O primeiro passo de tradução é o primeiro loop for:
122
Capítulo 2 Instruções: A Linguagem de Máquina
Lembre-se de que a instrução for em C possui três partes: inicialização, teste de loop e incremento da iteração. É necessária apenas uma instrução para inicializar i como 0, a primeira parte da instrução for:
(Lembre-se de que move é uma pseudoinstrução fornecida pelo montador para a conveniência do programador em assembly; ver seção “Montador”, anteriormente neste capítulo.) Também é necessária apenas uma instrução para incrementar i, a última parte da instrução for:
O loop deverá terminar se i
$a1, desviamos se o registrador $t0 for zero. Esse teste utiliza duas instruções:
O final do loop só desvia de volta para o teste do loop:
O código da estrutura do primeiro loop for é, então,
Voilà! Os exercícios exploram a escrita de código mais rápido para loops semelhantes. O segundo loop for se parece com o seguinte em C:
A parte de inicialização desse loop novamente é uma instrução:
O decremento de j no final do loop também tem uma instrução:
2.13 Um exemplo de ordenação em C para juntar tudo isso 123
O teste do loop possui duas partes. Saímos do loop se a condição falhar, de modo que o primeiro teste precisa terminar o loop se falhar (j<0):
Esse desvio pulará o segundo teste de condição. Se não pular, então j ≥ 0 . O segundo teste termina se V[j]>v[j +1] não for verdadeiro, ou seja, termina se v[j] ≤ v[j + 1] . Primeiro, criamos o endereço multiplicando j por 4 (pois precisamos de um endereço em bytes) e somamos ao endereço base de v:
Agora, lemos o conteúdo de v[j]:
Como sabemos que o segundo elemento é exatamente a palavra seguinte, somamos 4 ao endereço no registrador $t2 para obter v[j+1]:
O teste de v[j] ≤ v[j + 1] é o mesmo que v[j + 1] ≥ v[j] , de modo que as duas instruções do teste de saída são
O final do loop desvia de volta para o teste do loop interno:
Combinando as partes, a estrutura do segundo loop for se parece com o seguinte:
124
Capítulo 2 Instruções: A Linguagem de Máquina
A chamada de procedimento em sort
A próxima etapa é o corpo do segundo loop for:
Chamar swap é muito fácil:
Passando parâmetros em sort O problema vem quando queremos passar parâmetros, porque o procedimento sort precisa dos valores nos registradores $a0 e $a1, enquanto o procedimento swap precisa que seus parâmetros sejam colocados nesses mesmos registradores. Uma solução é copiar os parâmetros para sort em outros registradores antes do procedimento, deixando os registradores $a0 e $a1 disponíveis para a chamada de swap. (Essa cópia é mais rápida do que salvar e restaurar na pilha.) Primeiro, copiamos $a0 e $a1 para $s2 e $s3durante o procedimento:
Depois, passamos os parâmetros para swap com estas duas instruções:
Preservando registradores em sort
O único código restante é o salvamento e a restauração dos registradores. Com certeza, temos de salvar o endereço de retorno no registrador $ra, pois sort é um procedimento que foi chamado por outro procedimento. O procedimento sort também utiliza os registradores salvos $s0, $s1, $s2 e $s3, de modo que precisam ser salvos. O prólogo do procedimento sort, portanto, é
O final do procedimento simplesmente reverte todas essas instruções, depois acrescenta um jr para retornar. O procedimento sort completo
Agora, juntamos todas as partes na Figura 2.27, tendo o cuidado de substituir as referências aos registradores $a0 e $a1 nos loops for por referências aos registradores $s2 e $s3. Novamente para tornar o código mais fácil de acompanhar, identificamos cada bloco de código com sua finalidade no procedimento. Neste capítulo, nove linhas do procedimento sort em C tornaram-se 35 em assembly do MIPS.
2.13 Um exemplo de ordenação em C para juntar tudo isso 125
FIGURA 2.27 Versão em assembly do MIPS para o procedimento AAAA da Figura 2.26.
Detalhamento: Uma otimização que funciona com este exemplo é a utilização de procedimentos inline. Em vez de passar argumentos em parâmetros e invocar o código com uma instrução jal, o compilador copiaria o código do corpo do procedimento swap onde a chamada a swap aparece no código. Essa otimização evitaria quatro instruções neste exemplo. A desvantagem da otimização que utiliza procedimentos inline é que o código compilado seria maior se o procedimento inline fosse chamado de vários locais. Essa expansão de código poderia ter um desempenho inferior se aumentasse a taxa de falhas na cache; ver Capítulo 5.
126
Capítulo 2 Instruções: A Linguagem de Máquina
Entendendo o desempenho dos programas
A Figura 2.28 mostra o impacto da otimização do compilador sobre o desempenho do programa de ordenação, tempo de compilação, ciclos de clock, contagem de instruções e CPI. Observe que o código não otimizado tem o melhor CPI, e a otimização O1 tem a menor contagem de instruções, mas O3 é a mais rápida, lembrando que o tempo é a única medida precisa do desempenho do programa. A Figura 2.29 compara o impacto das linguagens de programação, compilação versus interpretação, e os algoritmos sobre o desempenho das ordenações. A quarta coluna mostra que o programa C otimizado é 8,3 vezes mais rápido do que o código Java interpretado para o Bubble Sort. O uso do compilador JIT torna o programa em Java 2,1 vezes mais rápido do que o programa em C não otimizado e dentro de um fator de 1,13 mais rápido do que o código C mais otimizado. (A Seção 2.15 do CD contém mais detalhes sobre interpretação versus compilação de Java e o código Java e MIPS para o Bubble Sort.) As razões não são tão próximas para o Quicksort na coluna 5, possivelmente porque é mais difícil amortizar o custo da compilação em runtime pelo tempo de execução mais curto. A última coluna demonstra o impacto de um algoritmo melhor, oferecendo um aumento de desempenho de três ordens de grandeza quando são ordenados 100.000 itens. Mesmo comparando o programa Java interpretado na coluna 5 com o programa C compilado com as melhores otimizações na coluna 4, o Quicksort vence o Bubble Sort por um fator de 50 (0,05 x 2468 ou 123 vezes mais rápido que o código C não otimizado versus 2,41). Detalhamento: Os compiladores MIPS sempre reservam espaço na pilha para os argumentos,
caso precisem ser armazenados, de modo que, na realidade, eles sempre decrementam o $sp de 16, de modo a dar espaço para todos os quatro registradores de argumento (16 bytes). Um motivo é que a linguagem C oferece varang que permite que um ponteiro apanhe, digamos, o terceiro argumento de um procedimento. Quando, raramente, o compilador encontra varang, ele copia os quatro registradores de argumento para os quatro locais reservados na pilha.
FIGURA 2.28 Comparando desempenho, contagem de instruções e CPI usando otimizações do compilador para o Bubble Sort. Os programas ordenaram 100.000 words com o array inicializado com valores aleatórios. Esses programas foram executados em um Pentium 4 com clock de 3,06GHz e um barramento de 533MHz com 2GB de memória SDRAM DDR PC2100. Ele usava o Linux versão 2.4.20.
FIGURA 2.29 Desempenho de dois algoritmos de ordenação em C e Java usando interpretação e compiladores otimizadores em relação à versão C não otimizada. A última coluna mostra a vantagem no desempenho do Quicksort em relação ao Bubble Sort para cada linguagem e opção de execução. Esses programas foram executados no mesmo sistema da Figura 2.28. A JVM é Sun versão 1.3.1, e o JIT é o Sun Hotspot versão 1.3.1.
2.14 Arrays versus ponteiros
2.14 Arrays versus ponteiros Um tópico desafiador para qualquer programador novo é entender os ponteiros. A comparação entre o código assembly que usa arrays e índices de array com o código assembly que usa ponteiros fornece esclarecimentos sobre ponteiros. Esta seção mostra as versões C e assembly do MIPS de dois procedimentos para zerar (clear) uma sequência de palavras na memória: uma usando índices de array e uma usando ponteiros. A Figura 2.30 mostra os dois procedimentos em C. A finalidade desta seção é mostrar como os ponteiros são mapeados em instruções MIPS, e não endossar um estilo de programação ultrapassado. Ao final da seção, veremos o impacto das otimizações do compilador moderno sobre esses dois procedimentos.
Versão de clear usando arrays Vamos começar com a versão que usa arrays, clear 1, focalizando o corpo do loop e ignorando o código de ligação do procedimento. Consideramos que os dois parâmetros array e size são encontrados nos registradores $a0 e $a1, e que i é alocado ao registrador $t0. A inicialização de i, a primeira parte do loop for, é simples:
Para definir array[i]como 0, temos primeiro de obter seu endereço. Comece multiplicando i por 4, para obter o endereço em bytes:
Como o endereço inicial do array está em um registrador, temos de somá-lo ao índice para obter o endereço de array[i] usando uma instrução add:
FIGURA 2.30 Dois procedimentos em C para definir um array com todos os valores iguais a zero. clear1 usa índices, enquanto clear2 usa ponteiros. O segundo procedimento precisa de alguma explicação para os que não estão acostumados com C. O endereço de uma variável é indicado por &, e a referência ao objeto apontando por um ponteiro é indicada por *. As declarações indicam que array e p são ponteiros para inteiros. A primeira parte do loop for em clear2 atribui o endereço do primeiro elemento do array ao ponteiro p. A segunda parte do loop for testa se o ponteiro está apontando além do último elemento do array. Incrementar um ponteiro em um, na última parte do loop for, significa mover o ponteiro para o próximo objeto sequencial do seu tamanho declarado. Como p é um ponteiro para inteiros, o compilador gerará instruções MIPS para incrementar p de quatro, o número de bytes de um inteiro MIPS. A atribuição no loop coloca 0 no objeto apontado por p.
127
128
Capítulo 2 Instruções: A Linguagem de Máquina
Finalmente, podemos armazenar 0 nesse endereço:
Essa instrução é o final do corpo do loop, de modo que o próximo passo é incrementar i:
O teste do loop verifica se i é menor do que size:
Agora, já vimos todas as partes do procedimento. Aqui está o código MIPS para zerar um array usando índices:
(Esse código funciona desde que size seja maior que 0; o ANSI C requer um teste de tamanho antes do loop, mas pularemos essa legalidade aqui.)
Versão de clear usando ponteiros O segundo procedimento que usa ponteiros aloca os dois parâmetros array e size aos registradores $a0 e $a1 e aloca p ao registrador $t0. O código para o segundo procedimento começa com a atribuição do ponteiro p ao endereço do primeiro elemento do array:
O código a seguir é o corpo do loop for, que simplesmente armazena 0 em p:
Essa instrução implementa o corpo do loop, de modo que o próximo código é o incremento da iteração, que muda p de modo que aponte para a próxima palavra:
Incrementar um ponteiro em 1 significa mover o ponteiro para o próximo objeto sequencial em C. Como p é um ponteiro para inteiros, cada um usando 4 bytes, o compilador incrementa p de 4.
2.14 Arrays versus ponteiros
O teste do loop vem em seguida. O primeiro passo é calcular o endereço do último elemento de array. Comece multiplicando size por 4 para obter seu endereço em bytes:
e depois acrescentamos o produto ao endereço inicial do array para obter o endereço da primeira word após o array:
O teste do loop é simplesmente para ver se p é menor do que o último elemento de
array:
Com todas essas partes completadas, podemos mostrar uma versão do código para zerar um array usando ponteiros:
Como no primeiro exemplo, esse código considera que size é maior do que 0. Observe que esse programa calcula o endereço do final do array em cada iteração do loop, embora não mude. Uma versão mais rápida do código move esse cálculo para fora do loop:
Comparando as duas versões de clear A comparação das duas sequências lado a lado ilustra a diferença entre os índices de array e ponteiros (as mudanças introduzidas pela versão de ponteiro estão destacadas):
129
130
Capítulo 2 Instruções: A Linguagem de Máquina
A versão da esquerda precisa ter a “multiplicação” e a soma dentro do loop, porque i é incrementado e cada endereço precisa ser recalculado a partir do novo índice; a versão usando ponteiros para a memória à direita incrementa o ponteiro p diretamente. A versão usando ponteiros reduz as instruções executadas por iteração de 6 para 4. Essas otimizações manuais correspondem a otimizações do compilador chamadas redução de força (deslocamento em vez de multiplicação) e eliminação da variável de indução (eliminando cálculos de endereço de array dentro dos loops). A Seção 2.15 no site descreve estas duas e muitas outras otimizações. Detalhamento: como mencionamos, o compilador C acrescentaria um teste para garantir que
size seja maior do que 0. Uma maneira seria acrescentar um desvio, imediatamente antes da primeira instrução do loop, para a instrução slt.
Entendendo o desempenho dos programas
As pessoas costumavam ser ensinadas a usar ponteiros em C para conseguir mais eficiência do que era possível com os arrays: “Use ponteiros, mesmo que você não consiga entender o código”. Os modernos compiladores com otimizações podem produzir um código muito bom para a versão usando arrays. A maioria dos programadores de hoje prefere que o compilador realize o trabalho pesado.
Material avançado: Compilando
2.15 C e interpretando Java
linguagem orientada a objetos Uma linguagem de programação que é orientada em torno de objetos, em vez de ações, ou dados versus lógica.
Esta seção oferece uma breve visão geral de como o compilador C funciona e como Java é executada. Como o compilador afetará significativamente o desempenho de um computador, entender a tecnologia do compilador hoje é fundamental para se entender o desempenho. Lembre-se de que o assunto da construção de compilador normalmente é lecionado em um curso de um ou dois semestres, de modo que nossa introdução necessariamente só tocará nos fundamentos. A segunda parte desta seção é para leitores interessados em ver como uma linguagem orientada a objeto, como Java, é executada em uma arquitetura MIPS. Ela mostra os bytecodes Java usados para a interpretação e o código MIPS para a versão Java de alguns dos segmentos C nas seções anteriores, incluindo o Bubble Sort. Ela aborda a Java Virtual Machine e os compiladores JIT. O restante desta seção está no site.
2.16
Vida real: instruções do ARM
ARM é a arquitetura de conjunto de instruções mais comum para dispositivos embutidos, com mais de três bilhões de dispositivos por ano usando ARM. Preparada originalmente para a Acorn RISC Machine, mais tarde modificada para Advanced RISC Machine, ARM surgiu no mesmo ano em que o MIPS e seguiu filosofias semelhantes. A Figura 2.31 lista as semelhanças. A principal diferença é que MIPS tem mais registradores e ARM tem mais modos de endereçamento. Existe um núcleo semelhante dos conjuntos de instruções para instruções aritmética-lógica e de transferência de dados para o MIPS e ARM, como mostra a Figura 2.32.
2.16 Vida real: instruções do ARM 131
FIGURA 2.31 Semelhanças nos conjuntos de instruções ARM e MIPS.
FIGURA 2.32 Instruções ARM registrador-registrador e transferência de dados equivalente ao núcleo MIPS. Os traços significam que a operação não está disponível nessa arquitetura ou não é sintetizada em poucas instruções. Se houver várias escolhas de instruções equivalente ao núcleo MIPS, elas são separadas por vírgulas. ARM inclui deslocamentos como parte de cada instrução de operação de dados, de modo que os shift com sobrescrito 1 1 são apenas uma variação de uma instrução move, como lsr . Observe que o ARM não possui instrução de divisão.
Modos de endereçamento A Figura 2.33 mostra os modos de endereçamento de dados admitidos pelo ARM. Diferente do MIPS, ARM não reserva um registrador para conter 0. Embora o MIPS tenha apenas três modos de endereçamento de dados simples (veja Figura 2.18), ARM tem nove, incluindo cálculos bastante complexos. Por exemplo, ARM tem um modo de endereçamento que pode deslocar um registrador por qualquer quantidade, somá-lo aos outros registradores a fim de formar o endereço, e depois atualizar um registrador com esse novo endereço.
132
Capítulo 2 Instruções: A Linguagem de Máquina
FIGURA 2.33 Resumo dos modos de endereçamento de dados. ARM tem modos de endereçamento separados registrador indireto e registrador + offset, em vez de colocar apenas 0 no deslocamento do segundo modo. Para obter um maior intervalo de endereçamento, o ARM desloca o offset à esquerda 1 ou 2 bits se o tamanho dos dados for halfword ou uma palavra inteira.
Comparação e desvio condicional MIPS usa o conteúdo dos registradores para avaliar desvios condicionais. ARM usa os quatro bits de código de condição tradicionais armazenados na palavra de status do programa: negativo, zero, carry e overflow. Eles podem ser definidos em qualquer instrução aritmética ou lógica; diferente das arquiteturas anteriores, essa configuração é opcional em cada instrução. Uma opção explícita leva a menos problemas em uma implementação em pipeline. ARM utiliza desvios condicionais para testar os códigos de condição a fim de determinar todas as relações sem sinal e com sinal possíveis. CMP subtrai um operando do outro, e a diferença define os códigos de condição. CMN (compare negative) soma um operando ao outro, e a soma define os códigos de condição. TST realiza um AND lógico sobre os dois operandos para definir todos os códigos de condição menos overflow, enquanto TEQ utiliza OR exclusivo a fim de definir os três primeiros códigos de condição. Um recurso incomum do ARM é que cada instrução tem a opção de executar condicionalmente, dependendo dos códigos de condição. Cada instrução começa com um campo de 4 bits que determina se ele atuará como uma instrução de nenhuma operação (nop) ou como uma instrução real, dependendo dos códigos de condição. Logo, os desvios condicionais são corretamente considerados como executando condicionalmente a instrução de desvio incondicional. A execução condicional permite evitar que um desvio salte sobre uma instrução isolada. É preciso menos espaço de código e tempo para apenas executar uma instrução condicionalmente. A Figura 2.34 mostra os formatos de instrução para ARM e MIPS. As principais diferenças são o campo de execução condicional de 4 bits em cada instrução e o campo de registrador menor, pois ARM tem metade do número de registradores.
Recursos exclusivos do ARM A Figura 2.35 mostra algumas instruções aritmética-lógica não encontradas no MIPS. Por não possuir um registrador dedicado para 0, ARM tem opcodes separados para realizar algumas operações que MIPS pode fazer com $zero. Além disso, ARM tem suporte para aritmética de múltiplas palavras. O campo imediato de 12 bits do ARM tem uma nova interpretação. Os oito bits menos significativos são estendidos em zero a um valor de 32 bits, depois girados para a direita pelo número de bits especificado nos quatro primeiros bits do campo multiplicado por dois. Uma vantagem é que esse esquema pode representar todas as potências de dois em
2.16 Vida real: instruções do ARM 133
FIGURA 2.34 Formatos de instrução, ARM e MIPS. As diferenças resultam de arquiteturas com 16 ou 32 registradores.
FIGURA 2.35 Instruções aritméticas/lógicas do ARM não encontradas no MIPS.
uma palavra de 32 bits. Um estudo interessante seria descobrir se essa divisão realmente apanha mais imediatos do que um campo simples de 12 bits. O deslocamento de operandos não é limitado a imediatos. O segundo registrador de todas as operações de processamento aritmético e lógico tem a opção de ser deslocado antes de ser atuado. As opções de deslocamento são shift left logical, shift right logical, shift right arithmetic e rotate right.
134
Capítulo 2 Instruções: A Linguagem de Máquina
ARM também possui instruções para salvar grupos de registradores, chamados loads e stores em bloco. Sob o controle de uma máscara de 16 bits dentro das instruções, qualquer um dos 16 registradores pode ser carregado ou armazenado na memória em uma única instrução. Essas instruções podem salvar e restaurar registradores na entrada e retorno do procedimento. Elas também podem ser usadas para cópia de memória em bloco, sendo este o uso mais importante dessa instrução hoje em dia. A beleza está toda nos olhos de quem vê. Margaret Wolfe Hungerford, Molly Bawn, 1877
2.17 Vida real: instruções do x86 Os projetistas de conjuntos de instruções às vezes oferecem operações mais poderosas do que aquelas encontradas no ARM e MIPS. O objetivo geralmente é reduzir o número de instruções executadas por um programa. O perigo é que essa redução pode ocorrer ao custo da simplicidade, aumentando o tempo que um programa leva para executar, pois as instruções são mais lentas. Essa lentidão pode ser o resultado de um tempo de ciclo de clock mais lento ou a requisição de mais ciclos de clock do que uma sequência mais simples. O caminho em direção à complexidade da operação é, portanto, repleto de perigos. A fim de evitar esses problemas, os projetistas passaram para instruções mais simples. A Seção 2.18 demonstra as armadilhas da complexidade.
A evolução do Intel x86 O ARM e o MIPS foram a visão de grupos pequenos e únicos em 1985; as partes dessas arquiteturas se encaixam muito bem e a arquitetura inteira pode ser descrita de forma sucinta. Isso não acontece com o X86; ele é o produto de vários grupos independentes, que evoluíram a arquitetura por quase 30 anos, acrescentando novos recursos ao conjunto de instruções original, como alguém poderia acrescentar roupas em uma mala pronta. Aqui estão os marcos importantes do X86: j
1978: a arquitetura Intel 8086 foi anunciada como uma extensão compatível com o assembly para o então bem-sucedido Intel 8080, um microprocessador de 8 bits. O 8086 é uma arquitetura de 16 bits, com todos os registradores internos com 16 bits de largura. Ao contrário do MIPS, os registradores possuem usos dedicados, e, por isso, o 8086 não é considerado uma arquitetura com registradores de uso geral.
j
1980: o coprocessador de ponto flutuante Intel 8087 foi anunciado. Essa arquitetura estende o 8086 com cerca de 60 instruções de ponto flutuante. Em vez de usar registradores, ele conta com uma pilha (veja Seção 2.20 e Seção 3.7).
j
1982: o 80286 estendeu a arquitetura 8086, aumentando o espaço de endereçamento para 24 bits, criando um modelo de mapeamento e proteção de memória elaborado (veja Capítulo 7) e acrescentando algumas instruções para preencher o conjunto de instruções e manipular o modelo de proteção.
j
1985: o 80386 estendeu a arquitetura 80286 para 32 bits. Além de uma arquitetura de 32 bits com registradores de 32 bits e um espaço de endereçamento de 32 bits, o 80386 acrescentou novos modos de endereçamento e operações adicionais. As instruções adicionais tornam o 80386 quase uma máquina de uso geral. O 80386 também acrescentou suporte para paginação além de endereçamento segmentado (veja Capítulo 5). Assim como o 80286, o 80386 possui um modo para executar programas do 8086 sem alteração.
j
1989-95: os posteriores 80486 em 1989, Pentium em 1992 e Pentium Pro em 1995 visaram a um desempenho maior, com apenas quatro instruções acrescentadas ao conjunto de instruções visíveis ao usuário: três para ajudar com o multiprocessamento (veja Capítulo 7) e uma instrução move condicional.
registradores de uso geral (GPR – General-Purpose Register) Um registrador que pode ser usado para endereços ou para dados, com praticamente qualquer instrução.
2.17 Vida real: instruções do x86 135
j
1997: depois que o Pentium e o Pentium Pro estavam sendo vendidos, a Intel anunciou que expandiria as arquiteturas Pentium e Pentium Pro com as Multi Media Extensions (MMX). Esse novo conjunto de 57 instruções utiliza a pilha de ponto flutuante de modo a acelerar aplicações de multimídia e comunicações. As instruções MMX normalmente operam sobre vários elementos de dados curtos de uma só vez, na tradição das arquiteturas de única instrução e múltiplos dados (SIMD – Single Instruction, Multiple Data) (veja Capítulo 7). O Pentium II não introduziu novas instruções.
j
1999: a Intel acrescentou outras 70 instruções, denominadas Streaming SIMD Extensions (SSE), como parte do Pentium III. As principais mudanças foram incluir oito registradores separados, dobrar sua largura para 128 bits e incluir um tipo de dados de ponto flutuante com precisão simples. Logo, quatro operações de ponto flutuante de 32 bits podem ser realizadas em paralelo. Para melhorar o desempenho da memória, as SSE incluem instruções de prefetch (pré-busca) da cache, mais instruções de armazenamento de streaming, que contornam as caches e escrevem diretamente na memória.
j
2001: a Intel acrescentou ainda outras 144 instruções, dessa vez denominadas SSE2. O novo tipo de dados tem aritmética de precisão dupla, o que permite pares de operações de ponto flutuante de 64 bits em paralelo. Quase todas essas 144 instruções são versões de instruções MMX e SSE existentes que operam sobre 64 bits de dados em paralelo. Essa mudança não apenas habilita mais operações de multimídia, mas dá ao compilador um alvo diferente para operações de ponto flutuante do que a arquitetura de pilha única. Os compiladores podem decidir usar os oito registradores SSE como registradores de ponto flutuante, como aqueles encontrados em outros computadores. Essa mudança aumentou o desempenho de ponto flutuante no Pentium 4, o primeiro microprocessador a incluir instruções SSE2.
j
2003: dessa vez, foi outra empresa, e não a Intel, que melhorou a arquitetura x86. A AMD anunciou um conjunto de extensões arquitetônicas para aumentar o espaço de endereçamento de 32 para 64 bits. Semelhante à transição do espaço de endereçamento de 16 para 32 bits em 1985, com o 80386, o AMD64 alarga todos os registradores para 64 bits. Ele também aumenta a quantidade de registradores para 16 e aumenta o número de registradores SSE de 128 bits para 16. A principal mudança no ISA vem da inclusão de um novo modo, chamado modo longo, que redefine a execução de todas as instruções x86 com endereços e dados de 64 bits. Para enfrentar a quantidade maior de registradores, ele acrescenta um novo prefixo às instruções. Dependendo de como você conta, o modo longo também acrescenta de 4 a 10 novas instruções e perde 27 antigas. O endereçamento de dados relativo ao PC é outra extensão. O AMD64 ainda possui um modo idêntico ao x86 (modo legado) e mais um modo que restringe os programas do usuário ao x86, mas permite que os sistemas operacionais utilizem o AMD64 (modo de compatibilidade). Esses modos permitem uma transição mais controlada para o endereçamento de 64 bits do que a arquitetura IA-64 da HP/Intel.
j
2004: a Intel se rende e abraça o AMD64, trocando seu nome para Extended Memory 64 Technology (EM64T). A principal diferença é que a Intel acrescentou uma instrução de comparação e troca atômica de 128 bits, que provavelmente deveria ter sido incluída no AMD64. Ao mesmo tempo, a Intel anunciou outra geração de extensões de mídia. O SSE3 acrescenta 13 instruções para dar suporte à aritmética complexa, operações gráficas sobre arrays de estruturas, codificação de vídeo, conversão de ponto flutuante e sincronismo de threads (veja Seção 2.11). A AMD oferecerá o SSE3 nos próximos chips e quase certamente incluirá a instrução de troca atômica que estava faltando no ADM64, para manter a compatibilidade binária com a Intel.
136
Capítulo 2 Instruções: A Linguagem de Máquina
j
2006: a Intel anuncia 54 novas instruções como parte das extensões do conjunto de instruções SSE4. Essas extensões realizam coisas como soma de diferenças absolutas, produtos escalares para arrays de estruturas, extensão de sinal ou zero de dados estreitos para tamanhos mais largos, contagem de população e assim por diante. Eles também acrescentaram suporte para máquinas virtuais (veja Capítulo 5).
j
2007: a AMD anuncia 170 instruções como parte das SSE5, incluindo 46 instruções do conjunto de instruções básico, que acrescenta três instruções de operando, como MIPS.
j
2008: a Intel anuncia a Advanced Vector Extension, que expande a largura de registrador das SSE de 128 para 256 bits, redefinindo, assim, cerca de 250 instruções e acrescentando 128 novas instruções.
Essa história ilustra o impacto das “algemas douradas” da compatibilidade com o x86, pois a base de software existente em cada etapa era muito importante para ser colocada em risco com mudanças arquitetônicas significativas. Se você examinou a vida do x86, em média a arquitetura foi estendida em uma instrução por mês! Quaisquer que sejam as falhas artísticas do x86, lembre-se de que houve mais incidência dessa família arquitetônica nos desktops do que de qualquer outra arquitetura, aumentando em 250 milhões por ano. Apesar disso, esse ancestral diversificado levou a uma arquitetura difícil de explicar e impossível de amar. Preste bem atenção ao que você está para ver! Não tente ler esta seção com o cuidado que precisaria para escrever programas x86; em vez disso, o objetivo é que você tenha alguma familiaridade com os pontos fortes e fracos da arquitetura para desktops mais popular do mundo. Em vez de mostrar o conjunto de instruções inteiro de 16 e 32 bits, nesta seção, vamos nos concentrar no subconjunto de 32 bits originado com o 80386, pois essa parte da arquitetura é a usada. Começamos nossa explicação com os registradores e os modos de endereçamento, prosseguimos para as operações com inteiros e concluímos com um exame da codificação da instrução. Registradores e modos de endereçamento de dados do x86
Os registradores do 80386 mostram a evolução do conjunto de instruções (Figura 2.36). O 80386 estendeu todos os registradores de 16 bits (exceto os registradores de segmento) para 32 bits, inserindo um E no início de seus nomes para indicar a versão de 32 bits. Vamos nos referir a eles genericamente como registradores de uso geral (ou GPRs – General-Purpose Registers). O 80386 contém apenas oito GPRs. Isso significa que os programas do MIPS podem usar quatro vezes isso e os ARM, duas vezes. As instruções aritméticas, lógicas e de transferência de dados são instruções de dois operandos que permitem as combinações mostradas na Figura 2.37. Existem duas diferenças importantes aqui. As instruções aritméticas e lógicas do x86 precisam ter um operando que atue como origem e destino; o ARM e o MIPS admitem registradores separados para origem e destino. Essa restrição coloca mais pressão sobre os registradores limitados, pois um registrador de origem precisa ser modificado. A segunda diferença importante é que um dos operandos pode estar na memória. Assim, praticamente qualquer instrução pode ter um operando na memória, ao contrário do ARM e do MIPS. Os modos de endereçamento de memória, descritos com detalhes a seguir, oferecem dois tamanhos de endereços dentro da instrução. Esses chamados deslocamentos podem ser de 8 bits ou de 32 bits. Embora um operando da memória possa usar qualquer modo de endereçamento, existem restrições em relação a quais registradores podem ser usados em um modo. A Figura 2.38 mostra os modos de endereçamento do x86 e quais GPRs não podem ser usados com cada modo, além de como obter o mesmo efeito usando instruções MIPS.
2.17 Vida real: instruções do x86 137
FIGURA 2.36 O conjunto de registradores do 80386. Começando com o 80386, os oito registradores iniciais foram estendidos para 32 bits e também poderiam ser usados como registradores de uso geral.
FIGURA 2.37 Tipos de instrução para instruções aritméticas, lógicas e de transferência de dados. O x86 permite as combinações mostradas. A única restrição é a ausência de um modo memória-memória. Os imediatos podem ser de 8, 16 ou 32 bits de extensão; um registrador é qualquer um dos 14 principais registradores da Figura 2.36 (não EIP ou EFLAGS).
Operações com inteiros do x86 O 8086 oferece suporte para tipos de dados de 8 bits (byte) e 16 bits (word). O 80386 acrescenta endereços e dados de 32 bits (double words) ao x86. (AMD64 acrescenta endereços e dados de 64 bits, chamados quad words; vamos ficar com o 80386 nesta seção.) As distinções de tipo de dados se aplicam a operações com registrador e também a acessos à memória. Quase toda operação funciona sobre dados de 8 bits e sobre um tamanho de dados maior. Esse tamanho é determinado pelo modo e é de 16 bits ou de 32 bits.
138
Capítulo 2 Instruções: A Linguagem de Máquina
FIGURA 2.38 Modos de endereçamento de 32 bits do x86 com restrições de registrador e o código MIPS equivalente. O modo de endereçamento Base mais Índice Escalado, que não aparece no ARM ou no MIPS, foi incluído para evitar as multiplicações por quatro (fator de escala 2) para transformar um índice de um registrador em um endereço em bytes (veja Figuras 2.25 e 2.27). Um fator de escala 1 é usado para dados de 16 bits e um fator de escala 3, para dados de 64 bits. O fator de escala 0 significa que o endereço não é escalado. Se o deslocamento for maior do que 16 bits no segundo ou quarto modos então o modo MIPS equivalente precisa de mais duas instruções: um lui para ler os 16 bits mais altos do deslocamento e um add para somar a parte alta do endereço ao registrador base $s1. (A Intel oferece dois nomes diferentes para o que é chamado modo de endereçamento com Base – com Base e Indexado –, mas eles são basicamente idênticos, e os combinamos aqui.)
Logicamente, alguns programas querem operar sobre dados de todos os três tamanhos, de modo que as arquiteturas 80386 oferecem uma forma conveniente de especificar cada versão sem expandir muito o tamanho do código. Elas decidiram que os dados de 16 bits ou de 32 bits dominam a maioria dos programas e, por isso, faz sentido poder definir um tamanho grande padrão. Esse tamanho de dados padrão é definido por um bit no registrador do segmento de código. Para redefini-lo, um prefixo de 8 bits é anexado à instrução a fim de dizer à máquina para usar o outro tamanho grande para essa instrução. A solução do prefixo foi emprestada do 8086, o que permite que múltiplos prefixos modifiquem o comportamento da instrução. Os três prefixos originais redefinem o registrador de segmento padrão, bloqueiam o barramento para dar suporte a sincronização (Seção 2.11) ou repetem a instrução seguinte até o registrador ECX chegar a 0. Esse último prefixo tinha por finalidade estar emparelhado com uma instrução mover byte para mover um número variável de bytes. O 80386 também acrescentou um prefixo para redefinir o tamanho de endereço padrão. As operações com inteiros do x86 podem ser divididas em quatro classes principais: 1. Instruções para movimentação de dados, incluindo move, push e pop 2. Instruções aritméticas e lógicas, incluindo operações aritméticas de teste, inteiros e decimais 3. Fluxo de controle, incluindo desvios condicionais, jumps incondicionais, chamadas e retornos 4. Instruções para manipulação de strings, incluindo movimento e comparação de strings As duas primeiras categorias não precisam de comentários, exceto que as operações de instruções aritméticas e lógicas permitem que o destino seja um registrador ou um local da memória. A Figura 2.39 mostra algumas instruções x86 típicas e suas funções. Os desvios condicionais no x86 são baseados em códigos de condição ou flags, assim como no ARM. Os códigos de condição são definidos como um efeito colateral de uma operação; a maioria é usada para comparar o valor de um resultado com 0. Os desvios, então, testam os códigos de condição. Os endereços de desvio relativos ao PC precisam ser especificados no número de bytes, visto que, ao contrário do ARM e MIPS, nem todas as instruções do 80386 possuem 4 bytes de extensão.
2.17 Vida real: instruções do x86 139
As instruções para manipulação de strings fazem parte da linhagem 8080 do x86 e não são comumente executadas na maioria dos programas. Em geral, são mais lentas do que as rotinas de software equivalentes (veja a falácia na Seção 2.18). A Figura 2.40 lista algumas das instruções do x86 com inteiros. Muitas das instruções estão disponíveis nos formatos byte e word.
FIGURA 2.39 Algumas instruções x86 típicas e suas funções. Uma lista de operações frequentes aparece na Figura 2.40. O CALL salva na pilha o EIP da próxima instrução. (EIP é o PC da Intel.)
FIGURA 2.40 Algumas operações típicas do x86. Muitas operações utilizam o formato registrador-memória, no qual a origem ou o destino pode ser a memória e o outro pode ser um registrador ou um operando imediato.
140
Capítulo 2 Instruções: A Linguagem de Máquina
Codificação de instruções do x86 Deixando o pior para o final, a codificação de instruções no 80386 é complexa, com muitos formatos de instrução diferentes. As instruções para o 80386 podem variar de 1 byte, quando não existem operandos, até 15 bytes. A Figura 2.41 mostra o formato de instrução para várias instruções de exemplo na Figura 2.39. O byte de opcode normalmente contém um bit indicando se o operando é de 8 bits ou de 32 bits. Para algumas instruções, o opcode pode incluir o modo de endereçamento e o registrador; isso acontece em muitas instruções que possuem a forma “registrador = registrador op imediato”. Outras instruções utilizam um “pós-byte”, ou byte de opcode extra, rotulado “mod, reg, r/m”, que contém a informação sobre o modo de endereçamento. Esse pós-byte é usado para muitas das instruções que endereçam a memória. O modo “base mais índice escalado” utiliza um segundo pós-byte, rotulado com “sc, índice, base”. A Figura 2.42 mostra a codificação dos dois especificadores de endereço pós-byte para os modos de 16 e 32 bits. Infelizmente, para entender quais registradores e quais modos de endereçamento estão disponíveis, você precisa ver a codificação de todos os modos de endereçamento e, às vezes, até mesmo a codificação das instruções.
FIGURA 2.41 Formatos típicos de instruções x86. A Figura 2.42 mostra a codificação do pós-byte. Muitas instruções contêm o campo de 1 bit w, que indica se a operação é de um byte ou double word. O campo d em MOV é usado em instruções que podem mover de ou para a memória, e mostra a direção do movimento. A instrução ADD requer 32 bits para o campo imediato, visto que no modo de 32 bits, os imediatos são de 8 bits ou de 32 bits. O campo imediato no TEST tem 32 bits de extensão, pois não existe um imediato de 8 bits para testar no modo de 32 bits. Em geral, as instruções podem variar de 1 a 17 bytes de extensão. O tamanho grande vem dos prefixos extras de 1 byte, tendo tanto um imediato de 4 bytes quanto um endereço de deslocamento de 4 bytes, usando um opcode de 2 bytes e usando o especificador do modo de índice escalado, que acrescenta outro byte.
2.18 Falácias e armadilhas 141
FIGURA 2.42 A codificação do primeiro especificador de endereço do x86: “mod, reg, r/m”. As quatro primeiras colunas mostram a codificação do campo reg de 3 bits, que depende do bit w do opcode e se a máquina está no modo de 16 bits (8086) ou no modo de 32 bits (80386). As demais colunas explicam os campos mod e r/m. O significado do campo r/m de 3 bits depende do valor do campo mod de 2 bits e do tamanho do endereço. Basicamente, os registradores utilizados no cálculo do endereço são listados na sexta e sétima colunas, sob mod = 0, com mod = 1 acrescentando um deslocamento de 8 bits e mod = 2 acrescentando um deslocamento de 16 ou 32 bits, dependendo do modo do endereço. As exceções são: 1) r/m = 6 quando mod = 1 ou mod = 2 no modo de 16 bits seleciona BP mais o deslocamento; 2) r/m = 5 quando mod = 1 ou mod = 2 no modo 16 bits seleciona EBP mais deslocamento; e 3) r/m = 4 no modo de 32 bits quando mod não é igual a 3, em que (sib) significa o uso do modo de índice escalado, mostrado na Figura 2.38. Quando mod = 3, o campo r/m indica um registrador, usando a mesma codificação que o campo reg combinado com o bit w.
Conclusão sobre o x86 A Intel tinha um microprocessador de 16 bits dois anos antes das arquiteturas mais elegantes de seus concorrentes, como o Motorola 68000, e essa dianteira levou à seleção do 8086 como CPU para o IBM PC. Os engenheiros da Intel geralmente reconhecem que o x86 é mais difícil de ser montado do que máquinas como ARM e MIPS, mas o mercado maior significa que a AMD e a Intel podem abrir mão de mais recursos para ajudar a contornar a complexidade adicional. O que o x86 perde no estilo é compensado na quantidade, tornando-o belo, do ponto de vista apropriado. A graça salvadora é que os componentes arquitetônicos mais usados do x86 não são tão difíceis de implementar, como a AMD e a Intel já demonstraram, melhorando rapidamente o desempenho dos programas com inteiros desde 1978. Para obter esse desempenho, os compiladores precisam evitar as partes da arquitetura difíceis de implementar com rapidez.
2.18
Falácias e armadilhas
Falácia: instruções mais poderosas significam maior desempenho. Parte do poder do Intel x86 são os prefixos que podem modificar a execução da instrução seguinte. Um prefixo pode repetir a instrução seguinte até que um contador chegue a 0. Assim, para mover dados na memória, pode parecer que a sequência de instruções natural seria usar move com o prefixo de repetição para realizar movimentações de memória para memória em 32 bits. Um método alternativo, que usa as instruções padrão encontradas em todos os computadores, é carregar os dados nos registradores e depois armazenar os registradores na memória. Essa segunda versão do programa, com o código replicado para reduzir o trabalho extra do loop, copia cerca de 1,5 vez mais rápido. Uma terceira versão, que usava os registradores de ponto flutuante maiores no lugar dos registradores inteiros do x86, copia cerca de 2,0 vezes mais rápido do que a instrução complexa. Falácia: escreva em assembly para obter o maior desempenho. Houve uma época em que os compiladores para as linguagens de programação produziam sequências de instrução ingênuas; a sofisticação cada vez maior dos compiladores significa que a lacuna entre o código compilado e o código produzido à mão está se
142
Capítulo 2 Instruções: A Linguagem de Máquina
fechando rapidamente. De fato, para competir com os compiladores atuais, o programador assembly precisa entender perfeitamente os conceitos dos Capítulos 4 e 5 (pipelining do processador e hierarquia de memória). Essa batalha entre compiladores e codificadores assembly é uma situação em que os humanos estão perdendo terreno. Por exemplo, a linguagem C oferece ao programador uma chance de dar uma sugestão ao compilador sobre quais variáveis manter em registradores, em vez de passar para a memória. Quando os compiladores eram fracos na alocação de registradores, essas sugestões eram vitais para o desempenho. De fato, alguns livros-texto sobre C gastavam muito tempo dando exemplos com sugestões de como usar registradores com eficiência. Os compiladores C de hoje, em geral, ignoram essas sugestões, pois o compilador realiza um trabalho melhor na alocação do que o programador. Mesmo se a escrita à mão resultasse em código mais rápido, os perigos de escrever em assembly são maior tempo gasto codificando e depurando, perda de portabilidade e dificuldade de manter esse código. Um dos poucos axiomas aceitos de modo generalizado na engenharia de software é que a codificação leva mais tempo se você escrever mais linhas, e claramente é preciso mais linhas para escrever um programa em assembly do que em C ou Java. Além do mais, uma vez codificado, o próximo perigo é que ele se torne um programa popular. Esses programas sempre vivem por mais tempo do que o esperado, significando que alguém terá de atualizar o código por vários anos e fazer com que funcione com novas versões dos sistemas operacionais e novos modelos de máquinas. A escrita em linguagem de alto nível no lugar do assembly não apenas permite que os compiladores futuros ajustem o código a máquinas futuras, mas também torna o software mais fácil de manter e permite que o programa execute em mais modelos de computadores. Falácia: a importância da compatibilidade binária comercial significa que os conjuntos de instruções bem-sucedidos não mudam. Embora a compatibilidade binária seja sacrossanta, a Figura 2.43 mostra que a arquitetura do x86 cresceu drasticamente. A média é mais de uma instrução por mês no decorrer do seu tempo de vida de 30 anos! Armadilha: esquecer que os endereços sequenciais de palavras em máquinas com endereçamento em bytes não diferem em um. Muitos programadores assembly têm lutado contra erros cometidos pela suposição de que o endereço da próxima palavra pode ser encontrado incrementando-se o endereço em um registrador por um, em vez do tamanho da palavra em bytes. Prevenir é melhor do que remediar! Armadilha: usando um ponteiro para uma variável automática fora de seu procedimento de definição.
FIGURA 2.43 Crescimento do conjunto de instruções do x86 com o tempo. Embora haja um valor técnico claro em algumas dessas extensões, essa mudança rápida também aumenta a dificuldade para outras empresas tentarem montar processadores compatíveis.
2.19 Comentários finais 143
Um engano comum ao lidar com ponteiros é passar um resultado de um procedimento que inclui um ponteiro para um array que é local a esse procedimento. Seguindo a disciplina de pilha da Figura 2.12, a memória que contém o array local será reutilizada assim que o procedimento retornar. Os ponteiros para variáveis automáticas podem levar ao caos.
2.19
Comentários finais
Os dois princípios do computador com programa armazenado são o uso de instruções que sejam indistintas de números e o uso de memória alterável para os programas. Esses princípios permitem que uma única máquina auxilie cientistas ambientais, consultores financeiros e autores de romance em suas especialidades. A seleção de um conjunto de instruções que a máquina possa entender exige um equilíbrio delicado entre a quantidade de instruções necessárias para executar um programa, a quantidade de ciclos de clock necessários por uma instrução e a velocidade do clock. Como ilustramos neste capítulo, quatro princípios de projeto orientam os autores de conjuntos de instruções para fazer esse equilíbrio delicado: 1. Simplicidade favorece a regularidade. A regularidade motiva muitos recursos do conjunto de instruções do MIPS: mantendo todas as instruções com um único tamanho, sempre exigindo três operandos de registrador nas instruções aritméticas e mantendo os campos de registrador no mesmo lugar em cada formato de instrução. 2. Menor é mais rápido. O desejo de velocidade é o motivo para que o MIPS tenha 32 registradores em vez de muito mais. 3. Torne o caso comum veloz. Alguns exemplos de tornar o caso comum do MIPS veloz são o endereçamento relativo ao PC para desvios condicionais e o endereçamento imediato para constantes como operandos. 4. Um bom projeto exige bons compromissos. Um exemplo do MIPS foi o compromisso entre providenciar endereços e constantes maiores nas instruções e manter todas as instruções com o mesmo tamanho. Acima desse nível de máquina está o assembly, uma linguagem que os humanos podem ler. O montador traduz isso para os números binários que as máquinas podem entender e até mesmo “estende” o conjunto de instruções, criando instruções simbólicas que não estão no hardware. Por exemplo, constantes ou endereços que são muito grandes são divididos em partes com tamanho apropriado, variações comuns de instruções recebem seu próprio nome, e assim por diante. A Figura 2.44 lista as instruções MIPS que abordamos até aqui, tanto instruções reais quanto pseudoinstruções. Cada categoria de instruções MIPS está associada a construções que aparecem nas linguagens de programação: j
As instruções aritméticas correspondem às operações encontradas nas instruções de atribuição.
j
As instruções de transferência de dados provavelmente ocorrerão quando se lidam com estruturas de dados, como arrays e estruturas.
j
Os desvios condicionais são usados em instruções if e em loops.
j
Os jumps incondicionais são usados em chamadas de procedimento e em retornos, e para instruções case/switch.
Essas instruções não nasceram iguais; a popularidade das poucas domina as muitas. Por exemplo, a Figura 2.45 mostra a popularidade de cada classe de instruções para o
Menos significa mais. Robert Browning, Andrea del Sarto, 1855
144
Capítulo 2 Instruções: A Linguagem de Máquina
FIGURA 2.44 O conjunto de instruções do MIPS explicado até aqui, com as instruções MIPS reais à esquerda e as pseudoinstruções à direita. O Apêndice B (Seção B.10) descreve a arquitetura MIPS completa. A Figura 2.1 mostra mais detalhes da arquitetura do MIPS revelada neste capítulo. As informações que aparecem aqui são encontradas nas colunas 1 e 2 do Guia de Instrução Rápida do MIPS, no início do livro.
FIGURA 2.45 Classes de instruções MIPS, exemplos, correspondência com construções de linguagem de programação de alto nível e porcentagem média de instruções do MIPS executadas por categoria dos cinco programas de inteiros do SPEC2006. A Figura 3.26 no Capítulo 3 mostra a porcentagem das instruções MIPS individuais executadas.
2.21 Exercícios 145
SPEC2000. A popularidade variada das instruções desempenha um papel importante nos capítulos sobre desempenho, caminho de dados, controle e pipelining. Depois que explicarmos a aritmética do computador no Capítulo 3, revelaremos mais da arquitetura do conjunto de instruções do MIPS.
2.20 Perspectiva histórica e leitura adicional Esta seção analisa a história das arquiteturas de conjuntos de instruções (ISAs) ao longo do tempo, além de oferecer um pequeno histórico das linguagens de programação e compiladores. As ISAs incluem arquiteturas de acumulador, arquiteturas de registrador de uso geral, arquiteturas de pilha e uma rápida história do ARM e do x86. Também revemos os assuntos controvertidos de arquiteturas de computadores de linguagem de alto nível e arquiteturas de computadores com conjunto de instruções reduzido. A história das linguagens de programação inclui Fortran, Lisp, Algol, C, Cobol, Pascal, Simula, Smalltalk, C + + e Java, e a história dos compiladores inclui os principais marcos e os pioneiros que os alcançaram. O restante desta seção está no site.
2.21 Exercícios1 1 Contribuição de John Oliver, da Cal Poly, San Luis Obispo, com colaborações de Nicole Kaiyan (Universidade de Adelaide) e Milos Prvulovic (Georgia Tech)
O Apêndice B descreve o simulador do MIPS, que é útil para estes exercícios. Embora o simulador aceite pseudoinstruções, tente não usá-las em qualquer exercício que pedir para produzir código do MIPS. Seu objetivo deverá ser aprender o conjunto de instruções MIPS real, e se você tiver de contar instruções, sua contagem deverá refletir as instruções reais executadas, e não as pseudoinstruções. Existem alguns casos em que as pseudoinstruções precisam ser usadas (por exemplo, a instrução la quando um valor real não é conhecido durante a codificação em assembly). Em muitos casos, elas são muito convenientes e resultam em código mais legível (por exemplo, as instruções li e move). Se você decidir usar pseudoinstruções por esses motivos, por favor, acrescente uma sentença ou duas à sua solução, indicando quais pseudoinstruções usou e por quê.
Exercício 2.1 Os problemas a seguir lidam com a tradução de C para MIPS. Suponha que as variáveis f, g, h e i sejam dadas e possam ser consideradas inteiros de 32 bits, conforme declarado em um programa C. a.
f=g-h;
b.
f=g+(h-5);
2.1.1 [5] <2.2> Para essas instruções C, qual é o código assembly do MIPS correspondente? Use um número mínimo de instruções assembly do MIPS. 2.1.2 [5] <2.2> Para essas instruções C, quantas instruções assembly do MIPS são necessárias a fim de executar a instrução C? 2.1.3 [5] <2.2> Se as variáveis f, g, h e i possuem o valor de 1, 2, 3 e 4, respectivamente, qual é o valor final de f?
146
Capítulo 2 Instruções: A Linguagem de Máquina
Os problemas a seguir lidam com a tradução de MIPS para C. Suponha que as variáveis
g, h, i e j sejam dadas e possam ser consideradas inteiros de 32 bits, conforme declarado
em um programa C. a.
addi f, f, 4
b.
add f, g, h add f, i, f
2.1.4 [5] <2.2> Para essas instruções MIPS, qual é a instrução C correspondente? 2.1.5 [5] <2.2> Se as variáveis f, g, h e i têm valores 1, 2, 3 e 4, respectivamente, qual é o valor final de f?
Exercício 2.2 Os problemas a seguir lidam com a tradução de C para MIPS. Considere que as variáveis g, h, i e j sejam dadas e possam ser consideradas inteiros de 32 bits, conforme declarado em um programa C. a.
f=g-f;
b.
f=i+(h-2);
2.2.1 [5] <2.2> Para essas instruções C, qual é o código assembly do MIPS correspondente? Use um número mínimo de instruções assembly do MIPS. 2.2.2 [5] <2.2> Para essas instruções C, quantas instruções assembly do MIPS são necessárias a fim de executar a instrução C? 2.2.3 [5] <2.2> Se as variáveis f, g, h e i têm valores 1, 2, 3 e 4, respectivamente, qual é o valor final de f? Os problemas a seguir lidam com a tradução de MIPS para C. Para o exercício a seguir, suponha que as variáveis f, g, h e i sejam dadas e possam ser consideradas inteiros de 32 bits, conforme declarado em um programa C. a. b.
add f, f, h sub f, $0, f addi f, f, 1
2.2.4 [5] <2.2> Para as instruções assembly de MIPS apresentadas, qual é a instrução C correspondente? 2.2.5 [5] <2.2> Se as variáveis f, g, h e i têm valores 1, 2, 3 e 4, respectivamente, qual é o valor final de f?
Exercício 2.3 Os problemas a seguir lidam com a tradução de C para MIPS. Considere que as variáveis f e g sejam dadas e possam ser consideradas inteiros de 32 bits, conforme declarado em um programa C. a.
f
b.
f = g + (−f − 5);
= −g − f;
2.21 Exercícios 147
2.3.1 [5] <2.2> Para essas instruções C, qual é o código assembly do MIPS correspondente? Use um número mínimo de instruções assembly do MIPS. 2.3.2 [5] <2.2> Para as instruções C anteriores, quantas instruções assembly do MIPS são necessárias a fim de executar a instrução C? 2.3.3 [5] <2.2> Se as variáveis f, g, h, i e j têm valores 1, 2, 3, 4 e 5, respectivamente, qual é o valor final de f? Os problemas a seguir lidam com a tradução de MIPS para C. Suponha que as variáveis g, h, i e j sejam dadas e possam ser consideradas inteiros de 32 bits, conforme declarado em um programa C. a.
addi
b.
add i, g, h
f, f, − 4
add f, i, f
2.3.4 [5] <2.2> Para essas instruções MIPS, qual é a instrução C correspondente? 2.3.5 [5] <2.2> Se as variáveis f, g, h e i têm valores 1, 2, 3 e 4, respectivamente, qual é o valor final de f?
Exercício 2.4 Os problemas a seguir lidam com a tradução de C para MIPS. Suponha que as variáveis f, g, h, i e j sejam atribuídas aos registradores $s0, $s1, $s2, $s3e $s4, respectivamente. Considere que o endereço de base dos arrays A e B estejam nos registradores $s6 e $s7, respectivamente. a.
f = g + h + B[4] ;
b.
f = g – A[B[4]] ;
2.4.1 [10] <2.2, 2.3> Para essas instruções C, qual é o código assembly do MIPS correspondente? 2.4.2 [5] <2.2, 2.3> Para as instruções C anteriores, quantas instruções assembly do MIPS são necessárias a fim de executar a instrução C? 2.4.3 [5] <2.2, 2.3> Para as mesmas instruções C, quantos registradores diferentes são necessários a fim de executar a instrução C? Os problemas a seguir lidam com a tradução de MIPS para C. Suponha que as variáveis f, g, h, i e j sejam atribuídas aos registradores $s0, $s1, $s2, $s3e $s4, respectivamente. Considere que o endereço de base dos arrays A e B estejam nos registradores $s6 e $s7, respectivamente.
148
Capítulo 2 Instruções: A Linguagem de Máquina
2.4.4 [10] <2.2, 2.3> Para as instruções assembly de MIPS apresentadas, qual é a instrução C correspondente? 2.4.5 [5] <2.2, 2.3> Para essas instruções assembly do MIPS, reescreva o código assembly de modo a minimizar o número de instruções MIPS (se possível) necessárias para executar a mesma função. 2.4.6 [5] <2.2, 2.3> Quantos registradores são necessários para executar o assembly MIPS conforme escrito anteriormente: se você pudesse reescrever esse código, qual é o número mínimo de registradores necessários?
Exercício 2.5 Nos problemas a seguir estaremos investigando as operações da memória no contexto de um processador MIPS. A tabela a seguir mostra os valores de um array armazenado na memória. Considere que o endereço de base do array está armazenado no registrador $s6 e faça o offset considerando o endereço de base do array. a.
b.
Endereço
Dados
20
4
24
5
28
3
32
2
34
1
Endereço
Dados
24
2
38
4
32
3
36
6
40
1
2.5.1 [10] <2.2, 2.3> Para os locais de memória na tabela anterior, escreva o código C de modo a classificar os dados do mais baixo ao mais alto, colocando o menor valor no menor local de memória mostrado na figura. Suponha que os dados mostrados representem a variável C chamada Array, que é um array do tipo int. Suponha que essa máquina em particular seja uma máquina endereçável por byte e uma word consista em 4 bytes. 2.5.2 [10] <2.2, 2.3> Para os locais de memória na tabela anterior, escreva o código MIPS que classifique os dados do mais baixo ao mais alto, colocando o menor valor no menor local de memória. Use um número mínimo de instruções MIPS. Suponha que o endereço de base de Array esteja armazenado no registrador $s6. 2.5.3 [5] <2.2, 2.3> A fim de classificar o array anterior, quantas instruções são necessárias para o código MIPS? Se você não tiver permissão para usar o campo imediato nas instruções lw e sw, de quantas instruções MIPS você precisa? Os problemas a seguir exploram a tradução de números hexadecimais para outros formatos numéricos. a.
0xabcdef12
b.
0x10203040
2.21 Exercícios 149
2.5.4 [5] <2.3> Traduza esses números hexadecimais para decimal. 2.5.5 [5] <2.3> Mostre como os dados na tabela seriam arrumados na memória de uma máquina little-endian ou big-endian. Suponha que os dados sejam armazenados começando no endereço 0.
Exercício 2.6 Os problemas a seguir lidam com a tradução de C para MIPS. Suponha que as variáveis f, g, h, i e j sejam atribuídas aos registradores $s0, $s1, $s2, $s3e $s4, respectivamente. Considere que o endereço de base dos arrays A e B estejam nos registradores $s6 e $s7, respectivamente, e que os elementos dos arrays A e B seja words de 4 bytes. a.
f = f + A[2] ;
b.
B[8] = A[i] + A[j] ;
2.6.1 [10] <2.2, 2.3> Para essas instruções C, qual é o código assembly do MIPS correspondente? 2.6.2 [5] <2.2, 2.3> Para essas instruções C, quantas instruções assembly do MIPS são necessárias a fim de executar a instrução C? 2.6.3 [5] <2.2, 2.3> Para as instruções C anteriores, quantos registradores são necessários a fim de executar a instrução C usando o código assembly do MIPS? Os problemas a seguir lidam com a tradução de MIPS para C. Suponha que as variáveis f, g, h, i e J sejam atribuídas aos registradores $s0, $s1, $s2, $s3 e $s4, respectivamente. Considere que o endereço de base dos arrays A e B esteja nos registradores $s6 e $s7, respectivamente.
2.6.4 [5] <2.2, 2.3> Para essas instruções assembly do MIPS, qual é a instrução C correspondente? 2.6.5 [5] <2.2, 2.3> Para o assembly MIPS anterior, considere que os registradores $s0, $s1, $s2, $s3 contêm os valores 0x0000000a, 0x00000014, 0x0000001e e 0x00000028, respectivamente. Além disso, suponha que o registrador $s6 contenha o valor 0x00000100, e que a memória contém os seguintes valores: Endereço
Valor
0x00000100
0x00000064
0x00000104
0x000000c8
0x00000106
0x0000012c
150
Capítulo 2 Instruções: A Linguagem de Máquina
Encontre o valor de $s0 ao final do código assembly. 2.6.6 [10] <2.3, 2.5> Em cada instrução MIPS, mostre o valor dos campos op, rs e rt. Para as instruções tipo I, mostre o valor do campo imediato, e com as instruções tipo R, mostre o valor do campo rd.
Exercício 2.7 Os problemas a seguir exploram as conversões numéricas de número binário com sinal e sem sinal para números decimais. a.
0010 0100 1001 0010 0100 1001 0010 0100dois
b.
0101 1111 1011 1110 0100 0000 0000 0000dois
2.7.1 [5] <2.4> Para os padrões acima, qual número de base 10 o código binário representa, considerando que ele é um inteiro de complemento de dois? 2.7.2 [5] <2.4> Para os padrões anteriores, que número de base 10 ele representa, supondo que seja um inteiro sem sinal? 2.7.3 [5] <2.4> Para os padrões anteriores, que número hexadecimal eles representam? Os problemas a seguir exploram conversões numéricas de números decimais para binários com sinal e sem sinal. a.
−1dec
b.
1024dec
2.7.4 [5] <2.4> Para esses números de base dez, converta para binário em complemento de dois. 2.7.5 [5] <2.4> Para os mesmos números de base dez, converta para hexadecimal em complemento de dois. 2.7.6 [5] <2.4> Ainda em relação aos números de base dez, converta os valores negados da tabela para hexadecimal em complemento de dois.
Exercício 2.8 Os problemas a seguir lidam com extensão de sinal e overflow. Os registradores $s0 e $s1 mantêm os valores mostrados na tabela a seguir. Você deverá executar uma assembly de instrução de código MIPS sobre esses registradores e mostrar o resultado.
2.8.1 [5] <2.4> Para o conteúdo dos registradores $t0 e $t1, conforme especificado anteriormente, qual é o valor de $t0 para o código assembly a seguir: add $t0,$s0,$s1
O resultado em $t0 é o resultado desejado ou houve overflow?
2.21 Exercícios 151
2.8.2 [5] <2.4> Para o conteúdo dos registradores $t0 e $t1 conforme especificado anteriormente, qual é o valor de $t0 para o seguinte código assembly: sub $t0,$s0,$s1
O resultado em $t0 é o resultado desejado ou houve overflow? 2.8.3 [5] <2.4> Para o conteúdo dos registradores $t0 e $t1 conforme especificado anteriormente, qual é o valor de $t0 para o seguinte código assembly: add $t0,$s0,$s1 add $t0,$t0,$s0
O resultado em $t0 é o resultado desejado ou houve overflow? Nos problemas a seguir, você executará várias operações MIPS sobre um par de registradores, $t0 e $t1. Dados os valores de $t0 e $t1 em cada uma das questões a seguir, indique se haverá overflow. a.
add $s0,$s0,$s1 add $s0,$s0,$s1
b.
add $s0,$s0,$s1 add $s0,$s0,$s1 add $s0,$s0,$s1
2.8.4 [5] <2.4> Suponha que o registrador $s0 = 0x7000 0000 e $s1 = 0x1000 0000. Para a tabela anterior, haverá overflow? 2.8.5 [5] <2.4> Suponha que o registrador $s0 = 0x4000 0000 e $s1 = 0x2000 0000. Para a tabela anterior, haverá overflow? 2.8.6 [5] <2.4> Suponha que o registrador $s0 = 0x8FFF FFFF e $s1 = 0xD000 0000. Para a tabela anterior, haverá overflow?
Exercício 2.9 A tabela a seguir contém diversos valores para o registrador $s1. Você deverá avaliar se haverá overflow para determinada operação. a.
−1dec
b.
1024dec
2.9.1 [5] <2.4> Considere que o registrador $s0 = 0x7000 0000 e $s1 tenha o valor dado na tabela. Se a instrução add $s0, $s0, $s1 for executada, haverá overflow? 2.9.2 [5] <2.4> Considere que o registrador $s0 = 0x8000 0000 e $s1 tenha o valor dado na tabela. Se a instrução sub $s0, $s0, $s1 for executada, haverá overflow? 2.9.3 [5] <2.4> Considere que o registrador $s0 = 0x7FFF FFFF e $s1 tenha o valor dado na tabela. Se a instrução add $s0, $s0, $s1 for executada, haverá overflow?
152
Capítulo 2 Instruções: A Linguagem de Máquina
A tabela a seguir contém diversos valores para o registrador $s1. Você deverá avaliar se haverá overflow para determinada operação. a.
1010 1101 0001 0000 0000 0000 0000 0010bin
b.
1111 1111 1111 1111 1011 0011 0101 0011bin
2.9.4 [5] <2.4> Considere que o registrador $s0 = 0x7000 0000 e $s1 tenha o valor dado na tabela. Se a instrução add $s0, $s0, $s1 for executada, haverá overflow? 2.9.5 [5] <2.4> Considere que o registrador $s0 = 0x7000 0000 e $s1 tenha o valor dado na tabela. Se a instrução add $s0, $s0, $s1 for executada, qual é o resultado em hexadecimal? 2.9.6 [5] <2.4> Considere que o registrador $s0 = 0x7000 0000 e $s1 tenha o valor dado na tabela. Se a instrução add $s0, $s0, $s1 for executada, qual é o resultado na base dez?
Exercício 2.10 Nos problemas a seguir, a tabela de dados contém bits que representam o opcode de uma instrução. Você deverá traduzir as entradas para o código assembly e determinar que formato da instrução MIPS os bits representam. a.
0000 0010 0001 0000 1000 0000 0010 0000dois
b.
0000 0001 0100 1011 0100 1000 0010 0010dois
2.10.1 [5] <2.5> Para essas entradas binárias, que instrução elas representam? 2.10.2 [5] <2.5> Que tipo de instrução (tipo I, tipo R) as mesmas entradas binárias representam? 2.10.3 [5] <2.4, 2.5> Se as entradas binárias anteriores fossem bits de dados, que número elas representariam em hexadecimal? Nos problemas a seguir, a tabela de dados contém instruções MIPS. Você deverá traduzir as entradas para os bits do opcode e determinar qual é o formato da instrução MIPS. a.
addi$t0,$t0,0
b.
sw $t1, 32($t2)
2.10.4 [5] <2.4, 2.5> Mostre a representação hexadecimal dessas instruções. 2.10.5 [5] <2.5> Que tipo (tipo I, tipo R) essas instruções representam? 2.10.6 [5] <2.5> Qual é a representação binária e hexadecimal dos campos opcode, Rs e Rt nessa instrução? Para as instruções de tipo R, qual é a representação hexadecimal dos campos Rd e funct? Para as instruções de tipo I, qual é a representação hexadecimal do campo imediato?
2.21 Exercícios 153
Exercício 2.11 Nos problemas a seguir, a tabela de dados contém bits que representam o opcode de uma instrução. Você deverá traduzir as entradas para código assembly e determinar que formato de instrução MIPS os bits representam. a.
0x01084020
b.
0x02538822
2.11.1 [5] <2.4, 2.5> Que número binário esse número hexadecimal acima representa? 2.11.2 [5] <2.4, 2.5> Que número decimal esse número hexadecimal representa? 2.11.3 [5] <2.5> Que instrução o número hexadecimal anterior representa? Nos problemas a seguir, a tabela de dados contém os valores de diversos campos das instruções MIPS. Você deverá determinar qual é a instrução e descobrir o formato MIPS para ela. a.
op = 0,rs = 3,rt = 2,rd = 3,shamt = 0,funct = 34
b.
op = 0x23,rs = 1,rt = 2,const = 0x4
2.11.4 [5] <2.5> Que tipo (tipo I, tipo R) essas instruções representam? 2.11.5 [5] <2.5> Qual é a instrução assembly do MIPS descrita anteriormente? 2.11.6 [5] <2.4, 2.5> Qual é a representação binária dessas instruções?
Exercício 2.12 Nos problemas a seguir, a tabela de dados contém diversas modificações que poderiam ser feitas à arquitetura do conjunto de instruções do MIPS. Você investigará o impacto dessas mudanças sobre o formato da instrução da arquitetura MIPS. a.
128 registradores
b.
Quatro vezes mais diferentes instruções
2.12.1 [5] <2.5> Se o conjunto de instruções do processador MIPS for modificado, o formato da instrução também deverá ser alterado. Para cada uma das mudanças sugeridas, mostre o tamanho dos campos de bit de uma instrução no formato de tipo R. Qual é o número total de bits necessários para cada instrução? 2.12.2 [5] <2.5> Se o conjunto de instruções do processador MIPS for modificado, o formato da instrução também deverá ser alterado. Para cada uma das mudanças sugeridas anteriormente, mostre o tamanho dos campos de bit de uma instrução no formato de tipo I. Qual é o número total de bits necessários para cada instrução? 2.12.3 [5] <2.5, 2.10> Por que a mudança sugerida na tabela anterior poderia diminuir o tamanho de um programa assembly do MIPS? Por que a mudança sugerida na tabela anterior aumenta o tamanho de um programa assembly do MIPS?
154
Capítulo 2 Instruções: A Linguagem de Máquina
Nos problemas a seguir, a tabela de dados contém valores hexadecimais. Você deverá determinar qual instrução MIPS o valor representa e descobrir o formato da instrução MIPS. a.
0x01090012
b.
0xAD090012
2.12.4 [5] <2.5> Para essas entradas, qual é o valor do número em decimal? 2.12.5 [5] <2.5> Para essas entradas hexadecimais, que instrução elas representam? 2.12.6 [5] <2.4, 2.5> Que tipo de instrução (tipo I, tipo R) as entradas binárias anteriores representam? Qual é o valor do campo op e do campo rt?
Exercício 2.13 Nos problemas a seguir, a tabela de dados contém os valores para os registradores $t0 e $t1. Você deverá realizar diversas operações lógicas do MIPS sobre esses registradores. a.
$t0 = 0xAAAAAAAA,
b.
$t0 = 0xF00DD00D,
$t1 = 0x12345678 $t1 = 0x11111111
2.13.1 [5] <2.6> Para essas linhas, qual é o valor de $t2 nesta sequência de instruções:
2.13.2 [5] <2.6> Com relação aos valores na tabela anterior, qual é o valor de $t2 para esta sequência de instruções:
2.13.3 [5] <2.6> Para essas linhas, qual é o valor de $t2 nesta sequência de instruções:
No exercício a seguir, a tabela de dados contém diversas operações lógicas do MIPS. Você deverá encontrar o resultado dessas operações dados os valores para os registradores $t0 e $t1.
2.13.4 [5] <2.6> Considere que $t0 = 0x0000A5A5 e que $t1 = 00005A5A. Qual é o valor de $t2 após as duas instruções na tabela? 2.13.5 [5] <2.6> Considere que $t0 = 0xA5A50000 e $t1 = A5A50000. Qual é o valor de $t2 após as duas instruções na tabela? 2.13.6 [5] <2.6> Considere que $t0 = 0xA5A5FFFF e $t1 = A5A5FFFF. Qual é o valor de $t2 após as duas instruções na tabela?
2.21 Exercícios 155
Exercício 2.14
Nos problemas a seguir, você deverá escrever instruções MIPS para extrair os bits “Campo” do registrador $t0 e colocá-los no registrador $t1 no local indicado na tabela a seguir.
2.14.1 [20] <2.6> Encontre a sequência mais curta de instruções MIPS que extrai um campo de $t0 para os valores constantes i = 22 e j = 5 e coloca o campo em $t1 no formato mostrado na tabela de dados. 2.14.2 [5] <2.6> Encontre a sequência mais curta de instruções MIPS que extrai um campo de $t0 para os valores constantes i = 4 e j = 0 e coloca o campo em $t1 no formato mostrado na tabela de dados. 2.14.3 [5] <2.6> Encontre a sequência mais curta de instruções MIPS que extrai um campo de $t0 para os valores constantes i = 31 e j = 28 e coloca o campo em $t1 no formato mostrado na tabela de dados. Nos problemas a seguir, você deverá escrever instruções MIPS para extrair os bits “Campo” do registrador $t0 mostrado na figura e colocá-los no registrador $t1 no local indicado na tabela a seguir. Os bits mostrados como “XXX” devem permanecer inalterados.
2.14.4 [20] <2.6> Encontre a sequência mais curta de instruções MIPS que extrai um campo de $t0 para os valores constantes i = 17 e j = 11 e coloca o campo em $t1 no formato mostrado na tabela de dados. 2.14.5 [5] <2.6> Encontre a sequência mais curta de instruções MIPS que extrai um campo de $t0 para os valores constantes i = 5 e j = 0 e coloca o campo em $t1 no formato mostrado na tabela de dados. 2.14.6 [5] <2.6> Encontre a sequência mais curta de instruções MIPS que extrai um campo de $t0 para os valores constantes i = 31 e j = 29 e coloca o campo em $t1 no formato mostrado na tabela de dados.
156
Capítulo 2 Instruções: A Linguagem de Máquina
Exercício 2.15 Para estes problemas, a tabela mantém algumas operações lógicas que não estão incluídas no conjunto de instruções MIPS. Como essas instruções podem ser implementadas? a.
not $t1,$t2
b.
orn $t1,$t2,$t3
// bit-wise invertido / / bit-wise OR de $t2,!$t3
2.15.1 [5] <2.6> Essas instruções lógicas não estão incluídas no conjunto de instruções MIPS, mas são descritas aqui. Se o valor de $t2 = 0x00FFA5A5 e o valor de $t3 = 0xFFFF003C, qual é o resultado em $t1? 2.15.2 [10] <2.6> Essas instruções lógicas não estão incluídas no conjunto de instruções MIPS, mas podem ser sintetizadas usando-se uma ou mais instruções assembly do MIPS. Forneça um conjunto mínimo de instruções MIPS que podem ser usadas no lugar das instruções na tabela anterior. 2.15.3 [5] <2.6> Para a sua sequência de instruções em 2.15.2, mostre a representação em nível de bit de cada instrução. Diversas instruções lógicas em nível de C aparecem na tabela a seguir. Neste exercício, você deverá avaliar as instruções e implementar essas instruções C usando instruções assembly do MIPS. a.
A = B|! A ;
b.
A = C[0] << 4 ;
2.15.4 [5] <2.6> A tabela anterior mostra diferentes instruções C que utilizam operadores lógicos. Se o local de memória em C[0] contém o valor inteiro 0x00001234 e os valores inteiros iniciais de A e B são 0x00000000 e 0x00002222, qual é o valor resultante de A? 2.15.5 [5] <2.6> Para as instruções C na tabela anterior, escreva uma sequência mínima de instruções assembly do MIPS que realiza a operação idêntica. 2.15.6 [5] <2.6> Para a sua sequência de instruções em 2.15.5, mostre a representação em nível de bit de cada instrução.
Exercício 2.16 Para estes problemas, a tabela mantém diversos valores binários para o registrador $t0. Dado o valor de $t0, você deverá avaliar o resultado de diferentes desvios. a.
0010 0100 1001 0010 0100 1001 0010 0100dois
b.
0101 1111 1011 1110 0100 0000 0000 0000dois
2.16.1 [5] <2.7> Suponha que o registrador $t0 contenha um desses valor e $t1 tenha o valor 0011 1111 1111 1000 0000 0000 0000 0000bin
2.21 Exercícios 157
Note o resultado da execução de tais instruções em certos registradores. Qual é o valor de $t2 depois das seguintes instruções?
2.16.2 [10] <2.7> Suponha que o registrador $t0 contenha um valor da tabela anterior e seja comparado com o valor X, conforme usado na instrução MIPS a seguir. Para que valores de X, se houver, $t0 será igual a 1? slti$t2,$t0,X
2.16.3 [5] <2.7> Suponha que o contador de programa (PC) seja definido como 0x0000 0020. É possível usar a instrução assembly do MIPS para salto (j) a fim de definir o PC como o endereço mostrado na tabela de dados anterior? É possível usar uma instrução assembly do MIPS branch-on-equal (beq) para definir o PC como o endereço mostrado na tabela de dados anterior? Para estes problemas, a tabela mantém diversos valores binários para o registrador $t0. Dado o valor de $t0, você deverá avaliar o resultado de diferentes desvios. a.
0x00101000
b.
0x80001000
2.16.4 [5] <2.7> Suponha que o registrador $t0 contenha um valor da tabela anterior. Qual é o valor de $t2 após as instruções a seguir?
2.16.5 [5] <2.6, 2.7> Suponha que o registrador $t0 contenha um valor da tabela anterior. Qual é o valor de $t2 após as instruções a seguir? sll$t0,$t0,2 slt $t2,$t0,$0
2.16.6 [5] <2.7> Suponha que o contador de programa (PC) seja definido como 0x2000 0000. É possível usar a instrução assembly do MIPS para salto (j) a fim de definir o PC como o endereço mostrado na tabela de dados anterior? É possível usar uma instrução assembly do MIPS branch-on-equal (beq) para definir o PC como o endereço mostrado na mesma tabela de dados? Note o formato das instruções de tipo J.
158
Capítulo 2 Instruções: A Linguagem de Máquina
Exercício 2.17 Para estes problemas, mostramos diversas instruções que não estão incluídas no conjunto de instruções do MIPS. a.
subi$t2,$t3,5
b.
rpt $t2,loop
# R[rt] = R[rs] − SignExtImm # if(R[rs] > 0)R[rs] = R[rs] − 1,PC = PC + 4 + BranchAddr
2.17.1 [5] <2.7> A tabela anterior contém algumas instruções não incluídas no conjunto de instruções do MIPS e a descrição de cada instrução. Por que essas instruções não estão incluídas no conjunto de instruções do MIPS? 2.17.2 [5] <2.7> A tabela anterior contém algumas instruções não incluídas no conjunto de instruções do MIPS e a descrição de cada instrução. Se essas instruções tivessem de ser implementadas no conjunto de instruções do MIPS, qual é o formato de instrução mais apropriado? 2.17.3 [5] <2.7> Para cada instrução na tabela anterior, encontre a sequência mais curta de instruções do MIPS que executa a mesma operação. Para estes problemas, a tabela mantém fragmentos de código assembly do MIPS. Você deverá avaliar cada um dos fragmentos de código, familiarizando-se com as diferentes instruções de desvio do MIPS.
2.17.4 [5] <2.7> Para os loops escritos em assembly do MIPS anterior, suponha que o registrador $t1 seja inicializado com o valor 10. Qual é o valor no registrador $t2 considerando que o $t2 é inicialmente zero? 2.17.5 [5] <2.7> Para cada um dos loops anteriores, escreva a rotina de código C equivalente. Considere que os registradores $t1, $t2, $t1 e $t2 sejam inteiros A, B, i e temp, respectivamente. 2.17.6 [5] <2.7> Para os loops escritos anteriormente em assembly MIPS, suponha que o registrador $t1 seja inicializado com o valor N. Quantas instruções MIPS são executadas?
Exercício 2.18 Para estes problemas, a tabela mantém algum código C. Você deverá avaliar essas instruções de código C no código assembly do MIPS.
2.21 Exercícios 159
2.18.1 [5] <2.7> Para a tabela anterior, desenhe um gráfico de fluxo de controle do código C. 2.18.2 [5] <2.7> Para a tabela anterior, traduza o código C para o código assembly do MIPS. Use um número mínimo de instruções. Suponha que os valores de a, b, i, j estejam nos registradores $s0, $s1, $t0, $t1, respectivamente. Além disso, suponha que o registrador $s2 mantenha o endereço de base do array D. 2.18.3 [5] <2.7> Quantas instruções MIPS são necessárias para implementar o código C? Se as variáveis a e b forem inicializadas como 10 e 1 e todos os elementos de D forem inicialmente 0, qual é o número total de instruções MIPS que são executadas para completar o loop? Para estes problemas, a tabela mantém fragmentos de código assembly do MIPS. Você deverá avaliar cada um dos fragmentos de código, familiarizando-se com as diferentes instruções de desvio do MIPS.
2.18.4 [5] <2.7> Qual é o número total de instruções MIPS executadas? 2.18.5 [5] <2.7> Traduza esses loops para C. Suponha que o inteiro i em nível de C seja mantido no registrador $t1, $s2 mantenha o inteiro em nível de C chamado result, e $s0 mantenha o endereço de base do inteiro MemArray. 2.18.6 [5] <2.7> Reescreva o loop para reduzir o número de instruções MIPS executadas.
Exercício 2.19 Para os problemas a seguir, a tabela mantém funções de código em C. Suponha que a primeira função listada na tabela se chame first. Você deverá traduzir essas rotinas de código C para o assembly do MIPS.
160
Capítulo 2 Instruções: A Linguagem de Máquina
2.19.1 [15] <2.8> Implemente o código C na tabela em assembly do MIPS. Qual é o número total de instruções MIPS necessárias para executar a função? 2.19.2 [5] <2.8> As funções normalmente podem ser implementadas pelos compiladores “em linha”. Uma função em linha é quando o corpo da função é copiado para o espaço do programa, permitindo que o overhead da chamada de função seja eliminado. Implemente uma versão “em linha” do código C da tabela em assembly do MIPS. Qual é a redução no número total de instruções assembly do MIPS necessárias para completar a função? Suponha que a variável C n seja inicializada como 5. 2.19.3 [5] <2.8> Para cada chamada de função, mostre o conteúdo da pilha após a chamada de função ser feita. Suponha que o ponteiro de pilha esteja originalmente no endereço 9x7ffffffc e siga as convenções de registrador especificadas na Figura 2.11. Os três problemas a seguir neste exercício referem-se a uma função f, que chama outra função func. O código para a função func em C já está compilado em outro módulo usando a convenção chamada MIPS da Figura 2.14. A declaração de função para func é “int func(int a, int b);”. O código para a função f é o seguinte:
2.19.4 [10] <2.8> Traduza a função f para o montador MIPS, também usando a convenção de chamada do MIPS da Figura 2.14. Se você precisar usar os registradores de $t0 até $t7, use primeiro os registradores de número mais baixo. 2.19.5 [5] <2.8> Podemos usar a otimização “tail-call” nesta função? Se negativo, explique por que não. Se afirmativo, qual é a diferença no número de instruções executadas em f com e sem a otimização? 2.19.6 [5] <2.8> Imediatamente antes que a sua função f do Problema 2.19.4 retorne, o que sabemos sobre o conteúdo dos registradores $t5, $s3, $ra e $sp? Lembre-se de que sabemos o conteúdo da função f , mas, para a função func , só conhecemos sua declaração.
Exercício 2.20 Este exercício lida com chamadas de procedimento recursivas. Para os problemas a seguir, a tabela tem um fragmento de código assembly que calcula o fatorial de um número. Porém, as entradas na tabela têm erros, e você deverá corrigi-los. Para o número n, fatorial de n = 1 x 2 x 3 x .. .. x n.
2.21 Exercícios 161
2.20.1 [5] <2.8> O programa assembly do MIPS anterior calcula o fatorial de determinada entrada. A entrada inteira é passada pelo registrador $a0 e o resultado é retornado no registrador $v0. No código assembly existem alguns erros. Corrija os erros do MIPS. 2.20.2 [10] <2.8> Para o programa MIPS de fatorial recursivo, suponha que a entrada seja 4. Reescreva o programa de fatorial para operar de uma maneira não recursiva. Restrinja seu uso de registrador aos registradores $s0-$s7. Qual é o número total de instruções usadas a fim de executar sua solução de 2.20.2 contra a versão recursiva do programa de fatorial? 2.20.3 [5] <2.8> Mostre o conteúdo da pilha após cada chamada de função, supondo que a entrada seja 4. Para os problemas a seguir, a tabela tem um fragmento de código assembly que calcula um número de Fibonacci. Porém, as entradas na tabela possuem erros e você deverá corrigi-los. Para o número n, o Fibonacci de n é calculado do seguinte modo: n
Fibonacci de n
1
1
2
1
3
2
4
3
5
5
6
8
7
13
8
21
162
Capítulo 2 Instruções: A Linguagem de Máquina
2.20.4 [5] <2.8> Esse programa em assembly MIPS calcula o número de Fibonacci de determinada entrada. A entrada inteira é passada pelo registrador $a0 e o resultado é retornado no registrador $v0. No código assembly, existem alguns erros. Corrija os erros do MIPS. 2.20.5 [10] <2.8> Para o programa de Fibonacci em MIPS anterior, suponha que a entrada seja 4. Reescreva o programa de Fibonacci a fim de operar de uma maneira não recursiva. Restrinja seu uso de registrador aos registradores $s0-$s7. Qual é o número total de instruções usadas para executar sua solução de 2.20.2 contra a versão recursiva do programa de fatorial? 2.20.6 [5] <2.8> Mostre o conteúdo da pilha após cada chamada de função, considerando que a entrada seja 4.
Exercício 2.21 Considere que a pilha e os segmentos de dados estáticos estejam vazios e que a pilha e os ponteiros globais comecem no endereço 0x7fff fffc e 0x1000 8000, respectivamente. Considere as convenções de chamada especificadas na Figura 2.11 e que as entradas de função são passadas usando-se os registradores $a0-$a3 e retornadas no registrador $r0. Suponha que as funções de folha só possam utilizar registradores salvos.
2.21 Exercícios 163
2.21.1 [5] <2.8> Escreva o código assembly do MIPS para o código da tabela anterior. 2.21.2 [5] <2.8> Mostre o conteúdo da pilha e os segmentos de dados estáticos depois de cada chamada de função. 2.21.3 [5] <2.8> Se a função leaf pudesse usar registradores temporários ($t0, $t1, etc.), escreva o código MIPS para o código na tabela anterior. Os três problemas a seguir neste exercício referem-se a essa função, escrita no assembler do MIPS seguindo as convenções de chamada da Figura 2.14:
2.21.4 [10] <2.8> Este código contém um erro que viola a convenção de chamada do MIPS. Qual é o erro e como ele deve ser consertado? 2.21.5 [10] <2.8> Qual é o equivalente C desse código? Suponha que os argumentos da função se chamem a, b, c etc. na versão C da função. 2.21.6 [10] <2.8> No ponto em que essa função é chamada, os registradores $a0, $a1, $a2 e $a3 têm os valores 1, 100, 1.000 e 30, respectivamente. Qual é o valor retornado por essa função? Se outra função g for chamada a partir de f, suponha que o valor retornado de g sempre seja 500.
164
Capítulo 2 Instruções: A Linguagem de Máquina
Exercício 2.22 Este exercício explora a conversão ASCII e Unicode. A tabela a seguir mostra strings de caracteres. a.
hello world
b.
0123456789
2.22.1 [5] <2.9> Traduza as strings para valores de bytes ASCII decimais. 2.22.2 [5] <2.9> Traduza as strings para Unicode com 16 bits (usando a notação hexa e o conjunto de caracteres Basic Latin). A tabela a seguir mostra valores de caracteres em ASCII hexadecimal. a.
41 44 44
b.
40 49 50 53
2.22.3 [5] <2.5, 2.9> Traduza os valores ASCII hexadecimais para texto.
Exercício 2.23 Neste exercício, você deverá escrever um programa em assembly MIPS que converte strings para o formato numérico especificado na tabela. a.
strings decimais inteiros positivos e negativos
b.
inteiros hexadecimais positivos
2.23.1 [10] <2.9> Escreva um programa em linguagem assembly do MIPS para converter uma string de números ASCII com as condições listadas na tabela anterior, para um inteiro. Seu programa deverá esperar que o registrador $a0 contenha o endereço de uma string terminada em nulo, contendo alguma combinação dos dígitos de 0 até 9. Seu programa deverá calcular o valor inteiro equivalente a essa string de dígitos, depois colocar o número no registrador $v0. Se um caractere não de dígito aparecer em qualquer lugar da string, seu programa deverá parar com um valor -1 no registrador $v0. Por exemplo, se o registrador $a0 apontar para uma sequência de três bytes 50dec, 52dec, 0dec (a string “24” terminada em nulo), então, quando o programa terminar, o registrador $v0 deverá conter o valor 24dec.
Exercício 2.24 Considere que o registrador $t1 contenha o endereço 0x1000 0000 e o registrador $t2 contenha o endereço 0x1000 0010. Note que a arquitetura MIPS utiliza endereçamento de extremidades.
2.21 Exercícios 165
2.24.1 [5] <2.9> Suponha que os dados (em hexadecimal) no endereço 0x1000 0000 sejam: 1000 0000
12
34
56
78
Que valor é armazenado no endereço apontado pelo registrador $t2? Considere que o local da memória apontado para $t2 seja inicializado como 0xFFFF FFFF. 2.24.2 [5] <2.9> Suponha que os dados (em hexadecimal) no endereço 0x1000 0000 sejam: 10000000
80
80
80
80
Que valor é armazenado no endereço apontado pelo registrador $t2? Suponha que o local de memória apontado para $t2 seja inicializado como 0x0000 0000. 2.24.3 [5] <2.9> Suponha que os dados (em hexadecimal) no endereço 0x1000 0000 sejam: 1000 0000
11
00
00
FF
Que valor é armazenado no endereço apontado pelo registrador $t2? Suponha que o local de memória apontado por $t2 seja inicializado como 0x5555 5555.
Exercício 2.25 Neste exercício, você explorará as constantes de 32 bits em MIPS. Para os problemas seguintes, você estará usando os dados binários desta tabela: a.
0010 0000 0000 0001 0100 1001 0010 0100dois
b.
0000 1111 1011 1110 0100 0000 0000 0000dois
2.25.1 [10] <2.10> Escreva o código MIPS que cria as constantes de 32 bits listadas anteriormente e armazena esse valor no registrador $t1. 2.25.2 [5] <2.6, 2.10> Se o valor atual do PC for 0x0000 0000, você pode usar uma única instrução de salto para chegar ao endereço do PC, conforme mostrado na tabela anterior? 2.25.3 [5] <2.6, 2.10> Se o valor atual do PC for 0x0000 0600, você pode usar uma única instrução de desvio a fim de chegar ao endereço do PC, conforme mostrado na tabela anterior? 2.25.4 [5] <2.6, 2.10> Se o valor atual do PC for 0x1FFFf000, você pode usar uma única instrução de desvio para chegar ao endereço do PC, conforme mostrado na tabela anterior? 2.25.5 [10] <2.10> Se o campo imediato de uma instrução MIPS tivesse apenas 8 bits de largura, escreva o código MIPS que cria as constantes de 32 bits listadas anteriormente e armazena esse valor no registrador $t1. Não use a instrução lui.
166
Capítulo 2 Instruções: A Linguagem de Máquina
Para os problemas a seguir, você estará usando o código assembly do MIPS conforme listado nesta tabela:
2.25.6 [5] <2.6, 2.10> Qual é o valor do registrador $t0 após a sequência de código na tabela anterior? 2.25.7 [5] <2.6, 2.10> Escreva o código C que é equivalente ao código assembly na tabela. Suponha que a maior constante que você pode carregar em um inteiro de 32 bits seja 16 bits.
Exercício 2.26 Neste exercício, você explorará a gama de instruções de desvio e salto no MIPS. Nos problemas seguintes, use os dados hexadecimais desta tabela: a.
0x00020000
b.
0xffffff00
2.26.1 [10] <2.6, 2.10> Se o PC estiver no endereço 0x0000 0000, quantas instruções de desvio (não de salto) você precisa para chegar ao endereço na tabela anterior? 2.26.2 [10] <2.6, 2.10> Se o PC estiver no endereço 0x0000 0000, de quantas instruções de salto (não de desvio ou de salto para registrador) você precisa para chegar ao endereço de destino na tabela anterior? 2.26.3 [10] <2.6, 2.10> Para reduzir o tamanho dos programas em MIPS, os projetistas do MIPS decidiram cortar o campo imediato das instruções tipo I de 16 bits para 8 bits. Se o PC estiver no endereço 0x0000 0000, quantas instruções de desvio são necessárias a fim de definir o PC como o endereço na tabela anterior? Nos problemas a seguir, você estará fazendo modificações na arquitetura do conjunto de instruções MIPS. a.
128 registradores
b.
Quatro vezes mais diferentes operações
2.26.4 [10] <2.6, 2.10> Se o conjunto de instruções do processador MIPS for modificado, o formato da instrução também deverá ser alterado. Para cada uma das mudanças sugeridas anteriormente, qual é o impacto sobre a faixa de endereços em uma instrução beq? Suponha que todas as instruções permaneçam com 32 bits de extensão e que quaisquer mudanças feitas ao formato das instruções de tipo I só aumentem/diminuam o campo imediato da instrução beq. 2.26.5 [10] <2.6, 2.10> Se o conjunto de instruções do processador MIPS for modificado, o formato da instrução também deverá ser alterado. Para cada uma das mudanças
2.21 Exercícios 167
sugeridas anteriormente, qual é o impacto sobre a faixa de endereços em uma instrução jump? Suponha que todas as instruções permaneçam com 32 bits de extensão e que quaisquer mudanças feitas ao formato das instruções de tipo J só afetem o campo de endereço da instrução jump. 2.26.6 [10] <2.6, 2.10> Se o conjunto de instruções do processador MIPS for modificado, o formato da instrução também deverá ser alterado. Para cada uma das mudanças sugeridas anteriormente, qual é o impacto sobre a faixa de endereços em uma instrução jump register, supondo que cada instrução precise ter 32 bits.
Exercício 2.27 Nos problemas a seguir, você explorará diferentes modos de endereçamento na arquitetura do conjunto de instruções MIPS. Esses diferentes modos de endereçamento são listados na tabela a seguir. a.
Endereçamento de Base ou Deslocamento
b.
Endereçamento Pseudodireto
2.27.1 [5] <2.10> Na tabela anterior existem modos de endereçamento diferentes do conjunto de instruções MIPS. Dê um exemplo de instruções MIPS que mostre o modo de endereçamento do MIPS. 2.27.2 [5] <2.10> Para as instruções em 2.27.1, qual é o tipo de formato de instrução usado em determinada instrução? 2.27.3 [5] <2.10> Liste benefícios e desvantagens de determinado modo de endereçamento do MIPS. Escreva o código do MIPS que mostra esses benefícios e as desvantagens. Nos problemas a seguir, você usará o código assembly do MIPS conforme listado a seguir, para explorar as opções do campo imediato nas instruções tipo I do MIPS.
2.27.4 [15] <2.10> Para as instruções MIPS da tabela anterior, mostre a representação da instrução em nível de bit de cada uma das instruções em hexadecimal. 2.27.5 [10] <2.10> Reduzindo o tamanho dos campos imediatos das instruções de tipo I e J, podemos economizar no número de bits necessários para representar instruções. Se o campo imediato das instruções tipo I fosse de 8 bits e o campo imediato das instruções de tipo J fosse de 18 bits, reescreva o código MIPS apresentado para refletir essa mudança. Evite usar a instrução lui. 2.27.6 [5] <2.10> Quantas instruções extras são necessárias para executar seu código nas instruções da tabela em 2.27.5 contra o código mostrado na tabela anterior?
168
Capítulo 2 Instruções: A Linguagem de Máquina
Exercício 2.28 A tabela a seguir contém código assembly do MIPS para um lock. Faça referência à definição de ll e pares sc das instruções MIPS.
2.28.1 [5] <2.11> Para cada teste e falha do store condicional, quantas instruções precisam ser executadas? 2.28.2 [5] <2.11> Para o código de load locked/store condicional apresentado, explique por que esse código poderá falhar. 2.28.3 [15] <2.11> Reescreva o código anterior de modo que ele possa operar corretamente. Não se esqueça de evitar quaisquer condições de race. Cada entrada na tabela a seguir possui código e também mostra o conteúdo de diversos registradores. A notação “($s1)” mostra o conteúdo de um local de memória apontado pelo registrador $s1. O código assembly em cada tabela é executado no ciclo mostrado em processadores paralelos, com um espaço de memória compartilhado. a. Processador 1 Processador 1
Processador 2
MEM
Processador 2
Ciclo
$t1
$t0
($s1)
$t1
$t0
0
1
2
99
30
40
11 $t1, 0($s1)
1
11 $t1, 0($s1)
2 sc $t0, 0($s1)
3
sc $t0, 0($s1)
4
b. Processador 1 Processador 1
Processador 2
11 $t1, 0($s1)
sc $t0, 0($s1)
MEM
Processador 2
Ciclo
$t1
$t0
($s1)
$t1
$t0
0
1
2
99
30
40
1 11 $t1, 0($s1)
2
Addi $t1, $t
3
sc $t0, 0($s1)
4 5
2.28.4 [5] <2.11> Preencha a tabela com o valor dos registradores para cada ciclo indicado.
2.21 Exercícios 169
Exercício 2.29 Os três primeiros problemas neste exercício referem-se à seção crítica na forma lock(lk); operação unlock(lk);
em que “operation” atualiza a variável compartilhada shvar utilizando a variável (nãocompartilhada) local x da seguinte maneira: Operação a.
shvar = max(shvar,x) ;
b.
if(shvar > 0) shvar = max(shvar,x) ;
2.29.1 [10] <2.11> Escreva o código do montador MIPS para essa seção crítica, supondo que o endereço da variável lk esteja em $a0, o endereço da variável shvar esteja em $a1 e o valor da variável x esteja em $a2. Sua seção crítica não deverá conter quaisquer chamadas de função, ou seja, você deverá incluir as instruções MIPS para as operações lock() , unlock() , max() e min() . Use instruções ll/sc a fim de implementar a operação lock(), e a operação unlock() é simplesmente uma instrução store comum. 2.29.2 [10] <2.11> Repita o Problema 2.29.1, mas desta vez use ll/sc para realizar uma atualização atômica da variável shvar diretamente, sem usar lock() e unlock(). Observe que, neste problema, não existe uma variável lk. 2.29.3 [10] <2.11> Compare o desempenho do melhor caso do seu código de 2.29.1 e 2.29.2, supondo que cada instrução leva um ciclo para ser executada. Nota: melhor caso significa que ll/sc sempre tem sucesso, o lock sempre está livre quando queremos usar lock() e, se houver um desvio, seguimos o caminho que completa a operação com a menor quantidade de instruções executadas. 2.29.4 [10] <2.11> Usando o seu código de 2.29.2 como exemplo, explique o que acontece quando dois processadores começam a executar essa seção crítica ao mesmo tempo, supondo que cada processador executa exatamente uma instrução por ciclo. 2.29.5 [10] <2.11> Explique por que no seu código de 2.29.2, o registrador $a1 contém o endereço da variável shvar e não o valor dessa variável, e por que o registrador $a2 contém o valor da variável x e não seu endereço. 2.29.6 [10] <2.11> Se quisermos executar atomicamente a mesma operação sobre duas variáveis compartilhadas (por exemplo, shvar1 e shvar2) na mesma seção crítica, podemos fazer isso facilmente usando a técnica de 2.29.1 (simplesmente coloque as duas atualizações entre a operação lock e a operação unlock correspondente). Explique por que não podemos fazer isso usando a técnica de 2.29.2, ou seja, por que não podemos usar ll/sc para acessar variáveis compartilhadas de um modo que garanta que ambas as atualizações sejam executadas juntas como uma única operação atômica.
170
Capítulo 2 Instruções: A Linguagem de Máquina
Exercício 2.30 Instruções assembler não são parte do conjunto de instruções do MIPS, mas normalmente aparecem nos programas do MIPS. A tabela a seguir contém algumas instruções do assembler do MIPS que são traduzidas para instruções reais do MIPS. a.
clear $t0
b.
beq $t1, large, LOOP
2.30.1 [5] <2.12> Para cada pseudoinstrução na tabela anterior, produza uma sequência mínima de instruções MIPS reais a fim de realizar a mesma coisa. Você pode ter de usar registradores temporários em alguns casos. Na tabela, large refere-se a um número que exige 32 bits para ser representado e small a um número que pode ser contido em 16 bits. A tabela a seguir contém algumas instruções assembly do MIPS que foram traduzidas para instruções reais do MIPS. a.
bltu $s0, $t1, Loop
b.
ulw, $v0, v
2.30.2 [5] <2.12> A instrução na tabela precisa ser editada durante a fase de link-edição? Por quê?
Exercício 2.31 A tabela a seguir contém os detalhes em nível de link de dois procedimentos diferentes. Neste exercício, você tomará o lugar do link-editor.
a.
Procedimento A Segmento de texto
Segmento de dados Informação de relocação
Tabela de símbolos
Procedimento B
Endereço
Instrução
Segmento de texto
Endereço
Instrução
0 4
lbu $a0, 0($gp)
0
sw $a1, 0($gp)
jal 0
4
0
(X)
jal 0
0
(Y)
—
—
Endereço
Tipo de instrução
Dependência
—
—
Endereço
Tipo de instrução
Dependência
0
lbu
4
jai
X
0
sw
Y
B
4
jal
A
Endereço
Símbolo
—
X
Endereço
Símbolo
—
Y
—
B
—
A
Segmento de dados Informação de relocação
Tabela de símbolos
b.
2.21 Exercícios 171
Procedimento A Segmento de texto
Segmento de dados Informação de relocação
Tabela de símbolos
Procedimento B
Endereço
Instrução
Endereço
Instrução
0
lui $at, 0
Segmento de texto
0
sw $a0, 0($gp)
4
ori $a0, $at, 0
4
jmp 0
—
—
—
—
0x84
jr $ra
0x180
jr $ra
—
—
—
…
0
(X)
0
(Y)
—
—
—
—
Endereço
Tipo de instrução
Endereço
Tipo de instrução
Segmento de dados Dependência
Informação de relocação
Dependência
0
lui
X
0
sw
Y
4
ori
X
4
jmp
F00 A
Endereço
Símbolo
—
X
Tabela de símbolos
0x180
jal
Endereço
Símbolo
—
Y
0x180
F00
—
A
2.31.1 [5] <2.12> Link-edite os arquivos objetos dessa tabela para formar o cabeçalho do arquivo executável. Suponha que o Procedimento A tenha um tamanho de texto 0x140, tamanho de dados 0x40 e o Procedimento B tenha um tamanho de texto 0x300 e tamanho de dados 0x50. Considere também a estratégia de alocação de memória mostrada na Figura 2.13. 2.31.2 [5] <2.12> Que limitações, se houver alguma, existem sobre o tamanho de um executável? 2.31.3 [5] <2.12> Dado o seu conhecimento das limitações das instruções de desvio e salto, por que um montador poderia ter problemas implementando diretamente as instruções de desvio e salto em um arquivo objeto?
Exercício 2.32 Os três primeiros problemas neste exercício consideram que a função swap, em vez do código da Figura 2.24, é definida em C da seguinte forma:
2.32.1 [10] <2.13> Traduza esta função para o código do montador MIPS. 2.32.2 [5] <2.13> O que precisa mudar na função sort? 2.32.3 [5] <2.13> Se estivéssemos classificando bytes de 8 bits, e não words de 32 bits, como o seu código MIPS para o swap em 2.32.1 mudaria?
172
Capítulo 2 Instruções: A Linguagem de Máquina
Para os três problemas restantes neste exercício, consideramos que a função sort da Figura 2.27 é alterada da seguinte forma: a.
Use a função swap desde o começo deste exercício.
b.
Ordene um array de n bytes ao invés de n words.
2.32.4 [5] <2.13> Essa mudança afeta o código para salvar e restaurar registradores na Figura 2.27? 2.32.5 [10] <2.13> Ao classificar um array de 10 elementos que já estava classificado, quantas instruções a mais (ou a menos) são executadas como resultado dessa mudança? 2.32.6 [10] <2.13> Ao classificar um array de 10 elementos que já estava classificado em ordem decrescente (oposto à ordem que sort() cria), quantas instruções a mais (ou a menos) são executadas como resultado dessa mudança?
Exercício 2.33 Os problemas neste exercício referem-se à função a seguir, dada como um código de array:
2.33.1 [10] <2.14> Traduza esta função para o assembly MIPS. 2.33.2 [10] <2.14> Converta esta função para o código baseado em ponteiro (em C). 2.33.3 [10] <2.14> Traduza seu código C baseado em ponteiro de 2.33.2 para o assembly MIPS. 2.33.4 [10] <2.14> Compare o número do pior caso das instruções executadas por iteração de loop não final em seu código baseado em array de 2.33.1 e seu código baseado em ponteiro de 2.33.3. Nota: o pior caso ocorre quando as condições de desvio são tais que o caminho mais longo pelo código é tomado, ou seja, se existe uma instrução if, o resultado da verificação de condição é tal que o caminho com mais instruções é escolhido. Porém, se o resultado da verificação de condição fizer com que o loop termine, então supomos que o caminho que nos mantém no loop é escolhido. 2.33.5 [5] <2.14> Compare o número de registradores temporários (registradores t) necessários para o seu código baseado em array de 2.33.1 e para o seu código baseado em ponteiro de 2.33.3. 2.33.6 [5] <2.14> O que mudaria na sua resposta de 2.33.4 se os registradores $t0-$t7 e $a0-$a3 na convenção de chamada do MIPS fossem salvos por quem foi chamado, assim como $s0-$s7?
Exercício 2.34 A tabela a seguir contém o código assembly do ARM. Nos problemas a seguir, você traduzirá o código assembly do ARM para MIPS.
2.21 Exercícios 173
2.34.1 [5] <2.16> Para a tabela anterior, traduza este código assembly ARM para código assembly MIPS. Suponha que os registradores ARM r0 , r1 e r2 mantenham os mesmos valores dos registradores MIPS $s0, $s1 e $s2, respectivamente. Use registradores temporários do MIPS ($t0 etc.) onde for necessário. 2.34.2 [5] <2.16> Para as instruções assembly ARM na tabela anterior, mostre os campos de bit que representam as instruções ARM. A tabela a seguir contém código assembly MIPS. Nos problemas seguintes, você traduzirá o código assembly MIPS para ARM. a.
nor $t0, # s0, 0 and $s1, $s1, $t0
b.
sll $s1, $s2, 16 srl $s0, $s2, 16 or
$s1, $s1, $s2
2.34.3 [5] <2.16> Para a tabela anterior, ache o código assembly ARM que corresponde à sequência de código assembly MIPS. 2.34.4 [5] <2.16> Mostre os campos de bit que representam o código assembly ARM.
Exercício 2.35 O processador ARM tem alguns modos de endereçamento diferentes que não são aceitos no MIPS. Os problemas a seguir exploram esses novos modos de endereçamento.
2.35.1 [5] <2.16> Identifique o tipo dos modos de endereçamento das instruções assembly do ARM na tabela anterior. 2.35.2 2 [5] <2.16> Para as instruções assembly do ARM anteriores, escreva uma sequência de instruções assembly do MIPS para conseguir a mesma transferência de dados. Nos problemas a seguir, você irá comparar o código escrito utilizar as configurações de instruções ARM e MIPS. A tabela seguinte mostra os códigos escritos na configuração de instruções ARM.
174
Capítulo 2 Instruções: A Linguagem de Máquina
2.35.3 [10] <2.16> Para o código assembly ARM anterior, escreva uma rotina de código assembly MIPS correspondente. 2.35.4 [5] <2.16> Qual é o número total de instruções assembly MIPS exigido para executar o código? Qual é o número total de instruções assembly MIPS exigido para executar o código? 2.35.5 [5] <2.16> Supondo que o CPI médio da rotina assembly MIPS é igual ao CPI médio da rotina assembly ARM, e o processador MIPS tem uma frequência de operação que é 1,5 vez o processador ARM, o quanto o processador ARM é mais rápido que o processador MIPS?
Exercício 2.36 O processador ARM tem um modo interessante de dar suporte a constantes imediatas. Esse exercício investiga essas diferenças. A tabela a seguir contém instruções ARM.
2.36.1 [5] <2.16> Escreva o código MIPS equivalente para o código assembly ARM anterior. 2.36.2 [5] <2.16> Se o registrador R1 tiver o valor constante de 8, reescreva seu código MIPS para diminuir o número de instruções assembly MIPS necessárias. 2.36.3 [5] <2.16> Se o registrador R1 tiver o valor constante de 0x0600 0000, reescreva seu código MIPS para minimizar o número de instruções assembly MIPS necessárias. A tabela a seguir contém instruções MIPS. a.
addi r3, r2, 0x2
b.
addi r3, r2, – 1
2.36.4 [5] <2.16> Para o código assembly MIPS acima, escreva o código assembly ARM equivalente.
Exercício 2.37 Este exercício explora as diferenças entre os conjuntos de instruções MIPS e x86. A tabela a seguir contém código assembly x86.
2.21 Exercícios 175
2.37.1 [10] <2.17> Escreva o pseudocódigo para a rotina indicada. 2.37.2 [[10] <2.17> Para o código da tabela anterior, qual é o equivalente MIPS para a rotina dada? A tabela seguinte contém instruções assembly x86. a.
push eax
b.
text eax, 0x00200010
2.37.3 [10] <2.17> Para cada instrução assembly, mostre o tamanho de cada um dos campos de bit que representam a instrução. Trate o rótulo MY_FUNCTION como uma constante de 32 bits. 2.37.4 [10] <2.17> Escreva as instruções assembly MIPS equivalentes.
Exercício 2.38 O conjunto de instruções x86 inclui o prefixo REP, que faz com que a instrução seja repetida por determinado número de vezes ou até que uma condição seja satisfeita. Note que as instruções x86 referem-se a 8 bits como um byte, 16 bits como um word e 32 bits como um doubleword. Os primeiros três problemas deste exercício referem-se às seguintes instruções x86:
2.38.1 [5] <2.17> Qual seria um uso típico para esta instrução? 2.38.2 [5] <2.17> Escreva o código MIPS que realiza a mesma operação, supondo que $a0 corresponda a ECX, $a1 a EDI, $a2 a ESI e $a3 a EAX. 2.38.3 [5] <2.17> Se a instrução x86 exige um ciclo para ler da memória, um ciclo para escrever a memória e um ciclo para cada atualização de registrador, e se o MIPS exigir um ciclo por instrução, qual é o ganho de velocidade do uso dessa instrução x86 em vez do código MIPS equivalente quando o ECX é muito grande? Suponha que o tempo de ciclo de clock para x86 e MIPS seja o mesmo. Os três problemas restantes neste exercício referem-se à função a seguir, dada em assembly C e x86. Para cada instrução x86, também mostramos seu tamanho no formato de instrução de tamanho variável x86 e a interpretação (o que a instrução faz). Observe que a arquitetura x86 tem muito poucos registradores em comparação com MIPS, e como resultado a convenção de chamada do x86 é empurrar todos os argumentos para a pilha. O valor de retorno de uma função x86 é passado de volta a quem chamou no registrador EAX.
176
Capítulo 2 Instruções: A Linguagem de Máquina
2.38.4 [5] <2.17> Traduza esta função para o assembly MIPS. Compare o tamanho (quantos bytes de memória de instrução são necessários) para este código x86 e para o seu código MIPS. 2.38.5 [5] <2.17> Se o processador pode executar duas instruções por ciclo, ele precisa pelo menos ser capaz de ler duas instruções consecutivas em cada ciclo. Explique como isso seria feito no MIPS e como seria feito no x86. 2.38.6 [5] <2.17> Se cada instrução MIPS ocupa um ciclo, e se cada instrução x86 ocupa um ciclo mais um ciclo para cada leitura ou escrita de memória que ele tem de executar, qual é o ganho de velocidade do uso do x86 em vez do MIPS? Suponha que o tempo de ciclo do clock seja o mesmo no x86 e MIPS, e que a execução toma o caminho mais curto possível pela função (ou seja, cada loop termina imediatamente e cada instrução if toma a direção que leva para o retorno da função). Observe que a instrução ret do x86 lê o endereço de retorno da pilha.
Exercício 2.39 O CPI dos diferentes tipos de instrução é dado na tabela a seguir. Aritmética
Load/Store
Desvio
a.
1
10
3
b.
4
40
3
2.39.1 [5] <2.18> Considere o seguinte desmembramento de instruções dado para executar determinado programa: Instruções (em milhões) Aritméticas
500
Load/Store
300
Desvio
100
2.21 Exercícios 177
Qual é o tempo de execução para o processador se a frequência de operação é de 5 GHz?
2.39.2 [5] <2.18> Suponha que instruções aritméticas novas e mais poderosas sejam acrescentadas ao conjunto de instruções. Na média, com o uso dessas instruções aritméticas mais poderosas, podemos reduzir em 25% o número de instruções aritméticas necessárias para executar um programa, e o custo de aumentar o tempo de ciclo de clock em apenas 10%. Essa é uma boa escolha de projeto? Por quê? 2.39.3 [5] <2.18> Suponha que achemos um modo de dobrar o desempenho das instruções aritméticas. Qual é o ganho de velocidade geral de nossa máquina? E se descobrirmos um modo de melhorar o desempenho das instruções aritméticas em 10 vezes!? A tabela a seguir mostra as proporções de execução de instruções para diferentes tipos de instrução. Aritméticas
Load/Store
Desvio
a.
70%
10%
20%
b.
50%
40%
10%
2.39.4 [5] <2.18> Dado o arranjo de instruções anterior e a suposição de que uma instrução aritmética requer dois ciclos, uma instrução de load/store utiliza seis ciclos, e uma instrução de desvio usa três ciclos, ache o CPI médio. 2.39.5 [5] <2.18> Para uma melhoria de 25% no desempenho, quantos ciclos, na média, uma instrução aritmética pode gastar se as instruções de load/store e desvio não forem melhoradas? 2.39.6 [5] <2.18> Para uma melhoria de 50% no desempenho, quantos ciclos, na média, uma instrução aritmética pode gastar se as instruções de load/store e desvio não forem melhoradas?
Exercício 2.40 Os três primeiros problemas neste exercício referem-se à função a seguir, dada em assembly MIPS. Infelizmente, o programador dessa função caiu na armadilha de supor que o MIPS é uma máquina endereçada por word, mas na realidade o MIPS é endereçado por byte.
178
Capítulo 2 Instruções: A Linguagem de Máquina
Observe que, no assembly MIPS, o caractere “;” indica que o restante da linha é um comentário. 2.40.1 [5] <2.18> A arquitetura MIPS requer que os acessos do tamanho de uma word (lw e sw) sejam alinhados na word, ou seja, os dois bits mais baixos do endereço devem ser, ambos, zero. Se um endereço não for alinhado na word, o processador levanta uma exceção de “erro de barramento”. Explique como esse requisito de barramento afeta a execução dessa função. 2.40.2 [5] <2.18> Se “a” fosse um ponteiro para o início de um array de elementos de um byte, e se substituíssemos lw e sw por lb (load byte) e sb (store byte), respectivamente, essa função estaria correta? Nota: lb lê um byte da memória, o estende por sinal e o coloca no registrador de destino, enquanto sb armazena o byte menos significativo do registrador na memória. 2.40.3 [5] <2.18> Mude esse código para torná-lo correto para inteiros de 32 bits. Os três problemas restantes neste exercício referem-se a um programa que aloca memória para um array, preenche o array com alguns números, chama a função sort da Figura 2.27 e depois imprime o array. A função principal do programa é a seguinte (dada como código C e MIPS):
A função my_alloc é definida como a seguir (dada como código C e MIPS). Observe que o programador dessa função caiu na armadilha de usar um ponteiro para uma variável automática arr fora da função em que é definida.
A função my_init é definida da seguinte forma (código MIPS):
2.21 Exercícios 179
2.40.4 [5] <2.18> Qual é o conteúdo (valores de todos os cinco elementos) do array v imediatamente antes de a instrução “jal sort” no código main ser executada? 2.40.5 [15] <2.18, 2.13> Qual é o conteúdo do array v imediatamente antes de a função sort entrar em seu loop mais externo pela primeira vez? Suponha que os registradores $sp, $s0, $s1, $s2 e $s3 tenham valores 0x1000, 20, 40 7 e 1, respectivamente, no início do código principal (imediatamente antes que “li $s0, 5” seja executado). 2.40.6 [10] <2.18, 2.13> Qual é o conteúdo do array de cinco elementos apontado por
v imediatamente após o “jal sort” retornar ao código main?
§2.2, página 80: MIPS, C, Java §2.3, página 87: 2) Muito lento §2.4, página 93: 3) –8dec §2.5, página 101: 4) sub $s2, $s0, $s1 §2.6, página 104: Ambos. AND com um padrão de máscara de 1s deixa 0s em todo lugar menos no campo desejado. O deslocamento à esquerda pela quantidade correta remove os bits da esquerda do campo. O deslocamento à direita pela quantidade apropriada coloca o campo nos bits mais à direita da palavra, com 0s no restante da palavra. Observe que AND deixa o campo onde ele estava originalmente e o par deslocado move o campo para a parte mais à direita da palavra. §2.7, página 111: I. Todos são verdadeiros. II. 1). §2.8, página 122: Ambos são verdadeiros. §2.9, página 127: I. 2) II. 3) §2.10, página 136: I. 4) +-128K. II. 6) um bloco de 256M. III. 4) sll §2.11, página 139: Ambos são verdadeiros. §2.12, página 148: 4) Independência de máquina.
Respostas das Seções “Verifique você mesmo”
3 Aritmética Computacional A precisão numérica é a própria alma da ciência. Sir D’arcy Wentworth Thompson On Growth and Form, 1917
3.1 Introdução 182 3.2
Adição e subtração 182
3.3 Multiplicação 186 3.4 Divisão 191 3.5
Ponto flutuante 197
3.6
Paralelismo e aritmética computacional: Associatividade 219
3.7
Vida real: ponto flutuante no x86 220
3.8
Falácias e armadilhas 222
3.9
Comentários finais 226
3.10
Perspectiva histórica e leitura adicional 228
3.11 Exercícios 228
Os cinco componentes clássicos de um computador
182
Capítulo 3 Aritmética Computacional
3.1 Introdução As palavras do computador são compostas de bits; assim, podem ser representadas como números binários. O Capítulo 2 mostra que os inteiros podem ser representados em formato decimal ou binário, mas e quanto aos outros números que ocorrem normalmente? Por exemplo: j
Como são representadas frações e outros números reais?
j
O que acontece se uma operação cria um número maior do que poderia ser representado?
j
E por trás de todas essas perguntas existe um mistério: como o hardware realmente multiplica ou divide números?
O objetivo deste capítulo é desvendar esse mistério, incluindo a representação dos números, algoritmos aritméticos, hardware que acompanha esses algoritmos e as implicações de tudo isso para os conjuntos de instruções. Essas ideias podem ainda explicar truques que você já pode ter encontrado nos computadores Subtração: o companheiro esquisito da adição No. 10, Top Ten Courses for Athletes at a Football Factory, David Letterman e outros, Book of Top Ten Lists, 1990
3.2 Adição e subtração A adição é exatamente o que você esperaria nos computadores. Dígitos são somados bit a bit, da direita para a esquerda, com carries (“vai-uns”), sendo passados para o próximo dígito à esquerda, como você faria manualmente. A subtração utiliza a adição: o operando apropriado é simplesmente negado antes de ser somado.
Adição e subtração binária
EXEMPLO
RESPOSTA
Vamos tentar somar 6dec a 7dec em binário e depois subtrair 6dec de 7dec em binário.
Os 4 bits à direita fazem toda a ação; a Figura 3.1 mostra as somas e os carries. Os carries aparecem entre parênteses, com as setas mostrando como são passados. A subtração de 6dec de 7dec pode ser feita diretamente:
ou por meio da soma, usando a representação de complemento de dois de –6:
3.2 Adição e subtração 183
FIGURA 3.1 Adição binária, mostrando carries da direta para a esquerda. O bit mais à direita adiciona 1 a 0, resultando em uma soma de 1 e um carry out de 0 para esse bit. Logo, a operação para o segundo dígito da direita é 0 + 1 + 1. Isso gera uma soma de 0 e um carry out de 1 para esse bit. O terceiro dígito é a soma de 1 + 1 + 1, resultando em um carry out de 1 e uma soma de 1 para esse dígito. O quarto bit é 1 + 0 + 0, tendo uma soma de 1 e nenhum carry.
Já dissemos que o overflow ocorre quando o resultado de uma operação não pode ser representado com o hardware disponível, nesse caso, uma palavra de 32 bits. Quando pode ocorrer um overflow na adição? Quando se somam operandos com sinais diferentes, não poderá haver overflow. O motivo é que a soma não pode ser maior do que um dos operandos. Por exemplo, –10 + 4 = –6. Como os operandos cabem nos 32 bits, e a soma não é maior do que um operando, a soma também precisa caber nos 32 bits. Portanto, nenhum overflow pode ocorrer ao somar operandos positivos e negativos. Existem restrições semelhantes à ocorrência do overflow durante a subtração, mas esse é apenas o princípio oposto: quando os sinais dos operandos são iguais, o overflow pode ocorrer. Para ver isso, lembre-se de que x – y = x + (-y), pois subtraímos negando o segundo operando e depois somamos. Assim, quando subtraímos operandos do mesmo sinal, acabamos somando operandos de sinais diferentes. Pelo parágrafo anterior, sabemos que não pode ocorrer overflow também nesse caso. Tendo examinado quando um overflow pode ocorrer na adição e na subtração, ainda não respondemos como detectar quando ele ocorre. Logicamente, a soma ou a subtração de dois números de 32 bits pode gerar um resultado que precisa de 33 bits para ser totalmente expresso. A falta de um 33° bit significa que, quando o overflow ocorre, o bit de sinal está sendo definido com o valor do resultado, no lugar do sinal apropriado do resultado. Como precisamos apenas de um bit extra, somente o bit de sinal pode estar errado. Logo, o overflow ocorre quando se somam dois números positivos, e a soma é negativa, ou vice-versa. Isso significa que um carry ocorreu no bit de sinal. O overflow ocorre na subtração quando subtraímos um número negativo de um número positivo e obtemos um resultado negativo, ou quando subtraímos um número positivo de um número negativo e obtemos um resultado positivo. Isso significa que houve um empréstimo do bit de sinal. A Figura 3.2 mostra a combinação de operações, operandos e resultados que indicam um overflow. Acabamos de ver como detectar o overflow para os números em complemento de dois em um computador. E com relação aos inteiros sem sinal? Os inteiros sem sinal normalmente são usados para endereços de memória em que os overflows são ignorados. Logo, o projetista de computador precisa oferecer uma maneira de ignorar o overflow em alguns casos e reconhecê-lo em outros. A solução do MIPS é ter dois tipos de instruções aritméticas para reconhecer as duas escolhas: j j
Adição (add), adição imediata (addi) e subtração (sub) causam exceções no overflow. Adição sem sinal (addu), adição imediata sem sinal (addiu) e subtração sem sinal (subu) não causam exceções no overflow.
FIGURA 3.2 Condições de overflow para adição e subtração.
184
Unidade Lógica e Aritmética (ALU) Hardware que realiza adição, subtração e normalmente operações lógicas como AND e OR.
Interface hardware/ software exceção Também chamada interrupção. Um evento não planejado que interrompe a execução do programa; usada para detectar overflow. interrupção Uma exceção que vem de fora do processador. (Algumas arquiteturas utilizam o termo interrupção para todas as exceções.)
Capítulo 3 Aritmética Computacional
Como a linguagem C ignora os overflows, os compiladores C do MIPS sempre gerarão as versões sem sinal das instruções aritméticas addu , addiu e subu , sem importar o tipo das variáveis. No entanto, os compiladores Fortran do MIPS apanham as instruções aritméticas apropriadas, dependendo do tipo dos operandos. O Apêndice C descreve o hardware que realiza a adição e subtração, que é chamado de Unidade Lógica e Aritmética, ou ULA.
O projetista de computador precisa decidir como tratar overflows aritméticos. Embora algumas linguagens como C e Java ignorem o overflow de inteiros, linguagens como Ada e Fortran exigem que o programa seja notificado. O programador ou o ambiente de programação precisa, então, decidir o que fazer quando ocorre o overflow. O MIPS detecta o overflow com uma exceção, também chamada de interrupção em muitos computadores. Uma exceção ou interrupção é basicamente uma chamada de procedimento não planejada. O endereço da instrução que gerou o overflow é salvo em um registrador e o computador desvia para um endereço predefinido, a fim de invocar a rotina apropriada para essa exceção. O endereço interrompido é salvo de modo que, em algumas situações, o programa possa continuar após o código corretivo ser executado. (A Seção 4.9 abrange as exceções com mais detalhes; os Capítulos 5 e 6 descrevem outras situações em que ocorrem exceções e interrupções.) O MIPS inclui um registrador, chamado contador de programa de exceção (EPC – Exception Program Counter), para conter o endereço da instrução que causou a exceção. A instrução move from system control (mfc0) é usada a fim de copiar o EPC para um registrador de uso geral, de modo que o software do MIPS tem a opção de retornar à instrução problemática por meio de uma instrução jump register.
Aritmética para Multimídia Já que cada microprocessador desktop por definição tem suas próprias telas gráficas, e como as quantidades de transistores aumentaram, foi inevitável que seria acrescentado suporte para operações gráficas. Muitos sistemas gráficos originalmente usavam 8 bits para representar cada uma das três cores primárias, mais 8 bits para um local de um pixel. O acréscimo de alto-falantes e microfones para teleconferência e jogos de vídeo sugeriu também o suporte para som. As amostras de áudio precisam de mais de 8 bits de precisão, mas 16 bits são suficientes. Cada microprocessador tem suporte especial, de modo que os bytes e meias palavras ocupam menos espaço quando armazenados na memória (veja Seção 2.9), mas, em razão da infrequência das operações aritméticas nesses tamanhos de dados nos programas de inteiros típicos existe pouco suporte além das transferências de dados. Os arquitetos reconhecem que muitas aplicações gráficas e de áudio realizariam a mesma operação sobre vetores desses dados. Particionando as cadeias de carry dentro de um somador de 64 bits, um processador poderia realizar operações simultâneas sobre vetores curtos de operandos de 8 bits, quatro operandos de 16 bits ou dois operandos de 32 bits. O custo desses somadores particionados era pequeno. Essas extensões têm sido chamadas de vetor ou SIMD, para única instrução, múltiplos dados (veja Seção 2.17 e Capítulo 7). Um recurso geralmente não encontrado nos microprocessadores de uso geral é a saturação de operações. A saturação significa que, quando um cálculo estoura, o resultado é definido como o maior número positivo ou o maior número negativo, em vez de um cálculo de módulo, como na aritmética do complemento de dois. A saturação provavelmente é o que você deseja para operações de mídia. Por exemplo, o botão de volume em um aparelho de rádio seria frustrante se, enquanto você o girasse, o volume aumentasse continuamente
3.2 Adição e subtração 185
por um tempo e depois imediatamente ficasse baixo. Um botão com saturação pararia no volume mais alto, não importa o quanto você o girasse. A Figura 3.3 mostra as operações aritméticas e lógicas encontradas em muitas extensões de multimídia dos conjuntos de instruções modernos.
FIGURA 3.3 Resumo do suporte de multimídia para computadores desktop.
Detalhamento: o MIPS pode interceptar um overflow, mas, diferente de muitos outros computadores, não existe desvio condicional para testar o overflow. A sequência de instruções do MIPS pode descobrir overflow. Para a adição com sinal, a sequência é a seguinte (veja o Detalhamento na Seção 2.6 do Capítulo 2, para obter uma descrição da instrução xor):
Para adição sem sinal ( $t0 = $t1 + $t2 ), o teste é
Resumo A questão principal desta seção é que, independente da representação, o tamanho finito da palavra dos computadores significa que as operações aritméticas podem criar resultados muito grandes para caber nesse tamanho de palavra fixo. É fácil detectar o overflow em números sem sinal, embora quase sempre sejam ignorados, pois os programas não querem detectar overflow para a aritmética de endereço, o uso mais comum dos números naturais. O complemento de dois apresenta um desafio maior, embora alguns sistemas de software exijam detecção de overflow, de modo que, hoje, todos os computadores tenham um meio de detectá-lo. A crescente popularidade das aplicações de multimídia levou a instruções aritméticas que dão suporte a operações mais estreitas, que podem operar facilmente em paralelo.
186
Capítulo 3 Aritmética Computacional
Verifique você mesmo
Algumas linguagens de programação permitem a aritmética de inteiros em complemento de dois com variáveis declaradas com um byte e meio. Que instruções do MIPS seriam usadas? 1. Load com lbu, lhu ; aritmética com add, sub, mult, div ; depois, armazenamento usando sb, sh. 2. Load com lb, lh ; aritmética com add, sub, mult, div ; depois, armazenamento usando sb, sh. 3. Load com lb, lh; aritmética com add, sub, mult, div; usando AND para mascarar o resultado com 8 ou 16 bits após cada operação; depois, armazenamento usando sb, sh. Detalhamento: no texto anterior, dissemos que você copia o EPC para um registrador por meio de mfc0 e depois retorna ao código interrompido por meio de jump register. Isso leva a uma pergunta interessante: já que você primeiro precisa transferir o EPC para um registrador a fim de usar com jump register, como jump register pode retornar ao código interrompido e restaurar os valores originais de todos os registradores? Você restaura os registradores antigos primeiro, destruindo, assim, seu endereço de retorno do EPC, que colocou em um registrador para uso em jump register, ou restaura todos os registradores, menos aquele com o endereço de retorno, para que possa desviar – significando que uma exceção resultaria em alterar esse único registrador a qualquer momento durante a execução do programa! Nenhuma dessas opções é satisfatória. Para auxiliar o hardware nesse dilema, os programadores MIPS concordaram em reservar os registradores $k0 e $k1 para o sistema operacional; esses registradores não são restaurados nas exceções. Assim como os compiladores MIPS evitam o uso do registrador $at, de modo que o montador possa utilizá-lo como um registrador temporário (veja a Seção “Interface Hardware/ Software”, na Seção 2.10), os compiladores também se abstêm do uso dos registradores $k0 e $k1, de modo que fiquem disponíveis para o sistema operacional. As rotinas de exceção colocam o endereço de retorno em um desses registradores e depois usam o jump register para armazenar o endereço da instrução.
Detalhamento: A velocidade da adição é aumentada determinando-se o carry in para os bits de alta ordem mais cedo. Existem diversos esquemas para antecipar o carry, de modo que o cenário do pior caso é uma função do log2 do número de bits no somador. Esses sinais antecipados são mais rápidos, pois percorrem menos portas na sequência, mas exigem mais portas para antecipar o carry apropriado. O mais comum é o carry lookahead, descrito na Seção Apêndice C no site. C.6 do
Multiplicação é vexação, Divisão também é ruim; A regra de três me intriga, e a prática me deixa louco. Anônimo, manuscrito de Elizabeth, 1570
3.3 Multiplicação Agora que completamos a explicação de adição e subtração, estamos prontos para montar a operação mais vexatória da multiplicação. Primeiro, vamos rever a multiplicação de números decimais à mão para nos lembrar das etapas e dos nomes dos operandos. Por motivos que logo se tornarão claros, limitamos esse exemplo decimal ao uso apenas dos dígitos 0 e 1. Multiplicando 1000dec por 1001dec: Multiplicando Multiplicador ×
Produto
1000dec 1001dec 1000 0000 0000 1000 1001000dec
3.3 Multiplicação 187
O primeiro operando é chamado multiplicando e o segundo é o multiplicador. O resultado final é chamado produto. Como você pode se lembrar, o algoritmo aprendido na escola é pegar os dígitos do multiplicador um a um, da direita para a esquerda, calculando a multiplicação do multiplicando pelo único dígito do multiplicador e deslocando o produto intermediário um dígito para a esquerda dos produtos intermediários anteriores. A primeira observação é que o número de dígitos no produto é muito maior do que o número no multiplicando ou no multiplicador. De fato, se ignorarmos os bits de sinal, o tamanho da multiplicação de um multiplicando de n bits por um multiplicador de m bits é um produto que possui n + m bits de largura. Ou seja, n + m bits são necessários para representar todos os produtos possíveis. Logo, como na adição, a multiplicação precisa lidar com o overflow, pois constantemente desejamos um produto de 32 bits como resultado da multiplicação de dois números de 32 bits. Neste exemplo, restringimos os dígitos decimais a 0 e 1. Com somente duas opções, cada etapa da multiplicação é simples: 1. Basta colocar uma cópia do multiplicando (1 x multiplicando) no lugar apropriado se o dígito do multiplicador for 1, ou 2. Colocar 0 (0 x multiplicando) no lugar apropriado se o dígito for 0. Embora o exemplo decimal anterior utilize apenas 0 e 1, a multiplicação de números binários sempre usa 0 e 1 e, por isso, sempre oferece apenas essas duas opções. Agora que já revisamos os fundamentos tradicionais da multiplicação, a próxima etapa é mostrar o hardware de multiplicação altamente otimizado. Quebramos essa tradição na crença de que você entenderá melhor vendo a evolução do hardware e do algoritmo de multiplicação no decorrer das diversas gerações. Por enquanto, vamos supor que estamos multiplicando apenas números positivos.
Versão sequencial do algoritmo e hardware de multiplicação Esse projeto imita o algoritmo que aprendemos na escola; o hardware aparece na Figura 3.4. Desenhamos o hardware de modo que os dados fluam de cima para baixo, para que fique mais semelhante à técnica do lápis e papel. Vamos supor que o multiplicador esteja no registrador Multiplicador de 32 bits e que o registrador Produto de 64 bits esteja inicializado como 0. Pelo exemplo de lápis e papel, visto anteriormente, fica claro que precisaremos mover o multiplicado para a esquerda um
FIGURA 3.4 Primeira versão do hardware de multiplicação. O registrador do Multiplicando, a ALU, e o registrador do Produto possuem 64 bits de largura, apenas com o registrador do Multiplicador contendo 32 bits. ( O Apêndice C descreve as ALUs.) O multiplicando com 32 bits começa na metade direita do registrador do Multiplicando e é deslocado à esquerda 1 bit em cada etapa. O multiplicador é deslocado na direção oposta em cada etapa. O algoritmo começa com o produto inicializado com 0. O controle decide quando deslocar os registradores Multiplicando e Multiplicador e quando escrever novos valores no registrador do Produto.
188
Capítulo 3 Aritmética Computacional
dígito a cada passo, pois pode ser somado aos produtos intermediários. Durante 32 etapas, um multiplicando de 32 bits moveria 32 bits para a esquerda. Logo, precisamos de um registrador Multiplicando de 64 bits, inicializado com o multiplicando de 32 bits na metade direita e 0 na metade esquerda. Esse registrador, em seguida, é deslocado 1 bit para a esquerda a cada etapa, de modo a alinhar o multiplicando com a soma sendo acumulada no registrador Produto de 64 bits. A Figura 3.5 mostra as três etapas clássicas necessárias para cada bit. O bit menos significativo do multiplicador (Multiplicador0) determina se o multiplicando é somado ao registrador Produto. O deslocamento à esquerda na etapa 2 tem o efeito de mover os operandos intermediários para a esquerda, assim como na multiplicação manual. O deslocamento à direita na etapa 3 nos indica o próximo bit do multiplicador a ser examinado na iteração seguinte. Essas três etapas são repetidas 32 vezes, para obter o produto. Se cada etapa usasse um ciclo de clock, esse algoritmo exigiria quase 100 ciclos de clock para multiplicar dois números de 32 bits. A importância relativa de operações aritméticas, como a multiplicação, varia com o programa, mas a soma e a subtração podem ser de 5 a 100 vezes mais comuns do que a multiplicação. Como consequência, em muitas aplicações, a multiplicação pode demorar vários ciclos de clock sem afetar o desempenho de forma significativa. Mesmo assim, a lei de Amdahl (veja Seção 1.8) nos lembra que até mesmo uma frequência moderada para uma operação lenta pode limitar o desempenho.
FIGURA 3.5 O primeiro algoritmo de multiplicação, usando o hardware mostrado na Figura 3.4. Se o bit menos significativo do multiplicador for 1, some o multiplicando ao produto. Caso contrário, vá para a etapa seguinte. Desloque o multiplicando para a esquerda e o multiplicador para a direita nas duas etapas seguintes. Essas três etapas são repetidas 32 vezes.
3.3 Multiplicação 189
Esse algoritmo e o hardware são facilmente refinados para usar 1 ciclo de clock por etapa. O aumento de velocidade vem da realização das operações em paralelo: o multiplicador e o multiplicando são deslocados enquanto o multiplicando é somado ao produto se o bit do multiplicador for 1. O hardware simplesmente precisa garantir que testará o bit da direita do multiplicador e receberá a versão previamente deslocada do multiplicando. O hardware normalmente é otimizado ainda mais para dividir a largura do somador e dos registradores ao meio, observando onde existem partes não utilizadas dos registradores e somadores. A Figura 3.6 mostra o hardware revisado.
Substituir a aritmética por deslocamentos também pode ocorrer quando se multiplica por constantes. Alguns compiladores substituem multiplicações por constantes curtas com uma série de deslocamentos e adições. Como deslocar um bit à esquerda representa um número duas vezes maior na base 2, o deslocamento de bits para a esquerda tem o mesmo efeito de multiplicar por uma potência de 2. Como dissemos no Capítulo 2, quase todo compilador realizará a otimização por redução de força substituindo uma multiplicação na potência de 2 por um deslocamento à esquerda.
Interface hardware/ software
FIGURA 3.6 Versão refinada do hardware de multiplicação. Compare com a primeira versão na Figura 3.4. O registrador Multiplicando, a ALU, e o registrador Multiplicador possuem 32 bits de extensão, com somente o registrador Produto restando nos 64 bits. Agora, o produto é deslocado para a direita. O registrador Multiplicador separado também desapareceu. O multiplicador é colocado na metade direita do registrador Produto. Essas mudanças estão destacadas. (O registrador Produto na realidade deverá ter 65 bits, a fim de manter o carry do somador, mas ele aparece aqui como 64 bits para destacar a evolução da Figura 3.4.)
Um algoritmo de multiplicação
Usando números de 4 bits para economizar espaço, multiplique 2 dec x 3 dec , ou 0010bin x 0011bin. A Figura 3.7 mostra o valor de cada registrador para cada uma das etapas rotuladas de acordo com a Figura 3.5, com o valor final de 0000 0110bin ou 6dec. A cor é usada para indicar os valores de registrador que mudam nessa etapa e o bit circulado é aquele examinado para determinar a operação da próxima etapa.
EXEMPLO RESPOSTA
190
Capítulo 3 Aritmética Computacional
FIGURA 3.7 Exemplo de multiplicação usando o algoritmo da Figura 3.5. O bit examinado para determinar a próxima etapa está circulado.
Multiplicação com sinal Até aqui, tratamos de números positivos. O modo mais fácil de entender como tratar dos números com sinal é primeiro converter o multiplicador e o multiplicando para números positivos e depois lembrar dos sinais originais. Os algoritmos deverão, então, ser executados por 31 iterações, deixando os sinais fora do cálculo. Conforme aprendemos na escola, o produto só será negativo se os sinais originais forem diferentes. Acontece que o último algoritmo funcionará para números com sinais se nos lembrarmos de que os números com que estamos lidando possuem dígitos infinitos e que só os estamos representando com 32 bits. Logo, as etapas de deslocamento precisariam estender o sinal do produto para números com sinal. Quando o algoritmo terminar, a palavra menos significativa terá o produto de 32 bits.
Multiplicação mais rápida A Lei de Moore ofereceu tantos recursos que os projetistas de hardware agora podem construir um hardware de multiplicação muito mais rápido. Não importa se o multiplicando deve ser somado ou não, isso é conhecido no início da multiplicação analisando cada um dos 32 bits do multiplicador. Multiplicações mais rápidas são possíveis basicamente fornecendo um somador de 32 bits para cada bit do multiplicador: uma entrada é o AND do multiplicando pelo bit do multiplicador e a outra é a saída de um somador anterior. Uma técnica simples seria conectar as saídas dos somadores à direita das entradas dos somados à esquerda, criando uma pilha de somadores com altura 32. Um modo alternativo de organizar essas 32 adições é em uma árvore paralela, como mostra a Figura 3.8. Em vez de esperar 32 tempos de add, esperamos apenas o log2(32) ou cinco tempos de add com 32 bits. A Figura 3.8 mostra como esse é o modo mais rápido de conectá-los. De fato, a multiplicação pode se tornar ainda mais rápida do que cinco tempos de add, veja uso de somadores para salvar carry (veja Seção C.6 no Apêndice C) e porque é fácil usar um pipeline nesse projeto para que possam ser realizadas muitas multiplicações simultaneamente (veja Capítulo 4).
Multiplicação no MIPS O MIPS oferece um par separado de registradores de 32 bits, de modo a conter o produto de 64 bits, chamados Hi e Lo. Para produzir um produto com ou sem o devido sinal, o MIPS possui duas instruções: multiply (mult) e multiply unsigned (multu). Para apanhar o produto de 32 bits inteiro, o programador usa move from lo (mflo). O montador MIPS gera uma pseudoinstrução para multiplicar, que especifica três registradores de uso geral, gerando instruções mflo e mfhi que colocam o produto nos registradores.
3.4 Divisão 191
FIGURA 3.8 Hardware da multiplicação rápida. Em vez de usar um único somador de 32 bits 31 vezes, esse hardware “desenrola o loop” para usar 31 somadores e depois os organiza para minimizar o atraso.
Resumo A multiplicação é feita pelo hardware simples de deslocamento e adição, derivado do método de lápis e papel que aprendemos na escola. Os compiladores utilizam até mesmo as instruções de deslocamento para multiplicações por potências de dois.
As duas instruções multiply do MIPS ignoram o overflow, de modo que fica a critério do software verificar se o produto é muito grande para caber nos 32 bits. Não existe overflow se Hi for 0 para multu ou o sinal replicado de Lo para mult. A instrução move from hi (mfhi) pode ser usada para transferir Hi a um registrador de uso geral, a fim de testar o overflow.
Interface hardware/ software
Divide et impera.
3.4 Divisão A operação recíproca da multiplicação é a divisão, ainda menos frequente e ainda mais peculiar. Ela oferece até mesmo a oportunidade de realizar uma operação matematicamente inválida: dividir por 0. Vamos começar com um exemplo de divisão longa usando números decimais, para lembrar os nomes dos operandos e do algoritmo de divisão que aprendemos na escola. Por motivos semelhantes aos da seção anterior, vamos limitar os dígitos decimais a apenas 0 ou 1. O exemplo é a divisão de 1.001.010dec por 1000dec: 1001dec Divisor1000dec 1001010dec −1000 10 101 1010 −1000 10dec
Quociente Dividendo
Resto
Tradução do latim para “Dividir e conquistar”, máxima política antiga, citada por Maquiavel, 1532
192
dividendo Um número sendo dividido.
divisor Um número pelo qual o dividendo é dividido.
quociente O resultado principal de uma divisão; um número que, quando multiplicado pelo divisor e somado ao resto, produz o dividendo.
resto O resultado secundário de uma divisão; um número que, quando somado ao produto do quociente pelo divisor, produz o dividendo.
Capítulo 3 Aritmética Computacional
Os dois operandos (dividendo e divisor) e o resultado (quociente) da divisão são acompanhados por um segundo resultado, chamado resto. Veja aqui outra maneira de expressar o relacionamento entre os componentes: Dividendo = Quociente × Divisor + Resto em que o resto é menor do que o divisor. Raramente os programas utilizam a instrução de divisão só para obter o resto, ignorando o quociente. O algoritmo básico de divisão, que aprendemos na escola, tenta ver o quanto um número pode ser subtraído, criando um dígito do quociente em cada tentativa. Nosso exemplo decimal cuidadosamente selecionado usa apenas os números 0 e 1, de modo que é fácil descobrir quantas vezes o divisor cabe na parte do dividendo: deve ser 0 ou 1. Os números binários contêm apenas 0 ou 1, de modo que a divisão binária é restrita a essas duas opções, simplificando, assim, a divisão binária. Vamos supor que o dividendo e o divisor sejam positivos e, logo, o quociente e o resto sejam não negativos. Os operandos da divisão e os dois resultados são valores de 32 bits, e ignoraremos o sinal por enquanto.
Algoritmo e hardware de divisão A Figura 3.9 mostra o hardware para imitar nosso algoritmo da escola. Começamos com o registrador Quociente de 32 bits definido como 0. Cada iteração do algoritmo precisa deslocar o divisor para a direita um dígito, de modo que começaremos com o divisor colocado na metade esquerda do registrador Divisor de 64 bits e o deslocaremos para a direita 1 bit a cada etapa, a fim de alinhá-lo com o dividendo. O registrador Resto é inicializado com o dividendo. A Figura 3.10 mostra três etapas do primeiro algoritmo de divisão. Ao contrário dos humanos, o computador não é inteligente o bastante para saber, com antecedência, se o divisor é menor do que o dividendo. Ele primeiro precisa subtrair o divisor na etapa 1; lembre-se de que é assim que realizamos a comparação na instrução set on less than. Se o resultado for positivo, o divisor foi menor ou igual ao dividendo, de modo que geramos um 1 no quociente (etapa 2a). Se o resultado é negativo, a próxima etapa é restaurar o valor original, somando o divisor de volta ao resto e gerar um 0 no quociente (etapa 2b). O divisor é deslocado para a direita e depois repetimos. O resto e o quociente serão encontrados em seus registradores de mesmo nome depois que as iterações terminarem.
FIGURA 3.9 Primeira versão do hardware de divisão. O registrador Divisor, a ALU e o registrador Resto possuem 64 bits de largura, com apenas o registrador Quociente tendo 32 bits. O divisor de 32 bits começa na metade esquerda do registrador Divisor e é deslocado 1 bit para a direita em cada iteração. O resto é inicializado com o dividendo. O controle decide quando deslocar os registradores Divisor e Quociente e quando escrever o novo valor para o registrador Resto.
3.4 Divisão 193
FIGURA 3.10 Um algoritmo de divisão, usando o hardware da Figura 3.9. Se o Resto é positivo, o divisor coube no dividendo, de modo que a etapa 2a gera um 1 no quociente. Um Resto negativo após a etapa 1 significa que o divisor não coube no dividendo, de modo que a etapa 2b gera um 0 no quociente e soma o divisor ao resto, revertendo, assim, a subtração da etapa 1. O deslocamento final, na etapa 3, alinha o divisor corretamente, em relação ao dividendo, para a próxima iteração. Essas etapas são repetidas 33 vezes.
Um algoritmo de divisão
Usando uma versão de 4 bits do algoritmo para economizar páginas, vamos tentar dividir 7dec por 2dec, ou 0000 0111bin por 0010bin. A Figura 3.11 mostra o valor de cada registrador para cada uma das etapas, com o quociente sendo 3dec e o resto sendo 1dec. Observe que o teste na etapa 2 (se o resto é positivo ou negativo) simplesmente testa se o bit de sinal do registrador Resto é um 0 ou um 1. O requisito surpreendente desse algoritmo é que ele utiliza n + 1 etapas para obter o quociente e resto corretos.
EXEMPLO RESPOSTA
194
Capítulo 3 Aritmética Computacional
FIGURA 3.11 Exemplo de divisão usando o algoritmo da Figura 3.10. O bit examinado para determinar a próxima etapa está em destaque.
Esse algoritmo e esse hardware podem ser refinados para que sejam mais rápidos e menos dispendiosos. A rapidez vem do deslocamento dos operandos e do quociente no mesmo momento da subtração. Essa melhoria divide ao meio a largura do somador e dos registradores, observando onde existem partes não usadas dos registradores e somadores. A Figura 3.12 mostra o hardware revisado.
Divisão com sinal Até aqui, ignoramos os números com sinal na divisão. A solução mais simples é lembrar os sinais do divisor e do dividendo e depois negar o quociente se os sinais forem diferentes. Detalhamento: Uma complicação da divisão com sinal é que também temos de definir o sinal do resto. Lembre-se de que a seguinte equação precisa ser sempre mantida:
Dividendo = Quociente × Divisor + Resto
Para entender como definir o sinal do resto, vejamos o exemplo da divisão de todas as combinações de ± 7dec por ± 2dec. O primeiro caso é fácil:
+7 ÷+ 2:Quociente =+ 3,Resto =+1
FIGURA 3.12 Uma versão melhorada do hardware de divisão. O registrador Divisor, a ALU e o registrador Quociente possuem 32 bits de largura, com apenas o registrador Resto ficando com 64 bits. Em comparação com a Figura 3.9, os registradores ALU e Divisor são divididos ao meio, e o resto é deslocado à esquerda. Essa versão também combina o registrador Quociente com a metade direita do registrador Resto. (Assim como na Figura 3.6, o registrador Resto na realidade deveria ter 65 bits para garantir que o carry do somador não se perca.)
3.4 Divisão 195
Verificando os resultados:
7 = 3 × 2 + (+1) = 6 + 1 Se você mudar o sinal do dividendo, o quociente também precisa mudar:
−7 ÷ +2 :Quociente = −3 Reescrevendo nossa fórmula básica para calcular o resto:
Resto = (Dividendo − Quociente × Divisor) = −7 − (−3 × +2) = −7 − (−6) = −1 Assim,
−7 ÷ +2 :Quociente = −3,Resto = −1 Verificando os resultados novamente:
−7 = −3 × 2 + (−1) = −6 − 1 O motivo pelo qual a resposta não é um quociente de –4 e um resto de +1, que também caberia nessa fórmula, é que o valor absoluto do quociente mudaria dependendo do sinal do dividendo e do divisor! Logicamente, se
−(x + y) ≠ (−x ) ÷ y a programação seria um desafio ainda maior. Esse comportamento anômalo é evitado seguindo-se a regra de que o dividendo e o resto devem ter os mesmos sinais, não importa quais sejam os sinais do divisor e do quociente. Calculamos as outras combinações seguindo a mesma regra:
+7 ÷ −2 :Quociente = −3,Resto = +1 −7 ÷ −2 :Quociente = +3,Resto = −1 Assim, o algoritmo de divisão com sinal nega o quociente se os sinais dos operandos foram opostos e faz com que o sinal do resto diferente de zero corresponda ao dividendo.
Divisão mais rápida Usamos muitos somadores para agilizar a multiplicação, mas não podemos fazer o mesmo truque para a divisão. O motivo é que precisamos saber o sinal da diferença antes de podermos realizar a próxima etapa do algoritmo, enquanto, com a multiplicação, poderíamos calcular os 32 produtos parciais imediatamente. Existem técnicas para produzir mais de um bit do quociente por etapa. A técnica de divisão SRT tenta descobrir vários bits do quociente por etapa, usando uma pesquisa numa tabela baseada nos bits mais significativos do dividendo e do resto. Ela conta com as etapas subsequentes para corrigir escolhas erradas. Um valor comum hoje é 4 bits. A chave é descobrir o valor para subtrair. Com a divisão binária, existe somente uma única opção. Esses algoritmos utilizam 6 bits do resto e 4 bits do divisor para indexar uma tabela que determina a opção para cada etapa. A precisão desse método rápido depende de haver valores apropriados na tabela de pesquisa. A falácia apresentada na Seção 3.8 mostra o que pode acontecer se a tabela estiver incorreta.
Divisão no MIPS Você já pode ter observado que o mesmo hardware sequencial pode ser usado para multiplicação e divisão nas Figuras 3.6 e 3.12. O único requisito é um registrador de 64 bits, que pode deslocar para a esquerda ou para a direita e uma ALU de 32 bits que soma ou subtrai. Logo, o MIPS utiliza os registradores Hi e Lo de 32 bits, tanto para multiplicação quanto para divisão. Como poderíamos esperar do algoritmo anterior, Hi contém o resto, e Lo contém o quociente após o término da instrução de divisão. Para lidar com inteiros com sinal e inteiros sem sinal, o MIPS possui duas instruções: divide (div) e divide unsigned (divu). O montador MIPS permite que as instruções de
196
Capítulo 3 Aritmética Computacional
divisão especifiquem três registradores, gerando as instruções mflo ou mfhi para colocar o resultado desejado em um registrador de uso geral.
Resumo O suporte de hardware comum para multiplicação e divisão permite que o MIPS ofereça um único par de registradores de 32 bits usados tanto para multiplicar quanto para dividir. A Figura 3.13 resume os acréscimos à arquitetura MIPS das duas últimas seções.
FIGURA 3.13 Arquitetura MIPS revelada até aqui. A memória e os registradores da arquitetura MIPS não estão incluídos por questões de espaço, mas esta seção acrescentou os registradores Hi e Lo para dar suporte à multiplicação e à divisão. A linguagem de máquina do MIPS aparece no Guia de referência rápida, no início do livro.
3.5 Ponto flutuante 197
Instruções de divisão MIPS ignoram o overflow, de modo que o software precisa determinar se o quociente é muito grande. Além do overflow, a divisão também pode resultar em um cálculo impróprio: divisão por 0. Alguns computadores distinguem esses dois eventos anômalos. O software MIPS precisa verificar o divisor para descobrir a divisão por 0 e também o overflow.
Interface hardware/ software
Detalhamento: um algoritmo ainda mais rápido não soma imediatamente o divisor se o resto for negativo. Ele simplesmente soma o dividendo ao resto deslocado na etapa seguinte, pois (r + d) x 2 – d = r x 2 + d x 2 – d = r x 2 + d. Esse algoritmo de divisão sem restauração, que usa um clock por etapa, é explorado ainda mais nos exercícios; o algoritmo aqui apresentado é chamado de divisão com restauração. Um terceiro algoritmo, que não salva o resultado da subtração se ele for negativo, é chamado algoritmo de divisão sem o retorno esperado. Ele reduz em média um terço de operações aritméticas.
3.5 Ponto flutuante
A velocidade não o leva a lugar algum se você estiver na direção errada. Provérbio americano
Indo além de inteiros com e sem sinal, as linguagens de programação admitem números com frações, que são chamados reais na matemática. Aqui estão alguns exemplos de números reais: 3,14159265…dec (pi) 2,71828…dec (e) 0,000000001dec ou1,0dec × 10−9 (segundos em um nanossegundo) 3.155.760.000dec ou 3,15576dec × 109 (segundosem um século típico) Observe que, no último caso, o número não representou uma fração pequena, mas foi maior do que poderíamos representar com um inteiro de 32 bits com sinal. A notação alternativa para os dois últimos números é chamada notação científica, que tem um único dígito à esquerda do ponto decimal. Um número na notação científica que não tem 0s à esquerda do ponto decimal é chamado de número normalizado, que é o modo normal como o escrevemos. Por exemplo, 1,0dec × 10-9 está em notação científica normalizada, mas 0,1dec × 10-8 e 10,0dec × 10-10 não estão. Assim como podemos mostrar números decimais em notação científica, também podemos mostrar números binários em notação científica:
notação científica Uma notação que apresenta números com um único dígito à esquerda do ponto decimal. normalizado Um número na notação de ponto flutuante que não possui 0s à esquerda do ponto decimal.
1,0 bin × 2−1 Para manter um número binário na forma normalizada, precisamos de uma base que possamos aumentar ou diminuir exatamente pelo número de bits que o número precisa ser deslocado para ter um dígito diferente de zero à esquerda do ponto decimal. Somente uma base de 2 atende à nossa necessidade. Como a base não é 10, também precisamos de um novo nome para o ponto decimal; o ponto binário funcionará bem. A aritmética computacional, que admite tais números, é chamada ponto flutuante porque representa os números em que o ponto binário não é fixo, como acontece para os inteiros. A linguagem de programação C utiliza o nome float para esses números. Assim como na notação científica, os números são representados como um único dígito diferente de zero à esquerda do ponto binário. Em binário, o formato é 1, xxxxxxxx bin × 2 yyyy
ponto flutuante Aritmética computacional que representa os números em que o ponto binário não é fixo.
198
Capítulo 3 Aritmética Computacional
(Embora o computador represente o expoente na base 2, bem como o restante do número, para simplificar a notação, mostramos o expoente em decimal.) Uma notação científica padrão para os números reais no formato normalizado oferece três vantagens. Ela simplifica a troca de dados, que incluem números em ponto flutuante; simplifica os algoritmos aritméticos de ponto flutuante, por saber que os números sempre estarão nessa forma; e aumenta a precisão dos números que podem ser armazenados em uma palavra, pois os 0s desnecessários são substituídos por dígitos reais à direita do ponto binário.
Representação em ponto flutuante fração O valor, geralmente entre 0 e 1, colocado no campo de fração.
expoente No sistema de representação numérica da aritmética de ponto flutuante, o valor colocado no campo de expoente.
31 s 1 bit
Um projetista de uma representação em ponto flutuante precisa encontrar um compromisso entre o tamanho da fração e o tamanho do expoente, pois um tamanho de palavra fixo significa que você precisa tirar um bit de um para acrescentar um bit ao outro. Essa troca é entre a precisão e o intervalo: aumentar o tamanho da fração melhora a precisão da fração, enquanto aumentar o tamanho do expoente aumenta o intervalo de números que podem ser representados. Conforme nosso guia de projetos do Capítulo 2 nos lembra, um bom projeto exige um bom compromisso. Os números em ponto flutuante normalmente são múltiplos do tamanho de uma palavra. A representação de um número em ponto flutuante MIPS aparece a seguir, em que s é o sinal do número de ponto flutuante (1 significa negativo), expoente é o valor do campo de expoente com 8 bits (incluindo o sinal do expoente) e fração é o número de 23 bits. Essa representação é chamada sinal e magnitude, pois o sinal possui um bit separado do restante do número.
30 29 28 27 26 25 24 23 22 21 20 19 18 17 16 15 14 13 12 11 10 expoente
fração
8 bits
23 bits
9
8
7
6
5
4
3
2
1
0
Em geral, os números em ponto flutuante estão no formato (−1)S × F × 2E
overflow (ponto flutuante) Uma situação em que um expoente positivo torna-se muito grande para caber no campo de expoente.
underflow (ponto flutuante) Uma situação em que um expoente negativo torna-se muito grande para caber no campo de expoente.
precisão dupla Um valor de ponto flutuante representado em duas palavras de 32 bits.
precisão simples Um valor de ponto flutuante representado em uma única palavra de 32 bits.
F envolve o valor no campo de fração e E envolve o valor no campo de expoente; o relacionamento exato com esses campos será explicado em breve. (Logo veremos que o MIPS faz algo ligeiramente mais sofisticado.) Esses tamanhos escolhidos de expoente e fração dão à aritmética do computador MIPS um intervalo extraordinário. Frações quase tão pequenas quanto 2,0dec × 10-38 e números quase tão grandes quanto 2,0dec × 1038 podem ser representados em um computador. Infelizmente, extraordinário é diferente de infinito, de modo que ainda é possível que os números sejam muito grandes. Assim, interrupções por overflow podem ocorrer na aritmética de ponto flutuante e também na aritmética de inteiros. Observe que overflow aqui significa que o expoente é muito grande para ser representado no campo de expoente. O ponto flutuante também oferece um novo tipo de evento excepcional. Assim como os programadores desejarão saber quando calcularam um número muito grande para ser representado, também desejarão saber se a fração diferente de zero que estão calculando tornou-se tão pequena que não pode ser representada; os dois eventos poderiam resultar em um programa com respostas incorretas. Para distinguir do overflow, as pessoas chamam esse evento de underflow. Essa situação ocorre quando o expoente negativo é muito grande para caber no campo de expoente. Uma maneira de reduzir as chances de underflow ou overflow é oferecer outro formato que tenha um expoente maior. Em C, esse número é chamado double, e as operações sobre doubles são indicadas como aritmética de ponto flutuante com precisão dupla; o ponto flutuante com precisão simples é o nome do formato anterior. A representação de um número em ponto flutuante com precisão dupla utiliza duas palavras MIPS, como vemos a seguir, em que s ainda é o sinal do número, expoente é o valor do campo de expoente em 11 bits, e fração é o número de 52 bits na fração.
31 s 1 bit
3.5 Ponto flutuante 199
30 29 28 27 26 25 24 23 22 21 20 19 18 17 16 15 14 13 12 11 10
9
expoente
fração
11 bits
20 bits fração (continuação) 32 bits
A precisão dupla do MIPS permite números quase tão pequenos quanto 2,0dec × 10–308 e quase tão grandes quanto 2,0dec × 10308. Embora a precisão dupla não aumente o intervalo do expoente, sua principal vantagem é sua maior precisão, em consequência do significando maior. Esses formatos vão além do MIPS. Eles fazem parte do padrão de ponto flutuante IEEE 754, encontrado em praticamente todo computador inventado desde 1980. Esse padrão melhorou bastante tanto a facilidade de portar programas de ponto flutuante quanto a qualidade da aritmética computacional. Para colocar ainda mais bits no significando, o IEEE 754 deixa implícito o bit 1 inicial dos números binários normalizados. Logo, o número tem, na realidade, 24 bits de largura na precisão simples (1 implícito e fração de 23 bits) e 53 bits de extensão na precisão dupla (1 + 52). Para ser exato, usamos o termo significando a fim de representar o número de 24 ou 53 bits que é 1 mais a fração, e fração quando queremos dizer o número de 23 ou 52 bits. Como 0 não possui um 1 inicial, ele recebe o valor de expoente reservado 0, de modo que o hardware não lhe acrescente um 1 inicial. Assim, 00…00bin representa 0; a representação do restante dos números usa a forma de antes, com o 1 oculto sendo acrescentado: (−1)S × (1 + Fração) × 2E em que os bits da fração representam um número entre 0 e 1, e E especifica o valor no campo de expoente, que será explicado em detalhes mais adiante. Se numerarmos os bits da fração da esquerda para a direita de s1, s2, s3, …, então o valor é (− (−1)S × (1 + (s1 × 2−1 ) + (s2 × 2−2 ) + (s3 × 2−3 ) + (s4 × 2−4 ) + …) × 2E A Figura 3.14 mostra as codificações dos números de ponto flutuante IEEE 754. Outros recursos do IEEE 754 são símbolos especiais para representar eventos incomuns. Por exemplo, em vez de interromper em uma divisão por 0, o software pode definir o resultado para um padrão de bits que represente +∞ ou –∞; o maior expoente é reservado a esses símbolos especiais. Quando o programador imprime os resultados, o programa imprimirá um símbolo de infinito. (Para os que são matematicamente treinados, a finalidade do infinito é formar o fechamento topológico dos reais.)
FIGURA 3.14 Codificação IEE 754 dos números de ponto flutuante. Um bit de sinal separado determina o sinal. Os números desnormalizados são descritos no Detalhamento na página 270. Essa informação também é encontrada na coluna 4 do Guia de Instrução Rápida no início deste livro.
8
7
6
5
4
3
2
1
0
200
Capítulo 3 Aritmética Computacional
O IEEE 754 até mesmo possui um símbolo para o resultado de operações inválidas, como 0/0, ou a subtração entre infinito e infinito. Esse símbolo é NaN, de Not a Number (não é um número). A finalidade dos NaNs é permitir que os programadores adiem alguns testes e decisões para outro momento no programa, quando for conveniente. Os projetistas do IEEE 754 também queriam uma representação de ponto flutuante que pudesse ser facilmente processada por comparações de inteiros, especialmente para ordenação. Esse desejo é o motivo pelo qual o sinal está no bit mais significativo, permitindo um teste rápido de menor que, maior que ou igual a 0. (Isso é um pouco mais complicado do que uma ordenação simples de inteiros, pois essa notação é basicamente sinal e magnitude, em vez do complemento de dois.) Colocar o expoente antes do significando simplifica a ordenação dos números de ponto flutuante usando instruções de comparação de inteiros, pois os números com expoentes maiores são maiores do que os números com expoentes menores, desde que os dois expoentes tenham o mesmo sinal. Expoentes negativos impõem um desafio à ordenação simplificada. Se usarmos o complemento de dois ou qualquer outra notação em que os expoentes negativos têm um 1 no bit mais significativo do campo de expoente, um expoente negativo se parecerá com um número grande. Por exemplo, 1,0bin × 2-1 seria representado como 31 30 29 28 27 26 25 24 23 22 21 20 19 18 17 16 15 14 13 12 11 10 0
1
1
1
1
1
1
1
1
0
0
0
0
0
0
0
0
0
0
0
0
0
9
8
7
6
5
4
3
2
1
0
0
0
0
0
0
0
0
.
.
.
(Lembre-se de que o 1 à esquerda do ponto é implícito no significando.) O valor 1,0bin × 2+1 seria semelhante a um número binário menor 31 30 29 28 27 26 25 24 23 22 21 20 19 18 17 16 15 14 13 12 11 10
9
8
7
6
5
4
3
2
1
0
0
0
0
0
0
0
0
0
.
.
.
0
0
0
0
0
0
0
1
0
0
0
0
0
0
0
0
0
0
0
0
0
A notação desejável, portanto, precisa representar o expoente mais negativo como 00 … 00bin e o mais positivo como 11 … 11bin. Essa convenção é chamada notação deslocada, com a inclinação (bias) sendo o número subtraído da representação normal, sem sinal, para determinar o valor real. O IEEE 754 usa um bias de 127 para a precisão simples, de modo que –1 é representado pelo padrão de bits do valor –1 + 127dec ou 126dec = 0111 1110bin, e +1 é representado por 1 + 127 ou 128dec = 1000 0000bin. O expoente deslocado significa que o valor representado por um número em ponto flutuante é, na realidade: (−1)S × (1 + Fração) × 2(Expoente−Bias) A faixa de números de precisão simples é, então, desde ±1.00000000000000000000000 bin × 2−126 até ±1.11111111111111111111111bin × 2+127 Vamos mostrar a representação.
Representação de ponto flutuante
EXEMPLO RESPOSTA
Mostre a representação binária IEEE 754 para o número –0,75dec em precisão simples e dupla. O número –0,75dec também é
2 −3 / 4 dec ou − 3 / 2dec
3.5 Ponto flutuante 201
Ele também é representado pela fração binária 2 −11bin / 2dec ou − 0,11bin
Em notação científica, o valor é −0,11bin × 20 e, na notação científica normalizada, ele é −1,1bin × 2−1 A representação geral para um número de precisão simples é (−1)S × (1 + Fração) × 2(Expoente−127) Quando subtraímos o bias 127 do expoente de –1,1bin × 2–1, o resultado é (−1)1 × (1 + .1000 0000 0000 0000 0000 000 bin ) × 2(126−127) A representação binária de precisão simples de 0,75dec, portanto, é 31 30 29 28 27 26 25 24 23 22 21 20 19 18 17 16 15 14 13 12 11 10 1
0
1
1
1
1 bit
1
1
1
0
1
0
0
0
0
0
0
0
0
0
0
8 bits
0
0
9
8
7
6
5
4
3
2
1
0
0
0
0
0
0
0
0
0
0
0
23 bits
A representação em precisão dupla é (−1)1 × (1 + .1000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 bin ) × 2(1022−1023)
31 30 29 28 27 26 25 24 23 22 21 20 19 18 17 16 15 14 13 12 11 10 1
0
1
1
1
1
0
0
0
0
1 bit 0
0
1
1
1
1
1
0
1
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
11 bits 0
0
0
9
8
7
6
5
4
3
2
1
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
20 bits 0
0
32 bits
Agora, vamos experimentar na outra direção.
Convertendo ponto flutuante binário para decimal
Que número decimal é representado por este float de precisão simples?
EXEMPLO
31 30 29 28 27 26 25 24 23 22 21 20 19 18 17 16 15 14 13 12 11 10 1
1
0
0
0
0
0
0
1
0
1
0
0
0
0
0
0
0
0
0
0
0
9
8
7
6
5
4
3
2
1
0
0
0
0
0
0
0
0
.
.
.
O bit de sinal é 1, o campo de expoente contém 129 e o campo de fração contém 1 × 2–2 = 1/4, ou 0,25. Usando a equação básica, (−1)S × (1 + Fração) × 2(Expoente-Bias) = = = =
(−1)1 × (1 + 0, 25) × 2(129−127) −1 × 1, 25 × 22 −1, 25 × 4 −5,0
RESPOSTA
202
Capítulo 3 Aritmética Computacional
Nas próximas seções, daremos os algoritmos para a adição e multiplicação em ponto flutuante. Em seu núcleo, eles utilizam operações inteiras correspondentes nos significandos, mas é preciso que haja manutenção extra para lidar com os expoentes e normalizar o resultado. Primeiro, oferecemos uma derivação intuitiva dos algoritmos em decimal, e depois uma versão mais detalhada, binária, nas figuras. Detalhamento: em uma tentativa de aumentar o intervalo sem remover bits do significando, alguns computadores antes do padrão IEEE 754 usavam uma base diferente de 2. Por exemplo, os computadores mainframe IBM 360 e 370 usam a base 16. Como mudar o expoente no IBM em um significa deslocar o significando em 4 bits, os números de base 16 “normalizados” podem ter até 3 bits à esquerda do ponto em 0s! Logo, os dígitos hexadecimais significam que até 3 bits precisam ser removidos do significando, o que leva a problemas surpreendentes na precisão da aritmética de ponto flutuante. Mainframes IBM recentes admitem IEEE 754 além do formato hexa.
Adição em ponto flutuante Vamos somar os números na notação científica manualmente, para ilustrar os problemas na adição em ponto flutuante: 9,999dec × 101 + 1,610dec × 10–1. Suponha que só possamos armazenar quatro dígitos decimais do significando e dois dígitos decimais do expoente. Etapa 1. Para poder somar esses números corretamente, temos de alinhar o ponto decimal do número que possui o menor expoente. Logo, precisamos de uma forma do número menor, 1,610dec × 10-1, que combine com o expoente maior. Obtemos isso observando que existem várias representações de um número em ponto flutuante não normalizado na notação científica: 1,610dec × 10−1 = 0,1610dec × 100 = 0,01610dec × 101 O número da direita é a versão que desejamos, pois seu expoente combina com o expoente do maior número, 9,999dec × 101. Assim, a primeira etapa desloca o significando do menor número à direita até que seu expoente corrigido combine com o do maior número. Contudo, só podemos representar quatro dígitos decimais, de modo que, após o deslocamento, o número é, na realidade: 0,016dec × 101 Etapa 2. Em seguida, vem a adição dos significandos: 9‚999dec + 0,016dec 10‚015dec A soma é 10,015dec × 101. Etapa 3. Essa soma não está na notação científica normalizada, de modo que precisamos ajustá-la: 10,015dec × 101 = 1,0015dec × 102 Assim, depois da adição, podemos ter de deslocar a soma para colocá-la na forma normalizada, ajustando o expoente de acordo. Esse exemplo mostra o deslocamento para a direita, mas, se um número fosse positivo e o outro negativo, é possível que a soma tenha muitos 0s iniciais, exigindo deslocamentos à esquerda. Sempre que o expoente é aumentado ou diminuído, temos de verificar o overflow ou underflow – ou seja, temos de verificar se o expoente ainda cabe em seu campo. Etapa 4. Como pressupomos que o significando só pode ter quatro dígitos de extensão (excluindo o sinal), temos de arredondar o número. Em nosso algoritmo que aprendemos
3.5 Ponto flutuante 203
na escola, as regras truncam o número se o dígito à direita do ponto desejado estiver entre 0 e 4 e somamos 1 ao dígito se o número à direita estiver entre 5 e 9. O número 1,0015dec × 102 é arredondado para quatro dígitos no significando, passando para 1,002dec × 102 pois o quarto dígito à direita do ponto decimal estava entre 5 e 9. Observe que, se não tivermos sorte no arredondamento, como ao somar 1 a uma sequência de 9s, a soma não pode mais ser normalizada, sendo necessário realizar a etapa 3 novamente. A Figura 3.15 mostra o algoritmo para a adição binária de ponto flutuante que acompanha este exemplo em decimal. As etapas 1 e 2 são semelhantes ao exemplo que discutimos:
FIGURA 3.15 Adição de ponto flutuante. O caminho normal é executar as etapas 3 e 4 uma vez, mas se o arredondamento fizer com que a soma fique não normalizada, temos de repetir a etapa 3.
204
Capítulo 3 Aritmética Computacional
ajustar o significando do número com o menor expoente e depois somar os dois significandos. A etapa 3 normaliza os resultados, forçando uma verificação de overflow ou underflow. O teste de overflow e underflow na etapa 3 depende da precisão dos operandos. Lembre-se de que o padrão de todos os bits zero no expoente é reservado e usado para a representação de ponto flutuante de zero. Além disso, o padrão de todos os bits um no expoente é reservado para indicar valores e situações fora do escopo dos números de ponto flutuante normais (veja a Seção “Detalhamento” na página 270). Assim, para a precisão simples, o expoente máximo é 127, e o expoente mínimo é –126. Os limites para precisão dupla são 1023 e –1022.
Adição de ponto flutuante em decimal
EXEMPLO
Tente somar os números 0,5 dec e –0,4375 dec em binário usando o algoritmo da Figura 3.15.
RESPOSTA
Primeiro, vejamos a versão binária dos dois números na notação científica normalizada, supondo que mantemos 4 bits de precisão: 0,5dec −0, 4375dec
= = = =
1 / 2 dec 0,1bin −7 /16dec −0,0111bin
= 1 / 21dec = 0,1bin × 20 = 1,000 bin × 2−1 4 = −7 / 2dec = −0,0111bin × 20 = −1,110 bin × 2−2
Agora, seguimos o algoritmo: Etapa 1. O significando do número com o menor expoente (-1,11bin × 2-2) é deslocado para a direita até seu expoente combinar com o maior número: −1,110 bin × 2−2 = −0,111bin × 2−1 Etapa 2. Some os significandos: 1,000 bin × 2−1 + (−0,111bin × 2−1 ) = 0,001bin × 2−1 Etapa 3. Normalize a soma, verificando overflow ou underflow: 0,001bin × 2−1 = 0,010 bin × 2−2 = 0,100 bin × 2−3 = 1,000 bin × 2−4 C omo 127 ≥ 04 ≥ –126, não existe overflow ou underflow. (O expoente deslocado seria – 4 + 127, ou 123, que está entre 1 e 254, o menor e o maior expoente deslocado não reservado.) Etapa 4. Arredonde a soma: 1,000 bin × 2−4 soma já cabe exatamente em 4 bits, de modo que não há mudança nos bits A em razão do arredondamento. Essa soma é, então 1,000 bin × 2−4 = 0,0001000 bin = 0,0001bin 4 = 1 / 2dec = 1 /16dec = 0,0625dec Essa soma é o que esperaríamos da soma de 0,5dec a –0,4375dec. Muitos computadores dedicam o hardware para executar operações de ponto flutuante o mais rápido possível. A Figura 3.16 esboça a organização básica do hardware para a adição de ponto flutuante.
3.5 Ponto flutuante 205
FIGURA 3.16 Diagrama de bloco de uma unidade aritmética dedicada à adição em ponto flutuante. As etapas da Figura 3.15 correspondem a cada bloco, de cima para baixo. Primeiro, o expoente de um operando é subtraído do outro usando a ALU pequena para determinar qual é maior e quanto. Essa diferença controla os três multiplexadores; da esquerda para a direita, eles selecionam o maior expoente, o significando do menor número e o significando do maior número. O menor significando é deslocado para a direita, e depois os significandos são somados usando a ALU grande. A etapa de normalização, então, desloca a soma para a esquerda ou para a direita e incrementa ou decrementa o expoente. O arredondamento, então, cria o resultado final, que pode exigir normalização novamente para produzir o resultado final.
Multiplicação em ponto flutuante Agora que já explicamos a adição em ponto flutuante, vamos experimentar a multiplicação em ponto flutuante. Começamos multiplicando os números decimais em notação científica na mão: 1,110dec × 1010 × 9,200dec × 10–5. Suponha que possamos armazenar apenas quatro dígitos do significando e dois dígitos do expoente. Etapa 1. Ao contrário da adição, calculamos o expoente do produto simplesmente somando os expoentes dos operandos: Novoexpoente = 10 + (−5) = 5 amos fazer isso com os expoentes deslocados, para obtermos o mesmo resulV tado: 10 + 127 = 137, e –5 + 127 = 122, assim
206
Capítulo 3 Aritmética Computacional
Novoexpoente = 137 + 122 = 259 sse resultado é muito grande para o campo de expoente de 8 bits, de modo que E há algo faltando! O problema é com o bias, pois estamos somando os biases e também os expoentes: Novoexpoente = (10 + 127) + (−5 + 127) = (5 + 2 × 127) = 259 De acordo com isso, para obter a soma deslocada correta quando somamos números deslocados, temos de subtrair o bias da soma: Novoexpoente = 137 + 122 − 127 = 259 − 127 = 132 = (5 + 127) e 5 é, na realidade, o expoente que calculamos inicialmente. Etapa 2. Em seguida, vem a multiplicação dos significandos: 1‚110dec 9‚200dec 0000 0000 2220 9990 10212000dec ×
xistem três dígitos à direita do ponto decimal para cada operando, de modo E que o ponto decimal é colocado seis dígitos a partir da direita no significando do produto: 10, 212000dec S upondo que só possamos manter três dígitos à direita do ponto decimal, o produto é 10,212 × 105. Etapa 3. Este produto não está normalizado, de modo que precisamos normalizá-lo: 10, 212dec × 105 = 1,0212dec × 106 ssim, após a multiplicação, o produto pode ser deslocado para a direita um dígito, A a fim de colocá-lo no formato normalizado, somando 1 ao expoente. Nesse ponto, podemos verificar o overflow e o underflow. O underflow pode ocorrer se os dois operandos forem pequenos – ou seja, se ambos tiverem expoentes negativos grandes. Etapa 4. Consideramos que o significando tem apenas quatro dígitos de largura (excluindo o sinal), de modo que devemos arredondar o número. O número 1,0212dec × 106 é arredondado para quatro dígitos no significando, para 1,021dec × 106 Etapa 5. O sinal do produto depende dos sinais dos operandos originais. Se forem iguais, o sinal é positivo; caso contrário, é negativo. Logo, o produto é +1,021dec × 106 O sinal da soma no algoritmo de adição foi determinado pela adição dos significandos; porém, na multiplicação, o sinal do produto é determinado pelos sinais dos operandos.
3.5 Ponto flutuante 207
Mais uma vez, como mostra a Figura 3.17, a multiplicação de números binários em ponto flutuante é muito semelhante às etapas que acabamos de concluir. Começamos calculando o novo expoente do produto, somando os expoentes deslocados, subtraindo um bias para obter o resultado apropriado. Em seguida, está a multiplicação de significandos, seguida por uma etapa de normalização opcional. O tamanho do expoente é verificado, em busca de overflow ou underflow, e depois o produto é arredondado. Se o arredondamento causar mais normalização, mais uma vez verificamos o tamanho do expoente. Finalmente, definimos o bit de sinal como 1 se os sinais dos operandos forem diferentes (produto negativo) ou como 0 se forem iguais (produto positivo).
FIGURA 3.17 Multiplicação em ponto flutuante. O caminho normal é executar as etapas 3 e 4 uma vez, mas se o arredondamento fizer com que a soma fique desnormalizada, temos de repetir a etapa 3.
208
Capítulo 3 Aritmética Computacional
EXEMPLO RESPOSTA
Multiplicação em ponto flutuante em decimal
Vamos tentar multiplicar os números 0,5 dec e –0,4375 dec , usando as etapas na Figura 3.17. Em binário, a tarefa é multiplicar 1,000bin × 2–1 por –1,110bin × 2–2. Etapa 1. Somando os expoentes sem bias: −1 + (−2) = −3 ou então, usando a representação deslocada: (−1 + 127) + (−2 + 127) − 127 = (−1 − 2) + (127 + 127 − 127) = − 3 + 127 = 124 Etapa 2. Multiplicando os significandos: 1‚000 bin × 1‚110 bin 0000 1000 1000 1000 1110000 bin O produto é 1,110000bin × 2–3, mas precisamos mantê-lo com 4 bits, de modo que é 1,110bin × 2–3. Etapa 3. Agora, verificamos o produto para ter certeza de que está normalizado e depois verificamos o expoente em busca de overflow ou underflow. O produto já está normalizado e, como 127 ≥ –3 ≥ –126, não existe overflow ou underflow. (Usando a representação deslocada, 254 ≥ 124 ≥ 1, de modo que o expoente cabe.) Etapa 4. O arredondamento do produto não causa mudança: 1,110 bin × 2−3 Etapa 5. Como os sinais dos operandos originais diferem, torne o sinal do produto negativo. Logo, o produto é −1,110 bin × 2−3 Convertendo para decimal, para verificar nossos resultados: −1,110 bin × 2−3 = −0,001110 bin = −0,00111bin = −7/25dec = −7/32dec = −0, 21875dec O produto entre 0,5dec e –0,4375dec é, na realidade, –0,21875dec.
Instruções de ponto flutuante no MIPS O MIPS admite os formatos de precisão simples e dupla do padrão IEEE 754 com estas instruções: j
Adição simples em ponto flutuante (add.s) e adição dupla (add.d)
j
Subtração simples em ponto flutuante (sub.s) e subtração dupla (sub.d)
3.5 Ponto flutuante 209
j
Multiplicação simples em ponto flutuante (mul.s) e multiplicação dupla (mul.d)
j
Divisão simples em ponto flutuante (div.s) e divisão dupla (div.d)
j
j
Comparação simples em ponto flutuante (c.x.s) e comparação dupla (c.x.d), em que x pode ser igual (eq), diferente (neq), menor que (lt), menor ou igual (le), maior que (gt) ou maior ou igual (ge) Desvio verdadeiro em ponto flutuante (belt) e desvio falso (bc1f)
A comparação em ponto flutuante define um bit como verdadeiro ou falso, dependendo da condição de comparação, e um desvio de ponto flutuante então decide se desviará ou não, dependendo da condição. Os projetistas do MIPS decidiram acrescentar registradores de ponto flutuante separados – chamados $f0, $f1, $f2… – usados para precisão simples ou precisão dupla. Logo, eles incluíram loads e stores separados para registradores de ponto flutuante: lwc1 e swc1 . Os registradores base para transferências de dados de ponto flutuante continuam sendo registradores inteiros. O código do MIPS para carregar dois números de precisão simples da memória, somá-los e depois armazenar a soma poderia se parecer com isto: lwc1
$f4,x($sp)
# Lê número P.F. 32 bits em F4
lwc1
$f6,y($sp)
# Lê número P.F. 32 bits em F6
add.s
$f2,$f4,$f6
#
F2 = F4 + F6 precisão simples
swc1
$f2,z($sp)
#
Armazena número P.F. 32 bits de F2
Um registrador de precisão dupla é, na realidade, um par de registradores (par e ímpar) de precisão simples, usando o número do registrador par como seu nome. Assim, o par de registradores $f2 e $f3 também forma o registrador de precisão dupla chamado $f2. A Figura 3.18 resume a parte de ponto flutuante da arquitetura MIPS revelada neste capítulo, com as adições para dar suporte ao ponto flutuante mostradas em destaque. Semelhante à Figura 2.19 no Capítulo 2, mostramos a codificação dessas instruções na Figura 3.19.
Uma questão que os projetistas de computador enfrentam no suporte à aritmética de ponto flutuante é se devem utilizar os mesmos registradores usados pelas instruções com inteiros ou acrescentar um conjunto especial de ponto flutuante. Como os programas normalmente realizam operações com inteiros e operações com ponto flutuante sobre dados diferentes, a separação dos registradores só aumentará ligeiramente o número de instruções necessárias para executar um programa. O maior impacto é criar um conjunto separado de instruções de transferência de dados para mover dados entre os registradores de ponto flutuante e a memória. Os benefícios dos registradores de ponto flutuante separados são a existência do dobro dos registradores sem utilizar mais bits no formato da instrução, ter o dobro da largura de banda de registrador, com conjuntos de registradores separados para inteiros e números de ponto flutuante, e ser capaz de personalizar registradores para ponto flutuante; por exemplo, alguns computadores convertem todos os operandos dimensionados nos registradores para um único formato inteiro.
Interface hardware/ software
210
Capítulo 3 Aritmética Computacional
FIGURA 3.18 Arquitetura de ponto flutuante do MIPS revelada até aqui. Ver Apêndice B, Seção B.10, para obter mais detalhes. Essa informação também é encontrada na coluna 2 do Guia de Instrução Rápida do MIPS, no início deste livro.
3.5 Ponto flutuante 211
FIGURA 3.19 Codificação de instruções de ponto flutuante do MIPS. Essa notação indica o valor de um campo por linha e por coluna. Por exemplo, na parte superior da figura, lw se encontra na linha número 4 (100bin para os bits de 31-29 da instrução) e na coluna número 3 (011bin para os bits 28-26 da instrução), de modo que o valor correspondente do campo op (bits 31-26) é 100011bin. O sublinhado indica que o campo é usado em outro lugar. Por exemplo, FlPt na linha 2 e coluna 1 (op = 010001bin) está definido na parte inferior da figura. Logo, sub.f na linha 0 e coluna 1 da seção inferior significa que o campo funct (bits 5-0 da instrução) é 000001bin e o campo op (bits 31-26) é 010001bin. Observe que o campo rs de 5 bits, especificado na parte do meio da figura, determina se a operação é de precisão simples (f = s, de modo que rs = 10000) ou precisão dupla (f= d, de modo que rs = 10001). De modo semelhante, o bit 16 da instrução determina se a instrução bc1.c testa o estado verdadeiro (bit 16 = 1 => bc1.t) ou falso (bit 16 = 1 => bc1.f). As instruções em negrito são descritas nos Capítulos 2 neste capítulo, com o Apêndice B abordando todas as instruções. Essa informação também é encontrada na coluna 2 do Guia de Instrução Rápida do MIPS, no início deste livro.
212
Capítulo 3 Aritmética Computacional
Compilando um programa C de ponto flutuante em código assembly do MIPS
EXEMPLO
Vamos converter uma temperatura em Fahrenheit para Celsius:
Considere que o argumento de ponto flutuante fahr seja passado em $f12 e o resultado deva ficar em $f0. (Ao contrário dos registradores inteiros, o registrador de ponto flutuante 0 pode conter um número.) Qual é o código assembly do MIPS?
RESPOSTA
Consideramos que o compilador coloca as três constantes de ponto flutuante na memória para serem alcançadas facilmente por meio do ponteiro global $gp. As duas primeiras instruções carregam as constantes 5.0 e 9.0 nos registradores de ponto flutuante:
Depois, elas são divididas para que se obtenha a fração 5.0/9.0:
(Muitos compiladores dividiriam 5.0 por 9.0 durante a compilação e guardariam uma única constante 5.0/9.0 na memória, evitando, assim, a divisão em tempo de execução.) Em seguida, carregamos a constante 32.0 e depois a subtraímos de fahr ($f12):
Finalmente, multiplicamos os dois resultados intermediários, colocando o produto em $f0 como resultado de retorno, e depois retornamos:
Agora, vamos realizar operações de ponto flutuante em matrizes, código comumente encontrado em programas científicos.
Compilando um procedimento em C de ponto flutuante com matrizes bidimensionais no MIPS
EXEMPLO
A maioria dos cálculos de ponto flutuante é realizada com precisão dupla. Vamos realizar uma multiplicação de matrizes X = X + Y * Z. Vamos supor que X, Y e Z sejam matrizes quadradas com 32 elementos em cada dimensão.
3.5 Ponto flutuante 213
Os endereços iniciais do array são parâmetros, de modo que estão em $a0, $a1 e $a2. Suponha que as variáveis inteiras estejam em $s0, $s1 e $s2, respectivamente. Qual é o código assembly do MIPS para o corpo do procedimento? Observe que x[i][j] é usado no loop mais interno. Como o índice do loop é k, o índice não afeta x[i][j], de modo que podemos evitar a leitura e o armazenamento de x[i][j] a cada iteração. Em vez disso, o compilador lê x[i][j] em um registrador fora do loop, acumula a soma dos produtos de y[i][k] e z[k][j] nesse mesmo registrador, e depois armazena a soma em x[i][j], ao terminar o loop mais interno. Mantemos o código mais simples, usando as pseudoinstruções em assembly li (que carrega uma constante em um registrador), e l.d e s.d (que o montador transforma em um par de instruções de transferência de dados, lwc1 ou swc1, para um par de registradores de ponto flutuante). O corpo do procedimento começa salvando o valor de término do loop (32) em um registrador temporário e depois inicializando as três variáveis do loop for:
Para calcular o endereço de x[i][j] precisamos saber como um array bidimensional de 32 × 32 é armazenado na memória. Como você poderia esperar, seu layout é como se houvesse 32 arrays unidimensionais, cada um com 32 elementos. Assim, a primeira etapa é pular os i “arrays unidimensionais”, ou linhas, para obter a que desejamos. Assim, multiplicamos o índice da primeira dimensão pelo tamanho da linha, 32. Como 32 é uma potência de 2, podemos usar um deslocamento em seu lugar:
Agora, acrescentamos o segundo índice para selecionar o elemento j da linha desejada:
Para transformar essa soma em um índice em bytes, multiplicamos pelo tamanho de um elemento da matriz em bytes. Como cada elemento tem 8 bytes para a precisão dupla, podemos deslocar à esquerda por 3:
Em seguida, somamos essa soma ao endereço base de x, dando o endereço de x[i]
[j], e depois carregamos o número de precisão dupla x[i][j] em $f4:
RESPOSTA
214
Capítulo 3 Aritmética Computacional
As cinco instruções a seguir são praticamente idênticas às cinco últimas: calcular o endereço e depois ler o número de precisão dupla z[k][j].
De modo semelhante, as cinco instruções a seguir são como as cinco últimas: calcular o endereço e depois carregar o número de precisão dupla y[i][k].
Agora que carregamos todos os dados, finalmente estamos prontos para realizar algumas operações em ponto flutuante! Multiplicamos os elementos de y e z localizados nos registradores $f18 e $f16, e depois acumulamos a soma em $f4.
O bloco final incrementa o índice k e retorna se o índice não for 32. Se for 32, ou seja, o final do loop mais interno, precisamos armazenar em x[i][j] a soma acumulada em $f4.
De modo semelhante, essas quatro instruções finais incrementam a variável de índice do loop do meio e do loop mais externo, voltando no loop se o índice não for 32 e saindo se o índice for 32.
Detalhamento: o layout do array discutido no exemplo, chamado ordem linhas primeiro, é usado pela linguagem C e muitas outras linguagens de programação. Fortran, por sua vez, usa a ordem colunas primeiro, pela qual o array é armazenado coluna por coluna. Detalhamento: Somente 16 dos 32 registradores de ponto flutuante do MIPS puderam ser
usados originalmente para operações de precisão simples: $f0, $f2, $f4,…, $f30. A precisão dupla é calculada usando pares desses registradores. Os registradores de ponto flutuante com números ímpares só foram usados para carregar e armazenar a metade direita
3.5 Ponto flutuante 215
dos números de ponto flutuante de 64 bits. MIPS-32 acrescentou l.d e s.d ao conjunto de instruções. MIPS-32 também acrescentou versões “simples emparelhadas” de todas as instruções de ponto flutuante, em que uma única instrução resulta em duas operações paralelas de ponto flutuante sobre dois operandos de 32 bits dentro de registradores de 64 bits. Por exemplo, add.ps F0, F2, F4 é equivalente a add.s F0, F2, F4, seguido por add.
ps F1, F3, F5
Detalhamento: Outro motivo para que os registradores inteiros e de ponto flutuante sejam separados é que os microprocessadores na década de 1980 não possuíam transistores suficientes para colocar a unidade ponto flutuante no mesmo chip da unidade de inteiros. Logo, a unidade de ponto flutuante, incluindo os registradores de ponto flutuante, opcionalmente estava disponível como um segundo chip. Esses chips aceleradores opcionais são chamados coprocessadores e explicam o acrônimo para os loads de ponto flutuante no MIPS: lwc1 significa “load word to coprocessor 1” (“leia uma palavra para o coprocessador 1”), que é a unidade de ponto flutuante. (O coprocessador 0 trata da memória virtual, descrita no Capítulo 5.) Desde o início da década de 1990, os microprocessadores têm integrado o ponto flutuante (e praticamente tudo o mais) no chip, e, por isso, o termo “coprocessador” reúne “acumulador” e “memória”.
Detalhamento: Conforme mencionamos na Seção 3.4, acelerar a divisão é mais complicado do que a multiplicação. Além de SRT, outra técnica para aproveitar um multiplicador rápido é a iteração de Newton, na qual a divisão é redefinida como a localização do zero de uma função para encontrar a recíproca 1/x, que é multiplicada pelo outro operando. As técnicas de iteração não podem ser arredondadas corretamente sem o cálculo de muitos bits extras. Um chip TI soluciona esse problema, calculando uma recíproca de precisão extra.
Detalhamento: Java abarca o padrão IEEE 754 por nome em sua definição dos tipos de dados e operações de ponto flutuante Java. Assim, o código no primeiro exemplo poderia muito bem ter sido gerado para um método de classe que convertesse graus Fahrenheit em Celsius. O segundo exemplo utiliza múltiplos arrays dimensionais, que não são admitidos explicitamente em Java. Java permite arrays de arrays, mas cada array pode ter seu próprio tamanho, ao contrário de vários arrays dimensionais em C. Como os exemplos no Capítulo 2, uma versão Java desse segundo exemplo exigiria muito código de verificação para os limites de array, incluindo um novo cálculo de tamanho no final da linha. Ela também precisaria verificar se a referência ao objeto não é nula.
Aritmética de precisão Ao contrário dos inteiros, que podem representar exatamente cada número entre o menor e o maior, os números de ponto flutuante, em geral, são aproximações para um número que não representam realmente. O motivo é que existe uma variedade infinita de números reais entre, digamos, 0 e 1, porém não mais do que 253 podem ser representados com exatidão em ponto flutuante de precisão dupla. O melhor que podemos fazer é utilizar a representação de ponto flutuante próxima ao número real. Assim, o padrão IEEE 754 oferece vários modos de arredondamento para permitir que o programador selecione a aproximação desejada. O arredondamento parece muito simples, mas arredondar com precisão exige que o hardware inclua bits extras no cálculo. Nos exemplos anteriores, fomos vagos com relação ao número de bits que uma representação intermediária pode ocupar, mas, claramente, se cada resultado intermediário tivesse de ser truncado ao número de dígitos exato, não haveria oportunidade para arredondar. O IEEE 754, portanto, sempre mantém dois bits extras à direita durante adições intermediárias, chamados guarda e arredondamento, respectivamente. Vamos fazer um exemplo decimal para ilustrar o valor desses dígitos extras.
guarda O primeiro dos dois bits extras mantidos à direita durante os cálculos intermediários de números de ponto flutuante, usados para melhorar a precisão do arredondamento.
arredondamento Método para fazer com que o resultado de ponto flutuante intermediário se encaixe no formato de ponto flutuante; o objetivo normalmente é encontrar o número mais próximo que pode ser representado no formato.
216
Capítulo 3 Aritmética Computacional
Arredondando com dígitos de guarda
EXEMPLO
RESPOSTA
Some 2,56 dec × 10 0 a 2,34 dec × 102 , supondo que temos três dígitos decimais significativos. Arredonde para o número decimal mais próximo com três dígitos decimais significativos, primeiro com dígitos guarda e arredondamento, e depois sem eles. Primeiro, temos de deslocar o número menor para a direita, a fim de alinhar os expoentes, de modo que 2,56dec × 100 torna-se 0,0256dec × 102. Como temos dígitos de guarda e arredondamento, podemos representar os dois dígitos menos significativos quando alinharmos os expoentes. O dígito de guarda mantém 5 e o dígito de arredondamento mantém 6. A soma é 2‚3400dec + 0‚0256dec 2‚3656dec Assim, a soma é 2,3656dec × 102. Como temos dois dígitos para arredondar, queremos que os valores de 0 a 49 arredondem para baixo e de 51 a 99 para cima, com 50 sendo o desempate. Arredondar a soma para cima com três dígitos significativos gera 2,37dec × 102. Fazer isso sem dígitos de guarda e arredondamento remove dois dígitos do cálculo. A nova soma é, então, 2‚34 dec + 0‚02dec 2‚36dec A resposta é 2,36dec × 102, arredondando no último dígito da soma anterior.
unidades na última casa (ulp) O número de bits com erro nos bits menos significativos do significando entre o número real e o número que pode ser representado.
Como o pior caso para o arredondamento seria quando o número real está a meio caminho entre duas representações de ponto flutuante, a precisão no ponto flutuante normalmente é medida em termos do número de bits em erro nos bits mais significativos do significando; a medida é denominada número de unidades na última casa, ou ulp (units in the last place). Se o número ficou defasado em 2 nos bits menos significativos, ele estaria defasado por 2 ulps. Desde que não haja qualquer overflow, underflow ou exceções de operação inválida, o IEEE 754 garante que o computador utiliza o número que está dentro de meia ulp. Detalhamento: Embora o exemplo anterior, na realidade, precisasse apenas de um dígito extra, a multiplicação pode precisar de dois. Um produto binário pode ter um bit 0 inicial; logo, a etapa de normalização precisa deslocar o produto 1 bit à esquerda. Isso desloca o dígito de guarda para o bit menos significativo do produto, deixando o bit de arredondamento para ajudar no arredondamento mais preciso do produto. O IEEE 754 tem quatro modos de arredondamento: sempre arredondar para cima (para +∞), sempre arredondar para baixo (para –∞), truncar e arredondar para o próximo par. O modo final determina o que fazer se o número estiver exatamente no meio. A Receita Federal americana sempre arredonda 0,50 dólares para cima, possivelmente para o benefício da Receita. Um modo mais imparcial seria arredondar para cima, nesse caso, na metade do tempo e arredondar para baixo na outra metade. O IEEE 754 diz que, se o bit menos significativo retido em um caso de meio do caminho for ímpar, some um; se for par, trunque. Esse método sempre cria um 0 no bit menos significativo no caso de desempate, dando nome ao arredondamento. Esse modo é o mais utilizado, e o único que o Java admite. O objetivo dos bits de arredondamento extras é permitir que o computador obtenha os mesmos resultados, como se os resultados intermediários fossem calculados para precisão
3.5 Ponto flutuante 217
infinita e depois arredondados. Para auxiliar nesse objetivo e arredondar para o par mais próximo, o padrão possui um terceiro bit além do bit de guarda e arredondamento; ele é definido sempre que existem bits diferentes de zero à direita do bit de arredondamento. Esse sticky bit permite que o computador veja a diferença entre 0,50 … 00dec e 0,50 … 01dec ao arredondar. O sticky bit pode ser definido, por exemplo, durante a adição, quando o menor número é deslocado para a direita. Suponha que somemos 5,01dec × 10-1 a 2,34ten × 102 no exemplo anterior. Mesmo com os bits de guarda e arredondamento, estaríamos somando 0,0050 a 2,34, com uma soma de 2,3450. O sticky bit seria definido, porque existem bits diferentes de zero à direita. Sem o sticky bit para lembrar se quaisquer 1s foram deslocados, consideraríamos que o número é igual a 2.345000…00 e arredondaríamos para o par mais próximo de 2,34. Com o sticky bit para lembrar que o número é maior do que 2,345000…00, arredondaríamos para 2,35.
sticky bit Um bit usado no arredondamento além dos bits de guarda e arredondamento, definido sempre que existem bits diferentes de zero à direita do bit de arredondamento.
Detalhamento: As arquiteturas PowerPC, SPARC64 e AMD SSE5 oferecem uma única instrução que realiza multiplicação e adição sobre três registradores: a = a + (b × c). Obviamente, essa instrução permite um desempenho de ponto flutuante potencialmente mais alto para essa operação comum. Igualmente importante é que, em vez de realizar dois arredondamentos — depois da multiplicação e após a adição — que aconteceria com instruções separadas, a instrução de multiplicação adição pode realizar um único arredondamento após a adição, o que aumenta a precisão da multiplicação adição. Essas operações com um único arredondamento são chamadas multiplicação adição fundida. Isso foi acrescentado no IEEE 754 revisado (veja Seção 3.10 no site).
Resumo A próxima seção “Colocando em perspectiva” reforça o conceito de programa armazenado do Capítulo 2; o significado da informação não pode ser determinado simplesmente examinando-se os bits, pois os mesmos bits podem representar uma série de objetos. Esta seção mostra que a aritmética computacional é finita e, assim, pode não combinar com a aritmética natural. Por exemplo, a representação de ponto flutuante do padrão IEEE 754 (−1)S × (1 + Fração) × 2(Expoente−Bias) é quase sempre uma aproximação do número real. Os sistemas computacionais precisam ter o cuidado de minimizar essa lacuna entre a aritmética computacional e a aritmética no mundo real, e os programadores às vezes precisam estar cientes das implicações dessa aproximação.
Padrões de bits não possuem significado inerente. Eles podem representar inteiros com sinal, inteiros sem sinal, números de ponto flutuante, instruções e assim por diante. O que é representado depende da instrução que opera sobre os bits na palavra. A principal diferença entre os números no computador e os números no mundo real é que os números no computador possuem tamanho limitado e, por isso, uma precisão limitada; é possível calcular um número muito grande ou muito pequeno para ser representado em uma palavra. Os programadores precisam se lembrar desses limites e escrever programas de acordo.
em
Colocando perspectiva
218
Capítulo 3 Aritmética Computacional
Tipo C
Tipo Java
Transferências de dados
Operações
int
int
lw, sw, lui addu, addiu, subu, mult, div, AND, ANDi, OR, ORi, NOR, slt, slti
unsigned int
—
lw, sw, lui addu, addiu, subu, mult, divu, AND, ANDi, OR, ORi, NOR, sltu, sltiu
char
—
lb, sb, lui add, addi, sub, mult, div, AND, ANDi, OR, ORi, NOR, slt, slti
—
char
lh, sh, lui addu, addiu, subu, multu, divu, AND, ANDi, OR, ORi, NOR, sltu, sltiu
float
float
lwc1, swc1
add.s, sub.s, mult.s, div.s, c.eq.s, c.lt.s, c.le.s
double
double
l.d, s.d
add.d, sub.d, mult.d, div.d, c.eq.d, c.lt.d, c.le.d
Interface hardware/ software
No capítulo anterior, apresentamos as classes de armazenamento da linguagem de programação C (veja a seção Interface Hardware/Software da Seção 2.7). A tabela anterior mostra alguns dos tipos de dados C e Java junto com as instruções de transferência de dados MIPS e instruções que operam sobre aqueles tipos que aparecem aqui e no Capítulo 2. Observe que Java omite inteiros sem sinal.
Verifique você mesmo
Suponha que houvesse um formato de ponto flutuante IEEE 754 de 16 bits com 5 bits de expoente. Qual seria o intervalo provável de números que ele poderia representar? 1. 1,0000 0000 00 × 20 a 1,1111 1111 11 × 231, 0 2. ±1,0000 0000 0 × 2-14 a ±1.1111 1111 1 × 215, ±0, ±∞, NaN 3. ±1,0000 0000 00 × 2-14 a ±1.1111 1111 11 × 215, ±0, ±∞, NaN 4. ±1,0000 0000 00 × 2-15 a ±1.1111 1111 11 × 214, ±0, ±∞, NaN Detalhamento: Para acomodar comparações que possam incluir NaNs, o padrão inclui ordenada e desordenada como opções para comparações. Logo, o conjunto de instruções MIPS inteiro possui muitos tipos de comparações para dar suporte a NaNs. (Java não admite comparações não ordenadas.) Em uma tentativa de espremer cada bit de precisão de uma operação de ponto flutuante, o padrão permite que alguns números sejam representados em forma não normalizada. Em vez de ter uma lacuna entre 0 e o menor número normalizado, o IEEE permite números não normalizados (também conhecidos como denorms ou subnormals). Eles têm o mesmo expoente que zero, mas um significando diferente de zero. Eles permitem que um número diminua no significado até se tornar 0, chamado underflow gradual. Por exemplo, o menor número normalizado positivo de precisão simples é
1,00000000000000000000000 bin × 2−126 mas o menor número não normalizado de precisão simples é
0,00000000000000000000001bin × 2−126 ,ou1,0 bin × 2−149 Para a precisão dupla, a lacuna denorm vai de 1,0 × 2-1022 a 1,0 × 2-1074. A possibilidade de um operando ocasional não normalizado tem dado dores de cabeça aos projetistas de ponto flutuante que estejam tentando criar unidades de ponto flutuante velozes. Logo, muitos computadores causam uma exceção se um operando for não normalizado, permitindo que o software complete a operação. Embora as implementações de software sejam perfeitamente válidas, seu menor desempenho diminuiu a popularidade dos denorms no software de ponto flutuante portável. Além disso, se os programadores não esperarem os denorms, seus programas poderão ser surpreendidos.
3.6 Paralelismo e aritmética computacional: associatividade 219
aralelismo e aritmética computacional: P 3.6 associatividade Os programas normalmente têm sido escritos primeiro para executarem sequencialmente antes de simultaneamente, de modo que uma pergunta natural é “as duas versões geram a mesma resposta?”. Se a resposta for não, você pode considerar que existe um defeito na versão paralela, que precisa ser localizado. Essa técnica considera que a aritmética do computador não afeta os resultados quando passa de sequencial para paralelo. Ou seja, se você tivesse de somar um milhão de números, obteria os mesmos resultados usando 1 processador ou 1.000 processadores. Essa suposição continua para inteiros no complemento de dois, mesmo que o cálculo estoure. Outro modo de dizer isso é que a adição de inteiros é associativa. Infelizmente, como os números de ponto flutuante são aproximações dos números reais, e como a aritmética computacional tem precisão limitada, isso não é verdade para os números de ponto flutuante. Ou seja, a adição de ponto flutuante não é associativa.
Testando a associatividade da adição de ponto flutuante
Veja se x + (y + z ) = (x + y) + z. Por exemplo, suponha que x = -1,5dec × 1038, y = 1,5dec × 1038, e z = 1,0, e que todos estes sejam números de precisão simples.
EXEMPLO
Dada a grande faixa de números que podem ser representados em ponto flutuante, ocorrem problemas quando se somam dois números grandes de sinais opostos, mais um número pequeno, conforme veremos:
RESPOSTA
x + ( y + z) = = (x + y) + z = = =
−1,5dec × 1038 + (1,5dec × 1038 + 1,0) −1,5dec × 1038 + (1,5dec × 1038 ) = 0,0 (−1,5dec × 1038 + 1,5dec × 1038 ) + 1,0 (0,0dec ) + 1,0 1,0
Portanto, x + (y + z) ≠ (x + y) + z, de modo que a adição de ponto flutuante não é associativa. Como os números de ponto flutuante possuem precisão limitada e resultam em aproximações dos resultados reais, 1,5dec × 1038 é tão maior que 1,0dec que 1,5dec × 1038 + 1,0 ainda é 1,5dec × 1038. É por isso que a soma de x, y e z é 0,0 ou 1,0, dependendo da ordem das adições de ponto flutuante, e, portanto, a adição de ponto flutuante não é associativa. Uma versão mais irritante dessa armadilha ocorre em um computador paralelo, em que o escalonador do sistema operacional pode usar um número diferente de processadores, dependendo do que outros programas estão executando em um computador paralelo. O programador paralelo desavisado pode se confundir com seu programa obtendo respostas ligeiramente diferentes toda vez que for executada exatamente com o mesmo código e entrada idêntica, pois o número variável de processadores em cada execução faria com que as somas de ponto flutuante fossem calculadas em diferentes ordens. Por causa desse dilema, os programadores que escrevem código paralelo com números de ponto flutuante precisam verificar se os resultados são confiáveis, mesmo que não deem exatamente a mesma resposta que o código sequencial. O campo que lida com essas questões é a análise numérica, abordada em diversos livros-texto voltados para esse assunto. Esses problemas são um motivo para a popularidade das bibliotecas numéricas, como LAPACK e SCALAPAK, que foram validadas em suas formas sequencial e paralela.
220
Capítulo 3 Aritmética Computacional
Detalhamento: Uma versão sutil do problema de associatividade ocorre quando dois processadores realizam um cálculo redundante que é executado em ordem diferente, de modo que eles recebem respostas ligeiramente diferentes, embora as duas respostas sejam consideradas precisas. O problema ocorre se um desvio condicional compara com um número de ponto flutuante e os dois processadores seguem caminhos diferentes quando o bom senso sugere que eles deveriam seguir o mesmo caminho.
3.7 Vida real: ponto flutuante no x86 A arquitetura x86 possui instruções regulares de multiplicação e divisão que operam inteiramente sobre os registradores normais, em vez de contar com Hi e Lo separados como no MIPS. (Na verdade, as versões posteriores do conjunto de instruções MIPS incluíram instruções semelhantes.) As diferenças principais são encontradas nas instruções de ponto flutuante. A arquitetura de ponto flutuante x86 é diferente de todos os outros computadores no mundo.
A arquitetura de ponto flutuante do x86 O coprocessador de ponto flutuante Intel 8087 foi anunciado em 1980. Essa arquitetura estendeu o 8086 com cerca de 60 instruções de ponto flutuante. A Intel proveu uma arquitetura de pilha com suas instruções de ponto flutuante: loads inserem números na pilha, operações encontram operandos nos dois elementos do topo da pilha e stores podem retirar elementos da pilha. A Intel complementou essa arquitetura de pilha com instruções e modos de endereçamento que permitem que a arquitetura tenha alguns dos benefícios do modelo registrador-memória. Além de localizar operandos nos dois elementos do topo da pilha, um operando pode estar na memória ou em um dos sete registradores do chip, abaixo do topo da pilha. Assim, um conjunto completo de instruções de pilha é complementado por um conjunto limitado de instruções registrador-memória. Essa mistura é ainda um modelo registrador-memória restrito, pois os loads sempre movem dados para o topo da pilha enquanto incrementam o ponteiro do topo da pilha, e os stores só podem mover do topo da pilha para a memória. A Intel usa a notação ST para indicar o topo da pilha, e ST(i) para representar o i-ésimo registrador abaixo do topo da pilha. Outro novo recurso dessa arquitetura é que os operandos são mais largos na pilha de registradores do que são armazenados na memória e todas as operações são realizadas nessa precisão interna larga. Ao contrário do máximo de 64 bits no MIPS, os operandos de ponto flutuante x86 na pilha possuem 80 bits de largura. Os números são convertidos automaticamente para o formato interno de 80 bits em um load e convertidos de volta para o tamanho apropriado em um store. Essa precisão dupla estendida não é aceita pelas linguagens de programação, embora tenha sido útil aos programadores de software matemático. Os dados da memória podem ser números de ponto flutuante de 32 bits (precisão simples) ou de 64 bits (precisão dupla). Antes de realizar a operação, a versão registrador-memória dessas instruções converterá o operando da memória para esse formato de 80 bits da Intel. As instruções de transferência de dados também converterão automaticamente inteiros de 16 e 32 bits para ponto flutuante, e vice-versa, para loads e stores de inteiros. As operações de ponto flutuante x86 podem ser divididas em quatro classes principais: 1. Instruções de movimentação de dados, incluindo load, load de constante e store 2. Instruções aritméticas, incluindo adição, subtração, multiplicação, divisão, raiz quadrada e módulo absoluto 3. Comparação, incluindo instruções para enviar o resultado ao processador de inteiros de modo que possa se desviar 4. Instruções transcendentais, incluindo seno, cosseno, logaritmo e exponenciação
3.7 Vida real: ponto flutuante no x86 221
A Figura 3.20 mostra algumas das 60 operações de ponto flutuante. Observe que obtemos ainda mais combinações quando incluímos os modos de operando para essas operações. A Figura 3.21 mostra as muitas opções para a adição de ponto flutuante. As instruções de ponto flutuante são codificadas por meio do opcode ESC do 8086 e o especificador de endereço pós-byte (veja Figura 2.47). As operações de memória reservam 2 bits para decidir se o operando é um ponto flutuante de 32 ou de 64 bits, ou um inteiro de 16 ou 32 bits. Esses mesmos 2 bits são usados em versões que não acessam a memória para decidir se o topo da pilha deve ser removido após a operação e se o topo da pilha ou um registrador inferior deve obter o resultado. O desempenho de ponto flutuante da família x86 tradicionalmente tem ficado atrás de outros computadores. Como resultado, a Intel criou uma arquitetura de ponto flutuante mais tradicional como parte do SSE2.
A arquitetura de ponto flutuante Streaming SIMD Extension 2 (SSE2) da Intel O Capítulo 2 observa que, em 2001, a Intel acrescentou 144 instruções à sua arquitetura, incluindo registradores e operações de ponto flutuante com precisão dupla. Isso inclui oito registradores de 64 bits que podem ser usados como operandos de ponto flutuante, dando ao compilador um alvo diferente para as operações de ponto flutuante do que a arquitetura de pilha exclusiva. Os compiladores podem decidir usar os oito registradores SSE2 como
FIGURA 3.20 As instruções de ponto flutuante do x86. Usamos as chaves { } para mostrar variações opcionais das operações básicas: {I} significa que existe uma versão inteira da instrução; {P} significa que essa variação retirará um operando da pilha após a operação; e {R} significa o reverso da ordem dos operandos nessa operação. A primeira coluna mostra as instruções de transferência de dados, que movem dados para a memória ou para um dos registradores abaixo do topo da pilha. As três últimas operações na primeira coluna colocam constantes na pilha: pi, 1,0 e 0,0. A segunda coluna contém as operações aritméticas descritas anteriormente. Observe que as três últimas operam apenas no topo da pilha. A terceira coluna contém as instruções de comparação. Como não existem instruções de desvio de ponto flutuante especiais, o resultado da comparação precisa ser transformado para a CPU de inteiros via instruções FSTSW, seja para o registrador AX ou para a memória, seguida por uma instrução SAHF a fim de definir os códigos de condição. A comparação de ponto flutuante pode, então, ser testada por meio de instruções de desvio inteiras. A última coluna oferece as operações de ponto flutuante de mais alto nível. Nem todas as combinações sugeridas pela notação são fornecidas. Logo, operações F{I}SUB{R}{P} representam estas instruções encontradas no x86: FSUB, FISUB, FSUBR, FI SUBR, FSUBP, FSUBRP. Para as instruções de subtração de inteiros, não existe um pop (FI SUBP) ou um pop reverso (FISUBRP).
FIGURA 3.21 As variações dos operandos para adição de ponto flutuante na arquitetura x86.
222
Capítulo 3 Aritmética Computacional
FIGURA 3.22 As instruções de ponto flutuante SSE/SSE2 do x86. xmm significa que um operando é um registrador SSE2 de 128 bits, e mem/xmm significa que o outro operando está na memória ou é um registrador SSE2. Usamos as chaves { } para mostrar variações opcionais das operações básicas: {SS} significa ponto flutuante de precisão Scalar Single, ou quatro operandos de 32 bits em um registrador de 128 bits; {SD} significa ponto flutuante de precisão Scalar Double, ou um operando de 64 bits em um registrador de 128 bits; {PD} significa ponto flutuante de precisão Packed Double, ou dois operandos de 64 bits em um registrador de 128 bits; {A} significa que o operando de 128 bits é alinhado na memória; {U} significa que o operando de 128 bits é desalinhado na memória; {H} significa mover a metade alta (high) do operando de 128 bits; e {L} significa mover a metade baixa (low) do operando de 128 bits.
registradores de ponto flutuante, como aqueles encontrados em outros computadores. A AMD expandiu o número para 16, como parte do AMD64, que a Intel passou a chamar de EM64T para seu uso. A Figura 3.22 resume as instruções SSE e SSE2. Além de manter um número de precisão simples ou de precisão dupla em um registrador, a Intel permite que vários operandos de ponto flutuante sejam encaixados em um único registrador SSE2 de 128 bits: quatro de precisão simples e dois de precisão dupla. Se os operandos podem ser organizados na memória como dados alinhados em 128 bits, então as transferências de dados de 128 bits podem carregar e armazenar vários operandos por instrução. Esse formato de ponto flutuante compactado é aceito por operações aritméticas que podem operar simultaneamente sobre quatro números de precisão simples (PS) ou dois de precisão dupla (PD). Essa arquitetura pode mais do que dobrar o desempenho em relação à arquitetura de pilha. Assim, a matemática pode ser definida como o assunto em que nunca sabemos do que estamos falando, nem se o que estamos dizendo é verdadeiro. Bertrand Russell, Recent Words on the Principles of Mathematics, 1901
3.8 Falácias e armadilhas As falácias e armadilhas aritméticas geralmente advêm da diferença entre a precisão limitada da aritmética computacional e da precisão ilimitada da aritmética natural. Falácia: assim como a instrução de deslocamento à esquerda pode substituir uma multiplicação de inteiros por uma potência de 2, um deslocamento à direita é o mesmo que uma divisão de inteiros por uma potência de 2. Lembre-se de que um número binário x, em que xi significa o bit na posição i, representa o número …+ (x 3 × 23 ) + (x 2 × 22 ) + (x1 × 21 ) + (x 0 × 20 ) Deslocar os bits de x para a direita de n bits pareceria ser o mesmo que dividir por 2n. E isso é verdade para inteiros sem sinal. O problema é com os inteiros com sinal. Por exemplo, suponha que queremos dividir –5dec por 4dec; o quociente seria –1dec. A representação no complemento de dois para –5dec é 1111 1111 1111 1111 1111 1111 1111 1011bin De acordo com essa falácia, deslocar para a direita por dois deverá dividir por 4dec (22): 0011 1111 1111 1111 1111 1111 1111 1110 bin
3.8 Falácias e armadilhas 223
Com um 0 no bit de sinal, esse resultado claramente está errado. O valor criado pelo deslocamento à direita é, na realidade, 1.073.741.822dec, e não –1dec. Uma solução seria ter um deslocamento aritmético à direita, que estende o bit de sinal, em vez de colocar 0s à esquerda num deslocamento à direita. Um deslocamento aritmético de 2 bits para a direita de –5dec produz 1111 1111 1111 1111 1111 1111 1111 1110 bin O resultado é –2dec, em vez de –1dec; próximo, mas não podemos comemorar. Armadilha: a instrução MIPS add immediate unsigned (addiu) estende o sinal de seu campo imediato de 16 bits. Apesar de seu nome, add immediate unsigned (addiu) é usada para somar constantes a inteiros com sinal quando não nos importamos com o overflow. O MIPS não possui uma instrução de subtração imediata e os números negativos precisam de extensão de sinal, de modo que os arquitetos do MIPS decidiram estender o sinal do campo imediato. Falácia: somente os matemáticos teóricos se importam com a precisão do ponto flutuante. As manchetes dos jornais de novembro de 1994 provam que essa afirmação é uma falácia (veja Figura 3.23). A seguir, está a história por trás das manchetes. O Pentium usa um algoritmo de divisão de ponto flutuante padrão, que gera bits de quociente múltiplos por etapa, usando os bits mais significativos do divisor e do dividendo para descobrir os 2 bits seguintes do quociente. A escolha vem de uma tabela de pesquisa contendo –2, –1, 0, +1 ou +2. A escolha é multiplicada pelo divisor e subtraída do resto a fim de gerar um novo resto. Assim como a divisão sem restauração, se uma escolha anterior obtiver um resto muito grande, o resto parcial é ajustado em uma passada subsequente. Evidentemente, havia cinco elementos da tabela do 80486 que a Intel pensou que nunca poderiam ser acessados, e eles otimizaram a PLA para retornar 0 no lugar de 2 nessas situa-
FIGURA 3.23 Uma amostra dos artigos de jornais e revistas de novembro de 1994, incluindo New York Times, San Jose Mercury News, San Francisco Chronicle e Infoworld. O bug da divisão de ponto flutuante do Pentium chegou até mesmo à “Lista dos 10 mais” do David Letterman Late Show na televisão. A Intel acabou tendo um custo de US$300 milhões para substituir os chips com defeito.
224
Capítulo 3 Aritmética Computacional
ções no Pentium. A Intel estava errada: embora os 11 primeiros bits sempre fossem corretos, erros apareceriam ocasionalmente nos bits de 12 a 52, ou do 4o ao 15o dígito decimal. A seguir está um roteiro dos fatos que aconteceram referentes ao bug do Pentium: j
Julho de 1994: a Intel descobre o bug no Pentium. O custo real para consertar o bug foi de várias centenas de milhares de dólares. Após os procedimentos normais de reparo do bug, levariam meses para fazer a mudança, verificar novamente e colocar o chip corrigido em produção. A Intel planejou colocar os chips bons em produção em janeiro de 1995, estimando que 3 a 5 milhões de Pentiums seriam produzidos com o bug.
j
Setembro de 1994: um professor de matemática no Lynchburg College, na Virgínia, Thomas Nicely, descobre o bug. Depois de ligar para o suporte técnico da Intel e não receber uma posição oficial, ele posta sua descoberta na Internet. Rapidamente surgiram os seguidores e alguns apontaram que até mesmo erros pequenos se tornam grandes ao multiplicar por grandes números: a fração de pessoas com uma doença rara vezes a população da Europa, por exemplo, poderia levar a uma estimativa errada do número de pessoas doentes.
j
7 de novembro de 1994: o Electronic Engineering Times coloca a matéria em sua capa, que logo foi seguido por outros jornais.
j
22 de novembro de 1994: a Intel emite um comunicado oficial, chamando-o de “glitch”. O Pentium “pode cometer erros no nono dígito. … Até mesmo a maioria dos engenheiros e analistas financeiros exige precisão apenas até a quarta ou quinta casa decimal. Usuários de planilhas eletrônicas e processadores de textos não precisam se preocupar. … Talvez haja algumas dezenas de pessoas a quem isso afetaria. Até aqui, só ouvimos falar de uma. … [Somente] matemáticos teóricos (com computadores Pentium comprados antes do verão) devem se preocupar”. O que aborreceu a muitos foi que os clientes foram solicitados a descrever sua aplicação à Intel, e depois a Intel decidiria se sua aplicação mereceria ou não um novo Pentium sem um bug de divisão.
j
5 de dezembro de 1994: a Intel afirma que a falha acontece uma vez em 27.000 anos para o usuário típico de planilha. A Intel considera que um usuário realiza 1.000 divisões por dia e multiplica a taxa de erro supondo que os números de ponto flutuante são aleatórios, o que é um em 9 bilhões, e depois apanha 9 milhões de dias, ou 27.000 anos. As coisas começam a acalmar, apesar de a Intel ter deixado de explicar por que um cliente comum acessaria números de ponto flutuante aleatoriamente.
j
12 de dezembro de 1994: a IBM Research Division discute o cálculo da Intel quanto à taxa de erros (você pode acessar esse artigo visitando www.mkp.com/books_catalog/cod/ links.htm). A IBM afirma que os programas comuns de planilha, calculando por 15 minutos por dia, poderiam produzir erros relacionados ao bug do Pentium com tanta frequência quanto uma vez a cada 24 dias. A IBM considera 5.000 divisões por segundo, por 15 minutos, gerando 4,2 milhões de divisões por dia, e não considera a distribuição aleatória de números, calculando em vez disso as chances como uma em 100 milhões. Como resultado, a IBM imediatamente deixa de enviar todos os computadores pessoais IBM baseados no Pentium. As coisas se aquecem novamente para a Intel.
j
21 de dezembro de 1994: a Intel lança o seguinte comunicado, assinado pelo presidente da Intel, pelo diretor executivo, pelo diretor de operações e pelo presidente do comitê: “Nós, da Intel, queremos sinceramente pedir desculpas por nosso tratamento da falha recentemente publicada do processador Pentium. O símbolo Intel Inside significa que seu computador possui um microprocessador que não fica atrás de nenhum outro em qualidade e desempenho. Milhares de funcionários da Intel trabalham muito para garantir que isso aconteça. Mas nenhum microprocessador é totalmente perfeito. O que a Intel continua a acreditar é que, tecnicamente, um problema extremamente pequeno assumiu vida própria. Embora a Intel mantenha
3.8 Falácias e armadilhas 225
a qualidade da versão atual do processador Pentium, reconhecemos que muitos usuários possuem problemas. Queremos resolvê-los. A Intel trocará a versão atual do processador Pentium por uma versão atualizada, em que essa falha de divisão de ponto flutuante está corrigida, para qualquer proprietário que o solicite, sem qualquer custo, durante toda a vida de seu computador”. Os analistas estimam que essa troca custou à Intel cerca de US$500 milhões, e os funcionários da Intel não receberam um bônus de Natal naquele ano. Essa história nos faz refletir sobre alguns pontos. Seria mais econômico ter consertado o bug em julho de 1994? Qual foi o custo para reparar o dano causado à reputação da Intel? E qual é a responsabilidade corporativa na divulgação de bugs em um produto tão utilizado e confiado como um microprocessador? Em abril de 1997, outro bug de ponto flutuante foi revelado nos microprocessadores Pentium Pro e Pentium II. Quando as instruções store de ponto flutuante para inteiro (fist, fistp) encontram um número de ponto flutuante negativo que seja muito grande para caber em uma word de 16 ou 32 bits sendo convertida para inteiro, elas definem o bit errado na palavra de status FPO (exceção de precisão, no lugar de exceção por operação inválida). Para o crédito da Intel, dessa vez, eles reconheceram publicamente o bug e ofereceram um reparo de software para contorná-lo – uma reação muito diferente da que aconteceu em 1994.
FIGURA 3.24 O conjunto de instruções MIPS. Este livro se concentra nas instruções da coluna da esquerda. Essa informação também se encontra nas colunas 1 e 2 do Guia de Instrução Rápida do MIPS no início deste livro.
226
Capítulo 3 Aritmética Computacional
3.9 Comentários finais Um efeito colateral do computador com programa armazenado é que os padrões de bits não possuem significado inerente. O mesmo padrão de bits pode representar um inteiro com sinal, um inteiro sem sinal, um número de ponto flutuante, uma instrução e assim por diante. É a instrução que opera sobre os bits que determina seu significado. A aritmética computacional é distinguida da aritmética de lápis e papel pelas restrições da precisão limitada. Esse limite pode resultar em operações inválidas, por meio do cálculo de números maiores ou menores do que os limites predefinidos. Essas anomalias, chamadas “overflow” ou “underflow”, podem resultar em exceções ou interrupções, eventos de emergência, semelhantes a chamadas de sub-rotina não planejadas. O Capítulo 4 discute as exceções com mais detalhes. A aritmética de ponto flutuante tem o desafio adicional de ser uma aproximação de números reais e é preciso tomar cuidado para garantir que o número selecionado pelo computador seja a representação mais próxima do número real. Os desafios da imprecisão e da representação limitada fazem parte da inspiração para o campo da análise numérica. A recente passagem para o paralelismo acenderá a tocha na análise numérica novamente, à medida que soluções que eram consideradas seguras nos computadores sequenciais precisam ser reconsideradas quando se tenta encontrar o algoritmo mais rápido para computadores paralelos, que ainda alcance um resultado correto. Com o passar dos anos, a aritmética computacional tornou-se padronizada, aumentando bastante a portabilidade dos programas. A aritmética de inteiros binários com complemento de dois e a aritmética de ponto flutuante binário do padrão IEEE 754 são encontradas na grande maioria dos computadores vendidos hoje. Por exemplo, cada computador desktop vendido desde que este livro foi impresso pela primeira vez segue essas convenções. Com a explicação sobre aritmética computacional deste capítulo vem uma descrição de muito mais do conjunto de instruções do MIPS. Uma questão que gera confusão são as instruções explicadas neste capítulo versus as instruções executadas pelos chips MIPS versus as instruções aceitas pelos montadores MIPS. As duas figuras seguintes tentam esclarecer isso. A Figura 3.24 lista as instruções MIPS abordadas neste capítulo e no Capítulo 2. Chamamos o conjunto de instruções da esquerda da figura de núcleo MIPS. As instruções à direita são chamadas núcleo aritmético MIPS. No lado esquerdo da Figura 3.25 estão as instruções que o processador MIPS executa que não se encontram na Figura 3.24. Chamamos o conjunto completo de instrução de hardware de MIPS-32. À direita da Figura 3.25 estão as instruções aceitas pelo montador, que não fazem parte do MIPS-32. Chamamos esse conjunto de instruções de PseudoMIPS. A Figura 3.26 indica a popularidade das instruções MIPS para os benchmarks de inteiro e de ponto flutuante SPEC2006. Todas as instruções listadas foram responsáveis por, pelo menos, 0,2% das instruções executadas. Observe que, embora os programadores e escritores de compilador possam utilizar MIPS-32 para ter um menu de opções mais rico, as instruções do núcleo MIPS dominam a execução SCPEC2006 de inteiros, e o núcleo de inteiros mais aritmético domina o ponto flutuante SPEC2006, como mostra a tabela a seguir. Inteiros
Pt. Flut.
Núcleo do MIPS
Subconjunto de instruções
98%
31%
Núcleo aritmético do MIPS
2%
66%
MIPS-32 restante
0%
3%
3.9 Comentários finais 227
FIGURA 3.25 Conjuntos de instruções MIPS-32 restantes e “pseudoMIPS”. f significa instruções de ponto flutuante com precisão simples (s) ou dupla (d) e s significa versões com sinal e sem sinal (u). MIPS-32 também possui instruções de PF para multiply e add/sub (madd.f/msub.f), ceiling (ceil.f), truncate (trunc.f), round (round.f) e reciprocal (recip.f). O sublinhado representa a letra a ser incluída para representar esse tipo de dados.
Para o restante do livro, vamos nos concentrar nas instruções do núcleo MIPS – o conjunto de instruções de inteiros, excluindo multiplicação e divisão – para facilitar a explicação do projeto do computador. Como podemos ver, o núcleo MIPS inclui as instruções MIPS mais comuns, e tenha certeza de que compreender um computador que execute o núcleo MIPS lhe dará base suficiente para entender computadores com projetos ainda mais ambiciosos.
228
Capítulo 3 Aritmética Computacional
FIGURA 3.26 Frequência das instruções MIPS para o benchmark de inteiros e ponto flutuante SPEC2000. Todas as instruções responsáveis por, pelo menos, 1% das instruções estão incluídas na tabela. As pseudoinstruções são convertidas em MIPS-32 antes da execução e, portanto, não aparecem aqui.
A Lei de Gresham (“dinheiro ruim expulsa o bom”) para os computadores diria: “o rápido expulsa o lento, mesmo que o rápido seja errado”. W. Kahan, 1992
Nunca ceda, nunca ceda, nunca, nunca, nunca – em nada, seja grande ou pequeno, importante ou insignificante – nunca ceda. Winston Churchill, discurso na Harrow School, 1941
3.10
Perspectiva histórica e leitura adicional
Esta seção estuda a história do ponto flutuante desde von Neumann, incluindo o esforço surpreendentemente controvertido dos padrões do IEEE, mais o raciocínio para a arquitetura de pilha de 80 bits para ponto flutuante do x86. Ver Seção 3.10 no site.
3.11
Exercícios
Exercício 3.1 O livro mostra como somar e subtrair números binários e decimais. Porém, outros sistemas de numeração também foram muito populares quando se tratavam de computadores. O sistema de numeração octal (base 8) foi um deles. A tabela a seguir mostra pares de números octais. A
B
a.
3174
0522
b.
4165
1654
3.11 Exercícios 229
3.1.1 [5] <3.2> Qual é a soma de A e B se eles representam números octais de 12 bits sem sinal? O resultado deverá ser escrito em octal. Mostre seu trabalho. 3.1.2 [5] <3.2> Qual é a soma de A e B se eles representam números octais de 12 bits com sinal armazenados em um formato de sinal e magnitude? O resultado deverá ser escrito em octal. Mostre seu trabalho. 3.1.3 [10] <3.2> Converta A em um número decimal, supondo que ele é sem sinal. Repita considerando que ele esteja armazenado em formato de sinal e magnitude. Mostre seu trabalho. A tabela a seguir também mostra pares de números octais. A
B
a.
7040
0444
b.
4365
3412
3.1.4 [5] <3.2> O que é A – B se eles representam números octais de 12 bits sem sinal? O resultado deverá ser escrito em octal. Mostre seu trabalho. 3.1.5 [5] <3.2> O que é A – B se eles representam números octais de 12 bits com sinal armazenados em formato de sinal e magnitude? O resultado deverá ser escrito em octal. Mostre seu trabalho. 3.1.6 [10] <3.2> Converta A em um número binário. O que torna a base 8 (octal) um sistema de numeração atraente para representar valores nos computadores?
Exercício 3.2 Hexadecimal (base 16) também é um sistema de numeração normalmente utilizado para representar valores nos computadores. Na verdade, ele se tornou muito mais comum que octal. A tabela a seguir mostra pares de números hexadecimais. A
B
a.
1446
672F
b.
2460
4935
3.2.1 [5] <3.2> Qual é a soma de A e B se eles representam números hexadecimais de 16 bits sem sinal? O resultado deverá ser escrito em hexadecimal. Mostre seu trabalho. 3.2.2 [5] <3.2> Qual é a soma de A e B se eles representam números hexadecimais de 16 bits com sinal, armazenados em formato de sinal e magnitude? O resultado deverá ser escrito em hexadecimal. Mostre seu trabalho. 3.2.3 [10] <3.2> Converta A para um número decimal, supondo que ele esteja sem sinal. Repita considerando que ele está armazenado em formato de sinal e magnitude. Mostre seu trabalho. A tabela a seguir também mostra pares de números hexadecimais. A
B
a.
C352
36AE
b.
5ED4
07A4
3.2.4 [5] <3.2> O que é A – B se eles representam números hexadecimais de 16 bits sem sinal? O resultado deverá ser escrito em hexadecimal. Mostre seu trabalho.
230
Capítulo 3 Aritmética Computacional
3.2.5 [5] <3.2> O que é A – B se eles representam números hexadecimais de 16 bits com sinal, armazenados em formato de sinal e magnitude? O resultado deverá ser escrito em hexadecimal. Mostre seu trabalho. 3.2.6 [10] <3.2> Converta A para um número binário. O que torna a base 16 (hexadecimal) um sistema de numeração atraente para representar valores em computadores?
Exercício 3.3 O overflow ocorre quando um resultado é muito grande para ser representado com precisão dado um tamanho de palavra finito. O underflow ocorre quando um número é muito pequeno para ser representado corretamente — um resultado negativo quando se realiza aritmética sem sinal, por exemplo. (O caso quando um resultado positivo é gerado pela adição de dois inteiros negativos também é considerado como underflow por muitos, mas neste livro isso é considerado overflow.) A tabela a seguir mostra pares de números decimais. A
B
a.
69
90
b.
102
44
3.3.1 [5] <3.2> Suponha que A e B sejam inteiros decimais de 8 bits sem sinal. Calcule A - B. Existe overflow, underflow o nenhum deles? 3.3.2 [5] <3.2> Suponha que A e B sejam inteiros decimais de 8 bits com sinal, armazenados em formato de magnitude de sinal. Calcule A + B. Existe overflow, underflow o nenhum deles? 3.3.3 [5] <3.2> Suponha que A e B sejam inteiros decimais de 8 bits com sinal, armazenados em formato de magnitude de sinal. Calcule A - B. Existe overflow, underflow ou nenhum deles? A tabela a seguir também mostra pares de números decimais. A
B
a.
200
103
b.
247
237
3.3.4 [10] <3.2> Suponha que A e B sejam inteiros decimais de 8 bits com sinal, armazenados no formato de complemento de dois. Calcule A + B usando a aritmética por saturação. O resultado deverá ser escrito em decimal. Mostre seu trabalho. 3.3.5 [10] <3.2> Suponha que A e B sejam inteiros decimais de 8 bits com sinal, armazenados no formato de complemento de dois. Calcule A - B usando a aritmética por saturação. O resultado deverá ser escrito em decimal. Mostre seu trabalho. 3.3.6 [10] <3.2> Suponha que A e B sejam inteiros de 8 bits sem sinal. Calcule A + B usando a aritmética por saturação. O resultado deverá ser escrito em decimal. Mostre seu trabalho.
Exercício 3.4 Vejamos a multiplicação com mais detalhes. Usaremos os números na tabela a seguir. A
B
a.
62
12
b.
35
26
3.11 Exercícios 231
3.4.1 [20] <3.3> Usando uma tabela semelhante à que mostramos na Figura 3.7, calcule o produto dos inteiros octais de 6 bits sem sinal A e B usando o hardware descrito na Figura 3.4. Você deverá mostrar o conteúdo de cada registrador em cada etapa. 3.4.2 [20] <3.3> Usando uma tabela semelhante à que mostramos na Figura 3.7, calcule o produto dos inteiros hexadecimais de 8 bits sem sinal A e B usando o hardware descrito na Figura 3.6. Você deverá mostrar o conteúdo de cada registrador em cada etapa. 3.4.3 [60] <3.3> Escreva um programa na linguagem assembly MIPS para calcular o produto dos inteiros A e B sem sinal, usando a técnica descrita na Figura 3.4. A tabela a seguir mostra pares de números octais. A
B
a.
41
33
b.
60
26
3.4.4 [30] <3.3> Ao multiplicar números com sinal, um modo de obter a resposta correta é converter o multiplicador e multiplicando para números positivos, salvar os sinais originais e depois ajustar o valor final de forma apropriada. Usando uma tabela semelhante à que mostramos na Figura 3.7, calcule o produto de A e B usando o hardware descrito na Figura 3.4. Você deverá mostrar o conteúdo de cada registrador em cada etapa, e incluir a etapa necessária para produzir o resultado sinalizado corretamente. Suponha que A e B estejam armazenados em formato de magnitude de sinal com 6 bits. 3.4.5 [30] <3.3> Ao deslocar um registrador um bit para a direita, existem várias maneiras de decidir qual será o novo bit entrando. Ele sempre pode ser um 0, ou sempre um 1, ou o bit entrando poderia ser aquele que está sendo empurrado pelo lado direito (transformando um deslocamento em uma rotação), ou o valor que já está no bit mais à esquerda pode simplesmente ser retido (chamado de deslocamento aritmético à direita, pois preserva o sinal do número que está sendo deslocado). Usando uma tabela semelhante à que mostramos na Figura 3.7, calcule o produto dos números em complemento de dois com 6 bits A e B usando o hardware descrito na Figura 3.6. Os deslocamentos à direita deverão ser feitos usando-se um deslocamento aritmético à direita. Observe que o algoritmo descrito no texto terá de ser modificado ligeiramente para que isso funcione — em particular, as coisas precisam ser feitas de forma diferente se o multiplicador for negativo. Você poderá encontrar detalhes pesquisando a Web. Mostre o conteúdo de cada registrador a cada etapa. 3.4.6 [60] <3.3> Escreva um programa em linguagem assembly MIPS para calcular o produto dos inteiros com sinal A e B. Indique se você está usando a técnica dada no Exercício 3.4.4 ou no Exercício 3.4.5.
Exercício 3.5 Por muitos motivos, gostaríamos de projetar multiplicadores que exijam menos tempo. Muitas técnicas diferentes foram utilizadas para se realizar esse objetivo. Na tabela a seguir, A representa a largura de um inteiro em bits, e B representa o número de unidades de tempo (ut) necessárias para realizar uma etapa de uma operação. A (largura em bits)
B (unidades de tempo)
a.
4
3 ut
b.
32
7 ut
3.5.1 [10] <3.3> Calcule o tempo necessário para realizar uma multiplicação usando a técnica dada nas Figuras 3.4 e 3.5 se um inteiro tiver A bits de largura e cada etapa da
232
Capítulo 3 Aritmética Computacional
operação exigir B unidades de tempo. Suponha que, na etapa 1a, uma adição sempre é realizada — ou o multiplicando será somado, ou então um 0 será somado. Suponha também que os registradores já foram inicializados (você está simplesmente contando quanto tempo é necessário para se realizar o próprio loop de multiplicação). Se isso estiver sendo feito no hardware, os deslocamentos do multiplicando e do multiplicador podem ser feitos simultaneamente. Se isso estiver sendo feito no software, eles terão de ser feitos um após o outro. Solucione para cada caso. 3.5.2 [10] <3.3> Calcule o tempo necessário para realizar uma multiplicação usando a técnica descrita no texto (31 somadores empilhados verticalmente) se um inteiro tiver A bits de largura e um somador exigir B unidades de tempo. 3.5.3 [20] <3.3> Calcule o tempo necessário para realizar uma multiplicação usando a técnica dada na Figura 3.8, se um inteiro tiver A bits de largura e um somador exigir B unidades de tempo.
Exercício 3.6 Neste exercício, veremos algumas das outras maneiras de melhorar o desempenho da multiplicação, com base principalmente em realizar mais deslocamentos e menos operações aritméticas. A tabela a seguir mostra pares de números hexadecimais. A
B
a.
33
55
b.
8a
6d
3.6.1 [20] <3.3> Conforme discutimos no texto, uma melhoria possível é realizar um deslocamento e soma em vez de uma multiplicação real. Como 9 × 6, por exemplo, pode ser escrito como (2 × 2 × 2 + 1) × 6, podemos calcular 9 × 6 deslocando 6 para a esquerda três vezes e depois somando 6 a esse resultado. Mostre a melhor maneira de calcular A × B usando deslocamentos e adições/subtrações. Suponha que A e B sejam inteiros de 8 bits sem sinal. 3.6.2 [20] <3.3> Mostre a melhor maneira de calcular A × B usando deslocamento e somas, se A e B forem inteiros de 8 bits com sinal armazenados em formato de magnitude de sinal. 3.6.3 [60] <3.3> Escreva um programa em linguagem assembly MIPS que realize uma multiplicação de inteiros com sinal usando deslocamento e somas, usando o enfoque descrito em 3.6.1. A tabela a seguir mostra outros pares de números hexadecimais. A
B
a.
F6
7F
b.
08
55
3.6.4 [30] <3.3> O algoritmo de Booth é outra técnica que reduz o número de operações aritméticas necessárias para realizar uma multiplicação. Esse algoritmo já existe há muitos anos e envolve identificar ciclos de 1s e 0s e realizar apenas deslocamentos durante os ciclos, ao invés de deslocamentos e adições. Ache uma descrição do algoritmo na internet e explique, com detalhes, como ele funciona. 3.6.5 [30] <3.3> Mostre o resultado passo a passo da multiplicação de A e B, usando o algoritmo de Booth. Suponha que A e B sejam inteiros de 8 bits em complemento de dois, armazenados em formato hexadecimal.
3.11 Exercícios 233
3.6.6 [60] <3.3> Escreva um programa em linguagem assembly MIPS para realizar a multiplicação de A e B usando o algoritmo de Booth.
Exercício 3.7 Vejamos a divisão com maiores detalhes. Usaremos os números octais da tabela a seguir. A
B
a.
74
21
b.
76
52
3.7.1 [20] <3.4> Usando uma tabela semelhante à que mostramos na Figura 3.11, calcule A dividido por B usando o hardware descrito na Figura 3.9. Você deverá mostrar o conteúdo de cada registrador em cada etapa. Suponha que A e B sejam inteiros de 6 bits sem sinal. 3.7.2 [30] <3.4> Usando uma tabela semelhante à que mostramos na Figura 3.11, calcule A dividido por B usando o hardware descrito na Figura 3.12. Você deverá mostrar o conteúdo de cada registrador em cada etapa. Suponha que A e B sejam inteiros de 6 bits sem sinal. Este algoritmo requer uma técnica ligeiramente diferente daquela mostrada na Figura 3.10. Você deverá pensar bem nisso, realizar um experimento ou dois, ou então vá à Web descobrir como fazer isso funcionar corretamente. (Dica: uma solução possível envolve o uso do fato de que a Figura 3.12 implica que o registrador de resto pode ser deslocado em qualquer direção.) 3.7.3 [60] <3.4> Escreva um programa em linguagem assembly MIPS para calcular A dividido por B, usando a técnica descrita na Figura 3.9. Suponha que A e B sejam inteiros de 6 bits sem sinal. A tabela a seguir mostra outros pares de números octais. A
B
a.
72
07
b.
75
47
3.7.4 [30] <3.4> Usando uma tabela semelhante à que mostramos na Figura 3.11, calcule A dividido por B usando o hardware descrito na Figura 3.9. Você deverá mostrar o conteúdo de cada registrador em cada etapa. Suponha que A e B sejam inteiros de 6 bits com sinal em formato de magnitude de sinal. Não se esqueça de incluir como você está calculando os sinais do quociente e resto. 3.7.5 [30] <3.4> Usando uma tabela semelhante à que mostramos na Figura 3.11, calcule A dividido por B usando o hardware descrito na Figura 3.12. Você deverá mostrar o conteúdo de cada registrador em cada etapa. Suponha que A e B sejam inteiros de 6 bits com sinal em formato de magnitude de sinal. Não se esqueça de incluir como você está calculando os sinais do quociente e do resto. 3.7.6 [60] <3.4> Escreva um programa na linguagem assembly MIPS para calcular A dividido por B, usando a técnica descrita na Figura 3.12. Suponha que A e B sejam inteiros com sinal.
Exercício 3.8 A Figura 3.10 descreve um algoritmo de divisão com restauração, pois quando subtrai o divisor do resto produz um resultado negativo, o divisor é somado de volta ao resto (restaurando, assim, o valor). Porém, existem outros algoritmos que foram desenvolvidos para eliminar a adição extra. Muitas referências a esses algoritmos são facilmente encontradas na Web. Exploraremos esses algoritmos usando os pares de números octais na tabela a seguir.
234
Capítulo 3 Aritmética Computacional
A
B
a.
26
05
b.
37
15
3.8.1 [30] <3.4> Usando uma tabela semelhante àquela mostrada na Figura 3.11, calcule A dividido por B usando a divisão sem restauração. Você deverá mostrar o conteúdo de cada registrador a cada etapa. Suponha que A e B sejam inteiros de 6 bits sem sinal. 3.8.2 [60] <3.4> Escreva um programa em linguagem assembly MIPS para calcular A dividido por B usando a divisão sem restauração. Suponha que A e B sejam inteiros de 6 bits com sinal (complemento de dois). 3.8.3 [60] <3.4> Compare o desempenho da divisão com e sem restauração. Demonstre exibindo o número de etapas necessárias para calcular A dividido por B usando cada método. Suponha que A e B sejam inteiros de 6 bits com sinal (magnitude de sinal). Você também pode escrever um programa para realizar as divisões com e sem restauração. A tabela a seguir mostra outros pares de números octais. A
B
a.
27
06
b.
54
12
3.8.4 [30] <3.4> Usando uma tabela semelhante à que mostramos na Figura 3.11, calcule A dividido por B usando a divisão nonperforming. Você deverá mostrar o conteúdo de cada registrador a cada etapa. Suponha que A e B sejam inteiros de 6 bits sem sinal. 3.8.5 [60] <3.4> Escreva um programa em linguagem assembly MIPS para calcular A dividido por B usando a divisão nonperforming. Suponha que A e B sejam inteiros de 3 bits com sinal em complemento de dois. 3.8.6 [60] <3.4> Como o desempenho da divisão nonrestoring e nonperforming se comparam? Demonstre exibindo o número de etapas necessárias para calcular A dividido por B usando cada método. Suponha que A e B sejam inteiros de 6 bits com sinal, armazenados em formato de magnitude de sinal. Você também pode escrever um programa para realizar as divisões nonperforming e nonrestoring.
Exercício 3.9 A divisão é tão demorada e difícil que o guia do CRAY T3E Fortran Optimization afirma: “A melhor estratégia para divisão é evitá-la sempre que for possível.” Este exercício examina as diferentes estratégias para realizar divisões. a.
Divisão sem restauração
b.
Divisão por multiplicação recíproca
3.9.1 [30] <3.4> Descreva o algoritmo com detalhes. 3.9.2 [60] <3.4> Use um fluxograma (ou um trecho de código de alto nível) para descrever como o algoritmo funciona. 3.9.3 [60] <3.4> Escreva um programa em linguagem assembly MIPS para realizar uma divisão usando o algoritmo.
3.11 Exercícios 235
Exercício 3.10 Em uma arquitetura de Von Neumann, grupos de bits não possuem significados intrínsecos por si próprios. O que um padrão de bits representa depende totalmente de como ele é utilizado. A tabela a seguir mostra os padrões de bits expressos em notação hexadecimal. a.
0x0C000000
b.
0xC4630000
3.10.1 [5] <3.5> Que número decimal o padrão de bits representa se ele for um inteiro em complemento de dois? E um inteiro sem sinal? 3.10.2 [10] <3.5> Se esse padrão de bits for colocado no Registrador de Instrução, que instrução MIPS será executada? 3.10.3 [10] <3.5> Que número decimal o padrão de bits representa se ele for um número de ponto flutuante? Use o padrão IEEE 754. A tabela a seguir mostra números decimais. a.
63,25
b.
146987, 40625
3.10.4 [10] <3.5> Escreva a representação binária do número decimal, considerando o formato de precisão simples IEEE 754. 3.10.5 [10] <3.5> Escreva a representação binária do número decimal, considerando o formato de precisão dupla IEEE 754. 3.10.6 [10] <3.5> Escreva a representação binária do número decimal considerando que ele foi armazenado usando-se o formato IBM de precisão simples (base 16, em vez da base 2, com 7 bits de expoente).
Exercício 3.11 No padrão de ponto flutuante IEEE 754, o expoente é armazenado em formato de “bias” (também conhecido como “Excess-N”). Essa técnica foi selecionada porque queremos que um padrão com apenas zeros seja o mais próximo de zero possível. Em razão do uso de um 1 oculto, se tivéssemos de representar o expoente no formato de complemento de dois, um padrão com apenas zeros na realidade seria o número 1! (Lembre-se de que qualquer coisa elevada à potência zero é 1 e, portanto, 1,00 = 1.) Há muitos outros aspectos do padrão IEEE 754 que ajudam as unidades de ponto flutuante do hardware a trabalharem mais rapidamente. Porém, em muitas máquinas mais antigas, os cálculos de ponto flutuante eram tratados no software, e, portanto, outros formatos foram utilizados. A tabela a seguir mostra números decimais. a.
−1,5625 × 10–1
b.
9,356875 × 102
3.11.1 [20] <3.5> Escreva o padrão de bits binário considerando um formato semelhante ao empregado pelo DEC PDP-8 (12 bits da esquerda são o expoente armazenado como um número de complemento de dois, e os 24 bits da direita são a mantissa armazenada como um número de complemento de dois.) Nenhum 1 oculto é utilizado. Compare o intervalo e a precisão desse padrão de 36 bits com os padrões IEEE 754 de precisão simples e dupla. 3.11.2 [20] <3.5> NVIDIA tem um formato “metade”, que é semelhante ao IEEE 754, exceto que tem apenas 16 bits de largura. O bit mais à esquerda ainda é o bit de sinal, o ex-
236
Capítulo 3 Aritmética Computacional
poente tem 5 bits de largura e é armazenado no formato Excess-16, e a mantissa tem 10 bits de extensão. Assume-se que existe um 1 oculto. Escreva o padrão de bits considerando uma versão modificada desse formato que utiliza um formato com excesso de 16 para armazenar o expoente. Comente sobre o intervalo e a precisão desse padrão de 16 bits com o padrão IEEE 754 de precisão simples. 3.11.3 [20] <3.5> Os Hewlett-Packard 2114, 2115 e 2116 usavam um formato com os 16 bits mais à esquerda sendo a mantissa armazenada no formato de complemento de dois, seguida por outro campo de 16 bits que tinha nos 8 bits mais à esquerda uma extensão da mantissa (fazendo com que a mantissa tenha 24 bits de extensão) e os 8 bits mais à direita representando o expoente. Porém, por um capricho interessante, o expoente era armazenado em formato de magnitude de sinal com o bit de sinal no canto direito! Escreva o padrão de bits considerando esse formato. Nenhum 1 oculto é utilizado. Compare o intervalo e a precisão desse padrão de 32 bits com o padrão IEEE 754 de precisão simples. A tabela a seguir mostra pares de números decimais. A
B
a.
2,6125 × 10
4,150390625 × 10–1
b.
–4,484375 × 101
1,3953125 × 101
1
3.11.4 [20] <3.5> Calcule a soma de A e B à mão, supondo que A e B sejam armazenados no formato NVIDIA de 16 bits descrito no Exercício 3.11.2 (e também descrito no texto). Considere um bit de guarda, um bit de arredondamento e um sticky bit, e arredonde para o par mais próximo. Mostre todas as etapas. 3.11.5 [60] <3.5> Escreva um programa em linguagem de máquina do assembly MIPS para calcular a soma de A e B, supondo que estes estejam armazenados no formato NVIDIA de 16 bits descrito no Exercício 3.11.2 (e também descrito no texto). Considere um bit de guarda, um bit de arredondamento e um sticky bit, e arredonde para o par mais próximo. 3.11.6 [60] <3.5> Escreva um programa em linguagem assembly MIPS para calcular a soma de A e B, considerando que eles estejam armazenados usando o formato descrito no Exercício 3.11.1. Agora, modifique o programa para calcular a soma considerando o formato descrito no Exercício 3.11.3. Que formato é mais fácil para um programador lidar? Compare-os com o formato IEEE 754. (Não se preocupe com os sticky bits nesta questão.)
Exercício 3.12 A multiplicação de ponto flutuante é ainda mais complicada e desafiadora que a adição de ponto flutuante, e ambas são mínimas se comparadas à divisão de ponto flutuante. A tabela a seguir apresenta pares de números decimais. A
B
a.
–8,0546875 × 100
–1,79931640625 × 10–1
b.
8,59375 × 10–2
8,125 × 10–1
3.12.1 [30] <3.5> Calcule o produto de A e B manualmente, considerando que A e B sejam armazenados no formato NVIDIA de 16 bits descrito no Exercício 3.11.2 (e também descrito no texto). Considere um bit de guarda, um bit de arredondamento e um sticky bit, e arredonde para o par mais próximo. Mostre todas as etapas; porém, como acontece no exemplo do texto, você pode realizar a multiplicação em formato legível aos humanos, em vez de usar as técnicas descritas nos Exercícios de 3.4 a 3.6. Indique se existe overflow ou underflow. Escreva sua resposta como um padrão de 16 bits e também como um número decimal. Qual é a precisão do seu resultado? Compare-o com o número que você obtém se realizar a multiplicação em uma calculadora.
3.11 Exercícios 237
3.12.2 [60] <3.5> Escreva um programa para calcular o produto de A e B, considerando que eles sejam armazenados no formato IEEE 754. Indique se existe overflow ou underflow. (Lembre-se de que o IEEE 754 considera um bit de guarda, um bit de arredondamento e um sticky bit, e arredonde para o par mais próximo.) 3.12.3 [60] <3.5> Escreva um programa em linguagem assembly MIPS para calcular o produto de A e B, considerando que eles sejam armazenados no formato descrito no Exercício 3.11.1. Agora, modifique o programa para calcular o produto considerando o formato descrito no Exercício 3.11.3. Que formato é mais fácil para um programador lidar? Compare-os com o formato IEEE 754. (Não se preocupe com os sticky bits nesta questão.) A tabela a seguir mostra outros pares de números decimais. A
B
a.
8,625 × 10
–4,875 × 100
b.
1,84375 × 100
1,3203125 × 100
1
3.12.4 [30] <3.5> Calcule A dividido por B manualmente. Mostre todas as etapas necessárias para se chegar à sua resposta. Suponha que exista um bit de guarda, de arredondamento e um sticky bit, e use-os se for necessário. Escreva a resposta final em formato de ponto flutuante com 16 bits descrito no exercício 3.11.2 e em decimal, comparando o resultado decimal com o que você obtém usando uma calculadora. Os Livermore Loops1 são um conjunto de kernels intensos de ponto flutuante tirados de programas científicos executados no Lawrence Livermore Laboratory. A tabela a seguir identifica os kernels individuais do conjunto. a.
Livermore Loop 3
b.
Livermore Loop 9
3.12.5 [60] <3.5> Escreva o loop na linguagem assembly MIPS. 3.12.6 [60] <3.5> Descreva, com detalhes, uma técnica para realizar divisão de ponto flutuante em um computador digital. Não se esqueça de incluir referências às fontes que você utilizou.
Exercício 3.13 As operações realizadas sobre inteiros de ponto fixo se comportam como se espera — as leis comutativa, associativa e distributiva permanecem. Contudo, isso nem sempre acontece quando se trabalha com números de ponto flutuante. Primeiro, vejamos a lei associativa. A tabela a seguir mostra conjuntos de números decimais. A
B
C
a.
3,984375 × 10–1
3,4375 × 10–1
1,771 × 103
b.
3,96875 × 100
8,46875 × 100
2,1921875 × 101
3.13.1 [20] <3.2, 3.5, 3.6> Calcule (A + B) + C manualmente, considerando que A, B e C são armazenados no formato NVIDIA de 16 bits, descrito no Exercício 3.11.2 (e também descrito no texto). Considere um bit de guarda, um bit de arredondamento e um sticky bit, e arredonde para o par mais próximo. Mostre todas as etapas e escreva sua resposta em formato de ponto flutuante de 16 bits e em decimal. 1
Você poderá encontrá-los em http://www.netlib.org/benchmark/livermore.
238
Capítulo 3 Aritmética Computacional
3.13.2 [20] <3.2, 3.5, 3.6> Calcule A + (B + C) manualmente, considerando que A, B e C são armazenados no formato NVIDIA de 16 bits, descrito no Exercício 3.11.2 (e também descrito no texto). Considere um bit de guarda, um bit de arredondamento e um sticky bit, e arredonde para o par mais próximo. Mostre todas as etapas, e escreva sua resposta em formato de ponto flutuante de 16 bits e em decimal. 3.13.3 [10] <3.2, 3.5, 3.6> Com base nas suas respostas dos Exercícios 3.13.1 e 3.13.2, (A + B) + C = A + (B + C)? A tabela a seguir mostra outros conjuntos de números decimais. A
B
C
a.
3,41796875 10–3
6,34765625 × 10–3
1,05625 × 102
b.
1,140625 × 102
–9,135 × 102
9,84375 × 10–1
3.13.4 [30] <3.3, 3.5, 3.6> (A × B) × C manualmente, considerando que A, B e C são armazenados no formato NVIDIA de 16 bits, descrito no Exercício 3.11.2 (e também descrito no texto). Considere um bit de guarda, um bit de arredondamento e um sticky bit, e arredonde para o par mais próximo. Mostre todas as etapas e escreva sua resposta em formato de ponto flutuante de 16 bits e em decimal. 3.13.5 [30] <3.3, 3.5, 3.6> Calcule A × (B × C) manualmente, considerando que A, B e C são armazenados no formato NVIDIA de 16 bits, descrito no Exercício 3.11.2 (e também descrito no texto). Considere um bit de guarda, um bit de arredondamento e um sticky bit, e arredonde para o par mais próximo. Mostre todas as etapas e escreva sua resposta em formato de ponto flutuante de 16 bits e em decimal. 3.13.6 [10] <3.3, 3.5, 3.6> Com base nas suas respostas dos Exercícios 3.13.4 e 3.13.5, (A × B) × C = A × (B × C)?
Exercício 3.14 A lei associativa não é a única que nem sempre se mantém quando se lidam com números de ponto flutuante. Existem outras coisas estranhas que também ocorrem. A tabela a seguir mostra conjuntos de números decimais. A
B
C
a.
1,666015625 × 100
1,9760 × 104
–1,9744 × 104
b.
3,48 × 102
6,34765625 × 10–2
–4,052734375 × 10–2
3.14.1 [30] <3.2, 3.3, 3.5, 3.6> Calcule A × (B + C) manualmente, considerando que A, B e C são armazenados no formato NVIDIA de 16 bits, descrito no Exercício 3.11.2 (e também descrito no texto). Considere um bit de guarda, um bit de arredondamento e um sticky bit, e arredonde para o par mais próximo. Mostre todas as etapas e escreva sua resposta em formato de ponto flutuante de 16 bits e em decimal. 3.14.2 [30] <3.2, 3.3, 3.5, 3.6> Calcule (A × B) + (A × C) manualmente, considerando que A, B e C são armazenados no formato NVIDIA de 16 bits, descrito no Exercício 3.11.2 (e também descrito no texto). Considere um bit de guarda, um bit de arredondamento e um sticky bit, e arredonde para o par mais próximo. Mostre todas as etapas e escreva sua resposta em formato de ponto flutuante de 16 bits e em decimal. 3.14.3 [10] <3.2, 3.3, 3.5, 3.6> Com base nas suas respostas dos Exercícios 3.14.1 e 3.14.2, (A × B) + (A × C) = A × (B + C)?
3.11 Exercícios 239
A tabela a seguir mostra dois pares, cada um consistindo em uma fração e um inteiro. A
B
a.
–1/4
4
b.
1/10
10
3.14.4 [10] <3.5> Usando o formato de ponto flutuante IEEE 754, escreva o padrão de bits que representaria A. Você consegue representar A com exatidão? 3.14.5 [10] <3.2, 3.3, 3.5, 3.6> O que você obtém se somar A a si mesmo B vezes? Quanto é A × B? Eles são iguais? O que deveriam ser? 3.14.6 [60] <3.2, 3.3, 3.4, 3.5, 3.6> O que você obtém se apanhar a raiz quadrada de B e depois multiplicar esse valor por si mesmo? O que você deveria obter? Faça isso para números de ponto flutuante com precisão simples e dupla. (Escreva um programa para realizar esses cálculos.)
Exercício 3.15 Números binários são utilizados no campo de mantissa, mas eles não precisam ser binários. A IBM usou números de base 16, por exemplo, em alguns de seus formatos de ponto flutuante. Existem outras técnicas que também são possíveis, cada uma com suas vantagens e desvantagens em particular. A tabela a seguir mostra frações a serem representadas em diversos formatos de ponto flutuante. a.
1/3
b.
1/10
3.15.1 [10] <3.5, 3.6> Escreva o padrão de bits na mantissa considerando um formato de ponto flutuante que usa números binários na mantissa (basicamente, o que você esteve fazendo neste capítulo). Suponha que existam 24 bits e você não precisa normalizar. Essa representação é exata? 3.15.2 [10] <3.5, 3.6> Escreva o padrão de bits na mantissa considerando um formato de ponto flutuante que usa números Binary Coded Decimal (base 10) na mantissa, em vez da base 2. Suponha que existam 24 bits e você não precisa normalizar. Essa representação é exata? 3.15.3 [10] <3.5, 3.6> Escreva o padrão de bits supondo que estamos usando números de base 15 na mantissa, em vez da base 2. (Números de base 16 utilizam os símbolos 0-9 e A-F. Números de base 15 usariam 0-9 e A-E.) Suponha que existam 24 bits e você não precisa normalizar. Essa representação é exata? 3.15.4 [20] <3.5, 3.6> Escreva o padrão de bits supondo que estamos usando números de base 30 na mantissa, em vez da base 2. (Números de base 16 utilizam os símbolos 0-9 e A-F. Números de base 30 usariam 0-9 e A-T.) Suponha que existam 24 bits e você não precisa normalizar. Essa representação é exata? Você consegue ver alguma vantagem no uso dessa técnica? §3.2: página 229: 3. §3.4: página 269: 3.
Respostas das Seções “Verifique você mesmo”
4 O Processador 4.1 Introdução 242
Em um assunto importante, nenhum detalhe é pequeno. Provérbio francês
4.2
Convenções lógicas de projeto 245
4.3
Construindo um caminho de dados 248
4.4
Um esquema de implementação simples 254
4.5
Visão geral de pipelining 265
4.6
Caminho de dados e controle usando pipeline 276
4.7
Hazards de dados: forwarding versus stalls 292
4.8
Hazards de controle 302
4.9 Exceções 309
4.10
Paralelismo e paralelismo avançado em nível de instrução 315
4.11
Vida real: o pipeline do AMD Opteron X4 (Barcelona) 325
4.12
Tópico avançado: uma introdução ao projeto digital usando uma linguagem de projeto de hardware para descrever e modelar um pipeline e mais ilustrações de pipelining 328
4.13
Falácias e armadilhas 328
4.14
Comentários finais 329
4.15
Perspectiva histórica e leitura adicional 330
4.16 Exercícios 330
Os cinco componentes clássicos de um computador
242
Capítulo 4 O Processador
4.1 Introdução O Capítulo 1 explica que o desempenho de um computador é determinado por três fatores principais: contagem de instruções, tempo de ciclo de clock e CPI (ciclos de clock por instrução). O compilador e o conjunto de instruções, que examinamos no Capítulo 2, determinam a contagem de instruções necessária para um determinado programa. Entretanto, tanto o tempo de ciclo de clock quanto o número de ciclos de clock por instrução são determinados pela implementação do processador. Neste capítulo, construímos o caminho de dados e a unidade de controle para duas implementações diferentes do conjunto de instruções MIPS. Este capítulo contém uma explicação dos princípios e das técnicas usados na implementação de um processador, começando com uma sinopse altamente abstrata e simplificada nesta seção. Ela é seguida de uma seção que desenvolve um caminho de dados e constrói uma versão simples de um processador suficiente para implementar conjuntos de instruções como o MIPS. O corpo do capítulo descreve uma implementação MIPS otimizada e mais realista, seguida de uma seção que desenvolve conceitos necessários para implementar conjuntos de instruções mais complexos, como o x86. Para o leitor interessado em entender a interpretação de alto nível de instruções e seu impacto sobre o desempenho do programa, esta seção inicial e a Seção 4.5 apresentam os conceitos básicos do pipelining. Tendências recentes são abordadas na Seção 4.10, e a Seção 4.11 descreve o recente microprocessador AMD Opteron X4 (Barcelona). Estas seções oferecem uma base suficiente para entender os conceitos de pipeline em um alto nível. Para os leitores que desejam um entendimento do processador e seu desempenho com mais profundidade, as Seções 4.3, 4.4 e 4.6 serão úteis. Aqueles interessados em aprender como montar um processador também devem ler as Seções 4.2, 4.7, 4.8 e 4.9. Para os leitores interessados no projeto de hardware moderno, a Seção 4.12 no site descreve como as linguagens de projeto de hardware e as ferramentas de CAD são usadas na implementação do hardware, e depois como usar uma linguagem de projeto de hardware para descrever uma implementação em pipeline. Ela também oferece várias outras ilustrações de como o hardware do pipelining é executado.
Uma implementação MIPS básica Analisaremos uma implementação que inclui um subconjunto do conjunto de instruções MIPS básico. j
As instruções de referência à memória load word (lw) e store word (sw).
j
As instruções lógicas e aritméticas add, sub, AND, OR e slt.
j
As instruções brench equal (beq) e jump (j), que acrescentamos depois.
Esse subconjunto não inclui todas as instruções de inteiro (por exemplo, shift, multiply e divide estão ausentes), nem inclui qualquer instrução de ponto flutuante. Entretanto, os princípios básicos usados na criação de um caminho de dados e no projeto do controle são ilustrados. A implementação das outras instruções é semelhante. Examinando a implementação, teremos a oportunidade de ver como o conjunto de instruções determina muitos aspectos da implementação e como a escolha de várias estratégias de implementação afeta a velocidade de clock e o CPI para o computador. Muitos dos princípios básicos de projeto apresentados no Capítulo 1 podem ser ilustrados considerando-se a implementação, como os princípios Torne o caso comum mais rápido e A simplicidade favorece a regularidade. Além disso, a maioria dos conceitos usados para implementar o subconjunto MIPS neste capítulo e no próximo envolvem as mesmas ideias básicas usadas para construir um amplo espectro de computadores, desde servidores de alto desempenho até microprocessadores de finalidade geral e processadores embutidos.
4.1 Introdução 243
Uma sinopse da implementação
No Capítulo 2 vimos instruções MIPS básicas, incluindo as instruções lógicas e aritméticas, as de referência à memória e as de desvio. Muito do que precisa ser feito para implementar essas instruções é igual, independente da classe exata da instrução. Para cada instrução, as duas primeiras etapas são idênticas: 1. Enviar o contador de programa (PC) à memória que contém o código e buscar a instrução dessa memória. 2. Ler um ou mais registradores, usando campos da instrução para selecionar os registradores a serem lidos. Para a instrução load word, precisamos ler apenas um registrador, mas a maioria das outras instruções exige a leitura de dois registradores. Após essas duas etapas, as ações necessárias para completar a instrução dependem da classe da instrução. Felizmente, para cada uma das três classes de instrução (referência à memória, lógica e aritmética, e desvios), as ações são quase as mesmas, seja qual for a instrução exata. A simplicidade e a regularidade do conjunto de instruções simplifica a implementação tornando semelhantes as execuções de muitas das classes de instrução. Por exemplo, todas as classes de instrução, exceto jump, usam a unidade lógica e aritmética (ALU) após a leitura dos registradores. As instruções de referência à memória usam a ALU para o cálculo de endereço, as instruções lógicas e aritméticas para a execução da operação e desvios para comparação. Após usar a ALU, as ações necessárias para completar várias classes de instrução diferem. Uma instrução de referência à memória precisará acessá-la a fim de escrever dados para um load ou ler dados para um store. Uma instrução lógica e aritmética precisa escrever os dados da ALU de volta a um registrador. Finalmente, para uma instrução de desvio, podemos ter de mudar o próximo endereço de instrução com base na comparação; caso contrário, o PC deve ser incrementado em 4 a fim de chegar ao endereço da próxima instrução. A Figura 4.1 mostra a visão em alto nível de uma implementação MIPS, focando as várias unidades funcionais e sua interconexão. Embora essa figura mostre a maioria do fluxo de dados pelo processador, ela omite dois importantes aspectos da execução da instrução. Primeiro, em vários lugares, a Figura 4.1 mostra os dados indo para uma determinada unidade, vindo de duas origens diferentes. Por exemplo, o valor escrito no PC pode vir de dois somadores, os dados escritos no banco de registradores podem vir da ALU ou da memória de dados, e a segunda entrada da ALU pode vir de um registrador ou do campo imediato da instrução. Na prática, essas linhas de dados não podem simplesmente ser interligadas; precisamos adicionar um elemento que escolha dentre as diversas origens e conduza uma dessas origens a seu destino. Essa seleção normalmente é feita com um dispositivo chamado multiplexador, embora uma melhor denominação desse dispositivo seria seletor de dados. O Apêndice C descreve o multiplexador, que seleciona entre várias entradas com base na configuração de suas linhas de controle. As linhas de controle são definidas principalmente com base na informação tomada da instrução sendo executada. A segunda omissão na Figura 4.1 é que várias das unidades precisam ser controladas de acordo com o tipo da instrução. Por exemplo, a memória de dados precisa ler em um load e escrever em um store. O banco de registradores precisa ser escrito em uma instrução load e em uma instrução lógica ou aritmética. E, é claro, a ALU precisa realizar uma de várias operações, como vimos no Capítulo 2. (O Apêndice C descreve o projeto detalhado da ALU.) Assim como os multiplexadores, essas operações são direcionadas por linhas de controle que são definidas com base nos vários campos das instruções. A Figura 4.2 mostra o caminho de dados da Figura 4.1 com os três multiplexadores necessários acrescentados, bem como as linhas de controle para as principais unidades funcionais. Uma unidade de controle, que tem a instrução como uma entrada, é usada para determinar como definir as linhas de controle para as unidades funcionais e dois dos multiplexadores. O terceiro multiplexador – que determina se PC + 4 ou o endereço de destino do desvio é escrito no PC – é definido com base na saída zero da ALU, usada para
244
Capítulo 4 O Processador
FIGURA 4.1 Uma visão abstrata da implementação do subconjunto MIPS mostrando as principais unidades funcionais e as principais conexões entre elas. Todas as instruções começam usando o contador de programa para fornecer o endereço de instrução para a memória de instruções. Depois que a instrução é buscada, os registradores usados como operandos pela instrução são especificados por campos dessa instrução. Uma vez que os operandos tenham sido buscados, eles podem ser operados de modo a calcular um endereço de memória (para um load ou store), calcular um resultado aritmético (para uma instrução lógica ou aritmética) ou a comparação (para um desvio). Se a instrução for uma instrução lógica ou aritmética, o resultado da ALU precisa ser escrito em um registrador. Se a operação for um load ou store, o resultado da ALU é usado como um endereço com a finalidade de armazenar o valor de um registrador ou ler um valor da memória para um registrador. O resultado da ALU ou memória é escrito de volta no banco de registradores. Os desvios exigem o uso da saída da ALU para determinar o próximo endereço de instrução, que vem da ALU (em que o offset do PC e do desvio são somados) ou de um somador que incrementa o PC atual em 4. As linhas grossas interconectando as unidades funcionais representam barramentos, que consistem em múltiplos sinais. As linhas são usadas para guiar o leitor sobre como as informações fluem. Como as linhas de sinal podem se cruzar, mostramos explicitamente quando as linhas que se cruzam estão conectadas pela presença de um ponto no local do cruzamento.
realizar a comparação da instrução beq. A regularidade e a simplicidade do conjunto de instruções MIPS significam que um simples processo de decodificação pode ser usado no sentido de determinar como definir as linhas de controle. No restante do capítulo, refinamos essa visão para preencher os detalhes, o que exige que acrescentemos mais unidades funcionais, aumentemos o número das conexões entre unidades e, é claro, adicionemos uma unidade de controle a fim de controlar que ações são realizadas para diferentes classes de instrução. As Seções 4.3 e 4.4 descrevem uma implementação simples que usa um único ciclo de clock longo para cada instrução e segue a forma geral das Figuras 4.1 e 4.2. Nesse primeiro projeto, cada instrução começa a execução em uma transição do clock e completa a execução na próxima transição do clock. Embora seja mais fácil de entender, esse método não é prático, já que o ciclo de clock precisa ser esticado para acomodar a instrução mais longa. Após projetar o controle desse computador simples, veremos uma implementação em pipeline com todas as suas complexidades, incluindo as exceções.
Verifique você mesmo
Quantos dos cinco componentes clássicos de um computador – mostrados no início deste capítulo – as Figuras 4.1 e 4.2 contêm?
4.2 Convenções lógicas de projeto 245
FIGURA 4.2 A implementação básica do subconjunto MIPS incluindo as linhas de controle e os multiplexadores necessários. O multiplexador superior (“Mux”) controla que valor substitui o PC (PC + 4 ou o endereço de destino do desvio); o multiplexador é controlado pela porta que realiza um AND da saída Zero da ALU com um sinal de controle que indica que a instrução é de desvio. O multiplexador cuja saída retorna para o banco de registradores é usado para conduzir a saída da ALU (no caso de uma instrução lógica ou aritmética) ou a saída da memória de dados (no caso de um load) a ser escrita no banco de registradores. Finalmente, o multiplexador da parte inferior é usado de modo a determinar se uma segunda entrada da ALU vem dos registradores (para uma instrução lógica-aritmética OU um desvio) ou do campo offset da instrução (para um load ou store). As linhas de controle acrescentadas são simples e determinam a operação realizada pela ALU, se a memória de dados deve ler ou escrever e se os registradores devem realizar uma operação de escrita. As linhas de controle são mostradas em tons de cinza para que sejam vistas com mais facilidade.
4.2 Convenções lógicas de projeto Para tratar do projeto de um computador, precisamos decidir como a implementação lógica do computador irá operar e como esse computador está sincronizado. Esta seção examina algumas ideias básicas na lógica digital que usaremos em todo o capítulo. Se você tiver pouco ou nenhum conhecimento em lógica digital, provavelmente será útil ler o Apêndice C antes de continuar. Os elementos do caminho de dados na implementação MIPS consistem em dois tipos diferentes de elementos lógicos: aqueles que operam nos valores dos dados e os que contêm estado. Os elementos que operam nos valores dos dados são todos combinacionais, significando que suas saídas dependem apenas das entradas atuais. Dada a mesma entrada, um elemento combinacional sempre produz a mesma saída. A ALU mostrada na Figura 4.1 e discutida no Apêndice C é um exemplo de elemento combinacional. Dado um conjunto de entradas, ele sempre produz a mesma saída porque não possui qualquer armazenamento interno.
elemento combinacional Um elemento operacional, como uma porta AND ou uma ALU.
246
elemento de estado Um elemento da memória, como um registrador ou uma memória.
ativo O sinal está logicamente alto, ou verdadeiro.
inativo O sinal está logicamente baixo, ou falso.
Capítulo 4 O Processador
Outros elementos no projeto não são combinatórios, mas contêm estado. Um elemento contém estado se tiver algum armazenamento interno. Chamamos esses elementos de elementos de estado, pois, se desconectássemos o computador da tomada, poderíamos reiniciá-lo carregando os elementos de estado com os valores que continham antes de interrompermos a energia. Além disso, se salvássemos e armazenássemos novamente os elementos de estado, seria como se o computador nunca tivesse perdido a energia. Na Figura 4.1, as memórias de instruções e de dados, bem como os registradores, são exemplos de elementos de estado. Um elemento de estado possui pelo menos duas entradas e uma saída. As entradas necessárias são os valores dos dados a serem escritos no elemento e o clock, que determina quando o valor dos dados deve ser escrito. A saída de um elemento de estado fornece o valor escrito em um ciclo de clock anterior. Por exemplo, um dos elementos de estado mais simples logicamente é um flip-flop tipo D (veja o Apêndice C), que possui exatamente essas duas entradas (um valor e um clock) e uma saída. Além dos flip-flops, nossa implementação MIPS também usa dois outros tipos de elementos de estado: memórias e registradores, ambos aparecendo na Figura 4.1. O clock é usado para determinar quando se deve escrever no elemento de estado; um elemento de estado pode ser lido a qualquer momento. Os componentes lógicos que contêm estado também são chamados de sequenciais porque suas saídas dependem de suas entradas e do conteúdo do estado interno. Por exemplo, a saída da unidade funcional representando os registradores depende dos números de registrador fornecidos e do que foi escrito nos registradores anteriormente. A operação dos elementos combinatórios e sequenciais e sua construção são discutidas em mais detalhes no Apêndice C. Usaremos o termo ativo para indicar um sinal que está logicamente alto, o termo ativar para especificar que um sinal deve ser conduzido a logicamente alto, e desativar ou inativo para representar o que é logicamente baixo. Metodologia de clocking
metodologia de clocking O método usado para determinar quando os dados são válidos e estáveis em relação ao clock.
sincronização acionada por transição Um esquema de clocking em que todas as mudanças de estado ocorrem em uma transição do clock.
Uma metodologia de clocking define quando os sinais podem ser lidos e quando podem ser escritos. Ela é importante para especificar a sincronização das leituras e escritas porque, se um sinal fosse escrito ao mesmo tempo em que fosse lido, o valor da leitura poderia corresponder ao valor antigo, ao valor recém-escrito ou mesmo alguma combinação dos dois! Obviamente, os projetos de computadores não podem tolerar essa imprevisibilidade. Uma metodologia de clocking tem o objetivo de garantir a previsibilidade. Para simplificar, consideraremos uma metodologia de sincronização acionada por transição. Uma metodologia de sincronização acionada por transição significa que quaisquer valores armazenados em um elemento lógico sequencial são atualizados apenas em uma transição do clock. Como apenas os elementos de estado podem armazenar valores de dados, qualquer coleção de lógica combinatória precisa ter suas entradas vindo de um conjunto de elementos de estado e suas saídas escritas em um conjunto de elementos de estado. As entradas são valores escritos em um ciclo de clock anterior, enquanto as saídas são valores que podem ser usados em um ciclo de clock seguinte. A Figura 4.3 mostra os dois elementos de estado em volta de um bloco de lógica combinatória, que opera em um único ciclo de clock: todos os sinais precisam se propagar
FIGURA 4.3 A lógica combinatória, os elementos de estado e o clock estão intimamente relacionados. Em um sistema digital síncrono, o clock determina quando os elementos com estado escreverão valores no armazenamento interno. Quaisquer entradas em um único elemento precisam alcançar um valor estável (ou seja, ter alcançado um valor do qual não mudarão até após a transição do clock) antes que a transição ativa do clock faça com que o estado seja atualizado. Todos os elementos de estado, incluindo a memória, são considerados acionados por transição.
4.2 Convenções lógicas de projeto 247
desde o elemento de estado 1, passando pela lógica combinatória e indo até o elemento 2 no tempo de um ciclo de clock. O tempo necessário para os sinais alcançarem o elemento 2 define a duração do ciclo de clock. Para simplificar, não mostraremos um sinal de controle de escrita quando um elemento de estado é escrito em cada transição ativa de clock. Por outro lado, se um elemento de estado não for atualizado em cada clock, um sinal de controle de escrita explícito é necessário. Tanto o sinal de clock quanto o sinal de controle de escrita são entradas, e o elemento de estado só é alterado quando o sinal de controle de escrita está ativo e ocorre uma transição do clock. Uma metodologia acionada por transição permite ler o conteúdo de um registrador, enviar o valor por meio de alguma lógica combinatória e escrever nesse registrador no mesmo ciclo de clock. A Figura 4.4 mostra um exemplo genérico. Não importa se consideramos que todas as escritas ocorrem na transição de subida do clock ou na transição de descida, já que as entradas no bloco de lógica combinatória não podem mudar exceto na transição de clock escolhida. Com uma metodologia de sincronização acionada por transição, não há qualquer feedback dentro de um único ciclo de clock, e a lógica na Figura 4.4 funciona corretamente. No Apêndice C, discutimos brevemente as outras limitações (como os tempos de setup e hold), bem como outras metodologias de sincronização.
sinal de controle Um sinal usado para seleção de multiplexador ou para direcionar a operação de uma unidade funcional; contrasta com um sinal de dados, que contém informações operadas por uma unidade funcional.
FIGURA 4.4 Uma metodologia acionada por transição permite que um elemento de estado seja lido e escrito no mesmo ciclo de clock sem criar uma disputa que poderia levar a valores de dados indeterminados. É claro que o ciclo de clock ainda precisa ser longo o suficiente para que os valores de entrada sejam estáveis quando a transição ativa do clock ocorrer. O feedback não pode ocorrer dentro de um ciclo de clock devido à atualização acionada por transição do elemento de estado. Se o feedback fosse possível, esse projeto não poderia funcionar corretamente. Nossos projetos neste capítulo e no próximo se baseiam na metodologia de sincronização acionada por transição e em estruturas como a mostrada nesta figura.
Para a arquitetura MIPS de 32 bits, quase todos esses elementos de estado e lógicos terão entradas e saídas contendo 32 bits de extensão, já que essa é a extensão da maioria dos dados manipulados pelo processador. Sempre que uma unidade tiver uma entrada ou saída diferente de 32 bits de extensão, deixaremos isso claro. As figuras indicarão barramentos (que são sinais mais largos do que 1 bit), com linhas mais grossas. Algumas vezes, desejaremos combinar vários barramentos para formar um barramento mais largo; por exemplo, podemos querer obter um barramento de 32 bits combinando dois de 16 bits. Nesses casos, rótulos nas linhas de barramento indicarão que estamos concatenando barramentos para formar um mais largo. Setas também são incluídas para ajudar a esclarecer a direção do fluxo dos dados entre elementos. Finalmente, o realce indica um sinal de controle em oposição a um sinal que conduz dados; essa distinção se tornará mais clara enquanto avançarmos neste capítulo. Verdadeiro ou falso: como o banco de registradores é lido e escrito no mesmo ciclo de clock, qualquer caminho de dados MIPS usando escritas acionadas por transição precisa ter mais de uma cópia do banco de registradores. Detalhamento: Há também uma versão de 64 bits da arquitetura MIPS e, naturalmente, a maioria dos caminhos em sua implementação teria 64 bits de largura. Além disso, usamos os termos ativar e desativar porque, às vezes, 1 representa logicamente alto e às vezes pode representar logicamente baixo.
Verifique você mesmo
248
Capítulo 4 O Processador
4.3 Construindo um caminho de dados elemento do caminho de dados Uma unidade funcional usada para operar sobre os dados ou conter esses dados dentro de um processador. Na implementação MIPS, os elementos do caminho de dados incluem as memórias de instruções e de dados, o banco de registradores, a unidade lógica e aritmética (ALU) e os somadores.
contador de programa (PC) O registrador contendo o endereço da instrução do programa sendo executado.
Uma maneira razoável de iniciar um projeto de caminho de dados é examinar os principais componentes necessários para executar cada classe de instrução MIPS. Vamos começar olhando quais elementos do caminho de dados cada instrução precisa. Quando mostramos os elementos do caminho de dados, também mostramos seus sinais de controle. A Figura 4.5a mostra o primeiro elemento de que precisamos: uma unidade de memória para armazenar as instruções de um programa e fornecer instruções dado um endereço. A Figura 4.5b mostra um registrador, que podemos chamar de contador de programa (PC), que, como vimos no Capítulo 2, é um registrador que contém o endereço da instrução atual. Finalmente, precisaremos de um somador a fim de incrementar o PC para o endereço da próxima instrução. Esse somador, que é combinatório, pode ser construído a partir da ALU que descrevemos em detalhes no Apêndice C, simplesmente interligando as linhas de controle de modo que o controle sempre especifique uma operação de adição. Representaremos uma ALU desse tipo com o rótulo Add, como na Figura 4.5, para indicar que ela se tornou permanentemente um somador e não pode realizar as outras funções da ALU.
FIGURA 4.5 Dois elementos de estado são necessários para armazenar e acessar instruções, e um somador é necessário para calcular o endereço da próxima instrução. Os elementos de estado são a memória de instruções e o contador de programa. A memória de instruções só precisa fornecer acesso de leitura porque o caminho de dados não escreve instruções. Como a memória de instruções apenas é lida, nós a tratamos como lógica combinatória: a saída em qualquer momento reflete o conteúdo do local especificado pela entrada de endereço, e nenhum sinal de controle de leitura é necessário. (Precisamos escrever na memória de instruções quando carregarmos o programa; isso não é difícil de incluir e o ignoramos em favor da simplicidade.) O contador de programa é um registrador de 32 bits que é escrito no final de cada ciclo de clock e, portanto, não precisa de um sinal de controle de escrita. O somador é uma ALU configurada para sempre realizar a adição das suas duas entradas de 32 bits e colocar o resultado em sua saída.
Para executar qualquer instrução, precisamos começar buscando a instrução na memória. A fim de preparar para executar a próxima instrução, também temos de incrementar o contador de programa de modo que aponte para a próxima instrução, 4 bytes depois. A Figura 4.6 mostra como combinar os três elementos da Figura 4.5 para formar um caminho de dados que busca instruções e incrementa o PC de modo a obter o endereço da próxima instrução sequencial. Agora, vamos considerar as instruções de formato R (veja a Figura 2.20). Todas elas leem dois registradores, realizam uma operação na ALU com o conteúdo dos registradores e escrevem o resultado em um registrador. Chamamos essas instruções de instruções tipo R ou instruções lógicas ou aritméticas (já que elas realizam operações lógicas ou aritméticas). Essa classe de instrução inclui add, sub, AND, OR e slt, que foram apresentadas no Capítulo 2. Lembre-se de que um caso típico desse tipo de instrução é add $t1, $t2, $t3, que lê $t2 e $t3 e escreve em $t1.
4.3 Construindo um caminho de dados 249
FIGURA 4.6 Uma parte do caminho de dados usada para buscar instruções e incrementar o contador do programa. A instrução buscada é usada por outras partes do caminho de dados.
Os registradores de uso geral de 32 bits do processador são armazenados em uma estrutura chamada banco de registradores. Um banco de registradores é uma coleção de registradores em que qualquer registrador pode ser lido ou escrito especificando o número do registrador no banco. O banco de registradores contém o estado dos registradores do computador. Além disso, precisaremos que uma ALU opere nos valores lidos dos registradores. Devido às instruções de formato R terem três operandos de registrador, precisaremos ler duas palavras de dados do banco de registradores e escrever uma palavra de dados no banco de registradores para cada instrução. A fim de que cada palavra de dados seja lida dos registradores, precisamos de uma entrada no banco de registradores que especifique o número do registrador a ser lido e uma saída do banco de registradores que conduzirá o valor lido dos registradores. Para escrever uma palavra de dados, precisaremos de duas entradas: uma para especificar o número do registrador a ser escrito e uma para fornecer os dados a serem escritos no registrador. O banco de registradores sempre gera como saída o conteúdo de quaisquer números de registrador que estejam nas entradas Registrador de leitura. As escritas, entretanto, são controladas pelo sinal de controle de escrita, que precisa ser ativo para que uma escrita ocorra na transição do clock. A Figura 4.7a mostra o resultado; precisamos de um total de quatro entradas (três para números de registrador e uma para dados) e duas saídas (ambas para dados). As entradas de número de registrador possuem 5 bits de largura para especificar um dos 32 registradores (32 = 25), enquanto a entrada de dados e os dois barramentos de saída de dados possuem 32 bits de largura cada um. A Figura 4.7b mostra a ALU, que usa duas entradas de 32 bits e produz um resultado de 32 bits, bem como um sinal de 1 bit se o resultado for 0. O sinal de controle de quatro bits da ALU é descrito em detalhes no Apêndice C; examinaremos o controle da ALU brevemente quando precisarmos saber como defini-lo. A seguir, considere as instruções MIPS load word e store word, que possuem o formato lw $t1,offset_value($t2) ou sw $t1,offset_value($t2). Essas instruções calculam um endereço de memória somando o registrador de base, que é $t2, com o campo offset de 16 bits com sinal contido na instrução. Se a instrução for um store, o valor a ser armazenado também precisará ser lido do banco de registradores em que reside, em $t1. Se a instrução for um load, o valor lido da memória precisará ser escrito no banco de registradores no registrador especificado, que é $t1. Consequentemente, precisaremos do banco de registradores e da ALU da Figura 4.7. Além disso, precisaremos de uma unidade a fim de estender o sinal do campo offset de 16 bits da instrução para um valor com sinal de 32 bits, e de uma unidade de memória da qual ler ou na qual escrever. A memória de dados precisa ser escrita com instruções store; portanto, ela tem sinais de controle de leitura e escrita, uma entrada de endereço e uma entrada para os dados serem escritos na memória. A Figura 4.8 mostra esses dois elementos.
banco de registradores Um elemento de estado que consiste em um grupo de registradores que podem ser lidos e escritos fornecendo um número de registrador a ser acessado.
estender o sinal Aumentar o tamanho de um item de dados replicando o bit mais alto de sinal do item de dados original nos bits mais altos do item de dados maior de destino.
250
Capítulo 4 O Processador
FIGURA 4.7 Os dois elementos necessários para implementar operações para a ALU no formato R são o banco de registradores e a ALU. O banco de registradores contém todos os registradores e possui duas portas para leitura e uma porta para escrita. O projeto dos bancos de registradores de várias portas é discutido na Seção C.8 do Apêndice C. O banco de registradores sempre gera como saídas os conteúdos dos registradores correspondentes às entradas Registrador de leitura nas saídas; nenhuma outra entrada de controle é necessária. Ao contrário, uma escrita em um registrador precisa ser explicitamente indicada ativando o sinal de controle de escrita. Lembre-se de que as escritas são acionadas por transição, de modo que todas as entradas de escrita (por exemplo, o valor a ser escrito, o número do registrador e o sinal de controle de escrita) precisam ser válidas na transição do clock. Como as escritas no banco de registradores são acionadas por transição, nosso projeto pode ler e escrever sem problemas no mesmo registrador dentro de um ciclo de clock: a leitura obterá o valor escrito em um ciclo de clock anterior, enquanto o valor escrito estará disponível para uma leitura em um ciclo de clock subsequente. As entradas com o número do registrador para o banco de registradores possuem todas 5 bits de largura, enquanto as linhas com os valores de dados possuem 32 bits de largura. A operação a ser realizada pela ALU é controlada com o sinal de Apêndice C. Em breve, usaremos a saída operação da ALU, que terá largura de 4 bits, usando a ALU projetada no de detecção Zero da ALU para implementar desvios. A saída de overflow não será necessária até a Seção 4.9, quando discutiremos as exceções; até lá, elas serão omitidas.
FIGURA 4.8 As duas unidades necessárias para implementar loads e stores, além do banco de registradores e da ALU da Figura 4.7, são a unidade de memória de dados e a unidade de extensão de sinal. A unidade de memória é um elemento de estado com entradas para os endereços e os dados de escrita, e uma única saída para o resultado da leitura. Existem controles de leitura e escrita separados, embora apenas um deles possa ser ativado em qualquer clock específico. A unidade de memória precisa de um sinal de leitura, já que, diferente do banco de registradores, ler o valor de um endereço inválido pode causar problemas, como veremos no Capítulo 5. A unidade de extensão de sinal possui uma entrada de 16 bits que tem o seu sinal estendido para que um resultado de 32 bits apareça na saída (veja o Capítulo 2). Consideramos que a memória de dados é acionada por transição para as escritas. Na verdade, os chips de memória padrão possuem um sinal “write enable” que é usado para escritas. Embora o write enable não seja acionado por transição, nosso projeto acionado por transição poderia facilmente ser adaptado para funcionar com chips de memória reais. Consulte a Seção C.8 do Apêndice C para ver uma discussão mais detalhada de como funcionam os chips de memória reais.
4.3 Construindo um caminho de dados 251
A instrução beq possui três operandos, dois registradores comparados para igualdade e um offset de 16 bits para calcular o endereço de destino do desvio relativo ao endereço da instrução desvio. Sua forma é beq $t1,$t2,offset. Para implementar essa instrução, precisamos calcular o endereço de destino somando o campo offset estendido com sinal da instrução com o PC. Há dois detalhes na definição de instruções de desvio (veja o Capítulo 2) para os quais precisamos prestar atenção: j
O conjunto de instruções especifica que a base para o cálculo do endereço de desvio é o endereço da instrução seguinte ao desvio. Como calculamos PC + 4 (o endereço da próxima instrução) no caminho de dados para busca de instruções, é fácil usar esse valor como a base para calcular o endereço de destino do desvio.
j
A arquitetura também diz que o campo offset é deslocado 2 bits para a esquerda de modo que seja um offset de uma palavra; esse deslocamento aumenta a faixa efetiva do campo offset por um fator de quatro vezes.
Para lidar com a última complicação, precisaremos deslocar o campo offset de dois bits. Além de calcular o endereço de destino do desvio, também precisamos determinar se a próxima instrução é a instrução que acompanha sequencialmente ou a instrução no endereço de destino do desvio. Quando a condição é verdadeira (isto é, os operandos são iguais), o endereço de destino do desvio se torna o novo PC e dizemos que o desvio é tomado. Se os operandos não forem iguais, o PC incrementado deve substituir o PC atual (exatamente como para qualquer outra instrução normal); nesse caso, dizemos que o desvio é não tomado. Portanto, o caminho de dados de desvio precisa de duas operações: calcular o endereço de destino do desvio e comparar o conteúdo do registrador. (Os desvios também afetam a parte da busca de instrução do caminho de dados, como veremos em breve.) A Figura 4.9 mostra a estrutura do segmento do caminho de dados que lida com os desvios. Para
FIGURA 4.9 O caminho de dados para um desvio usa a ALU a fim de avaliar a condição de desvio e um somador separado para calcular o destino do desvio como a soma do PC incrementado e os 16 bits mais baixos da instrução com sinal estendido (o deslocamento do desvio), deslocados de 2 bits para a esquerda. A unidade rotulada como Deslocamento de 2 à esquerda é simplesmente um direcionamento dos sinais entre entrada e saída que acrescenta 00bin à extremidade da direita do campo offset com sinal estendido; nenhum hardware de deslocamento real é necessário, já que a quantidade de “deslocamento” é constante. Como sabemos que o offset teve o sinal dos seus 16 bits estendido, o deslocamento irá descartar apenas “bits de sinal”. A lógica de controle é usada para decidir se o PC ou o destino do desvio incrementado deve substituir o PC, com base na saída Zero da ALU.
endereço de destino do desvio O endereço especificado em um desvio, que se torna o novo contador do programa (PC) se o desvio for tomado. Na arquitetura MIPS, o destino do desvio é dado pela soma do campo offset da instrução e o endereço da instrução seguinte ao desvio.
desvio tomado Um desvio em que a condição de desvio é satisfeita, e o contador do programa (PC) se torna o destino do desvio. Todos os desvios incondicionais são desvios tomados.
desvio não tomado Um desvio em que a condição de desvio é falsa e o contador do programa (PC) se torna o endereço da instrução que acompanha sequencialmente o desvio.
252
Capítulo 4 O Processador
calcular o endereço de destino do desvio, o caminho de dados de desvio inclui uma unidade de extensão de sinal, exatamente como a da Figura 4.8, e um somador. Para realizar a comparação, precisamos usar o banco de registradores mostrado na Figura 4.7a a fim de fornecer os dois operandos (embora não precisemos escrever no banco de registradores). Além disso, a comparação pode ser feita usando a ALU que projetamos no Apêndice C. Como essa ALU fornece um sinal de saída que indica se o resultado era 0, podemos enviar os dois operandos de registrador para a ALU com o conjunto de controle de modo a fazer uma subtração. Se o sinal Zero da ALU estiver ativo, sabemos que os dois valores são iguais. Embora a saída de Zero sempre sinalize quando o resultado é 0, nós a estaremos usando apenas para implementar o teste de igualdade dos desvios. Mais adiante, mostraremos exatamente como conectar os sinais de controle da ALU para uso no caminho de dados. A instrução jump funciona substituindo os 28 bits menos significativos do PC pelos 26 bits menos significativos da instrução deslocados de 2 bits à esquerda. Esse deslocamento é realizado simplesmente concatenando 00 ao offset do jump, como descrito no Capítulo 2. desvio atrasado Um tipo de desvio em que a instrução imediatamente seguinte ao desvio é sempre executada, independente de a condição do desvio ser verdadeira ou falsa.
Detalhamento: No conjunto de instruções MIPS, os desvios são atrasados, isso significa que a instrução imediatamente posterior ao desvio é sempre executada, independente de a condição de desvio ser verdadeira ou falsa. Quando a condição é falsa, a execução se parece com um desvio normal. Quando a condição é verdadeira, um desvio atrasado primeiro executa a instrução imediatamente posterior ao desvio na ordem sequencial antes de desviar para o endereço de destino do desvio. A motivação para os desvios atrasados surge de como o pipelining afeta os desvios (veja a Seção 4.8). Para simplificar, ignoramos os desvios atrasados neste capítulo e implementamos uma instrução beq como não sendo atrasado.
Criando um caminho de dados simples Agora que examinamos os componentes do caminho de dados necessários para as classes de instrução individualmente, podemos combiná-los em um único caminho de dados e acrescentar o controle para completar a implementação. O caminho de dados mais simples pode tentar executar todas as instruções em um único ciclo de clock. Isso significa que nenhum recurso do caminho de dados pode ser usado mais de uma vez por instrução e, portanto, qualquer elemento necessário mais de uma vez precisa ser duplicado. Então, precisamos de uma memória para instruções separada de uma memória para dados. Embora algumas unidades funcionais precisem ser duplicadas, muitos dos elementos podem ser compartilhados por diferentes fluxos de instrução. Para compartilhar um elemento do caminho de dados entre duas classes de instrução diferentes, talvez tenhamos de permitir múltiplas conexões com a entrada de um elemento usando um multiplexador e um sinal de controle para selecionar entre as múltiplas entradas.
EXEMPLO
Construindo um caminho de dados
As operações do caminho de dados das instruções lógicas e aritméticas (ou tipo R) e das instruções de acesso à memória são muito semelhantes. As principais diferenças são as seguintes: j
As instruções lógicas e aritméticas usam a ALU com as entradas vindas de dois registradores. As instruções de acesso à memória também podem usar a ALU para fazer o cálculo do endereço, embora a segunda entrada seja o campo offset de 16 bits com sinal estendido da instrução.
j
O valor armazenado em um registrador de destino vem da ALU (para uma instrução tipo R) ou da memória (para um load).
Mostre como construir um caminho de dados para a parte operacional das instruções de acesso à memória e das instruções lógicas e aritméticas, que use um
4.3 Construindo um caminho de dados 253
único banco de registradores e uma única ALU para manipular os dois tipos de instrução, incluindo quaisquer multiplexadores necessários. Para criar um caminho de dados com apenas um único banco de registradores e uma única ALU, precisamos suportar duas origens diferentes para a segunda entrada da ALU, bem como duas origens diferentes para os dados armazenados no banco de registradores. Portanto, um multiplexador é colocado na entrada da ALU e outro na entrada de dados para o banco de registradores. A Figura 4.10 mostra a parte operacional do caminho de dados combinado.
RESPOSTA
FIGURA 4.10 O caminho de dados para as instruções de acesso à memória e as instruções tipo R. Este exemplo mostra como um único caminho de dados pode ser montado das partes nas Figuras 4.7 e 4.8 acrescentando multiplexadores. Dois multiplexadores são necessários, como descrito no exemplo.
Agora, podemos combinar todas as partes de modo a criar um caminho de dados simples para a arquitetura MIPS incluindo um caminho de dados para busca de instruções (Figura 4.6), o caminho de dados das instruções tipo R e de acesso à memória (Figura 4.10) e o caminho de dados para desvios (Figura 4.9). A Figura 4.11 mostra o caminho de dados que obtemos compondo as partes separadas. A instrução de desvio usa a ALU principal para comparação dos registradores operandos, de modo que precisamos manter o somador da Figura 4.9 a fim de calcular o endereço de destino do desvio. Um multiplexador adicional é necessário para selecionar o endereço de instrução seguinte (PC + 4) ou o endereço de destino do desvio a ser escrito no PC. Agora que completamos este caminho de dados simples, podemos acrescentar a unidade de controle. A unidade de controle precisa ser capaz de ler entradas e gerar um sinal de escrita para cada elemento de estado, o controle seletor de cada multiplexador e o controle da ALU. O controle da ALU é diferente de várias maneiras e será útil projetá-lo primeiro, antes de projetarmos o restante da unidade de controle. I. Qual das seguintes afirmativas é correta para uma instrução load? Consulte a Figura 4.10. a. MemparaReg deve ser definido para fazer com que os dados da memória sejam enviados ao banco de registradores.
Verifique você mesmo
254
Capítulo 4 O Processador
FIGURA 4.11 O caminho de dados simples para a arquitetura MIPS combina os elementos necessários para diferentes classes de instrução. Os componentes vêm das Figuras 4.6, 4.9 e 4.10. Este caminho de dados pode executar as instruções básicas (load-store word, operações da ALU e desvios) em um único ciclo de clock. Um multiplexador adicional é necessário para integrar os desvios. O suporte para jumps será incluído mais tarde.
b. MemparaReg deve ser definido para fazer com que o registrador de destino correto seja enviado ao banco de registradores. c. Não precisamos nos importar com MemparaReg para loads. II. O caminho de dados de ciclo único descrito conceitualmente nesta seção precisa ter memórias de instrução e dados separadas, porque: a. os formatos dos dados e das instruções são diferentes no MIPS, e, portanto, memórias diferentes são necessárias. b. ter memórias separadas é menos dispendioso. c. o processador opera em um ciclo e não pode usar uma memória de porta simples para dois acessos diferentes dentro desse ciclo.
4.4 Um esquema de implementação simples Nesta seção, veremos o que poderia ser considerado a implementação mais simples possível do nosso subconjunto MIPS. Construímos essa implementação simples usando o caminho de dados da última seção e acrescentando uma função de controle simples. Essa implementação simples cobre as instruções load word (lw), store word (sw), branch equal (beq) e as instruções lógicas e aritméticas add, sub, AND, OR e set on less than. Posteriormente, desenvolveremos o projeto para incluir uma instrução jump (j).
O controle da ALU A ALU MIPS no controle:
Apêndice C define as 6 combinações a seguir das nossas entradas de
4.4 Um esquema de implementação simples 255
Linhas de controle da ALU
Função
0000
AND
0001
OR
0010
add
0110
subtract
0111
set on less than
1100
NOR
Dependendo da classe de instrução, a ALU precisará realizar uma dessas cinco primeiras funções. (NOR é necessária para outras partes do conjunto de instruções MIPS não encontradas no subconjunto que estamos implementando.) Para as instruções load word e store word, usamos a ALU para calcular o endereço de memória por adição. Para instruções tipo R, a ALU precisa realizar uma das cinco ações (AND, OR, subtract, add ou set on less than), dependendo do valor do campo funct (ou function – função) de 6 bits nos bits menos significativos da instrução (veja o Capítulo 2). Para branch equal, a ALU precisa realizar uma subtração. Podemos gerar a entrada do controle da ALU de 4 bits usando uma pequena unidade de controle que tenha como entradas o campo funct da instrução e um campo control de 2 bits, que chamamos de OpALU. OpALU indica se a operação a ser realizada deve ser add (00) para loads e stores, subtract (01) para beq ou determinada pela operação codificada no campo funct (10). A saída da unidade de controle da ALU é um sinal de 4 bits que controla diretamente a ALU gerando uma das combinações de 4 bits mostradas anteriormente. Na Figura 4.12, mostramos como definir as entradas do controle da ALU com base no controle OpALU de 2 bits e no código de função de 6 bits. Mais adiante neste capítulo, veremos como os bits de OpALU são gerados na unidade de controle principal.
FIGURA 4.12 A forma como os bits de controle da ALU são definidos depende dos bits de controle de OpALU e dos diferentes códigos de função para as instruções tipo R. O opcode, que aparece na primeira coluna, determina a definição dos bits de OpALU. Todas as codificações são mostradas em binário. Observe que quando o código de OpALU é 00 ou 01, a ação da ALU desejada não depende do campo de código funct; nesse caso, dizemos que “não nos importamos” (don’t care) com o valor do código de função e o campo funct é mostrado como XXXXXX. Quando o valor de OpALU é 10, então o código de função é usado para definir a entrada do controle da ALU. Veja o Apêndice C.
Esse estilo de usar vários níveis de decodificação – ou seja, a unidade de controle principal gera os bits de OpALU, que, então, são usados como entrada para o controle da ALU que gera os sinais reais para controlar a ALU – é uma técnica de implementação comum. Usar níveis múltiplos de controle pode reduzir o tamanho da unidade de controle principal. Usar várias unidades de controle menores também pode aumentar a velocidade da unidade de controle. Essas otimizações são importantes, pois a velocidade da unidade de controle normalmente é essencial para o tempo de ciclo de clock.
256
tabela verdade Pela lógica, uma representação de uma operação lógica listando todos os valores das entradas e em seguida, em cada caso, mostrando quais deverão ser as saídas resultantes.
termo don’t care Um elemento de uma função lógica em que a saída não depende dos valores de todas as entradas. Os termos don’t care podem ser especificados de diversas maneiras.
Capítulo 4 O Processador
Há várias maneiras diferentes de implementar o mapeamento do campo OpALU de 2 bits e do campo funct de 6 bits para os 3 bits de controle de operação da ALU. Como apenas um pequeno número dos 64 valores possíveis do campo funct são de interesse e o campo funct é usado apenas quando os bits de OpALU são iguais a 10, podemos usar uma pequena lógica que reconhece o subconjunto dos valores possíveis e faz a definição correta dos bits de controle da ALU. Como uma etapa no projeto dessa lógica, é útil criar uma tabela verdade para as combinações interessantes do campo de código funct e dos bits de OpALU, como fizemos na Figura 4.13; essa tabela verdade mostra como o controle da ALU de 3 bits é definido de acordo com esses dois campos de entrada. Como a tabela verdade inteira é muito grande (28 = 256 entradas) e não nos importamos com o valor do controle da ALU para muitas dessas combinações de entrada, mostramos apenas as entradas para as quais o controle da ALU precisa ter um valor específico. Em todo este capítulo, usaremos essa prática de mostrar apenas as entradas da tabela verdade que precisam ser declaradas e não mostrar as que estão zeradas ou que não nos interessam. (Essa prática possui uma desvantagem, que discutimos na Seção D.2 do Apêndice D.) Como, em muitos casos, não nos interessamos pelos valores de algumas das entradas e para mantermos as tabelas compactas, também incluímos termos don’t care. Um termo don’t care nessa tabela verdade (representado por um X em uma coluna de entrada) indica que a saída não depende do valor da entrada correspondente a essa coluna. Por exemplo, quando os bits de OpALU são 00, como na primeira linha da tabela na Figura 4.13, sempre definimos o controle da ALU em 010, independente do código funct. Nesse caso, então, as entradas do código funct serão don’t care nessa linha da tabela verdade. Depois, veremos exemplos de outro tipo de termo don’t care. Se você não estiver familiarizado com o conceito de termos don’t care, veja o Apêndice C para obter mais informações.
FIGURA 4.13 A tabela verdade para os 4 bits de controle da ALU (chamados Operação). As entradas são OpALU e o campo de código funct. Apenas as entradas para as quais o controle da ALU é ativado são mostradas. Algumas entradas don’t care foram incluídas. Por exemplo, como OpALU não usa a codificação 11, a tabela verdade pode conter entradas 1X e X1, em vez de 10 e 01. Além disso, quando o campo funct é usado, os dois primeiros bits (F5 e F4) dessas instruções são sempre 10; portanto, eles são termos don’t care e são substituídos por XX na tabela verdade.
Uma vez construída a tabela, ela pode ser otimizada e depois transformada em portas lógicas. Esse processo é completamente mecânico. Portanto, em vez de mostrar as etapas finais aqui, descrevemos o processo e o resultado na Seção D.2 do Apêndice D.
Projetando a unidade de controle principal Agora que descrevemos como projetar uma ALU que usa o código de função e um sinal de 2 bits como suas entradas de controle, podemos voltar a considerar o restante do controle. Para começar esse processo, vamos identificar os campos de uma instrução e as linhas de controle necessárias para o caminho de dados construído na Figura 4.11. A fim de entender como conectar os campos de uma instrução com o caminho de dados, é útil
4.4 Um esquema de implementação simples 257
FIGURA 4.14 As três classes de instrução (tipo R, acesso à memória e desvio) usam dois formatos de instrução diferentes. As instruções jump usam outro formato, que será discutido em breve. (a) Formato de instrução para instruções tipo R, as quais possuem todas opcode 0. Essas instruções possuem três registradores como operandos: rs, rt e rd. Os campos rs e rt são origens e rd é o destino. A função da ALU está no campo funct e é decodificada pelo projeto de controle da ALU da seção anterior. As instruções tipo R que implementamos são add, sub, AND, OR e slt. O campo shamt é usado apenas para deslocamentos; nós o ignoraremos neste capítulo. (b) Formato de instrução para instruções load (opcode = 35dec) e store (opcode = 43dec). O registrador rs é o registrador de base adicionado ao campo address de 16 bits de modo a formar o endereço de memória. Com os loads, rt é o registrador de destino para o valor lido. Com stores, rt é o registrador de origem cujo valor deve ser armazenado na memória. (c) Formato de instrução para branch equal (opcode = 4). Os registradores rs e rt são os registradores de origem que são comparados para igualdade. O campo address de 16 bits tem seu sinal estendido, é deslocado e somado ao PC para calcular o endereço de destino do desvio.
examinar os formatos das três classes de instrução: as instruções tipo R, as instruções de desvio e as instruções de acesso à memória. A Figura 4.14 mostra esses formatos. Existem várias observações importantes sobre esses formatos de instrução em que nos basearemos: j
Campo op, também chamado opcode, está sempre contido nos bits 31:26. Iremos nos referir a esse campo como Op[5:0].
j
Os dois registradores a serem lidos sempre são especificados pelos campos rs e rt, nas posições 25:21 e 20:16. Isso é verdade para as instruções tipo R, branch equal e store.
j
Registrador de base para as instruções load e store está sempre nas posições de bit 25:21 (rs).
j
Offset de 16 bits para branch equal, load e store está sempre nas posições 15:0.
j
Registrador de destino está em um de dois lugares. Para um load, ele está nas posições 20:16 (rt), enquanto para uma instrução tipo R, ele está nas posições 15:11 (rd). Portanto, precisaremos incluir um multiplexador a fim de selecionar que campo da instrução será usado para indicar o número de registrador a ser escrito.
O primeiro princípio de projeto do Capítulo 2 — a simplicidade favorece a regularidade — vale aqui na especificação do controle. Usando essas informações, podemos acrescentar os rótulos de instrução e o multiplexador extra (para a entrada Registrador para escrita do banco de registradores) no caminho de dados simples. A Figura 4.15 mostra essas adições, além do bloco de controle da ALU, os sinais de escrita para elementos de estado, o sinal de leitura para a memória de dados e os sinais de controle para os multiplexadores. Como todos os multiplexadores possuem duas entradas, cada um deles requer uma única linha de controle. A Figura 4.15 mostra sete linhas de controle de um único bit mais o sinal de controle OpALU de 2 bits. Já definimos como o sinal de controle OpALU funciona e é útil definir o que fazem os outros sete sinais de controle informalmente antes de determinarmos como definir esses sinais de controle durante a execução da instrução. A Figura 4.16 descreve a função dessas sete linhas de controle.
opcode O campo que denota a operação e o formato de uma instrução.
258
Capítulo 4 O Processador
FIGURA 4.15 O caminho de dados da Figura 4.12 com todos os multiplexadores necessários e todas as linhas de controle identificadas. As linhas de controle são mostradas em cor. O bloco de controle da ALU também foi acrescentado. O PC não exige um controle de escrita, já que ele é escrito uma vez no fim de cada ciclo de clock; a lógica de controle de desvio determina se ele é escrito com o PC incrementado ou o endereço de destino do desvio.
Agora que examinamos a função de cada um dos sinais de controle, podemos ver como defini-los. A unidade de controle pode definir todos menos um dos sinais de controle unicamente com base no campo opcode da instrução. A exceção é a linha de controle OrigPC. Essa linha de controle deve ser ativada se a instrução for branch on equal (uma decisão que a unidade de controle pode tomar) e a saída Zero da ALU, usada para comparação de igualdade, for verdadeira. Para gerar o sinal OrigPC, precisaremos realizar um AND de um sinal da unidade de controle, que chamamos Branch, com o sinal Zero da ALU. Esses nove sinais de controle (sete da Figura 4.16 e dois para OpALU) podem agora ser definidos baseados nos seis sinais de entrada da unidade de controle, que são os bits de opcode 31 a 26. A Figura 4.17 mostra o caminho de dados com a unidade de controle e os sinais de controle. Antes de tentarmos escrever um conjunto de equações ou uma tabela verdade para a unidade de controle, será útil definir a função de controle informalmente. Como a definição das linhas de controle depende apenas do opcode, definimos se cada sinal de controle deve ser 0, 1 ou don’t care (X) para cada um dos valores de opcode. A Figura 4.18 descreve como os sinais de controle devem ser definidos para cada opcode; essas informações seguem diretamente das Figuras 4.12, 4.16 e 4.17. Operação do caminho de dados
Com as informações contidas nas Figuras 4.16 e 4.18, podemos projetar a lógica da unidade de controle. Antes de fazer isso, porém, vejamos como cada instrução usa o caminho de dados. Nas próximas figuras, mostramos o fluxo das três classes de instrução diferentes por meio do caminho de dados. Os sinais de controle ativos e os elementos do caminho de dados ativos são destacados em cada uma das figuras. Observe que um multiplexador cujo controle é 0 tem uma ação definida, mesmo se sua linha de controle não estiver destacada. Sinais de controle de vários bits são destacados se qualquer sinal constituinte estiver ativo. A Figura 4.19 mostra a operação do caminho de dados para uma instrução tipo R, como add $t1,$t2,$t3. Embora tudo ocorra em um ciclo de clock, podemos pensar em quatro etapas para executar a instrução; essas etapas são ordenadas pelo fluxo da informação:
4.4 Um esquema de implementação simples 259
FIGURA 4.16 O efeito de cada um dos sete sinais de controle. Quando o controle de bit 1 de largura, para um multiplexador com duas entradas, está ativo, o multiplexador seleciona a entrada correspondente a 1. Caso contrário, se o controle não estiver ativo, o multiplexador seleciona a entrada 0. Lembre-se de que todos os elementos de estado têm o clock como uma entrada implícita e que o clock é usado para controlar escritas. O clock nunca vem externamente para um elemento de estado, já que isso pode criar problemas de sincronização. (Veja o Apêndice C para obter mais detalhes sobre esse problema.)
FIGURA 4.17 O caminho de dados simples com a unidade de controle. A entrada para a unidade de controle é o campo opcode de 6 bits da instrução. As saídas da unidade de controle consistem em três sinais de 1 bit usados para controlar multiplexadores (RegDst, OrigALU e MemparaReg), três sinais para controlar leituras e escritas no banco de registradores e na memória de dados (EscreveReg, LeMem e EscreveMem), um sinal de 1 bit usado na determinação de um possível desvio (Branch), e um sinal de controle de 2 bits para a ALU (OpALU). Uma porta AND é usada de modo a combinar o sinal de controle de desvio com a saída Zero da ALU; a saída da porta AND controla a seleção do próximo PC. Observe que OrigPC é agora um sinal derivado, em vez de um sinal vindo diretamente da unidade de controle. Portanto, descartamos o nome do sinal nas próximas figuras.
260
Capítulo 4 O Processador
FIGURA 4.18 A definição das linhas de controle é completamente determinada pelos campos opcode da instrução. A primeira linha da tabela corresponde às instruções formato R (add, sub, AND, OR e slt). Para todas essas instruções, os campos registradores de origem são rs e rt, e o campo registrador de destino é rd; isso especifica como os sinais OrigALU e RegDst são definidos. Além disso, uma instrução tipo R escreve em um registrador (EscreveReg = 1), mas não escreve ou lê a memória de dados. Quando o sinal de controle Branch é 0, o PC é incondicionalmente substituído por PC + 4; caso contrário, o PC é substituído pelo destino do desvio se a saída Zero da ALU também está ativa. O campo OpALU para as instruções tipo R é definido como 10 a fim de indicar que o controle da ALU deve ser gerado do campo funct. A segunda e a terceira linhas dessa tabela fornecem as definições dos sinais de controle para lw e sw. Esses campos OrigALU e OpALU são ativados para realizar o cálculo do endereço. LeMem e EscreveMem são ativados para realizar o acesso à memória. Finalmente, RegDst e EscreveReg são ativados para que um load faça o resultado ser armazenado no registrador rt. A instrução branch é semelhante à operação no formato R, já que ela envia os registradores rs e rt para a ALU. O campo OpALU para um desvio é definido como uma subtração (controle da ALU = 01), usada para testar a igualdade. Repare que o campo MemparaReg é irrelevante quando o sinal EscreveReg é 0: como o registrador não está sendo escrito, o valor dos dados na entrada Dados para escrita do banco de registradores não é usado. Portanto, a entrada MemparaReg nas duas últimas linhas da tabela é substituída por X (don’t care). Os don’t care também podem ser adicionados a RegDst quando EscreveReg é 0. Esse tipo de don’t care precisa ser acrescentado pelo projetista, uma vez que ele depende do conhecimento de como o caminho de dados funciona.
FIGURA 4.19 O caminho de dados em operação para uma instrução tipo R como add $t1,$t2,$t3. As linhas de controle, as unidades do caminho de dados e as conexões que estão ativas aparecem destacadas.
1. A instrução é buscada e o PC é incrementado. 2. Dois registradores, $t2 e $t3, são lidos do banco de registradores, e a unidade de controle principal calcula a definição das linhas de controle também durante essa etapa. 3. A ALU opera nos dados lidos do banco de registradores, usando o código de função (bits 5:0, que é o campo funct, da instrução) para gerar a função da ALU. 4. O resultado da ALU é escrito no banco de registradores usando os bits 15:11 da instrução para selecionar o registrador de destino ($t1).
4.4 Um esquema de implementação simples 261
Da mesma forma, podemos ilustrar a execução de um load word, como lw $t1, offset($t2)
em um estilo semelhante à Figura 4.19. A Figura 4.20 mostra as unidades funcionais ativas e as linhas de controle ativas para um load. Podemos pensar em uma instrução load como operando em cinco etapas (semelhante ao tipo R executado em quatro):
FIGURA 4.20 O caminho de dados em operação para uma instrução load. As linhas de controle, as unidades do caminho de dados e as conexões ativas aparecem destacadas. Uma instrução store operaria de maneira muito semelhante. A principal diferença seria que o controle da memória indicaria uma escrita em vez de uma leitura, a segunda leitura do valor de um registrador seria usada para os dados a serem armazenados e a operação de escrita do valor da memória de dados no banco de registradores não ocorreria.
1. Uma instrução é buscada da memória de instruções e o PC é incrementado. 2. Um valor de registrador ($t2) é lido do banco de registradores. 3. A ALU calcula a soma do valor lido do banco de registradores com os 16 bits menos significativos com sinal estendido da instrução (offset). 4. A soma da ALU é usada como o endereço para a memória de dados. 5. Os dados da unidade de memória são escritos no banco de registradores; o registrador de destino é fornecido pelos bits 20:16 da instrução ($t1). Finalmente, podemos mostrar a operação da instrução branch-on-equal, como beq $t1,$t2,offset, da mesma maneira. Ela opera de forma muito parecida com uma instrução de formato R, mas a saída da ALU é usada para determinar se o PC é escrito
262
Capítulo 4 O Processador
FIGURA 4.21 O caminho de dados em operação para uma instrução branch equal. As linhas de controle, as unidades do caminho de dados e as conexões que estão ativas aparecem destacadas. Após usar o banco de registradores e a ALU para realizar a comparação, a saída Zero é usada na seleção do próximo contador de programa dentre os dois candidatos.
com PC + 4 ou o endereço de destino do desvio. A Figura 4.21 mostra as quatro etapas da execução: 1. Uma instrução é buscada da memória de instruções e o PC é incrementado. 2. Dois registradores, $t1 e $t2, são lidos do banco de registradores. 3. A ALU realiza uma subtração dos valores de dados lidos do banco de registradores. O valor de PC + 4 é somado aos 16 bits menos significativos com sinal estendido (offset) deslocados de dois para a esquerda; o resultado é o endereço de destino do desvio. 4. O resultado Zero da ALU é usado para decidir qual resultado do somador deve ser armazenado no PC. Finalizando o controle
implementação de ciclo único Também chamada de implementação de ciclo de clock único. Uma implementação em que uma instrução é executada em um único ciclo de clock.
Agora que vimos como as instruções operam em etapas, vamos continuar com a implementação do controle. A função de controle pode ser definida precisamente usando o conteúdo da Figura 4.18. As saídas são as linhas de controle, e a entrada é o campo opcode de 6 bits, Op [5:0]. Portanto, podemos criar uma tabela verdade para cada uma das saídas com base na codificação binária dos opcodes. A Figura 4.22 mostra a lógica na unidade de controle como uma grande tabela verdade que combina todas as saídas e que usa os bits de opcode como entradas. Ela especifica completamente a função de controle, e podemos implementá-la diretamente em portas lógicas de uma maneira automatizada. Mostramos essa etapa final na Seção D.2 no Apêndice D. Agora que temos uma implementação de ciclo único da maioria do conjunto de instruções MIPS básico, vamos acrescentar a instrução jump para mostrar como o caminho de dados básico e o controle podem ser estendidos ao lidar com outras instruções no conjunto de instruções.
4.4 Um esquema de implementação simples 263
FIGURA 4.22 A função de controle para a implementação de ciclo único simples é completamente especificada por essa tabela verdade. A parte superior da tabela fornece combinações de sinais de entrada que correspondem aos quatro opcodes que determinam as definições de saída do controle. (Lembre-se de que Op [5:0] corresponde aos bits 31:26 da instrução, que é o campo op.) A parte inferior da tabela fornece as saídas. Portanto, a saída EscreveReg é ativada para duas combinações diferentes das entradas. Se considerarmos apenas os quatro opcodes mostrados nessa tabela, então, poderemos simplificar a tabela verdade usando don’t care na parte da entrada. Por exemplo, podemos detectar uma instrução no formato R com a expressão Op5 • Op2, uma vez que isso é suficiente para distinguir as instruções no formato R das instruções lw, sw e beq. Não tiramos vantagem dessa simplificação, já que o restante dos opcodes MIPS é usado em uma implementação completa.
Implementando jumps
A Figura 4.17 mostra a implementação de muitas das instruções vistas no Capítulo 2. Uma classe de instruções ausente é a da instrução jump. Estenda o caminho de dados e o controle da Figura 4.17 para incluir a instrução jump. Descreva como definir quaisquer novas linhas de controle. A instrução jump, mostrada na Figura 4.23, se parece um pouco com uma instrução branch, mas calcula o PC de destino de maneira diferente e não é condicional. Como um branch, os 2 bits menos significativos de um endereço jump são sempre 00bin. Os próximos 26 bits menos significativos desse endereço de 32 bits vêm do campo imediato de 26 bits na instrução. Os 4 bits superiores do endereço que deve substituir o PC vêm do PC da instrução jump mais 4. Portanto, podemos implementar um jump armazenando no PC a concatenação de: j
os 4 bits superiores do PC atual + 4 (esses são bits 31:28 do endereço da instrução imediatamente seguinte);
j
campo de 26 bits imediato da instrução jump;
j
os bits 00bin.
A Figura 4.24 mostra a adição do componente para jump à Figura 4.17. Um outro multiplexador é usado na seleção da origem para o novo valor do PC, que pode ser o PC incrementado (PC + 4), o PC de destino de um branch ou o PC de destino de um jump. Um sinal de controle adicional é necessário para o multiplexador adicional. Esse sinal de controle, chamado Jump, é ativado apenas quando a instrução é um jump – ou seja, quando o opcode é 2.
EXEMPLO
RESPOSTA
264
Capítulo 4 O Processador
FIGURA 4.23 Formato de instrução para a instrução jump (opcode = 2). O endereço de destino para uma instrução jump é formado pela concatenação dos 4 bits superiores do PC atual + 4 com o campo endereço de 26 bits na instrução jump e pela adição de 00 como os dois bits menos significativos.
FIGURA 4.24 O controle e o caminho de dados simples são estendidos para lidar com a instrução jump. Um multiplexador adicional (no canto superior direito) é usado na escolha entre o destino de um jump e o destino de um desvio ou a instrução sequencial seguinte a esta. Esse multiplexador é controlado pelo sinal de controle Jump. O endereço de destino do jump é obtido deslocando-se os 26 bits inferiores da instrução jump de 2 bits para a esquerda, efetivamente adicionando 00 como os bits menos significativos, e, depois, concatenando os 4 bits mais significativos do PC + 4 como os bits mais significativos, produzindo, assim, um endereço de 32 bits.
Por que uma implementação de ciclo único não é usada hoje Embora o projeto de ciclo único funcionasse corretamente, ele não seria usado nos projetos modernos porque é ineficiente. Para ver o porquê disso, observe que o ciclo de clock precisa ter a mesma duração para cada instrução nesse projeto de ciclo único. É claro, o ciclo de clock é determinado pelo caminho mais longo possível no processador. Esse caminho é, quase certamente, uma instrução load, que usa cinco unidades funcionais em série: a memória de instruções, o banco de registradores, a ALU, a memória de dados e o banco de registradores. Embora o CPI seja 1 (veja o Capítulo 1), o desempenho geral de uma implementação de ciclo único provavelmente será pobre, já que o ciclo de clock é muito longo. O ônus de usar o projeto de ciclo único com um ciclo de clock fixo é significativo, mas poderia ser considerado aceitável para esse conjunto de instruções pequeno. Historicamente, os primeiros computadores com conjuntos de instruções muito simples usavam essa tecnologia de implementação. Entretanto, se tentássemos implementar a unidade de ponto
4.5 Visão geral de pipelining 265
flutuante ou um conjunto de instruções com instruções mais complexas, esse projeto de ciclo único decididamente não funcionaria bem. Como precisamos considerar que o ciclo de clock é igual ao atraso de pior caso para todas as instruções, não podemos usar técnicas de implementação que reduzem o atraso do caso comum, mas não melhoram o tempo de ciclo de pior caso. Uma implementação de ciclo único, portanto, viola o nosso princípio básico de projeto do Capítulo 2 de tornar o caso comum mais rápido. Na próxima seção, veremos outra técnica de implementação, chamada pipelining, que usa um caminho de dados muito semelhante ao caminho de dados de ciclo único, mas é muito mais eficiente por ter uma vazão muito mais alta. O pipelining melhora a eficiência executando múltiplas instruções simultaneamente. Veja os sinais de controle na Figura 4.22. Você consegue combinar alguns deles? Algum sinal de controle na figura pode ser substituído pelo inverso de outro? (Dica: leve em conta os don’t care.) Nesse caso, você pode usar um sinal para o outro sem incluir um inversor?
4.5 Visão geral de pipelining Pipelining é uma técnica de implementação em que várias instruções são sobrepostas na execução. Hoje, a técnica de pipelining é praticamente universal. Esta seção utiliza bastante uma analogia para dar uma visão geral dos termos e aspectos da técnica de pipelining. Se você estiver interessado apenas no quadro geral, deverá se concentrar nesta seção e depois pular para as Seções 4.10 e 4.11, a fim de ver uma introdução às técnicas de pipelining avançadas, utilizadas nos processadores mais recentes, como o AMD Opteron X4 (Barcelona) ou Intel Core. Se estiver interessado em explorar a anatomia de um computador com pipeline, esta seção é uma boa introdução às Seções de 4.6 a 4.9. Qualquer um que tenha lavado muitas roupas intuitivamente já usou pipelining. A técnica sem pipeline para lavar roupas seria 1. Colocar a trouxa de roupa suja na lavadora. 2. Quando a lavadora terminar, colocar a trouxa de roupa molhada na secadora (se houver). 3. Quando a secadora terminar, colocar a trouxa de roupa seca na mesa e passar. 4. Quando terminar de passar, pedir ao seu colega de quarto para guardar as roupas. Quando seu colega terminar, então comece novamente com a próxima trouxa de roupa suja. A técnica com pipeline leva muito menos tempo, como mostra a Figura 4.25. Assim que a lavadora terminar com a primeira trouxa e ela for colocada na secadora, você carrega a lavadora com a segunda trouxa de roupa suja. Quando a primeira trouxa estiver seca, você a coloca na tábua para começar a passar e dobrar, move a trouxa de roupa molhada para a secadora e a próxima trouxa de roupa suja para a lavadora. Em seguida, você pede a seu colega para guardar a primeira remessa, começa a passar e dobrar a segunda, a secadora está com a terceira remessa e você coloca a quarta na lavadora. Nesse ponto, todas as etapas – denominadas estágios em pipelining – estão operando simultaneamente. Desde que haja recursos separados para cada estágio, podemos usar um pipeline para as tarefas. O paradoxo da técnica de pipelining é que o tempo desde a colocação de uma única trouxa de roupa suja na lavadora até que ela esteja seca, e seja passada e guardada não é mais curto (o processo) para a técnica de pipelining; o motivo pelo qual a técnica de pipelining é mais rápida para muitas trouxas é que tudo está trabalhando em paralelo, de modo que mais trouxas são terminadas por hora. A técnica de pipelining melhora a vazão do sistema de lavanderia sem melhorar o tempo para concluir uma única trouxa. Logo, a técnica de pipelining não diminuiria o tempo para concluir uma trouxa de roupas, mas, quando temos muitas trouxas para lavar, a melhoria na vazão diminui o tempo total de conclusão do trabalho.
Verifique você mesmo
Nunca perca tempo. Provérbio americano pipelining Uma técnica de implementação em que várias instruções são sobrepostas na execução, semelhante a uma linha de montagem.
266
Capítulo 4 O Processador
FIGURA 4.25 A analogia da lavagem de roupas para pipelining. Ana, Beto, Catarina e Davi possuem roupas sujas para serem lavadas, secadas, passadas e guardadas. O lavador, o secador, o passador e o guardador levam 30 minutos para sua tarefa. A lavagem sequencial levaria oito horas para quatro trouxas de roupas, enquanto a lavagem com pipeline levaria apenas 3,5 horas. Mostramos o estágio do pipeline de diferentes trouxas com o passar do tempo mostrando cópias dos quatro recursos nessa linha de tempo bidimensional, mas na realidade temos apenas um de cada recurso.
Se todos os estágios levarem aproximadamente o mesmo tempo e houver trabalho suficiente para realizar, então o ganho de velocidade devido à técnica de pipelining será igual ao número de estágios do pipeline, neste caso, quatro: lavar, secar, passar e guardar. Assim, a lavanderia com pipeline é potencialmente quatro vezes mais rápida do que a sem pipeline: 20 trouxas levariam cerca de cinco vezes o tempo de uma trouxa, enquanto 20 trouxas de lavagem sequencial levariam 20 vezes o tempo de uma trouxa. O ganho foi de apenas 2,3 vezes na Figura 4.25 porque mostramos apenas quatro trouxas. Observe que, no início e no final da carga de trabalho na versão com pipeline da Figura 4.25, o pipeline não está completamente cheio. Esse efeito no início e no fim afeta o desempenho quando o número de tarefas não é grande em comparação com a quantidade de estágios do pipeline. Se o número de trouxas for muito maior que 4, então os estágios estarão cheios na maior parte do tempo e o aumento na vazão será muito próximo de 4. Os mesmos princípios se aplicam a processadores em que usamos pipeline para a execução da instrução. As instruções MIPS normalmente exigem cinco etapas: 1. Buscar instrução da memória. 2. Ler registradores enquanto a instrução é decodificada. O formato das instruções MIPS permite que a leitura e a decodificação ocorram simultaneamente. 3. Executar a operação ou calcular um endereço. 4. Acessar um operando na memória de dados. 5. Escrever o resultado em um registrador. Logo, o pipeline MIPS que exploramos neste capítulo possui cinco estágios. O exemplo a seguir mostra que a técnica de pipelining agiliza a execução da instrução, assim como agiliza a lavagem de roupas.
4.5 Visão geral de pipelining 267
Desempenho de ciclo único versus desempenho com pipeline
Para tornar esta discussão concreta, vamos criar um pipeline. Neste exemplo, e no restante deste capítulo, vamos limitar nossa atenção a oito instruções: load word (lw), store word (sw), add (add), subtract (sub), AND (and), OR (or), set-less-than (slt) e branch-on-equal (beq). Compare o tempo médio entre as instruções de uma implementação em ciclo único, em que todas as instruções levam um ciclo de clock, com uma implementação com pipeline. Os tempos de operação para as principais unidades funcionais neste exemplo são de 200ps para acesso à memória, 200ps para operação com ALU e 100ps para leitura ou escrita de registradores. No modelo de ciclo único, cada instrução leva exatamente um ciclo de clock, de modo que o ciclo precisa ser esticado para acomodar a instrução mais lenta. A Figura 4.26 mostra o tempo exigido para cada uma das oito instruções. O projeto de ciclo único precisa contemplar a instrução mais lenta – na Figura 4.26, ela é lw – de modo que o tempo exigido para cada instrução é 800ps. Assim como na Figura 4.25, a Figura 4.27 compara a execução sem pipeline e com pipeline de três instruções load word. Desse modo, o tempo entre a primeira e a quarta instrução no projeto sem pipeline é 3 × 800ns, ou 2.400ps. Todos os estágios do pipeline utilizam um único ciclo de clock, de modo que ele precisa ser grande o suficiente para acomodar a operação mais lenta. Assim como o projeto de ciclo único de clock precisa levar o tempo do ciclo de clock no pior caso, de 800ps, embora algumas instruções possam ser tão rápidas quanto 500ps, o ciclo de clock da execução com pipeline precisa ter o ciclo de clock no pior caso de 200ps, embora alguns estágios levem apenas 100ps. O uso de pipeline ainda oferece uma melhoria de desempenho de quatro vezes: o tempo entre a primeira e a quarta instruções é de 3 × 200ps, ou 600ps.
FIGURA 4.26 Tempo total para cada instrução calculada a partir do tempo para cada componente. Esse cálculo considera que os multiplexadores, unidade de controle, acessos ao PC e unidade de extensão de sinal não possuem atraso.
Agora, podemos converter a discussão sobre ganho de velocidade com a técnica de pipelining em uma fórmula. Se os estágios forem perfeitamente balanceados, então o tempo entre as instruções no processador com pipeline – assumindo condições ideais – é igual a: Tempo entre instruções pipeline =
Tempo entre as instruções com pipeline Número de estágios do pipeline
Sob condições ideais e com uma grande quantidade de instruções, o ganho de velocidade com a técnica de pipelining é aproximadamente igual ao número de estágios do pipeline; um pipeline de cinco estágios é quase cinco vezes mais rápido.
EXEMPLO
RESPOSTA
268
Capítulo 4 O Processador
FIGURA 4.27 Em cima, execução em ciclo único, sem pipeline, versus execução com pipeline (embaixo). Ambas utilizam os mesmos componentes de hardware, cujo tempo está listado na Figura 4.26. Neste caso, vemos um ganho de velocidade de quatro vezes no tempo médio entre as instruções, de 800ps para 200ps. Compare com a Figura 4.25. Para a lavanderia, consideramos que todos os estágios eram iguais. Se a secadora fosse mais lenta, então o estágio da secadora definiria o tempo do estágio. Os tempos de estágio do pipeline dos computadores são limitados pelo recurso mais lento, seja a operação da ALU ou o acesso à memória. Consideramos que a escrita no banco de registradores ocorre na primeira metade do ciclo de clock, e a leitura do banco de registradores ocorre na segunda metade. Usamos essa suposição por todo este capítulo.
A fórmula sugere que um pipeline de cinco estágios deve oferecer uma melhoria de quase cinco vezes sobre o tempo sem pipeline de 800ps, ou um ciclo de clock de 160ps. Entretanto, o exemplo mostra que os estágios podem ser mal balanceados. Além disso, a técnica de pipelining envolve algum overhead, cuja origem se tornará mais clara adiante. Assim, o tempo por instrução no processador com pipeline será superior ao mínimo possível, e o ganho de velocidade será menor que o número de estágios do pipeline. Além do mais, até mesmo nossa afirmação de uma melhoria de quatro vezes para nosso exemplo não está refletida no tempo de execução total para as três instruções: são 1.400ps versus 2.400ps. Naturalmente, isso acontece porque o número de instruções não é grande. O que aconteceria se aumentássemos o número de instruções? Poderíamos estender os valores anteriores para 1.000.003 instruções. Acrescentaríamos 1.000.000 instruções no exemplo com pipeline; cada instrução acrescenta 200ps ao tempo de execução total. O tempo de execução total seria 1.000.000 × 200ps + 1.400ps, ou 200.001.400ps. No exemplo sem pipeline, acrescentaríamos 1.000.000 instruções, cada uma exigindo 800ps, de modo que o tempo de execução total seria 1.000.000 × 800ps + 2.400ps, ou 800.002.400ps. Sob essas condições ideais, a razão entre os tempos de execução total para os programas reais nos processadores sem pipeline e com pipeline é próximo da razão de tempos entre as instruções: 800‚002‚400 ps 800 ps ≈ ≈ 4.00 200‚001‚400 ps 200 ps A técnica de pipelining melhora o desempenho aumentando a vazão de instruções, em vez de diminuir o tempo de execução de uma instrução individual, mas a vazão de instruções é a medida importante, pois os programas reais executam bilhões de instruções.
4.5 Visão geral de pipelining 269
Projetando conjuntos de instruções para pipelining Mesmo com essa explicação simples sobre pipelining, podemos entender melhor o projeto do conjunto de instruções MIPS, projetado para execução com pipeline. Primeiro, todas as instruções MIPS têm o mesmo tamanho. Essa restrição torna muito mais fácil buscar instruções no primeiro estágio do pipeline e decodificá-las no segundo estágio. Em um conjunto de instruções como o x86, no qual as instruções variam de 1 byte a 17 bytes, a técnica de pipelining é muito mais desafiadora. As implementações recentes da arquitetura x86 na realidade traduzem instruções x86 em micro-operações simples, que se parecem com instruções MIPS e depois usam um pipeline de micro-operações no lugar das instruções x86 nativas! (Veja a Seção 4.10.) Em segundo lugar, o MIPS tem apenas alguns poucos formatos de instrução, com os campos de registrador de origem localizados no mesmo lugar em cada instrução. Essa simetria significa que o segundo estágio pode começar a ler o banco de registradores ao mesmo tempo em que o hardware está determinando que tipo de instrução foi lida. Se os formatos de instrução do MIPS não fossem simétricos, precisaríamos dividir o estágio 2, resultando em seis estágios de pipeline. Logo veremos a desvantagem dos pipelines mais longos. Em terceiro lugar, os operandos em memória só aparecem em loads ou stores no MIPS. Essa restrição significa que podemos usar o estágio de execução para calcular o endereço de memória e depois acessar a memória no estágio seguinte. Se pudéssemos operar sobre os operandos na memória, como na arquitetura x86, os estágios 3 e 4 se expandiriam para estágio de endereço, estágio de memória e, em seguida, estágio de execução. Em quarto lugar, conforme discutimos no Capítulo 2, os operandos precisam estar alinhados na memória. Logo, não precisamos nos preocupar com uma única instrução de transferência de dados exigindo dois acessos à memória de dados; os dados solicitados podem ser transferidos entre o processador e a memória em um único estágio do pipeline.
Hazards de pipeline Existem situações em pipelining em que a próxima instrução não pode ser executada no ciclo de clock seguinte. Esses eventos são chamados hazards, e existem três tipos diferentes. Hazards estruturais
O primeiro hazard é chamado hazard estrutural. Ele significa que o hardware não pode admitir a combinação de instruções que queremos executar no mesmo ciclo de clock. Um hazard estrutural na lavanderia aconteceria se usássemos uma combinação lavadora-secadora no lugar de lavadora e secadora separadas, ou se nosso colega estivesse ocupado com outra coisa e não pudesse guardar as roupas. Nosso pipeline, cuidadosamente programado, fracassaria. Como dissemos, o conjunto de instruções MIPS foi projetado para ser executado em um pipeline, tornando muito fácil para os projetistas evitar hazards estruturais quando projetaram o pipeline. Contudo, suponha que tivéssemos uma única memória, em vez de duas. Se o pipeline da Figura 4.27 tivesse uma quarta instrução, veríamos que, no mesmo ciclo de clock em que a primeira instrução está acessando dados da memória, a quarta instrução está buscando uma instrução dessa mesma memória. Sem duas memórias, nosso pipeline poderia ter um hazard estrutural.
hazard estrutural Uma ocorrência em que uma instrução planejada não pode ser executada no ciclo de clock apropriado, pois o hardware não admite a combinação de instruções definidas para executar em determinado ciclo de clock.
Hazards de dados
Os hazards de dados ocorrem quando o pipeline precisa ser interrompido porque uma etapa precisa esperar até que outra seja concluída. Suponha que você tenha encontrado uma meia na mesa de passar para a qual não exista um par. Uma estratégia possível é correr até o seu quarto e procurar em sua gaveta para ver se consegue encontrar o par. Obviamente, enquanto você está procurando, as roupas que ficaram secas e estão prontas para serem passadas, e aquelas que acabaram de ser lavadas e estão prontas para secarem deverão esperar.
hazard de dados Também chamado hazard de dados do pipeline. Uma ocorrência em que uma instrução planejada não pode ser executada no ciclo de clock correto porque os dados necessários para executar a instrução ainda não estão disponíveis.
270
Capítulo 4 O Processador
Em um pipeline de computador, os hazards de dados surgem quando uma instrução depende de uma anterior que ainda está no pipeline (um relacionamento que não existe realmente quando se lavam roupas). Por exemplo, suponha que tenhamos uma instrução add seguida imediatamente por uma instrução subtract que usa a soma ($ s 0):
forwarding Também chamado bypassing. Um método para resolver um hazard de dados utilizando o elemento de dados que falta a partir de buffers internos, em vez de esperar que chegue nos registradores visíveis ao programador ou na memória.
Sem intervenção, um hazard de dados poderia prejudicar o pipeline severamente. A instrução add não escreve seu resultado até o quinto estágio, significando que teríamos de acrescentar três bolhas ao pipeline. Embora pudéssemos contar com compiladores para remover todos esses hazards, os resultados não seriam satisfatórios. Essas dependências acontecem com muita frequência, e o atraso simplesmente é muito longo para se esperar que o compilador nos tire desse dilema. A solução principal é baseada na observação de que não precisamos esperar que a instrução termine antes de tentar resolver o hazard de dados. Para a sequência de código anterior, assim que a ALU cria a soma para o add, podemos fornecê-la como uma entrada para a subtração. O acréscimo de hardware extra para ter o item que falta antes do previsto, diretamente dos recursos internos, é chamado de forwarding ou bypassing.
Forwarding com duas instruções
EXEMPLO
RESPOSTA
Para as duas instruções anteriores, mostre quais estágios do pipeline estariam conectados pelo forwarding. Use o desenho da Figura 4.28 para representar o caminho de dados durante os cinco estágios do pipeline. Alinhe a cópia do caminho de dados para cada instrução, semelhante ao pipeline da lavanderia, na Figura 4.25. A Figura 4.29 mostra a conexão para o forwarding do valor em $s0 após o estágio de execução da instrução add como entrada para o estágio de execução da instrução sub.
FIGURA 4.28 Representação gráfica do pipeline de instrução, semelhante em essência ao pipeline da lavanderia, na Figura 4.25. Aqui, usamos símbolos representando os recursos físicos com as abreviações para estágios de pipeline usados no decorrer do capítulo. Os símbolos para os cinco estágios: IF para o estágio de busca de instrução, com a caixa representando a memória de instrução; ID para o estágio de leitura de decodificação de instrução/banco de registradores, com o desenho mostrando o banco de registradores sendo lido; EX para o estágio de execução, com o desenho representando a ALU; MEM para o estágio de acesso à memória, com a caixa representando a memória de dados; e WB para o estágio write-back, com o desenho mostrando o banco de registradores sendo escrito. O sombreamento indica que o elemento é usado pela instrução. Logo, MEM tem um fundo branco porque add não acessa a memória de dados. O sombreamento na metade direita do banco de registradores ou memória significa que o elemento é lido nesse estágio, e o sombreamento da metade esquerda significa que ele é escrito nesse estágio. Logo, a metade direita do ID é sombreada no segundo estágio porque o banco de registradores é lido, e a metade esquerda do WB é sombreada no quinto estágio, pois o banco de registradores é escrito.
4.5 Visão geral de pipelining 271
FIGURA 4.29 Representação gráfica do forwarding. A conexão mostra o caminho do forwarding desde a saída do estágio EX de add até a entrada do estágio EX para sub, substituindo o valor do registrador $s0 lido no segundo estágio de sub.
Nessa representação gráfica dos eventos, os caminhos de forwarding só são válidos se o estágio de destino estiver mais adiante no tempo do que o estágio de origem. Por exemplo, não pode haver um caminho de forwarding válido da saída do estágio de acesso à memória na primeira instrução para a entrada do estágio de execução da instrução seguinte, pois isso significaria voltar no tempo. O forwarding funciona muito bem e é descrito com detalhes na Seção 4.7. Entretanto, ele não pode impedir todos os stalls do pipeline. Por exemplo, suponha que a primeira instrução fosse um load de $s0 em vez de um add. Como podemos imaginar examinando a Figura 4.29, os dados desejados só estariam disponíveis depois do quarto estágio da primeira instrução na dependência, que é muito tarde para a entrada do terceiro estágio de sub. Logo, até mesmo com o forwarding, teríamos de atrasar um estágio para um hazard de dados no uso de load, como mostra a Figura 4.30. Essa figura mostra um conceito importante de pipeline, conhecido oficialmente como pipeline stall, mas normalmente recebendo o apelido de bolha. Veremos os stalls em outros lugares do pipeline. A Seção 4.7 mostra como podemos tratar de casos assim, usando a detecção de hardware e stalls ou software que reordena o código para evitar stalls de pipeline no uso de load, como este exemplo ilustra.
FIGURA 4.30 Precisamos de um stall até mesmo com forwarding quando uma instrução do formato R após um load tenta usar os dados. Sem o stall, o caminho da saída do estágio de acesso à memória para a entrada do estágio de execução estaria ao contrário no tempo, o que é impossível. Essa figura, na realidade, é uma simplificação, pois não podemos saber, antes que a instrução de subtração seja lida e decodificada, se um stall será necessário. A Seção 4.7 mostra os detalhes do que realmente acontece no caso de um hazard.
hazard de dados no uso de load Uma forma específica de hazard de dados em que os dados solicitados por uma instrução load ainda não estão disponíveis quando requisitados por outra instrução.
pipeline stall Também chamado bolha. Um stall iniciado a fim de resolver um hazard.
272
Capítulo 4 O Processador
Reordenando o código para evitar pipeline stalls
EXEMPLO
Considere o seguinte segmento de código em C: a = b + e; c = b + f;
Aqui está o código MIPS gerado para esse segmento, supondo que todas as variáveis estejam na memória e sejam endereçáveis como offsets a partir de $t0:
Encontre os hazards no segmento de código a seguir e reordene as instruções para evitar quaisquer pipeline stalls.
RESPOSTA
As duas instruções add possuem um hazard, devido à respectiva dependência da instrução lw imediatamente anterior. Observe que o bypassing elimina vários outros hazards em potencial, incluindo a dependência do primeiro add no primeiro lw e quaisquer hazards para instruções store. Subir a terceira instrução lw elimina os dois hazards:
Em um processador com pipeline com forwarding, a sequência reordenada será completada em dois ciclos a menos do que a versão original. O forwarding leva a outro detalhe da arquitetura MIPS, além dos quatro mencionados anteriormente. Cada instrução MIPS escreve no máximo um resultado e faz isso no último estágio do pipeline. O forwarding é mais difícil se houver vários resultados para encaminhar por instrução, ou se precisarem escrever um resultado mais cedo na execução da instrução. hazard de controle Também chamado hazard de desvio Quando a instrução apropriada não pode ser executada no devido ciclo de clock de pipeline porque a instrução buscada não é aquela necessária; ou seja, o fluxo de endereços de instrução não é o que o pipeline esperava.
Detalhamento: o nome “forwarding” vem da ideia de que o resultado é passado adiante a partir de uma instrução anterior para uma instrução posterior. “Bypassing” vem de passar o resultado pelo banco de registradores à unidade desejada.
Hazards de controle
O terceiro tipo de hazard é chamado hazard de controle, vindo da necessidade de tomar uma decisão com base nos resultados de uma instrução enquanto outras estão sendo executadas.
4.5 Visão geral de pipelining 273
Suponha que nosso pessoal da lavanderia receba a tarefa feliz de limpar os uniformes de um time de futebol. Como a roupa é muito suja, temos de determinar se o detergente e a temperatura da água que selecionamos são fortes o suficiente para limpar os uniformes, mas não tão forte para desgastá-los antes do tempo. Em nosso pipeline de lavanderia, temos de esperar até o segundo estágio e examinar o uniforme seco para ver se precisamos ou não mudar as opções da lavadora. O que fazer? Aqui está a primeira das duas soluções para controlar os hazards na lavanderia e seu equivalente nos computadores. Stall: Basta operar sequencialmente até que o primeiro lote esteja seco e depois repetir até você ter a fórmula correta. Essa opção conservadora certamente funciona, mas é lenta. A tarefa de decisão equivalente em um computador é a instrução de desvio. Observe que temos de começar a buscar a instrução após o desvio no próximo ciclo de clock. Contudo, o pipeline possivelmente não saberá qual deve ser a próxima instrução, pois ele só recebeu da memória a instrução de desvio! Assim como na lavanderia, uma solução possível é ocasionar um stall no pipeline imediatamente após buscarmos um desvio, esperando até que o pipeline determine o resultado do desvio para saber de que endereço apanhar a próxima instrução. Vamos supor que colocamos hardware extra suficiente de modo que possamos testar registradores, calcular o endereço de desvio e atualizar o PC durante o segundo estágio do pipeline (veja Seção 4.8 para obter mais detalhes). Até mesmo com esse hardware extra, o pipeline envolvendo desvios condicionais se pareceria com a Figura 4.31. A instrução lw, executada se o desvio não for tomado, fica em stall durante um ciclo de clock extra de 200ps antes de iniciar.
FIGURA 4.31 Pipeline mostrando o stall em cada desvio condicional como solução para controlar os hazards. Este exemplo considera que o desvio condicional é tomado, e a instrução no destino do desvio é a instrução OR. Existe um stall de um estágio no pipeline, ou bolha, após o desvio. Na realidade, o processo de criação de um stall é ligeiramente mais complicado, conforme veremos na Seção 4.8. No entanto, o efeito sobre o desempenho é o mesmo que ocorreria se uma bolha fosse inserida.
Desempenho do “stall no desvio”
Estime o impacto nos ciclos de clock por instrução (CPI) do stall nos desvios. Suponha que todas as outras instruções tenham um CPI de 1. A Figura 3.27 no Capítulo 3 mostra que os desvios são 17% das instruções executadas no SPECint2006. Como as outras instruções possuem um CPI de 1 e os desvios tomaram um ciclo de clock extra para o stall, então veríamos um CPI de 1,17 e, portanto, um stall de 1,17 em relação ao caso ideal.
EXEMPLO RESPOSTA
274
Capítulo 4 O Processador
Se não pudermos resolver o desvio no segundo estágio, como normalmente acontece para pipelines maiores, então veríamos um atraso ainda maior se ocorresse um stall nos desvios. O custo dessa opção é muito alto para a maioria dos computadores utilizar, e isso motiva uma segunda solução para o hazard de controle: Prever: se você estiver certo de que tem a fórmula correta para lavar os uniformes, então basta prever que ela funcionará e lavar a segunda remessa enquanto espera que a primeira seque. Essa opção não atrasa o pipeline quando você estiver correto. Entretanto, quando estiver errado, você terá de refazer a remessa que foi lavada enquanto pensa na decisão. Os computadores realmente utilizam a previsão para tratar dos desvios. Uma técnica simples é sempre prever que os desvios não serão tomados. Quando você estiver certo, o pipeline prosseguirá a toda velocidade. Somente quando os desvios são tomados é que o pipeline sofre um stall. A Figura 4.32 mostra um exemplo assim.
FIGURA 4.32 Prevendo que os desvios não serão tomados como solução para o hazard de controle. O desenho superior mostra o pipeline quando o desvio não é tomado. O desenho inferior mostra o pipeline quando o desvio é tomado. Conforme observamos na Figura 4.31, a inserção de uma bolha nesse padrão simplifica o que realmente acontece, pelo menos durante o primeiro ciclo de clock imediatamente após o desvio. A Seção 4.8 esclarecerá os detalhes.
previsão de desvio Um método de resolver um hazard de desvio que considera um determinado resultado para o desvio e prossegue a partir dessa suposição, em vez de esperar para verificar o resultado real.
Uma versão mais sofisticada de previsão de desvio teria alguns desvios previstos como tomados e alguns como não tomados. Em nossa analogia, os uniformes escuros ou de casa poderiam usar uma fórmula, enquanto os uniformes claros ou de sair poderiam usar outra. No caso da programação, no final dos loops existem desvios que voltam para o início do loop. Como provavelmente serão tomados e desviam para trás, sempre poderíamos prever como tomados os desvios para um endereço anterior. Essas técnicas rígidas para o desvio contam com o comportamento estereotipado e não são responsáveis pela individualidade de uma instrução de desvio específica. Previsores de hardware dinâmicos, ao contrário, fazem suas escolhas dependendo do comportamento de
4.5 Visão geral de pipelining 275
cada desvio, e podem mudar as previsões para um desvio durante a vida de um programa. Seguindo nossa analogia, na previsão dinâmica, uma pessoa veria como o uniforme estava sujo e escolheria a fórmula, ajustando a próxima escolha dependendo do sucesso das escolhas recentes. Uma técnica comum para a previsão dinâmica de desvios é manter um histórico de cada desvio como tomado ou não tomado, e depois usar o comportamento passado recente para prever o futuro. Como veremos mais adiante, a quantidade e o tipo de histórico mantido têm se tornado extensos, resultando em previsores de desvio dinâmicos que podem prever os desvios corretamente, com uma precisão superior a 90% (veja Seção 4.8). Quando a escolha estiver errada, o controle do pipeline terá de garantir que as instruções após o desvio errado não tenham efeito, devendo reiniciar o pipeline a partir do endereço de desvio apropriado. Em nossa analogia de lavanderia, temos de deixar de aceitar novas remessas para poder reiniciar a remessa prevista incorretamente. Como no caso de todas as outras soluções para controlar hazards, pipelines mais longos aumentam o problema, neste caso, aumentando o custo do erro de previsão. As soluções para controlar os hazards são descritas com mais detalhes na Seção 4.8. Detalhamento: Existe uma terceira técnica para o hazard de controle, chamada decisão adiada, mencionada anteriormente. Em nossa analogia, sempre que você tiver de tomar uma decisão sobre a lavanderia, basta colocar uma remessa de roupas que não sejam de futebol na lavadora, enquanto espera que os uniformes de futebol sequem. Desde que você tenha roupas sujas suficientes, que não sejam afetadas pelo teste, essa solução funcionará bem. Chamado de delayed branch (desvio adiado) nos computadores, essa é a solução realmente usada pela arquitetura MIPS. O delayed branch sempre executa a próxima instrução sequencial, com o desvio ocorrendo após esse atraso de uma instrução. Isso fica escondido do programador assembly do MIPS, pois o montador pode arrumar as instruções automaticamente para conseguir o comportamento de desvio desejado pelo programador. O software MIPS colocará uma instrução imediatamente após a instrução de delayed branch, que não é afetada pelo desvio, e um desvio tomado muda o endereço da instrução que vem após essa instrução segura. Em nosso exemplo, a instrução add antes do desvio na Figura 4.31 não o afeta, e pode ser movida para depois dele, a fim de esconder totalmente seu atraso. Como os delayed branches são úteis quando os desvios são curtos, nenhum processador usa um delayed branch de mais de um ciclo. Para atrasos em desvios maiores, a previsão de desvio baseada em hardware normalmente é usada.
Resumo da visão geral de pipelining Pipelining é uma técnica que explora o paralelismo entre as instruções em um fluxo de instruções sequenciais. Ela tem a vantagem substancial de que, diferente de programar um multiprocessador, ela é fundamentalmente invisível ao programador. Nas próximas seções deste capítulo, abordamos o conceito de pipelining usando o subconjunto de instruções MIPS da implementação de ciclo único. Depois, examinamos os problemas que a técnica de pipelining gera e o desempenho alcançável em certas situações. Se você quiser saber mais sobre o software e as implicações de desempenho da técnica de pipelining, agora terá base suficiente para pular para a Seção 4.10. A Seção 4.10 apresenta conceitos avançados de pipelining, como o escalonamento superescalar e dinâmico, e a Seção 4.11 examina os pipelines de microprocessadores recentes. Como alternativa, se você estiver interessado em entender como a técnica de pipelining é implementada e os desafios de lidar com hazards, poderá prosseguir para examinar o projeto de um caminho de dados com pipeline, explicado na Seção 4.6. Depois, você poderá usar esse conhecimento para explorar a implementação do forwarding e stalls na Seção 4.7. Você poderá, então, ler a Seção 4.8 e aprender mais sobre soluções para hazards de desvio, e depois ver como as exceções são tratadas, na Seção 4.9. Para cada sequência de código a seguir, indique se ela deverá sofrer stall, pode evitar stalls usando apenas forwarding, ou pode ser executada sem stall ou forwarding:
Verifique você mesmo
276
Capítulo 4 O Processador
Sequência 1
Entendendo o desempenho dos programas
em
Colocando perspectiva latência (pipeline) O número de estágios em um pipeline ou o número de estágios entre duas instruções durante a execução.
Sequência 2
Sequência 3
Fora do sistema de memória, a operação eficaz do pipeline normalmente é o fator mais importante para determinar o CPI do processador e, portanto, seu desempenho. Conforme veremos na Seção 4.10, compreender o desempenho de um processador moderno com múltiplos problemas é algo complexo e exige a compreensão de mais do que apenas as questões que surgem em um processador com pipeline simples. Apesar disso, os hazards estruturais, de dados e de controle continuam sendo importantes em pipelines simples e mais sofisticados. Para pipelines modernos, os hazards estruturais costumam girar em torno da unidade de ponto flutuante, que pode não ser totalmente implementada com pipeline, enquanto os hazards de controle costumam ser um problema maior nos programas de inteiros, que costumam ter maiores frequências de desvio, além de desvios menos previsíveis. Os hazards de dados podem ser gargalos de desempenho em programas de inteiros e de ponto flutuante. Em geral, é mais fácil lidar com hazards de dados em programas de ponto flutuante porque a menor frequência de desvios e os padrões de acesso mais regulares permitem que o compilador tente escalonar instruções para evitar os hazards. É mais difícil realizar essas otimizações em programas de inteiros, que possuem acesso menos regular e envolvem um maior uso de ponteiros. Conforme veremos na Seção 4.10, existem técnicas de compilação e de hardware mais ambiciosas que reduzem as dependências de dados para o escalonamento.
A técnica de pipelining aumenta o número de instruções em execução simultânea e a velocidade em que as instruções são iniciadas e concluídas. A técnica de pipelining não reduz o tempo gasto para completar uma instrução individual, também chamado de latência. Por exemplo, o pipeline de cinco estágios ainda usa cinco ciclos de clock para completar a instrução. Nos termos usados no Capítulo 4, a técnica de pipelining melhora a vazão de instruções, e não o tempo de execução ou latência das instruções individualmente. Os conjuntos de instruções podem simplificar ou dificultar a vida dos projetistas do pipeline, que já precisam enfrentar hazards estruturais, de controle e de dados. A previsão de desvio, o forwarding e os stalls ajudam a tornar um computador rápido enquanto ainda gera as respostas certas.
Caminho de dados e controle 4.6 usando pipeline
A Figura 4.33 mostra o caminho de dados de ciclo único da Seção 4.4 com os estágios de pipeline identificados. A divisão de uma instrução em cinco estágios significa um pipeline de cinco estágios, que, por sua vez, significa que até cinco instruções estarão em execução durante qualquer ciclo de clock. Assim, temos de separar o caminho de dados em cinco partes, com cada parte possuindo um nome correspondente a um estágio da execução da instrução:
4.6 Caminho de dados e controle usando pipeline 277
FIGURA 4.33 O caminho de dados da Seção 4.4 (semelhante à Figura 4.17). Cada etapa da instrução pode ser mapeada no caminho de dados da esquerda para a direita. As únicas exceções são a atualização do PC e a etapa de escrita do resultado, mostrada em cores, que envia o resultado da ALU ou os dados da memória para a esquerda, a fim de serem escritos no banco de registradores. (Normalmente, usamos linhas coloridas para controle, mas são linhas de dados.)
1. IF (Instruction Fetch): Busca de instruções. 2. ID (Instruction Decode): Decodificação de instruções e leitura do banco de registradores. 3. EX: Execução ou cálculo de endereço. 4. MEM: Acesso à memória de dados. 5. WB (Write Back): Escrita do resultado. Na Figura 4.33, esses cinco componentes correspondem aproximadamente ao modo como o caminho de dados é desenhado; as instruções e os dados em geral se movem da esquerda para a direita pelos cinco estágios enquanto completam a execução. Voltando à nossa analogia da lavanderia, as roupas ficam mais limpas, mais secas e mais organizadas à medida que prosseguem na fila, e nunca se movem para trás. Entretanto, existem duas exceções para esse fluxo de informações da esquerda para a direita: j
estágio de escrita do resultado, que coloca o resultado de volta no banco de registradores, no meio do caminho de dados;
j
a seleção do próximo valor do PC, escolhendo entre o PC incrementado e o endereço de desvio do estágio MEM.
Os dados fluindo da direita para a esquerda não afetam a instrução atual; somente as instruções seguintes no pipeline são influenciadas por esses movimentos de dados reversos. Observe que a primeira seta da direita para a esquerda pode levar a hazards de dados, e a segunda ocasiona hazards de controle.
278
Capítulo 4 O Processador
Uma maneira de mostrar o que acontece na execução com pipeline é fingir que cada instrução tem seu próprio caminho de dados, e depois colocar esses caminhos de dados em uma linha de tempo para mostrar seu relacionamento. A Figura 4.34 mostra a execução das instruções na Figura 4.27, exibindo seus caminhos de dados privados em uma linha de tempo comum. Usamos uma versão estilizada do caminho de dados na Figura 4.33 para mostrar os relacionamentos na Figura 4.34.
FIGURA 4.34 Instruções executadas usando o caminho de dados de ciclo único na Figura 4.33, assumindo a execução com pipeline. Semelhante às Figuras de 4.28 a 4.30, esta figura finge que cada instrução possui seu próprio caminho de dados e pinta cada parte de acordo com o uso. Ao contrário daquelas figuras, cada estágio é rotulado pelo recurso físico usado nesse estágio, correspondendo às partes do caminho de dados na Figura 4.33. IM representa a memória de instruções e o PC no estágio de busca da instrução, Reg significa banco de registradores e extensor de sinal no estágio de decodificação de instruções/leitura do banco de registradores (ID), e assim por diante. Para manter a ordem de tempo correta, esse caminho de dados estilizado divide o banco de registradores em duas partes lógicas: leitura de registradores durante a busca de registradores (ID) e registradores escritos durante a escrita do resultado (WB). Esse uso dual é representado pelo desenho da metade esquerda não sombreada do banco de registradores, usando linhas tracejadas no estágio ID, quando ele não estiver sendo escrito, e a metade direita não sombreada usando linhas tracejadas do estágio WB, quando não estiver sendo lido. Como antes, consideramos que o banco de registradores é escrito na primeira metade do ciclo de clock e é lido durante a segunda metade.
A Figura 4.34 parece sugerir que três instruções precisam de três caminhos de dados. Em vez disso, acrescentamos registradores para manter dados de modo que partes do caminho de dados pudessem ser compartilhadas durante a execução da instrução. Por exemplo, como mostra a Figura 4.34, a memória de instruções é usada durante apenas um dos cinco estágios de uma instrução, permitindo que seja compartilhada por outras instruções durante os outros quatro estágios. A fim de reter o valor de uma instrução individual para seus outros quatro estágios, o valor lido da memória de instruções precisa ser salvo em um registrador. Argumentos semelhantes se aplicam a cada estágio do pipeline, de modo que precisamos colocar registradores sempre que existam linhas divisórias entre os estágios na Figura 4.33. Retornando à nossa analogia da lavanderia, poderíamos ter um cesto entre cada par de estágios contendo as roupas para a próxima etapa. A Figura 4.35 mostra o caminho de dados usando pipeline com os registradores do pipeline destacados. Todas as instruções avançam durante cada ciclo de clock de um registrador do pipeline para o seguinte. Os registradores recebem os nomes dos dois estágios separados por esse registrador. Por exemplo, o registrador do pipeline entre os estágios IF e ID é chamado de IF/ID. Observe que não existe um registrador de pipeline no final do estágio de escrita do resultado (WB). Todas as instruções precisam atualizar algum estado no processador – o
4.6 Caminho de dados e controle usando pipeline 279
banco de registradores, memória ou o PC –, assim, um registrador de pipeline separado é redundante para o estado que é atualizado. Por exemplo, uma instrução load colocará seu resultado em um dos 32 registradores, e qualquer instrução posterior que precise desses dados simplesmente lerá o registrador apropriado. Naturalmente, cada instrução atualiza o PC, seja incrementando-o ou atribuindo a ele o endereço de destino de um desvio. O PC pode ser considerado um registrador de pipeline: um que alimenta o estágio IF do pipeline. Contudo, diferente dos registradores de pipeline sombreados na Figura 4.35, o PC faz parte do estado arquitetônico visível; seu conteúdo precisa ser salvo quando ocorre uma exceção, enquanto o conteúdo dos registradores de pipeline pode ser descartado. Na analogia da lavanderia, você poderia pensar no PC como correspondendo ao cesto que mantém a remessa de roupas sujas antes da etapa de lavagem.
FIGURA 4.35 A versão com pipeline do caminho de dados na Figura 4.33. Os registradores do pipeline, em cinza, separam cada estágio do pipeline. Eles são rotulados pelos nomes dos estágios que separam; por exemplo, o primeiro é rotulado com IF/ID porque separa os estágios de busca de instruções e decodificação de instruções. Os registradores precisam ser grandes o suficiente para armazenar todos os dados correspondentes às linhas que passam por eles. Por exemplo, o registrador IF/ID precisa ter 64 bits de largura, pois precisa manter a instrução de 32 bits lida da memória e o endereço incrementado de 32 bits no PC. Vamos expandir esses registradores no decorrer deste capítulo, mas, por enquanto, os outros três registradores de pipeline contêm 128, 97 e 64 bits, respectivamente.
Para mostrar como funciona a técnica de pipelining, no decorrer deste capítulo, apresentamos sequências de figuras para demonstrar a operação com o tempo. Essas páginas extras parecem exigir muito mais tempo para você entender. Mas não tema; as sequências levam muito menos tempo do que parece, pois você pode compará-las e ver que mudanças ocorrem em cada ciclo do clock. A Seção 4.7 descreve o que acontece quando existem hazards de dados entre instruções em um pipeline; ignore-as por enquanto. As Figuras 4.36 a 4.38, nossa primeira sequência, mostram as partes ativas do caminho de dados destacadas enquanto uma instrução de load passa pelos cinco estágios de execução do pipeline. Mostramos um load primeiro porque ele ativa todos os cinco estágios. Como nas Figuras de 4.28 a 4.30, destacamos a metade direita dos registradores ou memória quando estão sendo lidos e destacamos a metade esquerda quando estão sendo escritos. Mostramos a abreviação da instrução lw com o nome do estágio do pipeline que está ativo em cada figura. Os cinco estágios são os seguintes: 1. Busca de instruções: a parte superior da Figura 4.36 mostra a instrução sendo lida da memória usando o endereço no PC e depois colocada no registrador de pipeline IF/ID.
280
Capítulo 4 O Processador
FIGURA 4.36 IF e ID: primeiro e segundo estágios do pipe de uma instrução, com as partes ativas do caminho de dados da Figura 4.35 em destaque. A convenção de destaque é a mesma utilizada na Figura 4.28. Como na Seção 4.2, não há confusão quando se lê e escreve nos registradores, pois o conteúdo só muda na transição do clock. Embora o load só precise do registrador de cima no estágio 2, o processador não sabe que instrução está sendo decodificada, de modo que estende o sinal da constante de 16 bits e lê os dois registradores para o registrador de pipeline ID/EX. Não precisamos de todos os três operandos, mas simplifica o controle manter todos os três.
O endereço do PC é incrementado em 4 e depois escrito de volta ao PC, para que fique pronto para o próximo ciclo de clock. Esse endereço incrementado também é salvo no registrador de pipeline IF/ID caso seja necessário mais tarde para uma instrução, como beq. O computador não tem como saber que tipo de instrução está sendo buscada, de modo que precisa se preparar para qualquer instrução, passando informações potencialmente necessárias pelo pipeline.
4.6 Caminho de dados e controle usando pipeline 281
FIGURA 4.37 EX: o terceiro estágio do pipe de uma instrução load, destacando as partes do caminho de dados da Figura 4.35 usadas neste estágio do pipe. O registrador é acrescentado ao imediato com sinal estendido, e a soma é colocada no registrador de pipeline EX/MEM.
2. Decodificação de instruções e leitura do banco de registradores: A parte inferior da Figura 4.36 mostra a parte relativa à instrução do registrador de pipeline IF/ID, fornecendo o campo imediato de 16 bits, que tem seu sinal estendido para 32 bits, e os números dos dois registradores para leitura. Todos os três valores são armazenados no registrador de pipeline ID/EX, assim como o endereço no PC incrementado. Novamente, transferimos tudo o que possa ser necessário por qualquer instrução durante um ciclo de clock posterior. 3. Execução ou cálculo de endereço: a Figura 4.37 mostra que a instrução load lê o conteúdo do registrador 1 e o imediato com o sinal estendido do registrador de pipeline ID/EX e os soma usando a ALU. Essa soma é colocada no registrador de pipeline EX/ MEM. 4. Acesso à memória: a parte superior da Figura 4.38 mostra a instrução load lendo a memória de dados por meio do endereço vindo do registrador de pipeline EX/MEM e carregando os dados no registrador de pipeline MEM/WB. 5. Escrita do resultado: a parte inferior da Figura 4.38 mostra a etapa final: lendo os dados do registrador de pipeline MEM/WB e escrevendo-os no banco de registradores, no meio da figura. Essa revisão da instrução load mostra que qualquer informação necessária em um estágio posterior do pipe precisa ser passada a esse estágio por meio de um registrador de pipeline. A revisão de uma instrução store mostra a semelhança na execução da instrução, bem como a passagem da informação para os estágios posteriores. Aqui estão os cinco estágios do pipe da instrução store: 1. Busca de instruções: a instrução é lida da memória usando o endereço no PC e depois é colocada no registrador de pipeline IF/ID. Esse estágio ocorre antes que a instrução seja identificada, de modo que a parte superior da Figura 4.36 funciona para store e também para load.
282
Capítulo 4 O Processador
FIGURA 4.38 MEM e WB: o quarto e quinto estágios do pipe de uma instrução load, destacando as partes do caminho de dados da Figura 4.35 usadas nesses estágios do pipe. A memória de dados é lida por meio do endereço no registrador de pipeline EX/MEM, e os dados são colocados no registrador de pipeline MEM/WB. Em seguida, os dados são lidos do registrador de pipeline MEM/WB e escritos no banco de registradores, no meio do caminho de dados. Nota: existe um bug nesse projeto, que foi consertado na Figura 4.41.
2. Decodificação de instruções e leitura do banco de registradores: a instrução no registrador de pipeline IF/ID fornece os números de dois registradores para leitura e estende o sinal do imediato de 16 bits. Esses três valores de 32 bits são armazenados no registrador de pipeline ID/EX. A parte inferior da Figura 4.36 para instruções load também mostra as operações do segundo estágio para stores. Esses dois primeiros estágios são executados por todas as instruções, pois é muito cedo para saber o tipo da instrução.
4.6 Caminho de dados e controle usando pipeline 283
3. Execução e cálculo de endereço: a Figura 4.39 mostra a terceira etapa; o endereço efetivo é colocado no registrador de pipeline EX/MEM. 4. Acesso à memória: a parte superior da Figura 4.40 mostra os dados sendo escritos na memória. Observe que o registrador contendo os dados a serem armazenados foi lido em um estágio anterior e armazenado no ID/EX. A única maneira de disponibilizar os dados durante o estágio MEM é colocar os dados no registrador de pipeline EX/ MEM no estágio EX, assim como armazenar o endereço efetivo em EX/MEM. 5. Escrita do resultado: a parte inferior da Figura 4.40 mostra a última etapa do store. Para essa instrução, nada acontece no estágio de escrita do resultado. Como cada instrução por trás do store já está em progresso, não temos como acelerar essas instruções. Logo, uma instrução passa por um estágio mesmo que não haja nada a fazer, pois as instruções posteriores já estão prosseguindo em velocidade máxima.
FIGURA 4.39 EX: o terceiro estágio do pipe de uma instrução store. Ao contrário do terceiro estágio da instrução load na Figura 4.37, o segundo valor do registrador é carregado no registrador de pipeline EX/MEM a ser usado no próximo estágio. Embora não faça mal algum sempre escrever esse segundo registrador no registrador de pipeline EX/MEM, escrevemos o segundo registrador apenas em uma instrução store para tornar o pipeline mais fácil de entender.
A instrução store novamente ilustra que, para passar algo de um estágio anterior do pipe a um estágio posterior, a informação precisa ser colocada em um registrador de pipeline; caso contrário, a informação é perdida quando a próxima instrução entrar nesse estágio do pipeline. Para a instrução store, precisamos passar um dos registradores lidos no estágio ID para o estágio MEM, onde é armazenado na memória. Os dados foram colocados inicialmente no registrador de pipeline ID/EX e depois passados para o registrador de pipeline EX/MEM. Load e store ilustram um segundo ponto importante: cada componente lógico do caminho de dados – como memória de instruções, portas para leitura de registradores, ALU, memória de dados e porta para escrita de registradores – só pode ser usado dentro de um único estágio do pipeline. Caso contrário, teríamos um hazard estrutural (ver Seção “Hazards estruturais, anteriormente neste capítulo). Logo, esses componentes e seu controle podem ser associados a um único estágio do pipeline.
284
Capítulo 4 O Processador
FIGURA 4.40 MEM e WB: o quarto e quinto estágios do pipe de uma instrução store. No quarto estágio, os dados são escritos na memória de dados para o store. Observe que os dados vêm do registrador de pipeline EX/MEM e que nada é mudado no registrador de pipeline MEM/WB. Quando os dados são escritos na memória, não há nada mais para a instrução store fazer, de modo que nada acontece no estágio 5.
Agora, podemos descobrir um bug no projeto da instrução load. Você conseguiu ver? Qual registrador é alterado no estágio final da leitura? Mais especificamente, qual instrução fornece o número do registrador de escrita? A instrução no registrador de pipeline IF/ID fornece o número do registrador de escrita, embora essa instrução ocorra consideravelmente depois da instrução load! Logo, precisamos preservar o número do registrador de destino da instrução load. Assim como store passou o conteúdo do registrador do ID/EX ao registrador de pipeline EX/MEM para uso no estágio MEM, load precisa passar o número do registrador de ID/ EX por EX/MEM ao registrador de pipeline MEM/WB, para uso no estágio WB. Outra
4.6 Caminho de dados e controle usando pipeline 285
maneira de pensar sobre a passagem do número de registrador é que, para compartilhar o caminho de dados em pipeline, precisávamos preservar a instrução lida durante o estágio IF, de modo que cada registrador de pipeline contenha uma parte da instrução necessária para esse estágio e para os estágios posteriores. A Figura 4.41 mostra a versão correta do caminho de dados, passando o número do registrador de escrita primeiro ao registrador ID/EX, depois ao registrador EX/MEM, e finalmente ao registrador MEM/WB. O número do registrador é usado durante o estágio WB de modo a especificar o registrador a ser escrito. A Figura 4.42 é um único desenho do caminho de dados correto, destacando o hardware utilizado em todos os cinco estágios da instrução load word nas Figuras de 4.36 a 4.38. Veja na Seção 4.8 uma explicação de como fazer a instrução branch funcionar como esperado.
FIGURA 4.41 O caminho de dados em pipeline corrigido para lidar corretamente com a instrução load. O número do registrador de escrita agora vem do registrador de pipeline MEM/WB junto com os dados. O número do registrador é passado do estágio do pipe ID até alcançar o registrador de pipeline MEM/WB, acrescentando mais 5 bits aos três últimos registradores de pipeline. Esse novo caminho aparece em destaque.
FIGURA 4.42 A parte do caminho de dados na Figura 4.41 usada em todos os cinco estágios de uma instrução load.
286
Capítulo 4 O Processador
Representando pipelines graficamente Pipelining pode ser difícil de entender, pois muitas instruções estão executando simultaneamente em um único caminho de dados em cada ciclo de clock. Para ajudar na compreensão, existem dois estilos básicos de figuras de pipeline: diagramas de pipeline com múltiplos ciclos de clock, como a Figura 4.34, e diagramas de pipeline com único ciclo de clock, como as Figuras de 4.36 a 4.40. Os diagramas com múltiplos ciclos de clock são mais simples, mas não contêm todos os detalhes. Por exemplo, considere esta sequência de cinco instruções:
A Figura 4.43 mostra o diagrama de pipeline com múltiplos ciclos de clock para essas instruções. O tempo avança da esquerda para a direita na horizontal, semelhante ao pipeline da lavanderia, na Figura 4.25. Uma representação dos estágios do pipeline é colocada em cada parte do eixo de instruções, ocupando os ciclos de clock apropriados. Esses caminhos de dados estilizados representam os cinco estágios do nosso pipeline, mas um retângulo indicando o nome de cada estágio do pipe também funciona bem. A Figura 4.44 mostra a versão mais tradicional do diagrama de pipeline com múltiplos ciclos de clock. Observe que a Figura 4.43 mostra os recursos físicos utilizados em cada estágio, enquanto a Figura 4.44 usa o nome de cada estágio. Os diagramas de pipeline de ciclo único de clock mostram o estado do caminho de dados inteiro durante um único ciclo de clock, e normalmente todas as cinco instruções no
FIGURA 4.43 Diagrama de pipeline com múltiplos ciclos de clock das cinco instruções. Esse estilo de representação de pipeline mostra a execução completa das instruções em uma única figura. As instruções são listadas por ordem de execução, de cima para baixo, e os ciclos de clock se movem da esquerda para a direita. Ao contrário da Figura 4.28, aqui, mostramos os registradores de pipeline entre cada estágio. A Figura 4.44 mostra a maneira tradicional de desenhar esse diagrama.
4.6 Caminho de dados e controle usando pipeline 287
FIGURA 4.44 Diagrama de pipeline com múltiplos ciclos de clock tradicional, com as cinco instruções da Figura 4.43.
pipeline são identificadas por rótulos acima de seus respectivos estágios do pipeline. Usamos esse tipo de figura para mostrar os detalhes do que está acontecendo dentro do pipeline durante cada ciclo de clock; normalmente, os desenhos aparecem em grupos, para mostrar a operação do pipeline durante uma sequência de ciclos de clock. Usamos diagramas de ciclo múltiplo de clock a fim de oferecer sinopses de situações de pipelining. (A Seção 4.12 oferece mais ilustrações de diagramas de clock único se você quiser ver mais detalhes sobre a Figura 4.43.) Um diagrama de ciclo único de clock representa uma fatia vertical de um conjunto do diagrama com múltiplos ciclos de clock, mostrando o uso do caminho de dados em cada uma das instruções do pipeline no ciclo de clock designado. Por exemplo, a Figura 4.45 mostra o diagrama com ciclo único de clock correspondente ao ciclo de clock 5 das Figuras 4.43 e 4.44. Obviamente, os diagramas com único ciclo de clock possuem mais detalhes e ocupam muito mais espaço para mostrar o mesmo número de ciclos de clock. Os exercícios pedem que você crie esses diagramas para outras sequências de código.
FIGURA 4.45 O diagrama com ciclo único de clock correspondente ao ciclo de clock 5 do pipeline das Figuras 4.43 e 4.44. Como você pode ver, uma figura com ciclo único de clock é uma fatia vertical de um diagrama com múltiplos ciclos de clock.
288
Capítulo 4 O Processador
Verifique você mesmo
Um grupo de alunos discutia sobre a eficiência de um pipeline de cinco estágios quando um deles apontou que nem todas as instruções estão ativas em cada estágio do pipeline. Depois de decidir ignorar os efeitos dos hazards, eles fizeram as quatro afirmações a seguir. Quais delas estão corretas? 1. Permitir que jumps, branches e instruções da ALU utilizem menos estágios do que os cinco necessários pela instrução load aumentará o desempenho do pipeline sob todas as circunstâncias. 2. Tentar permitir que algumas instruções utilizem menos ciclos não ajuda, pois a vazão é determinada pelo ciclo do clock; o número de estágios do pipe por instrução afeta a latência, e não a vazão. 3. Você não pode fazer com que as instruções da ALU utilizem menos ciclos, devido à escrita do resultado, mas os branches e jumps podem utilizar menos ciclos, de modo que existe alguma oportunidade de melhoria. 4. Em vez de tentar fazer com que as instruções utilizem menos ciclos de clock, devemos explorar um meio de tornar o pipeline mais longo, de modo que as instruções utilizem mais ciclos, porém com ciclos mais curtos. Isso poderia melhorar o desempenho.
No computador 6600, talvez ainda mais do que em qualquer computador anterior, o sistema de controle é a diferença. James Thornton, Design of a Computer: The Control Data 6600, 1970
Controle de um pipeline Assim como acrescentamos controle ao caminho de dados simples na Seção 4.3, agora acrescentamos controle ao caminho de dados de um pipeline. Começamos com um projeto simples, que vê o problema por meio de óculos cor-de-rosa; nas Seções de 4.7 a 4.9 removemos os óculos para revelar os hazards do mundo real. O primeiro passo é rotular as linhas de controle no caminho de dados existente. A Figura 4.46 mostra essas linhas. Pegamos o máximo possível emprestado do controle para o caminho de dados simples da Figura 4.17. Em particular, usamos a mesma lógica de controle da ALU, lógica de desvio, multiplexador do registrador destino e linhas de controle. Essas funções são definidas nas Figuras 4.12, 4.16 e 4.18. Reproduzimos as principais informações nas Figuras 4.47 a 4.49 em uma única página de modo a facilitar o acompanhamento do restante do texto. Assim como ocorreu com a implementação com ciclo único, consideramos que o PC é escrito a cada ciclo de clock, de modo que não existe um sinal de escrita separado para o PC. Pelo mesmo argumento, não existem sinais de escrita para os registradores de pipeline (IF/ID, ID/EX, EX/MEM e MEM/WB), pois os registradores de pipeline também são escritos durante cada ciclo de clock. A fim de especificar o controle para o pipeline, só precisamos definir os valores de controle durante cada estágio do pipeline. Como cada linha de controle está associada a um componente ativo em apenas um estágio do pipeline, podemos dividir as linhas de controle em cinco grupos, de acordo com o estágio do pipeline. 1. Busca de instruções: os sinais de controle para ler a memória de instruções e escrever o PC sempre são ativados, de modo que não existe nada de especial para controlar nesse estágio do pipeline. 2. Decodificação de instruções/leitura do banco de registradores: como no estágio anterior, o mesmo acontece em cada ciclo de clock, de modo que não existem linhas de controle opcionais para definir. 3. Execução/cálculo de endereço: os sinais a serem definidos são RegDst, OpALU e OrigALU (veja as Figuras 4.47 e 4.48). Os sinais selecionam o registrador destino, a operação da ALU e Dados da leitura 2 ou um imediato com sinal estendido para a ALU.
4.6 Caminho de dados e controle usando pipeline 289
4. Acesso à memória: as linhas de controle definidas nesse estágio são Branch, LeMem e EscreveMem. Esses sinais são definidos pelas instruções branch equal, load e store, respectivamente. Lembre-se de que o OrigPC na Figura 4.48 seleciona o próximo endereço sequencial, a menos que o controle ative Branch e o resultado da ALU seja zero. 5. Escrita do resultado: as duas linhas de controle são MemparaReg, que decide entre enviar o resultado da ALU ou o valor da memória para o banco de registradores, e EscreveReg, que escreve o valor escolhido.
FIGURA 4.46 O caminho de dados em pipeline da Figura 4.41 com sinais de controle identificados. Esse caminho de dados toma emprestado a lógica de controle para a origem do PC, o número do registrador destino e o controle da ALU, da Seção 4.4. Observe que agora precisamos do campo funct (código de função) de 6 bits da instrução no estágio EX como entrada para o controle da ALU, de modo que esses bits também precisam ser incluídos no registrador de pipeline ID/EX. Lembre-se de que esses 6 bits também são os bits menos significativos do campo imediato da instrução, de modo que o registrador de pipeline ID/EX pode fornecê-los a partir do campo imediato, já que a extensão do sinal deixa esses bits inalterados.
FIGURA 4.47 Uma cópia da Figura 4.12. Essa figura mostra como os bits do controle da ALU são definidos dependendo dos bits de controle OpALU e dos diferentes códigos de função para instruções tipo R.
290
Capítulo 4 O Processador
FIGURA 4.48 Uma cópia da Figura 4.16. A função de cada um dos sete sinais de controle é definida. As linhas de controle da ALU (OpALU) são definidas na segunda coluna da Figura 4.47. Quando um controle de 1 bit para um multiplexador bidirecional é ativado, o multiplexador seleciona a entrada correspondente a 1. Caso contrário, se o controle for desativado, o multiplexador seleciona a entrada 0. Observe que OrigPC é controlado por uma porta lógica AND na Figura 4.46. Se o sinal Branch e o sinal Zero da ALU estiverem ativos, então OrigPC é 1; caso contrário, ele é 0. O controle define o sinal Branch somente durante uma instrução beq; caso contrário, o OrigPC é 0.
FIGURA 4.49 Os valores das linhas de controle são iguais aos da Figura 4.18, mas foram reorganizados em três grupos, correspondentes aos três últimos estágios do pipeline.
Como a utilização de um pipeline no caminho de dados deixa inalterado o significado das linhas de controle, podemos usar os mesmos valores de controle de antes. A Figura 4.49 tem os mesmos valores da Seção 4.4, mas agora as nove linhas de controle estão agrupadas por estágio do pipeline. A implementação do controle significa definir as nove linhas de controle desses valores em cada estágio, para cada instrução. A maneira mais simples de fazer isso é estender os registradores do pipeline de modo a incluir informações de controle. Como as linhas de controle começam com o estágio EX, podemos criar a informação de controle durante a decodificação da instrução. A Figura 4.50 mostra que esses sinais de controle são usados no respectivo estágio do pipeline à medida que a instrução se move pelo pipeline, assim como o número do registrador destino para loads se move pelo pipeline da Figura 4.41. A Figura 4.51 mostra o caminho de dados
4.6 Caminho de dados e controle usando pipeline 291
FIGURA 4.50 As linhas de controle para os três estágios finais. Observe que quatro das nove linhas de controle são usadas na fase EX, com as cinco linhas de controle restantes passadas adiante para o registrador de pipeline EX/MEM, para manter as linhas de controle; três são usadas durante o estágio MEM, e as duas últimas são passadas a MEM/WB, para uso no estágio WB.
FIGURA 4.51 O caminho de dados em pipeline da Figura 4.46, com os sinais de controle conectados às partes de controle dos registradores de pipeline. Os valores de controle para os três últimos estágios são criados durante o estágio de decodificação de instruções e depois colocados no registrador de pipeline ID/EX. As linhas de controle para cada estágio do pipe são usadas, e as linhas de controle restantes depois disso são passadas ao próximo estágio do pipeline.
292
Capítulo 4 O Processador
completo, com os registradores de pipeline estendidos e com as linhas de controle conectadas ao estágio apropriado. (A Seção 4.12 contém mais exemplos de código MIPS executando em hardware com pipeline usando diagramas de clock simples, se quiser ver mais detalhes.)
Como assim por que teve de ser criado? É um bypass. Você precisa criar bypasses. Douglas Adams, The Hitchhiker's Guide to the Galaxy, 1979
4.7 Hazards de dados: forwarding versus stalls Os exemplos da seção anterior mostram o poder da execução em pipeline e como o hardware realiza a tarefa. Agora é hora de retirarmos os óculos cor-de-rosa e examinarmos o que acontece com os programas reais. As instruções nas Figuras de 4.43 a 4.45 eram independentes; nenhuma delas usava os resultados calculados por qualquer uma das outras. Mesmo assim, na Seção 4.5, vimos que os hazards de dados são obstáculos para a execução em pipeline. Vejamos uma sequência com muitas dependências, indicadas com realce:
As quatro últimas instruções são todas dependentes do resultado no registrador $2 da primeira instrução. Se o registrador $2 tivesse o valor 10 antes da instrução subtract e -20 depois dela, o programador desejaria que -20 fosse usado nas instruções seguintes que se referem ao registrador $2. Como essa sequência funcionaria com nosso pipeline? A Figura 4.52 ilustra a execução dessas instruções usando uma representação de pipeline com múltiplos ciclos de clock. Para demonstrar a execução dessa sequência de instruções em nosso pipeline atual, o topo da Figura 4.52 mostra o valor do registrador $2, que muda durante o ciclo de clock 5, quando a instrução sub escreve seu resultado. O último hazard em potencial pode ser resolvido pelo projeto do hardware do banco de registradores: o que acontece quando um registrador é lido e escrito no mesmo ciclo de clock? Consideramos que a escrita está na primeira metade do ciclo de clock e a leitura está na segunda metade, de modo que esta fornece o que foi escrito. Como acontece para muitas implementações dos bancos de registradores, não temos hazard de dados nessa situação. A Figura 4.52 mostra que os valores lidos para o registrador $2 não seriam o resultado da instrução sub, a menos que a leitura ocorresse durante o ciclo de clock 5 ou posterior. Assim, as instruções que receberiam o valor correto de -20 são add e sw; as instruções AND e OR receberiam o valor incorreto de 10! Usando esse estilo de desenho, esses problemas se tornam aparentes quando uma linha de dependência retorna no tempo. Conforme dissemos na Seção 4.5, o resultado desejado está disponível no final do estágio EX ou no ciclo de clock 3. Quando os dados são realmente necessários pelas instruções AND e OR? No início do estágio EX, ou nos ciclos de clock 4 e 5, respectivamente. Assim, podemos executar esse segmento sem stalls se simplesmente os dados sofrerem forwarding assim que estiverem disponíveis para quaisquer unidades que precisam deles antes de estarem disponíveis para leitura do banco de registradores. Como funciona o forwarding? Para simplificar o restante desta seção, consideramos apenas o desafio de forwarding para uma operação no estágio EX, que pode ser uma operação da ALU ou um cálculo de endereço efetivo. Isso significa que, quando uma instrução tenta usar um registrador em seu estágio EX, que uma instrução anterior
4.7 Hazards de dados: forwarding versus stalls
293
FIGURA 4.52 Dependências em pipeline em uma sequência de cinco instruções usando caminhos de dados simplificados para mostrar as dependências. Todas as ações dependentes são mostradas em cinza, e “CC 1” no alto da figura significa o ciclo de clock 1. A primeira instrução escreve em $2, e todas as instruções seguintes leem de $2. Esse registrador é escrito no ciclo de clock 5, de modo que o valor correto está indisponível antes do ciclo de clock 5. (Uma leitura de um registrador durante um ciclo de clock retorna o valor escrito no final da primeira metade do ciclo, quando ocorre tal escrita.) As linhas coloridas do caminho de dados do topo para os inferiores mostram as dependências. Aquelas que precisam retornar no tempo são os hazards de dados do pipeline.
pretende escrever em seu estágio WB, na realidade precisamos dos valores como entradas para a ALU. Uma notação que nomeia os campos dos registradores de pipeline permite uma notação mais precisa das dependências. Por exemplo, “ID/EX.RegistradorRs” refere-se ao número de um registrador cujo valor se encontra no registrador de pipeline ID/EX; ou seja, aquele da primeira porta de leitura do banco de registradores. A primeira parte do nome, à esquerda do ponto, é o nome do registrador de pipeline; a segunda parte é o nome do campo nesse registrador. Usando essa notação, os dois pares de condições de hazard são: 1a. EX/MEM.RegistradorRd = ID/EX.RegistradorRs 1b. EX/MEM.RegistradorRd = ID/EX.RegistradorRt 2a. MEM/WB.RegistradorRd = ID/EX.RegistradorRs 2b. MEM/WB.RegistradorRd = ID/EX.RegistradorRt O primeiro hazard na sequência da Seção 4.7 está no registrador $2, entre o resultado de sub S2,$1,$3 e o primeiro operando de leitura de and $12,$2,$5. Esse hazard pode ser detectado quando a instrução and está no estágio EX, e a instrução anterior está no estágio MEM, de modo que este é o hazard 1a: EX/MEM.RegistradorRd = ID/EX.RegistradorRs = $2
294
Capítulo 4 O Processador
Detecção de dependência
EXEMPLO
Classifique as dependências nesta sequência da Seção 4.7:
RESPOSTA
Conforme já mencionamos, o sub-and é um hazard tipo 1a. Os outros hazards são j sub-or
é um hazard tipo 2b:
MEM/WB.RegistradorRd = ID/EX.RegistradorRt = $2 j
j
As duas dependências em sub-add não são hazards, pois o banco de registradores fornece os dados apropriados durante o estágio ID de add. Não existe hazard de dados entre sub e sw, porque sw lê $2 no ciclo de clock depois que sub escreve $2.
Como algumas instruções não escrevem em registradores, essa política não é exata; às vezes, poderia haver forwarding indevidamente. Uma solução é simplesmente verificar se o sinal EscreveReg estará ativo: examinando o campo de controle WB do registrador de pipeline durante os estágios EX e MEM, é possível determinar se EscreveReg está ativo. Lembre-se de que o MIPS exige que cada uso de $0 como operando deve gerar um valor de operando 0. Se uma instrução no pipeline tiver $0 como seu destino (por exemplo, sll $0,$1,2), queremos evitar o forwarding do seu valor possivelmente diferente de zero. Não encaminhar os resultados destinados a $0 libera o programador assembly e o compilador de qualquer requisito para evitar o uso de $0 como destino. As condições anteriores, portanto, funcionam corretamente desde que acrescentemos EX/MEM.RegistradorRd ≠ 0 à primeira condição de hazard e MEM/WB.RegistradorRd ≠ 0 à segunda. Agora que podemos detectar os hazards, metade do problema está resolvido – mas ainda precisamos fazer o forwarding dos dados corretos. A Figura 4.53 mostra as dependências entre os registradores de pipeline e as entradas da ALU para a mesma sequência de código da Figura 4.52. A mudança é que a dependência começa por um registrador de pipeline, em vez de esperar pelo estágio WB para escrever no banco de registradores. Assim, os dados exigidos existem a tempo para as instruções posteriores, com os registradores de pipeline mantendo os dados para forwarding. Se pudermos pegar as entradas da ALU de qualquer registrador de pipeline, e não apenas de ID/EX, então podemos fazer o forwarding dos dados corretos. Acrescentando multiplexadores à entrada da ALU e com os controles apropriados, podemos executar o pipeline em velocidade máxima na presença dessas dependências de dados. Por enquanto, vamos considerar que as únicas instruções para as quais precisamos de forwarding são as quatro instruções no formato R: add, sub, AND e OR. A Figura 4.54 mostra um detalhe da ALU e do registrador de pipeline antes e depois de acrescentar o forwarding. A Figura 4.55 mostra os valores das linhas de controle para os multiplexadores da ALU que selecionam os valores do banco de registradores ou um dos valores de forwarding. Esse controle de forwarding estará no estágio EX porque os multiplexadores de forwarding da ALU são encontrados nesse estágio. Assim, temos de passar os números dos registradores operandos do estágio ID por meio do registrador de pipeline ID/EX, para determinar se os valores devem sofrer forwarding. Já temos o campo rt (bits 20-16). Antes
4.7 Hazards de dados: forwarding versus stalls
295
FIGURA 4.53 As dependências entre os registradores de pipeline se movem para a frente no tempo, de modo que é possível fornecer as entradas para a ALU necessárias pela instrução AND e pela instrução OR fazendo forwarding dos resultados encontrados nos registradores de pipeline. Os valores nos registradores de pipeline mostram que o valor desejado está disponível antes de ser escrito no banco de registradores. Consideramos que o banco de registradores encaminha valores lidos e escritos durante o mesmo ciclo de clock, de modo que add não causa stall, mas os valores vêm do banco de registradores, e não de um registrador de pipeline. O “forwarding” do banco de registradores – ou seja, a leitura apanha o valor da escrita nesse ciclo de clock – é o motivo pelo qual o ciclo de clock 5 mostra o registrador $2 tendo o valor 10 no início e -20 no final do ciclo de clock. Como no restante desta seção, tratamos de todo o forwarding, exceto para o valor a ser armazenado por uma instrução store.
do forwarding, o registrador ID/EX não precisava incluir espaço a fim de manter o campo rs. Logo, rs (bits 25-21) é acrescentado a ID/EX. Agora, vamos escrever as duas condições para detectar hazards e os sinais de controle para resolvê-los: 1. Hazard EX:
Observe que o campo EX/MEM.RegistradorRd é o destino de registrador para uma instrução da ALU (que vem do campo Rd da instrução) ou um load (que vem do campo Rt). Esse caso faz o forwarding do resultado da instrução anterior para qualquer entrada da ALU. Se a instrução anterior tiver de escrever no banco de registradores e o número do registrador de escrita combinar com o número do registrador de leitura das entradas
296
Capítulo 4 O Processador
FIGURA 4.54 Em cima estão a ALU e os registradores de pipeline antes da inclusão do forwarding. Embaixo, os multiplexadores foram expandidos para acrescentar os caminhos de forwarding, e mostramos a unidade de forwarding. O hardware novo aparece em um destaque. No entanto, essa figura é um desenho estilizado, omitindo os detalhes do caminho de dados completo, como o hardware de extensão de sinal. Observe que o campo ID/EX.RegistradorRt aparece duas vezes, uma para conectar ao mux e uma para a unidade de forwarding, mas esse é um único sinal. Como na discussão anterior, isso ignora o forwarding de um valor armazenado por uma instrução store. Observe que esse mecanismo também funciona para instruções slt.
A ou B da ALU, desde que não seja o registrador 0, então direcione o multiplexador para pegar o valor, e não do registrador de pipeline EX/MEM. 2. Hazard MEM:
4.7 Hazards de dados: forwarding versus stalls
FIGURA 4.55 Os valores de controle para os multiplexadores de forwarding da Figura 4.54. O imediato com sinal que é outra entrada da ALU é descrito na Seção “Detalhamento” ao final desta seção.
Como dissemos, não existe hazard no estágio WB porque consideramos que o banco de registradores fornece o resultado correto se a instrução no estágio ID ler o mesmo registrador escrito pela instrução no estágio WB. Tal banco de registradores realiza outra forma de forwarding, mas isso ocorre dentro do banco de registradores. Uma complicação são os hazards de dados em potencial entre o resultado da instrução no estágio WB, o resultado da instrução no estágio MEM e o operando de origem da instrução no estágio ALU. Por exemplo, ao somar um vetor de números em um único registrador, uma sequência de instruções lerá e escreverá no mesmo registrador:
Nesse caso, o resultado sofre forwarding do estágio MEM, pois o resultado no estágio MEM é o mais recente. Assim, o controle para o hazard em MEM seria (com os acréscimos destacados):
A Figura 4.56 mostra o hardware necessário para dar suporte ao forwarding para operações que utilizam resultados durante o estágio EX. Observe que o campo EX/MEM. RegistradorRd é o destino do registrador para uma instrução ALU (que vem do campo Rd da instrução) ou um load (que vem do campo Rt). A Seção 4.12 no site mostra dois trechos de código MIPS com hazards que causam forwarding, se você quiser ver mais exemplos ilustrados usando desenhos de pipeline de ciclo único.
297
298
Capítulo 4 O Processador
FIGURA 4.56 O caminho de dados modificado para resolver os hazards via forwarding. Em comparação com o caminho de dados da Figura 4.51, os acréscimos são os multiplexadores para as entradas da ALU. Contudo, essa figura é um desenho mais estilizado, omitindo detalhes do caminho de dados completo, como o hardware de desvio e o hardware de extensão de sinal.
Detalhamento: o forwarding também pode ajudar com hazards quando instruções store dependem de outras instruções. Como elas utilizam apenas um valor de dados durante o estágio MEM, o forwarding é fácil. Mas considere os loads imediatamente seguidos por stores, útil quando se realiza cópias da memória para a memória na arquitetura MIPS. Como as cópias são frequentes, precisamos acrescentar mais hardware de forwarding a fim de fazer com que as cópias de memória para memória se tornem mais rápidas. Se tivéssemos de redesenhar a Figura 4.53, substituindo as instruções sub e AND por lw e sw, veríamos que é possível evitar um stall, pois os dados existem no registrador MEM/WB de uma instrução load em tempo para seu uso no estágio MEM de uma instrução store. Para essa opção, teríamos de acrescentar o forwarding para o estágio de acesso à memória. Deixamos essa modificação como um exercício para o leitor. Além disso, a entrada imediata com sinal para a ALU, necessária para loads e stores, não existe no caminho de dados da Figura 4.56. Como o controle central decide entre registrador e imediato, e como a unidade de forwarding escolhe o registrador de pipeline para uma entrada de registrador para a ALU, a solução mais fácil é acrescentar um multiplexador 2:1 que escolha entre a saída do multiplexador ForwardB e o imediato com sinal. A Figura 4.57 mostra esse acréscimo.
Se a princípio você não obteve sucesso, redefina sucesso. Anônimo
Hazards de dados e stalls Conforme dissemos na Seção 4.5, um caso em que o forwarding não pode salvar o dia é quando uma instrução tenta ler um registrador após uma instrução load que escreve no mesmo registrador. A Figura 4.58 ilustra o problema. Os dados ainda são lidos da memória no ciclo de clock 4, enquanto a ALU está realizando a operação para a instrução seguinte. Algo precisa ocasionar um stall no pipeline para a combinação de load seguida por uma instrução que lê seu resultado.
4.7 Hazards de dados: forwarding versus stalls
299
FIGURA 4.57 Uma visão de perto do caminho de dados da Figura 4.54 mostra um multiplexador 2:1, acrescentado para selecionar o imediato com sinal como uma entrada para a ALU.
FIGURA 4.58 Uma sequência de instruções em pipeline. Como a dependência entre o load e a instrução seguinte (and) recua no tempo, esse hazard não pode ser resolvido pelo forwarding. Logo, essa combinação precisa resultar em um stall pela unidade de detecção de hazard.
300
Capítulo 4 O Processador
Logo, além de uma unidade de forwarding, precisamos de uma unidade de detecção de hazard. Ela opera durante o estágio ID, de modo que pode inserir o stall entre o load e seu uso. Verificando as instruções load, o controle para a unidade de detecção de hazard é esta condição única:
nop Uma instrução que não realiza operação para mudar de estado.
A primeira linha testa se a instrução é um load: a única instrução que lê a memória de dados é um load. As duas linhas seguintes verificam se o campo do registrador destino do load no estágio EX combina com qualquer registrador origem da instrução no estágio ID. Se a condição permanecer, a instrução ocasiona um stall de um ciclo de clock no pipeline. Depois desse stall de um ciclo, a lógica de forwarding pode lidar com a dependência e a execução prossegue. (Se não houvesse forwarding, então as instruções na Figura 4.58 precisariam de outro ciclo de stall.) Se a instrução no estágio ID sofrer um stall, então a instrução no estágio IF também precisa sofrer; caso contrário, perderíamos a instrução lida da memória. Evitar que essas duas instruções tenham progresso é algo feito simplesmente impedindo-se que o registrador PC e o registrador de pipeline IF/ID sejam alterados. Desde que esses registradores sejam preservados, a instrução no estágio IF continuará a ser lida usando o mesmo PC, e os registradores no estágio ID continuarão a ser lidos usando os mesmos campos de instrução no registrador de pipeline IF/ID. Retornando à nossa analogia favorita, é como se você reiniciasse a lavadora com as mesmas roupas e deixasse a secadora continuando a trabalhar vazia. Naturalmente, assim como a secadora, a metade do pipeline que começa com o estágio EX precisa estar fazendo algo; o que ela está fazendo é executar instruções que não têm efeito algum: nops. Como podemos inserir esses nops, que atuam como bolhas, no pipeline? Na Figura 4.49, vimos que a desativação de todos os nove sinais de controle (colocando-os em 0) nos estágios EX, MEM e WB criará uma instrução que “não faz nada”, ou nop. Identificando o hazard no estágio ID, podemos inserir uma bolha no pipeline alterando os campos de controle EX, MEM e WB do registrador de pipeline ID/EX para 0. Esses valores de controle benignos são filtrados adiante em cada ciclo de clock com o efeito correto: nenhum registrador ou memória serão modificados se os valores forem todos 0. A Figura 4.59 mostra o que realmente acontece no hardware: a execução do slot do pipeline associado com a instrução AND transforma-se em um nop, e todas as instruções começando com a instrução AND são atrasadas um ciclo. Assim como uma bolha de ar em um cano de água, uma bolha de stall retarda tudo o que está atrás dela e prossegue pelo pipe de instruções um estágio a cada ciclo, até que saia no final. Neste exemplo, o hazard força as instruções AND e OR a repetir no ciclo de clock 4 o que fizeram no ciclo de clock 3: AND lê registradores e decodifica, e OR é apanhado novamente da memória de instruções. Esse trabalho repetido é um stall, mas seu efeito é esticar o tempo das instruções AND e OR e atrasar a busca da instrução add. A Figura 4.60 destaca as conexões do pipeline para a unidade de detecção de hazard e a unidade de forwarding. Como antes, a unidade de forwarding controla os multiplexadores da ALU a fim de substituir o valor de um registrador de uso geral pelo valor do registrador de pipeline apropriado. A unidade de detecção de hazard controla a escrita dos registradores PC e IF/ID mais o multiplexador que escolhe entre os valores de controle reais e 0s. A unidade de detecção de hazard insere um stall e desativa os campos de controle se o teste de hazard do uso do load for verdadeiro. A Seção 4.12, no site, contém um exemplo do código MIPS com hazards, que causa stalling, ilustrado por diagramas de pipeline com ciclo único de clock, se quiser ver mais detalhes.
4.7 Hazards de dados: forwarding versus stalls
301
FIGURA 4.59 O modo como os stalls são realmente inseridos no pipeline. Uma bolha é inserida a partir do ciclo de clock 4, alterando a instrução and para um nop. Observe que a instrução and na realidade é buscada e decodificada nos ciclos de clock 2 e 3, mas seu estágio EX é atrasado até o ciclo de clock 5 (ao contrário da posição sem stall no ciclo de clock 4). Da mesma forma, a instrução or é apanhada no ciclo de clock 3, mas seu estágio IF é atrasado até o ciclo de clock 5 (ao contrário da posição não atrasada no ciclo de clock 4). Após a inserção da bolha, todas as dependências seguem à frente no tempo, e nenhum outro hazard acontece.
FIGURA 4.60 Visão geral do controle em pipeline, mostrando os dois multiplexadores para forwarding, a unidade de detecção de hazard e a unidade de forwarding. Embora os estágios ID e EX tenham sido simplificados – a lógica de extensão de sinal imediato e de desvio estão faltando –, este desenho mostra a essência dos requisitos do hardware de forwarding.
302
Capítulo 4 O Processador
em
Colocando perspectiva
Embora o compilador geralmente conte com o hardware para resolver dependências de hazard e garantir a execução correta, o compilador precisa compreender o pipeline a fim de alcançar o melhor desempenho. Caso contrário, stalls inesperados reduzirão o desempenho do código compilado.
Detalhamento: Com relação ao comentário anterior sobre a colocação das linhas de controle em 0 para evitar a escrita de registradores ou memória: somente os sinais EscreveReg e EscreveMem precisam ser 0, enquanto os outros sinais de controle podem ser don’t care.
Para cada mal que está batendo na raiz há milhares pendurados nos galhos. Henry David Thoreau, Walden, 1854
4.8 Hazards de controle Até aqui, limitamos nossa preocupação aos hazards envolvendo operações aritméticas e transferências de dados. Entretanto, como vimos na Seção 4.5, também existem hazards de pipeline envolvendo desvios. A Figura 4.61 mostra uma sequência de instruções e indica quando o desvio ocorreria nesse pipeline. Uma instrução precisa ser buscada a cada ciclo de clock para sustentar o pipeline, embora, em nosso projeto, a decisão sobre o desvio não ocorra até o estágio MEM do pipeline. Conforme mencionamos na Seção 4.5, esse atraso para determinar a instrução própria a ser buscada é chamado de hazard de controle ou hazard de desvio, ao contrário dos hazards de dados, que acabamos de examinar. Esta seção sobre hazards de controle é mais curta do que as seções anteriores, sobre hazards de dados, porque os hazards de controle são relativamente simples de entender, e ocorrem com menos frequência que os hazards de dados. Além disso, não há nada tão eficiente contra os hazards de controle quanto o forwarding contra os hazards de dados. Logo, usamos esquemas mais simples. Veremos dois esquemas para resolver os hazards de controle e uma otimização para melhorar esses esquemas.
Considere que o desvio não foi tomado
flush Descartar instruções em um pipeline, normalmente devido a um evento inesperado.
Como vimos na Seção 4.5, fazer um stall até que o desvio termine é muito lento. Uma melhoria comum ao stall do desvio é considerar que o desvio não será tomado e, portanto, continuar no fluxo sequencial das instruções. Se o desvio for tomado, as instruções que estão sendo buscadas e decodificadas precisam ser descartadas. A execução continua no destino do desvio. Se os desvios não são tomados na metade das vezes, e se custar pouco descartar as instruções, essa otimização reduz ao meio o custo dos hazards de controle. Para descartar instruções, simplesmente alteramos os valores de controle para 0, assim como fizemos para o stall no hazard de dados no caso do load. A diferença é que também precisamos alterar as três instruções nos estágios IF, ID e EX quando o desvio atingir o estágio MEM; para os stalls no uso de load, simplesmente alteramos o controle para 0 no estágio ID e o deixamos prosseguir no pipeline. Descartar instruções, então, significa que precisamos ser capazes de dar flush nas instruções nos estágios IF, ID e EX do pipeline.
Reduzindo o atraso dos desvios Uma forma de melhorar o desempenho do desvio é reduzir o custo do desvio tomado. Até aqui, consideramos que o próximo PC para um desvio é selecionado no estágio MEM, mas, se movermos a execução do desvio para um estágio anterior do pipeline, então menos instruções precisam sofrer flush. A arquitetura do MIPS foi criada para dar suporte a desvios rápidos de ciclo único, que poderiam passar pelo pipeline com uma pequena penalidade no desvio. Os projetistas observaram que muitos desvios contam apenas com testes simples
4.8 Hazards de controle 303
FIGURA 4.61 O impacto do pipeline sobre a instrução branch. Os números à esquerda da instrução (40, 44, …) são os endereços das instruções. Como a instrução branch decide se deve desviar no estágio MEM – ciclo de clock 4 para a instrução beq, anterior –, as três instruções sequenciais que seguem o branch serão buscadas e iniciarão sua execução. Sem intervenção, essas três instruções seguintes começarão a executar antes que o beq desvie para lw na posição 72. (A Figura 4.31 considerou um hardware extra para reduzir o hazard de controle a um ciclo de clock; essa figura usa o caminho de dados não otimizado.)
(igualdade ou sinal, por exemplo) e que esses testes não exigem uma operação completa da ALU, mas podem ser feitos com no máximo algumas portas lógicas. Quando uma decisão de desvio mais complexa é exigida, uma instrução separada, que usa uma ALU para realizar uma comparação, é requisitada – uma situação semelhante ao uso de códigos de condição para os desvios (veja o Capítulo 2). Levar a decisão do desvio para cima exige que duas ações ocorram mais cedo: calcular o endereço de destino do desvio e avaliar a decisão do desvio. A parte fácil dessa mudança é subir com o cálculo do endereço de desvio. Já temos o valor do PC e o campo imediato no registrador de pipeline IF/ID, de modo que só movemos o somador do desvio do estágio EX para o estágio ID; naturalmente, o cálculo do endereço de destino do desvio será realizado para todas as instruções, mas só será usado quando for necessário. A parte mais difícil é a própria decisão do desvio. Para branch equal, compararíamos os dois registradores lidos durante o estágio ID para ver se são iguais. A igualdade pode ser testada primeiro realizando um OR exclusivo de seus respectivos bits e depois um OR de todos os resultados. Mover o teste de desvio para o estágio ID implica hardware adicional de forwarding e detecção de hazard, visto que um desvio dependente de um resultado ainda no pipeline precisará funcionar corretamente com essa otimização. Por exemplo, a fim de implementar branch-on-equal (e seu inverso), teremos de fazer um forwarding dos resultados para a lógica do teste de igualdade que opera durante o estágio ID. Existem dois fatores que comprometem o procedimento: 1. Durante o estágio ID, temos de decodificar a instrução, decidir se um bypass para a unidade de igualdade é necessário e completar a comparação de igualdade de modo que, se a instrução for um desvio, possamos atribuir ao PC o endereço de destino do
304
Capítulo 4 O Processador
desvio. O forwarding para os operandos dos desvios foi tratado anteriormente pela lógica de forwarding da ALU, mas a introdução da unidade de teste de igualdade no estágio ID exigirá nova lógica de forwarding. Observe que os operandos-fonte de um desvio que sofreram bypass podem vir dos latches do pipeline ALU/MEM ou MEM/ WB. 2. Como os valores em uma comparação de desvio são necessários durante o estágio ID, mas podem ser produzidos mais adiante no tempo, é possível que ocorra um hazard de dados e um stall seja necessário. Por exemplo, se uma instrução da ALU imediatamente antes de um desvio produz um dos operandos para a comparação no desvio, um stall será exigido, já que o estágio EX para a instrução da ALU ocorrerá depois do ciclo de ID do desvio. Por extensão, se um load for imediatamente seguido por um desvio condicional que está no resultado do load, dois ciclos de stall serão necessários, pois o resultado do load aparece no final do ciclo MEM, mas é necessário no início do ID do desvio. Apesar dessas dificuldades, mover a execução do desvio para o estágio ID é uma melhoria, pois reduz a penalidade de um desvio a apenas uma instrução se o desvio for tomado, a saber, aquela sendo buscada atualmente. Os exercícios exploram os detalhes da implementação do caminho de forwarding e a detecção do hazard. Para fazer um flush das instruções no estágio IF, acrescentamos uma linha de controle, chamada IF.Flush, que zera o campo de instrução do registrador de pipeline IF/ID. Apagar o registrador transforma a instrução buscada em um nop, uma instrução que não possui ação e não muda estado algum.
Desvios no pipeline
EXEMPLO
Mostre o que acontece quando o desvio é tomado nesta sequência de instruções, considerando que o pipeline está otimizado para desvios que não são tomados e que movemos a execução do desvio para o estágio ID:
RESPOSTA
A Figura 4.62 mostra o que acontece quando um desvio é tomado. Diferente da Figura 4.61, há somente uma bolha no pipeline para o desvio tomado.
Previsão dinâmica de desvios Supor que um desvio não seja tomado é uma forma simples de previsão de desvios. Nesse caso, prevemos que os desvios não são tomados, fazendo um flush no pipeline quando estivermos errados. Para o pipeline simples, com cinco estágios, essa técnica, possivelmente acoplada com a previsão baseada no compilador, deverá ser adequada. Com pipelines mais profundos, a penalidade do desvio aumenta quando medida em ciclos de clock. Da mesma forma, com a questão múltipla (veja a Seção 4.10), a penalidade do desvio aumenta em termos de instruções perdidas. Essa combinação significa que, em um pipeline agressivo,
4.8 Hazards de controle 305
FIGURA 4.62 O estágio ID do ciclo de clock 3 determina que um desvio precisa ser tomado, de modo que seleciona 72 como próximo endereço do PC e zera a instrução buscada para o próximo ciclo de clock. O ciclo de clock 4 mostra a instrução no local 72 sendo buscada e a única bolha ou instrução nop no pipeline como resultado do desvio tomado. (Como o nop na realidade é s11 $0,$0,0, é discutível se o estágio ID no clock 4 deve ou não ser destacado.)
um esquema de previsão estática provavelmente desperdiçará muito desempenho. Como mencionamos na Seção 4.5, com mais hardware, é possível tentar prever o comportamento do desvio durante a execução do programa. Uma técnica é pesquisar o endereço da instrução para ver se um desvio foi tomado na última vez que essa instrução foi executada e, se foi, começar a buscar novas instruções a partir do mesmo lugar da última vez. Essa técnica é chamada previsão dinâmica de desvios.
previsão dinâmica de desvios Previsão de desvios durante a execução, usando informações em tempo de execução.
306
Capítulo 4 O Processador
buffer de previsão de desvios Também chamado
Uma implementação dessa técnica é um buffer de previsão de desvios, ou tabela de histórico de desvios. Um buffer de previsão de desvios é uma pequena memória indexada pela parte menos significativa do endereço da instrução de desvio. A memória contém um bit que diz se o desvio foi tomado recentemente ou não. Esse é o tipo de buffer mais simples; na verdade, não sabemos se a previsão é a correta – ela pode ter sido colocada lá por outro desvio, que tem os mesmos bits de endereço menos significativos. Mas isso não afeta a exatidão. A previsão é apenas um palpite considerado correto, de modo que a busca começa na direção prevista. Se o palpite estiver errado, as instruções previstas incorretamente são excluídas, o bit de previsão é invertido e armazenado de volta, e a sequência apropriada é buscada e executada. Esse esquema de previsão de 1 bit tem um problema de desempenho: mesmo que um desvio quase sempre seja tomado, provavelmente faremos uma previsão incorreta duas vezes, em vez de uma, quando ele não for tomado. O exemplo a seguir mostra esse dilema.
tabela de histórico de desvios. Uma pequena memória indexada pela parte menos significativa do endereço da instrução de desvio e que contém um ou mais bits indicando se o desvio foi tomado recentemente ou não.
Loops e previsão
EXEMPLO
Considere um desvio de loop que se desvia nove vezes seguidas, depois não é tomado uma vez. Qual é a exatidão da previsão para esse desvio, supondo que o bit de previsão para o desvio permaneça no buffer de previsão?
RESPOSTA
O comportamento da previsão de estado fixo fará uma previsão errada na primeira e última iterações do loop. O erro de previsão na última iteração é inevitável, pois o bit de previsão dirá “tomado”, já que o desvio foi tomado nove vezes seguidas nesse ponto. O erro de previsão na primeira iteração acontece porque o bit é invertido na execução anterior da última iteração do loop, pois o desvio não foi tomado nessa iteração final. Assim, a exatidão da previsão para esse desvio tomado 90% do tempo é apenas de 80% (duas previsões incorretas contra oito corretas). O ideal é que a previsão do sistema combine com a frequência de desvio tomado para esses desvios altamente regulares. Para remediar esse ponto fraco, os esquemas de previsão de 2 bits são utilizados com frequência. Em um esquema de 2 bits, uma previsão precisa estar errada duas vezes antes de ser alterada. A Figura 4.63 mostra a máquina de estados finitos para um esquema de previsão de 2 bits. Um buffer de previsão de desvio pode ser implementado como um pequeno buffer especial, acessado com o endereço da instrução durante o estágio do pipe IF. Se a instrução for prevista como tomada, a busca começa a partir do destino assim que o PC for conhecido; conforme mencionamos anteriormente, isso pode ser até mesmo no estágio ID. Caso contrário, a busca e a execução sequencial continuam. Se a previsão for errada, os bits de previsão são trocados, como mostra a Figura 4.63. Detalhamento: Conforme descrevemos na Seção 4.5, em um pipeline de cinco estágios,
delay slot do desvio O slot diretamente após a instrução de delayed branch, que na arquitetura MIPS é preenchido por uma instrução que não afeta o desvio.
podemos tornar o hazard de controle em um recurso, redefinindo o desvio. Um delayed branch sempre executa a seguinte instrução, mas a segunda instrução após o desvio será afetada pelo desvio. Os compiladores e os montadores tentam colocar uma instrução que sempre executa após o desvio no delay slot do desvio. A tarefa do software é tornar as instruções sucessoras válidas e úteis. A Figura 4.64 mostra as três maneiras como o delay slot do desvio pode ser escalonado. As limitações sobre o escalonamento com delayed branch surgem de (1) as restrições sobre as instruções escalonadas nos delay slots e (2) nossa capacidade de prever durante a compilação se um desvio provavelmente será tomado ou não.
4.8 Hazards de controle 307
FIGURA 4.63 Os estados em um esquema de previsão de 2 bits. Usando 2 bits em vez de 1, um desvio que favoreça bastante a situação “tomado” ou “não tomado” – como muitos desvios fazem – será previsto incorretamente apenas uma vez. Os 2 bits são usados para codificar os quatro estados no sistema. O esquema de 2 bits é um caso geral de uma previsão baseada em contador, incrementado quando a previsão é exata e decrementado em caso contrário, e utiliza o ponto intermediário desse intervalo como divisão entre desvio tomado e não tomado.
FIGURA 4.64 Escalonando o delay slot do desvio. Para cada par de quadros, o quadro de cima mostra o código antes do escalonamento; o quadro de baixo mostra o código escalonado. Em (a), o delay slot é escalonado com uma instrução independente de antes do desvio. Essa é a melhor opção. As estratégias (b) e (c) são usadas quando (a) não é possível. Nas sequências de código para (b) e (c), o uso de $s1 na condição de desvio impede que a instrução add (cujo destino é $s1) seja movida para o delay slot do desvio. Em (b), o delay slot de desvio é escalonado a partir do destino do desvio; normalmente, a instrução de destino precisará ser copiada, pois pode ser alcançada por outro caminho. A estratégia (b) é preferida quando o desvio é tomado com alta probabilidade, como em um desvio de loop. Finalmente, o desvio pode ser escalonado a partir da sequência não tomada, como em (c). Para tornar essa otimização válida para (b) ou (c), deve ser “OK” executar a instrução sub quando o desvio seguir na direção inesperada. Com “OK”, queremos dizer que o trabalho é desperdiçado, mas o programa ainda será executado corretamente. Esse é o caso, por exemplo, se $t4 fosse um registrador temporário não utilizado quando o desvio entrasse na direção inesperada.
308
Capítulo 4 O Processador
O delayed branch foi uma solução simples e eficaz para um pipeline de cinco estágios despachando uma instrução a cada ciclo de clock. À medida que os processadores utilizam pipelines maiores, despachando múltiplas instruções por ciclo de clock (veja Seção 4.10), o atraso do desvio torna-se maior e um único delay slot é insuficiente. Logo, o delayed branch perdeu popularidade em comparação com as técnicas mais dispendiosas, porém mais flexíveis. Simultaneamente, o crescimento em transistores disponíveis por chip tornou a previsão dinâmica relativamente mais barata.
Detalhamento: um previsor de desvios nos diz se um desvio é tomado ou não, mas ainda buffer de destino de desvios Uma estrutura que coloca em cache o PC de destino ou a instrução de destino para um desvio. Ele normalmente é organizado como uma cache com tags, tornando-o mais dispendioso do que um buffer de previsão simples.
previsor correlato Um previsor de desvio que combina o comportamento local de determinado desvio e informações globais sobre o comportamento de algum número recente de desvios executados. previsor de desvio de torneio Um previsor de desvios com múltiplas previsões para cada desvio e um mecanismo de seleção que escolhe qual previsor deve ser usado para determinado desvio
exige o cálculo do destino do desvio. No pipeline de cinco estágios, esse cálculo leva um ciclo, significando que os desvios tomados terão uma penalidade de um ciclo. Os delayed branches são uma técnica para eliminar essa penalidade. Outra técnica é usar uma cache para manter o contador de programa de destino ou instrução de destino, usando um buffer de destino de desvios. O esquema de previsão dinâmica de 2 bits usa apenas informações sobre um determinado desvio. Os pesquisadores notaram que o uso de informações sobre um desvio local e um comportamento global de desvios executados recentemente, juntos, geram maior exatidão da previsão para o mesmo número de bits de previsão. Essas técnicas são chamadas de previsor correlato. Um previsor correlato simples poderia ter dois previsores de 2 bits para cada desvio, com a escolha entre os previsores feita com base em se o último desvio executado foi tomado ou não. Assim, o comportamento de desvio global pode ser imaginado como acrescentando bits de índice adicionais para a previsão. Uma inovação mais recente na previsão de desvios é o uso de previsões de torneio. Um previsor de torneio utiliza vários previsores, acompanhando, para cada desvio, qual previsor gera os melhores resultados. Um previsor de torneio típico poderia conter duas previsões para cada índice de desvio: uma baseada em informações locais e uma baseada no comportamento do desvio global. Um seletor escolheria qual previsor usar para qualquer previsão dada. O seletor pode operar semelhantemente a um previsor de 1 ou 2 bits, favorecendo qualquer um dos dois previsores que tenha sido mais preciso. Muitos microprocessadores avançados mais recentes utilizam esses previsores rebuscados.
Detalhamento: Uma maneira de reduzir o número de desvios condicionais é acrescentar instruções de move condicional. Em vez de mudar o PC com um desvio condicional, a instrução muda condicionalmente o registrador de destino do move. Se a condição falha, o move atua como um nop. Por exemplo, uma versão da arquitetura do conjunto de instruções MIPS tem duas novas instruções chamadas movn (move if not zero) e movz (move if zero). Assim, movn $8,$11,$4 copia o conteúdo do registrador 11 para o registrador 8, desde que o valor no registrador 4 seja diferente de zero; caso contrário, ela não faz nada. O conjunto de instruções ARM tem um campo de condição na maioria das instruções. Assim, os programas ARM poderiam ter menos desvios condicionais que os programas MIPS.
Resumo sobre pipeline Começamos na lavanderia, mostrando princípios de pipelining em um ambiente do dia a dia. Usando essa analogia como um guia, explicamos o pipelining de instruções passo a passo, começando com um caminho de dados de ciclo único e depois acrescentando registradores de pipeline, caminhos de forwarding, detecção de hazard de dados, previsão de desvio e com flushing de instruções em exceções. A Figura 4.65 mostra o caminho de dados e controle finais. Agora, estamos prontos para outro hazard de controle: a questão complicada das exceções.
Verifique você mesmo
Considere três esquemas de previsão de desvios: desvio não tomado, previsão tomada e previsão dinâmica. Suponha que todos eles tenham penalidade zero quando preveem corretamente e 2 ciclos quando estão errados. Suponha que a exatidão média da previsão do previsor dinâmico seja de 90%. Qual previsor é a melhor escolha para os seguintes desvios?
4.9 Exceções 309
FIGURA 4.65 O caminho de dados e controle final para este capítulo. Observe que essa é uma figura estilizada, em vez de um caminho de dados detalhado, de modo que não contém o mux OrigALU da Figura 4.57 e os controles multiplexadores da Figura 4.51.
1. Um desvio tomado com frequência de 5%. 2. Um desvio tomado com frequência de 95%. 3. Um desvio tomado com frequência de 70%.
4.9 Exceções Controle é o aspecto mais desafiador do projeto do processador: ele é a parte mais difícil de se acertar e a parte mais difícil de tornar mais rápida. Uma das partes mais difíceis do controle é implementar exceções e interrupções — eventos diferentes dos desvios ou saltos, que mudam o fluxo normal da execução da instrução. Eles foram criados inicialmente para tratar de eventos inesperados de dentro do processador, como o overflow aritmético. O mesmo mecanismo básico foi estendido para os dispositivos de E/S se comunicarem com o processador, conforme veremos no Capítulo 6. Muitas arquiteturas e autores não fazem distinção entre interrupções e exceções, normalmente usando o nome mais antigo interrupção para se referirem aos dois tipos de eventos. Por exemplo, o Intel x86 usa interrupção. Seguimos a convenção do MIPS, usando o termo exceção para indicar qualquer mudança inesperada no fluxo de controle, sem distinguir se a causa é interna ou externa; usamos o termo interrupção apenas quando o evento é causado externamente. Aqui estão alguns exemplos mostrando se a situação é gerada internamente pelo processador ou se é gerada externamente:
Fazer um computador com facilidades automáticas de interrupção de programa se comportar [sequencialmente] não foi uma tarefa fácil, pois o número de instruções em diversos estágios do processamento quando um sinal de interrupção ocorre pode ser muito grande. Fred Brooks Jr., Planning a Computer System: Project Stretch, 1962 exceção Também chamada interrupção. Um evento não programado que interrompe a execução do programa; usada para detectar overflow. interrupção Uma exceção que vem de fora do processador. (Algumas arquiteturas utilizam o termo interrupção para todas as exceções.)
310
Capítulo 4 O Processador
Tipo de evento
De onde?
Terminologia MIPS
Solicitação de dispositivo de E/S
Externa
Interrupção
Chamar o sistema operacional do programa do usuário
Interna
Exceção
Overflow aritmético
Interna
Exceção
Usar uma instrução indefinida
Interna
Exceção
Defeitos do hardware
Ambos
Exceção ou interrupção
Muitos dos requisitos para dar suporte a exceções vêm da situação específica que causa a ocorrência de uma exceção. Consequentemente, retornaremos a esse assunto no Capítulo 5, quando tratarmos de hierarquias de memória, e no Capítulo 6, quando discutirmos sobre E/S, e entendermos melhor a motivação para as capacidades adicionais no mecanismo de exceção. Nesta seção, lidamos com a implementação de controle de modo a detectar dois tipos de exceções que surgem das partes do conjunto de instruções e da implementação que já discutimos. Detectar condições excepcionais e tomar a ação apropriada normalmente está no percurso de temporização crítico de um processador, que determina o tempo de ciclo de clock e, portanto, o desempenho. Sem a devida atenção às exceções durante o projeto da unidade de controle, as tentativas de acrescentar exceções a uma implementação complicada podem reduzir o desempenho significativamente, bem como complicar a tarefa de corrigir o projeto.
Como as exceções são tratadas em uma arquitetura MIPS
interrupção vetorizada Uma interrupção para a qual o endereço para onde o controle é transferido é determinado pela causa da exceção.
Os dois tipos de exceções que nossa implementação atual pode gerar são a execução de uma instrução indefinida e um overflow aritmético. Usaremos o overflow aritmético na instrução add $1,$2,$1 como exemplo de exceção nas próximas páginas. A ação básica que o processador deve realizar quando ocorre uma exceção é salvar o endereço da instrução causadora no PC de Exceção (Exception Program Counter – EPC) e depois transferir o controle para o sistema operacional em algum endereço especificado. O sistema operacional pode então tomar a ação apropriada, que pode ser fornecer algum serviço ao programa do usuário, tomar alguma ação predefinida em resposta a um overflow, ou terminar a execução do programa e informar um erro. Depois de realizar qualquer ação necessária devido à exceção, o sistema operacional pode terminar o programa ou pode continuar sua execução, usando o EPC para determinar onde reiniciar a execução do programa. No Capítulo 5, veremos mais de perto a questão da retomada da execução. Para o sistema operacional tratar da exceção, ele precisa conhecer o motivo da exceção, além da instrução que a causou. Existem dois métodos principais usados para comunicar o motivo de uma exceção. O da arquitetura MIPS é incluir um registrador de status (chamado registrador Cause), que mantém um campo que indica o motivo da exceção. Um segundo método é usar interrupções vetorizadas. Em uma interrupção vetorizada, o endereço ao qual o controle é transferido é determinado pela causa da exceção. Por exemplo, para acomodar os dois tipos de exceção listados anteriormente, poderíamos definir os dois endereços de vetor de exceção a seguir: Tipo de exceção
Endereço do vetor de exceção (emhexa)
Instrução indefinida
8000 0000hexa
Overflow aritmético
8000 0180hexa
O sistema operacional sabe o motivo para a exceção pelo endereço em que ela é iniciada. Os endereços são separados por 32 bytes ou oito instruções, e o sistema operacional precisa registrar o motivo para a exceção e pode realizar algum processamento limitado nessa
4.9 Exceções 311
sequência. Quando a exceção não é vetorizada, um único ponto de entrada para todas as exceções pode ser utilizado, e o sistema operacional decodifica o registrador de status para encontrar a causa. Podemos realizar o processamento exigido para exceções acrescentando alguns registradores e sinais de controle extras à nossa implementação básica e estendendo o controle ligeiramente. Vamos supor que estejamos implementando o sistema de exceção utilizado na arquitetura MIPS, com o único ponto de entrada sendo o endereço 8000 0180hexa. (A implementação de exceções vetorizadas não é mais difícil.) Precisaremos acrescentar dois registradores adicionais à implementação MIPS: j
EPC: Um registrador de 32 bits usado para manter o endereço da instrução afetada. (Esse registrador é necessário mesmo quando as exceções são vetorizadas.)
j
Cause: Um registrador usado para registrar a causa da exceção. Na arquitetura MIPS, esse registrador tem 32 bits, embora alguns bits atualmente não sejam utilizados. Suponha que haja um campo de cinco bits que codifica as duas fontes de informação possíveis mencionadas anteriormente, com 10 representando uma instrução indefinida e 12 representando o overflow aritmético.
Exceções em uma implementação em pipeline Uma implementação em pipeline trata exceções como outra forma de hazard de controle. Por exemplo, suponha que haja um overflow aritmético em uma instrução add. Assim como fizemos para o desvio tomado na seção anterior, temos de dar flush nas instruções que vêm após a instrução add do pipeline e começar a buscar instruções do novo endereço. Usaremos o mesmo mecanismo que usamos para os desvios tomados, mas, desta vez, a exceção causa a desativação das linhas de controle. Quando lidamos com um desvio mal previsto, vimos como dar flush na instrução no estágio IF, transformando-a em um nop. Para dar flush nas instruções no estágio ID, usamos o multiplexador já presente no estágio ID que zera os sinais de controle para stalls. Um novo sinal de controle, chamado ID.Flush, realiza um OR com o sinal de stall da Unidade de Detecção de Hazards a fim de dar flush durante o ID. Para dar flush na instrução em EX, usamos um novo sinal, chamado EX.Flush, fazendo com que novos multiplexadores zerem as linhas de controle. Para começar a buscar instruções do local 8000 0180hexa, que é o local da exceção para o overflow aritmético, simplesmente acrescentamos uma entrada adicional ao multiplexador do PC, que envia 8000 0180hexa ao PC. A Figura 4.66 mostra essas mudanças. Este exemplo aponta um problema com as exceções: se não pararmos a execução no meio da instrução, o programador não poderá ver o valor original do registrador $1 que ajudou a causar o overflow, pois funcionará como registrador de destino da instrução add. Devido ao planejamento cuidadoso, a exceção de overflow é detectada durante o estágio EX; logo, podemos usar o sinal EX.Flush para impedir que a instrução no estágio EX escreva seu resultado no estágio WB. Muitas exceções exigem que, por fim, completemos a instrução que causou a exceção como se ela fosse executada normalmente. O modo mais fácil de fazer isso é dar flush na instrução e reiniciá-la desde o início após a exceção ser tratada. A etapa final é salvar o endereço da instrução problemática no Exception Program Counter (EPC). Na realidade, salvamos o endereço + 4, de modo que a rotina de tratamento da exceção primeiro deve subtrair 4 do valor salvo. A Figura 4.66 mostra uma versão estilizada do caminho de dados, incluindo o hardware de desvio e as acomodações necessárias para tratar das exceções.
312
Capítulo 4 O Processador
Exceção em um computador com pipeline
EXEMPLO
Dada esta sequência de instruções
considere que as instruções a serem invocadas em uma exceção comecem desta forma:
Mostre o que acontece no pipeline se houver uma exceção de overflow na instrução add.
RESPOSTA
A Figura 4.67 mostra os eventos, começando com a instrução add no estágio EX. O overflow é detectado durante essa fase, e 8000 0180hexa é forçado para o PC. O ciclo de clock 7 mostra que o add e as instruções seguintes sofrem flush, e a primeira instrução do código de exceção é buscada. Observe que o endereço da instrução seguinte ao add é salvo: 4Chexa + 4 = 50hexa.
FIGURA 4.66 O caminho de dados com controles para lidar com exceções. Os principais acréscimos incluem uma nova entrada, com o valor 8000 0180hexa, no multiplexador que fornece o novo valor do PC; um registrador Cause para registrar a causa da exceção; e um registrador PC de Exceção (Exception Program Counter – EPC) para salvar o endereço da instrução que causou a exceção. A entrada 8000 0180hexa para o multiplexador é o endereço inicial para começar a buscar instruções no caso de uma exceção. Embora não apareça, o sinal de overflow da ALU é uma entrada para a unidade de controle.
4.9 Exceções 313
FIGURA 4.67 O resultado de uma exceção devido a um overflow aritmético na instrução add. O overflow é detectado durante o estágio EX do clock 6, salvando o endereço após o add no registrador EPC (4C + 4 = 50hexa). O overflow faz com que todos os sinais Flush sejam ativados perto do final desse ciclo de clock, desativando os valores de controle (colocando-os em 0) para o add. O ciclo de clock 7 mostra as instruções convertidas para bolhas no pipeline mais a busca da primeira instrução da rotina de exceção – sw $25,1000($0) – a partir do local da instrução 8000 0180hexa. Observe que as instruções AND e OR, que estão antes do add, ainda completam. Embora não apareça, o sinal de overflow da ALU é uma entrada para a unidade de controle.
314
Capítulo 4 O Processador
Mencionamos cinco exemplos de exceções na tabela da Seção 4.9, e veremos outros nos Capítulos 5 e 6. Com cinco instruções ativas em qualquer ciclo de clock, o desafio é associar uma exceção à instrução apropriada. Além do mais, várias exceções podem ocorrer simultaneamente em um único ciclo de clock. A solução é priorizar as exceções de modo que seja fácil determinar qual será atendida primeiro. Na maioria das implementações MIPS, o hardware ordena as exceções de modo que a instrução mais antiga seja interrompida. Solicitações de dispositivos de E/S e defeitos do hardware não estão associados a uma instrução específica, de modo que a implementação possui alguma flexibilidade quanto ao momento de interromper o pipeline. Logo, usar o mecanismo utilizado para outras exceções funciona muito bem. O EPC captura o endereço das instruções interrompidas, e o registrador Cause do MIPS registra todas as exceções possíveis em um ciclo de clock, de modo que o software de exceção precisa combinar a exceção à instrução. Uma dica importante é saber em que estágio do pipeline um tipo de exceção pode ocorrer. Por exemplo, uma instrução indefinida é descoberta no estágio ID, e a chamada ao sistema operacional ocorre no estágio EX. As exceções são coletadas no registrador Cause em um campo de exceção pendente, de modo que o hardware possa interromper com base em exceções posteriores, uma vez que a mais antiga tenha sido atendida.
Interface hardware/ software
O hardware e o sistema operacional precisam trabalhar em conjunto para que as exceções se comportem conforme o esperado. O contrato do hardware normalmente é interromper a instrução problemática no meio do caminho, deixar que todas as instruções anteriores terminem, dar flush em todas as instruções seguintes, definir um registrador para mostrar a causa da exceção, salvar o endereço da instrução problemática e depois desviar para um endereço previamente arranjado. O contrato do sistema operacional é examinar a causa da exceção e atuar de forma apropriada. Para uma instrução indefinida, falha de hardware ou exceção por overflow aritmético, o sistema operacional normalmente encerra o programa e retorna um indicador do motivo. Para uma solicitação de dispositivo de E/S ou uma chamada de serviço ao sistema operacional, o sistema operacional salva o estado do programa, realiza a tarefa desejada e, em algum ponto no futuro, restaura o programa para continuar a execução. No caso das solicitações do dispositivo de E/S, normalmente podemos escolher executar outra tarefa antes de retomar a tarefa que requisitou a E/S, pois essa tarefa em geral pode não ser capaz de prosseguir até que a E/S termine. É por isso que é fundamental a capacidade de salvar e restaurar o estado de qualquer tarefa. Um dos usos mais importantes e frequentes das exceções é o tratamento de faltas de página e exceções de TLB; o Capítulo 5 descreve essas exceções e seu tratamento com mais detalhes.
Detalhamento: A dificuldade de sempre associar a exceção correta à instrução correta nos interrupção imprecisa Também chamada exceção imprecisa. As interrupções ou exceções nos computadores em pipeline não estão associadas à instrução exata que foi a causa da interrupção ou exceção.
interrupção precisa Também chamada exceção precisa. Uma interrupção ou exceção que está sempre associada à instrução correta nos computadores em pipeline.
computadores em pipeline levou alguns projetistas de computador a relaxarem esse requisito em casos não críticos. Alguns processadores são considerados como tendo interrupções imprecisas ou exceções imprecisas. No exemplo anterior, o PC normalmente teria 58hexa no início do ciclo de clock, depois que a exceção for detectada, embora a instrução com problema esteja no endereço 4Chexa. Um processador com exceções imprecisas poderia colocar 58hexa no EPC e deixar que o sistema operacional determinasse qual instrução causou o problema. O MIPS e a grande maioria dos computadores de hoje admitem interrupções precisas ou exceções precisas. (Um motivo é para dar suporte à memória virtual, que veremos no Capítulo 5.)
Detalhamento: Embora o MIPS utilize o endereço de entrada de exceção 8000 0180hexa para quase todas as exceções, ele usa o endereço 8000 0000hexa de modo a melhorar o desempenho do tratador de exceção para exceções de falta de TLB (veja Capítulo 5).
4.10 Paralelismo e paralelismo avançado em nível de instrução 315
Qual exceção deverá ser reconhecida primeiro nesta sequência? 1. add $1, $2, S1 # overflow aritmético
Verifique você mesmo
2. XXX $1, $2, $1 # instrução indefinida 3. sub $1, $2, $1 # erro de hardware
4.10
aralelismo e paralelismo avançado P em nível de instrução
Esteja avisado de que esta seção é uma breve introdução de assuntos fascinantes, porém avançados. Se você quiser saber mais detalhes, deverá consultar nosso livro mais avançado, Arquitetura de Computadores: uma abordagem quantitativa, quarta edição, no qual o material explicado nas próximas páginas é expandido para mais de 200 páginas (incluindo Apêndices)! A técnica de pipelining explora o paralelismo em potencial entre as instruções. Esse paralelismo é chamado de paralelismo em nível de instrução (ILP – Instruction-Level Parallelism). Existem dois métodos principais para aumentar a quantidade em potencial de paralelismo em nível de instrução. O primeiro é aumentar a profundidade do pipeline para sobrepor mais instruções. Usando nossa analogia da lavanderia e considerando que o ciclo da lavadora fosse maior do que os outros, poderíamos dividir nossa lavadora em três máquinas que lavam, enxáguam e centrifugam, como as etapas de uma lavadora tradicional. Poderíamos, então, passar de um pipeline de quatro para seis estágios. Para ganhar o máximo de velocidade, precisamos rebalancear as etapas restantes de modo que tenham o mesmo tamanho, nos processadores ou na lavanderia. A quantidade de paralelismo sendo explorada é maior, pois existem mais operações sendo sobrepostas. O desempenho é potencialmente maior, pois o ciclo de clock pode ser encurtado. Outra técnica é replicar os componentes internos do computador de modo que ele possa iniciar várias instruções em cada estágio do pipeline. O nome geral para essa técnica é despacho múltiplo. Uma lavanderia com despacho múltiplo substituiria nossa lavadora e secadora doméstica por, digamos, três lavadoras e três secadoras. Você também teria de recrutar mais auxiliares para passar e guardar três vezes a quantidade de roupas no mesmo período. A desvantagem é o trabalho extra de manter todas as máquinas ocupadas e transferir as trouxas de roupa para o próximo estágio do pipeline. Disparar várias instruções por estágio permite que a velocidade de execução da instrução exceda a velocidade de clock ou, de forma alternativa, que o CPI seja menor do que 1. Às vezes, é útil inverter a métrica e usar o IPC, ou instrução por ciclo de clock, principalmente quando os valores se tornam menores do que 1! Logo, um microprocessador de despacho múltiplo quádruplo de 4 GHz pode executar uma velocidade de pico de 16 bilhões de instruções por segundo e ter um CPI de 0,25 no melhor dos casos, ou um IPC de 4. Considerando um pipeline de cinco estágios, esse processador teria 20 instruções em execução em determinado momento. Os microprocessadores mais potentes de hoje tentam despachar de três a oito instruções a cada ciclo de clock. Entretanto, normalmente existem muitas restrições sobre os tipos das instruções que podem ser executadas simultaneamente e o que acontece quando surgem dependências. Existem duas maneiras importantes de implementar um processador de despacho múltiplo, com a principal diferença sendo a divisão de trabalho entre o compilador e o hardware. Como a divisão do trabalho indica se as decisões estão sendo feitas estaticamente (ou seja, durante a compilação) ou dinamicamente (ou seja, durante a execução), as técnicas às vezes são chamadas de despacho múltiplo estático e despacho múltiplo dinâmico. Como veremos, as duas técnicas possuem outros nomes, usados mais comumente, que podem ser menos precisos ou mais restritivos.
paralelismo em nível de instrução O paralelismo entre as instruções.
despacho múltiplo Um esquema pelo qual múltiplas instruções são disparadas em 1 ciclo de clock.
despacho múltiplo estático Uma técnica para implementar um processador de despacho múltiplo em que muitas decisões são tomadas pelo compilador antes da execução.
despacho múltiplo dinâmico Uma técnica para implementar um processador de despacho múltiplo em que muitas decisões são tomadas durante a execução pelo processador.
316
Capítulo 4 O Processador
Existem duas responsabilidades principais e distintas que precisam ser tratadas em um pipeline de despacho múltiplo: slots de despacho As posições das quais as instruções poderiam ser despachadas em determinado ciclo de clock; por analogia, correspondem a posições nos blocos iniciais para um sprint.
1. Empacotar as instruções em slots de despacho: como o processador determina quantas instruções e quais instruções podem ser despachadas em determinado ciclo de clock? Na maioria dos processadores de despacho estático, esse processo é tratado pelo menos parcialmente pelo compilador; nos projetos de despacho dinâmico, isso normalmente é tratado durante a execução pelo processador, embora o compilador em geral já tenha tentado ajudar a melhorar a velocidade do despacho colocando as instruções em uma ordem benéfica. 2. Lidar com hazards de dados e de controle: em processadores de despacho estático, algumas ou todas as consequências dos hazards de dados e controle são tratadas estaticamente pelo compilador. Ao contrário, a maioria dos processadores de despacho dinâmico tenta aliviar pelo menos algumas classes de hazards usando técnicas de hardware operando durante a execução. Embora as tenhamos descrito como técnicas distintas, na realidade, cada técnica pega algo emprestado da outra, e nenhuma pode afirmar ser perfeitamente pura.
O conceito de especulação especulação Uma técnica pela qual o compilador ou processador adivinha o resultado de uma instrução para removê-la como uma dependência na execução de outras instruções.
Um dos métodos mais importantes para localizar e explorar mais ILP é a especulação. Especulação é uma técnica que permite que o compilador ou o processador “adivinhem” as propriedades de uma instrução, de modo a permitir que a execução comece para outras instruções que possam depender da instrução especulada. Por exemplo, poderíamos especular a respeito do resultado de um desvio, de modo que as instruções após o desvio pudessem ser executadas mais cedo. Outro exemplo é que poderíamos especular que um store que precede um load não se refere ao mesmo endereço, o que permitiria que o load fosse executado antes do store. A dificuldade com a especulação é que ela pode estar errada. Assim, qualquer mecanismo de especulação deve incluir tanto um método para verificar se a escolha foi certa quanto um método para retornar ou retroceder os efeitos das instruções executadas de forma especulativa. A implementação dessa capacidade de retrocesso aumenta a complexidade. A especulação pode ser feita pelo compilador ou pelo hardware. Por exemplo, o compilador pode usar a especulação para reordenar as instruções, fazendo uma instrução passar por um desvio ou um load passar por um store. O hardware do processador pode realizar a mesma transformação durante a execução, usando técnicas que discutiremos mais adiante nesta seção. Os mecanismos de recuperação usados para a especulação incorreta são bem diferentes. No caso da especulação em software, o compilador normalmente insere instruções adicionais que verificam a precisão da especulação e oferecem uma rotina de reparo para usar quando a especulação tiver sido incorreta. Na especulação em hardware, o processador normalmente coloca os resultados especulativos em um buffer até que saiba que não são mais especulativos. Se a especulação estiver correta, as instruções são concluídas, permitindo que o conteúdo dos buffers seja escrito nos registradores ou na memória. Se a especulação estiver incorreta, o hardware faz um flush nos buffers e executa novamente, mas na sequência de instruções correta. A especulação apresenta outro problema possível: especular sobre certas instruções pode gerar exceções que anteriormente não estavam presentes. Por exemplo, suponha que uma instrução load seja movida de uma maneira especulativa, mas o endereço que usa não é válido quando a especulação for incorreta. O resultado é que ocorrerá uma exceção que não deveria ter ocorrido. O problema é complicado pelo fato de que, se a instrução load não fosse especulativa, então, a exceção deveria ocorrer! Na especulação feita pelo compilador, esses problemas são evitados pelo acréscimo de suporte especial à especulação, que permite que tais exceções sejam ignoradas até que esteja claro que elas realmente devam ocorrer. Na especulação por hardware, as exceções são simplesmente mantidas em um buffer até
4.10 Paralelismo e paralelismo avançado em nível de instrução 317
que fique claro que a instrução que as causa não é mais especulativa e está pronta para terminar; nesse ponto, a exceção é gerada, e prossegue o tratamento normal da exceção. Como a especulação pode melhorar o desempenho quando realizada corretamente e diminuir o desempenho quando feita descuidadamente, é preciso haver muito esforço na decisão de quando a especulação é apropriada. Mais adiante, nesta seção, vamos examinar as técnicas estática e dinâmica para a especulação.
Despacho múltiplo estático Todos os processadores de despacho múltiplo estático utilizam o compilador para ajudar no empacotamento de instruções e no tratamento de hazards. Em um processador de despacho estático, você pode pensar no conjunto de instruções despachadas em determinado ciclo de clock, o que é chamado pacote de despacho, como uma grande instrução com várias operações. Essa visão é mais do que uma analogia. Como um processador de despacho múltiplo estático normalmente restringe o mix de instruções que podem ser iniciadas em determinado ciclo de clock, é útil pensar no pacote de despacho como uma única instrução, permitindo várias operações em certos campos predefinidos. Essa visão levou ao nome original para essa técnica: VLIW (Very Long Instruction Word – palavra de instrução muito longa). A maioria dos processadores de despacho estático também conta com o compilador para assumir alguma responsabilidade por tratar de hazards de dados e controle. As responsabilidades do compilador podem incluir previsão estática de desvios e escalonamento de código, para reduzir ou impedir todos os hazards. Vejamos uma versão simples do despacho estático de um processador MIPS, antes de descrevermos o uso dessas técnicas em processadores mais agressivos. Um exemplo: despacho múltiplo estático com a ISA do MIPS
Para que você tenha uma ideia do despacho múltiplo estático, consideramos um processador MIPS simples capaz de despachar duas instruções por ciclo, sendo que uma das instruções pode ser uma operação da ALU com inteiros e a outra pode ser um load ou um store. Esse projeto é como aquele utilizado em alguns processadores MIPS embutidos. O despacho de duas instruções por ciclo exigirá a busca e a decodificação de 64 bits de instruções. Em muitos processadores de despacho múltiplo, e basicamente em todos os processadores VLIW, o layout do despacho de instruções simultâneas é restrito para simplificar a decodificação e o despacho da instrução. Logo, exigiremos que as instruções sejam emparelhadas e alinhadas em um limite de 64 bits, com a parte da ALU ou desvio aparecendo primeiro. Além do mais, se uma instrução do par não puder ser usada, exigimos que ela seja substituída por um nop. Assim, as instruções sempre são despachadas em pares, possivelmente com um nop em um slot. A Figura 4.68 mostra como as instruções aparecem enquanto entram no pipeline em pares.
FIGURA 4.68 Pipeline com despacho estático de duas instruções em operação. As instruções da ALU e de transferência de dados são despachadas ao mesmo tempo. Aqui, consideramos a mesma estrutura de cinco estágios utilizada para o pipeline de despacho único. Embora isso não seja estritamente necessário, possui algumas vantagens. Em particular, manter as escritas de registrador no final do pipeline simplifica o tratamento de exceções e a manutenção de um modelo de exceção preciso, que se torna mais difícil em processadores de despacho múltiplo.
pacote de despacho O conjunto de instruções despachadas juntas em um ciclo de clock; o pacote pode ser determinado estaticamente, pelo compilador, ou dinamicamente, pelo processador. VLIW (Very Long Instruction Word) Um estilo de arquitetura de conjunto de instruções que dispara muitas operações definidas para serem independentes em uma única instrução larga, normalmente com muitos campos de opcode separados.
318
Capítulo 4 O Processador
Os processadores de despacho múltiplo estático variam no modo como lidam com hazards de dados e controle em potencial. Em alguns projetos, o compilador tem responsabilidade completa por remover todos os hazards, escalonando o código e inserindo no-ops de modo que o código execute sem qualquer necessidade de detecção de hazard ou stalls gerados pelo hardware. Em outros, o hardware detecta os hazards de dados e gera stalls entre dois pacotes de despacho, enquanto exige que o compilador evite todas as dependências dentro de um par de instruções. Mesmo assim, um hazard geralmente força o pacote de despacho inteiro contendo a instrução dependente a sofrer stall. Se o software precisa lidar com todos os hazards ou apenas tentar reduzir a fração de hazards entre pacotes de despacho separados, a aparência de haver uma única grande instrução com várias operações é reforçada. Ainda assumiremos a segunda técnica para esse exemplo. Para emitir uma operação da ALU e uma operação de transferência de dados em paralelo, a primeira necessidade para o hardware adicional – além da lógica normal de detecção de hazard e stall – são portas extras no banco de registradores (veja Figura 4.69). Em um ciclo de clock, podemos ter de ler dois registradores para a operação da ALU e mais dois para um store, e também uma porta de escrita para uma operação da ALU e uma porta de escrita para um load. Como a ALU está presa à operação da ALU, também precisamos de um somador separado a fim de calcular o endereço efetivo para as transferências de dados. Sem esses recursos extras, nosso pipeline com despacho duplo seria atrapalhado pelos hazards estruturais. Claramente, esse processador com despacho duplo pode melhorar o desempenho por um fator de até 2. Entretanto, fazer isso exige que o dobro de instruções seja superposto na execução, e essa sobreposição adicional aumenta a perda de desempenho relativa aos
FIGURA 4.69 Um caminho de dados com despacho duplo estático. Os acréscimos necessários para o despacho duplo estão destacados: outros 32 bits da memória de instruções, mais duas portas de leitura e mais uma porta de escrita no banco de registradores, e outra ALU. Suponha que a ALU inferior trate dos cálculos de endereço para transferências de dados e a ALU superior trate de todo o restante..
4.10 Paralelismo e paralelismo avançado em nível de instrução 319
hazards de dados e controle. Por exemplo, em nosso pipeline simples de cinco estágios, os loads possuem uma latência de uso de um ciclo de clock, o que impede que uma instrução use o resultado sem sofrer stall. No pipeline com despacho duplo e cinco estágios, o resultado de uma instrução load não pode ser usado no próximo ciclo de clock. Isso significa que as duas instruções seguintes não podem usar o resultado do load sem sofrer stall. Além do mais, as instruções da ALU que não tiveram latência de uso no pipeline simples de cinco estágios agora possuem uma latência de uso de uma instrução, pois os resultados não podem ser usados no load ou store emparelhados. Para explorar com eficiência o paralelismo disponível em um processador com despacho múltiplo, é preciso utilizar técnicas mais ambiciosas de escalonamento de compilador ou hardware, e o despacho múltiplo estático requer que o compilador assuma essa função.
latência de uso Número de ciclos de clock entre uma instrução load e uma instrução que pode usar o resultado do load sem stall do pipeline.
Escalonamento de código simples para despacho múltiplo
Como este loop seria escalonado em um pipeline com despacho duplo estático para o MIPS?
EXEMPLO
Reordene as instruções para evitar o máximo de stalls do pipeline possível. Considere que os desvios são previstos, de modo que os hazards de controle sejam tratados pelo hardware. As três primeiras instruções possuem dependências de dados, bem como as duas últimas. A Figura 4.70 mostra o melhor escalonamento para essas instruções. Observe que apenas um par de instruções possui os dois slots utilizados. São necessários quatro clocks por iteração do loop; em quatro clocks para executar cinco instruções, obtemos o CPI decepcionante de 0,8 versus o melhor caso de 0,5, ou um IPC de 1,25 versus 2,0. Observe que, no cálculo do CPI ou do IPC, não contamos quaisquer nops executados como instruções úteis. Isso melhoraria o CPI, mas não o desempenho!
FIGURA 4.70 O código escalonado conforme apareceria em um pipeline MIPS com despacho duplo. Os slots vazios são nops.
RESPOSTA
320
Capítulo 4 O Processador
desdobramento de loop (loop unrolling) Uma técnica para
Uma técnica de compilador importante para conseguir mais desempenho dos loops é o desdobramento de loop (loop unrolling), em que são feitas várias cópias do corpo do loop. Após o desdobramento, haverá mais ILP disponível pela sobreposição de instruções de diferentes iterações.
conseguir mais desempenho dos loops que acessam arrays, em que são feitas várias cópias do corpo do loop e instruções de diferentes iterações são escalonadas juntas.
Desdobramento de loop para pipelines com despacho múltiplo
EXEMPLO
Veja como o trabalho de desdobramento do loop e escalonamento funciona no exemplo anterior. Para simplificar, suponha que o índice do loop seja um múltiplo de quatro.
RESPOSTA
Para escalonar o loop sem quaisquer atrasos, acontece que precisamos fazer quatro cópias do corpo do loop. Depois de desdobrar e eliminar as instruções de overhead de loop desnecessárias, o loop terá quatro cópias de lw, add e sw, mais um addi e um bne. A Figura 4.71 mostra o código desdobrado e escalonado. Durante o processo de desdobramento, o compilador introduziu registradores adicionais ($t1,$t2,$t3). O objetivo desse processo, chamado renomeação de registradores, é eliminar dependências que não são dependências de dados verdadeiras, mas que poderiam levar a hazards em potencial ou impedir que o compilador escalonasse o código de forma flexível. Considere como o código não desdobrado apareceria usando apenas $t0. Haveria instâncias repetidas de lw $t0,0($s1), addu $t0,$t0,$s2 seguidas por sw t0,4($s1), mas essas sequências, apesar do uso de $t0, na realidade são completamente independentes – nenhum valor de dados flui entre um par dessas instruções e o par seguinte. É isso que é chamado de antidependência ou dependência de nome, que é uma ordenação forçada puramente pela reutilização de um nome, em vez de uma dependência de dados real. Renomear os registradores durante o processo de desdobramento permite que o compilador mova subsequentemente essas instruções independentes de modo a escalonar melhor o código. O processo de renomeação elimina as dependências de nome, enquanto preserva as verdadeiras dependências. Observe agora que 12 das 14 instruções no loop são executadas como um par. São necessários oito clocks para quatro iterações do loop, ou dois clocks por iteração, o que gera um CPI de 8/14 = 0,57. O desdobramento e o escalonamento do loop com despacho dual nos deram um fator de melhoria de dois, parcialmente pela redução das instruções de controle de loop e parcialmente pela execução do despacho dual. O custo dessa melhoria de desempenho é usar quatro registradores temporários em vez de um, além de um aumento significativo no tamanho do código.
renomeação de registradores O restante dos registradores é usado, pelo compilador ou hardware, para remover antidependências.
antidependência Também chamada dependência de nome. Uma ordenação forçada pela reutilização de um nome, normalmente um registrador, em vez de uma dependência verdadeira que transporta um valor entre duas instruções.
FIGURA 4.71 O código desdobrado e escalonado da Figura 4.70 conforme apareceria no pipeline MIPS com despacho duplo estático. Os slots vazios são nops. Como a primeira instrução no loop decrementa $s1 em 16, os endereços lidos são o valor original de $s1, depois esse endereço menos 4, menos 8 e menos 12.
4.10 Paralelismo e paralelismo avançado em nível de instrução 321
Processadores com despacho múltiplo dinâmico Os processadores de despacho múltiplo dinâmico também são conhecidos como processadores superescalares, ou simplesmente superescalares. Nos processadores superescalares mais simples, as instruções são despachadas em ordem, e o processador decide se zero, uma ou mais instruções podem ser despachadas em determinado ciclo de clock. Obviamente, conseguir um bom desempenho em tal processador ainda exige que o compilador tente escalonar instruções para separar as dependências e, com isso, melhorar a velocidade de despacho de instruções. Mesmo com esse escalonamento de compilador, existe uma diferença importante entre essa arquitetura superescalar simples e um processador VLIW: o código, seja ele escalonado ou não, é garantido pelo hardware que será executado corretamente. Além do mais, o código compilado sempre será executado corretamente, independente da velocidade de despacho ou estrutura do pipeline do processador. Em alguns projetos VLIW, isso não tem acontecido, e a recompilação foi necessária quando da mudança por diferentes modelos de processador; em outros processadores de despacho estático, o código seria executado corretamente em diversas implementações, mas constantemente de uma forma tão pouco eficiente que torna a compilação necessária. Muitas arquiteturas superescalares estendem a estrutura básica das decisões de despacho dinâmico para incluir escalonamento dinâmico em pipeline. O escalonamento dinâmico em pipeline escolhe quais instruções serão executadas em determinado ciclo de clock, enquanto tenta evitar hazards e stalls. Vamos começar com um exemplo simples de impedimento de um hazard de dados. Considere a seguinte sequência de código:
superescalar Uma técnica de pipelining avançada que permite que o processador execute mais de uma instrução por ciclo de clock selecionando-as durante a execução.
escalonamento dinâmico em pipeline Suporte do hardware para modificar a ordem de execução das instruções de modo a evitar stalls.
Embora a instrução sub esteja pronta para executar, ela precisa esperar que lw e addu terminem primeiro, o que poderia exigir muitos ciclos de clock se a memória for lenta. (O Capítulo 5 explica as caches, motivo pelo qual os acessos à memória às vezes são muito lentos.) O escalonamento dinâmico em pipeline permite que tais hazards sejam evitados total ou parcialmente. Escalonamento dinâmico em pipeline
O escalonamento dinâmico em pipeline escolhe quais instruções serão executadas em seguida, possivelmente reordenando-as para evitar stalls. Nesses processadores, o pipeline é dividido em três unidades principais: uma unidade de busca e despacho de instruções, várias unidades funcionais (uma dezena ou mais nos projetos de alto nível em 2008) e uma unidade de commit. A Figura 4.72 mostra o modelo. A primeira unidade busca instruções, decodifica-as e envia cada instrução a uma unidade funcional correspondente para execução. Cada unidade funcional possui buffers, chamados estações de reserva, que mantêm os operandos e a operação. (Na próxima seção, discutiremos uma alternativa às estações de reserva utilizadas por muitos processadores recentes.) Assim que o buffer tiver todos os seus operandos e a unidade funcional estiver pronta para executar, o resultado será calculado. Quando o resultado for completado, ele será enviado a quaisquer estações de reserva esperando por esse resultado em particular, bem como a unidade de commit, que mantém o resultado em um buffer até que seja seguro colocar o resultado no banco de registradores ou, para um store, na memória. O buffer na unidade de commit, normalmente chamado de buffer de reordenação, também é usado para fornecer operandos, mais ou menos da mesma maneira como a lógica de forwarding faz em um pipeline escalonado estaticamente. Quando um resultado é submetido ao banco de registradores, ele pode ser apanhado diretamente de lá, como em um pipeline normal. A combinação de operandos em buffers nas estações de reserva e os resultados no buffer de reordenação oferecem uma forma de renomeação de registradores, assim como
unidade de commit A unidade em um pipeline de execução dinâmica ou fora de ordem que decide quando é seguro liberar o resultado de uma operação aos registradores e memória visíveis ao programador. estação de reserva Um buffer dentro de uma unidade funcional que mantém os operandos e a operação. buffer de reordenação O buffer que mantém resultados em um processador escalonado dinamicamente até que seja seguro armazenar os resultados na memória ou em um registrador.
322
Capítulo 4 O Processador
FIGURA 4.72 As três unidades principais de um pipeline escalonado dinamicamente. A etapa final da atualização do estado também é chamada de reforma ou graduação.
aquela utilizada pelo compilador em nosso exemplo anterior de desdobramento de loop, anteriormente neste capítulo. Para ver como isso funciona conceitualmente, considere as seguintes etapas: 1. Quando uma instrução é despachada, se um de seus operandos estiver no banco de registradores ou no buffer de reordenação, ele será copiado para a estação de reserva imediatamente, onde será colocado em um buffer até que todos os operandos e a unidade de execução estejam disponíveis. Para a instrução despachada, a cópia do registrador operando não é mais necessária, e se houvesse uma escrita nesse registrador, o valor poderia ser reescrito. 2. Se um operando não estiver no banco de registradores ou no buffer de reordenação, ele terá de estar esperando para ser produzido por uma unidade funcional. O nome da unidade funcional que produzirá o resultado é acompanhado. Quando essa unidade por fim produz o resultado, ele é copiado diretamente para a estação de reserva, que estava aguardando, a partir da unidade funcional, sem passar pelos registradores.
execução fora de ordem Uma situação na execução em pipeline quando uma instrução com execução bloqueada não faz com que as instruções seguintes esperem.
commit em ordem Um commit em que os resultados da execução em pipeline sejam escritos no estado visível ao programador na mesma ordem em que as instruções são buscadas.
Essas etapas efetivamente utilizam o buffer de reordenação e as estações de reserva para implementar a renomeação de registradores. Conceitualmente, você pode pensar em um pipeline escalonado de forma dinâmica como uma análise da estrutura de fluxo de dados de um programa. O processador executa as instruções em alguma ordem que preserva a ordem do fluxo de dados do programa. Esse estilo de execução é chamado de execução fora de ordem, pois as instruções podem ser executadas em uma ordem diferente daquela em que foram apanhadas. Para fazer com que os programas se comportem como se estivessem executando em um pipeline simples em ordem, a unidade de busca e decodificação de instruções precisa despachar instruções em ordem, o que permite que as dependências sejam acompanhadas, e a unidade de commit precisa escrever resultados nos registradores e na memória na ordem de execução do programa. Esse modo conservador é chamado de commit em ordem. Logo, se houver uma exceção, o computador poderá apontar para a última instrução executada, e os únicos registradores atualizados serão aqueles escritos pelas instruções antes da instrução que causa a exceção. Apesar de o front end (busca e despacho) e o back end (commit) do pipeline executarem em ordem, as unidades funcionais são livres para iniciar a execução sempre que os dados de que precisam estiverem disponíveis. Hoje, todos os pipelines escalonados dinamicamente utilizam o commit em ordem.
4.10 Paralelismo e paralelismo avançado em nível de instrução 323
Em geral, o escalonamento dinâmico é estendido pela inclusão da especulação baseada em hardware, especialmente para resultados de desvios. Prevendo a direção de um desvio, um processador escalonado dinamicamente pode continuar a buscar e executar instruções ao longo do caminho previsto. Como as instruções possuem um commit em ordem, sabemos se o desvio foi previsto corretamente ou não antes que quaisquer instruções do caminho previsto tenham seus resultados atualizados pelas unidades de commit. Um pipeline especulativo, escalonado dinamicamente, também pode admitir especulação nos endereços de load, permitindo uma reordenação load-store e usando a unidade de commit para evitar a especulação incorreta. Na próxima seção, veremos o uso do escalonamento dinâmico com especulação no projeto do AMD Opteron X4 (Barcelona).
Dado que os compiladores também podem escalonar o código em torno das dependências de dados, você poderia perguntar por que um processador superescalar usaria o escalonamento dinâmico. Existem três motivos principais. Primeiro, nem todos os stalls são previsíveis. Em particular, as falhas de cache (veja Capítulo 5) causam stalls imprevisíveis. O escalonamento dinâmico permite que o processador oculte alguns desses stalls continuando a executar instruções enquanto esperam que o stall termine. Segundo, se o processador especula sobre resultados de desvio usando a previsão de desvio dinâmica, ele não pode saber a ordem exata das instruções durante a compilação, pois isso depende do comportamento previsto e real dos desvios. A incorporação da especulação dinâmica para explorar mais ILP sem incorporar o escalonamento dinâmico restringiria significativamente os benefícios de tal especulação. Terceiro, como a latência do pipeline e a largura do despacho mudam de uma implementação para outra, a melhor maneira de compilar uma sequência de código também muda. Por exemplo, a forma de escalonar uma sequência de instruções dependentes é afetada tanto pela largura quanto pela latência do despacho. A estrutura do pipeline afeta o número de vezes que um loop precisa ser desdobrado para evitar stalls e também o processo de renomeação de registradores feito pelo compilador. O escalonamento dinâmico permite que o hardware oculte a maioria desses detalhes. Assim, os usuários e os distribuidores de software não precisam se preocupar em ter várias versões de um programa para diferentes implementações do mesmo conjunto de instruções. De modo semelhante, o código antigo legado receberá grande parte do benefício de uma nova implementação sem a necessidade de recompilação.
Tanto a técnica de pipelining quanto a execução com despacho múltiplo aumentam a vazão máxima de instruções e a tentativa de explorar o paralelismo em nível de instrução (ILP). No entanto, as dependências de dados e controle nos programas oferecem um limite superior sobre o desempenho sustentado, pois o processador às vezes precisa esperar que uma dependência seja resolvida. As técnicas centradas no software para a exploração do ILP contam com a capacidade do compilador de encontrar e reduzir os efeitos de tais dependências, enquanto as técnicas centradas no hardware contam com extensões para o pipeline e mecanismos de despacho. A especulação, realizada pelo compilador ou pelo hardware, pode aumentar a quantidade de ILP que pode ser explorada, embora se deva ter cuidado, visto que a especulação incorreta provavelmente reduzirá o desempenho.
Entendendo o desempenho dos programas
em
Colocando perspectiva
324
Capítulo 4 O Processador
Interface hardware/ software
Processadores modernos, de alto desempenho, são capazes de despachar várias instruções por clock; infelizmente, é muito difícil sustentar essa taxa de despacho. Por exemplo, apesar da existência de processadores com despacho de quatro a seis instruções por clock, muito poucas aplicações podem sustentar mais do que duas instruções por clock. Existem dois motivos principais para isso. Primeiro, dentro do pipeline, os principais gargalos no desempenho surgem das dependências que não podem ser aliviadas, reduzindo assim o paralelismo entre as instruções e a velocidade de despacho sustentada. Embora pouca coisa possa ser feita sobre as verdadeiras dependências dos dados, normalmente o compilador ou o hardware não sabe exatamente se uma dependência existe ou não e, por isso, precisa considerar de forma conservadora que a dependência existe. Por exemplo, o código que utiliza ponteiros, principalmente os que criam mais aliasing, levará a dependências em potencial mais implícitas. Ao contrário, a maior regularidade dos acessos a um array normalmente permite que um compilador deduza que não existem dependências. De modo semelhante, os desvios que não podem ser previstos com precisão, seja em tempo de execução ou de compilação, limitarão a capacidade de explorar o ILP. Em geral, o ILP adicional está disponível, mas a capacidade de o compilador ou o hardware encontrar ILP que possa estar bastante separado (às vezes pela execução de milhares de instruções) é limitada. Em segundo lugar, as perdas no sistema da memória (o tópico do Capítulo 5) também limitam a capacidade de manter o pipeline cheio. Alguns stalls do sistema de memória podem ser escondidos, mas quantidades limitadas de ILP também limitam a extensão à qual esses stalls podem ser escondidos.
Eficiência de potência e pipelining avançado A desvantagem do aumento da exploração do paralelismo em nível de instrução por meio do despacho múltiplo dinâmico e especulação é a eficiência de potência. Cada inovação foi capaz de transformar mais transistores em desempenho, mas geralmente eles faziam isso de modo muito ineficaz. Agora que atingimos o muro da potência, estamos vendo projetos com múltiplos processadores por chip em que os processadores não são tão profundamente dispostos em pipeline ou tão agressivamente especulativos quanto seus predecessores. A crença é que, embora os processadores mais simples não sejam tão rápidos quanto seus irmãos sofisticados, eles oferecem melhor desempenho por watt, de modo que podem oferecer mais desempenho por chip quando os projetos são restritos mais por potência do que por número de transistores. A Figura 4.73 mostra o número de estágios de pipeline, largura do despacho, nível de especulação, taxa de clock, cores por chip e potência de vários microprocessadores do passado e recentes. Observe a queda nos estágios de pipeline e potência enquanto as empresas passam para projetos multicore. Detalhamento: Uma unidade de commit controla atualizações no banco de registradores e na memória. Alguns processadores escalonados dinamicamente atualizam o banco de registradores imediatamente durante a execução, usando registradores extras para implementar a função de renomeação e preservar a cópia mais antiga de um registrador até que a instrução atualizando o registrador não seja mais especulativa. Outros processadores mantêm o resultado em buffer, normalmente em uma estrutura chamada buffer de reordenação, e a atualização real no banco de registradores ocorre depois, como parte do commit. Stores na memória precisam ser colocados em buffer até o momento do commit, seja em um buffer de store (veja Capítulo 5) ou no buffer de reordenação. A unidade de commit permite que o store escreva na memória a partir do buffer quando o buffer tiver um endereço e dados válidos, e quando o store não for mais dependente de desvios previstos.
4.11 Vida real: o pipeline do AMD Opteron X4 (Barcelona) 325
FIGURA 4.73 Registro dos microprocessadores Intel e Sun em termos de complexidade de pipeline, número de cores e potência. Os estágios de pipeline do Pentium 4 não incluem os estágios de commit. Se os incluíssemos, os pipelines do Pentium 4 seriam ainda mais profundos.
Detalhamento: Os acessos à memória se beneficiam das caches sem bloqueio, que continuam a atender acessos da cache durante uma falta de cache (veja Capítulo 5). Os processadores com execução fora de ordem precisam do projeto de cache para permitir que as instruções sejam executadas durante uma falha.
Indique se as técnicas ou componentes a seguir estão associados principalmente a uma técnica baseada em software ou hardware para a exploração do ILP. Em alguns casos, a resposta pode ser “ambos”. 1. Previsão de desvio 2. Despacho múltiplo 3. VLIW 4. Superescalar 5. Escalonamento dinâmico 6. Execução fora de ordem 7. Especulação 8. Buffer de reordenação 9. Renomeação de registradores
4.11
Vida real: o pipeline do AMD Opteron X4 (Barcelona)
Assim como a maioria dos computadores modernos, os microprocessadores X86 empregam técnicas de pipelining sofisticadas. Esses processadores, porém, ainda encaram o desafio de implementar o complexo conjunto de instruções x86, descrito no Capítulo 2. Tanto AMD quanto Intel buscam as instruções x86 e as traduzem internamente para instruções tipo MIPS, que o AMD chama de operações RISC (Rops) e a Intel chama de micro-operações. As operações RISC são então executadas por um pipeline especulativo sofisticado, escalonado dinamicamente, capaz de sustentar uma taxa de execução de três operações RISC por ciclo de clock no AMD Opteron X4 (Barcelona). Esta seção focaliza o pipeline da operação RISC.
Verifique você mesmo
326
Capítulo 4 O Processador
microarquitetura A organização do processador, incluindo as principais unidades funcionais, sua interconexão e controle.
Quando consideramos o projeto de processadores sofisticados, escalonados dinamicamente, o projeto de unidades funcionais, a cache e o banco de registradores, o despacho de instruções e o controle geral do pipeline se misturam, dificultando a separação entre o caminho de dados e o pipeline. Por causa disso, muitos engenheiros e pesquisadores adotaram o termo microarquitetura para se referirem à arquitetura interna detalhada de um processador. A Figura 4.74 mostra a microarquitetura do X4, focalizando as estruturas para execução das operações RISC. Outra maneira de examinar o X4 é ver os estágios do pipeline pelos quais uma instrução típica passa. A Figura 4.75 mostra a estrutura do pipeline e o número típico de ciclos de clock gastos em cada estágio; naturalmente, o número de ciclos de clock varia devido à natureza do escalonamento dinâmico e também aos requisitos das operações RISC individuais.
registradores da arquitetura Os registradores visíveis do conjunto de instruções de um processador; por exemplo, no MIPS, estes são os 32 registradores inteiros e 16 de ponto flutuante.
Detalhamento: o Opteron X4 usa um esquema para resolver antidependências e especulação incorreta, que utiliza um buffer de reordenação junto com a renomeação de registradores. A renomeação de registradores redefine explicitamente os registradores da arquitetura de um processador (16 no caso da versão de 64 bits da arquitetura x86) para um conjunto maior de registradores físicos (72 no X4). O Opteron X4 utiliza a renomeação de registradores para
FIGURA 4.74 A microarquitetura do AMD Opteron X4. As extensas filas permitem que até 106 operações RISC estejam pendentes, incluindo 24 operações de inteiros, 36 operações de ponto flutuante/SSE e 44 loads e stores. As unidades load e store, na realidade, são separadas em duas partes, com a primeira parte tratando do cálculo do endereço nas unidades da ALU para inteiros e a segunda parte responsável pela referência real à memória. Existe uma extensa rede entre as unidades funcionais para efetuar bypass; como o pipeline é dinâmico, e não estático, o bypass é feito marcando resultados e rastreando os operandos origem, de modo a permitir uma combinação quando um resultado é produzido para uma instrução que está em uma das filas e que precisa do resultado.
4.11 Vida real: o pipeline do AMD Opteron X4 (Barcelona) 327
FIGURA 4.75 O pipeline do Opteron X4 mostrando o fluxo do pipeline para uma instrução típica e o número de ciclos de clock para as principais etapas no pipeline de 12 estágios para operações RISC com inteiros. A fila de execução de ponto flutuante tem 17 estágios de extensão. Também aparecem os principais buffers em que as operações RISC esperam.
remover antidependências. A renomeação de registradores exige que o processador mantenha um mapa entre os registradores da arquitetura e os registradores físicos, indicando qual registrador físico é a cópia mais atualizada de um registrador da arquitetura. Registrando as renomeações que ocorreram, a renomeação de registradores oferece outra técnica para a recuperação no caso de especulação incorreta: basta desfazer os mapeamentos que ocorreram desde a primeira instrução especulada incorretamente. Isso fará com que o estado do processador retorne à última instrução executada corretamente, mantendo o mapeamento correto entre os registradores da arquitetura e os registradores físicos.
As afirmações a seguir são verdadeiras ou falsas? 1. O pipeline de despacho múltiplo do Opteron X4 executa instruções x86 diretamente.
Verifique você mesmo
2. O Opteron X4 utiliza o escalonamento dinâmico, mas não a especulação. 3. A microarquitetura do Opteron X4 possui muito mais registradores do que o x86 requer. 4. O X4 usa menos da metade dos estágios de pipeline do Pentium 4 Prescott anterior (veja Figura 4.73).
O Opteron X4 combina um pipeline de 12 estágios e despacho múltiplo agressivo para conseguir alto desempenho. Mantendo baixas as latências para operações back-to-back, o impacto das dependências de dados é reduzido. Quais são os gargalos de desempenho em potencial mais sérios para os programas executados nesse processador? A lista a seguir inclui alguns problemas de desempenho em potencial, com os três últimos podendo se aplicar de alguma forma a qualquer processador com pipeline de alto desempenho. j
O uso de instruções x86 que não são mapeadas para algumas operações RISC simples.
j
Desvios que são difíceis de se prever, causando stalls e reinícios mal previstos quando a especulação falha.
j
Dependências longas – normalmente causadas por instruções duradouras ou falhas de cache de dados, causando stalls.
j
Atrasos de desempenho que surgem no acesso à memória (veja Capítulo 5), fazendo com que o processador sofra stall.
Entendendo o desempenho dos programas
328
Capítulo 4 O Processador
4.12
T ópico avançado: uma introdução ao projeto digital usando uma linguagem de projeto de hardware para descrever e modelar um pipeline e mais ilustrações de pipelining
O projeto digital moderno é feito por meio de linguagens de descrição de hardware e modernas ferramentas de síntese auxiliadas por computador, que podem criar projetos de hardware detalhados a partir de descrições, usando bibliotecas e síntese lógica. Livros inteiros foram escritos sobre tais linguagens e seu uso no projeto digital. Esta seção, que aparece no site, oferece uma breve introdução e mostra como uma linguagem de projeto de hardware, Verilog neste caso, pode ser usada para descrever o controle do MIPS tanto comportamentalmente quanto em uma forma adequada para a síntese de hardware. Depois, ele oferece uma série de modelos comportamentais em Verilog do pipeline de cinco estágios do MIPS. O modelo inicial ignora hazards, e os acréscimos ao modelo destacam as mudanças para encaminhamento, hazard de dados e hazards de desvio. Depois, oferecemos cerca de doze ilustrações usando a representação de pipeline gráfico com ciclo único para os leitores que quiserem ver mais detalhes sobre como os pipelines funcionam para algumas sequências de instruções MIPS.
4.13 Falácias e armadilhas Falácia: pipelining é fácil. Nossos livros comprovam a sutileza da execução correta de um pipeline. Nosso livro avançado tinha um bug no pipeline em sua primeira edição, apesar de ter sido revisado por mais de 100 pessoas e testado nas salas de aula de 18 universidades. O bug só foi descoberto quando alguém tentou montar um computador com aquele livro. O fato de que o Verilog para descrever um pipeline como esse do Opteron X4 terá milhares de linhas é uma indicação da complexidade. Esteja atento! Falácia: as ideias de pipelining podem ser implementadas independentes da tecnologia. Quando o número de transistores no chip e a velocidade dos transistores tornaram um pipeline de cinco estágios a melhor solução, então o delayed branch (veja o primeiro “Detalhamento” da Seção “Previsão dinâmica de desvios”) foi uma solução simples para controlar os hazards. Com pipelines maiores, a execução superescalar e a previsão dinâmica de desvios, agora isso é redundante. No início da década de 1990, o escalonamento dinâmico em pipeline exigia muitos recursos e não era necessário para o alto desempenho, mas, à medida que a quantidade de transistores continuava a dobrar, a lógica se tornava muito mais rápida do que a memória, então as múltiplas unidades funcionais e os pipelines dinâmicos fizeram mais sentido. Hoje, a preocupação com a potência está levando a projetos menos agressivos. Armadilha: a falha em considerar o projeto do conjunto de instruções pode afetar o pipeline de forma adversa. Muitas das dificuldades em pipelining surgem por causa das complicações do conjunto de instruções. Aqui estão alguns exemplos: j
Tamanhos de instrução e tempos de execução muito variáveis podem causar desequilíbrio entre estágios do pipeline e complicar bastante a detecção de hazards em um projeto com pipeline, no nível do conjunto de instruções. Esse problema foi contornado, inicialmente no DEC VAX 8500, no final da década de 1980, usando
4.14 Comentários finais 329
o esquema de micropipeline que o Opteron X4 emprega hoje. Naturalmente, o overhead da tradução e a manutenção da correspondência entre as micro-operações e as instruções permanecem. j
Modos de endereçamento sofisticados podem levar a diferentes tipos de problemas. Os modos de endereçamento que atualizam registradores complicam a detecção de hazards. Outros modos de endereçamento que exigem múltiplos acessos à memória complicam bastante o controle do pipeline e tornam difícil manter o pipeline fluindo tranquilamente.
Talvez o melhor exemplo seja o DEC Alpha e o DEC NVAX. Em uma tecnologia comparável, o conjunto de instruções mais recente do Alpha permitiu uma implementação cujo desempenho tem mais do que o dobro da velocidade do NVAX. Em outro exemplo, Bhandarkar e Clark [1991] compararam o MIPS M/2000 e o DEC VAX 8700 contando os ciclos de clock dos benchmarks SPEC; eles concluíram que, embora o MIPS M/2000 execute mais instruções, o VAX na média executa 2,7 vezes mais ciclos de clock, de modo que o MIPS é mais rápido.
4.14 Comentários finais
Noventa por cento da sabedoria consiste em ser sensato no tempo. Provérbio americano
Como vimos neste capítulo, tanto o caminho de dados quanto o controle para um processador podem ser projetados começando com a arquitetura do conjunto de instruções e um conhecimento das características básicas da tecnologia. Na Seção 4.3, vimos como o caminho de dados para um processador MIPS poderia ser construído com base na arquitetura e na decisão de criar uma implementação de ciclo único. Naturalmente, a tecnologia básica também afeta muitas decisões de projeto, ditando quais componentes podem ser usados no caminho de dados, e também se uma implementação de ciclo único sequer faz sentido. A técnica de pipelining melhora a vazão, mas não o tempo de execução inerente (ou latência de instrução) das instruções; para algumas instruções, a latência é semelhante, em duração, à técnica de ciclo único. O despacho de instrução múltiplo acrescenta um hardware adicional ao caminho de dados para permitir que várias instruções sejam iniciadas a cada ciclo de clock, mas com um aumento na latência efetiva. O pipelining foi apresentado como reduzindo o tempo de ciclo de clock do caminho de dados de ciclo único simples. O despacho múltiplo de instruções, em comparação, focaliza claramente a redução dos ciclos de clock por instrução (CPI). A técnica de pipelining e o despacho múltiplo tentam explorar o paralelismo em nível de instrução. A presença de dependências de dados e o controle, que podem se tornar hazards, são as principais limitações para a exploração do paralelismo. Escalonamento e especulação, ambos no hardware e no software, são as principais técnicas utilizadas para reduzir o impacto das dependências sobre o desempenho. A passagem para pipelines maiores, despacho de instruções múltiplas e escalonamento dinâmico em meados da década de 1990 ajudou a sustentar os 60% de aumento anual de desempenho dos processadores que começou no início da década de 1980. Como dissemos no Capítulo 1, esses microprocessadores preservaram o modelo de programação sequencial, mas por fim se chocaram com o muro da potência. Assim, a indústria foi forçada a testar multiprocessadores, que exploram o paralelismo em níveis menos minuciosos (o assunto do Capítulo 7). Essa tendência também fez com que os projetistas reavaliassem as implicações de desempenho de potência de algumas invenções desde meados da década de 1990, resultando em uma simplificação dos pipelines em versões mais recentes das microarquiteturas. Para sustentar os avanços no desempenho de processamento por meio de processadores paralelos, a lei de Amdahl sugere que outra parte do sistema se torne o gargalo. Esse gargalo é o assunto do próximo capítulo: o sistema de memória.
latência de instrução O tempo de execução inerente para uma instrução.
330
Capítulo 4 O Processador
4.15
Perspectiva histórica e leitura adicional
Esta seção, que aparece no site, discute a história dos primeiros processadores em pipeline, os superescalares mais antigos e o desenvolvimento de técnicas para execuções fora de ordem e especulativas, além de desenvolvimentos importantes na tecnologia de compiladores que acompanha tudo isso.
4.16 Exercícios1 Exercício 4.1 Diferentes instruções utilizam diferentes blocos de hardware na implementação básica de ciclo único. Os três problemas seguintes neste exercício referem-se à seguinte instrução: Instrução
Interpretação
a.
AND Rd,Rs,Rt
Reg[Rd]=Reg[Rs] AND Reg[Rt]
b.
SW Rt,Offs(Rs)
Mem[Reg[Rs]+Offs]=Reg[Rt]
4.1.1 [5] <4.1> Quais são os valores dos sinais de controle gerados pelo controle na Figura 4.2 para essa instrução? 4.1.2 [5] <4.1> Quais recursos (blocos) realizam uma função útil para essa instrução? 4.1.3 [10] <4.1> Quais recursos (blocos) produzem saídas, mas suas saídas não são usadas para essa instrução? Quais recursos não produzem saídas para ela? Diferentes unidades de execução e blocos de lógica digital possuem diferentes latências (tempo necessário para realizar seu trabalho). Na Figura 4.2 existem sete tipos de blocos principais. As latências dos blocos, juntamente com o caminho crítico (latência mais longa) para uma instrução determinam a latência mínima dessa instrução. Para os três problemas restantes neste exercício, considere as seguintes latências de recurso: I-Mem
Add
Mux
ALU
Regs
D-Mem
a.
200ps
70ps
20ps
90ps
90ps
250ps
Controle 40ps
b.
750ps
200ps
50ps
250ps
300ps
500ps
300ps
4.1.4 [5] <4.1> Qual é o caminho crítico para uma instrução AND do MIPS? 4.1.5 [5] <4.1> Qual é o caminho crítico para uma instrução LD (load) do MIPS? 4.1.6 [10] <4.1> Qual é o caminho crítico para uma instrução BEQ do MIPS?
Exercício 4.2 A implementação básica de ciclo único do MIPS na Figura 4.2 só pode implementar algumas instruções. Novas instruções podem ser acrescentadas a uma ISA existente, mas a decisão de fazer isso ou não depende, entre outras coisas, do custo e da complexidade que tal acréscimo introduz no caminho de dados e controle do processador. Os três primeiros problemas neste exercício referem-se a esta nova instrução: 1
Contribuição de Milos Prvulovic, da Georgia Tech
4.16 Exercícios 331
Instrução
Interpretação
a.
SEQ Rd,Rs,Rt
Reg[Rd]=Valor booliano (0 or 1) de (Reg[Rs]==Reg[Rs])
b.
LWI Rt,Rd(Rs)
Reg[Rt]=Mem[Reg[Rd]+Reg[Rs]]
4.2.1 [10] <4.1> Que blocos existentes (se houver) podem ser usados para essa instrução? 4.2.2 [10] <4.1> De que novos blocos funcionais (se houver) precisamos para essa instrução? 4.2.3 [10] <4.1> De que novos sinais da unidade de controle (se houver) precisamos para dar suporte a essa instrução? Quando os projetistas de processador consideram uma melhoria possível no caminho de dados do processador, a decisão normalmente depende da escolha de custo/desempenho. Nos três problemas a seguir, considere que estamos começando com um caminho de dados da Figura 4.2, em que os blocos I-Mem, Add, Mux, ALU, Regs, D-Mem e Controle têm latências de 400ps, 100ps, 30ps, 120ps, 200ps, 350ps e 100ps, respectivamente, e custos de 1000, 30, 10, 100, 200, 2000 e 500, respectivamente. Os três problemas restantes neste exercício referem-se à seguinte melhoria do processador: Melhoria
Latência
Custo
Benefício
a.
Add o multiplicador ao ALU
+300ps para ALU
+600 para ALU
Permite a adição da instrução MUL e a execução de 5% menos instruções (o MUL não é mais emulado).
b.
Controles mais simples
+100ps para Controle
–400 para Controle
O controle se torna mais devagar, mas com uma lógica mais barata.
4.2.4 [10] <4.1> Qual é o tempo de ciclo de clock com e sem essa melhoria? 4.2.5 [10] <4.1> Qual é o ganho de velocidade obtido acrescentando essa melhoria? 4.2.6 [10] <4.1> Compare a razão custo/desempenho com e sem essa melhoria.
Exercício 4.3 Os problemas neste exercício referem-se ao seguinte bloco lógico: Bloco lógico a.
Pequeno Multiplexador (Mux) com quatro entradas de dados 8-bit
b.
Pequeno ALU de 8-bit que pode realizar AND, OR ou NOT
4.3.1 [5] <4.1, 4.2> Esse bloco contém apenas lógica, apenas flip-flops ou ambos? 4.3.2 [20] <4.1, 4.2> Mostre como esse bloco pode ser implementado. Use apenas AND, OR, NOT e elementos D. 4.3.3 [10] <4.1, 4.2> Repita o Exercício 4.3.2, mas todas as portas AND e OR que você usa precisam ser portas de duas entradas. O custo e a latência da lógica digital dependem dos tipos de elementos lógicos básicos (portas) que estão disponíveis e das propriedades dessas portas. Os três problemas restantes neste exercício referem-se a estas portas, latência e custos:
332
Capítulo 4 O Processador
AND ou OR de 2 entradas
NOT Latência
Custo
Latência
Custo
a.
10ps
2
12ps
b.
20ps
2
40ps
Cada entrada addl para AND/OR
Elemento D
Latência
Custo
Latência
Custo
4
+2ps
+1
30ps
10
3
+30ps
+1
80ps
9
4.3.4 [5] <4.1, 4.2> Qual é a latência da sua implementação do Exercício 4.3.2? 4.3.5 [5] <4.1, 4.2> Qual é o custo da sua implementação do Exercício 4.3.2? 4.3.6 [20] <4.1, 4.2> Mude o seu projeto para minimizar a latência, depois para minimizar o custo. Compare o custo e a latência desses dois projetos otimizados.
Exercício 4.4 Ao implementar uma expressão lógica na lógica digital, deve-se utilizar as portas lógicas disponíveis para implementar um operador para o qual uma porta não está disponível. Os problemas neste exercício referem-se às seguintes expressões lógicas: Sinal de controle 1
Sinal de controle 2
a.
(((A E B) XOR C) OU (A XOR C)) OU (A XOR B)
(A XOR B) OR (A XOR C)
b.
(((A OU B) E C) OU ((A OU C) OU (A OU B))
(A E C) OU (B E C)
4.4.1 [5] <4.2> Implemente a lógica para o sinal de controle 1. Seu circuito deverá implementar diretamente a expressão dada (não reorganize a expressão para “otimizá-la”), usando portas NOT e portas AND, OR e XOR de duas entradas. 4.4.2 [10] Supondo que todas as portas possuem latências iguais, qual é o tamanho (em portas) do caminho crítico no seu circuito do Exercício 4.4.1? 4.4.3 [10] <4.2> Quando múltiplas expressões lógicas são implementadas, é possível reduzir o custo de implementação usando os mesmos sinais em mais de uma expressão. Repita o Exercício 4.4.1, mas implemente o Sinal de controle 1 e o Sinal de controle 2 e tente “compartilhar” os circuitos entre as expressões sempre que for possível. Para os três problemas restantes neste exercício, consideramos que os seguintes elementos básicos da lógica digital estão presentes, e que sua latência e custo são os seguintes: AND de 2 entradas
OR de duas entradas
Latência
NOT Custo
Latência
Custo
Latência
Custo
Latência
Elemento D Custo
a.
10ps
2
12ps
4
20ps
5
30ps
10
b.
20ps
2
40ps
3
50ps
3
50ps
8
4.4.4 [10] <4.2> Qual é o tamanho do caminho crítico no seu circuito de 4.4.3? 4.4.5 [10] <4.2> Qual é o custo do seu circuito do Exercício 4.4.3? 4.4.6 [10] <4.2> Que fração do custo foi salva no seu circuito do Exercício 4.4.3 implementando esses dois sinais de controle juntos, ao invés de separadamente?
4.16 Exercícios 333
Exercício 4.5 Este exercício tem por finalidade ajudá-lo a familiarizar-se com o projeto e a operação dos circuitos lógicos sequenciais. Os problemas neste exercício referem-se a esta operação da ALU: Operação da ALU a.
Add (X + Y)
b.
Subtrair um (X–1) em complementos de dois
4.5.1 [20] <4.2> Crie um circuito com entradas de dados de 1 bit e saída de dados de 1 bit que realize essa operação em série, começando com o bit menos significativo. Em uma implementação serial, o circuito está processando operandos de entrada bit a bit, gerando os bits de saída um a um. Por exemplo, um circuito AND serial é simplesmente uma porta AND; no ciclo N, lhe damos o bit N de cada operando e obtemos o bit N do resultado. Além das entradas de dados, o circuito tem uma entrada Clk (clock) e uma entrada “Start” que é definida como 1 somente no primeiro ciclo da operação. No seu projeto, você pode usar elementos D e portas NOT, AND, OR e XOR. 4.5.2 [20] <4.2> Repita o Exercício 4.5.1, mas agora projete um circuito que realiza essa operação 2 bits de cada vez. No restante deste exercício, consideramos que os seguintes elementos básicos da lógica digital estão disponíveis, e que sua latência e custo são os seguintes: NOT
AND
OR
XOR
Elemento D
Latência
Custo
Latência
Custo
Latência
Custo
Latência
Custo
Latência
Custo
a.
10ps
2
12ps
4
12ps
4
14ps
6
30ps
10
b.
50ps
1
100ps
2
90ps
2
120ps
3
160ps
2
O tempo dado para um elemento D é seu tempo de preparação. A entrada de dados de um flip-flop precisa ter o valor correto do tempo de preparação antes da borda do clock (final do ciclo de clock) que armazena esse valor no flip-flop. 4.5.3 [10] <4.2> Qual é o tempo de ciclo para o circuito que você criou no Exercício 4.5.1? Quanto tempo é necessário para realizar a operação de 32 bits? 4.5.4 [10] <4.2> Qual é o tempo de ciclo para o circuito que você criou no Exercício 4.5.2? Qual é ganho de velocidade obtido usando esse circuito em vez daquele do Exercício 4.5.1 para uma operação de 32 bits? 4.5.5 [10] <4.2> Calcule o custo para o circuito que você criou no Exercício 4.5.1, e depois para o circuito que você criou no Exercício 4.5.2. 4.5.6 [5] <4.2> Compare as razões custo/benefício para os dois circuitos que você criou nos Exercícios 4.5.1 e 4.5.2. Para este problema, o desempenho de um circuito é o inverso do tempo necessário para realizar uma operação de 32 bits.
Exercício 4.6 Os problemas neste exercício consideram que os blocos lógicos necessários para implementar o caminho de dados do processador têm as seguintes latências: I-Mem
Add
Mux
ALU
Regs
D-Mem
Extensão de sinal
Shift-esq-2
a.
200ps
70ps
20ps
90ps
90ps
250ps
15ps
10ps
b.
750ps
200ps
50ps
250ps
300ps
500ps
100ps
0ps
334
Capítulo 4 O Processador
4.6.1 [10] <4.3> Se a única coisa que precisássemos fazer em um processador fosse buscar instruções consecutivas (Figura 4.6), qual seria o tempo do ciclo? 4.6.2 [10] <4.3> Considere um caminho de dados semelhante ao da Figura 4.11, mas para um processador que só tem um tipo de instrução: desvio incondicional relativo ao PC. Qual seria o tempo de ciclo para esse caminho de dados? 4.6.3 [10] <4.3> Repita o Exercício 4.6.2, mas desta vez precisamos dar suporte apenas a desvios condicionais relativos ao PC. Os três problemas restantes neste exercício referem-se ao seguinte bloco lógico (recurso) no caminho de dados: Recurso a.
Shift-esq-2
b.
Registradores
4.6.4 [10] <4.3> Que tipos de instruções exigem esse recurso? 4.6.5 [20] <4.3> Para que tipos de instruções (se houver) esse recurso está no caminho crítico? 4.6.6 [10] <4.3> Supondo que só temos suporte para instruções BEQ e ADD, discuta como as mudanças na latência indicada desse recurso afetam o tempo de ciclo do processador. Suponha que as latências de outros recursos não mudem.
Exercício 4.7 Neste exercício, examinamos como as latências dos componentes individuais do caminho de dados afetam o tempo do ciclo de clock do caminho de dados inteiro, e como esses componentes são utilizados pelas instruções. Para os problemas neste exercício, considere as seguintes latências para blocos lógicos no caminho de dados: I-Mem
Add
Mux
ALU
Regs
D-Mem
Extensão de sinal
Shift-esq-2
a.
200ps
70ps
20ps
90ps
90ps
250ps
15ps
10ps
b.
750ps
200ps
50ps
250ps
300ps
500ps
100ps
0ps
4.7.1 [10] <4.3> Qual é o tempo do ciclo de clock se o único tipo das instruções que precisamos dar suporte forem instruções da ALU (ADD, AND, etc.)? 4.7.2 [10] <4.3> Qual é o tempo do ciclo de clock se só tivermos de dar suporte a instruções LW? 4.7.3 [20] <4.3> Qual é o tempo do ciclo de clock se tivermos de dar suporte a instruções
ADD, BEQ, LW, e SW?
Para os problemas restantes neste exercício, considere que não existem stalls de pipeline e que o desmembramento das instruções executadas seja o seguinte: ADD
ADDI
NOT
BEQ
LW
SW
a.
20%
20%
0%
25%
25%
10%
b.
30%
10%
0%
10%
30%
20%
4.7.4 [10] <4.3> Em que fração de todos os ciclos a memória de dados é utilizada?
4.16 Exercícios 335
4.7.5 [10] <4.3> Em que fração de todos os ciclos a entrada do circuito por extensão de sinal é necessária? O que esse circuito está fazendo nos ciclos em que sua entrada não é necessária? 4.7.6 [10] <4.3> Se pudermos melhorar a latência de um dos componentes indicados do caminho de dados em 10%, que componente seria? Qual é o ganho de velocidade obtido por essa melhoria?
Exercício 4.8 Quando os chips de silício são fabricados, os defeitos nos materiais (por exemplo, o silício) e os erros de manufatura podem resultar em circuitos defeituosos. Um defeito muito comum é quando um fio afeta o sinal em outro. Isso é chamado de falha cross-talk. Uma classe especial de falhas cross-talk é quando um sinal está conectado a um fio que tem um valor lógico constante (por exemplo, um fio da fonte de alimentação). Nesse caso, temos uma falha stuck-at-0 ou stuck-at-1, e o sinal afetado sempre tem um valor lógico 0 ou 1, respectivamente. Os problemas a seguir referem-se ao seguinte sinal da Figura 4.24: Sinal a.
Registradores, entrada do Registrador Escrita, bit 0
b.
Add unidade ao canto superior direito, resultado do ALU, bit 0
4.8.1 [10] <4.3, 4.4> Vamos supor que o teste do processador seja feito preenchendo o PC, registradores e memórias de dados e instruções com alguns valores (você pode escolher quais valores), permitindo que uma única instrução seja executada e depois lendo o PC, memórias e registradores. Esses valores são então examinados para determinar se uma falha em particular está presente. Você conseguiria criar um teste (valores para PC, memórias e registradores) que determinaria se existe uma falha stuck-at-0 nesse sinal? 4.8.2 [10] <4.3, 4.4> Repita o Exercício 4.8.1 para uma falha stuck-at-1. Você conseguiria usar um único teste para stuck-at-0 e stuck-at-1? Caso afirmativo, explique como; se não, explique por que não. 4.8.3 [60] <4.3, 4.4> Se soubermos que o processador tem uma falha stuck-at-1 nesse sinal, o processador ainda é utilizável? Para isso, temos de poder converter qualquer programa que execute em um processador MIPS normal em um programa que funcione nesse processador. Você pode considerar que existe memória de instrução e memória de dados livre suficiente para tornar o programa maior e armazenar dados adicionais. Dica: o processador é utilizável se cada instrução “rompida” por essa falha puder ser substituída por uma sequência de instruções “funcionais” que conseguem o mesmo efeito. Os problemas a seguir referem-se à seguinte falha: Falha a.
Stuck-at-1
b.
Torna-se 0 se o sinal do controle RegDs é 0, caso contrário, nenhuma falha
4.8.4 [10] <4.3, 4.4> Repita o Exercício 4.8.1, mas agora o teste é se o sinal de controle “MemRead” tem essa falha. 4.8.5 [10] <4.3, 4.4> Repita o Exercício 4.8.1, mas agora o teste é se o sinal de controle “Jump” tem essa falha.
336
Capítulo 4 O Processador
4.8.6 [40] <4.3, 4.4> Usando um único teste descrito no Exercício 4.8.1, podemos testar falhas em diversos sinais diferentes, mas normalmente não em todos eles. Descreva uma série de testes para procurar essa falha em todas as saídas Mux (cada bit de saída de cada um dos cinco Muxes). Tente fazer isso com o mínimo possível de testes de única instrução.
Exercício 4.9 Neste exercício, examinamos a operação do caminho de dados de ciclo único para determinada instrução. Os problemas neste exercício referem-se à seguinte instrução MIPS: Instrução a.
SW R4,-100(R16)
b.
SLT R1,R2,R3
4.9.1 [10] <4.4> Qual é o valor da word de instrução? 4.9.2 [10] <4.4> Qual é o número de registrador fornecido à entrada “Ler registrador 1” do arquivo de registradores? Esse registro é realmente lido? E “Ler registrador 2”? 4.9.3 [10] <4.4> Qual é o número de registrador fornecido à entrada “Escrever registrador” do arquivo de registradores? Esse registro é realmente escrito? Diferentes instruções exigem que diferentes sinais de controle sejam ativados no caminho de dados. Os problemas restantes neste exercício referem-se aos dois sinais de controle a seguir, da Figura 4.24: Sinal de controle 1
Sinal de controle 2
a.
ALUSrc
Branch
b.
Jump
RegDst
4.9.4 [20] <4.4> Qual é o valor desses dois sinais para esta instrução? 4.9.5 [20] <4.4> Para o caminho de dados da Figura 4.24, desenhe o diagrama lógico para a parte da unidade de controle que implementa apenas o primeiro sinal. Considere que só precisamos dar suporte às instruções LW, SW, BEQ, ADD e J (jump). 4.9.6 [20] <4.4> Repita o Exercício 4.9.5, mas agora implemente esses dois sinais.
Exercício 4.10 Neste exercício, examinamos como o tempo do ciclo de clock do processador afeta o projeto da unidade de controle, e vice-versa. Os problemas neste exercício consideram que os blocos lógicos usados para implementar o caminho de dados têm as seguintes latências: I-Mem
Add
Mux
ALU
Regs
D-Mem
Extensão de sinal
Shift-esq-2
ALU Ctrl
a.
200ps
70ps
20ps
90ps
90ps
250ps
15ps
10ps
30ps
b.
750ps
200ps
50ps
250ps
300ps
500ps
100ps
5ps
70ps
4.10.1 [10] <4.2, 4.4> Para evitar estender o caminho crítico do caminho de dados mostrado na Figura 4.24, quanto tempo a unidade de controle pode levar para gerar o sinal MemWrite? 4.10.2 [20] <4.2, 4.4> Que sinal de controle na Figura 4.24 tem mais slack e quanto tempo a unidade de controle tem para gerá-lo se quiser evitar estar no caminho crítico?
4.16 Exercícios 337
4.10.3 [20] <4.2, 4.4> Qual sinal de controle na Figura 4.24 é o mais crítico para ser gerado rapidamente e quanto tempo a unidade de controle tem para gerá-lo se quiser evitar estar no caminho crítico? Os problemas restantes neste exercício consideram que o tempo necessário pela unidade de controle para gerar sinais de controle individuais é o seguinte: RegDst
Jump
Branch
MemRead
MemtoReg
OpALU
MemWrite
ALUSrc
RegWrite
a.
500ps
500ps
450ps
200ps
450ps
200ps
500ps
100ps
500ps
b.
1100ps
1000ps
1100ps
800ps
1200ps
300ps
1300ps
400ps
1200ps
4.10.4 [20] <4.4> Qual é o tempo do ciclo de clock do processador? 4.10.5 [20] <4.4> Se você puder agilizar a geração dos sinais de controle, mas o custo do processador inteiro aumentar em $1 para cada 5ps de melhoria de um único sinal de controle, que sinais de controle você agilizaria e por quanto maximizaria o desempenho? Qual é o custo (por processador) dessa melhoria de desempenho? 4.10.6 [30] <4.4> Se o processador já for muito caro, em vez de pagar para agilizá-lo, como fizemos no Exercício 4.10.5, queremos minimizar seu custo sem torná-lo mais lento. Se você puder usar uma lógica mais lenta para implementar sinais de controle, economizando $1 do custo do processador para cada 5ps que acrescenta à latência de um único sinal de controle, quais sinais de controle você tornaria mais lentos e por quanto reduziria o custo do processador sem torná-lo mais lento?
Exercício 4.11 Neste exercício, examinamos detalhadamente como uma instrução é executada em um caminho de dados de ciclo único. Os problemas neste exercício referem-se a um ciclo de clock em que o processador busca a seguinte word de instrução: Word de instrução a.
10101100011000100000000000010100
b.
00000000100000100000100000101010
4.11.1 [5] <4.4> Quais são as saídas da unidade de extensão de sinal e salto “Shift left 2” (topo da Figura 4.24) para essa palavra de instrução? 4.11.2 [10] <4.4> Quais são os valores das entradas da unidade de controle da ALU para essa instrução? 4.11.3 [10] <4.4> Qual é o novo endereço do PC após a execução dessa instrução? Destaque o caminho através do qual esse valor é determinado. Os problemas restantes neste exercício consideram que a memória de dados contém apenas zeros e que os registradores do processador possuem os seguintes valores no início do ciclo em que a word de instrução anterior é apanhada: R0
R1
R2
R3
R4
R5
R6
R8
R12
R31
a.
0
1
2
–3
–4
10
6
8
2
–16
b.
0
256
–122
19
–32
13
–6
–1
16
–2
4.11.4 [10] <4.4> Para cada Mux, mostre os valores de sua saída de dados durante a execução dessa instrução e esses valores de registrador.
338
Capítulo 4 O Processador
4.11.5 [10] <4.4> Para a ALU e as duas unidades de soma, quais são seus valores de entrada de dados? 4.11.6 [10] <4.4> Quais são os valores de todas as entradas para a unidade de “Registradores”?
Exercício 4.12 Neste exercício, examinamos como o pipelining afeta o tempo do ciclo de clock do processador. Os problemas neste exercício consideram que os estágios individuais do caminho de dados têm as seguintes latências: IF
ID
EX
MEM
WB
a.
250ps
350ps
150ps
300ps
200ps
b.
200ps
170ps
220ps
210ps
150ps
4.12.1 [5] <4.5> Qual é o tempo do ciclo de clock em um processador com e sem pipeline? 4.12.2 [10] <4.5> Qual é a latência total de uma instrução LW em um processador com e sem pipeline? 4.12.3 [10] <4.5> Se você pudesse dividir um estágio do caminho de dados com pipeline em dois novos estágios, cada um com metade da latência do estágio original, que estágio você dividiria e qual é o novo tempo do ciclo de clock do processador? Os problemas restantes neste exercício consideram que as instruções executadas pelo processador são desmembradas da seguinte forma: ALU
BEQ
LW
SW
a.
45%
20%
20%
15%
b.
55%
15%
15%
15%
4.12.4 [10] <4.5> Supondo que não haja stalls ou hazards, qual é a utilização da memória de dados? 4.12.5 [10] <4.5> Supondo que não haja stalls ou hazards, qual é a utilização da porta de escrita de registrador da unidade “Registradores”? 4.12.6 [30] <4.5> Em vez de uma organização de ciclo único, podemos usar uma organização multiciclos, em que cada instrução ocupa múltiplos ciclos, mas uma instrução termina antes que outra seja apanhada. Nessa organização, uma instrução só percorre os estágios que ela realmente precisa (por exemplo, ST só ocupa quatro ciclos, pois não precisa do estágio WB). Compare os tempos do ciclo de clock e os tempos de execução com a organização em ciclo único, multiciclos e em pipeline.
Exercício 4.13 Neste exercício, examinamos como as dependências de dados afetam a execução no pipeline básico de cinco estágios descrito na Seção 4.5. Os problemas neste exercício referem-se a esta sequência de instruções:
4.16 Exercícios 339
Sequência de instruções a.
SW R16,-100(R6) LW R4,8(R16) ADD R5,R4,R4
b.
OR R1,R2,R3 OR R2,R1,R4 OR R1,R1,R2
4.13.1 [10] <4.5> Indique as dependências e seu tipo. 4.13.2 [10] <4.5> Suponha que não haja forwarding nesse processador em pipeline. Indique hazards e acrescente instruções NOP para eliminá-los. 4.13.3 [10] <4.5> Suponha que haja forwarding completo. Indique os hazards e acrescente instruções NOP para eliminá-los. Os problemas restantes neste exercício consideram os seguintes tempos do ciclo de clock: Sem forwarding
Com forwarding completo
Apenas com forwarding ALU-ALU
a.
250ps
300ps
290ps
b.
180ps
240ps
210ps
4.13.4 [10] <4.5> Qual é o tempo de execução total dessa sequência de instruções sem forwarding e com forwarding completo? Qual é o ganho de velocidade obtido acrescentando-se forwarding completo a um pipeline que não tinha forwarding? 4.13.5 [10] <4.5> Acrescente instruções NOP a esse código para eliminar hazards se houver apenas forwarding ALU-ALU (nenhum forwarding do estágio MEM para EX). 4.13.6 [10] <4.5> Qual é o tempo de execução total dessa sequência de instruções apenas com forwarding ALU-ALU? Qual é o ganho de velocidade em relação a um pipeline sem forwarding?
Exercício 4.14 Neste exercício, examinamos como os hazards de recursos, os hazards de controle e o projeto da ISA podem afetar a execução em pipeline. Os problemas neste exercício referem-se ao seguinte fragmento de código MIPS: Sequência de instruções a.
b.
4.14.1 [10] <4.5> Para este problema, suponha que todos os desvios sejam perfeitamente previstos (isso elimina todos os hazards de controle) e que nenhum slot de delay seja utilizado. Se tivermos apenas uma memória (para instruções e dados), haverá um hazard estrutural toda vez que precisarmos apanhar uma instrução no mesmo ciclo em que outra
340
Capítulo 4 O Processador
instrução acessa dados. Para garantir o processo do forwarding, esse hazard sempre precisa ser resolvido em favor da instrução que acessa dados. Qual é o tempo de execução total dessa sequência de instruções no pipeline de cinco estágios que tem apenas uma memória? Vimos que os hazards de dados podem ser eliminados acrescentando NOPS ao código. Você conseguiria fazer o mesmo com esse hazard estrutural? Por quê? 4.14.2 [20] <4.5> Para este problema, suponha que todos os desvios sejam perfeitamente previstos (isso elimina todos os hazards de controle) e que nenhum slot de delay seja utilizado. Se mudarmos as instruções load/store para usar um registrador (sem um offset) como endereço, essas instruções não precisam mais usar a ALU. Como resultado, os estágios MEM e EX podem ser sobrepostos e o pipeline tem apenas quatro estágios. Mude esse código para acomodar essa ISA alterada. Supondo que essa mudança não afete o tempo do ciclo de clock, que ganho de velocidade é obtido nessa sequência de instruções? 4.14.3 [10] <4.5> Considerando stall-on-branch e nenhum slot de delay, que ganho de velocidade é obtido nesse código se os resultados do desvio forem determinados no estágio ID, em relação à execução em que os resultados do desvio são determinados no estágio EX? Os problemas restantes neste exercício consideram que os estágios de pipeline individuais possuem as seguintes latências: IF
ID
EX
MEM
WB
a.
200ps
120ps
150ps
190ps
100ps
b.
150ps
200ps
200ps
20ps
100ps
4.14.4 [10] <4.5> Dadas essas latências de estágio de pipeline, repita o cálculo de ganho de velocidade de 4.14.2, mas leve em conta a (possível) mudança no tempo do ciclo de clock. Quando EX e MEM são feitos em um único estágio, a maior parte do trabalho pode ser feita em paralelo. Como resultado, o estágio EX/MEM resultante tem uma latência que é a maior das duas originais, mais 20ps necessários para o trabalho que poderia ser feito em paralelo. 4.14.5 [10] <4.5> Dadas essas latências de estágio em pipeline, repita o cálculo de ganho de velocidade de 4.14.3, mas leve em conta a (possível) mudança no tempo do ciclo de clock. Suponha que a latência do estágio ID aumente em 50% e a latência do estágio EX diminua em 10ps quando a resolução do resultado do desvio é passada de EX para ID. 4.14.6 [10] <4.5> Considerando stall-on-branch e nenhum slot de delay, qual é o novo tempo do ciclo de clock e tempo de execução dessa sequência de instruções se o cálculo de endereço de beq for passado para o estágio MEM? Qual é o ganho de velocidade decorrente dessa mudança? Suponha que a latência do estágio EX seja reduzida em 20ps e a latência do estágio MEM fique inalterada quando a resolução do resultado do desvio for passada de EX para MEM.
Exercício 4.15 Neste exercício, examinamos como a ISA afeta o projeto do pipeline. Os problemas neste exercício referem-se à seguinte instrução nova: a.
ADDM Rd,Rt+Offs(Rs)
Rd=Rt+Mem[Offs+Rs]
b.
BEQM Rd,Rt,Offs(Rs)
if Rt=Mem[Offs+Rs] then PC=Rd
4.15.1 [20] <4.5> O que deverá ser mudado no caminho de dados em pipeline para acrescentar essa instrução à ISA do MIPS? 4.15.2 [10] <4.5> Que novos sinais de controle precisam ser acrescidos ao seu pipeline do Exercício 4.15.1?
4.16 Exercícios 341
4.15.3 [20] <4.5, 4.13> O suporte para essa instrução introduz novos hazards? Os stalls devidos aos hazards existentes se tornam piores? 4.15.4 [10] <4.5, 4.13> Dê um exemplo de onde essa instrução poderia ser útil e uma sequência de instruções MIPS existentes que são substituídas por essa instrução. 4.15.5 [10] <4.5, 4.11, 4.13> Se essa instrução já existir em uma ISA legada, explique como ela seria executada em um processador moderno, como o AMD Barcelona. O último problema neste exercício considera que cada uso da nova instrução substitui o número indicado das instruções originais, que a substituição pode ser feita uma vez no número indicado de instruções originais, e que, toda vez que a nova instrução for executada, o número indicado de ciclos de stall extras será acrescentado ao tempo de execução do programa: Substitui
Uma vez em cada
Ciclos de stall extras
a.
2
30
2
b.
3
40
1
4.15.6 [10] <4.5> Qual é o ganho de velocidade alcançado com o acréscimo dessa nova instrução? No seu cálculo, considere que o CPI do programa original (sem a nova instrução) seja 1.
Exercício 4.16 Os três primeiros problemas neste exercício referem-se à seguinte instrução MIPS: Instrução a.
SW R16,-100(R6)
b.
OR R2,R1,R0
4.16.1 [5] <4.6> Quando essa instrução é executada, o que é mantido em cada registrador localizado entre dois estágios do pipeline? 4.16.2 [5] <4.6> Que registradores precisam ser lidos, e quais registradores são realmente lidos? 4.16.3 [5] <4.6> O que essa instrução faz nos estágios EX e MEM? Os três problemas restantes neste exercício referem-se ao loop a seguir. Considere que a previsão de desvio perfeita é utilizada (sem stalls devido aos hazards de controle), que não existem slots de delay e que o pipeline possui suporte para forwarding completo. Considere também que muitas iterações desse loop são executadas antes que o loop termine. Loop a.
b.
342
Capítulo 4 O Processador
4.16.4 [10] <4.6> Mostre um diagrama de execução de pipeline para a terceira iteração desse loop, do ciclo em que apanhamos a primeira instrução dessa iteração até (mas não incluindo) o ciclo em que apanhamos a primeira instrução da iteração seguinte. Mostre todas as instruções que estão no pipeline durante esses ciclos (não apenas aquelas da terceira iteração). 4.16.5 [10] <4.6> Com que frequência (como uma porcentagem de todos os ciclos) temos um ciclo em que todos os cinco estágios do pipeline estão realizando trabalho útil? 4.16.6 [10] <4.6> No início do ciclo em que apanhamos a primeira instrução da terceira iteração desse loop, o que é armazenado no registrador IF/ID?
Exercício 4.17 Os problemas neste exercício consideram que as instruções executadas por um processador em pipeline são repartidas da seguinte forma: ADD
BEQ
LW
SW
a.
40%
30%
25%
5%
b.
60%
10%
20%
10%
4.17.1 [5] <4.6> Supondo que não haja stalls e que 60% de todos os desvios condicionais sejam tomados, em que porcentagem dos ciclos de clock o somador de desvio no estágio EX gera um valor que é realmente utilizado? 4.17.2 [5] <4.6> Supondo que não haja stalls, com que frequência (porcentagem de todos os ciclos) realmente precisamos usar todas as três portas de registrador (duas leituras e uma escrita) no mesmo ciclo? 4.17.3 [5] <4.6> Supondo que não haja stalls, com que frequência (porcentagem de todos os ciclos) usamos a memória de dados? Cada estágio de pipeline na Figura 4.33 tem alguma latência. Além disso, o pipelining introduz registradores entre os estágios (Figura 4.35), e cada um deles gera uma latência adicional. Os problemas restantes neste exercício assumem as seguintes latências para a lógica dentro de cada estágio do pipeline e para cada registrador entre dois estágios: IF
ID
EX
MEM
WB
Registrador do pipeline
a.
200ps
120ps
150ps
190ps
100ps
15ps
b.
150ps
200ps
200ps
200ps
100ps
15ps
4.17.4 [5] <4.6> Supondo que não haja stalls, qual é o ganho de velocidade obtido com o pipelining de um caminho de dados de ciclo único? 4.17.5 [10] <4.6> Podemos converter todas as instruções load/store em instruções baseadas em registrador (sem offset) e colocar o acesso à memória em paralelo com a ALU. Qual é o tempo do ciclo de clock se isso for feito no caminho de dados de ciclo único e em pipeline? Suponha que a latência do novo estágio EX/MEM é igual à maior de suas latências. 4.17.6 [10] <4.6> A mudança no Exercício 4.17.5 exige que muitas instruções LW/SW existentes sejam convertidas para sequências de duas instruções. Se isso for necessário para 50% dessas instruções, qual é o ganho de velocidade geral alcançado mudando do pipeline de cinco estágios para o pipeline de quatro estágios, em que EX e MEM são feitos em paralelo?
4.16 Exercícios 343
Exercício 4.18 Os três primeiros problemas neste exercício referem-se à execução da instrução a seguir no caminho de dados em pipeline da Figura 4.51, e considere o seguinte tempo de ciclo de clock, latência de ALU e latência de Mux: Tempo de ciclo de clock
Instrução
Latência de ALU
Latência de Mux
a.
LW R1,32(R2)
50ps
30ps
15ps
b.
OR R1,R5,R6
200ps
170ps
25ps
4.18.1 [10] <4.6> Para cada estágio do pipeline, quais são os valores dos sinais de controle ativados por essa instrução nesse estágio do pipeline? 4.18.2 [10] <4.6, 4.7> Quanto tempo a unidade de controle tem para gerar o sinal de controle ALUSrc? Compare isso com uma organização de ciclo único. 4.18.3 Qual é o valor do sinal PCSrc para essa instrução? Esse sinal é gerado cedo no estágio MEM (somente uma única porta AND). Qual seria um motivo a favor de fazer isso no estágio EX? Qual é o motivo contra fazer isso no estágio EX? Os problemas restantes neste exercício referem-se aos seguintes sinais da Figura 4.48: Sinal 1
Sinal 2
a.
ALUSrc
PCSrc
b.
Branch
RegWrite
4.18.4 [5] <4.6> Para cada um desses sinais, identifique o estágio do pipeline em que ele é gerado e o estágio em que ele é usado. 4.18.5 [5] <4.6> Para qual ou quais instruções MIPS esses dois sinais são definidos como 1? 4.18.6 [10] <4.6> Um desses sinais retorna pelo pipeline. Qual é esse sinal? Esse é um paradoxo de retorno no tempo? Explique.
Exercício 4.19 Esses problemas consideram que, de todas as instruções executadas em um processador, a fração dessas instruções a seguir tem um tipo particular de dependência de dados RAW. O tipo de dependência de dados RAW é identificado pelo estágio que produz o resultado (EX ou MEM) e a instrução que consome o resultado (1ª instrução que segue aquela que produz o resultado, 2ª instrução que a segue, ou ambas). Consideramos que a escrita do registrador é feita na primeira metade do ciclo de clock e que as leituras do registrador são feitas na segunda metade do ciclo, de modo que dependências “EX para 3ª” e “MEM para 3ª” não são contadas, pois não podem resultar em hazards de dados. Além disso, considere que o CPI do processador é 1 se não houver hazards de dados. EX para 1ª somente
MEM para 1ª somente
EX para 2ª somente
MEM para 2ª somente
EX para 1ª e MEM para 2ª
Outras dependências RAW
a.
5%
20%
5%
10%
10%
10%
b.
20%
10%
15%
10%
5%
0%
4.19.1 [10] <4.7> Se não usarmos forwarding, que fração dos ciclos estamos realizando stall devido aos hazards de dados?
344
Capítulo 4 O Processador
4.19.2 [5] <4.7> Se usarmos o forwarding completo (encaminhar todos os resultados que podem ser encaminhados), que fração dos ciclos estamos realizando stall devido aos hazards de dados? 4.19.3 [10] <4.7> Vamos supor que não tenhamos recursos para ter Muxes de três entradas que são necessários para o forwarding completo. Temos de decidir se é melhor encaminhar apenas do registrador de pipeline EX/MEM (forwarding do próximo ciclo) ou apenas do registrador de pipeline MEM/WB (forwarding de dois ciclos). Qual das duas opções resulta em menos ciclos de stall de dados? Os três problemas restantes neste exercício referem-se às seguintes latências para estágios individuais do pipeline. No estágio EX, as latências são dadas separadamente para um processador sem forwarding e um processador com diferentes tipos de forwarding. IF
ID
EX (sem FW)
EX(FW completo)
EX (FW apenas de EX/MEM)
EX(FW apenas de MEM/WB)
MEM
WB
a.
150ps
100ps
120ps
150ps
140ps
130ps
120ps
100ps
b.
300ps
200ps
300ps
350ps
330ps
320ps
290ps
100ps
4.19.4 [10] <4.7> Para as possibilidades de hazard e latências de estágio de pipeline indicadas, qual é o ganho de velocidade obtido acrescentando-se forwarding completo a um pipeline que não tinha forwarding? 4.19.5 [10] <4.7> Qual seria o ganho de velocidade adicional (relativo a um processador com forwarding) se acrescentássemos o forwarding de retorno no tempo que elimina todos os hazards de dados? Suponha que o circuito de retorno no tempo ainda a ser inventado acrescente 100ps à latência do estágio EX de forwarding completo. 4.19.6 [20] <4.7> Repita o Exercício 4.19.3, mas desta vez determine quais das duas opções resulta em menor tempo por instrução.
Exercício 4.20 Os problemas neste exercício referem-se a estas sequências de instrução: Sequência de instrução a.
b.
4.20.1 [5] <4.7> Encontre todas as dependências de dados nessa sequência de instrução. 4.20.2 [10] <4.7> Ache todos os hazards nessa sequência de instrução para um pipeline de cinco estágios com e depois sem forwarding. 4.20.3 [10] <4.7> Para reduzir o tempo do ciclo de clock, estamos considerando uma divisão do estágio MEM em dois estágios. Repita o Exercício 4.20.2 para esse pipeline de seis estágios. Os três problemas restantes neste exercício consideram que, antes que qualquer um dos anteriores seja executado, todos os valores na memória de dados são 0s e que os registradores de R0 a R3 têm os seguintes valores iniciais:
4.16 Exercícios 345
R0
R1
R2
R3
a.
0
–1
31
1500
b.
0
4
63
3000
4.20.4 [5] <4.7> Que valor é o primeiro a ser encaminhado e qual é o valor que ele redefine? 4.20.5 [10] <4.7> Se considerarmos que o forwarding será implementado quando projetarmos a unidade de detecção de hardware, mas depois nos esquecermos de realmente implementar o forwarding, quais são os valores finais dos registradores após essa sequência de instrução? 4.20.6 [10] <4.7> Para o projeto descrito no Exercício 4.20.5, acrescente nops a essa sequência de instrução de modo a garantir a execução correta apesar de faltar suporte para o forwarding.
Exercício 4.21 Este exercício tem por finalidade ajudá-lo a entender o relacionamento entre forwarding, detecção de hazard e projeto de ISA. Os problemas neste exercício referem-se a estas sequências de instrução, e considere que ele é executado em um caminho de dados com pipeline em cinco estágios. Sequência de instrução a.
b.
4.21.1 [5] <4.7> Se não houver forwarding ou detecção de hazard, insira nops para garantir a execução correta. 4.21.2 [10] <4.7> Repita o Exercício 4.21.1, mas agora use nops somente quando um hazard não puder ser evitado alterando ou rearrumando essas instruções. Você pode considerar que o registrador R7 pode ser usado para manter valores temporários no seu código modificado. 4.21.3 [10] <4.7> Se o processador tem forwarding, mas nos esquecemos de implementar a unidade de detecção de hazard, o que acontece quando esse código é executado? 4.21.4 [20] <4.7> Se houver forwarding, para os cinco primeiros ciclos durante a execução desse código, especifique quais sinais são ativados em cada ciclo pelas unidades de detecção de hazard e forwarding na Figura 4.60. 4.21.5 [10] <4.7> Se não houver forwarding, que novas entradas e sinais de saída precisamos para a unidade de detecção de hazard da Figura 4.60? Usando essa sequência de instruções como um exemplo, explique por que cada sinal é necessário. 4.21.6 [20] <4.7> Para a unidade de detecção de hazard do Exercício 4.21.5, especifique quais sinais de saída ela ativa em cada um dos cinco primeiros ciclos durante a execução desse código.
346
Capítulo 4 O Processador
Exercício 4.22 Este exercício tem por finalidade ajudá-lo a entender o relacionamento entre slots de delay, hazards de controle e execução de desvio em um processador com pipeline. Neste exercício, consideramos que o código MIPS a seguir é executado em um processador com um pipeline em cinco estágios, forwarding completo e um previsor de desvio tomado: a.
b.
4.22.1 [10] <4.8> Desenhe um diagrama de execução de pipeline para esse código, supondo que não existam slots de delay e que os desvios sejam executados no estágio EX. 4.22.2 [10] <4.8> Repita o Exercício 4.22.1, mas considere que os slots de delay sejam utilizados. No código apresentado, a instrução que vem após o desvio agora é a instrução do slot de delay para esse desvio. 4.22.3 [20] <4.8> Uma maneira de mover a resolução do desvio para um estágio anterior é não precisar de uma operação da ALU nos desvios condicionais. As instruções de desvio seriam “BEZ Rd,Label” e “BNEZ Rd,Label”, e haveria desvio se o registrador tivesse e não tivesse um valor 0, respectivamente. Mude esse código para usar essa instrução de desvio em vez de BEQ. Você pode considerar que o registrador $8 está disponível como um registrador temporário, e que uma instrução tipo R SEQ(set if equal) pode ser usada. A Seção 4.8 descreve como a rigidez dos hazards de controle pode ser reduzida movendo-se a execução do desvio para o estágio ID. Essa técnica envolve um comparador dedicado no estágio ID, como mostra a Figura 4.62. Porém, essa técnica tem o potencial de aumentar a latência do estágio ID, além de requerer lógica adicional de forwarding e detecção de hazard. 4.22.4 [10] <4.8> Usando como exemplo a primeira instrução de desvio no código apresentado, descreva a lógica de detecção de hazard necessária para dar suporte à execução do desvio no estágio ID como na Figura 4.62. Que tipo de hazard essa nova lógica deveria detectar? 4.22.5 [10] <4.8> Para o código apresentado, qual é o ganho de velocidade alcançado movendo-se a execução do desvio para o estágio ID? Explique sua resposta. No seu cálculo de ganho de velocidade, considere que a comparação adicional no estágio ID não afeta o tempo do ciclo de clock. 4.22.6 [10] <4.8> Usando como exemplo a primeira instrução de desvio no código apresentado, descreva o suporte para forwarding que precisa ser acrescentado para dar suporte à execução do desvio no estágio ID. Compare a complexidade dessa nova unidade de forwarding com a complexidade da unidade de forwarding existente na Figura 4.62.
Exercício 4.23 A importância de ter um bom previsor de desvio depende da frequência com que os desvios condicionais são executados. Juntamente com a precisão do previsor de desvio, isso
4.16 Exercícios 347
determinará quanto tempo será gasto com stall devido a desvios mal previstos. Neste exercício, considere o desmembramento das instruções dinâmicas em diversas categorias de instrução, como a seguir: Tipo R
BEQ
JMP
LW
SW
a.
40%
25%
5%
25%
5%
b.
60%
8%
2%
20%
10%
Além disso, considere as seguintes precisões do previsor de desvio: Sempre tomado
Sempre não tomado
2 bits
a.
45%
55%
85%
b.
65%
35%
98%
4.23.1 [10] <4.8> Os ciclos de stall devidos a desvios mal previstos aumentam o CPI. Qual é o CPI extra devido a desvios mal previstos com o previsor sempre tomado? Considere que os resultados do desvio sejam determinados no estágio EX, que não existem hazards de dados e que nenhum slot de delay seja utilizado. 4.23.2 [10] <4.8> Repita o Exercício 4.23.1 para o previsor “sempre não tomado”. 4.23.3 [10] <4.8> Repita o Exercício 4.23.1 para o previsor de 2 bits. 4.23.4 [10] <4.8> Com um previsor de 2 bits, que ganho de velocidade seria alcançado se pudéssemos converter metade das instruções de desvio de um modo que substitua uma instrução de desvio por uma instrução da ALU? Suponha que instruções previstas correta e incorretamente tenham a mesma chance de serem substituídas. 4.23.5 [10] <4.8> Com um previsor de 2 bits, que ganho de velocidade seria obtido se pudéssemos converter metade das instruções de desvio de um modo que substituísse cada instrução de desvio por duas instruções da ALU? Suponha que instruções previstas correta e incorretamente tenham a mesma chance de serem substituídas. 4.23.6 [10] <4.8> Algumas instruções de desvio são muito mais previsíveis do que outras. Se soubermos que 80% de todas as instruções de desvio executadas são desvios loop-back fáceis de prever, que sempre são previstos corretamente, qual é a precisão do previsor de 2 bits nos 20% restantes das instruções de desvio?
Exercício 4.24 Este exercício examina a precisão de vários previsores de desvios para o seguinte padrão repetitivo (como em um loop) de resultados do desvio: Resultados do desvio a.
T, T, NT, T
b.
T, NT, T, T, NT
4.24.1 [5] <4.8> Qual é a precisão dos previsores sempre tomado e sempre não tomado para essa sequência dos resultados do desvio? 4.24.2 [5] <4.8> Qual é a precisão do previsor de dois bits para os quatro primeiros desvios nesse padrão, supondo que o previsor comece no estado inferior esquerdo da Figura 4.63 (previsão não tomada). 4.24.3 [10] <4.8> Qual é a precisão do previsor de dois bits se esse padrão for repetido indefinidamente?
348
Capítulo 4 O Processador
4.24.4 [30] <4.8> Crie um previsor que alcance uma precisão perfeita se esse padrão for repetido indefinidamente. Seu previsor deverá ser um circuito sequencial com uma saída que oferece uma previsão (1 para tomado, 0 para não tomado) e nenhuma entrada que não seja o clock e o sinal de controle que indica que a instrução é um desvio condicional. 4.24.5 [10] <4.8> Qual é a precisão do seu previsor do Exercício 4.24.4 se ele receber um padrão repetitivo que é o oposto exato deste? 4.24.6 [20] <4.8> Repita o Exercício 4.24.4, mas agora o seu previsor deverá ser capaz de, mais cedo ou mais tarde (após um período de aquecimento durante o qual poderá fazer previsões erradas), começar a prever perfeitamente esse padrão e seu oposto. Seu previsor deverá ter uma entrada que lhe diga qual foi o resultado real. Dica: essa entrada permite que seu previsor determine qual dos dois padrões repetitivos ele recebe.
Exercício 4.25 Este exercício explora como o tratamento de exceção afeta o projeto do pipeline. Os três primeiros problemas neste exercício referem-se às duas instruções a seguir: Instrução 1
Instrução 2
a.
BNE R1,R2,Label
LW R1,0(R1)
b.
JUMP Label
SW R5,0(R1)
4.25.1 [5] <4.9> Que exceções cada uma dessas instruções pode disparar? Para cada uma dessas exceções, especifique o estágio do pipeline em que ela é detectada. 4.25.2 [10] <4.9> Se houver um endereço de handler separado para cada exceção, mostre como a organização do pipeline deve ser mudada para ser capaz de tratar dessa exceção. Você pode considerar que os endereços desses handlers são conhecidos quando o processador é projetado. 4.25.3 [10] <4.9> Se a segunda instrução dessa tabela for apanhada logo após a instrução da primeira tabela, descreva o que acontece no pipeline quando a primeira instrução causa a primeira exceção que você listou no Exercício 4.25.1. Mostre o diagrama de execução do pipeline do momento em que a primeira instrução é apanhada até o momento em que a primeira instrução do handler de exceção é concluída. Os três problemas restantes neste exercício consideram que os handlers de exceção estão localizados nos endereços a seguir: Overflow
Endereço de dados inválido
Instrução indefinida
Endereço de instrução inválido
Defeito do hardware
a.
0x1000CB05
0x1000D230
0x1000d780
0x1000E230
00x100F254
b.
0x450064E8
0xC8203E20
0C8203E20
0x678A0000
0x00000010
4.25.4 [5] <4.9> Qual é o endereço do handler de exceção no Exercício 4.25.3? O que acontece se houver uma instrução inválida nesse endereço na memória de instrução? 4.25.5 [20] <4.9> No tratamento de exceção com vetor, a tabela de endereços do handler de exceção está na memória de dados em um endereço conhecido (fixo). Mude o pipeline para implementar esse mecanismo de tratamento de exceção. Repita o Exercício 4.25.3 usando esse pipeline modificado e o tratamento de exceção com vetor. 4.25.6 [15] <4.9> Queremos simular o tratamento de exceção com vetor (descrito no Exercício 4.25.5) em uma máquina que tem apenas um endereço de handler fixo.
4.16 Exercícios 349
Escreva o código que deverá estar nesse endereço fixo. Dica: esse código deverá identificar a exceção, obter o endereço correto da tabela de vetor de exceção e transferir a execução para esse handler.
Exercício 4.26 Este exercício explora como o tratamento de exceção afeta o projeto da unidade de controle e o tempo do ciclo de clock do processador. Os três primeiros problemas neste exercício referem-se à seguinte instrução MIPS que dispara uma exceção: Instrução
Exceção
a.
BNE R1,R2,Label
Endereço inválido de alvo
b.
SUB R2,R4,R5
Overflow aritmético
4.26.1 [10] <4.9> Para cada estágio do pipeline, determine os valores dos sinais de controle da Figura 4.66 relacionados à exceção enquanto essa instrução passa por esse estágio do pipeline. 4.26.2 [5] <4.9> Alguns dos sinais de controle gerados no estágio ID são armazenados no registrador ID/EX do pipeline, e alguns vão diretamente para o estágio EX. Explique por que, usando essa instrução como um exemplo. 4.26.3 [10] <4.9> Podemos tornar o estágio EX mais rápido se verificarmos as exceções no estágio após aquela em que ocorre a condição excepcional. Usando essa instrução como exemplo, descreva a principal desvantagem dessa técnica. Os três problemas restantes neste exercício consideram que os estágios do pipeline possuem as seguintes latências: IF
ID
EX
MEM
WB
a.
220ps
150ps
250ps
200ps
200ps
b.
175ps
150ps
200ps
175ps
140ps
4.26.4 [10] <4.9> Se uma exceção de overflow ocorrer uma vez para cada 100.000 instruções executadas, qual é o ganho de velocidade geral se movermos a verificação do overflow para o estágio MEM? Considere que essa mudança reduz a latência de EX em 30ns e que o IPC alcançado pelo processador em pipeline é 1 quando não existem exceções. 4.26.5 [20] <4.9> Podemos gerar sinais de controle de exceção em EX em vez de ID? Explique como isso funcionará ou por que não funcionará, usando a instrução “BNE R4,R5,Label” e estas latências de estágio do pipeline como exemplo. 4.26.6 [10] <4.9> Supondo que cada Mux tenha uma latência de 40ps, determine o quanto da unidade de controle precisa gerar os sinais de flush. Que sinal é o mais crítico?
Exercício 4.27 Este exercício examina como o tratamento de exceção interage com as instruções de desvio e load/store. Os problemas neste exercício referem-se à seguinte instrução de desvio e à instrução do slot de delay correspondente: Desvio e slot de delay a. b.
350
Capítulo 4 O Processador
4.27.1 [20] <4.9> Suponha que esse desvio seja corretamente previsto como tomado, mas depois a instrução em “Label” é uma instrução indefinida. Descreva o que é feito em cada estágio do pipeline para cada ciclo, começando com o ciclo em que o desvio é decodificado até o ciclo em que a primeira instrução no handler de exceção é apanhada. 4.27.2 [10] <4.9> Repita o Exercício 4.27.1, mas dessa vez considere que a instrução no slot de delay também causa uma exceção de erro de hardware quando está no estágio MEM. 4.27.3 [10] <4.9> Qual é o valor do EPC se o desvio for tomado, mas o slot de delay causar uma exceção? O que acontece após a execução do handler de exceção ter sido concluída? Os três problemas restantes neste exercício também se referem à seguinte instrução store: Instrução store a.
SW R5,-40(R15)
b.
SW R1,0(R1)
4.27.4 [10] <4.9> O que acontece se o desvio for tomado, a instrução em “Label” for uma instrução inválida, a primeira instrução do handler de exceção for a instrução SW apresentada, e esse store acessar um endereço de dados inválido? 4.27.5 [10] <4.9> Se o cálculo do endereço de load/store puder estourar, podemos adiar a detecção da exceção de estouro para o estágio MEM? Use a instrução store indicada para explicar o que acontece. 4.27.6 [10] <4.9> Para a depuração, é útil ser capaz de detectar quando um valor em particular é escrito em um endereço de memória específico. Queremos acrescentar dois novos registradores, WADDR e WVAL. O processador deverá disparar uma exceção quando o valor igual a WVAL estiver para ser escrito no endereço WADDR. Como você mudaria o pipeline para implementar isso? Como essa instrução sw seria tratada pelo seu caminho de dados modificado?
Exercício 4.28 Neste exercício, comparamos o desempenho dos processadores de um despacho e processadores de dois despachos, levando em conta as transformações do programa que podem ser feitas para otimizar para a execução em dois despachos. Os problemas neste exercício referem-se ao seguinte loop (escrito em C): Código C a. b.
Ao escrever código MIPS, considere que as variáveis são mantidas em registradores da seguinte forma, e que todos os registradores, com exceção daqueles indicados como Livre, são usados para manter diversas variáveis, de modo que não podem ser usados para nada mais. I
J
A
B
C
Livre
a.
R2
R8
R9
R10
R11
R3,R4,R5
b.
R5
R6
R1
R2
R3
R10,R11,R12
4.16 Exercícios 351
4.28.1 [10] <4.10> Traduza esse código C para instruções MIPS. Sua tradução deverá ser direta, sem rearrumar as instruções para conseguir melhor desempenho. 4.28.2 [10] <4.10> Se o loop sair depois de executar apenas duas iterações, desenhe um diagrama de pipeline para o seu código MIPS do Exercício 4.28.1 executado em um processador com dois despachos mostrado na Figura 4.69. Suponha que o processador tenha previsão de desvio perfeita e possa buscar quaisquer duas instruções (não apenas instruções consecutivas) no mesmo ciclo. 4.28.3 [10] <4.10> Rearrume o seu código do Exercício 4.28.1 para alcançar o melhor desempenho em um processador de dois despachos escalonado estaticamente, da Figura 4.69. 4.28.4 [10] <4.10> Repita o Exercício 4.28.2, mas desta vez use seu código MIPS do Exercício 4.28.3. 4.28.5 [10] <4.10> Qual é o ganho de velocidade ao passar de um processador de um despacho para dois despachos da Figura 4.69? Use o seu código do Exercício 4.28.1 para um despacho e dois despachos, e considere que 1.000.000 iterações do loop são executadas. Assim como no Exercício 4.28.2, considere que o processador tenha previsões de desvio perfeitas, e que um processador de dois despachos possa buscar duas instruções quaisquer no mesmo ciclo. 4.28.6 [10] <4.10> Repita o Exercício 4.28.5, mas desta vez considere que, no processador de dois despachos, uma das instruções a serem executadas em um ciclo possa ser de qualquer tipo, e a outra seja uma instrução não de memória.
Exercício 4.29 Neste exercício, consideramos a execução de um loop em um processador superescalar escalonado estaticamente. Para simplificar o exercício, suponha que qualquer combinação dos tipos de instrução possa ser executada no mesmo ciclo, por exemplo, em um superescalar de três despachos, as três instruções podem ser três operações da ALU, três desvios, três instruções load/store ou qualquer combinação dessas instruções. Observe que isso só remove uma restrição de recurso, mas as dependências de dados e controle ainda precisam ser tratadas corretamente. Os problemas neste exercício referem-se ao loop a seguir: Loop a.
b.
4.29.1 [10] <4.10> Se muitas (por exemplo, 1.000.000) iterações desse loop forem executadas, determine a fração de todas as leituras de registrador que são úteis em um processador superescalar estático de dois despachos. 4.29.2 [10] <4.10> Se muitas (por exemplo, 1.000.000) iterações desse loop forem executadas, determine a fração de todas as leituras de registrador que são úteis em um processador superescalar estático de três despachos. Compare isso com o seu resultado para um processador de dois despachos do Exercício 4.29.1.
352
Capítulo 4 O Processador
4.29.3 [10] <4.10> Se muitas (por exemplo, 1.000.000) iterações desse loop forem executadas, determine a fração de ciclos em que duas ou três portas de escrita de registrador são usadas em um processador superescalar estático de três despachos. 4.29.4 [20] <4.10> Desdobre esse loop uma vez e escalone-o para um processador superescalar estático de dois despachos. Suponha que o loop sempre execute um número par de iterações. Você pode usar os registradores de $10 a $20 quando alterar o código para eliminar dependências. 4.29.5 [20] <4.10> Qual é o ganho de velocidade do uso do seu código do Exercício 4.29.4 em vez do código original com um processador superescalar estático de dois despachos? Suponha que o loop tenha muitas iterações (por exemplo, 1.000.000). 4.29.6 [10] <4.10> Qual é o ganho de velocidade de usar o seu código do Exercício 4.29.4 em vez do código original com um processador em pipeline (um despacho)? Suponha que o loop tenha muitas iterações (por exemplo, 1.000.000).
Exercício 4.30 Neste exercício, fazemos várias suposições. Primeiro, supomos que um processador superescalar de N despachos possa executar quaisquer N instruções no mesmo ciclo, independente de seus tipos. Segundo, supomos que cada instrução seja escolhida independentemente, sem considerar a instrução que a antecede ou sucede. Terceiro, supomos que não existem stalls devidos a dependências de dados, que nenhum slot de delay é utilizado e que os desvios são executados no estágio EX do pipeline. Finalmente, supomos que as instruções executadas no programa são distribuídas da seguinte forma: ALU
BEQ previstas corretamente
BEQ previstas incorretamente
a.
40%
20%
5%
25%
10%
b.
45%
4%
1%
30%
20%
LW
SW
4.30.1 [5] <4.10> Qual é o CPI obtido por um processador superescalar estático de dois despachos nesse programa? 4.30.2 [10] <4.10> Em um superescalar estático de dois despachos, cujo previsor só pode tratar de um desvio por ciclo, que ganho de velocidade é obtido acrescentando a capacidade de prever dois desvios por ciclo? Considere uma política de stall-no-desvio para os desvios que o previsor não pode tratar. 4.30.3 [10] <4.10> Em um processador superescalar estático de dois despachos, que só tenha uma porta de escrita de registrador, que ganho de velocidade é obtido acrescentando uma segunda porta de escrita de registrador? 4.30.4 [5] <4.10> Para um processador superescalar estático de dois despachos com um pipeline clássico de cinco estágios, que ganho de velocidade é obtido tornando a previsão de desvio perfeita? 4.30.5 [10] <4.10> Repita o Exercício 4.30.4, mas para um processador de quatro despachos. Que conclusão você pode chegar sobre a importância de uma boa previsão de desvio quando a largura da despacho do processador é aumentada? 4.30.6 <4.10> Repita o Exercício 4.30.5, mas agora suponha que o processador de quatro despachos tenha 50 estágios de pipeline. Considere que cada um dos cinco estágios
4.16 Exercícios 353
originais seja desmembrado em dez novos estágios, e que os desvios sejam executados no primeiro de dez novos estágios EX. A que conclusão você pode chegar sobre a importância da boa previsão de desvio quando a profundidade do pipeline do processador é aumentada?
Exercício 4.31 Os problemas neste exercício referem-se ao loop a seguir, que é dado como código x86 e também como uma tradução MIPS desse código. Você pode considerar que esse loop executa muitas iterações antes de terminar. Ao determinar o desempenho, isso significa que você só precisa determinar qual seria o desempenho no “estado fixo”, e não para as primeiras e últimas iterações do loop. Além disso, você pode considerar o suporte de encaminhamento total e previsão de desvio perfeita sem slots de delay, de modo que os únicos hazards com que você tem de se preocupar são hazards de recursos e hazards de dados. Observe que a maioria das instruções x86 nesse problema possui dois operandos cada. O último (normalmente segundo) operando da instrução indica o primeiro valor dos dados de origem e o destino. Se a operação precisar de um segundo valor de dados de origem, este é indicado pelo outro operando da instrução. Por exemplo, “sub (edx),eax” lê o local da memória apontado pelo registrador edx, subtrai esse valor do registrador eax e coloca o resultado de volta no registrador eax. Instruções x86
Tradução tipo MIPS
a.
b.
4.31.1 [20] <4.11> Que CPI seria alcançado se a versão MIPS desse loop fosse executada em um processador de um despacho com escalonamento estático e uma pipeline de cinco estágios? 4.31.2 [20] <4.11> Que CPI seria obtido se a versão x86 desse loop for executada em um processador de um despacho com escalonamento estático e um pipeline de sete estágios? Os estágios do pipeline são IF, ID, ARD, MRD, EXE e WB. Os estágios IF e ID são semelhantes àqueles no pipeline MIPS de cinco estágios. ARD calcula o endereço do local de memória a ser lido, MRD realiza a leitura da memória, EXE executa a operação e WB escreve o resultado no registrador ou na memória. A memória de dados tem uma porta de leitura (para instruções no estágio MRD) e uma porta de escrita separada (para instruções no estágio WB).
354
Capítulo 4 O Processador
4.31.3 [20] <4.11> Que CPI seria obtida se a versão x86 desse loop fosse executada em um processador que traduz essas instruções internamente para micro-operações tipo MIPS, depois executa essas micro-operações em um pipeline de cinco estágios e um despacho com escalonamento estático. Observe que o contador de instrução usado no cálculo do CPI para esse processador é o contador de instrução do x86. 4.31.4 [20] <4.11> Que CPI seria obtido se a versão MIPS desse loop fosse executada em um processador de um despacho com escalonamento dinâmico? Suponha que nosso processador não esteja realizando renomeação de registrador, de modo que você só possa reordenar as instruções que não possuem dependências de dados. 4.31.5 [30] <4.10, 4.11> Supondo que existam muitos registradores livres à disposição, renomeie a versão MIPS desse loop para eliminar o máximo de dependências de dados possível entre as instruções na mesma iteração do loop. Agora, repita o Exercício 4.31.4, usando seu novo código renomeado. 4.31.6 [20] <4.10, 4.11> Repita o Exercício 4.31.4, mas desta vez suponha que o processador atribui um novo nome ao resultado de cada instrução à medida que essa instrução é decodificada, e depois renomeia os registradores usados por instruções subsequentes para usar valores de registrador corretos.
Exercício 4.32 Os problemas neste exercício consideram que os desvios representam as frações de todas as instruções executadas e a precisão da previsão de desvio apresentadas na tabela a seguir. Considere que o processador nunca sofreu stall por dados e dependências de recursos, como a busca e execução do número máximo de instruções por ciclo se existirem hazards de controle. Para as dependências de controle, o processador utiliza a previsão de desvio e continua a fazer a busca no caminho de dados previsto. Se o desvio foi previsto erroneamente, o resultado do desvio é resolvido pelas instruções buscadas depois do desvio errado ter sido descartado e o próximo ciclo do processador fará a busca no caminho correto. Desvios como % de todas as instruções executadas
Precisão da previsão de desvio
a.
25
95%
b.
25
99%
4.32.1 [5] <4.11> Quantas instruções espera-se que sejam executadas entre o momento em que um erro de previsão de desvio é detectado e o momento em que o próximo erro de previsão de desvio é detectado? Os problemas restantes neste exercício consideram a seguinte profundidade de pipeline e que o resultado do desvio é determinado no estágio de pipeline seguinte (contando a partir do estágio 1): Profundidade do pipeline
Resultado do desvio conhecido no estágio
a.
15
12
b.
30
20
4.32.2 [5] <4.11> Em um processador de quatro despachos com esses parâmetros de pipeline, quantas instruções de desvio pode-se esperar que estejam “em andamento” (já apanhadas, mas ainda não confirmadas) em determinado momento?
4.16 Exercícios 355
4.32.3 [5] <4.11> Quantas instruções são apanhadas do caminho errado para cada erro de previsão de desvio em um processador de quatro despachos? 4.32.4 [10] <4.11> Qual é o ganho de velocidade obtido pela mudança do processador de quatro para oito despachos? Suponha que os processadores de oito e quatro despachos difiram apenas no número de instruções por ciclo, e de outras maneiras sejam idênticas (profundidade de pipeline, estágio de resolução de desvio etc.). 4.32.5 [10] <4.11> Qual é o ganho de velocidade da execução de desvios um estágio antes em um processador de quatro despachos? 4.32.6 [10] <4.11> Qual é o ganho de velocidade da execução dos desvios um estágio antes em um processador de oito despachos? Discuta a diferença entre esse resultado e o resultado do Exercício 4.32.5.
Exercício 4.33 Este exercício explora como a previsão do desvio afeta o desempenho de um processador de despacho múltiplo com pipeline profundo. Os problemas neste exercício referem-se a um processador com o seguinte número de estágios de pipeline e instruções emitidas por ciclo: Profundidade de pipeline
Largura do despacho
a.
15
2
b.
30
8
4.33.1 [10] <4.11> Quantas portas de leitura de registrador o processador deverá ter para evitar quaisquer hazards de recursos devido a leituras de registrador? 4.33.2 [10] <4.11> Se não houver erros de previsão de desvio e dependências de dados, qual é a melhoria de desempenho esperada em relação a um processador de um despacho com um pipeline clássico de cinco estágios? Suponha que o tempo de ciclo de clock diminua em proporção ao número de estágios de pipeline. 4.33.3 [10] <4.11> Repita o Exercício 4.33.2, mas desta vez cada instrução executada tem uma dependência de dados RAW com a instrução que é executada logo em seguida. Você pode assumir que nenhum ciclo de stall é necessário, ou seja, o forwarding permite que instruções consecutivas sejam executadas em ciclos back-to-back. Para os três problemas restantes neste exercício, a menos que o problema especifique de outra forma, considere as estatísticas a seguir sobre qual porcentagem de instruções são desvios, precisão do previsor e perda de desempenho devido a erros de previsão de desvio: Desvios como uma fração de todas as instruções executadas
Desvios executados no estágio
Precisão do previsor
Perda de desempenho
a.
10%
9
96%
5%
b.
10%
5
98%
1%
4.33.4 [10] <4.11> Se tivermos a fração dada de instruções de desvio e precisão da previsão de desvio, que porcentagem de todos os ciclos é gasta inteiramente apanhando instruções de caminho errado? Ignore o número da perda de desempenho.
356
Capítulo 4 O Processador
4.33.5 [20] <4.11> Se quisermos limitar stalls devido a desvios mal previstos a não mais que determinada porcentagem do tempo de execução ideal (sem stalls), qual deverá ser nossa precisão da previsão de desvio? Ignore o número de precisão do previsor indicado. 4.33.6 [10] <4.11> Qual deverá ser a precisão da previsão de desvio se quisermos ter um ganho de velocidade de 0,5 (metade) em relação ao mesmo processador com um previsor de desvio ideal?
Exercício 4.34 Este exercício tem por finalidade ajudá-lo a entender a discussão da falácia “Pipelining é fácil”, da Seção 4.13. Os quatro primeiros problemas neste exercício referem-se à seguinte instrução MIPS: Instrução
Interpretação
a.
AND Rd,Rs,Rt
Reg[Rd]=Reg[Rs] AND Reg[Rt]
b.
SW Rt,Offs(Rs)
Mem[Reg[Rs]+Offs]=Reg[Rt]
4.34.1 [10] <4.13> Descreva um caminho de dados em pipeline para dar suporte apenas a essa instrução. Seu caminho de dados deverá ser criado com a hipótese de que as únicas instruções que serão executadas são instâncias dessa instrução. 4.34.2 [10] <4.13> Descreva os requisitos das unidades de forwarding e detecção de hazard para o seu caminho de dados do Exercício 4.34.1. 4.34.3 [10] <4.13> O que precisa ser feito para dar suporte a exceções de instrução indefinidas no seu caminho de dados do Exercício 4.34.1? Observe que a exceção de instrução indefinida deverá ser ativada sempre que o processador encontrar qualquer outro tipo de instrução. Os dois problemas restantes neste exercício também se referem a esta instrução MIPS: Instrução
Interpretação
a.
AND Rd,Rs,Rt
Reg[Rd]=Reg[Rs] + Reg[Rt]
b.
ADDI Rt,Rs,Imm
Reg[Rt]=Reg[Rs]+Imm
4.34.4 [10] <4.13> Descreva como estender o seu caminho de dados do Exercício 4.34.1 de modo que também dê suporte a essa instrução. Seu caminho de dados estendido deverá ser criado para dar suporte apenas a instâncias dessas duas instruções. 4.34.5 [10] <4.13> Repita o Exercício 4.34.2 para o seu caminho de dados estendido do Exercício 4.34.4.
Exercício 4.35 Este exercício tem por finalidade ajudá-lo a entender melhor o relacionamento entre o projeto de ISA e o pipelining. Os problemas neste exercício consideram que temos um processador em pipeline com despacho múltiplo com o seguinte número de estágios de pipeline, instruções remetidas por ciclo, estágio em que os resultados do desvio são resolvidos e precisão do previsor de desvio.
4.16 Exercícios 357
Profundidade do pipeline
Largura do despacho
Desvios executados no estágio
Precisão do previsor de desvio
Desvios como % de instruções
a.
15
2
10
90%
25%
b.
25
4
15
96%
15%
4.35.1 [5] <4.8, 4.13> Os hazards de controle podem ser eliminados acrescentando slots de delay de desvio. Quantos slots de delay precisam acompanhar cada desvio se quisermos eliminar todos os hazards de controle neste processador? 4.35.2 [10] <4.8, 4.13> Qual é o ganho de velocidade que seria obtido usando quatro slots de delay de desvio para reduzir os hazards de controle neste processador? Considere que não existem dependências de dados entre as instruções e que todos os quatro slots de delay podem ser preenchidos com instruções úteis sem aumentar o número de instruções executadas. Para tornar seus cálculos mais fáceis, você também pode considerar que a instrução de desvio mal prevista sempre é a última instrução a ser apanhada em um ciclo, ou seja, nenhuma instrução que esteja no mesmo estágio do pipeline que o desvio é apanhada do caminho errado. 4.35.3 [10] <4.8, 4.13> Repita o Exercício 4.35.2, mas agora considere que 10% dos desvios executados têm todos os quatro slots de delay preenchidos com instrução útil, 20% têm apenas três instruções úteis nos slots de delay (o quarto slot de delay é um nop), 30% têm apenas duas instruções úteis nos slots de delay, e 40% não têm instruções úteis em seus slots de delay. Os três problemas restantes neste exercício referem-se ao seguinte loop em C: a.
b.
4.35.4 [10] <4.8, 4.13> Traduza este loop em C para instruções MIPS, considerando que nossa ISA requer apenas um slot de delay para cada desvio. Tente preencher os slots de delay com instruções não NOP sempre que possível. Você pode considerar que as variáveis a, b, c, i e j são mantidas nos registradores r1, r2,r3,r4 e r5. 4.35.5 [10] <4.7, 4.13> Repita o Exercício 4.35.4 para um processador que tem dois slots de delay para cada desvio. 4.35.6 [10] <4.10, 4.13> Quantas iterações do seu loop do Exercício 4.35.4 podem estar “em voo” dentro do pipeline desse processador? Dizemos que uma iteração está “em voo” quanto pelo menos uma de suas instruções foi apanhada e ainda não foi confirmada.
Exercício 4.36 Este exercício tem por finalidade ajudá-lo a entender melhor a última armadilha da Seção 4.13 — deixar de considerar o pipelining no projeto do conjunto de instruções. Os quatro primeiros problemas neste exercício referem-se à seguinte nova instrução MIPS: Instrução
Interpretação
a.
SWINC Rt,Offset(Rs)
Mem[Reg[Rs]+offset]=Reg[Rt] Reg[Rs]=Reg[Rs]+4
b.
SWI Rt,Rd(Rs)
Mem[Reg[Rd]+Reg[Rs]]=Reg[Rt]
358
Capítulo 4 O Processador
4.36.1 [10] <4.11, 4.13> Traduza esta instrução para micro-operações MIPS. 4.36.2 [10] <4.11, 4.13> Como você mudaria o pipeline MIPS de cinco estágios de modo a acrescentar suporte à tradução de micro-operação necessária para aceitar essa nova instrução? 4.36.3 [20] <4.13> Se quisermos acrescentar esta instrução à ISA do MIPS, discuta as mudanças no pipeline (quais estágios, quais estruturas em qual estágio) que são necessárias para admitir diretamente (sem micro-operações) esta instrução. 4.36.4 [10] <4.13> Com que frequência você espera que essa instrução possa ser usada? Você acha que haveria justificativa se acrescentássemos essa instrução à ISA do MIPS? Os dois problemas seguintes neste exercício são referentes à inclusão de uma nova instrução ADDM à ISA. Em um processador no qual ADDM foi acrescentada, estes problemas consideram o seguinte desmembramento de ciclos de clock, segundo o qual a instrução termina nesse ciclo (ou qual stall está impedindo que uma instrução termine): ADD
BEQ
LW
SW
ADDM
Stalls de controle
Stalls de dados
a.
25%
20%
20%
10%
3%
10%
12%
b.
25%
10%
25%
20%
5%
10%
5%
4.36.5 [10] <4.13> Dado esse desmembramento dos ciclos de execução no processador com suporte direto à instrução ADDM, que ganho de velocidade é alcançado usando essa instrução no lugar de uma sequência de três instruções (LW, ADD e depois SW)? Considere que a instrução ADDM é, de alguma forma (mágica), aceita com um pipeline clássico de cinco estágios, sem criar hazards de recurso. 4.36.6 [10] <4.13> Repita o Exercício 4.36.5, mas agora considere que ADDM fosse aceita incluindo um estágio do pipeline. Quando ADDM é traduzida, esse estágio extra pode ser removido e, como resultado, metade dos stalls de dados existentes são eliminados. Observe que a eliminação do stall de dados só se aplica a stalls que existiam antes da tradução de ADDM, e não a stalls acrescentados pela própria tradução de ADDM.
Exercício 4.37 Este exercício explora algumas das escolhas envolvidas no pipelining, como o tempo do ciclo de clock e a utilização dos recursos de hardware. Os três primeiros problemas neste exercício referem-se ao código MIPS a seguir. O código é escrito supondo-se que o processador não utiliza slots de delay. a.
b.
4.37.1 [5] <4.3, 4.14> Quais partes do caminho de dados de ciclo único são usadas por todas essas instruções? Quais partes são as menos utilizadas?
4.16 Exercícios 359
4.37.2 [10] <4.6, 4.14> Qual é a utilização para a porta de leitura e de escrita da unidade de memória de dados? 4.37.3 [10] <4.6, 4.14> Suponha que já temos um projeto de ciclo único. De quantos bits precisamos no total para os registradores de pipeline a fim de implementarmos o projeto em pipeline? Os três problemas restantes neste exercício consideram que os componentes do caminho de dados têm as seguintes latências: I-Mem
Add
Mux
ALU
Regs
Mem D
Extensão de sinal
Shift-esq-2
a.
200ps
70ps
20ps
90ps
90ps
250ps
15ps
10ps
b.
750ps
200ps
50ps
250ps
300ps
500ps
100ps
5ps
4.37.4 [10] <4.3, 4.5, 4.14> Dadas essas latências para elementos individuais do caminho de dados, compare os tempos de ciclo de clock do caminho de dados em pipeline de ciclo único e cinco estágios. 4.37.5 [10] <4.3, 4.5, 4.14> Repita o Exercício 4.37.4, mas agora considere que só queremos dar suporte a instruções ADD. 4.37.6 [20] <4.3, 4.5, 4.14> Se custar $1 para reduzir a latência de um único componente do caminho de dados em 1ps, qual seria o custo para reduzir o tempo de ciclo de clock em 20% no projeto de ciclo único e em pipeline?
Exercício 4.38 Este exercício explora a eficiência de energia e seu relacionamento com o desempenho. Os problemas neste exercício consideram o consumo de energia a seguir para a atividade na Memória de Instrução, Registradores e Memória de Dados. Você pode considerar que os outros componentes do caminho de dados gastam uma quantidade de energia insignificante. I-Mem
1 Leitura de Registrador
Escrita de Registrador
Leitura de Mem D
Escrita de Mem D
a.
140pJ
70pJ
60pJ
140pJ
120pJ
b.
70pJ
40pJ
40pJ
90pJ
100pJ
4.38.1 [10] <4.3, 4.6, 4.14> Quanta energia é gasta para executar uma instrução ADD em um projeto de ciclo único e no projeto em pipeline com cinco estágios? 4.38.2 [10] <4.6, 4.14> Qual é a instrução MIPS no pior caso em termos do consumo de energia, e qual é a energia gasta para executá-la? 4.38.3 [10] <4.6, 4.14> Se a redução de energia é fundamental, como você mudaria o projeto em pipeline? Qual é a redução percentual na energia gasta por uma instrução LW após essa mudança?
I-Mem
Controle
Registrador de leitura ou escrita
ALU
Leitura ou escrita de Mem D
a.
200ps
150ps
90ps
90ps
250ps
b.
750ps
500ps
300ps
250ps
500ps
360
Capítulo 4 O Processador
4.38.4 [10] <4.6, 4.14> Qual é o impacto das suas mudanças do Exercício 4.38.3 sobre o desempenho? 4.38.5 [10] <4.6, 4.14> Podemos eliminar o sinal de controle MemRead e fazer com que a memória de dados seja lida em cada ciclo, ou seja, podemos ter MemRead = 1 permanentemente. Explique por que o processador ainda funciona corretamente após essa mudança. Qual é o efeito dessa mudança sobre a frequência de clock e consumo de energia? 4.38.6 [10] <4.6, 4.14> Se uma unidade ociosa gasta 10% da potência que gastaria se estivesse ativa, qual é a energia gasta pela memória de instrução em cada ciclo? Que porcentagem da energia geral gasta pela memória de instrução essa energia ociosa representa?
Exercício 4.39 Os problemas neste exercício consideram que, durante uma execução do programa, os ciclos do processador são gastos como na tabela a seguir. Um ciclo é “gasto” em uma instrução se o processador concluir esse tipo de instrução nesse ciclo; um ciclo é “gasto” em um stall se o processador não puder concluir uma instrução nesse ciclo devido a um stall. ADD
BEQ
LW
SW
Stalls de controle
Stalls de dados
a.
25%
20%
20%
10%
10%
15%
b.
25%
10%
25%
20%
10%
10%
Os problemas neste exercício também consideram que os estágios de pipeline individuais possuem latências e consumos de energia a seguir. O estágio gasta essa energia para realizar seu trabalho dentro da latência indicada. Observe que nenhuma energia é gasta no estágio MEM durante um ciclo em que não existe acesso à memória. De modo semelhante, nenhuma energia é gasta no estágio WB em um ciclo no qual não existe escrita de registrador. Em vários dos problemas a seguir, fazemos suposições sobre como o consumo de energia muda se um estágio realizar seu trabalho mais lenta ou mais rapidamente do que isso. IF
ID
EX
MEM
WB
a.
250ps/100pJ
350ps/45pJ
150ps/50pJ
300ps/150pJ
200ps/50pJ
b.
200ps/75pJ
170ps/45pJ
220ps/100pJ
210ps/100pJ
150ps/35pJ
4.39.1 [10] <4.14> Qual é o desempenho (em instruções por segundo)? 4.39.2 [10] <4.14> Qual é a potência dissipada em watts (joules por segundo)? 4.39.3 [10] <4.6, 4.14> Que estágios do pipeline você pode tornar mais lentos e por quanto, sem afetar o tempo de ciclo de clock? 4.39.4 [20] <4.6, 4.14> Normalmente, é possível sacrificar alguma velocidade em um circuito a fim de reduzir seu consumo de energia. Suponha que possamos reduzir o consumo de energia por um fator de X (nova energia é 1/X vezes a energia antiga) se aumentarmos a latência por um fator de X (nova latência é X vezes a latência antiga). Agora, podemos ajustar as latências dos estágios do pipeline para minimizar o consumo de energia sem sacrificar qualquer desempenho. Repita o Exercício 4.39.2 para esse processador ajustado. 4.39.5 [10] <4.6, 4.14> Repita o Exercício 4.39.4, mas desta vez o objetivo é minimizar a energia gasta por instrução enquanto o tempo de ciclo de clock aumenta não mais do que 10%.
4.16 Exercícios 361
4.39.6 [10] <4.6, 4.14> Repita o Exercício 4.39.5, mas agora suponha que o consumo de energia seja reduzido por um fator de X2 quando a latência se torna X vezes maior. Quais são as economias de potência em comparação com o que você calculou para o Exercício 4.39.2? §4.1, página 303: 3 de 5: Controle, Caminho de dados, Memória, Entrada e Saída estão faltando. §4.2, página 307: falso. Elementos de estado disparados na borda tornam a leitura e escrita simultâneas tanto possíveis quanto não ambíguas. §4.3, página 315: I. A. II. C. §4.4, página 330: Sim, Desvio e OpALU0 são idênticos. Além disso, MemtoReg e RegDst são opostos um do outro. Você não precisa de um inversor; basta usar o outro sinal e inverter a ordem das entradas para o multiplexador! §4.5, página 343: 1. Stall no resultado LW. 2. Bypassing do primeiro resultado de ADD escrito em $t1. 3. Nenhum stall ou bypassing é necessário. §4.6, página 358: Afirmações 2 e 4 estão corretas; o restante está incorreto. §4.8, página 383: 1. Previsão não tomada. 2. Previsão tomada. 3. Previsão dinâmica. §4.9, página 391: A primeira instrução, pois ela é executada logicamente antes das outras. §4.10, página 403: 1. Ambos. 2. Ambos. 3. Software. 4. Hardware. 5. Hardware. 6. Hardware. 7. Ambos. 8. Hardware. 9. Ambos. §4.11, página 404: Duas primeiras são falsas e duas últimas são verdadeiras. §4.12, página 4.12-3: Afirmações 1 e 3 são ambas verdadeiras. §4.12, página 4.12-5: A melhor resposta é a 2 (veja a Elaboração na página 371).
Respostas das Seções “Verifique você mesmo”
5 O ideal seria ter uma capacidade de memória infinitamente grande a ponto de qualquer palavra específica … estar imediatamente disponível. … Somos … forçados a reconhecer a possibilidade de construir uma hierarquia de memórias, cada uma com capacidade maior do que a anterior, mas com acessibilidade menos rápida.
Grande e Rápida: Explorando a Hierarquia de Memória
A. W. Burks, H. H. Goldstine e J. von Neumann Preliminary Discussion of the Logical Design of an Electronic Computing Instrument, 1946
5.1 Introdução 364 5.2
Princípios básicos de cache 368
5.3
Medindo e melhorando o desempenho da cache 382
5.4
Memória virtual 396
5.5
Uma estrutura comum para hierarquias de memória 417
5.6
Máquinas virtuais 423
5.7
Usando uma máquina de estado finito para controlar uma cache simples 426
5.8
Paralelismo e hierarquias de memória: coerência de cache 430
5.9
Material avançado: implementando controladores de cache 434
5.10
Vida real: as hierarquias de memória do AMD Opteron X4 (Barcelona) e Intel Nehalem 434
5.11
Falácias e armadilhas 438
5.12
Comentários finais 440
5.13
Perspectiva histórica e leitura adicional 441
5.14 Exercícios 441
Os cinco componentes clássicos de um computador
364
Capítulo 5 Grande e Rápida: Explorando a Hierarquia de Memória
5.1 Introdução Desde os primeiros dias da computação, os programadores têm desejado quantidades ilimitadas de memória rápida. Os tópicos deste capítulo ajudam os programadores a criar essa ilusão. Antes de vermos como a ilusão é realmente criada, vamos considerar uma analogia simples que ilustra os princípios e mecanismos-chave utilizados. Suponha que você fosse um estudante fazendo um trabalho sobre os importantes desenvolvimentos históricos no hardware dos computadores. Você está sentado em uma biblioteca examinando uma pilha de livros retirada das estantes. Você descobre que vários computadores importantes, sobre os quais precisa escrever, são descritos nos livros encontrados, mas não há nada sobre o EDSAC. Então, volta às estantes e procura um outro livro. Você encontra um livro sobre os primeiros computadores britânicos, que fala sobre o EDSAC. Com uma boa seleção de livros sobre a mesa à sua frente, existe uma boa probabilidade de que muitos dos tópicos de que precisa possam ser encontrados neles. Com isso, você pode gastar mais do seu tempo apenas usando os livros na mesa sem voltar às estantes. Ter vários livros na mesa economiza seu tempo em comparação a ter apenas um livro e constantemente precisar voltar às estantes para devolvê-lo e apanhar outro. O mesmo princípio nos permite criar a ilusão de uma memória grande que podemos acessar tão rapidamente quanto uma memória muito pequena. Assim como você não precisou acessar todos os livros da biblioteca ao mesmo tempo com igual probabilidade, um programa não acessa todo o seu código ou dados ao mesmo tempo com igual probabilidade. Caso contrário, seria impossível tornar rápida a maioria dos acessos à memória e ainda ter memória grande nos computadores, assim como seria impossível você colocar todos os livros da biblioteca em sua mesa e ainda encontrar o desejado rapidamente. Esse princípio da localidade sustenta a maneira como você fez seu trabalho na biblioteca e o modo como os programas funcionam. O princípio da localidade diz que os programas acessam uma parte relativamente pequena do seu espaço de endereçamento em qualquer instante do tempo, exatamente como você acessou uma parte bastante pequena da coleção da biblioteca. Há dois tipos diferentes de localidade: localidade temporal O princípio
j
Localidade temporal (localidade no tempo): se um item é referenciado, ele tenderá a ser referenciado novamente em breve. Se você trouxe um livro à mesa para examiná-lo, é provável que precise examiná-lo novamente em breve.
j
Localidade espacial (localidade no espaço): se um item é referenciado, os itens cujos endereços estão próximos tenderão a ser referenciados em breve. Por exemplo, ao trazer o livro sobre os primeiros computadores ingleses para pesquisar sobre o EDSAC, você também percebeu que havia outro livro ao lado dele na estante sobre computadores mecânicos; então, resolveu trazer também esse livro, no qual, mais tarde, encontrou algo útil. Os livros sobre o mesmo assunto são colocados juntos na biblioteca para aumentar a localidade espacial. Veremos como a localidade espacial é usada nas hierarquias de memória um pouco mais adiante neste capítulo.
em que se um local de dados é referenciado, então, ele tenderá a ser referenciado novamente em breve.
localidade espacial O princípio da localidade em que, se um local de dados é referenciado, então, os dados com endereços próximos tenderão a ser referenciados em breve.
Assim como os acessos aos livros na estante exibem naturalmente a localidade, a localidade nos programas surge de estruturas de programa simples e naturais. Por exemplo, a maioria dos programas contém loops e, portanto, as instruções e os dados provavelmente são acessados de modo repetitivo, mostrando altas quantidades de localidade temporal. Como, em geral, as instruções são acessadas sequencialmente, os programas mostram alta localidade espacial. Os acessos a dados também exibem uma localidade espacial natural. Por exemplo, os acessos sequenciais aos elementos de um array ou de um registro terão altos índices de localidade espacial.
5.1 Introdução 365
Tiramos vantagem do princípio da localidade implementando a memória de um computador como uma hierarquia de memória. Uma hierarquia de memória consiste em múltiplos níveis de memória com diferentes velocidades e tamanhos. As memórias mais rápidas são mais caras por bit do que as memórias mais lentas e, portanto, são menores. Hoje, existem três tecnologias principais usadas na construção das hierarquias de memória. A memória principal é implementada por meio de DRAM (Dinamic Random Access Memory), enquanto os níveis mais próximos do processador (caches) usam SRAM (Static Random Access Memory). A DRAM é mais barata por bit do que a SRAM, embora seja substancialmente mais lenta. A diferença de preço ocorre porque a DRAM usa significativamente menos área por bit de memória e as DRAMs, portanto, têm maior capacidade para a mesma quantidade de silício; a diferença de velocidade ocorre devido a diversos fatores descritos na Seção C.9 do Apêndice C. A terceira tecnologia, usada para implementar o maior e mais lento nível na hierarquia, normalmente é o disco magnético. (A memória flash é usada no lugar dos discos em muitos dispositivos embutidos; veja Seção 6.4.) O tempo de acesso e o preço por bit variam muito entre essas tecnologias, como mostra a tabela a seguir, usando valores típicos em 2008: Tecnologia de memória
Tempo de acesso típico
US$ por GB em 2008
SRAM
0,5 a 2,5ns
2000 a 5.000
DRAM
50 a 70ns
20 a 75
Disco magnético
5.000.000 a 20.000.000ns
0,20 a 2
Devido a essas diferenças no custo e no tempo de acesso, é vantajoso construir memória como uma hierarquia de níveis. A Figura 5.1 mostra que a memória mais rápida está próxima do processador e a memória mais lenta e barata está abaixo dele. O objetivo é oferecer ao usuário o máximo de memória disponível na tecnologia mais barata, enquanto se fornece acesso na velocidade oferecida pela memória mais rápida. Da mesma forma, os dados são organizados como uma hierarquia: um nível mais próximo do processador em geral é um subconjunto de qualquer nível mais distante, e todos os dados são armazenados no nível mais baixo. Por analogia, os livros em sua mesa
FIGURA 5.1 A estrutura básica de uma hierarquia de memória. Implementando o sistema de memória como uma hierarquia, o usuário tem a ilusão de uma memória que é tão grande quanto o maior nível da hierarquia, mas pode ser acessada como se fosse totalmente construída com a memória mais rápida. A memória flash substituiu os discos em muitos dispositivos embutidos, e pode levar a um novo nível na hierarquia de armazenamento para computadores de desktop e servidor; veja Seção 6.4.
hierarquia de memória Uma estrutura que usa múltiplos níveis de memórias; conforme a distância da CPU aumenta, o tamanho das memórias e o tempo de acesso também aumentam.
366
bloco (ou linha) A unidade mínima de informação que pode estar presente ou ausente em uma cache.
taxa de acertos A proporção dos acessos à memória encontrados em um nível da hierarquia de memória.
taxa de falhas A proporção de acessos à memória não encontrados em um nível da hierarquia de memória. tempo de acerto O tempo necessário para acessar um nível da hierarquia de memória, incluindo o tempo necessário para determinar se o acesso é um acerto ou uma falha.
penalidade de falha O tempo necessário na busca de um bloco de um nível inferior para um nível superior da hierarquia de memória, incluindo o tempo para acessar o bloco, transmiti-lo de um nível a outro e inseri-lo no nível que experimentou a falha, e depois passar o bloco a quem o solicitou.
Capítulo 5 Grande e Rápida: Explorando a Hierarquia de Memória
formam um subconjunto da biblioteca onde você está trabalhando, que, por sua vez, é um subconjunto de todas as bibliotecas do campus. Além disso, conforme nos afastamos do processador, os níveis levam cada vez mais tempo para serem acessados, exatamente como poderíamos encontrar em uma hierarquia de bibliotecas de campus. Uma hierarquia de memória pode consistir em múltiplos níveis, mas os dados são copiados apenas entre dois níveis adjacentes ao mesmo tempo, de modo que podemos concentrar nossa atenção em apenas dois níveis. O nível superior – o que está mais perto do processador – é menor e mais rápido (já que usa tecnologia mais cara) do que o nível inferior. A Figura 5.2 mostra que a unidade de informação mínima que pode estar presente ou ausente na hierarquia de dois níveis é denominada um bloco ou uma linha; em nossa analogia da biblioteca, um bloco de informação seria um livro. Se os dados requisitados pelo processador aparecerem em algum bloco no nível superior, isso é chamado um acerto (análogo a encontrar a informação em um dos livros em sua mesa). Se os dados não forem encontrados no nível superior, a requisição é chamada uma falha. O nível inferior em uma hierarquia é, então, acessado para recuperar o bloco com os dados requisitados. (Continuando com nossa analogia, você vai da sua mesa até as estantes para encontrar o livro desejado.) A taxa de acertos é a fração dos acessos à memória encontrados no nível superior; ela normalmente é usada como uma medida do desempenho da hierarquia de memória. A taxa de falhas (1 – taxa de acertos) é a proporção dos acessos à memória não encontrados no nível superior. Como o desempenho é o principal objetivo de ter uma hierarquia de memória, o tempo para servir acertos e falhas é um aspecto importante. O tempo de acerto é o tempo para acessar o nível superior da hierarquia de memória, que inclui o tempo necessário para determinar se o acesso é um acerto ou uma falha (ou seja, o tempo necessário para consultar os livros na mesa). A penalidade de falha é o tempo de substituição de um bloco no nível superior pelo bloco correspondente do nível inferior, mais o tempo para transferir esse bloco ao processador (ou, o tempo de apanhar outro livro das estantes e colocá-lo na mesa). Como o nível superior é menor e construído usando partes de memória mais rápidas, o tempo de acerto será muito menor do que o tempo para acessar o próximo nível na hierarquia, que é o principal componente da penalidade de falha. (O tempo para examinar os livros na mesa é muito menor do que o tempo para se levantar e apanhar um novo livro nas estantes.) Como veremos neste capítulo, os conceitos usados para construir sistemas de memória afetam muitos outros aspectos de um computador, inclusive como o sistema operacional gerencia a memória e a E/S, como os compiladores geram código e mesmo como as aplicações usam o computador. É claro que, como todos os programas gastam muito do seu tempo acessando a memória, o sistema de memória é necessariamente um importante fator para se determinar o desempenho. A confiança nas hierarquias de memória para obter
FIGURA 5.2 Cada par de níveis na hierarquia de memória pode ser imaginado como tendo um nível superior e um nível inferior. Dentro de cada nível, a unidade de informação que está presente ou não é chamada de um bloco ou uma linha. Em geral, transferimos um bloco inteiro quando copiamos algo entre os níveis.
5.1 Introdução 367
desempenho tem indicado que os programadores (que costumavam pensar na memória como um dispositivo de armazenamento plano e de acesso aleatório) agora precisam entender as hierarquias de memória de modo a alcançarem um bom desempenho. Para mostrar como esse entendimento é importante, vamos fornecer alguns exemplos, como a Figura 5.18. Como os sistemas de memória são essenciais para o desempenho, os projetistas de computadores têm dedicado muita atenção a esses sistemas e desenvolvido sofisticados mecanismos voltados a melhorar o desempenho do sistema de memória. Neste capítulo, veremos as principais ideias conceituais, embora muitas simplificações e abstrações tenham sido usadas no sentido de manter o material praticável em tamanho e complexidade.
Os programas apresentam localidade temporal (a tendência de reutilizar itens de dados recentemente acessados) e localidade espacial (a tendência de referenciar itens de dados que estão próximos a outros itens recentemente acessados). As hierarquias de memória tiram proveito da localidade temporal mantendo mais próximos do processador os itens de dados acessados mais recentemente. As hierarquias de memória tiram proveito da localidade espacial movendo blocos consistindo em múltiplas palavras contíguas na memória para níveis superiores na hierarquia. A Figura 5.3 mostra que uma hierarquia de memória usa tecnologias de memória menores e mais rápidas perto do processador. Portanto, os acessos de acerto no nível mais alto da hierarquia podem ser processados rapidamente. Os acessos de falha vão para os níveis mais baixos da hierarquia, que são maiores, porém mais lentos. Se a taxa de acertos for bastante alta, a hierarquia de memória terá um tempo de acesso efetivo próximo ao tempo de acesso do nível mais alto (e mais rápido) e um tamanho igual ao do nível mais baixo (e maior). Na maioria dos sistemas, a memória é uma hierarquia verdadeira, o que significa que os dados não podem estar presentes no nível i a menos que também estejam presentes no nível i + 1.
FIGURA 5.3 Este diagrama mostra a estrutura de uma hierarquia de memória: conforme a distância do processador aumenta, o tamanho também aumenta. Essa estrutura, com os mecanismos de operação apropriados, permite que o processador tenha um tempo de acesso determinado principalmente pelo nível 1 da hierarquia e ainda tenha uma memória tão grande quanto o nível n. Manter essa ilusão é o assunto deste capítulo. Embora o disco local normalmente seja a parte inferior da hierarquia, alguns sistemas usam fita ou um servidor de arquivos numa rede local como os próximos níveis da hierarquia.
em
Colocando perspectiva
368
Verifique você mesmo
Capítulo 5 Grande e Rápida: Explorando a Hierarquia de Memória
Quais das seguintes afirmações normalmente são verdadeiras? 1. As caches tiram proveito da localidade temporal. 2. Em uma leitura, o valor retornado depende de quais blocos estão na cache. 3. A maioria do custo da hierarquia de memória está no nível mais alto. 4. A maioria da capacidade da hierarquia de memória está no nível mais baixo.
Cache: um lugar seguro para esconder ou guardar coisas. Webster's New World Dictionary of the American Language, Third College Edition (1988)
5.2 Princípios básicos de cache Em nosso exemplo da biblioteca, a mesa servia como uma cache – um lugar seguro para guardar coisas (livros) que precisávamos examinar. Cache foi o nome escolhido para representar o nível da hierarquia de memória entre o processador e a memória principal no primeiro computador comercial a ter esse nível extra. As memórias no caminho de dados, no Capítulo 4, são simplesmente substituídas por caches. Hoje, embora permaneça o uso dominante da palavra cache, o termo também é usado para referenciar qualquer armazenamento usado para tirar proveito da localidade de acesso. As caches apareceram inicialmente nos computadores de pesquisa no início da década de 1960 e nos computadores de produção mais tarde nessa mesma década; todo computador de uso geral construído hoje, dos servidores aos processadores embutidos de baixa capacidade, possui caches. Nesta seção, começaremos a ver uma cache muito simples na qual cada requisição do processador é uma palavra e os blocos também consistem em uma única palavra. (Os leitores que já estão familiarizados com os fundamentos de cache podem pular para a Seção 5.3.) A Figura 5.4 mostra essa cache simples, antes e depois de requisitar um item de dados que não está inicialmente na cache. Antes de requisitar, a cache contém uma coleção de referências recentes, X1, X2,… Xn-1, e o processador requisita uma palavra Xn que não está na cache. Essa requisição resulta em uma falha, e a palavra Xn é trazida da memória para a cache. Olhando o cenário na Figura 5.4, surgem duas perguntas a serem respondidas: como sabemos se o item de dados está na cache? Além disso, se estiver, como encontrá-lo? As respostas a essas duas questões estão relacionadas. Se cada palavra pode ficar exatamente em um lugar na cache, então, é fácil encontrar a palavra se ela estiver na cache. A maneira mais simples de atribuir um local na cache para cada palavra da memória é atribuir um local na cache baseado no endereço da palavra na memória. Essa estrutura de cache é chamada
FIGURA 5.4 A cache, imediatamente antes e após uma referência a uma palavra Xn que não está inicialmente na cache. Essa referência causa uma falha que força a cache a buscar Xn na memória e inseri-la na cache.
5.2 Princípios básicos de cache 369
de mapeamento direto, já que cada local da memória é mapeado diretamente para um local exato na cache. O mapeamento típico entre endereços e locais de cache para uma cache diretamente mapeada é simples. Por exemplo, quase todas as caches diretamente mapeadas usam o mapeamento
mapeamento direto Uma estrutura de cache em que cada local da memória é mapeado exatamente para um local na cache.
(Endereçode bloco)módulo(Númerode blocos decache na cache) Se o número de entradas na cache for uma potência de dois, então, o módulo pode ser calculado simplesmente usando os log2 bits menos significativos (tamanho da cache em blocos); assim, a cache pode ser acessada diretamente com os bits menos significativos. Por exemplo, a Figura 5.5 mostra como os endereços de memória entre 1dec (00001bin) e 29dec (11101bin) são mapeados para as posições 1dec (001bin) e 5dec (101bin) em uma cache diretamente mapeada de oito palavras. Como cada local da cache pode armazenar o conteúdo de diversos locais diferentes da memória, como podemos saber se os dados na cache correspondem a uma palavra requisitada? Ou seja, como sabemos se uma palavra requisitada está na cache ou não? Respondemos a essa pergunta incluindo um conjunto de tags na cache. As tags contêm as informações de endereço necessárias para identificar se uma palavra na cache corresponde à palavra requisitada. A tag precisa apenas conter a parte superior do endereço, correspondente aos bits que não são usados como índice para a cache. Por exemplo, na Figura 5.5, precisamos apenas ter os dois bits mais significativos dos cinco bits de endereço na tag, já que o campo índice com os três bits menos significativos do endereço seleciona o bloco. Os arquitetos omitem os bits de índice porque eles são redundantes, uma vez que, por definição, o campo índice de cada endereço precisa ter o mesmo valor. Também precisamos de uma maneira de reconhecer se um bloco de cache não possui informações válidas. Por exemplo, quando um processador é iniciado, a cache não tem dados válidos, e os campos de tag não terão significado. Mesmo após executar muitas instruções, algumas entradas de cache podem ainda estar vazias, como na Figura 5.4. Portanto, precisamos saber se a tag deve ser ignorada para essas entradas. O método
FIGURA 5.5 Uma cache diretamente mapeada com oito entradas mostrando os endereços das palavras de memória entre 0 e 31 que são mapeadas para os mesmos locais de cache. Como há oito palavras na cache, um endereço X é mapeado para a palavra de cache X módulo 8. Ou seja, os log2(8) = 3 bits menos significativos são usados como o índice da cache. Assim, os endereços 00001bin, 01001bin, 10001bin e 11001bin são todos mapeados para a entrada 001bin da cache, enquanto os endereços 00101bin, 01101bin, 10101bin e 11101bin são todos mapeados para a entrada 101bin da cache.
tag Um campo em uma tabela usado para uma hierarquia de memória que contém as informações de endereço necessárias para identificar se o bloco associado na hierarquia corresponde a uma palavra requisitada.
370
Capítulo 5 Grande e Rápida: Explorando a Hierarquia de Memória
bit de validade Um campo nas
mais comum é incluir um bit de validade indicando se uma entrada contém um endereço válido. Se o bit não estiver ligado, não pode haver uma correspondência para esse bloco. No restante desta seção, vamos nos concentrar em explicar como uma cache trata das leituras. Em geral, a manipulação de leituras é um pouco mais simples do que a manipulação de escritas, já que as leituras não precisam mudar o conteúdo da cache. Após vermos os aspectos básicos de como as leituras funcionam e como as falhas de cache podem ser tratadas, examinaremos os projetos de cache para computadores reais e detalharemos como essas caches manipulam as escritas.
tabelas de uma hierarquia de memória que indica que o bloco associado na hierarquia contém dados válidos.
Acessando uma cache A seguir, vemos uma sequência de nove referências da memória a uma cache vazia de oito blocos, incluindo a ação para cada referência. A Figura 5.6 mostra como o conteúdo da cache muda em cada falha. Como há oito blocos na cache, os três bits menos significativos de um endereço fornecem o número do bloco: Endereço decimal da referência
Endereço binário da referência
Acerto ou falha na cache
Bloco de cache atribuído (onde foi encontrado ou inserido)
22
10110bin
falha (7.6b)
(10110bin mod 8) = 110bin
26
11010bin
falha (7.6c)
(11010bin mod 8) = 010bin
22
10110bin
acerto
(10110bin mod 8) = 110bin
26
11010bin
acerto
(11010bin mod 8) = 010bin
16
10000bin
falha (7.6d)
(10000bin mod 8) = 000bin (00011bin mod 8) = 011bin
3
00011bin
falha (7.6e)
16
10000bin
acerto
(10000bin mod 8) = 000bin
18
10010bin
falha (7.6f)
(10010bin mod 8) = 010bin
16
10000bin
acerto
(10000bin mod 8) = 000bin
Como a cache está vazia, várias das primeiras referências são falhas; a legenda da Figura 5.6 descreve as ações de cada referência à memória. Na oitava referência, temos demandas em conflito para um bloco. A palavra no endereço 18 (10010bin) deve ser trazida para o bloco de cache 2 (010bin). Logo, ela precisa substituir a palavra no endereço 26 (11010bin), que já está no bloco de cache 2 (010bin). Esse comportamento permite que uma cache tire proveito da localidade temporal: palavras recentemente acessadas substituem palavras menos referenciadas recentemente. Essa situação é análoga a precisar de um livro da estante e não ter mais espaço na mesa para colocá-lo – algum livro que já esteja na sua mesa precisa ser devolvido à estante. Em uma cache diretamente mapeada, há apenas um lugar para colocar o item recém-requisitado e, portanto, apenas uma escolha do que substituir. Agora, sabemos onde olhar na cache para cada endereço possível: os bits menos significativos de um endereço podem ser usados para encontrar a entrada de cache única para a qual o endereço poderia ser mapeado. A Figura 5.7 mostra como um endereço referenciado é dividido em: j
um campo tag, usado para ser comparado com o valor do campo tag da cache;
j
um índice de cache, usado para selecionar o bloco.
O índice de um bloco de cache, juntamente com o conteúdo da tag desse bloco, especifica de modo único o endereço de memória da palavra contida no bloco de cache. Como o campo índice é usado como um endereço para acessar a cache e como um campo de n bits possui 2n valores, o número total de entradas em uma cache diretamente mapeada será uma potência de dois. Na arquitetura MIPS, uma vez que as palavras são alinhadas como
5.2 Princípios básicos de cache 371
FIGURA 5.6 O conteúdo da cache é mostrado para cada requisição de referência que falha, com os campos índice e tag mostrados em binário para a sequência de endereços na página 372. A cache inicialmente está vazia, com todos os bits de validade (entrada V da cache) inativos (N). O processador requisita os seguintes endereços: 10110bin (falha), 11010bin (falha), 10110bin (acerto), 11010bin (acerto), 10000bin (falha), 00011bin (falha), 10000bin (acerto) e 10010bin (falha). As figuras mostram o conteúdo da cache após cada falha na sequência ter sido tratada. Quando o endereço 10010bin (18) é referenciado, a entrada para o endereço 11010bin (26) precisa ser substituída, e uma referência a 11010bin causará uma falha subsequente. O campo tag conterá apenas a parte superior do endereço. O endereço completo de uma palavra contida no bloco de cache i com o campo tag j para essa cache é j × 8 + i ou, de forma equivalente, a concatenação do campo tag j e o campo índice i. Por exemplo, na cache f anterior, o índice 010bin possui tag 10bin e corresponde ao endereço 10010bin.
múltiplos de 4 bytes, os dois bits menos significativos de cada endereço especificam um byte dentro de uma palavra e, portanto, são ignorados ao selecionar uma palavra no bloco. O número total de bits necessários para uma cache é uma função do tamanho da cache e do tamanho do endereço, pois a cache inclui o armazenamento para os dados e as tags. O tamanho do bloco mencionado anteriormente era de uma palavra, mas normalmente é de várias palavras. Para as situações a seguir: j
Endereços em bytes de 32 bits.
j
Uma cache diretamente mapeada.
j
O tamanho da cache é 2n blocos, de modo que n bits são usados para o índice.
372
Capítulo 5 Grande e Rápida: Explorando a Hierarquia de Memória
FIGURA 5.7 Para esta cache, a parte inferior do endereço é usada para selecionar uma entrada de cache consistindo em uma palavra de dados e uma tag. Essa cache mantém 1024 palavras ou 4 KB. Consideramos endereços de 32 bits neste capítulo. A tag da cache é comparada com a parte superior do endereço para determinar se a entrada na cache corresponde ao endereço requisitado. Como a cache tem 210 (ou 1024) palavras e um tamanho de bloco de 1 palavra, 10 bits são usados para indexar a cache, deixando 32 – 10 – 2 = 20 bits para serem comparados com a tag. Se a tag e os 20 bits superiores do endereço forem iguais e o bit de validade estiver ligado, então, a requisição é um acerto na cache e a palavra é fornecida para o processador. Caso contrário, ocorre uma falha.
j
O tamanho do bloco é 2m palavras (2m+2 bytes), de modo que m bits são usados para a palavra dentro do bloco, e dois bits são usados para a parte de byte do endereço
o tamanho do campo de tag é 32 − (n + m + 2). O número total de bits em uma cache diretamente mapeada é 2n × (tamanhodo bloco + tamanhodo tag + tamanhodocampode validade). Como o tamanho do bloco é 2m palavras (2m+5 bits) e precisamos de 1 bit para o campo de validade, o número de bits nessa cache é 2n × (2m × 32 + (32 − n − m − 2) + 1) = 2n × (2m × 32 + 31 − n − m). Embora esse seja o tamanho real em bits, a convenção de nomeação é excluir o tamanho da tag e do campo de validade e contar apenas o tamanho dos dados. Assim, a cache na Figura 5.7 é chamada de cache de 4 KB.
5.2 Princípios básicos de cache 373
Bits em uma cache
Quantos bits no total são necessários para uma cache diretamente mapeada com 16KB de dados e blocos de 4 palavras, considerando um endereço de 32 bits? Sabemos que 16KB são 4K palavras, o que equivale a 212 palavras e, com um tamanho de bloco de 4 palavras (22), há 210 blocos. Cada bloco possui 4 × 32, ou 128 bits de dados mais uma tag, que é 32 – 10 – 2 – 2 bits, mais um bit de validade. Portanto, o tamanho de cache total é 210 × (4 × 32 + (32 − 10 − 2 − 2) + 1) = 210 × 147 = 147 Kbits
EXEMPLO RESPOSTA
ou 18,4KB para uma cache de 16KB. Para essa cache, o número total de bits na cache é aproximadamente 1,15 vezes o necessário apenas para o armazenamento dos dados.
Mapeando um endereço para um bloco de cache multipalavra
Considere uma cache com 64 blocos e um tamanho de bloco de 16 bytes. Para qual número de bloco o endereço em bytes 1200 é mapeado?
EXEMPLO
A fórmula foi vista no início da Seção 5.2. O bloco é dado por (Endereçodo bloco)módulo(Númerode blocosdecache) Em que o endereço do bloco é Endereçoem bytes Bytes por bloco Observe que esse endereço de bloco é o bloco contendo todos os endereços entre Endereçoem bytes × Bytes por bloco Bytes por bloco e Endereçoem bytes × Bytes por bloco + (Bytes por bloco − 1) Bytes por bloco Portanto, com 16 bytes por bloco, o endereço em bytes 1200 é o endereço de bloco 1200 = 75 6 que é mapeado para o número de bloco de cache (75 módulo 64) = 11. Na verdade, esse bloco mapeia todos os endereços entre 1200 e 1215. Blocos maiores exploram a localidade espacial para diminuir as taxas de falhas. Como mostra a Figura 5.8, aumentar o tamanho de bloco normalmente diminui a taxa de falhas. A taxa de falhas pode subir posteriormente se o tamanho de bloco se tornar uma fração significativa do tamanho de cache, uma vez que o número de blocos que pode ser armazenado na cache se tornará pequeno e haverá uma grande competição entre esses blocos. Como resultado, um bloco será retirado da cache antes que muitas de suas palavras
RESPOSTA
374
Capítulo 5 Grande e Rápida: Explorando a Hierarquia de Memória
FIGURA 5.8 Taxa de falhas versus tamanho de bloco. Note que a taxa de falhas realmente sobe se o tamanho de bloco for muito grande em relação ao tamanho da cache. Cada linha representa uma cache de tamanho diferente. (Esta figura é independente da associatividade, que será discutida em breve.) Infelizmente, os traces do SPEC2000 levariam tempo demais se o tamanho de bloco fosse incluído; portanto, esses dados são baseados no SPEC92.
sejam acessadas. Explicando de outra forma: a localidade espacial entre as palavras em um bloco diminui com um bloco muito grande; por conseguinte, os benefícios na taxa de falhas se tornam menores. Um problema mais sério associado a apenas aumentar o tamanho de bloco é que o custo de uma falha aumenta. A penalidade de falha é determinada pelo tempo necessário para buscar o bloco do próximo nível mais baixo na hierarquia e carregá-lo na cache. O tempo para buscar o bloco possui duas partes: a latência até a primeira palavra e o tempo de transferência para o restante do bloco. Claramente, a menos que mudemos o sistema de memória, o tempo de transferência – e, portanto, a penalidade de falha – aumentará conforme o tamanho de bloco aumenta. Além disso, o aumento na taxa de falhas começa a decrescer conforme os blocos se tornam maiores. O resultado é que o aumento na penalidade de falha suplanta o decréscimo na taxa de falhas para grandes blocos, diminuindo, assim, o desempenho da cache. Naturalmente, se projetarmos a memória para transferir blocos maiores de forma mais eficiente, poderemos aumentar o tamanho do bloco e obter mais melhorias no desempenho da cache. Discutiremos esse assunto na próxima seção. Detalhamento: Embora seja difícil fazer algo sobre o componente de latência mais longo da penalidade de falha para blocos grandes, podemos ser capazes de ocultar um pouco do tempo de transferência de modo que a penalidade de falha seja efetivamente menor. O método mais simples de fazer isso, chamado reinício precoce, é simplesmente retomar a execução assim que a palavra requisitada do bloco seja retornada, em vez de esperar o bloco inteiro. Muitos processadores usam essa técnica para acesso a instruções, que é onde ela funciona melhor. Como os acessos a instruções são extremamente sequenciais, se o sistema de memória puder entregar uma palavra a cada ciclo de clock, o processador poderá ser capaz de reiniciar sua operação quando a palavra requisitada for retornada, com o sistema de memória entregando novas palavras de instrução em tempo. Essa técnica normalmente é menos eficaz para caches de dados porque é provável que as palavras sejam requisitadas do bloco de uma maneira menos previsível; além disso, a probabilidade de que o processador precise de outra palavra de um bloco de cache diferente antes que a transferência seja concluída é alta. Se o processador não puder acessar a cache de dados porque uma transferência está em andamento, então, ele precisará sofrer stall. Um esquema ainda mais sofisticado é organizar a memória de modo que a palavra requisitada seja transferida da memória para a cache primeiro. O restante do bloco, então, é transferido, começando com o endereço após a palavra requisitada e retornando para o início do bloco.
5.2 Princípios básicos de cache 375
Essa técnica, chamada palavra requisitada primeiro, ou palavra crítica primeiro, pode ser um pouco mais rápida do que o reinício precoce, mas ela é limitada pelas mesmas propriedades que limitam o reinício precoce.
Tratando falhas de cache Antes de olharmos a cache de um sistema real, vamos ver como a unidade de controle lida com as falhas de cache. (Descrevemos um controlador de cache na Seção 5.7). A unidade de controle precisa detectar uma falha de cache e processá-la buscando os dados requisitados da memória (ou, como veremos, de uma cache de nível inferior). Se a cache reportar um acerto, o computador continua usando os dados como se nada tivesse acontecido. Modificar o controle de um processador para tratar um acerto é fácil; as falhas, no entanto, exigem um trabalho maior. O tratamento da falha de cache é feito com a unidade de controle do processador e com um controlador separado que inicia o acesso à memória e preenche novamente a cache. O processamento de uma falha de cache cria um stall semelhante aos stalls de pipeline (Capítulo 4), como oposto a uma interrupção, que exigiria salvar o estado de todos os registradores. Para uma falha de cache, podemos fazer um stall no processador inteiro, basicamente congelando o conteúdo dos registradores temporários e visíveis ao programador, enquanto esperamos a memória. Processadores fora de ordem mais sofisticados podem permitir a execução de instruções enquanto se espera por uma falha de cache, mas vamos considerar nesta seção os processadores em ordem, que fazem um stall nas perdas de cache. Vejamos um pouco mais de perto como as falhas de instrução são tratadas; o mesmo método pode ser facilmente estendido para tratar falhas de dados. Se um acesso à instrução resultar em uma falha, o conteúdo do registrador de instrução será inválido. Para colocar a instrução correta na cache, precisamos ser capazes de instruir o nível inferior na hierarquia de memória a realizar uma leitura. Como o contador do programa é incrementado no primeiro ciclo de clock da execução, o endereço da instrução que gera uma falha de cache de instruções é igual ao valor do contador de programa menos 4. Uma vez tendo o endereço, precisamos instruir a memória principal a realizar uma leitura. Esperamos a memória responder (já que o acesso levará vários ciclos) e, então, escrevemos as palavras na cache. Agora, podemos definir as etapas a serem realizadas em uma falha de cache de instruções: 1. Enviar o valor do PC original (PC atual – 4) para a memória. 2. Instruir a memória principal a realizar uma leitura e esperar que a memória complete seu acesso. 3. Escrever na entrada da cache, colocando os dados da memória na parte dos dados da entrada, escrevendo os bits mais significativos do endereço (vindo da ALU) no campo tag e ligando o bit de validade. 4. Reiniciar a execução da instrução na primeira etapa, o que buscará novamente a instrução, desta vez encontrando-a na cache. O controle da cache sobre um acesso de dados é basicamente idêntico: em uma falha, simplesmente suspendemos o processador até que a memória responda com os dados.
Tratando escritas As escritas funcionam de maneira um pouco diferente. Suponha que, em uma instrução store, escrevemos os dados apenas na cache de dados (sem alterar a memória principal); então, após a escrita na cache, a memória teria um valor diferente do valor na cache. Nesse caso, dizemos que a cache e a memória estão inconsistentes. A maneira mais simples de
falha de cache Uma requisição de dados da cache que não pode ser atendida porque os dados não estão presentes na cache.
376
write-through Um esquema em que as escritas sempre atualizam a cache e o próximo nível inferior da hierarquia de memória, garantindo que os dados sejam sempre consistentes entre os dois.
buffer de escrita Uma fila que contém os dados enquanto estão esperando para serem escritos na memória.
write-back Um esquema que manipula escritas atualizando valores apenas no bloco da cache e, depois, escrevendo o bloco modificado no nível inferior da hierarquia quando o bloco é substituído.
Capítulo 5 Grande e Rápida: Explorando a Hierarquia de Memória
manter consistentes a memória principal e a cache é sempre escrever os dados na memória e na cache. Esse esquema é chamado write-through. O outro aspecto importante das escritas é o que ocorre em uma falha de dados. Primeiro, buscamos as palavras do bloco da memória. Após o bloco ser buscado e colocado na cache, podemos substituir (sobrescrever) a palavra que causou a falha no bloco de cache. Também escrevemos a palavra na memória principal usando o endereço completo. Embora esse projeto trate das escritas de maneira muito simples, ele não oferece um desempenho muito bom. Com um esquema de write-through, toda escrita faz com que os dados sejam escritos na memória principal. Essas escritas levarão muito tempo, talvez mais de 100 ciclos de clock de processador, e tornariam o processador consideravelmente mais lento. Por exemplo, suponha que 10% das instruções sejam stores. Se o CPI sem falhas de cache fosse 1,0, gastar 100 ciclos extras em cada escrita levaria a um CPI de 1,0 + 100 × 10% = 11, reduzindo o desempenho por um fator maior que 10. Uma solução para esse problema é usar um buffer de escrita (ou write buffer), que armazena os dados enquanto estão esperando para serem escritos na memória. Após escrever os dados na cache e no buffer de dados, o processador pode continuar a execução. Quando uma escrita na memória principal é concluída, a entrada no buffer de escrita é liberada. Se o buffer de escrita estiver cheio quando o processador atingir uma escrita, o processador precisará sofrer stall até que haja uma posição vazia no buffer de escrita. Naturalmente, se a velocidade em que a memória pode completar escritas for menor do que a velocidade em que o processador está gerando escritas, nenhuma quantidade de buffer pode ajudar, pois as escritas estão sendo geradas mais rápido do que o sistema de memória pode aceitá-las. A velocidade em que as escritas são geradas também pode ser menor do que a velocidade em que a memória pode aceitá-las, e stalls ainda podem ocorrer. Isso pode acontecer quando as escritas ocorrem em bursts (ou rajadas). Para reduzir a ocorrência desses stalls, os processadores normalmente aumentam a profundidade do buffer de escrita para além de uma única entrada. A alternativa para um esquema write-through é um esquema chamado write-back, no qual, quando ocorre uma escrita, o novo valor é escrito apenas no bloco da cache. O bloco modificado é escrito no nível inferior da hierarquia quando ele é substituído. Os esquemas write-back podem melhorar o desempenho, especialmente quando os processadores podem gerar escritas tão rápido ou mais rápido do que as escritas podem ser tratadas pela memória principal; entretanto, um esquema write-back é mais complexo de implementar do que um esquema write-through. No restante desta seção, descreveremos as caches de processadores reais e examinaremos como elas tratam leituras e escritas. Na Seção 5.5, descreveremos o tratamento de escritas em mais detalhes. Detalhamento: As escritas introduzem várias complicações nas caches que não estão presentes para leituras. Discutiremos aqui duas delas: a política nas falhas de escrita e a implementação eficiente das escritas em caches write-back. Considere uma falha em uma cache write-through. A estratégia mais comum é alocar um bloco no cache, chamado alocar na escrita. O bloco é apanhado da memória e depois a parte apropriada do bloco é sobrescrita. Uma estratégia alternativa é atualizar a parte do bloco na memória, mas não colocá-la no cache, o que se chama não alocar na escrita. A motivação é que às vezes os programas escrevem blocos de dados, como quando o sistema operacional zera uma página de memória. Nesses casos, a busca associada com a falha de escrita inicial pode ser desnecessária. Alguns computadores permitem que a política de alocação de escrita seja alterada com base em cada página. Implementar stores de modo realmente eficaz em uma cache que usa uma estratégia write-back é mais complexo do que em uma cache write-through. Uma cache write-through pode escrever os dados na cache e ler a tag; se a tag for diferente, então haverá uma falha. Como o cache é write-through, a substituição do bloco no cache não é catastrófica, pois a memória tem o valor correto. Em uma cache write-back, precisamos escrever o bloco novamente na memória se os dados na cache estiverem modificados e tivermos uma falha de cache. Se simplesmente substituíssemos o bloco em uma instrução store antes de sabermos se o store teve acerto na cache (como poderíamos fazer para uma cache write-through), destruiríamos o conteúdo do bloco, que não é copiado no próximo nível da hierarquia da memória.
5.2 Princípios básicos de cache 377
Em uma cache write-back, como não podemos substituir o bloco, os stores ou exigem dois ciclos (um ciclo para verificar um acerto seguido de um ciclo para efetivamente realizar a escrita) ou exigem um buffer de escrita para conter esses dados – na prática, permitindo que o store leve apenas um ciclo por meio de um pipeline de memória. Quando um buffer de store é usado, o processador realiza a consulta de cache e coloca os dados no buffer de store durante o ciclo de acesso de cache normal. Considerando um acerto de cache, os novos dados são escritos do buffer de store para a cache no próximo ciclo de acesso de cache não usado. Por comparação, em uma cache write-through, as escritas sempre podem ser feitas em um ciclo. Lemos a tag e escrevemos a parte dos dados do bloco selecionado. Se a tag corresponder ao endereço do bloco escrito, o processador pode continuar normalmente, já que o bloco correto foi atualizado. Se a tag não corresponder, o processador gera uma falha de escrita para buscar o resto do bloco correspondente a esse endereço. Muitas caches write-back também incluem buffers de escrita usados para reduzir a penalidade de falha quando uma falha substitui um bloco modificado. Em casos como esse, o bloco modificado é movido para um buffer write-back associado com a cache enquanto o bloco requisitado é lido da memória. Depois, o buffer write-back é escrito novamente na memória. Considerando que outra falha não ocorra imediatamente, essa técnica reduz à metade a penalidade de falha quando um bloco modificado precisa ser substituído.
Uma cache de exemplo: o processador Intrinsity FastMATH O Intrinsity FastMATH é um microprocessador embutido veloz que usa a arquitetura MIPS e uma implementação de cache simples. Próximo ao final do capítulo, examinaremos o projeto de cache mais complexo do AMD Opteron X4 (Barcelona), mas começaremos com este exemplo simples, mas real, por questões didáticas. A Figura 5.9 mostra a organização da cache de dados do Intrinsity FastMATH.
FIGURA 5.9 Cada cache de 16KB no Intrinsity FastMATH contém 256 blocos com 16 palavras por bloco. O campo tag possui 18 bits de largura, e o campo índice possui 8 bits de largura, enquanto um campo de 4 bits (bits 5 a 2) é usado para indexar o bloco e selecionar a palavra do bloco usando um multiplexador de 16 para 1. Na prática, para eliminar o multiplexador, as caches usam uma RAM grande separada para os dados e uma RAM menor para as tags, com o offset de bloco fornecendo os bits de endereço extras para a RAM grande de dados. Nesse caso, a RAM grande possui 32 bits de largura e precisa ter 16 vezes o número de palavras como blocos na cache.
378
Capítulo 5 Grande e Rápida: Explorando a Hierarquia de Memória
Esse processador possui um pipeline de 12 estágios, semelhante ao discutido no Capítulo 4. Quando está operando na velocidade de pico, o processador pode requisitar uma palavra de instrução e uma palavra de dados em cada clock. Para satisfazer às demandas do pipeline sem stalls, são usadas caches de instruções e de dados separadas. Cada cache possui 16KB, ou 4K palavras, com blocos de 16 palavras. As requisições de leitura para a cache são simples. Como existem caches de dados e de instruções separadas, sinais de controle separados serão necessários para ler e escrever em cada cache. (Lembre-se de que precisamos atualizar a cache de instruções quando ocorre uma falha.) Portanto, as etapas para uma requisição de leitura para qualquer uma das caches são as seguintes: 1. Enviar o endereço à cache apropriada. O endereço vem do PC (para uma instrução) ou da ALU (para dados). 2. Se a cache sinalizar acerto, a palavra requisitada estará disponível nas linhas de dados. Como existem 16 palavras no bloco desejado, precisamos selecionar a palavra correta. Um campo índice de bloco é usado para controlar o multiplexador (mostrado na parte inferior da figura), que seleciona a palavra requisitada das 16 palavras do bloco indexado. 3. Se a cache sinalizar falha, enviaremos o endereço para a memória principal. Quando a memória retorna com os dados, nós os escrevemos na cache e, então, os lemos para atender à requisição. Para escritas, o Intrinsity FastMATH oferece write-through e write-back, deixando a cargo do sistema operacional decidir qual estratégia usar para cada aplicação. Ele possui um buffer de escrita de uma entrada.
FIGURA 5.10 Taxas de falhas de instruções e dados aproximadas para o processador Intrinsity FastMATH para benchmarks SPEC2000. A taxa de falhas combinada é a taxa de falhas efetiva para a combinação da cache de instruções de 16KB e da cache de dados de 16KB. Ela é obtida ponderando as taxas de falhas individuais de instruções e de dados pela frequência das referências a instruções e dados.
Que taxas de falhas de cache são atingidas com uma estrutura de cache como a usada pelo Intrinsity FastMATH? A Figura 5.10 mostra as taxas de falhas para as caches de instruções e de dados. A taxa de falhas combinada é a taxa de falhas efetiva por referência para cada programa após considerar a frequência diferente dos acessos a instruções e a dados. Embora a taxa de falhas seja uma característica importante dos projetos de cache, a medida decisiva será o efeito do sistema de memória sobre o tempo de execução do programa; em breve veremos como a taxa de falhas e o tempo de execução estão relacionados. cache dividida Um esquema em que um nível da hierarquia de memória é composto de duas caches independentes que operam em paralelo uma com a outra, com uma tratando instruções e a outra tratando dados.
Detalhamento: Uma cache combinada com um tamanho total igual à soma das duas caches divididas normalmente terá uma taxa de acertos melhor. Essa taxa mais alta ocorre porque a cache combinada não divide rigidamente o número de entradas que podem ser usadas por instruções daquelas que podem ser usadas por dados. Entretanto, muitos processadores usam uma instrução split e uma cache de dados para aumentar a largura de banda da cache. (Também pode haver menos falhas de conflito; veja Seção 5.5.) Aqui estão taxas de falhas para caches do tamanho dos encontrados no processador Intrinsity FastMATH, e para uma cache combinada cujo tamanho é igual ao total das duas caches: j Tamanho total da cache: 32KB. j Taxa de falhas efetiva da cache dividida: 3,24%. j Taxa de falhas da cache combinada: 3,18%.
5.2 Princípios básicos de cache 379
A taxa de falhas da cache dividida é apenas ligeiramente pior. A vantagem de dobrar a largura de banda da cache, suportando acessos a instruções e a dados simultaneamente, logo suplanta a desvantagem de uma taxa de falhas um pouco maior. Essa constatação é outro lembrete de que não podemos usar a taxa de falhas como a única medida de desempenho de cache, como mostra a Seção 5.3.
Projetando o sistema de memória para suportar caches As falhas de cache são satisfeitas pela memória principal, que é construída com DRAMs. Na Seção 5.1, vimos que as DRAMs são projetadas com a principal ênfase no custo e na densidade. Embora seja difícil reduzir a latência para buscar a primeira palavra da memória, podemos reduzir a penalidade de falha se aumentarmos a largura de banda da memória para a cache. Essa redução permite que tamanhos de bloco maiores sejam usados enquanto mantemos uma baixa penalidade de falhas, semelhante àquela para um bloco menor. O processador normalmente é conectado à memória por meio de um barramento. (Como veremos no Capítulo 6, essa tradição está mudando, mas a tecnologia de interconexão real não importa neste capítulo, e por isso usaremos o termo barramento.) A velocidade de clock do barramento geralmente é muito mais lenta do que a do processador. A velocidade desse barramento afeta a penalidade de falha. Para entender o impacto das diferentes organizações de memória, vamos definir um conjunto hipotético de tempos de acesso à memória. Considere: j
1 ciclo de clock de barramento de memória para enviar o endereço;
j
15 ciclos de clock de barramento de memória para cada acesso a DRAM iniciado;
j
1 ciclo de clock de barramento de memória para enviar uma palavra de dados.
Se tivermos um bloco de cache de quatro palavras e um banco de DRAMs com a largura de uma palavra, a penalidade de falha seria 1 + 4 × 15 + 4 × 1 = 65 ciclos de clock de barramento de memória. Portanto, o número de bytes transferidos por ciclo de clock de barramento para uma única falha seria 4×4 = 0‚25 65 A Figura 5.11 mostra três opções para projetar o sistema de memória. A primeira delas segue o que temos considerado: a memória possui uma palavra de largura, e todos os acessos são feitos sequencialmente. A segunda opção aumenta a largura de banda para a memória alargando a memória e os barramentos entre o processador e a memória; isso permite acessos paralelos a todas as palavras do bloco. A terceira opção aumenta a largura de banda alargando a memória, mas não o barramento de interconexão. Portanto, ainda pagamos um custo para transmitir cada palavra, mas podemos evitar pagar o custo da latência de acesso mais de uma vez. Vejamos em quanto essas outras duas opções melhoram a penalidade de falha de 65 ciclos que veríamos para a primeira opção (Figura 5.11a). Aumentar a largura da memória e do barramento aumentará a largura de banda da memória proporcionalmente, diminuindo as partes do tempo de acesso e do tempo de transferência da penalidade de falha. Com uma largura de memória principal de duas palavras, a penalidade de falha cai de 65 ciclos de clock de barramento de memória para 1 + (2 15) + 2 1 = 33 ciclos de clock de barramento de memória. A largura de banda para uma única falha é, então, 0,48 (quase duas vezes maior) byte por ciclo de clock de barramento para uma memória que tem duas palavras de largura. Os maiores custos dessa melhoria são o barramento mais largo e o possível aumento no tempo de acesso da cache devido ao multiplexador e à lógica de controle entre o processador e a cache. Em vez de tornar todo o caminho entre a memória e a cache mais largo, os chips de memória podem ser organizados em bancos para ler ou escrever múltiplas palavras em
380
Capítulo 5 Grande e Rápida: Explorando a Hierarquia de Memória
FIGURA 5.11 O principal método para obter largura de banda de memória mais alta é aumentar a largura física ou lógica do sistema de memória. Nesta figura, a largura de banda da memória é melhorada de duas maneiras. O projeto mais simples (a) usa uma memória na qual todos os componentes possuem uma palavra de largura; (b) mostra uma memória, um barramento e uma cache mais largos; enquanto (c) mostra um barramento e uma cache mais estreitos com uma memória intercalada. Em (b), a lógica entre a cache e o processador consiste em um multiplexador usado em leituras e lógica de controle para atualizar as palavras apropriadas da cache nas escritas.
um único tempo de acesso em vez de ler ou escrever uma única palavra em cada vez. Cada banco poderia ter uma palavra de largura para que a largura do barramento e da cache não precisassem mudar, mas enviar um endereço para vários bancos permite que todos eles leiam simultaneamente. Esse esquema, chamado de intercalação (interleaving), conserva a vantagem de incorrer a latência de memória completa apenas uma vez. Por exemplo, com quatro bancos, o tempo para obter um bloco de quatro palavras consistiria em um ciclo para transmitir o endereço e a requisição de leitura para os bancos, 15 ciclos para que todos os quatro bancos acessem a memória e quatro ciclos para enviar as quatro palavras de volta à cache. Isso produz uma penalidade de falha de 1 + (1 × 15) + 4 × 1 = 20 ciclos de clock de barramento de memória. Essa é uma largura de banda efetiva por falha de 0,80 byte por clock, ou cerca de três vezes a largura de banda para a memória e barramento de uma palavra de largura. Os bancos também são valiosos nas escritas. Cada banco pode escrever independentemente, quadruplicando a largura de banda de escrita e gerando menos stalls em uma cache write-through. Como veremos, uma estratégia alternativa para escritas torna a intercalação ainda mais atraente. Devido à onipresença das caches e ao desejo de tamanhos de bloco maiores, os fabricantes de DRAM fornecem um acesso em rajada aos dados de locais sequenciais na DRAM. O desenvolvimento mais recente são as DRAMs Double Data Rate (DDR). O nome significa que os dados são transferidos tanto na transição de subida quanto na transição de descida do clock, gerando, assim, o dobro da largura de banda que se poderia esperar com base na velocidade de clock e na largura de dados. Para oferecer uma largura de banda tão grande, a DRAM interna é organizada em bancos de memória intercalados.
5.2 Princípios básicos de cache 381
A vantagem dessas otimizações é que elas usam os circuitos já presentes amplamente nas DRAMs, adicionando pouco custo ao sistema enquanto atinge uma significativa melhoria na largura de banda. A arquitetura interna das DRAMs e como essas otimizações são implementadas são descritos na Seção C.9 do Apêndice C. Detalhamento: Os chips de memória são organizados para produzir vários bits de saída, normalmente de 4 a 32, sendo que 16 era o mais comum em 2008. Descrevemos a organização da RAM como d × w, onde d é o número dos locais endereçáveis (a profundidade), e w é a saída (ou a largura de cada local). As DRAMs são organizadas logicamente como arrays retangulares, e o tempo de acesso é dividido em acesso de linha e acesso de coluna. As DRAMs colocam uma linha de bits em um buffer. As transferências em rajada permitem acessos repetidos ao buffer sem um tempo de acesso de linha. O buffer atua como uma SRAM; mudando o endereço de coluna, bits aleatórios podem ser acessados no buffer até o próximo acesso de linha. Essa capacidade muda significativamente o tempo de acesso, já que o tempo de acesso para bits na mesma linha é muito menor. A Figura 5.12 mostra como a densidade, o custo e o tempo de acesso das DRAMs mudaram através dos anos.
FIGURA 5.12 Tamanho da DRAM aumentado por múltiplos de quatro aproximadamente uma vez a cada três anos até 1996 e, daí em diante, dobrando aproximadamente a cada dois anos. As melhorias no tempo de acesso têm sido mais lentas, porém contínuas, e o custo quase acompanha as melhorias na densidade, embora seja frequentemente afetado por outros fatores, como a disponibilidade e a demanda. O custo por gigabyte não está ajustado pela inflação.
Para melhorar a interface com os processadores, as DRAMs adicionaram clocks e são propriamente chamadas SDRAMs (Synchronous DRAMs). A vantagem das SDRAMs é que o uso de clock elimina o tempo de sincronização entre a memória e o processador.
Detalhamento: Um modo de medir o desempenho do sistema de memória por trás das caches é o benchmark Stream [McCalpin, 1995]. Ele mede o desempenho de longas operações de vetor. Elas não possuem localidade temporal e acessam arrays que são maiores do que a cache do computador sendo testado. Detalhamento: O modo de rajada para memória DDR também é encontrado nos barramentos de memória, como o Intel Duo Core Front Side Bus.
Resumo Começamos a seção anterior examinando a mais simples das caches: uma cache diretamente mapeada com um bloco de uma palavra. Nesse tipo de cache, tanto os acertos quanto as falhas são simples, já que uma palavra pode estar localizada exatamente
382
Capítulo 5 Grande e Rápida: Explorando a Hierarquia de Memória
em um lugar e existe uma tag separada para cada palavra. A fim de manter a cache e a memória consistentes, um esquema de write-through pode ser usado, de modo que toda escrita na cache também faz com que a memória seja atualizada. A alternativa ao write-through é um esquema write-back que copia um bloco de volta para a memória quando ele é substituído; discutiremos esse esquema mais detalhadamente em seções futuras. Para tirar vantagem da localidade espacial, uma cache precisa ter um tamanho de bloco maior do que uma palavra. O uso de um bloco maior diminui a taxa de falhas e melhora a eficiência da cache reduzindo a quantidade de armazenamento de tag em relação à quantidade de armazenamento de dados na cache. Embora um tamanho de bloco maior diminua a taxa de falhas, ele também pode aumentar a penalidade de falha. Se a penalidade de falha aumentasse linearmente com o tamanho de bloco, blocos maiores poderiam facilmente levar a um desempenho menor. Para evitar a perda de desempenho, a largura de banda da memória principal é aumentada de modo a transferir blocos de cache de maneira mais eficiente. Os dois métodos comuns para fazer isso são tornar a memória mais larga e a intercalação. Os projetistas de DRAM melhoraram bastante a interface entre o processador e a memória, a fim de aumentar a largura de banda das transferências no modo rajada e reduzir o custo dos tamanhos de bloco de cache maiores.
Verifique você mesmo
A velocidade do sistema de memória afeta a decisão do projetista sobre o tamanho do bloco de cache. Quais dos seguintes princípios de projeto de cache normalmente são válidos? 1. Quanto mais curta for a latência da memória, menor será o bloco de cache. 2. Quanto mais curta for a latência da memória, maior será o bloco de cache. 3. Quanto maior for a largura de banda da memória, menor será o bloco de cache. 4. Quanto maior for a largura de banda da memória, maior será o bloco de cache.
Medindo e melhorando o desempenho
5.3 da cache
Nesta seção, começamos examinando como medir e analisar o desempenho da cache; depois, exploramos duas técnicas diferentes para melhorar o desempenho da cache. Uma delas focaliza o decréscimo da taxa de falhas reduzindo a probabilidade de dois blocos de memória diferentes disputarem o mesmo local da cache. A segunda técnica reduz a penalidade de falha acrescentando um nível adicional na hierarquia. Essa técnica, chamada caching multinível, apareceu inicialmente nos computadores de topo de linha sendo vendidos por mais de US$100.000 em 1990; desde então, ela se tornou comum nos computadores desktop vendidos por menos de US$500! O tempo de CPU pode ser dividido nos ciclos de clock que a CPU gasta executando o programa e os ciclos de clock que gasta esperando o sistema de memória. Normalmente, consideramos que os custos do acesso à cache que são acertos são parte dos ciclos de execução normais da CPU. Portanto, TempodeCPU = (ciclosdeclock deexecuçãoda CPU + Ciclosdeclock destallde memória) × Tempodeciclodeclock
5.3 Medindo e melhorando o desempenho da cache 383
Os ciclos de clock de stall de memória vêm principalmente das falhas de cache, e é isso que iremos considerar aqui. Também limitamos a discussão a um modelo simplificado do sistema de memória. Nos processadores reais, os stalls gerados por leituras e escritas podem ser muito complexos, e a previsão correta do desempenho normalmente exige simulações extremamente detalhadas do processador e do sistema de memória. Os ciclos de clock de stall de memória podem ser definidos como a soma dos ciclos de stall vindo das leituras mais os provenientes das escritas: Ciclosdeclock destallde memória = Ciclosdestallde leitura + Ciclosdestalldeescrita Os ciclos de stall de leitura podem ser definidos em função do número de acessos de leitura por programa, a penalidade de falha nos ciclos de clock para uma leitura e a taxa de falhas de leitura: Ciclosdestallde leitura =
Leituras × Taxa defalhasde leitura × Penalidadedefalha de leitura Programa
As escritas são mais complicadas. Para um esquema write-through, temos duas origens de stalls: as falhas de escrita, que normalmente exigem que busquemos o bloco antes de continuar a escrita (veja a seção “Detalhamento” na Seção “Tratando escritas”, anteriormente neste capítulo, para obter mais informações sobre como lidar com escritas), e os stalls do buffer de escrita, que ocorrem quando o buffer de escrita está cheio ao ocorrer uma escrita. Assim, os ciclos de stall para escritas são iguais à soma desses dois fatores: Leituras Ciclosdestalldeescrita = × Taxa defalhasdeescrita × Penalidadedefalha deescrita Programa + Stallsdo buffer deescrita Como os stalls do buffer de escrita dependem da proximidade das escritas, e não apenas da frequência, não é possível fornecer uma equação simples para calcular esses stalls. Felizmente, nos sistemas com um buffer de escrita razoável (por exemplo, quatro ou mais palavras) e uma memória capaz de aceitar escritas em uma velocidade que excede significativamente a frequência de escrita média em programas (por exemplo, por um fator de duas vezes), os stalls do buffer de escrita serão pequenos e podemos ignorá-los. Se um sistema não atendesse a esse critério, ele não seria bem projetado; ao contrário, o projetista deveria ter usado um buffer de escrita mais profundo ou uma organização write-back. Os esquemas write-back também possuem stalls potenciais extras surgindo da necessidade de escrever um bloco de cache novamente na memória quando o bloco é substituído. Discutiremos mais o assunto na Seção 5.5. Na maioria das organizações de cache write-back, as penalidades de falha de leitura e escrita são iguais (o tempo para buscar o bloco da memória). Se considerarmos que os stalls do buffer de escrita são insignificantes, podemos combinar as leituras e escritas usando uma única taxa de falhas e a penalidade de falha: Ciclosdeclock destallde memória =
Acessosà memória × Taxa defalhas × Penalidadedefalha Programa
Também podemos fatorar isso como Ciclosdeclock destallde memória =
Instruções Falhas × × Penalidadedefalha Programa Instrução
Vamos considerar um exemplo simples para ajudar a entender o impacto no desempenho da cache sobre o desempenho do processador.
384
Capítulo 5 Grande e Rápida: Explorando a Hierarquia de Memória
Calculando o desempenho da cache
EXEMPLO
RESPOSTA
Suponha que uma taxa de falhas de cache de instruções para um programa seja de 2% e que uma taxa de falhas de cache de dados seja de 4%. Se um processador possui um CPI de 2 sem qualquer stall de memória e a penalidade de falha é de 100 ciclos para todas as falhas, determine o quão mais rápido um processador executaria com uma cache perfeita que nunca falhasse. Suponha que a frequência de todos os loads e stores seja 36%. O número de ciclos de falha da memória para instruções em termos da contagem de instruções (I) é Ciclosde falha deinstrução = I × 2% × 100 = 2,00 × I A frequência de todos os loads e stores é de 36%. Logo, podemos encontrar o número de ciclos de falha da memória para referências de dados: Ciclos de falha dedados = I × 36% × 4% × 100 = 1, 44 × I O número total de ciclos de stall da memória é 2,00 I + 1,44 I = 3,44 I. Isso é mais do que três ciclos de stall da memória por instrução. Portanto, o CPI com stalls da memória é 2 + 3,44 = 5,44. Como não há mudança alguma na contagem de instruções ou na velocidade de clock, a taxa dos tempos de execução da CPU é I × CPIstall × Ciclodeclock TempodeCPU com stalls = TempodeCPU comcache perfeita I × CPIperfeita × Ciclodeclock CPIstall 5‚44 = = CPIperfeita 2 O desempenho com a cache perfeita é melhor por um fator de 5,44/2 = 2,72.
O que acontece se o processador se tornar mais rápido, mas o sistema de memória não? A quantidade de tempo gasto nos stalls da memória tomará uma fração cada vez maior do tempo de execução; a Lei de Amdahl, que examinamos no Capítulo 1, nos lembra desse fato. Alguns exemplos simples mostram como esse problema pode ser sério. Suponha que aceleremos o computador do exemplo anterior reduzindo seu CPI de 2 para 1 sem mudar a velocidade de clock, o que pode ser feito com um pipeline melhorado. O sistema com falhas de cache, então, teria um CPI de 1 + 3,44 = 4,44, e o sistema com a cache perfeita seria 4‚44 = 4‚44 vezes mais rápido 1 A quantidade de tempo de execução gasto em stalls da memória teria subido de 3‚44 = 63% 5‚44 para 3‚44 = 77% 4‚44
5.3 Medindo e melhorando o desempenho da cache 385
Da mesma forma, aumentar a velocidade de clock sem mudar o sistema de memória também aumenta a perda de desempenho devido às falhas de cache. Os exemplos e equações anteriores consideram que o tempo de acerto não é um fator na determinação do desempenho da cache. Claramente, se o tempo de acerto aumentar, o tempo total para acessar uma palavra do sistema de memória crescerá, possivelmente causando um aumento no tempo de ciclo do processador. Embora vejamos em breve outros exemplos do que pode aumentar o tempo de acerto, um exemplo é aumentar o tamanho da cache. Uma cache maior pode ter um tempo de acesso maior, exatamente como se sua mesa na biblioteca fosse muito grande (digamos, 3 metros quadrados): você levaria mais tempo para localizar um livro. Um aumento no tempo de acerto provavelmente acrescenta outro estágio ao pipeline, já que podem ser necessários vários ciclos para um acerto de cache. Embora seja mais complexo calcular o impacto de desempenho de um pipeline mais profundo, em algum ponto, o aumento no tempo de acerto para uma cache maior pode dominar a melhoria na taxa de acertos, levando a uma redução no desempenho do processador. A fim de capturar o fato de que o tempo de acesso a dados para acertos e falhas afeta o desempenho, os projetistas às vezes usam tempo médio de acesso à memória (TMAM) como um modo de examinar os projetos de cache alternativos. O tempo médio de acesso à memória é o tempo médio para acessar a memória, considerando acertos e falhas e a frequência dos diferentes acessos; ele é igual ao seguinte: TMAM = Tempo para um acerto + Taxa defalha × Penalidadedefalha
Calculando o tempo médio de acesso à memória
Ache o TMAM para um processador com tempo de clock de 1 ns, uma penalidade de falha de 20 ciclos de clock, uma taxa de falha de 0,05 falhas por instrução e um tempo de acesso a cache (incluindo detecção de acerto) de 1 ciclo de clock. Suponha que as penalidades de perda de leitura e escrita sejam iguais e ignore outros stalls de escrita. O tempo médio de acesso à memória por instrução é TMAM = Tempo para um acerto + Taxa defalha × Penalidadedefalha = 1 + 0,05 × 20 = 2ciclosdeclock ou 2 ns. A próxima subseção discute organizações de cache alternativas que diminuem a taxa de falhas mas pode, algumas vezes, aumentar o tempo de acerto; outros exemplos aparecem em Falácias e armadilhas (Seção 5.11).
Reduzindo as falhas de cache com um posicionamento de blocos mais flexível Até agora, quando colocamos um bloco na cache, usamos um esquema de posicionamento simples: um bloco só pode entrar exatamente em um local na cache. Como já dissemos, esse esquema é chamado de mapeamento direto porque qualquer endereço de bloco na memória é diretamente mapeado para um único local no nível superior da hierarquia. Existe, na
EXEMPLO
RESPOSTA
386
cache totalmente associativa Uma estrutura de cache em que um bloco pode ser posicionado em qualquer local da cache.
cache associativa por conjunto Uma cache que possui um número fixo de locais (no mínimo dois) onde cada bloco pode ser colocado.
Capítulo 5 Grande e Rápida: Explorando a Hierarquia de Memória
verdade, toda uma faixa de esquemas para posicionamento de blocos. Em um extremo está o mapeamento direto, em que um bloco só pode ser posicionado exatamente em um local. No outro extremo está um esquema em que um bloco pode ser posicionado em qualquer local na cache. Esse esquema é chamado de totalmente associativo porque um bloco na memória pode ser associado com qualquer entrada da cache. Para encontrar um determinado bloco em uma cache totalmente associativa, todas as entradas da cache precisam ser pesquisadas, pois um bloco pode estar posicionado em qualquer uma delas. Para tornar a pesquisa exequível, ela é feita em paralelo com um comparador associado a cada entrada da cache. Esses comparadores aumentam muito o custo do hardware, na prática, tornando o posicionamento totalmente associativo viável apenas para caches com pequenos números de blocos. A faixa intermediária de projetos entre a cache diretamente mapeada e a cache totalmente associativa é chamada de associativa por conjunto. Em uma cache associativa por conjunto, existe um número fixo de locais (pelo menos dois) onde cada bloco pode ser colocado; uma cache associativa por conjunto com n locais para um bloco é chamado de cache associativa por conjunto de n vias. Uma cache associativa por conjunto de n vias consiste em diversos conjuntos, cada um consistindo em n blocos. Cada bloco na memória é mapeado para um conjunto único na cache, determinado pelo campo índice, e um bloco pode ser colocado em qualquer elemento desse conjunto. Portanto, um posicionamento associativo por conjunto combina o posicionamento diretamente mapeado e o posicionamento totalmente associativo: um bloco é diretamente mapeado para um conjunto e, então, uma correspondência é pesquisada em todos os blocos no conjunto. Por exemplo, a Figura 5.13 mostra onde o bloco 12 pode ser posicionado em uma cache com oito blocos no total, conforme as três políticas de posicionamento de bloco.
FIGURA 5.13 O local de um bloco de memória cujo endereço é 12 em uma cache com 8 blocos varia para posicionamento diretamente mapeado, associativo por conjunto e totalmente associativo. No posicionamento diretamente mapeado, há apenas um bloco de cache em que o bloco de memória 12 pode ser encontrado, e esse bloco é dado por (12 módulo 8) = 4. Em uma cache associativa por conjunto de duas vias, haveria quatro conjuntos e o bloco de memória 12 precisa estar no conjunto (12 mod 4) = 0; o bloco de memória pode estar em qualquer elemento do conjunto. Em um posicionamento totalmente associativo, o bloco de memória para o endereço de bloco 12 pode aparecer em qualquer um dos oito blocos de cache.
Lembre-se de que, em uma cache diretamente mapeada, a posição de um bloco de memória é determinada por (Númerodo bloco)módulo(Númerode blocos na cache) Em uma cache associativa por conjunto, o conjunto contendo um bloco de memória é determinado por (Númerodo bloco)módulo(Númerode conjuntos na cache) Como o bloco pode ser colocado em qualquer elemento do conjunto, todas as tags de todos os elementos do conjunto precisam ser pesquisadas. Em uma cache totalmente
5.3 Medindo e melhorando o desempenho da cache 387
associativa, o bloco pode entrar em qualquer lugar e todas as tags de todos os blocos na cache precisam ser pesquisadas. Podemos pensar em cada estratégia de posicionamento de bloco como uma variação da associatividade por conjunto. A Figura 5.14 mostra as possíveis estruturas de associatividade para uma cache de oito blocos. Uma cache diretamente mapeada é simplesmente uma cache associativa por conjunto de uma via: cada entrada de cache contém um bloco, e cada conjunto possui um elemento. Uma cache totalmente associativa com m entradas é simplesmente uma cache associativa por conjunto de m vias; ele tem um conjunto com m blocos, e uma entrada pode residir em qualquer bloco dentro desse conjunto.
FIGURA 5.14 Uma cache de oito blocos configurada como diretamente mapeada, associativa por conjunto de duas vias, associativa por conjunto de quatro vias e totalmente associativa. O tamanho total da cache em blocos é igual ao número de conjuntos multiplicado pela associatividade. Portanto, para uma cache de tamanho fixo, aumentar a associatividade diminui o número de conjuntos enquanto aumenta o número de elementos por conjunto. Com oito blocos, uma cache associativa por conjunto de oito vias é igual a uma cache totalmente associativa.
A vantagem de aumentar o grau da associatividade é que ela normalmente diminui a taxa de falhas, como mostra o próximo exemplo. A principal desvantagem, que veremos em mais detalhes em breve, é um potencial aumento no tempo de acerto.
Falhas e associatividade nas caches
Considere três caches pequenas, cada uma consistindo em quatro blocos de uma palavra cada. Uma cache é totalmente associativa, uma segunda cache é associativa por conjunto de duas vias, e a terceira cache é diretamente mapeada. Encontre o número de falhas para cada organização de cache, dada a seguinte sequência de endereços de bloco: 0, 8, 0, 6 e 8. O caso diretamente mapeado é mais fácil. Primeiro, vamos determinar para qual bloco de cache cada endereço de bloco é mapeado:
EXEMPLO
RESPOSTA
388
Capítulo 5 Grande e Rápida: Explorando a Hierarquia de Memória
Endereço do bloco
Bloco de cache
0
(0 módulo 4) = 0
6
(6 módulo 4) = 2
8
(8 módulo 4) = 0
Agora podemos preencher o conteúdo da cache após cada referência, usando uma entrada em branco para indicar que o bloco é inválido, texto colorido para mostrar uma nova entrada incluída na cache para a referência associada e um texto normal para mostrar uma entrada existente na cache: Endereço do bloco de memória associado
Acerto ou falha
Conteúdo dos blocos de cache após referência
0
falha
Memória[0]
8
falha
Memória[8]
0
falha
Memória[0]
6
falha
Memória[0]
Memória[6]
8
falha
Memória[8]
Memória[6]
0
1
2
3
A cache diretamente mapeada gera cinco falhas para os cinco acessos. A cache associativa por conjunto possui dois conjuntos (com índices 0 e 1) com dois elementos por conjunto. Primeiro, vamos determinar para qual conjunto cada endereço de bloco é mapeado: Endereço do bloco
Bloco de cache
0
(0 módulo 2) = 0
6
(6 módulo 2) = 0
8
(8 módulo 2) = 0
Já que temos uma escolha de qual entrada em um conjunto substituir em uma falha, precisamos de uma regra de substituição. As caches associativas por conjunto normalmente substituem o bloco menos recentemente usado dentro de um conjunto; ou seja, o bloco usado há mais tempo é substituído. (Discutiremos as regras de substituição mais detalhadamente em breve.) Usando essa regra de substituição, o conteúdo da cache associativa por conjunto após cada referência se parece com o seguinte: Endereço do bloco de memória associado
Acerto ou falha
Conteúdo dos blocos de cache após referência
0
falha
Memória[0]
8
falha
Memória[0]
Memória[8]
0
acerto
Memória[0]
Memória[8]
6
falha
Memória[0]
Memória[6]
8
falha
Memória[8]
Memória[6]
Conjunto 0
Conjunto 0
Conjunto 1
Conjunto 1
Observe que quando o bloco 6 é referenciado, ele substitui o bloco 8, já que o bloco 8 foi referenciado menos recentemente do que o bloco 0. A cache associativa por conjunto de duas vias possui quatro falhas, uma a menos do que a cache diretamente mapeada.
5.3 Medindo e melhorando o desempenho da cache 389
A cache totalmente associativa possui quatro blocos de cache (em um único conjunto); qualquer bloco de memória pode ser armazenado em qualquer bloco de cache. A cache totalmente associativa possui o melhor desempenho, com apenas três falhas: Endereço do bloco de memória associado
Acerto ou falha
Conteúdo dos blocos de cache após referência
0
falha
Memória[0]
8
falha
Memória[0]
Memória[8]
0
acerto
Memória[0]
Memória[8]
6
falha
Memória[0]
Memória[8]
Memória[6]
8
acerto
Memória[0]
Memória[8]
Memória[6]
Bloco 0
Bloco 1
Bloco 2
Bloco 3
Para essa série de referências, três falhas é o melhor que podemos fazer porque três endereços de bloco únicos são acessados. Repare que se tivéssemos oito blocos na cache, não haveria qualquer substituição na cache associativa por conjunto de duas vias (confira isso você mesmo), e ele teria o mesmo número de falhas da cache totalmente associativa. Da mesma forma, se tivéssemos 16 blocos, todas as três caches teriam o mesmo número de falhas. Até mesmo esse exemplo trivial mostra que o tamanho da cache e a associatividade não são independentes para a determinação do desempenho da cache.
Quanta redução na taxa de falhas é obtida pela associatividade? A Figura 5.15 mostra a melhoria para uma cache de dados de 64KB com um bloco de 16 palavras e mostra a associatividade mudando do mapeamento direto para oito vias. Passar da associatividade de uma via para duas vias diminui a taxa de falhas em aproximadamente 15%, mas há pouca melhora adicional em passar para uma associatividade mais alta.
FIGURA 5.15 As taxas de falhas da cache de dados para uma organização como o processador Intrinsity FastMATH para benchmarks SPEC2000 com associatividade variando de uma via a oito vias. Esses resultados para dez programas SPEC2000 são de Hennessy e Patterson [2003].
Localizando um bloco na cache Agora, vamos considerar a tarefa de encontrar um bloco em uma cache que é associativa por conjunto. Assim como em uma cache diretamente mapeada, cada bloco em uma cache associativa por conjunto inclui uma tag de endereço que fornece o endereço do bloco. A tag de cada bloco de cache dentro do conjunto apropriado é verificada para ver se corresponde ao endereço de bloco vindo do processador. A Figura 5.16 mostra como o endereço é decomposto. O valor de índice é usado para selecionar o conjunto contendo o endereço
FIGURA 5.16 As três partes de um endereço em uma cache associativa por conjunto ou diretamente mapeada. O índice é usado para selecionar o conjunto e, depois, a tag é usada para escolher o bloco por comparação com os blocos no conjunto selecionado. O offset do bloco é o endereço dos dados desejados dentro do bloco.
390
Capítulo 5 Grande e Rápida: Explorando a Hierarquia de Memória
de interesse, e as tags de todos os blocos no conjunto precisam ser pesquisadas. Como a velocidade é a essência da pesquisa, todas as tags no conjunto selecionado são pesquisadas em paralelo. Assim como em uma cache totalmente associativa, uma pesquisa sequencial tornaria o tempo de acerto de uma cache associativa por conjunto muito lento. Se o tamanho de cache total for mantido igual, aumentar a associatividade aumenta o número de blocos por conjunto, que é o número de comparações simultâneas necessárias para realizar a pesquisa em paralelo: cada aumento por um fator de dois na associatividade dobra o número de blocos por conjunto e divide por dois o número de conjuntos. Assim, cada aumento pelo dobro na associatividade diminui o tamanho do índice em 1 bit e aumenta o tamanho da tag em 1 bit. Em uma cache totalmente associativa, existe apenas um conjunto, e todos os blocos precisam ser verificados em paralelo. Portanto, não há qualquer índice, e o endereço inteiro, excluindo o offset do bloco, é comparado com a tag de cada bloco. Em outras palavras, a cache inteira é pesquisada sem qualquer indexação. Em uma cache diretamente mapeada, apenas um único comparador é necessário, pois a entrada pode estar apenas em um bloco, e acessamos a cache por meio da indexação. A Figura 5.17 mostra que em uma cache associativa por conjunto de quatro vias, quatro comparadores são necessários, juntamente com um multiplexador de 4 para 1 a fim de escolher entre os quatro números possíveis do conjunto selecionado. O acesso de cache consiste em indexar o conjunto apropriado e, depois, pesquisar as tags do conjunto. Os custos de uma cache associativa são os comparadores extras e qualquer atraso pela necessidade de comparar e selecionar entre os elementos do conjunto. A escolha entre mapeamento direto, associativo por conjunto ou totalmente associativo em qualquer hierarquia de memória dependerá do custo de uma falha em comparação com o custo da implementação da associatividade, ambos em tempo e em hardware extra.
FIGURA 5.17 A implementação de uma cache associativa por conjunto de quatro vias exige quatro comparadores e um multiplexador de 4 para 1. Os comparadores determinam qual elemento do conjunto selecionado (se houver) corresponde à tag. A saída dos comparadores é usada para selecionar os dados de um dos quatro blocos do conjunto indexado, usando um multiplexador com um sinal de seleção decodificado. Em algumas implementações, a saída permite que sinais nas partes de dados das RAMs de cache possam ser usados para selecionar a entrada no conjunto que controla a saída. A saída permite que o sinal venha dos comparadores, fazendo com que o elemento correspondente controle as saídas de dados. Essa organização elimina a necessidade do multiplexador.
5.3 Medindo e melhorando o desempenho da cache 391
Detalhamento: Uma Content Addressable Memory (CAM) é um circuito que combina comparação e armazenamento em um único dispositivo. Em vez de fornecer um endereço e ler uma palavra como uma RAM, você fornece os dados e a CAM verifica se tem uma cópia e retorna o índice da linha correspondente. As CAMs significam que os projetistas de cache podem proporcionar a implementação de uma associativa por conjunto muito mais alta do que se tivessem de construir o hardware a partir de SRAMs e comparadores. Em 2008, o maior tamanho e potência da CAM geralmente levam a uma associatividade por conjunto em duas vias e quatro vias sendo construída a partir de SRAMs padrão e comparadores, com oito vias em diante sendo construídas usando CAMs.
Escolhendo que bloco substituir Quando uma falha ocorre em uma cache diretamente mapeada, o bloco requisitado só pode entrar em exatamente uma posição, e o bloco ocupando essa posição precisa ser substituído. Em uma cache associativa, temos uma escolha de onde colocar o bloco requisitado e, portanto, uma escolha de qual bloco substituir. Em uma cache totalmente associativa, todos os blocos são candidatos à substituição. Em uma cache associativa por conjunto, precisamos escolher entre os blocos do conjunto selecionado. O esquema mais comum é o LRU (Least Recently Used – usado menos recentemente), que usamos no exemplo anterior. Em um esquema LRU, o bloco substituído é aquele que não foi usado há mais tempo. O exemplo associativo por conjunto anteriormente neste capítulo utiliza LRU, que é o motivo pelo qual substituímos Memória(0) ao invés de Memória(6) na Seção Falhas e associatividade nas caches. A substituição LRU é implementada monitorando quando cada elemento em um conjunto foi usado em relação aos outros elementos no conjunto. Para uma cache associativa por conjunto de duas vias, o controle de quando os dois elementos foram usados pode ser implementado mantendo um único bit em cada conjunto e definindo o bit para indicar um elemento sempre que este é referenciado. Conforme a associatividade aumenta, a implementação do LRU se torna mais difícil; na Seção 5.5, veremos um esquema alternativo para substituição.
LRU (Least Recently Used – usado menos recentemente) Um esquema de substituição em que o bloco substituído é aquele que não foi usado há mais tempo.
Tamanho das tags versus associatividade do conjunto
O acréscimo da associatividade requer mais comparadores e mais bits de tag por bloco de cache. Considerando uma cache de 4K blocos, um tamanho de bloco de quatro palavras e um endereço de 32 bits, encontre o número total de conjuntos e o número total de bits de tag para caches que são diretamente mapeadas, associativas por conjunto de duas e quatro vias e totalmente associativas. Como existem 16 (=24) bytes por bloco, um endereço de 32 bits produz 32 – 4 = 28 bits para serem usados para índice e tag. A cache diretamente mapeada possui um mesmo número de conjuntos e blocos e, portanto, 12 bits de índice, já que log2(4K) = 12; logo, o número total de bits de tag é (28 – 12) × 4K = 16 × 4K = 64 Kbits. Cada grau de associatividade diminui o número de conjuntos por um fator de dois e, portanto, diminui o número de bits usados para indexar a cache por um e aumenta o número de bits na tag por um. Consequentemente, para uma cache associativa por conjunto de duas vias, existem 2K de conjuntos, e o número total de bits de tag é (28 – 11) × 2 × 2K = 34 × 2K = 68 Kbits. Para uma cache associativa por conjunto de quatro vias, o número total de conjuntos é 1K, e o número total de bits de tag é (28 – 10) × 4 × 1K = 72 × 1K = 72 Kbits. Para uma cache totalmente associativa, há apenas um conjunto com blocos de 4K de blocos, e a tag possui 28 bits, produzindo um total de 28 × 4K × 1 = 112K de bits de tag.
EXEMPLO
RESPOSTA
392
Capítulo 5 Grande e Rápida: Explorando a Hierarquia de Memória
Reduzindo a penalidade de falha usando caches multiníveis Todos os computadores modernos fazem uso de caches. Para diminuir a diferença entre as rápidas velocidades de clock dos processadores modernos e o tempo relativamente longo necessário para acessar as DRAMs, muitos microprocessadores suportam um nível adicional de cache. Essa cache de segundo nível normalmente está no mesmo chip e é acessada sempre que ocorre uma falha na cache primária. Se a cache de segundo nível contiver os dados desejados, a penalidade de falha para a cache de primeiro nível será o tempo de acesso à cache de segundo nível, que será muito menor do que o tempo de acesso à memória principal. Se nem a cache primária nem a secundária contiverem os dados, um acesso à memória principal será necessário, e uma penalidade de falha maior será observada. Em que grau é significante a melhora de desempenho pelo uso de uma cache secundária? O próximo exemplo nos mostra.
Desempenho das caches multinível
EXEMPLO
RESPOSTA
Suponha que tenhamos um processador com um CPI básico de 1,0, considerando que todas as referências acertem na cache primária e uma velocidade de clock de 4GHz. Considere um tempo de acesso à memória principal de 100ns, incluindo todo o tratamento de falhas. Suponha que a taxa de falhas por instrução na cache primária seja de 2%. O quão mais rápido será o processador se acrescentarmos uma cache secundária que tenha um tempo de acesso de 5ns para um acerto ou uma falha e que seja grande o suficiente de modo a reduzir a taxa de falhas para a memória principal para 0,5%? A penalidade de falha para a memória principal é 100 ns = 400ciclosdeclock ns 0‚25 ciclodeclock O CPI efetivo com um nível de cache é dado por CPI total = CPI básico + Ciclosde stall de memória por instrução Para o processador com um nível de cache, CPI total = 1,0 + Ciclosde stall de memória por instrução = 1,0 + 2% × 400 = 9 Com dois níveis de cache, uma falha na cache primária (ou de primeiro nível) pode ser satisfeita pela cache secundária ou pela memória principal. A penalidade da falha para um acesso à cache de segundo nível é 5 ns = 20ciclosdeclock ns 0‚25 ciclodeclock Se a falha for satisfeita na cache secundária, essa será toda a penalidade de falha. Se a falha precisar ir à memória principal, então, a penalidade de falha total será a soma do tempo de acesso à cache secundária e do tempo de acesso à memória principal.
5.3 Medindo e melhorando o desempenho da cache 393
Logo, para uma cache de dois níveis, o CPI total é a soma dos ciclos de stall dos dois níveis de cache e o CPI básico: CPI total = 1 + Stalls primários por instrução + Stalls secundários por instrução = 1 + 2% × 20 + 0,5% × 400 = 1 + 0, 4 + 2,0 = 3, 4 Portanto, o processador com a cache secundária é mais rápido por um fator de 9‚0 = 2‚6 3‚4 Como alternativa, poderíamos ter calculado os ciclos de stall somando os ciclos de stall das referências que acertam na cache secundária ((2% – 0,5%) × 20 = 0,3) e as referências que vão à memória principal, que precisam incluir o custo para acessar a cache secundária, bem como o tempo de acesso à memória principal (0,5% × (20 + 400) = 2,1). A soma, 1,0 + 0,3 + 2,1, é novamente 3,4.
As considerações de projeto para uma cache primária e secundária são significativamente diferentes porque a presença da outra cache muda a melhor escolha em comparação com uma cache de nível único. Em especial, uma estrutura de cache de dois níveis permite que a cache primária se concentre em minimizar o tempo de acerto para produzir um ciclo de clock mais curto, enquanto permite que a cache secundária focalize a taxa de falhas no sentido de reduzir a penalidade dos longos tempos de acesso à memória. O efeito dessas mudanças nas duas caches pode ser visto comparando cada cache com o projeto ótimo para um nível único de cache. Em comparação com uma cache de nível único, a cache primária de uma cache multinível normalmente é menor. Além disso, a cache primária frequentemente usa um tamanho de bloco menor, para se adequar ao tamanho de cache menor e à penalidade de falha reduzida. Em comparação, a cache secundária normalmente será maior do que em uma cache de nível único, já que o tempo de acesso da cache secundária é menos importante. Com um tamanho total maior, a cache secundária pode usar um tamanho de bloco maior do que o apropriado com uma cache de nível único. Ela constantemente utiliza uma associatividade maior que a cache primária, dado o foco da redução de taxas de falha.
A classificação tem sido exaustivamente analisada para se encontrar algoritmos melhores: Bubble Sort, Quicksort e assim por diante. A Figura 5.18(a) mostra as instruções executadas por item pesquisado pelo Radix Sort em comparação com o Quicksort. Decididamente, para arrays grandes, o Radix Sort possui uma vantagem algorítmica sobre o Quicksort em termos do número de operações. A Figura 5.18(b) mostra o tempo por chave em vez das instruções executadas. Podemos ver que as linhas começam na mesma trajetória da Figura 5.18(a), mas, então, a linha do Radix Sort diverge conforme os dados a serem ordenados aumentam. O que está ocorrendo? A Figura 5.18(c) responde olhando as falhas de cache por item ordenado: o Quicksort possui muito menos falhas por item a ser ordenado. Infelizmente, a análise algorítmica padrão ignora o impacto da hierarquia de memória. À medida que velocidades de clock mais altas e a Lei de Moore permitem aos arquitetos compactarem todo o desempenho de um fluxo de instruções, um uso correto da hierarquia de memória é fundamental para a obtenção de um alto desempenho. Como dissemos na introdução, entender o comportamento da hierarquia de memória é vital para compreender o desempenho dos programas nos computadores atuais.
Detalhamento: Caches multiníveis envolvem diversas complicações. Primeiro, agora existem vários tipos diferentes de falhas e taxas de falhas correspondentes. No exemplo “Falhas e
cache multinível Uma hierarquia de memória com múltiplos níveis de cache, em vez de apenas uma cache e a memória principal.
Entendendo o desempenho dos programas
394
Capítulo 5 Grande e Rápida: Explorando a Hierarquia de Memória
FIGURA 5.18 Comparando o Quicksort e o Radix Sort por (a) instruções executadas por item ordenado, (b) tempo por item ordenado e (c) falhas de cache por item ordenado. Esses dados são de um artigo de LaMarca e Ladner [1996]. Embora os números mudassem para computadores mais novos, a ideia ainda permanece. Devido a esses resultados, foram criadas novas versões do Radix Sort que levam a hierarquia de memória em consideração, para readquirir suas vantagens logarítmicas (veja a Seção 5.11). A ideia básica das otimizações de cache é usar todos os dados em um bloco repetidamente antes de serem substituídos em uma falha.
taxa de falhas global A fração das referências que falham em todos os níveis de uma cache multinível.
taxa de falhas local A fração das referências a um nível de uma cache que falham; usada em hierarquias multiníveis.
associatividade nas caches”, anteriormente neste capítulo, vimos a taxa de falhas da cache primária e a taxa de falhas global – a fração das referências que falharam em todos os níveis de cache. Há também uma taxa de falhas para a cache secundária, que é a taxa de todas as falhas na cache secundária dividida pelo número de acessos. Essa taxa de falhas é chamada de taxa de falhas local da cache secundária. Como a cache primária filtra os acessos, especialmente aqueles com boa localidade espacial e temporal, a taxa de falhas local da cache secundária é muito mais alta do que a taxa de falhas global. No exemplo anterior citado, podemos calcular a taxa de falhas local da cache secundária como: 0,5%/2% = 25%! Felizmente, a taxa de falhas global determina a frequência com que precisamos acessar a memória principal.
5.3 Medindo e melhorando o desempenho da cache 395
Detalhamento: Com processadores que usam execução fora de ordem (ver Capítulo 4), o desempenho é mais complexo, já que executam instruções durante a penalidade de falha. Em vez da taxa de falhas de instruções e da taxa de falhas de dados, usamos falhas por instrução e esta fórmula:
Ciclosde stall da memória Falhas = × (Latência de falha total – Latência de falha sobreposta ) Instrução Instrução Não há uma maneira geral de calcular a latência de falha sobreposta; portanto, as avaliações das hierarquias de memória para processadores com execução fora de ordem inevitavelmente exigem simulações do processador e da hierarquia de memória. Somente vendo a execução do processador durante cada falha é que podemos ver se o processador sofre stall esperando os dados ou simplesmente encontra outro trabalho para fazer. Uma regra é que o processador muitas vezes oculta a penalidade de falha para uma falha de cache L1 que acerta na cache L2, mas raramente oculta uma falha para a cache L2.
Detalhamento: O desafio do desempenho para algoritmos é que a hierarquia de memória varia entre diferentes implementações da mesma arquitetura no tamanho de cache, na associatividade, no tamanho de bloco e no número de caches. Para fazer frente a essa variabilidade, algumas bibliotecas numéricas recentes parametrizam os seus algoritmos e, então, pesquisam o espaço de parâmetros em tempo de execução de modo a encontrar a melhor combinação para um determinado computador. Essa técnica é chamada de autotuning.
Qual das afirmações a seguir geralmente é verdadeira sobre um projeto com múltiplos níveis de cache? 1. As caches de primeiro nível são mais focalizadas no tempo de acerto, e as caches de segundo nível se preocupam mais com a taxa de falhas. 2. As caches de primeiro nível são mais focalizadas na taxa de falhas, e as caches de segundo nível se preocupam mais com o tempo de acerto.
Resumo Nesta seção, nos concentramos em três tópicos: o desempenho da cache, o uso da associatividade para reduzir as taxas de falhas e o uso das hierarquias de cache multinível para reduzir as penalidades de falha. O sistema de memória tem um efeito significativo sobre o tempo de execução do programa. O número de ciclos de stall de memória depende da taxa de falhas e da penalidade de falha. O desafio, como veremos na Seção 5.5, é reduzir um desses fatores sem afetar significativamente os outros fatores críticos na hierarquia de memória. Para reduzir a taxa de falhas, examinamos o uso dos esquemas de posicionamento associativos. Esses esquemas podem reduzir a taxa de falhas de uma cache permitindo um posicionamento mais flexível dos blocos dentro dela. Os esquemas totalmente associativos permitem que os blocos sejam posicionados em qualquer lugar, mas também exigem que todos os blocos da cache sejam pesquisados para atender a uma requisição. Os custos mais altos tornam as caches totalmente associativas inviáveis. As caches associativas por conjunto são uma alternativa prática, já que precisamos pesquisar apenas entre os elementos de um único conjunto, escolhido por indexação. As caches associativas por conjunto apresentam taxas de falhas mais altas, mas são mais rápidas de serem acessadas. O grau de associatividade que produz o melhor desempenho depende da tecnologia e dos detalhes da implementação. Finalmente, examinamos as caches multiníveis como uma técnica para reduzir a penalidade de falha permitindo uma cache secundária maior para tratar falhas na cache primária.
Verifique você mesmo
396
Capítulo 5 Grande e Rápida: Explorando a Hierarquia de Memória
As caches de segundo nível se tornaram comuns quando os projetistas descobriram que o silício limitado e as metas de altas velocidades de clock impedem que as caches primárias se tornem grandes. A cache secundária, que normalmente é 10 ou mais vezes maior do que a cache primária, trata muitos acessos que falham na cache primária. Nesses casos, a penalidade de falha é aquela do tempo de acesso à cache secundária (em geral, menos de dez ciclos de processador) contra o tempo de acesso à memória (normalmente mais de 100 ciclos de processador). Assim como na associatividade, as negociações de projeto entre o tamanho da cache secundária e seu tempo de acesso dependem de vários aspectos de implementação. …foi inventado um sistema para fazer a combinação entre os sistemas centrais de memória e os tambores de discos aparecer para o programador como um depósito de nível único, com as transferências necessárias ocorrendo automaticamente. Kilburn et al., One-level storage system, 1962 memória virtual Uma técnica que usa a memória principal como uma “cache” para armazenamento secundário.
endereço físico Um endereço na memória principal.
proteção Um conjunto de mecanismos para garantir que múltiplos processos compartilhando processador, memória ou dispositivos de E/S não possam interferir, intencionalmente ou não, um com o outro, lendo ou escrevendo dados no outro. Esses mecanismos também isolam o sistema operacional de um processo de usuário.
5.4 Memória Virtual Na seção anterior, vimos como as caches fornecem acesso rápido às partes recentemente usadas do código e dos dados de um programa. Da mesma forma, a memória principal pode agir como uma “cache” para o armazenamento secundário, normalmente implementado com discos magnéticos. Essa técnica é chamada de memória virtual. Historicamente, houve duas motivações principais para a memória virtual: permitir o compartilhamento seguro e eficiente da memória entre vários programas e remover os transtornos de programação de uma quantidade pequena e limitada de memória principal. Quatro décadas após sua invenção, o primeiro motivo é o que ainda predomina. Considere um grupo de programas executados ao mesmo tempo em um computador. É claro que, para permitir que vários programas compartilhem a mesma memória, precisamos ser capazes de proteger os programas uns dos outros, garantindo que um programa só possa ler e escrever as partes da memória principal atribuídas a ele. A memória principal precisa conter apenas as partes ativas dos muitos programas, exatamente como uma cache contém apenas a parte ativa de um programa. Portanto, o princípio da localidade possibilita a memória virtual e as caches, e a memória virtual nos permite compartilhar eficientemente o processador e a memória principal. Não podemos saber quais programas irão compartilhar a memória com outros programas quando os compilamos. Na verdade, os programas que compartilham a memória mudam dinamicamente enquanto estão sendo executados. Devido a essa interação dinâmica, gostaríamos de compilar cada programa para o seu próprio espaço de endereçamento – faixa distinta dos locais de memória acessível apenas a esse programa. A memória virtual implementa a tradução do espaço de endereçamento de um programa para os endereços físicos. Esse processo de tradução impõe a proteção do espaço de endereçamento de um programa contra outros programas. A segunda motivação para a memória virtual é permitir que um único programa do usuário exceda o tamanho da memória principal. Antigamente, se um programa se tornasse muito grande para a memória, cabia ao programador fazê-lo se adequar. Os programadores dividiam os programas em partes e, então, identificavam aquelas mutuamente exclusivas. Esses overlays eram carregados ou descarregados sob o controle do programa do usuário durante a execução, com o programador garantindo que o programa nunca tentaria acessar um overlay que não estivesse carregado e que os overlays carregados nunca excederiam o tamanho total da memória. Os overlays eram tradicionalmente organizados como módulos, cada um contendo código e dados. As chamadas entre procedimentos em módulos diferentes levavam um módulo a se sobrepor a outro. Como você pode bem imaginar, essa responsabilidade era uma carga substancial para os programadores. A memória virtual, criada para aliviar os programas dessa dificuldade, gerencia automaticamente os dois níveis da hierarquia de memória representados pela memória principal (às vezes, chamada de memória física para distingui-la da memória virtual) e pelo armazenamento secundário.
5.4 Memória Virtual 397
Embora os conceitos aplicados na memória virtual e nas caches sejam os mesmos, suas diferentes raízes históricas levaram ao uso de uma terminologia diferente. Um bloco de memória virtual é chamado de página, e uma falha da memória virtual é chamada de falta de página. Com a memória virtual, o processador produz um endereço virtual, traduzido por uma combinação de hardware e software para um endereço físico, que, por sua vez, pode ser usado de modo a acessar a memória principal. A Figura 5.19 mostra a memória endereçada virtualmente com páginas mapeadas na memória principal. Esse processo é chamado de mapeamento de endereço ou tradução de endereço. Hoje, os dois níveis de hierarquia de memória controlados pela memória virtual são as DRAMs e os discos magnéticos (veja o Capítulo 1, Seção “Um lugar seguro para os dados”). Se voltarmos à nossa analogia da biblioteca, podemos pensar no endereço virtual como o título de um livro e no endereço físico como seu local na biblioteca.
FIGURA 5.19 Na memória virtual, os blocos de memória (chamados de páginas) são mapeados de um conjunto de endereços (chamados de endereços virtuais) em outro conjunto (chamado de endereços físicos). O processador gera endereços virtuais enquanto a memória é acessada usando endereços físicos. Tanto a memória virtual quanto a memória física são desmembradas em páginas, de modo que uma página virtual é realmente mapeada em uma página física. Naturalmente, também é possível que uma página virtual esteja ausente da memória principal e não seja mapeada para um endereço físico, residindo no disco em vez disso. As páginas físicas podem ser compartilhadas fazendo dois endereços virtuais apontarem para o mesmo endereço físico. Essa capacidade é usada para permitir que dois programas diferentes compartilhem dados ou código.
A memória virtual também simplifica o carregamento do programa para execução fornecendo relocação. A relocação mapeia os endereços virtuais usados por um programa para diferentes endereços físicos antes que os endereços sejam usados no acesso à memória. Essa relocação nos permite carregar o programa em qualquer lugar na memória principal. Além disso, todos os sistemas de memória virtual em uso atualmente relocam o programa como um conjunto de blocos (páginas) de tamanho fixo, eliminando, assim, a necessidade de encontrar um bloco contíguo de memória para alocar um programa; em vez disso, o sistema operacional só precisa encontrar um número suficiente de páginas na memória principal. Na memória virtual, o endereço é desmembrado em um número de página virtual e um offset de página. A Figura 5.20 mostra a tradução do número de página virtual para um número de página física. O número de página física constitui a parte mais significativa do endereço físico, enquanto o offset de página, que não é alterado, constitui a parte menos significativa. O número de bits no campo offset de página determina o tamanho da página. O número de páginas endereçáveis com o endereço virtual não precisa corresponder ao número de páginas endereçáveis com o endereço físico. Ter um número de páginas virtuais maior do que as páginas físicas é a base para a ilusão de uma quantidade de memória virtual essencialmente ilimitada.
falta de página Um evento que ocorre quando uma página acessada não está presente na memória principal. endereço virtual Um endereço que corresponde a um local no espaço virtual e é traduzido pelo mapeamento de endereço para um endereço físico quando a memória é acessada. tradução de endereço Também chamada de mapeamento de endereço. O processo pelo qual um endereço virtual é mapeado a um endereço usado para acessar a memória.
398
Capítulo 5 Grande e Rápida: Explorando a Hierarquia de Memória
FIGURA 5.20 Mapeamento de um endereço virtual em um endereço físico. O tamanho de página é 212 = 4KB. O número de páginas físicas permitido na memória é 218, já que o número de página física contém 18 bits. Portanto, a memória principal pode ter, no máximo, 1GB, enquanto o espaço de endereço virtual possui 4GB.
Muitas escolhas de projeto nos sistemas de memória virtual são motivadas pelo alto custo de uma falha, que, na memória virtual, tradicionalmente é chamada de falta de página. Uma falta de página levará milhões de ciclos de clock para ser processada. (A tabela na Seção 5.1 mostra que a memória principal é aproximadamente 100.000 vezes mais rápida do que o disco.) Essa enorme penalidade de falha, dominada pelo tempo para obter a primeira palavra para tamanhos de página típicos, leva a várias decisões importantes nos sistemas de memória virtual: j
As páginas devem ser grandes o suficiente para tentar amortizar o longo tempo de acesso. Tamanhos de 4KB a 16KB são comuns atualmente. Novos sistemas de desktop e servidor estão sendo desenvolvidos para suportar páginas de 32KB e 64KB, embora novos sistemas embutidos estejam indo na outra direção, para páginas de 1KB.
j
Organizações que reduzem a taxa de faltas de página são atraentes. A principal técnica usada aqui é permitir o posicionamento totalmente associativo das páginas na memória.
j
As faltas de página podem ser tratadas em nível de software porque o overhead será pequeno se comparado com o tempo de acesso ao disco. Além disso, o software pode se dar ao luxo de usar algoritmos inteligentes para escolher como posicionar as páginas, já que mesmo pequenas reduções na taxa de falhas compensarão o custo desses algoritmos.
j
O write-through não funcionará para a memória virtual, visto que as escritas levam muito tempo. Em vez disso, os sistemas de memória virtual usam write-back.
As próximas subseções tratam desses fatores no projeto de memória virtual. Detalhamento: Embora normalmente imaginemos os endereços virtuais como muito maiores do que os endereços físicos, o contrário pode ocorrer quando o tamanho de endereço do processador é pequeno em relação ao estado da tecnologia de memória. Nenhum programa único pode se beneficiar, mas um grupo de programas executados ao mesmo tempo pode se beneficiar de não precisar ser trocado para a memória ou de ser executado em processadores paralelos. Para computadores servidores e desktops, processadores de 32 bits já são problemáticos.
5.4 Memória Virtual 399
Detalhamento: A discussão da memória virtual neste livro focaliza a paginação, que usa blocos de tamanho fixo. Há também um esquema de blocos de tamanho variável chamado segmentação. Na segmentação, um endereço consiste em duas partes: um número de segmento e um offset de segmento. O registrador de segmento é mapeado a um endereço físico e o offset é somado para encontrar o endereço físico real. Como o segmento pode variar em tamanho, uma verificação de limites é necessária para garantir que o offset esteja dentro do segmento. O principal uso da segmentação é suportar métodos de proteção mais avançados e compartilhar um espaço de endereçamento. A maioria dos livros de sistemas operacionais contém extensas discussões sobre a segmentação comparada com a paginação e sobre o uso da segmentação para compartilhar logicamente o espaço de endereçamento. A principal desvantagem da segmentação é que ela divide o espaço de endereço em partes logicamente separadas que precisam ser manipuladas como um endereço de duas partes: o número de segmento e o offset. A paginação, por outro lado, torna o limite entre o número de página e o offset invisível aos programadores e compiladores. Os segmentos também têm sido usados como um método para estender o espaço de endereçamento sem mudar o tamanho da palavra do computador. Essas tentativas têm sido malsucedidas devido à dificuldade e ao ônus de desempenho inerentes a um endereço de duas partes, dos quais os programadores e compiladores precisam estar cientes. Muitas arquiteturas dividem o espaço de endereçamento em grandes blocos de tamanho fixo que simplificam a proteção entre o sistema operacional e os programas de usuário e aumentam a eficiência da paginação. Embora essas divisões normalmente sejam chamadas de “segmentos”, esse mecanismo é muito mais simples do que a segmentação de tamanho de bloco variável e não é visível aos programas do usuário; discutiremos o assunto em mais detalhes em breve.
segmentação Um esquema de mapeamento de endereço de tamanho variável em que um endereço consiste em duas partes: um número de segmento, que é mapeado para um endereço físico, e um offset de segmento.
Posicionando uma página e a encontrando novamente Em razão da penalidade incrivelmente alta decorrente de uma falta de página, os projetistas reduzem a frequência das faltas de página otimizando seu posicionamento. Se permitirmos que uma página virtual seja mapeada em qualquer página física, o sistema operacional, então, pode escolher substituir qualquer página que desejar quando ocorrer uma falta de página. Por exemplo, o sistema operacional pode usar um sofisticado algoritmo e complexas estruturas de dados, que monitoram o uso de páginas, para tentar escolher uma página que não será necessária por um longo tempo. A capacidade de usar um esquema de substituição inteligente e flexível reduz a taxa de faltas de página e simplifica o uso do posicionamento de páginas totalmente associativo. Como mencionamos na Seção 5.3, a dificuldade em usar posicionamento totalmente associativo está em localizar uma entrada, já que ela pode estar em qualquer lugar no nível superior da hierarquia. Uma pesquisa completa é impraticável. Nos sistemas de memória virtual, localizamos páginas usando uma tabela que indexa a memória; essa estrutura é chamada de tabela de páginas e reside na memória. Uma tabela de páginas é indexada pelo número de página do endereço virtual para descobrir o número da página física correspondente. Cada programa possui sua própria tabela de páginas, que mapeia o espaço de endereçamento virtual desse programa para a memória principal. Em nossa analogia da biblioteca, a tabela de páginas corresponde a um mapeamento entre os títulos dos livros e os locais da biblioteca. Exatamente como o catálogo de cartões pode conter entradas para livros em outra biblioteca ou campus em vez da biblioteca local, veremos que a tabela de páginas pode conter entradas para páginas não presentes na memória. A fim de indicar o local da tabela de páginas na memória, o hardware inclui um registrador que aponta para o início da tabela de páginas; esse registrador é chamado de registrador de tabela de páginas. Por enquanto, considere que a tabela de páginas esteja em uma área fixa e contígua da memória.
A tabela de páginas, juntamente com o contador de programa e os registradores, especifica o estado de um programa. Se quisermos permitir que outro programa use o processador, precisamos salvar esse estado. Mais tarde, após restaurar esse estado, o programa pode continuar a execução. Frequentemente nos referimos a esse estado como um processo. O
tabela de páginas A tabela com as traduções de endereço virtual para físico em um sistema de memória virtual. A tabela, armazenada na memória, normalmente é indexada pelo número de página virtual; cada entrada na tabela contém o número da página física para essa página virtual se a página estiver atualmente na memória.
Interface hardware/ software
400
Capítulo 5 Grande e Rápida: Explorando a Hierarquia de Memória
processo é considerado ativo quando está de posse do processador; caso contrário, ele é considerado inativo. O sistema operacional pode tornar um processo ativo carregando o estado do processo, incluindo o contador de programa, o que irá iniciar a execução no valor salvo do contador de programa. O espaço de endereçamento do processo, e, consequentemente, todos os dados que ele pode acessar na memória, é definido pela sua tabela de páginas, que reside na memória. Em vez de salvar a tabela de páginas inteira, o sistema operacional simplesmente carrega o registrador de tabela de páginas de modo a apontar para a tabela de páginas do processo que ele quer tornar ativo. Cada processo possui sua própria tabela de páginas, já que diferentes processos usam os mesmos endereços virtuais. O sistema operacional é responsável por alocar a memória física e atualizar as tabelas de páginas, de modo que os espaços de endereço virtuais dos diferentes processos não colidam. Como veremos em breve, o uso de tabelas de páginas separadas também fornece proteção de um processo contra outro.
A Figura 5.21 usa o registrador de tabela de páginas, o endereço virtual e a tabela de páginas indicada para mostrar como o hardware pode formar um endereço físico. Um bit de validade é usado em cada entrada de tabela de páginas, exatamente como faríamos em uma cache. Se o bit estiver desligado, a página não está presente na memória principal e ocorre uma falta de página. Se o bit estiver ligado, a página está na memória e a entrada contém o número de página física.
FIGURA 5.21 A tabela de páginas é indexada pelo número de página virtual para obter a parte correspondente do endereço físico. Consideramos um endereço de 32 bits. O endereço inicial da tabela de páginas é dado pelo ponteiro da tabela de páginas. Nessa figura, o tamanho de página é 212 bytes, ou 4KB. O espaço de endereço virtual é 232 bytes, ou 4GB, e o espaço de endereçamento físico é 230 bytes, que permite uma memória principal de até 1GB. O número de entradas na tabela de páginas é 220, ou um milhão de entradas. O bit de validade para cada entrada indica se o mapeamento é legal. Se ele estiver desligado, a página não está presente na memória. Embora a entrada de tabela de páginas mostrada aqui só precise ter 19 bits de largura, ela normalmente seria arredondada para 32 bits a fim de facilitar a indexação. Os bits extras seriam usados para armazenar informações adicionais que precisam ser mantidas página a página, como a proteção.
5.4 Memória Virtual 401
Como a tabela de páginas contém um mapeamento para toda página virtual possível, nenhuma tag é necessária. Na terminologia da cache, o índice usado para acessar a tabela de páginas consiste no endereço de bloco inteiro, que é o número de página virtual.
Faltas de página Se o bit de validade para uma página virtual estiver desligado, ocorre uma falta de página. O sistema operacional precisa receber o controle. Essa transferência é feita pelo mecanismo de exceção, que abordaremos posteriormente nesta seção. Quando o sistema operacional obtém o controle, ele precisa encontrar a página no próximo nível da hierarquia (geralmente o disco magnético) e decidir onde colocar a página requisitada na memória principal. O endereço virtual por si só não diz imediatamente onde está a página no disco. Voltando à nossa analogia da biblioteca, não podemos encontrar o local de um livro nas estantes apenas sabendo seu título. Precisamos ir ao catálogo e consultar o livro, obter um endereço para o local nas estantes. Da mesma forma, em um sistema de memória virtual, é necessário monitorar o local no disco de cada página em um espaço de endereçamento virtual. Como não sabemos de antemão quando uma página na memória será escolhida para ser substituída, o sistema operacional normalmente cria o espaço no disco para todas as páginas de um processo no momento em que ele cria o processo. Esse espaço do disco é chamado de área de swap. Nesse momento, o sistema operacional também cria uma estrutura para registrar onde cada página virtual está armazenada no disco. Essa estrutura de dados pode ser parte da tabela de páginas ou pode ser uma estrutura de dados auxiliar indexada da mesma maneira que a tabela de páginas. A Figura 5.22 mostra a organização quando uma única tabela contém o número de página física ou o endereço de disco.
FIGURA 5.22 A tabela de páginas mapeia cada página na memória virtual em uma página na memória principal ou em uma página armazenada em disco, que é o próximo nível na hierarquia. O número de página virtual é usado para indexar a tabela de páginas. Se o bit de validade estiver ligado, a tabela de páginas fornece o número de página física (ou seja, o endereço inicial da página na memória) correspondente à página virtual. Se o bit de validade estiver desligado, a página reside atualmente apenas no disco, em um endereço de disco especificado. Em muitos sistemas, a tabela de endereços de página física e endereços de página de disco, embora sendo logicamente uma única tabela, é armazenada em duas estruturas de dados separadas. As tabelas duplas se justificam, em parte, porque precisamos manter os endereços de disco de todas as páginas, mesmo que elas estejam atualmente na memória principal. Lembre-se de que as páginas na memória principal e as páginas no disco são idênticas em tamanho.
área de swap O espaço no disco reservado para o espaço de memória virtual completo de um processo.
402
Capítulo 5 Grande e Rápida: Explorando a Hierarquia de Memória
O sistema operacional também cria uma estrutura de dados que controla quais processos e quais endereços virtuais usam cada página física. Quando ocorre uma falta de página, se todas as páginas na memória principal estiverem em uso, o sistema operacional precisa escolher uma página para substituir. Como queremos minimizar o número de faltas de página, a maioria dos sistemas operacionais tenta escolher uma página que supostamente não será necessária no futuro próximo. Usando o passado para prever o futuro, os sistemas operacionais seguem o esquema de substituição LRU (Least Recently Used – usado menos recentemente), que mencionamos na Seção 5.3. O sistema operacional procura a página usada menos recentemente, fazendo a suposição de que uma página que não foi usada por um longo período é menos provável de ser usada do que uma página acessada mais recentemente. As páginas substituídas são escritas na área de swap do disco. Caso você esteja curioso, o sistema operacional é apenas outro processo, e essas tabelas controlando a memória estão na memória; os detalhes dessa aparente contradição serão explicados em breve.
Interface hardware/ software bit de referência Também chamado de bit de uso. Um campo que é ligado sempre que uma página é acessada e que é usado para implementar LRU ou outros esquemas de substituição.
Implementar um esquema de LRU completamente preciso é muito caro, pois requer atualizar uma estrutura de dados a cada referência à memória. Como alternativa, a maioria dos sistemas operacionais aproxima a LRU monitorando que páginas foram e que páginas não foram usadas recentemente. Para ajudar o sistema operacional a estimar as páginas LRU, alguns computadores fornecem um bit de referência ou bit de uso, que é ligado sempre que uma página é acessada. O sistema operacional limpa periodicamente os bits de referência e, depois, os registra para que ele possa determinar que páginas foram tocadas durante um determinado período. Com essas informações de uso, o sistema operacional pode selecionar uma página que está entre as referenciadas menos recentemente (detectadas tendo seu bit de referência desligado). Se esse bit não for fornecido pelo hardware, o sistema operacional precisará encontrar outra maneira de estimar que páginas foram acessadas.
Detalhamento: Com um endereço virtual de 32 bits, páginas de 4KB e 4 bytes por entrada da tabela de páginas, podemos calcular o tamanho total da tabela de páginas:
232 = 220 12 2 Tamanhoda tabela de páginas = 220 entradasda tabela de páginas bytes × 22 = 4 MB entrada da tabela de página Númerodeentradas da tabela de páginas =
Ou seja, precisaríamos usar 4MB da memória para cada programa em execução em um dado momento. Essa quantidade não é ruim para um único programa. Mas, e se houver centenas de programas rodando, cada um com sua própria tabela de página? E como devemos tratar endereços de 64 bits, que por esse cálculo precisaríamos de 252 palavras? Diversas técnicas são usadas no sentido de reduzir a quantidade de armazenamento necessária para a tabela de páginas. As cinco técnicas a seguir visam a reduzir o armazenamento máximo total necessário, bem como minimizar a memória principal dedicada às tabelas de páginas: 1. A técnica mais simples é manter um registrador de limite que restrinja o tamanho da tabela de páginas para um determinado processo. Se o número de página virtual se tornar maior do que o conteúdo do registrador de limite, entradas precisarão ser
5.4 Memória Virtual 403
incluídas na tabela de páginas. Essa técnica permite que a tabela de páginas cresça à medida que um processo consome mais espaço. Assim, a tabela de páginas só será maior se o processo estiver usando muitas páginas do espaço de endereçamento virtual. Essa técnica exige que o espaço de endereçamento se expanda apenas em uma direção. 2. Permitir o crescimento apenas em uma direção não é o bastante, já que a maioria das linguagens exige duas áreas cujo tamanho seja expansível: uma área contém a pilha e a outra contém o heap. Devido à essa dualidade, é conveniente dividir a tabela de páginas e deixá-la crescer do endereço mais alto para baixo, assim como do endereço mais baixo para cima. Isso significa que haverá duas tabelas de páginas separadas e dois limites separados. O uso de duas tabelas de páginas divide o espaço de endereçamento em dois segmentos. O bit mais significativo de um endereço normalmente determina que segmento – e, portanto, que tabela de páginas – deve ser usado para esse endereço. Como o segmento é especificado pelo bit de endereço mais significativo, cada segmento pode ter a metade do tamanho do espaço de endereçamento. Um registrador de limite para cada segmento especifica o tamanho atual do segmento, que cresce em unidades de páginas. Esse tipo de segmentação é usado por muitas arquiteturas, inclusive MIPS. Diferente do tipo de segmentação abordado na seção “Detalhamento” da Seção 5.4, essa forma de segmentação é invisível ao programa de aplicação, embora não para o sistema operacional. A principal desvantagem desse esquema é que ele não funciona bem quando o espaço de endereçamento é usado de uma maneira esparsa e não como um conjunto contíguo de endereços virtuais. 3. Outro método para reduzir o tamanho da tabela de páginas é aplicar uma função de hashing no endereço virtual de modo que a estrutura de dados da tabela de páginas precise ser apenas do tamanho do número de páginas físicas na memória principal. Essa estrutura é chamada de tabela de páginas invertida. É claro que o processo de consulta é um pouco mais complexo com uma tabela de páginas invertida porque não podemos mais simplesmente indexar a tabela de páginas. 4. Múltiplos níveis de tabelas de páginas também podem ser usados no sentido de reduzir a quantidade total de armazenamento para a tabela de páginas. O primeiro nível mapeia grandes blocos de tamanho fixo do espaço de endereçamento virtual, talvez de 64 a 256 páginas no total. Esses grandes blocos são, às vezes, chamados de segmentos, e essa tabela de mapeamento de primeiro nível é chamada de tabela de segmentos, embora os segmentos sejam invisíveis ao usuário. Cada entrada na tabela de segmentos indica se alguma página nesse segmento está alocada e, se estiver, aponta para uma tabela de páginas desse segmento. A tradução de endereços ocorre primeiramente olhando na tabela de segmentos, usando os bits mais significativos do endereço. Se o endereço do segmento for válido, o próximo conjunto de bits mais significativos é usado para indexar a tabela de páginas indicada pela entrada da tabela de segmentos. Esse esquema permite que o espaço de endereçamento seja usado de uma maneira esparsa (vários segmentos não contíguos podem estar ativos), sem precisar alocar a tabela de páginas inteira. Esses esquemas são particularmente úteis com espaços de endereçamento muito grandes e em sistemas de software que exigem alocação não contígua. A principal desvantagem desse mapeamento de dois níveis é o processo mais complexo para a tradução de endereços. 5. A fim de reduzir a memória principal real consumida pelas tabelas de páginas, a maioria dos sistemas modernos também permite que as tabelas de páginas sejam paginadas. Embora isso pareça complicado, esse esquema funciona usando os mesmos conceitos básicos da memória virtual e simplesmente permite que as tabelas de páginas residam no espaço de endereçamento virtual. Entretanto, há alguns problemas pequenos mas cruciais, como uma série interminável de faltas de página, que precisam ser evitadas. A forma como esses problemas são resolvidos é um tema muito detalhado e, em geral, altamente específico ao processador. Em poucas palavras, esses problemas são evitados colocando todas as tabelas de páginas no espaço de endereçamento do
404
Capítulo 5 Grande e Rápida: Explorando a Hierarquia de Memória
sistema operacional e colocando pelo menos algumas das tabelas de páginas para o sistema em uma parte da memória principal que é fisicamente endereçada e está sempre presente – e, portanto, nunca no disco.
E quanto às escritas? A diferença entre o tempo de acesso à cache e à memória principal é de dezenas a centenas de ciclos, e os esquemas write-through podem ser usados, embora precisemos de um buffer de escrita para ocultar do processador a latência da escrita. Em um sistema de memória virtual, as escritas no próximo nível de hierarquia (disco) levam milhões de ciclos de clock de processador; portanto, construir um buffer de escrita para permitir que o sistema escreva diretamente no disco seria impraticável. Em vez disso, os sistemas de memória virtual precisam usar write-back, realizando as escritas individuais para a página na memória e copiando a página novamente para o disco quando ela é substituída na memória.
Interface hardware/ software
Um esquema write-back possui outra importante vantagem em um sistema de memória virtual. Como o tempo de transferência de disco é pequeno comparado com seu tempo de acesso, copiar de volta uma página inteira é muito mais eficiente do que escrever palavras individuais novamente no disco. Uma operação write-back, embora mais eficiente do que transferir páginas individuais, ainda é onerosa. Portanto, gostaríamos de saber se uma página precisa ser copiada de volta quando escolhemos substituí-la. Para monitorar se uma página foi escrita desde que foi lida para a memória, um bit de modificação (dirty bit) é acrescentado à tabela de páginas. O bit de modificação é ligado quando qualquer palavra em uma página é escrita. Se o sistema operacional escolher substituir a página, o bit de modificação indica se a página precisa ser escrita no disco antes que seu local na memória possa ser cedido a outra página. Logo, uma página modificada normalmente é chamada de “dirty page”.
Tornando a tradução de endereços rápida: a TLB
TLB (Translation-Lookaside Buffer) Uma cache que monitora os mapeamentos de endereços recentemente usados para evitar um acesso à tabela de páginas.
Como as tabelas de páginas são armazenadas na memória principal, cada acesso à memória por um programa pode levar, no mínimo, o dobro do tempo: um acesso à memória para obter o endereço físico e um segundo acesso para obter os dados. O segredo para melhorar o desempenho de acesso é basear-se na localidade da referência à tabela de páginas. Quando uma tradução para um número de página virtual é usada, ela provavelmente será necessária novamente no futuro próximo, pois as referências às palavras nessa página possuem localidade temporal e também espacial. Assim, os processadores modernos incluem uma cache especial que controla as traduções usadas recentemente. Essa cache especial de tradução de endereços é tradicionalmente chamada de TLB (translation-lookaside buffer), embora seria mais correto chamá-la de cache de tradução. A TLB corresponde àquele pequeno pedaço de papel que normalmente usamos para registrar o local de um conjunto de livros que consultamos no catálogo; em vez de pesquisar continuamente o catálogo inteiro, registramos o local de vários livros e usamos o pedaço de papel como uma cache da biblioteca. A Figura 5.23 mostra que cada entrada de tag na TLB contém uma parte do número de página virtual, e cada entrada de dados da TLB contém um número de página física. Como não iremos mais acessar a tabela de páginas a cada referência, em vez disso acessaremos a TLB, que precisará incluir outros bits de status, como o bit de modificação e o bit de referência.
5.4 Memória Virtual 405
FIGURA 5.23 A TLB age como uma cache da tabela de páginas apenas para as entradas que mapeiam as páginas físicas. A TLB contém um subconjunto dos mapeamentos de página virtual para física que estão na tabela de páginas. Os mapeamentos da TLB são mostrados em destaque. Como a TLB é uma cache, ela precisa ter um campo tag. Se não houver uma entrada correspondente na TLB para uma página, a tabela de páginas precisa ser examinada. A tabela de páginas fornece um número de página física para a página (que pode, então, ser usado na construção de uma entrada da TLB) ou indica que a página reside em disco, caso em que ocorre uma falta de página. Como a tabela de páginas possui uma entrada para cada página virtual, nenhum campo tag é necessário; ou seja, ela não é uma cache.
Em cada referência, consultamos o número de página virtual na TLB. Se tivermos um acerto, o número de página física é usado para formar o endereço e o bit de referência correspondente é ligado. Se o processador estiver realizando uma escrita, o bit de modificação também é ligado. Se ocorrer uma falha na TLB, precisamos determinar se ela é uma falta de página ou simplesmente uma falha de TLB. Se a página existir na memória, então a falha de TLB indica apenas que a tradução está faltando. Nesse caso, o processador pode tratar a falha de TLB lendo a tradução da tabela de páginas para a TLB e, depois, tentando a referência novamente. Se a página não estiver presente na memória, então a falha de TLB indica uma falta de página verdadeira. Nesse caso, o processador chama o sistema operacional usando uma exceção. Como a TLB possui muito menos entradas do que o número de páginas na memória principal, as falhas de TLB serão muito mais frequentes do que as faltas de página verdadeiras. As falhas de TLB podem ser tratadas no hardware ou no software. Na prática, com cuidado, pode haver pouca diferença de desempenho entre os dois métodos, uma vez que as operações básicas são iguais nos dois casos. Depois que uma falha de TLB tiver ocorrido e a tradução faltando tiver sido recuperada da tabela de páginas, precisaremos selecionar uma entrada da TLB para substituir. Como os bits de referência e de modificação estão contidos na entrada da TLB, precisamos copiar esses bits de volta para a entrada da tabela de páginas quando substituirmos uma entrada. Esses bits são a única parte da entrada da TLB que pode ser modificada. O uso de write-back – ou seja, copiar de volta essas entradas no momento da falha e não quando são escritas – é muito eficiente, já que esperamos que a taxa de falhas da TLB seja pequena. Alguns sistemas usam outras técnicas para aproximar os bits de referência e de modificação, eliminando a necessidade de escrever na TLB exceto para carregar uma nova entrada da tabela em caso de falha. Alguns valores comuns para uma TLB poderiam ser: j
Tamanho da TLB: 16 a 512 entradas.
j
Tamanho do bloco: uma a duas entradas da tabela de páginas (geralmente 4 a 8 bytes cada uma).
406
Capítulo 5 Grande e Rápida: Explorando a Hierarquia de Memória
j
Tempo de acerto: 0,5 a 1 ciclo de clock
j
Penalidade de falha: 10 a 100 ciclos de clock
j
Taxa de falhas: 0,01% a 1%
Os projetistas têm usado uma ampla gama de associatividades em TLBs. Alguns sistemas usam TLBs pequenas e totalmente associativas porque um mapeamento totalmente associativo possui uma taxa de falhas mais baixa; além disso, como a TLB é pequena, o custo de um mapeamento totalmente associativo não é tão alto. Outros sistemas usam TLBs grandes, normalmente com pequena associatividade. Com um mapeamento totalmente associativo, escolher a entrada a ser substituída se torna difícil, pois é muito caro implementar um esquema de LRU de hardware. Além do mais, como as falhas de TLB são muito mais frequentes do que as faltas de página e, portanto, precisam ser tratadas de modo mais econômico, não podemos utilizar um algoritmo de software caro, como para as falhas. Como resultado, muitos sistemas fornecem algum suporte para escolher aleatoriamente uma entrada a ser substituída. Veremos os esquemas de substituição mais detalhadamente na Seção 5.5. A TLB do Intrinsity FastMATH
Para ver essas ideias em um processador real, vamos dar uma olhada mais de perto na TLB do Intrinsity FastMATH. O sistema de memória usa páginas de 4KB e um espaço de endereçamento de 32 bits; portanto, o número de página virtual tem 20 bits de largura, como no alto da Figura 5.24. O endereço físico é do mesmo tamanho do endereço virtual. A TLB contém 16 entradas, é totalmente associativa e é compartilhada entre as referências de instruções e de dados. Cada entrada possui 64 bits de largura e contém uma tag de 20 bits (que é o número de página virtual para essa entrada de TLB), o número de página física correspondente (também 20 bits), um bit de validade, um bit de modificação e outros bits de contabilidade. A Figura 5.24 mostra a TLB e uma das caches, enquanto a Figura 5.25 mostra as etapas no processamento de uma requisição de leitura ou escrita. Quando ocorre uma falha de TLB, o hardware MIPS salva o número de página da referência em um registrador especial e gera uma exceção. A exceção chama o sistema operacional, que trata a falha no software. Para encontrar o endereço físico da página ausente, a rotina de falha de TLB indexa a tabela de páginas usando o número de página do endereço virtual e o registrador de tabela de páginas, que indica o endereço inicial da tabela de páginas do processo ativo. Usando um conjunto especial de instruções de sistema que podem atualizar a TLB, o sistema operacional coloca o endereço físico da tabela de páginas na TLB. Uma falha de TLB leva cerca de 13 ciclos de clock, considerando que o código e a entrada da tabela de páginas estejam na cache de instruções e na cache de dados, respectivamente. (Veremos o código TLB MIPS posteriormente neste capítulo). Uma falta de página verdadeira ocorre se a entrada da tabela de páginas não possuir um endereço físico válido. O hardware mantém um índice que indica a entrada recomendada a ser substituída, escolhida aleatoriamente. Existe uma complicação extra para requisições de escrita: o bit de acesso de escrita na TLB precisa ser verificado. Esse bit impede que o programa escreva em páginas para as quais tenha apenas acesso de leitura. Se o programa tentar uma escrita e o bit de acesso de escrita estiver desligado, uma exceção é gerada. O bit de acesso de escrita faz parte do mecanismo de proteção, que abordaremos em breve.
Integrando memória virtual, TLBs e caches Nossos sistemas de memória virtual e de cache funcionam em conjunto como uma hierarquia, de modo que os dados não podem estar na cache a menos que estejam presentes na memória principal. O sistema operacional desempenha um importante papel na manutenção dessa hierarquia removendo o conteúdo de qualquer página da cache quando
5.4 Memória Virtual 407
FIGURA 5.24 A TLB e a cache implementam o processo de ir de um endereço virtual para um item de dados no Intrinsity FastMATH. Essa figura mostra a organização da TLB e a cache de dados considerando um tamanho de página de 4KB. Este diagrama focaliza uma leitura; a Figura 5.25 descreve como tratar escritas. Repare que, diferente da Figura 5.9, as RAMs de tag e de dados são divididas. Endereçando a longa, mas estreita, RAM de dados com o índice de cache concatenado com o offset de bloco, selecionamos a palavra desejada no bloco sem um multiplexador 16:1. Embora a cache seja diretamente mapeada, a TLB é totalmente associativa. A implementação de uma TLB totalmente associativa exige que toda tag TLB seja comparada com o número de página virtual, já que a entrada desejada pode estar em qualquer lugar na TLB. (Ver memórias endereçáveis por conteúdo na seção “Detalhamento” na seção “Localizando um bloco na cache”.) Se o bit de validade da entrada correspondente estiver ligado, o acesso será um acerto de TLB e os bits do número de página física acrescidos aos bits do offset de página formarão o índice usado para acessar a cache.
decide migrar essa página para o disco. Ao mesmo tempo, o sistema operacional modifica as tabelas de páginas e a TLB de modo que uma tentativa de acessar quaisquer dados na página migrada gere uma falta de página. Sob as circunstâncias ideais, um endereço virtual é traduzido pela TLB e enviado para a cache em que os dados apropriados são encontrados, recuperados e devolvidos ao processador. No pior caso, uma referência pode falhar em todos os três componentes da hierarquia de memória: a TLB, a tabela de páginas e a cache. O exemplo a seguir ilustra essas interações em mais detalhes.
408
Capítulo 5 Grande e Rápida: Explorando a Hierarquia de Memória
FIGURA 5.25 Processando uma leitura ou uma escrita direta na TLB e na cache do Intrinsity FastMATH. Se a TLB gerar um acerto, a cache pode ser acessada com o endereço físico resultante. Para uma leitura, a cache gera um acerto ou uma falha e fornece os dados ou causa um stall enquanto os dados são trazidos da memória. Se a operação for uma escrita, uma parte da entrada de cache é substituída por um acerto e os dados são enviados ao buffer de escrita se considerarmos uma cache write-through. Uma falha de escrita é exatamente como uma falha de leitura exceto que o bloco é modificado após ser lido da memória. Uma cache write-back requer que as escritas liguem um bit de modificação para o bloco de cache; além disso, um buffer de escrita é carregado com o bloco inteiro apenas em uma falha de leitura ou falha de escrita se o bloco a ser substituído estiver com o bit de modificação ligado. Observe que um acerto de TLB e um acerto de cache são eventos independentes, mas um acerto de cache só pode ocorrer após um acerto de TLB, o que significa que os dados precisam estar presentes na memória. A relação entre as falhas de TLB e as falhas de cache é examinada mais a fundo no exemplo a seguir e nos exercícios no final do capítulo.
Operação geral de uma hierarquia de memória
EXEMPLO
RESPOSTA
Em uma hierarquia de memória como a da Figura 5.24, que inclui uma TLB e uma cache organizada como mostrado, uma referência de memória pode encontrar três tipos de falhas diferentes: uma falha de TLB, uma falta de página e uma falha de cache. Considere todas as combinações desses três eventos com uma ou mais ocorrendo (sete possibilidades). Para cada possibilidade, diga se esse evento realmente pode ocorrer e sob que circunstâncias. A Figura 5.26 mostra as circunstâncias possíveis e se elas podem ou não surgir na prática.
5.4 Memória Virtual 409
FIGURA 5.26 As possíveis combinações de eventos na TLB, no sistema de memória virtual e na cache. Três dessas combinações são impossíveis e uma é possível (acerto de TLB, acerto de memória virtual, falha de cache), mas nunca detectada.
Detalhamento: A Figura 5.26 considera que todos os endereços de memória são traduzidos para endereços físicos antes que a cache seja acessada. Nessa organização, a cache é fisicamente indexada e fisicamente rotulada (tanto o índice quanto a tag de cache são endereços físicos em vez de virtuais). Nesse sistema, a quantidade de tempo para acessar a memória, considerando um acerto de cache, precisa acomodar um acesso de TLB e um acesso de cache; naturalmente, esses acessos podem ser em pipeline. Como alternativa, o processador pode indexar a cache com um endereço que seja completa ou parcialmente virtual. Isso é chamado de cache virtualmente endereçada e usa tags que são endereços virtuais; portanto, esse tipo de cache é virtualmente indexado e virtualmente rotulado. Nessas caches, o hardware de tradução de endereço (TLB) não é usado durante o acesso de cache normal, já que a cache é acessada com um endereço virtual que não foi traduzido para um endereço físico. Isso tira a TLB do caminho crítico, reduzindo a latência da cache. Quando ocorre uma falha de cache, no entanto, o processador precisa traduzir o endereço para um endereço físico de modo que ele possa buscar o bloco de cache da memória principal. Quando a cache é acessada com um endereço virtual e páginas são compartilhadas entre programas (que podem acessá-las com diferentes endereços virtuais), há a possibilidade de aliasing. O aliasing ocorre quando o mesmo objeto possui dois nomes – nesse caso, dois endereços virtuais para a mesma página. Essa ambiguidade cria um problema porque uma palavra nessa página pode ser colocada na cache em dois locais diferentes, cada um correspondendo a diferentes endereços virtuais. Essa ambiguidade permitiria que um programa escrevesse os dados sem que o outro programa soubesse que eles foram mudados. As caches endereçadas completamente por endereços virtuais apresentam limitações de projeto na cache e na TLB para reduzir o aliasing ou exigem que o sistema operacional (e possivelmente o usuário) tome ações para garantir que o aliasing não ocorra. Uma conciliação comum entre esses dois pontos de projeto são as caches virtualmente indexadas (algumas vezes, usando apenas a parte do offset de página do endereço, que é um endereço físico, já que não é traduzida), mas usam tags físicas. Esses projetos, que são virtualmente indexados mas fisicamente rotulados, tentam unir as vantagens de desempenho das caches virtualmente indexadas às vantagens da arquitetura mais simples de uma cache fisicamente endereçada. Por exemplo, não existe qualquer problema de aliasing nesse caso. A Figura 5.24 considerou um tamanho de página de 4KB, mas na realidade ela tem 16KB, de modo que o Intrinsity FastMATH pode usar esse truque. Para isso, é preciso haver uma cuidadosa coordenação entre o tamanho de página mínimo, o tamanho da cache e a associatividade.
cache virtualmente endereçada Uma cache acessada com um endereço virtual em vez de um endereço físico.
aliasing Uma situação em que o mesmo objeto é acessado por dois endereços; pode ocorrer na memória virtual quando existem dois endereços virtuais para a mesma página física.
cache fisicamente endereçada Uma cache endereçada por um endereço físico.
410
Capítulo 5 Grande e Rápida: Explorando a Hierarquia de Memória
Implementando proteção com memória virtual Uma das funções mais importantes da memória virtual é permitir o compartilhamento de uma única memória principal por diversos processos, enquanto fornece proteção de memória entre esses processos e o sistema operacional. O mecanismo de proteção precisa garantir que, embora vários processos estejam compartilhando a mesma memória principal, um processo rebelde não pode escrever no espaço de endereçamento de outro processo do usuário ou no sistema operacional, intencionalmente ou não. O bit de acesso de escrita na TLB pode proteger uma página de ser escrita. Sem esse nível de proteção, os vírus de computador seriam ainda mais comuns.
Interface hardware/ software modo supervisor Também chamado de modo de kernel. Um modo que indica que um processo executado é um processo do sistema operacional.
chamada ao sistema Uma instrução especial que transfere o controle do modo usuário para um local dedicado no estágio de código supervisor, chamando o mecanismo de exceção no processo.
Para permitir que o sistema operacional implemente proteção no sistema de memória virtual, o hardware precisa fornecer pelo menos três capacidades básicas resumidas a seguir. 1. Suportar pelo menos dois modos que indicam se o processo em execução é de usuário ou de sistema operacional, normalmente chamado de processo supervisor, processo de kernel ou processo executivo. 2. Fornecer uma parte do estado do processador que um processo de usuário pode ler mas não escrever. Isso inclui o bit de modo usuário/supervisor, que determina se o processador está no modo usuário ou supervisor, o ponteiro para a tabela de páginas e a TLB. Para escrever esses elementos, o sistema operacional usa instruções especiais que só estão disponíveis no modo supervisor. 3. Fornecer mecanismos pelos quais o processador pode passar do modo usuário para o modo supervisor e vice-versa. A primeira direção normalmente é conseguida por uma exceção de chamada ao sistema, implementada como uma instrução especial (syscall no conjunto de instruções MIPS) que transfere o controle para um local dedicado no espaço de código supervisor. Como em qualquer outra exceção, o contador de programa do ponto da chamada de sistema é salvo no PC de exceção (EPC), e o processador é colocado no modo supervisor. Para retornar ao modo usuário da exceção, use a instrução return from exception (ERET), que retorna ao modo usuário e desvia para o endereço no EPC. Usando esses mecanismos e armazenando as tabelas de páginas no espaço de endereçamento do sistema operacional, o sistema operacional pode mudar as tabelas de páginas enquanto impede que um processo do usuário as modifique, garantindo que um processo do usuário só possa acessar o armazenamento fornecido pelo sistema operacional.
Também queremos evitar que um processo leia os dados de outro processo. Por exemplo, não desejamos que o programa de um aluno leia as notas enquanto elas estiverem na memória do processador. Uma vez que começamos a compartilhar a memória principal, precisamos fornecer a capacidade de um processo proteger seus dados de serem lidos e escritos por outro processo; caso contrário, a memória principal será um poço de permissividade! Lembre-se de que cada processo possui seu próprio espaço de endereçamento virtual. Portanto, se o sistema operacional mantiver as tabelas de páginas organizadas de modo que as páginas virtuais independentes mapeiem as páginas físicas separadas, um processo não será capaz de acessar os dados de outro. É claro que isso exige que um processo de usuário seja incapaz de mudar o mapeamento da tabela de páginas. O sistema operacional pode garantir segurança se ele impedir que o processo do usuário modifique suas próprias tabelas de páginas. No entanto, o sistema operacional precisa ser capaz de modificar as tabelas de páginas. Colocar as tabelas de páginas no espaço de endereçamento protegido do sistema operacional satisfaz a ambos os requisitos.
5.4 Memória Virtual 411
Quando os processos querem compartilhar informações de uma maneira limitada, o sistema operacional precisa assisti-los, já que o acesso às informações de outro processo exige mudar a tabela de páginas do processo que está acessando. O bit de acesso de escrita pode ser usado para restringir o compartilhamento apenas à leitura e, como o restante da tabela de páginas, esse bit pode ser mudado apenas pelo sistema operacional. Para permitir que outro processo, digamos, P1, leia uma página pertencente ao processo P2, P2 pediria ao sistema operacional para criar uma entrada na tabela de páginas para uma página virtual no espaço de endereço de P1 que aponte para a mesma página física que P2 deseja compartilhar. O sistema operacional poderia usar o bit de proteção de escrita a fim de impedir que P1 escrevesse os dados, se esse fosse o desejo de P2. Quaisquer bits que determinam os direitos de acesso a uma página precisam ser incluídos na tabela de páginas e na TLB, pois a tabela de páginas é acessada apenas em uma falha de TLB. Detalhamento: Quando o sistema operacional decide deixar de executar o processo P1 para executar o processo P2 (o que chamamos de troca de contexto ou troca de processo), ele precisa garantir que P2 não possa ter acesso às tabelas de páginas de P1 porque isso comprometeria a proteção. Se não houver uma TLB, basta mudar o registrador de tabela de páginas de modo que aponte para a tabela de páginas de P2 (em vez da de P1); com uma TLB, precisamos limpar as entradas de TLB que pertencem a P1 – tanto para proteger os dados de P1 quanto para forçar a TLB a carregar as entradas para P2. Se a taxa de troca de processos fosse alta, isso poderia ser bastante ineficiente. Por exemplo, P2 poderia carregar apenas algumas entradas de TLB antes que o sistema operacional trocasse novamente para P1. Infelizmente, P1, então, descobriria que todas as suas entradas de TLB desapareceram e precisaria pagar falhas de TLB para recarregá-las. Esse problema ocorre porque os endereços virtuais usados por P1 e P2 são iguais e precisamos limpar a TLB a fim de evitar confundir esses endereços. Uma alternativa comum é estender o espaço de endereçamento virtual acrescentando um identificador de processo ou identificador de tarefa. O Intrinsity FastMATH possui um campo ID do espaço de endereçamento (ASID) de 8 bits para essa finalidade. Esse pequeno campo identifica o processo que está atualmente sendo executado; ele é mantido em um registrador carregado pelo sistema operacional quando muda de processo. O identificador de processo é concatenado com a parte da tag da TLB, de modo que um acerto de TLB ocorra apenas se o número de página e o identificador de processo corresponderem. Essa combinação elimina a necessidade de limpar a TLB, exceto em raras ocasiões. Problemas semelhantes podem ocorrer para uma cache, já que, em uma troca de processo, a cache conterá dados do processo em execução. Esses problemas surgem de diferentes maneiras para caches física e virtualmente endereçadas; além disso, uma variedade de soluções diferentes, como identificadores de processo, são usadas para garantir que um processo obtenha seus próprios dados.
Tratando falhas de TLB e faltas de página Embora a tradução de endereços físicos para virtuais com uma TLB seja simples quando temos um acerto de TLB, o tratamento de falhas de TLB e de faltas de página é mais complexo. Uma falha de TLB ocorre quando nenhuma entrada na TLB corresponde a um endereço virtual. Uma falha de TLB pode indicar uma de duas possibilidades: 1. A página está presente na memória e precisamos apenas criar a entrada de TLB ausente. 2. A página não está presente na memória e precisamos transferir o controle para o sistema operacional a fim de lidar com uma falta de página. Como saber qual dessas duas circunstâncias ocorreu? Quando processarmos a falha de TLB, iremos procurar uma entrada na tabela de páginas para ser trazida para a TLB. Se a entrada na tabela de páginas correspondente tiver um bit de validade que esteja desligado, a página correspondente não está na memória e temos uma falta de página em vez de uma
troca de contexto Uma mudança no estado interno do processador para permitir que um processo diferente use o processador, o que inclui salvar o estado necessário e retornar ao processo sendo atualmente executado.
412
Capítulo 5 Grande e Rápida: Explorando a Hierarquia de Memória
simples falha de TLB. Se o bit de validade estiver ligado, podemos simplesmente recuperar a entrada desejada. Uma falha de TLB pode ser tratada por software ou por hardware, pois ela exigirá apenas uma curta sequência de operações que copia uma entrada válida da tabela de páginas da memória para a TLB. O MIPS tradicionalmente trata uma falha de TLB por software. Ele traz a entrada da tabela de páginas da memória e, depois, executa novamente a instrução que causou a falha de TLB. Na reexecução, ele terá um acerto de TLB. Se a entrada da tabela de páginas indicar que a página não está na memória, dessa vez ele terá uma exceção de falta de página. Tratar uma falha de TLB ou uma falta de página requer o uso do mecanismo de exceção para interromper o processo ativo, transferir o controle ao sistema operacional e, depois, retomar a execução do processo interrompido. Uma falta de página será reconhecida em algum momento durante o ciclo de clock usado para acessar a memória. A fim de reiniciar a instrução após a falta de página ser tratada, o contador de programa da instrução que causou a falta de página precisa ser salvo. Assim como no Capítulo 4, o contador de programa de exceção (EPC) é usado para conter esse valor. Além disso, uma falha de TLB ou uma exceção de falta de página precisa ser sinalizada no final do mesmo ciclo de clock em que ocorre o acesso à memória, de modo que o próximo ciclo de clock começará o processamento da exceção em vez de continuar a execução normal das instruções. Se a falta de página não fosse reconhecida nesse ciclo de clock, uma instrução load poderia substituir um registrador, e isso poderia ser desastroso quando tentássemos reiniciar a instrução. Por exemplo, considere a instrução lw $1,0($1): o computador precisa ser capaz de impedir que o estágio de escrita do resultado do pipeline ocorra; caso contrário, ele não poderia reiniciar corretamente a instrução, já que o conteúdo de $1 teria sido destruído. Uma complicação parecida surge nos stores. Precisamos impedir que a escrita na memória realmente seja concluída quando há uma falta de página; isso normalmente é feito desativando a linha de controle de escrita para a memória.
FIGURA 5.27 Registradores de controle MIPS. Considera-se que estes estejam no coprocessador 0, e por isso são lidos com mfc0 e escritos com mtc0.
Interface hardware/ software habilitar exceção Também chamado de “habilitar interrupção”. Uma ação ou sinal que controla se o processo responde ou não a uma exceção; necessário para evitar a ocorrência de exceções durante intervalos antes que o processador tenha seguramente salvado o estado necessário para a reinicialização.
Entre o momento em que começamos a executar o tratamento de exceção no sistema operacional e o momento em que o sistema operacional salvou todo o estado do processo, o sistema operacional se torna particularmente vulnerável. Por exemplo, se outra exceção ocorresse quando estivéssemos processando a primeira exceção no sistema operacional, a unidade de controle substituiria o contador de programa de exceção, tornando impossível voltar para a instrução que causou a falta de página! Podemos evitar esse desastre fornecendo a capacidade de desabilitar e habilitar exceções. Assim que uma exceção ocorre, o processador liga um bit que desabilita todas as outras exceções; isso poderia acontecer ao mesmo tempo em que o processador liga o bit de modo supervisor. O sistema operacional, então, salva o estado apenas suficiente para lhe permitir se recuperar se outra exceção ocorrer – a saber, os registradores do contador de programa de exceção (EPC) e Cause. EPC e Cause são dois dos registradores de controle especiais que ajudam com exceções, falhas de TLB e faltas de página; a Figura 5.27 mostra o restante.
5.4 Memória Virtual 413
O sistema operacional, então, pode habilitar novamente as exceções. Essas etapas asseguram que as exceções não façam com que o processador perca qualquer estado e, portanto, sejam incapazes de reiniciar a execução da instrução interruptora.
Uma vez que o sistema operacional conhece o endereço virtual que causou a falta de página, ele precisa completar três etapas: 1. Consultar a entrada de tabela de páginas usando o endereço virtual e encontrar o local em disco da página referenciada. 2. Escolher uma página física a ser substituída; se a página escolhida estiver com o bit de modificação ligado, ela precisará ser escrita no disco antes que possamos definir uma nova página virtual para essa página física. 3. Iniciar uma leitura de modo a trazer a página referenciada do disco para a página física escolhida. É claro que essa última etapa levará milhões de ciclos de clock de processador (assim como a segunda, se a página substituída estiver com o bit de modificação ligado); portanto, o sistema operacional normalmente selecionará outro processo para executar no processador até que o acesso ao disco seja concluído. Como o sistema operacional salvou o estado do processo, ele pode passar o controle do processador à vontade para outro processo. Quando a leitura da página do disco está completa, o sistema operacional pode restaurar o estado do processo que causou originalmente a falta de página e executar a instrução que retorna da exceção. Essa instrução irá redefinir o processador do modo kernel para o modo usuário, bem como restaurar o contador de programa. O processo do usuário, então, reexecuta a instrução que causou a falta de página, acessa a página requisitada com sucesso e continua a execução. As exceções de falta de página para acessos a dados são difíceis de implementar corretamente em um processador devido a uma combinação de três características: 1. Elas ocorrem no meio das instruções, diferente das faltas de página de instruções. 2. A instrução não pode ser completada antes que a exceção seja tratada. 3. Após tratar a exceção, a instrução precisa ser reinicializada como se nada tivesse ocorrido. Tornar instruções reinicializáveis, de modo que a exceção possa ser tratada e a instrução possa ser continuada, é relativamente fácil em uma arquitetura como o MIPS. Como cada instrução escreve apenas um item de dados e essa escrita ocorre no final do ciclo da instrução, podemos simplesmente impedir que a instrução seja concluída (não escrevendo) e reinicializar a instrução no começo. Vejamos o MIPS mais de perto. Quando uma falha de TLB ocorre, o hardware do MIPS salva o número de página da referência em um registrador especial chamado BadVAddr e gera uma exceção. A exceção chama o sistema operacional, que trata a falha por software. O controle é transferido para o endereço 8000 0000hexa (o local do handler da falha de TLB). A fim de encontrar o endereço físico para a página ausente, a rotina de falha de TLB indexa a tabela de páginas usando o número de página do endereço virtual e o registrador de tabela de páginas, que indica o endereço inicial da tabela de páginas do processo ativo. Para tornar essa indexação rápida, o hardware do MIPS coloca tudo que você precisa no registrador especial Context: os 12 bits mais significativos têm o endereço da base da tabela de páginas e os próximos 18 bits têm o endereço virtual da página ausente. Como cada entrada de tabela de páginas possui uma palavra, os últimos dois bits são 0. Portanto, as duas primeiras instruções copiam o registrador Context para o registrador temporário do kernel $k1 e, depois, carregam a entrada de tabela de páginas desse endereço em $k1. Lembre-se de
instrução reinicializável Uma instrução que pode retomar a execução após uma exceção ser resolvida sem que a exceção afete o resultado da instrução.
handler Nome de uma rotina de software chamada para “tratar” uma exceção ou interrupção.
414
Capítulo 5 Grande e Rápida: Explorando a Hierarquia de Memória
que $k0 e $k1 são reservados para uso do sistema operacional sem salvamento; um importante motivo dessa convenção é tornar rápido o handler de falha de TLB. A seguir está o código MIPS para um handler de falha de TLB típico:
Como mostrado anteriormente, o MIPS possui um conjunto especial de instruções de sistema que atualiza a TLB. A instrução tlbwr copia o registrador de controle EntryLo para a entrada de TLB selecionada pelo registrador de controle Random. Random implementa uma substituição aleatória e, portanto, é basicamente um contador de execução livre. Uma falha de TLB leva cerca de 12 ciclos de clock. Observe que o handler de falha de TLB não verifica se a entrada de tabela de páginas é válida. Como a exceção para a entrada de TLB ausente é muito mais frequente do que uma falta de página, o sistema operacional carrega a TLB da tabela de páginas sem examinar a entrada e reinicializa a instrução. Se a entrada for inválida, ocorre outra exceção diferente, e o sistema operacional reconhece a falta de página. Esse método torna rápido o caso frequente de uma falha de TLB, com uma pequena penalidade de desempenho para o raro caso de uma falta de página. Uma vez que o processo que gerou a falta de página tenha sido interrompido, ele transfere o controle para 8000 0180hexa, um endereço diferente do handler de falha de TLB. Esse é o endereço geral para exceção; a falha de TLB possui um ponto de entrada especial que reduz a penalidade para uma falha de TLB. O sistema operacional usa o registrador Cause de exceção a fim de diagnosticar a causa da exceção. Como a exceção é uma falta de página, o sistema operacional sabe que será necessário um processamento extenso. Portanto, diferente de uma falha de TLB, ele salva todo o estado do processo ativo. Esse estado inclui todos os registradores de uso geral e de ponto flutuante, o registrador de endereço de tabela de páginas, o EPC e o registrador Cause de exceção. Como os handlers de exceção normalmente não usam os registradores de ponto flutuante, o ponto de entrada geral não os salva, deixando isso para os poucos handlers que precisam deles. A Figura 5.28 esboça o código MIPS de um handler de exceção. Note que salvamos e restauramos o estado no código MIPS, tomando cuidado quando habilitamos e desabilitamos exceções, mas chamamos código C para tratar a exceção em particular. O endereço virtual que causou a falta de página depende se essa foi uma falta de instruções ou de dados. O endereço da instrução que gerou a falta está no EPC. Se ela fosse uma falta de página de instruções, o EPC contém o endereço virtual da página que gerou a falta; caso contrário, o endereço virtual que gerou a falta pode ser calculado examinando a instrução (cujo endereço está no EPC) para encontrar o registrador base e o campo offset. Detalhamento: Essa versão simplificada considera que o stack pointer (sp) é válido. Para não mapeada Uma parte do espaço de endereçamento que não pode ter faltas de página.
evitar o problema de uma falta de página durante esse código de exceção de baixo nível, o MIPS separa uma parte do seu espaço de endereçamento que não pode ter faltas de página, chamada não mapeada (unmapped). O sistema operacional insere o código para o ponto de entrada do tratamento de exceções e a pilha de exceção na memória não mapeada. O hardware MIPS traduz os endereços virtuais 8000 0000hexa a BFFF FFFFhexa para endereços físicos simplesmente ignorando os bits superiores do endereço virtual, colocando, assim, esses endereços na parte inferior da memória física. Portanto, o sistema operacional coloca os pontos de entrada dos tratamentos de exceções e as pilhas de exceção na memória não mapeada.
Detalhamento: O código na Figura 5.28 mostra a sequência de retorno da exceção do MIPS-32. O MIPS-I usa rfe e jr em vez de eret.
5.4 Memória Virtual 415
FIGURA 5.28 Código MIPS para salvar e restaurar o estado em uma exceção.
Detalhamento: Para processadores com instruções mais complexas, que podem tocar em muitos locais de memória e escrever muitos itens de dados, tornar as instruções reiniciáveis é muito mais difícil. Processar uma instrução pode gerar uma série de faltas de página no meio da instrução. Por exemplo, os processadores x86 possuem instruções de movimento em bloco que tocam em milhares de palavras de dados. Nesses processadores, as instruções normalmente não podem ser reiniciadas desde o início, como fazemos para instruções MIPS. Em vez disso, a instrução precisa ser interrompida e mais tarde continuada no meio de sua execução. Retomar uma instrução no meio de sua execução normalmente exige salvar algum estado especial, processar a exceção e restaurar esse estado especial. Para que isso seja feito corretamente, é preciso haver uma coordenação cuidadosa e detalhada entre o código de tratamento de exceção no sistema operacional e o hardware.
Resumo Memória virtual é o nome para o nível da hierarquia de memória que controla a cache entre a memória principal e o disco. A memória virtual permite que um único programa expanda seu espaço de endereçamento para além dos limites da memória principal. Mais
416
Capítulo 5 Grande e Rápida: Explorando a Hierarquia de Memória
importante, a memória virtual suporta o compartilhamento da memória principal entre vários processos simultaneamente ativos, de uma maneira protegida. Gerenciar a hierarquia de memória entre a memória principal e o disco é uma tarefa difícil devido ao alto custo das faltas de página. Várias técnicas são usadas para reduzir a taxa de falhas: 1. As páginas são ampliadas para tirar proveito da localidade espacial e para reduzir a taxa de falhas. 2. O mapeamento entre endereços virtuais e endereços físicos, que é implementado com uma tabela de páginas, é feito totalmente associativo para que uma página virtual possa ser colocada em qualquer lugar na memória principal. 3. O sistema operacional usa técnicas, como LRU e um bit de referência, para escolher que páginas substituir. Como as gravações no disco são caras, a memória virtual usa um esquema write-back e também monitora se uma página foi modificada (usando um bit de modificação) para evitar gravar páginas não alteradas novamente no disco. O mecanismo de memória virtual fornece tradução de endereços de um endereço virtual usado pelo programa para o espaço de endereçamento físico usado no acesso à memória. Essa tradução de endereços permite compartilhamento protegido da memória principal e oferece várias vantagens adicionais, como a simplificação da alocação de memória. Para garantir que os processos sejam protegidos uns dos outros, é necessário que apenas o sistema operacional possa mudar as traduções de endereços, o que é implementado impedindo que programas de usuário alterem as tabelas de páginas. O compartilhamento controlado das páginas entre processos pode ser implementado com a ajuda do sistema operacional e dos bits de acesso na tabela de páginas que indicam se o programa do usuário possui acesso de leitura ou escrita à página. Se um processador precisasse acessar uma tabela de páginas residente na memória para traduzir cada acesso, a memória virtual seria muito dispendiosa e a cache não teria sentido! Em vez disso, uma TLB age como uma cache para traduções da tabela de páginas. Os endereços são, então, traduzidos do virtual para o físico usando as traduções na TLB. As caches, a memória virtual e as TLBs se baseiam em um conjunto comum de princípios e políticas. A próxima seção aborda essa estrutura comum.
Entendendo o desempenho dos programas
Embora a memória virtual tenha sido criada para permitir que uma memória pequena aja como uma grande, a diferença de desempenho entre o disco e a memória significa que se um programa acessa rotineiramente mais memória virtual do que a memória física que possui, sua execução será muito lenta. Esse programa estaria continuamente trocando páginas entre a memória e o disco, o que chamamos de thrashing. O thrashing, embora raro, é um desastre quando ocorre. Se seu programa realiza thrashing, a solução mais fácil é executá-lo em um computador com mais memória ou comprar mais memória para o computador. Uma opção mais complexa é reexaminar suas estruturas de dados e algoritmo para ver se você pode mudar a localidade e, portanto, reduzir o número de páginas que seu programa usa simultaneamente. Esse conjunto de páginas é informalmente chamado de working set. Um problema de desempenho mais comum são as falhas de TLB. Como uma TLB pode tratar apenas de 32 a 64 entradas de página ao mesmo tempo, um programa poderia facilmente ver uma alta taxa de falhas de TLB, já que o processador pode acessar menos de um quarto de megabyte diretamente: 64 × 4KB = 0,25MB. Por exemplo, as falhas de TLB normalmente são um problema para o Radix Sort. A fim de tentar amenizar esse problema, a maioria das arquiteturas de computadores agora suporta tamanhos de página variáveis. Por exemplo, além da página de 4KB padrão, o hardware do MIPS suporta
5.5 Uma estrutura comum para hierarquias de memória 417
páginas de 16KB, 64KB, 256KB, 1MB, 4MB, 16MB, 64MB e 256MB. Consequentemente, se um programa usa grandes tamanhos de página, ele pode acessar mais memória diretamente sem falhas de TLB. Na prática, o problema é fazer o sistema operacional permitir que os programas selecionem esses tamanhos de página maiores. Mais uma vez, a solução mais complexa para reduzir as falhas de TLB é reexaminar as estruturas de dados e os algoritmos no sentido de reduzir o working set de páginas; dada a importância dos acessos à memória para o desempenho e a frequência de falhas de TLB, alguns programas com grandes working sets foram recriados com esse objetivo.
Associe o elemento da hierarquia de memória à esquerda com a frase correspondente à direita. 1. Cache L1
a. Uma cache para uma cache.
2. Cache L2
b. Uma cache para discos.
3. Memória principal
c. Uma cache para uma memória principal.
4. TLB
d. Uma cache para entradas de tabela de páginas.
Verifique você mesmo
Uma estrutura comum para hierarquias
5.5 de memória
Agora você reconhece que os diferentes tipos de hierarquias de memória compartilham muita coisa em comum. Embora muitos aspectos das hierarquias de memória difiram quantitativamente, muitas das políticas e recursos que determinam como uma hierarquia funciona são semelhantes em qualidade. A Figura 5.29 mostra como algumas características quantitativas das hierarquias de memória podem diferir. No restante desta seção, discutiremos os aspectos operacionais comuns das hierarquias de memória e como determinar seu comportamento. Examinaremos essas políticas como uma série de questões que se aplicam entre quaisquer dos níveis de uma hierarquia de memória, embora usemos principalmente terminologia de caches por motivo de simplicidade.
FIGURA 5.29 Os principais parâmetros quantitativos do projeto que caracterizam os principais elementos da hierarquia de memória em um computador. Estes são valores típicos para esses níveis em 2008. Embora o intervalo de valores seja grande, isso ocorre parcialmente porque muitos dos valores que mudaram com o tempo estão relacionados; por exemplo, à medida que as caches se tornam maiores para contornar maiores penalidades de falha, os tamanhos de bloco também crescem.
Questão 1: onde um bloco pode ser colocado? Vimos que o posicionamento de bloco no nível superior da hierarquia pode utilizar diversos esquemas, do diretamente mapeado ao associativo por conjunto e ao totalmente associativo. Como já dissemos, toda essa faixa de esquemas pode ser imaginada como variações em um
418
Capítulo 5 Grande e Rápida: Explorando a Hierarquia de Memória
esquema associativo por conjunto no qual o número de conjuntos e o número de blocos por conjunto variam: Nome do esquema
Número de conjuntos
Blocos por conjunto
Mapeamento Direto
Número de blocos na cache
1
Associativo por conjunto
Número de blocos na cache Associatividade
Associatividade (normalmente 2 a 16)
Totalmente associativo
1
Número de blocos na cache
A vantagem de aumentar o grau de associatividade é que normalmente isso diminui a taxa de falhas. A melhoria da taxa de falhas deriva da redução das falhas que disputam o mesmo local. Examinaremos essas falhas mais detalhadamente em breve. Antes, vejamos quanta melhoria é obtida. A Figura 5.30 mostra as taxas de falhas para diversos tamanhos de cache enquanto a associatividade varia de mapeamento direto para a associatividade por conjunto de duas vias, o que produz uma redução de 20% a 30% na taxa de falhas. Conforme crescem os tamanhos de cache, a melhoria relativa da associatividade aumenta apenas ligeiramente; como a perda geral de uma cache maior é menor, a oportunidade de melhorar a taxa de falhas diminui e a melhoria absoluta na taxa de falhas da associatividade é reduzida significativamente. As possíveis desvantagens da associatividade, como já mencionado, são o custo mais alto e o tempo de acesso mais longo.
FIGURA 5.30 As taxas de falhas da cache de dados para cada um dos oito tamanhos melhora à medida que a associatividade aumenta. Embora o benefício de passar de associação por conjunto de uma via (mapeamento direto) para de duas vias seja significativo, os benefícios de maior associatividade são menores (por exemplo, 1%-10% de melhoria passando de duas vias para quatro vias contra 20%-30% de melhoria passando de uma via para duas vias). Há ainda menos melhoria ao passar de quatro vias para oito vias, que, por sua vez, é muito próximo das taxas de falhas de uma cache totalmente associativa. As caches menores obtêm um benefício absoluto muito maior com a associatividade, pois a taxa de falhas básica de uma cache pequena é maior. A Figura 5.15 explica como esses dados foram coletados.
Questão 2: como um bloco é encontrado? A escolha de como localizamos um bloco depende do esquema de posicionamento do bloco, já que isso determina o número de locais possíveis. Poderíamos resumir os esquemas da seguinte maneira: Associatividade
Método de localização
Mapeamento Direto
Indexação
Comparações necessárias 1
Associativo por conjunto
Indexação do conjunto, pesquisa entre os elementos
Grau de associatividade
Total
Pesquisa de todas as entradas de cache
Tamanho da cache
Tabela de consulta separada
0
5.5 Uma estrutura comum para hierarquias de memória 419
A escolha entre os métodos mapeamento direto, associativo por conjunto ou totalmente associativo em qualquer hierarquia de memória dependerá do custo de uma falha comparado com o custo de implementar a associatividade, ambos em termos de tempo e de hardware extra. Incluir a cache L2 no chip permite uma associatividade muito mais alta, pois os tempos de acerto não são tão importantes, e o projetista não precisa se basear nos chips SRAM padrão como blocos de construção. As caches totalmente associativas são proibitivas exceto para pequenos tamanhos, nos quais o custo dos comparadores não é grande e as melhorias da taxa de falhas absoluta são as melhores. Nos sistemas de memória virtual, uma tabela de mapeamento separada (a tabela de páginas) é mantida para indexar a memória. Além do armazenamento necessário para a tabela, usar um índice exige um acesso extra à memória. A escolha da associatividade total para o posicionamento de página e da tabela extra é motivada pelos seguintes fatos: 1. A associatividade total é benéfica, já que as falhas são muito caras. 2. A associatividade total permite que softwares usem esquemas sofisticados de substituição projetados para reduzir a taxa de falhas. 3. O mapa completo pode ser facilmente indexado sem a necessidade de pesquisa e de qualquer hardware extra. Portanto, os sistemas de memória virtual quase sempre usam posicionamento totalmente associativo. O posicionamento associativo por conjunto é muitas vezes usado para caches e TLBs, no qual o acesso combina indexação e a pesquisa de um conjunto pequeno. Alguns sistemas têm usado caches com mapeamento direto devido às suas vantagens no tempo de acesso e da simplicidade. A vantagem no tempo de acesso ocorre porque a localização do bloco requisitado não depende de uma comparação. Essas escolhas de projeto dependem de muitos detalhes da implementação, como se a cache é on-chip, a tecnologia usada para implementar a cache e o papel vital do tempo de acesso na determinação do tempo de ciclo do processador.
Questão 3: que bloco deve ser substituído em uma falha de cache? Quando uma falha ocorre em uma cache associativa, precisamos decidir que bloco substituir. Em uma cache totalmente associativa, todos os blocos são candidatos à substituição. Se a cache for associativa por conjunto, precisamos escolher entre os blocos do conjunto. É claro que a substituição é fácil em uma cache diretamente mapeada porque existe apenas um candidato. Existem duas principais estratégias para substituição nas caches associativas por conjunto ou totalmente associativas: j
Substituição aleatória: os blocos candidatos são selecionados aleatoriamente, talvez usando alguma assistência do hardware. Por exemplo, o MIPS suporta substituição aleatória para falhas de TLB.
j
Substituição LRU (Least Recently Used): o bloco substituído é o que não foi usado há mais tempo.
Na prática, o LRU é muito oneroso de ser implementado para hierarquias com mais do que um pequeno grau de associatividade (geralmente, dois a quatro), já que é oneroso controlar o uso das informações. Mesmo para a associatividade por conjunto de quatro vias, o LRU normalmente é aproximado – por exemplo, monitorando qual par de blocos é o LRU (o que requer 1 bit) e, depois, monitorando que bloco em cada par é o LRU (o que requer 1 bit por par).
420
Capítulo 5 Grande e Rápida: Explorando a Hierarquia de Memória
Para maior associatividade, ou o LRU é aproximado ou a substituição aleatória é usada. Nas caches, o algoritmo de substituição está no hardware, o que significa que o esquema deve ser fácil de implementar. A substituição aleatória é simples de construir em hardware e, para uma cache associativa por conjunto de duas vias, a substituição aleatória possui uma taxa de falhas cerca de 1,1 vez mais alta do que a substituição LRU. Conforme as caches se tornam maiores, a taxa de falhas para as duas estratégias de substituição cai e a diferença absoluta se torna pequena. Na verdade, a substituição aleatória, algumas vezes, pode ser melhor do que as aproximações simples de LRU que são facilmente implementadas em hardware. Na memória virtual, alguma forma de LRU é sempre aproximada, já que mesmo uma pequena redução na taxa de falhas pode ser importante quando o custo de uma falha é enorme. Os bits de referência ou funcionalidade equivalente costumam ser fornecidos para facilitar que o sistema operacional monitore um conjunto de páginas usadas menos recentemente. Como as falhas são muito caras e relativamente raras, é aceitável aproximar essa informação, em especial, em nível de software.
Questão 4: o que acontece em uma escrita? Uma importante característica de qualquer hierarquia de memória é como ela lida com as escritas. Já vimos as duas opções básicas: j
Write-through: as informações são escritas no bloco da cache e no bloco do nível inferior da hierarquia de memória (memória principal para uma cache). As caches na Seção 5.2 usaram esse esquema.
j
Write-back: as informações são escritas apenas no bloco da cache. O bloco modificado é escrito no nível inferior da hierarquia apenas quando ele é substituído. Os sistemas de memória virtual sempre usam write-back, pelas razões explicadas na Seção 5.4.
Tanto write-back quanto write-through têm suas vantagens. As principais vantagens do write-back são as seguintes: j
As palavras individuais podem ser escritas pelo processador na velocidade em que a cache, não a memória, pode aceitar.
j
Diversas escritas dentro de um bloco exigem apenas uma escrita no nível inferior da hierarquia.
j
Quando blocos são escritos com write-back, o sistema pode fazer uso efetivo de uma transferência de alta largura de banda, já que o bloco inteiro é escrito.
O write-through possui estas vantagens: j
As falhas são mais simples e baratas porque nunca exigem que um bloco seja escrito de volta no nível inferior.
j
O write-through é mais fácil de ser implementado do que o write-back, embora, para ser prática, uma cache write-through precisaria usar um buffer de escrita.
Em sistemas de memória virtual, apenas uma política write-back é viável devido à longa latência de uma escrita no nível inferior da hierarquia (o disco). A taxa em que as escritas são geradas por um processador excederá a taxa em que o sistema de memória pode processá-las, até mesmo permitindo memórias física e logicamente mais largas. Como consequência, cada vez mais caches estão usando uma estratégia write-back.
5.5 Uma estrutura comum para hierarquias de memória 421
Embora as caches, as TLBs e a memória virtual inicialmente possam parecer muito diferentes, elas se baseiam nos mesmos dois princípios de localidade e podem ser entendidos examinando como lidam com quatro questões: Questão 1:
Onde um bloco pode ser colocado?
Resposta:
Em um local (mapeamento direto), em alguns locais (associatividade por conjunto) ou em qualquer local (associatividade total).
Questão 2:
Como um bloco é encontrado?
Resposta:
Existem quatro métodos: indexação (como em uma cache diretamente mapeada), pesquisa limitada (como em uma cache associativa por conjunto), pesquisa completa (como em uma cache totalmente associativa) e tabela de consulta separada (como em uma tabela de páginas).
Questão 3:
Que bloco é substituído em uma falha?
Resposta:
Em geral, o bloco usado menos recentemente ou um bloco aleatório.
Questão 4:
Como as escritas são tratadas?
Resposta:
Cada nível na hierarquia pode usar write-through ou write-back.
em
Colocando perspectiva
modelo dos três Cs Um modelo
Os Três Cs: um modelo intuitivo para entender o comportamento das hierarquias de memória Nesta seção, vamos examinar um modelo que esclarece as origens das falhas em uma hierarquia de memória e como as falhas serão afetadas por mudanças na hierarquia. Explicaremos as ideias em termos de caches, embora elas se apliquem diretamente a qualquer outro nível na hierarquia. Nesse modelo, todas as falhas são classificadas em uma de três categorias (os três Cs): j
Falhas compulsórias: são falhas de cache causadas pelo primeiro acesso a um bloco que nunca esteve na cache. Também são chamadas de falhas de partida a frio.
j
Falhas de capacidade: são falhas de cache causadas quando a cache não pode conter todos os blocos necessários durante a execução de um programa. As falhas de capacidade ocorrem quando os blocos são substituídos e, depois, recuperados.
j
Falhas de conflito: são falhas de cache que ocorrem em caches associativas por conjunto ou diretamente mapeadas quando vários blocos disputam o mesmo conjunto. As falhas de conflito são aquelas falhas em uma cache diretamente mapeada ou associativa por conjunto que são eliminadas em uma cache totalmente associativa do mesmo tamanho. Essas falhas de cache também são chamadas de falhas de colisão.
A Figura 5.31 mostra como a taxa de falhas se divide nas três origens. Essas origens de falhas podem ser diretamente atacadas mudando algum aspecto do projeto da cache. Como as falhas de conflito surgem diretamente da disputa pelo mesmo bloco de cache, aumentar a associatividade reduz as falhas de conflito. Entretanto, a associatividade pode aumentar o tempo de acesso, levando a um menor desempenho geral.
de cache em que todas as falhas são classificadas em uma de três categorias: falhas compulsórias, falhas de capacidade e falhas de conflito.
falha compulsória Também chamada de falha de partida a frio. Uma falha de cache causada pelo primeiro acesso a um bloco que nunca esteve na cache.
falha de capacidade Uma falha de cache que ocorre porque a cache, mesmo com associatividade total, não pode conter todos os blocos necessários para satisfazer à requisição. falha de conflito Também chamada de falha de colisão. Uma falha de cache que ocorre em uma cache associativa por conjunto ou diretamente mapeada quando vários blocos competem pelo mesmo conjunto e que são eliminados em uma cache totalmente associativa do mesmo tamanho.
422
Capítulo 5 Grande e Rápida: Explorando a Hierarquia de Memória
FIGURA 5.31 A taxa de falhas pode ser dividida em três origens de falha. Esse gráfico mostra a taxa de falhas total e seus componentes para uma faixa de tamanhos de cache. Esses dados são para os benchmarks de inteiro e ponto flutuante do SPEC2000 e são da mesma fonte dos dados na Figura 5.30. O componente da falha compulsória é de 0,006% e não pode ser visto nesse gráfico. O próximo componente é a taxa de falhas de capacidade, que depende do tamanho da cache. A parte do conflito, que depende da associatividade e do tamanho da cache, é mostrada para uma faixa de associatividades, de uma via a oito vias. Em cada caso, a seção rotulada corresponde ao aumento na taxa de falhas que ocorre quando a associatividade é alterada do próximo grau mais alto para o grau de associatividade rotulado. Por exemplo, a seção rotulada como duas vias indica as falhas adicionais surgindo quando o cache possui associatividade de dois em vez de quatro. Portanto, a diferença na taxa de falhas incorrida por uma cache diretamente mapeada em relação a uma cache totalmente associativa do mesmo tamanho é dada pela soma das seções rotuladas como oito vias, quatro vias, duas vias e uma via. A diferença entre oito vias e quatro vias é tão pequena que mal pode ser vista nesse gráfico.
As falhas de capacidade podem facilmente ser reduzidas aumentando a cache; na verdade, as caches de segundo nível têm se tornado constantemente maiores durante muitos anos. É claro que, quando tornamos a cache maior, também precisamos ser cautelosos quanto ao aumento no tempo de acesso, que pode levar a um desempenho geral mais baixo. Por isso, as caches de primeiro nível cresceram lentamente ou nem isso. Como as falhas compulsórias são geradas pela primeira referência a um bloco, a principal maneira de um sistema de cache reduzir o número de falhas compulsórias é aumentando o tamanho do bloco. Isso irá reduzir o número de referências necessárias para tocar cada bloco do programa uma vez, porque o programa consistirá em menos blocos de cache. Como já dissemos, aumentar demais o tamanho do bloco pode ter um efeito negativo sobre o desempenho devido ao aumento na penalidade de falha. A decomposição das falhas nos três Cs é um modelo qualitativo útil. Nos projetos de cache reais, muitas das escolhas de projeto interagem, e mudar uma característica de cache frequentemente afetará vários componentes da taxa de falhas. Apesar dessas deficiências, esse modelo é uma maneira útil de adquirir conhecimento sobre o desempenho dos projetos de cache.
em
Colocando perspectiva
A dificuldade de projetar hierarquias de memória é que toda mudança que melhore potencialmente a taxa de falhas também pode afetar negativamente o desempenho geral, como mostra a Figura 5.32. Essa combinação de efeitos positivos e negativos é o que torna o projeto de uma hierarquia de memória interessante.
5.6 Máquinas virtuais 423
FIGURA 5.32 Dificuldades do projeto de hierarquias de memória.
Quais das seguintes afirmativas (se houver) normalmente são verdadeiras? 1. Não há um meio de reduzir as falhas compulsórias. 2. As caches totalmente associativas não possuem falhas de conflito. 3. Na redução de falhas, a associatividade é mais importante do que a capacidade.
5.6 Máquinas virtuais Uma ideia quase tão antiga relacionada à memória virtual é a das máquinas virtuais (VM — Virtual Machines). Elas foram desenvolvidas inicialmente em meados da década de 1960, e continuaram sendo uma parte importante da computação de mainframe no decorrer dos anos. Embora bastante ignoradas no domínio dos computadores monousuários nas décadas de 1980 e 1990, elas recentemente ganharam popularidade devido a: j
A importância crescente do isolamento e da segurança nos sistemas modernos.
j
As falhas na segurança e na confiabilidade dos sistemas operacionais padrão.
j
O compartilhamento de um único computador entre muitos usuários não relacionados.
j
Os aumentos fantásticos na velocidade bruta dos processadores no decorrer das décadas, o que torna o overhead das VMs mais aceitável.
A definição mais geral das VMs inclui basicamente todos os métodos de emulação que oferecem uma interface de software padrão, como a Java VM. Nesta seção, estamos interessados nas VMs que oferecem um ambiente completo em nível de sistema, no nível da arquitetura de conjunto de instruções (ISA) binária. Embora algumas VMs excutem diferentes ISAs na VM do hardware nativo, consideramos que elas sempre correspondem ao hardware. Essas VMs são chamadas de (Operating) System Virtual Machines — máquinas virtuais do sistema (operacional). Alguns exemplos são IBM VM/370, VMware ESX Server e Xen. As máquinas virtuais do sistema apresentam a ilusão de que os usuários têm um computador inteiro para si, incluindo uma cópia do sistema operacional. Um único computador executa várias VMs e pode aceitar diversos sistemas operacionais (OSs) diferentes. Em uma plataforma convencional, um único OS “possui” todos os recursos do hardware, mas, com uma VM, vários OSs compartilham os recursos do hardware. O software que dá suporte às VMs é chamado de monitor de máquina virtual (VMM — Virtual Machine Monitor), ou hipervisor; o VMM é o centro da tecnologia de máquina virtual. A plataforma de hardware básica é chamada de host, e seus recursos são compartilhados entre as VMs guest. O VMM determina como mapear recursos virtuais a recursos físicos: um recurso físico pode ser de tempo compartilhado, particionado ou
Verifique você mesmo
424
Capítulo 5 Grande e Rápida: Explorando a Hierarquia de Memória
ainda simulado no software. O VMM é muito menor que um OS tradicional; a parte de isolamento de um VMM possui talvez apenas 10.000 linhas de código. Embora nosso interesse aqui seja as VMs para melhorar a proteção, elas oferecem dois outros benefícios que são comercialmente significativos: 1. Gerenciar o software. As VMs oferecem uma abstração que pode executar uma pilha de software completa, incluindo até mesmo sistemas operacionais antigos, como o DOS. Uma implantação típica poderia ser algumas VMs executando OSs legados, muitas executando a versão atual estável do OS, e algumas testando a próxima versão do OS. 2. Gerenciar o hardware. Um motivo para servidores múltiplos é ter cada aplicação executando com a versão compatível do sistema operacional em computadores separados, pois essa separação pode melhorar a confiabilidade. As VMs permitem que essas pilhas de software separadas sejam executadas independentemente enquanto compartilham o hardware, consolidando assim o número de servidores. Outro exemplo é que alguns VMMs admitem a migração de uma VM atual para um computador diferente, seja no sentido de balancear a carga ou sair do hardware com falha. Em geral, o custo da virtualização do processador depende da carga de trabalho. Os programas ligados ao processador em nível de usuário possuem overhead de virtualização zero, pois o OS raramente é chamado, de modo que tudo é executado nas velocidades nativas. As cargas de trabalhos com uso intenso de E/S em geral usam intensamente o OS, executando muitas chamadas do sistema e instruções privilegiadas, o que pode resultar em um alto overhead de virtualização. Por outro lado, se a carga de trabalho com uso intenso de E/S também for voltada para E/S, o custo da virtualização do processador pode ser completamente ocultado, pois o processador geralmente está ocioso, esperando pela E/S. O overhead é determinado pelo número de instruções que devem ser simuladas pelo VMM e por quanto tempo cada uma precisa simular. Logo, quando as VMs guest executam a mesma ISA que o host, como consideramos aqui, o objetivo da arquitetura e do VMM é executar quase todas as instruções diretamente no hardware nativo.
Requisitos de um monitor de máquina virtual O que um monitor de VM precisa fazer? Ele apresenta uma interface de software ao software guest, precisa isolar o estado dos guests um do outro e precisa proteger-se contra o software guest (incluindo os OSs guest). Os requisitos qualitativos são: j
O software guest deverá se comportar em uma VM exatamente como se estivesse sendo executado no hardware nativo, exceto pelo comportamento relacionado ao desempenho ou limitações de recursos fixos compartilhados por múltiplas VMs.
j
O software guest não deverá alterar diretamente a alocação de recursos reais do sistema.
Para “virtualizar” o processador, o VMM precisa controlar praticamente tudo — acesso ao estado privilegiado, tradução de endereços, E/S, exceções e interrupções — embora a VM guest e o OS atualmente em execução estejam temporariamente utilizando-os. Por exemplo, no caso de uma interrupção de um temporizador, a VMM suspenderia a VM guest atualmente em execução, salvaria seu estado, trataria da interrupção, determinaria qual VM guest será executada em seguida e depois carregaria seu estado. As VMs guest que contam com uma interrupção de temporizador recebem um temporizador virtual e uma interrupção de temporizador simulada pelo VMM. Para estar no controle, o VMM precisa estar em um nível de privilégio mais alto que a VM guest, que geralmente é executada no modo usuário; isso também garante que a execução de qualquer instrução privilegiada será tratada pelo VMM. Os requisitos básicos das máquinas virtuais do sistema são quase idênticos aos da memória virtual paginada, listados anteriormente:
5.6 Máquinas virtuais 425
j
Pelo menos dois modos de processador, sistema e usuário.
j
Um subconjunto de instruções privilegiado, que está disponível apenas no modo do sistema, resultado em um trap se executado no modo usuário; todos os recursos do sistema precisam ser controláveis apenas por meio dessas instruções.
(Falta de) suporte da arquitetura do conjunto de instruções para máquinas virtuais Se as VMs forem planejadas durante o projeto da ISA, será relativamente fácil reduzir o número de instruções que devem ser executadas por um VMM e sua velocidade de simulação. Uma arquitetura que permite que a VM seja executada diretamente no hardware recebe o título de virtualizável, e a arquitetura IBM 370 orgulhosamente ostenta esse rótulo. Infelizmente, como as VMs foram consideradas para aplicações de servidor baseadas em desktop e PC apenas recentemente, a maioria dos conjuntos de instruções foi criada sem a virtualização em mente. Esses culpados incluem a x86 e a maioria das arquiteturas RISC, incluindo ARM e MIPS. Como o VMM precisa garantir que o sistema guest só interaja com recursos virtuais, um OS guest convencional é executado como um programa no modo usuário em cima do VMM. Então, se um OS guest tentar acessar ou modificar informações relacionadas aos recursos do hardware por meio de uma instrução privilegiada (por exemplo, lendo ou escrevendo o ponteiro da tabela de páginas), isso será interceptado pelo VMM. O VMM poderá então efetuar as mudanças apropriadas nos recursos reais correspondentes. Portanto, se qualquer instrução que tenta ler ou escrever essas informações sensíveis for interceptada quando executada no modo usuário, o VMM poderá interceptá-la e dar suporte a uma versão virtual da informação sensível, conforme o OS guest espera. Na ausência desse suporte, outras medidas deverão ser tomadas. Um VMM precisa tomar precauções especiais para localizar todas as instruções problemáticas e garantir que elas se comportem corretamente quando executadas por um OS guest, aumentando assim a complexidade do VMM e reduzindo o desempenho da execução da VM.
Proteção e arquitetura do conjunto de instruções Proteção é um esforço conjunto da arquitetura e dos sistemas operacionais, mas os arquitetos tiveram de modificar alguns detalhes desajeitados das arquiteturas de conjunto de instruções existentes quando a memória virtual se tornou popular. Por exemplo, para dar suporte à memória virtual no IBM 370, os arquitetos tiveram de mudar a bemsucedida arquitetura do conjunto de instruções do IBM 360, que tinha sido anunciada apenas seis anos antes. Ajustes semelhantes estão sendo feitos hoje para acomodar as máquinas virtuais. Por exemplo, a instrução POPF do x86 carrega os registradores de flag do topo da pilha para a memória. Um dos flags é o flag Interrupt Enable (IE). Se você executar a instrução POPF no modo usuário, em vez de interceptá-la, ela simplesmente muda todos os flags exceto IE. No modo do sistema, ela muda o IE. Como um OS guest é executado no modo usuário dentro de uma VM, isso é um problema, pois espera ver um flag IE alterado. Historicamente, o hardware mainframe IBM e o VMM exigiam três etapas para melhorar o desempenho das máquinas virtuais: 1. Reduzir o custo da virtualização do processador. 2. Reduzir o custo de overhead da interrupção devido à virtualização. 3. Reduzir o custo da interrupção direcionando as interrupções para a VM apropriada sem chamar o VMM. Em 2006, novas propostas da AMD e Intel tentaram resolver o primeiro ponto, reduzindo o custo da virtualização do processador. Será interessante ver quantas gerações de arquitetura e modificações do VMM serão necessárias para resolver todos os três pontos,
426
Capítulo 5 Grande e Rápida: Explorando a Hierarquia de Memória
e quanto tempo passará antes que as máquinas virtuais do século XXI sejam tão eficientes quanto os mainframes IBM e VMMs da década de 1970. Detalhamento: Além de virtualizar o conjunto de instruções, outro desafio é a virtualização da memória virtual, à medida que cada OS guest em cada VM gerencia seu próprio conjunto de tabelas de página. Para que isso funcione, o VMM separa as noções de memória real e física (que geralmente são tratadas como sendo sinônimas), e torna a memória real um nível separado, intermediário entre a memória virtual e a memória física. (Alguns utilizam os termos memória virtual, memória física e memória de máquina para indicar os mesmos três níveis.) O OS guest mapeia a memória virtual para a memória real por meio de suas tabelas de página, e as tabelas de página do VMM mapeiam a memória real do guest para a memória física. A arquitetura da memória virtual é especificada ou por tabelas de página, como no IBM VM/370 e no x86, ou pela estrutura de TLB, como no MIPS. Em vez de pagar um nível extra de indireção em cada acesso à memória, o VMM mantém uma tabela de páginas de sombra que é mapeada diretamente a partir do espaço de endereços virtuais do guest para o espaço de endereços físicos do hardware. Detectando todas as modificações na tabela de página do guest, o VMM pode garantir que as entradas da tabela de página de sombra sendo usadas pelo hardware para traduções corresponda às do ambiente do OS guest, com a exceção das páginas físicas corretas substituídas pelas páginas reais nas tabelas do guest. Logo, o VMM precisa interceptar qualquer tentativa pelo OS guest de alterar sua tabela de páginas ou de acessar o ponteiro da tabela de páginas. Isso normalmente é feito protegendo as tabelas de página do guest e interceptando-se qualquer acesso ao ponteiro da tabela de páginas por um OS guest. Conforme observamos anteriormente, o segundo acontece naturalmente se o acesso ao ponteiro da tabela de páginas for uma operação privilegiada. A última parte da arquitetura a ser virtualizada é a E/S. Essa, com certeza, é a parte mais difícil da virtualização do sistema, devido ao número cada vez maior de dispositivos de E/S conectados ao computador e a crescente diversidade dos tipos de dispositivo de E/S. Outra dificuldade é o compartilhamento de um dispositivo real entre diversas VMs, além do suporte dos inúmeros drivers de dispositivo que são exigidos, especialmente se os diferentes OSs guest tiverem suporte no mesmo sistema de VM. A ilusão de VM pode ser mantida dando-se a cada VM versões genéricas de cada tipo de driver de dispositivo de E/S, e depois deixando para o VMM a tarefa de tratar da E/S real.
Usando uma máquina de estado finito
5.7 para controlar uma cache simples
Agora, podemos implementar o controle para uma cache, assim como implementamos o controle para os caminhos de dados de único ciclo e em pipeline, no Capítulo 4. Esta seção começa com uma definição de uma cache simples e depois uma descrição das máquinas de estado finito (MEF). Ela termina com a MEF de um controlador para essa cache simples. A Seção 5.9 no site entra em mais detalhes, mostrando a cache e o controlador em uma nova linguagem de descrição de hardware.
Uma cache simples Vamos projetar um controlador para uma cache simples. Aqui estão as principais características da cache: j
Cache mapeada diretamente.
j
Write-back usando alocação de escrita.
j
O tamanho do bloco é de 4 palavras (16 bytes ou 128 bits).
5.7 Usando uma máquina de estado finito para controlar uma cache simples 427
j
O tamanho da cache é de 16KB, de modo que ela mantém 1024 blocos.
j
Endereços de byte de 32 bits.
j
A cache inclui um bit de validade e um bit de modificação por bloco.
Pela Seção 5.2, podemos agora calcular os campos de um endereço para a cache: j
O índice da cache tem 10 bits.
j
O offset do bloco tem 4 bits.
j
O tamanho da tag tem 32 - (10 + 4) ou 18 bits.
Os sinais entre o processador e a cache são: j
1 bit de sinal Read ou Write.
j
1 bit de sinal Valid, dizendo se existe uma operação de cache ou não.
j
32 bits de endereço.
j
32 bits de dados do processador à cache.
j
32 bits de dados da cache ao processador.
j
1 bit de sinal Ready, dizendo que a operação da cache está completa.
Observe que essa é uma cache de bloqueio, pois o processador precisa esperar até que a cache tenha terminado a solicitação. A interface entre a memória e a cache tem os mesmos campos que entre o processador e a cache, exceto que os campos de dados agora têm 128 bits de largura. A largura de memória extra geralmente é encontrada nos microprocessadores de hoje, que lida com palavras de 32 bits ou 64 bits no processador, enquanto o controlador da DRAM normalmente tem 128 bits. Fazer com que o bloco de cache combine com a largura da DRAM simplificou o projeto. Aqui estão os sinais: j
1 bit de sinal Read ou Write.
j
1 bit de sinal Valid, dizendo se existe uma operação de memória ou não.
j
32 bits de endereço.
j
128 bits de dados da cache à memória.
j
128 bits de dados da memória à cache.
j
1 bit de sinal Ready, dizendo que a operação de memória está completa.
Observe que a interface para a memória não é um número fixo de ciclos. Consideramos um controlador de memória que notificará a cache por meio do sinal Ready quando a leitura ou escrita na memória terminar. Antes de descrever o controlador de cache, precisamos revisar as máquinas de estados finitos, o que nos permite controlar uma operação que pode utilizar múltiplos ciclos de clock.
Máquinas de estados finitos A fim de projetar a unidade de controle para o caminho de dados de único ciclo, usamos um conjunto de tabelas verdade que especificava a configuração dos sinais de controle com base na classe de instrução. Para uma cache, o controle é mais complexo porque a operação pode ser uma série de etapas. O controle para uma cache precisa especificar os sinais a serem definidos em qualquer etapa e a próxima etapa na sequência. O método de controle multietapas mais comum é baseado em máquinas de estados finitos, que normalmente são representadas graficamente. Uma máquina de estado finito
máquina de estados finitos Uma função lógica sequencial consistindo em um conjunto de entradas e saídas, uma função de próximo estado que mapeia o estado atual e as entradas para um novo estado, e uma função de saída que mapeia o estado atual e possivelmente as entradas para um conjunto de saídas ativas.
428
função de próximo estado Uma função combinacional que, dadas as entradas e o estado atual, determina o próximo estado de uma máquina de estados finitos.
Capítulo 5 Grande e Rápida: Explorando a Hierarquia de Memória
consiste em um conjunto de estados e instruções sobre como alterar os estados. As instruções são definidas por uma função de próximo estado, que mapeia o estado atual e as entradas de um novo estado. Quando usamos uma máquina de estado finito para controle, cada estado também especifica um conjunto de saídas que são declaradas quando a máquina está nesse estado. A implementação de uma máquina de estados finitos normalmente considera que todas as saídas que não estão declaradas explicitamente têm as declarações retiradas. De modo semelhante, a operação correta do caminho de dados depende do fato de que um sinal que não é declarado explicitamente tem a declaração retirada, em vez de atuar como um don’t care. Os controles multiplexadores são ligeiramente diferentes, pois selecionam uma das entradas, sejam elas 0 ou 1. Assim, na máquina de estado finito, sempre especificamos a definição de todos os controles multiplexadores com que nos importamos. Quando implementamos a máquina de estado finito com lógica, a definição de um controle como 0 pode ser o default e, portanto, pode não exigir quaisquer portas lógicas. Um exemplo simples de uma máquina de estados finitos aparece no Apêndice C, e se você não estiver familiarizado com o conceito de uma máquina de estado finito, pode querer examinar o Apêndice C antes de prosseguir. Uma máquina de estado finito pode ser implementada com um registrador temporário que mantém o estado atual e um bloco de lógica combinatória que determina os sinais do caminho de dados a serem declarado e o próximo estado. A Figura 5.33 Apêndice D descreve mostra como essa implementação poderia se parecer. O detalhadamente como a máquina de estados finitos é implementada usando essa esSeção C.3, a lógica de controle combinacional para uma máquina de trutura. Na estados finitos é implementada com uma ROM (Read-Only Memory) e uma PLA (Programmable Logic Array). (Veja também no Apêndice C uma descrição desses elementos lógicos.)
FIGURA 5.33 Controladores da máquina de estados finitos normalmente são implementados com um bloco de lógica combinacional e um registrador para manter o estado atual. As saídas da lógica combinacional são o número do próximo estado e os sinais de controle a serem declarados para o estado atual. As saídas da lógica combinacional são o estado atual e quaisquer entradas usadas para determinar o próximo estado. Nesse caso, as entradas são os bits de opcode do registrador de instrução. Observe que, na máquina de estados finitos utilizada neste capítulo, as saídas dependem apenas do estado atual, e não das entradas. A seção Detalhamento explica isso em minúcias.
5.7 Usando uma máquina de estado finito para controlar uma cache simples 429
Detalhamento: O estilo da máquina de estados finitos neste livro é chamado de máquinas de Moore, em homenagem a Edward Moore. Sua característica identificadora é que a saída depende apenas do estado atual. Com uma máquina de Moore, a caixa rotulada como lógica de controle combinacional pode ser dividida em duas partes. Uma parte tem a saída de controle e apenas a entrada de estado, enquanto a outra tem apenas a saída do próximo estado. Um estilo alternativo de máquina é uma máquina de Mealy, em homenagem a George Mealy. A máquina de Mealy permite que a entrada e o estado atual sejam usados para determinar a saída. As máquinas de Moore possuem vantagens de implementação em potencial na velocidade e no tamanho da unidade de controle. As vantagens na velocidade ocorrem porque as saídas de controle, que são necessárias cedo no ciclo de clock, não dependem das enApêndice C, quando a implementação dessa tradas, mas somente do estado atual. No máquina de estado finito é levada às portas lógicas, a vantagem do tamanho pode ser vista com clareza. A desvantagem em potencial de uma máquina de Moore é que isso pode exigir estados adicionais. Por exemplo, em situações em que existe uma diferença de um estado entre duas sequências de estados, a máquina de Mealy pode unificar os estados, fazendo com que as saídas dependam das entradas.
MEF para um controlador de cache simples A Figura 5.34 mostra os quatro estados do nosso controlador de cache simples:
FIGURA 5.34 Quatro estados do controlador simples.
j
Ocioso: Esse estado espera uma solicitação de leitura ou escrita válida do processador, que move a MEF para o estado Comparar Tag.
j
Comparar Tag: Como o nome sugere, esse estado testa se a leitura ou escrita solicitada é um acerto ou uma falha. A parte de índice do endereço seleciona a tag a ser comparada. Se ela for válida e a parte de tag do endereço combinar com a tag, é um acerto.
430
Capítulo 5 Grande e Rápida: Explorando a Hierarquia de Memória
Ou os dados são lidos da palavra selecionada ou são escritos na palavra selecionada, e depois o sinal Cache Ready é definido. Se for uma escrita, o bit de modificação é definido como 1. Observe que um acerto de escrita também define o bit de validade e o campo de tag; embora pareça desnecessário, ele é incluído porque a tag é uma única memória, de modo que, para mudar o bit de modificação, também precisamos mudar os campos de validade e tag. Se for um acerto e o bloco for válido, a MEF retorna ao estado ocioso. Uma falha primeiro atualiza a tag de cache e depois vai para o estado Write-Back, se o bloco nesse local tiver um valor de bit de modificação igual a 1, ou para o estado Alocar, se for 0. j
Write-Back: Esse estado escreve o bloco de 128 bits na memória usando o endereço composto da tag e do índice de cache. Continuamos nesse estado esperando pelo sinal Ready da memória. Quando a escrita na memória termina, a MEF vai para o estado Alocar.
j
Alocar: O novo bloco é apanhado da memória. Permanecemos nesse estado aguardando pelo sinal Ready da memória. Quando a leitura da memória termina, a MEF vai para o estado Comparar Tag. Embora pudéssemos ter ido para um novo estado para completar a operação em vez de reutilizar o estado Comparar Tag, existe muita sobreposição, incluindo a atualização da palavra apropriada no bloco se o acesso foi uma escrita.
Esse modelo simples facilmente poderia ser estendido com mais estados, para tentar melhorar o desempenho. Por exemplo, o estado Comparar Tag realiza a comparação e a leitura ou escrita dos dados de cache em um único ciclo de clock. Normalmente, a comparação e acesso à cache são feitos em estados separados, no sentido de tentar melhorar o tempo do ciclo de clock. Outra otimização seria acrescentar um buffer de escrita de modo que pudéssemos salvar o bloco de modificação e depois ler o novo bloco primeiro, de modo que o processador não tenha de esperar por dois acessos à memória em uma falha de modificação. A cache então escreveria o bloco modificado do buffer de escrita enquanto o processador está operando sobre os dados solicitados. A Seção 5.9, no site, possui mais detalhes sobre a MEF, mostrando o controlador completo em uma linguagem de descrição de hardware e um diagrama em blocos desse cache simples.
Paralelismo e hierarquias de memória:
5.8 coerência de cache
Dado que um multiprocessador multicore significa múltiplos processadores em um único chip, esses processadores provavelmente compartilham um espaço de endereçamento físico comum. O caching de dados compartilhados gera um novo problema, pois a visão da memória mantida por dois processadores diferentes é através de suas caches individuais, que, sem quaisquer precauções adicionais, poderiam acabar vendo dois valores diferentes. A Figura 5.35 ilustra o problema e mostra como dois processadores diferentes podem ter dois valores diferentes para o mesmo local. Essa dificuldade geralmente é referenciada como o problema de coerência de cache. Informalmente, poderíamos dizer que um sistema de memória é coerente se qualquer leitura de um item de dados retornar o valor escrito mais recentemente desse item de dados. Essa definição, embora intuitivamente atraente, é vaga e simples; a realidade é muito mais complexa. Essa definição simples contém dois aspectos diferentes do comportamento do sistema de memória, ambos críticos para escrever programas corretos de memória compartilhada. O primeiro aspecto, chamado de coerência, define que valores podem ser retornados por uma leitura. O segundo aspecto, chamado consistência, determina quando um valor escrito será retornado por uma leitura.
5.8 Paralelismo e hierarquias de memória: coerência de cache 431
FIGURA 5.35 O problema de coerência de cache para um único local da memória (X), lido e escrito por dois processadores (A e B). Assumimos inicialmente que nenhuma cache contém a variável e que X tem o valor 0. Também consideramos um cache write-through; um cache write-back acrescenta algumas complicações adicionais, porém semelhantes. Depois que o valor de X foi escrito por A, a cache de A e a memória contêm o novo valor, mas a cache de B não, e se B ler o valor de X, ele receberá 0!
Vejamos primeiro a coerência. Um sistema de memória é coerente se: 1. Uma leitura por um processador P para um local X que segue uma escrita por P a X, sem escritas de X por outro processador ocorrendo entre a escrita e a leitura por P, sempre retorna o valor escrito por P. Assim, na Figura 5.35, se a CPU A tivesse de ler X após a etapa de tempo 3, ela deverá ver o valor 1. 2. Uma leitura por um processador ao local X que segue uma escrita por outro processador a X retorna o valor escrito se a leitura e escrita forem suficientemente separadas no tempo e nenhuma outra escrita em X ocorrer entre os dois acessos. Assim, na Figura 5.35, precisamos de um mecanismo de modo que o valor 0 na cache da CPU B seja substituído pelo valor 1 após a CPU A armazenar 1 na memória no endereço X, na etapa de tempo 3. 3. As escritas no mesmo local são serializadas; ou seja, duas escritas no mesmo local por dois processadores quaisquer são vistas na mesma ordem por todos os processadores. Por exemplo, se a CPU B armazena 2 na memória no endereço X após a etapa de tempo 3, os processadores nunca podem ler o valor no local X como 2 e mais tarde lê-lo como 1. A primeira propriedade simplesmente preserva a ordem do programa — certamente esperamos que essa propriedade seja verdadeira nos processadores de 1 core, por exemplo. A segunda propriedade define a noção do que significa ter uma visão coerente da memória: se um processador pudesse ler continuamente um valor de dados antigo, claramente diríamos que a memória estava incoerente. A necessidade de serialização de escrita é mais sutil, mas igualmente importante. Suponha que não serializássemos as escritas, e o processador P1 escreve no local X seguido por P2 escrevendo no local X. Serializar as escritas garante que cada processador verá a escrita feita por P2 em algum ponto. Se não serializássemos as escritas, pode ser que algum processador veja a escrita de P2 primeiro e depois veja a escrita de P1, mantendo o valor escrito por P1 indefinidamente. O modo mais simples de evitar essas dificuldades é garantir que todas as escritas no mesmo local sejam vistas na mesma ordem; essa propriedade é chamada serialização de escrita.
Esquemas básicos para impor a coerência Em um multiprocessador coerente com a cache, as caches oferecem migração e replicação de itens de dados compartilhados: j
Migração: Um item de dados pode ser movido para uma cache local e usado lá de uma forma transparente. A migração reduz a latência para acessar um item de dados compartilhado que está alocado remotamente e a demanda de largura de banda sobre a memória compartilhado.
432
Capítulo 5 Grande e Rápida: Explorando a Hierarquia de Memória
j
Replicação: Quando os dados compartilhados estão sendo simultaneamente lidos, as caches fazem uma cópia do item de dados na cache local. A replicação reduz a latência de acesso e a disputa por um item de dados compartilhado lido.
É essencial, para o desempenho no acesso aos dados compartilhados, oferecer suporte a essa migração e replicação, de modo que muitos multiprocessadores introduzem um protocolo de hardware que mantém caches coerentes. Os protocolos para manter coerência a múltiplos processadores são chamados de protocolos de coerência de cache. Acompanhar o estado de qualquer compartilhamento de um bloco de dados é essencial para implementar um protocolo coerente com a cache. O protocolo de coerência de cache mais comum é o snooping. Cada cache que tem uma cópia dos dados de um bloco da memória física também tem uma cópia do status de compartilhamento do bloco, mas nenhum estado centralizado é mantido. As caches são todas acessíveis por algum meio de broadcast (um barramento ou rede), e todos os controladores monitoram ou vasculham o meio, a fim de determinar se eles têm ou não uma cópia de um bloco que é solicitado em um acesso ao barramento ou switch. Na próxima seção, explicamos a coerência de cache baseada em snooping conforme implementada com um barramento compartilhado, mas qualquer meio de comunicação que envia falhas de cache por broadcast a todos os processadores pode ser usado para implementar um esquema de coerência baseado em snooping. Esse broadcasting de todas as caches torna os protocolos de snooping simples de implementar, mas também limita sua escalabilidade.
Protocolos de snooping Um método para impor a coerência é garantir que um processador tenha acesso exclusivo a um item de dados antes de escrevê-lo. Esse estilo de protocolo é chamado protocolo de invalidação de escrita, pois invalida as cópias em outras caches em uma escrita. O acesso exclusivo garante que não existe qualquer outra cópia de um item passível de leitura ou escrita quando ocorre a escrita: todas as outras cópias do item em cache são invalidadas. A Figura 5.36 mostra um exemplo de um protocolo de invalidação para um barramento de snooping com caches write-back em ação. Para ver como esse protocolo garante a coerência, considere uma escrita seguida por uma leitura por outro processador: como a escrita requer acesso exclusivo, qualquer cópia mantida pelo processador de leitura precisa ser invalidada (daí o nome do protocolo). Sendo assim, quando ocorre a leitura, ela falha na cache, e esta é forçada a buscar uma nova cópia dos dados. Para uma escrita, exigimos que
FIGURA 5.36 Um exemplo de um protocolo de invalidação atuando sobre um barramento de snooping para um único bloco de cache (X) com caches write-back. Consideramos que nenhuma cache mantém X inicialmente e que o valor de X na memória é 0. O conteúdo da CPU e da memória mostra o valor após o processador e a atividade do barramento terem sido completados. Um espaço em branco indica nenhuma atividade ou nenhuma cópia em cache. Quando ocorre a segunda falha por B, a CPU A responde com o valor cancelando a resposta da memória. Além disso, tanto o conteúdo da cache de B quanto o conteúdo de memória de X são atualizados. Essa atualização de memória, que ocorre quando um bloco se torna compartilhado, simplifica o protocolo, mas é possível acompanhar a posse e forçar o write-back somente se o bloco for substituído. Isso requer a introdução de um estado adicional, chamado “owner” (proprietário), que indica que um bloco pode ser compartilhado, mas o processador que o possui é responsável por atualizar quaisquer outros processadores e memória quando muda o bloco ou o substitui.
5.8 Paralelismo e hierarquias de memória: coerência de cache 433
o processador escrevendo tenha acesso exclusivo, impedindo que qualquer outro processador seja capaz de escrever simultaneamente. Se dois processadores tentarem escrever os mesmos dados simultaneamente, um deles vence a corrida, fazendo com que a cópia do outro processador seja invalidada. Para que o outro processador complete sua escrita, ele precisa obter uma nova cópia dos dados, que agora precisa conter o valor atualizado. Portanto, esse protocolo também impõe a serialização da escrita.
Uma ideia interessante é que o tamanho do bloco desempenha um papel importante na coerência da cache. Por exemplo, considere o caso do snooping em uma cache com um tamanho de bloco de oito palavras, com uma única palavra alternativamente escrita e lida por dois processadores. A maioria dos protocolos troca blocos inteiros entre os processadores, aumentando assim as demandas da largura de banda de coerência. Blocos grandes também podem causar o que é chamado compartilhamento falso: quando duas variáveis compartilhadas não relacionadas estão localizadas no mesmo bloco de cache, o bloco inteiro é trocado entre os processadores, embora os processadores estejam acessando variáveis diferentes. Os programadores e compiladores deverão dispor os dados cuidadosamente para evitar o compartilhamento falso.
Detalhamento: Embora as três propriedades listadas no início desta seção sejam suficientes para garantir a coerência, a questão de quando um valor escrito será visto também é importante. Para ver por que, observe que não podemos exigir que uma leitura de X na Figura 5.35 veja instantaneamente o valor escrito para X por algum outro processador. Se, por exemplo, uma escrita de X em um processador preceder uma leitura de X em outro processador pouco antes, pode ser impossível garantir que a leitura retorne o valor dos dados escritos, pois estes podem nem sequer ter saído do processador nesse ponto. A questão de exatamente quando um valor escrito deverá ser visto por um leitor é definido por um modelo de consistência de memória. Fazemos as duas suposições a seguir. Primeiro, uma escrita não termina (e permite que ocorra a próxima escrita) até que todos os processadores tenham visto o efeito dessa escrita. Em segundo lugar, o processador não muda a ordem de qualquer escrita com relação a qualquer outro acesso à memória. Essas duas condições significam que, se um processador escreve no local X seguido pelo local Y, qualquer processador que vê o novo valor de Y também deve ver o novo valor de X. Essas restrições permitem que o processador reordene as leituras, mas força o processador a terminar uma escrita na ordem do programa. Detalhamento: O problema da coerência de cache para multiprocessadores e E/S (ver Capítulo 6), embora semelhante em origem, tem diferentes características que afetam a solução apropriada. Diferente da E/S, em que múltiplas cópias de dados são um evento raro — a ser evitado sempre que possível —, um programa sendo executado em múltiplos processadores normalmente terá cópias dos mesmos dados em várias caches.
Detalhamento: Além do protocolo de coerência de cache baseado em snooping, em que o status dos blocos compartilhados é distribuído, um protocolo de coerência de cache baseado em diretório mantém o status de compartilhamento de um bloco de memória física em apenas um local, chamado diretório. A coerência baseada em diretório tem um overhead de implementação ligeiramente mais alto que o snooping, mas pode reduzir o tráfego entre as caches e, portanto, se expandir para quantidades maiores de processadores.
Interface hardware/ software compartilhamento falso Quando duas variáveis compartilhadas não relacionadas estão localizadas no mesmo bloco de cache e o bloco inteiro é trocado entre os processadores, embora os processadores estejam acessando variáveis diferentes.
434
Capítulo 5 Grande e Rápida: Explorando a Hierarquia de Memória
Material avançado: implementando 5.9 controladores de cache Esta seção no site mostra como implementar o controle para uma cache, assim como implementamos o controle para os caminhos de dados de único ciclo e caminhos de dados no Capítulo 4. Esta seção começa com uma descrição das máquinas de estados finitos e a implementação de um controlador de cache para uma cache de dados simples, incluindo uma descrição do controlador de cache em uma linguagem de descrição de hardware. Depois, ela entra nos detalhes do exemplo de um protocolo de coerência de cache e das dificuldades na implementação de tal protocolo.
Vida real: as hierarquias de memória 5.10 do AMD Opteron X4 (Barcelona) e Intel Nehalem Nesta seção, veremos a hierarquia de memória de dois microprocessadores modernos: o processador AMD Opteron X4 (Barcelona) e o Intel Nehalem. A Figura 5.37 mostra a fotografia do die do Intel Nehalem, e a Figura 1.9 no Capítulo 1 mostra a fotografia do die do AMD Opteron X4. Ambos possuem caches secundárias e caches terciárias no die do processador principal. Essa integração reduz o tempo de acesso às caches de nível inferior e também reduz o número de pinos do chip, já que não existe necessidade de um barramento para uma cache secundária externa. Ambos possuem controladores de memória on-chip, o que reduz a latência para a memória principal.
FIGURA 5.37 Uma fotografia do die do processador Intel Nehalem com os componentes indicados. Este die de 13,5 por 19,6 mm tem 731 milhões de transistores. Ele contém quatro processadores que possuem, cada um, caches de instrução privados de 32 KB e 32 LKB, e uma cache L2 de 512 KB. Os quatro cores compartilham uma cache L3 de 8 MB. Os dois canais de memória de 128 bits são para a DRAM DDR3. Cada core também possui uma TLB de dois níveis. O controlador de memória agora está no die, de modo que não existe um chip north bridge separado, como no Intel Clovertown.
5.10 Vida real: as hierarquias de memória do AMD Opteron X4 (Barcelona) e Intel Nehalem 435
As hierarquias de memória do Nehalem e do Opteron A Figura 5.38 resume os tamanhos de endereço e as TLBs dos dois processadores. Observe que o AMD Opteron X4 (Barcelona) possui quatro TLBs e que os endereços virtuais e físicos não precisam corresponder ao tamanho da palavra. O X4 implementa apenas 48 dos 64 bits possíveis do seu espaço virtual e 48 dos 64 bits possíveis do seu espaço de endereço físico. O Nehalem tem três TLBs, o endereço virtual é de 48 bits e o endereço físico é de 44 bits.
FIGURA 5.38 Tradução de endereços e hardware TLB para o Intel Nehalem e o AMD Opteron X4. O tamanho da palavra define o tamanho máximo do endereço virtual, mas um processador não precisa usar todos esses bits. Os dois processadores fornecem suporte a páginas grandes, que são usadas para coisas como o sistema operacional ou no mapeamento de um buffer de quadro. O esquema de página grande evita o uso de um grande número de entradas para mapear um único objeto que está sempre presente. O Nehalem admite dois threads com suporte do hardware por core (veja Seção 7.5, no Capítulo 7).
A Figura 5.39 mostra suas caches. Cada processador no X4 tem suas próprias caches de instrução e dados L1 de 64KB e sua própria cache L2 de 512KB. Os quatro processadores compartilham uma única cache L3 de 2MB. O Nehalem tem uma estrutura semelhante, com cada processador tendo suas próprias caches de instrução e dados L1 de 32 KB e sua própria cache L2 de 512KB, e os quatro processadores compartilham uma única cache L3 de 8MB. A Figura 5.40 mostra o CPI, taxas de falha por mil instruções para as caches L1 e L2, e acessos à DRAM por mil instruções para o Opteron X4 executando os benchmarks SPECint 2006. Observe que o CPI e as taxas de falha de cache são altamente correlacionados. O coeficiente de correlação entre o conjunto de CPIs e o conjunto de falhas de L1 por 1000 instruções é 0,97. Embora não tenhamos as falhas de L3 reais, podemos deduzir a eficácia da cache L3 pela redução em acessos à DRAM versus falhas da L2. Embora alguns programas se beneficiem bastante da cache L3 de 2MB — h264avc, hmmer e bzip2 —, a maioria não se beneficia.
Técnicas para reduzir as penalidades de falha Tanto o Nehalem quanto o AMD Opteron X4 possuem otimizações adicionais que permitem reduzir a penalidade de falha. A primeira delas é o retorno da palavra requisitada primeiro em uma falha, como descrito na Seção “Detalhamento” da Seção “Projetando o
436
Capítulo 5 Grande e Rápida: Explorando a Hierarquia de Memória
FIGURA 5.39 Caches de primeiro nível, segundo nível e terceiro nível do Intel Nehalem e do AMD Opteron X4 2356 (Barcelona).
FIGURA 5.40 CPI, taxas de falhas e acessos à DRAM para a hierarquia de memória do Opteron modelo X4 2356 (Barcelona) executando o SPECInt2006. Infelizmente, os contadores de falhas da L3 não funcionaram nesse chip, de modo que só temos acessos à DRAM para deduzir a eficácia da cache L3. Observe que essa figura é para os mesmos sistemas e benchmarks da Figura 1.20, no Capítulo 1.
cache não bloqueante Uma cache que permite que o processador faça referências a ela enquanto a cache está tratando uma falha anterior.
sistema de memória para suportar caches”. Ambos permitem que o processador continue executando instruções que acessam a cache de dados durante uma falha de cache. Essa técnica, chamada cache não bloqueante, é comumente usada quando os projetistas tentam ocultar a latência da falha de cache usando processadores com execução fora de ordem. Eles implementam dois tipos de não bloqueio. Acerto sob falha permite acertos de cache
5.10 Vida real: as hierarquias de memória do AMD Opteron X4 (Barcelona) e Intel Nehalem 437
adicionais durante uma falha, enquanto falha sob acerto permite múltiplas falhas de cache pendentes. O objetivo do primeiro deles é ocultar alguma latência de falha com outro trabalho, enquanto o objetivo do segundo é sobrepor a latência de duas falhas diferentes. A sobreposição de uma grande fração dos tempos de falha para múltiplas falhas pendentes requer um sistema de memória de alta largura de banda, capaz de tratar múltiplas falhas em paralelo. Em sistemas desktop, a memória pode apenas ser capaz de tirar proveito limitado dessa capacidade, mas grandes servidores e multiprocessadores frequentemente possuem sistemas de memória capazes de tratar mais de uma falha pendente em paralelo. Os dois microprocessadores fazem uma pré-busca de instruções e possuem um mecanismo de pré-busca embutido no hardware para acessos a dados. Eles olham um padrão de falhas de dados e usam essas informações para tentar prever o próximo endereço a fim de começar a buscar os dados antes que a falha ocorra. Essas técnicas geralmente funcionam melhor ao acessar arrays em loops. Uma grande dificuldade enfrentada pelos projetistas de cache é suportar processadores como o Nehalem e o Opteron X4, que podem executar mais de uma instrução de acesso à memória por ciclo de clock. Várias requisições podem ser suportadas na cache de primeiro nível por duas técnicas diferentes. A cache pode ser multiporta, permitindo mais de um acesso simultâneo ao mesmo bloco de cache. Entretanto, as caches multiporta normalmente são muito caras, já que as células de RAM em uma memória multiporta precisam ser muito maiores do que as células de porta única. O esquema alternativo é desmembrar a cache em bancos e permitir acessos múltiplos e independentes, desde que sejam a bancos diferentes. A técnica é semelhante à memória principal intercalada (veja a Figura 5.11). A cache de dados L1 do Opteron X4 suporta duas leituras de 128 bits por ciclo de clock e tem oito bancos. O Nehalem e a maioria dos outros processadores seguem a política de inclusão em sua hierarquia de memória. Isso significa que uma cópia de todos os dados nas caches de nível mais alto também pode ser encontrada nas caches de nível inferior. Em contrapartida, os processadores AMD seguem a política de exclusão em sua cache de primeiro e segundo níveis, o que significa que um bloco de cache só pode ser encontrado nas caches de primeiro ou segundo níveis, mas não em ambas. Logo, em uma falha da L1, quando um bloco é apanhado da L2 para a L1, o bloco substituído é enviado de volta à cache L2. As sofisticadas hierarquias de memória desses chips e a grande fração dos dies dedicada às caches e às TLBs mostram o significativo esforço de projeto despendido para tentar diminuir a diferença entre tempos de ciclo de processador e latência de memória. Detalhamento: A cache L3 compartilhada do Opteron X4 nem sempre segue a exclusão. Como os blocos de dados podem ser compartilhados entre diversos processadores na cache L3, ela só remove o bloco de cache de L3 se nenhum outro processador a estiver compartilhando. Logo, o protocolo da cache L3 reconhece se o bloco de cache está ou não sendo compartilhado ou usado somente por um único processador.
Detalhamento: Assim como o Opteron X4 não segue a propriedade convencional de inclusão, ele também tem um relacionamento novo entre os níveis da hierarquia de memória. Em vez de a memória alimentar a cache L2, que, por sua vez, alimenta a cache L1, a cache L2 só mantém dados que foram expulsos da cache L1. Assim, a cache L2 pode ser chamada de cache vítima, pois só mantém blocos deslocados de L1 (“vítimas”). De modo semelhante, a cache L3 é uma cache vítima para a cache L2, só contendo blocos derramados de L2. Se uma falha de L1 não for encontrada na cache L2, mas for encontrada na cache L3, a cache L3 fornece os dados diretamente para a cache L1. Logo, uma falha de L1 pode ser atendida por um acerto de L2, ou um acerto de L3, ou pela memória.
438
Capítulo 5 Grande e Rápida: Explorando a Hierarquia de Memória
5.11 Falácias e armadilhas Como um dos aspectos mais naturalmente quantitativos da arquitetura de um computador, a hierarquia de memória pareceria ser menos vulnerável às falácias e armadilhas. Não só houve muitas falácias propagadas e armadilhas encontradas, mas algumas levaram a grandes resultados negativos. Começamos com uma armadilha que frequentemente pega estudantes em exercícios e exames. Armadilha: esquecer-se de considerar o endereçamento em bytes ou o tamanho de bloco de cache ao simular uma cache. Quando estamos simulando uma cache (manualmente ou por computador), precisamos levar em conta o efeito de um endereçamento em bytes e blocos multipalavra ao determinar para qual bloco de cache um certo endereço é mapeado. Por exemplo, se tivermos uma cache diretamente mapeada de 32 bytes com um tamanho de bloco de 4 bytes, o endereço em bytes 36 é mapeado no bloco 1 da cache, já que o endereço em bytes 36 é o endereço de bloco 9 e (9 mod 8) = 1. Por outro lado, se o endereço 36 for um endereço em palavras, então, ele é mapeado no bloco (36 mod 8) = 4. O problema deve informar claramente a base do endereço. De modo semelhante, precisamos considerar o tamanho do bloco. Suponha que tenhamos uma cache com 256 bytes e um tamanho de bloco de 32 bytes. Em que bloco o endereço em bytes 300 se encontra? Se dividirmos o endereço 300 em campos, poderemos ver a resposta:
O endereço em bytes 300 é o endereço de bloco 300 32 = 9 O número de blocos na cache é 256 32 = 8 O bloco número 9 cai no bloco de cache número (9 mod 8) = 1. Esse erro pega muitas pessoas, incluindo os autores (nos rascunhos anteriores) e instrutores que esquecem se pretendiam que os endereços estivessem em palavras, bytes ou números de bloco. Lembre-se dessa armadilha ao realizar os exercícios. Armadilha: ignorar o comportamento do sistema de memória ao escrever programas ou gerar código em um compilador. Isso poderia facilmente ser escrito como uma falácia: “Os programadores podem ignorar as hierarquias de memória ao escrever código.” Ilustramos com um exemplo usando multiplicação de matrizes, para complementar a comparação de ordenações na Figura 5.18. Aqui está o loop interno da versão da multiplicação de matrizes do Capítulo 3:
5.11 Falácias e armadilhas 439
Quando executado com entradas que são matrizes de dupla precisão de 500 × 500, o tempo de execução de CPU dos loops anteriores em uma CPU MIPS com uma cache secundária de 1MB foi aproximadamente metade da velocidade comparada a quando a ordem dos loops é alterada para k, j, i (de modo que i seja o mais interno)! A única diferença é como o programa acessa a memória e o efeito resultante na hierarquia de memória. Outras otimizações de compilador usando uma técnica chamada blocagem podem resultar em um tempo de execução que é outras quatro vezes mais rápido para esse código! Armadilha: ter menos associatividade em conjunto para uma cache compartilhada que o número de cores ou threads compartilhando essa cache. Sem cuidados adicionais, um programa paralelo sendo executado em 2n processadores ou threads pode facilmente alocar estruturas de dados a endereços que seriam mapeados para o mesmo conjunto de uma cache L2 compartilhada. Se a cache for associativa pelo menos em 2n vias, então esses conflitos acidentais ficam ocultos pelo hardware do programa. Se não, os programadores poderiam enfrentar bugs de desempenho aparentemente misteriosos — na realidade, devido a falhas de conflito L2 — ao migrar de, digamos, um projeto de 16 cores para 32 cores, se ambos utilizarem caches L2 associativas com 16 vias. Armadilha: usar tempo médio de acesso à memória para avaliar a hierarquia de memória de um processador com execução fora de ordem. Se um processador é suspenso durante uma falha de cache, você pode calcular separadamente o tempo de stall de memória e o tempo de execução do processador, e, portanto, avaliar a hierarquia de memória de forma independente usando o tempo médio de acesso à memória (veja página 387). Se o processador continuar executando instruções e puder até sustentar mais falhas de cache durante uma falha de cache, então, a única avaliação precisa da hierarquia de memória é simular o processador com execução fora de ordem juntamente com a hierarquia de memória. Armadilha: estender um espaço de endereçamento acrescentando segmentos sobre um espaço de endereçamento não segmentado. Durante a década de 1970, muitos programas ficaram tão grandes que nem todo o código e dados podiam ser endereçados apenas com um endereço de 16 bits. Os computadores, então, foram revisados para oferecer endereços de 32 bits, quer por meio de um espaço de endereçamento de 32 bits não segmentado (também chamado de espaço de endereçamento plano), quer acrescentando 16 bits de segmento ao endereço de 16 bits existente. De uma perspectiva de marketing, acrescentar segmentos que fossem visíveis ao programador e que forçassem o programador e o compilador a decomporem programas em segmentos podia resolver o problema de endereçamento. Infelizmente, existe problema toda vez que uma linguagem de programação quer um endereço que seja maior do que um segmento, como índices para grandes arrays, ponteiros irrestritos ou parâmetros por referência. Além disso, acrescentar segmentos pode transformar todos os endereços em duas palavras — uma para o número do segmento e outra para o offset do segmento —, causando problemas no uso dos endereços em registradores. Armadilha: implementar um monitor de máquina virtual em uma arquitetura de conjunto de instruções que não foi projetada para ser virtualizável. Muitos arquitetos nas décadas de 1970 e 1980 não tiveram o cuidado de garantir que todas as instruções lendo ou escrevendo informações relacionadas a informações de recurso de hardware fossem privilegiadas. Essa atitude laissez-faire causa problemas para os VMMs em todas essas arquiteturas, incluindo o x86, que usamos aqui como um exemplo.
440
Capítulo 5 Grande e Rápida: Explorando a Hierarquia de Memória
A Figura 5.41 descreve as 18 instruções que causam problemas para a virtualização [Robin e Irvine, 2000]. As duas classes gerais são instruções que: j
Leem os registradores de controle no modo usuário, o que revela que o sistema operacional guest está sendo executado em uma máquina virtual (como POPF, mencionada anteriormente).
j
Verificam a proteção conforme requisitado pela arquitetura segmentada, mas consideram que o sistema operacional está sendo executado no nível de privilégio mais alto.
FIGURA 5.41 Resumo de 18 instruções x86 que causam problemas para a virtualização [Robin e Irvine, 2000]. As cinco primeiras instruções no grupo de cima permitem que um programa no modo usuário leia um registrador de controle, como um registrador da tabela de descritores, sem causar uma interrupção. A instrução de “pop de flags” modifica um registrador de controle com informações sensíveis, mas falha silenciosamente quando está no modo usuário. A verificação de proteção da arquitetura segmentada do x86 é a ruína do grupo inferior, pois cada uma dessas instruções verifica o nível de privilégio implicitamente como parte da execução da instrução ao ler um registrador de controle. A verificação considera que o OS precisa estar no nível de privilégio mais alto, o que não acontece para as VMs guest. Somente “Mover para/de registradores de segmento” (MOVE) tenta modificar o estado de controle, e a verificação de proteção também falha.
Para simplificar as implementações dos VMMs no x86, tanto AMD quanto Intel propuseram extensões à arquitetura de um novo modo. O VT-x da Intel oferece um novo modo de execução para rodar VMs, uma definição projetada do estado da VM, instruções para trocar de VMs rapidamente e um grande conjunto de parâmetros para selecionar as circunstâncias em que um VMM precisa ser chamado. Ao todo, o VT-x acrescenta 11 novas instruções para o x86. O Pacifica da AMD tem propostas semelhantes. Uma alternativa para modificar o hardware é fazer pequenas modificações no sistema operacional de modo a evitar o uso de partes problemáticas da arquitetura. Essa técnica é chamada de paravirtualização, e o VMM Xen de fonte aberto é um bom exemplo. O VMM Xen oferece um OS guest com uma abstração de máquina virtual que utiliza apenas as partes fáceis de virtualizar do hardware físico do x86, em que o VMM é executado.
5.12 Comentários finais A dificuldade de construir um sistema de memória para fazer frente aos processadores mais rápidos é acentuada pelo fato de que a matéria-prima para a memória principal, DRAMs, ser essencialmente a mesma nos computadores mais rápidos que nos computadores mais lentos e baratos.
5.14 Exercícios 441
É o princípio da localidade que nos dá uma chance de superar a longa latência do acesso à memória — e a confiabilidade dessa técnica é demonstrada em todos os níveis da hierarquia de memória. Embora esses níveis da hierarquia pareçam muito diferentes em termos quantitativos, eles seguem estratégias semelhantes em sua operação e exploram as mesmas propriedades da localidade. As caches multiníveis possibilitam o uso mais fácil de outras otimizações por dois motivos. Primeiro, os parâmetros de projeto de uma cache de nível inferior são diferentes dos de uma cache de primeiro nível. Por exemplo, como uma cache de segundo ou terceiro níveis será muito maior, é possível usar tamanhos de bloco maiores. Segundo, uma cache de nível inferior não está constantemente sendo usada pelo processador, como em uma cache de primeiro nível. Isso nos permite considerar fazer com que, quando estiver ociosa, uma cache de nível inferior realize alguma tarefa que possa ser útil para evitar futuras falhas. Outra direção possível é recorrer à ajuda de software. Controlar eficientemente a hierarquia de memória usando uma variedade de transformações de programa e recursos de hardware é um importante foco dos avanços dos compiladores. Duas ideias diferentes estão sendo exploradas. Uma é reorganizar o programa para melhorar sua localidade espacial e temporal. Esse método focaliza os programas orientados para loops que usam grandes arrays como a principal estrutura de dados; grandes problemas de álgebra linear são um exemplo típico. Reestruturando os loops que acessam os arrays, podemos obter uma localidade – e, portanto, um desempenho de cache – substancialmente melhor. O exemplo anterior, na Seção 5.11, mostrou como poderia ser eficaz até mesmo uma simples mudança da estrutura de loop. Outra solução é o prefetching. Em prefetching, um bloco de dados é trazido para a cache antes de ser realmente referenciado. Muitos microprocessadores utilizam o prefetching de hardware para tentar prever os acessos, o que pode ser difícil para o software observar. Uma terceira técnica utiliza instruções especiais cientes da cache, que otimizam a transferência da memória. Por exemplo, os microprocessadores na Seção 7.10 do Capítulo 7 utilizam uma otimização que não apanha o conteúdo de um bloco da memória em uma falha de escrita, pois o programa irá escrever o bloco inteiro. Essa otimização reduz significativamente o tráfego da memória para um kernel. Como veremos no Capítulo 7, os sistemas de memória também são um importante tópico de projeto para processadores paralelos. A crescente importância da hierarquia de memória na determinação do desempenho do sistema significa que essa relevante área continuará a ser o foco de projetistas e pesquisadores ainda por vários anos.
5.13 Perspectiva histórica e leitura adicional Esta seção de história oferece um resumo das tecnologias de memória, das linhas de atraso de mercúrio à DRAM, a invenção da hierarquia de memória, mecanismos de proteção e máquinas virtuais, e conclui com uma breve história dos sistemas operacionais, incluindo CTSS, MULTICS, UNIX, BSD UNIX, MS-DOS, Windows e Linux.
5.14 Exercícios1 Exercício 5.1 Neste exercício, consideramos as hierarquias de memória para diversas aplicações, listadas na tabela a seguir. Contribuição de Jichuan Chang, Jacob Leverich, Kevin Lim e Parthasarathy Ranganathan (todos da Hewlett-Packard)
1
prefetching Uma técnica em que os blocos de dados necessários no futuro são colocados na cache pelo uso de instruções especiais que especificam o endereço do bloco.
442
Capítulo 5 Grande e Rápida: Explorando a Hierarquia de Memória
a.
Controle de versão de software
b.
Fazer ligações no telefone
5.1.1 [10] <5.1> Supondo que o cliente e o servidor estejam envolvidos no processo, primeiro nomeie os sistemas cliente e servidor. Onde as caches podem ser colocadas para agilizar o processo? 5.1.2 [10] <5.1> Crie uma hierarquia de memória para o sistema. Mostre o tamanho e a latência típicos em vários níveis da hierarquia. Qual é o relacionamento entre o tamanho da cache e sua latência de acesso? 5.1.3 [15] <5.1> Quais são as unidades de transferências de dados entre as hierarquias? Qual é o relacionamento entre o local dos dados, o tamanho dos dados e a latência da transferência? 5.1.4 [10] <5.1, 5.2> A largura de banda da comunicação e a largura de banda do processamento do servidor são dois fatores importantes a considerar quando se projeta uma hierarquia de memória. Como a largura de banda pode ser melhorada? Qual será o custo dessa melhoria? 5.1.5 [5] <5.1, 5.8> Agora, considerando múltiplos clientes acessando o servidor simultaneamente, esses cenários melhoram a localidade espacial e temporal? 5.1.6 [10] <5.1, 5.8> Dê um exemplo de quando a cache pode fornecer dados desatualizados. Como o cache deve ser projetado para aliviar ou evitar esses problemas?
Exercício 5.2 Neste exercício, veremos as propriedades de localidade de memória do cálculo de matriz. O código a seguir é escrito em C, em que os elementos dentro da mesma linha são armazenados de forma contígua.
a.
b.
5.2.1 [5] <5.1> Quantos inteiros de 32 bits podem ser armazenados em uma linha de cache de 16 bytes? 5.2.2 [5] <5.1> Referências a quais variáveis exibem localidade temporal? 5.2.3 [5] <5.1> Referências a quais variáveis exibem localidade espacial?
5.14 Exercícios 443
A localidade é afetada pela ordem de referência e pelo leiaute dos dados. O mesmo cálculo também pode ser escrito a seguir em Matlab, que difere da linguagem C armazenando elementos da matriz de forma contígua dentro da mesma coluna. a.
b.
5.2.4 [10] <5.1> Quantas linhas de cache de 16 bytes são necessárias para armazenar todos os elementos de matriz de 32 bits sendo referenciados? 5.2.5 [5] <5.1> Referências a quais variáveis exibem localidade temporal? 5.2.6 [5] <5.1> Referências a quais variáveis exibem localidade espacial?
Exercício 5.3 As caches são importantes para fornecer uma hierarquia de memória de alto desempenho aos processadores. A seguir está uma lista de 32 referências a endereços de memória de 32 bits, dadas como endereços de palavra. a.
3, 180, 43, 2, 191, 88, 190, 14, 181, 44, 186, 253
b.
21, 166, 201, 143, 61, 166, 62, 133, 111, 143, 144, 61
5.3.1 [10] <5.2> Para cada uma dessas referências, identifique o endereço binário, a tag e o índice dado uma cache de mapeamento direto com 16 blocos de uma palavra. Além disso, indique se cada referência é um acerto ou uma falha, supondo que a cache esteja inicialmente vazia. 5.3.2 [10] <5.2> Para cada uma dessas referências, identifique o endereço binário, a tag e o índice dado uma cache de mapeamento direto com blocos de duas palavras e um tamanho total de oito blocos. Liste também se cada referência é um acerto ou uma falha, supondo que a cache esteja inicialmente vazia. 5.3.3 [20] <5.2, 5.3> Você está encarregado de otimizar um projeto de cache para as referências indicadas. Existem três projetos de cache de mapeamento direto possíveis, todos com um total de oito palavras de dados: C1 tem blocos de uma palavra, C2 tem blocos de duas palavras e C3 tem blocos de quatro palavras. Em termos de taxa de falhas, que projeto de cache é o melhor? Se o tempo de stall de falha é de 25 ciclos, e C1 tem um tempo de acesso de 2 ciclos, C2 utiliza 3 ciclos e C3 utiliza 5 ciclos, qual é o melhor projeto de cache?
444
Capítulo 5 Grande e Rápida: Explorando a Hierarquia de Memória
Existem muitos parâmetros de projeto diferentes que são importantes para o desempenho geral de uma cache. A tabela a seguir lista parâmetros para diferentes projetos com mapeamento direto. Tamanho de dados da cache
Tamanho de bloco da cache
Tempo de acesso da cache
a.
32 KB
2 palavras
1 ciclo
b.
32KB
4 palavras
2 ciclos
5.3.4 [15] <5.2> Calcule o número total de bits necessários para a cache listada na tabela, considerando um endereço de 32 bits. Dado esse tamanho total, ache o tamanho total da cache de mapeamento direto mais próxima com blocos de 16 palavras do mesmo tamanho ou maior. Explique por que a segunda cache, apesar de seu tamanho de dados maior, poderia oferecer desempenho mais lento do que a primeira cache. 5.3.5 [20] <5.2, 5.3> Gere uma série de solicitações de leitura que possuem uma taxa de falhas em uma cache associativa em conjunto com duas vias de 2KB inferior à cache listada na tabela. Identifique uma solução possível que faria com que a cache listada na tabela tivesse uma taxa de falhas igual ou inferior à cache de 2KB. Discuta as vantagens e desvantagens de uma solução desse tipo. 5.3.6 [15] <5.2> A fórmula apresentada na Seção 5.2 mostra o método típico para indexar uma cache mapeada diretamente, especificamente, (Endereço do bloco) módulo (Número de blocos na cache). Supondo um endereço de 32 bits e 1024 blocos na cache, considere uma função de indexação diferente, especificamente, (Endereço de bloco[31:27] XOR Endereço de bloco[26:22]). É possível usar isso para indexar uma cache mapeada diretamente? Se for, explique por que e discuta quaisquer mudanças que poderiam ser necessárias na cache. Se não for possível, explique o motivo.
Exercício 5.4 Para um projeto de cache mapeada diretamente com endereço de 32 bits, os bits de endereço a seguir são usados para acessar a cache. Tag
Índice
Offset
a.
31-10
9-5
4-0
b.
31-12
11-6
5-0
5.4.1 [5] <5.2> Qual é o tamanho da linha de cache (em palavras)? 5.4.2 [5] <5.2> Quantas entradas a cache possui? 5.4.3 [5] <5.2> Qual é a razão entre o total de bits exigido para essa implementação de cache e os bits de armazenamento de dados? Desde que a alimentação esteja ligada, as seguintes referências de cache endereçadas por byte são registradas. Endereço
0
4
16
132
232
160
1024
30
140
3100
180
2180
5.4.4 [10] <5.2> Quantos blocos são substituídos? 5.4.5 [10] <5.2> Qual é a razão de acerto? 5.4.6 [20] <5.2> Indique o estado final da cache, com cada entrada válida representada como um registro de <índice, tag, dados>.
5.14 Exercícios 445
Exercício 5.5 Lembre-se de que temos duas políticas de escrita e políticas de alocação de escrita; suas combinações podem ser implementadas na cache L1 ou L2. L1
L2
a.
Write-through, sem alocação de escrita
Write-back, alocação de escrita
b.
Write-through, alocação de escrita
Write-back, alocação de escrita
5.5.1 [5] <5.2, 5.5> Os buffers são empregados entre diferentes níveis de hierarquia da memória para reduzir a latência de acesso. Para essa configuração dada, liste os possíveis buffers necessários entre as caches L1 e L2, bem como entre a cache L2 e a memória. 5.5.2 [20] <5.2, 5.5> Descreva o procedimento de tratamento de uma falha de escrita em L1, considerando o componente envolvido e a possibilidade de substituir um bloco modificado. 5.5.3 [20] <5.2, 5.5> Para uma configuração de cache exclusiva multinível (um bloco só pode residir em uma das caches L1 e L2), descreva o procedimento de tratamento de uma falha de escrita em L1, considerando o componente envolvido e a possibilidade de substituir um bloco modificado. Considere os seguintes comportamentos do programa e da cache. Leituras de dados por 1000 instruções
Escritas de dados por 1000 instruções
Taxa de perdas da cache de instruções
Taxa de perdas da cache de dados
Tamanho do bloco (byte)
a.
250
100
0,30%
2%
64
b.
200
100
0,30%
2%
64
5.5.4 [5] <5.2, 5.5> Para uma cache write-through, com alocação de escrita, quais são as larguras de banda mínimas de leitura e escrita (medidas em bytes-por-ciclo) necessárias para alcançar um CPI de 2? 5.5.5 [5] <5.2, 5.5> Para uma cache write-back, com alocação de escrita, considerando que 30% dos blocos de cache de dados substituídos são modificados, quais são as larguras de banda mínimas de leitura e escrita necessárias para um CPI de 2? 5.5.6 [5] <5.2, 5.5> Quais são as larguras de banda mínimas necessárias para alcançar o desempenho de CPI = 1,5?
Exercício 5.6 Aplicações de mídia que tocam arquivos de áudio ou vídeo fazem parte de uma classe de carga de trabalho chamada “streaming”; ou seja, elas trazem grandes quantidades de dados, mas não reutilizam grande parte dele. Considere uma carga de trabalho de streaming de vídeo que acessa um conjunto de trabalho de 512KB sequencialmente com o fluxo de endereço a seguir: 0, 2, 4, 6, 8, 10, 12, 14, 16,…
5.6.1 [5] <5.5, 5.3> Considere um cache com mapeamento direto de 64KB com uma linha de 32 bytes. Qual é a taxa de falhas para esse fluxo de endereços? De que modo essa taxa de falhas é sensível ao tamanho da cache ou ao conjunto de trabalho? Como você categorizaria as falhas que essa carga de trabalho está experimentando, com base no modelo 3C? 5.6.2 [5] <5.5, 5.1> Recalcule a taxa de falhas quando o tamanho da linha de cache é de 16 bytes, 64 bytes e 128 bytes. Que tipo de localidade essa carga de trabalho está explorando?
446
Capítulo 5 Grande e Rápida: Explorando a Hierarquia de Memória
5.6.3 [10] <5.10> “Prefetching” é uma técnica que aproveita padrões de endereço previsíveis para trazer linhas de cache adicionais quando determinada linha de cache é acessada. Um exemplo de prefetching é um buffer de fluxo que pré-busca linhas de cache sequencialmente adjacentes em um buffer separado quando determinada linha de cache é trazida. Se os dados forem encontrados no buffer de prefetch, eles são considerados um acerto e movidos para a cache, e a próxima linha de cache é pré-buscada. Considere um buffer de stream de duas entradas e suponha que a latência da cache seja tal que uma linha de cache possa ser carregada antes que o cálculo na linha de cache anterior seja concluído. Qual é a taxa de falhas para esse stream de endereços? O tamanho do bloco de cache (B) pode afetar a taxa de falhas e a latência de falha. Considerando a seguinte tabela de taxa de falhas, considerando uma máquina de 1 CPI com uma média de 1,35 referências (a instruções e dados) por instrução, ajude a encontrar o tamanho de bloco ideal dadas as seguintes taxas de falha para diversos tamanhos de bloco. 8
16
32
64
128
a.
4%
3%
2%
1,5%
1%
b.
8%
7%
6%
5%
4%
5.6.4 [10] <5.2> Qual é o tamanho de bloco ideal para uma latência de falha de 20 × B ciclos? 5.6.5 [10] <5.2> Qual é o tamanho de bloco ideal para uma latência de falha de 24 × B ciclos? 5.6.6 [10] <5.2> Para uma latência de falha constante, qual é o tamanho de bloco ideal?
Exercício 5.7 Neste exercício, veremos as diferentes maneiras como a capacidade afeta o desempenho geral. Normalmente, o tempo de acesso da cache é proporcional à capacidade. Suponha que os acessos à memória principal utilizem 70ns e que os acessos à memória sejam 36% de todas as instruções. A tabela a seguir mostra dados para caches L1 relacionados a cada um dos dois processadores, P1 e P2. Tamanho L1 a. b.
Taxa de falhas L1
Tempo de acerto L1
P1
2 KB
8,0%
0,66 ns
P2
4 KB
6,0%
0,90 ns
P1
16 KB
3,4%
1,08 ns
P2
32 KB
2,9%
2,02 ns
5.7.1 [5] <5.3> Considerando que o tempo de acerto de L1 determina os tempos de ciclo para P1 e P2, quais são suas respectivas taxas de clock? 5.7.2 [5] <5.3> Qual é o TMAM para cada um de P1 e P2? 5.7.3 [5] <5.3> Considerando um CPI base de 1,0 sem stalls de memória, qual é o CPI total para cada um de P1 e P2? Que processador é mais rápido? Tamanho L2
Taxa de falhas L2
Tempo de acerto L2
a.
1 MB
95%
5,62 ns
b.
8 MB
68%
23,52 ns
5.14 Exercícios 447
5.7.4 [10] <5.3> Qual é o TMAM para P1 com o acréscimo de uma cache L2? O TMAM é melhor ou pior com a cache L2? 5.7.5 [5] <5.3> Considerando um CPI base de 1,0 sem stalls de memória, qual é o CPI total para P1 com a adição de um cache L2 5.7.6 [10] <5.3> Que processador é mais rápido, agora que P1 tem uma cache L2? Se P1 é mais rápido, que taxa de falhas P2 precisaria em sua cache L1 para corresponder ao desempenho de P1? Se P2 é mais rápido, que taxa de falhas P1 precisaria em seu cache L1 para corresponder ao desempenho de P2?
Exercício 5.8 Este exercício examina o impacto de diferentes projetos de cache, especificamente comparando caches associativas com as caches mapeadas diretamente, da Seção 5.2. Para estes exercícios, consulte a tabela de streams de endereço mostrada no Exercício 5.3. 5.8.1 [10] <5.3> Usando as referências do Exercício 5.3, mostre o conteúdo final da cache para uma cache associativa em conjunto com três vias, com blocos de duas palavras e um tamanho total de 24 palavras. Use a substituição LRU. Em cada referência, identifique os bits de índice, os bits de tag, os bits de offset de bloco e se é um acerto ou uma perda. 5.8.2 [10] <5.3> Usando as referências do Exercício 5.3, mostre o conteúdo final da cache para uma cache totalmente associativa com blocos de uma palavra e um tamanho total de oito palavras. Use a substituição LRU. Para cada referência, identifique os bits de índice, os bits de tag, e se é um acerto ou uma perda. 5.8.3 [15] <5.3> Usando as referências do Exercício 5.3, qual é a taxa de perdas para uma cache totalmente associativa com blocos de duas palavras e um tamanho total de oito palavras, usando a substituição LRU? Qual é a taxa de perdas usando a substituição MRU (usado mais recentemente)? Finalmente, qual é a melhor taxa de perdas possível para essa cache, dada qualquer política de substituição?
Taxa de perda global com cache de 2° nível, associativo em conjunto com oito vias
Cache de segundo nível, velocidade associativa em conjunto com oito vias
Taxa de perda global com cache de 2° nível, mapeada diretamente
Cache de segundo nível, velocidade mapeada diretamente
Taxa de perdas da cache de 1° nível por instrução
Tempo de acesso à memória principal
Velocidade do processador
CPI base, sem stalls da memória
O caching multinível é uma técnica importante para contornar a quantidade limitada do espaço que uma cache de primeiro nível pode oferecer enquanto mantém sua velocidade. Considere um processador com os seguintes parâmetros:
a.
1,5
2GHz
100ns
7%
12 ciclos
3,5%
28 ciclos
1,5%
b.
1,0
2GHz
150ns
3%
15 ciclos
5,0%
20 ciclos
2,0%
5.8.4 [10] <5.3> Calcule o CPI para o processador na tabela usando: 1) apenas uma cache de primeiro nível, 2) uma cache de mapeamento direto de segundo nível, e 3) uma cache
448
Capítulo 5 Grande e Rápida: Explorando a Hierarquia de Memória
associativa em conjunto com oito vias de segundo nível. Como esses números mudam se o tempo de acesso da memória principal for dobrado? E se for cortado ao meio? 5.8.5 [10] <5.3> É possível ter uma hierarquia de cache ainda maior que dois níveis. Dado o processador anterior com uma cache de segundo nível mapeada diretamente, um projetista deseja acrescentar uma cache de terceiro nível que leve 50 ciclos para acessar e que reduzirá a taxa de falhas global para 1,3%. Isso ofereceria melhor desempenho? Em geral, quais são as vantagens e desvantagens de acrescentar uma cache de terceiro nível? 5.8.6 [20] <5.3> Em processadores mais antigos, como o Intel Pentium e o Alpha 21264, o segundo nível de cache era externo (localizado em um chip diferente) ao processador principal e à cache de primeiro nível. Embora isso permitisse grandes caches de segundo nível, a latência para acessar a cache era muito mais alta, e a largura de banda normalmente era menor, pois a cache de segundo nível trabalhava em uma frequência inferior. Suponha que uma cache de segundo nível de 512KB fora do chip tenha uma taxa de perdas global de 4%. Se cada 512KB adicionais de cache reduzisse as taxas de perdas globais em 0,7% e a cache tivesse um tempo de acesso total de 50 ciclos, que tamanho a cache deveria ter para corresponder ao desempenho da cache de segundo nível mapeada diretamente, listada na tabela? E ao desempenho da cache associativa em conjunto com oito vias?
Exercício 5.9 Para um sistema de alto desempenho, como um índice B-tree para banco de dados, o tamanho de página é determinado principalmente pelo tamanho dos dados e pelo desempenho do disco. Suponha que, na média, uma página de índice B-tree esteja 70% cheio com entradas de tamanho fixo. A utilidade de uma página é sua profundidade de B-tree, calculada como log2(entradas). A tabela a seguir mostra que, para entradas de 16 bytes, um disco com dez anos de uso, uma latência de 10ms e uma taxa de transferência de 10MB/s, o tamanho de página ideal é de 16K. Tamanho de página (KB) 2
Utilidade da página ou profundidade da B-tree (número de acessos ao disco salvos)
Custo do acesso à página de índice (ms)
Utilidade/ custo
6,49 (ou log2(2048/16 × 0,7))
10,2
0,64
4
7,49
10,4
0,72
8
8,49
10,8
0,79
16
9,49
11,6
0,82
32
10,49
13,2
0,79
64
11,49
16,4
0,70
128
12,49
22,8
0,55
256
13,49
35,6
0,38
5.9.1 [10] <5.4> Qual é o melhor tamanho de página se as entradas agora tiverem 128 bytes? 5.9.2 [10] <5.4> Com base no Exercício 5.9.1, qual é o melhor tamanho de página se as páginas estiverem completas até a metade? 5.9.3 [20] <5.4> Com base no Exercício 5.9.2, qual é o melhor tamanho de página se for usado um disco moderno com latência de 3ms e uma taxa de transferência de 100MB/s? Explique por que os servidores futuros provavelmente terão páginas maiores. Manter páginas “frequentemente utilizadas” (ou “quentes”) na DRAM pode economizar acessos ao disco, mas como determinamos o significado exato de “frequentemente utilizadas” para determinado sistema? Os engenheiros de dados utilizam a razão de custo entre o acesso
5.14 Exercícios 449
à DRAM e ao disco para quantificar o patamar de tempo de reuso para as páginas quentes. O custo de um acesso ao disco é $Disco/acessos_por_segundo, enquanto o custo de manter uma página na DRAM é $DRAM_MB/tamanho_pag. Os custos típicos de DRAM e disco, e os tamanhos típicos de página de banco de dados em diversos pontos no tempo, são listados a seguir: Ano
Custo da DRAM ($/MB)
Tamanho da página (KB)
Custo do disco ($/disco)
Taxa de acesso ao disco (acesso/seg)
1987
5000
1
15000
15
1997
15
8
2000
64
2007
0,05
64
80
83
5.9.4 [10] <5.1, 5.4> Quais são os patamares do tempo de reutilização para essas três gerações de tecnologia? 5.9.5 [10] <5.4> Quais são os patamares do tempo de reutilização se continuarmos usando o mesmo tamanho de página de 4K? Qual é a tendência aqui? 5.9.6 [20] <5.4> Que outros fatores podem ser alterados para continuar usando o mesmo tamanho de página (evitando assim a reescrita de software)? Discuta sua probabilidade com as tendências atuais de tecnologia e custo.
Exercício 5.10 Conforme descrevemos na Seção 5.4, a memória virtual utiliza uma tabela de página para rastrear o mapeamento entre endereços virtuais e endereços físicos. Este exercício mostra como essa tabela precisa ser atualizada enquanto os endereços são acessados. A tabela a seguir é um stream de endereços virtuais vistos em um sistema. Considere páginas de 4KB, um TLB totalmente associativo com quatro entradas, e substituição LRU verdadeira. Se as páginas tiverem de ser trazidas do disco, incremente o próximo número de página maior. a.
4669, 2227, 13916, 34587, 48870, 12608, 49225
b.
12948, 49419, 46814, 13975, 40004, 12707, 52236
TLB Válido
Tag
Número da página física
1
11
12
1
7
4
1
3
6
0
4
9
Tabela de página Válido
Página física ou no disco
1
5
0
Disco
0
Disco
1
6
1
9
1
11
0
Disco
450
Capítulo 5 Grande e Rápida: Explorando a Hierarquia de Memória
Válido
Página física ou no disco
1
4
0
Disco
0
Disco
1
3
1
12
5.10.1 [10] <5.4> Dado o stream de endereços na tabela, e o estado inicial mostrado do TBL e da tabela de página, mostre o estado final do sistema. Indique também, para cada referência, se ela é um acerto no TLB, um acerto na tabela de página ou uma falta de página. 5.10.2 [15] <5.4> Repita o Exercício 5.10.1, mas desta vez use páginas de 16KB em vez de páginas de 4KB. Quais seriam algumas das vantagens de ter um tamanho de página maior? Quais são algumas das desvantagens? 5.10.3 [15] <5.3, 5.4> Mostre o conteúdo final do TLB se ele for associativo em conjunto com duas vias. Mostre também o conteúdo do TLB se ele for mapeado diretamente. Discuta a importância de se ter um TLB para o desempenho mais alto. Como seriam tratados os acessos à memória virtual se não houvesse TLB? Existem vários parâmetros que afetam o tamanho geral da tabela de página. A seguir estão listados diversos parâmetros importantes da tabela de página. Tamanho do endereço virtual
Tamanho da página
Tamanho da entrada da tabela de página
a.
32 bits
4KB
4 bytes
b.
64 bits
16KB
8 bytes
5.10.4 [5] <5.4> Dados os parâmetros nessa tabela, calcule o tamanho total da tabela de página para um sistema executando cinco aplicações que utilizam metade da memória disponível. 5.10.5 [10] <5.4> Dados os parâmetros na tabela anterior, calcule o tamanho total da tabela de página para um sistema executando cinco aplicações que utilizam metade da memória disponível, dada uma técnica de tabela de página de dois níveis com 256 entradas. Suponha que cada entrada da tabela de página principal seja de 6 bytes. Calcule a quantidade mínima e máxima de memória exigida. 5.10.6 [10] <5.4> Um projetista de cache deseja aumentar o tamanho de uma cache de 4KB indexada virtualmente e marcada fisicamente com tags. Dado o tamanho de página listado na tabela anterior, é possível criar uma cache de 16KB com mapeamento direto, considerando duas palavras por bloco? Como o projetista aumentaria o tamanho dos dados da cache?
Exercício 5.11 Neste exercício, examinaremos as otimizações de espaço/tempo para as tabelas de página. A tabela a seguir mostra parâmetros de um sistema de memória virtual. Endereço virtual (bits)
DRAM física instalada
Tamanho da página
Tamanho da PTE (bytes)
a.
43
16 GB
4 KB
4
b.
38
8 GB
16 KB
4
5.14 Exercícios 451
5.11.1 [10] <5.4> Para uma tabela de página de único nível, quantas entradas da tabela de página (PTE) são necessárias? 5.11.2 [10] <5.4> O uso de uma tabela de página multinível pode reduzir o consumo de memória física das tabelas de página apenas mantendo as PTEs ativas na memória física. Quantos níveis de tabelas de página serão necessários nesse caso? E quantas referências de memória são necessárias para a tradução de endereço se estiverem faltando no TLB? 5.11.3 [15] <5.4> Uma tabela de página invertida pode ser usada para otimizar ainda mais o espaço e o tempo. Quantas PTEs são necessárias para armazenar a tabela de página? Considerando uma implementação de tabela de hash, quais são os números do caso comum e do pior caso das referências à memória necessárias para atender a uma falta de TLB? A tabela a seguir mostra o conteúdo de uma TLB com quatro entradas. ID entrada
Válido
Página VA
Modificado
Proteção
Página PA
1
1
140
1
RW
30
2
0
40
0
RX
34
3
1
200
1
RO
32
4
1
280
0
RW
31
5.11.4 [5] <5.4> Sob que cenários o bit de validade da entrada 2 seria definido como 0? 5.11.5 [5] <5.4> O que acontece quando uma instrução escreve na página VA 30? Quando uma TLB controlado por software seria mais rápido que uma TLB controlado por hardware? 5.11.6 [5] <5.4> O que acontece quando uma instrução escreve na página VA xxx?
Exercício 5.12 Neste exercício, examinaremos como as políticas de substituição afetam a taxa de falhas. Considere uma cache associativa em conjunto com duas vias e quatro blocos. Você poderá achar útil desenhar uma tabela (como aquelas encontradas na seção “Falhas e associatividade nas caches”, anteriormente neste capítulo) para solucionar os problemas neste exercício, conforme demonstramos nesta sequência de endereços “0, 1, 2, 3, 4). Endereço do bloco de memória acessado
Conteúdo dos blocos de cache após referência Acerto ou falha
0
Falha
Bloco expulso
Conjunto 0
1
Falha
Mem[0]
2
Falha
Mem[0]
3
Falha
4
Falha
Conjunto 0
Conjunto 1
Mem[2]
Mem[1]
0
Mem[1]
Mem[0]
Mem[2]
Mem[1]
Mem[3]
Mem[4]
Mem[2]
Mem[1]
Mem[3]
…
Esta tabela mostra as sequências de endereços. Sequência de endereços a.
0, 2, 4, 8, 10, 12, 14, 16, 0
b.
1, 3, 5, 1, 3, 1, 3, 5, 3
Conjunto 1
Mem[0]
452
Capítulo 5 Grande e Rápida: Explorando a Hierarquia de Memória
5.12.1 [5] <5.3, 5.5> Considerando uma política de substituição LRU, quantos acertos essa sequência de endereços exibe? 5.12.2 [5] <5.3, 5.5> Considerando uma política de substituição MRU (usado mais recentemente), quantos acertos essa sequência de endereços exibe? 5.12.3 [5] <5.3, 5.5> Simule uma política de substituição aleatória lançando uma moeda. Por exemplo, “cara” significa expulsar o primeiro bloco em um conjunto e “coroa” significa expulsar o segundo bloco em um conjunto. Quantos acertos essa sequência de endereços exibe? 5.12.4 [10] <5.3, 5.5> Que endereço deve ser expulso em cada substituição para maximizar o número de acertos? Quantos acertos essa sequência de endereços exibe se você seguir essa política “ideal”? 5.12.5 [10] <5.3, 5.5> Descreva por que é difícil implementar uma política de substituição de cache que seja ideal para todas as sequências de endereço. 5.12.6 [10] <5.3, 5.5> Considere que você poderia tomar uma decisão em cada referência de memória se deseja ou não que o endereço requisitado seja mantido em cache. Que impacto isso poderia ter sobre a taxa de falhas?
Exercício 5.13 Para dar suporte às máquinas virtuais, dois níveis de virtualização de memória são necessários. Cada máquina virtual ainda controla o mapeamento entre o endereço virtual (VA) e o endereço físico (PA), enquanto o hipervisor mapeia o endereço físico (PA) de cada máquina virtual e o endereço de máquina (MA) real. Para acelerar esses mapeamentos, uma técnica de software chamada “paginação de shadow” duplica as tabelas de página de cada máquina virtual no hipervisor, e intercepta as mudanças de mapeamento entre VA e PA para manter as duas cópias coerentes. A fim de remover a complexidade das tabelas de página de shadow, uma técnica de hardware chamada tabela de página aninhada (ou tabela de página estendida) oferece suporte explícito a duas classes de tabelas de página (VA → PA e PA → MA) e pode percorrer essas tabelas apenas no hardware. Considere esta sequência de operações: (1) Criar processo; (2) Falha de TLB; (3) Falta de página; (4) Troca de contexto;
5.13.1 [10] <5.4, 5.6> O que aconteceria à sequência de operação indicada, para a tabela de página de shadow e a tabela de página aninhada, respectivamente? 5.13.2 [10] <5.4, 5.6> Considerando uma tabela de página de quatro níveis baseada na tabela de página guest e aninhada, quantas referências à memória são necessárias para atender a uma falha de TLB à tabela de página nativa versus aninhada? 5.13.3 [15] <5.4, 5.6> Entre a taxa de falha de TLB, latência de falha de TLB, taxa de falta de página e latência do tratador de falta de página, quais métricas são mais importantes para a tabela de página de shadow? Quais são importantes para a tabela de página aninhada? A tabela a seguir mostra parâmetros para um sistema de página por sombra. Falhas de TLB por 1000 instruções
Latência de falha de TLB NPT
Faltas de página por 1000 instruções
Overhead de shadowing por falta de página
0,2
200 ciclos
0,001
30000 ciclos
5.14 Exercícios 453
5.13.4 [10] <5.6> Para um benchmark com CPI de execução nativo de 1, quais são os números de CPI se estiver usando tabelas de página de shadow versus NPT (considerando apenas o overhead de virtualização da tabela de página)? 5.13.5 [10] <5.6> Que técnicas podem ser usadas para reduzir o overhead induzido pelo shadowing da tabela de página? 5.13.6 [10] <5.6> Que técnicas podem ser usadas para reduzir o overhead induzido pelo NPT?
Exercício 5.14 Um dos maiores impedimentos para o uso generalizado das máquinas virtuais é o overhead de desempenho ocasionado pela execução de uma máquina virtual. A tabela a seguir lista diversos parâmetros de desempenho e comportamento de aplicação.
CPI base
Acessos privilegiados do O/S por 10.000 instruções
Impacto no desempenho de interceptar o O/S guest
Impacto no desempenho de interceptar a VMM
Acessos de E/S por 10.000 instruções
Tempo de acesso de E/S (inclui tempo para interceptar o O/S guest)
a.
1,5
120
15 ciclos
175 ciclos
30
1100 ciclos
b.
1,75
90
20 ciclos
140 ciclos
25
1200 ciclos
5.14.1 [10] <5.6> Calcule o CPI para o sistema listado, supondo que não existem acessos à E/S. Qual é o CPI se o impacto do desempenho da VMM dobrar? E se for cortado ao meio? Se uma empresa de software da máquina virtual deseja obter uma degradação de desempenho de 10%, qual é a maior penalidade possível para interceptar a VMM? 5.14.2 [10] <5.6> Os acessos de E/S normalmente possuem um grande impacto sobre o desempenho geral do sistema. Calcule o CPI de uma máquina usando as características de desempenho anteriores, considerando um sistema não virtualizado. Calcule o CPI novamente, desta vez usando um sistema virtualizado. Como esses CPIs mudam se o sistema tiver metade dos acessos de E/S? Explique por que as aplicações voltadas para E/S possuem um impacto semelhante da virtualização. 5.14.3 [30] <5.4, 5.6> Compare as ideias da memória virtual e das máquinas virtuais. Como os objetivos de cada um se comparam: quais são os prós e contras de cada um? Liste alguns casos em que a memória virtual é desejada, e alguns casos em que as máquinas virtuais são desejadas. 5.14.4 [20] <5.6> A Seção 5.6 discute a virtualização sob a hipótese de que o sistema virtualizado esteja executando a mesma ISA do hardware subjacente. Porém, um uso possível da virtualização é simular ISAs não nativas. Um exemplo disso é QEMU, que simula uma série de ISAs, como o MIPS, SPARC e PowerPC. Quais são algumas das dificuldades envolvidas nesse tipo de virtualização? É possível que um sistema simulado rode mais rápido do que em sua ISA nativa?
Exercício 5.15 Neste exercício, exploraremos a unidade de controle de um controlador de cache para um processador com um buffer de escrita. Use a máquina de estados finitos encontrada na Figura 5.34 como ponto de partida para projetar suas próprias máquinas de estados finitos.
454
Capítulo 5 Grande e Rápida: Explorando a Hierarquia de Memória
Suponha que o controlador de cache seja para a cache de mapeamento direto descrita na Seção 5.7, mas você acrescentará um buffer de escrita com uma capacidade de um bloco. Lembre-se de que a finalidade de um buffer de escrita é servir como armazenamento temporário, de modo que o processador não precisa esperar por dois acessos à memória em uma falha modificada. Em vez de escrever de volta o bloco modificado antes de ler o novo bloco, ele coloca o bloco modificado no buffer e começa imediatamente a ler o novo bloco. O bloco modificado pode então ser escrito na memória principal enquanto o processador está trabalhando. 5.15.1 [10] <5.5, 5.7> O que deve acontecer se o processador emitir uma solicitação que acerta no cache enquanto um bloco está sendo escrito de volta na memória principal a partir do buffer de escrita? 5.15.2 [10] <5.5, 5.7> O que deve acontecer se o processador emitir uma solicitação que falha na cache enquanto um bloco está sendo escrito de volta à memória principal a partir do buffer de escrita? 5.15.3 [30] <5.5, 5.7> Crie uma máquina de estado finito para permitir o uso de um buffer de escrita.
Exercício 5.16 A coerência da cache refere-se às visões de múltiplos processadores em determinado bloco de cache. A tabela a seguir mostra dois processadores e suas operações de leitura/escrita em duas palavras diferentes de um bloco de cache X (inicialmente, X[0] = X[1] = 0). P1
P2
a.
X[0] + +;X[1] = 3;
X[0] = 5;X[1]+ = 2;
b.
X[0] = 10;X[1] = 3;
X[0] = 5;X[1]+ = 2;
5.16.1 [15] <5.8> Liste os valores possíveis do bloco de cache indicado para uma implementação correta do protocolo de coerência de cache. Liste pelo menos um valor possível do bloco se o protocolo não garantir coerência de cache. 5.16.2 [15] <5.8> Para um protocolo de snooping, liste uma sequência de operação válida em cada processador/cache para terminar as operações de leitura/escrita listadas anteriormente. 5.16.3 [10] <5.8> Quais são os números no melhor caso e no pior caso das falhas de cache necessários para terminar as instruções de leitura/escrita listadas? A coerência da memória refere-se às visões de múltiplos itens de dados. A tabela a seguir mostra dois processadores e suas operações de leitura/escrita em diferentes blocos de cache (A e B inicialmente 0). P1
P2
a.
A = 1;B = 2; A+ = 2;B + +;
C = B;D = A;
b.
A = 1;B = 2; A = 5;B + +;
C = B;D = A;
5.16.4 [15] <5.8> Liste os valores possíveis de C e D para uma implementação que garante as suposições de consistência no início da Seção 5.8.
5.14 Exercícios 455
5.16.5 [15] <5.8> Liste pelo menos um par possível de valores para C e D se essas suposições não forem mantidas. 5.16.6 [15] <5.2, 5.8> Para diversas combinações de políticas de escrita e políticas de alocação de escrita, quais combinações tornam a implementação do protocolo mais simples?
Exercício 5.17 Tanto o Barcelona quanto o Nehalem são multiprocessadores em um chip (CMPs), com diversas cores e suas caches em um único chip. O projeto da cache L2 no chip CMP possui opções interessantes. A tabela a seguir mostra as taxas de falhas e as latências de acerto para dois benchmarks com projetos de cache L2 privada versus compartilhada. Considere falhas de cache L1 uma vez a cada 32 instruções. Privada
Compartilhada
Falhas por instrução no benchmark A
0,30%
0,12%
Falhas por instrução no benchmark B
0,06%
0,03%
A próxima tabela mostra as latências de acerto. Cache privada
Cache compartilhada
Memória
a.
5
20
180
b.
10
50
120
5.17.1 [15] <5.10> Qual projeto de cache é melhor para cada um desses benchmarks? Use dados para apoiar sua conclusão. 5.17.2 [15] <5.10> A latência da cache compartilhada aumenta com o tamanho do CMP. Escolha o melhor projeto se a latência da cache compartilhada dobrar. Como a largura de banda fora do chip torna-se o gargalo à medida que o número de cores CMP aumenta, escolha o melhor projeto se a latência da memória fora do chip dobrar. 5.17.3 [10] <5.10> Discuta os prós e os contras das caches L2 compartilhada versus privada para cargas de trabalho de único thread, multithreaded e multiprogramadas, e reconsidere-as se houver caches L3 no chip. 5.17.4 [15] <5.10> Considere que ambos os benchmarks têm um CPI base de 1 (cache L2 ideal). Se ter uma cache sem bloqueio melhora o número médio de falhas L2 concorrentes de 1 para 2, quanta melhoria de desempenho isso oferece sobre uma cache L2 compartilhada? Quanta melhoria pode ser obtida sobre a L2 privada? 5.17.5 [10] <5.10> Supondo que novas gerações de processadores dobrem o número de cores (núcleos) a cada 18 meses, para manter o mesmo nível de desempenho por core, quanta largura de banda fora do chip a mais é necessária para um processador em 2012? 5.17.6 [15] <5.10> Considerando a hierarquia de memória inteira, que tipos de otimizações podem melhorar o número de falhas simultâneas?
Exercício 5.18 Neste exercício, mostramos a definição de um log de servidor Web e examinamos otimizações de código para melhorar a velocidade de processamento do log. A estrutura de dados para o log é definida da seguinte forma:
456
Capítulo 5 Grande e Rápida: Explorando a Hierarquia de Memória
Algumas funções de processamento em um log são: a.
topK_sourceIP (int hour);
b.
browser_histogram (int srcIP); / /browsers of a given IP
5.18.1 [5] <5.11> Quais campos em uma entrada de log serão acessados para a função de processamento de log indicada? Considerando blocos de cache de 64 bytes e nenhum prefetching, quantas falhas de cache por entrada determinada função contrai na média? 5.18.2 [10] <5.11> Como você pode reconhecer a estrutura de dados para melhorar a utilização da cache e a localidade do acesso? Mostre seu código de definição da estrutura. 5.18.3 [10] <5.11> Dê um exemplo de outra função de processamento de log que preferiria um leiaute de estrutura de dados diferente. Se ambas as funções são importantes, como você reescreveria o programa para melhorar o desempenho geral? Suplemente a discussão com um trecho de código e dados. Para os problemas a seguir, use os dados de “Cache Performance for SPEC CPU2000 Benchmarks” (www.cs.wisc.edu/multifacet/misc/spec2000cache-data/) para os pares de benchmarks mostrados na tabela a seguir. a.
apsi/facerec
b.
perlbmk/ammp
5.18.4 [10] <5.11> Para caches de dados de 64KB com associatividades de conjunto variadas, quais são as taxas de falhas desmembradas por tipos de falha (falhas frias, de capacidade e de conflito) para cada benchmark? 5.18.5 [10] <5.11> Selecione a associatividade de conjunto a ser usada por uma cache de dados L1 de 64KB compartilhada por ambos os benchmarks. Se a cache L1 tiver de ser mapeada diretamente, selecione a associatividade de conjunto para a cache L2 de 1MB. 5.18.6 [20] <5.11> Dê um exemplo na tabela de taxa de falhas em que a associatividade de conjunto mais alta aumenta a taxa de falhas. Construa uma configuração de cache e fluxo de referência para demonstrar isso.
5.14 Exercícios 457
§5.1: 1 e 4. (3 é falso porque o custo da hierarquia de memória varia por computador, mas em 2008 o custo mais alto normalmente é a DRAM.) §5.2: 1 e 4: Uma penalidade de falha menor pode levar a blocos menores, pois você não tem tanta latência para amortizar, embora uma largura de banda de memória mais alta normalmente leve a blocos maiores, já que a penalidade de falha é apenas ligeiramente maior. §5.3: 1. §5.4: 1-a, 2-c, 3-c, 4-d. §5.5: 5, 2. (Tanto os tamanhos de bloco maiores quanto o prefetching podem reduzir as falhas compulsórias, de modo que 1 é falso.)
Respostas das Seções “Verifique você mesmo”
6 Armazenamento e outros tópicos de E/S Combinar largura de banda e armazenamento… permite acesso veloz e confiável às trovas de conteúdo em expansão nos discos e… repositórios que se proliferam na Internet. George Gilder. The End is Drawing Nigh, 2000
6.1 Introdução 460 6.2
Confiança, confiabilidade e disponibilidade 462
6.3
Armazenamento em disco 464
6.4
Armazenamento flash 468
6.5
Conectando processadores, memória e dispositivos de E/S 469
6.6
Interface dos dispositivos de E/S com processador, memória e sistema operacional 473
6.7
Medidas de desempenho de E/S: exemplos de sistemas de disco e de arquivos 480
6.8
Projetando um sistema de E/S 482
6.9
Paralelismo e E/S: Redundant Arrays of Inexpensive Disks (RAID) 483
6.10
Vida real: servidor Sun Fire x4150 488
6.11
Tópicos avançados: redes 494
6.12
Falácias e armadilhas 494
6.13
Comentários finais 498
6.14
Perspectiva histórica e leitura adicional 498
6.15 Exercícios 499
Os cinco componentes clássicos de um computador
460
Capítulo 6 Armazenamento e outros tópicos de E/S
6.1 Introdução Embora os usuários possam se frustrar se seus computadores travarem e tiverem de ser reinicializados, eles ficam irados se seu sistema de armazenamento falhar e informações forem perdidas. Assim, a tolerância à confiabilidade é muito mais alta em relação ao armazenamento do que à computação. As redes também são planejadas para tratar falhas na comunicação, incluindo diversos mecanismos para detectar e recuperar-se de tais falhas. Logo, os sistemas de E/S geralmente colocam muito mais ênfase sobre a confiabilidade e o custo, enquanto os processadores e a memória focalizam o desempenho e o custo. Os sistemas de E/S também precisam planejar a facilidade de expansão e a diversidade de dispositivos, o que não é um problema para os processadores. A facilidade de expansão está relacionada à capacidade de armazenamento, que é outro parâmetro de projeto para os sistemas de E/S; os sistemas podem precisar de um limite inferior de capacidade de armazenamento a fim de cumprir seu papel. Embora o desempenho tenha um papel secundário para E/S, ele é mais complexo. Por exemplo, com alguns dispositivos, precisamos cuidar principalmente da latência de acesso, enquanto em outros a vazão é fundamental. Além do mais, o desempenho depende de muitos aspectos do sistema, de características dos dispositivos, da conexão entre o dispositivo e o resto do sistema, da hierarquia de memória e do sistema operacional. Todos os componentes, dos dispositivos de E/S individuais ao processador e software de sistemas, afetarão a confiabilidade, a facilidade de expansão e o desempenho de tarefas que incluem E/S. A Figura 6.1 mostra a estrutura de um sistema simples com sua E/S. Os dispositivos de E/S são incrivelmente diversificados. Três características são úteis na organização dessa grande variedade:
FIGURA 6.1 Uma coleção típica de dispositivos de E/S. As conexões entre os dispositivos de E/S, processador e memória normalmente são chamadas de barramentos, embora o termo signifique fios paralelos compartilhados e a maioria das conexões de E/S hoje seja mais próxima de linhas seriais dedicadas. A comunicação entre os dispositivos e o processador utiliza interrupções e protocolos na interconexão, conforme veremos neste capítulo. A Figura 6.9 mostra a organização para um PC desktop.
6.1 Introdução 461
j
Comportamento: entrada (somente leitura), saída (somente escrita, não pode ser lido) ou armazenamento (pode ser relido e normalmente reescrito).
j
Parceria: um humano ou uma máquina está na outra extremidade do dispositivo de E/S, seja alimentando a entrada de dados ou lendo-os na saída.
j
Taxa de dados: a taxa de pico em que os dados podem ser transferidos entre o dispositivo de E/S e a memória principal ou processador. É útil saber qual é a demanda máxima que o dispositivo pode gerar ao projetar um sistema de E/S.
Por exemplo, um teclado é um dispositivo de entrada usado por um humano com uma taxa de dados máxima de 10 bytes por segundo. A Figura 6.2 mostra alguns dos dispositivos de E/S conectados aos computadores. No Capítulo 1, vimos rapidamente quatro dispositivos de E/S importantes: mouses, monitores gráficos, discos e redes. Neste capítulo, vamos nos aprofundar no armazenamento e nos itens relacionados. No site, há uma seção de tópicos avançados sobre redes, que também são tratadas em outros livros. O modo como devemos avaliar o desempenho da E/S normalmente depende da aplicação. Em alguns ambientes, podemos nos importar principalmente com a vazão do sistema. Nesses casos, a largura de banda de E/S será mais importante. Até mesmo a largura de banda de E/S pode ser medida de duas maneiras diferentes: 1. Quantos dados podemos mover pelo sistema em determinado momento? 2. Quantas operações de E/S podemos realizar por unidade de tempo? A decisão sobre a melhor medida de desempenho pode depender do ambiente. Por exemplo, em muitas aplicações de multimídia, a maioria das requisições de E/S é para fluxos de dados longos, e a largura de banda de transferência é a característica importante. Em outro ambiente, podemos querer processar um número maior de acessos pequenos e não relacionados a um dispositivo de E/S. Um exemplo desse ambiente poderia ser um escritório de processamento de impostos do National Income Tax Service (NITS). O NITS cuida principalmente do processamento de uma grande quantidade de formulários em determinado momento; cada formulário de imposto é armazenado separadamente
FIGURA 6.2 A diversidade de dispositivos de E/S. Os dispositivos de E/S podem ser distinguidos analisando se servem como dispositivos de entrada, saída ou armazenamento; seu parceiro de comunicação (pessoas ou outros computadores); e suas taxas de comunicação máximas. As taxas de dados se espalham por oito ordens de grandeza. Observe que uma rede pode ser um dispositivo de entrada ou saída, mas não pode ser usada para armazenamento. As taxas de transferência dos dispositivos sempre são indicadas na base 10, de modo que 10 Mbits/seg = 10.000.000 bits/seg.
462
requisições de E/S Leituras ou escritas em dispositivos de E/S.
Capítulo 6 Armazenamento e outros tópicos de E/S
e é muito pequeno. Um sistema orientado para transferência de arquivos grandes pode ser satisfatório, mas um sistema de E/S que possa admitir a transferência simultânea de muitos arquivos pequenos pode ser mais barato e mais rápido para processar milhões de formulários de imposto. Em outras aplicações, importamo-nos principalmente com o tempo de resposta, que, como você deve se lembrar, é o tempo total gasto para realizar uma tarefa em particular. Se as requisições de E/S forem extremamente grandes, o tempo de resposta dependerá muito da largura de banda, mas em muitos ambientes a maioria dos acessos será pequena, e o sistema de E/S com a menor latência por acesso oferecerá o melhor tempo de resposta. Em máquinas de monousuário, como computadores desktop e laptops, o tempo de resposta é a principal característica do desempenho. Uma grande quantidade de aplicações, especialmente no vasto mercado comercial para a computação, exige alta vazão e pouco tempo de resposta. Alguns exemplos incluem caixas eletrônicos de banco, sistemas de entrada de pedidos e acompanhamento de estoque, servidores de arquivos e servidores Web. Nesses ambientes, preocupamo-nos com o tempo usado para cada tarefa e quantas tarefas podemos processar em um segundo. A quantidade de solicitações de caixas eletrônicos que você pode processar por hora não importa se cada uma exige 15 minutos – você ficará sem clientes! De modo semelhante, se você puder processar cada solicitação dos caixas eletrônicos rapidamente, mas só pode lidar com uma pequena quantidade de requisições ao mesmo tempo, não poderá dar suporte a muitos caixas eletrônicos, ou então o custo do computador por caixa eletrônico será muito alto. Resumindo, as três classes, desktops, servidores e computadores embutidos são sensíveis à confiabilidade e ao custo da E/S. Sistemas de desktop e sistemas embutidos se concentram mais no tempo de resposta e na diversidade dos dispositivos de E/S, enquanto sistemas servidores focalizam mais a vazão e a facilidade de expansão dos dispositivos de E/S.
6.2 Confiança, confiabilidade e disponibilidade Os usuários imploram por armazenamento confiável, mas como podemos definir isso? Na indústria de computação, a questão é mais difícil do que consultar o dicionário. Após um considerável debate, a definição considerada padrão é a seguinte (Laprie, 1985): Confiança de um sistema computacional é a qualidade do serviço entregue de modo que a confiança possa ser justificadamente depositada sobre esse serviço. O serviço entregue por um sistema é o seu comportamento real observado como percebido por outro(s) sistema(s) interagindo com os usuários desse sistema. Cada módulo possui um comportamento especificado ideal, no qual uma especificação de serviço é uma descrição combinada do comportamento esperado. Uma falha do sistema ocorre quando o comportamento real se desvia do comportamento especificado. Assim, você precisa que uma especificação de referência do comportamento esperado seja capaz de determinar a confiança. Os usuários podem, então, ver um sistema alternando entre dois estados de serviço fornecido com relação à especificação deste: 1. Realização do serviço, na qual o serviço é entregue conforme especificado. 2. Interrupção do serviço, na qual o serviço entregue é diferente do serviço especificado. As transições do estado 1 para o estado 2 são causadas por falhas, e as transições do estado 2 para o estado 1 são causadas por restaurações. As falhas podem ser permanentes ou intermitentes. O último é o caso mais difícil de diagnosticar quando um sistema oscila entre os dois estados; as falhas permanentes são muito mais fáceis de diagnosticar. Essa definição ocasiona dois termos relacionados: confiabilidade e disponibilidade.
6.2 Confiança, confiabilidade e disponibilidade 463
Confiabilidade é uma medida da realização contínua do serviço – ou, de forma equivalente, do tempo para a falha – de um ponto de referência. Logo, o tempo médio para a falha (MTTF) dos discos na Figura 6.5 é uma medida de confiabilidade. Um termo relacionado é a taxa de falha anual (AFR), que é simplesmente a porcentagem dos dispositivos que falhariam em um ano para determinado MTTF. A interrupção do serviço é medida como o tempo médio para o reparo (MTTR). O tempo médio entre falhas (MTBF) é simplesmente a soma MTTF + MTTR. Embora o MTBF seja muito utilizado, o MTTF normalmente é o termo mais apropriado. Disponibilidade é uma medida da realização do serviço com relação à alternância entre os dois estados de realização e interrupção. A disponibilidade é quantificada estaticamente como Disponibilidade =
MTTF (MTTF + MTTR)
Observe que a confiabilidade e a disponibilidade são medidas quantificáveis, e não apenas sinônimos de confiança. Qual é a causa das falhas? A Figura 6.3 resume muitos documentos que coletaram dados sobre motivos para falhas de sistemas computacionais e sistemas de telecomunicações. Logicamente, os operadores humanos são uma fonte de falhas significativa.
FIGURA 6.3 Resumo dos estudos dos motivos para falhas. Embora seja difícil coletar dados para determinar se os operadores são a causa dos erros, como os operadores normalmente registram os motivos para as falhas, esses estudos capturaram esses dados. Constantemente havia outras categorias, como motivos ambientais para cortes de energia, mas eles em geral eram pequenos. As duas linhas iniciais vêm de um artigo clássico de Jim Gray [1990], que ainda é muito citado, quase 20 anos após a coleta dos dados. As duas linhas seguintes são de um artigo de Murphy e Gent, que estudaram casos de cortes em sistemas VAX com o tempo (“Measuring system and software reliability using an automated data collection process”, Quality and Reliability Engineering International 11:5, setembro–outubro de 1995, 341-53). As quinta e sexta linhas são estudos de dados de falhas do FCC sobre a rede telefônica pública dos Estados Unidos, por Kuhn (“Sources of failure in the public switched telephone network”, IEEE Computer 30:4, abril de 1997, 31-36) e por Patty Enriquez. O estudo mais recente de três servidores de internet vem de Oppenheimer, Ganapath e Patterson [2003].
Para aumentar o MTTF, você pode melhorar a qualidade dos componentes ou projetar sistemas para que continuem a operação na presença de componentes que falharam. Logo, a falha precisa ser definida em relação a um contexto. Uma falha em um componente pode não ocasionar uma falha do sistema. Para esclarecer essa distinção, o termo falha é usado indicando falha de um componente. Aqui estão três maneiras de melhorar o MTTF: 1. Impedimento de falha: evitar a ocorrência da falha pela construção. 2. Tolerância a falhas: uso de redundância para permitir que o serviço cumpra com a especificação de serviço apesar da ocorrência de falhas, o que se aplica principalmente a falhas do hardware. A Seção 6.9 descreve as técnicas de RAID para tornar o armazenamento confiável por meio da tolerância a falhas. 3. Previsão de falha: prever a presença e criação de falhas, o que se aplica a falhas do hardware e do software, permitindo que o componente seja substituído antes de falhar.
464
Capítulo 6 Armazenamento e outros tópicos de E/S
Verifique você mesmo
Diminuir o MTTR pode ajudar na disponibilidade tanto quanto aumentar o MTTF. Por exemplo, ferramentas para detecção, diagnóstico e reparo de falhas podem ajudar a reduzir o tempo e reparar falhas ocasionadas por pessoas, software e hardware. Quais das seguintes afirmações são verdadeiras sobre confiança? 1. Se um sistema estiver ativo, então todos os seus componentes estão realizando seu serviço esperado. 2. A disponibilidade é uma medida quantitativa da porcentagem de tempo em que um sistema está realizando seu serviço esperado. 3. A confiabilidade é uma medida quantitativa da realização contínua do serviço por um sistema. 4. A principal fonte de interrupções hoje é o software.
6.3 Armazenamento em disco não volátil Dispositivo de armazenamento em que os dados retêm seu valor mesmo quando a alimentação é removida.
trilha Um dos milhares de círculos concêntricos que compõem a superfície de um disco magnético.
setor Um dos segmentos que compõem uma trilha em um disco magnético; um setor é a menor quantidade de informação lida ou escrita em um disco.
seek O processo de posicionar uma cabeça de leitura/gravação na trilha correta de um disco.
Como mencionamos no Capítulo 1, os discos magnéticos contam com um prato giratório coberto por uma superfície magnética e utiliza uma cabeça de leitura/escrita móvel para acessar o disco. O armazenamento em disco é não volátil – os dados permanecem mesmo quando a alimentação é removida. Um disco magnético consiste em uma coleção de pratos (1-4), cada qual com duas superfícies de disco graváveis. A pilha de pratos gira a uma velocidade entre 5.400 a 15.000RPM e tem um diâmetro entre 2,5cm e 9cm. Cada superfície do disco é dividida em círculos concêntricos, chamados trilhas. Normalmente, existem de 10.000 a 50.000 trilhas por superfície. Cada trilha, por sua vez, é dividida em setores que contêm as informações; cada trilha pode ter de 100 a 500 setores. Os setores normalmente possuem 512 bytes de tamanho, embora exista uma iniciativa para aumentar o tamanho do setor para 4.096 bytes. A sequência gravada em mídia magnética é um número de setor, um gap, a informação para esse setor incluindo o código de correção de erro (veja Apêndice C, página C-66), um gap, o número de setor do próximo setor, e assim por diante. Originalmente, todas as trilhas tinham o mesmo número de setores e, portanto, o mesmo número de bits, mas com a introdução da ZBR (Zone Bit Recording – registro de bits por zona) no início da década de 1990, as unidades de disco passaram para um número variável de setores (portanto, bits) por trilha, em vez de manter constante o espaçamento entre os bits. O ZBR aumenta o número de bits nas trilhas externas e, assim, aumenta a capacidade da unidade. Como vimos no Capítulo 1, para ler e escrever informações, as cabeças de leitura/escrita precisam ser movidas de modo que estejam sobre o local correto. As cabeças de disco para cada superfície são conectadas e se movem em conjunto, de modo que cada cabeça esteja sobre a mesma trilha de cada superfície. O termo cilindro é usado para se referir a todas as trilhas sob as cabeças em determinado ponto para todas as superfícies. Para acessar dados, o sistema operacional precisa direcionar o disco por um processo em três estágios. O primeiro passo é posicionar a cabeça sobre a trilha apropriada. Essa operação é chamada seek, e o tempo para mover a cabeça até a trilha apropriada é chamado tempo de seek. Os fabricantes de disco informam o tempo de seek mínimo, o tempo de seek máximo e o tempo de seek médio em seus manuais. Os dois primeiros são fáceis de medir, mas a média está aberta a interpretações, pois ela depende da distância do seek. Os fabricantes decidiram calcular o tempo de seek médio como a soma do tempo para todos os seeks possíveis dividido pelo número de seeks possíveis. Os tempos de seek médios normalmente são anunciados como entre 3ms a 13ms, mas, dependendo da aplicação e do escalonamento das requisições de disco, o tempo de seek médio real pode ser de apenas 25% a 33% do número anunciado, devido à localidade das referências de disco. Essa localidade surge tanto por causa de acessos sucessivos ao mesmo arquivo quanto porque o sistema operacional tenta escalonar esses acessos juntos.
6.3 Armazenamento em disco 465
Quando a cabeça tiver atingido a trilha correta, temos de esperar até o setor desejado girar sob a cabeça de leitura/escrita. Esse tempo é chamado de latência rotacional ou atraso rotacional. A latência média para a informação desejada está a meio caminho ao redor do disco. Como os discos giram entre 5.400RPM a 15.000RPM, a latência rotacional média é entre Latência rotacional média =
0‚5rotação = 5400RPM
0‚5rotação
5400RPM/ 60 segundos minuto = 0‚0056 segundos = 5‚6 ms
latência rotacional Também chamada de atraso rotacional. O tempo exigido para que o setor desejado de um disco gire sob a cabeça de leitura/escrita; normalmente considerado metade do tempo de rotação.
E Latência rotacional média =
0‚5rotação = 15.000RPM
0‚5rotação
segundos 15.000RPM / 60 minuto = 0‚0020 segundos = 2‚0 ms
O último componente de um acesso ao disco, o tempo de transferência, é o tempo para transferir um bloco de bits. O tempo de transferência é uma função do tamanho do setor, da velocidade de rotação e da densidade de gravação de uma trilha. As taxas de transferência em 2008 estavam entre 70 e 125MB/seg. A única complicação é que a maioria dos controladores de disco possui uma cache interna que armazena setores enquanto eles passam; as taxas de transferência da cache normalmente são maiores e poderiam chegar até 375MB/seg (3 Gbit/seg) em 2008. Hoje, a maioria das transferências de disco possui o tamanho de múltiplos setores. Uma controladora de discos normalmente trata do controle detalhado do disco e da transferência entre o disco e a memória. A controladora acrescenta o componente final do tempo de acesso ao disco, o tempo da controladora, que é o overhead que a controladora impõe na realização do acesso de E/S. O tempo médio para realizar uma operação de E/S consistirá nesses quatro tempos mais qualquer espera que ocorra porque outros processos estão utilizando o disco.
Tempo de leitura do disco
Qual é o tempo médio para ler ou escrever um setor de 512 bytes em um disco típico girando a 15.000RPM? O tempo de seek médio anunciado é de 4ms, a taxa de transferência é de 100MB/seg e o overhead da controladora é de 0,2ms. Suponha que o disco esteja ocioso, de modo que não existe um tempo de espera. O tempo médio de acesso ao disco é igual ao Tempo médio de seek + Atraso rotacional médio + Tempo de transferência + Overhead da controladora. Usando o tempo de seek médio anunciado, a resposta é 0‚5rotação 0‚5KB 4‚0ms + + + 0‚2 ms = 4‚0 + 2‚0 + 0‚005 + 0‚2 = 6‚2 ms 15.000RPM 100 MB/seg Se o tempo médio de seek medido for 25% do tempo médio anunciado, a resposta é 1,0 ms + 2,0 ms + 0,005 ms + 0, 2ms = 3, 2 ms Observe que, quando consideramos o tempo médio de seek medido, ao contrário do tempo médio de seek anunciado, a latência rotacional pode ser o maior componente do tempo de acesso.
EXEMPLO
RESPOSTA
466
Capítulo 6 Armazenamento e outros tópicos de E/S
FIGURA 6.4 Seis discos magnéticos, variando em diâmetro de 35cm até 4,5cm. Os discos da figura foram introduzidos há mais de 15 anos e, portanto, não representam a melhor capacidade dos discos modernos desses mesmos diâmetros. Contudo, essa fotografia representa com precisão seus tamanhos físicos relativos. O maior dos discos é o DEC R81, contendo quatro pratos de 35,5cm de diâmetro e armazenando 456MB. Ele foi fabricado em 1985. O disco com diâmetro de 20cm vem da Fujitsu, e esse disco de 1984 armazena 130MB em seis pratos. O Micropolis RD53 possui cinco pratos de 13,3cm e armazena 85MB. O IBM 0361 também possui cinco pratos, mas possuem apenas 8,8cm de diâmetro. Esse disco de 1988 tem 320MB de capacidade. Em 2008, o disco de 8,8cm mais denso tinha dois pratos e tinha 1TB no mesmo espaço, ocasionando um aumento de densidade de aproximadamente 3000 vezes! O Conner CP 2045 possui dois pratos de 6,35cm, contendo 40MB, e foi fabricado em 1990. O menor disco desta fotografia é o Integral 1820. Esse disco de um único prato de 4,5cm contém 20MB e foi fabricado em 1992.
Advanced Technology Attachment (ATA) Um conjunto de comandos utilizado como padrão para dispositivos de E/S, que é muito popular no PC.
Small Computer Systems Interface (SCSI) Um conjunto de comandos usado como um padrão para dispositivos de E/S.
As densidades de disco têm continuado a aumentar há mais de 50 anos. O impacto dessa melhoria na densidade e na redução do tamanho físico de uma unidade de disco tem sido incríveis, como mostra a Figura 6.4. Os objetivos de diferentes projetistas de discos têm levado a uma grande variedade de unidades disponíveis em determinado momento. A Figura 6.5 mostra as características de quatro discos magnéticos. Em 2008, esses discos de um único fabricante custavam entre US$0,30 e US$5 por gigabyte. No mercado mais amplo, os preços geralmente variam entre US$0,20 e US$2 por gigabyte, dependendo do tamanho, da interface e do desempenho. Embora os discos permaneçam viáveis por um futuro previsível, o mesmo não ocorre com a sabedoria convencional sobre onde os números de bloco são encontrados. As suposições do modelo de setor-trilha-cilindro são que os blocos próximos estão na mesma trilha, os blocos no mesmo cilindro levam menos tempo para acessar, pois não existe tempo de seek, e algumas trilhas são mais próximas que outras. O motivo para o desmembramento foi o aumento do nível das interfaces. As interfaces inteligentes de nível mais alto, como ATA e SCSI, exigiram um microprocessador dentro de um disco, o que leva a otimizações de desempenho. Para aumentar a velocidade das transferências sequenciais, essas interfaces de nível mais alto organizam os discos mais como fitas do que como dispositivos de acesso aleatório. Os blocos lógicos são ordenados em formato de serpentina por uma única superfície, tentando capturar todos os setores que são gravados na mesma densidade de bits. Portanto, os blocos sequenciais podem estar em trilhas diferentes. Veremos um exemplo, na Figura 6.19, da armadilha de considerar o modelo convencional de setor-trilha-cilindro. Detalhamento: Essas interfaces de alto nível permitem que as controladoras de disco incluam caches. Essas caches permitem um acesso rápido aos dados lidos recentemente entre trans-
6.3 Armazenamento em disco 467
FIGURA 6.5 Características de quatro discos magnéticos de um único fabricante em 2008. As três unidades mais à esquerda são para servidores e desktops, enquanto a unidade mais à direita é para laptops. Observe que a terceira unidade tem apenas 6,35cm de diâmetro, mas é uma unidade de alto desempenho com a mais alta confiabilidade e tempo de seek mais rápido. Os discos mostrados aqui são versões seriais da interface para SCSI (SAS), um barramento de E/S padrão para muitos sistemas, ou a versão serial da ATA (SATA), um barramento de E/S padrão para PCs. A taxa de transferência da cache é de 3-5 vezes mais rápida do que a taxa de transferência da superfície do disco. O custo muito mais baixo por gigabyte da unidade de 8,8cm SATA ocorre principalmente devido ao mercado hipercompetitivo dos PCs, embora existam diferenças em desempenho em E/Ss por segundo devido à rotação mais rápida e tempos de seek mais rápidos para SAS. A vida útil para esses discos é de cinco anos. Observe que o MTTF cotado considera potência e temperatura normais. Os tempos de vida do disco podem ser muito mais curtos se a temperatura e a vibração não forem controlados. Veja o link da Seagate em www.seagate.com a fim de obter mais informações sobre essas unidades.
ferências solicitadas pelo processador. Elas utilizam write-through e não atualizam quando há falha na escrita. Elas normalmente também incluem algoritmos de prefetch para tentar antecipar a demanda. As controladoras também utilizam uma fila de comandos que permite que o disco decida em que ordem irá realizar os comandos para maximizar o desempenho enquanto mantém o comportamento correto. Naturalmente, essas capacidades complicam a medida de desempenho do disco e aumentam a importância da escolha da carga de trabalho na comparação de discos.
Quais dos seguintes itens são verdadeiros sobre unidades de disco? 1. Discos de 8,89cm realizam mais E/Ss por segundo que os discos de 6,35cm. 2. Discos de 6,35cm oferecem os mais altos índices de gigabytes por watt. 3. São necessárias horas para ler o conteúdo de um disco de alta capacidade sequencialmente. 4. São necessários meses para ler o conteúdo de um disco de alta capacidade usando setores aleatórios de 512 bytes.
Verifique você mesmo
468
Capítulo 6 Armazenamento e outros tópicos de E/S
6.4 Armazenamento flash Muitos tentaram inventar uma tecnologia para substituir os discos, e muitos falharam: memória CCD, memória de bolha e memória holográfica, todos ficaram a desejar. Quando uma nova tecnologia era entregue, os discos faziam avanços conforme já era previsto, os custos caíam proporcionalmente, e o produto desafiador ficava pouco atraente no mercado. O primeiro desafiador convincente é a memória flash. Essa memória semicondutora é não volátil como os discos, mas a latência é 100-1000 vezes mais rápida que o disco, e ela é menor, gasta menos energia e é mais resistente ao choque. Igualmente importante, devido à popularidade da memória flash nos telefones celulares, câmeras digitais e players MP3, existe um grande mercado a pagar pelo investimento na melhoria da tecnologia de memória flash. Recentemente, o custo da memória flash por gigabyte tem caído 50% por ano. Em 2008, o preço por gigabyte da flash era de $4 a $10 por gigabyte, ou cerca de 2 a 40 vezes mais alto que o disco e 5 a 10 vezes mais baixo que a DRAM. A Figura 6.6 compara três produtos baseados em flash. Embora seu custo por gigabyte seja mais alto que os discos, a memória flash é popular nos dispositivos móveis em parte porque vem em capacidades menores. Como resultado, os discos rígidos de 1 polegada de diâmetro estão desaparecendo de alguns mercados de embutidos. Por exemplo, em 2008, o MP3 player iPod Shuffle da Apple era vendido por US$50 e mantinha 1GB, enquanto o disco menor é de 4GB e é vendido por mais do que o MP3 player inteiro. A memória flash é um tipo de memória somente de leitura programável e eletricamente apagável (EEPROM). A primeira memória flash, chamada flash NOR devido à semelhança da célula de armazenamento com uma porta NOR padrão, era um concorrente direto com outras EEPROMs, sendo aleatoriamente endereçável, como qualquer memória. Há alguns anos, a memória flash NAND oferecia maior densidade de armazenamento, mas a memória só podia ser lida e escrita em blocos, pois a fiação necessária para os acessos aleatórios foi retirada. A flash NAND é muito menos dispendiosa por gigabyte e muito mais comum que a flash NOR; todos os produtos na Figura 6.6 utilizam flash NAND. A Figura 6.7 compara as principais características da memória flash NOR versus NAND. Diferente dos discos e da DRAM, mas assim como as tecnologias EEPROM, os bits da memória flash se desgastam (ver Figura 6.7). A fim de lidar com esses limites, a maioria dos produtos de flash NAND inclui um controlador para espalhar as escritas, remapeando blocos que foram escritos muitas vezes para blocos menos utilizados. Essa técnica é chamada de nivelamento de desgaste. Com o nivelamento de desgaste, produtos de consumidor
FIGURA 6.6 Características de três produtos de armazenamento flash. O pacote padrão CompactFlash foi proposto pela Sandisk Corporation em 1994 para as placas PCMCIA-ATA de PCs portáteis. Por seguir a interface ATA, ele simula uma interface de disco, incluindo comandos seek, trilhas lógicas e assim por diante. O produto RiDATA imita uma interface de disco SATA de 2,5 polegadas.
6.5 Conectando processadores, memória e dispositivos de E/S 469
FIGURA 6.7 Características da memória flash NOR versus NAND em 2008. Estes dispositivos podem ler bytes e palavras de 16 bits apesar de seus tamanhos de acesso grandes.
como telefones celulares, câmeras digitais, MP3 players ou chaves de memória têm muito poucas chances de excederem os limites de escrita na flash. Esses controladores reduzem o desempenho em potencial da flash, mas são necessários, a não ser que o software de nível mais alto monitore o desgaste do bloco. Porém, os controladores também podem melhorar o rendimento, mapeando as células de memória que foram manufaturadas incorretamente. Os limites de escrita são um motivo para a memória flash não ser comum nos computadores de desktop e servidor. Porém, em 2008, os primeiros laptops estão sendo vendidos com memória flash em vez de discos rígidos, a um custo considerável, para oferecer tempos de boot mais rápidos, tamanho menor e maior vida da bateria. Há também memórias flash disponíveis em tamanhos de disco padrão, como mostra a Figura 6.6. Combinando as duas ideias, os discos rígidos híbridos incluem, digamos, um gigabyte de memória flash, de modo que os laptops podem inicializar mais rapidamente e economizar energia, permitindo que os discos permaneçam ociosos com mais frequência. Nos próximos anos, parece que a memória flash competirá com sucesso com os discos rígidos para muitos dispositivos operados por bateria. À medida que a capacidade aumenta e o custo por gigabyte continua a cair, será interessante ver se o desempenho mais alto e a eficiência de energia da memória flash gerarão oportunidades também nos mercados de desktop e servidor. Quais dos seguintes itens são verdadeiros sobre a memória flash? 1. Assim como a DRAM, a memória flash é uma memória semicondutora. 2. Assim como os discos, a memória flash não perde informações se faltar energia. 3. O tempo de acesso de leitura da flash NOR é semelhante à DRAM. 4. A largura de banda de leitura da flash NAND é semelhante ao disco.
Conectando processadores, memória
6.5 e dispositivos de E/S
Em um sistema computacional, os diversos subsistemas precisam ter interfaces entre si. Por exemplo, a memória e o processador precisam se comunicar, assim como o processador e os dispositivos de E/S. Durante muitos anos, isso tem sido feito com um barramento. Um barramento é um link de comunicação compartilhado, que utiliza um conjunto de fios para conectar diversos subsistemas. As duas vantagens principais da organização do barramento são versatilidade e baixo custo. Definindo um único esquema de conexão, novos dispositivos podem ser facilmente acrescentados, e os periféricos podem ainda ser
Verifique você mesmo
470
barramento processadormemória Um barramento que conecta processador e memória, e que é curto, geralmente de alta velocidade, e correspondente ao sistema de memória, de modo a maximizar a largura de banda memória-processador.
barramento backplane Um barramento projetado para permitir que processadores, memória e dispositivos de E/S coexistam em um único barramento.
Capítulo 6 Armazenamento e outros tópicos de E/S
movidos entre os sistemas computacionais que utilizam o mesmo tipo de barramento. Além do mais, os barramentos são eficazes porque um único conjunto de fios é compartilhado de várias maneiras. A principal desvantagem de um barramento é que ele cria um gargalo de comunicação, possivelmente limitando a vazão máxima de E/S. Quando a E/S tiver de passar por um único barramento, a largura de banda desse barramento limita a vazão máxima da E/S. O principal desafio é projetar um sistema de barramento capaz de atender às demandas do processador e também conectar grandes quantidades de dispositivos de E/S à máquina. Os barramentos tradicionalmente são classificados como barramentos processadormemória, ou barramentos de E/S. Os barramentos processador-memória são curtos, geralmente de alta velocidade e correspondentes ao sistema de memória, de modo a maximizar a largura de banda memória-processador. Os barramentos de E/S, ao contrário, podem ser extensos, podem ter muitos tipos de dispositivos conectados a eles e normalmente possuem uma grande faixa de largura de banda de dados dos dispositivos conectados a eles. Os barramentos de E/S normalmente não realizam interface direta com a memória, mas utilizam um barramento processador-memória ou um barramento backplane para a conexão com a memória. Outros barramentos com características diferentes surgiram para funções especiais, como barramentos gráficos. Um motivo para o projeto de barramento ser tão difícil é que sua velocidade máxima é limitada principalmente pelos fatores físicos: a extensão do barramento e o número de dispositivos. Esses limites físicos nos impedem de executar o barramento arbitrariamente rápido. Além disso, a necessidade de dar suporte a uma gama de dispositivos com latências e taxas de transferência de dados muito variáveis também torna o projeto do barramento desafiador. Como é difícil trabalhar com muitos fios paralelos em alta velocidade devido a variações de clock e reflexão (veja Apêndice C), o setor está em transição, passando de barramentos paralelos compartilhados para interconexões seriais ponto a ponto de alta velocidade com switches. Assim, essas redes estão gradualmente substituindo os barramentos em nossos sistemas. Como resultado dessa transição, esta seção foi revisada nesta edição para enfatizar o problema geral de conectar dispositivos de E/S, processadores e memória, em vez de focalizar exclusivamente os barramentos.
Fundamentos sobre conexão transação de E/S Uma sequência de operações pela interconexão que inclui uma solicitação e pode incluir uma resposta, ambas podendo transportar dados. Uma transação é iniciada por uma única solicitação e pode exigir várias operações de barramento individuais.
Vamos considerar uma transação de E/S típica. Uma transação inclui duas partes: enviar o endereço e receber ou enviar os dados. As transações de barramento normalmente são definidas pelo que fazem com a memória. Uma transação de leitura transfere dados da memória (para o processador ou para um dispositivo de E/S), e uma transação de escrita escreve dados na memória. Logicamente, essa terminologia é confusa. Para evitar isso, vamos tentar usar os termos entrada e saída, que sempre são definidos do ponto de vista do processador: uma operação de entrada significa entrar dados do dispositivo para a memória, na qual o processador os poderá ler, e uma operação de saída significa sair com dados para um dispositivo a partir da memória, na qual o processador os escreve. A interconexão de E/S serve como um modo de expandir a máquina e conectar novos periféricos. Para facilitar isso, o setor de computadores desenvolveu diversos padrões. Os padrões servem como uma especificação para o fabricante de computador e para o fabricante de periféricos. Um padrão garante ao projetista do computador que os periféricos estarão disponíveis para uma nova máquina, e garante ao montador do periférico que os usuários poderão se conectar ao seu novo equipamento. A Figura 6.8 resume as principais características dos cinco padrões de E/S dominantes: Firewire, USB, PCI Express (PCIe), serial ATA (SATA) e Serial Attached SCSI (SAS). Eles conectam uma série de dispositivos aos computadores desktop, desde teclados a câmeras e discos.
6.5 Conectando processadores, memória e dispositivos de E/S 471
FIGURA 6.8 Principais características dos cinco padrões de barramento de E/S dominantes. A linha de uso intencionado indica se ele foi projetado para ser usado com cabos externos ao computador ou apenas dentro do computador, com cabos curtos ou fio nas placas de circuito impresso. PCIe pode admitir leituras e escritas simultâneas, de modo que muitas publicações dobram a largura de banda por pista, considerando uma divisão 50/50 de largura de banda de leitura versus escrita.
Os barramentos tradicionais são síncronos. Isso significa que o barramento inclui um clock nas linhas de controle e um protocolo fixo para comunicação que é relativo ao clock. Por exemplo, para realizar uma leitura da memória, poderíamos ter um protocolo que transmite o endereço e comando de leitura no primeiro ciclo de clock, usando as linhas de controle para indicar o tipo de solicitação. A memória poderia então precisar responder com a palavra de dados no quinto clock. Esse tipo de protocolo pode ser implementado com facilidade em uma máquina de estados finitos pequena. Como o protocolo é predeterminado e envolve pouca lógica, o barramento pode executar mais rapidamente, e a lógica da interface será pequena. Entretanto, os barramentos síncronos possuem duas grandes desvantagens. Primeiro, cada dispositivo no barramento precisa executar na mesma velocidade de clock. Segundo, devido a problemas de variação de clock, os barramentos síncronos não podem ser longos se forem rápidos (veja Apêndice C). Esses problemas levaram a interconexões assíncronas, que não utilizam clock. Por não terem clock, as interconexões assíncronas podem acomodar uma grande variedade de dispositivos, e o barramento pode ser estendido sem preocupação com problemas de variação de clock ou sincronismo. Todos os exemplos da Figura 6.8 são assíncronos. Para coordenar a transmissão de dados entre o emissor e o receptor, um barramento assíncrono utiliza um protocolo de handshaking. Um protocolo de handshaking consiste em uma série de etapas em que o emissor e o receptor prosseguem para a próxima etapa apenas quando as duas partes concordarem. O protocolo é implementado com um conjunto adicional de linhas de controle.
As interconexões de E/S dos processadores x86 A Figura 6.9 mostra o sistema de E/S de um PC tradicional. O processador se conecta a periféricos por meio de dois chips principais. O chip próximo ao processador é o hub controlador da memória, normalmente chamado bridge norte, e aquele conectado a ele é o hub controlador de E/S, chamado de bridge sul. A bridge norte é basicamente um controlador de DMA, conectando o processador à memória, possivelmente a uma placa gráfica e ao chip da bridge sul. A bridge sul conecta
barramento síncrono Um barramento que inclui um clock nas linhas de controle e um protocolo fixo para comunicação, relativo ao clock.
interconexão assíncrona Utiliza um protocolo de handshaking para coordenar o uso, em vez de um clock; pode acomodar uma grande variedade de dispositivos, de diferentes velocidades. protocolo de handshaking Uma série de etapas usadas para coordenar as transferências em barramentos assíncronos em que o emissor e o receptor só prosseguem para a próxima etapa quando as duas partes concordarem que a etapa atual foi concluída.
472
Capítulo 6 Armazenamento e outros tópicos de E/S
FIGURA 6.9 Organização do sistema de E/S em um servidor Intel usando o chip set Intel 5000P. Se você considerar que leituras e escritas são metade do tráfego cada, poderá dobrar a largura de banda por link para PCIe.
a bridge norte a diversos barramentos de E/S. A Intel, AMD, NVIDIA e outros fabricantes oferecem uma grande variedade de chip sets para conectar o processador ao mundo exterior. A Figura 6.10 mostra três exemplos dos chip sets. Observe que a AMD engoliu o chip da bridge norte no Opteron e outros produtos, reduzindo assim a quantidade de chips e a latência até a memória e placas gráficas, pulando uma travessia de chip. Visto que a Lei de Moore continua a vigorar, um número cada vez maior de controladoras de E/S, que antes estavam disponíveis como placas opcionais conectadas aos barramentos de E/S, têm sido incorporadas por esses chip sets. Por exemplo, o AMD Opteron X4 e o Intel Nehalem incluem a bridge norte dentro do microprocessador, e o chip da bridge sul do Intel 975 inclui uma controladora RAID (ver Seção 6.9). Essas interconexões de E/S oferecem conectividade elétrica entre os dispositivos de E/S, processadores e memória, e também definem o protocolo de mais baixo nível para a comunicação. Acima desse nível básico, temos de definir os protocolos de hardware e software a fim de controlar as transferências de dados entre os dispositivos de E/S e a memória, e de modo que o processador especifique comandos aos dispositivos de E/S. Esses assuntos serão abordados na próxima seção.
Verifique você mesmo
Redes e barramentos conectam componentes. Quais das seguintes afirmações são verdadeiras: 1. As redes e os barramentos de E/S são quase sempre padronizados. 2. As redes e os barramentos de E/S são quase sempre síncronos.
6.6 Interface dos dispositivos de E/S com processador, memória e sistema operacional 473
FIGURA 6.10 Dois chip sets de E/S da Intel e um da AMD. Observe que as funções da bridge norte estão incluídas no microprocessador AMD, pois estão no Intel Nehalem mais recente.
Interface dos dispositivos de E/S com
6.6 processador, memória e sistema operacional Um protocolo de barramento ou de rede define como uma palavra ou bloco de dados devem ser comunicados em um conjunto de fios. Isso ainda deixa várias outras tarefas que precisam ser realizadas para realmente fazer com que os dados sejam transferidos de um dispositivo para o espaço de endereçamento da memória de algum programa de usuário. Esta seção focaliza essas tarefas e responde a perguntas como estas: j
Como uma solicitação de E/S de um usuário é transformada em um comando de dispositivo e comunicada ao dispositivo?
j
Como os dados são realmente transferidos de ou para um local da memória?
j
Qual é o papel do sistema operacional?
Como veremos na resposta a essas perguntas, o sistema operacional desempenha um papel importante no tratamento da E/S, atuando como interface entre o hardware e o programa que solicita a E/S. As responsabilidades do sistema operacional surgem de três características dos sistemas de E/S: 1. Diversos programas usando o processador compartilham o sistema de E/S. 2. Os sistemas de E/S normalmente usam interrupções (exceções geradas externamente) para comunicar informações sobre operações de E/S. Como as interrupções causam
474
Capítulo 6 Armazenamento e outros tópicos de E/S
uma transferência ao modo kernel ou supervisor, elas precisam ser tratadas pelo sistema operacional (SO). 3. O controle de baixo nível de um dispositivo de E/S é complexo, pois exige o gerenciamento de um conjunto de eventos simultâneos e porque os requisitos para o controle correto do dispositivo normalmente são muito detalhados
Interface hardware/ software
As três características dos sistemas de E/S anteriores levam a diversas funções diferentes que o sistema operacional precisa oferecer: j
O sistema operacional garante que o programa de um usuário acessa apenas as partes de um dispositivo de E/S para as quais o usuário possui direitos. Por exemplo, o sistema operacional não pode permitir que um programa leia ou escreva num arquivo no disco se o proprietário do arquivo não tiver acesso a esse programa. Em um sistema com dispositivos de E/S compartilhados, a proteção não poderia ser fornecida se os programas de usuário pudessem realizar E/S diretamente.
j
O sistema operacional oferece abstrações para acessar dispositivos fornecendo rotinas que tratam as operações de baixo nível dos dispositivos.
j
O sistema operacional trata as interrupções geradas pelos dispositivos de E/S, assim como trata as exceções geradas por um programa.
j
O sistema operacional tenta oferecer acesso equilibrado aos recursos de E/S, além de escalonar acessos a fim de melhorar a vazão do sistema.
Para realizar essas funções em favor dos programas de usuário, o sistema operacional precisa ser capaz de se comunicar com os dispositivos de E/S e impedir que o programa do usuário se comunique com os dispositivos de E/S diretamente. Três tipos de comunicação são necessários: 1. O sistema operacional precisa ser capaz de dar comandos aos dispositivos de E/S. Esses comandos incluem não apenas operações como ler e escrever, mas também outras operações a serem feitas no dispositivo, como uma busca em um disco. 2. O dispositivo precisa ser capaz de notificar o sistema operacional quando o dispositivo de E/S tiver completado uma operação ou tiver encontrado um erro. Por exemplo, quando um disco completar uma busca, ele notificará o sistema operacional. 3. Os dados precisam ser transferidos entre a memória e um dispositivo de E/S. Por exemplo, o bloco sendo lido em uma leitura de disco precisa ser movido do disco para a memória. Nas próximas seções, veremos como essas comunicações são realizadas.
Dando comandos a dispositivos de E/S
E/S mapeada em memória Um esquema de E/S em que partes do espaço de endereçamento são atribuídas a dispositivos de E/S e leituras e escritas para esses endereços são interpretadas como comandos aos dispositivos de E/S.
Para dar um comando a um dispositivo de E/S, o processador precisa ser capaz de endereçar o dispositivo e fornecer uma ou mais palavras de comando. Dois métodos são usados para endereçar o dispositivo: E/S mapeada em memória e instruções de E/S especiais. Na E/S mapeada em memória, partes do espaço de endereçamento são atribuídas a dispositivos de E/S. Leituras e escritas para esses endereços são interpretadas como comandos aos dispositivos de E/S. Por exemplo, uma operação de escrita pode ser usada para enviar dados a um dispositivo de E/S, em que os dados serão interpretados como um comando. Quando o processador coloca o endereço e os dados no barramento da memória, o sistema de memória ignora a operação, porque o endereço indica uma parte do espaço de memória usado para E/S. O controlador de dispositivos, porém, vê a operação, registra os dados e os transmite
6.6 Interface dos dispositivos de E/S com processador, memória e sistema operacional 475
ao dispositivo como um comando. Os programas de usuário são impedidos de realizar operações de E/S diretamente, pois o sistema operacional não oferece acesso ao espaço de endereçamento atribuído aos dispositivos de E/S e, assim, os endereços são protegidos pela tradução de endereços. A E/S mapeada em memória também pode ser usada para transmitir dados, escrevendo ou lendo para selecionar endereços. O dispositivo utiliza o endereço para determinar o tipo de comando, e os dados podem ser fornecidos por uma escrita ou obtidos por uma leitura. Em qualquer evento, o endereço codifica a identidade do dispositivo e o tipo de transmissão entre o processador e o dispositivo. Na realidade, fazer uma leitura ou escrita de dados para cumprir uma solicitação do programa normalmente exige várias operações de E/S separadas. Além do mais, o processador pode ter de interrogar o status do dispositivo entre comandos individuais para determinar se o comando foi concluído com sucesso. Por exemplo, uma simples impressora possui dois registradores de dispositivo de E/S – um para informações de status e um para dados a serem impressos. O registrador de status contém um bit de pronto, ligado pela impressora quando ela tiver impresso um caractere, e um bit de erro, indicando que a impressora está com papel preso ou sem papel. Cada byte de dados a ser impresso é colocado no registrador de dados. O processador precisa, então, esperar até que a impressora ligue o bit pronto antes que possa colocar outro caractere no buffer. O processador também precisa verificar o bit de erro para determinar se houve um problema. Cada uma dessas operações exige um acesso separado ao dispositivo de E/S. Detalhamento: A alternativa à E/S mapeada em memória é usar instruções de E/S dedicadas no processador. Essas instruções de E/S podem especificar o número do dispositivo e a palavra de comando (ou o local da palavra de comando na memória). O processador comunica o endereço do dispositivo por meio de um conjunto de fios normalmente incluídos como parte do barramento de E/S. O comando real pode ser transmitido pelas linhas de dados do barramento. Exemplos de computadores com instruções de E/S são os computadores Intel x86 e o IBM 370. Tornando as instruções de E/S ilegais para serem executas quando fora do modo kernel ou supervisor, os programas de usuário são impedidos de acessar os dispositivos diretamente.
instrução de E/S Uma instrução dedicada, usada para dar um comando a um dispositivo de E/S e que especifica o número do dispositivo e a palavra de comando (ou o local da palavra de comando na memória).
Comunicação com o processador O processo de verificar periodicamente os bits de status para ver se é hora da próxima operação de E/S, como no exemplo anterior, é chamado de polling. O polling é a forma mais simples para um dispositivo de E/S se comunicar com o processador. O dispositivo de E/S simplesmente coloca a informação no registrador de status, e o processador deve vir e apanhar a informação. O processador está totalmente no controle e realiza todo o trabalho. O polling pode ser usado de várias maneiras diferentes. As aplicações embutidas de tempo real sondam os dispositivos de E/S porque as taxas de E/S são predeterminadas e isso torna o overhead da E/S mais previsível, o que é útil para tempo real. Como veremos, isso permite que o polling seja usado mesmo quando a taxa de E/S é um pouco maior. A desvantagem do polling é que ele pode desperdiçar muito tempo de processador, pois os processadores são muito mais rápidos do que os dispositivos de E/S. O processador pode ler o registrador de status muitas vezes, para descobrir que o dispositivo não completou uma operação de E/S comparativamente lenta, ou que o mouse não saiu do lugar desde a última vez em que foi sondado. Quando o dispositivo completar uma operação, ainda teremos de ler o status para determinar se ele teve sucesso. O overhead em uma interface de polling foi reconhecido há muito tempo, levando à invenção de interrupções para notificar o processador quando um dispositivo de E/S exigir atenção do processador. A E/S controlada por interrupção, usada por quase todos os sistemas pelo menos para alguns dispositivos, emprega interrupções de E/S para indicar ao processador que um dispositivo de E/S precisa de atenção. Quando um dispositivo deseja notificar o processador de que completou alguma operação ou que precisa de atenção, isso faz com que o processador seja interrompido. Uma interrupção de E/S é exatamente como as exceções vistas nos Capítulos 4 e 5, com duas distinções importantes:
polling O processo de verificar periodicamente o status de um dispositivo de E/S para determinar a necessidade de atender ao dispositivo.
E/S controlada por interrupção Um esquema de E/S que emprega interrupções para indicar ao processador que um dispositivo de E/S precisa de atenção.
476
Capítulo 6 Armazenamento e outros tópicos de E/S
1. Uma interrupção de E/S é assíncrona com relação à execução da instrução. Ou seja, a interrupção não é associada a qualquer instrução e não impede o término da instrução. Isso é muito diferente de quaisquer exceções de falta de página ou exceções como overflow aritmético. Nossa unidade de controle só precisa verificar uma interrupção de E/S pendente no momento em que iniciar uma nova instrução. 2. Além do fato de que uma interrupção de E/S ocorreu, gostaríamos de transmitir informações adicionais, como a identidade do dispositivo gerando a interrupção. Além do mais, as interrupções representam dispositivos que podem ter diferentes prioridades e cujas solicitações de interrupção possuem diferentes urgências associadas a elas. Para comunicar informações ao processador, como a identidade do dispositivo que gera a interrupção, um sistema pode usar interrupções vetorizadas ou um registrador de causa da exceção. Quando o processador reconhece a interrupção, o dispositivo pode enviar o endereço do vetor ou um campo de status para colocar no registrador de causa. Como resultado, quando o sistema operacional adquire o controle, ele sabe a identidade do dispositivo que causou a interrupção e pode interrogar imediatamente o dispositivo. Um mecanismo de interrupção elimina a necessidade de o processador sondar o dispositivo e, em vez disso, permite que o processador seja focalizado nos programas em execução.
Níveis de prioridade de interrupção Para lidar com as diferentes prioridades dos dispositivos de E/S, a maioria dos mecanismos de interrupção possui vários níveis de prioridade; sistemas operacionais UNIX utilizam de quatro a seis níveis. Essas prioridades indicam a ordem em que o processador deverá processar interrupções. Exceções geradas internamente e interrupções de E/S externas possuem prioridades; em geral, as interrupções de E/S possuem prioridade menor do que as exceções internas. Pode haver várias prioridades de interrupção de E/S, com dispositivos de alta velocidade associados às prioridades mais altas. Para dar suporte a níveis de prioridade para interrupções, o MIPS oferece as primitivas que deixam o sistema operacional implementar a política, de modo semelhante ao modo como o MIPS trata de falhas de TLB. A Figura 6.11 mostra os principais registradores, e a Seção B.7 no Apêndice B oferece mais detalhes. O registrador Status determina quem pode interromper o computador. Se o bit Interrupções habilitadas for 0, então ninguém poderá interromper. Um bloqueio de interrupções mais refinado está disponível no campo de máscara de interrupções. Existe um bit na máscara correspondente a cada bit no campo interrupções pendentes do registrador Cause. Para habilitar a interrupção correspondente, é preciso haver um 1 no campo de máscara no bit dessa posição. Quando ocorre uma interrupção, o sistema operacional pode encontrar o
FIGURA 6.11 Os registradores Cause e Status. Essa versão do registrador Cause corresponde à arquitetura MIPS-32. A arquitetura MIPS I mais antiga tinha três conjuntos aninhados de bits kernel/usuário e de bits de habilitação de interrupções para dar suporte a interrupções aninhadas. A Seção B.7 no Apêndice B contém mais detalhes sobre esses registradores.
6.6 Interface dos dispositivos de E/S com processador, memória e sistema operacional 477
motivo no campo de código de exceção do registrador Status: 0 significa que uma interrupção ocorreu, com outros valores para as exceções mencionadas no Capítulo 5. Aqui estão as etapas que precisam ocorrer no tratamento de uma exceção: 1. Realize um AND lógico entre o campo interrupções pendentes e o campo máscara de interrupções para ver quais interrupções ativas poderiam ser as culpadas. São feitas cópias desses dois registradores usando a instrução mfc0 . 2. Selecione a prioridade mais alta dessas interrupções. A convenção do software é que a mais à esquerda seja a prioridade mais alta. 3. Salve o campo de máscara de interrupções do registrador Status. 4. Mude o campo de máscara de interrupções para desativar todas as interrupções de prioridade igual ou inferior. 5. Salve o estado do processador necessário para lidar com a interrupção. 6. A fim de permitir interrupções de prioridade mais alta, coloque o bit interrupções habilitadas do registrador Cause em 1. 7. Chame a rotina de interrupção apropriada. 8. Antes de restaurar o estado, coloque o bit interrupções habilitadas do registrador Cause em 0. Isso permite restaurar o campo de máscara de interrupções. O Apêndice B mostra um handler de exceções para uma tarefa de E/S simples. Como os níveis de prioridade de interrupção (IPL – Interrupt Priority Levels) correspondem a esses mecanismos? O IPL é uma invenção do sistema operacional. Ele é armazenado na memória do processo, e cada processo recebe um IPL. No IPL mais baixo, todas as interrupções são permitidas. Ao contrário, no IPL mais alto, todas as interrupções são bloqueadas. Levantar e reduzir o IPL envolve mudanças no campo de máscara de interrupção do registrador Status. Detalhamento: Os dois bits menos significativos dos campos interrupções pendentes e máscara de interrupções são para interrupções de software, que são de prioridade inferior. Eles normalmente são usados por interrupções de prioridade mais alta para deixar trabalho para interrupções de menor prioridade realizarem depois que o motivo imediato da interrupção for tratado. Quando a interrupção de maior prioridade terminar, as tarefas de prioridade inferior serão observadas e tratadas.
Transferindo os dados entre um dispositivo e a memória Vimos dois métodos diferentes que permitem que um dispositivo se comunique com o processador. Essas duas técnicas – polling e interrupções de E/S – formam a base para dois métodos de implementação da transferência de dados entre o dispositivo de E/S e a memória. Essas duas técnicas funcionam melhor com dispositivos de menor largura de banda, nos quais estamos mais interessados em reduzir o custo do controlador de dispositivo e interface do que oferecer uma transferência com largura de banda alta. Tanto o polling quanto as transferências controladas por interrupção colocam o trabalho de mover dados e gerenciar a transferência sob a responsabilidade do processador. Depois de examinar esses dois esquemas, veremos um outro mais adequado para dispositivos de maior desempenho ou coleções de dispositivos. Podemos usar o processador para transferir dados entre um dispositivo e a memória com base no polling. Em aplicações de tempo real, o processador carrega dados dos registradores do dispositivo de E/S e os armazena na memória. Um outro mecanismo é fazer a transferência de dados controlada por interrupção. Nesse caso, o sistema operacional ainda transferiria dados em pequenos números de bytes de ou para o dispositivo. Entretanto, como a operação de E/S é controlada por interrupção, o sistema operacional simplesmente atua sobre outras tarefas enquanto os dados estão sendo lidos ou escritos no dispositivo. Quando o sistema operacional reconhece uma interrupção a partir do dispositivo, ele lê o status para verificar a ocorrência de erros. Se não houver,
478
acesso direto à memória (DMA) Um mecanismo que oferece a um controlador de dispositivo a capacidade de transferir dados diretamente da memória ou para ela sem envolver o processador.
master Uma unidade na interconexão E/S que pode iniciar requisições de transferência.
Capítulo 6 Armazenamento e outros tópicos de E/S
o sistema operacional poderá fornecer a próxima parte dos dados, por exemplo, por uma sequência de escritas mapeadas em memória. Quando o último byte de uma solicitação de E/S tiver sido transmitido e a operação de E/S for concluída, o sistema operacional poderá informar ao programa. O processador e o sistema operacional realizam todo o trabalho nesse processo, acessando o dispositivo e a memória para cada item de dados transferido. A E/S controlada por interrupção libera o processador de ter de esperar por cada evento de E/S, embora, se usássemos esse método para transferir dados de ou para um disco rígido, o overhead ainda poderia ser intolerável, pois isso poderia consumir uma grande fração do processador quando o disco estivesse transferindo. Para dispositivos com alta largura de banda, como discos rígidos, as transferências consistem principalmente em blocos de dados relativamente grandes (centenas a milhares de bytes). Assim, os projetistas de computadores inventaram um mecanismo para desafogar o processador e fazer com que o controlador de dispositivo transfira dados diretamente de ou para a memória sem envolver o processador. Esse mecanismo é chamado de acesso direto à memória (DMA – Direct Memory Access). O mecanismo de interrupção ainda é usado pelo dispositivo para a comunicação com o processador, mas somente no término da transferência de E/S ou quando ocorre um erro. O DMA é implementado com um controlador especializado, que transfere dados entre um dispositivo de E/S e a memória, independente do processador. O controlador de DMA torna-se o master e direciona as leituras e escritas entre si mesmo e a memória. Existem três etapas em uma transferência de DMA: 1. O processador configura o DMA fornecendo a identidade do dispositivo, a operação a realizar no dispositivo, o endereço de memória que é a origem ou o destino dos dados a serem transferidos e o número de bytes a transferir. 2. O DMA inicia a operação no dispositivo e arbitra o acesso à interconexão. Quando os dados estão disponíveis (do dispositivo ou da memória), ele transfere os dados. O dispositivo de DMA fornece o endereço de memória para a leitura ou a escrita. Se a solicitação exigir mais de uma transferência, a unidade de DMA gera o próximo endereço de memória e inicia a próxima transferência. Usando esse mecanismo, a unidade de DMA pode completar uma transferência inteira, que pode ter milhares de bytes de tamanho, sem incomodar o processador. Muitos controladores de DMA contêm alguma memória para permitir que eles tratem de modo flexível atrasos na transferência ou aqueles ocorridos na espera para se tornar o master. 3. Quando a transferência de DMA termina, o controlador interrompe o processador, que pode então determinar, interrogando o dispositivo de DMA ou examinando a memória, se a operação inteira foi concluída com sucesso. Pode haver vários dispositivos de DMA em um sistema de computador. Por exemplo, em um sistema com um único barramento processador-memória e vários barramentos de E/S, cada controlador de barramento de E/S normalmente terá um processador de DMA que trata de quaisquer transferências entre um dispositivo no barramento de E/S e a memória. Ao contrário do polling ou da E/S controlada por interrupção, o DMA pode ser usado para realizar interface de um disco rígido sem consumir todos os ciclos de processador para uma única E/S. Naturalmente, se o processador também estiver brigando pela memória, ele será atrasado quando a memória estiver ocupada realizando uma transferência de DMA. Usando caches, o processador pode evitar ter de acessar a memória na maior parte do tempo, deixando assim a maior parte da largura de banda da memória livre para uso por dispositivos de E/S. Detalhamento: Para reduzir ainda mais a necessidade de interromper o processador e ocupá-lo no tratamento de uma solicitação de E/S que possa envolver a realização de várias operações reais, o controlador de E/S pode se tornar mais inteligente. Controladores inteligentes normalmente são chamados de processadores de E/S (bem como controladores de E/S ou controladores de canal). Esses processadores especializados executam uma série de operações de E/S, chamadas de programa de E/S. O programa pode estar armazenado no processador de E/S, ou pode estar armazenado na memória e ser buscado pelo processador de E/S. Ao usar um processador de E/S, o sistema operacional normalmente configura um programa de E/S que
6.6 Interface dos dispositivos de E/S com processador, memória e sistema operacional 479
indica as operações de E/S a serem realizadas, além do tamanho e do endereço de transferência para quaisquer leituras ou escritas. O processador de E/S, então, busca as operações do programa de E/S e interrompe o processador apenas quando o programa inteiro estiver completo. Os processadores de DMA são processadores de uso especial (normalmente, de único chip e não programáveis), enquanto os processadores de E/S normalmente são implementados com microprocessadores de uso geral, que executam um programa de E/S especializado.
Acesso direto à memória e o sistema de memória Quando o DMA é incorporado a um sistema de E/S, o relacionamento entre o sistema de memória e o processador muda. Sem DMA, todos os acessos ao sistema de memória vêm do processador e, assim, prosseguem pela tradução de endereços e acesso à cache como se o processador gerasse as referências. Com DMA, existe outro caminho para o sistema de memória – que não passa pelo mecanismo de tradução de endereços ou pela hierarquia de cache. Essa diferença gera alguns problemas nos sistemas de memória virtual e em sistemas com caches. Esses problemas normalmente são solucionados com uma combinação de técnicas de hardware e suporte do software. As dificuldades de ter DMA em um sistema de memória virtual surgem porque as páginas possuem um endereço físico e um endereço virtual. O DMA também cria problemas para sistemas com caches, pois pode haver duas cópias de um item de dados: uma na cache e uma na memória. Como o processador de DMA realiza solicitações de memória diretamente à memória, e não pela cache do processador, o valor de um local de memória visto pela unidade de DMA e pelo processador pode ser diferente. Considere uma leitura do disco que a unidade de DMA coloque diretamente na memória. Se alguns dos locais em que o DMA escreve estiverem na cache, o processador receberá o valor antigo quando fizer uma leitura. De modo semelhante, se a cache for write-back, o DMA poderá ler um valor diretamente da memória quando um valor mais novo estiver na cache, e o valor não foi escrito de volta. Isso é chamado de problema de dados antigos, ou problema de coerência (veja Capítulo 5). Vimos três métodos diferentes para transferir dados entre um dispositivo de E/S e a memória. Ao passar do polling para uma E/S controlada por interrupção e para uma interface de DMA, mudamos o peso do gerenciamento de uma operação de E/S do processador para um controlador de E/S progressivamente mais inteligente. Esses métodos têm a vantagem de liberar os ciclos do processador. Sua desvantagem é que eles aumentam o custo do sistema de E/S. Por causa disso, determinado sistema computacional pode escolher qual ponto nesse espectro é apropriado para os dispositivos de E/S se conectarem a ele. Antes de discutirmos o projeto dos sistemas de E/S, vejamos rapidamente as medidas de desempenho deles na próxima seção. Na avaliação das três maneiras de realizar E/S, quais afirmações são verdadeiras? 1. Se quisermos a menor latência para uma operação de E/S a um único dispositivo de E/S, a ordem é polling, DMA e E/S controlada por interrupção. 2. Em termos de menor impacto na utilização do processador a partir de um único dispositivo de E/S, a ordem é DMA, E/S controlada por interrupção e polling.
Em um sistema com memória virtual, o DMA deverá funcionar com endereços virtuais ou com endereços físicos? O problema óbvio com os endereços virtuais é que a unidade de DMA precisará traduzir os endereços virtuais em endereços físicos. O problema principal com o uso de um endereço físico em uma transferência de DMA é que a transferência não pode cruzar com facilidade um limite de página. Se uma solicitação de E/S cruzasse um limite de página, então os locais de memória para os quais ela estava sendo transferida não necessariamente seriam contíguos na memória virtual. Consequentemente, se usarmos endereços físicos, teremos de restringir todas as transferências de DMA para permanecerem dentro de uma página. Um método para permitir que o sistema inicie transferências de DMA que cruzam limites de página é fazer com que o DMA funcione em endereços virtuais. Nesse sistema, a unidade
Verifique você mesmo
Interface hardware/ software
480
Capítulo 6 Armazenamento e outros tópicos de E/S
de DMA possui um pequeno número de entradas de mapa que oferecem mapeamento virtual para físico para uma transferência. O sistema operacional provê o mapeamento quando a E/S for iniciada. Usando esse mapeamento, a unidade de DMA não precisa se preocupar com o local das páginas virtuais envolvidas na transferência. Outra técnica é que o sistema operacional divida a transferência de DMA em uma série de transferências, cada uma confinada dentro de uma única página física. As transferências, então, são encadeadas e entregues a um processador de E/S ou unidade de DMA inteligente, que executa a sequência inteira de transferências; como alternativa, o sistema operacional pode solicitar as transferências individualmente. Qualquer que seja o método utilizado, o sistema operacional ainda precisa cooperar não remapeando as páginas enquanto uma transferência de DMA que envolve essa página estiver em andamento.
Interface hardware/ software
O problema de coerência para dados de E/S é evitado pelo uso de uma de três técnicas importantes. Uma técnica é rotear a atividade de E/S por meio da cache. Isso garante que as leituras vejam o valor mais recente enquanto as escritas atualizam quaisquer dados na cache. O roteamento de toda a E/S pela cache é dispendioso e possui um grande impacto potencial negativo no desempenho do processador, pois os dados de E/S raramente são usados de imediato e podem deslocar dados úteis de que um programa em execução precisa. Uma segunda opção é ter o sistema operacional invalidando a cache seletivamente para uma leitura de E/S ou forçar a ocorrência de write-backs para uma escrita de E/S (normalmente chamado de flush de cache). Essa técnica exige uma pequena quantidade de suporte do hardware e provavelmente é mais eficiente se o software puder realizar a função de forma fácil e eficiente. Como esse flush de grandes partes da cache só precisa acontecer nos acessos em bloco ao DMA, ele será relativamente pouco frequente. A terceira técnica é oferecer um mecanismo de hardware para fazer o flush (ou invalidar) seletivamente às entradas de cache. A invalidação do hardware para garantir coerência da cache é comum em sistemas multiprocessador, e a mesma técnica pode ser usada para E/S; discutimos esse assunto com detalhes no Capítulo 5.
Medidas de desempenho de E/S: exemplos
6.7 de sistemas de disco e de arquivos
Como devemos comparar sistemas de E/S? Essa é uma pergunta complexa, porque o desempenho da E/S depende de muitos aspectos do sistema e diferentes aplicações enfatizam diferentes aspectos do sistema de E/S. Além do mais, um projeto pode fazer escolhas complexas entre tempo de resposta e vazão, tornando impossível medir apenas um aspecto isoladamente. Por exemplo, tratar um pedido o mais cedo possível em geral minimiza o tempo de resposta, embora uma vazão maior possa ser alcançada se tentarmos lidar com solicitações relacionadas juntas. De acordo com isso, podemos aumentar a vazão em um disco agrupando solicitações que acessam locais próximos. Essa política aumentará o tempo de resposta para algumas solicitações, provavelmente levando a uma variação maior no tempo de resposta. Embora a vazão seja maior, alguns benchmarks restringem o tempo de resposta máximo a qualquer solicitação, tornando tais otimizações potencialmente problemáticas. Nesta seção, damos alguns exemplos de medidas propostas para determinar o desempenho dos sistemas de disco. Esses benchmarks são afetados por uma variedade de recursos do sistema, incluindo tecnologia de disco, como os discos são conectados, o sistema de memória, o processador e o sistema de arquivos fornecido pelo sistema operacional. Antes de discutirmos esses benchmarks, precisamos explicar um ponto confuso sobre terminologia e unidades. O desempenho dos sistemas de E/S depende da velocidade em
6.7 Medidas de desempenho de E/S: exemplos de sistemas de disco e de arquivos 481
que o sistema transfere dados. A velocidade de transferência depende da velocidade do clock, que normalmente é dada em GHz = 109 ciclos por segundo. A taxa de transferência normalmente é cotada em GB/seg. Nos sistemas de E/S, GBs são medidos usando a base 10 (ou seja, 1GB = 109 = 1.000.000.000 bytes), diferente da memória principal, em que a base 2 é utilizada (ou seja, 1GB = 230 = 1.073.741.824). Além de aumentar a confusão, essa diferença gera a necessidade de conversão entre a base 10 (1K = 1000) e a base 2 (1K = 1024), porque muitos acessos à E/S são para blocos de dados que possuem um tamanho que é uma potência de dois. Em vez de complicar todos os nossos exemplos, convertendo com precisão uma das duas medidas, ressaltamos aqui essa distinção e o fato de que tratar as duas medidas como se as unidades fossem idênticas produz um pequeno erro. Ilustramos esse erro na Seção 6.12.
Benchmarks de E/S de processamento de transações Aplicações de processamento de transações (TP – Transaction Processing) envolvem um requisito de tempo de resposta e uma medida de desempenho baseada na vazão. Além do mais, a maioria dos acessos de E/S é pequena. Por causa disso, as aplicações de TP tratam principalmente da taxa de E/S, medida como o número de acessos ao disco por segundo, ao contrário da taxa de dados, medida como bytes de dados por segundo. As aplicações de TP geralmente envolvem mudanças em um banco de dados grande, com o sistema atendendo a alguns requisitos de tempo de resposta e tratando de forma controlada certos tipos de falhas. Essas aplicações são muito críticas e sensíveis ao custo. Por exemplo, os bancos normalmente utilizam sistemas de TP porque se preocupam com uma série de características, entre elas: garantir que as transações não são perdidas, tratar das transações rapidamente e minimizar o custo do processamento de cada transação. Embora a confiabilidade em face da falha seja um requisito absoluto em tais sistemas, o tempo de resposta e a vazão são fundamentais para criar sistemas econômicos. Diversos benchmarks de processamento de transações foram desenvolvidos. O conjunto mais conhecido de benchmarks é uma série desenvolvida pelo Transaction Processing Council (TPC). O TPC-C, inicialmente criado em 1992, simula um ambiente de consulta complexo. O TPC-H modela o apoio à decisão ocasional – as consultas não são relacionadas, e o conhecimento de consultas passadas não pode ser usado para otimizar futuras consultas; o resultado é que os tempos de execução da consulta podem ser muito longos. O TPC-W é um benchmark de aplicações baseadas na Web, que simula as atividades de um servidor Web transacional orientado a negócios. Ele exercita o sistema de banco de dados e também o software básico do servidor Web. O TPC-App é um benchmark de servidor de aplicações e Web services. O mais recente é o TPC-E, que simula a carga de trabalho de processamento de transações de uma firma de corretagem. Os benchmarks TPC são descritos em www.tpc.org. Todos os benchmarks de TCP medem o desempenho em transações por segundo. Além disso, eles incluem um requisito de tempo de resposta, de modo que o desempenho da vazão é medido apenas quando o limite do tempo de resposta é atendido. Para modelar sistemas do mundo real, as velocidades de transação mais altas também estão associadas a sistemas maiores, tanto em termos de usuários quanto o tamanho do banco de dados ao qual as transações são aplicadas. Logo, a capacidade de armazenamento precisa se expandir com o desempenho. Finalmente, o custo do sistema para um sistema de benchmark também precisa ser incluído, permitindo comparações precisas de custo-desempenho.
Benchmarks de E/S para sistema de arquivos e para Web Além de benchmarks de processador, o SPEC oferece um benchmark de servidor de arquivos (SPECSFS) e um benchmark de servidor Web (SPECWeb). O SPECSFS é um benchmark destinado a medir o desempenho do NFS (Network File System) usando um script de solicitações para servidores de arquivos; ele testa o desempenho do sistema de E/S, incluindo disco e rede, além do processador. SPECSFS é um benchmark orientado a vazão, mas com requisitos importantes de tempo de resposta. SPECWeb é um benchmark de servidor Web que simula vários clientes solicitando páginas estáticas e dinâmicas de um servidor, além de clientes postando dados ao servidor (veja Capítulo 1).
processamento de transações Um tipo de aplicação que envolve o tratamento de pequenas operações curtas (chamadas transações) que normalmente exigem tanto E/S quanto cálculo. As aplicações de processamento de transações normalmente possuem requisitos de tempo de resposta e uma medida de desempenho baseada na vazão das transações. taxa de E/S A medida de desempenho das E/Ss por unidade de tempo, como leituras por segundo. taxa de dados Medida de desempenho de bytes por unidade de tempo, como GB/segundo
482
Capítulo 6 Armazenamento e outros tópicos de E/S
O esforço SPEC mais recente é para medir a potência. O SPECPower mede as características de potência e desempenho de pequenos servidores. A Sun recentemente anunciou o filebench, um framework de benchmark do sistema de arquivos. Em vez de uma carga de trabalho padrão, ele oferece uma linguagem que lhe permite descrever a carga de trabalho que você gostaria de executar nos seus sistemas de arquivos. Porém, existem exemplos de cinco cargas de trabalho que têm como finalidade simular aplicações comuns de sistemas de arquivos.
Verifique As seguintes afirmativas são verdadeiras ou falsas? Ao contrário dos benchmarks de processador, os benchmarks de E/S: você mesmo 1. concentram-se na vazão, em vez da latência. 2. podem exigir que os dados definam a escala em tamanho ou número de usuários para conseguir os marcos de desempenho. 3. normalmente relatam o desempenho em termos de custo.
6.8 Projetando um sistema de E/S Existem dois tipos principais de especificação que os projetistas encontram nos sistemas de E/S: restrições de latência e restrições de largura de banda. Nos dois casos, o conhecimento do padrão de tráfego afeta o projeto e a análise. As restrições de latência envolvem garantir que a latência para completar uma operação de E/S esteja limitada por uma certa quantidade. No caso simples, o sistema pode ser descarregado, e o projetista também precisa garantir que algum limite de latência seja realizado, pois isso é fundamental para a aplicação ou porque o dispositivo precisa receber certo serviço garantido que impeça erros. Da mesma forma, determinar a latência de um sistema não carregado é relativamente fácil, pois envolve rastrear o caminho da operação de E/S e somar as latências individuais. Encontrar a latência média (ou a distribuição da latência) sob uma carga é um problema muito mais complexo. Esses problemas são resolvidos ou por teoria de filas (quando o comportamento das solicitações da carga de trabalho e os tempos de atendimento de E/S podem ser aproximados por distribuições simples) ou por simulação (quando o comportamento dos eventos de E/S é complexo). Os dois tópicos estão além do escopo deste texto. Projetar um sistema de E/S para atender a um conjunto de restrições de largura de banda dado uma carga de trabalho é o outro problema comum que os projetistas enfrentam. Como alternativa, o projetista pode receber um sistema de E/S parcialmente configurado e ser solicitado a balancear o sistema para manter a largura de banda máxima alcançável conforme ditado pela parte pré-configurada do sistema. Esse último problema de projeto é uma versão simplificada do primeiro. A técnica geral para projetar tal sistema é a seguinte: 1. Encontrar o elo mais fraco no sistema de E/S, que é o componente no caminho da E/S que restringirá o projeto. Dependendo da carga de trabalho, esse componente pode estar em qualquer lugar, incluindo nos processadores, nos controladores de E/S ou nos dispositivos. Os limites da carga de trabalho e de configuração podem ditar onde está localizado o elo mais fraco. 2. Configurar esse componente para sustentar a largura de banda exigida. 3. Determinar os requisitos para o restante do sistema e configurá-los para dar suporte a essa largura de banda. 4. O modo mais fácil de entender essa metodologia é com um exemplo. Faremos uma análise simples do sistema de E/S do servidor Sun Fire x4150 na Seção 6.10, para mostrar como essa metodologia funciona.
6.9 Paralelismo e E/S: Redundant Arrays of Inexpensive Disks (RAID) 483
Paralelismo e E/S: Redundant Arrays 6.9 of Inexpensive Disks (RAID) A lei de Amdahl no Capítulo 1 nos lembra que é precipitado negligenciar a E/S nessa revolução paralela. Um exemplo simples demonstra isso.
Impacto da E/S sobre o desempenho do sistema
Suponha que tenhamos um benchmark executado em 100 segundos de tempo decorrido, dos quais 90 segundos é tempo de CPU e o restante é tempo de E/S. Suponha que o número de processadores dobre a cada dois anos, mas os processadores permanecem na mesma velocidade, e o tempo de E/S não melhora. O quanto mais rápido nosso programa será executado ao final de seis anos?
EXEMPLO
Sabemos que
RESPOSTA
Tempodecorrido = tempodeCPU + tempode E/S 100 = 90 + tempode E/S Tempode E/S = 10 segundos Os novos tempos de CPU e os tempos decorridos resultantes são calculados na tabela a seguir. Após n anos 0 ano
Tempo de CPU 90 segundos
Tempo de E/S
Tempo decorrido
% de tempo de E/S
10 segundos
100 segundos
10%
2 anos
90 = 45 segundos 2
10 segundos
55 segundos
18%
4 anos
45 = 23 segundos 2
10 segundos
33 segundos
31%
6 anos
23 = 11 segundos 2
10 segundos
21 segundos
47%
A melhoria no desempenho da CPU após seis anos é 90 =8 11 Porém, a melhoria no tempo decorrido é de apenas 100 = 4‚7 21 e o tempo de E/S aumentou de 10% para 47% do tempo decorrido. Logo, a revolução paralela precisa chegar à E/S e também ao cálculo, ou o esforço gasto paralelizando poderia ser gasto sempre que programas realizam E/S, o que todos eles precisam fazer. Acelerar o desempenho de E/S foi a motivação original dos arrays de disco (veja Seção 6.14 no site), No final dos anos 1980, o armazenamento em alto desempenho preferido eram discos grandes e dispendiosos, como os maiores na Figura 6.4. O argumento foi que, substituindo alguns discos grandes por muitos discos pequenos, o desempenho melhoria porque haveria mais cabeças de leitura. Essa passagem é uma boa escolha para processadores múltiplos
484
RAID (RedundantArrays of Inexpensive Disks) Uma organização de discos que usa um array de discos pequenos e baratos para aumentar o desempenho e a confiabilidade.
Capítulo 6 Armazenamento e outros tópicos de E/S
também, pois muitas cabeças de leitura/escrita significam que o sistema de armazenamento poderia dar suporte a muito mais acessos independentes, e também transferências grandes se espalhariam por muitos discos. Ou seja, você poderia conseguir altas taxas de E/S por segundo e altas taxas de transferência de dados. Além do desempenho mais alto, poderia haver vantagens no custo, na potência e no espaço, pois discos menores geralmente são mais eficientes por gigabyte do que discos maiores. A falha no argumento foi que os arrays de disco poderiam tornar a confiabilidade muito pior. Essas unidades menores e menos dispendiosas tinham menores valores de MTTF que as unidades grandes, porém, mais importante que isso, substituindo uma única unidade por, digamos, 50 unidades pequenas, a taxa de falha subiria por um fator de pelo menos 50! A solução foi acrescentar redundância de modo que o sistema pudesse lidar com as falhas de disco sem perder informações. Tendo muitos discos pequenos, o custo da redundância extra para melhorar a confiabilidade é pequeno em relação a solução com alguns discos grandes. Assim, a confiabilidade era mais econômica se você construísse um array redundante de discos mais baratos. Essa observação levou ao seu nome: array redundante de discos pouco dispendiosos, abreviado como RAID. Em retrospecto, embora sua invenção fosse motivada pelo desempenho, a confiabilidade foi o principal motivo para a popularidade geral do RAID. A revolução paralela destacou a questão do desempenho original do RAID. O restante desta seção analisa as opções para confiabilidade e seus impactos sobre custo e desempenho. De quanta redundância você precisa? Você precisa de informações extras para encontrar as falhas? Importa como você organiza os dados e as informações de verificação extra nesses discos? O artigo que criou o termo deu uma resposta evolutiva a essas questões, começando com a solução mais simples, porém mais dispendiosa. A Figura 6.12 mostra a evolução e um exemplo de custo no número de discos de verificação extras. Para acompanhar a evolução, os autores numeraram os estágios do RAID, e eles ainda são usados hoje.
FIGURA 6.12 RAID para um exemplo de quatro discos de dados, mostrando discos de verificação extras por nível de RAID e empresas que utilizam cada nível. As Figuras 6.13 e 6.14 explicam a diferença entre RAID 3, RAID 4 e RAID 5.
6.9 Paralelismo e E/S: Redundant Arrays of Inexpensive Disks (RAID) 485
Nenhuma redundância (RAID 0)
O simples espalhamento dos dados por vários discos, chamado striping, força automaticamente os acessos a vários discos. O striping por um conjunto de discos faz com que a coleção apareça ao software como um único disco grande, que simplifica o gerenciamento do armazenamento. Isso também melhora o desempenho para acessos grandes, pois muitos discos podem operar ao mesmo tempo. Os sistemas de edição de vídeo, por exemplo, normalmente repartem seus dados e podem não se preocupar com a confiabilidade tanto quanto, digamos, os bancos de dados. RAID 0 é um nome errado, pois não existe redundância. Entretanto, os níveis de RAID normalmente são deixados para o operador definir ao criar um sistema de armazenamento, e RAID 0 normalmente está listado como uma das opções. Logo, o termo RAID 0 tornou-se muito utilizado.
striping Alocação de blocos logicamente sequenciais por discos separados para permitir maior desempenho do que um único disco pode oferecer.
Espelhamento (RAID 1)
Esse esquema tradicional para tolerar falhas de disco, chamado espelhamento ou shadowing, utiliza o dobro da quantidade de discos do RAID 0. Sempre que os dados são gravados em um disco, esses dados também são gravados em um disco redundante, de modo que sempre existem duas cópias da informação. Se um disco falhar, o sistema simplesmente vai ao “espelho” e lê seu conteúdo para obter a informação desejada. O espelhamento é a solução de RAID mais dispendiosa, pois exige mais discos.
espelhamento Escrever dados idênticos em vários discos, para aumentar a disponibilidade dos dados.
Código de detecção e correção de erros (RAID 2)
RAID 2 utiliza um esquema de detecção e correção de erros que é mais utilizado para memórias (veja Apêndice C). Como RAID 2 caiu em desuso, não iremos descrevê-lo aqui. Paridade intercalada por bit (RAID 3)
O custo da disponibilidade mais alta pode ser reduzido para 1/n, onde n é o número de discos em um grupo de proteção. Em vez de ter uma cópia completa dos dados originais para cada disco, só precisamos acrescentar informações redundantes suficientes para restaurar a informação perdida em uma falha. Leituras ou escritas vão para todos os discos no grupo, com um disco extra para manter as informações de verificação caso haja uma falha. RAID 3 é comum em aplicações com grandes conjuntos de dados, como multimídia e alguns códigos científicos. Paridade é um esquema desse tipo. Os leitores não acostumados com a paridade podem pensar no disco redundante como aquele com a soma de todos os dados dos outros discos. Quando um disco falha, então você subtrai todos os dados nos discos bons do disco de paridade; a informação restante deverá ser a informação que falta. A paridade é simplesmente a soma módulo dois. Diferente de RAID 1, muitos discos precisam ser lidos para determinar os dados que faltam. A suposição por trás dessa técnica é a de que levar mais tempo para recuperar-se de uma falha, mas gastar menos com armazenamento redundante, é uma boa escolha. Paridade intercalada por bloco (RAID 4)
RAID 4 usa a mesma razão de discos de dados e discos de verificação do RAID 3, mas eles acessam dados de formas diferentes. A paridade é armazenada como blocos e associada a um conjunto de blocos de dados. Em RAID 3, cada acesso ia para todos os discos. Contudo, algumas aplicações preferem acessos menores, permitindo que acessos independentes ocorram em paralelo. Essa é a finalidade do RAID níveis 4 a 6. Como a informação de detecção de erro em cada setor é verificada nas leituras para ver se os dados estão corretos, essas “leituras pequenas” a cada disco podem ocorrer de forma independente, desde que o acesso mínimo seja de um setor. No contexto do RAID, um acesso pequeno vai para apenas um disco em um grupo de proteção, enquanto um acesso grande vai para todos os discos em um grupo de proteção. As escritas são outro problema. Pode parecer que cada escrita pequena exigiria que todos os outros discos fossem acessados para ler o restante das informações necessárias no recálculo da nova paridade, como na Figura 6.13. Uma “escrita pequena” exigiria a
grupo de proteção O grupo de discos de dados ou blocos que compartilham um disco ou bloco de verificação comum.
486
Capítulo 6 Armazenamento e outros tópicos de E/S
FIGURA 6.13 Pequena atualização de escrita em RAID 4. Essa otimização para pequenas escritas reduz a quantidade de acessos ao disco, bem como a quantidade de discos ocupados. Essa figura considera que temos quatro blocos de dados e um bloco de paridade. O ingênuo cálculo de paridade do RAID 4 à esquerda da figura lê os blocos D1, D2 e D3 antes de acrescentar o bloco D0’ para calcular a nova paridade P’. (Caso você esteja questionando, os novos dados D0’ vêm diretamente da CPU, de modo que os discos não estão envolvidos na sua leitura.) O atalho RAID 4 à direita lê o valor antigo D0 e o compara com o novo valor D0’ para ver quais bits mudarão. Em seguida, você lê a paridade antiga P e depois muda os bits correspondentes para formar P’. A função lógica OR exclusivo faz exatamente o que queremos. Esse exemplo substitui três leituras de disco (D1, D2, D3) e duas escritas (D0’, P’) envolvendo todos os discos para duas leituras de disco (D0, P) e duas escritas de disco (D0’, P’), que envolvem apenas dois discos. Aumentar o tamanho do grupo de paridade aumenta as economias do atalho. RAID 5 utiliza o mesmo atalho.
leitura dos dados antigos e da paridade antiga, adicionando as novas informações e depois escrevendo a nova paridade no disco de paridade e os novos dados no disco de dados. A ideia principal para reduzir esse overhead é que a paridade é simplesmente uma soma de informações; observando quais bits mudam quando escrevemos as novas informações, só precisamos mudar os bits correspondentes no disco de paridade. O lado direito da Figura 6.13 mostra o atalho. Temos de ler os dados antigos do disco sendo escrito, comparar os dados antigos com os novos para ver quais bits mudam, ler a paridade antiga, alterar os bits correspondentes, depois escrever os novos dados e a nova paridade. Assim, a pequena escrita envolve quatro acessos de disco a dois discos, em vez de acessar todos os discos. Essa organização é RAID 4. Paridade distribuída intercalada por bloco (RAID 5)
RAID 4 aceita de forma eficiente uma mistura de leituras grandes, escritas grandes e leituras pequenas, e também permite escritas pequenas. Uma desvantagem para o sistema é que o disco de paridade precisa ser atualizado em cada escrita, de modo que o disco de paridade é o gargalo para escritas back-to-back. Para resolver o gargalo da escrita de paridade, a informação de paridade pode ser espalhada por todos os discos, de modo que não haja um único gargalo para escritas. A organização da paridade distribuída é RAID 5. A Figura 6.14 mostra como os dados são distribuídos no RAID 4 versus RAID 5. Como vemos na organização da direita, em RAID 5, a paridade associada a cada linha de blocos de dados não é mais restrita a um único disco. Essa organização permite que várias escritas ocorram simultaneamente, desde que os blocos de paridade não estejam localizados no mesmo disco. Por exemplo, uma escrita no bloco 8 à direita também precisa acessar seu bloco de paridade P2, ocupando assim o primeiro e terceiro discos. Uma segunda escrita no bloco 5, à direita, implicando uma atualização no seu bloco de paridade P1, acessa o segundo e quarto discos e, assim, poderia ocorrer simultaneamente com a escrita no bloco 8. Essas mesmas escritas na organização à esquerda resultam em mudanças nos blocos P1 e P2, ambas no quinto disco, que é um gargalo. Redundância P + Q (RAID 6)
Os esquemas baseados em paridade protegem contra uma única falha autoidentificável. Quando uma correção de única falha não é suficiente, a paridade pode ser generalizada
6.9 Paralelismo e E/S: Redundant Arrays of Inexpensive Disks (RAID) 487
FIGURA 6.14 Paridade intercalada por bloco (RAID 4) versus paridade distribuída intercalada por bloco (RAID 5). Distribuindo os blocos de paridade a todos os discos, algumas escritas pequenas podem ser realizadas em paralelo.
para ter um segundo cálculo sobre os dados e outro disco de verificação de informações. Esse segundo bloco de verificação permite a recuperação de uma segunda falha. Assim, o overhead do armazenamento é o dobro daquele do RAID 5. O atalho de escrita pequena da Figura 6.13 também funciona, exceto que agora existem seis acessos a disco, em vez de quatro para atualizar as informações de P e Q. Resumo de RAID
RAID 1 e RAID 5 são bastante utilizados em servidores; uma estimativa é de que 80% de discos nos servidores se encontrem em algum sistema RAID. Um ponto fraco dos sistemas RAID é o reparo. Primeiro, para evitar tornar os dados indisponíveis durante o reparo, o array precisa ser designado de modo a permitir que os discos que falharam sejam substituídos sem ter de desligar o sistema. RAIDs possuem redundância suficiente para permitir a operação contínua, mas o hot swapping de discos impõe demandas sobre o projeto físico e elétrico do array e as interfaces de disco. Segundo, outra falha poderia ocorrer durante o reparo, de modo que o tempo de reparo afeta as chances de perder dados: quanto maior for o tempo de reparo, maiores as chances de outra falha que causará perda de dados. Em vez de ter de esperar que o operador traga um disco bom, alguns sistemas incluem reservas em standby, de modo que os dados podem ser reconstruídos imediatamente na descoberta da falha. O operador pode, então, substituir os discos que falharam sem tanta pressa. Perceba que um operador humano que, em última análise, determina quais discos devem ser removidos. Como mostra a Figura 6.3, os operadores são apenas humanos, de modo que ocasionalmente poderão remover um disco bom no lugar do disco com defeito, ocasionando uma falha de disco irrecuperável. Além de projetar o sistema RAID para reparo, existem questões sobre como a tecnologia de disco muda com o tempo. Embora os fabricantes de disco citem um MTTF muito alto para seus produtos, esses números estão sob condições nominais. Se um array de disco em particular tiver sido sujeito a ciclos de temperatura devido a, digamos, a falha do sistema de ar-condicionado, ou a sacudidas devido a um projeto, uma construção ou uma instalação de rack ineficaz, as taxas de falha podem ser de três a seis vezes maior (veja a falácia posteriormente neste capítulo). O cálculo de confiabilidade RAID considera independência entre falhas de disco, mas as falhas poderiam estar correlacionadas, pois tal dano devido ao ambiente provavelmente aconteceria em todos os discos no array. Outro problema é que, a largura de banda do disco está crescendo mais lentamente que a capacidade do disco, o tempo para reparo de um disco em um sistema RAID está aumentando, o que, por sua vez, aumenta as chances de uma segunda falha. Por exemplo, um disco SATA de 1000GB
hot swapping Substituição de um componente de hardware enquanto o sistema está em execução.
reservas em standby Recursos de hardware de reserva que podem imediatamente tomar o lugar de um componente defeituoso.
488
Capítulo 6 Armazenamento e outros tópicos de E/S
poderia levar quase três horas para ser lido sequencialmente, sem considerar interferência. Dado que o RAID danificado provavelmente continuará a atender com dados, a reconstrução poderia ser bastante esticada. Além de aumentar esse tempo, outro problema é que a leitura de muito mais dados durante a reconstrução significa aumentar a chance de uma falha irrecuperável de leitura de mídia, que resultaria em perda de dados. Outros argumentos para preocupação com múltiplas falhas simultâneas são o aumento do número de discos nos arrays e o uso de discos SATA, que são mais lentos e têm maior capacidade que os discos tradicionais para empresa. Logo, essas tendências levaram a um interesse cada vez maior na proteção contra mais de uma falha, e, portanto, o RAID 6 está cada vez mais sendo oferecido como uma opção e sendo usado no setor.
Verifique você mesmo
Quais das seguintes afirmações são verdadeiras sobre os níveis RAID 1, 3, 4, 5 e 6? 1. Os sistemas RAID contam com a redundância para conseguir a alta disponibilidade. 2. RAID 1 (espelhamento) possui o mais alto overhead de disco de verificação. 3. Para pequenas escritas, RAID 3 (paridade intercalada por bit) possui a pior vazão. 4. Para grandes escritas, RAID 3, 4 e 5 possuem a mesma vazão. Detalhamento: Uma questão é como o espelhamento interage com o striping. Suponha que você tivesse, digamos, quatro discos de dados para armazenar e oito discos físicos para usar. Você criaria quatro pares de discos – cada um organizado como RAID 1 – e depois faria striping dos dados nos quatro pares RAID 1? Como alternativa, você criaria dois conjuntos de quatro discos – cada um organizado como RAID 0 – e depois espelharia as escritas nos dois conjuntos RAID 0? A terminologia RAID evoluiu para chamar o primeiro de RAID 1 + 0, ou RAID 10 (“espelhos com striping”) e o segundo de RAID 0 + 1 ou RAID 01 (“striping espelhado”).
6.10
Vida real: servidor Sun Fire x4150
Além da revolução no modo como os microprocessadores são construídos, estamos vendo uma revolução no modo como o software é entregue. Em vez do modelo tradicional do software vendido em um CD ou entregue pela Internet para ser instalado no seu computador, a alternativa é o software como um serviço. Ou seja, você vai à Internet para realizar seu trabalho em um computador que roda o software que você deseja usar para fornecer o serviço desejado. O exemplo mais comum provavelmente seja a pesquisa na Web, mas existem serviços para edição e armazenamento de foto, processamento de documentos, armazenamento de banco de dados, mundos virtuais e outros. Se você procurar, provavelmente poderá encontrar uma versão de serviço de quase todo programa que utiliza no seu computador desktop. Essa mudança levou à construção de grandes centros de dados para manter computadores e discos executando os serviços utilizados por milhões de usuários externos. Como deverão ser os computadores se eles forem projetados para serem colocados nesses grandes centros de dados? Certamente não é preciso que todos tenham monitores e teclados. Claramente, a eficiência no espaço e a eficiência na energia serão importantes se você tiver 10.000 deles em um centro de dados, além dos aspectos tradicionais do custo e desempenho. A questão relacionada é: como deverá ser o armazenamento em um centro de dados? Embora existam muitas opções, uma versão comum é incluir discos com o processador e a memória, e tornar essa unidade inteira o bloco de montagem. Para contornar questões sobre confiabilidade, a própria aplicação faz cópias redundantes e é responsável por mantê-las coerentes e recuperar-se de falhas.
6.10 Vida real: servidor Sun Fire x4150 489
A indústria de TI em grande parte concorda com alguns padrões no projeto físico dos computadores para o centro de dados, especificamente o rack utilizado para manter os computadores no centro de dados. O mais comum é o rack de 19 polegadas (48cm) de largura. Os computadores projetados para o rack são chamados, naturalmente, de montagem de rack, mas também são chamados de subrack ou simplesmente prateleira. Como o espaçamento tradicional entre os furos para conectar as prateleiras é de 1,75 polegadas (4,45cm), essa distância normalmente é chamada de unidade de rack, ou simplesmente unidade (U). O rack mais comum de 48cm tem 42 U de altura, que é 42 x 4,45, ou aproximadamente 187cm de altura. A profundidade da prateleira varia. Logo, o menor computador para montagem em rack é de 48cm de largura e 4,45cm de altura, normalmente chamados de computadores 1U ou servidores 1U. Devido às suas dimensões, eles ganharam o apelido de caixas de pizza. A Figura 6.15 mostra um exemplo de um rack padrão, preenchido com 42 servidores 1U.
FIGURA 6.15 Um rack padrão de 48cm preenchido com 42 servidores 1U. Este rack tem 42 servidores “caixa de pizza” 1U. Fonte: http://gchelpdesk.ualberta.ca/news/07mar06/cbhd_news_07mar06.php.
490
Capítulo 6 Armazenamento e outros tópicos de E/S
A Figura 6.16 mostra o Sun Fire x4150, um exemplo de um servidor 1U. Configurado de forma máxima, essa caixa 1U contém: j
8 processadores de 2,66GHz, espalhados por dois soquetes (2 Intel Xeon 5345).
j
64GB de DRAM DDR2-667, espalhadas por 16 FBDIMMs de 4GB.
j
8 unidades de disco SAS de 73GB e 6,35cm a 15.000 RPM.
j
1 controlador RAID (admitindo RAID 0, RAID 1, RAID 5 e RAID 6).
j
4 portas Ethernet 10/100/1000.
j
3 portas PCI Express x8.
j
4 portas USB 2.0 externas e 1 interna.
A Figura 6.17 mostra a conectividade e larguras de banda dos chips da placa mãe. As Figuras 6.9 e 6.10 descrevem o chip set de E/S para o Intel 5345, e a Figura 6.5 descreve os discos SAS no Sun Fire x4150. Para esclarecer o aviso sobre o projeto de um sistema de E/S na Seção 6.8, vamos realizar uma avaliação de desempenho simples para ver onde poderiam estar os gargalos de uma aplicação hipotética.
FIGURA 6.16 Frente e fundos do servidor 1U Sun Fire x4150. As dimensões são 4,45cm de altura por 48cm de largura. As oito unidades de disco de 6,35cm podem ser substituídas da frente. No canto superior direito está um DVD e duas portas USB. A figura de baixo rotula os itens na parte traseira do servidor. Ela tem fontes de alimentação e ventiladores redundantes, para permitir que o servidor continue operando apesar de falhas de um desses componentes.
6.10 Vida real: servidor Sun Fire x4150 491
FIGURA 6.17 Conexões lógicas e larguras de banda dos componentes no Sun Fire x4150. Os três conectores PCIe permitem que placas x16 sejam conectadas, mas somente oferece oito pistas de largura de banda ao MCH. Fonte: Figura 5 do “SUN FIRE™ X4150 AND X4450 SERVER ARCHITECTURE” (veja www.sun.com/servers/x64/x4150/).
Projeto do sistema de E/S
Considere o seguinte sobre o Sun Fire x4150: j
O programa do usuário utiliza 200.000 instruções por operação de E/S.
j
O sistema operacional utiliza em média 100.000 instruções por operação de E/S.
j
A carga de trabalho consiste em leituras de 64KB.
j
Cada processador sustenta 1 bilhão de instruções por segundo.
EXEMPLO
Ache a taxa máxima de E/S sustentável para um Sun Fire x4150 totalmente carregado para leituras aleatórias e leituras sequenciais. Considere que as leituras sempre podem ser feitas em um disco ocioso, se houver um (ou seja, ignore conflitos de disco) e que o controlador RAID não é o gargalo. Vamos primeiro achar a taxa de E/S de um único processador. Cada E/S utiliza 200.000 instruções do usuário e 100.000 instruções do SO, de modo que Taxa de E/S máxima de 1 processador = E/Ss Taxa deexecuçãoda instrução 1 × 109 = = 3‚333 3 Instruções por E/S (200 + 100) × 10 seg
RESPOSTA
492
Capítulo 6 Armazenamento e outros tópicos de E/S
Como um único soquete Intel 5345 tem quatro processadores, ele pode realizar 13,333 IOPS. Dois soquetes com oito processadores podem realizar 26.667 IOPS. Vamos determinar os IOPS por disco para leituras aleatórias e sequenciais para o disco SAS de 6,35cm descrito na Figura 6.5. Em vez de usar o tempo médio de busca do fabricante de disco, vamos supor que ele seja apenas um quarto desse tempo, como normalmente acontece (veja Seção 6.3). O tempo por leitura aleatória de um único disco: Tempo por E / S nodisco = Busca + temporotacional + Tempode transferência =
2‚9 64 KB + 2‚0 ms + = 3‚3ms 4 ms 112 MB/seg
Assim, cada disco pode completar 1000ms/3,3ms ou 303 E/Ss por segundo, e oito discos realizam 2424 leituras aleatórias por segundo. Para leituras sequenciais, isso é apenas o tempo de transferência dividido pela largura de banda do disco: 112 MB/seg = 1750IOPS 64 KB Oito discos podem realizar 14.000 leituras sequenciais de 64KB. Precisamos ver se os caminhos dos discos para a memória e os processadores são um gargalo. Vamos começar com a interconexão PCI Express da placa RAID para chip da bridge norte. Cada pista de uma PCIe é de 250MB/segundo, de modo que oito pistas podem realizar 2GB/segundo. Taxa de E / S máxima da PCIe x8 =
Largura de banda PCI 2 × 109 E / Ss = = 31‚250 Bytes por E / S 64 × 103 segundo
Até mesmo oito discos transferindo sequencialmente utilizam menos de metade do link PCIe x8. Quando os dados chegam à MCB, eles precisam ser escritos na DRAM. A largura de banda de uma FBDIMM DDR2 de 667MHz é de 5336MB/segundo. Uma única DIMM pode executar 5336 MB / seg = 83‚375IOPS 64 KB A memória não é um gargalo mesmo com uma DIMM, e temos 16 em um Sun Fire x4150 totalmente configurado. O link final na cadeia é o Front Side Bus que conecta o hub da bridge norte ao soquete Intel 5345. Sua largura de banda de pico é de 10,6GB/seg, mas a Seção 7.10 lhe sugere não obter mais de metade do pico. Cada E/S transfere 64KB, de modo que Taxa máx.E/SdoFSB =
Largura de banda do barramento 5‚3 × 109 E/Ss = 81.540 = Bytes por E/S 64 × 103 segundo
Existe um Front Side Bus por soquete, de modo que o pico FSB dual é mais de 150.000 IOPS, e mais uma vez, o FSB não é um gargalo. Logo, um Sun Fire x4150 totalmente configurado pode sustentar a largura de banda de pico dos oito discos, que é 2424 leituras aleatórias por segundo ou 14.000 leituras sequenciais por segundo. Observe o número significativo de suposições de simplificação que são necessárias para realizar este exemplo. Na prática, muitas dessas simplificações poderiam não ser mantidas para aplicações críticas com uso intenso de E/S. Por esse motivo, executar uma carga de trabalho realista ou benchmark relevante normalmente é a única forma plausível de avaliar o desempenho da E/S.
6.10 Vida real: servidor Sun Fire x4150 493
Conforme mencionamos no início desta seção, esses novos centros de dados se preocupam com a potência e o espaço, além do custo e do desempenho. A Figura 6.18 mostra a potência ociosa e de pico exigida por um Sun Fire x4150 totalmente configurado, com um desmembramento por cada componente. Vejamos as configurações alternativas do Sun Fire x4150 para economizar energia.
Avaliação de potência do sistema de E/S
Reconfigure um Sun Fire x4150 para minimizar a potência, supondo que a carga de trabalho no exemplo anterior seja a única atividade nesse servidor 1U. Para conseguir as 2424 leituras aleatórias de 64KB por segundo do exemplo anterior, precisamos de todos os oito discos e da controladora PCI RAID. Pelos cálculos anteriores, uma única memória DIMM pode admitir mais de 80.000 IOPS, de modo que podemos economizar potência na memória. A memória mínima do Sun Fire x4150 é de duas DIMMs, de modo que podemos economizar a potência (e custo) de 14 DIMMs de 4GB. Um único soquete pode admitir 13.333 IOPS, de modo que também podemos reduzir o número de soquetes Intel E5345 por um. Usando os números na Figura 6.18, a potência total do sistema agora é:
EXEMPLO RESPOSTA
Potência Ociosa leituras aleatórias = 154 + 2 × 10 + 8 × 8 + 15 = 253 watts Potência Pico leituras aleatórias = 215 + 2 × 11 + 8 × 8 + 15 = 316 watts ou uma redução na potência por um fator de 1,6 a 1,7. O sistema original pode desempenhar 14.000 leituras sequenciais de 64KB por segundo. Ainda precisamos de todos os discos e da controladora de disco, e o mesmo número de DIMMs pode tratar dessa carga mais alta. Essa carga de trabalho excede uma potência de processamento do único soquete Intel E5345, de modo que precisamos acrescentar um segundo. Potência Ociosa leituras sequenciais = 154 + 22 + 2 × 10 + 8 × 8 + 15 = 275 watts Potência Pico leituras sequenciais = 215 + 79 + 2 × 11 + 8 × 8 + 15 = 395 watts ou uma redução na potência por um fator de 1,4 a 1,5.
FIGURA 6.18 Potência de pico e idle do Sun Fire x4150 totalmente configurado. Esses experimentos vieram enquanto executando SPECJBB com 29 configurações diferentes, então o pico de potência poderia ser diferente enquanto executando aplicações diferentes. Fonte: www.sun.com/servers/x64/x4150/calc.
494
Capítulo 6 Armazenamento e outros tópicos de E/S
6.11 Tópicos avançados: Redes
As redes estão ganhando mais popularidade com o passar do tempo e, diferente de outros dispositivos de E/S, existem muitos livros e cursos sobre elas. Para os leitores que não fizeram nenhum curso nem leram livros sobre redes, a Seção 6.11, no site, oferece uma visão geral dos tópicos e da terminologia, incluindo interligação de redes, o modelo OSI, famílias de protocolos, como TCP/IP, redes de longa distância, como ATM, redes locais, como Ethernet, e redes sem fio, como IEEE 802.11.
6.12 Falácias e armadilhas
Falácia: o tempo médio para falha indicado para discos é 1.200.000 horas ou quase 140 anos, de modo que os discos praticamente nunca falham. As práticas de marketing atuais dos fabricantes de disco podem enganar os usuários. Como esse MTTF é calculado? No início do processo, os fabricantes colocam milhares de discos em uma sala, os colocam para trabalhar por alguns meses, e contam a quantidade que falha. Eles calculam o MTTF como o número total de horas que os discos estiveram acumuladamente ativos dividido pelo número que falhou. Um problema é que esse número é muito superior ao tempo de vida de um disco, que normalmente é cinco anos ou 43.800 horas. Para esse grande MTTF fazer algum sentido, esses fabricantes argumentam que o cálculo corresponde a um usuário que compra um disco, e depois continua substituindo o disco a cada cinco anos – o tempo de vida planejado do disco. A reivindicação é que, se muitos clientes (e seus bisnetos) fizessem isso para o próximo século, na média eles substituiriam um disco 27 vezes antes de uma falha, ou cerca de 140 anos. Uma medida mais útil seria a porcentagem de discos que falham, chamada taxa anual de falha (AFR). Considere 1.000 discos com um MTTF de 1.200.000 horas e que os discos sejam usados 24 horas por dia. Se você substituísse os discos que falharam por um novo com as mesmas características de confiabilidade, o número que falharia por ano (8.760 horas) é Discos falhos =
1.000unidades × 8.760horas/unidade = 7‚3 1.200.000horas/falha
Explicando de uma forma alternativa, a AFR é 0,73%. Os fabricantes de disco estão começando a citar a AFR além do MTTB para dar aos usuários uma melhor intuição sobre o que esperar a respeito de seus produtos. Falácia: as taxas de falha de disco em campo combinam com suas especificações. Dois estudos recentes avaliaram grandes coleções de discos para verificar o relacionamento entre os resultados em campo comparados com as especificações. Um estudo foi de quase 100.000 discos ATA e SCSI que tinham uma cotação de MTTF de 1.000.000 a 1.500.000 horas, ou AFR de 0,6% a 0,8%. Eles descobriram que AFRs de 2% a 4% são comuns, normalmente três a cinco vezes as taxas especificadas [Schroeder e Gibson, 2007]. Um segundo estudo de mais de 100.000 discos ATA, que tinham um valor AFR de aproximadamente 1,5%, viu taxas de falha de 1,7% das unidades em seu primeiro ano subirem para 8,6% das unidades em seu terceiro ano, ou cerca de cinco a seis vezes a taxa especificada [Pinheiro, Weber e Barroso, 2007]. Falácia: uma interconexão de 1GB/seg pode transferir 1GB de dados em 1 segundo.
6.12 Falácias e armadilhas 495
Primeiro, você em geral não pode usar 100% de qualquer recurso do computador. Para um barramento, você ficaria satisfeito em conseguir 70% a 80% da largura de banda de pico. O tempo para enviar o endereço, o tempo para confirmar os sinais e os atrasos enquanto se espera para usar um barramento ocupado estão entre os motivos para você não poder usar 100% de um barramento. Segundo, a definição de um gigabyte de armazenamento e um gigabyte por segundo de largura de banda não correspondem. Conforme discutimos na Seção 6.7, as medidas de largura de banda de E/S normalmente são cotadas em base 10 (ou seja, 1GB/seg = 109 bytes/ seg), enquanto 1GB de dados normalmente é uma medida na base 2 (ou seja, 1GB = 230 bytes). Qual é o significado dessa distinção? Se pudéssemos usar 100% do barramento para a transferência de dados, o tempo para transferir 1GB de dados em uma interconexão de 1GB/seg seria, na realidade, 230 1.073.741.824 = = 1‚073741824 ≈ 1‚07 segundo 109 1.000.000.000 Armadilha: tentar oferecer recursos apenas dentro da rede versus fim a fim. O problema é fornecer em um nível inferior recursos que só podem ser cumpridos no nível mais alto, satisfazendo assim apenas parcialmente à demanda da comunicação. Saltzer, Reed e Clark [1984] explicam o argumento de fim a fim como A função em questão só pode ser especificada completa e corretamente com o conhecimento e a ajuda da aplicação que fica nas extremidades do sistema de comunicação. Portanto, não é possível oferecer essa função questionada como um recurso do próprio sistema de comunicação. Seu exemplo da armadilha foi uma rede no MIT que usava vários gateways, cada qual acrescentando uma soma de verificação de um gateway para o seguinte. Os programadores da aplicação assumiram a precisão garantida pela soma de verificação, acreditando incorretamente que a mensagem estava protegida enquanto armazenada na memória de cada gateway. Um gateway tinha uma falha intermitente que trocava um par de bytes para cada milhão de bytes transferidos. Com o tempo, o código-fonte de um sistema operacional era repetidamente passado pelo gateway, adulterando, dessa forma, o código. A única solução foi corrigir os arquivos-fonte infectados, comparando as listagens em papel e reparando o código manualmente! Se as somas de verificação tivessem sido calculadas e verificadas pela aplicação rodando nos sistemas na ponta, a segurança teria sido garantida. No entanto, existe uma função útil para verificações intermediárias, desde que a verificação fim a fim esteja disponível. Ela pode mostrar que algo está errado entre dois nós, mas não aponta onde se encontra o problema. As verificações intermediárias podem descobrir qual componente está errado. Você precisa de ambos para reparar. Armadilha: mover funções da CPU para o processador de E/S, esperando melhorar o desempenho sem uma análise cuidadosa. Existem muitos exemplos dessa armadilha pegando as pessoas, embora os processadores de E/S, quando usados de forma correta, certamente podem melhorar o desempenho. Um caso frequente dessa falácia é o uso de interfaces de E/S inteligentes que, devido ao maior overhead para configurar uma requisição de E/S, pode ter uma latência pior do que uma atividade de E/S controlada pelo processador (embora, se o processador for liberado suficientemente, a vazão do sistema ainda possa aumentar). Constantemente, o desempenho cai quando o processador de E/S tem um desempenho muito inferior ao do processador principal. Como consequência, uma quantidade pequena do tempo de processador principal é substituída por uma quantidade maior de tempo do processador de E/S. Os projetistas de estações de trabalho têm visto esses dois fenômenos repetidamente. Myer e Sutherland [1968] escreveram um artigo clássico sobre a escolha entre complexidade e desempenho nos controladores de E/S. Apanhando emprestado o conceito religioso da “roda da reencarnação”, eles, por fim, observaram que eram apanhados em
496
Capítulo 6 Armazenamento e outros tópicos de E/S
um loop de aumentar continuamente a potência de um processador de E/S até que ele precisasse do seu próprio coprocessador mais simples: Enfrentamos a tarefa começando com um esquema simples e depois acrescentando comandos e recursos que achamos que melhorariam o poder da máquina. Gradualmente, o processador [de vídeo] tornava-se mais complexo… Finalmente, o processador de vídeo ficou semelhante a um computador completo, com alguns recursos gráficos especiais. E depois aconteceu uma coisa estranha. Sentimo-nos compelidos a acrescentar ao processador um segundo processador subsidiário que, por si só, começou a aumentar em complexidade. Foi então que descobrimos a verdade perturbadora. Projetar um processador de vídeo pode se tornar um processo cíclico sem fim. Na verdade, descobrimos que o processo era tão frustrante que passamos a chamá-lo de “roda da reencarnação”. Armadilha: usar fitas magnéticas para o backup de discos. Mais uma vez, isso é uma falácia e uma armadilha. As fitas magnéticas têm feito parte dos sistemas de computador tanto quanto os discos, pois utilizam tecnologia semelhante aos discos e, por isso, historicamente têm seguido as mesmas melhorias na densidade. A diferença de custo-desempenho histórica entre discos e fitas é baseada em um disco selado, rotativo, com menor tempo de acesso do que o acesso sequencial à fita, mas os spools removíveis de fita magnética significam que muitas fitas podem ser usadas por leitora e que elas podem ser muito longas, de modo que possuem alta capacidade. De modo que, no passado, uma única fita magnética poderia manter o conteúdo de muitos discos, e por ser de 10 a 100 vezes mais barata por gigabyte do que os discos, esse era um meio de backup útil. A alegação foi de que as fitas magnéticas precisam acompanhar os discos, pois as inovações nos discos precisam ajudar as fitas. Essa alegação foi importante porque as fitas eram um pequeno mercado e não poderiam dispor de um grande esforço de pesquisa e desenvolvimento separado. Um motivo para o mercado ser pequeno é que os proprietários de desktop geralmente não fazem backup de discos em fita, e assim, enquanto os desktops são um grande mercado para discos, eles são um pequeno mercado para fitas. Infelizmente, o maior mercado levou os discos a melhorarem muito mais rapidamente do que as fitas. Entre 2000 a 2002, o disco muito mais popular era maior do que a maior fita. Nesse mesmo espaço de tempo, o preço por gigabyte de discos ATA caiu para menos do que o das fitas. Os defensores da fita agora alegam que elas possuem requisitos de compatibilidade que não são impostos sobre os discos; as leitoras de fita precisam ler ou escrever a geração atual e anterior de fitas e precisam ler as quatro últimas gerações de fitas. Como os discos são sistemas fechados, as cabeças de disco só precisam ler os pratos embutidos, e essa vantagem explica por que os discos estão melhorando muito mais rapidamente. Hoje, algumas organizações retiraram as fitas, usando redes e discos remotos para replicar os dados geograficamente. Na verdade, muitas empresas oferecendo software como serviço utilizam componentes baratos, mas replicam os dados em nível de aplicação por diferentes locais. Os locais são selecionados de modo que os desastres não prejudiquem os dois locais, permitindo um tempo de recuperação instantâneo. (Um tempo de recuperação longo é outra desvantagem séria da natureza serial das fitas magnéticas.) Essa solução depende dos avanços na capacidade do disco e na largura de banda da rede, para fazer sentido economicamente, mas esses dois estão recebendo um investimento muito maior e, portanto, possuem registros de realização recentes melhores do que a fita. Falácia: os sistemas operacionais são o melhor local para programar acessos ao disco. Como dissemos na Seção 6.3, interfaces de nível mais alto, como ATA e SCSI, oferecem endereços de bloco lógicos para o sistema operacional hospedeiro. Dada essa abstração de alto nível, o melhor que um SO pode fazer para tentar ajudar no desempenho é classificar os endereços lógicos de bloco em ordem crescente. Porém, como o disco conhece o mapeamento real dos endereços lógicos na geometria física de setores, trilhas e superfícies, ele pode reduzir as latências de rotação e de busca pelo reescalonamento. Por exemplo, suponha que a carga de trabalho seja quatro leituras [Anderson, 2003]:
6.12 Falácias e armadilhas 497
Operação
LBA inicial
Tamanho
Leitura
724
8
Leitura
100
16
Leitura
9987
1
Leitura
26
128
O hospedeiro poderia reordenar as quatro leituras por ordem de bloco lógico: Operação
LBA inicial
Tamanho
Leitura
26
128
Leitura
100
16
Leitura
724
8
Leitura
9987
1
Dependendo do local relativo dos dados no disco, a reordenação poderia tornar isso pior, como mostra a Figura 6.19. As leituras programadas pelo disco terminam em três quartos de uma rotação do disco, mas as leituras programadas pelo SO exigem três rotações. Armadilha: usar uma taxa de transferência de pico de uma parte do sistema de E/S para fazer projeções de desempenho ou comparações de desempenho. Muitos dos componentes de um sistema de E/S, desde os dispositivos até os controladores e barramentos, são especificados por meio de suas larguras de banda de pico. Na prática, essas medidas de largura de banda de pico em geral são baseadas em suposições irrealistas sobre o sistema ou não são alcançáveis, devido a outras limitações do sistema. Por exemplo, cotando o desempenho do barramento, a velocidade de transferência de pico às vezes é especificada usando um sistema de memória impossível de criar. Para sistemas em rede, o overhead do software para iniciar a comunicação é ignorado. O barramento PCI de 32 bits, 33MHz, possui uma largura de banda de pico de cerca de 133MB/seg. Na prática, até mesmo para transferências longas, é difícil sustentar mais do que cerca de 80MB/seg para sistemas de memória reais. A Lei de Amdahl também nos lembra que a vazão de um sistema de E/S será limitada pelo componente de menor desempenho no caminho de E/S.
FIGURA 6.19 Exemplo mostrando acessos programados pelo SO versus disco, rotulados com “fila ordenada pelo hospedeiro” e “fila ordenada pela unidade”. O primeiro leva três rotações para completar as quatro leituras, enquanto o segundo as completa em apenas três quartos de uma rotação (de Anderson [2003]).
498
Capítulo 6 Armazenamento e outros tópicos de E/S
6.13
Comentários finais
Os sistemas de E/S são avaliados em diversas características diferentes: confiança; a variedade de dispositivos de E/S aceitos; o número máximo de dispositivos de E/S; custo; e desempenho, medidos tanto em latência quanto em vazão. Esses objetivos levam a esquemas bastante variados para interface de dispositivos de E/S. Nos sistemas inferiores e intermediários, o DMA com buffer provavelmente será o mecanismo de transferência dominante. Nos sistemas de alto nível, a latência e a largura de banda podem ser ambos importantes, e o custo pode ser secundário. Vários caminhos para dispositivos de E/S com buffer limitado normalmente caracterizam sistemas de E/S de alto nível. Em geral, ser capaz de acessar os dados em um dispositivo de E/S a qualquer tempo (alta disponibilidade) torna-se mais importante quando os sistemas crescem. Como resultado, a redundância e os mecanismos de correção de erros tornam-se mais e mais prevalentes enquanto ampliamos o sistema. As demandas de armazenamento e rede estão crescendo em velocidades sem precedentes, em parte devido às demandas crescentes para que toda a informação esteja na ponta dos seus dedos. Uma estimativa é que a quantidade de informação criada em 2002 foi de 5 exabytes – equivalente a 500.000 cópias do texto da Biblioteca do Congresso dos Estados Unidos –, e essa quantidade total de informações no mundo dobrou nos últimos três anos [Lyman e Varian, 2003]. As direções futuras da E/S incluem expandir o alcance das redes com e sem fio, com quase todo dispositivo potencialmente tendo um endereço IP, e a expansão do papel da memória flash nos sistemas de armazenamento.
Entendendo o desempenho dos programas
O desempenho de um sistema de E/S, seja ele medido por largura de banda ou latência, depende de todos os elementos no caminho entre o dispositivo e a memória, incluindo o sistema operacional que gera os comandos de E/S. A largura de banda da interconexão, da memória e do dispositivo determinam a velocidade de transferência máxima do dispositivo ou para o dispositivo. De modo semelhante, a latência depende da latência do dispositivo, junto com qualquer latência imposta pelo sistema de memória ou barramentos. A largura de banda efetiva e a latência de resposta também dependem de outras requisições de E/S que podem causar disputa por algum recurso no caminho. Finalmente, o sistema operacional é um gargalo. Em alguns casos, o sistema operacional leva muito tempo para entregar uma solicitação de E/S de um programa de usuário a um dispositivo de E/S, levando a uma alta latência. Em outros casos, o sistema operacional efetivamente limita a largura de banda de E/S, devido às limitações no número de operações de E/S simultâneas que ele pode admitir. Lembre-se de que, embora o desempenho possa ajudar a vender um sistema de E/S, os usuários, em sua maioria, exigem confiabilidade e capacidade dos seus sistemas de E/S.
6.14 Perspectiva histórica e leitura adicional A história dos sistemas de E/S é fascinante. A Seção 6.14 oferece um breve histórico dos discos magnéticos, RAID, memória flash, bancos de dados, a internet, a world wide web e como a ethernet continua a triunfar sobre seus desafiantes.
6.15 Exercícios 499
6.15 Exercícios1
Exercício 6.1 A Figura 6.2 descreve diversos dispositivos de E/S em termos de seu comportamento, parceria e taxa de dados. Porém, essas classificações normalmente não oferecem uma imagem completa do fluxo de dados dentro de um sistema. Explore as classificações de dispositivo para os seguintes dispositivos: a.
Piloto automático
b.
Termostato automatizado
6.1.1 [5] <6.1> Em relação aos dispositivos listados na tabela, identifique as interfaces de E/S e classifique-as em termos de seu comportamento e parceria. 6.1.2 [5] <6.1> Para as interfaces identificadas no problema anterior, estime sua taxa de dados. 6.1.3 [5] <6.1> Para as interfaces identificadas no problema anterior, determine se a taxa de dados ou a taxa de operação é a melhor medida do desempenho.
Exercício 6.2 Mean Time Between Failures (MTBF), Mean Time To Replacement (MTTR) e Mean Time To Failure (MTTF) são medidas úteis para avaliar a confiabilidade e a disponibilidade de um recurso de armazenamento. Explore esses conceitos respondendo às perguntas sobre dispositivos com as métricas a seguir. MTTF
MTTR
a.
3 anos
1 dia
b.
7 anos
3 dias
6.2.1 [5] <6.1, 6.2> Calcule o MTBF para cada um dos dispositivos na tabela. 6.2.2 [5] <6.1, 6.2> Calcule a disponibilidade para cada um dos dispositivos na tabela. 6.2.3 [5] <6.1, 6.2> O que acontece à disponibilidade quando o MTTR se aproxima de 0. Essa situação é real? 6.2.4 [5] <61, 6.2> O que acontece com a disponibilidade quando o MTTR se torna muito alto, ou seja, um dispositivo é difícil de reparar? Isso significa que o dispositivo tem baixa disponibilidade?
Exercício 6.3 Os tempos médio e mínimo para ler e escrever nos dispositivos de armazenamento são medições comuns usadas para comparar dispositivos. Usando as técnicas do Capítulo 6, calcule os valores relacionados ao tempo de leitura e escrita para discos com as características a seguir. 1
Contribuição de Perry Alexander, da Universidade do Kansas.
500
Capítulo 6 Armazenamento e outros tópicos de E/S
Tempo de busca médio
RPM
Taxa de transferência de disco
Taxa de transferência da controladora
a.
10 ms
7.500
90 MBytes/s
100 MBits/s
b.
7 ms
10.000
40 MBytes/s
200 MBits/s
6.3.1 [10] <6.2, 6.3> Calcule o tempo médio para ler ou escrever um setor de 1024 bytes de cada disco listado na tabela. 6.3.2 [10] <6.2, 6.3> Calcule o tempo mínimo para ler ou escrever um setor de 2048 bytes de cada disco listado na tabela. 6.3.3 [10] <6.2, 6.3> Para cada disco na tabela, determine o fator dominante ao desempenho. Especificamente, se você pudesse fazer uma melhoria em qualquer aspecto do disco, o que escolheria? Se não houver um fator dominante, explique por quê.
Exercício 6.4 No fim, o projeto do sistema de armazenamento requer consideração de cenários de uso e também de parâmetros de disco. Diferentes situações exigem diferentes métricas. Vamos tentar avaliar sistematicamente os sistemas de disco. Explore diferenças no modo como os sistemas de armazenamento devem ser avaliados respondendo as perguntas sobre as aplicações a seguir. a.
Sistema de controle de aeronaves
b.
Central telefônica
6.4.1 [5] <6.2, 6.3> Para cada aplicação, diminuir o tamanho do setor durante leituras e escritas melhoraria o desempenho? Explique sua resposta. 6.4.2 [5] <6.2, 6.3> Para cada aplicação, aumentar a velocidade de rotação de disco melhora o desempenho? Explique sua resposta. 6.4.3 [5] <6.2, 6.3> Para cada aplicação, aumentar a velocidade de rotação do disco melhora o desempenho do sistema dado que o MTTF diminui? Explique sua resposta.
Exercício 6.5 A memória FLASH é um dos primeiros competidores verdadeiros para as unidades de disco tradicionais. Explore as implicações da memória FLASH respondendo as perguntas sobre as aplicações a seguir. a.
Sistema de controle de aeronaves
b.
Central telefônica
6.5.1 [5] <6.2, 6.3, 6.4> Ao passarmos para unidades de estado sólido construídas de memória FLASH, o que mudará sobre os tempos de leitura de disco considerando que a taxa de transferência de dados permanece constante? 6.5.2 [10] <6.2, 6.3, 6.4> Cada aplicação se beneficiaria de uma unidade FLASH em estado sólido, dado que o custo é um fator de projeto? 6.5.3 [10] <6.2, 6.3, 6.4> Cada aplicação seria imprópria para uma unidade FLASH no estado sólido, dado que o custo NÃO é um fator de projeto?
6.15 Exercícios 501
Exercício 6.6 Explore a natureza da memória FLASH respondendo as perguntas relacionadas a desempenho para memórias FLASH com as características a seguir. Taxa de transferência de dados
Taxa de transferência da controladora
a.
120 MB/s
100 MB/s
b.
100 MB/s
90 MB/s
6.6.1 [10] <6.2, 6.3, 6.4> Calcule o tempo médio para leitura ou escrita de um setor de 1024 bytes para cada memória FLASH listada na tabela. 6.6.2 [10] <6.2, 6.3, 6.4> Calcule o tempo mínimo para leitura ou escrita de um setor de 512 bytes para cada memória FLASH listada na tabela. 6.6.3 [5] <6.2, 6.3, 6.4> A Figura 6.6 mostra que os tempos de acesso de leitura e escrita da memória FLASH aumentam à medida que a memória FLASH se torna maior. Isso é inesperado? Que fatores causam isso?
Exercício 6.7 A E/S pode ser realizada sincrônica ou assincronicamente. Explore as diferenças respondendo as perguntas de desempenho sobre os periféricos a seguir. a.
Impressora
b.
Scanner
6.7.1 [5] <6.5> Qual seria o tipo de barramento mais apropriado (síncrono ou assíncrono) para tratar das comunicações entre uma CPU e os periféricos listados na tabela? 6.7.2 [5] <6.5> Que problemas os barramentos longos e síncronos causariam para as conexões entre uma CPU e os periféricos listados na tabela? 6.7.3 [5] <6.5> Que problemas os barramentos assíncronos causariam para as conexões entre uma CPU e os periféricos listados na tabela?
Exercício 6.8 Entre os tipos de barramento mais comuns utilizados na prática atualmente estão FireWire (IEEE 1394), USB, PCI e SATA. Embora todos os quatro sejam assíncronos, eles são implementados de diferentes maneiras, dando-lhes diferentes características. Explore as diferentes estruturas de barramento respondendo as perguntas sobre os barramentos e os periféricos a seguir. a.
Mouse
b.
Coprocessador Gráfico
6.8.1 [5] <6.5> Selecione um barramento apropriado (FireWire, USB, PCI ou SATA) para os periféricos listados na tabela. Explique por que o barramento selecionado é apropriado. (Veja na Figura 6.8 as principais características de cada barramento.) 6.8.2 [20] <6.5> Use os recursos on-line ou de biblioteca e resuma a estrutura de comunicação para cada tipo de barramento. Identifique o que o controlador de barramento faz e onde o controle se encontra fisicamente.
502
Capítulo 6 Armazenamento e outros tópicos de E/S
6.8.3 [15] <6.5> Explique as limitações de cada um dos tipos de barramento. Explique por que essas limitações precisam ser levadas em consideração quando se usa o barramento.
Exercício 6.9 A comunicação com dispositivos de E/S é alcançada por meio de combinações de polling, tratamento de interrupção, mapeamento de memória e comandos especiais de E/S. Responda as perguntas sobre a comunicação com subsistemas de E/S para as aplicações a seguir usando combinações dessas técnicas. a.
Piloto automático
b.
Termostato automatizado
6.9.1 [5] <6.6> Descreva o polling do dispositivo. Cada aplicação na tabela seria apropriada para a comunicação usando as técnicas de polling? Explique. 6.9.2 [5] <6.6> Descreva a comunicação controlada por interrupção. Para cada aplicação na tabela, se o polling for impróprio, explique as técnicas controladas por interrupção que poderiam ser usadas. 6.9.3 [10] <6.6> Para as aplicações listadas na tabela, esboce um projeto de comunicação mapeada na memória. Identifique os locais de memória reservados e esboce seu conteúdo. 6.9.4 [10] <6.6> Para as aplicações listadas na tabela, esboce um projeto para os comandos implementando a comunicação controlada por comando. Identifique os comandos e sua interação com o dispositivo. 6.9.5 [5] <6.6> Faz sentido definir os subsistemas de E/S que usam uma combinação de mapeamento de memória e comunicação controlada por comando? Explique sua resposta.
Exercício 6.10 A Seção 6.6 define um processo de oito etapas para tratar das interrupções. Os registradores Cause e Status juntos oferecem informações sobre a causa da interrupção e o status do sistema de tratamento da interrupção. Explore o tratamento da interrupção respondendo as perguntas sobre as seguintes combinações de interrupções. a.
Controlador de dados Ethernet
Controlador do Mouse
Reiniciar
b.
Controlador do Mouse
Desligamento
Superaquecimento
6.10.1 [5] <6.6> Quando uma interrupção é detectada, o registrador Status é salvo e tudo além da interrupção de mais alta prioridade é desabilitado. Por que as interrupções de baixa prioridade são desabilitadas? Por que o registrador Status é salvo antes de desabilitar as interrupções? 6.10.2 [10] <6.6> Priorize as interrupções a partir dos dispositivos listados em cada linha da tabela. 6.10.3 [10] <6.6> Esboce como uma interrupção de cada um dos dispositivos listados na tabela seria tratada. 6.10.4 [5] <6.6> O que acontece se o bit “interrupt enable” do registrador Cause não for definido no tratamento de uma interrupção? Que valor é assumido pela máscara de interrupção para realizar a mesma coisa?
6.15 Exercícios 503
6.10.5 [5] <6.6> A maioria dos sistemas de tratamento de interrupção é implementada no sistema operacional. Que suporte do hardware poderia ser acrescentado de modo a tornar o tratamento de interrupção mais eficiente? Compare sua solução com o suporte de hardware em potencial para as chamadas de função. 6.10.6 [5] <6.6> Em algumas implementações de tratamento de interrupção, uma interrupção causa um salto imediato para um vetor de interrupção. Em vez de um registrador Cause, em que cada interrupção define um bit, cada interrupção tem seu próprio vetor de interrupção. O mesmo sistema de interrupção de prioridade pode ser implementado usando essa técnica? Existe alguma vantagem nessa técnica?
Exercício 6.11 Direct Memory Access (DMA) permite que os dispositivos acessem a memória diretamente em vez de utilizar a CPU. Isso pode agilizar bastante o desempenho dos periféricos, mas aumenta a complexidade das implementações do sistema de memória. Explore as implicações do DMA respondendo as perguntas sobre os periféricos a seguir. a.
Controlador do mouse
b.
Controlador da ethernet
6.11.1 [5] <6.6> A CPU abre mão do controle da memória quando o DMA está ativo? Por exemplo, um periférico pode simplesmente se comunicar com a memória diretamente, evitando a CPU por completo? 6.11.2 [10] <6.6> Dos periféricos listados na tabela, qual se beneficiaria com o DMA? Que critérios determinam se o DMA é apropriado? 6.11.3 [10] <6.6> Dos periféricos listados na tabela, qual poderia causar problemas de coerência com o conteúdo da cache? Que critérios determinam se as questões de coerência devem ser enfocadas? 6.11.4 [5] <6.6> Descreva os problemas que poderiam ocorrer quando se mistura DMA e memória virtual. Qual dos periféricos na tabela poderia gerar esses problemas? Como eles podem ser evitados?
Exercício 6.12 A métrica para desempenho de E/S pode variar bastante de uma aplicação para outra. Enquanto o número de transações processadas domina o desempenho em algumas situações, a vazão de dados domina em outras. Explore a avaliação do desempenho de E/S respondendo as perguntas para as aplicações a seguir. a.
Computações matemáticas
b.
Chat on-line
6.12.1 [10] <6.7> Para cada aplicação na tabela, o desempenho da E/S domina o desempenho do sistema? 6.12.2 [10] <6.7> Para cada aplicação na tabela, o desempenho da E/S é medido melhor usando a vazão de dados brutos? 6.12.3 [5] <6.7> Para cada aplicação na tabela, o desempenho da E/S é medido melhor usando o número de transações processadas?
504
Capítulo 6 Armazenamento e outros tópicos de E/S
6.12.4 [5] <6.7> Existe algum relacionamento entre as medidas de desempenho dos dois problemas anteriores e escolher entre o uso da comunicação por polling ou controlada por interrupção? E a escolha entre usar E/S mapeada pela memória ou controlada por comando?
Exercício 6.13 Os benchmarks desempenham um papel importante na avaliação e seleção de dispositivos periféricos. Para que os benchmarks sejam úteis, eles devem exibir propriedades semelhante àquelas experimentadas por um dispositivo em uso normal. Explore os benchmarks e a seleção de dispositivo respondendo as perguntas sobre as aplicações a seguir. a.
Computações matemáticas
b.
Chat on-line
6.13.1 [5] <6.7> Para cada aplicação na tabela, defina as características que um conjunto de benchmarks deve exibir quando se avaliar um subsistema de E/S? 6.13.2 [15] <6.7> Usando recursos on-line ou de biblioteca, identifique um conjunto de benchmarks padrão para aplicações na tabela. Por que os benchmarks padrão ajudam? 6.13.3 [5] <6.7> Faz sentido avaliar um subsistema de E/S fora do sistema maior do qual ele faz parte?
Exercício 6.14 RAID está entre as técnicas mais comuns de paralelismo e redundância nos sistemas de armazenamento. O nome Redundant Arrays of Inexpensive Disks implica em várias coisas sobre arrays RAID que exploraremos no contexto das atividades a seguir. a.
Computações matemáticas de alta perfomance
b.
Serviços de video on-line
6.14.1 [10] <6.9> RAID 0 utiliza o striping para forçar o acesso paralelo entre muitos discos. Por que o striping melhora o desempenho do disco? Para cada uma das atividades listadas na tabela, o striping ajudará a alcançar melhor seus objetivos? 6.14.2 [5] <6.9> RAID 1 espelha dados entre vários discos. Supondo que discos pouco dispendiosos possuem MTBF mais baixo que os discos dispendiosos, como a redundância usando discos pouco dispendiosos pode resultar em um sistema com MTBF inferior? Use a definição matemática do MTBF para explicar sua resposta. Para cada uma das atividades listadas na tabela, RAID 1 ajudará a conseguir melhor seus objetivos? 6.14.3 [5] <6.9> Assim como RAID 1, RAID 3 oferece disponibilidade de dados mais alta. Explique a escolha entre RAID 1 e RAID 3. Cada uma das aplicações listadas na tabela se beneficiaria de RAID 3 em vez de RAID 1?
Exercício 6.15 RAID 3, RAID 4 e RAID 5 utilizam o sistema de paridade para proteger blocos de dados. Especificamente, um bloco de paridade está associado a uma coleção de blocos de dados. Cada linha na tabela a seguir mostra os valores dos blocos de dados e paridade, conforme descritos na Figura 6.13. Novo D0
D0
D1
D2
D3
P
a.
7453
AB9C
AABB
0098
549C
2FFF
b.
F245
7453
DD25
AABB
FEFE
FEFF
6.15 Exercícios 505
6.15.1 [10] <6.9> Calcule a nova paridade P’ para RAID 3 para as linhas a e b da tabela. 6.15.2 [10] <6.9> Calcule a nova paridade P’ para RAID 4 para as linhas a e b da tabela. 6.15.3 [5] <6.9> RAID 3 ou RAID 4 é mais eficiente? Existem motivos para RAID 3 ser preferível a RAID 4? 6.15.4 [5] <6.9> RAID 4 e RAID 5 utilizam aproximadamente o mesmo mecanismo para calcular e armazenar a paridade para blocos de dados. Como RAID 5 difere de RAID 4 e para que aplicações RAID 5 seria mais eficiente? 6.15.5 [5] <6.9> As melhorias de velocidade do RAID 4 e RAID 5 crescem com relação a RAID 3 à medida que o tamanho do bloco protegido aumenta. Por que isso acontece? Existe alguma situação em que RAID 4 e RAID 5 não seria mais eficiente do que RAID 3?
Exercício 6.16 O aparecimento de servidores Web para e-commerce, armazenamento on-line e comunicação tornou os servidores de disco aplicações fundamentais. A disponibilidade e a velocidade são medidas bem conhecidas para servidores de disco, mas o consumo de energia está se tornando cada vez mais importante. Responda as perguntas sobre configuração e avaliação de servidores de disco com os parâmetros a seguir. Instruções de programa/ Operações de E/S
Instruções do SO/Operação de E/S
Carga de Trabalho (KB lidos)
Velocidade do processador (Instruções/Segundo)
a.
100.000
150.000
64
2 bilhões
b.
200.000
200.000
128
3 bilhões
6.16.1 [10] <6.8, 6.10> Ache a taxa de E/S sustentada máxima para leituras e escritas aleatórias. Ignore os conflitos de disco e suponha que a controladora RAID não seja o gargalo. Siga a mesma técnica esboçada na Seção 6.10, fazendo suposições semelhantes onde for necessário. 6.16.2 [10] <6.8, 6.10> Suponha que estejamos configurando um servidor Sun Fire x4150 conforme descrito na Seção 6.10. Determine se uma configuração de oito discos apresenta um gargalo de E/S. Repita para as configurações de 16, 4 e 2 discos. 6.16.3 [10] <6.8, 6.10> Determine se o barramento PCI, DIMM ou o Front Side Bus apresenta um gargalo de E/S. Use os mesmos parâmetros e suposições usados na Seção 6.10. 6.16.4 [5] <6.8, 6.10> Explique por que os sistemas reais utilizam benchmarks ou aplicações reais para avaliar o desempenho real.
Exercício 6.17 Determinar o desempenho de um único servidor com dados relativamente completos é uma tarefa fácil. Porém, ao comparar servidores de diferentes vendedores oferecendo dados diferentes, escolher entre as alternativas pode ser difícil. Explore o processo de encontrar e avaliar servidores respondendo as perguntas sobre a aplicação a seguir. Servidor de banco de dados
6.17.1 [15] <6.8, 6.10> Para a aplicação listada, identifique as características de runtime para o sistema operacional. Escolha características que darão suporte à avaliação semelhante à que foi realizada para o Exercício 6.16.
506
Capítulo 6 Armazenamento e outros tópicos de E/S
6.17.2 [15] <6.8, 6.10> Com relação à aplicação listada anteriormente, encontre um servidor disponível no mercado que você acredita que seria apropriado para executar a aplicação. Antes de avaliar o servidor, identifique motivos pelos quais ele foi selecionado. 6.17.3 [20] <6.8, 6.10> Usando métricas semelhantes às que foram usadas no Capítulo 6 e no Exercício 6.16, avalie o servidor que você identificou no Exercício 6.17.2 em comparação com o servidor Sun Fire x4150 avaliado no Exercício 6.16. Qual você escolheria? Os resultados da sua análise o surpreenderam? Especificamente, você escolheria de outra forma diferente? 6.17.4 [15] <6.8, 6.10> Identifique um conjunto de benchmark padrão que seria útil para comparar o servidor que você identificou no Exercício 6.17.2 com o Sun Fire x4150.
Exercício 6.18 As medições e as estatísticas fornecidas pelos vendedores de armazenamento devem ser cuidadosamente interpretadas para se obter previsões significativas sobre o comportamento do sistema. A tabela a seguir oferece dados para diversas unidades de disco. Número de unidades
Horas/Unidade
Horas/Falha
a.
1.000
10.512
1.200.000
b.
1.250
8.760
1.200.000
6.18.1 [10] <6.12> Calcule a taxa de falha anual (AFR) para os discos na tabela. 6.18.2 [10] <6.12> Suponha que a taxa de falha anual varie pelo tempo de vida dos discos na tabela anterior. Especificamente, suponha que a AFR seja três vezes esse valor no primeiro mês de operação e o dobro a cada ano começando no quinto ano. Quantos discos seriam substituídos após sete anos de operação? E depois de dez anos? 6.18.3 [10] <6.12> Suponha que os discos com taxas de falha inferiores sejam mais dispendiosos. Especificamente, os discos estão disponíveis a um custo mais alto, que começará a dobrar sua taxa de falha no ano 8, ao invés do ano 5. Quanto mais você pagaria pelos discos se a sua intenção for mantê-los por 7 anos? E por 10 anos?
Exercício 6.19 Para os discos na tabela do Exercício 6.18, considere que o seu vendedor ofereça uma configuração RAID 0 que aumentará a vazão do sistema de armazenamento em 70% e uma configuração RAID 1 que reduzirá a AFR dos pares de discos por 2. Suponha que o custo de cada solução é 1,6 vezes o custo da solução original. 6.19.1 [5] <6.9, 6.12> Dados apenas os parâmetros do problema original, você recomendaria fazer o upgrade para RAID 0 ou RAID 1, supondo que os parâmetros individuais do disco permaneçam iguais aos da tabela anterior? 6.19.2 [5] <6.9, 6.12> Dado que sua empresa opera um mecanismo de busca global com uma grande farm de disco, o upgrading para RAID 0 ou RAID 1 faz sentido econômico, visto que seu modelo de receita é baseado no número de anúncios atendidos? 6.19.3 [5] <6.9, 6.12> Repita o Exercício 6.19.2 para uma grande farm de discos operada por uma empresa de backup on-line. O upgrading para RAID 0 ou RAID 1 faz sentido econômico, visto que seu modelo de receita é baseado na disponibilidade do seu servidor?
6.15 Exercícios 507
Exercício 6.20 A avaliação e a manutenção diárias dos sistemas operando no computador envolvem muitos dos conceitos discutidos no Capítulo 6. Explore os detalhes da avaliação dos sistemas explorando as perguntas a seguir. 6.20.1 [20] <6.10, 6.12> Configure o Sun Fire x4150 para fornecer 10 terabytes de armazenamento para um array de processadores de 1000 processadores, rodando simulações de bioinformática. Sua configuração deverá minimizar o consumo de potência enquanto enfatiza questões de vazão e disponibilidade para o array de discos. Certifique-se de considerar as propriedades de grandes simulações ao realizar sua configuração. 6.20.2 [20] <6.10, 6.12> Recomende um sistema de backup e arquivamento de dados para o array de discos do Exercício 6.20.1. Compare as capacidades de disco, fita e backup on-line. Use a internet e recursos de biblioteca para identificar servidores em potencial. Avalie o custo e a adequação para a aplicação usando parâmetros descritos no Capítulo 6. Selecione parâmetros de comparação usando propriedades da aplicação e também os requisitos especificados. 6.20.3 [15] <6.10, 6.12> Vendedores concorrentes para os sistemas que você identificou no Exercício 6.20.2 se ofereceram para permitir que você avalie seus sistemas no local. Identifique os benchmarks que você usará para determinar qual sistema é melhor à sua aplicação. Determine quanto tempo será necessário para colher dados suficientes e tomar a sua decisão. §6.2: 2 e 3 são verdadeiros. §6.3: 3 e 4 são verdadeiros. §6.4: Todos são verdadeiros (considerando que 40MB/s seja compatível com100MB/s). §6.5: 1 é verdadeiro. §6.6: 1 e 2. §6.7: 1 e 2. 3 é falso, pois a maioria dos benchmarks TPC inclui custo. §6.9: Todos são verdadeiros.
Respostas das Seções “Verifique você mesmo”
7 Existem, no mar, peixes melhores do que os que já foram pescados. Provérbio irlandês
Multicores, multiprocessadores e clusters 7.1 Introdução 510 7.2
A dificuldade de criar programas com processamento paralelo 512
7.3
Multiprocessadores de memória compartilhada 515
7.4
Clusters e outros multiprocessadores de passagem de mensagens 517
7.5
Multithreading do hardware 521
7.6
SISD, MIMD, SIMD, SPMD e vetor 524
7.7
Introdução às unidades de processamento de gráficos 528
7.8
Introdução às topologias de rede multiprocessador 534
7.9
Benchmarks de multiprocessador 537
7.10
Roofline: um modelo de desempenho simples 539
7.11
Vida real: benchmarking de quatro multicores usando o modelo roofline 546
7.12
Falácias e armadilhas 552
7.13
Comentários finais 553
7.14
Perspectiva histórica e leitura adicional 555
7.15 Exercícios 555
Organização de multiprocessador ou cluster
510
“Sobre as montanhas da lua, pelo vale das sombras, cavalgue, cavalgue corajosamente.” Respondeu a sombra: “Se você procurar o Eldorado!” Edgar Allan Poe, “Eldorado”, stanza 4, 1849
multiprocessador Um sistema de computador com pelo menos dois processadores. Isso é o contrário do processador, que tem apenas um.
paralelismo em nível de tarefa ou paralelismo em nível de processo Utilizar vários processadores executando programas independentes simultaneamente.
programa de processamento paralelo Um único programa que é executado em vários processadores simultaneamente.
cluster Um conjunto de computadores conectados por uma rede local (LAN) que funciona como um único e grande multiprocessador. microprocessador multicore Um microprocessador contendo vários processadores (“cores”) em um único circuito integrado.
Capítulo 7 Multicores, multiprocessadores e clusters
7.1 Introdução Há muito tempo, os arquitetos de computadores têm buscado o Eldorado do projeto de computadores: criar computadores poderosos simplesmente conectando muitos computadores menores existentes. Essa visão dourada é a origem dos multiprocessadores. O cliente pede tantos processadores quantos seu orçamento permitir e recebe uma quantidade correspondente de desempenho. Portanto, o software para multiprocessadores precisa ser projetado para trabalhar com um número variável de processadores. Como dissemos no Capítulo 1, a potência tornou-se o fator limitante para centros de dados e microprocessadores. Substituir grandes processadores ineficazes por muitos processadores eficazes e menores pode oferecer melhor desempenho por watt ou por joule tanto no grande quanto no pequeno, se o software puder utilizá-los com eficiência. Assim, a melhor eficiência de potência se junta ao desempenho escalável no caso para os multiprocessadores. Como o software multiprocessador é escalável, alguns projetos podem suportar operar mesmo com a ocorrência de quebras no hardware; ou seja, se um único processador falhar em um multiprocessador com n processadores, o sistema fornece serviço continuado com n – 1 processadores. Portanto, os multiprocessadores também podem melhorar a disponibilidade (veja Capítulo 6). Alto desempenho pode significar alta vazão para tarefas independentes, chamado paralelismo em nível de tarefa, ou paralelismo em nível de processo. Essas tarefas paralelas são aplicações independentes, e são um uso importante e comum dos computadores paralelos. Essa técnica é contrária à execução de uma única tarefa em vários processadores. Usamos o termo programa de processamento paralelo para indicar um único programa que é executado em vários processadores simultaneamente. Há muito tempo existem problemas científicos que precisam de computadores muito mais rápidos, e essa classe de problemas tem sido usada para justificar muitos computadores paralelos novos no decorrer das últimas décadas. Veremos vários deles neste capítulo. Alguns desses problemas podem ser tratados de forma simples, usando um cluster composto de microprocessadores abrigados em muitos servidores ou PCs independentes. Além disso, os clusters podem servir a aplicações igualmente exigentes fora das ciências, como mecanismos de busca, servidores Web e bancos de dados. Como dissemos no Capítulo 1, os multiprocessadores ganharam destaque porque o problema da potência significa que aumentos futuros no desempenho aparentemente virão de mais processadores por chip em vez de taxas de clock mais altas e CPI melhorado. Eles são chamados microprocessadores multicore e não microprocessadores multiprocessador, provavelmente para evitar redundância de nomeação. Logo, os processadores normalmente são chamados cores em um chip multicore. O número de cores deverá dobrar a cada dois anos. Assim, os programadores que se preocupam com o desempenho precisam se tornar programadores paralelos, pois programas sequenciais significa programas lentos. O grande desafio enfrentado pela indústria é criar hardware e software que facilite a escrita de programas de processamento paralelo, que sejam eficientes no desempenho e potência à medida que o número de cores por chip aumenta geometricamente. Essa mudança repentina no projeto do microprocessador apanhou muitos de surpresa, de modo que ainda existe muita confusão sobre a terminologia e o que ela significa. A Figura 7.1 tenta esclarecer os termos serial, paralelo, sequencial e concorrente. As colunas dessa figura representam o software, que é inerentemente sequencial ou concorrente. As linhas da figura representam o hardware, que é serial ou paralelo. Por exemplo, os programadores de compiladores pensam neles como programas sequenciais: as etapas são análise léxica, parsing, geração de código, otimização e assim por diante. Ao contrário, os programadores de sistemas operacionais normalmente pensam neles como programas concorrentes: processos em cooperação tratando de eventos de E/S devido a tarefas independentes executando em um computador.
7.1 Introdução 511
FIGURA 7.1 Categorização e exemplos de hardware/software do ponto de vista da concorrência e do paralelismo.
O motivo desses dois eixos da Figura 7.1 é que o software concorrente pode ser executado no hardware serial, como os sistemas operacionais para o processador Intel Pentium 4, ou no hardware paralelo, como um SO no mais recente Intel Xeon e5345 (Clovertown). O mesmo acontece para o software sequencial. Por exemplo, o programador MATLAB escreve uma multiplicação de matriz pensando nela sequencialmente, mas poderia executá-la serialmente no hardware do Pentium 4 ou em paralelo no hardware do Xeon e5345. Você poderia supor que o único desafio da revolução paralela é descobrir como fazer com que o software naturalmente sequencial tenha alto desempenho no hardware paralelo, mas também fazer com que os programas concorrentes tenham alto desempenho nos multiprocessadores, à medida que o número de processadores aumenta. Com essa distinção, no restante deste capítulo usaremos programa de processamento paralelo ou software paralelo para indicar o software sequencial ou concorrente executando em hardware paralelo. A próxima seção descreve por que é difícil criar programas eficientes para processamento paralelo. As Seções 7.3 e 7.4 descrevem as duas alternativas de uma característica fundamental do hardware paralelo, que é se todos os processadores nos sistemas contam ou não com um único endereço físico. As duas versões comuns dessas alternativas são chamadas microprocessadores de memória compartilhada e clusters. A Seção 7.5 descreve então o multithreading, um termo que geralmente é confundido com multiprocessamento, em parte porque se baseia em uma concorrência semelhante nos programas. A Seção 7.6 descreve um esquema de classificação mais antigo que na Figura 7.1. Além disso, ela descreve dois estilos de arquiteturas de conjunto de instruções que dão suporte à execução de aplicações sequenciais em hardware paralelo, a saber, SIMD e vetor. A Seção 7.7 descreve um estilo de computador relativamente novo, da comunidade de hardware gráfico, chamado unidade de processamento de gráficos (GPU — Graphics Processing Unit). O Apêndice A descreve as GPUs com mais detalhes. Em seguida, discutimos a dificuldade de se achar benchmarks paralelos, na Seção 7.9. Essa seção é seguida por uma descrição de um modelo de desempenho novo e simples, porém introspectivo, que ajuda no projeto de aplicações e também de arquiteturas. Na Seção 7.11, usamos esse modelo para avaliar quatro computadores multicore recentes em dois kernels de aplicação. Terminamos com falácias e armadilhas e nossas conclusões sobre o paralelismo. Antes de prosseguirmos ainda mais até o paralelismo, não se esqueça das nossas incursões iniciais dos capítulos anteriores: j
Capítulo 2, Seção 2.11: Paralelismo e sincronização de instruções.
j
Capítulo 3, Seção 3.6: Paralelismo e aritmética computacional: associatividade.
j
Capítulo 4, Seção 4.10: Paralelismo e paralelismo avançado em nível de instrução.
j
Capítulo 5, Seção 5.8: Paralelismo e hierarquias de memória: coerência de cache.
j
Capítulo 6, Seção 6.9: Paralelismo e E/S: Redundant Arrays of Inexpensive Disks.
Verdadeiro ou falso: para que se beneficie de um multiprocessador, uma aplicação precisa ser concorrente.
Verifique você mesmo
512
Capítulo 7 Multicores, multiprocessadores e clusters
A dificuldade de criar programas
7.2 com processamento paralelo
A dificuldade com o paralelismo não está no hardware; é que muito poucos programas de aplicação importantes foram escritos para completar as tarefas mais cedo nos multiprocessadores. É difícil escrever software que usa processadores múltiplos para completar uma tarefa mais rápido, e o problema fica pior à medida que o número de processadores aumenta. Mas por que isso acontece? Por que os programas de processamento paralelo devem ser tão mais difíceis de desenvolver do que os programas sequenciais? A primeira razão é que você precisa obter um bom desempenho e eficiência do programa paralelo em um multiprocessador; caso contrário, você usaria um programa sequencial em um processador, já que a programação é mais fácil. Na verdade, as técnicas de projeto de processadores, como execução superescalar e fora de ordem, tiram vantagem do paralelismo em nível de instrução (ver Capítulo 4), normalmente sem envolvimento do programador. Tais inovações reduzem a necessidade de reescrever programas para multiprocessadores, já que os programadores poderiam não fazer nada e ainda assim seus programas sequenciais seriam executados mais rapidamente nos novos computadores. Por que é difícil escrever programas de multiprocessador que sejam rápidos, especialmente quando o número de processadores aumenta? No Capítulo 1, usamos a analogia de oito repórteres tentando escrever um único artigo na esperança de realizar o trabalho oito vezes mais rápido. Para ter sucesso, a tarefa precisa ser dividida em oito partes de mesmo tamanho, pois senão alguns repórteres estariam ociosos enquanto esperam que aqueles com partes maiores terminem. Outro perigo do desempenho seria que os repórteres gastariam muito tempo se comunicando entre si em vez de escrever suas partes do artigo. Para essa analogia e para a programação paralela, os desafios incluem escalonamento, balanceamento de carga, tempo para sincronização e overhead para a comunicação entre as partes. O desafio é ainda maior quando aumenta o número de repórteres para um artigo do jornal e quanto aumenta o número de processadores para a programação paralela. Nossa discussão no Capítulo 1 revela outro obstáculo, conhecido como a Lei de Amdahl. Ela nos lembra que mesmo as pequenas partes de um programa precisam estar em paralelo para que o programa faça bom proveito dos muitos cores.
DESAFIO do speed-up
EXEMPLO RESPOSTA
Suponha que você queira alcançar um speed-up 90 vezes mais rápido com 100 processadores. Que fração da computação original pode ser sequencial? A Lei de Amdahl (Capítulo 1) diz que: Tempodeexecuçãoapós melhoria = Tempodeexecuçãoafetado pela melhoria + Tempodeexecução nãoafetado Quantidadede melhoria Podemos reformular a lei de Amdahl em termos de speed-up versus o tempo de execução original: Speed-up =
Tempo de execução antes (Tempode execução antes-Tempo de execução afetado) +
Tempo de execução afetado 100
7.2 A dificuldade de criar programas com processamento paralelo 513
Essa fórmula normalmente é reescrita considerando-se que o tempo de execução antes é 1 para alguma unidade de tempo, e o tempo de execução afetado pela melhoria é considerado a fração do tempo de execução original: Speed up =
1 (1 − Fraçãode tempoafetada) +
Fraçãode tempoafetada 100
Substituindo pela meta de um speed-up de 90 na fórmula anterior: 90 =
1 (1 − Fraçãode tempoafetada) +
Fraçãode tempoafetada 100
Então, simplificando a fórmula e resolvendo para a fração de tempo afetada: 90 × (1 − 0,99 × Fração de tempo afetada) = 1 90 − (90 × 0,99 × Fração de tempo afetada) = 1 90 − 1 = 90 × 0,99 × Fração de tempo afetada Fração de tempo afetada = 89/89,1 = 0,999 Portanto, para obter um speed-up de 90 com 100 processadores, a porcentagem sequencial só poderá ser 0,1%. Entretanto, existem aplicações com um substancial paralelismo.
DESAFIO do speed-up, ainda maior
Suponha que você queira realizar duas somas: uma é a soma de duas variáveis escalares e outra é uma soma matricial de um par de arrays bidimensionais, com dimensões 10 × 10. Que speed-up você obtém com 10 versus 100 processadores? Em seguida, calcule os speed-ups supondo que as matrizes crescem para 100 por 100. Se considerarmos que o desempenho é uma função do tempo para uma adição, t, então há 10 adições que não se beneficiam dos processadores paralelos e 100 adições que se beneficiam. Se o tempo para um único processador é 110t, o tempo de execução para 10 processadores é Tempodeexecuçãoapós melhoria = Tempodeexecuçãoafetado pela melhoria + Tempodeexecução nãoafetado Quantidadede melhoria 110t + 10t = 20t Tempodeexecuçãoapós melhoria = 10 Então, o speed-up com 10 processadores é 110t/20t = 5,5. O tempo de execução para 100 processadores é Tempodeexecuçãoapós melhoria =
100t + 10t = 11t 100
de modo que o speed-up com 100 processadores é 100t/11t = 10.
EXEMPLO
RESPOSTA
514
Capítulo 7 Multicores, multiprocessadores e clusters
Assim, para o tamanho deste problema, obtemos cerca de 55% do speed-up em potencial com 10 processadores, mas somente 10% com 100. Veja o que acontece quando aumentamos a matriz. O programa sequencial agora utiliza 10t + 10.000t = 10.010t. O tempo de execução para 10 processadores é Tempodeexecuçãoapós melhoria =
10.000t + 10t = 1010t 10
de modo que o speed-up com 10 processadores é 10.010t/1010t = 9,9. O tempo de execução para 100 processadores é Tempodeexecuçãoapós melhoria =
10.000t + 10t = 110t 100
de modo que o speed-up com 100 processadores é 10.010t/110t = 91. Assim, para esse tamanho de problema maior, obtemos cerca de 99% do speed-up em potencial com 10 processadores e mais de 90% com 100.
expansão forte Speed-up alcançado em um multiprocessador sem aumentar o tamanho do problema. expansão fraca Speed-up alcançado em um multiprocessador enquanto se aumenta o tamanho do problema proporcionalmente ao aumento no número de processadores.
Esses exemplos mostram que obter um bom speed-up em um multiprocessador enquanto se mantém o tamanho do problema fixo é mais difícil do que conseguir um bom speed-up aumentando o tamanho do problema. Isso nos permite apresentar dois termos que descrevem maneiras de expandir. Expansão forte significa medir o speed-up enquanto se mantém o tamanho do problema fixo. Expansão fraca significa que o tamanho do problema cresce proporcionalmente com o aumento no número de processadores. Vamos supor que o tamanho do problema, M, seja o conjunto de trabalho na memória principal, e que temos P processadores. Então, a memória por processador para a expansão forte é aproximadamente M/P, e para a expansão fraca ela é aproximadamente M. Dependendo da aplicação, você pode argumentar em favor de qualquer uma dessas técnicas de expansão. Por exemplo, o benchmark de banco de dados débito-crédito TPC-C (Capítulo 6) requer que você aumente o número de contas de cliente para conseguir um maior número de transações por minuto. O argumento é que não faz sentido pensar que determinada base de clientes de repente começará a usar caixas eletrônicos 100 vezes por dia só porque o banco adquiriu um computador mais rápido. Em vez disso, se você for demonstrar um sistema que pode funcionar 100 vezes o número de transações por minuto, deverá fazer uma experiência com 100 vezes a quantidade de clientes. Este exemplo final mostra a importância do balanceamento de carga.
DESAFIO do speed-up: balanceamento DE CARGA
EXEMPLO
RESPOSTA
Para conseguir o speed-up de 91 no problema maior, mostrado anteriormente, com 100 processadores, consideramos que a carga foi balanceada perfeitamente. Ou seja, cada um dos 100 processadores teve 1% do trabalho a realizar. Em vez disso, mostre o impacto sobre o speed-up se a carga de um processador for maior que todo o restante. Calcule em 2% e 5%. Se um processador tem 2% da carga paralela, então ele precisa realizar 2% × 10.000 ou 200 adições, e os outros 99 compartilharão as 9800 restantes. Como eles estão operando simultaneamente, podemos simplesmente calcular o tempo de execução como um máximo
7.3 Multiprocessadores de memória compartilhada 515
9.800t 200t Tempodeexecuçãoapós melhoria = Max , + 10t = 210t 99 1 O speed-up cai para 10.010t/210t = 48. Se um processador tem 5% da carga, ele precisa realizar 500 adições: 9.500t 500t Tempodeexecuçãoapós melhoria = Max , + 10t = 510t 99 1 O speed-up cai ainda mais para 10.010t/510t = 20. Esse exemplo demonstra o valor do balanceamento de carga, pois apenas um único processador com o dobro da carga dos outros reduz o speed-up quase ao meio, e cinco vezes a carga em um processador reduz o speed-up por quase um fator de cinco. Verdadeiro ou falso: a expansão forte não está ligada à lei de Amdahl.
Verifique você mesmo
Multiprocessadores de memória
7.3 compartilhada
Dada a dificuldade de reescrever programas antigos para que funcionem bem em hardware paralelo, uma pergunta natural é o que os projetistas de computador podem fazer para simplificar a tarefa. Uma resposta para isso foi oferecer um único espaço de endereços físico que todos os processadores possam compartilhar, de modo que os programas não precisem se preocupar com o local onde são executados, apenas que podem ser executados em paralelo. Nessa técnica, todas as variáveis de um programa podem ficar disponíveis a qualquer momento para qualquer processador. A alternativa é ter um espaço de endereços separado por processador, o que requer que o compartilhamento seja explícito; vamos descrever essa opção na próxima seção. Quando o espaço de endereços físico é comum — que normalmente acontece para chips multicore —, então o hardware normalmente oferece coerência de cache para dar uma visão consistente da memória compartilhada (veja Seção 5.8 do Capítulo 5.) Um multiprocessador de memória compartilhada (SMP – shared memory multiprocessor) é aquele que oferece ao programador um único espaço de endereços físico para todos os processadores, embora um termo mais preciso teria sido multiprocessador de endereço compartilhado. Observe que esses sistemas ainda podem executar tarefas independentes em seus próprios espaços de endereços virtuais, mesmo que todos compartilhem um espaço de endereços físico. Os processadores se comunicam por meio de variáveis compartilhadas na memória, com todos os processadores capazes de acessar qualquer local da memória por meio de loads e stores. A Figura 7.2 mostra a organização clássica de um SMP. Microprocessadores com único espaço de endereços podem ser de dois tipos. O primeiro leva aproximadamente o mesmo tempo para acessar a memória principal, não importa qual processador o solicite e não importa qual palavra é solicitada. Essas máquinas são chamadas multiprocessadores de acesso uniforme à memória (UMA). No segundo estilo, alguns acessos à memória são muito mais rápidos do que outros, dependendo de qual processador pede qual palavra. Essas máquinas são chamadas multiprocessadores de acesso não uniforme à memória (NUMA). Como você poderia esperar, os desafios de programação são mais difíceis para um multiprocessador NUMA do que para um multiprocessador UMA, mas as máquinas NUMA podem expandir para tamanhos maiores, e as NUMAs podem ter latência inferior para a memória próxima.
multiprocessador de memória compartilhada (SMP) Um processador paralelo com um único espaço de endereços, implicando na comunicação implícita com loads e stores.
acesso uniforme à memória (UMA) Um multiprocessador em que os acessos à memória principal levam aproximadamente a mesma quantidade de tempo não importa qual processador acessa e não importa qual palavra é solicitada.
acesso não uniforme à memória (NUMA) Um tipo de multiprocessador com espaço de endereços único em que alguns acessos à memória são muito mais rápidos do que outros, dependendo de qual processador solicita qual palavra.
516
Capítulo 7 Multicores, multiprocessadores e clusters
FIGURA 7.2 Organização clássica de um multiprocessador de memória compartilhada.
sincronização O processo de coordenar o comportamento de dois ou mais processos, que podem estar sendo executados em diferentes processadores. lock Um dispositivo de sincronização que permite o acesso aos dados somente por um processador de cada vez.
Como os processadores operando em paralelo normalmente compartilharão dados, eles também precisam coordenar quando operarão sobre dados compartilhados; caso contrário, um processador poderia começar a trabalhar nos dados antes que outro tenha terminado. Essa coordenação é chamada de sincronização. Quando o compartilhamento tem o suporte de um único espaço de endereços, é preciso haver um mecanismo separado para sincronização. Uma técnica utiliza um lock para uma variável compartilhada. Somente um processador de cada vez pode adquirir o lock, e outros processadores interessados nos dados compartilhados precisam esperar até que o processador original libere a variável. A Seção 2.11 do Capítulo 2 descreve as instruções para o locking no MIPS.
Um programa de processamento paralelo simples para um espaço de endereços compartilhado
EXEMPLO
RESPOSTA
redução Uma função que processa uma estrutura de dados e retorna um único valor.
Suponha que queremos somar 100.000 números em um computador com multiprocessador com tempo de acesso à memória uniforme. Vamos considerar que temos 100 processadores. A primeira etapa novamente seria dividir o conjunto de números em subconjuntos do mesmo tamanho. Não alocamos os subconjuntos a um espaço de memória diferente, já que existe uma única memória para essa máquina; apenas atribuímos endereços iniciais diferentes a cada processador. Pn é o número que identifica o processador, entre 0 e 99. Todos os processadores começam o programa executando um loop que soma seu subconjunto de números:
A próxima etapa é fazer essas muitas somas parciais. Essa etapa se chama redução. Dividimos para conquistar. A metade dos processadores soma pares de somas parciais, depois, um quarto soma pares das novas somas parciais e assim por diante, até que tenhamos uma única soma final. A Figura 7.3 ilustra a natureza hierárquica dessa redução. Neste exemplo, os dois processadores precisam ser sincronizados antes que o processador “consumidor” tente ler o resultado do local da memória escrito pelo
7.4 Clusters e outros multiprocessadores de passagem de mensagens 517
processador “produtor”; caso contrário, o consumidor pode ler o valor antigo dos dados. Queremos que cada processador tenha sua própria versão da variável contadora de loop i, de modo que precisamos indicar que ela é uma variável “privada”. Aqui está o código (half também é privada):
Verdadeiro ou falso: multiprocessadores de memória compartilhada não podem tirar proveito do paralelismo em nível de tarefa.
Verifique você mesmo
FIGURA 7.3 Os quatro últimos níveis de uma redução que soma os resultados de cada processador, de baixo para cima. Para todos os processadores cujo número i é menor que half, adicione a soma produzida pelo processador número (i+half) à sua soma.
Detalhamento: Uma alternativa ao compartilhamento do espaço de endereço físico seria ter espaços de endereços físicos separados, mas compartilhar um espaço de endereços virtuais comum, deixando para o sistema operacional a tarefa de cuidar da comunicação. Essa técnica tem sido experimentada, mas possui um alto overhead para oferecer uma abstração de memória compartilhada prática ao programador.
lusters e outros multiprocessadores C 7.4 de passagem de mensagens A técnica alternativa ao compartilhamento de um espaço de endereços é que cada processador tenha seu próprio espaço privado de endereços físicos. A Figura 7.4 mostra a organização clássica de um multiprocessador com múltiplos espaços de endereços privados. Esse multiprocessador alternativo precisa se comunicar por meio da passagem de mensagens explícita, que tradicionalmente é o nome desse estilo de computadores. Desde que o sistema tenha rotinas para enviar e receber mensagens, a coordenação é embutida na passagem da mensagem, pois um processador sabe quando uma mensagem é enviada, e o processador receptor sabe quando uma mensagem chega. Se o emissor precisar de confirmação de que a mensagem chegou, o
passagem de mensagens Comunicação entre vários processadores enviando e recebendo informações explicitamente. rotina para enviar mensagem Uma rotina usada por um processador em máquinas com memórias privadas para passar uma mensagem a outro processador.
rotina para receber mensagem Uma rotina usada por um processador em máquinas com memórias privadas para aceitar uma mensagem de outro processador.
518
Capítulo 7 Multicores, multiprocessadores e clusters
FIGURA 7.4 Organização clássica de um multiprocessador com múltiplos espaços de endereços privados, tradicionalmente chamado de multiprocessador de passagem de mensagens. Observe que, diferente do SMP da Figura 7.2, a rede de interconexão não está entre as caches e a memória, mas entre os nós processador-memória.
clusters Coleções de computadores conectados por E/S por switches de rede padrão para formar um multiprocessador de passagem de mensagens.
processador receptor poderá então enviar uma mensagem de confirmação para o emissor. Algumas aplicações concorrentes funcionam bem em hardware paralelo, não importa se ele oferece endereços compartilhados ou passagem de mensagens. Em particular, paralelismo e aplicações em nível de tarefa com pouca comunicação — como busca na web, servidores de correio e servidores de arquivo — não exigem que o endereçamento compartilhado funcione bem. Houve várias tentativas de construir computadores de alto desempenho com base em redes de passagem de mensagens de alto desempenho, e eles ofereceram melhor desempenho de comunicação absoluta do que os clusters criados por meio de redes locais. O problema foi que eles eram muito mais caros. Poucas aplicações poderiam justificar o desempenho de comunicação mais alto, dados os custos muito mais altos. Logo, os clusters se tornaram o exemplo mais divulgado atualmente do computador de passagem de mensagens. Os clusters geralmente são coleções de computadores básicos que são conectados entre si por sua interconexão de E/S, através de switches e cabos de rede padrão. Cada um executa uma cópia distinta do sistema operacional. Praticamente cada serviço da internet conta com clusters de servidores e switches básicos. Uma desvantagem dos clusters foi que o custo de administrar um cluster de n máquinas é cerca do mesmo que o custo de administrar n máquinas independentes, enquanto o custo de administrar um multiprocessador de memória compartilhada com n processadores é aproximadamente o mesmo que administrar uma única máquina. Esse ponto fraco é um dos motivos para a popularidade das máquinas virtuais (Capítulo 5), pois as VMs tornam os clusters mais fáceis de administrar. Por exemplo, as VMs possibilitam parar ou iniciar programas atomicamente, o que simplifica as atualizações de software. As VMs podem ainda migrar um programa de um computador em um cluster para outro sem interromper o programa, permitindo que um programa migre a partir de um hardware defeituoso. Outra desvantagem dos clusters é que os processadores em um cluster normalmente são conectados por meio da interconexão de E/S de cada computador, enquanto os cores em um multiprocessador normalmente são conectados na interconexão de memória do computador. A interconexão de memória tem largura de banda mais alta e latência mais baixa, permitindo um desempenho de comunicação muito melhor. Um último ponto fraco é o overhead na divisão de memória: um cluster de n máquinas tem n memórias independentes e n cópias do sistema operacional, mas um multiprocessador de memória compartilhada permite que um único programa utilize quase toda a memória no computador, e só precisa de uma única cópia do SO.
7.4 Clusters e outros multiprocessadores de passagem de mensagens 519
Eficiência da memória
Suponha que um único processador de memória compartilhada tenha 20GB de memória principal, cinco computadores em cluster com 4GB cada, e o SO ocupe 1GB. Quanto espaço a mais existe para os usuários com memória compartilhada?
EXEMPLO
A razão da memória disponível para os programas do usuário no computador de memória compartilhada versus o cluster seria
RESPOSTA
20 − 1 19 = ≈ 1‚ 25 5 × (4 − 1) 15 de modo que os computadores com memória compartilhada têm cerca de 25% mais espaço. Vamos refazer o exemplo de soma da seção anterior para ver o impacto das memórias privadas múltiplas e a comunicação explícita.
Um programa simples de processamento paralelo para passagem de mensagens
Suponha que queremos somar 100.000 números em um multiprocessador por passagem de mensagens com 100 processadores, cada um com múltiplas memórias privadas. Como esse computador possui múltiplos espaços de endereçamento, o primeiro passo é distribuir os 100 subconjuntos para cada uma das memórias locais. O processador contendo os 100.000 números envia os subconjuntos para cada um dos 100 nós de memória de processador. A próxima etapa é obter a soma de cada subconjunto. Essa etapa é simplesmente um loop que toda unidade de execução segue: ler uma palavra da memória local e adicioná-la a uma variável local:
A última etapa é a redução que soma essas 100 somas parciais. A parte difícil é que cada soma parcial está localizada em uma unidade de execução diferente. Consequentemente, precisamos usar a rede de interconexão para enviar somas parciais e acumular a soma final. Em vez de enviar todas as somas parciais a um único processador, o que resultaria em acrescentar sequencialmente as somas, mais uma vez dividimos para conquistar. Primeiro, metade dos processadores envia suas somas parciais para a outra metade dos processadores, em que duas somas parciais são feitas. Depois, um quarto das unidades de execução (metade da metade) envia essa nova soma parcial para o outro quarto dos processadores (a metade da metade restante) para a próxima rodada de somas. Essas divisões, envios e recepções continuam até haver uma única soma
EXEMPLO
RESPOSTA
520
Capítulo 7 Multicores, multiprocessadores e clusters
de todos os números. Seja Pn o número da unidade de execução, send(x,y) uma rotina que envia pela rede de interconexão para a unidade de execução número x o valor y e receive() a função que recebe um valor da rede para esse processador :
Esse código divide todos os processadores em emissores ou receptores, e cada processador receptor recebe apenas uma mensagem, de modo que podemos presumir que um processador receptor estará suspenso até receber uma mensagem. Portanto, send e receive podem ser usados como primitivas para sincronização e para comunicação, já que os processadores estão cientes da transmissão dos dados. Se houver um número ímpar de nós, o nó central não participa da emissão/ recepção. O limite, então, é definido de modo que esse nó seja o nó mais alto na próxima iteração.
Detalhamento: Este exemplo considera implicitamente que a passagem de mensagens é tão rápida quanto a adição. Na realidade, a emissão e recepção de mensagens é muito mais lenta. Uma otimização para balancear melhor o cálculo e a comunicação poderia ser o uso de menos nós recebendo muitas somas de outros processadores.
Interface hardware/ software
Computadores que contam com a passagem de mensagens para a comunicação, em vez da memória compartilhada coerente com a cache, são muito mais fáceis para os projetistas de hardware (veja Seção 5.8 do Capítulo 5). A vantagem para os programadores é que a comunicação é explícita, o que significa que existem menos surpresas de desempenho do que com a comunicação implícita nos computadores de memória compartilhada coerentes com a cache. A desvantagem para os programadores é que é mais difícil transportar um programa sequencial para um computador com passagem de mensagens, pois cada comunicação precisa ser identificada antecipadamente, ou o programa não funcionará. A memória compartilhada coerente com a cache permite que o hardware descubra quais dados precisam ser comunicados, o que facilita o transporte. Existem diferenças de opinião quanto ao caminho mais curto para o alto desempenho, dados os prós e contras da comunicação implícita.
A limitação de memórias separadas para memória do usuário torna-se uma vantagem na disponibilidade do sistema. Como um cluster consiste em computadores independentes conectados por meio de uma rede local, é muito mais fácil substituir uma máquina sem paralisar o sistema em um cluster do que em um SMP. Fundamentalmente, o endereçamento compartilhado significa que é difícil isolar um processador e substituí-lo sem um árduo trabalho por parte do sistema operacional. Já que o software de cluster é uma camada que roda sobre o sistema operacional executado em cada computador, é muito mais fácil desconectar e substituir uma máquina defeituosa.
7.5 Multithreading do hardware 521
Como os clusters são construídos por meio de computadores inteiros e redes independentes e escaláveis, esse isolamento também facilita expandir o sistema sem paralisar a aplicação que executa sobre o cluster. Menor custo, alta disponibilidade, maior eficiência de energia e a rápida e gradual expansibilidade tornam os clusters atraentes para provedores de serviços para a world wide web. Os mecanismos de busca que milhões de nós utilizamos todos os dias dependem dessa tecnologia. eBay, Google, Microsoft, Yahoo e outros possuem múltiplos centros de dados, cada um com clusters de dezenas de milhares de processadores. Logicamente, o uso de múltiplos processadores nas empresas de serviço de internet tem sido muito bem-sucedido. Detalhamento: Outra forma de computação em grande escala é a computação em grade, em que os computadores são espalhados por grandes áreas, e depois os programas que executam neles precisam se comunicar por redes de longa distância. A forma mais comum e exclusiva de computação em grade foi promovida pelo projeto SETI@home. Observou-se que milhões de PCs ficam ociosos em determinado momento, sem realizar nada de útil, e eles poderiam ser apanhados e ter boa utilidade se alguém desenvolvesse software que pudesse rodar nesses computadores e depois dar a cada PC uma parte independente do problema para atuar. O primeiro exemplo foi o Search for ExtraTerrestrial Intelligence (SETI). Mais de 5 milhões de usuários de computador em mais de 200 países se inscreveram para o SETI@ home e contribuíram coletivamente com mais de 19 bilhões de horas de tempo de processamento de computador. Ao final de 2006, a grade SETI@home operava em 257 TeraFLOPS.
1. Verdadeiro ou falso: assim como os SMPs, os computadores com passagem de mensagens contam com locks para a sincronização.
Verifique você mesmo
2. Verdadeiro ou falso: diferentemente dos SMPs, os computadores com passagem de mensagens precisam de múltiplas cópias do programa de processamento paralelo e do sistema operacional.
7.5 Multithreading do hardware O multithreading do hardware permite que várias threads compartilhem as unidades funcionais de um único processador de um modo sobreposto. Para permitir esse compartilhamento, o processador precisa duplicar o estado independente de cada thread. Por exemplo, cada thread teria uma cópia separada do banco de registradores e do PC. A memória em si pode ser compartilhada por meio de mecanismos de memória virtual, que já suportam multiprogramação. Além disso, o hardware precisa suportar a capacidade de mudar para uma thread diferente com relativa rapidez. Em especial, uma troca de thread deve ser muito mais eficiente do que uma troca de processo, que normalmente exige centenas a milhares de ciclos de processador, enquanto uma troca de thread pode ser instantânea. Existem dois métodos principais de multithreading do hardware. O multithreading fine-grained comuta entre threads a cada instrução, resultando em execução intercalada de várias threads. Essa intercalação normalmente é feita de forma circular, saltando quaisquer threads que estejam suspensas nesse momento. Para tornar o multithreading fine-grained prático, o processador precisa ser capaz de trocar threads a cada ciclo de clock. Uma importante vantagem do multithreading fine-grained é que ele pode ocultar as perdas de vazão que surgem dos stalls curtos e longos, já que as instruções de outras threads podem ser executadas quando uma thread é suspensa. A principal desvantagem do multithreading fine-grained é que ele torna mais lenta a execução das threads individuais, já que uma thread que está pronta para ser executada sem stalls será atrasada por instruções de outras threads.
multithreading do hardware Aumentar a utilização de um processador trocando para outra thread quando uma thread é suspensa.
multithreading fine-grained Uma versão do multithreading do hardware que sugere a comutação entre as threads após cada instrução.
522
Capítulo 7 Multicores, multiprocessadores e clusters
multithreading coarse-grained Uma versão do
O multithreading coarse-grained foi criado como uma alternativa para o multithreading fine-grained. Esse método de multithreading comuta threads apenas em stalls onerosos, como as falhas de cache de nível 2. Essa mudança reduz a necessidade de tornar a comutação de thread essencialmente gratuita e tem muito menos chance de tornar mais lenta a execução de uma thread individual, visto que só serão despachadas instruções de outras threads quando uma thread encontrar um stall oneroso. Entretanto, o multithreading coarse-grained sofre de uma grande desvantagem: é limitado em sua capacidade de sanar perdas de vazão, especialmente de stalls mais curtos. Essa limitação surge dos custos de inicialização de pipeline do multithreading coarse-grained. Como um processador com multithreading coarse-grained despacha instruções por meio de uma única thread, quando ocorre um stall, o pipeline precisa ser esvaziado ou congelado. A nova thread que começa a ser executada após o stall precisa preencher o pipeline antes que as instruções consigam ser concluídas. Devido a esse overhead de inicialização, o multithreading coarse-grained é muito mais útil para reduzir a penalidade dos stalls de alto custo, em que a reposição de pipeline é insignificante comparada com o tempo de stall. O simultaneous multithreading (SMT) é uma variação do multithreading do hardware que usa os recursos de um processador de despacho múltiplo escalonado dinamicamente para explorar paralelismo em nível de thread ao mesmo tempo em que explora o paralelismo em nível de instrução. O princípio mais importante que motiva o SMT é que os processadores de despacho múltiplo modernos normalmente possuem mais paralelismo de unidade funcional do que uma única thread efetivamente pode usar. Além disso, com a renomeação de registradores e o escalonamento dinâmico, diversas instruções de threads independentes podem ser despachadas sem considerar as dependências entre elas; a resolução das dependências pode ser tratada pela capacidade de escalonamento dinâmico. Como estamos contando com os mecanismos dinâmicos existentes, SMT não troca de recurso a cada ciclo, mas sempre está executando instruções de múltiplas threads, deixando para o hardware a associação de slots de instrução e registradores renomeados com suas threads apropriadas. A Figura 7.5 ilustra conceitualmente as diferenças na capacidade de um processador de explorar recursos superescalares para as configurações de processador a seguir. A parte superior mostra como quatro threads seriam executadas de forma independente em um superescalar sem suporte a multithreading. A parte inferior mostra como as quatro threads poderiam ser combinadas para serem executadas no processador de maneira mais eficiente usando três opções de multithreading:
multithreading do hardware que sugere a comutação entre as threads somente após eventos significativos, como uma falha de cache.
simultaneous multithreading (SMT) Uma versão do multithreading que reduz o custo do multithreading, utilizando os recursos necessários para a microarquitetura de despacho múltiplo, escalonada dinamicamente.
j
Um superescalar com multithreading coarse-grained.
j
Um superescalar com multithreading fine-grained.
j
Um superescalar com simultaneous multithreading.
No superescalar sem suporte a multithreading do hardware, o uso dos slots de despacho é limitado por uma falta de paralelismo em nível de instrução. Além disso, um importante stall, como uma falha de cache de instruções, pode deixar o processador inteiro ocioso. No superescalar com multithreading coarse-grained, os longos stalls são parcialmente ocultados pela comutação para outra thread que usa os recursos do processador. Embora isso reduza o número de ciclos de clock completamente ociosos, o overhead de inicialização do pipeline ainda produz ciclos ociosos, e as limitações do paralelismo em nível de instrução significam que nem todos os slots de despacho serão utilizados. Em um processador com multithreading coarse-grained, a intercalação de threads elimina quase todos os slots totalmente vazios. Porém, como apenas uma thread despacha instruções em um determinado ciclo de clock, as limitações do paralelismo em nível de instrução ainda geram um número significativo de slots ociosos dentro de alguns ciclos de clock. No caso SMT, o paralelismo em nível de thread e o paralelismo em nível de instrução são explorados simultaneamente, com múltiplas threads usando os slots de despacho
7.5 Multithreading do hardware 523
FIGURA 7.5 Como quatro threads usam os slots de despacho de um processador superescalar em diferentes métodos. As quatro threads no alto mostram como cada uma seria executada em um processador superescalar padrão sem suporte a multithreading. Os três exemplos embaixo mostram como elas seriam executadas juntas em três opções de multithreading. A dimensão horizontal representa a capacidade de despacho de instrução em cada ciclo de clock. A dimensão vertical representa uma sequência dos ciclos de clock. Uma caixa vazia (branco) indica que o slot de despacho correspondente está vago nesse ciclo de clock. Os tons de cinza e preto correspondem a quatro threads diferentes nos processadores multithreading. Os efeitos de inicialização de pipeline adicionais para multithreading coarse, que não estão ilustrados nessa figura, levariam a mais perda na vazão para multithreading coarse.
em um único ciclo de clock. O ideal é que o uso de slots de despacho seja limitado por desequilíbrios nas necessidades e na disponibilidade de recursos entre múltiplas threads. Na prática, outros fatores podem restringir o número de slots usados. Embora a Figura 7.5 simplifique bastante a operação real desses processadores, ela ilustra as potenciais vantagens de desempenho em potencial do multithreading em geral e do SMT em particular. Por exemplo, o recente multicore Intel Nehalem suporta SMT com duas threads para melhorar a utilização do core. Vamos concluir com três observações. Primeiro, pelo Capítulo 1, sabemos que a barreira da potência está forçando um projeto em direção a processadores mais simples e mais eficientes em termos de potência em um chip. Pode ser que os recursos subutilizados dos processadores fora de ordem possam ser reduzidos e, portanto, formas mais simples de multithreading sejam utilizadas. Por exemplo, o microprocessador Sun UltraSPARC T2 (Niagara 2) na Seção 7.11 é um exemplo de um retorno a microarquiteturas mais simples e, portanto, ao uso do multithreading fine-grained. Segundo, um desafio de desempenho importante é tolerar a latência decorrente das falhas de cache. Computadores fine-grained, como o UltraSPARC T2, trocam para outra thread em caso de falha, o que provavelmente é mais eficaz para esconder a latência da memória do que tentar preencher slots de despacho não usados, como em um SMT. Uma terceira observação é que o objetivo do multithreading do hardware deve usar o hardware com mais eficiência compartilhando os componentes entre diferentes tarefas. Os projetos multicore também compartilham recursos. Por exemplo, dois processadores poderiam compartilhar uma unidade de ponto flutuante ou uma cache L3.
524
Capítulo 7 Multicores, multiprocessadores e clusters
Esse compartilhamento reduz alguns dos benefícios do multithreading em comparação com o oferecimento de mais cores não multithreaded.
Verifique você mesmo
1. Verdadeiro ou falso: tanto o multithreading quanto o multicore contam com o paralelismo para obter mais eficiência de um chip. 2. Verdadeiro ou falso: o multithreading simultâneo utiliza threads para melhorar a utilização de recursos de um processador fora de ordem, escalonado dinamicamente.
7.6 SISD, MIMD, SIMD, SPMD e vetor SISD ou Single Instruction stream, Single Data stream. Um processador único.
MIMD ou Multiple Instruction
Outra categorização do hardware paralelo proposta na década de 1960 ainda está em uso atualmente. Ela foi baseada no número de fluxos de instruções e no número de fluxos de dados. A Figura 7.6 mostra as categorias. Assim, um processador convencional tem um único fluxo de instruções e um único fluxo de dados, e um multiprocessador convencional possui fluxos de instruções e fluxos de dados múltiplos. Essas duas categorias são abreviadas como SISD e MIMD, respectivamente.
streams, Multiple Data streams. Um multiprocessador.
FIGURA 7.6 Categorização de hardware e exemplos baseados no número de fluxos de instruções e fluxos de dados: SISD, SIMD, MISD e MIMD.
SPMD Single Program, Multiple Data streams. O modelo de programação MIMD convencional, em que um único programa é executado em todos os processadores.
SIMD ou Single Instruction stream, Multiple Data streams. Um multiprocessador. A mesma instrução é aplicada a muitos fluxos de dados, assim como em um processador de vetor ou de array.
Embora seja possível escrever programas separados que são executados em diferentes processadores em um computador MIMD e ainda trabalharem juntos para um objetivo grandioso e coordenado, os programadores normalmente escrevem um único programa que executa em todos os processadores de um computador MIMD, contando com instruções condicionais quando diferentes processadores deveriam executar diferentes seções de código. Esse estilo é chamado Single Program Multiple Data (SPMD), mas é apenas o modo normal de programar um computador MIMD. Embora seja difícil oferecer exemplos de computadores úteis que sejam classificados como múltiplos fluxos de instruções e fluxo de instrução único (MISD), o inverso faz muito mais sentido. Computadores SIMD operam sobre vetores de dados. Por exemplo, uma única instrução SIMD poderia acrescentar 64 números enviando 64 fluxos de dados a 64 ALUs, para formar 64 somas dentro de um único ciclo de clock. As virtudes do SIMD são que todas as unidades de execução paralelas são sincronizadas e todas elas respondem a uma única instrução que emana de um único contador de programa (PC). Do ponto de vista de um programador, isso é próximo do já conhecido SISD. Embora cada unidade esteja executando a mesma instrução, cada unidade de execução tem seus próprios registradores de endereço, e portanto cada unidade pode ter diferentes endereços de dados. Assim, em termos da Figura 7.1, uma aplicação sequencial poderia ser compilada para executar em hardware serial organizado como um SISD ou em hardware paralelo que foi organizado como um SIMD.
7.6 SISD, MIMD, SIMD, SPMD e vetor 525
A motivação original por trás do SIMD foi amortizar o custo da unidade de controle por dezenas de unidades de execução. Outra vantagem é o tamanho reduzido da memória do programa — SIMD só precisa de uma cópia do código que está sendo executado simultaneamente, enquanto os MIMDs com passagem de mensagem podem precisar de uma cópia em cada processador, e o MIMD com memória compartilhada precisará de múltiplas caches de instrução. SIMD funciona melhor quando lida com arrays em loops for. Logo, para o paralelismo funcionar no SIMD, é preciso haver muitos dados estruturados de forma idêntica, o que é chamado de paralelismo em nível de dados. SIMD é mais fraco em instruções case ou switch, em que cada unidade de execução precisa realizar uma operação diferente sobre seus dados, dependendo de quais dados ela tenha. As unidades de execução com os dados errados são desativadas, de modo que as unidades com dados corretos possam continuar. Essas situações basicamente executam em desempenho 1/n, onde n é o número de casos. Os chamados processadores de array que inspiraram a categoria SIMD desapareceram na história (veja Seção 7.14 no site), mas duas interpretações do SIMD permanecem ativas hoje.
SIMD no x86: extensões de multimídia A variação mais utilizada do SIMD encontra-se em quase todo microprocessador de hoje, e é a base das centenas de instruções MMX e SSE do microprocessador x86 (veja Capítulo 2). Elas foram acrescentadas para melhorar o desempenho dos programas de multimídia. Essas instruções permitem que o hardware tenha muitas ALUs operando simultaneamente ou, de modo equivalente, particione uma ALU única e larga em muitas ALUs menores paralelas, que operam simultaneamente. Por exemplo, você poderia considerar que um único componente de hardware seja uma ALU de 64 bits ou duas ALUs de 32 bits ou quatro ALUs de 16 bits ou oito ALUs de 8 bits. Loads e stores simplesmente possuem a largura da ALU mais larga, de modo que o programador pode pensar na mesma instrução de transferência de dados como transferindo um único elemento de dados de 64 bits, ou dois elementos de dados de 32 bits, ou quatro elementos de dados de 16 bits, ou oito elementos de dados de 8 bits. Esse paralelismo a um custo muito baixo para dados inteiros estreitos foi a inspiração original das instruções MMX do x86. À medida que a lei de Moore continuava, mais hardware era acrescentado a essas extensões de multimídia, e agora o SSE2 admite a execução simultânea de um par de números de ponto flutuante de 64 bits. A largura da operação e dos registradores é codificada no opcode dessas instruções de multimídia. Enquanto a largura de dados de registradores e operações crescia, o número de opcodes para instruções de multimídia explodia, e agora existem centenas de instruções SSE para realizar as combinações úteis (veja Capítulo 2).
Vetor Uma interpretação mais antiga e mais elegante do SIMD é a chamada arquitetura de vetor, que tem sido identificada de perto com os Cray Computers. Essa é novamente uma grande combinação com os problemas com muito paralelismo em nível de dados. Em vez de ter 64 ALUs realizando 64 adições simultaneamente, como os antigos processadores de array, as arquiteturas de vetor colocaram a ALU em pipeline para obter bom desempenho com custo reduzido. A filosofia básica da arquitetura de vetor é coletar elementos de dados da memória, colocá-los em ordem em um grande conjunto de registradores, operar sobre eles sequencialmente nos registradores e depois escrever os resultados de volta para a memória. Um recurso importante das arquiteturas de vetor é um conjunto de registradores de vetor. Assim, uma arquitetura de vetor poderia ter 32 registradores de vetor, cada um com 64 elementos de 64 bits.
paralelismo em nível de dados Paralelismo obtido operando-se sobre dados independentes.
526
Capítulo 7 Multicores, multiprocessadores e clusters
Comparando código de vetor com código convencional
EXEMPLO
Suponha que estendamos a arquitetura do conjunto de instruções MIPS com instruções de vetor e registradores de vetor. As operações de vetor utilizam os mesmos nomes das operações MIPS, mas com a letra “V” acrescentada. Por exemplo, addv.d soma dois vetores de precisão dupla. As instruções de vetor apanham como entrada um par de registradores de vetor (addv.d) ou um registrador de vetor e um registrador escalar (addvs.d). No segundo caso, o valor no registrador escalar é usado como a entrada para todas as operações — a operação addvs.d somará o conteúdo de um registrador escalar a cada elemento em um registrador de vetor. Os nomes lv e sv indicam load de vetor e store de vetor, e carregam ou armazenam um vetor inteiro de dados de precisão dupla. Um operando é o registrador de vetor a ser carregado ou armazenado; o outro operando, que é um registrador MIPS de uso geral, é o endereço inicial do vetor na memória. Dada essa descrição curta, mostre o código MIPS convencional versus o código MIPS de vetor para Y = a × X +Y onde X e Y são vetores de 64 números de ponto flutuante com precisão dupla, inicialmente residentes na memória, e a é uma variável escalar de precisão dupla. (Esse exemplo é o chamado loop DAXPY, que forma o loop interno do benchmark Linpack; DAXPY significa Double precision a × X Plus Y.) Suponha que os endereços iniciais de X e Y estejam em $s0 e $s1, respectivamente.
RESPOSTA
Aqui está o código MIPS convencional para o DAXPY:
Aqui está o código MIPS de vetor para o DAXPY:
Existem algumas comparações interessantes entre os dois segmentos de código neste exemplo. A mais impressionante é que o processador de vetor reduz bastante a largura de banda de instrução dinâmica, executando apenas seis instruções contra quase 600 para o MIPS. Essa redução ocorre tanto porque as operações de vetor trabalham sobre 64 elementos quanto porque as instruções de overhead que constituem quase metade do
7.6 SISD, MIMD, SIMD, SPMD e vetor 527
loop no MIPS não estão presentes no código de vetor. Como você poderia esperar, essa redução nas instruções buscadas e executadas economiza energia. Outra diferença importante é a frequência dos hazards de pipeline (Capítulo 4). No código MIPS direto, cada add.d precisa esperar por um mul.d, e cada s.d precisa esperar pelo add.d. No processador de vetor, cada instrução de vetor só gerará stall para o primeiro elemento em cada vetor, e depois os elementos subsequentes fluirão tranquilamente pelo pipeline. Assim, os stalls do pipeline só são necessários uma vez por operação de vetor, em vez de uma vez por elemento de vetor. Neste exemplo, a frequência de stall do pipeline no MIPS será de aproximadamente 64 vezes maior do que no VMIPS. Os stalls do pipeline podem ser reduzidos no MIPS usando desdobramento de loop (veja Capítulo 4). Porém, a grande diferença na largura de banda de instrução não pode ser reduzida. Detalhamento: O loop no exemplo anterior combinou exatamente com o tamanho do vetor. Quando os loops são mais curtos, as arquiteturas de vetor utilizam um registrador que reduz o tamanho das operações de vetor. Quando os loops são maiores, acrescentamos código de contabilidade para percorrer operações de vetor de tamanho total e tratar do restante. Esse último processo é conhecido como strip mining (ou mineração a céu aberto).
Vetor versus escalar As instruções de vetor possuem várias propriedades importantes em comparação com os arquivos convencionais de conjunto de instruções, que são chamadas arquiteturas escalares nesse contexto: j
Uma única instrução de vetor especifica muito trabalho — isso é equivalente a executar um loop inteiro. A largura de banda de busca e decodificação de instrução necessária é bastante reduzida.
j
Usando uma instrução de vetor, o compilador ou programador indica que o cálculo de cada resultado no vetor é independente do cálculo de outros resultados no mesmo vetor, de modo que o hardware não tem de verificar hazards de dados dentro de uma instrução de vetor.
j
Arquiteturas e compiladores de vetor têm uma reputação de tornar muito mais fácil que os multiprocessadores MIMD escrever aplicações eficientes quando elas contêm paralelismo em nível de dados.
j
O hardware só precisa verificar hazards de dados entre duas instruções de vetor uma vez por operando de vetor, e não uma vez para cada elemento dentro dos vetores. A redução de verificações pode economizar potência.
j
Instruções de vetor que acessam a memória possuem um padrão de acesso conhecido. Se os elementos do vetor forem todos adjacentes, então buscar o vetor de um conjunto de bancos de memória bastante intervalados funciona muito bem. Assim, o custo da latência para a memória principal é visto apenas uma vez para o vetor inteiro, e não uma vez para cada palavra do vetor.
j
Como um loop inteiro é substituído por uma instrução de vetor cujo comportamento é predeterminado, os hazards de controle que normalmente surgiriam do desvio do loop são inexistentes.
j
A economia na largura de banda da instrução e verificação de hazard mais o uso eficaz da largura de banda da memória dão às arquiteturas de vetor vantagens em potência e energia contra as arquiteturas escalares.
Por esses motivos, as operações de vetor podem se tornar mais rápidas que uma sequência de operações escalares sobre o mesmo número de itens de dados, e os projetistas são motivados a incluir unidades de vetor se o domínio da aplicação puder usá-las com frequência.
528
Capítulo 7 Multicores, multiprocessadores e clusters
Vetor versus extensões de multimídia Assim como as extensões de multimídia encontradas nas instruções SSE do x86, uma instrução de vetor especifica múltiplas operações. Porém, as extensões de multimídia normalmente especificam algumas poucas operações, enquanto o vetor especifica dezenas de operações. Diferente das extensões de multimídia, o número de elementos em uma operação de vetor não está no opcode, mas em um registrador separado. Isso significa que diferentes versões da arquitetura de vetor podem ser implementadas com um número diferente de elementos apenas mudando o conteúdo desse registrador e retendo, portanto, a compatibilidade binária. Ao contrário, um novo grande conjunto de opcodes é acrescentado toda vez que o tamanho do “vetor” muda na arquitetura da extensão de multimídia do x86. Também diferente das extensões de multimídia, as transferências de dados não precisam ser contíguas. Os vetores admitem os acessos “strided”, em que o hardware carrega cada n-ésimo elemento de dados na memória, e acessos indexados, em que o hardware encontra os endereços dos itens a serem carregados em um registrador de vetor. Assim como as extensões de multimídia, o vetor facilmente captura a flexibilidade nas larguras de dados, de modo que é fácil fazer uma operação funcionar em 32 elementos de dados de 64 bits ou em 64 elementos de dados de 32 bits ou em 128 elementos de dados de 16 bits ou 256 elementos de dados de 8 bits. Geralmente, as arquiteturas de vetor são um meio muito eficaz de executar programas de processamento paralelo de dados; elas combinam melhor com a tecnologia de compilador do que extensões de multimídia; são mais fáceis de evoluir com o tempo do que as extensões de multimídia na arquitetura x86.
Verifique Verdadeiro ou falso: conforme exemplificamos no x86, as extensões de multimídia podem você mesmo ser consideradas como uma arquitetura de vetor com vetores curtos que suportam apenas transferências sequenciais de dados de vetor.
Detalhamento: Dadas as vantagens do vetor, por que eles não são mais comuns fora da computação de alto desempenho? Havia preocupações sobre o estado maior para registradores de vetor aumentando o tempo de troca de contexto e a dificuldade de tratar das falhas de página nos loads e stores de vetor, e as instruções SIMD conseguiram alguns dos benefícios das instruções de vetor. Porém, os anúncios recentes da Intel sugerem que os vetores desempenharão um papel mais importante. A Advanced Vector Instructions (AVI) da Intel, disponível em 2010, expandirá a largura dos registradores SSE de 128 bits para 256 bits imediatamente, e permitirá a eventual expansão para 1024 bits. Essa última largura é equivalente a 16 números de ponto flutuante de precisão dupla. Se haverá instruções load e store de vetor, isso ainda não está claro. Além disso, a entrada da Intel no mercado discreto de GPU para 2010 — apelidado de “Larrabee” — supostamente terá instruções de vetor.
Detalhamento: Outra vantagem das extensões de vetor e multimídia é que é relativamente fácil estender uma arquitetura de conjunto de instruções escalar com essas instruções para melhorar o desempenho das operações paralelas com dados.
Introdução às unidades de processamento
7.7 de gráficos
Uma justificativa importante para o acréscimo de instruções SIMD às arquiteturas existentes foi que muitos microprocessadores eram conectados a telas gráficas em PCs e estações de trabalho, de modo que uma fração cada vez maior do tempo de processamento era usada para os gráficos. Daí, quando a lei de Moore aumentou o número de transistores disponíveis nos microprocessadores, fez sentido melhorar o processamento gráfico.
7.7 Introdução às unidades de processamento de gráficos 529
Assim como a lei de Moore permitiu que a CPU melhorasse o processamento gráfico, também permitiu que os chips do controlador gráfico de vídeo acrescentassem funções para acelerar gráficos 2D e 3D. Além do mais, na ponta estavam as placas gráficas caras, geralmente da Silicon Graphics, que poderiam ser acrescentadas às estações de trabalho, para permitir a criação de imagens com qualidade fotográfica. Essas placas gráficas de alto nível foram comuns na criação de imagens geradas por computador, que mais tarde entraram nos anúncios de televisão e depois nos filmes. Assim, os controladores gráficos de vídeo tinham um alvo direcionado quando os recursos de processamento cresceram, assim como os supercomputadores ofereceram um rico recurso de ideias para os microprocessadores na busca por maior desempenho. Uma força motriz importante para melhorar o processamento gráfico foi a indústria de jogos de computador, tanto em PCs quanto em consoles de jogos dedicados, como o PlayStation da Sony. O mercado de jogos em rápido crescimento encorajou muitas empresas a fazerem investimentos cada vez maiores no desenvolvimento de hardware gráfico mais rápido, e esse feedback positivo levou o processamento gráfico a melhorar em um ritmo mais rápido do que o processamento de uso geral nos microprocessadores centrais. Dado que a comunidade de gráficos e jogos teve objetivos diferentes da comunidade de desenvolvimento de microprocessador, ela evoluiu seu próprio estilo de processamento e terminologia. Quando os processadores gráficos aumentaram sua potência, eles ganharam o nome Graphics Processing Units, ou GPUs, para distingui-los das CPUs. Aqui estão algumas das principais características de como as GPUs se distinguem das CPUs: j
GPUs são aceleradores que complementam uma CPU, de modo que não precisam ser capazes de realizar todas as tarefas de uma CPU. Esse papel lhes permitiu dedicar todos os seus recursos aos gráficos. Não importa se as GPUs realizam algumas tarefas mal ou que não realizem, visto que, em um sistema com uma CPU e uma GPU, a CPU pode realizá-las se for preciso. Assim, a combinação CPU-GPU é um exemplo de multiprocessamento heterogêneo, em que nem todos os processadores são idênticos. (Outro exemplo é a arquitetura IBM Cell da Seção 7.11, que também foi projetada para acelerar gráficos 2D e 3D.)
j
As instruções de programação das GPUs são interfaces de programação de aplicação (APIs) de alto nível, como OpenGL e Microsoft's DirectX, junto com linguagens de sombreamento gráfico de alto nível, como C for Graphics (Cg) da NVIDIA e High Level Shader Language (HLSL) da Microsoft. Os compiladores de linguagem são voltados para linguagens intermediárias padrão da indústria, em vez de instruções de máquina. O software de driver GPU gera instruções de máquina otimizadas, específicas para GPU. Embora essas APIs e linguagens evoluam rapidamente para abranger novos recursos de GPU habilitados pela lei de Moore, a liberdade da compatibilidade com a instrução binária permite que os projetistas de GPU explorem novas tecnologias sem temer que sejam seladas para sempre com a implementação de experimentos falhos. Esse ambiente leva à inovação mais rápida em GPUs do que em CPUs.
j
O processamento gráfico envolve o desenho de vértices de primitivas de geometria 3D, como linhas e triângulos, e sombreamento ou renderização de fragmentos de pixels de primitivas geométricas. Os video games por exemplo, desenham 20 a 30 vezes mais pixels que vértices.
j
Cada vértice pode ser desenhado independentemente, assim como na renderização de cada fragmento de pixel. Para renderizar milhões de pixels por frame rapidamente, a GPU evoluiu para executar muitos threads de programas sombreadores de vértice e pixel em paralelo.
j
Os tipos de dados gráficos são vértices, consistindo em coordenadas (x, y, z, w), e pixels, consistindo em componentes de cor (vermelho, verde, azul, alfa). (Veja o Apêndice A para descobrir mais sobre vértices e pixels.) GPUs representam cada
530
Capítulo 7 Multicores, multiprocessadores e clusters
componente do vértice como um número de ponto flutuante de 32 bits. Cada um dos quatro componentes de pixel foi originalmente um inteiro não sinalizado de 8 bits, mas as GPUs recentes agora representam cada componente como um número de ponto flutuante de precisão simples, entre 0,0 e 1,0. j
O conjunto de trabalho pode ter centenas de megabytes, e ele não mostra a mesma localidade temporal que os dados nas principais aplicações. Além do mais, existe muito paralelismo em nível de dados nessas tarefas.
Essas diferenças têm levado a diferentes estilos de arquitetura: j
Talvez a maior diferença seja que as GPUs não contam com caches multinível para contornar a longa latência para a memória, como nas CPUs. Em vez disso, as GPUs contam em ter threads suficientes para ocultar a latência para a memória. Ou seja, entre o momento de uma solicitação de memória e o momento em que os dados chegam, a GPU executa centenas ou milhares de threads que são independentes dessa solicitação.
j
As GPUs contam com um extenso paralelismo para obter alto desempenho, implementando muitos processadores paralelos e muitos threads concorrentes.
j
A memória principal da GPU é assim orientada para largura de banda, em vez de latência. Existem até mesmo chips de DRAM separados para GPUs que são mais largas e possuem largura de banda mais alta que os chips de DRAM para as CPUs. Além disso, as memórias da GPU tradicionalmente têm tido memória principal menor que os microprocessadores convencionais. Em 2008, as GPUs normalmente tinham 1GB ou menos, enquanto as CPUs tinham de 2 a 32GB. Finalmente, lembre-se de que, para a computação de uso geral, você precisa incluir o tempo para transferir os dados entre a memória da CPU e a memória da GPU, pois a GPU é um coprocessador.
j
Dada a confiança em muitos threads para oferecer boa largura de banda de memória, as GPUs podem acomodar muitos processadores paralelos, além de muitos threads. Logo, cada processador de GPU é altamente multithreaded.
j
No passado, as GPUs contavam com processadores heterogêneos de uso especial para oferecer o desempenho necessário a aplicações gráficas. GPUs recentes estão voltadas para processadores idênticos de uso geral, de modo a oferecer mais flexibilidade na programação, tornando-os mais semelhantes aos projetos multicore encontrados na computação principal.
j
Dada a natureza de quatro elementos dos tipos de dados gráficos, as GPUs historicamente possuem instruções SIMD, como as CPUs. Contudo, as GPUs recentes estão focalizando mais as instruções escalares para melhorar a facilidade de programação e a eficiência.
j
Diferente das CPUs, não tem havido suporte para a aritmética de ponto flutuante com precisão dupla, pois ela não é necessária nas aplicações gráficas. Em 2008, foram anunciadas as primeiras GPUs a ter suporte para precisão dupla no hardware. Apesar disso, as operações de precisão simples ainda serão de oito a dez vezes mais rápidas que a precisão dupla, mesmo nessas novas GPUs, enquanto a diferença no desempenho para as CPUs seja limitada a benefícios na transferência de menos bytes no sistema de memória, devido ao uso de dados estreitos.
Embora as GPUs fossem projetadas para um conjunto mais estreito de aplicações, alguns programadores questionaram se poderiam especificar suas aplicações em uma forma que lhes permitissem aproveitar o alto desempenho em potencial das GPUs. Para distinguir esse estilo de uso das GPUs, alguns a chamam de General Purpose GPUs, ou GPGPUs. Depois de cansar de tentar especificar seus problemas usando as APIs gráficas e linguagens de sombreamento de gráficos, eles desenvolveram linguagens de
7.7 Introdução às unidades de processamento de gráficos 531
programação inspiradas em C para permitir que escrevam programas diretamente às GPUs. Um exemplo é Brook, uma linguagem de streaming para GPUs. O próximo passo na facilidade de programação do hardware e da linguagem de programação é a CUDA (Compute Unified Device Architecture) da NVIDIA, que permite que o programador escreva programas em C para execução nas GPUs, embora com algumas restrições. O uso de GPUs para a computação paralela está aumentando com sua crescente facilidade de programação.
Introdução à arquitetura de GPU NVIDIA O Apêndice A contém muito mais detalhes sobre GPUs e apresenta minuciosamente a arquitetura de GPU da NVIDIA, chamada Tesla. Como as GPUs evoluíram em seu próprio ambiente, elas não apenas têm arquiteturas diferentes, conforme sugerimos anteriormente, mas também têm um conjunto de termos diferente. Quando você aprender os termos da GPU, verá as semelhanças nas técnicas apresentadas nas seções anteriores, como multithreading fine-grained e vetores. Para ajudá-lo com essa transição ao novo vocabulário, apresentamos uma rápida introdução aos termos e às ideias na arquitetura de GPU Tesla e no ambiente de programação CUDA. Um chip GPU discreto se encontra em uma placa separada conectada a um PC padrão através da interconexão PCI-Express. As chamadas placa-mãe de GPU são integradas ao chip set da placa mãe, como uma north bridge ou uma south bridge (Capítulo 6). As GPUs geralmente são oferecidas como uma família de chips em diferentes pontos de desempenho de preço, com todas sendo compatíveis em software. Os chips de GPUs baseadas em Tesla são oferecidas com algo entre 1 e 16 nós, que a NVIDIA chama de multiprocessadores. No início de 2008, a maior versão é chamada de GeForce 8800 GTX, que tem 16 multiprocessadores e uma taxa de clock de 1,35GHz. Cada multiprocessador contém oito unidades de ponto flutuante de precisão simples multithreaded e unidades de processamento de inteiros, que a NVIDIA chama de processadores streaming. Como a arquitetura inclui uma instrução de multiplicação-adição de ponto flutuante com precisão simples, o desempenho máximo da multiplicação-adição de precisão simples do chip 8800 GTX é: 16 MPs ×
8SPs 2FLOPs/instr. 1instr. 1‚35 × 109 clocks × × × MP SP clock seg. 16 × 8 × 2 × 1,35GFLOPs = seg. 345‚6 GFLOPs = seg.
Cada um dos 16 multiprocessadores do GeForce 8800 GTX tem um store local gerenciado por software com uma capacidade de 16KB mais 8192 registradores de 32 bits. O sistema de memória do 8800 GTX consiste em seis partições de 900MHz Graphics DDR3 DRAM, cada uma com 8 bytes de largura e com 128 MB de capacidade. O tamanho de memória local é, portanto, 768MB. A largura de banda de pico da memória GDDR3 é 6×
8Bytes 2 transf. 0‚9 × 109 clocks 6 × 8 × 2 × 0‚9GB 86‚ 4GB × × = = transf. clock seg. seg. seg.
Para ocultar a latência da memória, cada processador de streaming tem threads com suporte do hardware. Cada grupo de 32 threads é chamado de warp. Um warp é a unidade de escalonamento, e as threads ativas em um warp — até 32 — executam em
532
Capítulo 7 Multicores, multiprocessadores e clusters
FIGURA 7.7 Comparando o único core de um Sun UltraSPARC T2 (Niagara 2) com um único multiprocessador Tesla. O core T2 é um único processador e usa multithreading com suporte do hardware, com oito threads. O multiprocessador Tesla contém oito processadores streaming e usa multithreading com suporte do hardware com 24 warps de 32 threads (oito processadores vezes quatro ciclos de clock). O T2 pode comutar a cada ciclo de clock, enquanto o Tesla pode comutar apenas a cada dois ou quatro ciclos de clock. Um modo de comparar os dois é que o T2 só pode realizar multithread do processador com o tempo, enquanto o Tesla pode realizar o multithread com o tempo e espaço; ou seja, pelos oito processadores de streaming e também segmentos de quatro ciclos de clock.
paralelo no padrão SIMD. Contudo, a arquitetura multithreaded lida com condições, permitindo que os threads tomem caminhos de desvio diferentes. Quando as threads de um warp tomam caminhos divergentes, o warp executa sequencialmente os caminhos de código com algumas threads inativas, o que faz com que as threads ativas sejam executadas de forma mais lenta. O hardware junta as threads de volta em um warp totalmente ativo assim que os caminhos condicionais são concluídos. Para obter o melhor desempenho, todos as 32 threads de um warp precisam ser executadas juntas em paralelo. Em um estilo semelhante, o hardware também examina os fluxos de endereço vindo de diferentes threads para tentar mesclar as solicitações individuais em menos transferências de bloco de memória, porém maiores, no sentido de aumentar o desempenho da memória. A Figura 7.7 combina todos esses recursos e compara um multiprocessador Tesla com um core Sun UltraSPARC T2, descrito nas Seções 7.5 e 7.11. Ambos são multithreaded por hardware escalonando-se threads com o tempo, como mostra o eixo vertical. Cada multiprocessador Tesla consiste em oito processadores streaming, cada um executando oito threads paralelas por clock, mostradas horizontalmente. Como dissemos, o melhor desempenho vem quando todas as 32 threads de um warp são executadas juntas em um padrão tipo SIMD, que a arquitetura Tesla chama de Single-Instruction Multiple-Thread (SIMT). SIMT descobre dinamicamente quais threads de um warp podem executar a mesma instrução juntas, e quais threads independentes estão ociosas nesse ciclo. O core T2 contém apenas um único processador multithreaded. Cada ciclo executa uma instrução para uma thread. O multiprocessador Tesla usa multithreading de hardware fine-grained para escalonar 24 warps com o tempo, que aparecem verticalmente em blocos de quatro ciclos de clock. De modo semelhante, o UltraSPARC T2 escalona oito threads com suporte do hardware com o tempo, uma thread por ciclo, mostrado verticalmente. Dessa forma, assim como o hardware T2 comuta entre threads para manter o core T2 ocupado, o hardware Tesla alterna entre warps para manter o multiprocessador Tesla ocupado. A principal diferença é que o core T2 tem um processador que pode alternar as threads a cada ciclo de clock,
7.7 Introdução às unidades de processamento de gráficos 533
enquanto a unidade de comutação mínima dos warps no microprocessador Tesla é de dois ciclos de clock por oito cores streaming. Como Tesla é voltado para programas com muito paralelismo em nível de dados, os projetistas acreditaram que existe pouca diferença de desempenho entre a comutação a cada dois ou quatro ciclos de clock, contra cada ciclo de clock, e o hardware se tornou muito mais simples restringindo a frequência de comutação. O ambiente de programação CUDA também possui sua própria terminologia. Um programa CUDA é um programa C/C + + unificado para um sistema de CPU e GPU heterogêneo. Ele é executado na CPU e despacha o trabalho paralelo para a GPU. Esse trabalho consiste em uma transferência de dados da memória principal e um despacho de thread. Uma thread é um pedaço do programa para a GPU. Os programadores especificam o número de threads em um bloco de threads, e o número de blocos de threads que eles querem começar a executar na GPU. O motivo para os programadores se importarem com os blocos de threads é que todos os threads no bloco de threads são escalonados para serem executados no mesmo multiprocessador, de modo que todos compartilham a mesma memória local. Assim, eles podem se comunicar por meio de loads e stores, em vez de mensagens. O compilador CUDA aloca registradores a cada thread, sob a restrição de que os registradores por thread vezes threads por bloco de threads não ultrapasse os 8192 registradores por multiprocessador. Um bloco de threads pode ter até 512 threads. Cada grupo de 32 threads em um bloco de threads é empacotado em warps. Grandes blocos de threads possuem melhor eficiência do que os pequenos, e eles podem ser tão pequenos quanto uma única thread. Como dissemos, os blocos de threads e warps com menos de 32 threads operam de modo menos eficiente do que os completos. Um escalonador de hardware tenta escalonar múltiplos blocos de threads por multiprocessador quando for possível. Se fizer isso, o escalonador também particiona o store local de 16KB dinamicamente entre os diferentes blocos de threads.
Colocando as GPUs em perspectiva GPUs como a arquitetura Tesla da NVIDIA não se encaixam muito bem nas classificações anteriores dos computadores, como a Figura 7.6. Claramente, o GeForce 8800 GTX, com 16 multiprocessadores Tesla, é um MIMD. A questão é como classificar cada um dos multiprocessadores Tesla e os oitos processadores principais que compõem um multiprocessador Tesla. Lembre-se de que já dissemos que o SIMD funcionou melhor com loops for e pior com instruções case e switch. O Tesla visa o alto desempenho para o paralelismo em nível de dados, enquanto facilita para os programadores lidarem com cases paralelos independentes em nível de thread. O Tesla permite que o programador pense que o multiprocessador é um MIMD multithreaded de oito processadores streaming, mas o hardware tenta reunir os oito processadores streaming para que atuem no padrão SIMT quando múltiplos threads do mesmo warp podem estar executando juntos. Quando os threads operam independentemente e seguem um caminho de execução independente, eles são executados mais lentamente do que no padrão SIMT, pois todos os 32 threads de um warp compartilham uma única unidade de busca de instrução. Se todos os 32 threads de um warp estivessem executando instruções independentes, cada thread operaria em 1/16 do desempenho máximo de um warp completo de 32 threads executando em oito processadores streaming por quatro clocks. Assim, cada thread independente tem seu próprio PC efetivo, de modo que os progra madores podem pensar no multiprocessador Tesla como MIMD, mas os programadores precisam ter o cuidado de escrever instruções de fluxo de controle que permitem que o hardware SIMT execute programas CUDA no padrão SIMD para oferecer o desempenho desejado. Ao contrário das arquiteturas de vetor, que contam com um compilador de vetorização para reconhecer o paralelismo em nível de dados em tempo de compilação e gerar
534
Capítulo 7 Multicores, multiprocessadores e clusters
instruções de vetor, as implementações de hardware da arquitetura Tesla descobrem o paralelismo em nível de dados entre os threads em tempo de execução. Assim, GPUs Tesla não precisam de compiladores de vetorização, e tornam mais fácil para o programador lidar com as partes do programa que não possuem paralelismo em nível de dados. Para explicar melhor essa técnica exclusiva, a Figura 7.8 coloca as GPUs em uma classificação que compara o paralelismo em nível de instrução com o paralelismo em nível de dados e se ele é descoberto em tempo de compilação ou execução. Essa categorização é uma indicação de que a GPU Tesla está ganhando terreno na arquitetura do computador.
FIGURA 7.8 Categorização de hardware das arquiteturas de processador e exemplos baseados em estático versus dinâmico e ILP versus DLP.
Verifique você mesmo
Verdadeiro ou falso: GPUs contam com chips de DRAM gráficos para reduzir a latência da memória e, portanto, aumentar o desempenho em aplicações gráficas.
Introdução às topologias de rede
7.8 multiprocessador
Os chips multicore exigem que as redes nos chips conectem os cores. Esta seção revisa os prós e os contras de diferentes redes de multiprocessadores. Os custos de rede incluem o número de switches, o número de links em um switch que se conectam à rede, a largura (número de bits) por link, o tamanho dos links quando a rede é mapeada no chip. Por exemplo, alguns cores podem ser adjacentes e outros podem estar no outro lado do chip. O desempenho da rede também tem muitas faces. Ele inclui a latência em uma rede não carregada para enviar e receber uma mensagem, a vazão em termos do número máximo de mensagens que podem ser transmitidas em determinado período de tempo, atrasos causados pela disputa por uma parte da rede, e desempenho variável dependendo do padrão de comunicação. Outra obrigação da rede pode ser tolerância a falhas, pois os sistemas podem ter de operar na presença de componentes defeituosos. Finalmente, nesta era de chips de potência limitada, a eficiência de potência das diferentes organizações pode superar outros aspectos. As redes normalmente são desenhadas como gráficos, com cada arco do gráfico representando um link da rede de comunicação. O nó processador-memória aparece como um quadrado preto, e o switch aparece como um círculo colorido. Nesta seção, todos os links são bidirecionais; ou seja, a informação pode fluir em qualquer direção. Todas as redes consistem em switches cujos links vão para os nós processador-memória e para outros switches. A primeira melhoria em relação a um barramento é uma rede que conecta uma sequência de nós:
Essa topologia é chamada de anel. Como alguns nós não são conectados diretamente, algumas mensagens terão um salto por nós intermediários até que cheguem ao destino final.
7.8 Introdução às topologias de rede multiprocessador 535
Diferente de um barramento, um anel é capaz de realizar muitas transferências simultâneas. Como existem diversas topologias para escolher, métricas de desempenho são necessárias a fim de distinguir esses projetos. Duas são comuns. A primeira é a largura de banda de rede total, que é a largura de banda de cada link multiplicado pelo número de links, e representa o melhor caso. Para a rede de anel apresentada, com P processadores, a largura de banda de rede total seria P vezes a largura de banda do link; a largura de banda de rede total de um barramento é a largura de banda desse barramento, ou duas vezes a largura de banda desse link. Para balancear esse melhor caso, incluímos outra métrica que é mais próxima do pior caso: a largura de banda da corte. Esta é calculada dividindo-se a máquina em duas partes, cada uma com metade dos nós. Depois você soma a largura de banda dos links que cruzam essa linha divisória imaginária. A largura de banda de corte de um anel é duas vezes a largura de banda do link, e é uma vez a largura de banda de link para o barramento. Se um único link for tão rápido quanto o barramento, o anel tem apenas o dobro da velocidade de um barramento no pior caso, mas é P vezes mais rápido no melhor caso. Como algumas topologias de rede não são simétricas, surge a questão de onde desenhar a linha imaginária quando fizer o corte da máquina. Essa é uma métrica do pior caso, de modo que a resposta é escolher a divisão que gera o desempenho de rede mais pessimista. Em outras palavras, calcule todas as larguras de banda de corte e escolha a menor. Tomamos a visão pessimista porque programas paralelos normalmente são limitados pelo elo mais fraco na cadeia de comunicação. No outro extremo de um anel está a rede totalmente conectada, em que cada processador tem um link bidirecional com cada outro processador. Para as redes totalmente conectadas, a largura de banda de rede total é P × (P - 1)/2, e a largura de banda de corte é (P/2)2. A tremenda melhoria no desempenho das redes totalmente conectadas é anulada pelo enorme aumento no custo. Isso inspira os engenheiros a inventarem novas topologias que estão entre o custo dos anéis e o desempenho das redes totalmente conectadas. A avaliação do sucesso depende em grande parte da natureza da comunicação na carga de trabalho de programas paralelos executados na máquina. O número de topologias diferentes que foram discutidas nas diversas publicações seria difícil de contar, mas somente uma minoria foi utilizada em processadores paralelos comerciais. A Figura 7.9 ilustra duas das topologias mais comuns. As máquinas reais constantemente acrescentam links extras a essas topologias simples para melhorar o desempenho e a confiabilidade.
FIGURA 7.9 Topologias de rede que apareceram nos processadores paralelos comerciais. Os círculos coloridos representam switches, e os quadrados pretos representam nós processador-memória. Embora um switch tenha muitos links, geralmente apenas um vai para o processador. A topologia de cubo n booliana é uma interconexão n-dimensional com 2n nós, exigindo n links por switch (mais um para o processador) e, portanto, n nós com o vizinho mais próximo. Constantemente, essas topologias básicas têm sido suplementadas com arcos extras para melhorar o desempenho e a confiabilidade.
largura de banda de rede Informalmente, a taxa de transferência de pico de uma rede; pode se referir à velocidade de um único link ou a taxa de transferência coletiva de todos os links na rede.
largura de banda de corte A largura de banda entre duas partes iguais de um multiprocessador. Essa medida é para uma divisão do multiprocessador no pior caso.
rede totalmente conectada Uma rede que conecta nós processador-memória fornecendo um link de comunicação dedicado entre cada nó.
536
Capítulo 7 Multicores, multiprocessadores e clusters
FIGURA 7.10 Topologias comuns de rede multiestágio para oito nós. Os switches nesses desenhos são mais simples do que nos desenhos anteriores, pois os links são unidirecionais; os dados entram na parte de baixo e saem pelo link da direita. A caixa de switch em c pode passar A para C e B para D ou B para C e A para D. O crossbar usa n2 switches, em que n é o número de processadores, enquanto a rede Ômega usa 2n log2n das caixas de switch grandes, cada uma composta logicamente por quatro dos switches menores. Nesse caso, o crossbar utiliza 64 switches contra 12 caixas de switch, ou 48 switches, na rede Ômega. O crossbar, porém, pode aceitar qualquer combinação de mensagens entre os processadores, enquanto a rede Ômega não pode.
rede multiestágio Uma rede que fornece um pequeno switch em cada nó.
rede totalmente conectada Uma rede que conecta nós processadores de memórias por meio do fornecimento de um link de comunicação dedicado entre cada nó. rede crossbar Uma rede que permite que qualquer nó se comunique com qualquer outro nó em uma passada pela rede.
Uma alternativa a colocar um processador em cada nó de uma rede é deixar apenas o switch em alguns desses nós. Os switches são menores que os nós processador-memória-switch, e assim podem ser compactados de forma mais densa, reduzindo assim a distância e aumentando o desempenho. Essas redes normalmente são chamadas redes multiestágio para refletir as múltiplas etapas em que uma mensagem pode trafegar. Os tipos de redes multiestágio são tão numerosos quanto as redes de único estágio; A Figura 7.10 ilustra duas das organizações multiestágio mais comuns. Uma rede totalmente conectada ou crossbar permite que qualquer nó se comunique com qualquer outro nó em uma passada pela rede. Uma rede Ômega usa menos hardware do que a rede crossbar (2n log2n contra n2 switches), mas pode ocorrer disputa entre as mensagens, dependendo do padrão de comunicação. Por exemplo, a rede Ômega na Figura 7.10 não pode enviar uma mensagem de P0 a P6 ao mesmo tempo em que envia uma mensagem de P1 a P7.
Implementando topologias de rede Esta análise simples de todas as redes nesta seção ignora considerações práticas importantes na construção de uma rede. A distância de cada link afeta o custo de comunicação em uma alta taxa de clock — geralmente, quanto maior a distância, mais dispendioso é trabalhar em uma taxa de clock alta. Distâncias mais curtas também facilitam a atribuição de mais fios
7.9 Benchmarks de multiprocessador 537
no link, pois a potência para conduzir por muitos fios a partir de um chip é menor se os fios forem curtos. Fios mais curtos também são mais baratos do que os mais longos. Outra limitação prática é que os desenhos tridimensionais precisam ser mapeados nos chips que são basicamente mídia bidimensional. A preocupação final é com a potência. Problemas de potência podem forçar chips multicore a contarem com topologias de grade simples, por exemplo. A conclusão é que as topologias que parecem ser elegantes quando esboçadas em um quadro podem ser impraticáveis quando construídas em silício.
7.9 Benchmarks de multiprocessador Como vimos no Capítulo 1, sistemas de benchmarking sempre é um assunto delicado, pois é uma forma altamente visível de tentar determinar qual sistema é melhor. Os resultados afetam não apenas as vendas de sistemas comerciais, mas também a reputação dos projetistas desses sistemas. Logo, os participantes querem ganhar a competição, mas eles também querem ter certeza de que, se alguém mais ganhar, eles mereçam ganhar porque possuem um sistema genuinamente melhor. Esse desejo leva a regras para garantir que os resultados do benchmark não sejam simplesmente truques de engenharia para esse benchmark, mas, em vez disso, avanços que melhoram o desempenho das aplicações reais. Para evitar possíveis truques, uma boa regra é que você não pode mudar o benchmark. O código-fonte e os conjuntos de dados são fixos, e existe uma única resposta apropriada. Qualquer desvio dessas regras torna os resultados inválidos. Muitos benchmarks de multiprocessador seguem essas tradições. Uma exceção comum é ser capaz de aumentar o tamanho do problema de modo que você possa executar o benchmark em sistemas com um número bem diferente de processadores. Ou seja, muitos benchmarks permitem pouca facilidade de expansão, em vez de exigir muita facilidade, embora você deva ter cuidado ao comparar resultados para programas executando problemas com diferentes tamanhos. A Figura 7.11 é um resumo de vários benchmarks paralelos, também descritos a seguir: j
Linpack é uma coleção de rotinas de álgebra linear, e as rotinas para realizar a eliminação Gaussiana constituem o que é conhecido como benchmark Linpack. A rotina DAXPY no exemplo da Seção 7.6 representa uma pequena fração do código fonte do benchmark Linpack, mas é responsável pela maior parte do tempo de execução do benchmark. Ele permite expansão fraca, deixando que o usuário escolha qualquer tamanho de problema. Além do mais, ele permite que o usuário reescreva o Linpack em qualquer formato e em qualquer linguagem, desde que calcule o resultado apropriado. Duas vezes por ano, os 500 computadores com o desempenho Linpack mais rápido são publicados em www.top500.org. O primeiro nessa lista é considerado pela imprensa como o computador mais rápido do mundo.
j
SPECrate é uma métrica de vazão baseada nos benchmarks SPEC CPU, como SPEC CPU 2006 (veja Capítulo 1). Em vez de relatar o desempenho dos programas individuais, SPECrate executa muitas cópias do programa simultaneamente. Assim, ele mede o paralelismo em nível de tarefa, pois não há comunicação entre as tarefas. Você pode executar tantas cópias dos programas quantas desejar, de modo que essa novamente é uma forma de expansão fraca.
j
SPLASH e SPLASH 2 (Stanford Parallel Applications for Shared Memory) foram esforços realizados por pesquisadores na Stanford University na década de 1990 para reunir um conjunto de benchmarks paralelo, semelhante em objetivos ao conjunto de benchmarks SPEC CPU. Ele inclui kernels e aplicações, além de muitos da comunidade de computação de alto desempenho. Esse benchmark requer expansão forte, embora venha com dois conjuntos de dados.
538
Capítulo 7 Multicores, multiprocessadores e clusters
FIGURA 7.11 Exemplos de benchmarks paralelos. j
Os benchmarks paralelos NAS (NASA Advanced Supercomputing) foram outra tentativa da década de 1990 de realizar o benchmark em multiprocessadores. Tomados da dinâmica de fluidos computacional, eles consistem em cinco kernels, e permitem expansão fraca, definindo alguns poucos conjuntos de dados. Assim como o Linpack, esses benchmarks podem ser reescritos, mas as regras exigem que a linguagem de programação só possa ser C ou Fortran.
j
O recente conjunto de benchmarks PARSEC (Princeton Application Repository for Shared Memory Computers) consiste em programas multithreaded que usam Pthreads (POSIX threads) e OpenMP (Open MultiProcessing). Eles focalizam mercados emergentes e consistem em nove aplicações e três kernels. Oito contam com paralelismo de dados, três contam com paralelismo em pipeline, e um com paralelismo não estruturado.
Pthreads Uma API do UNIX para criar e manipular threads. Ela vem com uma biblioteca. OpenMP Uma API para multiprocessamento de memória compartilhada em C, C++ ou Fortran, que é executada em plataformas UNIX e Microsoft. Ela inclui diretivas de compilador, uma biblioteca e diretivas de runtime.
7.10 Roofline: um modelo de desempenho simples 539
O lado negativo dessas restrições tradicionais dos benchmarks é que a inovação é limitada principalmente a arquitetura e compilador. Estruturas de dados melhores, algoritmos, linguagens de programação e assim por diante geralmente não podem ser usados, pois isso geraria um resultado ilusório. O sistema poderia ganhar, digamos, por causa do algoritmo, e não por causa do hardware ou do compilador. Embora essas orientações sejam compreensíveis quando os alicerces da computação são relativamente estáveis — como eram na década de 1990 e na primeira metade desta década —, elas são indesejáveis no início de uma revolução. Para que essa revolução tenha sucesso, precisamos encorajar a inovação em todos os níveis. Uma técnica recente foi defendida pelos pesquisadores na Universidade da Califórnia em Berkeley. Eles identificaram 13 padrões de projeto que afirmam que será parte das aplicações do futuro. Esses padrões de projeto são implementados por frameworks ou kernels. Alguns exemplos são matrizes esparsas, grade estruturada, máquinas de estados finitos, redução de mapa e travessia de gráfico. Mantendo as definições em um alto nível, elas esperam encorajar inovações em qualquer nível do sistema. Assim, o sistema com o solucionador de matriz esparsa mais rápido está livre para usar qualquer estrutura de dados, algoritmo e linguagem de programação, além de novas arquiteturas e compiladores. Veremos exemplos desses benchmarks na Seção 7.11. Verdadeiro ou falso: a principal desvantagem com as técnicas convencionais de benchmarks para computadores paralelos é que as regras que garantem justiça também suprimem a inovação.
7.10
Roofline: um modelo de desempenho simples
Esta seção é baseada em um artigo de Williams e Patterson [2008]. No passado, a sabedoria convencional em arquitetura de computador levou a projetos de microprocessador semelhantes. Quase todo computador desktop e servidor utilizava caches, pipelining, emissão de instrução superescalar, previsão de desvio e execução fora de ordem. Os conjuntos de instruções variavam, mas os microprocessadores eram todos da mesma escola de projeto. A passagem para multicore provavelmente significa que os microprocessadores se tornarão mais diversos, pois não existe sabedoria convencional sobre qual arquitetura tornará mais fácil escrever programas de processamento paralelo corretos, que executem de forma eficiente e se expandam à medida que o número de cores aumenta com o tempo. Além do mais, à medida que o número de cores por chip aumenta, um único fabricante provavelmente oferecerá diferentes números de cores por chip em diferentes pontos de preço ao mesmo tempo. Dada a diversidade crescente, seria especialmente útil se tivéssemos um modelo simples que oferecesse ideias para o desempenho de diferentes projetos. Ele não precisa ser perfeito, apenas criterioso. O modelo 3Cs do Capítulo 5 é uma analogia. Ele não é um modelo perfeito, pois ignora fatores potencialmente importantes, como tamanho de bloco, diretiva de alocação de bloco e diretiva de substituição de bloco. Além do mais, ele possui algumas esquisitices. Por exemplo, uma falha pode ser atribuída à capacidade em um projeto e a uma falha de conflito em outra cache do mesmo tamanho. Mesmo assim, o modelo 3Cs tem sido popular há 20 anos, pois oferece ideias para o comportamento dos programas, ajudando arquitetos e programadores a melhorarem suas criações com base em concepções desse modelo. Para descobrir esse modelo, vamos começar com os 13 padrões de projeto de Berkeley, na Figura 7.9. A ideia dos padrões de projeto é que o desempenho de determinada aplicação é na realidade a soma ponderada de vários kernels que implementam esses padrões de projeto. Vamos avaliar os kernels individuais aqui, mas lembre-se de que as aplicações reais são combinações de muitos kernels.
Verifique você mesmo
540
Capítulo 7 Multicores, multiprocessadores e clusters
Embora haja versões com diferentes tipos de dados, ponto flutuante é comum em várias implementações. Logo, o desempenho de pico em ponto flutuante é um limite sobre a velocidade desses kernels em determinado computador. Para chips multicore, o desempenho de pico em ponto flutuante é o desempenho de pico coletivo de todos os cores no chip. Se houvesse múltiplos microprocessadores no sistema, você multiplicaria o pico por chip pelo número total de chips. As demandas no sistema de memória podem ser estimadas dividindo-se esse desempenho de pico em ponto flutuante pelo número médio de operações de ponto flutuante por byte acessado: OperaçõesdePF/Seg = Bytes/Seg OperaçõesdePF/Byte intensidade aritmética A razão entre as operações de ponto flutuante em um programa e o número de bytes de dados acessados por um programa a partir da memória principal.
A razão entre operações de ponto flutuante por byte de memória acessada é chamada de intensidade aritmética. Ela pode ser calculada apanhando-se o número total de operações de ponto flutuante para um programa dividido pelo número total de bytes de dados transferidos para a memória principal durante a execução do programa. A Figura 7.12 mostra a intensidade aritmética de vários dos padrões de projeto de Berkeley da Figura 7.11.
FIGURA 7.12 Intensidade aritmética, especificada como o número de operações de ponto flutuante para executar o programa dividido pelo número de bytes acessados na memória principal [Williams, Patterson, 2008]. Alguns kernels possuem uma intensidade aritmética que se expande com o tamanho do problema, como Matrizes Densas, mas existem muitos kernels com intensidades aritméticas independentes do tamanho do problema. Para os kernels nesse primeiro caso, a expansão fraca pode levar a diferentes resultados, pois coloca muito menos demanda sobre o sistema de memória.
O modelo roofline O modelo simples proposto reúne desempenho de ponto flutuante, intensidade aritmética e desempenho da memória em um gráfico bidimensional [Williams, Patterson, 2008]. O desempenho de pico em ponto flutuante pode ser encontrado usando as especificações de hardware mencionadas anteriormente. O conjunto dos kernels que consideramos aqui não se encaixa em caches no chip, de modo que o desempenho de pico da memória pode ser definido pelo sistema de memória por trás das caches. Um modo de encontrar o desempenho de pico da memória é o benchmark Stream. (Veja a seção Detalhamento na Seção “Projetando o sistema de memória para suportar caches”, no Capítulo 5.) A Figura 7.13 mostra o modelo, que é feito uma vez para um computador, e não para cada kernel. O eixo-Y vertical é o desempenho de ponto flutuante alcançável de 0,5 a 64,0 GFLOPs/segundo. O eixo-X horizontal é a intensidade aritmética, variando de 1/8 FLOPs/DRAM acessados por byte a 16 FLOPs/DRAM acessados por byte. Observe que o gráfico é uma escala log-log.
7.10 Roofline: um modelo de desempenho simples 541
FIGURA 7.13 Modelo Roofline [Williams, Patterson, 2008]. Este exemplo tem um desempenho de pico de 16 GFLOPs/seg e uma largura de banda de memória de pico de 16GB/seg do benchmark Stream. (Como o Stream na realidade tem quatro medições, essa linha é a média das quatro.) A linha vertical pontilhada à esquerda representa o Kernel 1, que tem uma intensidade aritmética de 0,5 FLOPs/byte. Ela é limitada pela largura de banda de memória a não mais que 8 GFLOPs/seg nesse Opteron X2. A linha vertical pontilhada à direita representa o Kernel 2, que tem uma intensidade aritmética de 4 FLOPs/byte. Ela é limitada apenas computacionalmente a 16 GFLOPs/seg. (Esses dados são baseados no AMD Opteron X2 (Revision F) usando dual cores executando a 2GHz em um sistema dual socket.)
Para determinado kernel, podemos encontrar um ponto no eixo X com base em sua intensidade aritmética. Se desenhássemos uma linha vertical passando por esse ponto, o desempenho do kernel nesse computador teria de ficar em algum lugar nessa linha. Podemos desenhar uma linha horizontal mostrando o desempenho de pico em ponto flutuante do computador. Obviamente, o desempenho real em ponto flutuante não pode ser maior que a linha horizontal, pois esse é um limite do hardware. Como poderíamos desenhar o desempenho de pico da memória? Como o eixo X é FLOPs/byte e o eixo Y é FLOPs/segundo, bytes/segundo é simplesmente uma linha diagonal em um ângulo de 45 graus nessa figura. Logo, podemos desenhar uma terceira linha que mostre o desempenho máximo em ponto flutuante que o sistema de memória desse computador pode suportar para determinada intensidade aritmética. Podemos expressar os limites como uma fórmula para desenhar a linha no gráfico da Figura 7.13: GFLOPs/seg alcançável = Min(LBmemória de pico × Intensidadearitmética, Desempenhode picoem ponto flutuante) As linhas horizontal e diagonal dão nome a esse modelo simples e indicam seu valor. A “roofline” define um limite superior no desempenho de um kernel, dependendo de sua intensidade aritmética. Se pensarmos na intensidade aritmética como um poste que atinge o telhado, ou ele atinge a parte plana do telado, o que significa que o desempenho é computacionalmente limitado, ou atinge a parte inclinada do telado, o que significa que por fim está limitado pela largura de banda da memória. Na Figura 7.13, o kernel 2 é um exemplo do primeiro, e o kernel 1 é um exemplo do segundo. Dada uma roofline de um computador, você pode aplicá-la repetidamente, pois ela não varia por kernel. Observe que o “ponto de cumeeira”, em que os telhados diagonal e horizontal se encontram, oferece uma percepção interessante para o computador. Se for muito longe à direita, então somente os kernels com intensidade aritmética muito alta podem alcançar o desempenho máximo desse computador. Se for muito à esquerda, então quase todo kernel poderá potencialmente atingir o desempenho máximo. Veremos exemplos de ambos em breve.
542
Capítulo 7 Multicores, multiprocessadores e clusters
Comparando duas gerações de Opterons O AMD Opteron X4 (Barcelona) com quatro cores é o sucessor do Opteron X2 com dois cores. Para simplificar o projeto da placa, eles usam o mesmo soquete. Logo, eles possuem os mesmos canais de DRAM e, portanto, a mesma largura de banda de memória de pico. Além de dobrar o número de cores, o Opteron X4 também tem o dobro do desempenho de pico em ponto flutuante por core: os cores do Opteron X4 podem emitir duas instruções SSE2 de ponto flutuante por ciclo de clock, enquanto os cores do Opteron X2 emite no máximo uma. Como os dois sistemas que estamos comparando possuem taxas de clock semelhantes — 2,2GHz para o Opteron X2 versus 2,3GHz para o Opteron X4 — o Opteron X4 tem mais de quatro vezes do desempenho de ponto flutuante de pico do Opteron X2 com a mesma largura de banda de DRAM. O Opteron X4 também tem uma cache L3 de 2MB, que não é encontrada no Opteron X2. A Figura 7.14 compara os modelos roofline para ambos os sistemas. Como poderíamos esperar, o ponto de cumeeira passa de 1 no Opteron X2 para 5 no Opteron X4. Logo, para ver um ganho de desempenho na próxima geração, os kernels precisam de uma intensidade aritmética maior que 1, ou seus conjuntos de trabalho terão de caber nas caches do Opteron X4. O modelo roofline oferece um limite superior para o desempenho. Suponha que seu programa esteja muito abaixo desse limite. Que otimizações você deverá realizar, e em que ordem? Para reduzir os gargalos computacionais, as duas otimizações a seguir podem ajudar a quase todo kernel: 1. Mix de operações de ponto flutuante. O desempenho de pico em ponto flutuante para um computador normalmente exige um número igual de adições e multiplicações quase simultâneas. Esse equilíbrio é necessário ou porque o computador admite uma instrução multiplicação-adição unificada (veja a seção Detalhamento na Seção “Aritmética de precisão”, no Capítulo 3) ou porque a unidade de ponto flutuante tem um número igual de somadores de ponto flutuante e multiplicadores de ponto flutuante. O melhor desempenho também requer que uma fração significativa do mix de instruções seja operações de ponto flutuante, e não instruções de inteiros.
FIGURA 7.14 Modelos roofline de duas gerações de Opterons. A roofline do Opteron X2, que é a mesma que na Figura 7.11, está em preto, e a roofline do Opteron X4 está colorida. O ponto de cumeeira maior do Opteron X4 significa que os kernels que eram computacionalmente limitados no Opteron X2 poderiam ser limitados pelo desempenho da memória no Opteron X4.
7.10 Roofline: um modelo de desempenho simples 543
2. Melhore o paralelismo em nível de instrução e aplique SIMD. Para arquiteturas superescalares, o desempenho mais alto surge com a busca, execução e commit de três a quatro instruções por ciclo de clock (veja Capítulo 4). O objetivo aqui é melhorar o código do compilador para aumentar o ILP. Uma forma é desdobrando loops. Para as arquiteturas x86, uma única instrução SIMD pode operar sobre pares de operandos de precisão dupla, de modo que elas devem ser usadas sempre que possível. Para reduzir os gargalos da memória, as duas otimizações a seguir podem ajudar: 1. Pré-busca do software. Normalmente, o desempenho mais alto exige manter muitas operações da memória no ato, que é mais fácil de se fazer executando instruções de pré-busca do software, em vez de esperar até que os dados sejam exigidos pela computação. 2. Afinidade de memória. A maioria dos microprocessadores de hoje inclui um controlador de memória no mesmo chip com o microprocessador. Se o sistema tiver múltiplos chips, isso significa que os mesmos endereços vão para a DRAM que é local a um chip, e o restante requer que os acessos pela interconexão do chip acessem a DRAM que é local a outro chip. O segundo caso reduz o desempenho. Essa otimização tenta alocar dados e as threads encarregadas de operar sobre esses dados no mesmo par memória-processador, de modo que os processadores raramente precisam acessar a memória dos outros chips. O modelo roofline pode ajudar a decidir quais dessas otimizações serão realizadas e em que ordem. Podemos pensar em cada uma dessas otimizações como um “teto” abaixo da roofline apropriada, significando que você não pode ultrapassar um teto sem realizar a otimização associada. A roofline computacional pode ser encontrada nos manuais, e a roofline de memória pode ser encontrada executando-se o benchmark Stream. Os tetos computacionais, como o equilíbrio de ponto flutuante, também vêm dos manuais desse computador. O teto de memória exige a execução de experimentos em cada computador, para determinar a lacuna entre eles. A boa notícia é que esse processo só precisa ser feito uma vez por computador, pois quando alguém caracterizar os tetos de um computador, todos poderão usar os resultados a fim de priorizar suas otimizações para esse computador. A Figura 7.15 acrescenta tetos ao modelo roofline da Figura 7.13, mostrando os tetos computacionais no gráfico superior e os tetos da largura de banda de memória no gráfico inferior. Embora os tetos mais altos não sejam rotulados com as duas otimizações, isso está implícito nessa figura; para ultrapassar o teto mais alto, você já deverá ter ultrapassado todos os tetos abaixo. A espessura da lacuna entre o teto e o próximo limite mais alto é a recompensa por tentar essa otimização. Assim, a Figura 7.15 sugere que a otimização 2, que melhora o ILP, tem um grande benefício para melhorar a computação nesse computador, e a otimização 4, que melhora a afinidade de memória, tem um grande benefício para melhorar a largura de banda da memória nesse computador. A Figura 7.16 combina os tetos da Figura 7.15 em um único gráfico. A intensidade aritmética de um kernel determina a região de otimização, que, por sua vez, sugere quais otimizações tentar. Observe que as otimizações computacionais e as otimizações de largura de banda da memória se sobrepõem para grande parte da intensidade aritmética. Três regiões são sombreadas de formas diferentes na Figura 7.16 para indicar as diferentes estratégias de otimização. Por exemplo, o Kernel 2 cai no trapezóide azul à direita, que sugere trabalhar apenas nas otimizações computacionais. O Kernel 1 cai no paralelogramo azul-cinza no meio, que sugere tentar os dois tipos de otimização. Além do mais, ele sugere começar com as otimizações 2 e 4. Observe que as linhas verticais do Kernel 1 caem abaixo da otimização de desequilíbrio de ponto flutuante, de modo que a otimização 1 pode ser desnecessária. Se um kernel caísse no triângulo cinza no canto inferior esquerdo, isso sugeriria tentar apenas otimizações de memória.
544
Capítulo 7 Multicores, multiprocessadores e clusters
FIGURA 7.15 roofline com tetos. O gráfico superior mostra os “tetos” computacionais de 8 GFLOPs/seg se o mix de operações de ponto flutuante estiver desequilibrado e 2 GFLOPs/seg se as otimizações para aumentar o ILP e o SIMD também estiverem faltando. O gráfico inferior mostra os tetos de largura de banda da memória de 11GB/ seg sem pré-busca de software e 4,8GB/seg se as otimizações de afinidade de memória também estiverem faltando.
Até aqui, estivemos supondo que a intensidade aritmética é fixa, mas esse não é real mente o caso. Primeiro, existem kernels cuja intensidade aritmética aumenta com o tamanho do problema, como para os problemas Matriz Densa e N-body (veja Figura 7.12). Na realidade, esse pode ser o motivo para os programadores terem mais sucesso com a expansão fraca do que com a expansão forte. Segundo, as caches afetam o número de acessos que vão para a memória, de modo que as otimizações que melhoram o desempenho da cache também melhoram a intensidade aritmética. Um exemplo é melhorar a localidade temporal desdobrando loops e depois agrupando instruções com endereços semelhantes. Muitos computadores possuem instruções de cache especiais, que alocam dados em uma cache, mas não preenchem primeiro os dados da memória nesse endereço, pois eles logo
7.10 Roofline: um modelo de desempenho simples 545
FIGURA 7.16 Modelo roofline com tetos, áreas sobrepostas sombreadas e os dois kernels da Figura 7.13. Os kernels cuja intensidade aritmética se encontra no trapezóide azul à direita deverão focalizar otimizações de computação, e os kernels cuja intensidade aritmética se encontra no triângulo cinza no canto inferior esquerdo devem focalizar otimizações de largura de banda de memória. Aqueles que se encontram no paralelogramo azul-cinza no meio precisam se preocupar com ambos. Quando o Kernel 1 cai no paralelogramo do meio, tente otimizar ILP e SIMD, afinidade de memória e pré-busca de software. O Kernel 2 cai no trapezoide à direita; portanto, tente otimizar ILP e SIMD e o equilíbrio das operações de ponto flutuante.
serão modificados. Essas duas otimizações reduzem o tráfego da memória, movendo assim o poste da intensidade aritmética para a direita por um fator de, digamos, 1,5. Esse deslocamento para a direita poderia colocar o kernel em uma região de otimização diferente. A próxima seção usa o modelo roofline para demonstrar a diferença em quatro microprocessadores multicore recentes, para dois kernels de aplicação reais. Embora os exemplos anteriores mostrem como ajudar os programadores a melhorarem o desempenho, o modelo também pode ser usado por arquitetos para decidir onde eles otimizariam o hardware para melhorar o desempenho dos kernels que acreditam que serão importantes. Detalhamento: Os tetos são ordenados de modo que os mais baixos são mais fáceis de otimizar. Logicamente, um programador pode otimizar em qualquer ordem, mas ter essa sequência reduz as chances de desperdiçar esforço em uma otimização que não possui benefício devido a outras restrições. Assim como o modelo 3Cs, desde que o modelo roofline ofereça percepções, um modelo pode ter esquisitices. Por exemplo, ele supõe que o programa tem balanceamento de carga entre todos os processadores. Detalhamento: Uma alternativa ao benchmark Stream é usar a largura de banda bruta da DRAM como roofline. Enquanto as DRAMs definem um limite rígido, o desempenho real da memória normalmente está tão distante desse limite que não é tão útil como um limite superior. Ou seja, nenhum programa pode chegar perto desse limite. A desvantagem de usar o Stream é que uma programação muito cuidadosa pode exceder os resultados do Stream, de modo que a roofline da memória pode não ser um limite tão rígido quanto a roofline computacional. Ficamos com o Stream porque menos programadores serão capazes de oferecer mais largura de banda de memória do que o Stream descobre. Detalhamento: Os dois eixos usados anteriormente eram operações de ponto flutuante por segundo e intensidade aritmética dos acessos à memória principal. O modelo roofline poderia ser usado para outros kernels e computadores cujo desempenho foi uma função de diferentes métricas de desempenho.
546
Capítulo 7 Multicores, multiprocessadores e clusters
Por exemplo, se o conjunto de trabalho couber na cache L2 do computador, a largura de banda desenhada na roofline diagonal poderia ser largura de banda de cache L2, em vez da largura de banda da memória principal, e a intensidade aritmética no eixo X seria baseada em FLOPs por byte da cache L2 acessado. A linha de desempenho L2 diagonal subiria, e o ponto de cumeeira provavelmente se moveria para a esquerda. Como um segundo exemplo, se o kernel fosse classificado, os registros classificados por segundo poderiam substituir as operações de ponto flutuante por instrução no eixo X e a intensidade aritmética se tornaria registros por byte de DRAM acessado. O modelo roofline poderia ainda funcionar para um kernel com uso intenso de E/S. O eixo Y seria operações de E/S por segundo, o eixo X seria o número médio de instruções por operação de E/S, e a roofline mostraria a largura de banda de E/S de pico.
Detalhamento: Embora o modelo roofline apresentado seja para processadores multicores, ele certamente também funcionaria para um processador.
7.11
Vida real: benchmarking de quatro multicores usando o modelo roofline
Dada a incerteza sobre a melhor maneira de proceder nessa revolução paralela, não é surpresa que vejamos tantos projetos diferentes quantos chips multicore. Nesta seção, vamos examinar quatro sistemas multicore para dois kernels dos padrões de projeto da Figura 7.11: matriz esparsa e grade estruturada. (As informações nesta seção são de [Williams, Oliker, et al., 2007], [Williams, Carter, et al., 2008], [Williams and Patterson, 2008].)
Quatro sistemas multicore A Figura 7.17 mostra a organização básica dos quatro sistemas, e a Figura 7.18 lista as principais características dos exemplos desta seção. Estes são todos sistemas de soquete dual. A Figura 7.19 mostra o modelo de desempenho roofline para cada sistema. O Intel Xeon e5345 (apelidado de “Clovertown”) contém quatro cores por soquete, empacotando dois chips dual core em um único soquete. Esses dois chips compartilham um barramento front side que é conectado a um chip set north bridge separado (ver Capítulo 6). Esse chip set north bridge admite dois barramentos front side e, portanto, dois soquetes. Ele inclui o controlador de memória para as Fully Buffered DRAM DIMMs (FBDIMMs) de 667MHz. Esse sistema de soquete dual usa uma taxa de clock do processador de 2,33GHz e tem o mais alto desempenho de pico dos quatro exemplos: 75 GFLOPs. Porém, o modelo roofline na Figura 7.19 mostra que isso só pode ser obtido com intensidades aritméticas de 8 e acima. O motivo é que os barramentos front side duais interferem um com o outro, gerando largura de banda de memória relativamente baixa aos programas. O AMD Opteron X4 2356 (Barcelona) contém quatro cores por chip, e cada soquete tem um único chip. Cada chip tem um controlador de memória na placa e seu próprio caminho para a DRAM DDR2 de 667MHz. Esses dois soquetes se comunicam por links Hypertransport separados, dedicados, o que possibilita a criação de um sistema multichip “sem cola”. Esse sistema de soquete dual utiliza uma taxa de clock de processador de 2,30GHz e tem um desempenho de pico de aproximadamente 74GFLOPs. A Figura 7.19 mostra que o ponto de cumeeira no modelo roofline está à esquerda do Xeon e5345 (Clovertown), em uma intensidade aritmética de aproximadamente 5 FLOPs por byte. O Sun UltraSPARC T2 5140 (apelidado de “Niagara 2”) é muito diferente das duas microarquiteturas x86. Ele usa oito cores relativamente simples por chip, com uma taxa de clock muito mais baixa. Também oferece multithreading fine-grained com oito threads
7.11 Vida real: benchmarking de quatro multicores usando o modelo roofline 547
FIGURA 7.17 Quatro multiprocessadores recentes, cada um usando dois soquetes para os processadores. Começando com o canto superior esquerdo, os computadores são: (a) Intel Xeon e5345 (Clovertown), (b) AMD Opteron X4 2356 (Barcelona), (c) Sun UltraSPARC T2 5140 (Niagara 2) e (d) IBM Cell QS20. Observe que o Intel Xeon e5345 (Clovertown) tem um chip north bridge separado, não encontrado nos outros microprocessadores.
FIGURA 7.18 Características dos quatro multicores recentes. Embora o Xeon e5345 e o Opteron X4 tenham DRAMs com a mesma velocidade, o benchmark Stream mostra uma largura de banda de memória prática mais alta devido a ineficiências do barramento front side no Xeon e5345.
por core. Um único chip tem quatro controladores de memória que poderiam impulsionar quatro conjuntos de FBDIMMs de 667MHz. Para juntar dois chips UltraSPARC T2, dois dos quatro canais de memória são conectados, deixando dois canais de memória por chip. Esse sistema de soquete dual tem um desempenho de pico de cerca de 22 GFLOPs, e o ponto de cumeeira é uma intensidade aritmética incrivelmente baixa, com apenas 1/3 FLOPs por byte. O IBM Cell QS20 novamente é diferente das duas microarquiteturas x86 e do UltraSPARC T2. Esse é um projeto heterogêneo, com um core PowerPC relativamente simples e com oito SPEs (Synergistic Processing Elements) que têm seu próprio conjunto de instruções exclusivo em estilo SIMD. Cada SPE também tem sua própria memória local, em
548
Capítulo 7 Multicores, multiprocessadores e clusters
FIGURA 7.19 Modelo roofline para os multiprocessadores multicore na Figura 7.15. Os tetos são os mesmos que na Figura 7.13. Começando com o canto superior esquerdo, os computadores são: (a) Intel Xeon e5345 (Clovertown), (b) AMD Opteron X4 2356 (Barcelona), (c) Sun UltraSPARC T2 5140 (Niagara 2) e (d) IBM Cell QS20. Observe que os pontos de cumeeira para os quatro microprocessadores cruzam o eixo X nas intensidades aritméticas de 6, 4, 1/3 e 3/4, respectivamente. As linhas verticais tracejadas são para os dois kernels desta seção e as estrelas marcam o desempenho obtido para esses kernels após todas as otimizações. SpMV é o par de linhas verticais tracejadas à esquerda. Ele tem duas linhas porque sua intensidade aritmética melhorou de 0,166 para 0,255 com base nas otimizações de bloqueio de registrador. LBHMD são as linhas verticais tracejadas à direita. Ele tem um par de linhas em (a) e (b) porque uma otimização de cache pula o preenchimento do bloco de cache em uma falha quando o processador escreveria novos dados no bloco inteiro. Essa otimização aumenta a intensidade aritmética de 0,70 para 1,07. Essa é uma única linha a 0,70 em (c) porque o UltraSPARC T2 não oferece a otimização da cache. Essa é uma única linha a 1,07 em (d) porque o Cell possui store local carregado pelo DMA, de modo que o programa não busca dados desnecessários, como fazem as caches.
vez de uma cache. Um SPE deve transferir dados da memória principal para a memória local, a fim de operar sobre ela e depois de volta à memória principal, quando é concluído. Ele usa DMA, que tem alguma semelhança com a pré-busca de software. Os dois soquetes são conectados por meio de links dedicados a comunicações multichip. A taxa de clock desse sistema é a mais alta dos quatro multicores em 3,2GHz, e usa chips de DRAM XDR, que normalmente são encontrados em consoles de jogos. Eles possuem muita largura de banda, mas pouca capacidade. Dado que a aplicação principal do Cell era de gráficos, ele tem desempenho em precisão simples muito mais alto do que o desempenho em precisão dupla. O desempenho de pico em precisão dupla dos SPEs no sistema de soquete dual é 29 GFLOPs, e o ponto de cumeeira da intensidade aritmética é 0,75 FLOPs por byte.
7.11 Vida real: benchmarking de quatro multicores usando o modelo roofline 549
Embora as duas arquiteturas x86 tivessem muito menos cores por chip que as ofertas da IBM e Sun no início de 2008, é exatamente aí que elas estão hoje. À medida que se espera que o número de cores dobre a cada geração da tecnologia, será interessante ver se as arquiteturas x86 fecharão a “lacuna de core” ou se IBM e Sun poderão sustentar um número maior de cores, dado que seu foco principal está nos servidores e não no desktop. Observe que essas máquinas utilizam técnicas muito diferentes para o sistema de memória. O Xeon e5345 usa uma cache L1 privada convencional e então pares de processadores compartilham uma cache L2. Estes são conectados por meio de um controlador de memória fora do chip a uma memória comum por dois barramentos. Ao contrário, o Opteron X4 tem um controlador de memória separado e memória por chip, e cada core tem caches L1 e L2 privados. UltraSPARC T2 tem o controlador de memória no chip e quatro canais de DRAM separados por chip, e todos os cores compartilham a cache L2, que tem quatro bancos para melhorar a largura de banda. Seu multithreading fine-grained no topo de seu projeto multicore permite que ele mantenha muitos acessos à memória no ato. O mais radical é o Cell. Ele tem memórias privadas locais por SPE e usa DMA para transferir dados entre a DRAM conectada a cada chip e memória local. Ele sustenta muitos acessos à memória no ato tendo muitos cores e depois muitas transferências de DMA por core. Vejamos como esses quatro multicores em contraste funcionam nos dois kernels.
Matriz esparsa O primeiro kernel de exemplo do padrão de projeto computacional de matriz esparsa é o Sparse Matrix-Vector multiply (SpMV). SpMV é comum na computação científica, modelagem econômica e recuperação de informações. Infelizmente, as implementações convencionais normalmente executam em menos de 10% do desempenho de pico dos processadores. Um motivo é o acesso irregular à memória, que você poderia esperar de um kernel trabalhando com matrizes esparsas. O cálculo é y = A× x onde A é uma matriz esparsa e x e y são vetores densos. Quatorze matrizes esparsas tomadas de uma série de aplicações reais foram usadas para avaliar o desempenho do SpMV, mas somente o desempenho mediano é relatado aqui. A intensidade aritmética varia de 0,166 antes de uma otimização de bloqueio de registrador para 0,250 FLOPs por byte depois disso. O código primeiro foi paralelizado para utilizar todos os cores. Dado que a intensidade aritmética baixa do SpMV esteve abaixo do ponto de cumeeira de todos os multicores na Figura 7.19, a maioria das otimizações envolveu o sistema de memória: j
Pré-busca. Para obter o máximo dos sistemas de memória, as pré-buscas de software e hardware foram utilizadas.
j
Afinidade de memória. Essa otimização reduz os acessos à memória DRAM conectada ao outro soquete nos três sistemas que têm memória DRAM local.
j
Compactação de estruturas de dados. Como a largura de banda de memória provavelmente limita o desempenho, essa otimização usa estruturas de dados menores para aumentar o desempenho — por exemplo, usando um índice de 16 bits em vez de um índice de 32 bits, e usando representações dos não zeros com uso mais eficiente do espaço nas linhas de uma matriz esparsa.
A Figura 7.20 mostra o desempenho no SpMV para os quatro sistemas em comparação com o número de cores. (Os mesmos resultados são encontrados na Figura 7.19, mas é difícil comparar o desempenho quando se usa uma escala logarítmica.) Observe que, apesar de ter o desempenho de pico mais alto na Figura 7.18 e o desempenho de core isolado mais alto, o Intel Xeon e5345 tem o desempenho oferecido mais baixo dos quatro multicores. O Opteron X4 dobra seu desempenho. O gargalo do Xeon e5345 são os barramentos front side duais. Apesar da taxa de clock mais baixa, o número maior de cores simples do Sun
550
Capítulo 7 Multicores, multiprocessadores e clusters
FIGURA 7.20 Desempenho do SpMV nos quatro multicores.
UltraSPARC T2 supera os dois processadores x86. O IBM Cell tem o desempenho mais alto dos quatro. Observe que todos menos o Xeon e5345 se expandem bem com o número de cores, embora o Opteron X4 se expanda mais lentamente com quatro ou mais cores.
Grade estruturada O segundo kernel é um exemplo do padrão de projeto de grade estruturada. Lattice-Boltzmann Magneto-Hydrodynamics (LBMHD) é comum para a dinâmica de fluido computacional; ele é um código de grade estruturada com uma série de etapas de tempo. Cada ponto envolve leitura e escrita de aproximadamente 75 números de ponto flutuante de precisão dupla e cerca de 1300 operações de ponto flutuante. Assim como SpMV, LBMHD tende a conseguir uma pequena fração do desempenho de pico nos processadores, devido à complexidade das estruturas de dados e da irregularidade dos padrões de acesso à memória. A razão entre FLOPs e bytes é muito maior, 0,70, em comparação com menos de 0,25 no SpMV. Não preenchendo o bloco de cache da memória em uma perda de escrita quando o programa for sobrescrever o bloco inteiro, a intensidade sobe para 1,07. Todos os multicores menos o UltraSPARC T2 (Niagara 2) oferecem essa otimização de cache. A Figura 7.19 mostra que a intensidade aritmética do LBMHD é tão alta que as otimizações de largura de banda computacional e da memória fazem sentido em todos os multicores, menos o UltraSPARC T2, cujo ponto de cumeeira da roofline está abaixo do ponto do LBMHD. O UltraSPARC T2 pode alcançar a roofline usando apenas as otimizações computacionais. Além de colocar o código em paralelo, de modo que possa usar todos os cores, as otimizações a seguir foram usadas para LBMHD: j
Afinidade de memória: Essa otimização novamente é útil pelos mesmos motivos mencionados anteriormente.
j
Minimização de perda de TLB: Para reduzir as perdas de TLB significativamente no LBMHD, use uma estrutura de arrays e combine alguns loops juntos em vez da técnica convencional de usar um array de estruturas.
j
Desdobramento e reordenação de loop: Para expor paralelismo suficiente e melhorar a utilização de cache, os loops foram desdobrados e depois reordenados para agrupar as instruções com endereços semelhantes.
j
SIMD: Os compiladores dos dois sistemas x86 não poderiam gerar bom código SSE, de modo que estes tiveram de ser escritos à mão em linguagem assembly.
7.11 Vida real: benchmarking de quatro multicores usando o modelo roofline 551
FIGURA 7.21 Desempenho do LBMHD nos quatro multicores.
FIGURA 7.22 Desempenho de base versus totalmente otimizado dos quatro cores nos dois kernels. Observe a alta fração de desempenho totalmente otimizado oferecido pelo Sun UltraSPARC T2 (Niagara 2). Não existe coluna de desempenho de base para o IBM Cell, pois não existe como portar o código para os SPEs sem caches. Embora você possa executar o código no core do Power, ele tem um desempenho com uma ordem de grandeza a menos que o SPES, e por isso o ignoramos nesta figura.
A Figura 7.21 mostra o desempenho dos quatro sistemas em comparação com o número de cores para LBMHD. Assim como o SpMV, o Intel Xeon e5345 tem a pior escalabilidade. Desta vez, os cores mais poderosos do Opteron X4 são superiores aos cores simples do UltraSPARC T2, apesar de ter metade do número de cores. Mais uma vez, o IBM Cell é o sistema mais rápido. Tudo menos o Xeon e5345 se expande bem com o número de cores, embora T2 e Cell se expandam mais tranquilamente do que o Opteron X4.
Produtividade Além do desempenho, outra questão importante para a revolução da computação paralela é a produtividade, ou a dificuldade da programação de alcançar o desempenho. Para ilustrar as diferenças, a Figura 7.22 compara o desempenho simples com o desempenho totalmente otimizado para os quatro cores nos dois kernels. O mais fácil foi o UltraSPARC T2, devido à sua grande largura de banda de memória e seus cores fáceis de entender. O conselho para esses dois kernels no UltraSPARC T2 é simplesmente tentar obter código de bom desempenho do compilador e depois usar o máximo de threads possível. O único cuidado para outros kernels é que o UltraSPARC T2 pode cair na armadilha sobre garantir que a associatividade de conjunto combina com o número de threads de hardware (veja Seção 5.11, no Capítulo 5). Cada chip admite 64 threads de hardware, enquanto o cache L2 é associativo em conjunto com quatro vias. Essa divergência pode exigir a reestruturação de loops para reduzir as falhas por conflito.
552
Capítulo 7 Multicores, multiprocessadores e clusters
O Xeon e5346 era considerado complexo porque não era fácil entender o comportamento de memória dos barramentos front side duais, era difícil entender como funcionava a pré-busca do hardware, assim como obter um bom código SIMD do compilador. O código C para ele e para o Opteron X4 são repletos de instruções intrínsecas envolvendo instruções SIMD para obter um bom desempenho. O Opteron X4 beneficiou-se da maioria dos tipos de otimizações, de modo que precisou de mais esforço que o Xeon e5345, embora o comportamento da memória do Opteron X4 fosse mais fácil de entender que o do Xeon e5345. O Cell forneceu dois tipos de desafios. Primeiro, as instruções SIMD do SPE eram difíceis de compilar, de modo que às vezes você precisava ajudar o compilador, inserindo instruções intrínsecas com instruções em linguagem assembly no código C. Segundo, o sistema de memória era mais interessante. Como cada SPE tem memória local em um espaço de endereço separado, você não poderia simplesmente transportar o código e começar a executar no SPE. Portanto, não há uma coluna de código base para o IBM Cell na Figura 7.22, e você precisava mudar o programa para emitir comandos de DMA e transferir dados entre o armazenamento local e a memória. A boa notícia é que o DMA desempenhou o papel de pré-busca de software nas caches, e o DMA é muito mais fácil de usar e conseguir um bom desempenho da memória. O Cell foi capaz de oferecer quase 90% da “roofline” de largura de banda de memória para esses kernels, em comparação com 50% ou menos para os outros multicores.
Por mais de uma década os analistas anunciam que a organização de um único computador alcançou seus limites e que avanços verdadeiramente significantes só podem ser feitos pela interconexão de uma multiplicidade de computadores de tal modo que permita solução cooperativa... Demonstrou-se a continuada validade do método de processador único... Gene Amdahl, “Validity of the single processor approach to achieving large scale computing capabilities”, Spring Joint Computer Conference, 1967
7.12
Falácias e armadilhas
Os muitos ataques ao processamento paralelo revelaram inúmeras falácias e armadilhas. Veremos três delas aqui. Falácia: a Lei de Amdahl não se aplica aos computadores paralelos. Em 1987, o diretor de uma organização de pesquisa afirmou que a Lei de Amdahl tinha sido quebrada por uma máquina de multiprocessador. Para tentar entender a base dos relatos da mídia, vejamos a citação que nos trouxe a Lei de Amdahl [1967, p. 483]: Uma conclusão bastante óbvia que pode ser tirada nesse momento é que o esforço despendido em conseguir altas velocidades de processamento paralelo é desperdiçado se não for acompanhado de conquistas de mesmas proporções nas velocidades de processamento sequencial. Essa afirmação ainda deve ser verdadeira; a parte ignorada do programa deve limitar o desempenho. Uma interpretação da lei leva ao seguinte princípio: partes de cada programa precisam ser sequenciais e, portanto, precisa haver um limite superior lucrativo para o número de processadores – digamos, 100. Mostrando speed-up linear com 1.000 processadores, esse princípio se torna falso e, então, a Lei de Amdahl foi quebrada. O método dos pesquisadores foi mudar a entrada para o benchmark: em vez de ir 1.000 vezes mais rápido, eles calcularam 1.000 vezes mais trabalho em tempo comparável. Para o algoritmo deles, a parte sequencial do programa era constante, independente do tamanho da entrada, e o restante era totalmente paralelo – daí, speed-up linear com 1.000 processadores. Não vemos razão para que a Lei de Amdahl não se aplique aos processadores paralelos. O que essa pesquisa salienta é um dos principais usos de computadores rápidos é executar problemas grandes, mas ter ciência de como os algoritmos escalam com o crescimento do tamanho do problema. Falácia: o desempenho de pico segue o desempenho observado. Por exemplo, a Seção 7.11 mostra que o Intel Xeon e5345, o microprocessador com o desempenho de pico mais alto, foi o mais lento dos quatro microprocessadores multicore para dois kernels.
7.13 Comentários finais 553
A indústria de supercomputadores usou essa métrica no marketing, e a falácia é enfatizada com as máquinas paralelas. Não apenas os marketeiros estão usando o desempenho de pico quase inatingível de um nó processador, mas eles também o estão multiplicando pelo número total de processadores, considerando speed-up perfeito! A lei de Amdahl sugere como é difícil alcançar qualquer um desses picos; multiplicar os dois multiplica os pecados. O modelo roofline ajuda a entender melhor o desempenho de pico. Armadilha: não desenvolver o software para tirar proveito de (ou otimizar) uma arquitetura de multiprocessador. Há um longo histórico de software ficando para trás nos processadores paralelos, possivelmente porque os problemas do software são muito mais difíceis. Temos um exemplo para mostrar a sutileza dessas questões, mas existem muitos exemplos que poderíamos escolher! Um problema encontrado com frequência ocorre quando o software projetado para um processador único é adaptado a um ambiente multiprocessador. Por exemplo, o sistema operacional SGI protegia originalmente a tabela de página com um único lock, supondo que a alocação de página é pouco frequente. Em um processador único, isso não representa um problema de desempenho, mas em um multiprocessador, pode se tornar um gargalo de desempenho importante para alguns programas. Considere um programa que usa um grande número de páginas que são inicializadas quando começa a ser executado, o que o UNIX faz para as páginas alocadas estaticamente. Suponha que o programa seja colocado em paralelo, de modo que múltiplos processos aloquem as páginas. Como a alocação de página requer o uso da tabela de página, que é bloqueada sempre que está em uso, até mesmo um kernel do SO que permita múltiplas threads no SO será colocado em série se todos os processos tentarem alocar suas páginas ao mesmo tempo (que é exatamente o que poderíamos esperar no momento da inicialização!). Essa serialização da tabela de página elimina o paralelismo na inicialização e tem um impacto significativo sobre o desempenho paralelo geral. Esse gargalo de desempenho persiste até mesmo para o paralelismo em nível de tarefa. Por exemplo, suponha que dividamos o programa de processamento paralelo em tarefas separadas e as executemos, uma tarefa por processador, de modo que não haja compartilhamento entre elas. (É exatamente isso o que um usuário fez, pois acreditava que o problema de desempenho era devido ao compartilhamento não intencional ou interferência em sua aplicação.) Infelizmente, o lock ainda coloca todas as tarefas em série — de modo que, até mesmo o desempenho da tarefa independente é fraco. Essa armadilha indica os tipos de bugs de desempenho sutis, porém significativos, que podem surgir quando o software é executado em multiprocessadores. Assim como muitos outros componentes de software essenciais, os algoritmos do SO e as estruturas de dados precisam ser repensadas em um contexto de multiprocessador. Colocar locks em partes menores da tabela de página efetivamente elimina o problema.
7.13
Comentários finais
O sonho de construir computadores apenas agregando processadores existe desde os primeiros dias da computação. No entanto, o progresso na construção e no uso de processadores paralelos eficientes tem sido lento. Essa velocidade de progresso foi limitada pelos difíceis problemas de software bem como por um longo processo de evolução da arquitetura dos multiprocessadores para melhorar a usabilidade e a eficiência. Discutimos muitos dos problemas de software neste capítulo, incluindo a dificuldade de escrever programas que obtêm bom speed-up devido à Lei de Amdahl. A grande variedade de métodos arquitetônicos diferentes e o sucesso limitado e a vida curta de muitas arquiteturas até agora se juntam às dificuldades de software. Abordaremos a história do desenvolvimento desses multiprocessadores na Seção 7.14, no site.
Estamos dedicando todo o nosso desenvolvimento de produto futuro aos projetos multicore. Acreditamos que esse seja um ponto de inflexão importante para a indústria. ... Essa não é uma corrida, é uma mudança de mares na computação. Paul Otellini, Presidente da Intel, Intel Developers Forum, 2004.
554
Capítulo 7 Multicores, multiprocessadores e clusters
Como dissemos no Capítulo 1, apesar desse longo e sinuoso passado, a indústria da tecnologia de informação agora tem seu futuro ligado à computação paralela. Embora seja fácil apontar fatos para que esse esforço falhe como muitos no passado, existem motivos para termos esperança: j
Claramente, o software como um serviço está ganhando mais importância, e os clusters provaram ser um modo muito bem-sucedido de oferecer tais serviços. Oferecendo redundância em um nível mais alto, incluindo centros de dados geograficamente distribuídos, esses serviços têm oferecido disponibilidade 24 × 7 × 365 para os clientes no mundo inteiro. É difícil não imaginar que o número de servidores por centro de dados e o número de centros de dados continuarão a crescer. Certamente, esses centros de dados abraçarão projetos multicore, pois eles já podem usar milhares de processadores em suas aplicações.
j
O uso de processamento paralelo em domínios como a computação científica e de engenharia é comum. Esse domínio de aplicação possui uma necessidade quase ilimitada de mais computação. Ele também possui muitas aplicações com uma grande quantidade de paralelismo natural. Mais uma vez, os clusters dominam essa área de aplicação. Por exemplo, usando o relatório do Linpack 2007, os clusters representam mais de 80% dos 500 computadores mais rápidos. Entretanto, isso não tem sido fácil: programar processadores paralelos até para essas aplicações continua sendo um desafio. Ainda assim, esse grupo certamente também acolherá os chips multicore, pois novamente eles têm experiência com centenas a milhares de processadores.
j
Todos os fabricantes de microprocessador de desktop e servidor estão construindo multiprocessadores para alcançar o desempenho mais alto, de modo que, diferente do passado, não existe um caminho fácil para o desempenho mais alto para aplicações sequenciais. Logo, os programadores que precisam de desempenho mais alto precisam colocar seus códigos em paralelo ou escrever novos programas de processamento paralelo.
j
Processadores múltiplos no mesmo chip permitem uma velocidade de comunicação muito diferente dos projetos de múltiplos chips, oferecendo latência muito mais baixa e largura de banda muito mais alta. Essas melhorias podem facilitar a oferta de bom desempenho.
j
No passado, os microprocessadores e os multiprocessadores estavam sujeitos a diferentes definições de sucesso. Ao escalar o desempenho do processador único, os arquitetos de microprocessador ficavam felizes se o desempenho com uma única thread subisse pela raiz quadrada da área de silício aumentada. Assim, eles ficavam felizes com um desempenho sublinear em termos de recursos. O sucesso do multiprocessador era definido como um speed-up linear como função do número de processadores, supondo que o custo da compra ou o custo da administração de n processadores era n vezes o custo de um processador. Agora que o paralelismo está acontecendo no chip via multicore, podemos usar o microprocessador tradicional com sucesso na melhoria do desempenho sublinear.
j
O sucesso da compilação em tempo de execução just-in-time torna viável pensar no software adaptando-se para tirar proveito do número cada vez maior de cores por chip, o que oferece uma flexibilidade que não está disponível quando limitado a compiladores estáticos.
j
Diferente do passado, o movimento do código-fonte aberto tornou-se uma parte fundamental da indústria de software. Esse movimento é uma meritocracia, em que melhores soluções de engenharia podem ganhar a fatia de desenvolvedores em relação a questões legadas. Ele também alcança a inovação, convidando a mudança no software antigo e recebendo novas linguagens e produtos de software. Essa cultura aberta poderia ser extremamente útil nessa época de mudança rápida.
7.15 Exercícios 555
Essa revolução na interface de hardware/software talvez seja o maior desafio que esse campo encarou nos últimos 50 anos. Ela oferecerá muitas novas oportunidades de pesquisa e negócios dentro e fora do campo de TI, e as empresas que dominam a era do multicore podem não ser as mesmas que dominaram a era do processador único. Talvez você será um dos inovadores que aproveitará as oportunidades que certamente aparecerão nos tempos de incerteza mais adiante.
7.14 Perspectiva histórica e leitura adicional Esta seção no site oferece um histórico rico e geralmente desastroso dos multiprocessadores nos últimos 50 anos.
7.15 Exercícios1 Exercício 7.1 Primeiro, escreva uma lista das atividades diárias que você realiza normalmente em um fim de semana. Por exemplo, você poderia levantar da cama, tomar um banho, se vestir, tomar o café, secar seu cabelo, escovar os dentes etc. Lembre-se de distribuir sua lista de modo que tenha um mínimo de dez atividades. 7.1.1 <7.2> Agora considere qual dessas atividades já está explorando alguma forma de paralelismo (por exemplo, escovar vários dentes ao mesmo tempo em vez de um de cada vez, carregar um livro de cada vez para a escola em vez de colocar todos eles na sua mochila, e depois carregá-los “em paralelo”). Para cada uma de suas atividades, discuta se elas já estão sendo executadas em paralelo, mas se não, por que não estão. 7.1.2 <7.2> Em seguida, considere quais das atividades poderiam ser executadas simultaneamente (por exemplo, tomar café e escutar as notícias). Para cada uma das suas atividades, descreva qual outra atividade poderia ser emparelhada com essa atividade. 7.1.3 <7.2> Para o Exercício 7.1.2, o que poderíamos mudar sobre os sistemas atuais (por exemplo, banhos, roupas, TVs, carros) de modo que pudéssemos realizar mais tarefas em paralelo? 7.1.4 <7.2> Estime quanto tempo a menos seria necessário para executar essas atividades se você tentasse executar o máximo de tarefas em paralelo possível.
Exercício 7.2 Muitas aplicações de computador envolvem a pesquisa por um conjunto de dados e a classificação dos dados. Diversos algoritmos eficientes de busca e classificação foram criados para reduzir o tempo de execução dessas tarefas tediosas. Neste problema, vamos considerar como é melhor colocar essas tarefas em paralelo. 1
Contribuição de David Kaeli, da Northeastern University
556
Capítulo 7 Multicores, multiprocessadores e clusters
7.2.1 [10] <7.2> Considere o seguinte algoritmo de busca binária (um algoritmo clássico do tipo dividir e conquistar) que procura um valor X em um array de N elementos A e retorna o índice da entrada correspondente:
Suponha que você tenha Y cores em um processador multicore para executar BinarySearch. Supondo que Y seja muito menor que N, expresse o fator de speed-up que você poderia esperar obter para os valores e Y e N. Desenhe isso em um gráfico. 7.2.2 [5] <7.2> Em seguida, suponha que Y seja igual a N. Como isso afetaria suas conclusões na sua resposta anterior? Se você estivesse encarregado de obter o melhor fator de speed-up possível (ou seja, expansão forte), explique como poderia mudar esse código para obter isso.
Exercício 7.3 Considere o seguinte trecho de código em C:
O código MIPS correspondente a esse fragmento é:
As instruções têm as seguintes latências associadas (em ciclos): ADD.D
L.D
S.D
DADDIU
4
6
1
2
7.15 Exercícios 557
7.3.1 [10] <7.2> Quantos ciclos são necessários para que todas as instruções em uma única iteração do loop anterior sejam executadas? 7.3.2 [10] <7.2> Quando uma instrução em uma iteração posterior de um loop depende do valor de dados produzido em uma iteração anterior do mesmo loop, dizemos que existe uma dependência carregada pelo loop entre as iterações do loop. Identifique as dependências carregadas pelo loop no código anterior. Identifique a variável de programa dependente e os registradores em nível de assembly. Você pode ignorar a variável de indução de loop j. 7.3.3 [10] <7.2> O desdobramento de loop foi descrito no Capítulo 4. Aplique o desdobramento de loop a esse loop e depois considere a execução desse código em um sistema de passagem de mensagem com memória distribuída com 2 nós. Suponha que usaremos a passagem de mensagem conforme descrito na Seção 7.4, na qual apresentamos uma nova operação send(x,y) que envia ao nó x o valor y, e uma operação receive( ) que espera pelo valor sendo enviado a ele. Suponha que as operações send gastem um ciclo para emitir (ou seja, outras instruções no mesmo nó podem prosseguir para o próximo ciclo), mas gastem 10 ciclos para serem recebidas no nó receptor. Operações receive provocam stall da execução no nó em que são executadas até que recebam uma mensagem. Produza um schedule para os dois nós; considere um fator de desdobramento de 4 para o corpo do loop (ou seja, o corpo do loop aparecerá quatro vezes). Calcule o número de ciclos necessários para que o loop seja executado no sistema de passagem de mensagens. 7.3.4 [10] <7.2> A latência da rede de interconexão desempenha um papel importante na eficiência dos sistemas de passagem de mensagens. Que velocidade a interconexão precisa ter a fim de obter qualquer speed-up com o uso do sistema distribuído descrito no Exercício 7.3.3?
Exercício 7.4 Considere o seguinte algoritmo mergesort recursivo (outro algoritmo clássico para dividir e conquistar). Mergesort foi descrito inicialmente por John von Neumann em 1945. A ideia básica é dividir uma lista não classificada x de m elementos em duas sublistas de aproximadamente metade do tamanho da lista original. Repita essa operação em cada sublista e continue até que tenhamos listas de tamanho 1. Depois, começando com sublistas de tamanho 1, faça o “merge” das duas sublistas em uma única lista classificada.
A etapa do merge é executada pelo seguinte código:
558
Capítulo 7 Multicores, multiprocessadores e clusters
7.4.1 [10] <7.2> Suponha que você tenha Y cores em um processador multicore para executar o MergeSort. Supondo que Y seja muito menor que length(m), expresse o fator de speed-up que você poderia esperar obter para os valores de Y e length(m). Desenhe isso em um gráfico. 7.4.2 [10] <7.2> Em seguida, considere que Y é igual a length(m). Como isso afetaria suas conclusões na sua resposta anterior? Se você estivesse encarregado de obter o melhor fator de speed-up possível (ou seja, expansão forte), explique como poderia mudar esse código para obtê-lo.
Exercício 7.5 Você está tentando preparar três tortas de mirtilo. Os ingredientes são os seguintes: 1 xícara de manteiga 1 xícara de açúcar 4 ovos grandes 1 colher de chá de extrato de baunilha 1/2 colher de chá de sal 1/4 colher de chá de noz moscada 1 1/2 xícaras de farinha de trigo 1 xícara de mirtilos A receita para uma única torta é a seguinte: Passo 1: pré-aqueça o forno a 160 °C. Unte e polvilhe farinha na forma. Passo 2: em uma bacia grande, bata com a batedeira a manteiga e o açúcar em velocidade média até que a massa fique leve e macia. Acrescente ovos, baunilha, sal e noz moscada. Bata até que tudo fique totalmente misturado. Reduza a velocidade da batedeira e acrescente farinha de trigo, 1/2 xícara por vez, batendo até ficar bem misturado. Passo 3: inclua os mirtilos aos poucos. Espalhe uniformemente na forma da torta. Leve ao forno 60 minutos. 7.5.1 [5] <7.2> Sua tarefa é cozinhar três tortas da forma mais eficiente possível. Supondo que você só tenha um forno com tamanho suficiente para conter uma torta, uma bacia
7.15 Exercícios 559
grande, uma forma de torta e uma batedeira, prepare um plano para fazer as três tortas o mais rapidamente possível. Identifique os gargalos para completar essa tarefa. 7.5.2 [5] <7.2> Suponha agora que você tenha três bacias, três formas de torta e três batedeiras. O quanto o processo fica mais rápido, agora que você tem esses recursos adicionais? 7.5.3 [5] <7.2> Agora suponha que você tem dois amigos que o ajudarão a cozinhar, e que você tem um forno grande, que possa acomodar todas as três tortas. Como isso mudará o plano que você preparou no Exercício 7.5.1? 7.5.4 [5] <7.2> Compare a tarefa de preparação da torta com o cálculo de três iterações de um loop em um computador paralelo. Identifique o paralelismo em nível de dados e o paralelismo em nível de tarefa no loop de preparação da torta.
Exercício 7.6 A multiplicação de matriz desempenha um papel importante em diversas aplicações. Duas matrizes só podem ser multiplicadas se o número de colunas da primeira matriz for igual ao número de linhas na segunda. Vamos supor que tenhamos uma matriz m × n A e queiramos multiplicá-la por uma matriz n × p B. Podemos expressar seu produto como uma matriz m × p indicada por AB(ou A · B). Se atribuirmos C = AB, e ci,j indicar a entrada em C na posição (i, j), então n
ci , j = ∑ ai ,r br , j = ai ,1 b1, j + ai ,2 b2, j +…+ ai ,n bn, j i =1
para cada elemento i e j com 1 ≤ i ≤ m e 1 ≤ j ≤ p. Agora, queremos ver se podemos fazer o cálculo de C em paralelo. Suponha que as matrizes estejam dispostas na memória sequencialmente da seguinte forma: a1,1, a2,1, a3,1, a4,1, …, etc. 7.6.1 [10] <7.3> Suponha que iremos calcular C em uma máquina de memória compartilhada de único core e uma máquina com memória compartilhada de 4 cores. Calcule o speed-up que esperaríamos obter em uma máquina de 4 cores, ignorando quaisquer problemas de memória. 7.6.2 [10] <7.3> Repita o Exercício 7.6.1, supondo que as atualizações em C incorrem em uma falha de cache, devida ao compartilhamento falso quando os elementos consecutivos que estão em sequência (ou seja, índice i) são atualizados. 7.6.3 [10] <7.3> Como você consertaria o problema de compartilhamento falso que pode ocorrer?
Exercício 7.7 Considere as seguintes partes de dois programas diferentes rodando ao mesmo tempo em quatro processadores em um processador multicore simétrico (SMP). Suponha que, antes que esse código seja executado, tanto x quanto y sejam 0. Core 1: x = 2; Core 2: y = 2; Core 3: w = x + y + 1; Core 4: z = x + y;
560
Capítulo 7 Multicores, multiprocessadores e clusters
7.7.1 [10] <7.3> Quais são todos os valores resultantes possíveis de w, x, y e z? Para cada resultado possível, explique como poderíamos chegar a esses resultados. Você precisará examinar todas as intercalações possíveis das instruções. 7.7.2 [5] <7.3> Como você poderia tornar a execução mais determinística, de modo que somente um conjunto de valores seja possível?
Exercício 7.8 Em um sistema de memória compartilhada CC-NUMA, as CPUs e a memória física são divididas entre os nós de computação. Cada CPU possui caches locais. Para manter a coerência da memória, podemos acrescentar bits de status em cada bloco de cache, ou podemos introduzir diretórios de memória dedicados. Usando diretórios, cada nó oferece uma tabela de hardware dedicada para gerenciar o status de cada bloco de memória que seja “local” a esse nó. O tamanho de cada diretório é uma função do tamanho do espaço compartilhado CC-NUMA (uma entrada é fornecida para cada bloco de memória local a um nó). Se armazenarmos informações de coerência na cache, acrescentamos essa informação a cada cache em cada sistema (ou seja, a quantidade de espaço de armazenamento é uma função do número de blocos de cache disponíveis em todas as caches). 7.8.1 [15] <7.3> Se tivermos P CPUs no sistema com T nós no sistema CC-NUMA, com cada CPU tendo C blocos de memória, e mantivermos um byte de informação de coerência em cada bloco de cache, elabore uma equação que expresse a quantidade de memória que estará presente nas caches em um único nó do sistema para manter a coerência. Não inclua o espaço de armazenamento de dados real consumido nessa equação, só considerando o espaço usado para armazenar informações de coerência. 7.8.2 [15] <7.3> Se cada entrada de diretório mantiver um byte de informação para cada CPU, se nosso sistema CC-NUMA tiver um total de S blocos de memória e o sistema tiver T nós, elabore uma equação que expresse a quantidade de memória que estará presente em cada diretório.
Exercício 7.9 Considerando o sistema CC-NUMA descrito no Exercício 7.8, suponha que o sistema tenha quatro nós, cada um com uma CPU de único core (cada CPU tem sua própria cache de dados L1 e cache de dados L2). A cache de dados L1 é store-through, embora a cache de dados L2 seja write-back. Suponha que o sistema tenha uma carga de trabalho na qual uma CPU escreve em um endereço e todas as outras CPUs leiam os dados que são escritos. Suponha também que o endereço em que os dados são escritos esteja inicialmente apenas na memória, e não em qualquer cache local. Além disso, depois da escrita, suponha que o bloco atualizado só esteja presente nas caches L1 do core para formar a escrita. 7.9.1 [10] <7.3> Para um sistema que mantém coerência usando status de bloco baseado em cache, descreva o tráfego entre nós que será gerado à medida que cada um dos quatro cores escreve em um endereço exclusivo, após o qual cada endereço escrito é lido por cada um dos três cores restantes. 7.9.2 [10] <7.3> Para um mecanismo de coerência baseado em diretório, descreva o tráfego entre nós gerado quando o mesmo padrão de código é executado. 7.9.3 [20] <7.3> Repita os Exercícios 7.9.1 e 7.9.2 supondo que cada CPU agora seja uma CPU multicore, com quatro cores por CPU, cada uma mantendo uma cache de dados L1, mas provido de uma cache de dados L2 compartilhada pelos quatro cores. Cada core realizará a escrita, seguida por leituras por cada um dos 15 outros cores.
7.15 Exercícios 561
7.9.4 [10] <7.3> Considere o sistema descrito no Exercício 7.9.3, agora assumindo que cada core escreve em dois bytes diferentes armazenados no mesmo bloco de cache. Como isso afeta o tráfego do barramento? Explique.
Exercício 7.10 Em um sistema CC-NUMA, o custo de acessar a memória não local pode limitar nossa capacidade de utilizar o multiprocessamento com eficiência. A tabela a seguir mostra os custos associados aos dados de acesso na memória local versus memória não local e a localidade da nossa aplicação expressa como a proporção de acesso que é local. Load/store local (ciclo)
Load/store não local (ciclos)
% acessos locais
25
200
20
Responda às perguntas a seguir supondo que os acessos à memória sejam distribuídos uniformemente pela aplicação. Além disso, suponha que somente uma única operação da memória possa estar ativa durante qualquer ciclo. Indique todas as suposições sobre a ordenação das operações de memória local versus não local. 7.10.1 [10] <7.3> Se, na média, precisamos acessar a memória uma vez a cada 75 ciclos, qual é o impacto sobre nossa aplicação? 7.10.2 [10] <7.3> Se, na média, precisamos acessar a memória uma vez a cada 50 ciclos, qual é o impacto sobre nossa aplicação? 7.10.3 [10] <7.3> Se, na média, precisamos acessar a memória uma vez a cada 100 ciclos, qual é o impacto sobre nossa aplicação?
Exercício 7.11 O problema do jantar dos filósofos é um problema clássico de sincronização e concorrência. O problema geral é enunciado como filósofos sentados em volta de uma mesa redonda fazendo uma de duas coisas: comendo ou pensando. Quando eles estão comendo, não estão pensando, e quando estão pensando, não estão comendo. Há uma tigela de macarrão no centro. Um garfo é colocado entre cada filósofo. O resultado é que cada filósofo tem um garfo à sua esquerda e um garfo à sua direita. Devido à forma como se come macarrão, o filósofo precisa de dois garfos para comer, e só pode usar os garfos do seu lado esquerdo e direito. Os filósofos não conversam entre si. 7.11.1 [10] <7.4> Descreva o cenário em que nenhum dos filósofos consegue comer (ou seja, inanição). Qual é a sequência de eventos que leva a esse problema? 7.11.2 [10] <7.4> Descreva como podemos solucionar esse problema introduzindo o conceito de uma prioridade. Mas podemos garantir que trataremos de todos os filósofos de forma justa? Explique. Agora, suponha que contratemos um garçom encarregado de atribuir garfos aos filósofos. Ninguém pode pegar um garfo até que o garçom lhe diga que pode. O garçom tem conhecimento global de todos os garfos. Além disso, se impusermos a diretriz de que os filósofos sempre solicitarão para apanhar seu garfo da esquerda antes de apanhar seu garfo da direita, então podemos garantir que o impasse (deadlock) será evitado. 7.11.3 [10] <7.4> Podemos implementar as solicitações ao garçom como uma fila de solicitações ou como uma retentativa periódica de uma solicitação. Com uma fila, as solicitações são tratadas na ordem em que são recebidas. O problema com o uso da
562
Capítulo 7 Multicores, multiprocessadores e clusters
fila é que podemos nem sempre ser capazes de atender ao filósofo cuja solicitação está no início da fila (devido à indisponibilidade de recursos). Descreva um cenário com cinco filósofos, em que uma fila é fornecida, mas o serviço não é concedido mesmo que haja garfos disponíveis para outro filósofo (cuja solicitação está mais profunda na fila) utilizar. 7.11.4 [10] <7.4> Se implementarmos solicitações ao garçom repetindo periodicamente nossa solicitação até que os recursos estejam disponíveis, isso solucionará o problema descrito no Exercício 7.11.3? Explique.
Exercício 7.12 Considere as três organizações de CPU a seguir: CPU SS: Um microprocessador superescalar de dois cores que oferece capacidades de emissão fora de ordem em duas unidades funcionais (FUs). Somente uma única thread pode ser executada em cada core de cada vez. CPU MT: Um processador multithreaded fine-grained que permite que instruções de duas threads sejam executadas simultaneamente (ou seja, existem duas unidades funcionais), embora somente instruções de uma única thread possam ser emitidas em cada ciclo. CPU SMT: Um processador SMT que permite que instruções de duas threads sejam executadas simultaneamente (ou seja, existem duas unidades funcionais), e as instruções de qualquer uma ou ambas as threads podem ser emitidas para executar em qualquer ciclo. Suponha que tenhamos duas threads, X e Y, para executar nessas CPUs, o que inclui as seguintes operações: Thread X
Thread Y
A1 – leva 3 ciclos para executar
B1 – leva 2 ciclos para executar
A2 – sem dependências
B2 – conflitos para uma unidade funcional com B1
A3 – conflitos para uma unidade funcional com A1
B3 – depende do resultado de B2
A4 – depende do resultado de A3
B4 – sem dependências e leva 2 ciclos para executar
Suponha que todas as instruções utilizem um único ciclo para serem executadas, a menos que observado de outra forma ou que encontrem um hazard. 7.12.1 [10]<7.5> Suponha que você tenha uma CPU SS. Quantos ciclos são necessários para executar essas duas threads? Quantos slots de emissão são desperdiçados devido a hazards? 7.12.2 [10]<7.5> Agora, suponha que você tenha uma CPU MT. Quantos ciclos são necessários para executar essas duas threads? Quantos slots de emissão são desperdiçados devido a hazards? 7.12.3 [10]<7.5> Suponha que você tenha uma CPU SMT. Quantos ciclos são necessários para executar essas duas threads? Quantos slots de emissão são desperdiçados devido a hazards?
Exercício 7.13 O software de virtualização está sendo agressivamente implantado para reduzir os custos de gerenciamento dos servidores de alto desempenho de hoje. Empresas como VMWare,
7.15 Exercícios 563
Microsoft e IBM desenvolveram diversos produtos de virtualização. O conceito geral, descrito no Capítulo 5, é que uma camada hipervisora pode ser introduzida entre o hardware e o sistema operacional para permitir que vários sistemas operacionais compartilhem o mesmo hardware físico. A camada hipervisora é então responsável por alocar recursos de CPU e memória, além de tratar os serviços normalmente tratados pelo sistema operacional (por exemplo, E/S). A virtualização oferece uma visão abstrata do hardware subjacente ao sistema operacional host e ao software de aplicação. Isso exigirá que repensemos como os sistemas multicore e multiprocessador serão projetados no futuro para dar suporte ao compartilhamento das CPUs e memórias por diversos sistemas operacionais simultaneamente. 7.13.1 [30] <7.5> Selecione dois hipervisores no mercado hoje e compare como eles virtualizam e gerenciam o hardware subjacente (CPUs e memória). 7.13.2 [15] <7.5> Discuta quais mudanças podem ser necessárias nas plataformas de CPU multicore do futuro a fim de que sejam mais coerentes com as demandas de recursos impostas sobre esses sistemas. Por exemplo, o multithreading pode desempenhar um papel eficaz para reduzir a competição por recursos de computação?
Exercício 7.14 Gostaríamos de executar o loop a seguir da forma mais eficiente possível. Temos duas máquinas diferentes, uma máquina MIMD e uma máquina SIMD.
7.14.1 [10] <7.6> Para uma máquina MIMD com quatro CPUs, mostre a sequência de instruções MIPS que você executaria em cada CPU. Qual é o speed-up para essa máquina MIMD? 7.14.2 [20] <7.6> Para uma máquina SIMD de largura 8 (ou seja, oito unidades funcionais SIMD paralelas), escreva um programa em assembly usando suas próprias extensões SIMD para o MIPS para executar o loop. Compare o número de instruções executadas na máquina SIMD com a máquina MIMD.
Exercício 7.15 Um array sistólico é um exemplo de uma máquina MISD. Um array sistólico é uma rede de pipeline ou “wavefront” de elementos de processamento de dados. Cada um desses elementos não precisa de um contador de programa, pois a execução é disparada pela chegada de dados. Os arrays sistólicos com clock são computados em “lock-step”, com cada processador realizando fases alternadas de computação e comunicação. 7.15.1 [10] <7.6> Considere as implementações propostas de um array sistólico (você pode encontrá-las na internet ou em publicações técnicas). Depois, tente programar o loop fornecido no Exercício 7.14 usando esse modelo MISD. Discuta quaisquer dificuldades que você encontrar. 7.15.2 [10] <7.6> Discuta as semelhanças e diferenças entre uma máquina MISD e SIMD. Responda a essa pergunta em termos de paralelismo em nível de dados.
564
Capítulo 7 Multicores, multiprocessadores e clusters
Exercício 7.16 Suponha que queiramos executar o loop DAXP mostrado na Seção 7.6 em assembly MIPS na GPU NVIDIA 8800 GTX descrita neste capítulo. Nesse problema, vamos supor que todas as operações matemáticas sejam realizadas em números de ponto flutuante com precisão simples (vamos mudar o nome do loop para SAXP). Suponha que as instruções utilizem o seguinte número de ciclos para serem executadas. Loads
Stores
Add.S
Mult.S
5
2
3
4
7.16.1 [20] <7.7> Descreva como você construirá warps para o loop SAXP explorar os oito cores fornecidos em um único multiprocessador.
Exercício 7.17 Faça o download do CUDA Toolkit e SDK em www.nvidia.com/object/cuda_get.html. Lembre-se de usar a versão “emurelês” (Emulation Mode) do código (você não precisará do hardware NVIDIA real para esse trabalho). Crie os programas de exemplo fornecidos no SDK e confirme se eles rodarão no emulador. 7.17.1 [90] <7.7> Usando o “template” de exemplo de SDK como ponto de partida, escreva um programa CUDA para realizar o seguinte vetor de operações: 1. a - b (subtração vetor-vetor) 2. a · b (produto pontual de vetor) O produto pontual de dois vetores a = [a1, a2,…, an] e b = [b1, b2,…, bn] é definido como: n
a ⋅ b = ∑ ai bi = a1 b1 + a2 b2 +…+ an bn i =1
Submeta o código para cada programa que demonstra cada operação e verifica a exatidão dos resultados. 7.17.2 [90] <7.7> Se você tiver hardware GPU disponível, complete uma análise de desempenho do seu programa, examinando o tempo de computação para a GPU e uma versão de CPU do seu programa para uma faixa de tamanhos de vetor. Explique quaisquer resultados que você encontrar.
Exercício 7.18 A AMD recentemente anunciou que estarão integrando uma unidade de processamento gráfica com seus cores x86 em um único pacote, embora com diferentes clocks para cada um dos cores. Este é um exemplo de um sistema multiprocessador heterogêneo que esperamos ver produzido comercialmente no futuro próximo. Um dos principais pontos de projeto será permitir a comunicação de dados rápida entre a CPU e a GPU. Atualmente a comunicação deve ser realizada entre chips de CPU e GPU discretos. Mas isso está mudando na arquitetura Fusion da AMD. Atualmente, o plano é usar múltiplos canais PCI express (pelo menos, 16) para facilitar a intercomunicação. A Intel também está saltando para essa arena com seu chip Larrabee. A Intel está considerando o uso de sua tecnologia de interconexão QuickPath.
7.15 Exercícios 565
7.18.1 [25] <7.7> Compare a largura de banda e latência associadas a essas duas tecnologias de interconexão.
Exercício 7.19 Consulte a Figura 7.9b, que mostra uma topologia de interconexão de cubo n de ordem 3, que interconecta oito nós. Um recurso atraente de uma topologia de rede de interconexão de cubo n é sua capacidade de sustentar links partidos e ainda oferecer conectividade. 7.19.1 [10] <7.8> Desenvolva uma equação que calcule quantos links no cubo n (onde n é a ordem do cubo) podem falhar e ainda podemos garantir que um link não partido existirá para conectar qualquer nó no cubo n. 7.19.2 [10] <7.8> Compare a resiliência a falha do cubo n com uma rede de interconexão totalmente conectada. Desenhe uma comparação da confiabilidade como uma função do número de links que podem falhar para as duas topologias.
Exercício 7.20 O benchmarking é um campo de estudo que envolve identificar cargas de trabalho representativas para rodar em plataformas de computação específicas a fim de poder comparar objetivamente o desempenho de um sistema com outro. Neste exercício, vamos comparar duas classes de benchmarks: o benchmark Whetstone CPU e o pacote de benchmark PARSEC. Selecione um programa do PARSEC. Todos os programas deverão estar disponíveis gratuitamente na internet. Considere a execução de múltiplas cópias do Whetstone contra a execução do benchmark PARSEC em qualquer um dos sistemas descritos na Seção 7.11. 7.20.1 [60] <7.9> O que é inerentemente diferente entre essas duas classes de carga de trabalho quando executadas nesses sistemas multicore? 7.20.2 [60] <7.9, 7.10> Em termos do modelo roofline, que dependência terão os resultados que você obtiver ao executar esses benchmarks na quantidade de compartilhamento e sincronização presente na carga de trabalho utilizada?
Exercício 7.21 Ao realizar cálculos sobre matrizes esparsas, a latência na hierarquia de memória torna-se um fator muito importante. As matrizes esparsas não possuem a localidade espacial no fluxo de dados, normalmente encontrada nas operações de matriz. Como resultado, novas representações de matriz foram propostas. Uma das representações de matriz esparsa mais antigas é o Yale Sparse Matrix Format. Ele armazena uma matriz esparsa inicial m × n, M, em formato de linha usando três arrays unidimensionais. Suponha que R indique o número de entradas diferentes de zero em M; podemos construir um array A de tamanho R que contém todas as entradas diferentes de zero de M (na ordem da esquerda para a direita e de cima para baixo). Também construímos um segundo array IA de tamanho m + 1 (ou seja, uma entrada por linha, mais um). IA(i) contém o índice de A do primeiro elemento diferente de zero da linha i. A linha i da matriz original se estende de A(IA(i)) até A(IA(i + 1) − 1). O terceiro array, JA, contém o índice de coluna de cada elemento de A, de modo que também tem tamanho R. 7.21.1 [15] <7.9> Considere a matriz esparsa X a seguir e escreva o código C que armazenaria esse código no Yale Sparse Matrix Format.
566
Capítulo 7 Multicores, multiprocessadores e clusters
Linha 1[1,2,0,0,0,0] Linha 2 [0,0,1,1,0,0] Linha 3 [0,0,0,0,9,0] Linha 4 [0,0, 3, 3,0,7] Linha 5 [1, 3,0,0,0,1]
7.21.2 [10] <7.9> Em termos do espaço de armazenamento, supondo que cada elemento na matriz X tenha formato de ponto flutuante com precisão simples, calcule a quantidade de armazenamento usada para armazenar a matriz acima no Yale Sparse Matrix Format. 7.21.3 [15] <7.9> Realize a multiplicação de matriz da Matriz X pela Matriz Y mostrada a seguir. [2,
4,
1,
99,
7,
2]
Coloque esse cálculo em um loop e meça o tempo de sua execução. Não se esqueça de aumentar o número de vezes que esse loop é executado para obter uma boa resolução na sua medição do tempo. Compare o tempo de execução do uso de uma representação simples da matriz e do Yale Sparse Matrix Format. 7.21.4 [15] <7.9> Você consegue achar uma representação de matriz esparsa mais eficiente (em termos de overhead de espaço e computacional)?
Exercício 7.22 Nos sistemas do futuro, esperamos ver plataformas de computação heterogêneas construídas a partir de CPUs heterogêneas. Começamos a ver algumas aparecendo no mercado de processamento embutido nos sistemas que contêm DSPs de ponto flutuante e CPUs de microcontrolador em um pacote de módulo multichip. Suponha que você tenha três classes de CPU: CPU A — Uma CPU multicore de velocidade moderada (com uma unidade de ponto flutuante) que pode executar múltiplas instruções por ciclo. CPU B — Uma CPU inteira de único core rápida (ou seja, sem unidade de ponto flutuante) que pode executar uma única instrução por ciclo. CPU C — Uma CPU de vetor lenta (com capacidade de ponto flutuante) que pode executar múltiplas cópias da mesma instrução por ciclo. Suponha que nossos processadores executem nas seguintes frequências: CPU A
CPU B
CPU C
1 GHz
3 GHz
250 MHz
A CPU A pode executar 2 instruções por ciclo, a CPU B pode executar 1 instrução por ciclo, e a CPU C pode executar 8 instruções (embora a mesma instrução) por ciclo. Suponha que todas as operações possam concluir sua execução em um único ciclo de latência sem quaisquer hazards. Todas as três CPUs possuem a capacidade de realizar aritmética com inteiros, embora a CPU B não possa realizar aritmética de ponto flutuante diretamente. As CPUs A e B têm um conjunto de instruções semelhante a um processador MIPS. A CPU C só pode realizar operações de soma e subtração de ponto flutuante, assim como loads e stores de
7.15 Exercícios 567
memória. Suponha que todas as CPUs tenham acesso à memória compartilhada e que a sincronização tenha custo zero. A tarefa em mãos é comparar duas matrizes X e Y, que contêm cada uma 1024 × 1024 elementos de ponto flutuante. A saída deverá ser uma contagem dos índices numéricos em que o valor em X foi maior que o valor em Y. 7.22.1 [10] <7.11> Descreva como você particionaria o problema nas três CPUs diferentes para obter o melhor desempenho. 7.22.2 [10] <7.11> Que tipo de instrução você acrescentaria à CPU de vetor C para obter o melhor desempenho?
Exercício 7.23 Suponha que um sistema de computador quad-core possa processar transações de banco de dados em uma taxa de estado constante de solicitações por segundo. Suponha também que cada instrução leve, na média, uma quantidade de tempo fixa para ser processada. A tabela a seguir mostra pares de latência de transação e taxa de processamento. Latência de transação média
Taxa de processamento de transação máxima
1 ms
5000/seg
2 ms
5000/seg
1 ms
10.000/seg
2 ms
10.000/seg
Para cada um dos pares na tabela, responda às seguintes perguntas: 7.23.1 [10] <7.11> Na média, quantas solicitações estão sendo processadas em determinado instante? 7.23.2 [10] <7.11> Se você passasse para um sistema de 8 cores, na forma ideal, o que aconteceria com a vazão do sistema (ou seja, quantas transações/segundo o computador processará)? 7.23.3 [10] <7.11> Discuta por que raramente obtemos esse tipo de speed-up simplesmente aumentando o número de cores. §7.1: Falso. O paralelismo em nível de tarefa pode ajudar as aplicações sequenciais e as aplicações sequenciais podem ser criadas para executar em hardware paralelo, embora isso seja mais difícil. §7.2: Falso. A expansão fraca pode compensar uma parte serial do programa que, de outra forma, limitaria a expansão. §7.3: Falso. Como o endereço compartilhado é um endereço físico, múltiplas tarefas em seus próprios espaços de endereço virtual podem executar bem em um multiprocessador de memória compartilhada. §7.4: 1. Falso. Enviar e receber uma mensagem é uma sincronização implícita, além de um modo de compartilhar dados. 2. Verdadeiro. §7.5: 1. Verdadeiro. 2. Verdadeiro. §7.6: Verdadeiro. §7.7: Falso. DIMMs de DRAM gráfica são apreciadas por sua largura de banda mais alta. §7.9: Verdadeiro. Provavelmente precisamos de inovação em todos os níveis da pilha de hardware e software para vencer a aposta da indústria na computação paralela.
Respostas das Seções “Verifique você mesmo”
A A
P
Ê
N
D
I
Imaginação é mais importante que conhecimento. Albert Einstein, On Science, 1930s
C
E
Gráficos e GPUs de computação John Nickolls Diretor de Arquitetura, NVIDIA David Kirk Cientista Chefe, NVIDIA
A.1 Introdução 569 A.2
Arquiteturas de sistemas GPU 572
A.3
Programando GPUs 576
A.4
Arquitetura de multiprocessador multithreaded 587
A.5
Sistema de memória paralela 597
A.6
Aritmética de ponto flutuante 601
A.7
Vida real: o NVIDIA GeForce 8800 605
A.8
Vida real: mapeando aplicações a GPUs 612
A.9
Falácias e armadilhas 626
A.10
Comentários finais 629
A.11
Perspectiva histórica e leitura adicional 629
A.1 Introdução Este apêndice focaliza a GPU — a onipresente unidade de processamento gráfico em cada PC, laptop, computador desktop e estação de trabalho. Em sua forma mais básica, a GPU gera gráficos 2D e 3D, imagens e vídeo que habilitam sistemas operacionais baseados em janelas, interfaces gráficas com o usuário, jogos de vídeo, aplicações de representação visual e vídeo. A GPU moderna que descrevemos aqui é um multiprocessador altamente paralelo, altamente multithreaded, otimizado para computação visual. Para oferecer interação visual em tempo real com objetos calculados via gráficos, imagens e vídeo, a GPU tem uma arquitetura gráfica e de computação unificada, que serve como um processador gráfico programável e uma plataforma de computação paralela escalável. PCs e consoles de jogo combinam uma GPU com uma CPU para formar sistemas heterogêneos.
Breve histórico da evolução da GPU Há quinze anos, não havia algo do tipo GPU. Os gráficos em um PC eram realizados por um controlador VGA (Video Graphics Array). Um controlador VGA era simplesmente um controlador de memória e gerador de vídeo conectado a alguma DRAM. Na década de 1990, a tecnologia de semicondutor avançou suficientemente para que mais funções pudessem ser acrescentadas ao controlador VGA. Em 1997, os controladores VGA estavam começando a incorporar algumas funções de aceleração tridimensional (3D), incluindo hardware para configuração e rasterização de triângulos (cortar triângulos em pixels individuais) e mapeamento e sombreamento de textura (aplicar “decalques” ou padrões aos pixels e misturar cores). Em 2000, o processador gráfico de único chip incorporou quase todos os detalhes do pipeline gráfico da estação de trabalho de alto nível tradicional e, portanto, mereceu um novo nome além de controlador VGA. O termo GPU foi usado para indicar que o dispositivo gráfico tornou-se um processador. Com o tempo, as GPUs tornaram-se mais programáveis, pois os processadores programáveis substituíram a lógica dedicada de função fixa enquanto mantinham a organização básica do pipeline gráfico 3D. Além disso, os cálculos se tornaram mais precisos com o tempo, progredindo da aritmética indexada até inteiros e ponto flutuante, até ponto flutuante de precisão simples, e recentemente para ponto flutuante de precisão dupla.
unidade de processamento gráfico (GPU) Um processador otimizado para gráficos 2D e 3D, vídeo, computação visual e exibição.
computação visual Uma mistura de processamento gráfico e computação, que lhe permite interagir visualmente com os objetos computados através de gráficos, imagens e vídeo.
sistema heterogêneo Um sistema combinando diferentes tipos de processador. Um PC é um sistema heterogêneo CPU-GPU.
A-570
Apêndice A Gráficos e GPUs de computação
GPUs tornaram-se processadores programáveis maciçamente paralelos, com centenas de cores e milhares de threads. Recentemente, instruções de processador e hardware de memória foram acrescentadas para dar suporte a linguagens de programação de uso geral, e um ambiente de programação foi criado no sentido de permitir que as GPUs fossem programadas usando linguagens conhecidas, como C e C + +. Essa inovação torna a GPU um processador totalmente de uso geral, programável e de muitos cores, apesar de ainda ter alguns benefícios e limitações especiais. Tendências gráficas da GPU
interface de programação de aplicação (API) Um conjunto de definições de função e estrutura de dados que fornece uma interface para uma biblioteca de funções.
GPUs e seus drivers associados implementam os modelos OpenGL e DirectX do processamento gráfico. OpenGL é um padrão aberto para a programação gráfica 3D, disponível para a maioria dos computadores. DirectX é uma série de interfaces de programação de multimídia da Microsoft, incluindo Direct3D para gráficos 3D. Como essas interfaces de programação de aplicação (APIs) possuem comportamento bem definido, é possível criar aceleração de hardware eficaz das funções de processamento gráfico definidas pelas APIs. Esse é um dos motivos (além de aumentar a densidade do dispositivo) para que novas GPUs estejam sendo desenvolvidas a cada 12 a 18 meses, o que dobra o desempenho da geração anterior nas aplicações existentes. A duplicação frequente do desempenho da GPU possibilita novas aplicações, que não eram possíveis anteriormente. A interseção de processamento gráfico e computação paralela convida um novo paradigma para gráficos, conhecido como computação visual. Ela substitui grandes seções do modelo tradicional de pipeline gráfico de hardware sequencial por elementos programáveis para programas de geometria, vértice e pixel. A computação visual em uma GPU moderna combina o processamento gráfico e a computação paralela em novas maneiras, que permitem que novos algoritmos gráficos sejam implementados, uma porta aberta para aplicações de processamento paralelo inteiramente novas sobre GPUs predominantes de alto desempenho.
Sistema heterogêneo Embora a GPU seja discutivelmente o processador mais paralelo e mais poderoso em um PC típico, certamente não é o único. A CPU, agora multicore, é um processador complementar, antes de tudo serial, para a GPU manycore maciçamente paralela. Juntos, esses dois tipos de processadores compreendem um sistema multiprocessador heterogêneo. O melhor desempenho para muitas aplicações vem do uso da CPU e da GPU. Este apêndice o ajudará a entender como e quando dividir melhor o trabalho entre esses dois processadores cada vez mais paralelos.
GPU evolui para processador paralelo escalável GPUs evoluíram funcionalmente de controladores VGA fisicamente conectados, de capacidade limitada, para processadores paralelos programáveis. Essa evolução prosseguiu alterando o pipeline gráfico lógico (baseado em API) para incorporar elementos programáveis e também tornando os estágios de pipeline de hardware básicos menos especializados e mais programáveis. Por fim, fez sentido mesclar diferentes elementos de pipeline programáveis em um array unificado de muitos processadores programáveis. Na geração de GPUs GeForce 8-series, o processamento de geometria, vértice e pixel é executado no mesmo tipo de processador. Essa unificação permite uma escalabilidade incrível. Mais cores de processador programável aumentam a vazão total do sistema. Unificar os processadores também oferece balanceamento de carga bastante eficaz, pois qualquer função de processamento pode usar o array de processadores inteiro. Na outra ponta do espectro, um array de processadores agora pode ser montado com muito poucos processadores, pois todas as funções podem ser executadas nos mesmos processadores.
A.1 Introdução A-571
Por que CUDA e computação GPU? Esse array de processadores uniforme e escalável convida a um novo modelo de programação para a GPU. A grande quantidade de poder de processamento de ponto flutuante no array de processadores GPU é muito atraente para solucionar problemas não gráficos. Dado o grande grau de paralelismo e a faixa de escalabilidade do array de processadores para aplicações gráficas, o modelo de programação para a computação mais geral deverá expressar diretamente o forte paralelismo, mas levando em conta uma execução escalável. Computação GPU é o termo criado para usar a GPU para computação através de uma linguagem de programação paralela e API, sem usar a API gráfica tradicional e o modelo de pipeline de gráfico. Isso é contrário à técnica mais antiga da General Purpose computation on GPU (GPGPU), que envolve programar a GPU usando uma API gráfica e pipeline gráfico para realizar tarefas não gráficas. Compute Unified Device Architecture (CUDA) é um modelo de programação paralela escalável e plataforma de software para a GPU e outros processadores paralelos, que permite que o programador evite a API gráfica e interfaces gráficas da GPU e simplesmente programe em C ou C + +. O modelo de programação CUDA tem um estilo de software SPMD (Single-Program Multiple Data – único programa, múltiplos dados), no qual um programador escreve um programa para uma thread que é instanciada e executada por muitas threads em paralelo nos múltiplos processadores da GPU. De fato, CUDA também oferece uma facilidade para programar também múltiplos cores de CPU, de modo que CUDA é um ambiente para escrever programas para todo o sistema de computador heterogêneo.
GPU unifica gráficos e computação Com o acréscimo de CUDA e computação GPU às capacidades da GPU, agora é possível usar a GPU como um processador gráfico e um processador de computação ao mesmo tempo, e combinar esses usos nas aplicações de computação visual. A arquitetura do processador de suporte da GPU é exposta de duas maneiras: primeiro, implementando as APIs gráficas programáveis, e segundo, como um array de processadores altamente paralelos e programáveis em C/C + + com CUDA. Embora os processadores de suporte da GPU sejam unificados, não é necessário que todos os programas de thread SPMD sejam iguais. A GPU pode executar programas de sombreamento gráfico para o aspecto gráfico da GPU, para processar geometria, vértices e pixels, e também executar programas de thread em CUDA. A GPU é, na realidade, uma arquitetura de multiprocessador versátil, com suporte a uma série de tarefas de processamento. As GPUs são excelentes em gráficos e computação visual, pois foram projetadas especificamente para essas aplicações. As GPUs também são excelentes em muitas aplicações de vazão de uso geral, que são “primos de primeiro grau” dos gráficos, pois realizam muito trabalho paralelo, além de ter muita estrutura de problema regular. Em geral, elas são uma boa combinação para os problemas paralelos de dados (veja Capítulo 7), em especial os grandes, mas nem tanto para problemas menores e menos regulares.
Aplicações de computação visual da GPU A computação visual inclui os tipos tradicionais de aplicações gráficas além de muitas aplicações novas. O escopo original de uma GPU era “tudo com pixels”, mas agora ele inclui muitos problemas sem pixels, mas com computação regular e/ou estrutura de dados. As GPUs são eficazes em gráficos 2D e 3D, pois essa é a finalidade para a qual elas foram projetadas. Deixar de oferecer esse desempenho da aplicação seria fatal. Gráficos 2D e 3D utilizam a GPU em seu “modo gráfico”, acessando o poder de processamento da GPU através das APIs gráficas, OpenGLTM e DirectXTM. Jogos são construídos sobre a capacidade de processamento de gráficos 3D.
Computação GPU Usar uma GPU para computação através de uma linguagem de programação paralela e API.
GPGPU Usar uma GPU para computação de uso geral através de uma API gráfica tradicional e pipeline gráfico. CUDA Um modelo de programação paralelo escalável e linguagem baseada em C/C + +. Essa é uma plataforma de programação paralela para GPUs e CPUs multicore.
A-572
Apêndice A Gráficos e GPUs de computação
Além dos gráficos 2D e 3D, o processamento de imagens e vídeo são aplicações importantes para as GPUs. Estes podem ser implementados usando as APIs gráficas ou como programas computacionais, usando CUDA para programar a GPU no modo de computação. Usando CUDA, o processamento de imagens é simplesmente outro programa de array paralelo de dados. Na medida em que o acesso aos dados for regular e existir boa localidade, o programa será eficiente. Na prática, o processamento de imagem é uma aplicação muito boa para GPUs. O processamento de vídeo, especialmente a codificação e a decodificação (compactação e descompactação de acordo com alguns algoritmos padrão) é muito eficiente. A maior oportunidade para aplicações de computação visual sobre GPUs é “romper o pipeline gráfico”. As primeiras GPUs implementavam apenas APIs gráficas específicas, embora com um desempenho muito alto. Isso era maravilhoso se a API aceitasse as operações que você queria fazer. Se não, a GPU não poderia acelerar sua tarefa, pois a funcionalidade inicial da GPU era imutável. Agora, com o advento da computação da GPU e CUDA, essas GPUs podem ser programadas para implementar um pipeline virtual diferente, simplesmente escrevendo-se um programa CUDA para descrever a computação e o fluxo de dados que se deseja. Assim, todas as aplicações agora são possíveis, o que estimulará novas técnicas de computação visual.
A.2 Arquiteturas de sistemas GPU Nesta seção, analisamos as arquiteturas de sistemas GPU em uso comum hoje em dia. Discutimos as configurações do sistema, funções e serviços da GPU, interfaces de programação padrão e uma arquitetura interna básica da GPU.
Arquitetura heterogênea de sistema CPU-GPU Uma arquitetura heterogênea de sistema de computação usando uma GPU e uma CPU pode ser descrita em um nível alto por duas características principais: primeiro, quantos subsistemas funcionais e/ou chips são usados e quais são suas tecnologias de interconexão e topologia, e segundo, quais subsistemas de memória estão disponíveis a esses subsistemas funcionais. Veja no Capítulo 6 uma base sobre os sistemas de E/S e chip sets do PC. O PC histórico (por volta de 1990)
PCI-Express (PCIe) Uma interconexão de E/S padrão do sistema, que usa links ponto a ponto. Os links têm um número configurável de pistas e largura de banda.
A Figura A.2.1 é um diagrama em blocos de alto nível de um PC legado, por volta de 1990. A bridge norte (veja Capítulo 6) contém interfaces com alta largura de banda, conectando a CPU, memória e barramento PCI. A bridge sul contém interfaces e dispositivos legados: barramento ISA (áudio, LAN), controladora de interrupção; controladora de DMA; hora/ contador. Nesse sistema, a exibição foi controlada por um subsistema de framebuffer simples, conhecido como VGA (Video Graphics Array), que foi conectado ao barramento PCI. Os subsistemas gráficos com elementos de processamento embutidos (GPUs) não existiam no panorama do PC de 1990. A Figura A.2.2 ilustra duas configurações comuns em uso atualmente. Estas são caracterizadas por uma GPU (GPU discreta) e CPU separadas, com respectivos subsistemas de memória. Na Figura A.2.2a, com uma CPU da Intel, vemos a GPU conectada por meio de um link PCI-Express 2.0 de 16 pistas, para oferecer uma taxa de transferência máxima de 16GB/s (máximo de 8GB/s em cada direção). De modo semelhante, na Figura A.2.2b, com uma CPU da AMD, a GPU está conectada ao chip set, também por meio de PCI-Express, com a mesma largura de banda disponível. Nos dois casos, as GPUs e CPUs podem acessar a memória um do outro, embora com menos largura de banda disponível que seu acesso às memórias conectadas mais diretamente. No caso do sistema da AMD, a bridge norte ou a controladora de memória é integrada ao mesmo die que a CPU.
A.2 Arquiteturas de sistemas GPU A-573
FIGURA A.2.1 PC histórico. A controladora VGA controla a exibição gráfica da memória do framebuffer.
FIGURA A.2.2 PCs contemporâneos com CPUs Intel e AMD. Veja no Capítulo 6 uma explicação dos componentes e interconexões nesta figura.
A-574
Apêndice A Gráficos e GPUs de computação
Unified Memory Architecture (UMA) Uma arquitetura de
Uma variação de baixo custo desses sistemas, um sistema Unified Memory Architecture (UMA), usa apenas memória do sistema da CPU, omitindo a memória da GPU do sistema. Esses sistemas têm GPUs com desempenho relativamente baixo, pois seu desempenho alcançado é limitado pela largura de banda disponível da memória do sistema e latência aumentada do acesso à memória, enquanto a memória da GPU dedicada oferece alta largura de banda e baixa latência. Uma variação do sistema de alto desempenho utiliza múltiplas GPUs conectadas, normalmente duas a quatro trabalhando em paralelo, com suas telas encadeadas em forma de margarida. Um exemplo é o sistema multi-GPU NVIDIA SLI (Scalable Link Interconnect), projetado para jogos de alto desempenho e estações de trabalho. A próxima categoria de sistema integra a GPU com a bridge norte (Intel) ou chipset (AMD) com e sem memória gráfica dedicada. O Capítulo 5 explica como as caches mantêm coerência em um espaço de endereço compartilhado. Com CPUs e GPUs, existem múltiplos espaços de endereço. As GPUs podem acessar sua própria memória local física e a memória física do sistema da CPU usando endereços virtuais que são traduzidos por uma MMU na GPU. O kernel do sistema operacional gerencia as tabelas de página da GPU. Uma página física do sistema pode ser acessada usando transações PCI-Express coerentes ou não coerentes, determinadas por um atributo na tabela de página da GPU. A CPU pode acessar a memória local da GPU através de um intervalo de endereços (também chamado de abertura) no espaço de endereços PCI-Express.
sistema em que a CPU e a GPU compartilham uma memória de sistema comum.
Consoles de jogos
Sistemas de console, como o Sony PlayStation 3 e o Microsoft Xbox 360 são semelhantes às arquiteturas de sistemas do PC, descritas anteriormente. Os sistemas de console são projetados para serem entregues com desempenho e funcionalidade idênticas por um espaço de tempo que pode durar cinco anos ou mais. Durante esse tempo, um sistema pode ser reimplementado muitas vezes para explorar processos de manufatura com silício mais avançados, e assim oferecer capacidade constante a custos ainda mais baixos. Os sistemas de console não precisam ter seus subsistemas expandidos e atualizados da forma como os sistemas de PC fazem, de modo que os principais barramentos internos do sistema tendem a ser customizados, em vez de padronizados.
Interfaces e drivers de GPU AGP Uma versão estendida do barramento de E/S PCI original, que fornecia até oito vezes a largura de banda do barramento PCI original para um slot de única placa. Sua finalidade principal era conectar os subsistemas gráficos nos sistemas de PC.
Em um PC de hoje, as GPUs são conectadas a uma CPU por meio da PCI-Express. As gerações anteriores usavam AGP. As aplicações gráficas chamam funções de API OpenGL [Segal e Akeley, 2006] ou Direct3D [Microsoft DirectX Specification] que usam a GPU como um coprocessador. As APIs enviam comandos, programas e dados à GPU por meio de um driver de dispositivo gráfico otimizado para a GPU em particular.
Pipeline lógico gráfico O pipeline lógico gráfico é descrito na Seção A.3. A Figura A.2.3 ilustra os principais estágios de processamento, destacando os estágios programáveis importantes (estágios de sombreamento de vértice, geometria e pixel).
Mapeando o pipeline gráfico a processadores de GPU unificados A Figura A.2.4 mostra como o pipeline lógico compreendendo estágios programáveis independentes separados é mapeado em um array físico distribuído de processadores.
FIGURA A.2.3 Pipeline lógica gráfica. Os estágios de sombreamento programável estão sombreados, e os blocos de função fixa aparecem com fundo branco.
A.2 Arquiteturas de sistemas GPU A-575
FIGURA A.2.4 Pipeline lógico mapeado nos processadores físicos. Os estágios de sombreamento programável são executados no array dos processadores unificados, e o fluxo de dados do pipeline gráfico lógico recircula pelos processadores.
Arquitetura unificada básica da GPU As arquiteturas unificadas da GPU são baseadas em um array paralelo de muitos processadores programáveis. Elas unificam o processamento do sombreamento de vértice, geometria e pixel e a computação paralela nos mesmos processadores, diferente das GPUs anteriores, que tinham processadores separados dedicados a cada tipo de processamento. O array de processador programável é altamente integrado com processadores de função fixa para filtragem de textura, rasterização, operações de rastreio, anti-aliasing, compactação, descompactação, exibição, decodificação de vídeo e processamento de vídeo de alta definição. Embora os processadores de função fixa excedam significativamente os processadores programáveis mais genéricos em termos de desempenho absoluto, restringido por um orçamento de área, custo ou potência, vamos focalizar aqui os processadores programáveis. Em comparação com CPUs multicore, GPUs manycore possuem um objetivo de projeto arquitetônico diferente, focalizado na execução de muitas threads paralelas eficientemente em muitos cores de processador. Usando muitos cores mais simples e otimizando para o comportamento paralelo de dados entre os grupos de threads, mais do orçamento de transistores por chip é dedicado à computação, e menos às caches no chip e overhead. Array de processadores
Um array de processador GPU unificado contém muitos cores de processador, normalmente organizados em multiprocessadores multithreaded. A Figura A.2.5 mostra uma GPU com um array de 112 cores de processador streaming (SP), organizados como 14 multiprocessadores streaming (SM) multithreaded. Cada core de SP é altamente multithreaded, controlando 96 threads concorrentes e seu estado no hardware. Os processadores se conectam a quatro partições de DRAM com 64 bits de largura por meio de uma rede de interconexão. Cada SM tem oito cores SP, duas unidades de função especial (SFUs), caches de instrução e constante, uma unidade de instrução multithreaded e uma memória compartilhada. Essa é a arquitetura Tesla básica implementada pelo NVIDIA GeForce 8800. Ele tem uma arquitetura unificada em que os programas gráficos tradicionais para o sombreamento de vértice, geometria e pixel executam nas SMs unificadas e seus cores SP, e programas de cálculo executam nos mesmos processadores. A arquitetura de array do processador é escalável para configurações de GPU menores e maiores, escalando o número de multiprocessadores e o número de partições de memória. A Figura A.2.5 mostra sete clusters de dois SMs compartilhando uma unidade de textura e uma cache L1 de textura. A unidade de textura gera resultados filtrados ao SM dado um conjunto de coordenadas em um mapa de textura. Como as regiões de filtro do suporte normalmente se sobrepõem para solicitações de textura sucessivas, uma pequena cache de textura L1 de streaming é eficaz para reduzir o número de solicitações ao sistema de memória. O array de processadores se conectam com processadores de operação de
A-576
Apêndice A Gráficos e GPUs de computação
FIGURA A.2.5 Arquitetura unificada básica da GPU. GPU de exemplo com 112 cores de processador streaming (SP) organizados em 14 multiprocessadores streaming (SMs); os cores são altamente multithreaded. Ela tem a arquitetura Tesla básica de um NVIDIA GeForce 8800. Os processadores se conectam com quatro partições DRAM com 64 bits de largura por meio de uma rede de interconexão. Cada SM tem oito cores SP, duas unidades de função especial (SFUs), caches de instrução e constante, uma unidade de instrução multithreaded e uma memória compartilhada.
rastreio (ROP), caches de textura L2, memórias DRAM externas e memória do sistema por meio de uma rede de interconexão da largura da GPU. O número de processadores e o número de memórias podem se expandir para projetar sistemas de GPU balanceados para diferentes segmentos de desempenho e mercado.
A.3 Programando GPUs Programar GPUs de multiprocessador é qualitativamente diferente de programar outros multiprocessadores, como CPUs multicore. As GPUs oferecem duas ou três ordens de grandeza de paralelismo de thread e dados que as CPUs, chegando a centenas de cores de processador e dezenas de milhares de threads simultâneos em 2008. As GPUs continuam a aumentar seu paralelismo, dobrando-o aproximadamente a cada 12 a 18 meses, segundo a lei de Moore [1965] de aumento da densidade do circuito integrado e pela melhoria na eficiência arquitetônica. Para transpor a grande faixa de preço e desempenho dos diferentes segmentos de mercado, diferentes produtos de GPU implementam quantidades bastante variadas de processadores e threads. Mesmo assim, os usuários esperam que aplicações de jogos, gráficos, imagens e cálculo funcionem em qualquer GPU, independente de quantas threads paralelas ela executa ou quantos cores de processador paralelos ela tenha, e eles esperam que GPUs mais caras (com mais threads e cores) rodem as aplicações mais rapidamente. Como resultado, os modelos de programação de GPU e os programas de aplicação são projetados para se expandirem transparentemente a uma grande extensão de paralelismo. A força motriz por trás do grande número de threads e cores paralelos em uma GPU é o desempenho gráfico em tempo real – a necessidade de renderizar cenas 3D complexas
A.3 Programando GPUs A-577
com alta resolução em taxas de frames interativas, a pelo menos 60 frames por segundo. De modo correspondente, os modelos de programação escaláveis das linguagens de sombreamento gráfico, como Cg (C para gráficos) e HLSL (High-Level Shading Language), são projetados para explorar grandes graus de paralelismo por meio de muitas threads paralelas independentes e expandir para qualquer número de cores de processador. O modelo de programação paralela escalável CUDA, de modo semelhante, permite que aplicações de computação paralela em geral aproveitem grandes números de threads paralelas e se expandam para qualquer número de cores de processador paralelos, transparentemente à aplicação. Nesses modelos de programação escaláveis, o programador escreve código para uma única thread, e a GPU executa milhares de instâncias de thread em paralelo. Os programas, assim, se expandem transparentemente por uma grande faixa de paralelismo de hardware. Esse paradigma simples surgiu a partir das APIs gráficas e linguagens de sombreamento que descrevem como sombrear um vértice ou um pixel. Ele continuou sendo um paradigma eficaz à medida que as GPUs rapidamente aumentaram seu paralelismo e desempenho desde o final dos anos 90. Esta seção descreve rapidamente as GPUs de programação para aplicações gráficas de tempo real usando APIs gráficas e linguagens de programação. Depois ela descreve as GPUs de programação para aplicações de computação visual e cálculo paralelo em geral usando a linguagem C e o modelo de programação CUDA.
Programando gráficos em tempo real As APIs desempenharam um papel importante no desenvolvimento rápido e bem-sucedido das GPUs e processadores. Existem duas APIs gráficas padrão principais: OpenGL e Direct3D, uma das interfaces de programação de multimídia DirectX da Microsoft. OpenGL, um padrão aberto, foi proposto originalmente e definido pela Silicon Graphics Incorporated. O desenvolvimento e extensão contínua do padrão OpenGL ([Segal e Akeley, 2006], [Kessenich, 2006]) é algo gerenciado pela Khronos, um consórcio do setor. Direct3D [Blythe, 2006], um padrão de fato, é definido e desenvolvido ainda mais pela Microsoft e seus parceiros. OpenGL e Direct3D são estruturados de modo semelhante, e continuam a se desenvolver rapidamente com os avanços do hardware de GPU. Eles definem um pipeline de processamento gráfico lógico que é mapeado no hardware de GPU e processadores, juntamente com modelos de programação e linguagens para os estágios de pipeline programáveis.
OpenGL Uma API gráfica de padrão aberto.
Direct3D Uma API gráfica definida pela Microsoft e seus parceiros.
Pipeline gráfico lógico A Figura A.3.1 ilustra o pipeline gráfico lógico Direct3D 10. OpenGL tem uma estrutura de pipeline gráfico semelhante. A API e pipeline lógico oferecem uma infraestrutura de
FIGURA A.3.1 Pipeline gráfico Direct3D 10. Cada estágio de pipeline gráfico é mapeado no hardware de GPU ou em um processador de GPU. Os estágios de sombreamento programável estão em azul, os blocos de função fixa estão em branco e os objetos da memória estão em cinza. Cada estágio processa um vértice, primitivo geométrico ou pixel em um padrão de fluxo de dados de streaming.
A-578
textura Um array 1D, 2D ou 3D que admite pesquisas amostradas e filtradas com coordenadas interpoladas.
sombreamento Um programa que opera sobre dados gráficos, como um vértice ou um fragmento de pixel.
linguagem de shading Uma linguagem de renderização de gráficos, normalmente com um modelo de programação de fluxo de dados ou streaming.
Apêndice A Gráficos e GPUs de computação
fluxo de dados de streaming e canalização para os estágios de sombreamento programável, mostrados em azul. A aplicação 3D envia à GPU uma sequência de vértices agrupados em primitivos geométricos – pontos, linhas, triângulos e polígonos. O montador de entrada coleta vértices e primitivos. O programa de sombreamento de vértice executa o processamento por cada vértice, incluindo a transformação da posição 3D do vértice em uma posição de tela e acendendo o vértice para determinar sua cor. O programa de sombreamento de geometria executa o processamento por cada primitivo e pode adicionar ou remover primitivos. A unidade de configuração e o rasterizador geram fragmentos de pixel (fragmentos são contribuições em potencial aos pixels) que são cobertos por um primitivo geométrico. O programa de sombreamento de pixel realiza o processamento por fragmento, incluindo a interpolação de parâmetros por fragmento, texturização e coloração. Os sombreamentos de pixel utilizam bastante as pesquisas amostradas e filtradas em grandes arrays 1D, 2D ou 3D, chamadas texturas, usando coordenadas de ponto flutuante interpoladas. Sombreamentos utilizam acessos à textura para mapas, funções, decalques, imagens e dados. O estágio de processamento de operações de rastreio (ou misturador de saída) realiza teste de profundidade do buffer Z e teste de estêncil, que pode descartar um fragmento de pixel oculto ou substituir a profundidade do pixel pela profundidade do fragmento, e realiza uma operação de combinação de cores, que combina a cor do fragmento com a cor do pixel e escreve o pixel com a cor combinada. A API gráfica e o pipeline gráfico oferecem objetos de entrada, saída, memória e infraestrutura para os programas de sombreamento que processam cada fragmento de vértice, primitivo ou pixel.
Programas de sombreamento gráfico Aplicações gráficas de tempo real utilizam muitos programas de sombreamento diferentes para modelar o modo como a luz interage com diferentes materiais e renderizar iluminação e sombras complexas. As linguagens de shading (ou sombreado) são baseadas em um modelo de programação de fluxo de dados ou streaming, que corresponde ao pipeline gráfico lógico. Os programas de sombreamento de vértice mapeiam a posição dos vértices do triângulo na tela, alterando sua posição, cor ou orientação. Normalmente, uma thread do sombreamento de vértice entra com uma posição de vértice em ponto flutuante (x, y, z, w) e calcula uma posição de tela em ponto flutuante (x, y, z). Os programas de sombreamento de geometria operam sobre primitivos geométricos (como linhas e triângulos) definidos por múltiplos vértices, alterando-os ou gerando primitivos adicionais. Os sombreamentos de fragmento de pixel “sombreiam” um pixel cada, calculando uma contribuição de cor vermelho, verde, azul, alfa (RGBA) de ponto flutuante para a imagem renderizada em sua posição de imagem (x, y) da amostra do pixel. Sombreamentos (e GPUs) utilizam aritmética de ponto flutuante para todos os cálculos de cor de pixel, a fim de eliminar artefatos visíveis enquanto calcula a faixa extrema de valores de contribuição de pixel encontrados na renderização de cenas com iluminação complexa, sombras e alta faixa dinâmica. Para todos os três tipos de sombreamentos gráficos, muitas instâncias de programa podem ser executadas em paralelo, como threads paralelas independentes, pois cada uma trabalha sobre dados independentes, produz resultados independentes e não tem efeitos colaterais. Vértices, primitivos e pixels independentes ainda permitem que o mesmo programa gráfico seja executado em GPUs de tamanhos diferentes, que processam diferentes quantidades de vértices, primitivos e pixels em paralelo. Assim, programas gráficos se expandem transparentemente às GPUs com diferentes quantidades de paralelismo e desempenho. Os usuários programam todas as três threads gráficas com uma linguagem de alto nível dirigida comum. HLSL (High-Level Shading Language) e Cg (C para gráficos) normalmente são usadas. Elas possuem sintaxe tipo C e um rico conjunto de funções de biblioteca para operações de matriz, trigonometria, interpolação e acesso e filtragem de textura, mas estão longe de ser linguagens de computação de uso geral: atualmente não possuem acesso à memória geral, ponteiros, E/S de arquivo e recursão. HLSL e Cg consideram que os programas residem dentro de um pipeline gráfico lógico, e, portanto, a E/S é implícita.
A.3 Programando GPUs A-579
Por exemplo, um sombreamento de fragmento de pixel pode esperar que as coordenadas de texto geométrica normal e múltipla tenham sido interpoladas a partir dos valores de vértice por estágios de função fixa anteriores, e pode simplesmente atribuir um valor ao parâmetro de saída COLOR para que seja passado adiante, de modo a ser misturado com um pixel em uma posição (x, y) implícita. O hardware da GPU cria uma nova thread independente para executar um programa de sombreamento de vértice, geometria ou pixel para cada vértice, cada primitivo e cada fragmento de pixel. Nos video games, a maior parte dos threads executa programas de sombreamento de pixel, pois normalmente existem de 10 a 20 vezes ou mais fragmentos de pixel do que vértices, e iluminação e sombras complexas exigem razões ainda maiores entre threads de sombreamento de pixel e vértice. O modelo de programação de sombreamento gráfico impulsionou a arquitetura de GPU a executar de modo eficiente milhares de threads fine-grained independentes em muitos cores paralelos do processador.
Exemplo de sombreamento de pixel Considere o seguinte programa sombreamento de pixel Cg, que implementa a técnica de renderização de “mapeamento de ambiente”. Para cada thread de pixel, esse sombreamento recebe cinco parâmetros, incluindo coordenadas da imagem de textura em ponto flutuante 2D, necessárias para amostrar a cor da superfície, e um vetor de ponto flutuante 3D dando a reflexão da direção da visão a partir da superfície. Os outros três parâmetros “uniformes” não variam de uma instância de pixel (thread) para a seguinte. O sombreamento pesquisa a cor nas duas imagens de textura: um acesso de textura 2D para a cor da superfície e um acesso de textura 3D em um mapa de cubo (seis imagens correspondentes às faces de um cubo) para obter a cor do mundo exterior correspondente à direção de reflexão. Depois, a cor de ponto flutuante final com quatro componentes (vermelho, verde, azul, alfa) é calculada usando uma média ponderada chamada “lerp”, ou função de interpolação linear.
Embora esse programa de sombreamento tenha apenas três linhas, ele ativa muito hardware da GPU. Para cada busca de textura, o subsistema de textura da GPU faz diversos acessos à memória para amostrar cores da imagem nas vizinhanças das coordenadas de amostragem, e depois interpola o resultado final com a aritmética de filtragem em ponto flutuante. A GPU multithreaded executa milhares dessas threads peso leve de sombreamento de pixel Cg em paralelo, intercalando-as profundamente para ocultar a busca de textura e latência de memória.
A-580
Apêndice A Gráficos e GPUs de computação
FIGURA A.3.2 Imagem renderizada pela GPU. Para que a pele tenha profundidade e translucidez visual, o programa de sombreamento de pixel modela três camadas de pele separadas, cada uma com comportamento exclusivo de espalhamento da subsuperfície. Ele executa 1400 instruções para renderizar os componentes de cor vermelho, verde, azul e alfa de cada fragmento de pixel da pele.
Cg focaliza a visão do programador de um único vértice ou primitivo ou pixel, que a GPU implementa como uma única thread; o programa de sombreamento é dimensionado transparentemente para explorar o paralelismo de thread nos processadores disponíveis. Sendo específica da aplicação, Cg oferece um rico conjunto de tipos de dados úteis, funções de biblioteca e construções de linguagem para expressar técnicas de renderização variadas. A Figura A.3.2 mostra a pele renderizada por um sombreamento de pixel de fragmento. A pele real parece muito diferente da tinta de cor da carne, pois a luz salta muito antes de emergir novamente. Nesse sombreamento complexo, três camadas de pele separadas, cada uma com comportamento de espalhamento de subsuperfície exclusivo, são modeladas para oferecer à pele profundidade e translucidez visual. O espalhamento pode ser modelado por uma convolução ofuscante em um espaço de “textura” aplainado, com o vermelho sendo ofuscado mais que o verde, e o azul ofuscado menos. O sombreamento Cg compilado executa 1400 instruções para calcular a cor de um pixel de pele. Como as GPUs desenvolveram um desempenho de ponto flutuante superior e largura de banda de memória streaming muito alta para gráficos em tempo real, elas atraíram aplicações altamente paralelas além dos gráficos tradicionais. Inicialmente, o acesso a esse poder estava disponível apenas formulando uma aplicação como um algoritmo de renderização de gráficos, mas essa técnica GPGPU normalmente era esquisita e limitadora. Mais recentemente, o modelo de programação CUDA forneceu um modo muito mais fácil de explorar a largura de banda escalável de ponto flutuante e memória de alto desempenho das GPUs com a linguagem de programação C.
Programando aplicações de computação paralela CUDA, Brook e CAL são interfaces de programação para GPUs que são focadas na computação paralela dos dados, em vez de gráficos. CAL (Compute Abstraction Layer) é uma interface de linguagem assembler de baixo nível para GPUs da AMD. Brook é uma
A.3 Programando GPUs A-581
linguagem de streaming adaptada para BPUs por Buck et al. [2004]. CUDA, desenvolvida pela NVIDIA [2007], é uma extensão às linguagens C e C + + para a programação paralela escalável de GPUs manycore e CPUs multicore. O modelo de programação CUDA é descrito a seguir, adaptado de um artigo de Nickolls, Buck, Garland e Skadron [2008]. Com o novo modelo, a GPU excede em cálculo paralelo de dados e vazão, executando aplicações de cálculo de alto desempenho além de aplicações gráficas. Decomposição do problema paralelo de dados
Para mapear grandes problemas de cálculo de modo eficaz a uma arquitetura de processamento altamente paralela, o programador ou compilador decompõe o problema em muitos problemas pequenos, que podem ser solucionados em paralelo. Por exemplo, o programador divide um array de dados de resultado grande em blocos e divide ainda mais cada bloco em elementos, de modo que os blocos de resultado possam ser calculados independentemente em paralelo, e os elementos dentro de cada bloco sejam calculados em paralelo. A Figura A.3.3 mostra uma decomposição de um array de dados de resultado em uma grande de blocos 3 × 2, em que cada bloco é decomposto ainda mais em um array 5 × 3 de elementos. A decomposição paralela em dois níveis é mapeada naturalmente para a arquitetura da GPU: multiprocessadores paralelos calculam blocos de resultado, e threads paralelas calculam elementos de resultado. O programador escreve um programa que calcula uma sequência de grades de dados de resultado, dividindo cada grade de resultado em blocos de resultado coarse-grained, que podem ser calculados independentemente em paralelo. O programa calcula cada bloco de resultado com um array de threads paralelas fine-grained, particionando o trabalho entre threads, de modo que cada uma calcule um ou mais elementos de resultado.
Programação paralela escalável com CUDA O modelo de programação paralela escalável CUDA estende as linguagens C e C + + para explorar grandes graus de paralelismo para aplicações gerais em multiprocessadores altamente paralelos, particularmente GPUs. A experiência inicial com CUDA mostra que muitos programas sofisticados podem ser prontamente expressos com algumas
FIGURA A.3.3 Decompondo os dados de resultado em uma grade de blocos de elementos a serem calculados em paralelo.
A-582
Apêndice A Gráficos e GPUs de computação
a bstrações facilmente entendidas. Como a NVIDIA lançou o modelo CUDA em 2007, os desenvolvedores rapidamente criaram programas paralelos escaláveis para uma grande faixa de aplicações, incluindo processamento de dados sísmicos, química computacional, álgebra linear, solucionadores de matriz esparsa, classificação, pesquisa, modelos da física e computação visual. Essas aplicações se expandem transparentemente para centenas de cores de processador e milhares de threads simultâneas. As GPUs NVIDIA com a arquitetura gráfica e de computação unificada Tesla (descrita nas Seções A.4 e A.7) executam programas CUDA em C, e se encontram facilmente em laptops, PCs, estações de trabalho e servidores. O modelo CUDA também se aplica a outras arquiteturas de processamento paralelo de memória compartilhada, incluindo CPUs multicore [Stratton, 2008]. CUDA oferece três abstrações principais – uma hierarquia de grupos de threads, memórias compartilhadas e sincronismo de barreira –, que oferecem uma estrutura paralela clara ao código C convencional para uma thread da hierarquia. Múltiplos níveis de threads, memória e sincronização oferecem paralelismo de dados fine-grained e paralelismo de threads, aninhados dentro do paralelismo de dados coarse-grained e paralelismo de tarefas. As abstrações orientam o programador a dividir o problema em subproblemas grosseiros, que podem ser solucionados independentemente em paralelo, e depois em partes mais minuciosas, que podem ser solucionadas em paralelo. O modelo de programação se expande transparentemente para quantidades maiores de cores de processador: um programa CUDA compilado é executado em qualquer número de processadores, e somente o sistema de runtime precisa saber a quantidade física de processadores.
O paradigma CUDA kernel Um programa ou função para uma thread, projetado para ser executado por muitas threads. bloco de threads Um conjunto de threads simultâneas, que executam o mesmo programa de thread e podem cooperar para calcular um resultado. grade Um conjunto de blocos de threads que executam o mesmo programa do kernel.
CUDA é uma extensão mínima das linguagens de programação C e C + +. O programador escreve um programa serial que chama kernels paralelos, que podem ser funções simples ou programas completos. Um kernel é executado em paralelo a um conjunto de threads paralelas. O programador organiza essas threads em uma hierarquia de blocos de threads e grades de blocos de threads. Um bloco de threads é um conjunto de threads simultâneas que podem cooperar entre si através da sincronização de barreira e através do acesso compartilhado ao espaço de memória privado do bloco. Uma grade é um conjunto de blocos de threads que podem ser executadas independentemente e, portanto, podem ser executadas em paralelo. Ao chamar um kernel, o programador especifica o número de threads por bloco e o número de blocos que compreendem a grade. Cada grade recebe um número de thread ID exclusivo, threadIdx, dentro do seu bloco de threads, numerado com 0, 1, 2, ..., blockDim-1, e cada bloco de threads recebe um número de block ID exclusivo, blockIdx, dentro de sua grade. CUDA admite blocos de threads contendo até 512 threads. Por conveniência, os blocos de threads e grades podem ter uma, duas ou três dimensões, acessadas por meio dos campos de índice .x, .y e .z. Como um exemplo muito simples de programação paralela, suponha que recebamos dois vetores x e y de n números de ponto flutuante cada, e que queiramos calcular o resultado de y = ax + y para algum valor escalar a. Esse é o chamado kernel SAXPY, definido pela bib de álgebra linear BLAS. A Figura A.3.4 mostra o código C para realizar esse cálculo em um processador serial e em paralelo, usando CUDA. O especificador de declaração _global_ indica que o procedimento é um ponto de entrada do kernel. Os programas em CUDA disparam kernels paralelos com a sintaxe estendida de chamada de função: kernel <<< dimGrid, dimBlock >>> (...lista de parâmetros...);
onde dimGrid e dimBlock são vetores de três elementos do tipo dim3, que especificam as dimensões da grade em blocos e as dimensões dos blocos em threads, respectivamente. Dimensões não especificadas são um por default.
A.3 Programando GPUs A-583
FIGURA A.3.4 Código sequencial (em cima) em C versus código paralelo (embaixo) em CUDA para SAXPY (veja Capítulo 7). Threads paralelas CUDA substituem o loop serial C — cada thread calcula o mesmo resultado como uma iteração do loop. O código paralelo calcula n resultados com n threads organizados em blocos de 256 threads.
Na Figura A.3.4, disparamos uma grade de n threads que atribui uma thread a cada elemento dos vetores e coloca 256 threads em cada bloco. Cada thread individual calcula um índice de elemento de sua thread e IDs de bloco, e depois realiza o cálculo desejado nos elementos de vetor correspondentes. Comparando as versões serial e paralelo desse código, vemos que elas são muito semelhantes. Isso representa um padrão bastante comum. O código serial consiste em um loop em que cada iteração é independente de todas as outras. Esses loops podem ser transformados matematicamente em kernels paralelos: cada iteração do loop se torna uma thread independente. Atribuindo uma única thread a cada elemento de saída, evitamos a necessidade de qualquer sincronização entre as threads ao escrever resultados na memória. O texto de um kernel CUDA é simplesmente uma função C para uma thread sequencial. Assim, ele geralmente é fácil de escrever e mais simples do que escrever código paralelo para operações de vetor. O paralelismo é determinado clara e explicitamente especificando-se as dimensões de uma grade e seus blocos de threads ao iniciar um kernel. A execução paralela e o gerenciamento de threads é automático. Toda criação, escalonamento e término de thread é tratado para o programador pelo sistema subjacente. Na verdade, uma GPU com arquitetura Tesla realiza todo o gerenciamento de threads diretamente no hardware. As threads de um bloco são executadas simultaneamente e podem sincronizar em uma barreira de sincronização chamando o _syncthreads () intrínseco. Isso garante que nenhuma thread no bloco possa prosseguir até que todas as threads em um bloco tenham chegado à barreira. Depois de passar pela barreira, essas threads também têm garantias de ver todas as escritas na memória realizadas pelas threads no bloco antes da barreira. Assim, as threads em um bloco podem se comunicar entre si escrevendo e lendo a memória compartilhada por bloco em uma barreira de sincronização. Como as threads em um bloco podem compartilhar memória e sincronizar por meio de barreiras, elas residirão juntas no mesmo processador ou multiprocessador físico. Porém,
barreira de sincronização Threads esperam em uma barreira de sincronização até que todas as threads no bloco de threads cheguem à barreira.
A-584
operação de memória atômica Uma sequência de operações de leitura, modificação e escrita de memória que termina sem qualquer acesso interveniente.
memória local Memória local por thread, privativa à thread. memória compartilhada Memória por bloco, compartilhada por todas as threads do bloco. memória global Memória por aplicação, compartilhada por todas as threads.
Single-Program Multiple Data (SPMD) Um estilo de modelo de programação paralela em que todas as threads executam o mesmo programa. Threads SPMD normalmente são coordenados com sincronização de barreira.
Apêndice A Gráficos e GPUs de computação
o número de blocos de threads pode ser muito superior ao número de processadores. O modelo de programação de threads CUDA virtualiza os processadores e dá ao programador a flexibilidade de paralelizar em qualquer granularidade que seja mais conveniente. A virtualização em threads e blocos de threads permite decomposições intuitivas do problema, pois o número de blocos pode ser ditado pelo tamanho dos dados sendo processados, em vez do número de processadores no sistema. Ele também permite que o mesmo programa CUDA se expanda para números bastante variáveis de cores de processador. Para gerenciar essa virtualização do elemento de processamento e oferecer escalabilidade, o CUDA requer que os blocos de threads possam ser executados independentemente. Deverá ser possível executar os blocos em qualquer ordem, em paralelo ou em série. Diferentes blocos não têm meios de comunicação direta, embora possam coordenar suas atividades usando operações de memória atômicas na memória global visível a todas as threads – incrementando atomicamente os ponteiros de fila, por exemplo. Esse requisito de independência permite que os blocos de threads sejam escalonados em qualquer ordem por qualquer número de cores, tornando o modelo CUDA expansível por um número qualquer de cores, bem como por uma série de arquiteturas paralelas. Ele também ajuda a evitar a possibilidade de deadlock. Uma aplicação pode executar múltiplas grades de forma dependente ou independente. Grades independentes podem ser executadas simultaneamente, dados recursos de hardware suficientes. Grades dependentes são executadas sequencialmente, com uma barreira implícita entre os kernels, garantindo assim que todos os blocos da primeira grade terminem antes do início de qualquer bloco da segunda grade, dependente. As threads podem acessar dados de vários espaços da memória durante sua execução. Cada thread tem uma memória local particular. O modelo CUDA utiliza a memória local para variáveis particulars da thread, que não se encaixam nos registradores da thread, além de frames de pilha e derramamento de registrador. Cada bloco de thread tem uma memória compartilhada, visível a todas as threads do bloco, que tem o mesmo tempo de vida do bloco. Finalmente, todas as threads têm acesso à mesma memória global. Os programas declaram variáveis na memória compartilhada e global com os qualificadores de tipo _ shared_ e _device_ . Em uma GPU na arquitetura Tesla, esses espaços de memória correspondem a memórias fisicamente separadas: a memória compartilhada por bloco é uma RAM no chip com baixa latência, enquanto a memória global reside na DRAM rápida da placa gráfica. A memória compartilha deverá ser uma memória de baixa latência perto de cada processador, semelhante a uma cache L1. Portanto, ela pode oferecer comunicação de alto desempenho e compartilhamento de dados entre as threads de um bloco de threads. Por ter o mesmo tempo de vida do seu bloco de threads correspondente, o código do kernel normalmente inicializará os dados nas variáveis compartilhadas, calculará usando variáveis compartilhadas e copiará os resultados da memória compartilhada para a memória global. Os blocos de threads de grades sequencialmente dependentes se comunicam por meio da memória global, usando-a para ler entrada e escrever resultados. A Figura A.3.5 diagrama os níveis aninhados das threads, blocos de threads e grades dos blocos de threads. Ela mostra ainda os níveis correspondentes de compartilhamento de memória: memórias locais, compartilhadas e globais para o compartilhamento de dados por thread, por bloco de threads e por aplicação. Um programa gerencia o espaço da memória global visível aos kernels por meio de chamadas ao runtime CUDA, como cudaMalloc() e cudaFree(). Os kernels podem ser executados em um dispositivo fisicamente separado, como é o caso quando os kernels são executados na GPU. Consequentemente, a aplicação precisa usar cudaMemcpy() para copiar dados entre o espaço alocado e a memória do sistema host. O modelo de programação CUDA é semelhante em estilo ao conhecido modelo singleprogram multiple data (SPMD) — ele expressa paralelismo explicitamente, e cada kernel executa em um número fixo de threads. Porém, CUDA é mais flexível que a maioria das realizações do modelo SPMD, pois cada chamada do kernel cria dinamicamente uma nova grade com o número correto de blocos de thread e threads para essa etapa da aplicação. O
A.3 Programando GPUs A-585
FIGURA A.3.5 Níveis de granularidade aninhados — thread, bloco de threads e grade — possuem níveis de compartilhamento de memória correspondentes – local, compartilhado e global. A memória local por thread é particular à thread. A memória compartilhada por bloco é compartilhada por todas as threads do bloco. A memória global por aplicação é compartilhada por todas as threads.
programador pode usar um grau de paralelismo conveniente para cada kernel, em vez de ter de projetar todas as fases do cálculo para usar o mesmo número de threads. A Figura A.3.6 mostra um exemplo de uma sequência de código CUDA tipo SPMD. Primeiro, o código instancia o kernelF em uma grade 2D de 3 × 2 blocos, em que cada bloco de threads 2D consiste em 5 × 3 threads. Depois, ele instancia kernelG em uma grade 1D de quatro blocos de threads 1D com seis threads cada. Como kernelG depende dos resultados de kernelF, eles são separados por uma barreira de sincronização entre kernels. As threads simultâneas de um bloco de threads expressam paralelismo de dados finegrained e paralelismo de thread. Os blocos de threads independentes de uma grade expressam paralelismo de dados coarse-grained. Grades independentes expressam o paralelismo de tarefas coarse-grained. Um kernel é simplesmente código C para uma thread da hierarquia.
Restrições Por eficiência, e para simplificar sua implementação, o modelo de programação CUDA tem algumas restrições. Threads e blocos de threads só podem ser criados chamando-se um kernel paralelo, não de dentro de um kernel paralelo. Junto com a independência exigida dos blocos de threads, isso possibilita executar programas CUDA com um scheduler simples, que introduz um overhead mínimo em runtime. De fato, a arquitetura GPU Tesla implementa gerenciamento de hardware e escalonamento de threads e blocos de threads. O paralelismo de tarefa pode ser expresso no nível de bloco de threads, mas é difícil de expressar dentro de um bloco de threads, pois as barreiras de sincronização de thread operam sobre todas as threads do bloco. Para permitir que os programas CUDA sejam executados em qualquer número de processadores, as dependências entre os blocos de threads dentro da mesma grade de kernel não são permitidas — os blocos precisam ser executados independentemente. Como CUDA exige que os blocos de threads sejam independentes e
A-586
Apêndice A Gráficos e GPUs de computação
FIGURA A.3.6 Sequência de kernel F instanciado em uma grade 2D de blocos de threads 2D, uma barreira de sincronização entre kernels, seguida por kernel G em uma grade 1D de blocos de threads 1D.
permite que os blocos sejam executados em qualquer ordem, a combinação dos resultados gerados por múltiplos blocos em geral precisa ser feita iniciando um segundo kernel em uma nova grade de blocos de threads (embora os blocos de threads possam coordenar suas atividades usando operações de memória atômicas na memória global visível a todas as threads — incrementando atomicamente os ponteiros de fila, por exemplo). Chamadas de função recursivas atualmente não são permitidas em kernels CUDA. A recursão é pouco atraente em um kernel maciçamente paralelo, pois oferecer espaço de pilha para as dezenas de milhares de threads que podem estar ativas exigiria quantidades substanciais de memória. Algoritmos seriais que normalmente são expressos usando recursão, como o quicksort, normalmente são implementados melhor usando paralelismo de dados aninhado em vez da recursão explícita. Para dar suporte a uma arquitetura de sistema heterogênea combinando uma CPU e uma GPU, cada uma com seu próprio sistema de memória, os programas CUDA precisam copiar dados e resultados entre a memória do host e a memória do dispositivo. O overhead da interação CPU-GPU e transferências de dados é minimizado usando-se mecanismos de transferência em bloco por DMA e interconexões velozes. Problemas com uso intenso de cálculo, grandes o suficiente para precisar de um aumento de desempenho da GPU, amortizam o overhead melhor do que os problemas pequenos.
A.4 Arquitetura de multiprocessador multithreaded A-587
Implicações para a arquitetura Os modelos de programação paralela para gráficos e computação têm feito com que a arquitetura da GPU seja diferente da arquitetura da CPU. Os principais aspectos dos programas da GPU impulsionando a arquitetura de processador da GPU são: j
Uso extenso do paralelismo de dados fine-grained: Programas de sombreamento descrevem como processar um único pixel ou vértice, e programas CUDA descrevem como calcular um resultado individual.
j
Modelo de programação altamente encadeado: Um programa de thread de sombreamento processa um único pixel ou vértice, e um programa de thread CUDA pode gerar um único resultado. Uma GPU precisa criar e executar milhões desses programas de thread por frame, a 60 frames por segundo.
j
Escalabilidade: Um programa precisa aumentar automaticamente seu desempenho quando receber processadores adicionais, sem recompilação.
j
Cálculo intenso de ponto flutuante (ou inteiro).
j
Suporte para cálculos com alta vazão.
Arquitetura de multiprocessador
A.4 multithreaded
Para lidar com diferentes segmentos do mercado, as GPUs implementam números escaláveis de multiprocessadores — na verdade, as GPUs são multiprocessadores compostos de multiprocessadores. Além do mais, cada multiprocessador é altamente multithreaded para executar muitas threads de sombreamento de vértice e pixel fine-grained de forma eficiente. Uma GPU básica de qualidade tem dois a quatro multiprocessadores, enquanto a GPU ou plataforma de computação de um entusiasta em jogos tem dezenas deles. Esta seção examina a arquitetura de um multiprocessador multithreaded desse tipo, uma versão simplificada do multiprocessador streaming (SM) Tesla da NVIDIA, descrito na Seção A.7. Por que usar um multiprocessador, em vez de vários processadores independentes? O paralelismo dentro de cada multiprocessador oferece alto desempenho localizado e admite multithreading extenso para os modelos de programação paralela fine-grained descritos na Seção A.3. As threads individuais de um bloco de threads são executadas juntas dentro de um multiprocessador para compartilhar dados. O projeto de multiprocessador multithreaded que descrevemos aqui tem oito cores de processador escalares em uma arquitetura altamente acoplada, e executa até 512 threads (o SM descrito na Seção A.7 executa até 768 threads). Por questão de eficiência de espaço e potência, o multiprocessador compartilha unidades grandes e complexas entre oito cores de processador, incluindo a cache de instruções, a unidade de instrução multithreaded e a RAM de memória compartilhada.
Multithreading maciço Processadores GPU são altamente multithreaded para alcançar vários objetivos: j
Cobrir a latência de loads da memória e buscas de textura da DRAM.
j
Admitir modelos de programação de sombreamento gráfico paralelo fine-grained.
j
Admitir modelos de programação de cálculo paralelo fine-grained.
j
Virtualizar os processadores físicos como threads e blocos de threads para oferecer escalabilidade transparente.
j
Simplificar o modelo de programação paralelo para escrever um programa serial para uma thread.
A-588
Apêndice A Gráficos e GPUs de computação
A latência da busca de memória e textura podem exigir centenas de clocks de processador, pois as GPUs normalmente têm pequenos caches streaming em vez de grandes caches do conjunto de trabalho, como as CPUs. Uma solicitação de busca geralmente requer uma latência de acesso completa da DRAM mais a latência de interconexão e buffering. O multithreading ajuda a cobrir a latência com computação útil — enquanto uma thread está esperando que um load ou busca de textura termine, o processador pode executar outra thread. Os modelos de programação paralela fine-grained oferecem literalmente milhares de threads independentes que podem manter muitos processadores ocupados, apesar da longa latência de memória vista pelas threads individuais. Um programa gráfico de sombreamento de vértice ou pixel é um programa para uma única thread que processa um vértice ou um pixel. De modo semelhante, um programa CUDA é um programa em C para uma única thread, que calcula um resultado. Programas gráficos e de computação instanciam muitas threads paralelas para renderizar imagens complexas e calcular grandes arrays de resultado. Para balancear dinamicamente o vértice de deslocamento e cargas de trabalho do thread de sombreamento de pixel, cada multiprocessador executa simultaneamente múltiplos programas de thread diferentes e diferentes tipos de programas de sombreamento. Para dar suporte ao modelo de programação de vértice, primitivo e pixel independentes das linguagens de sombreamento gráfico e o modelo de programação de única thread do CUDA C/C + +, cada thread da GPU tem seus próprios registradores privados, memória por thread particular, contador de programa e estado de execução de thread, e pode executar um caminho de código independente. Para executar de modo eficiente centenas de threads leves simultâneas, o multiprocessador da GPU é multithreaded por hardware — ele controla e executa centenas de threads simultâneas no hardware sem overhead de escalonamento. Threads simultâneas dentro de blocos de threads podem sincronizar em uma barreira com uma única instrução. A criação de threads peso leve, escalonamento de threads com overhead zero, e sincronização de barreira rápida efetivamente dão suporte ao paralelismo bastante fine-grained.
Arquitetura de multiprocessador Um multiprocessador gráfico e de computação unificado executa programas de sombreamento de vértice, geometria e fragmento de pixel, e programas de computação paralelos. Como mostra a Figura A.4.1, o multiprocessador de exemplo consiste em oito cores de processador escalar (SP), cada um com um grande arquivo de registrador (RF) multithreaded, duas unidades de função especial (SFU), uma unidade de instrução multithreaded, uma cache de instrução, uma cache constante apenas de leitura e uma memória compartilhada. A memória compartilhada de 16KB mantém buffers de dados gráficos e dados de computação compartilhados. Variáveis CUDA declaradas como __shared__ residem na memória compartilhada. Para mapear a carga de trabalho da pipeline gráfica lógica através dos múltiplos tempos do multiprocessador, como mostra a Seção A.2, threads de vértice, geometria e pixel possuem buffers de entrada e saída independente, e as cargas de trabalho chegam e saem independentemente da execução da thread. Cada core do SP contém unidades aritméticas escalares de inteiros e ponto flutuante que executam a maioria das instruções. O SP é multithreaded por hardware, aceitando até 64 threads. Cada core de SP em pipeline executa uma instrução escalar por thread por clock, que varia de 1,2GHz a 1,6GHz em diferentes produtos de GPU. Cada core SP tem um arquivo de registrador (RF) grande de 1024 registradores de uso geral de 32 bits, divididos entre suas threads atribuídas. Os programas declaram sua demanda de registrador, normalmente 16 a 64 registradores escalares de 32 bits por thread. O SP pode executar simultaneamente muitas threads que usam alguns registradores ou menos threads que usam mais registradores. O compilador otimiza a alocação do registrador para balancear o custo do spilling de registradores versus o custo de menos threads. Os programas de sombreamento de pixel normalmente utilizam 16 ou menos registradores, permitindo que cada SP execute até 64 threads de sombreamento de pixel para cobrir
A.4 Arquitetura de multiprocessador multithreaded A-589
FIGURA A.4.1 Multiprocessador multithreaded com oito cores de processador escalar (SP). Oito cores SP possuem um grande arquivo de registrador (RF) multithreaded cada, e compartilham uma cache de instrução, unidade de emissão de instrução multithreading, cache constante, duas unidades de função especial (SFUs), rede de interconexão e uma memória compartilhada multibanco.
buscas de textura de longa latência. Programas CUDA compilados normalmente precisam de 32 registradores por thread, limitando cada SP a 32 threads, que limita esse programa de kernel a 256 threads por bloco de threads nesse multiprocessador de exemplo, em vez do seu máximo de 512 threads. As SFUs em pipeline executam instruções de thread que calculam funções especiais e interpolam atributos de pixel dos atributos de vértice primitivos. Essas instruções podem ser executadas simultaneamente com as instruções nos SPs. A SFU é descrita mais adiante. O multiprocessador executa instruções de busca de textura na unidade de textura por meio da interface de textura, e usa a interface de memória para instruções de load e store da memória externa, e instruções atômicas de acesso. Essas instruções podem executar simultaneamente com instruções nos SPs. O acesso à memória compartilhada usa uma rede de interconexão de baixa latência entre os processadores SP e os bancos de memória compartilhada.
Single-Instruction Multiple-Thread (SIMT) Para controlar e executar centenas de threads rodando diversos programas diferentes de modo eficaz, o multiprocessador emprega uma arquitetura Single-Instruction MultipleThread (SIMT). Ele cria, controla, escalona e executa threads simultâneas em grupos de threads paralelas, chamados warps. O termo warp é originado da tecelagem, a primeira tecnologia de thread paralela. A fotografia na Figura A.4.2 mostra um warp de threads paralelas surgindo de um tear. Esse multiprocessador de exemplo utiliza um tamanho de warp SIMT de 32 threads, executando quatro threads em cada um dos oito cores SP por quatro clocks. O multiprocessador Tesla SM descrito na Seção A.7 também usa um tamanho de warp de 32 threads paralelas, executando quatro threads por core SP por eficiência em threads de pixel e threads de cálculo abundantes. Os blocos de threads consistem em um ou mais warps.
Single-Instruction Multiple-Thread (SIMT) Uma arquitetura de processador que aplica uma instrução a múltiplas threads independentes em paralelo.
warp O conjunto de threads paralelas que executam a mesma instrução juntas em uma arquitetura SIMT.
A-590
Apêndice A Gráficos e GPUs de computação
FIGURA A.4.2 Escalonamento de warp multithreaded SIMT. O escalonador seleciona um warp pronto e emite uma instrução sincronamente às threads paralelas compondo o warp. Como os warps são independentes, o escalonador pode selecionar um warp diferente a cada vez.
Esse multiprocessador SIMT de exemplo controla um pool de 16 warps, um total de 512 threads. Threads paralelas individuais compondo um warp são do mesmo tipo e começam juntas no mesmo endereço de programa, mas de outras maneiras são livres para se desviar e executar independentemente. No momento da emissão de cada instrução, a unidade de instrução multithreaded da arquitetura SIMT seleciona um warp que está pronto para executar sua próxima instrução, depois emite essa instrução às threads ativas do warp. Uma instrução SIMT é enviada por broadcast de forma síncrona às threads paralelas ativas de um warp; threads individuais podem ser inativas devido ao desvio ou previsão independente. Nesse multiprocessador, cada core do processador escalar SP executa uma instrução por quatro threads individuais de um warp usando quatro clocks, refletindo a razão 4:1 das threads de warp aos cores. A arquitetura de processador SIMT é semelhante ao projeto Single-Instruction Multiple Data (SIMD), que aplica uma instrução a múltiplas pistas de dados, mas difere porque SIMT aplica uma instrução a múltiplas threads independentes em paralelo, e não apenas a múltiplas pistas de dados. Uma instrução para um processador SIMD controla um vetor de múltiplas pistas de dados juntas, enquanto uma instrução para um processador SIMT controla uma thread individual, e a unidade de instrução SIMT emite uma instrução a um warp de threads paralelas independentes por eficiência. O processador SIMT encontra o paralelismo em nível de dados entre as threads em tempo de execução, semelhante ao modo como um processador superescalar encontra o paralelismo em nível de instrução entre as instruções em tempo de execução. Um processador SIMT observa eficiência e desempenho plenos quando todas as threads de um warp tomam o mesmo caminho de execução. Se as threads de um warp divergirem por meio de um desvio condicional dependente dos dados, a execução serializa para cada caminho de desvio tomado, e quando todos os caminhos terminam, as threads convergem para o mesmo caminho de execução. Para caminhos de mesmo tamanho, um bloco de código if-else divergente é 50% eficiente. O multiprocessador utiliza uma pilha de sincronização de desvio para gerenciar threads independentes que divergem e convergem.
A.4 Arquitetura de multiprocessador multithreaded A-591
Diferentes warps são executadas independentemente em velocidade plena, sem levar em conta se estão executando caminhos de código comuns ou desconexos. Como resultado, GPUs SIMT são muito mais eficientes e flexíveis no código de desvio do que as GPUs anteriores, pois seus warps são muito mais estreitos que a largura SIMD das GPUs anteriores. Em comparação com as arquiteturas de vetor SIMD, SIMT permite que os programadores escrevam código paralelo em nível de thread para threads individuais independentes, bem como código paralelo de dados para muitas threads coordenadas. Para a exatidão do programa, o programador pode basicamente ignorar os atributos de execução SIMT dos warps; porém, melhorias de desempenho substanciais podem ser observadas cuidado-se para que o código raramente exija que as threads em um warp divirjam. Na prática, isso é semelhante ao papel das linhas de cache nos códigos tradicionais: o tamanho da linha de cache pode ser seguramente ignorado quando se projeta por exatidão, mas deve ser considerado na estrutura de código quando se projeta por desempenho de pico.
Execução e divergência de warp SIMT A técnica SIMT de escalonar warps independentes é mais flexível que o escalonamento de arquiteturas GPU anteriores. Um warp compreende threads paralelos do mesmo tipo: vértice, geometria, pixel ou cálculo. A unidade básica de processamento de sombreamento do fragmento de pixel é o quad de pixel 2 por 2, implementado como quatro threads de sombreamento de pixel. O controlador do multiprocessador empacota os quads de pixel em um warp. Um bloco de threads compreende um ou mais warps. O projeto SIMT compartilha a unidade de busca e emissão de instrução eficientemente por threads paralelos de um warp, mas requer um warp completo de threads ativas para obter eficiência de desempenho completa. Esse multiprocessador unificado escalona e executa múltiplos tipos de warp simultaneamente, permitindo que execute warps de vértice e pixel simultaneamente. Seu escalonador de warp opera em menos do que a taxa de clock do processador, pois existem quatro pistas de thread por core de processador. Durante cada ciclo de escalonamento, ele seleciona um warp para executar uma instrução de warp SIMT, como mostra a Figura A.4.2. Uma instrução de warp emitida executa como quatro conjuntos de oito threads por quatro ciclos do processador de vazão. O pipeline do processador utiliza vários clocks de latência para concluir cada instrução. Se o número de warps ativos vezes os clocks por warp ultrapassar a latência do pipeline, o programador pode ignorar a latência do pipeline. Para esse multiprocessador, um schedule round-robin de oito warps tem um período de 32 ciclos entre instruções sucessivas para o mesmo warp. Se o programa puder manter 256 threads ativos por multiprocessador, latências de instrução de até 32 ciclos podem ser escondidas de uma thread sequencial individual. Porém, com poucos warps ativos, a profundidade do pipeline do processador torna-se visível e pode causar stall dos processadores. Um problema de projeto desafiador é implementar o escalonamento de warp com overhead zero para uma mistura dinâmica de diferentes programas de warp e tipos de programa. O escalonador de instruções precisa selecionar um warp a cada quatro clocks para emitir uma instrução por clock por thread, equivalente a um IPC de 1,0 por core de processador. Como os warps são independentes, as únicas dependências estão entre instruções sequenciais do mesmo warp. O escalonador usa um scoreboard de dependência de registrador para qualificar warps cujas threads ativas estão prontas para executar uma instrução. Ele prioriza todos esses warps prontos e seleciona o que possui mais alta prioridade para emissão. A priorização precisa considerar tipo de warp, tipo de instrução e o desejo de ser imparcial para todos os warps ativos.
Gerenciando threads e blocos de threads O controlador do multiprocessador e a unidade de instrução gerenciam threads e blocos de threads. O controlador aceita solicitações de trabalho e dados de entrada, e arbitra o acesso aos recursos compartilhados, incluindo a unidade de textura, caminho de acesso à memória e caminhos de E/S. Para cargas de trabalhos gráficos, ele cria e gerencia três
A-592
array de thread cooperativo (CTA) Um conjunto de threads que executa o mesmo programa de thread e pode cooperar para calcular um resultado. Um CTA de GPU implementa um bloco de threads CUDA.
Apêndice A Gráficos e GPUs de computação
tipos de threads gráficas simultaneamente: vértice, geometria e pixel. Cada um dos tipos de trabalho gráfico possui caminhos independentes de entrada e saída. Ele acumula e empacota cada um desses tipos de trabalho de entrada em warps SIMT de threads paralelas executando o mesmo programa de thread. Ele aloca um warp livre, aloca registradores para as threads de warp e inicia a execução do warp no multiprocessador. Cada programa declara sua demanda de registrador por thread; o controlador inicia um warp somente quando ele pode alocar o contador de registrador solicitado para as threads do warp. Quando todas as threads do warp terminam, o controlador desempacota os resultados e libera os registradores e recursos do warp. O controlador cria arrays de thread cooperativos (CTAs), que implementam blocos de threads CUDA como um ou mais warps de threads paralelos. Ele cria um CTA quando pode criar todos os warps CTA e alocar todos os recursos CTA. Além dos threads e registradores, um CTA requer a alocação de memória compartilhada e barreiras. O programa declara as capacidades exigidas, e o controlador espera até que possa alocar essas quantidades antes de iniciar o CTA. Depois, dele cria warps CTA na taxa de escalonamento de warp, de modo que um programa CTA começa a executar imediatamente no desempenho total do multiprocessador. O controlador monitora quando todas as threads de um CTA saíram, e libera os recursos compartilhados do CTA e seus recursos de warp.
Instruções de thread Os processadores de thread SP executam instruções escalares para threads individuais, diferente das arquiteturas de instrução de vetor da GPU anteriores, que executavam instruções de vetor de quatro componentes para cada programa de sombreamento de vértice ou pixel. Os programas de vértice geralmente calculam vetores de posição (x, y, z, w), enquanto os programas de sombreamento de pixel calculam vetores de cor (vermelho, verde, azul, alfa). Porém, os programas de sombreamento estão se tornando maiores e mais escalares, e é cada vez mais difícil ocupar totalmente até mesmo dois componentes de uma arquitetura de vetor de quatro componentes de GPU legada. Com efeito, a arquitetura SIMT paraleliza por 32 threads de pixel independentes, em vez de paralelizar os quatro componentes de vetor dentro de um pixel. Programas CUDA C/C + + têm código predominantemente escalar por thread. GPUs anteriores empregavam empacotamento de vetor (por exemplo, combinar subvetores de trabalho para ganhar eficiência), mas isso complicava o hardware de escalonamento e também o compilador. Instruções escalares são mais simples e amigáveis a compilador. As instruções de textura continuam sendo baseadas em vetor, tomando um vetor de coordenada de origem e retornando um vetor de cor filtrada. Para dar suporte a múltiplas GPUs com diferentes formatos de microinstrução binária, gráficos de alto nível e compiladores de linguagem de computação geram instruções intermediárias em nível de assembler (por exemplo, instruções de vetor Direct3D ou instruções escalares PTX), que são então otimizadas e traduzidas para microinstruções GPU binárias. A definição do conjunto de instruções PTX (execução paralela de thread) da NVIDIA [2007] oferece uma ISA de destino estável para compiladores, e oferece compatibilidade por várias gerações de GPUs com arquiteturas de microinstrução binária em evolução. O otimizador prontamente expande as instruções de vetor Direct3D para múltiplas microinstruções binárias escalares.. Instruções escalares PTX são traduzidas quase um para um com microinstruções binárias escalares, embora algumas instruções PTX se expandam para múltiplas microinstruções binárias, e múltiplas instruções PTX possam se desdobrar em uma microinstrução binária. Como as instruções intermediárias em nível de assembler utilizam registradores virtuais, o otimizador analisa as dependências de dados e aloca registradores reais. O otimizador elimina o código morto, reúne instruções quando for viável e otimiza pontos de divergência e convergência de desvio SIMT.
Instruction Set Architecture (ISA) A thread ISA descrita aqui é uma versão simplificada da arquitetura Tesla PTX ISA, um conjunto de instruções escalar baseado em registrador compreendendo funções de ponto
A.4 Arquitetura de multiprocessador multithreaded A-593
flutuante, inteiro, lógicas, de conversão, especiais, controle de fluxo, acesso à memória e operações de textura. A Figura A.4.3 lista as instruções de thread PTX GPU básicas; veja detalhes na especificação NVIDIA PTX [2007]. O formato da instrução é: opcode.type d, a, b, c;
onde d é o operando de destino, a, b, c são operandos de origem, e .type é um destes:
FIGURA A.4.3 Instruções básicas de thread da GPU PTX.
A-594
Apêndice A Gráficos e GPUs de computação
Tipo
Especificador .tipo
Bits não tipados 8, 16, 32 e 64 bits
.b8, .b16, .b32, .b64
Inteiro sem sinal 8, 16, 32 e 64 bits
.u8, .u16, .u32, .u64
Inteiro com sinal 8, 16, 32 e 64 bits
.s8, .s16, .s32, .s64
Ponto flutuante 16, 32 e 64 bits
.f16, .f32, .f64
Operandos de origem são valores escalares de 32 bits ou 64 bits nos registradores, um valor imediato ou uma constante; operandos de predicado são valores boolianos de 1 bit. Destinos são registradores, exceto para store na memória. As instruções têm predicados iniciando-as com @p ou @!p, onde p é um registrador de predicado. Instruções de memória e textura transferem escalares ou vetores de dois a quatro componentes, até 128 bits no total. Instruções PTX especificam o comportamento de um thread. As instruções aritméticas PTX operam sobre tipos de ponto flutuante, inteiro com sinal e inteiro sem sinal de 32 e 64 bits. GPUs recentes admitem ponto flutuante de precisão dupla com 64 bits; ver Seção A.6. Nas GPUs atuais, instruções PTX com inteiros de 64 bits e lógicas são traduzidas para duas ou mais microinstruções binárias, que realizam operações de 32 bits. As instruções de função especial da GPU são limitadas a ponto flutuante de 32 bits. As instruções de fluxo de controle de thread são branch condicional, call e return de função, exit de thread e bar.sync (sincronização de barreira). A instrução de desvio condicional @p bra target usa um registrador de predicado p (ou !p) definido anteriormente por uma instrução setp de comparação e definição de predicado, para determinar se a thread apanha o desvio ou não. Outras instruções também podem ter predicados em um registrador de predicado verdadeiro ou falso. Instruções de acesso à memória
A instrução tex busca e filtra amostras de textura de arrays de textura 1D, 2D e 3D na memória por meio do subsistema de textura. As buscas de textura geralmente utilizam coordenadas de ponto flutuante interpoladas para endereçar uma textura. Quando uma thread de sombreamento de pixel gráfico calcula sua cor de fragmento de pixel, o processador de operações de rastreio a mistura com a cor do pixel em sua posição de pixel atribuída (x, y) e escreve a cor final na memória. Para dar suporte às necessidades de cálculo e da linguagem C/C + +, a ISA PTX Tesla implementa as instruções load/store da memória. Elas utilizam endereçamento de inteiros por byte, com aritmética de registrador mais endereço de offset para facilitar as otimizações de código convencionais do compilador. As instruções load/store da memória são comuns nos processadores, mas são uma nova capacidade significativa nas GPUs da arquitetura Tesla, pois GPUs anteriores forneciam apenas a textura e os acessos de pixel exigidos pelas APIs gráficas. Para cálculo, as instruções load/store acessam três espaços de leitura/escrita que implementam os espaços de memória CUDA correspondentes na Seção A.3: j
Memória local para dados temporários endereçáveis por thread (implementada na DRAM externa)
j
Memória compartilhada para acesso de baixa latência aos dados compartilhados por threads em cooperação no mesmo bloco CTA/thread (implementada na SRAM no chip)
j
Memória global para grandes conjuntos de dados compartilhados por todas as threads de uma aplicação de cálculo (implementada na DRAM externa)
As instruções load/store da memória ld.global , st.global , ld.shared , st. shared, ld.local e st.local acessam os espaços de memória global, compartilhado e local. Programas de cálculo utilizam a instrução de sincronização de barreira rápida bar. sync para sincronizar threads dentro de um bloco CTA/thread que se comunica um com o outro por meio da memória compartilhada e global.
A.4 Arquitetura de multiprocessador multithreaded A-595
Para melhorar a largura de banda da memória e reduzir o overhead, as instruções load/ store locais e globais juntam solicitações de thread paralelas individuais a partir do mesmo warp SIMT em uma única solicitação de bloco de memória quando os endereços caem no mesmo bloco e atendem critérios de alinhamento. A junção de solicitações de memória oferece um aumento de desempenho significativo em relação a solicitações separadas de threads individuais. A grande contagem de threads do multiprocessador, juntamente com o suporte para muitas solicitações de carga pendentes, ajuda a cobrir a latência de carga para uso da memória local e global implementada na DRAM externa. As GPUs mais recentes da arquitetura Tesla também oferecem eficientes operações de memória atômicas na memória com as instruções atom.op.u32, incluindo operações com inteiros add, min, max, and, or, xor, exchange e cas (compare-and-swap), facilitando reduções paralelas e gerenciamento de estruturas de dados paralelas. Sincronização de barreira para comunicação de threads
O sincronismo rápido de barreira permite que programas CUDA se comuniquem frequentemente por meio de memória compartilhada e memória global, simplesmente chamando _syncthreads(); como parte de cada etapa de comunicação entre threads. A função de sincronização intrínseca gera uma única instrução bar.sync. Porém, implementar a sincronização de barreira rápida entre até 512 threads por bloco de threads CUDA é um desafio. O agrupamento de threads em warps SIMT de 32 threads reduz a dificuldade de sincronização por um fator de 32. As threads esperam em uma barreira no escalonador de threads SIMT de modo que não consumam quaisquer ciclos de processador enquanto esperam. Quando uma thread executar uma instrução bar.sync , ela incrementa o contador de chegada de thread da barreira e o escalonador marca a thread como esperando na barreira. Quando todas as threads CTA chegarem, o contador da barreira é igual ao contador terminal esperado, e o escalonador libera todas as threads esperando na barreira e continua executando threads.
Streaming Processor (SP) O processador streaming (SP) multithreaded é o principal processador de instruções de thread no multiprocessador. Seu arquivo de registradores (RF) oferece 1024 registradores escalares de 32 bits para até 64 threads. Ele executa todas as operações fundamentais de ponto flutuante, incluindo add.f32, mul.f32, mad.f32 (floating multiply-add), min. f32, max.f32 e sept.f32 (floating compare and set predicate). As operações de adição e multiplicação em ponto flutuante são compatíveis com o padrão IEEE 754 para números de PF em precisão simples, incluindo valores not-a-number (NaN) e infinito. O core SP também implementa todas as instruções PTX aritméticas, de comparação, conversão e lógicas com inteiros de 32 e 64 bits na Figura A.4.3. As operações de ponto flutuante add e mul empregam o arredondamento-para-par-maispróximo do IEEE como modo de arredondamento padrão. A operação de multiplicaçãoadição de ponto flutuante mad.f32 realiza uma multiplicação truncada, seguida por uma adição com arredondamento-para-par-mais-próximo. O SP limpa os operandos desnormalizados da entrada para zero-com-sinal-preservado. Os resultados com underflow da faixa de expoentes da saída de destino são limpos para zero-com-sinal-preservado após o arredondamento.
Special Function Unit (SFU) Certas instruções de thread podem ser executadas nas SFUs, simultaneamente com outras instruções de thread executando nos SPs. A SFU implementa as instruções de função especial da Figura A.4.3, que calcula aproximações de ponto flutuante de 32 bits para funções transcendentais recíprocas, de raiz quadrada recíproca e de chave. Ela também implementa interpolação de atributo planar em ponto flutuante para sombreamentos de pixel, oferecendo interpolação precisa de atributos como coordenadas de cor, profundidade e textura.
A-596
Apêndice A Gráficos e GPUs de computação
Cada SFU em pipeline gera um resultado de função especial de ponto flutuante de 32 bits por ciclo; as duas SFUs por multiprocessador executam instruções de função especial em um quarto da taxa de instrução simples dos oito SPs. As SFUs também executam a instrução de multiplicação mul.f32 simultaneamente com os oito SPs, aumentando a taxa de cálculo máxima para 50%, para threads com uma mistura de instruções adequada. Por avaliação funcional, a SFU da arquitetura Tesla emprega interpolação quadrática, com base nas aproximações minimax avançadas, para aproximar as funções recíprocas, raiz quadrada recíproca, log2x, 2x e sin/cos. A precisão da função estima intervalos de 22 a 24 bits de mantissa. Veja mais detalhes sobre aritmética de SFU na Seção A.6.
Comparando com outros multiprocessadores Em comparação com as arquiteturas de vetor SIMD, como SSE x86, o multiprocessador SIMT pode executar threads individuais independentemente, em vez de sempre executá-las juntas em grupos síncronos. O hardware SIMT encontra paralelismo de dados entre threads independentes, enquanto o hardware SIMD requer que o software expresse o paralelismo de dados explicitamente em cada instrução de vetor. Uma máquina SIMT executa um warp de 32 threads de forma síncrona quando as threads tomam o mesmo caminho de execução, embora possa executar cada thread independentemente quando elas divergirem. A vantagem é significativa, pois os programas e as instruções SIMT simplesmente descrevem o comportamento de uma única thread independente, ao em vez de um vetor de dados SIMD de quatro ou mais pistas de dados. Apesar disso, o multiprocessador SIMT possui eficiência tipo SIMD, espalhando a superfície e o custo de uma unidade de instrução pelas 32 threads de um warp e pelos oito cores de processador streaming. SIMT oferece o desempenho do SIMD junto com a produtividade do multithreading, evitando a necessidade de codificar explicitamente os vetores SIMD do código para condições de aresta e divergência parcial. O multiprocessador SIMT impõe pouco overhead, pois é multithreaded por hardware com sincronização de barreira de hardware. Isso permite que sombreamentos gráficos e threads CUDA expressem um paralelismo bastante fine-grained. Programas gráficos e CUDA utilizam threads para expressar paralelismo de dados fine-grained em um programa por thread, em vez de forçar o programador a expressá-lo como instruções de vetor SIMD. É mais simples e mais produtivo desenvolver código de única thread escalar que o código de vetor, e o multiprocessador SIMT executa o código com eficiência tipo SIMD. Juntar oito cores de processador streaming em um multiprocessador e depois implementar um número escalável desses multiprocessadores cria um multiprocessador de dois níveis composto de multiprocessadores. O modelo de programação CUDA explora a hierarquia de dois níveis oferecendo threads individuais para cálculos paralelos finegrained, e oferecendo grades de blocos de thread para operações paralelas coarse-grained. O mesmo programa de thread pode oferecer operações fine-grained e coarse-grained. Ao contrário, as CPUs com instruções de vetor SIMD precisam usar dois modelos de programação diferentes para oferecer operações fine-grained e coarse-grained: threads paralelas coarse-grained nos diferentes cores, e instruções de vetor SIMD para o paralelismo de dados fine-grained.
Conclusão sobre multiprocessador multithreaded O multiprocessador da GPU de exemplo baseado na arquitetura Tesla é altamente multithreaded, executando um total de até 512 threads peso leve simultaneamente para dar suporte a sombreamentos de pixel fine-grained e threads CUDA. Ele utiliza uma variação da arquitetura SIMD e multithreading, chamada SIMT (Single-Instruction Multiple-Thread) para enviar uma instrução por broadcast, de forma eficiente, para um warp de 32 threads paralelas, enquanto permite que cada thread se desvie e seja executada independentemente. Cada thread executa seu fluxo de instruções em um dos oito cores do processador streaming (SP), que são multithreaded para até 64 threads.
A.5 Sistema de memória paralela A-597
A ISA PTX é uma ISA escalar de load/store baseada em registrador, que descreve a execução de uma única thread. Como as instruções PTX são otimizadas e traduzidas para microinstruções binárias para uma GPU específica, as instruções de hardware podem evoluir rapidamente sem atrapalhar compiladores e ferramentas de software que geram instruções PTX.
A.5 Sistema de memória paralela Fora a própria GPU, o subsistema de memória é o determinante mais importante do desempenho de um sistema gráfico. As cargas de trabalhos gráficos exigem taxas de transferência muito altas de e para a memória. Operações de escrita e blend (ler-modificaescrever) de pixel, leituras e escritas de buffer em profundidade, leituras de mapa de textura, além de leituras de dados de vértice e atributo de objeto, compreendem a maior parte do tráfego da memória. As GPUs modernas são altamente paralelas, como mostramos na Figura A.2.5. Por exemplo, o GeForce 8800 pode processar 32 pixels por clock, a 600MHz. Cada pixel normalmente requer uma leitura e escrita de cor e uma leitura e escrita de profundidade de um pixel de 4 bytes. Normalmente, uma média de dois ou três texels de quatro bytes cada são lidos para gerar a cor do pixel. Assim, para um caso típico, existe uma demanda de 28 bytes vezes 32 pixels = 896 bytes por clock. Nitidamente, a demanda de largura de banda no sistema de memória é enorme. Para fornecer esses requisitos, os sistemas de memória da GPU têm as seguintes características: j
Eles são amplos, significando que existe um grande número de pinos para transmitir dados entre a GPU e seus dispositivos de memória, e o próprio array de memória compreende muitos chips DRAM para fornecer a largura total do barramento de dados.
j
Eles são rápidos, significando que técnicas de sinalização agressivas são usadas para maximizar a taxa de dados (bits/segundo) por pino.
j
As GPUs buscam usar cada ciclo disponível para transferir dados de e para o array de memória. Para conseguir isso, as GPUs especificamente não visam minimizar a latência ao sistema de memória. Alta vazão (eficiência de utilização) e latência curta estão fundamentalmente em conflito.
j
Técnicas de compactação são utilizadas, tanto com perdas, das quais o programador precisa estar ciente, quanto sem perdas, que é invisível à aplicação e oportunista.
j
Caches e estruturas de junção de trabalho são usadas para reduzir a quantidade de tráfego fora do chip necessário e garantir que os ciclos gastos movendo-se dados são usados o mais totalmente possível.
Considerações sobre DRAM As GPUs precisam levar em consideração as características exclusivas da DRAM. Chips de DRAM são arrumados internamente como múltiplos bancos (normalmente, de quatro a oito), em que cada banco inclui um número de linhas na potência de 2 (normalmente, por volta de 16.384), e cada linha contém um número de bits na potência de dois (normalmente, 8192). As DRAMs impõem uma série de requisitos de temporização em seu processador de controle. Por exemplo, dezenas de ciclos são exigidos para ativar uma linha, mas, uma vez ativados, os bits dentro dessa linha são acessíveis aleatoriamente com um novo endereço de coluna a cada quatro clocks. DRAMs síncronas Double-Data Rate (DDR) transferem dados nas arestas de subida e descida do clock de interface (veja Capítulo 5). Assim, uma
A-598
Apêndice A Gráficos e GPUs de computação
DRAM DDR com clock de 1GHz transfere dados a 2 gigabits por segundo por pino de dados. DRAMs DDR gráficas normalmente possuem 32 pinos de dados bidirecionais, de modo que oito bytes podem ser lidos ou escritos a partir da DRAM por clock. As GPUs internamente possuem um grande número de geradores de tráfego de memória. Diferentes estágios do pipeline gráfico lógico possuem cada um seus próprios streams de solicitação: busca de atributo de comando e vértice, busca e load/store de textura de sombreamento, e leitura-escrita de profundidade e cor de pixel. Em cada estágio lógico, normalmente existem múltiplas unidades independentes para oferecer a vazão paralela. Estas são solicitadores de memória independentes. Quando vistas no sistema de memória, existe um número enorme de solicitações não correlacionadas durante a execução. Essa é uma divergência natural do padrão de referência preferido pelas DRAMs. Uma solução é que o controlador de memória da GPU mantenha heaps separadas de tráfego voltado para diferentes bancos de DRAM, e esperem até que um tráfego suficiente para determinada linha da DRAM esteja pendente antes de ativar essa linha e transferir todo o tráfego ao mesmo tempo. Observe que acumular solicitações pendentes, embora seja bom para a localidade de linha de DRAM e, portanto, para o uso eficiente do barramento de dados, leva a uma latência média mais longa, conforme visto pelos solicitantes cujas solicitações gastam tempo esperando por outras. O projeto precisa cuidar para que nenhuma solicitação em particular espere muito tempo, ou então algumas unidades de processamento podem “morrer de fome” esperando por dados e por fim fazer com que os processadores vizinhos se tornem ociosos. Os subsistemas de memória da GPU são arrumados como múltiplas partições de memória, cada um compreendendo um controlador de memória totalmente independente e um ou dois dispositivos de DRAM que são totalmente e exclusivamente possuídos por essa partição. Para conseguir o melhor balanceamento de carga e, portanto, aproximar o desempenho teórico de n partições, os endereços são intercalados detalhadamente por todas as partições de memória. O caminho de intercalação da partição em geral é um bloco de algumas centenas de bytes. O número de partições de memória é projetado para balancear o número de processadores e outros solicitadores de memória.
Caches As cargas de trabalho da GPU normalmente possuem conjuntos de trabalho muito grandes – na ordem de centenas de megabytes para gerar um único frame gráfico. Diferente das CPUs, não é prático construir caches em chips grandes o suficiente para manter qualquer coisa próxima do conjunto de trabalho inteiro de uma aplicação gráfica. Enquanto as CPUs podem assumir taxas de acerto de cache muito altas (99,9% ou mais), as GPUs experimentam taxas de acerto mais próximas de 90% e, portanto, precisam lidar com muitas falhas durante a execução. Embora uma CPU possa ser razoavelmente projetada para interromper enquanto espera por uma falha de cache rara, uma GPU precisa prosseguir com falhas e acertos misturados. Chamamos isso de arquitetura de cache streaming. Caches da GPU precisam oferecer largura de banda muito alta aos seus clientes. Considere o caso de uma cache de textura. Uma unidade de textura comum pode avaliar duas interpolações bilineares para cada um dos quatro pixels por ciclo de clock, e uma GPU pode ter muitas dessas unidades de textura, todas operando independentemente. Cada interpolação bilinear requer quatro texels separados, e cada texel poderia ser um valor de 64 bits. É mais comum ter quatro componentes de 16 bits. Assim, a largura de banda total é 2 × 4 × 4 × 64 = 2048 bits por clock. Cada texel de 64 bits separado é endereçado independentemente, de modo que a cache precisa tratar de 32 endereços exclusivos por clock. Isso naturalmente favorece um arranjo multibanco e/ou multiporta de arrays SRAM.
MMU GPUs modernas são capazes de traduzir endereços virtuais para endereços físicos. No GeForce 8800, todas as unidades de processamento geram endereços de memória em um
A.5 Sistema de memória paralela A-599
espaço de endereço virtual de 40 bits. Para cálculo, as instruções de thread de load e store utilizam endereços de byte com 32 bits, que são estendidos para um endereço virtual de 40 bits acrescentando um offset de 40 bits. Uma unidade de gerenciamento de memória realiza tradução de endereço de virtual para físico; o hardware lê as tabelas de página da memória local para responder a falhas em favor de uma hierarquia de buffers lookaside de tradução espalhados entre os processadores e mecanismos de renderização. Além dos bits da página física, as entradas da tabela de página da GPU especificam o algoritmo de compactação para cada página. Os tamanhos de página variam de 4 a 128 kilobytes.
Espaços de memória Conforme apresentado na Seção A.3, CUDA expõe diferentes espaços de memória para permitir que o programador armazene valores de dados de uma forma que seja ideal para o desempenho. Para a discussão a seguir, assumimos GPUs da arquitetura Tesla da NVIDIA.
Memória global A memória global é armazenada na DRAM externa; ela não é local a qualquer multiprocessador de streaming (SM) físico isolado, pois visa a comunicação entre diferentes CTAs (blocos de threads) em diferentes grades. Na verdade, os muitos CTAs que referenciam um local na memória global podem não estar executando na GPU ao mesmo tempo; por projeto, em CUDA, um programador não sabe a ordem relativa em que os CTAs são executados. Como o espaço de endereço é distribuído uniformemente entre todas as partições de memória, é preciso haver um caminho de leitura/escrita de qualquer multiprocessador streaming para qualquer partição da DRAM. O acesso à memória global por diferentes threads (e diferentes processadores) não tem garantias de ter consistência sequencial. Os programas com threads veem um modelo de ordenação de memória relaxado. Dentro de uma thread, a ordem das leituras e escritas na memória para o mesmo endereço é preservada, mas a ordem dos acessos a diferentes endereços pode não ser preservada. As leituras e escritas na memória solicitadas por diferentes threads são desordenadas. Dentro de um CTA, a instrução de sincronização de barreira bar.sync pode ser usada para obter ordenação de memória estrita entre as threads do CTA. A instrução de thread membar oferece uma operação de barreira/cerca de memória que valida os acessos anteriores à memória e os torna visíveis a outras threads antes de prosseguir. As threads também podem usar as operações atômicas da memória descritas na Seção A.4 para coordenar o trabalho na memória que elas compartilham.
Memória compartilhada A memória compartilhada por CTA só é visível às threads que pertencem a esse CTA, e a memória compartilhada só ocupa armazenamento a partir do momento em que um CTA é criado até o momento em que ele termina. A memória compartilhada, portanto, pode residir no chip. Essa técnica tem muitos benefícios. Primeiro, o tráfego da memória compartilhada não precisa competir com a largura de banda limitada fora do chip, necessária para referências à memória global. Segundo, é prático criar estruturas de memória com largura de banda muito alta no chip para dar suporte às demandas de leitura/escrita de cada multiprocessador streaming. De fato, a memória compartilhada é bastante acoplada ao multiprocessador streaming. Cada multiprocessador streaming contém oito processadores de thread físicos. Durante um ciclo de memória compartilhada, cada processador de thread pode processar dois threads de instruções, de modo que 16 threads de solicitações de memória compartilhada precisam ser tratados em cada clock. Como cada thread pode gerar seus próprios endereços, e os endereços normalmente são exclusivos, a memória compartilhada é montada usando-se 16 bancos SRAM endereçáveis independentemente. Para os padrões de acesso comuns, 16 bancos são suficientes para manter a vazão, mas casos patológicos são possíveis; por exemplo, todas as 16 threads poderiam acessar um endereço diferente
A-600
Apêndice A Gráficos e GPUs de computação
em um banco de SRAM. Deverá ser possível rotear uma solicitação a partir de qualquer pista de thread para qualquer banco de SRAM, de modo que é exigida uma rede de interconexão de 16 por 16.
Memória local A memória local por thread é a memória particular visível apenas a uma única thread. A memória local é arquitetonicamente maior que o arquivo de registrador da thread, e um programa pode calcular endereços na memória local. Para dar suporte a grandes alocações de memória local (lembre-se de que a alocação total é a alocação por thread vezes o número de threads ativas), a memória local é alocada na DRAM externa. Embora a memória global e a local por thread residam fora do chip, elas são bem adequadas a serem mantidas em cache no chip.
Memória constante A memória constante é somente de leitura para um programa rodando no SM (ela pode ser escrita por meio de comandos à GPU). Ela é armazenada na DRAM externa e mantida em cache no SM. Como normalmente a maioria ou todas as threads em um warp SIMT leem do mesmo endereço na memória constante, uma única pesquisa de endereço por clock é suficiente. A cache constante é projetada para enviar valores escalares por broadcast às threads em cada warp.
Memória de textura A memória de textura mantém grandes arrays de dados somente de leitura. As texturas para computação têm os mesmos atributos e capacidades das texturas usadas com gráficos 3D. Embora as texturas sejam normalmente imagens bidimensionais (arrays 2D de valores de pixel), texturas 1D (lineares) e 3D (volume) também estão disponíveis. Um programa de cálculo referencia uma textura usando uma instrução tex. Os operandos incluem um identificador para nomear a textura, e 1, 2 ou 3 coordenadas, com base na dimensionalidade da textura. As coordenadas de ponto flutuante incluem uma parte fracionária que especifica um local de amostra normalmente entre os locais de texel. Coordenadas não inteiras invocam uma interpolação ponderada bilinear dos quatro valores mais próximos (para uma textura 2D) antes que o resultado seja retornado ao programa. Buscas de textura são mantidas em cache em uma hierarquia de cache streaming projetada para otimizar a vazão das buscas de textura de milhares de threads simultâneas. Alguns programas usam buscas de textura como um modo de manter a memória global em cache.
Superfícies Superfície é um termo genérico para um array unidimensional, bidimensional ou tridimensional de valores de pixel e um formato associado. Diversos formatos são definidos; por exemplo, um pixel pode ser definido como quatro componentes inteiros RGBA de 8 bits, ou quatro componentes de ponto flutuante de 16 bits. Um kernel de programa não precisa conhecer o tipo da superfície. Uma instrução tex converte seus valores de resultado como ponto flutuante, dependendo do formato da superfície.
Acesso de load/store As instruções load/store com endereçamento de byte inteiro permitem a escrita e compilação de programas em linguagens convencionais, como C e C + +. Programas CUDA utilizam instruções load/store para acessar a memória. Para melhorar a largura de banda da memória e reduzir o overhead, as instruções load/ store globais juntam solicitações de thread paralelas individuais a partir do mesmo warp em uma única solicitação de bloco de memória quando os endereços caem no mesmo
A.6 Aritmética de ponto flutuante A-601
bloco e atendem aos critérios de alinhamento. Juntar solicitações de memória pequenas em solicitações de bloco maiores oferece um aumento de desempenho significativo sobre solicitações separadas. A grande quantidade de threads, junto com o suporte para muitas solicitações de load pendentes, ajuda a cobrir a latência de load-para-uso para a memória local e global implementada na DRAM externa.
ROP Como podemos ver na Figura A.2.5, GPUs da arquitetura Tesla NVIDIA compreendem um array de processador streaming (SPA), que realiza todos os cálculos programáveis da GPU, e um sistema de memória escalável, que compreende controle de DRAM externa e Raster Operation Processors (ROPs) de função fixa, que realizam operações de buffer de frame de cor e profundidade diretamente na memória. Cada unidade ROP é emparelhada com uma partição de memória específica. Partições ROP são alimentadas a partir dos SMs por meio de uma rede de interconexão. Cada ROP é responsável por testes e atualizações de profundidade e estêncil, além de mistura de cores. Os controladores de ROP e memória cooperam para implementar compactação de cor e profundidade sem perda (até 8:1) para reduzir a largura de banda externa. Unidades ROP também realizam operações atômicas na memória.
A.6 Aritmética de ponto flutuante As GPUs hoje realizam a maioria das operações aritméticas nos cores de processador programáveis usando operações de ponto flutuante de 32 bits com precisão simples compatíveis com IEEE 754 (veja Capítulo 3). A aritmética de ponto flutuante das primeiras GPUs foi sucedida por ponto flutuante de 16 bits, 24 bits e 32 bits, depois ponto flutuante de 32 bits compatível com IEEE 754. Alguma lógica de função fixa dentro de uma GPU, como hardware de filtragem de textura, continua a usar os formatos numéricos proprietários. GPUs recentes também oferecem instruções de ponto flutuante de 64 bits com precisão dupla compatível com IEEE 754.
Formatos aceitos O padrão IEEE 754 para aritmética de ponto flutuante [2008] especifica formatos básicos e de armazenamento. As GPUs usam dois dos formatos básicos para computação, ponto flutuante binário de 32 e 64 bits, normalmente chamados de precisão simples e precisão dupla. O padrão também especifica um formato de ponto flutuante de armazenamento binário de 16 bits, meia precisão. GPUs e a linguagem de sombreamento Cg empregam o formato de dados estreito de 16 bits para armazenamento e movimentação de dados eficiente, embora mantendo alta faixa dinâmica. GPUs realizam muitos cálculos de filtragem de textura e mistura de pixel em meia precisão dentro da unidade de filtragem de textura e a unidade de operações de rastreio. O formato de arquivo de imagem de alta faixa dinâmica OpenEXR, desenvolvido pela Industrial Light and Magic [2003], usa o formato de metade idêntico para valores de componente de cor em aplicações de imagens de computador e desenho animado.
meia precisão Um formato de ponto flutuante binário de 16 bits, com 1 bit de sinal, expoente de 5 bits, fração de 10 bits e um bit de inteiro implícito.
Aritmética básica As operações comuns de ponto flutuante com precisão simples em cores programáveis da GPU incluem adição, multiplicação, multiplicação-adição, mínimo, máximo, comparação, definição de predicado e conversões entre números inteiros e de ponto flutuante. As instruções de ponto flutuante normalmente oferecem modificadores de operando de origem para negação e valor absoluto.
multiplicação-adição (MAD) Uma instrução única em ponto flutuante que realiza uma operação composta: multiplicação seguida por adição.
A-602
Apêndice A Gráficos e GPUs de computação
As operações de adição e multiplicação em ponto flutuante da maioria das GPUs atualmente são compatíveis com o padrão IEEE 754 para números de PF de precisão simples, incluindo not-a-number (NaN) e valores infinitos. As operações de adição e multiplicação de PF utilizam o arredondamento-para-par-mais-próximo como o modo de arredondamento default. Para aumentar a vazão da instrução em ponto flutuante, as GPUs normalmente utilizam uma instrução de multiplicação-adição composta (mad). A operação de multiplicação-adição de ponto flutuante realiza multiplicação de PF com truncamento, seguida por adição de PF com arredondamento-para-para-mais-próximo. Ela oferece duas operações de ponto flutuante em um ciclo de emissão, sem exigir que o escalonador de instrução despache duas instruções separadas, mas o cálculo não é fundido e trunca o produto antes da adição. Isso a torna diferente da instrução de multiplicação-adição fundida, discutida no Capítulo 3, e mais adiante nesta seção. GPUs normalmente limpam operandos de origem desnormalizados para zero preservado por sinal, e eles limpam resultados que passam por underflow da faixa de expoente de saída de destino para zero preservado por sinal após o arredondamento.
Aritmética especializada
unidade de função especial (SFU) Uma unidade de hardware que calcula funções especiais e interpola atributos planares.
As GPUs oferecem hardware para acelerar o cálculo de função especial, interpolação de atributo e filtragem de textura. Instruções de função especial incluem cosseno, seno, exponencial binário, logaritmo binário, recíproco e raiz quadrada recíproca. Instruções de interpolação de atributo oferecem geração eficiente de atributos de pixel, derivados da avaliação da equação do plano. A unidade de função especial (SFU) introduzida na Seção A.4 calcula funções especiais e interpola atributos planares [Oberman e Siu, 2005]. Existem vários métodos para avaliar funções especiais no hardware. Mostrou-se que a interpolação quadrática baseada em Enhanced Minimax Approximations é um método muito eficiente para aproximar funções no hardware, incluindo recíproco, raiz quadrada recíproca, log2x, 2x, seno e cosseno. Podemos resumir o método de interpolação quadrática SFU. Para um operando de entrada binário X com significando de n bits, o significando é dividido em duas partes: Xu é a parte superior, contendo m bits, e Xl é a parte inferior, contendo n-m bits. Os m bits superiores Xu são usados para consultar um conjunto de três tabelas de pesquisa para retornar três coeficientes de palavra finita C0, C1 e C2. Cada função a ser aproximada requer um conjunto exclusivo de tabelas. Esses coeficientes são usados para aproximar determinada função f(X) no intervalo Xu <= X < Xu + 2−m avaliando a expressão: f(X) = C 0 + C1 X 1 + C 2 X 12 A precisão de cada função estima os intervalos de 22 a 24 bits significativos. As estatísticas de função de exemplo aparecem na Figura A.6.1.
FIGURA A.6.1 Estatísticas de aproximação de função especial. Para a unidade de função especial (SFU) do NVIDIA GeForce 8800.
A.6 Aritmética de ponto flutuante A-603
O padrão IEEE 754 especifica requisitos de arredondamento exato para divisão e raiz quadrada; contudo, para muitas aplicações de GPU, a compatibilidade exata não é exigida. Em vez disso, para essas aplicações, a vazão de cálculo mais alta é mais importante do que a precisão até o último bit. Para as funções especiais da SFU, a biblioteca de matemática CUDA oferece uma função de precisão completa e uma função rápida com a precisão da instrução SFU. Outra operação aritmética especializada em uma GPU é a interpolação de atributo. Os principais atributos normalmente são especificados para vértices de primitivos que compõem uma cena a ser renderizada. Exemplos de atributos são coordenadas de cor, profundidade e textura. Esses atributos precisam ser interpolados no espaço de tela (x, y) conforme a necessidade, para determinar os valores dos atributos em cada local de pixel. O valor de determinado atributo U em um plano (x, y) pode ser expresso usando-se equações de plano na forma: U(x, y) = A u x + Bu y + C u onde A, B e C são parâmetros de interpolação associadas a cada atributo U. Os parâmetros de interpolação A, B e C são todos representados como números de ponto flutuante de precisão simples. Dada a necessidade para um avaliador de função e um interpolador de atributo em um processador de sombreamento de pixel, uma única SFU que realiza as duas funções por eficiência poderá ser projetada. As duas funções usam uma operação de soma de produtos para interpolar resultados, e o número de termos a ser resumido nas duas funções é muito semelhante. Operações de textura
Mapeamento e filtragem de textura é outro conjunto principal de operações aritméticas de ponto flutuante especializadas em uma GPU. As operações usadas para mapeamento de textura incluem: 1. Receber endereço de textura (s, t) para o pixel de tela atual (x, y), onde s e t são números de ponto flutuante de precisão simples. 2. Calcular o nível de detalhe para identificar o nível de mapa MIP com textura correta. 3. Calcular a fração de interpolação trilinear. 4. Escalar endereço de textura (s, t) para o nível de mapa de MIP selecionado. 5. Acessar memória e recuperar os texels desejados (elementos de textura). 6. Realizar operação de filtragem sobre os texels. O mapeamento de textura requer uma quantidade significativa de cálculo de ponto flutuante para a operação em velocidade plena, grande parte feita em meia precisão com 16 bits. Como um exemplo, o GeForce 8800 Ultra emite cerca de 500 GFLOPS de cálculo de ponto flutuante em formato próprio para instruções de mapeamento de textura, além de suas instruções convencionais IEEE de ponto flutuante com precisão simples. Para obter mais detalhes sobre mapeamento e filtragem de textura, consulte Foley e van Dam [1995].
Desempenho O hardware aritmético de adição e multiplicação em ponto flutuante utiliza pipelines em sua totalidade, e a latência é otimizada para balancear atraso e área. Em pipeline, a vazão das funções especiais é menor que as operações de adição e multiplicação em ponto flutuante. A vazão em um quarto de velocidade para as funções especiais é o desempenho típico nas GPUs modernas, com uma SFU compartilhada por quatro cores do SP. Ao contrário, as CPUs normalmente têm uma vazão significativamente menor para funções semelhantes,
mapa de MIP Uma frase em Latin multum in parvo, ou muito em um espaço pequeno. Um mapa de MIP contém imagens pré-calculadas de diferentes resoluções, usadas para aumentar a velocidade de renderização e reduzir artefatos.
A-604
Apêndice A Gráficos e GPUs de computação
como divisão e raiz quadrada, embora com resultados mais exatos. O hardware de interpolação de atributo normalmente utiliza pipelines em sua totalidade, para permitir sombreamentos de pixel em velocidade plena.
Dupla precisão GPUs mais recentes, como a Tesla T10P, também admitem operações de precisão dupla IEEE 754 com 64 bits no hardware. As operações aritméticas de ponto flutuante padrão em precisão dupla incluem adição, multiplicação e conversões entre diferentes formatos de ponto flutuante e inteiro. O padrão de ponto flutuante IEEE 754 de 2008 inclui especificação para a operação fundida de multiplicação-adição reunida (FMA), conforme discutimos no Capítulo 3. A operação FMA realiza uma multiplicação de ponto flutuante seguida por uma adição, com um único arredondamento. As operações de multiplicação e adição fundidas retêm precisão total nos cálculos intermediários. Esse comportamento permite cálculos de ponto flutuante mais precisos, envolvendo o acúmulo de produtos, incluindo produtos de ponto, multiplicação de matriz e avaliação polinomial. A instrução FMA também permite implementações de software eficientes de divisão e raiz quadrada arredondadas exatamente, evitando a necessidade de uma unidade de divisão ou raiz quadrada no hardware. Uma unidade FMA de precisão dupla no hardware implementa adição, multiplicação e conversões com 64 bits, além da própria operação FMA. A arquitetura de uma unidade FMA de precisão dupla permite o suporte para número desnormalizado em velocidade plena nas entradas e saídas. A Figura A.6.2 mostra um diagrama em blocos de uma unidade FMA. Como podemos ver na Figura A.6.2, os significandos de A e B são multiplicados para formar um produto de 106 bits, com os resultados mantidos na forma de carry-save (salvar vai-um). Em paralelo, o somando de 53 bits C é condicionalmente invertido e alinhado ao produto de 106 bits. Os resultados de soma e vai-um do produto de 106 bits são somados
FIGURA A.6.2 Unidade fundida de multiplicação-adição (FMA) com precisão dupla. Hardware para implementar A × B + C em ponto flutuante para precisão dupla.
A.7 Vida real: o NVIDIA GeForce 8800 A-605
com o somando alinhado por um somador carry-save (CSA) com 161 bits de largura. A saída do carry-save é então somada em um somador carry-propagate (propagar vai-um) para produzir um resultado não arredondando na forma não redundante, de complemento de dois. O resultado é recomplementado condicionalmente, de modo a retornar um resultado na forma de magnitude com sinal. O resultado complementado é normalizado, e depois é arredondado para caber dentro do formato de destino.
A.7 Vida real: o NVIDIA GeForce 8800 O NVIDIA GeForce 8800 GPU, introduzido em novembro de 2006, é um projeto de processador unificado de vértice e pixel, que também admite aplicações de cálculo paralelo escritas em C usando o modelo de programação paralela CUDA. Essa é a primeira implementação da arquitetura unificada de gráficos e cálculo descrita na Seção A.4 e em Lindholm, Nickolls, Oberman e Montrym [2008]. Uma família de GPUs da arquitetura Tesla enfoca as diferentes necessidades dos laptops, desktops, estações de trabalho e servidores.
Array de processador streaming (SPA) A GPU GeForce 8800 mostrada na Figura A.7.1 contém 128 cores de processador streaming (SP) organizados como 16 multiprocessadores streaming (SMs). Dois SMs compartilham uma unidade de textura em cada cluster textura/processador (TPC). Um array de oito TPCs compõe o array de processador streaming (SPA), que executa todos os programas de sombreamento gráfico e programas de cálculo.
FIGURA A.7.1 Arquitetura unificada de gráficos e cálculo da GPU NVIDIA Tesla. Esse GeForce 8800 tem 128 cores de processador streaming (SP) em 16 multiprocessadores streaming (SM), arrumados em oito clusters de textura/processador (TPC). Os processadores se conectam a seis partições DRAM com 64 bits de largura por meio de uma rede de interconexão. Outras GPUs implementando a arquitetura Tesla variam o número de cores SP, SMs, partições de DRAM e outras unidades.
A-606
Apêndice A Gráficos e GPUs de computação
A unidade de interface host se comunica com a CPU host por meio do barramento PCI-Express, verifica a consistência do comando e realiza a mudança de contexto. O montador de entrada coleta primitivos geométricos (ponto, linhas, triângulos). Os blocos de distribuição de trabalho despacham vértices, pixels e arrays de threads de cálculo para os TPCs no SPA. Os TPCs executam programas de sombreamento de vértice e geometria e programas de cálculo. Os dados geométricos de saída são enviados ao bloco viewport/ clip/setup/raster/zcull para serem rasterizados em fragmentos de pixel, que são então redistribuídos de volta ao SPA para a execução de programas de sombreamento de pixel. Os pixels sombreados são enviados pela rede de interconexão para serem processados pelas unidades ROP. A rede também direciona solicitações de leitura de memória de textura do SPA para a DRAM e lê dados da DRAM por uma cache de nível 2 de volta ao SPA.
Cluster de textura/processador (TPC) Cada TPC contém um controlador de geometria, um controlador SM (SMC), dois multiprocessadores streaming (SMs) e uma unidade de textura, como mostra a Figura A.7.2. O controlador de geometria mapeia o pipeline de vértice gráfico lógico em recirculação nos SMs físicos, direcionando todo atributo de primitivo e vértice e fluxo de topologia no TPC. O SMC controla múltiplos SMs, arbitrando a unidade de textura compartilhada, caminho de load/store e caminho de E/S. O SMC atende a três cargas de trabalhos gráficos simultaneamente: vértice, geometria e pixel. A unidade de textura processa uma instrução de textura para um vértice, geometria ou pixel quad, ou quatro threads de cálculo por ciclo. As origens da instrução de textura são
FIGURA A.7.2 Cluster de textura/processador (TPC) e um multiprocessador streaming (SM). Cada SM tem oito cores de processador streaming (SP), duas SFUs e uma memória compartilhada.
A.7 Vida real: o NVIDIA GeForce 8800 A-607
coordenadas de textura, e as saídas são amostras ponderadas, normalmente uma cor de ponto flutuante com quatro componentes (RGBA). A unidade de textura utiliza pipelines em profundidade. Embora contenha uma cache streaming para capturar a localidade da filtragem, ela flui acertos misturados com falhas sem gerar stalls.
Multiprocessador streaming (SM) O SM é um multiprocessador gráfico e de cálculo unificado, que executa programas de sombreamento de vértice, geometria e fragmento de pixel e programas de cálculo paralelos. O SM consiste em oito cores de processador de thread SP, duas SFUs, uma unidade de busca e emissão de instrução multithreaded (emissão MT), uma cache de instrução, uma cache de constante somente de leitura e uma memória compartilhada de leitura/escrita com 16KB. Ele executa instruções escalares para threads individuais. O clock do GeForce 8800 Ultra trabalha com cores SP e SFUs a 1,5GHz, para um máximo de 36 GFLOPS por SM. Para otimizar a eficiência de potência e área, algumas unidades não de caminho de dados do SM operam com metade da taxa de clock do SP. Para executar de forma eficiente centenas de threads paralelas enquanto executa vários programas diferentes, o SM é multithreaded por hardware. Ele controla e executa até 768 threads simultâneos no hardware com overhead de escalonamento zero. Cada thread tem seu próprio estado de execução de thread e pode executar um caminho de código independente. Um warp consiste em até 32 threads do mesmo tipo — vértice, geometria, pixel ou cálculo. O projeto SIMT, anteriormente descrito na Seção A.4, compartilha a unidade de busca e emissão de instrução SM eficientemente por 32 threads, mas requer um warp completo de threads ativas para eficiência de desempenho plena. O SM escalona e executa diversos tipos de warp simultaneamente. A cada ciclo de emissão, o escalonador seleciona um dos 24 warps para executar uma instrução de warp SIMT. Uma instrução de warp emitida é executada como quatro conjuntos de 8 threads por quatro ciclos de processador. As unidades SP e SFU executam instruções independentemente, e emitindo instruções entre elas em ciclos alternados, o escalonador pode manter ambas totalmente ocupadas. Um scoreboard qualifica cada warp para emitir cada ciclo. O escalonador de instrução prioriza todos os warps prontos e seleciona um com a prioridade mais alta para emissão. A priorização considera o tipo de warp, o tipo de instrução e a “imparcialidade” para todos os warps sendo executados no SM. O SM executa arrays de threads cooperativos (CTAs) como múltiplos warps simultâneos que acessam uma região de memória compartilhada alocada dinamicamente para o CTA.
Conjunto de instruções Threads executam instruções escalares, diferente das arquiteturas anteriores de instrução de vetor da GPU. As instruções escalares são mais simples e amigáveis do compilador. As instruções de textura permanecem baseadas em vetor, apanhando um vetor de coordenadas de origem e retornando um vetor de cores filtrado. O conjunto de instruções baseado em registrador inclui todas as instruções de aritmética de ponto flutuante e inteiro, transcendentais, lógicas, controle de fluxo, load/ store de memória e instruções de textura listadas na tabela de instruções PTX da Figura A.4.3. As instruções load/store da memória utilizam endereçamento de byte inteiro com aritmética de endereço de registrador-mais-offset. Para cálculos, as instruções load/store acessam três espaços de memória de leitura-escrita: memória local para dados por thread, privados, temporários; memória compartilhada para dados por CTA de baixa latência, pelas threads do CTA; e memória global para dados compartilhados por todas as threads. Os programas de cálculo utilizam a instrução rápida de sincronização de barreira bar. sync para sincronizar threads dentro de um CTA que se comunicam entre si por meio da memória compartilhada e global. As GPUs da arquitetura Tesla mais recente implementam operações de memória atômicas de PTX, que facilita reduções paralelas e gerenciamento paralelo da estrutura de dados.
A-608
Apêndice A Gráficos e GPUs de computação
Processador streaming (SP) O core SP multithreaded é o processador de threads principal, apresentado na Seção A.4. Seu arquivo de registradores oferece 1024 registradores escalares de 32 bits para até 96 threads (mais threads que o SP de exemplo da Seção A.4). Suas operações de adição e multiplicação em ponto flutuante são compatíveis com o padrão IEEE 754 para números de PF com precisão simples, incluindo not-a-number (NaN) e infinito. As operações de adição e multiplicação utilizam o arredondamento-para-par-mais-próximo do IEEE como modo de arredondamento padrão. O core SP também implementa todas as instruções PTX de aritmética com inteiros, comparação, conversão e lógicas de 32 e 64 bits. O processador utiliza o pipeline por completo, e a latência é otimizada para balancear atraso e área.
Unidade de função especial (SFU) A SFU admite cálculo de funções transcendentais e interpolação de atributo planar. Conforme descrevemos na Seção A.6, ela utiliza a interpolação quadrática com base nas aproximações minimax avançadas para aproximar as funções de recíproco, raiz quadrada recíproca, log2x, 2x e sin/cos em um resultado por ciclo. A SFU também admite interpolação de atributo de pixel como coordenadas de cor, profundidade e textura em quatro amostras por ciclo.
Rasterização Primitivos de geometria dos SMs entram em sua ordem de entrada round-robin original no bloco viewport/clip/setup/raster/zcull. As unidades de viewport e clip cortam os primitivos até o frustum da view e até quaisquer planos de corte do usuário ativados, e depois transformam os vértices em espaço de tela (pixel). Os primitivos sobreviventes então vão para a unidade de configuração, que gera equações de aresta para o rasterizador. Um estágio de rasterização coarse gera todos os pedaços de pixels que estão pelo menos parcialmente dentro do primitivo. A unidade zcull mantém uma superfície z hierárquica, rejeitando os pedaços de pixels se forem conservadoramente conhecidos como ocultados pelos pixels desenhados anteriormente. A taxa de rejeição é de até 256 pixels por clock. Os pixels que sobrevivem ao zcull vão então para o estágio de rasterização fina, que gera informações de cobertura e valores de profundidade detalhados. O teste de profundidade e atualização pode ser realizado antes do sombreamento de fragmento, ou depois, dependendo do estado atual. O SMC monta pixels sobreviventes em warps para serem processados por um SM rodando o sombreamento de pixel atual. O SMC, então, envia o pixel sobrevivente e dados associados ao ROP.
Processador de operações de rastreio (ROP) e o sistema de memória Cada ROP é emparelhado com uma partição de memória específica. Para cada fragmento de pixel emitido por um programa de sombreamento de pixel, os ROPs realizam teste e atualizações de profundidade e estêncil, e, em paralelo, mistura de cores e atualizações. A compactação de cores sem perda (até 8:1) e a compactação de profundidade (até 8:1) são usadas para reduzir a largura de banda da DRAM. Cada ROP tem uma taxa máxima de quatro pixels por clock e admite formatos HDR de ponto flutuante com 16 e 32 bits. ROPs admitem processamento com profundidade de taxa dupla quando as escritas de cor são desativadas. O suporte para antialiasing inclui até 16 multisampling e supersampling. O algoritmo de antialiasing de amostragem de cobertura (CSAA) calcula e armazena a cobertura Booleana em até 16 amostras e compacta informações redundantes de cor, profundidade e estêncil no footprint da memória e uma largura de banda de quatro ou oito amostras para melhorar o desempenho.
A.7 Vida real: o NVIDIA GeForce 8800 A-609
O barramento de dados da memória DRAM é de 384 pinos, organizados em seis partições independentes de 64 pinos cada. Cada partição admite protocolos de taxa de dados dupla DDR2 e GDDR3 orientado a gráficos, em até 1,0GHz, gerando uma largura de banda de cerca de 16GB/s por partição, ou 96GB/s. Os controladores de memória admitem uma grande faixa de taxas de clock de DRAM, protocolos, densidades de dispositivo e larguras de barramento de dados. Solicitações de textura e load/store podem ocorrer entre qualquer TPC e qualquer partição de memória, de modo que uma rede de interconexão roteia solicitações e respostas.
Escalabilidade A arquitetura unificada Tesla é projetada por escalabilidade. Variar o número de SMs, TPCs, ROPs, caches e partições de memória oferece o equilíbrio certo para alvos de desempenho e custo nos segmentos de mercado da GPU. A interconexão de link escalável (SLI) conecta múltiplas GPUs, oferecendo mais escalabilidade.
Desempenho O GeForce 8800 Ultra usa 1,5GHz de clock nos cores do processador de threads SP e SFUs, para um pico de operação teórico de 576 GFLOPS. O GeForce 8800 GTX tem um clock de processador de 1,35GHz e um máximo correspondente de 518 GFLOPS. As três seções seguintes comparam o desempenho de uma GPU GeForce 8800 com uma CPU multicore em três aplicações diferentes — álgebra linear densa, transformações de Fourier rápidas e classificação. Os programas e bibliotecas da GPU são código C CUDA compilado. O código da CPU utiliza a biblioteca MKL 10.0 da Intel multithreading com precisão simples para aproveitar instruções SSE e múltiplos cores.
Desempenho da álgebra linear densa Os cálculos de álgebra linear densa são fundamentais em muitas aplicações. Volkov e Demmel [2008] apresentam resultados de desempenho de GPU e CPU para multiplicação matriz-matriz densa com precisão simples (a rotina SGEMM) e fatorações de matriz LU, QR e Cholesky. A Figura A.7.3 compara as taxas em GFLOPS sobre a multiplicação matrizmatriz densa SGEMM para uma GPU GeForce 8800 GTX com uma CPU quad-core. A Figura A.7.4 compara as taxas em GFLOPS sobre a fatoração de matriz para uma GPU com uma CPU quad-core. Como a multiplicação matriz-matriz SGEMM e as rotinas BLAS3 semelhantes são o centro do trabalho na fatoração de matriz, seu desempenho define um limite superior na taxa de fatoração. Como a ordem de matriz aumenta além de 200 para 400, o problema de fatoração torna-se tão grande que SGEMM pode aproveitar o paralelismo da GPU e contornar o sistema CPU-GPU e overhead de cópia. A multiplicação matriz-matriz SGEMM de Volkov alcança 206 GFLOPS, cerca de 60% da taxa de multiplicação-adição máxima do GeForce 8800 GTX, enquanto a fatoração QR alcançou 192 GFLOPS, cerca de 4,3 vezes a CPU quad-core.
Desempenho de FFT Fast Fourier Transforms são usadas em muitas aplicações. Grandes transformações e transformações multidimensionais são particionadas em lotes de transformações 1D menores. A Figura A.7.5 compara o desempenho FFT de precisão simples complexo em 1D de um GeForce 8800 GTX a 1,35GHz (datado de final de 2006) com uma série Intel Xeon E5462 quad-core a 2,8GHz (apelidado de “Harpertown”, datado de final de 2007). O desempenho da CPU era medido usando a Intel Math Kernel Library (MKL) 10.0 FFT com quatro threads. O desempenho da GPU foi medido usando a biblioteca NVIDIA CUFFT 2.1 e FFTs de decimação-em-frequência com raiz 16 1D em lote. O desempenho
A-610
Apêndice A Gráficos e GPUs de computação
FIGURA A.7.3 Taxas de desempenho da multiplicação matriz-matriz densa SGEMM. O gráfico mostra as taxas em GFLOPS de precisão simples obtidas na multiplicação de matrizes N × N quadradas (linhas sólidas) e matrizes N × 64 e 64 × N (linhas tracejadas). Adaptado da Figura 6 de Volkov e Demmel [2008]. As linhas pretas são um GeForce 8800 GTX a 1,35GHz usando o código SGEMM de Volkov (agora no NVIDIA CUBLAS 2.0) em matrizes na memória GPU. As linhas azuis são um Intel Core2 Quad Q6600 quad-core a 2,4GHz, Linux com 64 bits, Intel MKL 10.0 em matrizes na memória da CPU.
FIGURA A.7.4 Taxas de desempenho de fatoração de matriz densa. O gráfico mostra taxas GFLOPS obtidas em fatorações de matriz usando a GPU e usando apenas a CPU. Adaptado da Figura 7 de Volkov e Demmel [2008]. As linhas pretas são um NVIDIA GeForce 8800 GTX a 1,35GHz, CUDA 1.1, Windows XP conectado a um Intel Core2 Duo E6700 a 2,67GHz, incluindo todos os tempos de transferência de dados CPU-GPU. As linhas azuis são um Intel Core2 Quad Q6600 a 2,4GHz, Linux de 64 bits, Intel MKL 10.0.
A.7 Vida real: o NVIDIA GeForce 8800 A-611
FIGURA A.7.5 Desempenho de vazão Fast Fourier Transform. O gráfico compara o desempenho das FFTs complexas no local unidimensional em lote, em um GeForce 8800 GTX de 1,35GHz com um Intel Xeon E5462 quad-core de 2,8GHz (apelidado “Harpertown”), cache L2 de 6MB, 4GB de memória, 1600 FSB, Red Hat Linux, Intel MKL 10.0.
da vazão da CPU e GPU foi medido usando-se FFTs em lote, com tamanho de lote 224/n, onde n é o tamanho da transformação. Assim, a carga de trabalho para cada tamanho de transformação foi 128MB. Para determinar a taxa em GFLOPS, o número de operações por transformação foi tomado como 5n log2n.
Desempenho da classificação Ao contrário das aplicações que discutimos, a classificação requer uma coordenação muito mais substancial entre as threads paralelas, e a expansão paralela é correspondentemente mais difícil de obter. Apesar disso, diversos algoritmos de classificação bem conhecidos podem ser eficientemente colocados em paralelo para rodar em na GPU. Satish et al. [2008] detalham o projeto de algoritmos de classificação em CUDA, e os resultados que eles informam para radix sort são resumidos a seguir. A Figura A.7.6 compara o desempenho da classificação paralela de um GeForce 8800 Ultra com um sistema Intel Clovertown de oito cores, ambos datando do início de 2007. Os cores da CPU são distribuídos entre dois soquetes físicos. Cada soquete contém um módulo multichip com chips Core2 gêmeos, e cada chip tem uma cache L2 de 4MB. Todas as rotinas de classificação foram projetadas para classificação pares de chave-valor em que chaves e valores são inteiros de 32 bits. O algo principal sendo estudado é o radix sort, embora o procedimento parralel_sort() baseado no Quicksort fornecido pelos Threading Building Blocks da Intel também seja incluído por comparação. Dos dois códigos radix sort baseados na CPU, um foi implementado usando apenas o conjunto de instruções escalares e o outro utiliza rotinas de linguagem assembly cuidadosamente ajustadas, que tiram proveito das instruções de vetor SSE2 SIMD. O próprio gráfico mostra a taxa de classificação alcançada — definida como o número de elementos classificados dividido pelo tempo a classificar — para um intervalo de tamanhos de sequência. Fica aparente por esse gráfico que a radix sort da GPU alcançou a taxa de classificação mais alta para todas as sequências de elementos de 8K e maiores. Nessa faixa,
A-612
Apêndice A Gráficos e GPUs de computação
FIGURA A.7.6 Desempenho da classificação paralela. Este gráfico compara as taxas de classificação para implementações de radix sort paralelas em um GeForce 8800 Ultra a 1,5GHz e um sistema de oito cores Intel Core2 Xeon E5345 a 2,33GHz.
ela é na média 2,6 vezes mais rápida que a rotina baseada no Quicksort e aproximadamente duas vezes mais rápida que as rotinas radix sort, todas usando os oito cores da CPU disponíveis. O desempenho da radix sort da CPU varia bastante, provavelmente devido à fraca localidade de cache de suas permutações globais.
A.8 Vida real: mapeando aplicações a GPUs O advento das CPUs multicore e GPUs manycore significa que os principais chips de processador agora são sistemas paralelos. Além do mais, seu paralelismo continua a se expandir com a lei de Moore. O desafio é desenvolver aplicações importantes de cálculo visual e cálculo de alto desempenho que expandam transparentemente seu paralelismo para aproveitar o número cada vez maior de cores de processador, assim como as aplicações gráficas 3D expandem transparentemente seu paralelismo a GPUs com quantidades bastantes variadas de cores. Esta seção apresenta exemplos de mapeamento de aplicações de cálculo paralelo escaláveis à GPU usando CUDA.
Matrizes esparsas Uma grande variedade de algoritmos paralelos pode ser escrita em CUDA de uma maneira razoavelmente simples, mesmo quando as estruturas de dados envolvidas não são simples grades regulares. A multiplicação de vetor de matriz esparsa (SpMV) é um bom exemplo de um bloco de montagem importante que pode ser paralelizado diretamente usando as abstrações fornecidas pelo modelo CUDA. Os kernels que discutimos a seguir, quando combinados com as rotinas de vetor CUBLAS fornecidas, simplificam a escrita de solucionadores iterativos, como o método de gradiente conjugado. Uma matriz esparsa n × n é aquela em que o número de entradas diferentes de zero m é somente uma pequena fração do total. As representações em matriz esparsa buscam
A.8 Vida real: mapeando aplicações a GPUs A-613
armazenar apenas os elementos diferentes de zero de uma matriz. Como é muito comum que uma matriz esparsa n × n contenha apenas n = O(n) elementos diferentes de zero, isso representa uma economia substancial no espaço de armazenamento e tempo de processamento. Uma das representações mais comuns para as matrizes esparsas não estruturadas gerais é a representação da linha esparsa compactada (CSR). Os m elementos diferentes de zero da matriz A são armazenados em ordem principal de linha em um array Av. Um segundo array Aj registra o índice de coluna correspondente para cada entrada de Av. Finalmente, um array Ap de n + 1 elementos registra a extensão de cada linha nos arrays anteriores; as entradas para a linha i em Aj e Av se estendem do índice Ap[i] até, mas não incluindo, o índice Ap[i+1]. Isso significa que Ap[0] sempre será 0 e Ap[n] sempre será o número de elementos diferentes de zero na matriz. A Figura A.8.1 mostra um exemplo da representação CSR de uma matriz simples. Dada uma matriz A em formato CSR e um vetor x, podemos calcular uma única linha do produto y = Ax usando o procedimento multiply_row() mostrado na Figura A.8.2. Calcular o produto total é então simplesmente uma questão de percorrer todas as linhas e calcular o resultado para essa linha usando multiply_row(), como no código serial em C mostrado na Figura A.8.3. Esse algoritmo pode ser traduzido para um kernel CUDA paralelo muito facilmente. Simplesmente espalhamos o loop em csrmul_serial() por muitas threads paralelas. Cada thread calculará exatamente uma linha do vetor de saída y. O código para esse kernel aparece na Figura A.8.4. Observe que ele se parece exatamente com o loop serial usado no procedimento csrmul_serial(). Na realidade, existem apenas dois pontos de diferença. Primeiro, o índice row para cada thread é calculado a partir dos índices de bloco e thread atribuídos a cada thread, eliminando o loop for. Segundo, temos uma condicional que só avalia o produto de uma linha se o índice da linha estiver dentro dos limites da matriz (isso é necessário porque o número de linha n não precisa ser um múltiplo do tamanho de bloco usado na partida do kernel).
FIGURA A.8.1 Matriz de linha esparsa compactada (CSR).
FIGURA A.8.2 Código serial em C para uma única linha de multiplicação de vetor de matriz esparsa.
A-614
Apêndice A Gráficos e GPUs de computação
FIGURA A.8.3 Código serial para multiplicação de vetor de matriz esparsa.
FIGURA A.8.4 Versão CUDA da multiplicação de vetor de matriz esparsa.
Supondo que as estruturas de dados da matriz já tenham sido copiadas para a memória do dispositivo de GPU, a partida desse kernel se parecerá com isto:
O padrão que vemos aqui é muito comum. O algoritmo serial original é um loop cujas interações são independentes uma da outra. Esses loops podem ser colocados em paralelo facilmente pela simples atribuição de uma ou mais iterações do loop a cada thread paralela. O modelo de programação fornecido pelo modelo CUDA torna a expressão desse tipo de paralelismo particularmente simples. Essa estratégia geral de decompor cálculos em blocos de trabalho independente, e mais especificamente desmembrar iterações de loop independentes, não é exclusivo do modelo CUDA. Essa é uma técnica comum utilizada, de uma forma ou de outra, por diversos sistemas de programação paralelos, incluindo OpenMP e Threading Building Blocks da Intel.
A.8 Vida real: mapeando aplicações a GPUs A-615
Caching na memória compartilhada Os algoritmos SpMV esboçados aqui são muito simples. Existem diversas otimizações que podem ser feitas nos códigos da CPU e GPU que podem melhorar o desempenho, incluindo desdobramento de loop, reordenação de matriz e bloqueio de registrador. Os kernels paralelos também podem ser reimplementados em termos de operações scan paralelas com dados, apresentadas por Sengupta, et al. [2007]. Um dos recursos arquitetônicos importantes expostos pelo modelo CUDA é a presença da memória compartilhada por bloco, uma pequena memória no chip, com latência muito baixa. Tirar proveito dessa memória pode oferecer melhorias de desempenho substanciais. Um modo importante de fazer isso é usar memória compartilhada como uma cache gerenciada pelo software para manter dados reutilizados com frequência. Modificações usando memória compartilhada podem ser vistas na Figura A.8.5. No contexto da multiplicação de matriz esparsa, observamos que várias linhas de A podem usar um elemento de array em particular x[i]. Em muitos casos comuns, e particularmente quando a matriz foi reordenada, as linhas usando x[i] serão linhas próximas da linha i. Portanto, podemos implementar um esquema de caching simples e esperar alcançar algum benefício no desempenho. O bloco de threads processando as linhas de i a j carregará de x[i] a x[j] nessa memória compartilhada. Desdobraremos o loop multiply_row() e buscaremos os elementos de x da cache sempre que for possível. O código resultante aparece na Figura A.8.5. A memória compartilhada também pode ser usada para fazer outras otimizações, como a busca de Ap[row+1] a partir de uma thread adjacente, em vez de buscá-lo novamente da memória. Como a arquitetura Tesla oferece uma memória compartilhada no chip controlada explicitamente, em vez de uma cache de hardware implicitamente ativa, é muito comum incluir esse tipo de otimização. Embora isso possa impor algum peso de desenvolvimento adicional para o programador, ele é relativamente pequeno, e os benefícios em potencial do desempenho podem ser substanciais. No exemplo mostrado anteriormente, até mesmo esse uso muito simples da memória compartilhada retorna uma melhoria de desempenho de cerca de 20% nas matrizes representativas derivadas de malhas de superfície 3D. A disponibilidade de uma memória controlada explicitamente no lugar de uma cache implícita também tem a vantagem de que políticas de caching e prefetching podem ser ajustadas especificamente às necessidades da aplicação. Esses são kernels muito simples, cuja finalidade é ilustrar as técnicas básicas na escrita de programas CUDA, em vez de como conseguir o máximo de desempenho. Diversas avenidas de otimização possíveis estão disponíveis, várias delas exploradas por Williams et al. [2007] em algumas arquiteturas multicore diferentes. Apesar disso, ainda é interessante examinar o desempenho comparativo até mesmo desses kernels simples. Em um processador Intel Core2 Xeon E5335 a 2GHz, o kernel csrmul_serial() roda em aproximadamente 202 milhões de valores diferentes de zero processados por segundo, para uma coleção de matrizes Laplacianas derivadas de malhas de superfície triangulada 3D. O paralelismo desse kernel com a construção parrallel_for fornecida pelo Threading Building Blocks da Intel produz speed-ups paralelos de 2,0, 2,1 e 2,3 rodando em dois, quatro e oito cores da máquina, respectivamente. Em um GeForce 8800 Ultra, os kernels csrmul_kernel() e csrmul_cached() alcançam taxas de processamento de aproximadamente 772 e 920 milhões de valores diferentes de zero por segundo, correspondentes a speed-ups paralelos de 3,8 a 4,6 vezes em relação do desempenho serial de um único core da CPU.
Varredura e redução O scan paralelo, também conhecido como soma de prefixo paralela, é um dos blocos de montagem mais importantes para os algoritmos paralelos de dados [Blelloch, 1990]. Dada uma sequência a e n elementos: [a 0 , a1 , . . ., a n-1 ]
A-616
Apêndice A Gráficos e GPUs de computação
FIGURA A.8.5 Versão de memória compartilhada da multiplicação de vetor de matriz esparsa.
e um operador associativo binário ⊕ , a função scan calcula a sequência: scan(a, ⊕) = [a 0 ,(a 0 ⊕ a 1 ),...,(a 0 ⊕ a1 ⊕ ... ⊕ a n −1 )] Como exemplo, se consideramos ⊕ como o operador de adição normal, então aplicar scan ao array de entrada a = [317 0 416 3] produzirá esta sequência de somas parciais: scan(a, +) = [3 411111516 22 25]
A.8 Vida real: mapeando aplicações a GPUs A-617
Essa operação de scan é um scan inclusivo, no sentido de que o elemento i da sequência de saída incorpora o elemento ai da entrada. A incorporação apenas dos elementos anteriores geraria um operador de scan exclusivo, também conhecido como operação de soma de prefixo. A implementação serial dessa operação é extremamente simples. Ela é simplesmente um loop que se repete uma vez pela sequência inteira, como mostra a Figura A.8.6. À primeira vista, pode parecer que essa operação é inerentemente serial. Porém, ela pode realmente ser implementada em paralelo eficientemente. A principal observação é que, como a adição é associativa, estamos livres para mudar a ordem em que os elementos são somados. Por exemplo, podemos imaginar a soma de pares de elementos consecutivos em paralelo, e depois a soma dessas somas parciais, e assim por diante. Um esquema simples para fazer isso vem de Hillis e Steele [1989]. Uma implementação de seu algoritmo em CUDA aparece na Figura A.8.7. Ela considera que o array de entrada x[] terá exatamente um elemento por thread do bloco de threads. Ela realiza log2n iterações de um loop coletando somas parciais. Para entender a ação desse loop, considere a Figura A.8.8, que ilustra o caso simples para n = 8 threads e elementos. Cada nível do diagrama representa um passo do loop. As linhas indicam o local do qual os dados estão sendo buscados. Para cada elemento da saída (ou seja, a linha final do diagrama), estamos montando uma árvore de somatório pelos elementos da entrada. As arestas destacadas em azul mostram a forma dessa árvore de somatório para o elemento final. As folhas dessa árvore são todas elementos iniciais. Voltando a partir de qualquer elemento da saída, vemos que ele incorpora todos os valores de entrada até ele mesmo, inclusive. Embora simples, esse algoritmo não é tão eficiente quanto gostaríamos. Examinando a implementação serial, vemos que ela realiza O(n) adições. A implementação paralela, ao
FIGURA A.8.6 Modelo para plus-scan serial.
FIGURA A.8.7 Modelo CUDA para plus-scan paralelo.
A-618
Apêndice A Gráficos e GPUs de computação
FIGURA A.8.8 Referências de dados de varredura paralelos baseados em árvore.
contrário, realiza O(n log n) adições. Por esse motivo, ela não é eficiente ao trabalho, pois realiza mais trabalho que a implementação serial para calcular o mesmo resultado. Felizmente, existem outras técnicas para implementar scan que são eficientes ao trabalho. Os detalhes sobre técnicas de implementação mais eficientes e a extensão desse procedimento por bloco a arrays multiblocos são fornecidos por Sengupta, et al. [2007]. Em alguns casos, podemos só estar interessados em calcular a soma de todos os elementos em um array, em vez de uma sequência de todas as somas de prefixo retornadas por scan. Esse é o problema da redução paralela. Poderíamos simplesmente usar um algoritmo de scan para realizar esse cálculo, mas a redução geralmente pode ser implementada de forma mais eficiente que o scan. A Figura A.8.9 mostra o código para calcular uma redução usando a adição. Nesse exemplo, cada thread simplesmente carrega um elemento da sequência de entrada (ou seja, ela inicialmente soma uma subsequência de comprimento 1). Ao final da redução, queremos que a thread 0 contenha a soma de todos os elementos carregados inicialmente pelas threads de seu bloco. O loop nesse kernel monta implicitamente uma árvore de somatório sobre os elementos de entrada, de modo semelhante ao algoritmo de scan acima. Ao final este loop, a thread 0 contém a soma de todos os valores carregados por esse bloco. Se quisermos que o valor final do local apontado por total contenha o total de todos os elementos no array, temos de combinar as somas parciais de todos os blocos na grade. Uma estratégia para seria fazer com que cada globo escreva sua soma parcial em um segundo array e depois iniciar o kernel de redução novamente, repetindo o processo até que tenhamos reduzido a sequência a um único valor. Uma alternativa mais atraente admitida pela arquitetura de GPU Tesla é usar o primitivo atomicAdd(), um primitivo de leitura-modificação-escrita atômico admitido pelo subsistema de memória. Isso elimina a necessidade de arrays temporários adicionais e partidas de kernel repetidas. A redução paralela é um primitivo essencial para a programação paralela e realça a importância da memória compartilhada por bloco e barreiras de baixo custo para tornar eficiente a cooperação entre as threads. Esse grau de mistura de dados entre as threads seria proibitivamente caro se fosse feito na memória global fora do chip.
Radix sort Uma aplicação importante das primitivas de scan é na implementação de rotinas de classificação. O código na Figura A.8.10 implementa um radix sort de inteiros por um único bloco de threads. Ele aceita como entrada um array values contendo um inteiro de 32 bits para cada thread do bloco. Por eficiência, esse array deverá ser armazenado na
A.8 Vida real: mapeando aplicações a GPUs A-619
FIGURA A.8.9 Implementação CUDA de plus-reduction.
FIGURA A.8.10 Código CUDA para radix sort.
memória compartilhada por bloco, mas isso não é exigido para que a classificação ocorra corretamente. Essa é uma implementação muito simples do radix sort. Ela considera a disponibilidade de um procedimento partition_by_bit() que dividirá o array indicado de modo que todos os valores com um 0 no bit designado chegarão antes de todos os valores com 1 nesse bit. Para produzir a saída correta, esse particionamento deverá ser estável. A implementação de um procedimento de particionamento é uma aplicação simples do scan. A thread i mantém o valor xi e precisa calcular o índice de saída correto para escrever esse valor. Para fazer isso, ela precisa calcular (1) o número de threads j < i para as quais o bit designado é 1 e (2) o número total de bits para os quais o bit designado é 0. O código CUDA para partition_by_bit() aparece na Figura A.8.11. Uma estratégia semelhante pode ser aplicada para implementar um kernel de radix sort que classifica um array de tamanho grande, em vez de apenas um array de um bloco. A
A-620
Apêndice A Gráficos e GPUs de computação
FIGURA A.8.11 Código CUDA para particionar dados com base em cada bit, como parte do radix sort.
etapa fundamental continua sendo o procedimento de scan, embora, quando um cálculo é particionado por múltiplos kernels, devamos dobrar o buffer do array de valores em vez de fazer o particionamento no local. Satish, Harris e Garland [2008] fornecem detalhes para a realização de radix sorts sobre arrays grandes de forma eficiente.
Aplicações N-body em uma GPU1 Nyland, Harris e Prins [2007] descrevem um kernel de cálculo simples, porém útil, com excelente desempenho de GPU — o algoritmo all-pairs N-body. Esse é um componente demorado de muitas aplicações científicas. Simulações N-body calculam a evolução de um sistema de corpos em que cada corpo interage continuamente com cada outro corpo. Um exemplo é uma simulação astrofísica em que cada corpo representa uma estrela individual, e os corpos atraem gravitacionalmente uns aos outros. Outros exemplos são desdobramento de proteína, em que a simulação N-body é usada para calcular forças eletrostáticas e van der Waals; simulação de fluxo de fluido turbulento; e iluminação global em gráficos de computador. O algoritmo all-pairs N-body calcula a força total em cada corpo no sistema pelo cálculo da força de cada par no sistema, somando para cada corpo. Muitos cientistas consideram esse método como o mais preciso, com a única perda de precisão vindo das operações de hardware de ponto flutuante. A desvantagem é sua complexidade de cálculo O(n2), que é muito grande para os sistemas com mais de 106 corpos. Para contornar esse custo alto, várias simplificações foram propostas na geração de algoritmos O(n log n) e O(n);
Adaptado de Nyland, Harris e Prins [2007], “Fast N-Body Simulation with CUDA”, Capítulo 31 de GPU Gems 3. 1
A.8 Vida real: mapeando aplicações a GPUs A-621
alguns exemplos são o algoritmo de Barnes-Hut, o Fast Multipole Method e o somatório Particle-Mesh-Ewald. Todos os métodos rápidos ainda contam com o método all-pais como um kernel para o cálculo preciso de forças de curta duração; assim, ele continua a ser importante. Matemática N-body
Para a simulação gravitacional, calcule a força corpo-corpo usando a física elementar. Entre dois corpos indexados por i e j, o vetor força 3D é: fij = G
mi m j rij
2
×
rij rij
A magnitude da força é calculada no termo da esquerda, enquanto a direção é calculada à direita (vetor unidade apontando de um corpo para o outro). Dada uma lista de corpos interagindo (um sistema inteiro ou um subconjunto), o cálculo é simples: para todos os pares de interações, calcule a força e a soma para cada corpo. Quando as forças totais são calculadas, elas são usadas para atualizar a posição e a velocidade de cada corpo, com base na posição e velocidade anterior. O cálculo das forças tem complexidade O(n2), enquanto a atualização é O(n). O código do cálculo de força serial usa dois loops for aninhados interagindo por pares de corpos. O loop externo seleciona o corpo para o qual a força total está sendo calculada, e o loop interno percorre todos os corpos. O loop interno chama uma função que calcula a força por cada par, depois soma a força a uma soma acumulada. Para calcular as forças em paralelo, atribuímos uma thread a cada corpo, pois o cálculo da força sobre cada corpo é independente do cálculo em todos os outros corpos. Quando todas as forças são calculadas, as posições e velocidades dos corpos podem ser atualizadas. O código para as versões serial e paralela aparece na Figura A.8.12 e na Figura A.8.13. A versão serial tem dois loops for aninhados. A conversão para CUDA, como em muitos outros exemplos, converte o loop externo serial para um kernel por thread, em que cada thread calcula a força total sobre um único corpo. O kernel CUDA calcula uma ID de thread global para cada thread, substituindo a variável repetidora do loop externo serial. Os dois kernels terminam armazenando a aceleração total em um array global usado para calcular os novos valores de posição e velocidade em uma etapa subsequente. O loop externo é substituído por uma grade de kernel CUDA que dispara N threads, um para cada corpo. Otimização para execução da GPU
O código CUDA mostrado está funcionalmente correto, mas não é eficiente, pois ignora recursos arquitetônicos importantes. O melhor desempenho pode ser alcançado com três otimizações principais. Primeiro, a memória compartilhada pode ser usada para evitar
FIGURA A.8.12 Código serial para comparar todas as forças por par em N corpos.
A-622
Apêndice A Gráficos e GPUs de computação
FIGURA A.8.13 Código de thread CUDA para calcular a força total em um único corpo.
leituras de memória idênticas entre as threads. Segundo, o uso de múltiplas threads por corpo melhora o desempenho para valores pequenos de N. Terceiro, o desdobramento de loop reduz o overhead com o loop. Usando memória compartilhada
A memória compartilhada pode manter um subconjunto de posições de corpo, como uma cache, eliminando solicitações de memória redundantes entre as threads. Otimizamos o código mostrado anteriormente para que cada uma das p threads em um bloco de threads carregue uma posição na memória compartilhada (para um total de p posições). Quando todas as threads tiverem carregado um valor na memória compartilhada, garantido por _syncthreads(), cada thread pode então realizar p interações (usando os dados na memória compartilhada). Isso é repetido N/p vezes para completar o cálculo de força para cada corpo, o que reduz o número de solicitações à memória por um fator de p (normalmente, na faixa de 32 a 128). A função chamada accel_on_one_body() requer algumas mudanças para admitir essa otimização. O código modificado aparece na Figura A.8.14. O loop que anteriormente percorria todos os corpos agora salta pela dimensão de bloco p. Cada iteração do loop externo carrega p posições sucessivas na memória compartilhada (uma posição por thread). As threads se sincronizam e depois p cálculos da força são calculados por cada thread. Uma segunda sincronização é necessária para garantir que novos valores não sejam carregados na memória compartilhada antes de todas as threads, completando os cálculos de força com os dados atuais. O uso da memória compartilhada reduz a largura de banda de memória exigida para menos de 10% da largura de banda total que a GPU pode sustentar (usando menos de 5GB/s). Essa otimização mantém a aplicação ocupada realizando cálculo, em vez de esperar os acessos à memória, como aconteceria sem o uso da memória compartilhada. O desempenho para valores variados de N aparece na Figura A.8.15. Usando múltiplas threads por corpo
A Figura A.8.15 mostra a degradação de desempenho para problema com pequenos valores de N (N < 4096) no GeForce 8800 GTX. Muitos esforços de pesquisa que contam com cálculos N-body focalizam em um N pequeno (para longos tempos de simulação), tornando-o um alvo para nossos esforços de otimização. Nossa suposição para explicar o desempenho mais baixo foi que simplesmente não havia trabalho suficiente para manter a GPU ocupada quando N é pequeno. A solução é alocar mais threads por corpo. Mudamos as dimensões do bloco de threads de (p, 1, 1) para (p, q, 1), em que q threads dividem o trabalho de um único corpo em partes iguais. Alocando as threads adicionais dentro do mesmo bloco de threads, resultados parciais podem ser armazenados na memória compartilhada. Quando todos os cálculos de força forem feitos, os q resultados parciais podem ser coletados e somados para calcular o resultado final. O uso de duas ou quatro threads por corpo leva a grandes melhorias para um N pequeno. Como um exemplo, o desempenho no 8800 GTX salta por 110% quando N = 1024 (uma thread alcança 90 GFLOPS, em que quatro alcançam 190 GFLOPS). O desempenho cai
A.8 Vida real: mapeando aplicações a GPUs A-623
FIGURA A.8.14 Código CUDA para calcular a força total em cada corpo, usando memória compartilhada para melhorar o desempenho.
FIGURA A.8.15 Medidas de desempenho da aplicação N-body em um GeForce 8800 GTX e um GeForce 9600. O 8800 tem 128 processadores de stream a 1,35GHz, enquanto o 9600 tem 64 a 0,80GHz (cerca de 30% do 8800). O desempenho de pico é 242 GFLOPS. Para uma GPU com mais processadores, o problema precisa ser maior para alcançar desempenho pleno (o pico de 9600 fica em torno de 2048 corpos, enquanto o 8800 não alcança seu máximo até 16.384 corpos). Para um N pequeno, mais de uma thread por corpo pode melhorar o desempenho significativamente, mas por fim contrai uma penalidade no desempenho à medida que N cresce.
ligeiramente em um N grande, de modo que só usamos essa otimização para N menores que 4096. Os aumentos de desempenho aparecem na Figura A.8.15 para uma GPU com 128 processadores e uma GPU menor com 64 processadores com clock a dois terços da velocidade.
Comparação de desempenho O desempenho do código N-body aparece nas Figuras A.8.15 e A.8.16. Na Figura A.8.15, podemos ver o desempenho das GPUs como alto e médio, junto com as melhorias alcançadas usando múltiplas threads por corpo. O desempenho na GPU mais rápida varia de 90 a pouco menos de 250 GFLOPS.
A-624
Apêndice A Gráficos e GPUs de computação
A Figura A.8.16 mostra um código quase idêntico (C + + versus CUDA) rodando em CPUs Core2 da Intel. O desempenho da CPU é cerca de 1% da GPU, na faixa de 0,2 a 2 GFLOPS, permanecendo quase constante pela grande gama de tamanhos de problema. O gráfico também mostra os resultados da compilação da versão CUDA do código para uma CPU, em que o desempenho melhora em 24%. CUDA, como linguagem de programação, expõe paralelismo, permitindo que o compilador faça melhor uso da unidade de vetor SSE em um único core. A versão CUDA do código N-body também é naturalmente mapeada para CPUs multicore (com grades de blocos), nas quais alcança expansão quase perfeita em um sistema de oito cores com N = 4096 (razões de 2,0, 3,97 e 7,94 em dois, quatro e oito cores, respectivamente). Resultados
Com um esforço modesto, desenvolvemos um kernel de cálculo que melhora o desempenho da GPU por CPUs multicore por um fator de até 157. O tempo de execução para o código N-body rodando em uma CPU recente da Intel (Penryn X9775 a 3.2 GHz, único core) levou mais de 3 segundos por frame para rodar o mesmo código que roda a uma taxa de frame de 44 Hz em uma GPU GeForce 8800. Em CPUs anteriores ao Penryn, o código exige 6-16 segundos, e nos processadores Core2 mais antigos e no processador Pentium IV, o tempo é de cerca de 25 segundos. Temos de dividir o aumento aparente no desempenho ao meio, pois a CPU requer apenas metade dos cálculos para gerar o mesmo resultado (usando a otimização de que as forças em um par de corpos são iguais em força e opostas em direção). Como a GPU pode agilizar o código tanto assim? A resposta exige uma inspeção nos detalhes arquitetônicos. O cálculo da força por par requer 20 operações de ponto flutuante, compreendendo principalmente instruções de adição e multiplicação (algumas das quais podem ser combinadas usando-se uma instrução de multiplicação-adição), mas também existem instruções de divisão e raiz quadrada para normalização de vetor. As CPUs da Intel utilizam muitos ciclos para instruções de divisão e raiz quadrada com precisão simples,2
FIGURA A.8.16 Medidas de desempenho no código N-body em uma CPU. O gráfico mostra o desempenho N-body de precisão simples usando CPUs Core2 da Intel, indicados por seu número de modelo de CPU. Observe a drástica redução no desempenho em GFLOPS (mostrado em GFLOPS no eixo y), demonstrando o quão mais rápida é a GPU em comparação com a CPU. O desempenho na CPU geralmente independe do tamanho do problema, exceto para um desempenho anomalamente baixo quando N = 16,384 na CPU X9775. O gráfico também mostra os resultados da execução da versão CUDA do código (usando o compilador CUDA-for-CPU) em um único core da CPU, em que ele supera o código C + + em 24%. Como uma linguagem de programação, CUDA expõe paralelismo e localidade, que um compilador pode explorar. As CPUs da Intel são um Extreme X9775 a 3,2GHz (apelidado de “Penryn”), um E8200 a 2,66GHz (apelidado de “Wolfdale”), um desktop, CPU anterior ao Penryn, e um T2400 a 1,83GHz (apelidado de “Yonah”), uma CPU de laptop de 2007. A versão Penryn da arquitetura Core 2 é particularmente interessante para cálculos de N-body com seu divisor de 4 bits, permitindo que operações de divisão e raiz quadrada sejam executadas quatro vezes mais rápido que as CPUs Intel anteriores.
As instruções SSE do x86 de raiz quadrada recíproca (RSQRT*) e recíproca (RCP*) não foram consideradas, pois sua precisão é muito baixa para ser comparável.
2
A.8 Vida real: mapeando aplicações a GPUs A-625
embora isso tenha melhorado na família de CPUs Penryn mais recente, com seu divisor de 4 bits mais rápido.3 Além do mais, as limitações na capacidade de registrador levam a muitas instruções MOV no código x86 (supostamente de/para a cache L1). Ao contrário, o GeForce 8800 executa uma instrução de thread de raiz quadrada recíproca em quatro clocks; veja na Seção A.6 a precisão da função especial. Ele tem um arquivo de registradores maior (por thread) e memória compartilhada que pode ser acessada como um operando de instrução. Finalmente, o compilador CUDA emite 15 instruções para uma interação do loop, em comparação com mais de 40 instruções de uma série de compiladores de CPU x86. Maior paralelismo, execução mais rápida de instruções complexas, mais espaço de registrador e um compilador eficiente se combinam para explicar a melhoria de desempenho dramático do código N-body entre a CPU e a GPU. Em um GeForce 8800, o algoritmo all-pairs N-body emite mais de 240 GFLOPS de desempenho, em comparação com menos de 2 GFLOPS nos processadores sequenciais recentes. A compilação e execução da versão CUDA do código em uma CPU demonstra que o programa se expande bem para CPUs multicore, mas ainda é significativamente mais lento que uma única GPU. Acoplamos a simulação N-body da GPU com uma exibição gráfica do movimento, e podemos exibir interativamente 16K corpos interagindo a 44 frames por segundo. Isso permite que eventos astrofísicos e biofísicos sejam exibidos e navegados em taxas interativas. Além disso, podemos parametrizar muitas configurações, como redução de ruído, damping e técnicas de integração, exibindo imediatamente seus efeitos sobre a dinâmica do sistema. Isso oferece aos cientistas uma imagem visual incrível, melhorando suas percepções sobre sistemas de outra forma invisíveis (muito grandes ou pequenos, muito rápidos ou muito lentos), permitindo-lhes criar melhores modelos dos fenômenos físicos. A Figura A.8.17 mostra uma exibição de série de tempo de uma simulação astrofísica de 16K corpos, com cada corpo atuando como uma galáxia. A configuração inicial é uma
FIGURA A.8.17 Doze imagens capturadas durante a evolução de um sistema N-body com 16.384 corpos.
3 Intel Corporation, Intel 64 and IA-32 Architectures Optimization Reference Manual. Novembro de 2007. Order Number: 248966-016. Também disponível em www3.intel.com/design/processor/ manuals/248966.pdf.
A-626
Apêndice A Gráficos e GPUs de computação
concha esférica de corpos girando em torno do eixo z. Um fenômeno de interesse para os astrofísicos é o agrupamento que ocorre, juntamente com a mistura de galáxias com o tempo. Para o leitor interessado, o código CUDA para essa aplicação está disponível no SDK CUDA, em www.nvidia.com/CUDA.
A.9 Falácias e armadilhas As GPUs evoluíram e mudaram tão rapidamente que surgiram muitas falácias e armadilhas. Abordamos algumas delas aqui. Falácia: GPUs são apenas multiprocessadores de vetor SIMD. É fácil chegar à falsa conclusão de que as GPUs são simplesmente multiprocessador de vetor SIMD. GPUs possuem um modelo de programação em estilo SPMD, pois um programador pode escrever um único programa que é executado em múltiplas instâncias de thread com múltiplos dados. Contudo, a execução dessas threads não é puramente SIMD ou vetor; ela é SingleInstruction Multiple-Thread (SIMT), descrito na Seção A.4. Cada thread da GPU tem seus próprios registradores escalares, memória particular da thread, estado de execução da thread, ID de thread, execução independente, caminho de desvio e contador de programa eficaz, além de poder endereçar a memória independentemente. Embora um grupo de threads (por exemplo, um warp de 32 threads) seja executado mais eficientemente quando os PCs para as threads são os mesmos, isso não é necessário. Assim, os multiprocessadores não são puramente SIMD. O modelo de execução de thread é MIMD com sincronização de barreira e otimizações SIMT. A execução é mais eficiente se os acessos individuais de load/store da thread também puderem ser reunidos em acessos em bloco. Porém, isso não é estritamente necessário. Em uma arquitetura de vetor puramente SIMD, os acessos à memória/registrador para diferentes threads precisam ser alinhados em um padrão de vetor regular. Uma GPU não possui tal restrição para acessos a registrador ou memória; porém, a execução é mais eficiente se os warps de threads acessarem blocos de dados locais. Saindo ainda mais de um modelo SIMD puro, uma GPU SIMT pode executar mais de um warp de threads simultaneamente. Em aplicações gráficas, pode haver vários grupos de programas de vértice, programas de pixel e programas de geometria rodando no array multiprocessador simultaneamente. Os programas de cálculo também podem executar diferentes programas simultaneamente em diferentes warps. Falácia: o desempenho da GPU não pode ficar mais rápido do que a lei de Moore. A lei de Moore é simplesmente uma taxa. Ela não é um limite da “velocidade da luz” para qualquer outra taxa. A lei de Moore descreve uma expectativa de que, com o tempo, à medida que a tecnologia de semicondutores avança e os transistores se tornam menores, o custo de manufatura por transistor cairá exponencialmente. Em outras palavras, dado um custo de manufatura constante, o número de transistores aumentará exponencialmente. Gordon Moore [1965] previu que essa progressão ofereceria aproximadamente duas vezes o número de transistores para o mesmo custo de manufatura a cada ano, e mais tarde o revisou para dobrar a cada dois anos. Embora Moore fizesse a previsão inicial em 1965, quando havia apenas 50 componentes por circuito integrado, ele provou ser muito coerente. A redução do tamanho do transistor historicamente teve outros benefícios, como menor potência por transistor e velocidades de clock mais rápidas em potência constante. Essa generosidade crescente de transistores é usada por arquiteturas de chip para criar processadores, memória e outros componentes. Por algum tempo, os projetistas de CPU usaram os transistores extras para aumentar o desempenho do processador em uma taxa semelhante à lei de Moore, tanto que muitas pessoas acham que o crescimento do desempenho do processador de duas vezes a cada 18-24 meses é a lei de Moore. Na verdade, não é. Os projetistas de microprocessador gastam alguns dos novos transistores nos cores do processador, melhorando a arquitetura e o projeto, e realizando o pipelining para mais
A.9 Falácias e armadilhas A-627
velocidade de clock. O restante dos novos transistores é usado para oferecer mais cache e tornar o acesso à memória mais rápido. Ao contrário, os projetistas de GPU utilizam quase nenhum dos novos transistores para oferecer mais cache; a maioria dos transistores é usada para melhorar os cores do processador e acrescentar mais cores do processador. As GPUs ficam mais rápidas por quatro mecanismos. Primeiro, os projetistas de GPU colhem os frutos da lei de Moore diretamente pela aplicação de exponencialmente mais transistores na montagem de processadores mais paralelos e, portanto, mais rápidos. Segundo, os projetistas de GPU podem melhorar a arquitetura com o tempo, aumentando a eficiência do processamento. Terceiro, a lei de Moore considera custo constante, de modo que a taxa da lei de Moore claramente pode ser excedida gastando-se mais para chips maiores com mais transistores. Quarto, os sistemas de memória da GPU aumentaram sua largura de banda efetiva em um ritmo quase comparável à taxa de processamento, usando memórias mais rápidas, memórias mais largas, compactação de dados e caches melhores. A combinação dessas quatro técnicas historicamente tem permitido que o desempenho da GPU dobre regularmente, a cada 12 a 18 meses mais ou menos. Essa taxa, ultrapassando a taxa da lei de Moore, foi demonstrada em aplicações gráficas por aproximadamente dez anos, e não mostra sinais de diminuição de velocidade significativa. O limitador de taxa mais desafiador parece ser o sistema de memória, mas a inovação competitiva também está avançando isso rapidamente. Falácia: a GPUs exibem gráficos 3D; elas não podem fazer cálculos gerais. As GPUs são criadas para exibir gráficos 3D e também gráficos e vídeo 3D. Para atender as demandas do desenvolvedor de software gráfico expressas nas interfaces e requisitos de desempenho/ recursos das APIs gráficas, as GPUs se tornaram processadores de ponto flutuante programáveis maciçamente paralelos. No domínio gráfico, esses processadores são programados por meio das APIs gráficas e com linguagens de programação gráfica misteriosas (GLSL, Cg e HLSL, em OpenGL e Direct3D). Porém, não há nada que impeça os arquitetos de GPU de exporem os cores de processador paralelo a programadores sem a API gráfica ou as linguagens gráficas misteriosas. De fato, a família de GPUs da arquitetura Tesla expõe os processadores por meio de um ambiente de software conhecido como CUDA, que permite que os programadores desenvolvam programas de aplicação gerais usando a linguagem C e logo C + +. As GPUs são processadores completos de Turing, de modo que podem executar qualquer programa que uma CPU pode executar, embora talvez não tão bem. E talvez mais rápido. Falácia: GPUs não podem executar programas de ponto flutuante com precisão dupla com rapidez. No passado, as GPUs não podiam executar programa algum de ponto flutuante com precisão dupla. E isso não é muito rápido de forma alguma. As GPUs progrediram da representação aritmética indexada (tabelas de pesquisa para cores) para inteiros de 8 bits por componente de cor, aritmética de ponto fixo, ponto flutuante com precisão simples e, recentemente, adicionaram a precisão dupla. As GPUs modernas realizam praticamente todos os cálculos em aritmética de ponto flutuante IEEE com precisão simples, e estão começando a usar também a precisão dupla. Por um pequeno custo adicional, uma GPU pode admitir ponto flutuante com precisão dupla além de ponto flutuante com precisão simples. Hoje, a precisão dupla roda mais lentamente que a velocidade de precisão simples, cerca de cinco a dez vezes mais lenta. Pelo custo adicional incremental, o desempenho da precisão dupla pode ser aumentado com relação à precisão simples em estágios, conforme a maioria das aplicações exige. Falácia: GPUs não computam ponto flutuante corretamente. As GPUs, pelo menos na família de processadores da arquitetura Tesla, realizam o processamento de ponto flutuante com precisão simples em um nível prescrito pelo padrão de ponto flutuante IEEE 754. Assim, em termos de precisão, as GPUs são iguais a quaisquer outros processadores compatíveis com IEEE 754. Hoje, as GPUs não implementam alguns dos recursos específicos descritos no padrão, como o tratamento de números desnormalizados e o oferecimento de exceções de
A-628
Apêndice A Gráficos e GPUs de computação
ponto flutuante precisas. Porém, a GPU Tesla T10P, introduzida recentemente, oferece arredondamento IEEE completo, multiplicação-adição fundida e suporte para número desnormalizado para precisão dupla. Armadilha: só use mais threads para cobrir latências de memória maiores. Os cores da CPU normalmente são projetados para executar uma única thread em velocidade total. Para executar em velocidade total, cada instrução e seus dados precisam estar disponíveis quando for o momento para essa instrução executar. Se a próxima instrução não estiver pronta ou os dados exigidos para essa instrução não estiverem disponíveis, a instrução não poderá ser executada e o processador emite um stall. A memória externa está distante do processador, de modo que exige muitos ciclos de execução desperdiçados para apanhar dados da memória. Consequentemente, as CPUs exigem grandes caches locais para continuar executando sem stalling. A latência da memória é longa, de modo que é evitada esforçando-se para executar na cache. Em algum ponto, as demandas do conjunto de trabalho do programa podem ser maiores que qualquer cache. Algumas CPUs têm usado o multithreading para tolerar a latência, mas o número de threads por core geralmente tem sido limitado a um pequeno número. A estratégia da GPU é que diferentes cores da GPU sejam projetados para executar muitas threads simultaneamente, mas apenas uma instrução de qualquer thread de uma vez. Outra forma de dizer isso é que uma GPU executa cada thread lentamente, mas em conjunto executa as threads de uma maneira mais eficiente. Cada thread pode tolerar alguma quantidade de latência da memória, pois outras threads podem executar. A desvantagem disso é que múltiplas threads são necessárias para cobrir a latência da memória. Além disso, se os acessos à memória forem espalhados e não correlacionados entre as threads, o sistema de memória será cada vez mais lento na resposta a cada solicitação individual. Por fim, até mesmo as múltiplas threads não serão capazes de cobrir a latência. Assim, a armadilha é que, para a estratégia de trabalho “só usar mais threads” para cobrir a latência, você precisa ter threads suficientes, e as threads precisam ser bem comportadas em termos de localidade do acesso à memória. Falácia: algoritmos O(n) são difíceis de aumentar a velocidade. Não importa como a GPU é rápida no processamento de dados, as etapas de transferência de dados de e para o dispositivo podem limitar o desempenho dos algoritmos com a complexidade O(n) (com uma pequena quantidade de trabalho por dado). A taxa de transferência mais alta em relação ao barramento PCIe é aproximadamente 48GB/segundo quando são usadas transferências de DMA, e ligeiramente menor para transferências não DMA. A CPU, pelo contrário, tem velocidades de acesso típicas de 8-12GB/segundo para a memória do sistema. Problemas de exemplo, como a adição em vetor, serão limitados pela transferência de entradas à GPU e a saída de retorno do cálculo. Há três maneiras de contornar o custo de transferência de dados. Primeiro, tente deixar os dados na GPU pelo máximo de tempo possível, em vez de mover os dados para lá e para cá em diferentes etapas de um algoritmo complicado. CUDA deliberadamente deixa dados na GPU entre as partidas para dar suporte a isso. Segundo, a GPU admite as operações simultâneas de copy-in, copy-out e cálculo, de modo que os dados podem ser enviados para dentro e fora do dispositivo enquanto ele está calculando. Esse modelo é útil para qualquer stream de dados que possa ser processado enquanto eles chegam. Alguns exemplos são processamento de vídeo, roteamento de rede, compactação/descompactação de dados e até mesmo cálculos simples, como matemática com grandes vetores. A terceira sugestão é usar a CPU e a GPU juntas, melhorando o desempenho ao atribuir um subconjunto do trabalho a cada um, tratando o sistema como uma plataforma de computação heterogênea. O modelo de programação CUDA admite a alocação de trabalho a uma ou mais GPUs junto com o uso continuado da CPU, sem o uso de threads (por meio de funções assíncronas da GPU), de modo que é relativamente simples manter todas as GPUs e uma CPU trabalhando simultaneamente para solucionar problemas ainda mais rapidamente.
A.11 Perspectiva histórica e leitura adicional A-629
A.10 Comentários finais As GPUs são processadores maciçamente paralelos e se tornaram bastante utilizados, não apenas para gráficos 3D, mas também para muitas outras aplicações. Essa ampla aplicação foi possibilitada pela evolução de dispositivos gráficos nos processadores programáveis. O modelo de programação de aplicação gráfica para GPUs normalmente é uma API como DirectX™ ou OpenGL™. Para uma computação de uso geral, o modelo de programação CUDA usa um estilo SPMD (Single-Program Multiple Data), executando um programa com muitas threads paralelas. O paralelismo da GPU continuará a se expandir segundo a lei de Moore, principalmente aumentando o número de processadores. Somente os modelos de programação paralela que podem se expandir prontamente para centenas de cores de processador e milhares de threads terão sucesso no suporte a GPUs e CPUs manycore. Além disso, somente as aplicações que tiverem muitas tarefas paralelas em grande parte independentes serão aceleradas pelas arquiteturas manycore maciçamente paralelas. Os modelos de programação paralela para GPUs estão se tornando mais flexíveis, tanto para gráficos quanto para a computação paralela. Por exemplo, CUDA está evoluindo rapidamente na direção da funcionalidade plena do C/C + +. APIs gráficas e modelos de programação provavelmente se adaptarão às capacidades de computação paralela e modelos CUDA. Seu modelo de threading no estilo SPMD é escalável, e é um modelo conveniente, sucinto e facilmente aprendido, para expressar grandes quantidades de paralelismo. Impulsionada por essas mudanças nos modelos de programação, a arquitetura de GPU, por sua vez, está se tornando mais flexível e mais programável. As unidades de função fixa da GPU estão se tornando acessíveis a partir de programas em geral, acompanhando o modo como os programas CUDA já utilizam funções intrínsecas de textura para realizar pesquisas de textura usando a instrução de textura e unidade de textura da GPU. A arquitetura da GPU continuará a se adaptar aos padrões de uso de gráficos e outros programadores de aplicação. As GPUs continuarão a se expandir para incluir mais poder de processamento através de cores de processador adicionais, além de aumentar a largura de banda de thread e memória disponível para os programas. Além disso, os modelos de programação precisam evoluir para incluir a programação de sistemas manycore heterogêneos, incluindo GPUs e CPUs.
Agradecimentos Este apêndice é o trabalho de vários autores sobre NVIDIA. Somos gratos às contribuições significativas de Michael Garland, John Montrym, Doug Voorhies, Lars Nyland, Erik Lindholm, Paulius Micikevicius, Massimiliano Fatica, Stuart Oberman e Vasily Volkov.
A.11
Perspectiva histórica e leitura adicional
Esta seção, que aparece no site, analisa a história das unidades de processamento gráfico (GPUs) em tempo real programáveis desde o início dos anos 80 até hoje, à medida que diminuíram de preço por duas ordens de grandeza e aumentaram o desempenho também por duas ordens de grandeza. Ela acompanha a evolução da GPU desde pipelines de função fixa até os processadores gráficos programáveis, com perspectivas sobre computação de GPU, gráficos unificados e processadores de cálculo, computação visual e GPUs escaláveis.
B A
P
Ê
N
D
I
C
E
O receio do insulto sério não pode justificar sozinho a supressão da livre expressão. Louis Brandeis, Whitney v. California, 1927
Montadores, Link-editores e o Simulador SPIM James R. Larus Microsoft Research, Microsoft
B.1 Introdução 631 B.2 Montadores 636 B.3 Link-editores 642 B.4 Carga 643 B.5
Uso da memória 643
B.6
Convenção para chamadas de procedimento 645
B.7
Exceções e interrupções 654
B.8
Entrada e saída 658
B.9 SPIM 659 B.10
Assembly do MIPS R2000 663
B.11
Comentários finais 690
B.12 Exercícios 691
B.1 Introdução Codificar instruções como números binários é algo natural e eficiente para os computadores. Os humanos, porém, têm muita dificuldade para entender e manipular esses números. As pessoas leem e escrevem símbolos (palavras) muito melhor do que longas sequências de dígitos. O Capítulo 2 mostrou que não precisamos escolher entre números e palavras, pois as instruções do computador podem ser representadas de muitas maneiras. Os humanos podem escrever e ler símbolos, e os computadores podem executar os números binários equivalentes. Este apêndice descreve o processo pelo qual um programa legível ao ser humano é traduzido para um formato que um computador pode executar, oferece algumas dicas sobre a escrita de programas em assembly e explica como executar esses programas no SPIM, um simulador que executa programas MIPS. As versões UNIX, Windows e Mac OS X do simulador SPIM estão disponíveis no site. Assembly é a representação simbólica da codificação binária – linguagem de máquina – de um computador. O assembly é mais legível do que a linguagem de máquina porque utiliza símbolos no lugar de bits. Os símbolos no assembly nomeiam padrões de bits que ocorrem comumente, como opcodes (códigos de operação) e especificadores de registradores, de modo que as pessoas possam ler e lembrar-se deles. Além disso, o assembly permite que os programadores utilizem rótulos para identificar e nomear palavras particulares da memória que mantêm instruções ou dados. Uma ferramenta chamada montador traduz do assembly para instruções binárias. Os montadores oferecem uma representação mais amigável do que os 0s e 1s de um computador, o que simplifica a escrita e a leitura de programas. Nomes simbólicos para operações e locais são uma faceta dessa representação. Outra faceta são as facilidades de programação que aumentam a clareza de um programa. Por exemplo, as macros, discutidas na Seção B.2, permitem que um programador estenda o assembly, definindo novas operações. Um montador lê um único arquivo-fonte em assembly e produz um arquivo-objeto com instruções de máquina e informações de trabalho que ajudam a combinar vários arquivosobjeto em um programa. A Figura B.1.1 ilustra como um programa é montado. A maioria dos programas consiste em vários arquivos – também chamados de módulos – que são escritos, compilados e montados de forma independente. Um programa também pode usar rotinas pré-escritas fornecidas em uma biblioteca de programa. Um módulo normalmente contém referências a sub-rotinas e dados definidos em outros módulos e em bibliotecas. O código em um módulo não pode ser executado quando contém referências não resolvidas para rótulos em outros arquivos-objeto ou bibliotecas. Outra ferramenta,
linguagem de máquina A representação binária utilizada para a comunicação dentro de um sistema computacional.
montador Um programa que traduz uma versão simbólica de uma instrução para a versão binária. macro Uma facilidade de combinação e substituição de padrões que oferece um mecanismo simples para nomear uma sequência de instruções utilizada com frequência.
referência não resolvida Uma referência que exige mais informações de um arquivo externo para estar completa.
B-632
Apêndice B Montadores, Link-editores e o Simulador SPIM
FIGURA B.1.1 O processo que produz um arquivo executável. Um montador traduz um arquivo em assembly para um arquivo-objeto, que é link-editado a outros arquivos e bibliotecas para um arquivo executável.
link-editor Também chamado linker. Um programa de sistema que combina programas em linguagem de máquina montados independentemente e resolve todos os rótulos indefinidos em um arquivo executável.
chamada link-editor, combina uma coleção de arquivos-objeto e biblioteca em um arquivo executável, que um computador pode executar. Para ver a vantagem do assembly, considere a sequência de figuras mostrada a seguir, todas contendo uma pequena sub-rotina que calcula e imprime a soma dos quadrados dos inteiros de 0 a 100. A Figura B.1.2 mostra a linguagem de máquina que um computador MIPS executa. Com esforço considerável, você poderia usar as tabelas de formato de opcode e instrução do Capítulo 2 para traduzir as instruções em um programa simbólico, semelhante à Figura B.1.3. Essa forma de rotina é muito mais fácil de ler, pois as operações e os operandos estão escritos com símbolos, em vez de padrões de bits. No entanto, esse assembly ainda é difícil de acompanhar, pois os locais da memória são indicados por seus endereços, e não por um rótulo simbólico.
FIGURA B.1.2 Código em linguagem de máquina MIPS para uma rotina que calcula e imprime a soma dos quadrados dos inteiros entre 0 a 100.
diretiva do montador Uma operação que diz ao montador como traduzir um programa, mas não produz instruções de máquina; sempre começa com um ponto.
A Figura B.1.4 mostra o assembly que rotula endereços de memória com nomes mnemônicos. A maior parte dos programadores prefere ler e escrever dessa forma. Os nomes que começam com um ponto, por exemplo, .data e .globl, são diretivas do montador, que dizem ao montador como traduzir um programa, mas não produzem instruções de máquina. Nomes seguidos por um sinal de dois-pontos, como str: ou main:, são rótulos que denominam o próximo local da memória. Esse programa é tão legível quanto a maioria dos programas em assembly (exceto por uma óbvia falta de comentários), mas ainda é difícil de acompanhar, pois muitas operações simples são exigidas para realizar tarefas simples e porque a falta de construções de fluxo de controle do assembly oferece poucos palpites sobre a operação do programa.
B.1 Introdução B-633
FIGURA B.1.3 A mesma rotina escrita em assembly. Entretanto, o código para a rotina não rotula registradores ou locais de memória, nem inclui comentários.
FIGURA B.1.4 A mesma rotina escrita em assembly com rótulos, mas sem comentários. Os comandos que começam com pontos são diretivas do montador (ver páginas A-34 a A-35). .text indica que as linhas seguintes contêm instruções. .data indica que elas contêm dados. .align n indica que os itens nas linhas seguintes devem ser alinhados em um limite de 2n bytes. Logo, .align 2 significa que o próximo item deverá estar em um limite da palavra. .globl main declara que main é um símbolo global, que deverá ser visível ao código armazenado em outros arquivos. Finalmente, .asciiz armazena na memória uma string terminada em nulo.
Ao contrário, a rotina em C na Figura B.1.5 é mais curta e mais clara, pois as variáveis possuem nomes mnemônicos e o loop é explícito, em vez de construído com desvios. Na verdade, a rotina em C é a única que escrevemos. As outras formas do programa foram produzidas por um compilador C e um montador. Em geral, o assembly desempenha duas funções (ver Figura B.1.6). A primeira é como uma linguagem de saída dos compiladores. Um compilador traduz um programa escrito em uma linguagem de alto nível (como C ou Pascal) para um programa equivalente em linguagem de máquina ou assembly. A linguagem de alto nível é chamada de linguagemfonte, e a saída do compilador é sua linguagem destino.
linguagem-fonte A linguagem de alto nível em que um programa é escrito originalmente.
B-634
Apêndice B Montadores, Link-editores e o Simulador SPIM
FIGURA B.1.5 A rotina escrita na linguagem de programação C.
FIGURA B.1.6 O assembly é escrito por um programador ou é a saída de um compilador.
A outra função do assembly é como uma linguagem para a escrita de programas. Essa função costumava ser a dominante. Hoje, porém, devido a memórias maiores e compiladores melhores, a maioria dos programadores utiliza uma linguagem de alto nível e raramente ou nunca vê as instruções que um computador executa. Apesar disso, o assembly ainda é importante para a escrita de programas em que a velocidade ou o tamanho são fundamentais, ou para explorar recursos do hardware que não possuem correspondentes nas linguagens de alto nível. Embora este apêndice dê destaque ao assembly do MIPS, a programação assembly na maioria das outras máquinas é muito semelhante. As outras instruções e os modos de endereçamento nas máquinas CISC, como o VAX, podem tornar os programas assembly mais curtos, mas não mudam o processo de montagem de um programa, nem oferecem ao assembly as vantagens das linguagens de alto nível, como verificação de tipos e fluxo de controle estruturado.
Quando usar o assembly O motivo principal para programar em assembly, em vez de outra linguagem de alto nível, é que a velocidade ou o tamanho de um programa têm extrema importância. Por exemplo, imagine um computador que controla um mecanismo qualquer, como os freios de um carro. Um computador incorporado em outro dispositivo, como um carro, é chamado de computador embutido. Esse tipo de computador precisa responder rápida e previsivelmente aos eventos no mundo exterior. Como um compilador introduz incerteza sobre o custo de tempo das operações, os programadores podem achar difícil garantir que um programa em linguagem de alto nível responderá dentro de um intervalo de tempo definido – digamos, 1 milissegundo após um sensor detectar que um pneu está derrapando. Um programador assembly, por outro lado, possui mais controle sobre as instruções executadas. Além disso, em aplicações embutidas, reduzir o tamanho de um programa, de modo que caiba em menos chips de memória, reduz o custo do computador embutido. Uma técnica híbrida, em que a maior parte de um programa é escrita em uma linguagem de alto nível e seções fundamentais são escritas em assembly, aproveita os pontos fortes das duas linguagens. Os programas normalmente gastam a maior parte do seu tempo executando uma pequena fração do código-fonte do programa. Essa observação é exatamente o princípio da localidade em que as caches se baseiam (veja Seção 5.1, no Capítulo 5).
B.1 Introdução B-635
O perfil do programa mede onde um programa gasta seu tempo e pode localizar as partes de tempo crítico de um programa. Em muitos casos, essa parte do programa pode se tornar mais rápida com melhores estruturas de dados ou algoritmos. No entanto, às vezes, melhorias de desempenho significativas só são obtidas com a recodificação de uma parte crítica de um programa em assembly. Essa melhoria não é necessariamente uma indicação de que o compilador da linguagem de alto nível falhou. Os compiladores costumam ser melhores do que os programadores na produção uniforme de código de máquina de alta qualidade por um programa inteiro. Entretanto, os programadores entendem os algoritmos e o comportamento de um programa em um nível mais profundo do que um compilador e podem investir esforço e engenhosidade consideráveis melhorando pequenas seções do programa. Em particular, os programadores em geral consideram vários procedimentos simultaneamente enquanto escrevem seu código. Os compiladores compilam cada procedimento isoladamente e precisam seguir convenções estritas governando o uso dos registradores nos limites de procedimento. Retendo valores comumente usados nos registradores, até mesmo entre os limites dos procedimentos, os programadores podem fazer um programa ser executado com mais rapidez. Outra vantagem importante do assembly está na capacidade de explorar instruções especializadas, por exemplo, instruções de cópia de string ou combinação de padrões. Os compiladores, na maior parte dos casos, não podem determinar se um loop de programa pode ser substituído por uma única instrução. Contudo, o programador que escreveu o loop pode substituí-lo facilmente por uma única instrução. Hoje, a vantagem de um programador sobre um compilador tornou-se difícil de manter, pois as técnicas de compilação melhoram e os pipelines das máquinas aumentaram de complexidade (Capítulo 4). O último motivo para usar assembly é que não existe uma linguagem de alto nível disponível em um computador específico. Computadores muito antigos ou especializados não possuem um compilador, de modo que a única alternativa de um programador é o assembly.
Desvantagens do assembly O assembly possui muitas desvantagens, que argumentam fortemente contra seu uso generalizado. Talvez sua principal desvantagem seja que os programas escritos em assembly são inerentemente específicos à máquina e precisam ser reescritos para serem executados em outra arquitetura de computador. A rápida evolução dos computadores, discutida no Capítulo 1, significa que as arquiteturas se tornam obsoletas. Um programa em assembly permanece firmemente ligado à sua arquitetura original, mesmo depois que o computador for substituído por máquinas mais novas, mais rápidas e mais econômicas. Outra desvantagem é que os programas em assembly são maiores do que os programas equivalentes escritos em uma linguagem de alto nível. Por exemplo, o programa em C da Figura B.1.5 possui 11 linhas de extensão, enquanto o programa em assembly da Figura B.1.4 possui 31 linhas. Em programas mais complexos, a razão entre o assembly e a linguagem de alto nível (seu fator de expansão) pode ser muito maior do que o fator de três nesse exemplo. Infelizmente, estudos empíricos mostraram que os programadores escrevem quase o mesmo número de linhas de código por dia em assembly e em linguagens de alto nível. Isso significa que os programadores são aproximadamente x vezes mais produtivos em uma linguagem de alto nível, onde x é o fator de expansão do assembly. Para aumentar o problema, programas maiores são mais difíceis de ler e entender e contêm mais bugs. O assembly realça esse problema, devido à sua completa falta de estrutura. Os idiomas de programação comuns, como instruções if-then e loops, precisam ser criados a partir de desvios e jumps. Os programas resultantes são difíceis de ler, pois o leitor precisa recriar cada construção de nível mais alto a partir de suas partes, e cada instância de uma instrução pode ser ligeiramente diferente. Por exemplo, veja a Figura B.1.4 e responda a estas perguntas: que tipo de loop é utilizado? Quais são seus limites inferior e superior?
B-636
Apêndice B Montadores, Link-editores e o Simulador SPIM
Detalhamento: Os compiladores podem produzir linguagem de máquina diretamente, em vez de contar com um montador. Esses compiladores executam muito mais rapidamente do que aqueles que invocam um montador como parte da compilação. Todavia, um compilador que gera linguagem de máquina precisa realizar muitas tarefas que um montador normalmente trata, como resolver endereços e codificar instruções como números binários. A escolha é entre velocidade de compilação e simplicidade do compilador.
Detalhamento: Apesar dessas considerações, algumas aplicações embutidas são escritas em uma linguagem de alto nível. Muitas dessas aplicações são programas grandes e complexos, que precisam ser muito confiáveis. Os programas em assembly são maiores e mais difíceis de escrever e ler do que os programas em linguagem de alto nível. Isso aumenta bastante o custo da escrita de um programa em assembly e torna muito difícil verificar a exatidão desse tipo de programa. Na verdade, essas considerações levaram o Departamento de Defesa, que paga por muitos sistemas embutidos complexos, a desenvolver a Ada, uma nova linguagem de alto nível para a escrita de sistemas embutidos.
B.2 Montadores
rótulo externo Também chamado rótulo global. Um rótulo que se refere a um objeto que pode ser referenciado a partir de arquivos diferentes daquele em que está definido. rótulo local Um rótulo que se refere a um objeto que só pode ser usado dentro do arquivo em que está definido.
Um montador traduz um arquivo de instruções em assembly para um arquivo de instruções de máquina binárias e dados binários. O processo de tradução possui duas etapas principais. A primeira etapa é encontrar locais de memória com rótulos, de modo que o relacionamento entre os nomes simbólicos e endereços é conhecido quando as instruções são traduzidas. A segunda etapa é traduzir cada instrução assembly combinando os equivalentes numéricos dos opcodes, especificadores de registradores e rótulos em uma instrução válida. Como vemos na Figura B.1.1, o montador produz um arquivo de saída, chamado de arquivo-objeto, que contém as instruções de máquina, dados e informações de manutenção. Um arquivo-objeto normalmente não pode ser executado porque referencia procedimentos ou dados em outros arquivos. Um rótulo é externo (também chamado global) se o objeto rotulado puder ser referenciado a partir de arquivos diferentes de onde está definido. Um rótulo é local se o objeto só puder ser usado dentro do arquivo em que está definido. Na maior parte dos montadores, os rótulos são locais por padrão e precisam ser declarados como globais explicitamente. As sub-rotinas e variáveis globais exigem rótulos externos, pois são referenciados a partir de muitos arquivos em um programa. Rótulos locais ocultam nomes que não devem ser visíveis a outros módulos – por exemplo, funções estáticas em C, que só podem ser chamadas por outras funções no mesmo arquivo. Além disso, nomes gerados pelo compilador – por exemplo, um nome para a instrução no início de um loop – são locais, de modo que o compilador não precisa produzir nomes exclusivos em cada arquivo.
Rótulos locais e globais
EXEMPLO
RESPOSTA
Considere o programa na Figura B.1.4. A sub-rotina possui um rótulo externo (global) main. Ela também contém dois rótulos locais – loop e str – visíveis apenas dentro do seu arquivo em assembly. Finalmente, a rotina também contém uma referência não resolvida a um rótulo externo printf, que é a rotina da biblioteca que imprime valores. Quais rótulos na Figura B.1.4 poderiam ser referenciados a partir de outro arquivo? Somente os rótulos globais são visíveis fora de um arquivo, de modo que o único rótulo que poderia ser referenciado por outro arquivo é main.
B.2 Montadores B-637
Como o montador processa cada arquivo em um programa individual e isoladamente, ele só sabe os endereços dos rótulos locais. O montador depende de outra ferramenta, o link-editor, para combinar uma coleção de arquivos-objeto e bibliotecas em um arquivo executável, resolvendo os rótulos externos. O montador auxilia o link-editor, oferecendo listas de rótulos e referências não resolvidas. No entanto, até mesmo os rótulos locais apresentam um desafio interessante a um montador. Ao contrário dos nomes na maioria das linguagens de alto nível, os rótulos em assembly podem ser usados antes de serem definidos. No exemplo, na Figura B.1.4, o rótulo str é usado pela instrução la antes de ser definido. A possibilidade de uma referência à frente, como essa, força um montador a traduzir um programa em duas etapas: primeiro encontre todos os rótulos e depois produza as instruções. No exemplo, quando o montador vê a instrução la, ele não sabe onde a palavra rotulada com str está localizada ou mesmo se str rotula uma instrução ou um dado. A primeira passada de um montador lê cada linha de um arquivo em assembly e a divide em suas partes componentes. Essas partes, chamadas lexemas, são palavras, números e caracteres de pontuação individuais. Por exemplo, a linha ble
referência à frente Um rótulo usado antes de ser definido.
$t0,100,loop
contém seis lexemas: o opcode ble , o especificador de registrador $t0 , uma vírgula, o número 100, uma vírgula e o símbolo loop. Se uma linha começa com um rótulo, o montador registra em sua tabela de símbolos o nome do rótulo e o endereço da palavra de memória que a instrução ocupa. O montador, então, calcula quantas palavras de memória ocupará a instrução na linha atual. Acompanhando os tamanhos das instruções, o montador pode determinar onde a próxima instrução entrará. Para calcular o tamanho de uma instrução de tamanho variável, como aquelas no VAX, um montador precisa examiná-la em detalhes. Por outro lado, instruções de tamanho fixo, como aquelas no MIPS, exigem apenas um exame superficial. O montador realiza um cálculo semelhante para estabelecer o espaço exigido para instruções de dados. Quando o montador atinge o final de um arquivo assembly, a tabela de símbolos registra o local de cada rótulo definido no arquivo. O montador utiliza as informações na tabela de símbolos durante uma segunda passada pelo arquivo, que, na realidade, produz o código de máquina. O montador novamente examina cada linha no arquivo. Se a linha contém uma instrução, o montador combina as representações binárias de seu opcode e operandos (especificadores de registradores ou endereço de memória) em uma instrução válida. O processo é semelhante ao usado na Seção 2.5 do Capítulo 2. As instruções e as palavras de dados que referenciam um símbolo externo definido em outro arquivo não podem ser completamente montadas (elas não estão resolvidas) porque o endereço do símbolo não está na tabela de símbolos. Um montador não reclama sobre referências não resolvidas porque o rótulo correspondente provavelmente estará definido em outro arquivo.
O assembly é uma linguagem de programação. Sua principal diferença das linguagens de alto nível, como BASIC, Java e C, é que o assembly oferece apenas alguns tipos simples de dados e fluxo de controle. Os programas em assembly não especificam o tipo de valor mantido em uma variável. Em vez disso, um programador precisa aplicar as operações apropriadas (por exemplo, adição de inteiro ou ponto flutuante) a um valor. Além disso, em assembly, os programas precisam implementar todo o fluxo de controle com go tos. Os dois fatores tornam a programação em assembly para qualquer máquina – MIPS ou 80x86 – mais difícil e passível de erro do que a escrita em uma linguagem de alto nível.
tabela de símbolos Uma tabela que faz a correspondência entre os nomes dos rótulos e os endereços das palavras de memória que as instruções ocupam.
Colocando perspectiva
em
B-638
Apêndice B Montadores, Link-editores e o Simulador SPIM
Detalhamento: Se a velocidade de um montador for importante, esse processo em duas backpatching Um método para traduzir do assembly para instruções de máquina, em que o montador tem uma representação binária (possivelmente incompleta) de cada instrução em uma passada por um programa e depois retorna para preencher rótulos previamente indefinidos.
etapas pode ser feito em uma passada pelo arquivo assembly com uma técnica conhecida como backpatching. Em sua passada pelo arquivo, o montador monta uma representação binária (possivelmente incompleta) de cada instrução. Se a instrução referencia um rótulo ainda não definido, o montador consulta essa tabela para encontrar todas as instruções que contêm uma referência à frente ao rótulo. O montador volta e corrige sua representação binária para incorporar o endereço do rótulo. O backpatching agiliza o assembly porque o montador só lê sua entrada uma vez. Contudo, isso exige que um montador mantenha uma representação binária inteira de um programa na memória, de modo que as instruções possam sofrer backpatching. Esse requisito pode limitar o tamanho dos programas que podem ser montados. O processo é complicado por máquinas com diversos tipos de desvios que se espalham por diferentes intervalos de instruções. Quando o montador vê inicialmente um rótulo não resolvido em uma instrução de desvio, ele precisa usar o maior desvio possível ou arriscar ter de voltar e reajustar muitas instruções para criar espaço para um desvio maior.
Formato do arquivo-objeto Os montadores produzem arquivos-objeto. Um arquivo-objeto no UNIX contém seis seções distintas (veja Figura B.2.1): j
O cabeçalho do arquivo-objeto descreve o tamanho e a posição das outras partes do arquivo.
j
O segmento de texto contém o código em linguagem de máquina para rotinas no arquivo de origem. Essas rotinas podem ser não executáveis devido a referências não resolvidas.
j
O segmento de dados contém uma representação binária dos dados no arquivo de origem. Os dados também podem estar incompletos devido a referências não resolvidas a rótulos em outros arquivos.
j
As informações de relocação identificam instruções e palavras de dados que dependem de endereços absolutos. Essas referências precisam mudar se partes do programa forem movidas na memória.
j
A tabela de símbolos associa endereços a rótulos externos no arquivo de origem e lista referências não resolvidas.
j
As informações de depuração contêm uma descrição concisa da maneira como o programa foi compilado, de modo que um depurador possa descobrir quais endereços de instrução correspondem às linhas em um arquivo de origem e imprimir as estruturas de dados em formato legível.
segmento de texto O segmento de um arquivo-objeto do UNIX que contém o código em linguagem de máquina para as rotinas no arquivo de origem.
segmento de dados O segmento de um objeto ou arquivo executável do UNIX que contém uma representação binária dos dados inicializados, usados pelo programa
informações de relocação O segmento de um arquivo-objeto do UNIX que identifica instruções e palavras de dados que dependem de endereços absolutos. endereço absoluto O endereço real na memória de uma variável ou rotina.
O montador produz um arquivo-objeto que contém uma representação binária do programa e dos dados, além de informações adicionais para ajudar a link-editar partes de um programa. Essas informações de relocação são necessárias porque o montador não sabe quais locais da memória um procedimento ou parte de dados ocupará depois de ser link-editado com o restante do programa. Os procedimentos e dados de um arquivo são armazenados em uma parte contígua da memória, mas o montador não sabe onde essa memória estará localizada. O montador também passa algumas entradas da tabela de símbolos para o link-editor. Em particular, o montador precisa registrar quais símbolos externos são definidos em um arquivo e quais referências não resolvidas ocorrem em um arquivo.
FIGURA B.2.1 Arquivo-objeto. Um montador do UNIX produz um arquivo-objeto com seis seções distintas.
B.2 Montadores B-639
Detalhamento: Por conveniência, os montadores consideram que cada arquivo começa no mesmo endereço (por exemplo, posição 0) com a expectativa de que o link-editor reposicione o código e os dados quando receberem locais na memória. O montador produz informações de relocação, que contém uma entrada descrevendo cada instrução ou palavra de dados no arquivo que referencia um endereço absoluto. No MIPS, somente as instruções call, load e store da sub-rotina referenciam endereços absolutos. As instruções que usam endereçamento relativo ao PC, como desvios, não precisam ser relocadas.
Facilidades adicionais Os montadores oferecem diversos recursos convenientes que ajudam a tornar os programas em assembly mais curtos e mais fáceis de escrever, mas não mudam fundamentalmente o assembly. Por exemplo, as diretivas de layout de dados permitem que um programador descreva os dados de uma maneira mais concisa e natural do que sua representação binária. Na Figura B.1.4, a diretiva
armazena caracteres da string na memória. Compare essa linha com a alternativa de escrever cada caractere como seu valor ASCII (a Figura 2.15, no Capítulo 2, descreve a codificação ASCII para os caracteres):
A diretiva .asciiz é mais fácil de ler porque representa caracteres como letras, e não como números binários. Um montador pode traduzir caracteres para sua representação binária muito mais rapidamente e com mais precisão do que um ser humano. As diretivas de layout de dados especificam os dados em um formato legível aos seres humanos, que um montador traduz para binário. Outras diretivas de layout são descritas na Seção B.10.
Diretiva de string
Defina a sequência de bytes produzida por esta diretiva:
EXEMPLO
RESPOSTA
B-640
Apêndice B Montadores, Link-editores e o Simulador SPIM
As macros são uma facilidade de combinação e troca de padrão, que oferece um mecanismo simples para nomear uma sequência de instruções usada com frequência. Em vez de digitar repetidamente as mesmas instruções toda vez que forem usadas, um programador chama a macro e o montador substitui a chamada da macro pela sequência de instruções correspondente. As macros, como as sub-rotinas, permitem que um programador crie e nomeie uma nova abstração para uma operação comum. No entanto, diferente das sub-rotinas, elas não causam uma chamada e um retorno de sub-rotina quando o programa é executado, pois uma chamada de macro é substituída pelo corpo da macro quando o programa é montado. Depois dessa troca, a montagem resultante é indistinguível do programa equivalente, escrito sem macros.
EXEMPLO
Macros
Como um exemplo, suponha que um programador precise imprimir muitos números. A rotina de biblioteca printf aceita uma string de formato e um ou mais valores para imprimir como seus argumentos. Um programador poderia imprimir o inteiro no registrador $7 com as seguintes instruções:
A diretiva .data diz ao montador para armazenar a string no segmento de dados do programa, e a diretiva .text diz ao montador para armazenar as instruções em seu segmento de texto. Entretanto, a impressão de muitos números dessa maneira é tediosa e produz um programa extenso, difícil de ser entendido. Uma alternativa é introduzir uma macro, print_int, para imprimir um inteiro:
B.2 Montadores B-641
A macro possui um parâmetro formal, $arg, que nomeia o argumento da macro. Quando a macro é expandida, o argumento de uma chamada é substituído pelo parâmetro formal em todo o corpo da macro. Depois, o montador substitui a chamada pelo corpo recém-expandido da macro. Na primeira chamada em print_int, o argumento é $7, de modo que a macro se expande para o código
parâmetro formal Uma variável que é o argumento de um procedimento ou macro; substituída por esse argumento quando a macro é expandida.
Em uma segunda chamada em print_int, digamos, print_int($t0), o argumento é $t0, de modo que a macro expande para
Para o que a chamada print_int($a0) se expande?
RESPOSTA Esse exemplo ilustra uma desvantagem das macros. Um programador que utiliza essa macro precisa estar ciente de que print_int utiliza o registrador $a0 e por isso não pode imprimir corretamente o valor nesse registrador.
Alguns montadores também implementam pseudoinstruções, que são instruções fornecidas por um montador mas não implementadas no hardware. O Capítulo 2 contém muitos exemplos de como o montador MIPS sintetiza pseudoinstruções e modos de endereçamento do conjunto de instruções de hardware do MIPS. Por exemplo, a Seção 2.7, no Capítulo 2, descreve como o montador sintetiza a instrução blt a partir de duas outras instruções: slt e bne. Estendendo o conjunto de instruções, o montador MIPS torna a programação em assembly mais fácil sem complicar o hardware. Muitas pseudoinstruções também poderiam ser simuladas com macros, mas o montador MIPS pode gerar um código melhor para essas instruções, pois pode usar um registrador dedicado ($at) e é capaz de otimizar o código gerado. Detalhamento: Os montadores, é importante pontuar, montam condicionalmente partes de código, o que permite que um programador inclua ou exclua grupos de instruções quando um programa é montado. Esse recurso é particularmente útil quando várias versões de um programa diferem por um pequeno valor. Em vez de manter esses programas em arquivos separados – o que complica bastante o reparo de bugs no código comum –, os programadores normalmente mesclam as versões em um único arquivo. O código particular a uma versão é montado condicionalmente, de modo que possa ser excluído quando outras versões do programa forem montadas. Se as macros e a montagem condicional são tão úteis, por que os montadores para sistemas UNIX nunca ou quase nunca as oferecem? Um motivo é que a maioria dos programadores nesses sistemas escreve programas em linguagens de alto nível, como C. A maior parte do código assembly é produzida por compiladores, que acham mais conveniente repetir o código do que definir macros. Outro motivo é que outras ferramentas no UNIX – como cpp, o pré-processador C, ou m4, um processador de macro de uso geral – podem oferecer macros e montagem condicional para programas em assembly.
Interface hardware/ software
B-642
Apêndice B Montadores, Link-editores e o Simulador SPIM
B.3 Link-editores compilação separada Dividir um programa em muitos arquivos, cada qual podendo ser compilado sem conhecimento do que está nos outros arquivos.
A compilação separada permite que um programa seja dividido em partes que são armazenadas em arquivos diferentes. Cada arquivo contém uma coleção logicamente relacionada de sub-rotinas e estruturas de dados que formam um módulo de um programa maior. Um arquivo pode ser compilado e montado independente de outros arquivos, de modo que as mudanças em um módulo não exigem a recompilação do programa inteiro. Conforme já discutimos, a compilação separada necessita da etapa adicional de link-edição para combinar os arquivos-objeto de módulos separados e consertar suas referências não resolvidas. A ferramenta que mescla esses arquivos é o link-editor (veja Figura B.3.1). Ele realiza três tarefas: j
Pesquisa as bibliotecas de programa para encontrar rotinas de biblioteca usadas pelo programa.
j
Determina os locais da memória que o código de cada módulo ocupará e realoca suas instruções ajustando referências absolutas.
j
Resolve referências entre os arquivos.
A primeira tarefa de um link-editor é garantir que um programa não contenha rótulos indefinidos. O link-editor combina os símbolos externos e as referências não resolvidas a partir dos arquivos de um programa. Um símbolo externo em um arquivo resolve uma referência de outro arquivo se ambos se referirem a um rótulo com o mesmo nome. As referências não combinadas significam que um símbolo foi usado, mas não definido em qualquer lugar do programa. Referências não resolvidas nesse estágio do processo de link-edição não necessariamente significam que um programador cometeu um erro. O programa poderia ter referenciado uma rotina de biblioteca cujo código não estava nos arquivos-objeto passados ao linkeditor. Depois de combinar os símbolos no programa, o link-editor pesquisa as bibliotecas de programa do sistema para encontrar sub-rotinas e estruturas de dados predefinidas que
FIGURA B.3.1 O link-editor pesquisa uma coleção de arquivos-objeto e bibliotecas de programa para encontrar rotinas usadas em um programa, combina-as em um único arquivo executável e resolve as referências entre as rotinas em arquivos diferentes.
B.5 Uso da memória B-643
o programa referencia. As bibliotecas básicas contêm rotinas que leem e escrevem dados, alocam e liberam memória, e realizam operações numéricas. Outras bibliotecas contêm rotinas para acessar bancos de dados ou manipular janelas de terminal. Um programa que referencia um símbolo não resolvido que não está em qualquer biblioteca é errôneo e não pode ser link-editado. Quando o programa usa uma rotina de biblioteca, o link-editor extrai o código da rotina da biblioteca e o incorpora ao segmento de texto do programa. Essa nova rotina, por sua vez, pode depender de outras rotinas de biblioteca, de modo que o link-editor continua a buscar outras rotinas de biblioteca até que nenhuma referência externa esteja não resolvida ou até que uma rotina não possa ser encontrada. Se todas as referências externas forem resolvidas, o link-editor em seguida determina os locais da memória que cada módulo ocupará. Como os arquivos foram montados isoladamente, o montador não poderia saber onde as instruções ou os dados de um módulo seriam colocados em relação a outros módulos. Quando o link-editor coloca um módulo na memória, todas as referências absolutas precisam ser relocadas para refletir seu verdadeiro local. Como o link-editor possui informações de relocação que identificam todas as referências relocáveis, ele pode eficientemente localizar e remendar essas referências. O link-editor produz um arquivo executável que pode ser executado em um computador. Normalmente, esse arquivo tem o mesmo formato de um arquivo-objeto, exceto que não contém referências não resolvidas ou informações de relocação.
B.4 Carga Um programa que link-edita sem um erro pode ser executado. Antes de ser executado, o programa reside em um arquivo no armazenamento secundário, como um disco. Em sistemas UNIX, o kernel do sistema operacional traz o programa para a memória e inicia sua execução. Para iniciar um programa, o sistema operacional realiza as seguintes etapas: 1. Lê o cabeçalho do arquivo executável para determinar o tamanho dos segmentos de texto e de dados. 2. Cria um novo espaço de endereçamento para o programa. Esse espaço de endereçamento é grande o suficiente para manter os segmentos de texto e de dados, junto com um segmento de pilha (veja a Seção B.5). 3. Copia instruções e dados do arquivo executável para o novo espaço de endereçamento. 4. Copia argumentos passados ao programa para a pilha. 5. Inicializa os registradores da máquina. Em geral, a maioria dos registradores é apagada, mas o stack pointer precisa receber o endereço do primeiro local da pilha livre (veja a Seção B.5). 6. Desvia para a rotina de partida, que copia os argumentos do programa da pilha para os registradores e chama a rotina main do programa. Se a rotina main retornar, a rotina de partida termina o programa com a chamada do sistema exit.
B.5 Uso da memória As próximas seções elaboram a descrição da arquitetura MIPS apresentada anteriormente no livro. Os capítulos anteriores focalizaram principalmente o hardware e seu relacionamento com o software de baixo nível. Essas seções tratavam principalmente de como os programadores assembly utilizam o hardware do MIPS. Essas seções descrevem um conjunto de convenções seguido em muitos sistemas MIPS. Em sua maior parte, o hardware não
B-644
Apêndice B Montadores, Link-editores e o Simulador SPIM
impõe essas convenções. Em vez disso, elas representam um acordo entre os programadores para seguirem o mesmo conjunto de regras, de modo que o software escrito por diferentes pessoas possa atuar junto e fazer uso eficaz do hardware MIPS. Os sistemas baseados em processadores MIPS normalmente dividem a memória em três partes (veja Figura B.5.1). A primeira parte, próxima do início do espaço de endereçamento (começando no endereço 400000hexa), é o segmento de texto, que mantém as instruções do programa.
FIGURA B.5.1 Layout da memória.
dados estáticos A parte da memória que contém dados cujo tamanho é conhecido pelo compilador e cujo tempo de vida é a execução inteira do programa.
Interface hardware/ software
A segunda parte, acima do segmento de texto, é o segmento de dados, dividido ainda mais em duas partes. Os dados estáticos (começando no endereço 10000000hexa) contêm objetos cujo tamanho é conhecido pelo compilador e cujo tempo de vida – o intervalo durante o qual um programa pode acessá-los – é a execução inteira do programa. Por exemplo, em C, as variáveis globais são alocadas estaticamente, pois podem ser referenciadas a qualquer momento durante a execução de um programa. O link-editor atribui objetos estáticos a locais no segmento de dados e resolve referências a esses objetos. Como o segmento de dados começa muito acima do programa, no endereço 10000000hexa, as instruções load e store não podem referenciar diretamente os objetos de dados com seus campos de offset de 16 bits (veja Seção 2.5, no Capítulo 2). Por exemplo, para carregar a palavra no segmento de dados no endereço 10010020hexa para o registrador $v0, são necessárias duas instruções:
(O 0x antes de um número significa que ele é um valor hexadecimal. Por exemplo, 0x800 é 8000hexa ou 32.768dec.) Para evitar repetir a instrução lui em cada load e store, os sistemas MIPS normalmente dedicam um registrador ($gp) como um ponteiro global para o segmento de dados estático. Esse registrador contém o endereço 10008000hexa, de modo que as instruções load e store podem usar seus campos de 16 bits com sinal para acessar os primeiros 64KB do segmento de dados estático. Com esse ponteiro global, podemos reescrever o exemplo como uma única instrução:
Naturalmente, um ponteiro global torna os locais de endereçamento entre 10000000hexa10010000hexa mais rápidos do que outros locais do heap. O compilador MIPS normalmente armazena variáveis globais nessa área, pois essas variáveis possuem locais fixos e se ajustam melhor do que outros dados globais, como arrays.
B.6 Convenção para chamadas de procedimento B-645
Imediatamente acima dos dados estáticos estão os dados dinâmicos. Esses dados, como seu nome sugere, são alocados pelo programa enquanto ele é executado. Nos programas C, a rotina de biblioteca malloc localiza e retorna um novo bloco de memória. Como um compilador não pode prever quanta memória um programa alocará, o sistema operacional expande a área de dados dinâmica para atender à demanda. Conforme indica a seta para cima na figura, malloc expande a área dinâmica com a chamada do sistema sbrk, que faz com que o sistema operacional acrescente mais páginas ao espaço de endereçamento virtual do programa (veja Seção 5.4, no Capítulo 5) imediatamente acima do segmento de dados dinâmico. A terceira parte, o segmento de pilha do programa, reside no topo do espaço de endereçamento virtual (começando no endereço 7fffffffhexa). Assim como os dados dinâmicos, o tamanho máximo da pilha de um programa não é conhecido antecipadamente. À medida que o programa coloca valores na pilha, o sistema operacional expande o segmento de pilha para baixo, em direção ao segmento de dados. Essa divisão de três partes da memória não é a única possível. Contudo, ela possui duas características importantes: os dois segmentos dinamicamente expansíveis são bastante distantes um do outro, e eles podem crescer para usar o espaço de endereços inteiro de um programa.
segmento de pilha A parte da memória usada por um programa para manter frames de chamada de procedimento.
B.6 Convenção para chamadas de procedimento As convenções que controlam o uso dos registradores são necessárias quando os procedimentos em um programa são compilados separadamente. Para compilar um procedimento em particular, um compilador precisa saber quais registradores pode usar e quais são reservados para outros procedimentos. As regras para usar os registradores são chamadas de convenções para uso dos registradores ou convenções para chamadas de procedimento. Como o nome sugere, essas regras são, em sua maior parte, convenções seguidas pelo software, em vez de regras impostas pelo hardware. No entanto, a maioria dos compiladores e programadores tenta seguir essas convenções estritamente, pois sua violação causa bugs traiçoeiros. A convenção para chamadas descrita nesta seção é aquela utilizada pelo compilador gcc. O compilador nativo do MIPS utiliza uma convenção mais complexa, que é ligeiramente mais rápida. A CPU do MIPS contém 32 registradores de uso geral, numerados de 0 a 31. O registrador $0 contém o valor fixo 0. j
j
j
j
j
j
convenção para uso dos registradores Também chamada convenção para chamadas de procedimento. Um protocolo de software que controla o uso dos registradores por procedimentos.
Os registradores $at (1), $k0 (26) e $k1 (27) são reservados para o montador e o sistema operacional e não devem ser usados por programas do usuário ou compiladores. Os registradores $a0—$a3 (4-7) são usados para passar os quatro primeiros argumentos às rotinas (os argumentos restantes são passados na pilha). Os registradores $v0 e $v1 (2, 3) são usados para retornar valores das funções. Os registradores $t0 —$t9 (8-15, 24, 25) são registradores salvos pelo caller, usados para manter quantidades temporárias que não precisam ser preservadas entre as chamadas (veja Seção 2.8, no Capítulo 2).
registrador salvo pelo caller Um
Os registradores $s0—$s7 (16-23) são registradores salvos pelo callee, que mantêm valores de longa duração os quais devem ser preservados entre as chamadas.
registrador salvo pelo callee Um registrador salvo pela rotina sendo chamada.
O registrador $gp (28) é um ponteiro global que aponta para o meio de um bloco de 64K de memória no segmento de dados estático. O registrador $sp (29) é o stack pointer, que aponta para o último local na pilha. O registrador $fp (30) é o frame pointer. A instrução jal escreve no registrador $ra (31) o endereço de retorno de uma chamada de procedimento. Esses dois registradores são explicados na próxima seção.
registrador salvo pela rotina que faz uma chamada de procedimento.
B-646
Apêndice B Montadores, Link-editores e o Simulador SPIM
As abreviações e os nomes de duas letras para esses registradores – por exemplo, $sp para o stack pointer – refletem os usos intencionados na convenção de chamada de procedimento. Ao descrever essa convenção, usaremos os nomes em vez de números de registrador. A Figura B.6.1 lista os registradores e descreve seus usos intencionados.
FIGURA B.6.1 Registradores do MIPS e convenção de uso.
Chamadas de procedimento
frame de chamada de procedimento Um bloco de memória usado para manter valores passados a um procedimento como argumentos, a fim de salvar registradores que um procedimento pode modificar mas que o caller não deseja que sejam alterados, e fornecer espaço para variáveis locais a um procedimento.
Esta seção descreve as etapas que ocorrem quando um procedimento (caller) invoca outro procedimento (callee). Os programadores que escrevem em uma linguagem de alto nível (como C ou Pascal) nunca veem os detalhes de como um procedimento chama outro, pois o compilador cuida dessa manutenção de baixo nível. Contudo, os programadores assembly precisam implementar explicitamente cada chamada e retorno de procedimento. A maior parte da manutenção associada a uma chamada gira em torno de um bloco de memória chamado frame de chamada de procedimento. Essa memória é usada para diversas finalidades: j Para manter valores passados a um procedimento como argumentos. j Para salvar registradores que um procedimento pode modificar, mas que o caller não deseja que sejam alterados. j Para oferecer espaço para variáveis locais a um procedimento.
B.6 Convenção para chamadas de procedimento B-647
FIGURA B.6.2 Layout de um frame de pilha. O frame pointer ($fp) aponta para a primeira palavra do frame de pilha do procedimento em execução. O stack pointer ($sp) aponta para a última palavra do frame. Os quatro primeiros argumentos são passados em registradores, de modo que o quinto argumento é o primeiro armazenado na pilha.
Na maioria das linguagens de programação, as chamadas e retornos de procedimento seguem uma ordem estrita do tipo último a entrar, primeiro a sair (LIFO – Last-In, First-Out), de modo que essa memória pode ser alocada e liberada como uma pilha, motivo pelo qual esses blocos de memória às vezes são chamados frames de pilha. A Figura B.6.2 mostra um frame de pilha típico. O frame consiste na memória entre o frame pointer ($fp), que aponta para a primeira palavra do frame, e o stack pointer ($sp), que aponta para a última palavra do frame. A pilha cresce para baixo a partir dos endereços de memória mais altos, de modo que o frame pointer aponta para cima do stack pointer. O procedimento que está executando utiliza o frame pointer para acessar rapidamente os valores em seu frame de pilha. Por exemplo, um argumento no frame de pilha pode ser lido para o registrador $v0 com a instrução
Um frame de pilha pode ser construído de muitas maneiras diferentes; porém, o caller e o callee precisam combinar a sequência de etapas. As etapas a seguir descrevem a convenção de chamada utilizada na maioria das máquinas MIPS. Essa convenção entra em ação em três pontos durante uma chamada de procedimento: imediatamente antes de o caller invocar o callee, assim que o callee começa a executar e imediatamente antes de o callee retornar ao caller. Na primeira parte, o caller coloca os argumentos da chamada de procedimento em locais padrões e invoca o callee para fazer o seguinte: 1. Passar argumentos. Por convenção, os quatro primeiros argumentos são passados nos registradores $a0—$a3. Quaisquer argumentos restantes são colocados na pilha e aparecem no início do frame de pilha do procedimento chamado. 2. Salvar registradores salvos pelo caller. O procedimento chamado pode usar esses registradores ($a0—$a3 e $t0—$t9) sem primeiro salvar seu valor. Se o caller espera utilizar um desses registradores após uma chamada, ele deverá salvar seu valor antes da chamada. 3. Executar uma instrução jal (veja Seção 2.8 do Capítulo 2), que desvia para a primeira instrução do callee e salva o endereço de retorno no registrador $ra. Antes que uma rotina chamada comece a executar, ela precisa realizar as seguintes etapas para configurar seu frame de pilha: 1. Alocar memória para o frame, subtraindo o tamanho do frame do stack pointer. 2. Salvar os registradores salvos pelo callee no frame. Um callee precisa salvar os valores desses registradores ($s0—$s7, $fp e $ra) antes de alterá-los, pois o caller espera
B-648
Apêndice B Montadores, Link-editores e o Simulador SPIM
encontrar esses registradores inalterados após a chamada. O registrador $fp é salvo para cada procedimento que aloca um novo frame de pilha. No entanto, o registrador $ra só precisa ser salvo se o callee fizer uma chamada. Os outros registradores salvos pelo callee, que são utilizados, também precisam ser salvos. 3. Estabelecer o frame pointer somando o tamanho do frame de pilha menos 4 a $sp e armazenando a soma no registrador $fp.
Interface hardware/ software
A convenção de uso dos registradores do MIPS oferece registradores salvos pelo caller e pelo callee, pois os dois tipos de registradores são vantajosos em circunstâncias diferentes. Os registradores salvos pelo caller são usados para manter valores de longa duração, como variáveis de um programa do usuário. Esses registradores só são salvos durante uma chamada de procedimento se o caller espera utilizar o registrador. Por outro lado, os registradores salvos pelo callee são usados para manter quantidades de curta duração, que não persistem entre as chamadas, como valores imediatos em um cálculo de endereço. Durante uma chamada, o caller não pode usar esses registradores para valores temporários de curta duração. Finalmente, o callee retorna ao caller executando as seguintes etapas: 1. Se o callee for uma função que retorna um valor, coloque o valor retornado no registrador $v0. 2. Restaure todos os registradores salvos pelo callee que foram salvos na entrada do procedimento. 3. Remova o frame de pilha somando o tamanho do frame a $sp. 4. Retorne desviando para o endereço no registrador $ra.
procedimentos recursivos Procedimentos que chamam a si mesmos direta ou indiretamente através de uma cadeia de chamadas.
Detalhamento: Uma linguagem de programação que não permite procedimentos recursivos – procedimentos que chamam a si mesmos direta ou indiretamente, por meio de uma cadeia de chamadas – não precisa alocar frames em uma pilha. Em uma linguagem não recursiva, o frame de cada procedimento pode ser alocado estaticamente, pois somente uma invocação de um procedimento pode estar ativa ao mesmo tempo. As versões mais antigas de Fortran proibiam a recursão porque frames alocados estaticamente produziam código mais rápido em algumas máquinas mais antigas. Todavia, em arquiteturas load-store, como MIPS, os frames de pilha podem ser muito rápidos porque o registrador frame pointer aponta diretamente para o frame de pilha ativo, que permite que uma única instrução load ou store acesse valores no frame. Além disso, a recursão é uma técnica de programação valiosa.
Exemplo de chamada de procedimento Como exemplo, considere a rotina em C
B.6 Convenção para chamadas de procedimento B-649
que calcula e imprime 10! (o fatorial de 10, 10! = 10 × 9 × ... × 1). fact é uma rotina recursiva que calcula n! multiplicando n vezes (n – 1)!. O código assembly para essa rotina ilustra como os programas manipulam frames de pilha. Na entrada, a rotina main cria seu frame de pilha e salva os dois registradores salvos pelo callee que serão modificados: $fp e $ra. O frame é maior do que o exigido para esses dois registradores, pois a convenção de chamada exige que o tamanho mínimo de um frame de pilha seja 24 bytes. Esse frame mínimo pode manter quatro registradores de argumento ($a0—$a3) e o endereço de retorno $ra, preenchidos até um limite de double palavra (24 bytes). Como main também precisa salvar o $fp, seu frame de pilha precisa ter duas palavras a mais (lembre-se de que o stack pointer é mantido alinhado em um limite de double palavra).
A rotina main, então, chama a rotina de fatorial e lhe passa o único argumento 10. Depois que fact retorna, main chama a rotina de biblioteca printf e lhe passa uma string de formato e o resultado retornado de fact:
Finalmente, depois de imprimir o fatorial, main retorna. Entretanto, primeiro, ela precisa restaurar os registradores que salvou e remover seu frame de pilha:
A rotina de fatorial é semelhante em estrutura a main. Primeiro, ela cria um frame de pilha e salva os registradores salvos pelo callee que serão usados por ela. Além de salvar $ra e $fp , fact também salva seu argumento ($a0 ), que ela usará para a chamada recursiva:
B-650
Apêndice B Montadores, Link-editores e o Simulador SPIM
O núcleo da rotina fact realiza o cálculo do programa em C. Ele testa se o argumento é maior do que 0. Caso contrário, a rotina retorna o valor 1. Se o argumento for maior do que 0, a rotina é chamada recursivamente para calcular fact(n-1) e multiplica esse valor por n:
Finalmente, a rotina de fatorial restaura os registradores salvos pelo callee e retorna o valor no registrador $v0:
Pilha em procedimentos recursivos
EXEMPLO
A Figura B.6.3 mostra a pilha na chamada fact(7). mainexecuta primeiro, de modo que seu frame está mais abaixo na pilha. main chama fact(10), cujo frame de pilha vem em seguida na pilha. Cada invocação chama fact recursivamente para calcular o próximo fatorial mais inferior. Os frames de pilha fazem um parale-
B.6 Convenção para chamadas de procedimento B-651
lo com a ordem LIFO dessas chamadas. Qual é a aparência da pilha quando a chamada a fact(10) retorna?
FIGURA B.6.3 Frames da pilha durante a chamada de fact(7).
RESPOSTA
Detalhamento: A diferença entre o compilador MIPS e o compilador gcc é que o compilador MIPS normalmente não usa um frame pointer, de modo que esse registrador está disponível como outro registrador salvo pelo callee, $s8. Essa mudança salva algumas das instruções na chamada de procedimento e sequência de retorno. Contudo, isso complica a geração do código, porque um procedimento precisa acessar seu frame de pilha com $sp, cujo valor pode mudar durante a execução de um procedimento se os valores forem colocados na pilha.
Outro exemplo de chamada de procedimento Como outro exemplo, considere a seguinte rotina que calcula a função tak, que é um benchmark bastante utilizado, criado por Ikuo Takeuchi. Essa função não calcula nada de útil, mas é um programa altamente recursivo que ilustra a convenção de chamada do MIPS.
B-652
Apêndice B Montadores, Link-editores e o Simulador SPIM
O código assembly para esse programa está logo em seguida. A função tak primeiro salva seu endereço de retorno no frame de pilha e seus argumentos nos registradores salvos pelo callee, pois a rotina pode fazer chamadas que precisam usar os registradores $a0—$a2 e $ra . A função utiliza registradores salvos pelo callee, pois eles mantêm valores que persistem por toda a vida da função, o que inclui várias chamadas que potencialmente poderiam modificar registradores.
A rotina, então, inicia a execução testando se y
Se y
Observe que o resultado da primeira chamada recursiva é salvo no registrador $s3, de modo que pode ser usado mais tarde. A função agora prepara argumentos para a segunda chamada recursiva.
Nas instruções a seguir, o resultado dessa chamada recursiva é salvo no registrador $s0. No entanto, primeiro, precisamos ler, pela última vez, o valor salvo do primeiro argumento a partir desse registrador.
B.6 Convenção para chamadas de procedimento B-653
Depois de três chamadas recursivas mais internas, estamos prontos para a chamada recursiva final. Depois da chamada, o resultado da função está em $v0, e o controle desvia para o epílogo da função.
Esse código no rótulo L1 é a consequência da instrução if-then-else. Ele apenas move o valor do argumento z para o registrador de retorno e entra no epílogo da função.
O código a seguir é o epílogo da função, que restaura os registradores salvos e retorna o resultado da função a quem chamou.
A rotina main chama a função tak com seus argumentos iniciais, e depois pega o resultado calculado (7) e o imprime usando a chamada ao sistema do SPIM para imprimir inteiros:
B-654
Apêndice B Montadores, Link-editores e o Simulador SPIM
B.7 Exceções e interrupções A Seção 4.9 do Capítulo 4 descreve o mecanismo de exceção do MIPS, que responde a exceções causadas por erros durante a execução de uma instrução e a interrupções externas causadas por dispositivos de E/S. Esta seção descreve o tratamento de interrupção e exceção com mais detalhes.1 Nos processadores MIPS, uma parte da CPU, chamada coprocessador 0, registra as informações de que o software precisa para lidar com exceções e interrupções. O simulador SPIM do MIPS não implementa todos os registradores do coprocessador 0, pois muitos não são úteis em um simulador ou fazem parte do sistema de memória, que o SPIM não modela. Contudo, o SPIM oferece os seguintes registradores do coprocessador 0:
tratamento de interrupção Um trecho de código executado como resultado de uma exceção ou interrupção.
Nome do registrador
Número do registrador
BadVAddr
8
endereço de memória em que ocorreu uma referência de memória problemática
Uso
Count
9
temporizador
Compare
11
valor comparado com o temporizador que causa interrupção quando combinam
Status
12
máscara de interrupções e bits de habilitação
Cause
13
tipo de exceção e bits de interrupções pendentes
EPC
14
endereço da instrução que causou a exceção
Config
16
configuração da máquina
Esses sete registradores fazem parte do conjunto de registradores do coprocessador 0. Eles são acessados pelas instruções mfc0 and mtc0. Após uma exceção, o registrador EPC contém o endereço da instrução executada quando a exceção ocorreu. Se a exceção foi causada por uma interrupção externa, então a instrução não terá iniciado a execução. Todas as outras exceções são causadas pela execução da instrução no EPC, exceto quando a instrução problemática está no delay slot de um branch ou jump. Nesse caso, o EPC aponta para a instrução de branch ou jump e o bit BD é ligado no registrador Cause. Quando esse bit está ligado, o handler de exceção precisa procurar a instrução problemática em EPC + 4. No entanto, de qualquer forma, um handler de exceção retoma o programa corretamente, retornando à instrução no EPC. Se a instrução que causou a exceção fez um acesso à memória, o registrador BadVAddr contém o endereço do local de memória referenciado. O registrador Count é um timer que incrementa em uma taxa fixa (como padrão, a cada 10 milissegundos) enquanto o SPIM está executando. Quando o valor no registrador Count for igual ao valor no registrador Compare, ocorre uma interrupção de hardware com nível de prioridade 5. A Figura B.7.1 mostra o subconjunto dos campos do registrador Status implementados pelo simulador SPIM do MIPS. O campo interrupt mask contém um bit para cada um dos seis níveis de interrupção de hardware e dois de software. Um bit de máscara 1 permite que as interrupções nesse nível parem o processador. Um bit de máscara 0 desativa as interrupções nesse nível. Quando uma interrupção chega, ela liga seu bit de interrupção
Esta seção discute as exceções na arquitetura MIPS-32, que é o que o SPIM implementa na Versão 7.0 em diante. As versões anteriores do SPIM implementaram a arquitetura MIPS-I, que tratava exceções de forma ligeiramente diferente. A conversão de programas a partir dessas versões para execução no MIPS-32 não deverá ser difícil, pois as mudanças são limitadas aos campos dos registradores Status e Cause e à substituição da instrução rfe pela instrução eret.
1
B.7 Exceções e interrupções B-655
FIGURA B.7.1 O registrador Status.
FIGURA B.7.2 O registrador Cause.
pendente no registrador Cause, mesmo que o bit de máscara esteja desligado. Quando uma interrupção está pendente, ela interromperá o processador quando seu bit de máscara for habilitado mais tarde. O bit de modo do usuário é 0 se o processador estiver funcionando no modo kernel e 1 se estiver funcionando no modo usuário. No SPIM, esse bit é fixado em 1, pois o processador SPIM não implementa o modo kernel. O bit de nível de exceção normalmente é 0, mas é colocado em 1 depois que ocorre uma exceção. Quando esse bit é 1, as interrupções são desativadas e o EPC não é atualizado se outra exceção ocorrer. Esse bit impede que um handler de exceção seja incomodado por uma interrupção ou exceção, mas deve ser reiniciado quando o handler termina. Se o bit interrupt enable for 1, as interrupções são permitidas. Se for 0, elas estão inibidas. A Figura B.7.2 mostra o subconjunto dos campos do registrador Cause que o SPIM implementa. O bit de branch delay é 1 se a última exceção ocorreu em uma instrução executada no slot de retardo de um desvio. Os bits de interrupções pendentes tornam-se 1 quando uma interrupção é gerada em determinado nível de hardware ou software. O registrador de código de exceção descreve a causa de uma exceção por meio dos seguintes códigos:
Número
Nome
00
Int
Causa da exceção interrupção (hardware)
04
AdEL
exceção de erro de endereço (load ou busca de instrução)
05
AdES
exceção de erro de endereço (store)
06
IBE
erro de barramento na busca da instrução
07
DBE
erro de barramento no load ou store de dados
08
Sys
exceção de syscall
09
Bp
exceção de breakpoint
10
RI
exceção de instrução reservada
11
CpU
12
Ov
exceção de overflow aritmético
13
Tr
trap
15
FPE
coprocessador não implementado
ponto flutuante
B-656
Apêndice B Montadores, Link-editores e o Simulador SPIM
Exceções e interrupções fazem com que um processador MIPS desvie para uma parte do código, no endereço 80000180hexa (no espaço de endereçamento do kernel, não do usuário), chamada handler de exceção. Esse código examina a causa da exceção e desvia para um ponto apropriado no sistema operacional. O sistema operacional responde a uma exceção terminando o processo que causou a exceção ou realizando alguma ação. Um processo que causa um erro, como a execução de uma instrução não implementada, é terminado pelo sistema operacional. Por outro lado, outras exceções, como faltas de página, são solicitações de um processo para o sistema operacional realizar um serviço, como trazer uma página do disco. O sistema operacional processa essas solicitações e retoma o processo. O último tipo de exceções são interrupções de dispositivos externos. Elas em geral fazem com que o sistema operacional mova dados de ou para um dispositivo de E/S e retome o processo interrompido. O código no exemplo a seguir é um handler de exceção simples, que invoca uma rotina para imprimir uma mensagem a cada exceção (mas não interrupções). Esse código é semelhante ao handler de exceção (exceptions.s) usado pelo simulador SPIM.
Handler de exceções
EXEMPLO
O handler de exceção primeiro salva o registrador $at, que é usado em pseudoinstruções no código do handler, depois salva $a0 e $a1, que mais tarde utiliza para passar argumentos. O handler de exceção não pode armazenar os valores antigos a partir desses registradores na pilha, como faria uma rotina comum, pois a causa da exceção poderia ter sido uma referência de memória que usou um valor incorreto (como 0) no stack pointer. Em vez disso, o handler de exceção armazena esses registradores em um registrador de handler de exceção ($k1, pois não pode acessar a memória sem usar $at) e dois locais da memória (save0 e save1). Se a própria rotina de exceção pudesse ser interrompida, dois locais não seriam suficientes, pois a segunda exceção gravaria sobre valores salvos durante a primeira exceção. Entretanto, esse handler de exceção simples termina a execução antes de permitir interrupções, de modo que o problema não surge.
O handler de exceção, então, move os registradores Cause e EPC para os registradores da CPU. Os registradores Cause e EPC não fazem parte do conjunto de registradores da CPU. Em vez disso, eles são registradores no coprocessador 0, que é a parte da CPU que trata das exceções. A instrução mfc0 $k0, $13 move o registrador 13 do coprocessador 0 (o registrador Cause) para o registrador da CPU $k0. Observe que o handler de exceção não precisa salvar os registradores $k0 e $k1, pois os programas do usuário não deveriam usar esses registradores. O handler de exceção usa o valor do registrador Cause para testar se a exceção foi causada por uma interrupção (ver a tabela anterior). Se tiver sido, a exceção é ignorada. Se a exceção não foi uma interrupção, o handler chama print_excp para imprimir uma mensagem.
B.7 Exceções e interrupções B-657
Antes de retornar, o handler de exceção apaga o registrador Cause, reinicia o registrador Status para ativar interrupções e limpar o bit EXL, de modo a permitir que exceções subsequentes mudem o registrador EPC, e restaura os registradores $a0, $a1 e $at. Depois, ele executa a instrução eret (retorno de exceção), que retorna à instrução apontada pelo EPC. Esse handler de exceção retorna à instrução após aquela que causou a exceção, a fim de não reexecutar a instrução que falhou e causar a mesma exceção novamente.
Detalhamento: Em processadores MIPS reais, o retorno de um handler de exceção é mais
complexo. O handler de exceção não pode sempre desviar para a instrução após o EPC. Por exemplo, se a instrução que causou a exceção estivesse em um delay slot de uma instrução de desvio (veja Capítulo 4), a próxima instrução a executar pode não ser a instrução seguinte na memória.
B-658
Apêndice B Montadores, Link-editores e o Simulador SPIM
B.8 Entrada e saída O SPIM simula um dispositivo de E/S: um console mapeado em memória em que um programa pode ler e escrever caracteres. Quando um programa está executando, o SPIM conecta seu próprio terminal (ou uma janela de console separada na versão xspim do X-Windows ou na versão PCSpim do Windows) ao processador. Um programa MIPS executando no SPIM pode ler os caracteres que você digita. Além disso, se o programa MIPS escreve caracteres no terminal, eles aparecem no terminal do SPIM ou na janela de console. Uma exceção a essa regra é Control-C: esse caractere não é passado ao programa, mas, em vez disso, faz com que o SPIM pare e retorne ao modo de comando. Quando o programa para de executar (por exemplo, porque você digitou Control-C ou porque o programa atingiu um ponto de interrupção), o terminal é novamente conectado ao SPIM para que você possa digitar comandos do SPIM. Para usar a E/S mapeada em memória (ver a seguir), o spim ou o xspim precisam ser iniciados com o flag —mapped_io.PCSpim pode ativar a E/S mapeada em memória por meio de um flag de linha de comando ou pela caixa de diálogo “Settings” (configurações). O dispositivo de terminal consiste em duas unidades independentes: um receptor e um transmissor. O receptor lê caracteres digitados no teclado. O transmissor exibe caracteres no vídeo. As duas unidades são completamente independentes. Isso significa, por exemplo, que os caracteres digitados no teclado não são ecoados automaticamente no monitor. Em vez disso, um programa ecoa um caractere lendo-o do receptor e escrevendo-o no transmissor. Um programa controla o terminal com quatro registradores de dispositivo mapeados em memória, como mostra a Figura B.8.1. “Mapeado em memória” significa que cada registrador aparece como uma posição de memória especial. O registrador Receiver Control está na posição ffff0000hexa. Somente dois de seus bits são realmente usados. O bit 0 é chamado “pronto”: se for 1, isso significa que um caractere chegou do teclado, mas ainda não foi lido do registrador Receiver Data. O bit de pronto é apenas de leitura: tentativas de sobrescrevê-lo são ignoradas. O bit de pronto muda de 0 a 1 quando um caractere é digitado no teclado, e ele muda de 1 para 0 quando o caractere é lido do registrador Receiver Data.
FIGURA B.8.1 O terminal é controlado por quatro registradores de dispositivo, cada um deles parecendo com uma posição de memória no endereço indicado. Somente alguns bits desses registradores são realmente utilizados. Os outros sempre são lidos como 0s e as escritas são ignoradas.
B.9 SPIM B-659
O bit 1 do registrador Receiver Control é o “interrupt enable” do teclado. Esse bit pode ser lido e escrito por um programa. O interrupt enable inicialmente é 0. Se ele for colocado em 1 por um programa, o terminal solicita uma interrupção no nível de hardware 1 sempre que um caractere é digitado e o bit de pronto se torna 1. Todavia, para que a interrupção afete o processador, as interrupções também precisam estar ativadas no registrador Status (veja Seção B.7). Todos os outros bits do registrador Receiver Control não são utilizados. O segundo registrador de dispositivo de terminal é o registrador Receiver Data (no endereço ffff0004hexa). Os oito bits menos significativos desse registrador contêm o último caractere digitado no teclado. Todos os outros bits contêm 0s. Esse registrador é apenas de leitura e muda apenas quando um novo caractere é digitado no teclado. A leitura do registrador Receiver Data reinicia o bit de pronto no registrador Receiver Control para 0. O valor nesse registrador é indefinido se o registrador Receiver Control for 0. O terceiro registrador de dispositivo de terminal é o registrador Transmitter Control (no endereço ffff0008hexa). Somente os dois bits menos significativos desse registrador são usados. Eles se comportam de modo semelhante aos bits correspondentes no registrador Receiver Control. O bit 0 é chamado de “pronto” e é apenas de leitura. Se esse bit for 1, o transmissor estará pronto para aceitar um novo caractere para saída. Se for 0, o transmissor ainda está ocupado escrevendo o caractere anterior. O bit 1 é “interrupt enable” e pode ser lido e escrito. Se esse bit for colocado em 1, então o terminal solicita uma interrupção no nível de hardware 0 sempre que o transmissor estiver pronto para um novo caractere e o bit de pronto se torna 1. O registrador de dispositivo final é o registrador Transmitter Data (no endereço ffff000chexa). Quando um valor é escrito nesse local, seus oito bits menos significativos (ou seja, um caractere ASCII como na Figura 2.15, no Capítulo 2) são enviados para o console. Quando o registrador Transmitter Data é escrito, o bit de pronto no registrador Transmitter Control é retornado para 0. Esse bit permanece sendo 0 até passar tempo suficiente para transmitir o caractere para o terminal; depois, o bit de pronto se torna 1 novamente. O registrador Transmitter Data só deverá ser escrito quando o bit de pronto do registrador Transmitter Control for 1. Se o transmissor não estiver pronto, as escritas no registrador Transmitter Data são ignoradas (as escritas parecem ter sucesso, mas o caractere não é enviado). Computadores reais exigem tempo para enviar caracteres a um console ou terminal. Esses retardos de tempo são simulados pelo SPIM. Por exemplo, depois que o transmissor começa a escrever um caractere, o bit de pronto do transmissor torna-se 0 por um tempo. O SPIM mede o tempo em instruções executadas, e não em tempo de clock real. Isso significa que o transmissor não fica pronto novamente até que o processador execute um número fixo de instruções. Se você interromper a máquina e examinar o bit de pronto, ele não mudará. Contudo, se você deixar a máquina executar, o bit por fim mudará de volta para 1.
B.9 SPIM SPIM é um simulador de software que executa programas em assembly escritos para processadores que implementam a arquitetura MIPS-32, especificamente o Release 1 dessa arquitetura com um mapeamento de memória fixo, sem caches e apenas os coprocessador 0 e 1.2 O nome do SPIM é simplesmente MIPS ao contrário. O SPIM pode ler e executar os arquivos em assembly. O SPIM é um sistema autocontido para executar programas do MIPS. Ele contém um depurador e oferece alguns serviços de forma semelhante ao 2 As primeiras versões do SPIM (antes da 7.0) implementaram a arquitetura MIPS-1 utilizada nos processadores MIPS R2000 originais. Essa arquitetura é quase um subconjunto apropriado da arquitetura MIPS-32, sendo que a diferença é a maneira como as exceções são tratadas. O MIPS-32 também introduziu aproximadamente 60 novas instruções, que são aceitas pelo SPIM. Os programas executados nas versões anteriores do SPIM e que não usavam exceções deverão ser executados sem modificação nas versões mais recentes do SPIM. Os programas que usavam exceções exigirão pequenas mudanças.
B-660
Apêndice B Montadores, Link-editores e o Simulador SPIM
sistema operacional. SPIM é muito mais lento do que um computador real (100 ou mais vezes). Entretanto, seu baixo custo e grande disponibilidade não têm comparação com o hardware real! Uma pergunta óbvia é: por que usar um simulador quando a maioria das pessoas possui PCs que contêm processadores executando muito mais rápido do que o SPIM? Um motivo é que o processador nos PCs são 80x86s da Intel, cuja arquitetura é muito menos regular e muito mais complexa de entender e programar do que os processadores MIPS. A arquitetura do MIPS pode ser a síntese de uma máquina RISC simples e limpa. Além disso, os simuladores podem oferecer um ambiente melhor para a programação em assembly do que uma máquina real, pois podem detectar mais erros e oferecer uma interface melhor do que um computador real. Finalmente, os simuladores são uma ferramenta útil no estudo de computadores e dos programas neles executados. Como eles são implementados em software, não em silício, os simuladores podem ser examinados e facilmente modificados para acrescentar novas instruções, criar novos sistemas, como os multiprocessadores, ou apenas coletar dados.
Simulação de uma máquina virtual
máquina virtual Um computador virtual que parece ter desvios e loads não delayed e um conjunto de instruções mais rico do que o hardware real.
Uma arquitetura MIPS básica é difícil de programar diretamente, por causa dos delayed branches, delayed loads e modos de endereçamento restritos. Essa dificuldade é tolerável, pois esses computadores foram projetados para serem programados em linguagens de alto nível e apresentam uma interface criada para compiladores, em vez de programadores assembly. Uma boa parte da complexidade da programação é resultante de instruções delayed. Um delayed branch exige dois ciclos para executar (veja as seções “Detalhamentos” do Capítulo 4). No segundo ciclo, a instrução imediatamente após o desvio é executada. Essa instrução pode realizar um trabalho útil que normalmente teria sido feito antes do desvio. Ela também pode ser um nop (nenhuma operação), que não faz nada. De modo semelhante, os delayed loads exigem dois ciclos para trazer um valor da memória, de modo que a instrução imediatamente após um load não pode usar o valor (veja Seção 4.2 do Capítulo 4). O MIPS sabiamente escolheu ocultar essa complexidade fazendo com que seu montador implemente uma máquina virtual. Esse computador virtual parece ter branches e loads não delayed e um conjunto de instruções mais rico do que o hardware real. O montador reorganiza instruções para preencher os delay slots. O computador virtual também oferece pseudoinstruções, que aparecem como instruções reais nos programas em assembly. O hardware, porém, não sabe nada a respeito de pseudoinstruções, de modo que o montador as traduz para sequências equivalentes de instruções de máquina reais. Por exemplo, o hardware do MIPS só oferece instruções para desvio quando um registrador é igual ou diferente de 0. Outros desvios condicionais, como aquele que desvia quando um registrador é maior do que outro, são sintetizados comparando-se os dois registradores e desviando quando o resultado da comparação é verdadeiro (diferente de zero). Como padrão, o SPIM simula a máquina virtual mais rica, pois essa é a máquina que a maioria dos programadores achará útil. Todavia, o SPIM também pode simular os desvios e loads delayed no hardware real. A seguir, descrevemos a máquina virtual e só mencionamos rapidamente os recursos que não pertencem ao hardware real. Ao fazer isso, seguimos a convenção dos programadores (e compiladores) assembly do MIPS, que normalmente utilizam a máquina estendida como se estivesse implementada em silício.
Introdução ao SPIM O restante deste apêndice é uma introdução ao SPIM e ao Assembly R2000 do MIPS. Muitos detalhes nunca deverão preocupá-lo; porém, o grande volume de informações às vezes poderá obscurecer o fato de que o SPIM é um programa simples e fácil de usar. Esta seção começa com um tutorial rápido sobre o uso do SPIM, que deverá permitir que você carregue, depure e execute programas MIPS simples.
B.9 SPIM B-661
O SPIM vem em diferentes versões para diferentes tipos de sistemas. A única constante é a versão mais simples, chamada spim , que é um programa controlado por linha de comandos, executado em uma janela de console. Ele opera como a maioria dos programas desse tipo: você digita uma linha de texto, pressiona a tecla Enter (ou Return) e o spim executa seu comando. Apesar da falta de uma interface sofisticada, o spim pode fazer tudo que seus primos mais sofisticados fazem. Existem dois primos sofisticados do spim. A versão que roda no ambiente X-Windows de um sistema UNIX ou Linux é chamada xspim. xspim é um programa mais fácil de aprender e usar do que o spim, pois seus comandos sempre são visíveis na tela e porque ele continuamente apresenta os registradores e a memória da máquina. A outra versão sofisticada se chama PCspim e roda no Microsoft Windows. As versões do SPIM para UNIX e Windows estão neste CD (clique em Tutoriais). Os tutoriais (em inglês) sobre xspim, PCspim, spim e as opções da linha de comandos do SPIM estão neste CD (clique em Software). Se você for executar o spim em um PC com o Microsoft Windows, deverá primeiro dar uma olhada no tutorial sobre PCspim neste CD. Se você executar o spim em um computador com UNIX ou Linux, deverá ler o tutorial (em inglês) sobre xspim (clique em Tutoriais).
Recursos surpreendentes Embora o SPIM fielmente simule o computador MIPS, o SPIM é um simulador, e certas coisas não são idênticas a um computador real. As diferenças mais óbvias são que a temporização da instrução e o sistema de memória não são idênticos. O SPIM não simula caches ou a latência da memória, nem reflete com precisão os atrasos na operação de ponto flutuante ou nas instruções de multiplicação e divisão. Além disso, as instruções de ponto flutuante não detectam muitas condições de erro, o que deveria causar exceções em uma máquina real. Outra surpresa (que também ocorre na máquina real) é que uma pseudoinstrução se expande para várias instruções de máquina. Quando você examina a memória passo a passo, as instruções que encontra são diferentes daquelas do programa-fonte. A correspondência entre os dois conjuntos de instruções é muito simples, pois o SPIM não reorganiza as instruções para preencher delay slots.
Ordem de bytes Os processadores podem numerar os bytes dentro de uma palavra de modo que o byte com o número mais baixo seja o mais à esquerda ou o mais à direita. A convenção usada por uma máquina é considerada sua ordem de bytes. Os processadores MIPS podem operar com a ordem de bytes big-endian ou little-endian. Por exemplo, em uma máquina big-endian, a diretiva .byte 0, 1, 2, 3 resultaria em uma palavra de memória contendo Byte # 0
1
2
3
enquanto, em uma máquina little-endian, a palavra seria Byte # 3
2
1
0
O SPIM opera com duas ordens de bytes. A ordem de bytes do SPIM é a mesma ordem de bytes da máquina utilizada para executar o simulador. Por exemplo, em um Intel 80x86, o SPIM é little-endian, enquanto em um Macintosh ou Sun SPARC, o SPIM é big-endian.
B-662
Apêndice B Montadores, Link-editores e o Simulador SPIM
Chamadas ao sistema O SPIM oferece um pequeno conjunto de serviços semelhantes aos oferecidos pelo sistema operacional, por meio da instrução de chamada ao sistema (syscall). Para requisitar um serviço, um programa carrega o código da chamada ao sistema (veja Figura B.9.1) no registrador $v0 e os argumentos nos registradores $a0—$a3 (ou $f12, para valores de ponto flutuante). As chamadas ao sistema que retornam valores colocam seus resultados no registrador $v0 (ou $f0 para resultados de ponto flutuante). Por exemplo, o código a seguir imprime “the answer=5”:
A chamada ao sistema print_int recebe um inteiro e o imprime no console.print_ float imprime um único número de ponto flutuante; print_double imprime um número de precisão dupla; e print_string recebe um ponteiro para uma string terminada em nulo, que ele escreve no console. As chamadas ao sistema read_int, read_float e read_double leem uma linha inteira da entrada, até o caractere de newline, inclusive. Os caracteres após o número são
FIGURA B.9.1 Serviços do sistema.
B.10 Assembly do MIPS R2000 B-663
ignorados. read_string possui a mesma semântica da rotina de biblioteca do UNIX fgets. Ela lê até n – 1 caracteres para um buffer e termina a string com um byte nulo. Se menos de n – 1 caracteres estiverem na linha atual, read_string lê até o caractere de newline, inclusive, e novamente termina a string com nulo. Aviso: os programas que usam essas syscalls para ler do terminal não deverão usar E/S mapeada em memória (ver Seção B.8). sbrk retorna um ponteiro para um bloco de memória contendo n bytes adicionais. exit interrompe o programa que o SPIM estiver executando.exit2 termina o programa SPIM, e o argumento de exit2 torna-se o valor retornado quando o próprio simulador SPIM termina. print_char e read_char escrevem e leem um único caractere, respectivamente. open, read, write e close são as chamadas da biblioteca padrão do UNIX.
B.10 Assembly do MIPS R2000
Um processador MIPS consiste em uma unidade de processamento de inteiros (a CPU) e uma coleção de coprocessadores que realizam tarefas auxiliares ou operam sobre outros tipos de dados, como números de ponto flutuante (veja Figura B.10.1). O SPIM simula dois coprocessadores. O coprocessador 0 trata de exceções e interrupções. O coprocessador 1 é a unidade de ponto flutuante. O SPIM simula a maior parte dos aspectos dessa unidade.
Modos de endereçamento O MIPS é uma arquitetura load-store, o que significa que somente instruções load e store acessam a memória. As instruções de cálculo operam apenas sobre os valores nos registradores. A máquina pura oferece apenas um modo de endereçamento de memória: c(rx), que usa a soma do c imediato e do registrador rx como endereço. A máquina virtual oferece os seguintes modos de endereçamento para instruções load e store: Formato (registrador)
Cálculo de endereço conteúdo do registrador
imm
imediato
imm (registrador)
imediato + conteúdo do registrador
rótulo
endereço do rótulo
rótulo ± imediato
endereço do rótulo + ou – imediato
rótulo ± imediato (registrador)
endereço do rótulo + ou – (imediato + conteúdo do registrador)
A maior parte das instruções load e store opera apenas sobre dados alinhados. Uma quantidade está alinhada se seu endereço de memória for um múltiplo do seu tamanho em bytes. Portanto, um objeto halfword precisa ser armazenado em endereços pares e um objeto palavra (word) precisa ser armazenado em endereços que são múltiplos de quatro. No entanto, o MIPS oferece algumas instruções para manipular dados não alinhados (lwl, lwr, swl e swr).
Detalhamento: O montador MIPS (e SPIM) sintetiza os modos de endereçamento mais complexos, produzindo uma ou mais instruções antes que o load ou o store calculem um endereço complexo. Por exemplo, suponha que o rótulo table referenciasse o local de memória 0 × 10000004 e um programa tivesse a instrução
B-664
Apêndice B Montadores, Link-editores e o Simulador SPIM
FIGURA B.10.1 CPU e FPU do MIPS R2000.
O montador traduziria essa instrução para as instruções
A primeira instrução carrega os bits mais significativos do endereço do rótulo no registrador $at, que é o registrador que o montador reserva para seu próprio uso. A segunda instrução acrescenta o conteúdo do registrador $a1 ao endereço parcial do rótulo. Finalmente, a instrução load utiliza o modo de endereçamento de hardware para adicionar a soma dos bits menos significativos do endereço do rótulo e o offset da instrução original ao valor no registrador $at.
Sintaxe do montador Os comentários nos arquivos do montador começam com um sinal de sharp (#). Tudo desde esse sinal até o fim da linha é ignorado. Os identificadores são uma sequência de caracteres alfanuméricos, símbolos _ e pontos (.), que não começam com um número. Os opcodes de instrução são palavras reservadas que não podem ser usadas como identificadores. Rótulos são declarados por sua colocação no início de uma linha e seguidos por um sinal de dois-pontos, por exemplo:
B.10 Assembly do MIPS R2000 B-665
Os números estão na base 10 por padrão. Se eles forem precedidos por 0x, serão interpretados como hexadecimais. Logo, 256 e 0x100 indicam o mesmo valor. As strings são delimitadas com aspas (“). Caracteres especiais nas strings seguem a convenção da linguagem C: j
nova linha \n
j
tabulação \t
j
aspas \”
O SPIM admite um subconjunto das diretivas do montador do MIPS: .align n
Alinha o próximo dado em um limite de 2n bytes. Por exemplo, .align 2 alinha o próximo valor em um limite da palavra. .align 0 desativa o alinhamento automático das diretivas .half, .word, .float e .double até a próxima diretiva .data ou .kdata.
.ascii str
Armazena a string str na memória, mas não a termina com nulo.
.asciiz str
Armazena a string str na memória e a termina com nulo.
.byte b1,..., bn
Armazena os n valores em bytes sucessivos da memória.
.data
Itens subsequentes são armazenados no segmento de dados. Se o argumento opcional end estiver presente, os itens subsequentes são armazenados a partir do endereço end.
.double d1,...,dn
Armazena os n números de precisão dupla em ponto flutuante em locais de memória sucessivos.
.extern sym tamanho
Declara que o dado armazenado em sym possui tamanho bytes de extensão e é um rótulo global. Essa diretiva permite que o montador armazene o dado em uma parte do segmento de dados que é acessado eficientemente por meio do registrador $gp
.float f1,..., fn
Armazena os n números de precisão simples em ponto flutuante nos locais de memória sucessivos.
.globl sym
Declara que o rótulo sym é global e pode ser referenciado a partir de outros arquivos.
.half h1,..., hn
Armazena as n quantidades de 16 bits em halfwords sucessivas da memória.
.kdata
Itens de dados subsequentes são armazenados no segmento de dados do kernel. Se o argumento opcional end estiver presente, itens subsequentes são armazenados a partir do endereço end.
.ktext
Itens subsequentes são colocados no segmento de texto do kernel. No SPIM, esses itens só podem ser instruções ou palavras (ver a diretiva .word, mais adiante). Se o argumento opcional end estiver presente, os itens subsequentes são armazenados a partir do endereço end.
.set noat e .sat at
A primeira diretiva impede que o SPIM reclame sobre instruções subsequentes que utilizam o registrador $at. A segunda diretiva reativa a advertência. Como as pseudoinstruções se expandem para o código que usa o registrador $at, os programadores precisam ter muito cuidado ao deixar valores nesse registrador.
.space n
Aloca n bytes de espaço no segmento atual (que precisa ser o segmento de dados no SPIM).
.text
Itens subsequentes são colocados no segmento de texto do usuário. No SPIM, esses itens só podem ser instruções ou palavras (ver a diretiva .word a seguir). Se o argumento opcional end estiver presente, os itens subsequentes são armazenados a partir do endereço end.
.word w1,..., wn
Armazena as n quantidades de 32 bits em palavras de memória sucessivas.
O SPIM não distingue as várias partes do segmento de dados (.data, .rdata e .sdata).
B-666
Apêndice B Montadores, Link-editores e o Simulador SPIM
Codificando instruções do MIPS A Figura B.10.2 explica como uma instrução MIPS é codificada em um número binário. Cada coluna contém codificações de instrução para um campo (um grupo de bits contíguo) a partir de uma instrução. Os números na margem esquerda são valores para um campo. Por exemplo, o opcode j possui um valor 2 no campo opcode. O texto no topo de uma
FIGURA B.10.2 Mapa de opcode do MIPS. Os valores de cada campo aparecem à sua esquerda. A primeira coluna mostra os valores na base 10 e a segunda mostra a base 16 para o campo op (bits 31 a 26) na terceira coluna. Esse campo op especifica completamente a operação do MIPS, exceto para 6 valores de op: 0, 1, 16, 17, 18 e 19. Essas operações são determinadas pelos outros campos, identificados por ponteiros. O último campo (funct) utiliza “f ” para indicar “s” se rs = 16 e op = 17 ou “d” se rs = 17 e op = 17. O segundo campo (rs) usa “z” para indicar “0“, “1”, “2” ou “3” se op = 16, 17, 18 ou 19, respectivamente. Se rs = 16, a operação é especificada em outro lugar: se z = 0, as operações são especificadas no quarto campo (bits 4 a 0); se z = 1, então as operações são no último campo com f = s. Se rs = 17 e z = 1, então as operações estão no último campo com f = d.
B.10 Assembly do MIPS R2000 B-667
coluna nomeia um campo e especifica quais bits ele ocupa em uma instrução. Por exemplo, o campo op está contido nos bits 26-31 de uma instrução. Esse campo codifica a maioria das instruções. No entanto, alguns grupos de instruções utilizam campos adicionais para distinguir instruções relacionadas. Por exemplo, as diferentes instruções de ponto flutuante são especificadas pelos bits 0-5. As setas a partir da primeira coluna mostram quais opcodes utilizam esses campos adicionais.
Formato de instrução O restante deste apêndice descreve as instruções implementadas pelo hardware MIPS real e as pseudoinstruções fornecidas pelo montador MIPS. Os dois tipos de instruções podem ser distinguidos facilmente. As instruções reais indicam os campos em sua representação binária. Por exemplo, em: Adição (com overflow)
a instrução add consiste em seis campos. O tamanho de cada campo em bits é o pequeno número abaixo do campo. Essa instrução começa com 6 bits em 0. Os especificadores de registradores começam com um r, de modo que o próximo campo é um especificador de registrador de 5 bits chamado rs. Esse é o mesmo registrador que é o segundo argumento no assembly simbólico à esquerda dessa linha. Outro campo comum é imm16, que é um número imediato de 16 bits. As pseudoinstruções seguem aproximadamente as mesmas convenções, mas omitem a informação de codificação de instrução. Por exemplo: Multiplicação (sem overflow)
Nas pseudoinstruções, rdest e rsrcl são registradores, e src2 é um registrador ou um valor imediato. Em geral, o montador e o SPIM traduzem uma forma mais geral de uma instrução (por exemplo, add $v1, $a0, 0x55) para uma forma especializada (por exemplo, addi $v1, $a0, 0x55).
Instruções aritméticas e lógicas Valor absoluto
Coloca o valor absoluto do registrador rsrc no registrador rdest. Adição (com overflow)
Adição (sem overflow)
Coloca a soma dos registradores rs e rt no registrador rd.
B-668
Apêndice B Montadores, Link-editores e o Simulador SPIM
Adição imediato (com overflow)
Adição imediato (sem overflow)
Coloca a soma do registrador rs e o imediato com sinal estendido no registrador rt. AND
Coloca o AND lógico dos registradores rs e rt no registrador rd. AND imediato
Coloca o AND lógico do registrador rs e o imediato estendido com zeros no registrador rt. Contar uns iniciais
Contar zeros iniciais
Conta o número de uns (zeros) iniciais da palavra no registrador rs e coloca o resultado no registrador rd. Se uma palavra contém apenas uns (zeros), o resultado é 32. Divisão (com overflow)
Divisão (sem overflow)
Divide o registrador rs pelo registrador. Deixa o quociente no registrador e o resto no registrador hi. Observe que, se um operando for negativo, o restante não será especificado pela arquitetura MIPS e dependerá da convenção da máquina em que o SPIM é executado.
B.10 Assembly do MIPS R2000 B-669
Divisão (com overflow)
Divisão (sem overflow)
Coloca o quociente do registrador rsrcl pelo src2 no registrador rdest. Multiplicação
Multiplicação sem sinal
Multiplica os registradores rs e rt. Deixa a palavra menos significativa do produto no registrador lo e a palavra mais significativa no registrador hi. Multiplicação (sem overflow)
Coloca os 32 bits menos significativos do produto de rs e rt no registrador rd. Multiplicação (com overflow)
Multiplicação sem sinal (com overflow)
Coloca os 32 bits menos significativos do produto do registrador rsrcl e src2 no registrador rdest. Multiplicação adição
Multiplicação adição sem sinal
Multiplica os registradores rs e rt e soma o produto de 64 bits resultante ao valor de 64 bits nos registradores concatenados lo e hi.
B-670
Apêndice B Montadores, Link-editores e o Simulador SPIM
Multiplicação subtração
Multiplicação subtração sem sinal
Multiplica os registradores rs e rt e subtrai o produto de 64 bits resultante do valor de 64 bits nos registradores concatenados lo e hi. Negar valor (com overflow)
Negar valor (sem overflow)
Coloca o negativo do registrador rsrc no registrador rdest. NOR
Coloca o NOR lógico dos registradores rs e rt para o registrador rd. NOT
Coloca a negação lógica bit a bit do registrador rsrc no registrador rdest. OR
Coloca o OR lógico dos registradores rs e rt no registrador rd. OR imediato
Coloca o OR lógico do registrador rs e o imediato estendido com zero no registrador rt. Resto
B.10 Assembly do MIPS R2000 B-671
Resto sem sinal
Coloca o resto do registrador rsrc1 dividido pelo registrador rsrc2 no registrador rdest. Observe que se um operando for negativo, o resto não é especificado pela arquitetura MIPS e depende da convenção da máquina em que o SPIM é executado. Shift lógico à esquerda
Shift lógico à esquerda variável
Shift aritmético à direita
Shift aritmético à direita variável
Shift lógico à direita
Shift lógico à direita variável
Desloca o registrador rt à esquerda (direita) pela distância indicada pelo shamt imediato ou pelo registrador rs e coloca o resultado no registrador rd. Observe que o argumento rs é ignorado para sll, sra e srl. Rotate à esquerda
Rotate à direita
Gira o registrador rsrc1 à esquerda (direita) pela distância indicada por rsrc2 e coloca o resultado no registrador rdest.
B-672
Apêndice B Montadores, Link-editores e o Simulador SPIM
Subtração (com overflow)
Subtração (sem overflow)
Coloca a diferença dos registradores rs e rt no registrador rd. OR exclusivo
Coloca o XOR lógico dos registradores rs e rt no registrador rd. XOR imediato
Coloca o XOR lógico do registrador rs e o imediato estendido com zeros no registrador rt.
Instruções para manipulação de constantes Load superior imediato
Carrega a halfword menos significativa do imediato imm na halfword mais significativa do registrador rt. Os bits menos significativos do registrador são colocados em 0. Load imediato
Move o imediato imm para o registrador rdest.
Instruções de comparação Set se menor que
Set se menor que sem sinal
Coloca o registrador rd em 1 se o registrador rs for menor que rt ; caso contrário, coloca-o em 0.
B.10 Assembly do MIPS R2000 B-673
Set se menor que imediato
Set se menor que imediato sem sinal
Coloca o registrador rt em 1 se o registrador rs for menor que o imediato estendido com sinal, e em 0 em caso contrário. Set se igual
Coloca o registrador rdest em 1 se o registrador rsrcl for igual a rsrc2, e em 0 caso contrário. Set se maior ou igual
Set se maior ou igual sem sinal
Coloca o registrador rdest em 1 se o registrador rsrc1 for maior ou igual a rsrc2, e em 0 caso contrário. Set se maior que
Set se maior que sem sinal
Coloca o registrador rdest em 1 se o registrador rsrc1 for maior que rsrc2, e em 0 caso contrário. Set se menor ou igual
Set se menor ou igual sem sinal
Coloca o registrador rdest em 1 se o registrador rsrc1 for menor ou igual a rsrc2, e em 0 caso contrário.
B-674
Apêndice B Montadores, Link-editores e o Simulador SPIM
Set se diferente
Coloca o registrador rdest em 1 se o registrador rsrc1 não for igual a rsrc2, e em 0 caso contrário.
Instruções de desvio As instruções de desvio utilizam um campo offset de instrução de 16 bits com sinal; logo, elas podem desviar 215 – 1 instruções (não bytes) para frente ou 215 instruções para trás. A instrução jump contém um campo de endereço de 26 bits. Em processadores MIPS reais, as instruções de desvio são delayed branches, que não transferem o controle até que a instrução após o desvio (seu “delay slot”) tenha sido executada (veja Capítulo 4). Os delayed branches afetam o cálculo de offset, pois precisam ser calculados em relação ao endereço da instrução do delay slot (PC + 4), que é quando o desvio ocorre. O SPIM não simula esse delay slot, a menos que os flags —bare ou —delayed_branch sejam especificados. No código assembly, os offsets normalmente não são especificados como números. Em vez disso, uma instrução desvia para um rótulo, e o montador calcula a distância entre o desvio e a instrução destino. No MIPS-32, todas as instruções de desvio condicional reais (não pseudo) têm uma variante “provável” (por exemplo, a variável provável de beq é beql), que não executa a instrução no delay slot do desvio se o desvio não for tomado. Não use essas instruções; elas poderão ser removidas em versões subsequentes da arquitetura. O SPIM implementa essas instruções, mas elas não são descritas daqui por diante. Branch
Desvia incondicionalmente para a instrução no rótulo. Branch co-processador falso
Branch co-processador verdadeiro
Desvia condicionalmente pelo número de instruções especificado pelo offset se o flag de condição de ponto flutuante numerado como cc for falso (verdadeiro). Se cc for omitido da instrução, o flag de código de condição 0 é assumido. Branch se for igual
Desvia condicionalmente pelo número de instruções especificado pelo offset se o registrador rs for igual a rt.
B.10 Assembly do MIPS R2000 B-675
Branch se for maior ou igual a zero
Desvia condicionalmente pelo número de instruções especificado pelo offset se o registrador rs for maior ou igual a 0. Branch se for maior ou igual a zero e link
Desvia condicionalmente pelo número de instruções especificado pelo offset se o registrador rs for maior ou igual a 0. Salva o endereço da próxima instrução no registrador 31. Branch se for maior que zero
Desvia condicionalmente pelo número de instruções especificado pelo offset se o registrador rs for maior que 0. Branch se for menor ou igual a zero
Desvia condicionalmente pelo número de instruções especificado pelo offset se o registrador rs for menor ou igual a 0. Branch se for menor e link
Desvia condicionalmente pelo número de instruções especificado pelo offset se o registrador rs for menor que 0. Salva o endereço da próxima instrução no registrador 31. Branch se for menor que zero
Desvia condicionalmente pelo número de instruções especificado pelo offset se o registrador rs for menor que 0. Branch se for diferente
Desvia condicionalmente pelo número de instruções especificado pelo offset se o registrador rs não for igual a rt.
B-676
Apêndice B Montadores, Link-editores e o Simulador SPIM
Branch se for igual a zero
Desvia condicionalmente para a instrução no rótulo se rsrc for igual a 0. Branch se for maior ou igual
Branch se for maior ou igual com sinal
Desvia condicionalmente até a instrução no rótulo se o registrador rsrc1 for maior ou igual a rsrc2. Branch se for maior
Branch se for maior sem sinal
Desvia condicionalmente para a instrução no rótulo se o registrador rsrc1 for maior do que src2. Branch se for menor ou igual
Branch se for menor ou igual sem sinal
Desvia condicionalmente para a instrução no rótulo se o registrador rsrc1 for menor ou igual a rsrc2. Branch se for menor
Branch se for menor sem sinal
Desvia condicionalmente para a instrução no rótulo se o registrador rsrc1 for menor do que src2. Branch se não for igual a zero
Desvia condicionalmente para a instrução no rótulo se o registrador rsrc não for igual a 0.
B.10 Assembly do MIPS R2000 B-677
Instruções de jump Jump
Desvia incondicionalmente para a instrução no destino. Jump-and-link
Desvia incondicionalmente para a instrução no destino. Salva o endereço da próxima instrução no registrador $ra. Jump-and-link registrador
Desvia incondicionalmente para a instrução cujo endereço está no registrador rs. Salva o endereço da próxima instrução no registrador rd (cujo default é 31). Jump registrador
Desvia incondicionalmente para a instrução cujo endereço está no registrador rs.
Instruções de trap Trap se for igual
Se o registrador rs for igual ao registrador rt, gera uma exceção de Trap. Trap se for igual imediato
Se o registrador rs for igual ao valor de imm com sinal estendido, gera uma exceção de Trap. Trap se não for igual
Se o registrador rs não for igual ao registrador rt, gera uma exceção de Trap.
B-678
Apêndice B Montadores, Link-editores e o Simulador SPIM
Trap se não for igual imediato
Se o registrador rs não for igual ao valor de imm com sinal estendido, gera uma exceção de Trap. Trap se for maior ou igual
Trap sem sinal se for maior ou igual
Se o registrador rs for maior ou igual ao registrador rt, gera uma exceção de Trap. Trap se for maior ou igual imediato
Trap sem sinal se for maior ou igual imediato
Se o registrador rs for maior ou igual ao valor de imm com sinal estendido, gera uma exceção de Trap. Trap se for menor
Trap sem sinal se for menor
Se o registrador rs for menor que o registrador rt, gera uma exceção de Trap. Trap se for menor imediato
Trap sem sinal se for menor imediato
Se o registrador rs for menor do que o valor de imm com sinal estendido, gera uma exceção de Trap.
B.10 Assembly do MIPS R2000 B-679
Instruções load Load endereço
Carrega o endereço calculado – não o conteúdo do local – para o registrador rdest. Load byte
Load byte sem sinal
Carrega o byte no endereço para o registrador rt. O byte tem sinal estendido por lb, mas não por lbu. Load halfword
Load halfword sem sinal
Carrega a quantidade de 16 bits (halfword) no endereço para o registrador rt. A halfword tem sinal estendido por lh, mas não por lhu. Load word
Carrega a quantidade de 32 bits (palavra) no endereço para o registrador rt. Load word coprocessador 1
Carrega a palavra no endereço para o registrador ft da unidade de ponto flutuante. Load word à esquerda
B-680
Apêndice B Montadores, Link-editores e o Simulador SPIM
Load word à direita
Carrega os bytes da esquerda (direita) da palavra do endereço possivelmente não alinhado para o registrador rt. Load doubleword
Carrega a quantidade de 64 bits no endereço para os registradores rdest e rdest+1. Load halfword não alinhada
Load halfword sem sinal não alinhada
Carrega a quantidade de 16 bits (halfword) no endereço possivelmente não alinhado para o registrador rdest. A halfword tem extensão de sinal por ulh, mas não ulhu. Load word não alinhada
Carrega a quantidade de 32 bits (palavra) no endereço possivelmente não alinhado para o registrador rdest. Load Linked
Carrega a quantidade de 32 bits (palavra) no endereço para o registrador rt e inicia uma operação ler-modificar-escrever indivisível. Essa operação é concluída por uma instrução de armazenamento condicional (sc), que falhará se outro processador escrever no bloco que contém a palavra carregada. Como o SPIM não simula processadores múltiplos, a operação de armazenamento condicional sempre tem sucesso.
Instruções store Store byte
Armazena o byte baixo do registrador rt no endereço. Store halfword
Armazena a halfword baixa do registrador rt no endereço.
B.10 Assembly do MIPS R2000 B-681
Store word
Armazena a palavra do registrador rt no endereço. Store word coprocessador 1
Armazena o valor de ponto flutuante no registrador ft do coprocessador de ponto flutuante no endereço. Store double coprocessador 1
Armazena o valor de ponto flutuante da dupla palavra nos registradores ft e ft +1 do coprocessador de ponto flutuante em endereço. O registrador ft precisa ser um número par. Store word à esquerda
Store word à direita
Armazena os bytes da esquerda (direita) do registrador rt no endereço possivelmente não alinhado. Store doubleword
Armazena a quantidade de 64 bits nos registradores rsrc e rsrc+1 no endereço. Store halfword não alinhada
Armazena a halfword baixa do registrador r s r c no endereço possivelmente não alinhado. Store word não alinhada
Armazena a palavra do registrador rsrc no endereço possivelmente não alinhado.
B-682
Apêndice B Montadores, Link-editores e o Simulador SPIM
Store condicional
Armazena a quantidade de 32 bits (palavra) no endereço rt para a memória no endereço e completa uma operação ler-modificar-escrever indivisível. Se essa operação indivisível tiver sucesso, a palavra da memória será modificada e o registrador rt será colocado em 1. Se a operação indivisível falhar porque outro processador escreveu em um local no bloco contendo a palavra endereçada, essa instrução não modifica a memória e escreve 0 no registrador rt. Como o SPIM não simula diversos processadores, a instrução sempre tem sucesso.
Instruções para movimentação de dados Move
Move o registrador rsrc para rdest. Move de hi
Move de lo
A unidade de multiplicação e divisão produz seu resultado em dois registradores adicionais, hi e lo. Essas instruções movem os valores de e para esses registradores. As pseudoinstruções de multiplicação, divisão e resto que fazem com que essa unidade pareça operar sobre os registradores gerais movem o resultado depois que o cálculo terminar. Move o registrador hi (lo) para o registrador rd. Move para hi
Move para lo
Move o registrador rs para o registrador hi (lo). Move do coprocessador 0
B.10 Assembly do MIPS R2000 B-683
Move do coprocessador 1
Os co-processadores têm seus próprios conjuntos de registradores. Essas instruções movem valores entre esses registradores e os registradores da CPU. Move o registrador rd em um co-processador (registrador fs na FPU) para o registrador rt da CPU. A unidade de ponto flutuante é o co-processador 1. Move double do coprocessador 1
Move os registradores de ponto flutuante frsc1 e frsrc1 +1 para os registradores da CPU rdest e rdest+1. Move para coprocessador 0
Move para coprocessador 1
Move o registrador da CPU rt para o registrador rd em um coprocessador (registrador fs na FPU). Move condicional diferente de zero
Move o registrador rs para o registrador rd se o registrador rt não for 0. Move condicional zero
Move o registrador rs para o registrador rd se o registrador rt for 0. Move condicional em caso de FP falso
Move o registrador da CPU rs para o registrador rd se o flag de código de condição da FPU número cc for 0. Se cc for omitido da instrução, o flag de código de condição 0 será assumido. Move condicional em caso de FP verdadeiro
Move o registrador da CPU rs para o registrador rd se o flag de código de condição da FPU número cc for 1. Se cc for omitido da instrução, o bit de código de condição 0 é assumido.
B-684
Apêndice B Montadores, Link-editores e o Simulador SPIM
Instruções de ponto flutuante O MIPS possui um coprocessador de ponto flutuante (número 1) que opera sobre números de ponto flutuante de precisão simples (32 bits) e precisão dupla (64 bits). Esse coprocessador tem seus próprios registradores, que são numerados de $f0 a $f31. Como esses registradores possuem apenas 32 bits, dois deles são necessários para manter doubles, de modo que somente registradores de ponto flutuante com números pares podem manter valores de precisão dupla. O coprocessador de ponto flutuante também possui 8 flags de código de condição (cc), numerados de 0 a 7, que são alterados por instruções de comparação e testados por instruções de desvio (bclf ou bclt) e instruções move condicionais. Os valores são movidos para dentro e para fora desses registradores uma palavra (32 bits) de cada vez pelas instruções lwc1, swc1, mtc1 e mfc1ou um double (64 bits) de cada vez por ldc1 e sdc1, descritos anteriormente, ou pela pseudoinstruções l.s, l.d, s.s e s.d, descritas a seguir. Nas instruções reais a seguir, os bits 21-26 são 0 para precisão simples e 1 para precisão double. Nas pseudoinstruções a seguir, fdest é um registrador de ponto flutuante (por exemplo, $f2). Valor absoluto de ponto flutuante double
Valor absoluto de ponto flutuante single
Calcula o valor absoluto do double (single) de ponto flutuante no registrador fs e o coloca no registrador fd. Adição de ponto flutuante double
Adição de ponto flutuante single
Calcula a soma dos doubles (singles) de ponto flutuante nos registradores fs e ft e a coloca no registrador fd. Teto de ponto flutuante para word
Calcula o teto do double (single) de ponto flutuante no registrador fs, converte para um valor de ponto fixo de 32 bits e coloca a palavra resultante no registrador fd.
B.10 Assembly do MIPS R2000 B-685
Comparação igual double
Comparação igual single
Compara o double (single) de ponto flutuante no registrador fs com aquele em ft e coloca o flag de condição de ponto flutuante cc em 1 se forem iguais. Se cc for omitido, o flag de código de condição 0 é assumido. Comparação menor ou igual double
Comparação menor ou igual single
Compara o double (single) de ponto flutuante no registrador fs com aquele no ft e coloca o flag de condição de ponto flutuante cc em 1 se o primeiro for menor ou igual ao segundo. Se o cc for omitido, o flag de código de condição 0 é assumido. Comparação menor que double
Comparação menor que single
Compara o double (single) de ponto flutuante no registrador fs com aquele no ft e coloca o flag de condição de ponto flutuante cc em 1 se o primeiro for menor que o segundo. Se o cc for omitido, o flag de código de condição 0 é assumido. Converte single para double
Converte integer para double
Converte o número de ponto flutuante de precisão simples ou inteiro no registrador fs para um número de precisão dupla (simples) e o coloca no registrador fd.
B-686
Apêndice B Montadores, Link-editores e o Simulador SPIM
Converte double para single
Converte integer para single
Converte o número de ponto flutuante de precisão dupla ou inteiro no registrador fs para um número de precisão simples e o coloca no registrador fd. Converte double para integer
Converte single para integer
Converte o número de ponto flutuante de precisão dupla ou simples no registrador fs para um inteiro e o coloca no registrador fd. Divisão de ponto flutuante double
Divisão de ponto flutuante single
Calcula o quociente dos números de ponto flutuante de precisão dupla (simples) nos registradores fs e ft e o coloca no registrador fd. Piso de ponto flutuante para palavra
Calcula o piso do número de ponto flutuante de precisão dupla (simples) no registrador fs e coloca a palavra resultante no registrador fd. Carrega double de ponto flutuante
Carrega single de ponto flutuante
Carrega o número de ponto flutuante de precisão dupla (simples) em address para o registrador fdest.
B.10 Assembly do MIPS R2000 B-687
Move ponto flutuante double
Move ponto flutuante single
Move o número de ponto flutuante de precisão dupla (simples) do registrador fs para o registrador fd. Move condicional de ponto flutuante double se falso
Move condicional de ponto flutuante single se falso
Move o número de ponto flutuante de precisão dupla (simples) do registrador fs para o registrador fd se o flag do código de condição cc for 0. Se o cc for omitido, o flag de código de condição 0 é assumido. Move condicional de ponto flutuante double se verdadeiro
Move condicional de ponto flutuante single se verdadeiro
Move o double (single) de ponto flutuante do registrador fs para o registrador fd se o flag do código de condição cc for 1. Se o cc for omitido, o flag do código de condição 0 será assumido. Move ponto flutuante double condicional se não for zero
Move ponto flutuante single condicional se não for zero
Move o número de ponto flutuante double (single) do registrador fs para o registrador fd se o registrador rt do processador não for 0.
B-688
Apêndice B Montadores, Link-editores e o Simulador SPIM
Move ponto flutuante double condicional se for zero
Move ponto flutuante single condicional se for zero
Move o número de ponto flutuante double (single) do registrador fs para o registrador fd se o registrador rt do processador for 0. Multiplicação de ponto flutuante double
Multiplicação de ponto flutuante single
Calcula o produto dos números de ponto flutuante double (single) nos registradores fs e ft e o coloca no registrador fd. Negação double
Negação single
Nega o número de ponto flutuante double (single) no registrador fs e o coloca no registrador fd. Arredondamento de ponto flutuante para palavra
Arredonda o valor de ponto flutuante double (single) no registrador fs, converte para um valor de ponto fixo de 32 bits e coloca a palavra resultante no registrador fd. Raiz quadrada de double
B.10 Assembly do MIPS R2000 B-689
Raiz quadrada de single
Calcula a raiz quadrada do número de ponto flutuante double (single) no registrador fs e a coloca no registrador fd. Store de ponto flutuante double
Store de ponto flutuante single
Armazena o número de ponto flutuante double (single) no registrador fdest em address. Subtração de ponto flutuante double
Subtração de ponto flutuante single
Calcula a diferença dos números de ponto flutuante double (single) nos registradores fs e ft e a coloca no registrador fd. Truncamento de ponto flutuante para palavra
Trunca o valor de ponto flutuante double (single) no registrador fs, converte para um valor de ponto fixo de 32 bits e coloca a palavra resultante no registrador fd.
Instruções de exceção e interrupção Retorno de exceção
Coloca em 0 o bit EXL no registrador Status do coprocessador 0 e retorna à instrução apontada pelo registrador EPC do coprocessador 0.
B-690
Apêndice B Montadores, Link-editores e o Simulador SPIM
Chamada ao sistema
O registrador $v0 contém o número da chamada ao sistema (veja Figura B.9.1) fornecido pelo SPIM. Break
Causa a exceção código. A Exceção 1 é reservada para o depurador. Nop
Não faz nada.
B.11 Comentários finais
A programação em assembly exige que um programador escolha entre os recursos úteis das linguagens de alto nível – como estruturas de dados, verificação de tipo e construções de controle – e o controle completo sobre as instruções que um computador executa. Restrições externas sobre algumas aplicações, como o tempo de resposta ou o tamanho do programa, exigem que um programador preste muita atenção a cada instrução. No entanto, o custo desse nível de atenção são programas em assembly maiores, mais demorados para escrever e mais difícil de manter do que os programas em linguagem de alto nível. Além do mais, três tendências estão reduzindo a necessidade de escrever programas em assembly. A primeira tendência é em direção à melhoria dos compiladores. Os compiladores modernos produzem código comparável ao melhor código escrito manualmente – e, às vezes, melhor ainda. A segunda tendência é a introdução de novos processadores, que não apenas são mais rápidos, mas, no caso de processadores que executam várias instruções ao mesmo tempo, também são mais difíceis de programar manualmente. Além disso, a rápida evolução dos computadores modernos favorece os programas em linguagem de alto nível que não estejam presos a uma única arquitetura. Finalmente, temos testemunhado uma tendência em direção a aplicações cada vez mais complexas, caracterizadas por interfaces gráficas complexas e muito mais recursos do que seus predecessores. Grandes aplicações são escritas por equipes de programadores e exigem recursos de modularidade e verificação semântica fornecidos pelas linguagens de alto nível.
Leitura adicional Aho, B., R. Sethi e J. Ullman [1985]. Compilers: Principles, Techniques, and Tools, Reading, MA: Addison-Wesley. Ligeiramente desatualizado e faltando a cobertura das arquiteturas modernas, mas ainda é a referência padrão sobre compiladores. Sweetman, D. [1999]. See MIPS Run, San Francisco CA: Morgan Kaufmann Publishers.
B.12 Exercícios B-691
Uma introdução completa, detalhada e envolvente sobre o conjunto de instruções do MIPS e a programação em assembly nessas máquinas. A documentação detalhada sobre a arquitetura MIPS-32 está disponível na Web: MIPS32™ Architecture for Programmers Volume I: Introduction to the MIPS-32 Architecture (http://mips.com/content/Documentation/MIPSDocumentation/ProcessorArchitecture/ ArchitectureProgrammingPublicationsforMIPS32/MD00082-2B-MIPS32INT-AFP-02.00. pdf/getDownload) MIPS32™ Architecture for Programmers Volume II: The MIPS-32 Instruction Set (http://mips.com/content/Documentation/MIPSDocumentation/ProcessorArchitecture/ ArchitectureProgrammingPublicationsforMIPS32/MD00086-2B-MIPS32BIS-AFP-02.00. pdf/getDownload) MIPS32™ Architecture for Programmers Volume III: The MIPS-32 Privileged Resource Architecture (http://mips.com/content/Documentation/MIPSDocumentation/ProcessorArchitecture/ ArchitectureProgrammingPublicationsforMIPS32/MD00090-2B-MIPS32PRA-AFP-02.00. pdf/getDownload)
B.12 Exercícios
B.1 [5] <§B.5> A Seção B.5 descreveu como a memória é dividida na maioria dos sistemas MIPS. Proponha outra maneira de dividir a memória, que cumpra os mesmos objetivos. B.2 [20] <§B.6> Reescreva o código para fact utilizando menos instruções. B.3 [5] <§B.7> É seguro que um programa do usuário utilize os registradores $k0 ou $k1? B.4 [25] <§B.7> A Seção B.7 contém código para um handler de exceção muito simples. Um problema sério com esse handler é que ele desativa as interrupções por um longo tempo. Isso significa que as interrupções de um dispositivo de E/S rápido podem ser perdidas. Escreva um handler de exceção melhor, que não possa ser interrompido mas que ative as interrupções o mais rápido possível. B.5 [15] <§B.7> O handler de exceção simples sempre desvia para a instrução após a exceção. Isso funciona bem, a menos que a instrução que causou a exceção esteja no delay slot de um desvio. Nesse caso, a próxima instrução será o alvo do desvio. Escreva um handler melhor, que use o registrador EPC para determinar qual instrução deverá ser executada após a exceção. B.6 [5] <§B.9> Usando o SPIM, escreva e teste um programa de calculadora que leia inteiros repetidamente e os adicione a um acumulador. O programa deverá parar quando receber uma entrada 0, imprimindo a soma nesse ponto. Use as chamadas do sistema do SPIM descritas na Seção B.9. B.7 [5] <§B.9> Usando o SPIM, escreva e teste um programa que leia três inteiros e imprima a soma dos dois maiores desses três. Use as chamadas do sistema do SPIM descritas nas páginas B-43 e B-45. Você pode usar o critério de desempate que desejar.
B-692
Apêndice B Montadores, Link-editores e o Simulador SPIM
B.8 [5] <§B.9> Usando o SPIM, escreva e teste um programa que leia um inteiro positivo usando as chamadas do sistema do SPIM. Se o inteiro não for positivo, o programa deverá terminar com a mensagem “Entrada inválida”; caso contrário, o programa deverá imprimir os nomes dos dígitos dos inteiros por extenso, delimitados por exatamente um espaço. Por exemplo, se o usuário informou “728”, a saída deverá ser “Sete Dois Oito”. B.9 [25] <§B.9> Escreva e teste um programa em assembly do MIPS para calcular e imprimir os 100 primeiros números primos. Um número n é primo se ele só puder ser dividido exatamente por ele mesmo e por 1. Duas rotinas deverão ser implementadas: j testa_primo (n)
retorna 1 se n for primo e 0 se n não for primo.
j main()
percorre os inteiros, testando se cada um deles é primo. Imprime os 100 primeiros números primos.
Teste seus programas executando-os no SPIM. B.10 [10] <§§B.6, B.9> Usando o SPIM, escreva e teste um programa recursivo para solucionar um problema matemático clássico, denominado Torres de Hanói. (Isso exigirá o uso de frames de pilha para admitir a recursão.) O problema consiste em três pinos (1, 2 e 3) e n discos (o número n pode variar; os valores típicos poderiam estar no intervalo de 1 a 8). O disco 1 é menor que o disco 2, que, por sua vez, é menor que o disco 3, e assim por diante, com o disco n sendo o maior. Inicialmente, todos os discos estão no pino 1, começando com o disco n na parte inferior, o disco n – 1 acima dele, e assim por diante, até o disco 1 no topo. O objetivo é mover todos os discos para o pino 2. Você só pode mover um disco de cada vez, ou seja, o disco superior de qualquer um dos três pinos para o topo de qualquer um dos outros dois pinos. Além do mais, existe uma restrição: não é possível colocar um disco maior em cima de um disco menor. O programa em C a seguir pode ser usado como uma base para a escrita do seu programa em assembly:
Índice remissivo
A abstrações definição, 13 interface de hardware/software, 13–15 princípio, 15 acerto sob perda, 436 acertos de cache, 409 acrônimos, 5 add (Add), 61 adição, 181–185 binária, 181–182 instruções, B-667 operandos, 182 ponto flutuante, 202–205, 209, B-684–B-685 significandos, 202 velocidade, 185 Ver também aritmética adição de ponto flutuante, 202–205 binária, 203, 204 diagrama de blocos de unidade aritmética, 205 etapas, 202–203 ilustração, 203 instruções, 209, B-684–B-685 testando a associatividade, 219 adição sem sinal, instrução, 182 Advanced Technology Attachment (ATA), discos, 465, 494, 495 AGP, A-574 algoritmo de divisão, 193 algoritmo de multiplicação, 189 algoritmos de classificação, 127 aliasing, 409 All-pairs N-body, algoritmo, A-620 alocação de espaço na heap, 94–97 na pilha, 94 Alpha, arquitetura definição, 424 ALU, bloco de controle, 257 ALU, controle, 254–256 bits, 255 Ver também Arithmetic Logic Unit (ALU) ALUOp, 254 bits, 255, 256 sinal de controle, 257 AMD Opteron X4 (Barcelona), 13, 34–39, 242 benchmark de CPU SPEC, 36–38 benchmark de potência SPEC, 38–39
cache L3 compartilhado, 437 caches, 436 características, 546 CPI, taxas de falta e acessos à DRAM, 437 definição, 546 desempenho básico versus totalmente otimizado, 551 desempenho LBMHD, 551 desempenho SpMV, 550 hardware TLB, 435 hierarquias de memória, 435–437 ilustração de pipeline, 327 ilustração, 547 microarquitetura, 325, 326 modelo roofline, 548 pipeline, 325–327 registradores arquiteturais, 325 técnicas de redução de penalidade de falta, 436–437 tradução de endereço, 435 AMD64, 136 American Standard Code for Information Interchange. Ver ASCII AND, operação, 81–82, B-668 Annual Failure Rate (AFR), 462, 494 antidependência, 320 Application Binary Interface (ABI), 15 Application Programming Interfaces (APIs) definição, A-570 gráficos, A-578 Arithmetic Logic Unit (ALU) antes do forwarding, 296 caminho de dados de desvio, 252 entrada imediata com sinal, 298 hardware, 182 operações em formato R, 250 para valores de registrador, 248 uso de instrução de referência de memória, 243 Ver também ALU controle; unidades de controle aritmética, 179–228 adição, 181–185 divisão, 191–197 multiplicação, 186–191 para multimídia, 183–184 ponto flutuante, 197–219 subtração, 181–185 aritmética de multimídia, 183–184 aritmética de ponto flutuante (GPUs), A-601 básica, A-601
desempenho, A-603 especializada, A-601–A-603 formatos admitidos, A-601 operações de textura, A-603 precisão dupla, A-604, A-627 ARM, instruções, 130–133 cálculos, 130–132 campo de condição, 308 campo imediato de 12 bits, 132 características, 132–133 compare e desvio condicional, 132 formatos, 132 loads e stores em bloco, 133 lógicas, 133 modos de endereçamento, 130–132 registrador-registrador, 131 semelhanças do MIPS, 131 transferência de dados, 131 armadilhas acessos de disco pelo sistema operacional, 496–497 associatividade, 439 avaliação de processador fora de ordem, 439 backups de fita magnética, 496 definição, 39 desempenho da taxa de transferência de pico, 497 desenvolvimento de software com multiprocessadores, 553 endereços de word sequenciais, 142 extensão do espaço de endereços, 439 GPUs, A-627–A-628 hierarquias de memória, 437–441 ignorando o comportamento do sistema de memória, 438 implementação de VMM, 439–441 movendo funções para processador de E/S, 496 pipelining, 328–329 ponteiro para variáveis automáticas, 142 provisão de recurso da rede, 495–496 simulando cache, 437–438 subconjunto de equação de desempenho, 40–41 Ver também falácias armazenamento disco, 464–467 flash, 468–469 não volátil, 464
693
694
armazenamento de disco, 464–467 características, 467 densidades, 465 interfaces, 465–467 latência de rotação, 465 não volátil, 464 setores, 464 tempo de busca, 464 tempo de transferência, 465 trilhas, 464 armazenamento não volátil, 464 arquitetura de GPU unificada, A-575–A-576 array de processador, A-575–A-576 ilustração, A-575 arquitetura de multiprocessador multithreaded, A-587 comparação de multiprocessador, A-596 conclusão, A-596 gerenciamento de threads/blocos de threads, A-591 instruções de thread, A-592 ISA, A-592–A-595 multiprocessador, A-588–A-589 multithreading massivo, A-587–A-588 processador streaming (SP), A-595 SIMT, 589tunidades de função especial (SFUs), A-596 arquitetura do conjunto de instruções ARM, 130–133 cálculo de endereço de desvio, 250 definição, 15, 41 história, 145 mantendo, 41 proteção, 425–426 suporte para máquina virtual, 424–425 thread, A-592–A-595 arquivos de registradores definição, 248 únicos, 253 arquivos executáveis, B-630 definição, 113 produção do link-editor, B-643 arquivos fonte, B-630 arquivos-objeto, 113, B-630 cabeçalho, 113, B-638 definição, B-636 formato, B-638–B-639 informação de depuração, 113 informação de relocação, 113 link-edição, 114–116 segmento de dados estático, 113 segmento de texto, 113 tabela de símbolos, 113 arrays dimensão múltipla, 215 ponteiros versus, 127–130 procedimentos para definir como zero, 128 arredondamento bits, 217 com dígitos de guarda, 216 definição, 215 modos IEEE, 754, 217 preciso, 215
Índice remissivo
ASCII definição, 97 número binários versus, 98 representação de caractere, 97 símbolos, 100 Assembly, linguagem definição, 7, 111 desvantagens, B-635–B-636 ilustração, 8 linguagens de alto nível versus, B-637 MIPS, 61, 78–79, B-663–B-690 ponto flutuante, 210 produção, B-634–B-635 programas, 111 quando utilizar, B-634–B-635 traduzindo para linguagem de máquina, 78–79 associatividade aumentando o grau, 387, 417 aumentando, 391–392 conjunto, tamanho de tag versus, 391–392 em caches, 387–389 teste de adição de ponto flutuante, 219 atalho de negação, 72–73 atalho de verificação de limites, 87 atraso no pior caso, 265 Average Memory Access Time (AMAT), 385 calculando, 385 definição, 385
B backpatching, B-638 backups, 496 balanceamento de carga, 514–515 bancos de registradores, 248, 253 barramento backplane, 469 barramento processador-memória, 469 barramento síncrono, 470 barramentos, 471, 472 backplane, 469 processador-memória, 469 síncronos, 470 benchmark de servidor de arquivos (SPECFS), 481 benchmark de servidor Web (SPECWeb), 481 benchmarks definição, 36 E/S, 480–482 Linpack, 537 multicores, 531–552 multiprocessador, 537–539 NAS paralelo, 539 paralelos, 538 PARSEC, 539 SPEC CPU, 36–38 SPEC power, 38–39 SPECrate, 537 SPLASH/SPLASH, 2, 537–539 Stream, 546 bibliotecas de programas, B-630 big-endian, ordem de byte, 66, B-661 bit de erro, 475 bit de modificação, 404
bit de pronto, 475 bit de referência, 402 bit de sinal, 71 bit de validade, 369 bit mais significativo definição, 69 bits ALUOp, 255, 256 arredondamento, 217 definição, 7 erro, 475 guarda, 215–216 modificados, 404 padrões, 217 prontos, 475 referência, 402 sinal, 71 sticky, 217 válidos, 369 bits menos significativos definição, 70 bits sticky, 217 bloco básico, 85–86 blocos dados válidos, 369 definição, 366 encontrando, 418–419 estratégias de posicionamento, 387 estratégias de substituição, 419–420 exploração de localidade espacial, 374 loads/stores, 133 locais de posicionamento, 417–418 localizando na cache, 389–391 multiword, mapeando endereços para, 373–374 posicionamento flexível, 385–389 seleção de substituição, 391 taxa de perda, 374 usados menos recentemente (LRU), 391 blocos de thread, 533 compartilhamento de memória, A-584 criação, A-585 definição, A-582 gerenciando, A-591 sincronização, A-584 bolhas, 301 branch delay, slots definição, 306 escalonamento, 308 branch-on-equal, instrução, 262 Bubble Sort, 126 buffer de frame, 12 buffer de renovação de rastreio, 12 buffers de armazenamento, 325 buffers de escrita cache write-back, 377 definição, 376 stalls, 383 buffers de reordenação, 321, 324, 325 busca-e-incremento atômico, 111 bytes endereçamento, 66 ordem, 66, B-661
Índice remissivo 695
C C, linguagem algoritmos de classificação, 127 atribuição, compilando no MIPS, 62–63 compilando loops while na, 84–85 compilando, 130 hierarquia de tradução, 112 tarefa de compilação com registradores, 64–65 tradução para linguagem assembly do MIPS, 62 variáveis, 93 caches, 368–382 acessando, 370–374 associativas em conjunto, 385 associatividade, 387–389 bits necessários, 370 bits, 373 campo de tag, 370 controlador de disco, 467 definição, 13, 368 divisão, 378 escritas, 375–377 esvaziando, 479 fisicamente endereçadas, 409 fisicamente indexadas, 408 fisicamente marcadas, 408 FSM para controlar, 426–434 GPU, A-598 ilustração do conteúdo, 371 inconsistentes, 375 índice, 370 Intrinsity FastMATH, exemplo, 377–378 locais, 369 localizando blocos, 389–391 mapeadas diretamente, 368, 370, 373, 385 memória virtual e integração de TLB, 406–409 multinível, 382, 392–395 não bloqueantes, 436 primárias, 393, 396 projeto de sistema de memória, 379–382 resumo, 382 secundárias, 393, 396 simulando, 437–438 tamanho, 372 totalmente associativas, 385 vazias, 370 virtualmente endereçadas, 409 virtualmente indexadas, 409 virtualmente marcadas, 409 write-back, 376, 377, 420 write-through, 376, 377, 420 Ver também blocos caches associativos em conjunto, 385–386 definição, 385 duas vias, 387 escolha, 419 estratégias de substituição de bloco, 420 falhas, 387–389 local de bloco da memória, 386 n-vias, 385 partes de endereço, 389
quádruplas, 387, 391 Ver também caches caches de mapeamento direto comparador único, 391 definição, 368, 385 escolha, 419 faltas, 387 ilustração, 370 local de bloco de memória, 386 número total de bits, 373 partes de endereço, 389 Ver também caches caches divididas, 378 caches endereçadas fisicamente, 409 caches multinível complicações, 393 definição, 382, 393 desempenho, 392–393 penalidade de falta, reduzindo, 392–395 resumo, 395–396 Ver também caches caches sem bloqueio, 325, 436 caches totalmente associativas definição, 385 escolha, 419 estratégias de substituição em bloco, 420 local de bloco da memória, 386 perdas, 389 Ver também caches caches virtualmente endereçados, 409 caches write-back buffers de escrita, 377 complexidade, 377 definição, 376, 420 stalls, 383 vantagens, 420 Ver também caches caixas de pizza, 489 cálculo de execução/endereço instrução load, 281 instrução store, 283 linha de controle, 288 callee, 89, 91 caller, 89 caminho de dados de desvio ALU, 252 operações, 251 caminhos de dados definição, 12 desvio, 251, 252 dois despachos estáticos, 318 em operação para instrução branch-on-equal, 262 em operação para instrução load, 261 em operação para instrução tipo R, 260 montagem, 247–254 operação, 258–262 para arquitetura MIPS, 254 para instrução de salto, 264 para instruções de busca, 249 para instruções de memória, 253 para instruções tipo R, 253, 260 para resolução de hazard via forwarding, 297
pipeline, 276–288 projeto, 247 tratamento de exceção, 311 único ciclo, 277 únicos, criando, 252–254 unidade de controle, 259 caminhos de dados de ciclo único execução de instrução, 278 ilustração, 277 Ver também caminhos de dados caminhos de dados em pipeline, 276–288 com sinais de controle conectados, 291 com sinais de controle, 288 corrigidos, 285 ilustração, 279 nos estágios da instrução load, 285 campo de condição, 308 campos definição, 75 MIPS, 76–77 nomes, 77 registrador Cause, B-654, B-655 registrador de Status, B-654, B-655 caracteres em Java, 100–101 representação ASCII, 97 carga, B-643 carregadores, 116 cartões de memória removíveis baseados em flash, 16 caso comum rápido, 143 Central Processor Unit (CPU) co-processor 0, B-654 definição, 12 desempenho, 22–23 equação de desempenho clássica, 26–28 medições de tempo, 23 tempo de execução, 22, 23, 24 tempo do sistema, 22 tempo do usuário, 22 tempo, 382 Ver também processadores centros de dados, 2 Cg, programa shader de pixel, A-579–A-580 chamadas de procedimento convenção, B-645–B-653 exemplos, B-647–B-653 frame, B-646 preservação por, 93 chamadas do sistema, B-661–B-663 código, B-662 definição, 410 carga, B-661 chips. Ver circuitos integrados (ICs) ciclos de clock atraso no pior caso, 265 definição, 23 número de registradores, 64 stall de memória, 382, 383 ciclos de clock de stall da memória, 382, 383 ciclos de clock por instrução (CPI), 25–26, 274 dois níveis de caching, 393 um nível de caching, 393
696
ciclos de stall de escrita, 383 ciclos de stall de leitura, 383 circuitos integrados (ICs) custo, 35 definição, 19 processo de manufatura, 34 Ver também chips específicos clusters definição, 510, 517 desvantagens, 518 isolamento, 520 organização, 509 overhead na divisão da memória, 518 co-processadores co-processador 0, B-654 definição, 215 instruções move, B-682–B-683 codificação instrução de ponto flutuante, 211 instrução MIPS, 78, 107, B-667 instrução x86, 141 código de correção, 485 código de função, 77 código de máquina, 75 coerência de cache, 430–433 coerência, 430 consistência, 431 esquemas de imposição, 432 migração, 432 problema, 430, 431, 433 protocolo snooping, 432–433 protocolos, 432 replicação, 432 comandos para dispositivos de E/S, 475–475 commit na ordem, 322 Compact Disks (CDs), 16, 17 compactação de estrutura de dados, 549 comparações, 85–86 com sinal versus sem sinal, 87 operandos constantes, 86 compare e swap atômicos, 111 compartilhamento falso, 433 compilação C, instruções de atribuição, 62–63 C, linguagem, 84–85, 130 if-then-else, 83 loops while, 84–85 procedimentos recursivos, 92–93 procedimentos, 89, 92–93 programas de ponto flutuante, 209–214 compiladores, 111 criação de desvio, 84 definição, 7 especulação, 316–317 função, 9, 111, B-632 Just In Time (JIT), 119 otimização, 130 produção de linguagem de máquina, B-634–B-635, B-636 complemento a um, 74 Compressed Sparse Row (CSR), matriz, A-612, A-613
Índice remissivo
computação GPU aplicações visuais, A-571–A-572 definição, A-570 Ver também Graphics Processing Units (GPUs) computação visual, A-569 computadores aplicações, 2 aritmética, 179–228 classes de aplicação, 2–4 componentes, 9, 180, 458 desktop, 2, 10 embutidos, 2–4, B-634 laptop, 12 medida de projeto, 42 montagem em rack, 488 organização de componente, 9 princípios, 79 representação da instrução, 74–80 revolução da informação, 2 servidores, 2 computadores desktop definição, 2 ilustração, 11 computadores embutidos definição, B-634 design, 5 requisitos da aplicação, 4 computadores laptop, 12 Compute Unified Device Architecture. Ver CUDA, ambiente de programação comunicação, 17–18 reduzindo o overhead, 33 thread, A-595 conceito de programa armazenado, 60 ilustração, 80 princípio do computador, 79 princípios, 143 confiabilidade, 462 conjunto de trabalho, 416 conjuntos de instruções ARM, 308 MIPS-32, 227 MIPS, 60, 144, 225 NVIDIA GeForce, 8800, A-607 projeto para pipelining, 269 Pseudo MIPS, 227 x86, crescimento, 143 consoles de jogo, A-574 contador de instruções, 26, 27 contadores de programa (PCs), 247 atualizações de instrução, 279 definição, 89, 247 exceção, 410, 412 incrementando, 247, 249 mudando com desvio condicional, 308 Content Addressable Memory (CAM), 391 controladores de cache, 433 controladores de canal, 478 controladores de disco caches, 467 definição, 465 tempo, 465
controle ALU, 254–256 desafio, 309 forwarding, 294 para instrução de salto, 264 pipeline, 288–292 terminando, 262 controle em pipeline, 288–292 especificando, 288 ilustração do esboço, 302 linhas de controle, 288, 289 Ver também controle Cooperative Thread Arrays (CTAs), A-591 copy back. Ver write-back cores definição, 31 número por chip, 32 CUDA, ambiente de programação, 533, A-571 abstrações de chave, A-582 definição, A-571 desenvolvimento, A-580, A-582 hierarquias de grupos de threads, A-582 implementação plus-reduction, A-619 kernels, A-582, A-586 memória compartilhada por bloco, A-615 memórias compartilhadas, A-582 paradigma, A-582–A-585 programação paralela escalável com, A-580–A-585 programas, A-571, A-586 SDK, 141 sincronização de barreira, A-582, A-595 template paralela plus-scan, A-617 threads, A-596
D dados estáticos dados dinâmicos, B-644 definição, B-644 segmento, 94 data race, 109 decisão adiada, 275 decodificando linguagem de máquina, 106 decomposição do problema paralelo de dados, A-580, A-582 dependência de nome, 320 dependências detecção, 293 entre registradores de pipeline e entradas da ALU, 294 entre registradores de pipeline, 295 inserção de bolha, 301 nome, 320 sequência, 292 dependências em pipeline, 293 desafio de speed-up, 512–515 balanceando a carga, 514–515 problema maior, 513–514 desdobramento de loop definição, 320 para pipelines de despacho múltiplo, 320 renomeação de registrador, 320
Índice remissivo 697
desempenho, 19–29 armazenando, A-611–A-612 avaliando, 19–20 componentes, 28 CPU, 22–23 definição, 20–22 equação clássica da CPU, 26–28 instrução, 25–26 medição de tempo, 22 medindo, 22–23 melhorando, 23–25 programa, 28 razão, 22 relativo, 21 tempo de resposta, 20, 21 throughput, 20 usando equação, 26 desempenho da cache, 382–396 calculando, 384 impacto sobre desempenho do processador, 383–384 tempo de acerto, 385 desempenho de classificação, A-611–A-612 desempenho do programa elementos afetando, 28 entendendo, 77 desempenho máximo de ponto flutuante, 540 desempenho relativo, 21 despacho de threads, 533 despacho múltiplo, 314–322 definição, 314 desdobramento de loop, 320 dinâmico, 316, 320–322 escalonamento de código, 319 estático, 316, 317–320 pacotes de despacho, 317 processadores, 314, 316 vazão, 323 destino do desvio buffers, 308 endereços, 250 desvio igual, 302 desvio não tomado definição, 251 suposição, 302 desvio tomado definição, 251 redução de custo, 302 desvios adiados, 87, 252, 275, 302–305, 306, 308 condição, 252 criação de compilador, 84 decisão, subindo, 302 endereçamento, 103–105 endereço de destino, 304 execução no estágio ID, 304 incondicionais, 83 pipeline, 304 terminando, 85 Ver também desvios condicionais desvios adiados, 87 definição, 252 limitações de escalonamento, 306
pipelines de cinco estágios, 308 reduzindo, 302–305 solução do hazard de controle, 275 Ver também desvios desvios condicionais alterando o contador de programa com, 308 ARM, 132 compilando if-then-else em, 83 definição, 83 em loops, 103 endereçamento relativo ao PC, 103 implementação, 88 desvios incondicionais, 83 desviosem pipeline, 304 detecção de erro, 485 diagramas de pipeline de múltiplo ciclo de clock, 286 cinco instruções, 286 definição, 286 ilustração, 286 diagramas de pipeline de único ciclo de clock, 286 definição, 286 ilustração, 288 dicing, 35 dies, 35 Digital Video Disks (DVDs), 16, 17 dígitos binários. Ver bits dígitos de guarda arredondamento com, 216 definição, 215 Direct Memory Access (DMA) configuração, 478 definição, 477 múltiplos dispositivos, 478 transferências, 478, 479 Direct3D, A-577 diretivas de leiaute de dados, B-639 diretivas do montador, B-632 discos magnéticos. Ver discos rígidos discos óticos definição, 16 tecnologia, 17 discos rígidos cabeça de leitura-escrita, 15 definição, 15 diâmetros, 16 ilustração, 15 tempos de acesso, 16 discos rígidos híbridos, 468 disponibilidade, 462 dispositivos de entrada, 10 dispositivos de saída, 10 dividendo, 192 divisão, 191–197 algoritmo, 193 com sinal, 193–195 dividendo, 192 divisor, 192 hardware, 192–193 hardware, versão melhorada, 194 instruções, B-668–B-669
mais rápida, 195 no MIPS, 195–197 operandos, 192 ponto flutuante, 209, B-686 quociente, 192 resto, 192 SRT, 195 Ver também aritmética divisor, 192 don't cares termo, 256 Double Data Rate RAMs (DDRRAMs), 381 DRAM síncrona (SRAM), 381 Dynamic Random Access Memory (DRAM), 365, 379 crescimento de capacidade, 20 custo, 16 definição, 12 Double Date Rate (DDR), 381 GPU, A-597–A-598 largura de banda externa, 382 síncrona (SDRAM), 381 tamanho, 382 velocidade, 16 Dynamically Linked Libraries (DLLs), 116–117 definição, 117 versão da link-edição de procedimento tardio, 117, 118
E E/S controlada por interrupção, 475 E/S mapeada na memória definição, 475 uso, B-658 E/S, B-658–B-659 chip sets, 473 comunicação do processador, 475–476 controlada por interrupção, 475 controladores, 478, 496 desempenho, 461 direções futuras, 498 impacto no desempenho do sistema, 483–484 instruções, 475 largura de banda, 498 mapeada na memória, 475, B-658 medidas de desempenho, 480–482 padrões, 471 paralelismo, 483–488 problema de coerência, 479 sistemas, 459 solicitações, 461, 498 taxa, 480, 492, 493 transações, 470 E/S, benchmarks, 480–481 processamento de transação, 480–481 sistema de arquivos, 481–482 Web, 481–482 Ver também benchmarks
698
E/S, dispositivos características, 460 comandos, 475 diversidade, 460 expansibilidade, 461 ilustração, 459 interfaces, 473–479 leituras/escritas, 461 múltiplos caminhos, 498 número máximo, 497 prioridades, 476–477 transferências, 472, 477–478 E/S, interconexões função, 470 processadores x86, 471–473 E/S, sistemas projeto, 482–483 avaliação de potência, 493 desempenho, 498 elo mais fraco, 482 exemplo de projeto, 491–493 história, 498 organização, 472 responsabilidades do sistema operacional, 474 taxa de transferência máxima, 497 Electrically Erasable Programmable Read-Only Memory (EEPROM), 468 elementos caminho de dados, 247, 252 combinacionais, 245 estado, 246, 247, 248 elementos combinacionais, 245 elementos de estado armazenando/acessando instruções, 248 clock, 247 definição, 246 entradas, 246 lógica combinacional, 247 elementos do caminho de dados compartilhamento, 252 definição, 247 endereçamento base, 105 deslocamento, 105 imediatos de 32 bit, 102–109 intermediário, 105 modos do MIPS, 105 modos x86, 136, 138 pseudodireto, 105 registrador, 105 relativo ao PC, 103, 105 saltos e desvios, 103–105 endereçamento de base, 66, 105 endereçamento de registradores, 105 endereçamento intermediário, 105 endereçamento por deslocamento, 105 endereçamento pseudodireto, 105 endereçamento relativo ao PC, 103, 105 endereço de retorno, 89
Índice remissivo
endereços base, 66 byte, 66 definição, 65 imediatos de 32 bits, 102–109 memória, 72 virtuais, 397–398, 414 endereços físicos, 397 definição, 396 espaço, 515, 517 mapeando, 397 endereços virtuais causando falhas de página, 414 definição, 397 mapeamento, 397 tamanho, 398 entradas, 256 escalonamento de pipeline dinâmico, 321–322 buffer de reordenação, 321 conceito, 322 definição, 321 especulação baseada em hardware, 322 estação de reserva, 321 unidade de commit, 321 unidades primárias, 321 escritas cache write-back, 376, 377 cache write-through, 376, 377 complicações, 376 custo, 416 esquemas, 376 memória virtual, 404 tratamento, 375–377 tratamento da hierarquia de memória, 420 espaço de endereços plano, 439 espaço de endereços, 396, 399 compartilhado, 516–517 estendendo, 439 físico único, 515 ID (ASID), 411 não mapeado, 414 plano, 439 virtual, 411 especulação baseada em hardware, 322 especulação, 316–317 baseada em hardware, 322 definição, 316 desempenho, 317 implementação, 316 mecanismo de recuperação, 317 problemas, 317 espelhamento, 485 estações de reserva definição, 321 operandos de buffering, 322 estado componentes lógicos, 246 especificação, 399 exceção, salvando/restaurando, 415 no esquema de previsão de 2 bits, 306 estágio de acesso à memória instrução load, 281
instrução store, 283 linha de controle, 291 estágio de busca de instrução instrução load, 279 instrução store, 283 linha de controle, 288 estágio de decodificação de instrução/leitura de banco de registradores instrução load, 279 instrução store, 283 linha de controle, 288 estágio do cálculo de execução ou endereço, 281, 283 Ethernet, 17, 18 EX, estágio detecção de exceção de overflow, 311 instruções load, 281 instruções store, 283 exceções, 309–314, B-655–B-656 associação, 313 caminho de dados com controles para tratamento, 311 definição, 183, 310 detectando, 310 estágio para salvar/restaurar, 415 estouro, 311 exemplo de computador em pipeline, 312 imprecisas, 313 instruções, B-689 interrupções versus, 309–310 motivos, 310–311 na arquitetura MIPS, 310–311 na implementação em pipeline, 311–314 PC, 410, 412 precisas, 313 resultado devido a estouro na instrução add, 387 tipos de evento, 310 exception enable, 412 Exception Program Counters (EPCs), 310 captura de endereço, 313 copiando, 183 definição, 183, 311 determinando o reinício, 310 transferindo, 185 exclusão mútua, 109 exclusive OR (XOR), instruções, B-672 execução fora de ordem complexidade do desempenho, 393 definição, 322 processadores, 325 exemplo de shader de pixel, A-579–A-580 expansão forte, 514, 515 fraco, 514 expansão forte, 514, 515 expansão fraca, 514 expoentes, 197–198 extensão de sinal, 250 atalho, 73–74 definição, 98 extensões de multimídia vetor versus, 528
Índice remissivo 699
F facilidades, B-639–B-641 falácias adição imediata sem sinal, 223 baixa utilização usa pouca potência, 40 definição, 39 desempenho de pico, 552–553 deslocamento à direita, 222–223 GPUs, A-626–A-627, A-628 importância da compatibilidade binária comercial, 142 instruções poderosas significam maior desempenho, 141 lei de Amdahl, 552 linguagem assembly para desempenho, 141–142 MTTF, 494 pipelining, 328 taxas de falha de disco, 494–495 falhas motivos para, 463 taxas de disco, 494–495 tempo médio entre (MTBF), 462 tempo médio para (MTTF), 462, 463, 494 falta sob falta, 436 faltas de cache cache associativo em conjunto, 387–389 cache de mapeamento direto, 387 cache totalmente associativa, 389 capacidade, 421 ciclos de clock de stall da memória, 382 compulsórias, 421 conflito, 421 definição, 375 etapas, 375 na cache write-through, 376 reduzindo com posicionamento de bloco flexível, 385–389 substituição de bloco, 419–420 tratamento, 375 faltas de página, 401 definição, 397 endereço virtual causando, 414 para acesso a dados, 413 tratamento, 398, 411–416 Ver também memória virtual Fast Fourier Transforms (FFT), A-609 Filebench, 481 fitas magnéticas, 496 definição, 16 histórico de uso, 496 fluxo de instruções da esquerda para a direita, 278 formato de instrução tipo J, 103 formato R, 257 definição, 77 operações da ALU, 250 formato-I, 77 formatos de instrução ARM, 132 definição, 75 instrução de salto, 263
MIPS, 132 tipo I, 77 tipo J, 103 tipo R, 77, 257 x86, 141 forwarding, 292–302 ALU antes, 296 caminho de dados para resolução de hazard, 297 com duas instruções, 269–270 controle, 294 definição, 269 funcionamento, 293 multiplexadores, 297 múltiplos resultados, 272 registradores de pipeline antes, 296 representação gráfica, 270 frações, 197, 198, 199 função do próximo estado, 427 definição, 427 funções de controle definindo, 258 para implementação de único ciclo, 262 Fused-Multiply-Add (FMA), operação, 217, A-604
G General Purpose GPUs (GPGPUs), 530, A-571 gigabytes, 16 GPU, arquiteturas do sistema, A-572–A-576 heterogêneas, A-572–A-574 implicações, A-586 interfaces e drivers, A-574 pipeline lógico gráfico, A-574 unificadas, A-575–A-576 grades, A-582 Graphics Processing Units (GPUs), 528–534 aplicações N-body, A-620–A-626 aritmética de ponto flutuante, A-580, A-601, A-627 arquitetura NVIDIA, 530–533 caches multinível, 530 como aceleradoras, 529 computação geral, A-627 definição, 34, 512, A-569 dobrando o desempenho, A-570 evolução, A-571 falácias e armadilhas, A-626–A-628 General Purpose (GPGPUs), 530, A-571 geração GeForce série, 8, A-571 gráficos de tempo real, A-577 história, A569 interfaces de programação, 529, A-580 interpolação de atributo, A-602–A-603 mapeando aplicações, 612t–683f memória principal, 530 memória, 530 modo gráfico, A-571 paralelismo, 530, A-629 perspectiva, 533–534 pipeline gráfico lógico, A-577–A-578 programando, A-576–A-586
resumo, A-629 sistema de memória paralela, A-596–A-600 software de driver, 530 tendências gráficas, A-570 Ver também computação GPU
H halfwords, 100 handlers definição, 413 perda de TLB, 414 handlers de interrupção, B-654 hardware como camada hierárquica, 6 linguagem, 7–9 operações, 60–63 procedimentos de suporte, 88–97 virtualizável, 424 hardware virtualizável, 424 hazard de dados de uso de load, 271, 302 hazards, 269–275 controle, 272–275, 302–309 dados, 269–272, 292–302 definição, 269 estruturais, 269, 283 forwarding, 298 Ver também pipelining hazards de controle, 272–275, 302–309 definição, 272, 302 previsão de desvio como solução, 275 previsão de desvio dinâmico, 306–308 processadores estáticos de despacho múltiplo, 317 redução branch delay, 302–305 resumo de pipeline, 308–309 simplicidade, 302 soluções, 273 stalls de pipeline como solução, 273 suposição de desvio não tomado, 302 técnica de decisão adiada, 275 hazards de dados, 269–272, 292–302 definição, 269 forwarding, 269, 292–302 uso de load, 271, 302 stalls, 298–301 Ver também hazards hazards de desvio. Ver hazards de controle hazards estruturais, 269, 283 heap alocando espaço, 94–97 definição, 94 hierarquias de memória armadilhas, 437–445 bloco (ou linha), 366 caches, 368–382 definição, 365 dependência de, 366 desafios de projeto, 423 desempenho de cache, 382–396 diagrama de estrutura, 367 estrutura comum, 417–423 estrutura, 366
700
hierarquias de memória (cont.) explorando, 361–546 inclusão, 437 memória virtual, 396–416 múltiplos níveis, 366 operação geral, 408 paralelismo, 430–433 parâmetros de projeto quantitativos, 417 pares de nível, 366 tempo de execução de programa, 395 variância, 395 hot-swapping, 487
I IBM Cell QS20 características, 546 definição, 549 desempenho básico versus totalmente otimizado, 551 desempenho SpMV, 550 ilustração, 547 LBMHD performance, 551 modelo roofline, 548 ID, estágio execução de desvio, 304 instrução store, 280 instruções load, 280 identificadores de processo, 411 identificadores de tarefa, 411 IEEE, 754, padrão de ponto flutuante, 199, 200 modos de arredondamento, 217 Ver também ponto flutuante If-then-else, 83 If, instruções, 103 implementação de único ciclo definição, 262 desempenho com pipeline versus, 266–267 execução sem pipeline versus execução com pipeline, 268 função de controle, 262 não uso da, 263–265 penalidade, 265 índice fora dos limites, verificação, 87 informação de depuração, B-638 informação de relocação, B-638, B-639 instruções, 58–178 acesso à memória, A-594–A-595 add imediato, 67 adição, 182, B-667 ARM, 130–133 assembly, 63 básicas, 228 bloco básico, 85–86 busca, 249 campos, 75 cientes da cache, 441 codificação, 78 como words, 60 comparação, B-672–B-674 conversão, B-685–B-686 definição, 7, 60 desempenho, 25–26
Índice remissivo
desvio condicional, 83 desvio, B-674–B-676 divisão, B-668–B-669 E/S, 475 exceção e interrupção, B-689 flushing, 302, 304, 313 fluxo da esquerda para a direita, 278 imediatas, 67 introdução, 60 load ligado, 110 load, 65, B-679–B-680 lógicas aritméticas, 248, B-667–B-672 manipulação de constante, B-672 move condicional, 308 movimentação de dados, B-682–B-683 multiplicação, 190, B-669 negação, B-670 nop, 300 operações lógicas, 80–83 OR exclusivo, B-672 ponto flutuante (x86), 221 ponto flutuante, 209–211, B-684–B-690 PTX, A-592, A-593 referência à memória, 243 reiniciáveis, 413 representação no computador, 74–80 resto, B-671 retomando, 416 salto, 87, 89, B-677 sequência de pipeline, 298 shift, B-671 sinais eletrônicos, 74 store condicional, 110–111 store, 67, B-680–B-682 subtração, 182, B-671–B-672 thread, A-592 tipo R, 248–249 tomada de decisão, 83–88 transferência de dados, 65 trap, B-677–B-679 vetor, 527 x86, 133–141 Ver também instruções aritméticas; MIPS; operandos instruções aritméticas lógicas, 248 MIPS, B-667–B-672 operandos, 63 Ver também instruções instruções cientes da cache, 441 instruções de acesso à memória, A-594– A-595 instruções de comparação, B-672–B-674 lista, B-672–B-674 ponto flutuante, B-685 instruções de conjunto, 86 instruções de conversão, B-685–B-686 instruções de deslocamento, 80, B-671 instruções de desvio, B-674–B-676 impacto do pipeline, 302 instrução de salto versus, 263 lista, B-674–B-676 instruções de flushing, 302, 304
definição, 302 exceções, 313 instruções de manipulação de constante, B-672 instruções de máquina, 75 instruções de movimentação de dados, B-682–B-683 instruções de movimento condicional, 308 instruções de negação, B-670, B-688–B-689 instruções de ponto flutuante, B-684–B-690 adição, B-684–B-685 comparação, B-685 conversão, B-685–B-686 divisão, B-686 load, B-686–B-687 move, B-687–B-688 multiplicação, B-688 negação, B-688–B-689 raiz quadrada, B-689 store, B-689 subtração, B-689–B-690 truncamento, B-689 valor absoluto, B-684 instruções de jump, 252 controle e caminho de dados, 264 formato de instrução, 263 implementando, 263 instrução de desvio versus, 263 lista, B-677 instruções de tipo R, 248–249 caminho de dados em operação para, 260 caminho de dados para, 260 instruções de tomada de decisão, 83–88 instruções de transferência de dados definição, 65 load, 66 offset, 66 store, 67 Ver também instruções instruções de trap, B-677–B-679 instruções imediatas, 67 instruções load acesso, A-601 bloco, 133 caminho de dados em pipeline, 285 caminho de dados na operação, 261 com sinal, 98 compilando, 67 definição, 66 detalhes, B-679–B-680 estágio EX, 281 estágio ID, 280 estágio IF, 280 estágio MEM, 282 estágio WB, 282 halfword sem sinal, 100 interligadas, 110, 111 lista, B-679–B-680 load byte sem sinal, 98 load half, 100 load upper immediate, 102, 103 ponto flutuante, B-686–B-687 registrador de base, 257 sem sinal, 98
Índice remissivo 701
unidade para implementação, 251 Ver também instruções store instruções por ciclo de clock (IPC), 314 instruções reiniciáveis, 413 Intel Nehalem caches, 436 foto do processador, 434 hierarquias de memória, 435–437 técnicas de redução de penalidade de falha, 435–437 TLB hardware for, 435 tradução de endereço, 435 Intel Threading Building Blocks, A-615 Intel Xeon e5345 básico versus totalmente otimizado características, 546 definição, 546 desempenho do LBMHD, 551 desempenho do SpMV, 550 desempenho, 551 ilustração, 546 modelo roofline, 548 intensidade aritmética, 540 intercalação, 380, 382 interconexão assíncrona, 470 interpolação de atributos, A-602–A-603 interrupções definição, 183, 310 exceções versus, 309–310 imprecisas, 313 instruções, B-689 precisas, 313 tipos de evento, 310 vetorizadas, 311 interrupções imprecisas, 313 interrupções precisas, 313 interrupções vetorizadas, 311 interrupt enable, 412 Interrupt Priority Levels (IPLs), 476–477 definição, 477 mais altos, 477 Intrinsity FastMATH, processador, 377–378 caches, 378 definição, 377 processamento de leitura, 408 processamento write-through, 408 taxas de perda de dados, 378, 389 TLB, 406
J Java algoritmos de classificação, 127 bytecode, 118 caracteres, 100–101 hierarquia de tradução, 119 interpretando, 119, 130 objetivos, 117 programas, iniciando, 117–119 strings, 100–101 Java Virtual Machine (JVM), 118 Just In Time (JIT), compiladores, 119, 554
K kernels CUDA, A-582, A-586 definição, A-582
L LAPACK, 219 largura de banda bisseção, 535 cache L2, 546 E/S, 498 externa à DRAM, 382 memória, 379, 380 rede, 535 largura de banda de bisseção, 535 latência instrução, 329 memória, A-628 pipeline, 276 restrições, 482 rotacional, 465 uso, 318, 319 latência de instrução, 329 latência de uso definição, 318 uma instrução, 319 latência rotacional, 465 Lattice Boltzmann Magneto-Hydrodynamics (LBMHD), 549–551 definição, 549 otimizações, 550–551 performance, 551 lei de Amdahl, 384, 512 corolário, 40 definição, 39 falácia, 552 Lei de Gresham, 228 Lei de Moore, 528, 626t–683f linguagem de máquina offset de desvio, 104–105 decodificação, 106 definição, 7, 75, B-630 ponto flutuante, 210 ilustração, 8 MIPS, 79 SRAM, 13 traduzindo linguagem assembly MIPS para, 78–79 linguagem fonte, B-632 linguagens de alto nível, 7–9, B-632 benefícios, 9 definição, 8 importância, 8 linguagens de programação orientadas a objeto, 130 variáveis, 64 Ver também linguagens específicas linguagens de shading, A-578 linguagens orientadas a objeto definição, 130 Ver também Java
linhas. Ver blocos linhas de controle acesso à memória, 291 ativadas, 260 busca de instrução, 288 configuração, 258, 260 decodificação de instrução/leitura de banco de registradores, 288 execução/cálculo de endereço, 288 no caminho de dados, 257 três estágios finais, 288 valores, 289 write-back, 291 link-editores, 113–116, B-642–B-643 arquivos executáveis, 113, B-643 definição, 113, B-630 etapas, 113 ilustração da função, B-643 usando, 114–116 linkagem de arquivos objeto, 114–116 Linpack, 537 Liquid Crystal Displays (LCDs), 11 little-endian, ordem de bytes, B-661 load word, 65, 67 localidade espacial, 364–365, 367 princípio, 364, 365 temporal, 364, 365, 367 localidade espacial, 364–365 definição, 364 exploração de bloco grande, 374 tendência, 367 localidade temporal, 365 definição, 364 tendência, 367 locks, 516 lógica combinacional, 247 componentes, 246 lógica combinacional, 247 loops, 84–85 definição, 84 desvios condicionais, 103 for, 127 previsão, 306 teste, 128 while, compilando, 84–85 loops for, 127
M macros definição, B-630 exemplo, B-640–B-641 uso, B-640 mapa de MIP, A-603 mapas de bits, 12 armazenamento, 12 definição, 11, 68 objetivo, 12 mapeando aplicações, A-612–A-626 máquinas de estados finitos (FSMs), 426–430 controladores, 429
702
máquinas de estados finitos (FSMs) (cont.) definição, 427 estilo, 429 função do próximo estado, 427 implementação, 427 Mealy, 429 Moore, 429 para controlador de cache simples, 429 máquinas virtuais (VMs), 423–426 benefícios, 424 definição, B-660 ilusão, 426 melhoria do desempenho, 425 para melhoria da proteção, 424 simulação, B-660 suporte à arquitetura do conjunto de instruções, 424–425 matrizes esparsas, A-612–A-614 Mealy, máquina, 429 Mean Time Between Failures (MTBF), 462 Mean Time To Failure (MTTF), 462, 463 avaliações, 484 falácias, 494 Mean Time To Repair (MTTR), 462, 463 meia precisão, A-601 memória afinidade, 549, 550 atômica, A-585 cache, 13, 368–396 CAM, 391 compartilhada, A-585, A-599–A-600 constante, A-600 definição, 12 DRAM, 12, 365, 379, 381 eficiência, 518 endereços, 72 espaços, A-599 flash, 15, 16, 468–469 global, A-585, A-599 GPU, 530 instruções, caminho de dados para, 253 largura de banda, 379, 380 layout, B-644 local, A-585, A-600 não volátil, 15 operandos, 65–66 principal, 15 SDRAM, 381 secundária, 15 sistema paralelo, A-596–A-600 stalls, 385 tecnologias para criação, 18–19 textura, A-600 uso, B-644–B-645 virtual, 396–416 volátil, 15 memória compartilhada bancos de SRAM, A-600 caching, A-615 CUDA, A-615 definição, A-585 memória de baixa latência, A-585 n-body, A-622
Índice remissivo
por CTA, A-599 Ver também memória memória constante, A-600 memória de textura, A-600 memória física. Ver memória principal memória flash, 468–469 características, 16, 468 definição, 15, 468 EEPROM, 468 nivelamento de desgaste, 468 NOR, 468 memória global, A-585, A-599 memória local, A-585, A-600 memória não volátil, 15 memória primária. Ver memória principal memória principal, 397 definição, 15 endereços físicos, 396, 397 tabelas de página, 404 Ver também memória memória secundária, 15 memória virtual, 396–416 definição, 396 escritas, 404 faltas de página, 397, 401 implementação da proteção, 409–411 integração, 406–409 mecanismo, 416 motivações, 396–397 resumo, 416 segmentação, 398 tradução de endereço, 397, 404–406 virtualização, 426 Ver também páginas memória volátil, 15 metodologia de clocking disparada por transição, 246, 247 metodologia de clocking, 246–247 definição, 246 disparada por transição, 246, 247 por previsibilidade, 246 métodos estáticos, B-644 microarquiteturas AMD Opteron X4 (Barcelona), 326 definição, 325 migração, 432 milhões de instruções por segundo (MIPS), 41 MIPS-32, conjunto de instruções, 227 MIPS, 61, 78–79, B-663–B-690 alocação de memória para programa e dados, 94 campos, 76–77 classes de instruções, 145 codificação de instrução, 78, 107, B-667 compilando atribuição C complexa para, 62–63 compilando instruções de atribuição C para, 62 conjunto de instruções, 60, 144, 225 convenções de registrador, 96 CPU, B-663 despacho múltiplo estático, 317–320
divisão, 195–197 endereçamento para imediatos de 32 bits, 102–109 endereços de memória, 66 exceções, 310–311 formatos de instrução, 109, 132, B-667 FPU, B-663 instruções aritméticas, 60, B-667–B-672 instruções de comparação, B-672–B-674 instruções de desvio, B-674–B-676 instruções de manipulação de constante, B-672 instruções de ponto flutuante, 209–211 instruções de salto, B-677–B-679 instruções lógicas, B-667–B-672 linguagem de máquina, 79 mapa de opcode, B-666 mapeamento de instrução assembly, 75 modos de endereçamento, B-663–B-664 multiplicação, 190 núcleo aritmético, 226 operandos, 61 pseudo, 226, 227 registradores de controle, 412 semelhanças do ARM, 131 sintaxe do assembler, B-664–B-667 suporte a diretiva do assembler, B-664–B-667 MIPS, conjunto de instruções básico, 228 ilustração da implementação, 245 implementação, 242–244 subconjunto, 242–243 visão abstrata, 243 visão geral, 243–244 Ver também MIPS MIPS, core arquitetura, 196 conjunto de instruções, 228, 242–244 mix de instruções, 28 modelo de consistência de memória, 433 modelo dos três Cs, 421 modo kernel, 410 modos de endereçamento, B-663–B-664 módulos, B-630 montadores, 112–113, B-636–B-641 aceitação de número, 113 arquivo objeto, 113 definição, 7, B-630 função, 113, B-636 informação de relocação, B-638, B-639 macros, B-630, B-640–B-641 montagem de código condicional, B-641 pseudoinstruções, B-641 tabela de símbolos, B-637 velocidade, B-638 Moore, máquinas, 429 mouse, anatomia, 11 move, instruções, B-682–B-683 co-processadores, B-682–B-683 detalhes, B-682–B-683 ponto flutuante, B-687–B-688
Índice remissivo 703
Multiple Instruction Multiple Data (MIMD), 533 definição, 524 Multiple Instruction Single Data (MISD), 525 multiplexadores controle de seletor, 253 controles, 427 definição, 243 forwarding, valores de controle, 297 no caminho de dados, 257 multiplicação com sinal, 189 multiplicação de ponto flutuante, 205–209 binária, 206–208 etapas, 205–206 ilustração, 207 instruções, 209 significandos, 205 multiplicação-adição (MAD), A-601 multiplicação, 186–191 assinada, 189 hardware, 187–189 instruções, 190, B-669 mais rápida, 190 multiplicador, 187 multiplicando, 187 no MIPS, 190 operandos, 187 ponto flutuante, 205–207, B-688 primeiro algoritmo, 188 produto, 187 rápida, hardware, 191 Ver também aritmética multiplicador, 187 multiplicando, 187 multiprocessador Tesla, 532 multiprocessadores arquitetura multithreaded, A-588–A-589, A-596 benchmarks, 537–539 definição, 510 desempenho, 553–554 memória compartilhada, 511, 515–517 organização, 509, 517 passagem de mensagens, 517–521 perspectiva histórica, 555 software, 510 UMA, 516 multiprocessadores mudança de projeto, 511 multicore, 5, 31, 510 multiprocessadores de memória compartilhada (SMP), 515–517 definição, 511, 515 espaço único de endereços físicos, 515 sincronização, 516 multiprocessadores multicore, 31 benchmarking com modelo roofline, 546–552 características, 546 definição, 5, 510 dois soquetes, 547 organização do sistema, 547 multithreading coarse-grained, 521–522
multithreading do hardware, 521–524 coarse-grained, 521–522 definição, 521 fine-grained, 521, 523 opções, 522 simultâneo, 522–524 multithreading fine-grained, 521, 523 multithreading simultâneo (SMT), 522–524 definição, 522 paralelismo em nível de thread, 523 slots de despacho não usados, 524 suporte, 523 multithreading, A-587–A-588 coarse-grained, 521–522 definição, 512 fine-grained, 521, 523 hardware, 521–524 simultâneo (SMT), 522–524
N n-body algoritmo all-pairs, A-620 comparação de desempenho, A-623–A-624 GPU, simulação, A-625 matemática, A-620 otimização, A-622 resultados, A-624–A-626 uso da memória compartilhada, A-622 NAS (NASA Advanced Supercomputing), 539 Newton, iteração, 215 níveis de prioridade, 476–477 nivelamento de desgaste, 468 Nonuniform Memory Access (NUMA), 516 nops, 300 NOR, memória flash, 468 NOR, operação, 82–83, B-670 north bridge, 471 NOT, operação, 82, B-671 notação científica definição, 197 para reais, 197 somando números, 202 notação imparcial, 74, 200 números binários, 68 com sinal, 68–74 computador versus mundo real, 217 decimais, 68, 71 desnormalizados, 218 hexadecimais, 75–76 sem sinal, 68–74 números binários ASCII versus, 98 conversão para números decimais, 71 conversão para números hexadecimais, 76 definição, 68 números com sinal, 68–74 sinal e magnitude, 70 tratando como sem sinal, 87 números decimais conversão de números binários para, 71 definição, 68
números desnormalizados, 219 números hexadecimais, 75–76 conversão de números binários para, 76 definição, 75 números não sinalizados, 68–74 NVIDIA GeForce, 8800, A-605–A-612 algoritmo all-pairs N-body, A-625 cálculos de álgebra linear densa, A-609 conjunto de instruções, A-607 desempenho FFT, A-609 desempenho na classificação, A-611–A-612 desempenho, A-609 escalabilidade, A-609 estatísticas de aproximação de função especial, A-602 processador streaming, A-608 rasterização, A-608 ROP, A-608 Special Function Unit (SFU), A-608 Streaming Multiprocessor (SM), A-607 Streaming Processor Array (SPA), A-605 Texture/Processor Cluster (TPC), A-606–A-607 NVIDIA, arquitetura de GPU, 530–533
O opcodes definição, 77, 257 definição de linha de controle, 260 OpenGL, A-577 OpenMP (Open MultiProcessing), 539 operação de memória atômica, A-585 operações atômicas, implementando, 110 hardware, 60–63 inteiros do x86, 136–140 lógicas, 80–83 operações lógicas, 80–83 AND, 81–82, B-668 ARM, 133 definição, 80–83 MIPS, B-667–B-672 NOR, 82–83, B-670 NOT, 82, B-671 OR, 82, B-671 shifts, 80 operandos constantes, 67–68 em comparações, 86 ocorrência frequente, 68 operandos, 63–68 compilação quando na memória, 65 constante, 67–68 deslocamento, 132 divisão, 192 imediatos de 32 bits, 102–103 instruções aritméticas, 63 memória, 65–66 MIPS, 61 multiplicação, 187 ponto flutuante, 210 somando, 182 Ver também instruções
704
OR, operação, 82, B-671 ordem principal de linha, 214 otimização compilador, 130 manual, 130 overflow definição, 70, 198 detecção, 182 exceções, 311 ocorrência, 71 ponto flutuante, 198 saturação, 183–184 subtração, 182
P P + Q, redundância, 486 Packed, formato de ponto flutuante, 222 pacotes de despacho, 317 páginas definição, 397 localizando, 399 LRU, 402 modificadas, 404 número físico, 397 número virtual, 397 offset, 397 posicionando, 399 tamanho, 398 Ver também memória virtual páginas modificadas, 404 paralelismo, 31, 314–325 benefícios de desempenho, 33 E/S, 483–488 emissão múltipla, 314–322 GPUs e, 530, A-629 hierarquias de memória, 430–433 multicore, 524 multithreading, 524 nível de dados, 525 nível de instrução, 31, 314, 324 nível de processo, 510 nível de tarefa, 510 tarefa, A-586 thread, A-585 paralelismo de tarefas, A-586 paralelismo de threads, A-585 paralelismo em nível de dados, 525 paralelismo em nível de instrução (ILP) definição, 31, 314 exploração, aumentando, 324 Ver também paralelismo paralelismo em nível de processo, 510 paralelismo em nível de tarefa, 510 parâmetros formais, B-641 paravirtualização, 441 paridade, 485 disco, 486 intercalada por bit, 485 intercalada por bloco, 485–486 intercalada por bloco distribuído, 486–487 PARSEC (Princeton Application Repository for Shared Memory Computers), 539
Índice remissivo
passagem de mensagens definição, 517 multiprocessadores, 517–521 PCI-Express (PCIe), A-573 penalidade de falta caches multinível, reduzindo, 392–395 definição, 366 determinação, 374 técnicas de redução, 435–437 Pentium, jogada de moralidade do bug, 223–225 perdas compulsórias, 421 perdas de capacidade, 421 perdas de conflito, 421 petabytes, 2 pilhas alocando espaço, 94 definição, 89 para argumentos, 126 pop, 89 procedimentos recursivos, B-650–B-651 push, 89, 91 pipeline de projeto digital, 327–328 pipeline lógico gráfico, A-575 pipelines AMD Opteron X4 (Barcelona), 325–327 cinco estágios, 267, 279–281, 288 despacho duplo estático, 317 diagramas de múltiplos ciclos de clock, 286 diagramas de único ciclo de clock, 286 estágio de acesso à memória, 281, 283 estágio de busca de instrução, 279, 283 estágio de decodificação de instrução e leitura de arquivo de registrador, 279, 283 estágio de execução e cálculo de endereço, 281, 283 estágio de write-back, 281, 283 estágios, 267 gargalos de desempenho, 324 impacto da instrução de desvio, 302 latência, 276 representação gráfica, 270, 286–288 sequência de instruções, 298 pipelining, 265–276 analogia da lavanderia, 265 armadilha, 328–329 avançado, 324–325 benefícios, 265 definição, 265 exceções, 311–314 falácias, 328 fórmula de speed-up, 267 hazards de controle, 272–275 hazards de dados, 269–272 hazards estruturais, 269, 283 hazards, 269–275 instruções de execução simultânea, 276 melhoria de desempenho, 269 paradoxo, 265 projeto do conjunto de instruções, 269 resumo, 275 tempo de execução, 276
vazão, 276 visão geral, 265–276 placas-mãe, 12 polling, 475 ponteiros arrays versus, 127–130 aumentando, 128 frame, 94 globais, 93 pilha, 89, 91 ponteiros de frame, 94 ponteiros globais, 93 ponto flutuante, 197 arquitetura SSE2, 222 arredondamento, 215–216 cálculos imediatos, 215 codificação de instruções, 211 conversão binário para decimal, 201 definição, 197 desafios, 226 desvio, 209 dígitos de guarda, 215–216 divisão, 209 forma, 198 formato empacotado, 222 frequência de instrução MIPS, 228 instruções MIPS, 209–211 linguagem assembly, 210 linguagem de máquina, 210 multiplicação e adição reunidas, 217 no x86, 220–222 operandos, 210 overflow, 198 padrão IEEE, 754, 199, 200 precisão, 219 procedimento com matrizes bidimensionais, 212–214 programas, compilando, 209–214 registradores, 214 representação, 197–202 sinal e magnitude, 198 subtração, 209 underflow, 198 unidades, 216 variação de operandos no x86, 222 pop, 89 potência eficiência, 324–325 natureza crítica, 42 relativa, 30 taxa de clock e, 29 potência relativa, 30 precisão dupla definição, 198 FMA, A-604 GPU, A-604, A-627 representação, 201 Ver também precisão simples precisão simples definição, 198 representação binária, 200 Ver também precisão dupla prefetching, 441, 549
Índice remissivo 705
previsão desvio dinâmico, 306–308 esquema de 2 bits, 306 estado fixo, 306 loops, 306 precisão, 306, 307 previsão de desvio buffers, 306, 307 como solução do hazard de controle, 275 definição, 274 dinâmica, 274, 275, 306–308 estática, 317 previsão de desvio dinâmica, 306–308 buffer de previsão de desvio, 306 definição, 306 loops, 306 Ver também hazards de controle previsão de desvio estática, 317 previsão de estado fixo, 306 previsor de correlação, 308 previsores de desvio correlação, 308 informação, 308 precisão, 306 torneio, 308 previsores de desvio de torneio, 308 previsores de hardware dinâmicos, 274 primeira word crítica, 375 primeira word requisitada, 375 procedimentos, 88–97 aninhados, 91–93 compilando, 89 compilando, mostrando ligação de procedimento aninhado, 92–93 cópia de string, 98–100 definição, 88 etapas de execução, 88 folha, 91 frames, 94 para definir arrays em zero, 128 recursivos, 96, B-647–B-649 sort, 120–125 strcpy, 99, 100 swap, 119–120 procedimentos aninhados, 91–93 compilando procedimento recursivo mostrando, 92–93 definição, 91 procedimentos de folha definição, 91 exemplo, 100 Ver também procedimentos procedimentos recursivos, 96, B-647–B-649 definição, B-647 invocação de clone, 91 pilha, B-650–B-651 Ver também procedimentos processadores de despacho múltiplo estático, 316, 317–320 com MIPS ISA, 317–320 conjuntos de instruções, 317 hazards de controle, 317 Ver também despacho múltiplo
processadores de vetor, 525–528 comparação de código convencional, 526 escalares versus, 527 extensões de multimídia, 528 instruções, 527 Ver também processadores processadores dinâmicos de despacho múltiplo, 316, 320–322 escalonamento de pipeline, 321–322 superescalar, 320 Ver também despacho múltiplo processadores streaming, 531, A-595 array (SPA), A-601, A-605 GeForce, 8800, A-608 processadores, 239–329 caminho de dados, 12 como cores, 31 comunicação de E/S com, 475–476 controle, 12 crescimento do desempenho, 32 definição, 9, 12 despacho duplo, 318 despacho múltiplo dinâmico, 316 despacho múltiplo estático, 316, 317–320 despacho múltiplo, 314, 316 especulação, 316–317 execução fora de ordem, 325, 393 ROP, A-576, A-601 streaming, 531, A-595 superescalar, 320, 321–322, 522 tecnologias para criação, 18–19 vetor, 525–528 VLIW, 317 produto, 187 programas iniciando, 111–119 Java, iniciando, 117–119 linguagem assembly, 111 processamento paralelo, 512–515 traduzindo, 111–119 programas de processamento paralelo, 512–515 definição, 510 dificuldade na criação, 512–515 para espaço de endereços compartilhado, 516–517 para passagem de mensagens, 518–519 uso, 553 programas de shader gráficos, A-578 projeto caminho de dados, 247 comprometimentos, 143 conjuntos de instruções de pipeline, 269 digital, 327–328 hierarquia de memória, desafios, 423 lógico, 244–247 sistema de E/S, 482–483 unidade de controle principal, 256–262 proteção definição, 396 grupo, 485 implementando, 409–411 VMs para, 424
protocolo de handshaking, 471 protocolo snooping, 432–433 protocolos de invalidação de escrita, 432, 433 pseudo MIPS conjunto de instruções, 227 definição, 226 pseudoinstruções definição, 112 resumo, 112 Pthreads (POSIX threads), 539 PTX, instruções, A-592, A-593 push definição, 89 usando, 91
Q quad words, 136 quantidade de deslocamento, 77 Quicksort, 393, 394 quociente, 192
R Radix sort, 393, 394, A-618–A-620 CUDA, código, A-620 implementação, A-618–A-620 RAID. Ver Redundant Arrays of Inexpensive Disks raiz quadrada, instruções, B-689 Raster Operation (ROP), processadores, A-576, A-601 função fixa, A-601 GeForce, 8800, A-608 rasterização, A-608 redes crossbar, 536 redes locais (LANs) definição, 18 Ver também redes redes multiestágio, 536 redes totalmente conectadas, 535, 536 redes, 17–18, 493–494 crossbar, 536 largura de banda, 535 locais (LANs), 18 multiestágios, 536 remotas (WANs), 18 totalmente conectadas, 535, 536 vantagens, 17 redução paralela, A-618 redução, 517 Redundant Arrays of Inexpensive Disks (RAID), 484–488 cálculo, 487 controlador PCI, 493 definição, 484 ilustração de exemplo, 485 popularidade, 484 RAID 0, 485 RAID 1 + 0, 488 RAID 1, 485 RAID 2, 485 RAID 3, 485
706
Redundant Arrays of Inexpensive Disks (RAID) (cont.) RAID 4, 485–486 RAID 5, 486 RAID 6, 486 resumo, 487 referências absolutas, 113 forward, B-636 não resolvidas, B-630, B-642 referências absolutas, 113 referências de forwarding, B-636 referências não resolvidas definição, B-630 link-editores e, B-642 registrador Cause, 476 campos, B-654, B-655 definição, 311 ilustração, 477 registrador contador, B-654 registrador de controle do receptor, B-659 registrador de dados do receptor, B-658, B-659 registrador de status, 476 campos, B-654, B-655 ilustração, 477 registrador salvo pelo callee, B-645 registrador salvo pelo caller, B-645 registradores arquiteturais, 325 base, 66 Cause, 311, 476, 477, B-655 compilando a atribuição C com, 64–65 contador, B-654 convenção de uso, B-645, B-646 convenções MIPS, 96 definição, 63 destino, 78, 257 especificação numérica, 249 mapeando, 74 metade direita, 279 metade esquerda, 279 pipeline, 294, 295, 296, 298 ponto flutuante, 214 primitivos, 63–64 Receiver Control, B-659 Receiver Data, B-658, B-659 renomeando, 320 salvo pelo callee, B-646 salvo pelo caller, B-646 spilling, 67 Status, 311, 476, 477, B-655 tabela de página, 400 tempo de ciclo de clock, 64 temporários, 64, 90 Transmitter Control, B-659 Transmitter Data, B-659 variáveis, 64 x86, 136 registradores arquiteturais, 325 registradores de base, 66 registradores de pipeline antes do forwarding, 296
Índice remissivo
dependências, 294, 295 seleção de unidade de forwarding, 298 registradores temporários, 64, 90 reinício antecipado, 375 replicação, 432 representação em complemento a dois, 70, 71 atalho de extensão de sinal, 73–74 atalho de negação, 72–73 definição, 70 regra, 74 vantagem, 71 reservas em standby, 487 restaurações, 462 resto definição, 192 instruções, B-671 restrição de alinhamento, 66 retorno de exceção (ERET), 410 RISC. Ver RISCs de desktop e servidor; RISCs embutidas; Reduced Instruction Set Computer (RISC), arquiteturas roofline, modelo, 539–546 benchmarking de multicores com, 546–552 com áreas sobrepostas sombreadas, 545 com ceilings, 544, 545 com dois kernels, 545 desempenho de pico da memória, 541 desempenho de ponto flutuante máximo, 540 IBM Cell QS20, 548 ilustração, 541 Intel Xeon e5345, 548 kernel intensivo de E/S, 546 Opteron, gerações, 542 roofline computacional, 543 Sun UltraSPARC T2, 548 rotina receber mensagem, 517 rótulos externos, B-636 globais, B-636 locais, B-636
S saturação, 183–184 SCALAPAK, 219 segmentação, 398 segmento de dados, B-638 segmento de pilha, B-645 segmento de texto, B-638 seletores de dados, 244 sem alocação de escrita, 376 semicondutores, 34 send message, rotina, 517 serialização da escrita, 431–432 servidores custo e capacidade, 2 definição, 2 Ver também RISCs de desktop e servidor setores, 464 shaders
aritmética de ponto flutuante, A-578 definição, A-578 exemplo de pixel, A-579–A-580 gráficos, A-578 adição, 202 multiplicação, 205 significandos, 199 silício como tecnologia básica de hardware, 41 definição, 34 lingote de cristal, 34 wafers, 34 SIMD (Single Instruction Multiple Data), 525, 533 arquitetura de vetor, 525–528 no x86, 525 vetor de dados, A-596 simplicidade, 143 sinais ativados, 246 controle, 247, 257, 258, 259 desativados, 246 sinais de controle ALUOp, 257 bits múltiplos, 259 caminhos de dados em pipeline, 288 definição, 247 efeito, 258 sinais desativados, 246 sinal e magnitude, 198 sincronização, 109–111 barreira, A-582, A-584, A-595 definição, 516 lock, 109 overhead, reduzindo, 33 unlock, 109 sincronização de barreira, A-582 definição, A-584 para comunicação de threads, A-595 sincronização de desbloqueio, 109 sincronização de lock, 109 Single Instruction Single Data (SISD), 524 Single-Instruction Multiple-Thread (SIMT), 589t–651f arquitetura de processador, A-590 definição, A-589 escalonamento de warp multithreaded, A-590 execução e divergência de warp, A-591 overhead, A-596 Single-Program Multiple Data (SPMD), 524, A-585 sistema de memória paralelo, A-596–A-600 acesso de load/store, A-601 caches, A-598 considerações de DRAM, A-597–A-598 espaços de memória, A-599 memória compartilhada, A-599–A-600 memória constante, A-600 memória de textura, A-600 memória global, A-599 memória local, A-600 MMU, A-598–A-599
Índice remissivo 707
ROP, A-601 superfícies, A-601 Ver também Graphics Processing Units (GPUs) sistemas heterogêneos, A-570 arquitetura, A-572–A-574 definição, A-569 sistemas operacionais armadilha de escalonamento do acesso ao disco, 496–497 definição, 6 encapsulamento, 15 Small Computer Systems Interface (SCSI), discos, 465, 494 software camadas, 6 como serviço, 488, 553 driver GPU, 530 multiprocessador, 510 paralelo, 511 sistemas, 6 software de sistemas, 6 software paralelo, 511 somadores carry save, 190 Sort, procedimento, 120–125 alocação de registrador, 121 chamada de procedimento, 123 código para o corpo, 121–123 definição, 120 passando parâmetros, 124 preservando registradores, 124 procedimento completo, 124–125 Ver também procedimentos south bridge, 471 Sparse Matrix-Vector Multiply (SpMV), 549, 550, A-612, A-614, A-615 código serial, A-614 CUDA, versão, A-614 versão de memória compartilhada, A-616 SPEC benchmark de CPU, 36–38 benchmark de potência, 38–39 SPEC2006, 228 SPECPower, 481 SPECrate, 537 SPECratio, 36 Special Function Units (SFUs), A-596 definição, A-602 GeForce, 8800, A-608 spilling de registradores, 67, 90 SPIM, B-659–B-663 chamadas do sistema, B-661–B-663 definição, B-659 iniciação, B-661 ordem de byte, B-661 recursos, B-661 simulação de máquina virtual, B-660 suporte a diretivas do montador MIPS, B-664–B-667 velocidade, B-660 versões, B-661 SPLASH/SPLASH 2 (Stanford Parallel Applications for Shared Memory), 537–539
stack pointers ajuste, 91 definição, 89 valores, 91 stalls de uso de load, 302 stalls do pipeline, 271–272 definição, 271 evitando com reordenação de código, 271–272 hazards de dados, 298–301 inserção, 301 solução para hazards de controle, 273 uso de load, 302 stalls, 271–272 buffer de escrita, 383 como solução para hazard de controle, 273 definição, 271 esquema write-back, 383 evitando com reordenação de código, 271–272 hazards de dados, 298–301 inserção em pipeline, 301 memória, 385 uso de load, 302 Static Random Access Memories (SRAMs) definição, 13 store word, 67 store, instruções acesso, A-601 bloco, 133 compilando com, 67 condicional, 110–111 definição, 67 dependência de instrução, 298 detalhes, B-680–B-682 estágio EX, 283 estágio ID, 280 estágio IF, 280 estágio MEM, 284 estágio WB, 284 lista, B-680–B-682 ponto flutuante, B-689 registrador de base, 257 unidade para implementar, 251 Ver também instruções load Strcpy, procedimento, 99 definição, 98 ponteiros, 100 procedimento de folha, 100 Ver também procedimentos Stream, benchmark, 546 Streaming Multiprocessor (SM), A-607 Streaming SIMD Extension 2 (SSE2) arquitetura de ponto flutuante, 222 strings definição, 98 em Java, 100–101 representação, 98 striping, 485 subnormais, 219 subtração, 181–186 binária, 181–182 instruções, B-671–B-672
número negativo, 182 overflow, 182 ponto flutuante, 209, B-689–B-690 Ver também aritmética subtrilhas, 488 Sun Fire x4150, servidor, 488–493 conexões lógicas e larguras de banda, 491 ilustração de posterior/anterior, 490 memória mínima, 493 potência ociosa e máxima, 493 Sun UltraSPARC T2 (Niagara 2), 523, 532 básico versus totalmente otimizado características, 546 definição, 546 desempenho, 551 desempenho do LBMHD, 550 desempenho SpMV, 550 ilustração, 547 modelo roofline, 548 supercomputadores, 2 superescalares definição, 320 escalonamento de pipeline dinâmico, 321, 321–322 opções de multithreading, 522 superfícies, A-601 swap, espaço, 401 Swap, procedimento, 119–120 alocação de registradores, 119–120 código do corpo, 120 completo, 120, 121 definição, 119 Ver também procedimentos System Performance Evaluation Cooperative. Ver SPEC
T tabelas de histórico de desvio. Ver previsão de desvio, buffers tabelas de página, 419 atualizando, 399 definição, 399 ilustração, 402 indexando, 400 invertidas, 402 memória principal, 404 níveis, 402–404 registrador, 400 técnicas de redução de armazenamento, 402–404 VMM, 426 tabelas de símbolos, 113, B-637, B-638 tabelas verdade definição, 255 par bits de controle, 256 tags definição, 369 localizando bloco, 389 tabelas de página, 401 tamanho, 391–392 tail call, 96 taxa de acerto, 366
708
taxa de clock definição, 23 frequência comutada como função da, 30 potência, 29 taxa de dados, 480 taxa de transferência máxima, 497 taxas de falhas locais, 393 taxas de falta cache de dados, 418 cache repartido, 378 definição, 366 global, 393 Intrinsity FastMATH, processador, 378 locais, 393 melhoria, 374 origens de falta, 422 tamanho de bloco versus, 375 taxas de perda globais, 393 telas gráficas LCD, 11 suporte de hardware do computador, 12 Telsa PTX ISA, A-592–A-595 instruções aritméticas, A-594 instruções de acesso à memória, A-594–A-595 instruções de thread de GPU, A-593 sincronização de barreira, A-595 tempo de acerto definição, 366 desempenho de cache, 385 tempo de busca, 464 tempo de execução como medida de desempenho válida, 41 CPU, 22, 23, 24 pipelining, 276 tempo de leitura de disco, 465 tempo de resposta, 20, 21 tempo de transferência, 465 tempo médio entre falhas (MTBF), 462 tempo médio para falha. Ver Mean Time To Failure (MTTF) tempo médio para reparo. Ver Mean Time To Repair (MTTR) terabytes, 2 Texture/Processor Cluster (TPC), A-606–A-607 thrashing, 416 threads criação, A-585 CUDA, A-596 gerenciando, A-591 ISA, A-592–A-595 latências de memória, A-628 warps, A-589 TLB, falhas, 405 handler, 414 minimização, 550 ocorrência, 411 ponto de entrada, 414 problema, 416 tratamento, 411–416 Ver também Translation-Lookaside Buffer (TLB)
Índice remissivo
topologias de rede, 534–537 implementando, 536–537 multiestágios, 537 tradução de endereço AMD Opteron X4, 435 definição, 397 Intel Nehalem, 435 rápida, 404–406 TLB para, 404–406 Transaction Processing (TP) benchmarks de E/S, 480–481 definição, 480 Transaction Processing Council (TPC), 480 transistores, 19 Translation-Lookaside Buffer (TLB), 404–406 associatividades, 405 definição, 404 ilustração, 404 integração, 406–409 Intrinsity FastMATH, 406 valores típicos, 405 Ver também TLB, falhas Transmitter Control, registrador, B-659 Transmitter Data, registrador, B-659 tratadores de exceção, B-656–B-657 definição, B-655 retorno dos, B-658 trilhas, 464 troca atômica, 109 troca de contexto, 411 tubos de vácuo, 19
U underflow, 198 Unicode alfabetos, 100 alfabetos de exemplo, 101 definição, 100 unidades commit, 321, 324 controle, 244, 254–255 definição, 216 detecção de hazard, 298, 300 função especial (SFUs), A-596, A-602, A-608 para implementação de load/store, 251 ponto flutuante, 216 rank, 488, 489 unidades de avaliação, 488, 489 unidades de commit buffer, 321 definição, 321 no controle de atualização, 324 unidades de controle, 244 ilustração, 259 principais, projetando, 256–262 saída, 254–255 Ver também Arithmetic Logic Unit (ALU) unidades de detecção de hazard, 298 conexões de pipeline, 300 funções, 300
Uniform Memory Access (UMA), 515–516, A-574 definição, 515 multiprocessadores, 516 usado menos recentemente (LRU) definição, 391 estratégia de substituição de bloco, 420 páginas, 402
V variáveis classe de armazenamento, 93 estáticas, 93 linguagem C, 93 linguagem de programação, 64 registrador, 64 tipo, 93 variáveis estáticas, 93 varredura paralela, A-615 baseada em árvore, A-618 definição, A-615 inclusiva, A-615 modelo CUDA, A-617 varredura paralela baseada em árvore, A-618 vazão definição, 20 despacho múltiplo, 323 pipelining, 276, 323 Very Large-Scale Integrated (VLSI), circuitos, 19 Very Long Instruction Word (VLIW) definição, 317 processadores, 317 Video Graphics Array (VGA), controladores, A-569 Virtual Machine Monitors (VMMs) atitude laissez-faire, 440 definição, 424 implementando, 439–441 na melhoria do desempenho, 425 requisitos, 424 tabelas de página, 426
W wafers, 34 defeitos, 34 definição, 34 dies, 34 yield, 34 warps, 531, A-589 while, loops, 84–85 Wide Area Networks (WANs) definição, 18 Ver também redes words acessando, 65 definição, 64 duplas, 136 load, 65, 67 quad, 136 store, 67
Índice remissivo 709
words duplas, 136 write-back, estágio instrução load, 281 instrução store, 283 linha de controle, 291 write-through, caches vantagens, 420 definição, 376, 420 divergência de tag, 377 Ver também caches
X X86, 133–141 codificação de instruções, 140–141 codificação do especificador do primeiro endereço, 141 conclusão, 141 crescimento do conjunto de instruções, 143 evolução, 133–136 formatos de instrução, 141 instruções de ponto flutuante, 221
instruções/funções típicas, 140 interconexões de E/S, 471–473 linha de tempo histórica, 133–136 modos de endereçamento de dados, 136, 138 operações com inteiros, 136–140 operações típicas, 141 ponto flutuante, 220–222 registradores, 136 SIMD, 525 tipos de instruções, 137