Arquitetura de Computadores:
Uma Abordagem Quantitativa
Quinta Edição
David A. Patterson
John L. Hennessy
ARQU TETURA DE
COMPUTADORES
UMA ABORDAGEM QUANTITATIVA
Tradugao da
sa Edigao
Tradugao: Eduardo Kraslcluk
Revisao Tecnica: Ricardo Pannain
(2
ELSEVIER
CAMPUS
Do original: Computer Architecture: A Quantitative Approach
Tradução autorizada do idioma inglês da edição publicada por Morgan Kaufmann,
an imprint of Elsevier, Inc.
Copyright © 2012 Elsevier Inc.
© 2014, Elsevier Editora Ltda.
Todos os direitos reservados e protegidos pela Lei 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.
Copidesque: Andréa Vidal
Revisão Gráfica: Adriana Maria Patrício Takaki / Marco Antonio Corrêa / Roberto Mauro
dos Santos Facce:
Editoração Eletrônica: Thomson Digital
Elsevier Editora Ltda.
Conhecimento sem Fronteiras
Rua Sete de Setembro, 111 – 16o andar
20050-006 – Centro - Rio de Janeiro – RJ - Brasil
Rua Quintana, 753/8o andar
04569-011 Brooklin - São Paulo - SP - Brasil
Serviço de Atendimento ao Cliente
O800-0265340
Atendimento1@elsevier.com
ISBN: 978-85-352-6122-6
ISBN (versão digital): 978-85-352-6411-1
Edição original: ISBN 978-0-12-383872-8
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, ou bens, originados do uso desta publicação.
CIP-BRASIL. CATALOGAÇÃO NA PUBLICAÇÃO
SINDICATO NACIONAL DOS EDITORES DE LIVROS, RJ
H436a
Hennessy, John L.
Arquitetura de computadores : uma abordagem quantitativa / John L. Hennessy,
David A. Petterson ; tradução Eduardo Kraszczuk. - [5. ed.] - Rio de Janeiro : Elsevier,
2014.
744 p. : il. ; 28 cm.
Tradução de: Computer architecture, 5th ed. : a quantitative approach
Inclui apêndice
ISBN 978-85-352-6122-6
1. Arquitetura de computador. I. Patterson, David A. II. Título.
13-05666
CDD: 004.22
CDU: 004.2
Sobre os Autores
John L. HennessyéodécimopresidentedaUniversidadedeStanford,ondeémembro
docorpodocentedesde1977,nosdepartamentosdeEngenhariaElétricaeCiênciada
Computação.HennessyémembrodoIEEEeACM,membrodaAcademiaNacionalde
EngenhariaedaSociedadeAmericanadeFilosoiaemembrodaAcademiaAmericana
deArteseCiências.EntreseusmuitosprêmiosestãooPrêmioEckert-Mauchlyde2001,
porsuascontribuiçõesparaatecnologiaRISC,oPrêmioSeymourCraydeEngenhariada
Computaçãode2001eoPrêmioJohnvonNeumannde2000,queeledividiucomDavid
Patterson.Eletambémrecebeusetedoutoradoshonorários.
Em1981,eleiniciouoProjetoMIPS,emStanford,comumgrupodeestudantesde
pós-graduação.Depoisdecompletaroprojetoem1984,tiroulicençadauniversidade
paraco-fundaraMIPSComputerSystems(hojeMIPSTechnologies),quedesenvolveu
umdosprimeirosmicroprocessadoresRISCcomerciais.Em2006,maisde2bilhõesde
microprocessadoresMIPSforamvendidosemdispositivos,variandodevideogamese
computadorespalmtopaimpressoraslasereswitchesderede.Emseguida,Hennessy
liderouoprojetoDASH(DirectorArchitetureforSharedMemory–ArquiteturaDiretora
paraMemóriaCompartilhada),quecriouoprotótipodoprimeiromicroprocessador
comcachecoerenteescalável.Muitasdasideias-chavedesseprojetoforamadotadasem
multiprocessadoresmodernos.Alémdesuasatividadestécnicaseresponsabilidadesna
universidade,elecontinuouatrabalharcomdiversasempresasstartupcomoconselheiro
nosestágiosiniciaisecomoinvestidor.
David A. PattersonensinaarquiteturadecomputadoresnaUniversidadedaCalifórnia,
emBerkeley,desdequesejuntouaocorpodocenteem1977,ondeeleocupaaCadeira
PardeedeCiênciadaComputação.SuadocênciafoihonradacomoPrêmiodeEnsino
NotáveldaUniversidadedaCalifórnia,oPrêmioKarlstromdaACM,aMedalhaMulligan
deEducaçãoeoPrêmiodeEnsinoUniversitáriodoIEEE.PattersonrecebeuoPrêmiode
RealizaçãoTécnicadoIEEEeoPrêmioEckert-MauchlyporcontribuiçõesparaoRISCe
dividiuoPrêmioJohnsondeArmazenamentodeInformaçõesporcontribuiçõesparao
RAID.EletambémdividiuaMedalhaJohnvonNeumanndoIEEEeoPrêmioC&Ccom
JohnHennessy.Comoseucoautor,PattersonémembrodaAcademiaAmericanadeArtes
eCiências,doMuseudaHistóriadosComputadores,ACMeIEEE,efoieleitoparaaAcademiaNacionaldeEngenharia,AcademiaNacionaldeCiênciaseparaoHalldaFamada
EngenhariadoValedoSilício.EleatuounoComitêConsultivodeTecnologiadaInformaçãodopresidentedosEstadosUnidos,comopresidentedadivisãodeCSnodepartamento
EECSemBerkeley,comopresidentedaAssociaçãodePesquisaemComputaçãoecomo
PresidentedaACM.EstehistóricolevouaprêmiosdeServiçoDestacadodaACMeCRA.
EmBerkeley,PattersonliderouoprojetoeaimplementaçãodoRISCI,provavelmenteo
primeirocomputadorcomconjuntoreduzidodeinstruçõesVLSI,eafundaçãodaarquiteturacomercialSPARC.ElefoilíderdoprojetoArraysRedundantesdeDiscosBaratos
(RedundantArrayofInexpensiveDisks–RAID),quelevouasistemasdearmazenamento
v
vi
Sobre os Autores
coniáveisparamuitasempresas.EletambémseenvolveunoprojetoRededeWorkstations(NetworkofWorkstations–NOW),quelevouàtecnologiadeclustersusada
pelasempresasdeInternete,maistarde,àcomputaçãoemnuvem.Essesprojetosvaleram
trêsprêmiosdedissertaçãodaACM.SeusprojetosdepesquisaatuaissãooLaboratório
Algoritmo-Máquina-PessoaseoLaboratóriodeComputaçãoParalela,ondeeleéodiretor.
OobjetivodoLaboratórioAMPédesenvolveralgoritmosdeaprendizadodemáquinaescaláveis,modelosdeprogramaçãoamigáveisparacomputadoresemescaladedepósitoe
ferramentasdecrowd-sourcingparaobterrapidamenteinsightsvaliososdemuitosdados
nanuvem.OobjetivodolaboratórioParédesenvolvertecnologiasparaentregarsoftwares
escaláveis,portáveis,eicienteseprodutivosparadispositivospessoaismóveisparalelos.
Para Andrea, Linda, e nossos quatro filhos
Elogios para Arquitetura de Computadores: Uma Abordagem Quantitativa
Quinta Edição
“A 5a edição de Arquitetura de Computadores: Uma Abordagem Quantitativa continua o
legado, fornecendo aos estudantes de arquitetura de computadores as informações mais
atualizadas sobre as plataformas computacionais atuais e insights arquitetônicos para ajudálos a projetar sistemas futuros. Um destaque da nova edição é o capítulo significativamente
revisado sobre paralelismo em nível de dados, que desmistifica as arquiteturas de GPU
com explicações claras, usando terminologia tradicional de arquitetura de computadores.”
—Krste Asanovic, Universidade da Califórnia, Berkeley
“Arquitetura de Computadores: Uma Abordagem Quantitativa é um clássico que, como
um bom vinho, fica cada vez melhor. Eu comprei meu primeiro exemplar quando estava
terminando a graduação e ele continua sendo um dos volumes que eu consulto com mais
frequência. Quando a quarta edição saiu, havia tanto conteúdo novo que eu precisava
comprá-la para continuar atualizado. E, enquanto eu revisava a quinta edição, percebi que
Hennessy e Patterson tiveram sucesso de novo. Todo o conteúdo foi bastante atualizado
e só o Capítulo 6 já torna esta nova edição uma leitura necessária para aqueles que realmente
querem entender a computação em nuvem e em escala de depósito. Somente Hennessy
e Patterson têm acesso ao pessoal do Google, Amazon, Microsoft e outros provedores de
computação em nuvem e de aplicações em escala de Internet, e não existe melhor cobertura
dessa importante área em outro lugar da indústria.”
—James Hamilton, Amazon Web Services
“Hennessy e Patterson escreveram a primeira edição deste livro quando os estudantes de
pós-graduação construíam computadores com 50.000 transistores. Hoje, computadores
em escala de depósito contêm esse mesmo número de servidores, cada qual consistindo
de dúzias de processadores independentes e bilhões de transistores. A evolução
da arquitetura de computadores tem sido rápida e incansável, mas Arquitetura de
Computadores: Uma Abordagem Quantitativa acompanhou o processo com cada edição
explicando e analisando com precisão as importantes novas ideias que tornam esse
campo tão excitante.”
—James Larus, Microsoft Research
“Esta nova adição adiciona um soberbo novo capítulo sobre paralelismo em nível de dados em
SIMD de vetor e arquiteturas de GPU. Ele explica conceitos-chave de arquitetura no interior
das GPUs de mercado de massa, mapeando-os para termos tradicionais e comparando-os
com arquiteturas de vetor e SIMD. Ele chega no momento certo e é relevante à mudança
generalizada para a computação por GPU paralela. Arquitetura de Computadores: Uma
Abordagem Quantitativa continua sendo o primeiro a apresentar uma cobertura completa
da arquitetura de importantes novos desenvolvimentos!”
—John Nickolls, NVIDIA
“A nova edição deste livro – hoje um clássico – destaca a ascendência do paralelismo
explícito (dados, thread, requisição) dedicando um capítulo inteiro a cada tipo. O capítulo
sobre paralelismo de dados é particularmente esclarecedor: a comparação e o contraste
entre SIMD de vetor, SIMD em nível de instrução e GPU ultrapassam o jargão associado a
cada arquitetura e expõem as similaridades e diferenças entre elas.”
—Kunle Olukotun, Universidade de Stanford
“A 5a edição de Arquitetura de Computadores: Uma Abordagem Quantitativa explora
os diversos conceitos paralelos e seus respectivos trade-offs. Assim como as edições
anteriores, esta nova edição cobre as mais recentes tendências tecnológicas. Um destaque
é o grande crescimento dos dispositivos pessoais móveis (Personal Mobile Devices – PMD) e
da computação em escala de depósito (Warehouse-Scale Computing – WSC), cujo foco
mudou para um equilíbrio mais sofisticado entre desempenho e eficiência energética em
comparação com o desempenho bruto. Essas tendências estão alimentando nossa demanda
por mais capacidade de processamento, que, por sua vez, está nos levando mais longe no
caminho paralelo.”
—Andrew N. Sloss, Engenheiro consultor, ARM
Autor de ARM System Developer's Guide
Agradecimentos
Embora este livro ainda esteja na quinta edição, criamos dez versões diferentes do
conteúdo: três versões da primeira edição (alfa, beta e final) e duas versões da segunda,
da terceira e da quarta edições (beta e final). Nesse percurso, recebemos a ajuda de
centenas de revisores e usuários. Cada um deles ajudou a tornar este livro melhor. Por
isso, decidimos fazer uma lista de todas as pessoas que colaboraram em alguma versão
deste livro.
COLABORADORES DA QUINTA EDIÇÃO
Assim como nas edições anteriores, este é um esforço comunitário que envolve diversos
voluntários. Sem a ajuda deles, esta edição não estaria tão bem acabada.
Revisores
Jason D. Bakos, University of South Carolina; Diana Franklin, The University of California,
Santa Barbara; Norman P. Jouppi, HP Labs; Gregory Peterson, University of Tennessee;
Parthasarathy Ranganathan, HP Labs; Mark Smotherman, Clemson University; Gurindar
Sohi, University of Wisconsin–Madison; Mateo Valero, Universidad Politécnica de Cataluña; Sotirios G. Ziavras, New Jersey Institute of Technology.
Membros do Laboratório Par e Laboratório RAD da University of California–Berkeley, que
fizeram frequentes revisões dos Capítulos 1, 4 e 6, moldando a explicação sobre GPUs
e WSCs: Krste Asanovic, Michael Armbrust, Scott Beamer, Sarah Bird, Bryan Catanzaro,
Jike Chong, Henry Cook, Derrick Coetzee, Randy Katz, Yun-sup Lee, Leo Meyervich, Mark
Murphy, Zhangxi Tan, Vasily Volkov e Andrew Waterman.
Painel consultivo
Luiz André Barroso, Google Inc.; Robert P. Colwell, R&E Colwell & Assoc. Inc.; Krisztian
Flautner, VP de R&D na ARM Ltd.; Mary Jane Irwin, Penn State; David Kirk, NVIDIA; Grant
Martin, cientista-chefe, Tensilica; Gurindar Sohi, University of Wisconsin–Madison; Mateo
Valero, Universidad Politécnica de Cataluña.
Apêndices
Krste Asanovic, University of California–Berkeley (Apêndice G); Thomas M. Conte, North
Carolina State University (Apêndice E); José Duato, Universitat Politècnica de València and
Simula (Apêndice F); David Goldberg, Xerox PARC (Apêndice J); Timothy M. Pinkston,
University of Southern California (Apêndice F).
José Flich, da Universidad Politécnica de Valencia, deu contribuições significativas para a
atualização do Apêndice F.
xi
xii
Agradecimentos
Estudos de caso e exercícios
Jason D. Bakos, University of South Carolina (Capítulos 3 e 4); Diana Franklin, University
of California, Santa Barbara (Capítulo 1 e Apêndice C); Norman P. Jouppi, HP Labs
(Capítulo 2); Naveen Muralimanohar, HP Labs (Capítulo 2); Gregory Peterson, University
of Tennessee (Apêndice A); Parthasarathy Ranganathan, HP Labs (Capítulo 6); Amr Zaky,
University of Santa Clara (Capítulo 5 e Apêndice B).
Jichuan Chang, Kevin Lim e Justin Meza auxiliaram no desenvolvimento de testes dos
estudos de caso e exercícios do Capítulo 6.
Material adicional
John Nickolls, Steve Keckler e Michael Toksvig da NVIDIA (Capítulo 4, NVIDIA GPUs);
Victor Lee, Intel (Capítulo 4, comparação do Core i7 e GPU); John Shalf, LBNL (Capítulo 4,
arquiteturas recentes de vetor); Sam Williams, LBNL (modelo roofline para computadores
no Capítulo 4); Steve Blackburn, da Australian National University, e Kathryn McKinley,
da University of Texas, em Austin (Desempenho e medições de energia da Intel, no
Capítulo 5); Luiz Barroso, Urs Hölzle, Jimmy Clidaris, Bob Felderman e Chris Johnson
do Google (Google WSC, no Capítulo 6); James Hamilton, da Amazon Web Services (Distribuição de energia e modelo de custos, no Capítulo 6).
Jason D. Bakos. da University of South Carolina, desenvolveu os novos slides de aula para
esta edição.
Mais uma vez, nosso agradecimento especial a Mark Smotherman, da Clemson University,
que fez a leitura técnica final do nosso manuscrito. Mark encontrou diversos erros e
ambiguidades, e, em consequência disso, o livro ficou muito mais limpo.
Este livro não poderia ter sido publicado sem uma editora, é claro. Queremos agradecer
a toda a equipe da Morgan Kaufmann/Elsevier por seus esforços e suporte. Pelo trabalho
nesta edição, particularmente, queremos agradecer aos nossos editores Nate McFadden e
Todd Green, que coordenaram o painel consultivo, o desenvolvimento dos estudos de caso
e exercícios, os grupos de foco, as revisões dos manuscritos e a atualização dos apêndices.
Também temos de agradecer à nossa equipe na universidade, Margaret Rowland e Roxana
Infante, pelas inúmeras correspondências enviadas e pela “guarda do forte” em Stanford
e Berkeley enquanto trabalhávamos no livro.
Nosso agradecimento final vai para nossas esposas, pelo sofrimento causado pelas leituras,
trocas de ideias e escrita realizadas cada vez mais cedo todos os dias.
COLABORADORES DAS EDIÇÕES ANTERIORES
Revisores
George Adams, Purdue University; Sarita Adve, University of Illinois, Urbana–Champaign; Jim
Archibald, Brigham Young University; Krste Asanovic, Massachusetts Institute of Technology;
Jean-Loup Baer, University of Washington; Paul Barr, Northeastern University; Rajendra V.
Boppana, University of Texas, San Antonio; Mark Brehob, University of Michigan; Doug
Burger, University of Texas, Austin; John Burger, SGI; Michael Butler; Thomas Casavant; Rohit
Chandra; Peter Chen, University of Michigan; as turmas de SUNY Stony Brook, Carnegie
Mellon, Stanford, Clemson e Wisconsin; Tim Coe, Vitesse Semiconductor; Robert P. Colwell;
David Cummings; Bill Dally; David Douglas; José Duato, Universitat Politècnica de València
and Simula; Anthony Duben, Southeast Missouri State University; Susan Eggers, University of
Washington; Joel Emer; Barry Fagin, Dartmouth; Joel Ferguson, University of California, Santa
Agradecimentos
Cruz; Carl Feynman; David Filo; Josh Fisher, Hewlett-Packard Laboratories; Rob Fowler, DIKU;
Mark Franklin, Washington University (St. Louis); Kourosh Gharachorloo; Nikolas Gloy,
Harvard University; David Goldberg, Xerox Palo Alto Research Center; Antonio González,
Intel and Universitat Politècnica de Catalunya; James Goodman, University of Wisconsin–
Madison; Sudhanva Gurumurthi, University of Virginia; David Harris, Harvey Mudd College;
John Heinlein; Mark Heinrich, Stanford; Daniel Helman, University of California, Santa
Cruz; Mark D. Hill, University of Wisconsin–Madison; Martin Hopkins, IBM; Jerry Huck,
Hewlett-Packard Laboratories; Wen-mei Hwu, University of Illinois at Urbana–Champaign;
Mary Jane Irwin, Pennsylvania State University; Truman Joe; Norm Jouppi; David Kaeli,
Northeastern University; Roger Kieckhafer, University of Nebraska; Lev G. Kirischian, Ryerson
University; Earl Killian; Allan Knies, Purdue University; Don Knuth; Jeff Kuskin, Stanford;
James R. Larus, Microsoft Research; Corinna Lee, University of Toronto; Hank Levy; Kai Li,
Princeton University; Lori Liebrock, University of Alaska, Fairbanks; Mikko Lipasti, University
of Wisconsin–Madison; Gyula A. Mago, University of North Carolina, Chapel Hill; Bryan
Martin; Norman Matloff; David Meyer; William Michalson, Worcester Polytechnic Institute;
James Mooney; Trevor Mudge, University of Michigan; Ramadass Nagarajan, University
of Texas at Austin; David Nagle, Carnegie Mellon University; Todd Narter; Victor Nelson;
Vojin Oklobdzija, University of California, Berkeley; Kunle Olukotun, Stanford University;
Bob Owens, Pennsylvania State University; Greg Papadapoulous, Sun Microsystems; Joseph
Pfeiffer; Keshav Pingali, Cornell University; Timothy M. Pinkston, University of Southern
California; Bruno Preiss, University of Waterloo; Steven Przybylski; Jim Quinlan; Andras
Radics; Kishore Ramachandran, Georgia Institute of Technology; Joseph Rameh, University
of Texas, Austin; Anthony Reeves, Cornell University; Richard Reid, Michigan State University;
Steve Reinhardt, University of Michigan; David Rennels, University of California, Los Angeles;
Arnold L. Rosenberg, University of Massachusetts, Amherst; Kaushik Roy, Purdue University;
Emilio Salgueiro, Unysis; Karthikeyan Sankaralingam, University of Texas at Austin; Peter
Schnorf; Margo Seltzer; Behrooz Shirazi, Southern Methodist University; Daniel Siewiorek,
Carnegie Mellon University; J. P. Singh, Princeton; Ashok Singhal; Jim Smith, University
of Wisconsin–Madison; Mike Smith, Harvard University; Mark Smotherman, Clemson
University; Gurindar Sohi, University of Wisconsin–Madison; Arun Somani, University of
Washington; Gene Tagliarin, Clemson University; Shyamkumar Thoziyoor, University of
Notre Dame; Evan Tick, University of Oregon; Akhilesh Tyagi, University of North Carolina,
Chapel Hill; Dan Upton, University of Virginia; Mateo Valero, Universidad Politécnica de
Cataluña, Barcelona; Anujan Varma, University of California, Santa Cruz; Thorsten von
Eicken, Cornell University; Hank Walker, Texas A&M; Roy Want, Xerox Palo Alto Research
Center; David Weaver, Sun Microsystems; Shlomo Weiss, Tel Aviv University; David Wells;
Mike Westall, Clemson University; Maurice Wilkes; Eric Williams; Thomas Willis, Purdue
University; Malcolm Wing; Larry Wittie, SUNY Stony Brook; Ellen Witte Zegura, Georgia
Institute of Technology; Sotirios G. Ziavras, New Jersey Institute of Technology.
Apêndices
O apêndice sobre vetores foi revisado por Krste Asanovic, do Massachusetts Institute
of Technology. O apêndice sobre ponto flutuante foi escrito originalmente por David
Goldberg, da Xerox PARC.
Exercícios
George Adams, Purdue University; Todd M. Bezenek, University of Wisconsin–Madison
(em memória de sua avó, Ethel Eshom); Susan Eggers; Anoop Gupta; David Hayes; Mark
Hill; Allan Knies; Ethan L. Miller, University of California, Santa Cruz; Parthasarathy
Ranganathan, Compaq Western Research Laboratory; Brandon Schwartz, University of
xiii
xiv
Agradecimentos
Wisconsin–Madison; Michael Scott; Dan Siewiorek; Mike Smith; Mark Smotherman; Evan
Tick; Thomas Willis
Estudos de caso e exercícios
Andrea C. Arpaci-Dusseau, University of Wisconsin–Madison; Remzi H. Arpaci Dusseau, University of Wisconsin–Madison; Robert P. Colwell, R&E Colwell & Assoc., Inc.; Diana Franklin,
California Polytechnic State University, San Luis Obispo; Wen-mei W. Hwu, University of
Illinois em Urbana–Champaign; Norman P. Jouppi, HP Labs; John W. Sias, University of
Illinois em Urbana–Champaign; David A. Wood, University of Wisconsin–Madison
Agradecimentos especiais
Duane Adams, Defense Advanced Research Projects Agency; Tom Adams; Sarita Adve,
University of Illinois, Urbana–Champaign; Anant Agarwal; Dave Albonesi, University
of Rochester; Mitch Alsup; Howard Alt; Dave Anderson; Peter Ashenden; David Bailey; Bill Bandy, Defense Advanced Research Projects Agency; Luiz Barroso, Compaq's
Western Research Lab; Andy Bechtolsheim; C. Gordon Bell; Fred Berkowitz; John Best, IBM;
Dileep Bhandarkar; Jeff Bier, BDTI; Mark Birman; David Black; David Boggs; Jim Brady;
Forrest Brewer; Aaron Brown, University of California, Berkeley; E. Bugnion, Compaq's
Western Research Lab; Alper Buyuktosunoglu, University of Rochester; Mark Callaghan;
Jason F. Cantin; Paul Carrick; Chen-Chung Chang; Lei Chen, University of Rochester; Pete
Chen; Nhan Chu; Doug Clark, Princeton University; Bob Cmelik; John Crawford; Zarka
Cvetanovic; Mike Dahlin, University of Texas, Austin; Merrick Darley; the staff of the
DEC Western Research Laboratory; John DeRosa; Lloyd Dickman; J. Ding; Susan Eggers,
University of Washington; Wael El-Essawy, University of Rochester; Patty Enriquez, Mills;
Milos Ercegovac; Robert Garner; K. Gharachorloo, Compaq's Western Research Lab; Garth
Gibson; Ronald Greenberg; Ben Hao; John Henning, Compaq; Mark Hill, University of
Wisconsin–Madison; Danny Hillis; David Hodges; Urs Hölzle, Google; David Hough; Ed
Hudson; Chris Hughes, University of Illinois em Urbana–Champaign; Mark Johnson;
Lewis Jordan; Norm Jouppi; William Kahan; Randy Katz; Ed Kelly; Richard Kessler; Les
Kohn; John Kowaleski, Compaq Computer Corp; Dan Lambright; Gary Lauterbach,
Sun Microsystems; Corinna Lee; Ruby Lee; Don Lewine; Chao-Huang Lin; Paul Losleben, Defense Advanced Research Projects Agency; Yung-Hsiang Lu; Bob Lucas, Defense
Advanced Research Projects Agency; Ken Lutz; Alan Mainwaring, Intel Berkeley Research
Labs; Al Marston; Rich Martin, Rutgers; John Mashey; Luke McDowell; Sebastian Mirolo,
Trimedia Corporation; Ravi Murthy; Biswadeep Nag; Lisa Noordergraaf, Sun Microsystems;
Bob Parker, Defense Advanced Research Projects Agency; Vern Paxson, Center for Internet
Research; Lawrence Prince; Steven Przybylski; Mark Pullen, Defense Advanced Research
Projects Agency; Chris Rowen; Margaret Rowland; Greg Semeraro, University of Rochester;
Bill Shannon; Behrooz Shirazi; Robert Shomler; Jim Slager; Mark Smotherman, Clemson
University; o SMT research group, University of Washington; Steve Squires, Defense
Advanced Research Projects Agency; Ajay Sreekanth; Darren Staples; Charles Stapper; Jorge
Stolfi; Peter Stoll; os estudantes de Stanford e de Berkeley, que deram suporte às nossas
primeiras tentativas de escrever este livro; Bob Supnik; Steve Swanson; Paul Taysom;
Shreekant Thakkar; Alexander Thomasian, New Jersey Institute of Technology; John Toole,
Defense Advanced Research Projects Agency; Kees A. Vissers, Trimedia Corporation; Willa
Walker; David Weaver; Ric Wheeler, EMC; Maurice Wilkes; Richard Zimmerman.
John Hennessy, David Patterson
Introdução
Por Luiz André Barroso, Google Inc.
A primeira edição de Arquitetura de Computadores: Uma Abordagem Quantitativa, de
Hennessy e Patterson, foi lançada durante meu primeiro ano na universidade. Eu pertenço, portanto, àquela primeira leva de profissionais que aprenderam a disciplina usando
este livro como guia. Sendo a perspectiva um ingrediente fundamental para um prefácio
útil, eu me encontro em desvantagem, dado o quanto dos meus próprios pontos de vista
foram coloridos pelas quatro edições anteriores deste livro. Outro obstáculo para uma
perspectiva clara é que a reverência de estudante a esses dois superastros da Ciência da
Computação ainda não me abandonou, apesar de (ou talvez por causa de) eu ter tido
a chance de conhecê-los nos anos seguintes. Essas desvantagens são mitigadas pelo fato
de eu ter exercido essa profissão continuamente desde a primeira edição deste livro, o que
me deu a chance de desfrutar sua evolução e relevância duradora.
A última edição veio apenas dois anos depois que a feroz corrida industrial por maior
frequência de clock de CPU chegou oficialmente ao fim, com a Intel cancelando o desenvolvimento de seus núcleos únicos de 4 GHz e abraçando as CPUs multicore. Dois
anos foi tempo suficiente para John e Dave apresentarem essa história não como uma
atualização aleatória da linha de produto, mas como um ponto de inflexão definidor
da tecnologia da computação na última década. Aquela quarta edição teve ênfase reduzida
no paralelismo em nível de instrução (Instruction-Level Parallelism – ILP) em favor de
um material adicional sobre paralelismo, algo em que a edição atual vai além, dedicando
dois capítulos ao paralelismo em nível de thread e dados, enquanto limita a discussão
sobre ILP a um único capítulo. Os leitores que estão sendo apresentados aos novos engines
de processamento gráfico vão se beneficiar especialmente do novo Capítulo 4, que se
concentra no paralelismo de dados, explicando as soluções diferentes mas lentamente
convergentes oferecidas pelas extensões multimídia em processadores de uso geral e
unidades de processamento gráfico cada vez mais programáveis. De notável relevância
prática: se você já lutou com a terminologia CUDA, veja a Figura 4.24 (teaser: a memória
compartilhada, na verdade, é local, e a memória global se parece mais com o que você
consideraria memória compartilhada).
Embora ainda estejamos no meio dessa mudança para a tecnologia multicore, esta edição
abarca o que parece ser a próxima grande mudança: computação em nuvem. Nesse caso,
a ubiquidade da conectividade à Internet e a evolução de serviços Web atraentes estão
trazendo para o centro do palco dispositivos muito pequenos (smartphones, tablets) e
muito grandes (sistemas de computação em escala de depósito). O ARM Cortex A8, uma
CPU popular para smartphones, aparece na seção “Juntando tudo” do Capítulo 3, e um
Capítulo 6 totalmente novo é dedicado ao paralelismo em nível de requisição e dados
no contexto dos sistemas de computação em escala de depósito. Neste novo capítulo,
John e Dave apresentam esses novos grandes clusters como uma nova classe distinta de
computadores – um convite aberto para os arquitetos de computadores ajudarem a moldar
xv
xvi
Introdução
esse campo emergente. Os leitores vão apreciar o modo como essa área evoluiu na última
década, comparando a arquitetura do cluster Google descrita na terceira edição com a
encanação mais moderna apresentada no Capítulo 6 desta versão.
Aqueles que estão retomando este livro vão poder apreciar novamente o trabalho de dois
destacados cientistas da computação que, ao longo de suas carreiras, aperfeiçoaram a
arte de combinar o tratamento das ideias com princípios acadêmicos com uma profunda
compreensão dos produtos e tecnologias de ponta dessa indústria. O sucesso dos autores
nas interações com a indústria não será uma surpresa para aqueles que testemunharam
como Dave conduz seus retiros bianuais de projeto, foruns meticulosamente elaborados
para extrair o máximo das colaborações acadêmico-industriais. Aqueles que se lembram
do sucesso do empreendimento de John com o MIPS ou esbarraram com ele em um
corredor no Google (o que às vezes acontece comigo) também não vão se surpreender.
E talvez o mais importante: leitores novos e antigos vão obter aquilo por que pagaram.
O que fez deste livro um clássico duradouro foi o fato de que cada edição não é uma
atualização, mas uma extensa revisão que apresenta as informações mais atuais e insights
incomparáveis sobre esse campo fascinante e rapidamente mutável. Para mim, depois
de vinte anos nessa profissão, ele é também outra oportunidade de experimentar aquela
admiração de estudante por dois professores notáveis.
Prefácio
Por que escrevemos este livro
Ao longo das cinco edições deste livro, nosso objetivo tem sido descrever os princípios
básicos por detrás dos desenvolvimentos tecnológicos futuros. Nosso entusiasmo com
relação às oportunidades em arquitetura de computadores não diminuiu, e repetimos o
que dissemos sobre essa área na primeira edição: “Essa não é uma ciência melancólica de
máquinas de papel que nunca funcionarão. Não! É uma disciplina de interesse intelectual
incisivo, que exige o equilíbrio entre as forças do mercado e o custo-desempenho-potência,
levando a gloriosos fracassos e a alguns notáveis sucessos”.
O principal objetivo da escrita de nosso primeiro livro era mudar o modo como as pessoas
aprendiam e pensavam a respeito da arquitetura de computadores. Acreditamos que esse
objetivo ainda é válido e importante. Esse campo está mudando diariamente e precisa ser
estudado com exemplos e medidas reais sobre computadores reais, e não simplesmente
como uma coleção de definições e projetos que nunca precisarão ser compreendidos.
Damos boas-vindas entusiasmadas a todos os que nos acompanharam no passado e
também àqueles que estão se juntando a nós agora. De qualquer forma, prometemos o
mesmo enfoque quantitativo e a mesma análise de sistemas reais.
Assim como nas versões anteriores, nos esforçamos para elaborar uma nova edição que
continuasse a ser relevante tanto para os engenheiros e arquitetos profissionais quanto para
aqueles envolvidos em cursos avançados de arquitetura e projetos de computador. Assim
como os livros anteriores, esta edição visa desmistificar a arquitetura de computadores com
ênfase nas escolhas de custo-benefício-potência e bom projeto de engenharia. Acreditamos
que o campo tenha continuado a amadurecer, seguindo para o alicerce quantitativo
rigoroso das disciplinas científicas e de engenharia bem estabelecidas.
Esta edição
Declaramos que a quarta edição de Arquitetura de Computadores: Uma Abordagem Quantitativa
podia ser a mais significativa desde a primeira edição, devido à mudança para chips
multicore. O feedback que recebemos dessa vez foi de que o livro havia perdido o foco
agudo da primeira edição, cobrindo tudo igualmente, mas sem ênfase nem contexto.
Estamos bastante certos de que não se dirá isso da quinta edição.
Nós acreditamos que a maior parte da agitação está nos extremos do tamanho da computação, com os dispositivos pessoais móveis (Personal Mobile Devices – PMDs), como
telefones celulares e tablets, como clientes e computadores em escala de depósito oferecendo computação na nuvem como servidores. (Bons observadores devem ter notado
a dica sobre computação em nuvem na capa do livro.) Estamos impressionados com o
tema comum desses dois extremos em custo, desempenho e eficiência energética, apesar
de sua diferença em tamanho. Como resultado, o contexto contínuo em cada capítulo é
xvii
xviii
Prefácio
a computação para PMDs e para computadores em escala de depósito, e o Capítulo 6 é
totalmente novo com relação a esse tópico.
O outro tema é o paralelismo em todas as suas formas. Primeiro identificamos os dois tipos
de paralelismo em nível de aplicação no Capítulo 1, o paralelismo em nível de dados (Data-Level
Parallelism – DLP), que surge por existirem muitos itens de dados que podem ser operados
ao mesmo tempo, e o paralelismo em nível de tarefa (Task-Level Parallelism – TLP), que surge
porque são criadas tarefas que podem operar independentemente e, em grande parte, em
paralelo. Então, explicamos os quatro estilos arquitetônicos que exploram DLP e TLP: paralelismo em nível de instrução (Instruction-Level Parallelism – ILP) no Capítulo 3; arquiteturas de
vetor e unidades de processamento gráfico (GPUs) no Capítulo 4, que foi escrito para esta edição;
paralelismo em nível de thread no Capítulo 5; e paralelismo em nível de requisição (Request-Level
Parallelism – RLP), através de computadores em escala de depósito no Capítulo 6, que
também foi escrito para esta edição. Nós deslocamos a hierarquia de memória mais para o
início do livro (Capítulo 2) e realocamos o capítulo sobre sistemas de armazenamento no
Apêndice D. Estamos particularmente orgulhosos do Capítulo 4, que contém a mais clara e
mais detalhada explicação já dada sobre GPUs, e do Capítulo 6, que é a primeira publicação
dos detalhes mais recentes de um computador em escala de depósito do Google.
Como nas edições anteriores, os primeiros três apêndices do livro fornecem o conteúdo básico sobre o conjunto de instruções MIPS, hierarquia de memória e pipelining
aos leitores que não leram livros como Computer Organization and Design. Para manter os custos baixos e ainda assim fornecer material suplementar que seja do interesse
de alguns leitores, disponibilizamos mais nove apêndices onlines em inglês na página
www.elsevier.com.br/hennessy. Há mais páginas nesses apêndices do que neste livro!
Esta edição dá continuidade à tradição de usar exemplos reais para demonstrar as ideias, e
as seções “Juntando tudo” são novas – as desta edição incluem as organizações de pipeline e
hierarquia de memória do processador ARM Cortex A8, o processador Intel Core i7, as GPUs
NVIDIA GTX-280 e GTX-480, além de um dos computadores em escala de depósito do Google.
Seleção e organização de tópicos
Como nas edições anteriores, usamos uma técnica conservadora para selecionar os tópicos,
pois existem muito mais ideias interessantes em campo do que poderia ser abordado de
modo razoável em um tratamento de princípios básicos. Nós nos afastamos de um estudo abrangente de cada arquitetura, com que o leitor poderia se deparar por aí. Nossa
apresentação enfoca os principais conceitos que podem ser encontrados em qualquer
máquina nova. O critério principal continua sendo o da seleção de ideias que foram
examinadas e utilizadas com sucesso suficiente para permitir sua discussão em termos
quantitativos.
Nossa intenção sempre foi enfocar o material que não estava disponível em formato
equivalente em outras fontes, por isso continuamos a enfatizar o conteúdo avançado
sempre que possível. Na realidade, neste livro existem vários sistemas cujas descrições
não podem ser encontradas na literatura. (Os leitores interessados estritamente em uma
introdução mais básica à arquitetura de computadores deverão ler Organização e projeto
de computadores: a interface hardware/software.)
Visão geral do conteúdo
Nesta edição o Capítulo 1 foi aumentado: ele inclui fórmulas para energia, potência estática, potência dinâmica, custos de circuito integrado, confiabilidade e disponibilidade.
Esperamos que esses tópicos possam ser usados ao longo do livro. Além dos princípios
Prefácio
quantitativos clássicos do projeto de computadores e medição de desempenho, a seção
PIAT foi atualizada para usar o novo benchmark SPECPower.
Nossa visão é de que hoje a arquitetura do conjunto de instruções está desempenhando
um papel inferior ao de 1990, de modo que passamos esse material para o Apêndice A. Ele
ainda usa a arquitetura MIPS64 (para uma rápida revisão, um breve resumo do ISA MIPS
pode ser encontrado no verso da contracapa). Para os fãs de ISAs, o Apêndice K aborda
10 arquiteturas RISC, o 80x86, o VAX da DEC e o 360/370 da IBM.
Então, prosseguimos com a hierarquia de memória no Capítulo 2, uma vez que é fácil
aplicar os princípios de custo-desempenho-energia a esse material e que a memória é
um recurso essencial para os demais capítulos. Como na edição anterior, Apêndice B
contém uma revisão introdutória dos princípios de cache, que está disponível caso você
precise dela. O Capítulo 2 discute 10 otimizações avançadas dos caches. O capítulo inclui máquinas virtuais, que oferecem vantagens em proteção, gerenciamento de software
e gerenciamento de hardware, e tem um papel importante na computação na nuvem.
Além de abranger as tecnologias SRAM e DRAM, o capítulo inclui material novo sobre a
memória Flash. Os exemplos PIAT são o ARM Cortex A8, que é usado em PMDs, e o Intel
Core i7, usado em servidores.
O Capítulo 3 aborda a exploração do paralelismo em nível de instrução nos processadores
de alto desempenho, incluindo execução superescalar, previsão de desvio, especulação,
escalonamento dinâmico e multithreading. Como já mencionamos, o Apêndice C é uma
revisão do pipelining, caso você precise dele. O Capítulo 3 também examina os limites do
ILP. Assim como no Capítulo 2, os exemplos PIAT são o ARM Cortex A8 e o Intel Core i7.
Como a terceira edição continha muito material sobre o Itanium e o VLIW, esse conteúdo
foi deslocado para o Apêndice H, indicando nossa opinião de que essa arquitetura não
sobreviveu às primeiras pretensões.
A crescente importância das aplicações multimídia, como jogos e processamento de vídeo,
também aumentou a relevância das arquiteturas que podem explorar o paralelismo em
nível de dados. Há um crescente interesse na computação usando unidades de processamento gráfico (Graphical Processing Units – GPUs). Ainda assim, poucos arquitetos
entendem como as GPUs realmente funcionam. Decidimos escrever um novo capítulo em
grande parte para desvendar esse novo estilo de arquitetura de computadores. O Capítulo 4
começa com uma introdução às arquiteturas de vetor, que serve de base para a construção
de explicações sobre extensões de conjunto de instrução SIMD e GPUS (o Apêndice G
traz mais detalhes sobre as arquiteturas de vetor). A seção sobre GPUs foi a mais difícil
de escrever – foram feitas muitas tentativas para obter uma descrição precisa que fosse
também fácil de entender. Um desafio significativo foi a terminologia. Decidimos usar
nossos próprios termos e, ao traduzi-los, estabelecer uma relação entre eles e os termos
oficiais da NVIDIA (uma cópia dessa tabela pode ser encontrada no verso das capas).
Esse capítulo apresenta o modelo roofline de desempenho, usando-o para comparar o
Intel Core i7 e as GPUs NVIDIA GTX 280 e GTX 480. O capítulo também descreve a GPU
Tegra 2 para PMDs.
O Capítulo 5 descreve os processadores multicore. Ele explora as arquiteturas de memória
simétricas e distribuídas, examinando os princípios organizacionais e o desempenho.
Os tópicos de sincronismo e modelos de consistência de memória vêm em seguida. O
exemplo é o Intel Core i7.
Como já mencionado, o Capítulo 6 descreve o mais novo tópico em arquitetura de computadores: os computadores em escala de depósito (Warehouse-Scale Computers – WSCs).
Com base na ajuda de engenheiros da Amazon Web Services e Google, esse capítulo integra
xix
xx
Prefácio
detalhes sobre projeto, custo e desempenho dos WSCs que poucos arquitetos conhecem.
Ele começa com o popular modelo de programação MapReduce antes de descrever a
arquitetura e implementação física dos WSCs, incluindo o custo. Os custos nos permitem
explicar a emergência da computação em nuvem, porque pode ser mais barato usar WSCs
na nuvem do que em seu datacenter local. O exemplo PIAT é uma descrição de um WSC
Google que inclui informações publicadas pela primeira vez neste livro.
Isso nos leva aos Apêndices A a L. O Apêndice A aborda os princípios de ISAs, incluindo
MIPS64, e o Apêndice K descreve as versões de 64 bits do Alpha, MIPS, PowerPC e SPARC,
além de suas extensões de multimídia. Ele inclui também algumas arquiteturas clássicas
(80x86, VAX e IBM 360/370) e conjuntos de instruções embutidas populares (ARM,
Thumb, SuperH, MIPS16 e Mitsubishi M32R). O Apêndice H está relacionado a esses
conteúdos, pois aborda arquiteturas e compiladores para ISAs VLIW.
Como já dissemos, os Apêndices B e C são tutoriais sobre conceitos básicos de pipelining
e caching. Os leitores relativamente iniciantes em caching deverão ler o Apêndice B antes
do Capítulo 2, e os novos em pipelining deverão ler o Apêndice C antes do Capítulo 3.
O Apêndice D, “Sistemas de Armazenamento”, traz uma discussão maior sobre confiabilidade e disponibilidade, um tutorial sobre RAID com uma descrição dos esquemas RAID
6, e estatísticas de falha de sistemas reais raramente encontradas. Ele continua a fornecer
uma introdução à teoria das filas e benchmarks de desempenho de E/S. Nós avaliamos o
custo, o desempenho e a confiabilidade de um cluster real: o Internet Archive. O exemplo
“Juntando tudo” é o arquivador NetApp FAS6000.
O Apêndice E, elaborado por Thomas M. Conte, consolida o material embutido em um só lugar.
O Apêndice F, sobre redes de interconexão, foi revisado por Timothy M. Pinkston e José Duato. O
Apêndice G, escrito originalmente por Krste Asanovic, inclui uma descrição dos processadores
vetoriais. Esses dois apêndices são parte do melhor material que conhecemos sobre cada tópico.
O Apêndice H descreve VLIW e EPIC, a arquitetura do Itanium.
O Apêndice I descreve as aplicações de processamento paralelo e protocolos de coerência
para o multiprocessamento de memória compartilhada em grande escala. O Apêndice J,
de David Goldberg, descreve a aritmética de computador.
O Apêndice L agrupa as “Perspectivas históricas e referências” de cada capítulo em um
único apêndice. Ele tenta dar o crédito apropriado às ideias presentes em cada capítulo
e o contexto histórico de cada invenção. Gostamos de pensar nisso como a apresentação
do drama humano do projeto de computador. Ele também dá referências que o aluno
de arquitetura pode querer pesquisar. Se você tiver tempo, recomendamos a leitura de
alguns dos trabalhos clássicos dessa área, que são mencionados nessas seções. É agradável
e educativo ouvir as ideias diretamente de seus criadores. “Perspectivas históricas” foi uma
das seções mais populares das edições anteriores.
Navegando pelo texto
Não existe uma ordem melhor para estudar os capítulos e os apêndices, mas todos os
leitores deverão começar pelo Capítulo 1. Se você não quiser ler tudo, aqui estão algumas
sequências sugeridas:
j
j
j
j
Hierarquia de memória: Apêndice B, Capítulo 2 e Apêndice D
Paralelismo em nível de instrução: Apêndice C, Capítulo 3, e Apêndice H
Paralelismo em nível de dados: Capítulos 4 e 6, Apêndice G
Paralelismo em nível de thread: Capítulo 5, Apêndices F e I
Prefácio
j
j
Paralelismo em nível de requisição: Capítulo 6
ISA: Apêndices A e K
O Apêndice E pode ser lido a qualquer momento, mas pode ser mais bem aproveitado se for
lido após as sequências de ISA e cache. O Apêndice J pode ser lido sempre que a aritmética
atraí-lo. Você deve ler a parte correspondente ao Apêndice L depois de finalizar cada capítulo.
Estrutura dos capítulos
O material que selecionamos foi organizado em uma estrutura coerente, seguida em todos
os capítulos. Começamos explorando as ideias de um capítulo. Essas ideias são seguidas
pela seção “Questões cruzadas”, que mostra como as ideias abordadas em um capítulo
interagem com as dadas em outros capítulos. Isso é seguido pela “Juntando tudo”, que
une essas ideias, mostrando como elas são usadas em uma máquina real.
Na sequência vem a seção “Falácias e armadilhas”, que permite aos leitores aprender com
os erros de outros. Mostramos exemplos de enganos comuns e armadilhas arquitetônicas
que são difíceis de evitar, mesmo quando você sabe que estão à sua espera. “Falácias e
armadilhas” é uma das seções mais populares do livro. Cada capítulo termina com uma
seção de “Comentários finais”.
Estudos de caso com exercícios
Cada capítulo termina com estudos de caso e exercícios que os acompanham. Criados
por especialistas do setor e acadêmicos, os estudos de caso exploram os principais
conceitos do capítulo e verificam o conhecimento dos leitores por meio de exercícios
cada vez mais desafiadores. Provavelmente, os instrutores vão achar os estudos de caso
detalhados e robustos o bastante para permitir que os leitores criem seus próprios
exercícios adicionais.
A numeração de cada exercício (<capítulo.seção > ) indica a seção de maior relevância
para completá-lo. Esperamos que isso ajude os leitores a evitarem exercícios relacionados a
alguma seção que ainda não tenham lido, além de fornecer a eles um trecho para revisão.
Os exercícios possuem uma classificação para dar aos leitores uma ideia do tempo necessário para concluí-los:
[10] Menos de 5 minutos (para ler e entender)
[15] 5-15 minutos para dar uma resposta completa
[20] 15-20 minutos para dar uma resposta completa
[25] 1 hora para dar uma resposta completa por escrito
[30] Pequeno projeto de programação: menos de 1 dia inteiro de programação
[40] Projeto de programação significativo: 2 semanas
[Discussão] Tópico para discussão com outros
As soluções para estudos de caso e exercícios estarão disponíveis em inglês para os
instrutores que se registrarem na página do livro (www.elsevier.com.br/hennessy)
Material complementar
Uma variedade de recursos está disponível online em www.elsevier.com.br/hennessy,
incluindo:
j
j
apêndices de referência – alguns com autoria de especialistas sobre o assunto,
convidados – abordando diversos tópicos avançados;
material de perspectivas históricas que explora o desenvolvimento das principais
ideias apresentadas em cada um dos capítulos do texto;
xxi
xxii
Prefácio
j
j
j
j
slides para o instrutor em PowerPoint;
figuras do livro nos formatos PDF, EPS e PPT;
links para material relacionado na Web;
lista de erratas.
Novos materiais e links para outros recursos disponíveis na Web serão adicionados
regularmente.
Ajudando a melhorar este livro
Finalmente, é possível ganhar dinheiro lendo este livro (Isso é que é custo-desempenho!).
Se você ler os “Agradecimentos”, a seguir, verá que nos esforçamos muito para corrigir
os erros. Como um livro passa por muitas reimpressões, temos a oportunidade de fazer
várias correções. Por isso, se você descobrir qualquer bug extra, entre em contato com a
editora norte-americana pelo e-mail <ca5comments@mkp.com>.
Comentários finais
Mais uma vez, este livro é resultado de uma verdadeira coautoria: cada um de nós escreveu
metade dos capítulos e uma parte igual dos apêndices. Não podemos imaginar quanto
tempo teria sido gasto sem alguém fazendo metade do trabalho, servindo de inspiração
quando a tarefa parecia sem solução, proporcionando um insight-chave para explicar um
conceito difícil, fazendo críticas aos capítulos nos fins de semana e se compadecendo
quando o peso de nossas outras obrigações tornava difícil continuar escrevendo (essas
obrigações aumentaram exponencialmente com o número de edições, como mostra o
minicurriculum de cada um). Assim, mais uma vez, compartilhamos igualmente a responsabilidade pelo que você está para ler.
John Hennessy & David Patterson
CAP ÍTULO 1
Fundamentos do projeto e análise quantitativos
“Eu acho justo dizer que os computadores pessoais se tornaram a ferramenta
mais poderosa que já criamos. Eles são ferramentas de comunicação, são
ferramentas de criatividade e podem ser moldados por seu usuário.”
Bill Gates, 24 de fevereiro de 2004
1.1 Introdução ...............................................................................................................................................1
1.2 Classes de computadores ......................................................................................................................4
1.3 Definição da arquitetura do computador ..............................................................................................9
1.4 Tendências na tecnologia ....................................................................................................................14
1.5 Tendências na alimentação dos circuitos integrados ........................................................................19
1.6 Tendências no custo .............................................................................................................................24
1.7 Dependência..........................................................................................................................................30
1.8 Medição, relatório e resumo do desempenho.....................................................................................32
1.9 Princípios quantitativos do projeto de computadores .......................................................................39
1.10 Juntando tudo: desempenho e preço-desempenho ........................................................................46
1.11 Falácias e armadilhas .........................................................................................................................48
1.12 Comentários finais ..............................................................................................................................52
1.13 Perspectivas históricas e referências ................................................................................................54
Estudos de caso e exercícios por Diana Franklin......................................................................................54
1.1
INTRODUÇÃO
A tecnologia de computação fez um progresso incrível no decorrer dos últimos 65 anos,
desde que foi criado o primeiro computador eletrônico de uso geral. Hoje, por menos
de US$ 500 se compra um computador pessoal com mais desempenho, mais memória
principal e mais armazenamento em disco do que um computador comprado em 1985
por US$ 1 milhão. Essa melhoria rápida vem tanto dos avanços na tecnologia usada para
montar computadores quanto da inovação no projeto de computadores.
Embora as melhorias tecnológicas tenham sido bastante estáveis, o progresso advindo de
arquiteturas de computador aperfeiçoadas tem sido muito menos consistente. Durante os
primeiros 25 anos de existência dos computadores eletrônicos, ambas as forças fizeram
uma importante contribuição, promovendo a melhoria de desempenho de cerca de 25%
por ano. O final da década de 1970 viu o surgimento do microprocessador. A capacidade
do microprocessador de acompanhar as melhorias na tecnologia de circuito integrado
1
2
CAPÍTULO 1 :
Fundamentos do projeto e análise quantitativos
levou a uma taxa de melhoria mais alta — aproximadamente 35% de crescimento por
ano, em desempenho.
Essa taxa de crescimento, combinada com as vantagens do custo de um microprocessador
produzido em massa, fez com que uma fração cada vez maior do setor de computação fosse
baseada nos microprocessadores. Além disso, duas mudanças significativas no mercado
de computadores facilitaram, mais do que em qualquer outra época, o sucesso comercial
com uma nova arquitetura: 1) a eliminação virtual da programação em linguagem Assembly reduziu a necessidade de compatibilidade de código-objeto; 2) a criação de sistemas
operacionais padronizados, independentes do fornecedor, como UNIX e seu clone, o
Linux, reduziu o custo e o risco de surgimento de uma nova arquitetura.
Essas mudanças tornaram possível o desenvolvimento bem-sucedido de um novo conjunto
de arquiteturas com instruções mais simples, chamadas arquiteturas RISC (Reduced Instruction Set Computer — computador de conjunto de instruções reduzido), no início da
década de 1980. As máquinas baseadas em RISC chamaram a atenção dos projetistas para
duas técnicas críticas para o desempenho: a exploração do paralelismo em nível de instrução
(inicialmente por meio do pipelining e depois pela emissão de múltiplas instruções) e o
uso de caches (inicialmente em formas simples e depois usando organizações e otimizações
mais sofisticadas).
Os computadores baseados em RISC maximizaram o padrão de desempenho, forçando
as arquiteturas anteriores a acompanhar esse padrão ou a desaparecer. O Vax da Digital
Equipment não fez isso e, por essa razão, foi substituído por uma arquitetura RISC. A
Intel acompanhou o desafio, principalmente traduzindo instruções 80x86 (ou IA-32)
para instruções tipo RISC, internamente, permitindo a adoção de muitas das inovações
pioneiras nos projetos RISC. À medida que a quantidade de transistores aumentava no
final dos anos 1990, o overhead do hardware para traduzir a arquitetura x86 mais complexa tornava-se insignificante. Em aplicações específicas, como telefones celulares, o custo
com relação à potência e à área de silício relativo ao overhead da tradução do x86 ajudou
uma arquitetura RISC, a ARM, a se tornar dominante.
A Figura 1.1 mostra que a combinação de melhorias na organização e na arquitetura dos
computadores fez com que o crescimento do desempenho fosse constante durante 17 anos,
a uma taxa anual de mais de 50% — ritmo sem precedentes no setor de computação.
Quatro foram os impactos dessa notável taxa de crescimento no século XX. Primeiro, ela
melhorou consideravelmente a capacidade disponível aos usuários de computador. Para
muitas aplicações, os microprocessadores de desempenho mais alto de hoje ultrapassam
o supercomputador de menos de 10 anos atrás.
Em segundo lugar, essa melhoria drástica em custo/desempenho levou a novas classes de
computadores. Os computadores pessoais e workstations emergiram nos anos 1980 com a
disponibilidade do microprocessador. A última década viu o surgimento dos smartphones
e tablets, que muitas pessoas estão usando como plataformas primárias de computação
no lugar dos PCs. Esses dispositivos clientes móveis estão usando a internet cada vez
mais para acessar depósitos contendo dezenas de milhares de servidores, que estão sendo
projetados como se fossem um único gigantesco computador.
Em terceiro lugar, a melhoria contínua da fabricação de semicondutores, como previsto
pela lei de Moore, levou à dominância de computadores baseados em microprocessadores
por toda a gama de projetos de computador. Os minicomputadores, que tradicionalmente
eram feitos a partir de lógica pronta ou de gate arrays, foram substituídos por servidores
montados com microprocessadores. Os mainframes foram praticamente substituídos por
1.1
Introdução
FIGURA 1.1 Crescimento no desempenho do processador desde o fim da década de 1970.
Este gráfico mostra o desempenho relativo ao VAX 11/780, medido pelos benchmarks SPECint (Seção 1.8). Antes de meados da década de 1980,
o crescimento no desempenho do processador era, em grande parte, controlado pela tecnologia e, em média, era de 25% por ano. O aumento no
crescimento, para cerca de 52% desde então, é atribuído a ideias arquitetônicas e organizacionais mais avançadas. Em 2003, esse crescimento levou a
uma diferença no desempenho de cerca de um fator de 25 versus se tivéssemos continuado com a taxa de 25%. O desempenho para cálculos orientados
a ponto flutuante aumentou ainda mais rapidamente. Desde 2003, os limites de potência, paralelismo disponível em nível de instrução e latência longa da
memória reduziram o desempenho do uniprocessador para não mais de 22% por ano ou cerca de cinco vezes mais lento do que se tivéssemos continuado
com 52% ao ano. (O desempenho SPEC mais rápido desde 2007 teve a paralelização automática ativada, com um número cada vez maior de núcleos por
chip a cada ano, então a velocidade do uniprocessador é difícil de medir. Esses resultados se limitam a sistemas de soquete único para reduzir o impacto
da paralelização automática.) A Figura 1.11, na página 22, mostra a melhoria nas taxas de clock para essas mesmas três eras. Como o SPEC foi alterado
no decorrer dos anos, o desempenho das máquinas mais novas é estimado por um fator de escala que relaciona o desempenho para duas versões
diferentes do SPEC (por exemplo, SPEC89, SPEC92, SPEC95, SPEC2000 e SPEC2006).
um pequeno número de microprocessadores encapsulados. Até mesmo os supercomputadores de ponta estão sendo montados com grupos de microprocessadores.
Essas inovações de hardware levaram ao renascimento do projeto de computadores, que
enfatizou tanto a inovação arquitetônica quanto o uso eficiente das melhorias da tecnologia. Essa taxa de crescimento foi aumentada de modo que, em 2003, os microprocessadores de alto desempenho eram cerca de 7,5 vezes mais rápidos do que teriam alcançado
contando-se apenas com a tecnologia, incluindo a melhoria do projeto do circuito. Ou
seja, 52% ao ano versus 35% ao ano.
O renascimento do hardware levou ao quarto impacto sobre o desenvolvimento de software. Essa melhoria de 25.000 vezes no desempenho desde 1978 (Fig. 1.1) permitiu aos
programadores da atualidade trocar o desempenho pela produtividade. Em vez de utilizar
linguagens orientadas ao desempenho, como C e C++, hoje as programações utilizam
mais as linguagens, como Java e C#, chamadas de managed programming languages. Além
do mais, linguagens script, como Python e Ruby, que são ainda mais produtivas, estão
ganhando popularidade juntamente com frameworks de programação, como Ruby on
Rails. Para manter a produtividade e tentar eliminar o problema do desempenho, os interpretadores com compiladores just-in-time e compilação trace-based estão substituindo os
3
4
CAPÍTULO 1 :
Fundamentos do projeto e análise quantitativos
compiladores e o linkers tradicionais do passado. A implementação de software também
está mudando, com o software como serviço (Software as a Service — SaaS) usado na internet, substituindo os softwares comprados em uma mídia (shirink-wrapped software),
que devem ser instalados e executados em um computador local.
A natureza das aplicações também muda. Fala, som, imagens e vídeo estão tornando-se
cada vez mais importantes, juntamente com o tempo de resposta previsível, tão crítico para
o usuário. Um exemplo inspirador é o Google Goggles. Esse aplicativo permite apontar a
câmera do telefone celular para um objeto e enviar a imagem pela internet sem fio para um
computador em escala wharehouse, que reconhece o objeto e dá informações interessantes
sobre ele. O aplicativo pode traduzir textos do objeto para outro idioma, ler o código de
barras da capa de um livro e dizer se ele está disponível on-line e qual é o seu preço ou,
se fizer uma panorâmica com a câmera do celular, dizer quais empresas estão próximas
a você, quais são seus sites, números telefônicos e endereços.
Porém, a Figura 1.1 também mostra que esse renascimento de 17 anos acabou. Desde 2003,
a melhoria de desempenho dos uniprocessadores únicos caiu para cerca de 22% por ano,
devido tanto à dissipação máxima de potência dos chips resfriados a ar como à falta de
maior paralelismo no nível de instrução que resta para ser explorado com eficiência. Na
realidade, em 2004, a Intel cancelou seus projetos de uniprocessadores de alto desempenho
e juntou-se a outras empresas ao mostrar que o caminho para um desempenho mais alto
seria através de vários processadores por chip, e não de uniprocessadores mais rápidos.
Isso sinaliza uma passagem histórica, de contar unicamente com o paralelismo em nível de
instrução (Instruction-Level Parallelism — ILP), foco principal das três primeiras edições
deste livro, para contar com o paralelismo em nível de thread (Thread-Level Parallelism —
TLP) e o paralelismo em nível de dados (Data-Level Parallelism — DLP), que são abordados
na quarta edição e expandidos nesta. Esta edição também inclui computadores em escala
wharehouse. Embora o compilador e o hardware conspirem para explorar o ILP implicitamente sem a atenção do programador, DLP, TLP e RLP são explicitamente paralelos,
exigindo a reestruturação do aplicativo para que ele possa explorar o paralelismo explícito.
Em alguns casos, isso é fácil. Em muitos, é uma nova grande carga para os programadores.
Este capítulo focaliza as ideias arquitetônicas e as melhorias no compilador que as acompanham e que possibilitaram a incrível taxa de crescimento no século passado, além dos
motivos para a surpreendente mudança e os desafios e enfoques promissores iniciais
para as ideias arquitetônicas e compiladores para o século XXI. No centro está o enfoque
quantitativo para o projeto e a análise de compilador, que usa observações empíricas dos
programas, experimentação e simulação como ferramentas. Esse estilo e esse enfoque do
projeto de computador são refletidos neste livro. O objetivo, aqui, é estabelecer a base
quantitativa na qual os capítulos e apêndices a seguir se baseiam.
Este livro foi escrito não apenas para explorar esse estilo de projeto, mas também para
estimulá-lo a contribuir para esse progresso. Acreditamos que essa técnica funcionará
para computadores explicitamente paralelos do futuro, assim como funcionou para os
computadores implicitamente paralelos do passado.
1.2
CLASSES DE COMPUTADORES
Essas alterações prepararam o palco para uma mudança surpreendente no modo como
vemos a computação, nas aplicações computacionais e nos mercados de computadores,
neste novo século. Nunca, desde a criação do computador pessoal, vimos mudanças tão
notáveis em como os computadores se parecem e como são usados. Essas mudanças no uso
1.2
Classes de computadores
FIGURA 1.2 Um resumo das cinco classes de computação principais e suas características de sistema.
As vendas em 2010 incluíram cerca de 1,8 bilhão de PMDs (90% deles em telefones celulares), 350 milhões de PCs desktop e 20 milhões de servidores.
O número total de processadores embarcados vendidos foi de quase 19 bilhões. No total, 6,1 bilhões de chips baseados em tecnologia ARM foram vendidos
em 2010. Observe a ampla faixa de preços de servidores e sistemas embarcados, que vão de pendrives USB a roteadores de rede. Para servidores,
essa faixa varia da necessidade de sistemas multiprocessadores com escala muito ampla ao processamento de transações de alto nível.
do computador geraram três mercados de computador diferentes, cada qual caracterizado
por diferentes aplicações, requisitos e tecnologias de computação. A Figura 1.2 resume
essas classes principais de ambientes de computador e suas características importantes.
Dispositivo pessoal móvel (PMD)
Dispositivo pessoal móvel (Personal Mobile Device — PMD) é o nome que aplicamos a uma
coleção de dispositivos sem fio com interfaces de usuário multimídia, como telefones
celulares, tablets, e assim por diante. O custo é a principal preocupação, dado que o preço
para o consumidor de todo o produto é de algumas centenas de dólares. Embora a ênfase
na eficiência energética seja frequentemente orientada pelo uso de baterias, a necessidade
de usar materiais menos caros — plástico em vez de cerâmica — e a ausência de uma
ventoinha para resfriamento também limitam o consumo total de energia. Examinamos
a questão da energia e da potência em detalhes na Seção 1.5. Aplicativos para PMDs
muitas vezes são baseados na web e orientados para a mídia, como no exemplo acima
(Google Goggles). Os requisitos de energia e tamanho levam ao uso de memória Flash
para armazenamento (Cap. 2) no lugar de discos magnéticos.
A capacidade de resposta e previsibilidade são características-chave para aplicações de
mídia. Um requisito de desempenho em tempo real significa que um segmento da aplicação
tem um tempo absoluto máximo de execução. Por exemplo, ao se reproduzir vídeo em
um PMD, o tempo para processar cada quadro de vídeo é limitado, pois o processador
precisa aceitar e processar o próximo quadro rapidamente. Em algumas aplicações, existe
um requisito mais sutil: o tempo médio para determinada tarefa é restrito, tanto quanto o
número de ocorrências quando um tempo máximo é ultrapassado. Essas técnicas, também
chamadas tempo real flexível, são necessárias quando é possível perder, ocasionalmente, a
restrição de tempo em um evento, desde que não haja muita perda. O desempenho em
tempo real costuma ser altamente dependente da aplicação.
Outras características-chave em muitas aplicações PMD são a necessidade de minimizar a
memória e a necessidade de minimizar o consumo de potência. A eficiência energética é
orientada tanto pela potência da bateria quanto pela dissipação de calor. A memória pode
ser uma parte substancial do custo do sistema, e é importante otimizar o tamanho dessa
memória nesses casos. A importância do tamanho da memória é traduzida com ênfase
no tamanho do código, pois o tamanho dos dados é ditado pela aplicação.
5
6
CAPÍTULO 1 :
Fundamentos do projeto e análise quantitativos
Computação de desktop
O primeiro e maior mercado em termos financeiros ainda é o de computadores desktop.
A computação desktop varia desde sistemas inferiores, vendidos por menos de US$ 300,
até estações de trabalho de ponta altamente configuradas, que podem custar US$ 2.500.
Desde 2008, mais da metade dos computadores desktops fabricados, por ano, corresponde
a computadores laptop alimentados por bateria.
Por todo esse intervalo de preço e capacidade, o mercado de desktop costuma ser orientado a otimizar a relação preço-desempenho. Essa combinação de desempenho (medido
principalmente em termos de desempenho de cálculo e desempenho de gráficos) e preço
de um sistema é o que mais importa para os clientes nesse mercado e, portanto, para os
projetistas de computadores. Como resultado, os microprocessadores mais novos, de
desempenho mais alto, e os microprocessadores de custo reduzido normalmente aparecem
primeiro nos sistemas de desktop (ver, na Seção 1.6, uma análise das questões que afetam
o custo dos computadores).
A computação de desktop também costuma ser razoavelmente bem caracterizada em
termos de aplicações e benchmarking, embora o uso crescente de aplicações centradas na
web, interativas, imponha novos desafios na avaliação do desempenho.
Servidores
Com a passagem para a computação desktop nos anos 1980, o papel dos servidores
cresceu para oferecer serviços de arquivo e computação em maior escala e mais seguros.
Tais servidores se tornaram a espinha dorsal da computação empresarial de alta escala,
substituindo o mainframe tradicional.
Para os servidores, diferentes características são importantes. Primeiro, a disponibilidade é
crítica (discutimos a dependência na Seção 1.7). Considere os servidores que suportam as
máquinas de caixa eletrônico para bancos ou os sistemas de reserva de linhas aéreas. As falhas
desses sistemas de servidor são muito mais catastróficas do que as falhas de um único desktop,
pois esses servidores precisam operar sete dias por semana, 24 horas por dia. A Figura 1.3
estima as perdas de receita em função do tempo de paralisação para aplicações de servidor.
FIGURA 1.3 Os custos arredondados para o milhar mais próximo de um sistema não disponível são mostrados com uma análise do custo
do tempo de paralisação (em termos de receita perdida imediatamente), considerando três níveis de disponibilidade diferentes
e que o tempo de paralisação é distribuído uniformemente.
Esses dados são de Kembel (2000) e foram coletados e analisados pela Contingency Planning Research.
1.2
Classes de computadores
Por fim, os servidores são projetados para um throughput eficiente. Ou seja, o desempenho geral do servidor — em termos de transações por minuto ou páginas web atendidas
por segundo — é o fator crucial. A capacidade de resposta a uma solicitação individual
continua sendo importante, mas a eficiência geral e a eficiência de custo, determinadas
por quantas solicitações podem ser tratadas em uma unidade de tempo, são as principais
métricas para a maioria dos servidores. Retornamos à questão de avaliar o desempenho
para diferentes tipos de ambientes de computação na Seção 1.8.
Computadores clusters/escala wharehouse
O crescimento do software como serviço (Software as a Service — SaaS) para aplicações
como busca, redes sociais, compartilhamento de vídeo, games multiplayer, compras on-line,
e assim por diante, levou ao crescimento de uma classe de computadores chamados clusters.
Clusters são coleções de computadores desktop ou servidores conectados por redes locais
para funcionar como um único grande computador. Cada nó executa seu próximo sistema
operacional, e os nós se comunicam usando um protocolo de rede. Os maiores clusters são
chamados computadores de armazenamento em escala (Warehouse-Scale Computers — WSCs),
uma vez que eles são projetados para que dezenas de milhares de servidores possam funcionar como um só. O Capítulo 6 descreve essa classe de computadores extremamente grandes.
A relação preço-desempenho e o consumo de potência são críticos para os WSCs, já que
eles são tão grandes. Como o Capítulo 6 explica, 80% do custo de US$ 90 milhões de um
WSC é associado à potência e ao resfriamento interior dos computadores. Os próprios
computadores e o equipamento de rede custam outros US$ 70 milhões e devem ser substituídos após alguns anos de uso. Ao comprar tanta computação, você precisa fazer isso
com sabedoria, já que uma melhoria de 10% no desempenho de preço significa uma
economia de US$ 7 milhões (10% de 70 milhões).
Os WSCs estão relacionados com os servidores no sentido de que a disponibilidade é
crítica. Por exemplo, a Amazon.com teve US$ 13 bilhões de vendas no quarto trimestre de
2010. Como em um trimestre há cerca de 2.200 horas, a receita média por hora foi de quase
US$ 6 milhões. Durante uma hora de pico de compras no Natal, a perda potencial seria
muitas vezes maior. Como explicado no Capítulo 6, a diferença em relação aos servidores
é que os WSCs usam componentes redundantes baratos, como building blocks, confiando
em uma camada de software para capturar e isolar as muitas falhas que vão ocorrer com
a computação nessa escala. Note que a escalabilidade para um WSC é tratada pela rede
LAN que conecta os computadores, e não por um hardware integrado de computador,
como no caso dos servidores.
Uma categoria relacionada comos WSCs é a dos supercomputadores, que custam dezenas
de milhões de dólares, mas os supercomputadores são diferentes, pois enfatizam o
desempenho em ponto flutuante e, a cada vez, executam programas em lotes grandes,
com comunicação pesada, por semanas. Esse acoplamento rígido leva ao uso de redes
internas muito mais rápidas. Em contraste, os WSCs enfatizam aplicações interativas,
armazenamento em grande escala, dependência e grande largura de banda de internet.
Computadores embarcados
Os computadores embarcados são encontrados em máquinas do dia a dia: fornos de
micro-ondas, máquinas de lavar, a maioria das impressoras, switches de rede e todos os
carros contêm microprocessadores embarcados simples.
Muitas vezes, os processadores em um PMD são considerados computadores embarcados,
mas os estamos colocando em uma categoria separada, porque os PMDs são plataformas
que podem executar softwares desenvolvidos externamente e compartilham muitas das
7
8
CAPÍTULO 1 :
Fundamentos do projeto e análise quantitativos
características dos computadores desktop. Outros dispositivos embarcados são mais limitados em sofisticação de hardware e software. Nós usamos a capacidade de executar software
de terceiros como a linha divisória entre computadores embarcados e não embarcados.
Os computadores embarcados possuem a mais extensa gama de poder de processamento
e custo. Eles incluem processadores de 8 e 16 bits, que podem custar menos de 10 centavos de dólar, microprocessadores de 32 bits, que executam 100 milhões de instruções
por segundo e custam menos de US$ 5, e processadores de ponta para switches de rede
mais recentes, que custam US$ 100 e podem executar bilhões de instruções por segundo.
Embora a gama da capacidade de computação no mercado de computação embarcada
seja muito extensa, o preço é um fator importante no projeto de computadores para esse
espaço. Existem requisitos de desempenho, é claro, mas o objetivo principal normalmente
é atender a necessidade de desempenho a um preço mínimo, em vez de conseguir desempenho mais alto a um preço mais alto.
A maior parte deste livro se aplica ao projeto, uso e desempenho de processadores embarcados, sejam eles microprocessadores encapsulados, sejam núcleos de microprocessadores que
serão montados com outro hardware de uso específico. Na realidade, a terceira edição deste
livro incluiu exemplos de computação embarcada para ilustrar as ideias em cada capítulo.
Infelizmente, a maioria dos leitores considerou esses exemplos insatisfatórios, pois os
dados levam ao projeto quantitativo e à avaliação de computadores desktop, e servidores
ainda não foram bem estendidos para a computação embarcada (ver os desafios com o
EEMBC, por exemplo, na Seção 1.8). Portanto, por enquanto ficamos com as descrições
qualitativas, que não se ajustam bem ao restante do livro. Como resultado, nesta edição,
consolidamos o material embarcado em um único novo apêndice. Acreditamos que o
Apêndice E melhore o fluxo de ideias no texto, permitindo ainda que os leitores percebam
como os diferentes requisitos afetam a computação embarcada.
Classes de paralelismo e arquiteturas paralelas
Paralelismo em múltiplos níveis é a força impulsionadora do projeto de computadores
pelas quatro classes de computadores, tendo a energia e o custo como as principais restrições. Existem basicamente dois tipos de paralelismo em aplicações:
1. Paralelismo em nível de dados (Data-Level Parallelism — DLP): surge porque existem
muitos itens de dados que podem ser operados ao mesmo tempo.
2. Paralelismo em nível de tarefas (Task-Level Parallelism — TLP): surge porque
são criadas tarefas que podem operar de modo independente e principalmente
em paralelo.
O hardware do computador pode explorar esses dois tipos de paralelismo de aplicação
de quatro modos principais:
1. O paralelismo em nível de instruções explora o paralelismo em nível de dados a níveis
modestos com auxílio do compilador, usando ideias como pipelining
e em níveis médios usando ideias como execução especulativa.
2. As arquiteturas vetoriais e as unidades de processador gráfico (Graphic Processor Units —
GPUs) exploram o paralelismo em nível de dados aplicando uma única instrução
a uma coleção de dados em paralelo.
3. O paralelismo em nível de thread explora o paralelismo em nível de dados
ou o paralelismo em nível de tarefas em um modelo de hardware fortemente
acoplado, que permite a interação entre threads paralelos.
4. O paralelismo em nível de requisição explora o paralelismo entre tarefas muito
desacopladas especificadas pelo programador ou pelo sistema operacional.
1.3
Definição da arquitetura do computador
Esses quatro modos de o hardware suportar o paralelismo em nível de dados e o paralelismo em nível de tarefas têm 50 anos. Quando Michael Flynn (1966) estudou os esforços de
computação paralela nos anos 1960, encontrou uma classificação simples cujas abreviações
ainda usamos hoje. Ele examinou o paralelismo nos fluxos de instrução e dados chamados
pelas instruções no componente mais restrito do multiprocessador, colocando todos os
computadores em uma de quatro categorias:
1. Fluxo simples de instrução, fluxo simples de dados (Single Instruction Stream, Single
Data Stream — SISD). Essa categoria é o uniprocessador. O programador pensa
nela como o computador sequencial padrão, mas ele pode explorar o paralelismo
em nível de instrução. O Capítulo 3 cobre as arquiteturas SISD que usam técnicas
ILP, como a execução superescalar e a execução especulativa.
2. Fluxo simples de instrução, fluxos múltiplos de dados (Single Instruction Stream,
Multiple Data Streams — SIMD). A mesma instrução é executada por múltiplos
processadores usando diferentes fluxos de dados. Computadores SIMD exploram
o paralelismo em nível de dados ao aplicar as mesmas operações a múltiplos itens
em paralelo. Cada processador tem sua própria memória de dados (daí o MD
de SIMD), mas existe uma única memória de instruções e um único processador de
controle, que busca e envia instruções. O Capítulo 4 cobre o DLP e três diferentes
arquiteturas que o exploram: arquiteturas vetoriais, extensões multimídia
a conjuntos de instruções-padrão e GPUs.
3. Fluxos de múltiplas instruções, fluxo simples de dados (Multiple Instruction Stream,
Single Data Stream — MISD). Nenhum microprocessador comercial desse
tipo foi construído até hoje, mas ele completa essa classificação simples.
4. Fluxos múltiplos de instruções, fluxos múltiplos de dados (Multiple Instruction Streams,
Multiple Data Streams — MIMD). Cada processador busca suas próprias instruções
e opera seus próprios dados, buscando o paralelismo em nível de tarefa. Em geral,
o MIMD é mais flexível do que o SIMD e, por isso, em geral é mais aplicável,
mas é inerentemente mais caro do que o SIMD. Por exemplo, computadores MIMD
podem também explorar o paralelismo em nível de dados, embora o overhead
provavelmente seja maior do que seria visto em um computador SIMD.
Esse overhead significa que o tamanho do grão deve ser suficientemente grande
para explorar o paralelismo com eficiência. O Capítulo 5 cobre arquiteturas MIMD
fortemente acopladas que exploram o paralelismo em nível de thread,
uma vez que múltiplos threads em cooperação operam em paralelo. O Capítulo 6
cobre arquiteturas MIMD fracamente acopladas — especificamente, clusters
e computadores em escala — que exploram o paralelismo em nível de requisição,
em que muitas tarefas independentes podem ocorrer naturalmente em paralelo,
com pouca necessidade de comunicação ou sincronização.
Essa taxonomia é um modelo grosseiro, já que muitos processadores paralelos são híbridos
das classes SISD, SIMD e MIMD. Mesmo assim, é útil colocar um framework no espaço
de projeto para os computadores que veremos neste livro.
1.3
DEFINIÇÃO DA ARQUITETURA DO COMPUTADOR
A tarefa que o projetista de computador desempenha é complexa: determinar quais
atributos são importantes para um novo computador, depois projetar um computador
para maximizar o desempenho enquanto permanece dentro das restrições de custo,
potência e disponibilidade. Essa tarefa possui muitos aspectos, incluindo o projeto do
conjunto de instruções, a organização funcional, o projeto lógico e a implementação. A
implementação pode abranger o projeto do circuito integrado, o acondicionamento, a
9
10
CAPÍTULO 1 :
Fundamentos do projeto e análise quantitativos
potência e o resfriamento. A otimização do projeto requer familiaridade com uma gama
de tecnologias muito extensa, desde compiladores e sistemas operacionais até o projeto
lógico e o o acondicionamento.
No passado, o nome arquitetura de computadores normalmente se referia apenas ao projeto
do conjunto de instruções. Outros aspectos do projeto de computadores eram chamados
de implementação, normalmente insinuando que a implementação não é interessante ou
é menos desafiadora.
Acreditamos que essa visão seja incorreta. A tarefa do arquiteto ou do projetista é muito
mais do que projetar o conjunto de instruções, e os obstáculos técnicos nos outros aspectos
do projeto provavelmente são mais desafiadores do que aqueles encontrados no projeto do
conjunto de instruções. Veremos rapidamente a arquitetura do conjunto de instruções
antes de descrever os desafios maiores para o arquiteto de computador.
Arquitetura do conjunto de instruções
Neste livro, usamos o nome arquitetura do conjunto de instruções (Instruction Set Architecture
— ISA) para nos referir ao conjunto de instruções visíveis pelo programador. A arquitetura do conjunto de instruções serve como interface entre o software e o hardware. Essa
revisão rápida da arquitetura do conjunto de instruções usará exemplos do 80x86, do ARM
e do MIPS para ilustrar as sete dimensões de uma arquitetura do conjunto de instruções. Os
Apêndices A e K oferecem mais detalhes sobre as três arquiteturas de conjunto de instruções.
1. Classes de ISA. Hoje, quase todas as arquiteturas de conjunto de instruções são
classificadas como arquiteturas de registradores de propósito geral (GPRs),
em que os operandos são registradores ou locais de memória. O 80x86 contém
16 registradores de propósito geral e 16 que podem manter dados de ponto
flutuante (FPRs), enquanto o MIPS contém 32 registradores de propósito geral
e 32 de ponto flutuante (Fig. 1.4). As duas versões populares dessa classe são
arquiteturas de conjunto de instruções registrador-memória, como o 80x86,
FIGURA 1.4 Registradores do MIPS e convenções de uso.
Além dos 32 registradores de propósito geral (R0-R31), o MIPS contém 32 registradores de ponto flutuante (F0-F31)
que podem manter um número de precisão simples de 32 bits ou um número de precisão dupla de 64 bits.
1.3
2.
3.
4.
5.
6.
7.
Definição da arquitetura do computador
que podem acessar a memória como parte de muitas instruções, e arquiteturas
de conjunto de instruções load-store, como o MIPS, que só podem acessar a memória
com instruções load ou store. Todas as arquiteturas de conjunto de instruções
recentes são load-store.
Endereçamento de memória. Praticamente todos os computadores desktop
e servidores, incluindo o 80x86 e o MIPS, utilizam endereçamento de byte
para acessar operandos da memória. Algumas arquiteturas, como ARM e MIPS,
exigem que os objetos estejam alinhados. Um acesso a um objeto com tamanho
de s bytes no endereço de byte A está alinhado se A mod s = 0 (Fig. A.5 na
página A-7.) O 80x86 não exige alinhamento, mas os acessos geralmente são mais
rápidos se os operandos estiverem alinhados.
Modos de endereçamento. Além de especificar registradores e operandos constantes,
os modos de endereçamento especificam o endereço de um objeto na memória.
Os modos de endereçamento do MIPS são registrador, imediato (para constantes)
e deslocamento, em que um deslocamento constante é acrescentado a um registrador
para formar o endereço da memória. O 80x86 suporta esses três modos
de endereçamento e mais três variações de deslocamento: nenhum registrador
(absoluto), dois registradores (indexados pela base com deslocamento) e dois
registradores em que um registrador é multiplicado pelo tamanho do operando
em bytes (base com índice em escala e deslocamento). Ele contém mais dos três últimos,
sem o campo de deslocamento, mais o indireto por registrador, indexado
e base com índice em escala. O ARM tem os três modos de endereçamento MIPS mais
o endereçamento relativo a PC, a soma de dois registradores e a soma de dois registradores
em que um registrador é multiplicado pelo tamanho do operando em bytes. Ele também
tem endereçamento por autoincremento e autodecremento, em que o endereço calculado
substitui o conteúdo de um dos registradores usados para formar o endereço.
Tipos e tamanhos de operandos. Assim como a maioria das arquiteturas de conjunto
de instruções, o MIPS, o ARM e o 80x86 admitem tamanhos de operando
de 8 bits (caractere ASCII), 16 bits (caractere Unicode ou meia palavra), 32 bits
(inteiro ou palavra), 64 bits (dupla palavra ou inteiro longo) e ponto flutuante
IEEE 754 com 32 bits (precisão simples) e 64 bits (precisão dupla). O 80x86
também admite ponto flutuante de 80 bits (precisão dupla estendida).
Operações. As categorias gerais de operações são transferência de dados, lógica
e aritmética, controle (analisado em seguida) e ponto flutuante. O MIPS
é uma arquitetura de conjunto de instruções simples e fáceis de executar
em um pipeline, representando as arquiteturas RISC usadas em 2011. A Figura 1.5
resume a arquitetura do conjunto de instruções do MIPS. O 80x86 possui
um conjunto de operações maior e muito mais rico (Apêndice K).
Instruções de fluxo de controle. Praticamente todas as arquiteuras de conjunto de
instruções, incluindo essas três, admitem desvios condicionais, saltos incondicionais,
chamadas e retornos de procedimento. As três usam endereçamento relativo ao
PC, no qual o endereço de desvio é especificado por um campo de endereço que é
somado ao PC. Existem algumas pequenas diferenças. Desvios condicionais do MIPS
(BE, BNE etc.) testam o conteúdo dos registradores, enquanto os desvios do 80x86
e ARM testam o conjunto de bits de código de condição como efeitos colaterais
das operações aritméticas/lógicas. As chamadas de procedimento do ARM e MIPS
colocam o endereço de retorno em um registrador, enquanto a chamada do 80x86
(CALLF) coloca o endereço de retorno em uma pilha na memória.
Codificando uma arquitetura de conjunto de instruções. Existem duas opções básicas
na codificação: tamanho fixo e tamanho variável. Todas as instruções do ARM e MIPS
possuem 32 bits de extensão, o que simplifica a decodificação da instrução.
11
12
CAPÍTULO 1 :
Fundamentos do projeto e análise quantitativos
FIGURA 1.5 Subconjunto das instruções no MIPS64.
SP = single precision (precisão simples), DP = double precision (precisão dupla). O Apêndice A contém detalhes sobre o MIPS64. Para os dados, o número
do bit mais significativo é 0; o menos significativo é 63.
1.3
Definição da arquitetura do computador
FIGURA 1.6 Formatos de arquitetura do conjunto de instruções MIPS64.
Todas as instruções possuem 32 bits de extensão. O formato R é para operações registrador para registrador inteiro,
como DADDU, DSUBU, e assim por diante. O formato I é para transferências de dados, desvios e instruções imediatas,
como LD, SD, BEQZ e DADDIs. O formato J é para saltos, o formato FR é para operações de ponto flutuante, e o
formato FI é para desvios em ponto flutuante.
A Figura 1.6 mostra os formatos de instruções do MIPS. A codificação do 80x86 tem
tamanho variável de 1-18 bytes. As instruções de tamanho variável podem ocupar
menos espaço que as instruções de tamanho fixo, de modo que um programa
compilado para o 80x86 normalmente é menor que o mesmo programa compilado
para MIPS. Observe que as opções mencionadas anteriormente afetarão o modo
como as instruções são codificadas em uma representação binária. Por exemplo,
o número de registradores e o número de modos de endereçamento possuem
impacto significativo sobre o tamanho das instruções, pois o campo de registrador
e o campo de modo de endereçamento podem aparecer muitas vezes em uma única
instrução. (Observe que o ARM e o MIPS, mais tarde, ofereceram extensões para
fornecer instruções com 16 bits de extensão para reduzir o tamanho do programa,
chamado Thumb ou Thumb-2 e MIPS16, respectivamente.)
No presente, os outros desafios enfrentados pelo arquiteto de computador, além do
projeto da arquitetura do conjunto de instruções, são particularmente críticos quando
as diferenças entre os conjuntos de instruções são pequenas e existem áreas de aplicação
distintas. Portanto, a partir da última edição, o núcleo do material do conjunto de instruções, além dessa revisão rápida, pode ser encontrado nos apêndices (Apêndices A e K).
Neste livro, usamos um subconjunto do MIPS64 como exemplo de arquitetura do conjunto
de instruções porque ele é tanto dominante para redes quanto um exemplo elegante das
arquiteturas RISC mencionadas, das quais o ARM (Advanced RISC Machine) é o exemplo
mais popular. Os processadores ARM estavam em 6,1 bilhões de chips fabricados em 2010,
ou aproximadamente 20 vezes o número de chips produzidos de processadores 80x86.
Arquitetura genuína de computador: projetando a organização
e o hardware para atender objetivos e requisitos funcionais
A implementação de um computador possui dois componentes: organização e hardware.
O termo organização inclui os aspectos de alto nível do projeto de um computador, como o
sistema de memória, a interconexão de memória e o projeto do processador interno ou
CPU (unidade central de processamento, na qual são implementados a aritmética, a
13
14
CAPÍTULO 1 :
Fundamentos do projeto e análise quantitativos
lógica, os desvios e as transferências de dados). O termo microarquitetura também é usado
no lugar de organização. Por exemplo, dois processadores com as mesmas arquiteturas de
conjunto de instruções, mas com organizações diferentes, são o AMD Opteron e o Intel
Core i7. Ambos implementam o conjunto de instruções x86, mas possuem organizações
de pipeline e cache muito diferentes.
A mudança para os processadores múltiplos de microprocessadores levou ao termo core
usado também para processador. Em vez de se dizer microprocessador multiprocessador, o
termo multicore foi adotado. Dado que quase todos os chips têm múltiplos processadores,
o nome unidade central de processamento, ou CPU, está tornando-se popular.
Hardware refere-se aos detalhes específicos de um computador, incluindo o projeto lógico
detalhado e a tecnologia de encapsulamento. Normalmente, uma linha de computadores
contém máquinas com arquiteturas de conjunto de instruções idênticas e organizações
quase idênticas, diferindo na implementação detalhada do hardware. Por exemplo, o Intel
Core i7 (Cap. 3) e o Intel Xeon 7560 (Cap. 5) são praticamente idênticos, mas oferecem
taxas de clock e sistemas de memória diferentes, tornando o Xeon 7560 mais eficiente
para computadores mais inferiores.
Neste livro, a palavra arquitetura abrange os três aspectos do projeto de computadores:
arquitetura do conjunto de instruções, organização e hardware.
Os arquitetos dessa área precisam projetar um computador para atender aos requisitos funcionais e também aos objetivos relacionados com preço, potência, desempenho e disponibilidade. A Figura 1.7 resume os requisitos a considerar no projeto de um novo computador.
Normalmente, os arquitetos também precisam determinar quais são os requisitos funcionais,
o que pode ser uma grande tarefa. Os requisitos podem ser recursos específicos inspirados
pelo mercado. O software de aplicação normalmente controla a escolha de certos requisitos
funcionais, determinando como o computador será usado. Se houver um grande conjunto
de software para certa arquitetura de conjunto de instruções, o arquiteto poderá decidir
que o novo computador deve implementar um dado conjunto de instruções. A presença de
um grande mercado para determinada classe de aplicações pode encorajar os projetistas a
incorporarem requisitos que tornariam o computador competitivo nesse mercado. Muitos
desses requisitos e recursos são examinados em profundidade nos próximos capítulos.
Os arquitetos precisam estar conscientes das tendências importantes, tanto na tecnologia
como na utilização dos computadores, já que elas afetam não somente os custos no futuro
como também a longevidade de uma arquitetura.
1.4
TENDÊNCIAS NA TECNOLOGIA
Para ser bem-sucedida, uma arquitetura de conjunto de instruções precisa ser projetada
para sobreviver às rápidas mudanças na tecnologia dos computadores. Afinal, uma nova
arquitetura de conjunto de instruções bem-sucedida pode durar décadas — por exemplo, o núcleo do mainframe IBM está em uso há quase 50 anos. Um arquiteto precisa
planejar visando às mudanças de tecnologia que possam aumentar o tempo de vida de
um computador bem-sucedido.
Para planejar a evolução de um computador, o projetista precisa estar ciente das rápidas
mudanças na tecnologia de implementação. Quatro dessas tecnologias, que mudam em
ritmo notável, são fundamentais para as implementações modernas:
j
Tecnologia do circuito lógico integrado. A densidade de transistores aumenta em cerca
de 35% ao ano, quadruplicando em pouco mais de quatro anos. Os aumentos no
1.4
Tendências na tecnologia
FIGURA 1.7 Resumo de alguns dos requisitos funcionais mais importantes com os quais um arquiteto se depara.
A coluna da esquerda descreve a classe de requisitos, enquanto a coluna da direita oferece exemplos específicos. A coluna da direita também contém
referências a capítulos e apêndices que lidam com os requisitos específicos.
j
tamanho do die são menos previsíveis e mais lentos, variando de 10-20%
por ano. O efeito combinado é o crescimento na contagem de transistores
de um chip em cerca de 40-55% por ano, ou dobrando a cada 18-24 meses.
Essa tendência é conhecida popularmente como lei de Moore. A velocidade
do dispositivo aumenta mais lentamente, conforme mencionamos a seguir.
DRAM semicondutora (memória dinâmica de acesso aleatório). Agora que
a maioria dos chips DRAM é produzida principalmente em módulos DIMM, é mais
difícil rastrear a capacidade do chip, já que os fabricantes de DRAM costumam
oferecer produtos de diversas capacidades ao mesmo tempo, para combinar
com a capacidade do DIMM. A capacidade por chip DRAM tem aumentado
15
16
CAPÍTULO 1 :
Fundamentos do projeto e análise quantitativos
FIGURA 1.8 Mudança na taxa de melhoria na capacidade da DRAM ao longo do tempo.
As duas primeiras edições chamaram essa taxa de regra geral de crescimento da DRAM, uma vez que havia sido
bastante confiável desde 1977, com a DRAM de 16 kilobits, até 1996, com a DRAM de 64 megabits. Hoje, alguns
questionam se a capacidade da DRAM pode melhorar em 5- 7 anos, devido a dificuldades em fabricar uma célula
de DRAM cada vez mais tridimensional (Kim, 2005).
j
j
j
em cerca de 25-40% por ano, dobrando aproximadamente a cada 2-3 anos. Essa
tecnologia é a base da memória principal e será discutida no Capítulo 2. Observe que
a taxa de melhoria continuou a cair ao longo das edições deste livro, como mostra
a Figura 1.8. Existe até mesmo uma preocupação: a taxa de crescimento vai parar
no meio desta década devido à crescente dificuldade em produzir com eficiência
células DRAM ainda menores (Kim, 2005)? O Capítulo 2 menciona diversas outras
tecnologias que podem substituir o DRAM, se ele atingir o limite da capacidade.
Flash semicondutor (memória somente para leitura eletricamente apagável e programável).
Essa memória semicondutora não volátil é o dispositivo-padrão de armazenamento
nos PMDs, e sua popularidade alavancou sua rápida taxa de crescimento
em capacidade. Recentemente, a capacidade por chip Flash vem aumentando em
cerca de 50-60% por ano, dobrando aproximadamente a cada dois anos. Em 2011,
a memória Flash era 15-20 vezes mais barata por bit do que a DRAM. O Capítulo 2
descreve a memória Flash.
Tecnologia de disco magnético. Antes de 1990, a densidade aumentava em cerca
de 30% por ano, dobrando em três anos. Ela aumentou para 60% por ano depois
disso e para 100% por ano em 1996. Desde 2004, caiu novamente para cerca
de 40% por ano ou dobrou a cada três anos. Os discos são 15-25 vezes mais
baratos por bit do que a Flash. Dada a taxa de crescimento reduzido da DRAM,
hoje os discos são 300-500 vezes mais baratos por bit do que a DRAM. É a principal
tecnologia para o armazenamento em servidores e em computadores em escala
warehouse (vamos discutir essas tendências em detalhes no Apêndice D).
Tecnologia de rede. O desempenho da rede depende do desempenho dos switches
e do desempenho do sistema de transmissão (examinaremos as tendências em redes
no Apêndice F).
Essas tecnologias que mudam rapidamente modelam o projeto de um computador que, com
melhorias de velocidade e tecnologia, pode ter um tempo de vida de 3-5 anos. As principais
tecnologias, como DRAM, Flash e disco, mudam o suficiente para que o projetista precise
planejar essas alterações. Na realidade, em geral, os projetistas projetam para a próxima tecnologia sabendo que, quando um produto começar a ser entregue em volume, essa tecnologia
pode ser a mais econômica ou apresentar vantagens de desempenho. Tradicionalmente, o
custo tem diminuído aproximadamente na mesma taxa em que a densidade tem aumentado.
Embora a tecnologia melhore continuamente, o impacto dessas melhorias pode ocorrer
em saltos discretos, à medida que um novo patamar para uma capacidade seja alcançado.
Por exemplo, quando a tecnologia MOS atingiu um ponto, no início da década de 1980,
1.4
Tendências na tecnologia
quando cerca de 25.000-50.000 transistores poderiam caber em um único chip, foi possível montar um microprocessador de único chip de 32 bits. Ao final da mesma década,
as caches de primeiro nível puderam ser inseridos no mesmo chip. Eliminando os cruzamentos do processador dentro do chip e entre o processador e a cache, foi possível alcançar
uma melhoria incrível no custo-desempenho e na potência-desempenho. Esse projeto
era simplesmente inviável até que a tecnologia alcançasse determinado ponto. Com os
microprocessadores multicore e número de cores aumentando a cada geração, mesmo os
computadores servidores estão se dirigindo para ter um único chip para todos os processadores. Esses limites de tecnologia não são raros e possuem um impacto significativo
sobre grande variedade de decisões de projeto.
Tendências de desempenho: largura de banda sobre latência
Como veremos na Seção 1.8, largura de banda ou throughput é a quantidade total de trabalho
feito em determinado tempo, como megabytes por segundo, para uma transferência de
disco. Ao contrário, latência ou tempo de resposta é o tempo entre o início e o término de um
evento, como milissegundos, para um acesso ao disco. A Figura 1.9 representa a melhoria
relativa na largura de banda e a latência para os marcos da tecnologia de microprocessadores, memória, redes e discos. A Figura 1.10 descreve os exemplos e os marcos com
mais detalhes.
O desempenho é o principal diferenciador para microprocessadores e redes, de modo
que eles têm visto os maiores ganhos: 10.000-20.000X em largura de banda e 30-80X em
latência. A capacidade geralmente é mais importante do que o desempenho para memória
e discos, de modo que a capacidade melhorou mais, embora seus avanços de largura de
banda de 300-1.200X ainda sejam muito maiores do que seus ganhos em latência de 6-8X.
FIGURA 1.9 Representação simples dos marcos de largura de banda e latência da Figura 1.10 em relação
ao primeiro marco.
Observe que a latência melhorou de 6X a 80X, enquanto a largura de banda melhorou cerca de 300X a 25.000X.
Atualização de Patterson (2004).
17
18
CAPÍTULO 1 :
Fundamentos do projeto e análise quantitativos
FIGURA 1.10 Marcos de desempenho por 25-40 anos para microprocessadores, memória, redes e discos.
Os marcos do microprocessador são várias gerações de processadores IA-32, variando desde um 80286 microcodificado com barramento
de 16 bits até um Core i7 multicor, com execução fora de ordem, superpipelined. Os marcos de módulo de memória vão da DRAM plana de 16 bits de
largura até a DRAM síncrona versão 3 com taxa de dados dupla com 64 bits de largura. As redes Ethernet avançaram de 10 Mb/s até 100 Gb/s.
Os marcos de disco são baseados na velocidade de rotação, melhorando de 3.600 RPM até 15.000 RPM. Cada caso é a largura de banda
no melhor caso, e a latência é o tempo para uma operação simples, presumindo-se que não haja disputa. Atualização
de Patterson (2004).
1.5
Tendências na alimentação dos circuitos integrados
Claramente, a largura de banda ultrapassou a latência por essas tecnologias e provavelmente continuará dessa forma. Uma regra prática simples é que a largura de banda cresce
ao menos pelo quadrado da melhoria na latência. Os projetistas de computadores devem
levar isso em conta para o planejamento.
Escala de desempenho de transistores e fios
Os processos de circuito integrado são caracterizados pela característica de tamanho, que
é o tamanho mínimo de um transistor ou de um fio na dimensão x ou y. Os tamanhos
diminuíram de 10 m em 1971 para 0,0032 m em 2011; na verdade, trocamos as unidades,
de modo que a produção em 2011 agora é referenciada como “32 nanômetros”, e chips de
22 nanômetros estão a caminho. Como a contagem de transistores por milímetro quadrado de silício é determinada pela superfície de um transistor, a densidade de transistores
quadruplica com uma diminuição linear no tamanho do recurso.
Porém, o aumento no desempenho do transistor é mais complexo. À medida que os
tamanhos diminuem, os dispositivos encolhem quadruplicadamente nas dimensões
horizontal e vertical. O encolhimento na dimensão vertical requer uma redução na
voltagem de operação para manter a operação e a confiabilidade dos transistores correta.
Essa combinação de fatores de escala leva a um inter-relacionamento entre o desempenho
do transistor e a carecterística de tamanho do processo. Para uma primeira aproximação,
o desempenho do transistor melhora linearmente com a diminuição de seu tamanho.
O fato de a contagem de transistores melhorar em quatro vezes, com uma melhoria linear
no desempenho do transistor, é tanto o desafio quanto a oportunidade para a qual os
arquitetos de computadores foram criados! Nos primeiros dias dos microprocessadores, a
taxa de melhoria mais alta na densidade era usada para passar rapidamente de microprocessadores de 4 bits para 8 bits, para 16 bits, para 32 bits, para 64 bits. Mais recentemente,
as melhorias de densidade admitiram a introdução de múltiplos microprocessadores por
chip, unidades SIMD maiores, além de muitas das inovações em execução especulativa e
em caches encontradas nos Capítulos 2, 3, 4 e 5.
Embora os transistores geralmente melhorem em desempenho com a diminuição do
tamanho, os fios em um circuito integrado não melhoram. Em particular, o atraso de sinal
em um fio aumenta na proporção com o produto de sua resistência e de sua capacitância.
Naturalmente, à medida que o tamanho diminui, os fios ficam mais curtos, mas a resistência e a capacitância por tamanho unitário pioram. Esse relacionamento é complexo, pois
tanto a resistência quanto a capacitância dependem de aspectos detalhados do processo,
da geometria de um fio, da carga sobre um fio e até mesmo da adjacência com outras estruturas. Existem aperfeiçoamentos ocasionais no processo, como a introdução de cobre,
que oferecem melhorias de uma única vez no atraso do fio.
Porém, em geral, o atraso do fio não melhora muito em comparação com o desempenho
do transistor, criando desafios adicionais para o projetista. Nos últimos anos, o atraso do
fio tornou-se uma limitação de projeto importante para grandes circuitos integrados e
normalmente é mais crítico do que o atraso do chaveamento do transistor. Frações cada vez
maiores de ciclo de clock têm sido consumidas pelo atraso de propagação dos sinais nos fios.
1.5 TENDÊNCIAS NA ALIMENTAÇÃO
DOS CIRCUITOS INTEGRADOS
Hoje, a energia é o segundo maior desafio enfrentado pelo projetista de computadores
para praticamente todas as classes de computador. Primeiramente, a alimentação precisa
19
20
CAPÍTULO 1 :
Fundamentos do projeto e análise quantitativos
ser trazida e distribuída pelo chip, e os microprocessadores modernos utilizam centenas
de pinos e várias camadas de interconexão apenas para alimentação e terra. Além disso,
a energia é dissipada como calor e precisa ser removida.
Potência e energia: uma perspectiva de sistema
Como um arquiteto de sistema ou um usuário devem pensar sobre o desempenho, a
potência e a energia? Do ponto de vista de um projetista de sistema, existem três preocupações principais.
Em primeiro lugar, qual é a potência máxima que um processador pode exigir? Atender a
essa demanda pode ser importante para garantir a operação correta. Por exemplo, se um
processador tenta obter mais potência do que a fornecida por um sistema de alimentação
(obtendo mais corrente do que o sistema pode fornecer), em geral o resultado é uma queda
de tensão que pode fazer o dispositivo falhar. Processadores modernos podem variar muito
em consumo de potência com altos picos de corrente. Portanto, eles fornecem métodos
de indexação de tensão que permitem ao processador ficar mais lento e regular a tensão
dentro de uma margem grande. Obviamente, fazer isso diminui o desempenho.
Em segundo lugar, qual é o consumo de potência sustentado? Essa métrica é amplamente
chamada projeto térmico de potência (Thermal Design Power — TDP), uma vez que determina o requisito de resfriamento. O TDP não é nem potência de pico, em alguns momentos
cerca de 1,5 vez maior, nem a potência média real que será consumida durante um dado
cálculo, que provavelmente será ainda menor. Geralmente, uma fonte de alimentação típica
é projetada para atender ou exceder o TDP. Não proporcionar resfriamento adequado vai
permitir à temperatura de junção no processador exceder seu valor máximo, resultando
em uma falha no dispositivo, possivelmente com danos permanentes. Os processadores
modernos fornecem dois recursos para ajudar a gerenciar o calor, uma vez que a potência
máxima (e, portanto, o calor e o aumento de temperatura) pode exceder, a longo prazo,
a média especificada pela TDP. Primeiro, conforme a temperatura se aproxima do limite
de temperatura de junção, os circuitos reduzem a taxa de clock, reduzindo também a
potência. Se essa técnica não tiver sucesso, um segundo protetor de sobrecarga é ativado
para desativar o chip.
O terceiro fator que os projetistas e usuários devem considerar é a energia e a eficiência
energética. Lembre-se de que potência é simplesmente energia por unidade de tempo: 1
watt = 1 joule por segundo. Qual é a métrica correta para comparar processadores: energia ou
potência? Em geral, a energia é sempre uma métrica melhor, porque está ligada a uma tarefa
específica e ao tempo necessário para ela. Em particular, a energia para executar uma carga
de trabalho é igual à potência média vezes o tempo de execução para a carga de trabalho.
Assim, se quisermos saber qual dentre dois processadores é mais eficiente para uma dada
tarefa, devemos comparar o consumo de energia (não a potência) para realizá-la. Por
exemplo, o processador A pode ter um consumo de potência médio 20% maior do que
o processador B, mas se A executar a tarefa em apenas 70% do tempo necessário para B,
seu consumo de energia será 1,2 × 0,7 = 0,84, que é obviamente melhor.
Pode-se argumentar que, em um grande servidor ou em uma nuvem, é suficiente considerar
a potência média, uma vez que muitas vezes se supõe que a carga de trabalho seja infinita,
mas isso é equivocado. Se nossa nuvem fosse ocupada por processadores B em vez de
processadores A, faria menos trabalho pela mesma quantidade de energia gasta. Usar a
energia para comparar as alternativas evita essa armadilha. Seja uma carga de trabalho
fixa, uma nuvem warehouse-scale, seja um smartphone, comparar a energia será o modo
correto de comparar alternativas de processador, já que tanto a conta de eletricidade para
1.5
Tendências na alimentação dos circuitos integrados
a nuvem quanto o tempo de vida da bateria para o smartphone são determinados pela
energia consumida.
Quando o consumo de potência é uma medida útil? O uso primário legítimo é como
uma restrição; por exemplo, um chip pode ser limitado a 100 watts. Isso pode ser usado
como métrica se a carga de trabalho for fixa, mas então é só uma variação da verdadeira
métrica de energia por tarefa.
Energia e potência dentro de um microprocessador
Para os chips de CMOS, o consumo de energia dominante tradicional tem ocorrido no
chaveamento de transistores, também chamada energia dinâmica. A energia exigida por
transistor é proporcional ao produto da capacitância de carga do transistor ao quadrado
da voltagem:
Energiadinâmica ∝ Carga capacitiva × Voltagem 2
Essa equação é a energia de pulso da transição lógica de 0→1→0 ou 1→0→1. A energia
de uma única transição (0→1 ou 1→0) é, então:
Energiadinâmica ∝ 1 / 2Carga capacitiva × Voltagem 2
A potência necessária por transistor é somente o produto da energia de uma transição
multiplicada pela frequência das transições:
Potênciadinâmica ∝ 1 / 2Carga capacitiva × Voltagem 2 × Frequência de chaveamento
Para uma tarefa fixa, reduzir a taxa de clock reduz a potência, mas não a energia.
Obviamente, a potência dinâmica e a energia são muito reduzidas quando se reduz a
voltagem, por isso as voltagens caíram de 5V para pouco menos de 1V em 20 anos. A carga
capacitiva é uma função do número de transistores conectados a uma saída e à tecnologia,
que determina a capacitância dos fios e transistores.
Exemplo
Resposta
Hoje, alguns microprocessadores são projetados para ter voltagem ajustável, de modo que uma redução de 15% na voltagem pode resultar em uma
redução de 15% na frequência. Qual seria o impacto sobre a energia dinâmica
e a potência dinâmica?
Como a capacitância é inalterada, a resposta para a energia é a razão das
voltagens, uma vez que a capacitância não muda:
Energianova
(Voltagem × 0,85)2
= 0,852 = 0,72
= 0,72 ×
Energiavelha
Voltagem 2
reduzindo assim a potência para cerca de 72% da original. Para a potência,
adicionamos a taxa das frequências
Energianova
(Frequência dechaveamento × 0,85)
= 0,72 ×
= 0,61
Energiavelha
Frequência dechaveamento
reduzindo a potência para cerca de 61% do original.
Ao passarmos de um processo para outro, o aumento no número de transistores chaveados
e a frequência com que eles chaveiam dominam a diminuição na capacitância de carga e
voltagem, levando a um crescimento geral no consumo de potência e energia. Os primeiros
microprocessadores consumiam menos de 1 watt, e os primeiros microprocessadores de
32 bits (como o Intel 80386) usavam cerca de 2 watts, enquanto um Intel Core i7 de
3,3 GHz consome 130 watts. Visto que esse calor precisa ser dissipado de um chip com cerca
de 1,5 cm em um lado, estamos alcançando os limites do que pode ser resfriado pelo ar.
21
22
CAPÍTULO 1 :
Fundamentos do projeto e análise quantitativos
Dada a equação anterior, você poderia esperar que o crescimento da frequência de clock
diminuísse se não pudéssemos reduzir a voltagem ou aumentar a potência por chip. A
Figura 1.11 mostra que esse é, de fato, o caso desde 2003, mesmo para os microprocessadores que tiveram os melhores desempenhos a cada ano. Observe que esse período de
taxas constantes de clock corresponde ao período de baixa melhoria de desempenho na
Figura 1.1.
Distribuir a potência, retirar o calor e impedir pontos quentes têm tornado-se desafios cada
vez mais difíceis. A potência agora é a principal limitação para o uso de transistores; no
passado, era a área bruta do silício. Portanto, os microprocessadores modernos oferecem
muitas técnicas para tentar melhorar a eficiência energética, apesar das taxas de clock e
tensões de alimentação constantes:
1. Não fazer nada bem. A maioria dos microprocessadores de hoje desliga o clock
de módulos inativos para economizar energia e potência dinâmica. Por exemplo,
se nenhuma instrução de ponto flutuante estiver sendo executada, o clock
da unidade de ponto flutuante será desativado. Se alguns núcleos estiverem inativos,
seus clocks serão interrompidos.
2. Escalamento dinâmico de voltagem-frequência (Dynamic Voltage-Frequency Scaling —
DVFS). A segunda técnica vem diretamente das fórmulas anteriores. Dispositivos
pessoais móveis, laptops e, até mesmo, servidores têm períodos de baixa atividade,
em que não há necessidade de operar em frequências de clock e voltagens mais
elevadas. Os microprocessadores modernos costumam oferecer algumas frequências
FIGURA 1.11 Crescimento na taxa de clock dos microprocessadores na Figura 1.1.
Entre 1978 e 1986, a taxa de clock aumentou menos de 15% por ano, enquanto o desempenho aumentou em 25%
por ano. Durante o “período de renascimento” de 52% de melhoria de desempenho por ano entre 1986 e 2003, as
taxas de clock aumentaram em quase 40% por ano. Desde então, a taxa de clock tem sido praticamente a mesma,
crescendo menos de 1% por ano, enquanto o desempenho de processador único melhorou menos de 22% por ano.
1.5
Tendências na alimentação dos circuitos integrados
FIGURA 1.12 Economias de energia para um servidor usando um microprocessador AMD Opteron, 8 GB
de DRAM, e um disco ATA.
A 1,8 GHz, o servidor só pode lidar até dois terços da carga de trabalho sem causar violações de nível de serviço, e a
1,0 GHz ele só pode lidar com a segurança de um terço da carga de trabalho (Figura 5.11, em Barroso e Hölzle, 2009).
de clock e voltagens que usam menor potência e energia. A Figura 1.12 mostra
as economias potenciais de potência através de DVFS para um servidor, conforme
a carga de trabalho diminui para três diferentes taxas de clock: 2,4 GHz, 1,8 GHz
e 1 GHz. A economia geral de potência no servidor é de cerca de 10-15% para cada
um dos dois passos.
3. Projeto para um caso típico. Dado que os PMDs e laptops muitas vezes estão inativos,
a memória e o armazenamento oferecem modos de baixa potência para poupar
energia. Por exemplo, DRAMs têm uma série de modos de potência cada vez
menores para aumentar a vida da bateria em PMDs e laptops, e há propostas
de discos que têm um modo de girar a taxas menores quando inativos, para poupar
energia. Infelizmente, você não pode acessar DRAMs ou discos nesses modos, então
deve retornar a um modo totalmente ativo para ler ou gravar, não importa quão
baixa seja a taxa de acesso. Como mencionado, os microprocessadores
para PCs, ao contrário, foram projetados para um caso mais típico de uso pesado
a altas temperaturas de operação, dependendo dos sensores de temperatura
no chip para detectar quando a atividade deve ser automaticamente reduzida
para evitar sobreaquecimento. Essa “redução de velocidade de emergência”
permite aos fabricantes projetar para um caso mais típico e, então, depender desse
mecanismo de segurança se alguém realmente executar programas que consumam
muito mais potência do que é típico.
4. Overclocking. Em 2008, a Intel começou a oferecer o modo Turbo, em que o chip
decide que é seguro rodar a uma taxa maior de clock por um curto período,
possivelmente em alguns poucos núcleos, até que a temperatura comece a subir.
Por exemplo, o Core i7 de 3,3 GHz pode rodar em explosões curtas a 3,6 GHz.
De fato, todos os microprocessadores de maior desempenho a cada ano desde 2008,
indicados na Figura 1.1, ofereceram overclocking temporário de cerca de 10% acima
da taxa de clock nominal. Para código de thread único, esses microprocessadores
podem desligar todos os núcleos, com exceção de um, e rodá-lo a uma taxa
de clock ainda maior. Observe que, enquanto o sistema operacional pode desligar
o modo Turbo, não há notificação, uma vez que ele seja habilitado. Assim,
os programadores podem se surpreender ao ver que seus programas variam
em desempenho devido à temperatura ambiente!
23
24
CAPÍTULO 1 :
Fundamentos do projeto e análise quantitativos
Embora a potência dinâmica seja a principal fonte de dissipação de potência na CMOS,
a potência estática está se tornando uma questão importante, pois a corrente de fuga flui
até mesmo quando um transistor está desligado:
Potênciaestática ∝ Correnteestática × Voltagem
ou seja, a potência estática é proporcional ao número de dispositivos.
Assim, aumentar o número de transistores aumenta a potência, mesmo que eles estejam
inativos, e a corrente de fuga aumenta em processadores com transistores de menor
tamanho. Como resultado, sistemas com muito pouca potência ainda estão passando a
voltagem para módulos inativos, a fim de controlar a perda decorrente da fuga. Em 2011,
a meta para a fuga era de 25% do consumo total de energia, mas a fuga nos projetos de
alto desempenho muitas vezes ultrapassou bastante esse objetivo. A fuga pode ser de até
50% em tais chips, em parte por causa dos maiores caches de SRAM, que precisam de
potência para manter os valores de armazenamento (o “S” em SRAM é de estático — static).
A única esperança de impedir a fuga é desligar a alimentação de subconjuntos dos chips.
Por fim, como o processador é só uma parte do custo total de energia de um sistema,
pode fazer sentido usar um que seja mais rápido e menos eficiente em termos de energia
para permitir ao restante do sistema entrar em modo sleep. Essa estratégia é conhecida
como race-to-halt.
A importância da potência e da energia aumentou o cuidado na avaliação sobre a eficiência
de uma inovação, então agora a avaliação primária inclui as tarefas por joule ou desempenho por watt, ao contrário do desempenho por mm2 de silício. Essa nova métrica afeta as
abordagens do paralelismo, como veremos nos Capítulos 4 e 5.
1.6
TENDÊNCIAS NO CUSTO
Embora existam projetos de computador nos quais os custos costumam ser menos importantes — especificamente, supercomputadores —, projetos sensíveis ao custo tornam-se
primordiais. Na realidade, nos últimos 30 anos, o uso de melhorias tecnológicas para
reduzir o custo, além de aumentar o desempenho, tem sido um tema importante no setor
de computação.
Os livros-texto normalmente ignoram a parte “custo” do par custo-desempenho, porque os
custos mudam, tornando os livros desatualizados e porque essas questões são sutis, diferindo
entre os segmentos do setor. Mesmo assim, ter compreensão do custo e de seus fatores é
essencial para os projetistas tomarem decisões inteligentes quanto a um novo recurso ser ou
não incluído nos projetos em que o custo é importante (imagine os arquitetos projetando
prédios sem qualquer informação sobre os custos das vigas de aço e do concreto!).
Esta seção trata dos principais fatores que influenciam o custo de um computador e o
modo como esses fatores estão mudando com o tempo.
O impacto do tempo, volume e commodities
O custo de um componente de computador manufaturado diminui com o tempo, mesmo sem que haja grandes melhorias na tecnologia de implementação básica. O princípio
básico que faz os custos caírem é a curva de aprendizado — os custos de manufatura diminuem com o tempo. A própria curva de aprendizado é mais bem medida pela mudança
no rendimento — a porcentagem dos dispositivos manufaturados que sobrevivem ao
procedimento de teste. Seja um chip, uma placa, seja um sistema, os projetos que têm o
dobro de rendimento terão a metade do custo.
1.6
Entender como a curva de aprendizado melhora o rendimento é fundamental para proteger
os custos da vida de um produto. Um exemplo disso é que, a longo prazo, o preço por
megabyte da DRAM tem caído. Como as DRAMs costumam ter seu preço relacionado com
o custo — com exceção dos períodos de escassez ou de oferta em demasia —, o preço e o
custo da DRAM andam lado a lado.
Os preços de microprocessadores também caem com o tempo, mas, por serem menos
padronizados que as DRAMs, o relacionamento entre preço e custo é mais complexo.
Em um período de competitividade significativa, o preço costuma acompanhar o custo
mais de perto, embora seja provável que os vendedores de microprocessador quase nunca
tenham perdas.
O volume é o segundo fator importante na determinação do custo. Volumes cada vez
maiores afetam o custo de várias maneiras. Em primeiro lugar, eles reduzem o tempo necessário para diminuir a curva de aprendizado, que é parcialmente proporcional ao número
de sistemas (ou chips) manufaturados. Em segundo lugar, o volume diminui o custo, pois
aumenta a eficiência de compras e manufatura. Alguns projetistas estimaram que o custo
diminui cerca de 10% para cada duplicação do volume. Além do mais, o volume diminui
a quantidade de custo de desenvolvimento que precisa ser amortizada por computador,
permitindo que os preços de custo e de venda sejam mais próximos.
Commodities são produtos essencialmente idênticos vendidos por vários fornecedores em
grandes volumes. Quase todos os produtos ofertados nas prateleiras de supermercados são
commodities, assim como DRAMs, memória Flash, discos, monitores e teclados comuns.
Nos últimos 25 anos, grande parte da ponta inferior do negócio de computador tornou-se
um negócio de commodity, focalizando a montagem de computadores desktop e laptops
que rodam o Microsoft Windows.
Como muitos fornecedores entregam produtos quase idênticos, isso é altamente competitivo. É natural que essa competição diminua a distância entre preço de custo e preço
de venda, mas que também aumente o custo. As reduções ocorrem porque um mercado de
commodity possui volume e clara definição de produto, de modo que vários fornecedores
podem competir pela montagem dos componentes para o produto. Como resultado,
o custo geral desse produto é mais baixo, devido à competição entre os fornecedores
dos componentes e à eficiência de volume que eles podem conseguir. Isso fez com que
o negócio de computador, de produtos finais, fosse capaz de alcançar melhor preçodesempenho do que outros setores e resultou em maior crescimento, embora com lucros
limitados (como é comum em qualquer negócio de commodity).
Custo de um circuito integrado
Por que um livro sobre arquitetura de computadores teria uma seção sobre custos de
circuito integrado? Em um mercado de computadores cada vez mais competitivo, no
qual partes-padrão — discos, memória Flash, DRAMs etc. — estão tornando-se parte
significativa do custo de qualquer sistema, os custos de circuito integrado tornam-se uma
parte maior do custo que varia entre os computadores, especialmente na parte de alto
volume do mercado, sensível ao custo. De fato, com a dependência cada vez maior dos
dispositivos pessoais móveis em relação a sistemas em um chip (Systems On a Chip — SOC)
completos, o custo dos circuitos integrados representa grande parte do custo do PMD.
Assim, os projetistas de computadores precisam entender os custos dos chips para entender
os custos dos computadores atuais.
Embora os custos dos circuitos integrados tenham caído exponencialmente, o processo
básico de manufatura do silício não mudou: um wafer ainda é testado e cortado em dies,
Tendências no custo
25
26
CAPÍTULO 1 :
Fundamentos do projeto e análise quantitativos
que são encapsulados (Figs. 1.13, 1.14 e 1.15). Assim, o custo de um circuito integrado
finalizado é:
Custo do c.i. =
Custo do die + Custo de testar o die + Custo de encapsulamento e teste final
Rendimento de teste final
FIGURA 1.13 Fotografia de um die de microprocessador Intel Core i7, que será avaliado nos Capítulos 2 a 5.
As dimensões são 18,9 mm por 13,6 mm (257 mm2) em um processo de 45 nm (cortesia da Intel).
FIGURA 1.14 Diagrama do die do Core i7 na Figura 1.13, à esquerda, com close-up do diagrama do segundo núcleo, à direita.
1.6
FIGURA 1.15 Esse wafer de 300 mm contém 280 dies Sandy Dridge, cada um com 20,7 por 10,5 mm
em um processo de 32 nm.
(O Sandy Bridge é o sucessor da Intel para o Nehalem usado no Core i7.) Com 216 mm2, a fórmula para dies por wafer
estima 282 (cortesia da Intel).
Nesta seção, focalizamos o custo dos dies, resumindo os principais problemas referentes
ao teste e ao encapsulamento no final.
Aprender a prever o número de chips bons por wafer exige aprender primeiro quantos
dies cabem em um wafer e, depois, como prever a porcentagem deles que funcionará. A
partir disso, é simples prever o custo:
Custo do die =
Custo do wafer
Dies por wafer × Rendimento do die
O recurso mais interessante desse primeiro termo da equação de custo do chip é sua
sensibilidade ao tamanho do die, como veremos a seguir.
O número de dies por wafer é aproximadamente a área do wafer dividida pela área do
die. Ela pode ser estimada com mais precisão por
Dies por wafer =
π × (Diâmetro do wafer / 2)2 π × Diâmetro do wafer
−
Área do die
2 × Área do die
O primeiro termo é a razão da área do wafer (πr2) pela área do die. O segundo compensa
o problema do “encaixe quadrado em um furo redondo” — dies retangulares perto da
periferia de wafers redondos. A divisão da circunferência (πd) pela diagonal de um die
quadrado é aproximadamente o número de dies ao longo da borda.
Tendências no custo
27
28
CAPÍTULO 1 :
Fundamentos do projeto e análise quantitativos
Exemplo
Resposta
Encontre o número de dies por wafer de 300 mm (30 cm) para um die que
possui 1,5 cm em um lado e para um die que possui 1,0 cm em outro.
Quando a área do die é 2,25 cm2:
Diespor wafer =
706,9 94,2
π × (30 / 2)2
π × 30
−
=
=
= 270
2,25
2 × 2,25 2,25 2,12
Uma vez que a área do die maior é 2,25 vezes maior, há aproximadamente
2,25 vezes dies menores por wafer:
Diespor wafer =
706,9 94,2
π × (30 / 2)2
π × 30
−
=
=
= 640
1,00
2 × 1,00 1,00 1,41
Porém, essa fórmula só dá o número máximo de dies por wafer. A questão decisiva é: qual
é a fração de dies bons em um wafer, ou seja, o rendimento de dies? Um modelo simples
de rendimento de circuito integrado que considera que os defeitos são distribuídos aleatoriamente sobre o wafer e esse rendimento é inversamente proporcional à complexidade
do processo de fabricação, leva ao seguinte:
Rendimento do die = Rendimento do wafer × 1 / (1 + Defeitos por unidade de área × Área do die)N
Essa fórmula de Bose-Einstein é um modelo empírico desenvolvido pela observação do
rendimento de muitas linhas de fabricação (Sydow, 2006). O rendimento do wafer considera
os wafers que são completamente defeituosos e, por isso, não precisam ser testados. Para
simplificar, vamos simplesmente considerar que o rendimento do wafer seja de 100%. Os
defeitos por unidade de área são uma medida dos defeitos de fabricação aleatórios que
ocorrem. Em 2010, esse valor normalmente é de 0,1-0,3 defeito por centímetro quadrado,
ou 0,016-0,057 defeito por centímetro quadrado, para um processo de 40 nm, pois isso
depende da maturidade do processo (lembre-se da curva de aprendizado mencionada).
Por fim, N é um parâmetro chamado fator de complexidade da fabricação. Para processos de
40 nm em 2010, N variou de 11,5-15,5.
Exemplo
Resposta
Encontre o rendimento para os dies com 1,5 cm de um lado e 1,0 cm de outro, considerando uma densidade de defeito de 0,031 por cm2 e N de 13,5.
As áreas totais de die são 2,25 cm2 e 1,00 cm2. Para um die maior, o rendimento é:
Rendimentododie = 1 / (1 + 0,031 × 2,25)13,5 = 0,40
Para o die menor, ele é o rendimento do die:
Rendimentododie = 1 / (1 + 0,031 × 1,00)13,5 = 0,66
Ou seja, menos da metade de todo o die grande é bom, porém mais de dois
terços do die pequeno são bons.
O resultado é o número de dies bons por wafer, que vem da multiplicação dos dies por
wafer pelo rendimento do die usado para incorporar os impactos dos defeitos. Os exemplos anteriores preveem cerca de 109 dies bons de 2,25 cm2 e 424 dies bons de 1,00 cm2
no wafer de 300 mm. Muitos microprocessadores se encontram entre esses dois tamanhos.
Os processadores embarcados de 32 bits de nível inferior às vezes possuem até 0,10 cm2,
1.6
e os processadores usados para controle embarcado (em impressoras, automóveis etc.) às
vezes têm menos de 0,04 cm2.
Devido às consideráveis pressões de preço sobre os produtos de commodity, como
DRAM e SRAM, os projetistas incluíram a redundância como um meio de aumentar
o rendimento. Por vários anos, as DRAMs regularmente incluíram algumas células de
memória redundantes, de modo que certo número de falhas possa ser acomodado. Os
projetistas têm usado técnicas semelhantes, tanto em SRAMs padrão quanto em grandes
arrays de SRAM, usados para caches dentro dos microprocessadores. É óbvio que a
presença de entradas redundantes pode ser usada para aumentar significativamente
o rendimento.
O processamento de um wafer de 300 mm (12 polegadas) de diâmetro em tecnologia de
ponta custava US$ 5.000-6.000 em 2010. Considerando um custo de wafer processado
de US$ 5.500, o custo do die de 1,00 cm2 seria em torno de US$ 13, mas o custo por
die de 2,25 cm2 seria cerca de US$ 51, quase quatro vezes o custo para um die com pouco
mais que o dobro do tamanho.
Por que um projetista de computador precisa se lembrar dos custos do chip? O processo
de manufatura dita o custo e o rendimento do wafer, e os defeitos por unidade de área, de
modo que o único controle do projetista é a área do die. Na prática, como o número
de defeitos por unidade de área é pequeno, o número de dies bons por wafer e, portanto,
o custo por die crescem rapidamente, conforme o quadrado da área do die. O projetista
de computador afeta o tamanho do die e, portanto, o custo, tanto pelas funções incluídas
ou excluídas quanto pelo número de pinos de E/S.
Antes que tenhamos uma parte pronta para uso em um computador, os dies precisam ser
testados (para separar os bons dies dos ruins), encapsulados e testados novamente após
o encapsulamento. Esses passos aumentam consideravelmente os custos.
Essa análise focalizou os custos variáveis da produção de um die funcional, que é apropriado para circuitos integrados de alto volume. Porém, existe uma parte muito importante
do custo fixo que pode afetar significativamente o custo de um circuito integrado para
baixos volumes (menos de um milhão de partes), o custo de um conjunto de máscaras.
Cada etapa do processo de circuito integrado requer uma máscara separada. Assim, para
os modernos processos de fabricação de alta densidade, com quatro a seis camadas de
metal, os custos por máscara ultrapassam US$ 1 milhão. Obviamente, esse grande custo
fixo afeta o custo das rodadas de prototipagem e depuração, e — para produção em baixo
volume — pode ser uma parte significativa do custo de produção. Como os custos por
máscara provavelmente continuarão a aumentar, os projetistas podem incorporar a lógica
reconfigurável para melhorar a flexibilidade de uma parte ou decidir usar gate-arrays (que
possuem número menor de níveis de máscara de customização) e, assim, reduzir as implicações de custo das máscaras.
Custo versus preço
Com os computadores se tornando commodities, a margem entre o custo para a manufatura de um produto e o preço pelo qual o produto é vendido tem diminuído. Essa margem
considera a pesquisa e desenvolvimento (P&D), o marketing, as vendas, a manutenção
do equipamento de manufatura, o aluguel do prédio, o custo do financiamento, os lucros pré-taxados e os impostos de uma empresa. Muitos engenheiros ficam surpresos ao
descobrir que a maioria das empresas gasta apenas de 4% (no negócio de PC commodity)
a 12% (no negócio de servidor de alto nível) de sua receita em P&D, que inclui toda a
engenharia.
Tendências no custo
29
30
CAPÍTULO 1 :
Fundamentos do projeto e análise quantitativos
Custo de fabricação versus custo de operação
Nas primeiras quatro edições deste livro, custo queria dizer o valor gasto para construir um
computador e preço significava a quantia para comprar um computador. Com o advento
de computadores em escala warehouse, que contêm dezenas de milhares de servidores, o
custo de operar os computadores é significativo em adição ao custo de compra.
Como o Capítulo 6 mostra, o preço de compra amortizado dos servidores e redes corresponde
a um pouco mais de 60% do custo mensal para operar um computador em escala warehouse,
supondo um tempo de vista curto do equipamento de TI de 3-4 anos. Cerca de 30% dos custos
operacionais mensais têm relação com o uso de energia e a infraestrutura amortizada para
distribuí-la e resfriar o equipamento de TI, apesar de essa infraestrutura ser amortizada ao
longo de 10 anos. Assim, para reduzir os custos em um computador em escala de warehouse,
os arquitetos de computadores precisam usar a energia com eficiência.
1.7
DEPENDÊNCIA
Historicamente, os circuitos integrados sempre foram um dos componentes mais confiáveis
de um computador. Embora seus pinos fossem vulneráveis e pudesse haver falhas nos
canais de comunicação, a taxa de erro dentro do chip era muito baixa. Mas essa sabedoria
convencional está mudando à medida que chegamos a tamanhos de 32 nm e ainda
menores: falhas transientes e permanentes se tornarão mais comuns, de modo que os
arquitetos precisarão projetar sistemas para lidar com esses desafios. Esta seção apresenta
um rápido panorama dessas questões de dependência, deixando a definição oficial dos
termos e das técnicas para a Seção D.3 do Apêndice D.
Os computadores são projetados e construídos em diferentes camadas de abstração.
Podemos descer recursivamente por um computador, vendo os componentes se ampliarem para subsistemas completos até nos depararmos com os transistores individuais.
Embora algumas falhas, como a falta de energia, sejam generalizadas, muitas podem ser
limitadas a um único componente em um módulo. Assim, a falha pronunciada de um
módulo em um nível pode ser considerada meramente um erro de componente em
um módulo de nível superior. Essa distinção é útil na tentativa de encontrar maneiras de
montar computadores confiáveis.
Uma questão difícil é decidir quando um sistema está operando corretamente. Esse ponto
filosófico tornou-se concreto com a popularidade dos serviços de internet. Os provedores
de infraestrutura começaram a oferecer Service Level Agreements (SLA) ou Service Level
Objectives (SLO) para garantir que seu serviço de rede ou energia fosse confiável. Por
exemplo, eles pagariam ao cliente uma multa se não cumprissem um acordo por mais de
algumas horas por mês. Assim, um SLA poderia ser usado para decidir se o sistema estava
ativo ou inativo.
Os sistemas alternam dois status de serviço com relação a um SLA:
1. Realização do serviço, em que o serviço é entregue conforme o que foi especificado.
2. Interrupção de serviço, em que o serviço entregue é diferente do SLA.
As transições entre esses dois status são causadas por falhas (do status 1 para o 2) ou restaurações (do status 2 para o 1). Quantificar essas transições leva às duas principais medidas
de dependência:
j
Confiabilidade do módulo é uma medida da realização contínua do serviço (ou,
de forma equivalente, do tempo para a falha) de um instante inicial de referência.
Logo, o tempo médio para a falha (Mean Time To Failure — MTTF) é uma medida
1.7
j
de confiabilidade. O recíproco do MTTF é uma taxa de falhas, geralmente informada
como falhas por bilhão de horas de operação ou falhas em tempo (Failures In Time
— FIT). Assim, um MTTF de 1.000.000 de horas é igual a 109/106 ou 1.000 FIT.
A interrupção do serviço é medida como tempo médio para o reparo (Mean Time
To Repair — MTTR). O tempo médio entre as falhas (Mean Time Between Failures —
MTBF) é simplesmente a soma MTTF + MTTR. Embora o MTBF seja bastante usado,
normalmente o MTTF é o termo mais apropriado. Se uma coleção de módulos tiver
tempos de vida distribuídos exponencialmente — significando que a idade de um
módulo não é importante na probabilidade de falha —, a taxa de falha geral
do conjunto é a soma das taxas de falha dos módulos.
Disponibilidade do módulo é uma medida da realização do serviço com relação
à alternância de dois status de realização e interrupção. Para sistemas não
redundantes com reparo, a disponibilidade do módulo é
Disponibilidade do módulo =
MTTF
(MTTF + MTTR)
Observe que agora a confiabilidade e a disponibilidade são medições quantificáveis,
em vez de sinônimos de dependência. A partir dessas definições, podemos estimar
a confiabilidade de um sistema quantitativamente se fizermos algumas suposições sobre a
confiabilidade dos componentes e se essas falhas forem independentes.
Exemplo
Resposta
Considere um subsistema de disco com os seguintes componentes
e MTTF:
j
10 discos, cada qual classificado em 1.000.000 horas de MTTF
j
1 controladora SCSI, 500.000 horas de MTTF
j
1 fonte de alimentação, 200.000 horas de MTTF
j
1 ventilador, 200.000 horas de MTTF
j
1 cabo SCSI, 1.000.000 de horas de MTTF
Usando as suposições simplificadas de que os tempos de vida são distribuídos exponencialmente e de que as falhas são independentes, calcule
o MTTF do sistema como um todo.
A soma das taxas de falha é:
1
1
1
1
1
+
+
+
+
1.000.000 500.000 200.000 200.000 1.000.000
10 + 2 + 5 + 5 + 1
23
23.000
=
+
+
1.000.000 horas 1.000.000 1.000.000.000 horas
Taxa de falhasistema = 10 ×
ou 23.000 FIT. O MTTF para o sistema é exatamente o inverso da taxa de
falha:
MTTFsistema =
1
1.000.000.000 horas
=
= 43.500 horas
Taxa de falhasistema
23.000
ou pouco menos de cinco anos.
A principal maneira de lidar com a falha é a redundância, seja em tempo (repita a operação
para ver se ainda está com erro), seja em recursos (tenha outros componentes para utilizar
no lugar daquele que falhou). Quando o componente é substituído e o sistema totalmente
reparado, a dependência do sistema é considerada tão boa quanto nova. Vamos quantificar
os benefícios da redundância com um exemplo.
Dependência
31
32
CAPÍTULO 1 :
Fundamentos do projeto e análise quantitativos
Exemplo
Resposta
Os subsistemas de disco normalmente possuem fontes de alimentação
redundantes para melhorar a sua pendência. Usando os componentes e
MTTFs do exemplo anterior, calcule a confiabilidade de uma fonte de alimentação redundante. Considere que uma fonte de alimentação é suficiente
para alimentar o subsistema de disco e que estamos incluindo uma fonte de
alimentação redundante.
Precisamos de uma fórmula para mostrar o que esperar quando podemos
tolerar uma falha e ainda oferecer serviço. Para simplificar os cálculos,
consideramos que os tempos de vida dos componentes são distribuídos exponencialmente e que não existe dependência entre as falhas de componente.
O MTTF para nossas fontes de alimentação redundantes é o tempo médio
até que uma fonte de alimentação falhe dividido pela chance de que a outra
falhará antes que a primeira seja substituída. Assim, se a possibilidade de
uma segunda falha antes do reparo for pequena, o MTTF do par será grande.
Como temos duas fontes de alimentação e falhas independentes, o tempo
médio até que um disco falhe é MTTFfonte de alimentação/2. Uma boa aproximação da probabilidade de uma segunda falha é MTTR sobre o tempo médio
até que a outra fonte de alimentação falhe. Logo, uma aproximação razoável
para um par redundante de fontes de alimentação é
MTTFpar de fontesde alimentação =
MTTFfonte de alimentação / 2 MTTF2 fonte de alimentação / 2
MTTF2 fonte de alimentação
=
=
MTTRfonte de alimentação
MTTRfonte de alimentação
2 × MTTRfonte de alimentação
MTTFfonte de alimentação
Usando os números de MTTF anteriores, se considerarmos que são necessárias em média 24 horas para um operador humano notar que uma fonte
de alimentação falhou e substituí-la, a confiabilidade do par de fontes de
alimentação tolerante a falhas é
MTTFpar de fontesde alimentação =
MTTF2 fonte de alimentação
200.0002
=
≅ 830.000.000
2 × MTTRfonte de alimentação
2 × 24
tornando o par cerca de 4.150 vezes mais confiável do que uma única fonte
de alimentação.
Tendo quantificado o custo, a alimentação e a dependência da tecnologia de computadores,
estamos prontos para quantificar o desempenho.
1.8
MEDIÇÃO, RELATÓRIO E RESUMO DO DESEMPENHO
Quando dizemos que um computador é mais rápido do que outro, o que isso significa?
O usuário de um computador desktop pode afirmar que um computador é mais rápido
quando um programa roda em menos tempo, enquanto um administrador do Amazon.
com pode dizer que um computador é mais rápido quando completa mais transações por
hora. O usuário do computador está interessado em reduzir o tempo de resposta — o tempo
entre o início e o término de um evento —, também conhecido como tempo de execução.
O administrador de um grande centro de processamento de dados pode estar interessado
em aumentar o throughput — a quantidade total de trabalho feito em determinado tempo.
Comparando as alternativas de projeto, normalmente queremos relacionar o desempenho de dois computadores diferentes, digamos, X e Y. A frase “X é mais rápido do que Y”
é usada aqui para significar que o tempo de resposta ou o tempo de execução é inferior
em X em relação a Y para determinada tarefa. Em particular, “X é n vezes mais rápido do
que Y” significará:
Tempo de execuçãoY
=n
Tempo de execuçãoX
1.8
Medição, relatório e resumo do desempenho
Como o tempo de execução é o recíproco do desempenho, existe o seguinte relacionamento:
1
Tempo de execuçãoY DesempenhoY DesempenhoX
=
=
n=
1
DesempenhoY
Tempo de execuçãoX
DesempenhoX
A frase “O throughput de X é 1,3 vez maior que Y” significa que o número de tarefas completadas por unidade de tempo no computador X é 1,3 vez o número completado em Y.
Infelizmente, o tempo nem sempre é a métrica cotada em comparação com o desempenho
dos computadores. Nossa posição é de que a única medida consistente e confiável do
desempenho é o tempo de execução dos programas reais, e todas as alternativas propostas
para o tempo como medida ou aos programas reais como itens medidos por fim levaram
a afirmações enganosas ou até mesmo a erros no projeto do computador.
Até mesmo o tempo de execução pode ser definido de diferentes maneiras, dependendo
do que nós contamos. A definição mais direta do tempo é chamada tempo de relógio de
parede, tempo de resposta ou tempo decorrido, que é a latência para concluir uma tarefa, incluindo acessos ao disco e à memória, atividades de entrada/saída, overhead do sistema
operacional — tudo. Com a multiprogramação, o processador trabalha em outro programa
enquanto espera pela E/S e pode não minimizar necessariamente o tempo decorrido de
um programa. Logo, precisamos de um termo para considerar essa atividade. O tempo
de CPU reconhece essa distinção e significa o tempo que o processador está computando, não
incluindo o tempo esperando por E/S ou executando outros programas (é claro que o tempo
de resposta visto pelo usuário é o tempo decorrido do programa, e não o tempo de CPU).
Os usuários que executam rotineiramente os mesmos programas seriam os candidatos
perfeitos para avaliar um novo computador. Para fazer isso, os usuários simplesmente
comparariam o tempo de execução de suas cargas de trabalho — a mistura de programas e
comandos do sistema operacional que os usuários executam em um computador. Porém,
poucos estão nessa situação feliz. A maioria precisa contar com outros métodos para avaliar
computadores — e normalmente outros avaliadores —, esperando que esses métodos
prevejam o desempenho para o uso do novo computador.
Benchmarks
A melhor escolha de benchmarks para medir o desempenho refere-se a aplicações reais,
como o Google Goggles da Seção 1.1. As tentativas de executar programas muito mais
simples do que uma aplicação real levaram a armadilhas de desempenho. Alguns exemplos são:
j
j
j
kernels, que são pequenas partes-chave das aplicações reais
programas de brinquedo, que são programas de 100 linhas das primeiras tarefas de
programação, como o quicksort
benchmarks sintéticos, que são programas inventados para tentar combinar o perfil e
o comportamento de aplicações reais, como o Dhrystone
Hoje, os três estão desacreditados, porque o projetista/arquiteto do compilador pode conspirar para fazer com que o computador pareça mais rápido nesses programas do que em
aplicações reais. Infelizmente para os autores deste livro, que, na quarta edição, derrubaram
a falácia sobre usar programas sintéticos para caracterizar o desempenho achando que
os arquitetos de programas concordavam que ela era indiscutível, o programa sintético
Dhrystone ainda é o benchmark mais mencionado para processadores embarcados!
33
34
CAPÍTULO 1 :
Fundamentos do projeto e análise quantitativos
Outra questão está relacionada com as condições nas quais os benchmarks são executados.
Um modo de melhorar o desempenho de um benchmark tem sido usar flags específicos de
benchmark. Esses flags normalmente causavam transformações que seriam ilegais em muitos
programas ou prejudicariam o desempenho de outros. Para restringir esse processo e aumentar a
significância dos resultados, os desenvolvedores de benchmark normalmente exigem que o vendedor use um compilador e um conjunto de flags para todos os programas na mesma linguagem
(C ou C++). Além dos flags (opções) do compilador, outra questão é a que se refere à permissão
das modificações do código-fonte. Existem três técnicas diferentes para resolver essa questão:
1. Nenhuma modificação do código-fonte é permitida.
2. As modificações do código-fonte são permitidas, mas basicamente impossíveis.
Por exemplo, os benchmarks de banco de dados contam com programas de
banco de dados padrão, que possuem dezenas de milhões de linhas de código. As
empresas de banco de dados provavelmente não farão mudanças para melhorar o
desempenho de determinado computador.
3. Modificações de fonte são permitidas, desde que a versão modificada produza a
mesma saída.
O principal problema que os projetistas de benchmark enfrentam ao permitir a modificação do fonte é se ela refletirá a prática real e oferecerá ideias úteis aos usuários ou se
simplesmente reduzirá a precisão dos benchmarks como previsões do desempenho real.
Para contornar o risco de “colocar muitos ovos em uma cesta”, séries de aplicações de benchmark, chamadas pacotes de benchmark, são uma medida popular do desempenho dos processadores com uma variedade de aplicações. Naturalmente, esses pacotes são tão bons quanto os
benchmarks individuais constituintes. Apesar disso, uma vantagem importante desses pacotes
é que o ponto fraco de qualquer benchmark é reduzido pela presença de outros benchmarks. O
objetivo de um pacote desse tipo é caracterizar o desempenho relativo dos dois computadores,
particularmente para programas não incluídos, que os clientes provavelmente usarão.
Para dar um exemplo cauteloso, o EDN Embedded Microprocessor Benchmark Consortium
(ou EEMBC) é um conjunto de 41 kernels usados para prever o desempenho de diferentes
aplicações embarcadas: automotiva/industrial, consumidor, redes, automação de escritórios
e telecomunicações. O EEMBC informa o desempenho não modificado e o desempenho
“fúria total”, em que quase tudo entra. Por utilizar kernels, e devido às operações de relacionamento, o EEMBC não tem a reputação de ser uma boa previsão de desempenho relativo
de diferentes computadores embarcados em campo. O programa sintético Dhrystone, que
o EEMBC estava tentando substituir, ainda é relatado em alguns círculos embarcados.
Uma das tentativas mais bem-sucedidas para criar pacotes de aplicação de benchmark padronizadas foi a SPEC (Standard Performance Evaluation Corporation), que teve suas raízes
nos esforços do final da década de 1980 para oferecer melhores benchmarks para estações
de trabalho. Assim como o setor de computador tem evoluído com o tempo, também
evoluiu a necessidade de diferentes pacotes de benchmark — hoje existem benchmarks
SPEC para abranger diferentes classes de aplicação. Todos os pacotes de benchmark SPEC
e seus resultados relatados são encontrados em <www.spec.org>.
Embora o enfoque de nossa análise seja nos benchmarks SPEC em várias das seções
seguintes, também existem muitos benchmarks desenvolvidos para PCs rodando o sistema operacional Windows.
Benchmarks de desktop
Os benchmarks de desktop são divididos em duas classes amplas: benchmarks com uso
intensivo do processador e benchmarks com uso intensivo de gráficos, embora muitos
1.8
Medição, relatório e resumo do desempenho
benchmarks gráficos incluam atividade intensa do processador. Originalmente, a SPEC
criou um conjunto de benchmarks enfocando o desempenho do processador (inicialmente
chamado SPEC89), que evoluiu para sua quinta geração: SPEC CPU2006, que vem após
SPEC2000, SPEC95, SPEC92 e SPEC89. O SPEC CPU2006 consiste em um conjunto de
12 benchmarks inteiros (CINT2006) e 17 benchmarks de ponto flutuante (CFP2006).
A Figura 1.16 descreve os benchmarks SPEC atuais e seus ancestrais.
FIGURA 1.16 Programas do SPEC2006 e a evolução dos benchmarks SPEC com o tempo, com programas inteiros na parte superior
e programas de ponto flutuante na parte inferior.
Dos 12 programas inteiros do SPEC2006, nove são escritos em C e o restante em C++. Para os programas de ponto flutuante, a composição é de seis
em FORTRAN, quatro em C++, três em C e quatro misturados entre C e Fortran. A figura mostra os 70 programas nas versões de 1989, 1992, 1995,
2000 e 2006. As descrições de benchmark, à esquerda, são apenas para o SPEC2006 e não se aplicam às anteriores. Os programas na mesma linha
de diferentes gerações do SPEC geralmente não estão relacionados; por exemplo, fpppp não é o código CFD como bwaves. Gcc é o mais antigo do grupo.
Somente três programas inteiros e três programas de ponto flutuante são novos para o SPEC2006. Embora alguns sejam levados de uma geração
para outra, a versão do programa muda e a entrada ou o tamanho do benchmark normalmente é alterado para aumentar seu tempo de execução
e evitar perturbação na medição ou domínio do tempo de execução por algum fator diferente do tempo de CPU.
35
36
CAPÍTULO 1 :
Fundamentos do projeto e análise quantitativos
Os benchmarks SPEC são programas reais modificados para serem portáveis e minimizar o
efeito da E/S sobre o desempenho. Os benchmarks inteiros variam de parte de um compilador
C até um programa de xadrez ou 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ícula para dinâmica molecular e códigos de álgebra linear
dispersa para dinâmica de fluidos. O pacote SPEC CPU é útil para o benchmarking de processador para sistemas de desktop e servidores de único processador. Veremos os dados sobre
muitos desses programas no decorrer deste capítulo. Entretanto, observe que esses programas
compartilham pouco com linguagens de programação e ambientes e o Google Goggles, que
a Seção 1.1 descreve. O sete usa C++, o oito usa C e o nove usa Fortran! Eles estão até ligados
estatisticamente, e os próprios aplicativos são simples. Não está claro que o SPENCINT2006
e o SPECFP2006 capturam o que é excitante sobre a computação no século XXI.
Na Seção 1.11, descrevemos as armadilhas que têm ocorrido no desenvolvimento do pacote
de benchmark SPEC, além dos desafios na manutenção de um pacote de benchmark útil
e previsível.
O SPEC CPU2006 visa ao desempenho do processador, mas o SPEC oferece muitos outros
benchmarks.
Benchmarks de servidor
Assim como os servidores possuem funções múltiplas, também existem múltiplos tipos
de benchmark. O benchmark mais simples talvez seja aquele orientado a throughput do
processador. O SPEC CPU2000 usa os benchmarks SPEC CPU para construir um benchmark de throughput simples, em que a taxa de processamento de um multiprocessador
pode ser medida pela execução de várias cópias (normalmente tantas quanto os processadores) de cada benchmark SPEC CPU e pela conversão do tempo de CPU em uma taxa.
Isso leva a uma medida chamada SPECrate, que é uma medida de paralelismo em nível
de requisição, da Seção 1.2. Para medir o paralelismo de nível de thread, o SPEC oferece o
que chamamos benchmarks de computação de alto desempenho com o OpenMP e o MPI.
Além da SPECrate, a maioria das aplicações de servidor e benchmarks possui atividade significativa de E/S vinda do disco ou do tráfego de rede, incluindo benchmarks para sistemas
de servidor de arquivos, para servidores Web e para sistemas de banco de dados e processamento de transação. O SPEC oferece um benchmark de servidor de arquivos (SPECSFS)
e um benchmark de servidor Web (SPECWeb). O SPECSFS é um benchmark para medir o
desempenho do NFS (Network File System) usando um script de solicitações ao servidor
de arquivos; ele testa o desempenho do sistema de E/S (tanto E/S de disco quanto de
rede), além do processador. O SPECSFS é um benchmark orientado a throughput, mas
com requisitos importantes de tempo de resposta (o Apêndice D tratará detalhadamente
de alguns benchmarks de arquivo e do sistema de E/S). O SPECWeb é um benchmark de
servidor Web que simula vários clientes solicitando páginas estáticas e dinâmicas de um
servidor, além dos clientes postando dados no servidor. O SPECjbb mede o desempenho
do servidor para aplicativos Web escritos em Java. O benchmark SPEC mais recente é o
SPECvirt_Sc2010, que avalia o desempenho end-to-end de servidores virtualizados de data
center incluindo hardware, a camada de máquina virtual e o sistema operacional virtualizado. Outro benchmark SPEC recente mede a potência, que examinaremos na Seção 1.10.
Os benchmarks de processamento de transação (Transaction-Processing — TP) medem a
capacidade de um sistema para lidar com transações, que consistem em acessos e atualizações de banco de dados. Os sistemas de reserva aérea e os sistemas de terminal bancário
são exemplos simples típicos de TP; sistemas de TP mais sofisticados envolvem bancos
de dados complexos e tomada de decisão. Em meados da década de 1980, um grupo de
1.8
Medição, relatório e resumo do desempenho
engenheiros interessados formou o Transaction Processing Council (TPC) independente
de fornecedor, para tentar criar benchmarks realistas e imparciais para TP. Os benchmarks
do TPC são descritos em <www.tpc.org>.
O primeiro benchmark TPC, TPC-A, foi publicado em 1985 e desde então tem sido substituído e aprimorado por vários benchmarks diferentes. O TPC-C, criado inicialmente em
1992, simula um ambiente de consulta complexo. O TPC-H molda o suporte à decisão
ocasional — as consultas não são relacionadas e o conhecimento de consultas passadas
não pode ser usado para otimizar consultas futuras. O TCP-E é uma nova carga de trabalho de processamento de transação on-line (On-Line Transaction Processing — OLTP)
que simula as contas dos clientes de uma firma de corretagem. O esforço mais recente é
o TPC Energy, que adiciona métricas de energia a todos os benchmarks TPC existentes.
Todos os benchmarks TPC medem o desempenho em transações por segundo. Além disso,
incluem um requisito de tempo de resposta, de modo que o desempenho do throughput
é medido apenas quando o limite de tempo de resposta é atendido. Para modelar sistemas do mundo real, taxas de transação mais altas também estão associadas a sistemas
maiores, em termos de usuários e do banco de dados ao qual as transações são aplicadas.
Finalmente, cabe incluir o custo do sistema para um sistema de benchmark, permitindo
comparações precisas de custo-desempenho. O TPC modificou sua política de preços
para que exista uma única especificação para todos os benchmarks TPC e para permitir a
verificação dos preços que a TPC publica.
Reportando resultados de desempenho
O princípio orientador dos relatórios das medições de desempenho deve ser a propriedade de
serem reproduzíveis — listar tudo aquilo de que outro experimentador precisaria para duplicar
os resultados. Um relatório de benchmark SPEC exige uma descrição extensa do computador
e dos flags do compilador, além da publicação da linha de referência e dos resultados otimizados. Além das descrições de parâmetros de ajuste de hardware, software e linha de referência,
um relatório SPEC contém os tempos de desempenho reais, mostrados tanto em formato de
tabulação quanto como gráfico. Um relatório de benchmark TPC é ainda mais completo, pois
precisa incluir resultados de uma auditoria de benchmarking e informação de custo. Esses
relatórios são excelentes fontes para encontrar o custo real dos sistemas de computação, pois
os fabricantes competem em alto desempenho e no fator custo-desempenho.
Resumindo resultados do desempenho
No projeto prático do computador, você precisa avaliar milhares de opções de projeto por
seus benefícios quantitativos em um pacote de benchmarks que acredita ser relevante.
Da mesma forma, os consumidores que tentam escolher um computador contarão com
medidas de desempenho dos benchmarks, que esperam ser semelhantes às aplicações do
usuário. Nos dois casos é útil ter medições para um pacote de benchmarks de modo que
o desempenho das aplicações importantes seja semelhante ao de um ou mais benchmarks
desse pacote e que a variabilidade no desempenho possa ser compreendida. No caso ideal,
o pacote se parece com uma amostra estatisticamente válida do espaço da aplicação, mas
requer mais benchmarks do que normalmente são encontrados na maioria dos pacotes,
exigindo uma amostragem aleatória que quase nenhum pacote de benchmark utiliza.
Depois que escolhermos medir o desempenho com um pacote de benchmark, gostaríamos
de poder resumir os resultados desse desempenho em um único número. Uma técnica
simples para o cálculo de um resultado resumido seria comparar as médias aritméticas dos
tempos de execução dos programas no pacote. Infelizmente, alguns programas SPEC gastam
quatro vezes mais tempo do que outros, de modo que esses programas seriam muito mais
37
38
CAPÍTULO 1 :
Fundamentos do projeto e análise quantitativos
importantes se a média aritmética fosse o único número utilizado para resumir o desempenho. Uma alternativa seria acrescentar um fator de peso a cada benchmark e usar a média
aritmética ponderada como único número para resumir o desempenho. O problema seria,
então, como selecionar os pesos. Como a SPEC é um consórcio de empresas concorrentes,
cada empresa poderia ter seu próprio conjunto favorito de pesos, o que tornaria difícil
chegar a um consenso. Uma solução é usar pesos que façam com que todos os programas
executem por um mesmo tempo em algum computador de referência, mas isso favorece
os resultados para as características de desempenho do computador de referência.
Em vez de selecionar pesos, poderíamos normalizar os tempos de execução para um
computador de referência, dividindo o tempo no computador de referência pelo tempo
no computador que está sendo avaliado, gerando uma razão proporcional ao desempenho. O SPEC utiliza essa técnica, chamando a razão de SPECRatio. Ele possui uma propriedade particularmente útil, que combina o modo como comparamos o desempenho
do computador no decorrer deste capítulo, ou seja, comparando razão de desempenho.
Por exemplo, suponha que o SPECRatio do computador A em um benchmark tenha sido
1,25 vez maior que o do computador B; então, você saberia que:
Tempo de execuçãoreferência
Tempo de execuçãoB DesempenhoA
SPECRatioA
Tempo de execuçãoA
1,25 =
=
=
=
SPECRatioB Tempo de execuçãoreferência Tempo de execuçãoA DesempenhoB
Tempo de execuçãoB
Observe que os tempos de execução no computador de referência caem e a escolha do
computador de referência é irrelevante quando as comparações são feitas como uma razão,
que é a técnica que utilizamos coerentemente. A Figura 1.17 apresenta um exemplo.
FIGURA 1.17 Tempos de execução do SPECfp2000 (em segundos) para o Sun Ultra 5 — o computador de referência do SPEC2000 —
e tempos de execução e SPECRatios para o AMD Opteron e Intel Itanium 2.
(O SPEC2000 multiplica a razão dos tempos de execução por 100 para remover as casas decimais do resultado, de modo que 20,86 é informado
como 2.086.) As duas últimas colunas mostram as razões dos tempos de execução e SPECRatios. Esta figura demonstra a irrelevância do computador
de referência no desempenho relativo. A razão dos tempos de execução é idêntica à razão dos SPECRatios, e a razão da média geométrica
(27,12/20,86 = 1,30) é idêntica à média geométrica das razões (1,3).
1.9
Princípios quantitativos do projeto de computadores
Como um SPECRatio é uma razão, e não um tempo de execução absoluto, a média precisa
ser calculada usando a média geométrica (como os SPECRatios não possuem unidades, a
comparação de SPECRatios aritmeticamente não tem sentido). A fórmula é:
n
Medida geométrica = n ∏ amostra 1
i =1
No caso do SPEC, amostrai é o SPECRatio para o programa i. O uso da média geométrica
garante duas propriedades importantes:
1. A média geométrica das razões é igual à razão das médias geométricas.
2. A razão das médias geométricas é igual à média geométrica das razões de
desempenho, o que implica que a escolha do computador de referência é
irrelevante.
Logo, as motivações para usar a média geométrica são substanciais, especialmente quando
usamos razões de desempenho para fazer comparações.
Exemplo
Resposta
Mostre que a razão das médias geométricas é igual à média geométrica das
razões de desempenho e que a comunicação de referência do SPECRatio
não importa.
Considere dois computadores, A e B, e um conjunto de SPECRatios para
cada um.
n
n
Medida geométricaA
=
Medida geométricaB
∏ SPECRatio A
i
n
n
n
SPECRatio Ai
i =1 SPECRatio Bi
=n∏
i =1
∏ SPECRatio B
i
i =1
Tempo de execuçãoreferênciai
n
n
Tempo de execuçãoBi
DesempenhoAi
Tempo de execuçãoAi
=n∏
=n∏
=n∏
referênciai
Tempo
de
execução
i =1
i =1 Tempo de execuçãoAi
i =1 DesempenhoBi
Tempo de execuçãoBi
n
Ou seja, a razão das médias geométricas dos SPECRatios de A e B é a média
geométrica das razões de desempenho de A para B de todos os benchmarks
no pacote. A Figura 1.17 demonstra a validade usando exemplos da SPEC.
1.9 PRINCÍPIOS QUANTITATIVOS DO PROJETO
DE COMPUTADORES
Agora que vimos como definir, medir e resumir desempenho, custo, dependência e potência, podemos explorar orientações e princípios que são úteis no projeto e na análise de
computadores. Esta seção introduz observações importantes sobre projeto, além de duas
equações para avaliar alternativas.
Tire proveito do paralelismo
Tirar proveito do paralelismo é um dos métodos mais importantes para melhorar o
desempenho. Cada capítulo deste livro apresenta um exemplo de como o desempenho é
melhorado por meio da exploração do paralelismo. Oferecemos três exemplos rápidos,
que serão tratados mais amplamente em outros capítulos.
Nosso primeiro exemplo é o uso do paralelismo em nível do sistema. Para melhorar o
desempenho de throughput em um benchmark de servidor típico, como SPECWeb ou
TPC-C, vários processadores e múltiplos discos podem ser usados. A carga de trabalho
39
40
CAPÍTULO 1 :
Fundamentos do projeto e análise quantitativos
de tratar com solicitações pode, portanto, ser distribuída entre os processadores e discos,
resultando em um throughput melhorado. Ser capaz de expandir a memória e o número
de processadores e discos é o que chamamos escalabilidade, e constitui um bem valioso para
os servidores. A disrtribuição dos dados por vários discos para leituras e gravações paralelas
habilita o paralelismo em nível de dados. O SPECWeb também depende do paralelismo em
nível de requisição para usar muitos processadores, enquanto o TPC-C usa paralelismo
em nível de thread para o processamento mais rápido de pesquisas em bases de dados.
Em nível de um processador individual, tirar proveito do paralelismo entre as instruções é
crucial para conseguir alto desempenho. Uma das maneiras mais simples de fazer isso é por
meio do pipelining (isso será explicado com detalhes no Apêndice C, e é o foco principal do
Capítulo 3). A ideia básica por trás do pipelining é sobrepor a execução da instrução para
reduzir o tempo total a fim de completar uma sequência de instruções. Um insight importante
que permite que o pipelining funcione é que nem toda instrução depende do seu predecessor
imediato; portanto, executar as instruções completa ou parcialmente em paralelo é possível.
O pipelining é o exemplo mais conhecido de paralelismo em nível de instrução.
O paralelismo também pode ser explorado no nível de projeto digital detalhado. Por
exemplo, as caches associadas por conjunto utilizam vários bancos de memória que
normalmente são pesquisados em paralelo para se encontrar um item desejado. As
unidades lógica e aritmética (Arithmetic-Logical Units — ALUs) modernas utilizam
carry-lookahead, que usa o paralelismo para acelerar o processo de cálculo de somas de
linear para logarítmico no número de bits por operando. Esses são mais exemplos
de paralelismo em nível de dados.
Princípio de localidade
Observações fundamentais importantes vêm das propriedades dos programas. A propriedade dos programas mais importante que exploramos regularmente é o princípio de localidade:
os programas costumam reutilizar dados e instruções que usaram recentemente. Uma regra
prática bastante aceita é de que um programa gasta 90% de seu tempo de execução em
apenas 10% do código. Uma aplicação desse princípio é a possibilidade de prever com
razoável precisão as instruções e os dados que um programa usará num futuro próximo
com base em seus acessos num passado recente. O princípio de localidade também se
aplica aos acessos a dados, embora não tão fortemente quanto aos acessos ao código.
Dois tipos diferentes de localidade têm sido observados. No tocante à localidade temporal, é
provável que os itens acessados recentemente sejam acessados num futuro próximo. A localidade espacial afirma que os itens cujos endereços estão próximos um do outro costumam ser
referenciados em curto espaço de tempo. Veremos esses princípios aplicados no Capítulo 2.
Foco no caso comum
Talvez o princípio mais importante e penetrante do projeto de computador seja focar no
caso comum: ao fazer uma escolha de projeto, favoreça o caso frequente em vez do caso
pouco frequente. Esse princípio se aplica à determinação de como gastar recursos, pois o
impacto da melhoria será mais alto se a ocorrência for frequente.
Focar no caso comum funciona tanto para a potência como para os recursos de alocação
e desempenho. As unidades de busca e decodificação de instruções de um processador
pode ser usada com muito mais frequência do que um multiplicador, por isso deve ser
otimizada primeiro. Isso também funciona na dependência. Se um servidor de banco
de dados possui 50 discos para cada processador, como na próxima seção, a dependência de armazenamento dominará a dependência do sistema.
1.9
Princípios quantitativos do projeto de computadores
Além disso, o caso frequente normalmente é mais simples e pode ser feito com mais rapidez do que o caso pouco frequente. Por exemplo, ao somar dois números no processador,
podemos esperar que o estouro (overflow) seja uma circunstância rara e, assim, podemos
melhorar o desempenho, otimizando o caso mais comum, ou seja, sem nenhum estouro.
Isso pode atrasar o caso em que ocorre estouro, mas, se isso for raro, o desempenho geral
será melhorado, otimizando o processador para o caso normal.
Veremos muitos casos desse princípio em todo este capítulo. Na aplicação desse princípio
simples, temos que decidir qual é o caso frequente e quanto desempenho pode ser melhorado tornando-o mais rápido. Uma lei fundamental, chamada lei de Amdahl, pode ser
usada para quantificar esse princípio.
Lei de Amdahl
O ganho de desempenho obtido com a melhoria de alguma parte de um computador pode
ser calculado usando a lei de Amdahl. Essa lei estabelece que a melhoria de desempenho
a ser conseguida com o uso de algum modo de execução mais rápido é limitada pela fração
de tempo que o modo mais rápido pode ser usado.
A lei de Amdahl define o ganho de velocidade, que pode ser obtido usando-se um recurso
em particular. O que é ganho de velocidade? Suponha que possamos fazer uma melhoria em um computador que aumentará seu desempenho quando ele for usado. O ganho
de velocidade é a razão:
Ganho de velocidade =
Desempenho para a tarefa inteira usando a melhoria quando possível
Desempenho para a tarefa inteira sem usar a melhoria
Como alternativa,
Ganho de velocidade =
Desempenho para a tarefa inteira sem usar a melhoria
Desempenho para a tarefa inteira usando a melhoria quando possível
O ganho de velocidade nos diz quão mais rápido uma tarefa rodará usando o computador
com a melhoria em vez do computador original.
A lei de Amdahl nos dá um modo rápido de obter o ganho de velocidade a partir de alguma
melhoria, o que depende de dois fatores:
1. A fração do tempo de computação no computador original que pode ser convertida para
tirar proveito da melhoria. Por exemplo, se 20 segundos do tempo de execução de um
programa que leva 60 segundos no total puderem usar uma melhoria, a fração será
20/60. Esse valor, que chamaremos de Fraçãomelhorada, será sempre menor ou igual a 1.
2. A melhoria obtida pelo modo de execução melhorado, ou seja, quão mais rápido a tarefa
seria executada se o modo melhorado fosse usado para o programa inteiro. Esse valor
é o tempo do modo original sobre o tempo do modo melhorado. Se o modo
melhorado levar, digamos, 2 segundos para uma parte do programa, enquanto
é de 5 segundos no modo original, a melhoria será de 5/2. Chamaremos esse valor,
que é sempre maior que 1, de Ganho de velocidademelhorado.
O tempo de execução usando o computador original com o modo melhorado será o
tempo gasto usando a parte não melhorada do computador mais o tempo gasto usando
a melhoria:
Tempo de execuçãonovo = Tempo de execuçãoantigo × (1 − Fraçãomelhorada ) +
Fraçãomelhorada
Ganho de velocidademelhorado
41
42
CAPÍTULO 1 :
Fundamentos do projeto e análise quantitativos
O ganho de velocidade geral é a razão dos tempos de execução:
Ganho de velocidadegeral =
Tempo de execuçãoantigo
=
Tempo de execuçãonovo (1 − Fraçãomelhorada ) +
1
Fraçãomelhorada
Ganho de velocidademelhorado
Exemplo Suponha que queiramos melhorar o processador usado para serviço na Web. O
novo processador é 10 vezes mais rápido em computação na aplicação de serviço
da Web do que o processador original. Considerando que o processador original
está ocupado com cálculos 40% do tempo e esperando por E/S 60% do tempo,
qual é o ganho de velocidade geral obtido pela incorporação da melhoria?
Resposta Fraçãomelhorada = 0,4;Ganhode velocidademelhorado = 10;
Ganhode velocidadegeral =
1
0,4
0,6 +
10
=
1
≈ 1,56
0,64
A lei de Amdahl expressa os retornos diminuídos: a melhoria incremental no ganho de
velocidade obtida pela melhoria de apenas uma parte da computação diminui à medida
que as melhorias são acrescentadas. Um corolário importante da lei de Amdahl é que, se
uma melhoria só for utilizável por uma fração de uma tarefa, não poderemos agilizar essa
tarefa mais do que o inverso de 1 menos essa fração.
Um engano comum na aplicação da lei de Amdahl é confundir “fração de tempo convertida para usar uma melhoria” com “fração de tempo após a melhoria estar em uso”. Se, em
vez de medir o tempo que poderíamos usar a melhoria em um cálculo, medirmos o tempo
após a melhoria estar em uso, os resultados serão incorretos!
A lei de Amdahl pode servir de guia para o modo como uma melhoria incrementará o
desempenho e como distribuir recursos para melhorar o custo-desempenho. O objetivo,
claramente, é investir recursos proporcionais onde o tempo é gasto. A lei de Amdahl é
particularmente útil para comparar o desempenho geral do sistema de duas alternativas,
mas ela também pode ser aplicada para comparar duas alternativas de um projeto de
processador, como mostra o exemplo a seguir.
Exemplo
Resposta
Uma transformação comum exigida nos processadores de gráficos é a raiz
quadrada. As implementações de raiz quadrada com ponto flutuante (PF) variam muito em desempenho, sobretudo entre processadores projetados para
gráficos. Suponha que a raiz quadrada em PF (FPSQR) seja responsável por
20% do tempo de execução de um benchmark gráfico crítico. Uma proposta é
melhorar o hardware de FPSQR e agilizar essa operação por um fator de 10.
A outra alternativa é simplesmente tentar fazer com que todas as operações
de PF no processador gráfico sejam executadas mais rapidamente por um
fator de 1,6; as instruções de PF são responsáveis por metade do tempo de
execução para a aplicação. A equipe de projeto acredita que pode fazer com
que todas as instruções de PF sejam executadas 1,6 vez mais rápido com o
mesmo esforço exigido para a raiz quadrada rápida. Compare essas duas
alternativas de projeto.
Podemos comparar essas alternativas comparando os ganhos de velocidade:
Ganhode velocidadeFP
1
1
=
= 1,22
0,2 0,82
(1 − 0,2) +
10
1
1
=
=
= 1,23
0,5 0,8125
(1 − 0,5) +
1,6
Ganhode velocidadeFPSQR =
Melhorar o ganho de velocidade das operações de PF em geral é ligeiramente
melhor devido à frequência mais alta.
1.9
Princípios quantitativos do projeto de computadores
A lei de Amdahl se aplica além do desempenho. Vamos refazer o exemplo de confiabilidade
da páginas 31 e 32 depois de melhorar a confiabilidade da fonte de alimentação, por meio
da redundância, de 200.000 horas para 830.000.000 horas MTTF ou 4.150 vezes melhor.
Exemplo
O cálculo das taxas de falha do subsistema de disco foi
1
1
1
1
1
+
+
+
+
1.000.000 500.000 200.000 200.000 1.000.000
23
= 10 + 2 + 5 + 5 + 1 =
1.000.000 horas 1.000.000 horas
Taxa de falhasistema = 10 ×
Resposta
Portanto, a fração da taxa de falha que poderia ser melhorada é 5 por milhão
de horas, das 23 para o sistema inteiro, ou 0,22.
A melhoria de confiabilidade seria
Melhoriapar de fontes =
1
(1 − 0,22) +
0,22
4150
= 1 = 1,28
0,78
Apesar de uma impressionante melhoria de 4.150 vezes na confiabilidade
de um módulo, do ponto de vista do sistema a mudança possui um benefício
mensurável, porém pequeno.
Nos exemplos precedentes, precisamos da fração consumida pela versão nova e melhorada;
costuma ser difícil medir esses tempos diretamente. Na seção seguinte, veremos outra forma
de fazer essas comparações com base no uso de uma equação que decompõe o tempo de
execução da CPU em três componentes separados. Se soubermos como uma alternativa
afeta esses componentes, poderemos determinar seu desempenho geral. Normalmente é
possível montar simuladores que medem esses componentes antes que o hardware seja
realmente projetado.
A equação de desempenho do processador
Basicamente todos os computadores são construídos usando um clock que trabalha a uma
taxa constante. Esses eventos de tempo discretos são chamados de ticks, ticks de clock, períodos
de clock, clocks, ciclos ou ciclos de clock. Os projetistas de computador referem-se ao tempo de
um período de clock por sua duração (por exemplo, 1 ns) ou por sua frequência (por exemplo, 1 GHz). O tempo de CPU para um programa pode, então, ser expresso de duas maneiras:
Tempo de CPU = Ciclos de clock de CPU para um programa × Tempo do ciclo de clock
ou
Tempo de CPU =
Ciclos de clock de CPU para um programa
Frequência de clock
Além do número de ciclos de clock necessários para executar um programa, também
podemos contar o número de instruções executadas — o tamanho do caminho de instrução
ou número de instruções (Instruction Count — IC). Se soubermos o número de ciclos de
clock e o contador de instruções, poderemos calcular o número médio de ciclos de clock por
instruções (Clock Cycles Per Instruction — CPI). Por ser mais fácil de trabalhar e porque
neste livro lidaremos com processadores simples, usaremos o CPI. Às vezes, os projetistas
também usam instruções por clock (Instructions Per Clock — IPC), que é o inverso do CPI.
O CPI é calculado como
CPI =
Ciclos de clock de CPU para um programa
Número de instruções
43
44
CAPÍTULO 1 :
Fundamentos do projeto e análise quantitativos
Esse valor de mérito do processador oferece visões para diferentes estilos de conjuntos de
instruções e de implementações, e o usaremos bastante nos quatro capítulos seguintes.
Transpondo o número de instruções na fórmula anterior, os ciclos de clock podem ser
definidos como IC × CPI. Isso nos permite usar o CPI na fórmula do tempo de execução:
Tempo de CPU = Número de instruções × Ciclos por instrução × Tempo de ciclo de clock
Expandindo a primeira fórmula para as unidades de medida, vemos como as partes se
encaixam:
Segundos
Segundos
Instruções Ciclos de clock
×
×
=
= Tempo de CPU
Programa
Instrução
Ciclos de clock Programa
Conforme a fórmula demonstra, o desempenho do processador depende de três características: ciclo de clock (ou frequência), ciclos de clock por instruções e número de instruções.
Além do mais, o tempo de CPU depende igualmente dessas três características: a melhoria
de 10% em qualquer um deles leva à melhoria de 10% no tempo de CPU.
Infelizmente, é difícil mudar um parâmetro de modo completamente isolado dos outros,
pois as tecnologias básicas envolvidas na mudança de cada característica são interdependentes:
j
j
j
Tempo de ciclo de clock — Tecnologia e organização do hardware
CPI — Organização e arquitetura do conjunto de instruções
Número de instruções — Arquitetura do conjunto de instruções e tecnologia do
computador
Por sorte, muitas técnicas potenciais de melhoria de desempenho melhoram principalmente um componente do desempenho do processador, com impactos pequenos ou
previsíveis sobre os outros dois.
Às vezes, é útil projetar o processador para calcular o número total de ciclos de clock do
processador como
n
Ciclos de clock da CPU = ∑ ICi × CPIi
i =1
onde ICi representa o número de vezes que a instrução i é executada em um programa e
CPIi representa o número médio de clocks por instrução para a instrução i. Essa forma
pode ser usada para expressar o tempo de CPU como
n
Tempo de CPU = ∑ ICi × CPIi × Tempo de ciclo de clock
i =1
e o CPI geral como
n
∑ IC × CPI
i
CPI =
i
i =1
Número de instruções
n
ICi
× CPIi
Número
de
instruções
i =1
=∑
A última forma do cálculo do CPI utiliza cada CPIi e a fração de ocorrências dessa instrução em um programa (ou seja, ICi ÷ número de instruções). O CPIi deve ser medido,
e não apenas calculado a partir de uma tabela no final de um manual de referência, pois
precisa levar em consideração os efeitos de pipeline, as faltas de cache e quaisquer outras
ineficiências do sistema de memória.
1.9
Princípios quantitativos do projeto de computadores
Considere nosso exemplo de desempenho da página 42, modificado aqui para usar
medições da frequência das instruções e dos valores de CPI da instrução, que, na prática,
são obtidos pela simulação ou pela instrumentação do hardware.
Exemplo
Resposta
Suponha que tenhamos feito as seguintes medições:
Frequência das operações de PF = 25%
CPI médio das operações de PF = 4,0
CPI médio das outras instruções = 1,33
Frequência da FPSQR = 2%
CPI da FPSQR = 20
Considere que as duas alternativas de projeto sejam diminuir o CPI da
FPSQR para 2 ou diminuir o CPI médio de todas as operações de PF para 2,5.
Compare essas duas alternativas de projeto usando a equação de desempenho do processador.
Em primeiro lugar, observe que somente o CPI muda; a taxa de clock e o
número de instruções permanecem idênticos. Começamos encontrando o
CPI original sem qualquer melhoria:
n
ICi
CPIoriginal = ∑ CPIi ×
Númerodeinstruções
i =1
= (4 × 25%) + (1,33 × 75%) = 2,0
Podemos calcular o CPI para a FPSQR melhorada subtraindo os ciclos salvos
do CPI original:
CPIcom nova FPSQR = CPIoriginal − 2% × (CPIFPSQR antiga − CPIde nova FPSQR apenas )
= 2,0 − 2% × (20 − 2) = 1,64
Podemos calcular o CPI para a melhoria de todas as instruções de PF da
mesma forma ou somando os CPIs de PF e de não PF. Usando a última
técnica, temos
CPInovo FP = (75% × 1,33) + (25% × 2,5) = 1,625
Como o CPI da melhoria geral de PF é ligeiramente inferior, seu desempenho será um pouco melhor. Especificamente, o ganho de velocidade para a
melhoria de PF geral é
TempodeCPUoriginal
TempodeCPUnova FP
IC × Ciclodeclock × CPioriginal
=
IC × Ciclodeclock × CPinova FP
CPioriginal 2,00
=
=
= 1,23
CPinova FP 1,625
Ganhode velocidadenovo FP =
Felizmente, obtemos esse mesmo ganho de velocidade usando a lei
de Amdahl na página 41.
Normalmente, é possível medir as partes constituintes da equação de desempenho do
processador. Essa é uma vantagem importante do uso dessa equação versus a lei de Amdahl
no exemplo anterior. Em particular, pode ser difícil medir itens como a fração do tempo de
execução pela qual um conjunto de instruções é responsável. Na prática, isso provavelmente
seria calculado somando-se o produto do número de instruções e o CPI para cada uma das
instruções no conjunto. Como os pontos de partida normalmente são o número de instruções e as medições de CPI, a equação de desempenho do processador é incrivelmente útil.
Para usar a equação de desempenho do processador como uma ferramenta de projeto,
precisamos ser capazes de medir os diversos fatores. Para determinado processador,
é fácil obter o tempo de execução pela medição, enquanto a velocidade do clock padrão é
45
46
CAPÍTULO 1 :
Fundamentos do projeto e análise quantitativos
conhecida. O desafio está em descobrir o número de instruções ou o CPI. A maioria dos
novos processadores inclui contadores para instruções executadas e para ciclos de clock.
Com o monitoramento periódico desses contadores, também é possível conectar o tempo
de execução e o número de instruções a segmentos do código, o que pode ser útil para
programadores que estão tentando entender e ajustar o desempenho de uma aplicação.
Em geral, um projetista ou programador desejará entender o desempenho em um nível
mais detalhado do que o disponibilizado pelos contadores do hardware. Por exemplo,
eles podem querer saber por que o CPI é o que é. Nesses casos, são usadas técnicas de
simulação como aquelas empregadas para os processadores que estão sendo projetados.
Técnicas que ajudam na eficiência energética, como escalamento dinâmico de frequência,
de voltagem e overclocking (Seção 1.5), tornam essa equação mais difícil de usar, já que a
velocidade do clock pode variar enquanto medimos o programa. Uma abordagem simples
é desligar esses recursos para tornar os resultados passíveis de reprodução. Felizmente, já
que muitas vezes o desempenho e a eficiência energética estão altamente correlacionados
— levar menos tempo para rodar um programa geralmente poupa energia —, provavelmente é seguro considerar o desempenho sem se preocupar com o impacto do DVFS ou
overclocking sobre os resultados.
1.10 JUNTANDO TUDO: DESEMPENHO
E PREÇO-DESEMPENHO
Nas seções “Juntando tudo” que aparecem próximo ao final de cada capítulo, mostramos exemplos reais que utilizam os princípios explicados no capítulo. Nesta seção,
veremos medidas de desempenho e preço-desempenho nos sistemas de desktop usando
o benchmark SPECpower.
A Figura 1.18 mostra os três servidores multiprocessadores que estamos avaliando e seu
preço. Para manter justa a comparação de preços, todos são servidores Dell PowerEdge. O
primeiro é o PowerEdge R710, baseado no microprocessador Intel Xeon X5670, com uma
frequência de clock de 2,93 GHz. Ao contrário do Intel Core i7 abordado nos Capítulos 2
a 5, que tem quatro núcleos e um cache L3 de 8MB, esse chip da Intel tem seis núcleos e
um cache L3 de 12 MB, embora os próprios núcleos sejam idênticos. Nós selecionamos um
sistema de dois soquetes com 12 GB de DRAM DDR3 de 1.333 MHz protegida por ECC.
O próximo servidor é o PowerEdge R815, baseado no microprocessador AMD Opteron
6174. Um chip tem seis núcleos e um cache L3 de 6 MB, e roda a 2,20 GHz, mas a AMD
coloca dois desses chips em um único soquete. Assim, um soquete tem 12 núcleos e dois
caches L3 de 6 MB. Nosso segundo servidor tem dois soquetes com 24 núcleos e 16 GB
de DRAM DDR3 de 1.333 MHz protegido por ECC, e nosso terceiro servidor (também
um PowerEdge R815) tem quatro soquetes com 48 núcleos e 32 GB de DRAM. Todos
estão rodando a IBM J9 JVM e o sistema operacional Microsoft Windows 2008 Server
Enterprise x64 Edition.
Observe que, devido às forças do benchmarking (Seção 1.11), esses servidores são configurados de forma pouco usual. Os sistemas na Figura 1.18 têm pouca memória em relação
à capacidade de computação e somente um pequeno disco de estado sólido com 50 GB.
É barato adicionar núcleos se você não precisar acrescentar aumentos proporcionais em
memória e armazenamento!
Em vez de rodar, estatisticamente, programas do SPEC CPU, o SPECpower usa a mais
moderna pilha de software escrita em Java. Ele é baseado no SPECjbb e representa o lado
do servidor das aplicações de negócios, com o desempenho medido como o número de
1.10
Juntando tudo: desempenho e preço-desempenho
FIGURA 1.18 Três servidores Dell PowerEdge e seus preços com base em agosto de 2010.
Nós calculamos o custo dos processadores subtraindo o custo de um segundo processador. Do mesmo modo, calculamos o custo geral da memória vendo
qual seria o custo da memória extra. Portanto, o custo-base do servidor é ajustado subtraindo o custo estimado do processador e a memória-padrão.
O Capítulo 5 descreve como esses sistemas multissoquetes se conectam.
transações por segundo, chamado ssj_ops para operações por segundo do lado do servidor
Java. Ele utiliza não só o processador do servidor, como o SPEC CPU, mas também as
caches, o sistema de memória e até mesmo o sistema de interconexão dos multiprocessadores. Além disso, utiliza a Java Virtual Machine (JVM), incluindo o compilador de
runtime JIT e o coletor de lixo, além de partes do sistema operacional.
Como mostram as duas últimas linhas da Figura 1.18, o vencedor em desempenho e preçodesempenho é o PowerEdge R815, com quatro soquetes e 48 núcleos. Ele atinge 1,8 M
ssj_ops, e o ssj_ops por dólar é o mais alto, com 145. Surpreendentemente, o computador
com o maior número de núcleos é o mais eficiente em termos de custo. Em segundo lugar
está o R815 de dois soquetes, com 24 núcleos, e o R710 com 12 núcleos em último lugar.
Enquanto a maioria dos benchmarks (e dos arquitetos de computadores) se preocupa
somente com o desempenho dos sistemas com carga máxima, os computadores raramente
rodam com carga máxima. De fato, a Figura 6.2, no Capítulo 6, mostra os resultados da
medição, utilizando dezenas de milhares de servidores ao longo de seis meses no Google,
e menos de 1% operam com uma utilização média de 100%. A maioria tem utilização
média entre 10-50%. Assim, o benchmark SPECpower captura a potência conforme a
carga de trabalho-alvo varia de pico em intervalos de 10% até 0%, chamado Active Idle.
A Figura 1.19 mostra o ssj_ops (operações SSJ/segundo) por watt, e a potência média
conforme a carga-alvo varia de 100% a 0%. O Intel R710 tem sempre a menor potência e
o melhor ssj_ops por watt em todos os níveis de carga de trabalho-alvo.
Uma razão é a fonte de alimentação muito maior para o R815 com 1.110 watts versus
570 no R715. Como o Capítulo 6 mostra, a eficiência da fonte de alimentação é muito
47
48
CAPÍTULO 1 :
Fundamentos do projeto e análise quantitativos
FIGURA 1.19 Desempenho de potência dos três servidores na Figura 1.18.
Os valores de ssj_ops/watt estão no eixo esquerdo, com as três colunas associadas a eles, e os valores em watts estão no eixo direito, com as três linhas
associadas a eles. O eixo horizontal mostra a carga de trabalho-alvo e também consome a menor potência a cada nível.
importante na eficiência energética geral de um computador. Uma vez que watts = joules/
segundo, essa métrica é proporcional às operações SSJ por joule:
ssj _ ops/s ssj _ ops/s ssj _ ops/s
=
=
Watt
Joule/s
Joule
Para calcular um número único para comparar a eficiência energética dos sistemas, o
SPECpower usa:
ssj _ ops/watt médio =
∑ ssj _ ops
∑ potência
O ssj_ops/watt médio dos três servidores é de 3034 para o Intel R710, de 2357 para o AMD
R815 de dois soquetes e de 2696 para o AMD R815 de quatro soquetes. Portanto, o Intel
R710 tem a melhor potência/desempenho. Dividindo pelo preço dos servidores, o ssj_ops/
watt/US$ 1.000 é de 324 para o Intel R710, de 254 para o AMD R815 de dois soquetes e de
213 para AMD R815 de quatro soquetes. Assim, adicionar potência reverte os resultados
da competição preço-desempenho, e o troféu do preço-potência-desempenho vai para o
Intel R710; o R815 de 48 núcleos vem em último lugar.
1.11
FALÁCIAS E ARMADILHAS
A finalidade desta seção, que consta de todos os capítulos, é explicar algumas das crenças
erradas ou conceitos indevidos que você deverá evitar. Chamamos a esses conceitos
falácias. Ao analisar uma falácia, tentamos oferecer um contraexemplo. Também dis-
1.11
cutimos as armadilhas — erros cometidos com facilidade. Essas armadilhas costumam ser
generalizações de princípios que são verdadeiros em um contexto limitado. A finalidade
dessas seções é ajudá-lo a evitar cometer esses erros nos computadores que você projeta.
Falácia. Multiprocessadores são uma bala de prata.
A mudança para múltiplos processadores por chip em meados de 2005 não veio de
nenhuma descoberta que simplificou drasticamente a programação paralela ou tornou
mais fácil construir computadores multicore. Ela ocorreu porque não havia outra opção,
devido aos limites de ILP e de potência. Múltiplos processadores por chip não garantem
potência menor. Certamente é possível projetar um chip multicore que use mais potência.
O potencial que existe é o de continuar melhorando o desempenho com a substituição de
um núcleo ineficiente e com alta taxa de clock por diversos núcleos eficientes e com taxa
de clock mais baixa. Conforme a tecnologia melhora na redução dos transistores, isso pode
encolher um pouco a capacitância e a tensão de alimentação para que possamos obter um
modesto aumento no número de núcleos por geração. Por exemplo, nos últimos anos, a
Intel tem adicionado dois núcleos por geração.
Como veremos nos Capítulos 4 e 5, hoje o desempenho é o fardo dos programadores. A
época de o programador não levantar um só dedo e confiar nos projetistas de hardware
para fazer seus programas funcionarem mais rápido está oficialmente terminada. Se os
programadores quiserem que seus programas funcionem mais rápido a cada geração,
deverão tornar seus programas mais paralelos.
A versão popular da lei de Moore — aumentar o desempenho a cada geração da tecnologia
— está agora a cargo dos programadores.
Armadilha. Desprezar a lei de Amdahl.
Praticamente todo arquiteto de computadores praticante conhece a lei de Amdahl. Apesar
disso, quase todos nós, uma vez ou outra, empenhamos um esforço enorme otimizando
algum recurso antes de medir seu uso. Somente quando o ganho de velocidade geral é
decepcionante nos lembramos de que deveríamos tê-lo medido antes de gastar tanto esforço para melhorá-lo!
Armadilha. Um único ponto de falha.
Os cálculos de melhoria de confiabilidade utilizando a lei de Amdahl na página 43 mostram que a dependência não é mais forte do que o elo mais fraco de uma corrente. Não
importa quão mais dependente façamos as fontes de alimentação, como fizemos em nosso exemplo — o único ventilador limitará a confiabilidade do subsistema de disco. Essa
observação da lei de Amdahl levou a uma regra prática para sistemas tolerantes a falhas
para certificar que cada componente fosse redundante, de modo que nenhuma falha em
um componente isolado pudesse parar o sistema inteiro.
Falácia. As melhorias de hardware que aumentam o desempenho incrementam a eficiência
energética ou, no pior dos casos, são neutras em termos de energia.
Esmaelizadeh et al. (2011) mediram o SPEC2006 em apenas um núcleo de um Intel Core
i7 de 2,67 GHz usando o modo Turbo (Seção 1.5). O desempenho aumentou por um
fator de 1,07, quando a taxa de clock aumentou para 2,94 GHz (ou um fator de 1,10), mas
o i7 usou um fator de 1,37 mais joules e um fator de 1,47 mais watts-hora!
Armadilha. Benchmarks permanecem válidos indefinidamente.
Diversos fatores influenciam a utilidade de um benchmark como previsão do desempenho
real e alguns mudam com o passar do tempo. Um grande fator que influencia a utilidade
Falácias e armadilhas
49
50
CAPÍTULO 1 :
Fundamentos do projeto e análise quantitativos
de um benchmark é a sua capacidade de resistir ao “cracking”, também conhecido como
“engenharia de benchmark” ou “benchmarksmanship”. Quando um benchmark se torna
padronizado e popular, existe uma pressão tremenda para melhorar o desempenho por
otimizações direcionadas ou pela interpretação agressiva das regras para execução do
benchmark. Pequenos kernels ou programas que gastam seu tempo em um número muito
pequeno de linhas de código são particularmente vulneráveis.
Por exemplo, apesar das melhores intenções, o pacote de benchmark SPEC89 inicial incluía
um pequeno kernel, chamado matrix300, que consistia em oito multiplicações diferentes
de matrizes de 300 × 300. Nesse kernel, 99% do tempo de execução estava em uma única
linha (SPEC, 1989). Quando um compilador IBM otimizava esse loop interno (usando
uma ideia chamada bloqueio, discutida nos Capítulos 2 e 4), o desempenho melhorava
por um fator de 9 em relação à versão anterior do compilador! Esse benchmark testava o
ajuste do compilador e, naturalmente, não era uma boa indicação do desempenho geral
nem do valor típico dessa otimização em particular.
Por um longo período, essas mudanças podem tornar obsoleto até mesmo um benchmark
bem escolhido; o Gcc é o sobrevivente solitário do SPEC89. A Figura 1.16 apresenta o
status de todos os 70 benchmarks das diversas versões SPEC. Surpreendentemente, quase
70% de todos os programas do SPEC2000 ou anteriores foram retirados da versão seguinte.
Armadilha. O tempo médio para falha avaliado para os discos é de 1.200.000 horas ou quase
140 anos, então 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 colocarão milhares
de discos em uma sala, deixarão em execução por alguns meses e contarão a quantidade
que falha. Eles calculam o MTTF como o número total de horas que os discos trabalharam
acumuladamente dividido pelo número daqueles que falharam.
Um problema é que esse número é muito superior ao tempo de vida útil de um disco,
que normalmente é considerado cinco anos ou 43.800 horas. Para que esse MTTF grande
faça algum sentido, os fabricantes de disco argumentam que o modelo corresponde a um
usuário que compra um disco e depois o substitui a cada cinco anos — tempo de vida
útil planejado do disco. A alegação é que, se muitos clientes (e seus bisnetos) fizerem isso
no século seguinte, substituirão, em média, um disco 27 vezes antes de uma falha ou por
cerca de 140 anos.
Uma medida mais útil seria a porcentagem de discos que falham. Considere 1.000 discos
com um MTTF de 1.000.000 de horas e que os discos sejam usados 24 horas por dia. Se
você substituir os discos que falharam por um novo com as mesmas características de
confiabilidade, a quantidade que falhará em um ano (8.760 horas) será
Número de discos × Período de tempo
MTTF
1.000 discos × 8.760 horas / drive
=
=9
1.000.000 horas / falha
Discos que falham =
Em outras palavras, 0,9% falharia por ano ou 4,4% por um tempo de vida útil de cinco
anos.
Além do mais, esses números altos são cotados com base em intervalos limitados de
temperaturas e vibração; se eles forem ultrapassados, todas as apostas falharão. Um
estudo recente das unidades de disco em ambientes reais (Gray e Van Ingen, 2005)
afirma que cerca de 3-7% dos drives falham por ano, ou um MTTF de cerca de 125.000-
1.11
Falácias e armadilhas
300.000 horas, e cerca de 3-7% das unidades ATA falham por ano, ou um MTTF de
cerca de 125.000-300.000 horas. Um estudo ainda maior descobriu taxas de falha de disco
de 2-10% (Pinheiro, Weber e Barroso, 2007). Portanto, o MTTF do mundo real é de cerca de
2-10 vezes pior que o MTTF do fabricante.
A única definição universalmente verdadeira do desempenho de pico é “o nível de desempenho que um computador certamente não ultrapassará”. A Figura 1.20 mostra a
porcentagem do desempenho de pico para quatro programas e quatro multiprocessadores.
Ele varia de 5-58%. Como a diferença é muito grande e pode variar significativamente por
benchmark, o desempenho de pico geralmente não é útil na previsão do desempenho
observado.
Armadilha. Detecção de falha pode reduzir a disponibilidade.
Essa armadilha aparentemente irônica ocorre pelo fato de o hardware de computador
possuir grande quantidade de estados que nem sempre são cruciais para determinada
operação. Por exemplo, não é fatal se um erro ocorrer em uma previsão de desvio (branch),
pois somente o desempenho será afetado.
Em processadores que tentam explorar agressivamente o paralelismo em nível de instrução,
nem todas as operações são necessárias para a execução correta do programa. Mukherjee
et al. (2003) descobriram que menos de 30% das operações estavam potencialmente no
caminho crítico para os benchmarks SPEC2000 rodando em um Itanium 2.
A mesma observação é verdadeira sobre os programas. Se um registrador estiver “morto”
em um programa — ou seja, se o programa escrever antes de ler novamente —, os erros
não importarão. Se você tivesse que interromper um programa ao detectar uma falha
transiente em um registrador morto, isso reduziria a disponibilidade desnecessariamente.
FIGURA 1.20 Porcentagem do desempenho de pico para quatro programas em quatro multiprocessadores
aumentados para 64 processadores.
O Earth Simulator e o X1 são processadores de vetor (Cap. 4 e o Apêndice G). Eles não apenas oferecem uma fração mais
alta do desempenho de pico, mas também têm o maior desempenho de pico e as menores taxas de clock. Exceto
para o programa da Paratec, os sistemas Power 4 e Itanium 2 oferecem entre 5-10% de seu pico. De Oliker et al. (2004).
51
52
CAPÍTULO 1 :
Fundamentos do projeto e análise quantitativos
A Sun Microsystems viveu essa armadilha em 2000, com uma cache L2 que incluía paridade, mas não correção de erro, em seus sistemas Sun E3000 a Sun E10000. As SRAMs
usadas para criar as caches tinham falhas intermitentes, que a paridade detectou. Se
os dados na cache não fossem modificados, o processador simplesmente os relia. Como os
projetistas não protegeram a cache com ECC, o sistema operacional não tinha opção
a não ser informar um erro aos dados sujos e interromper o programa. Os engenheiros de
campo não descobriram problemas na inspeção de mais de 90% desses casos.
Para reduzir a frequência de tais erros, a Sun modificou o sistema operacional Solaris para
“varrer” a cache com um processo que escreve proativamente os dados sujos na memória.
Como os chips dos processadores não tinham pinos suficientes para acrescentar ECC, a
única opção de hardware para os dados sujos foi duplicar a cache externa, usando a cópia
sem o erro de paridade para corrigir o erro.
A armadilha está na detecção de falhas sem oferecer um mecanismo para corrigi-las. A
Sun provavelmente não disponibilizará outro computador sem ECC nas caches externas.
1.12
COMENTÁRIOS FINAIS
Este capítulo introduziu uma série de conceitos e forneceu um framework quantitativo
que expandiremos ao longo do livro. A partir desta edição, a eficiência energética é a nova
companheira do desempenho.
No Capítulo 2, iniciaremos a importantíssima área do projeto de sistema de memória.
Vamos examinar uma vasta gama de técnicas que conspiram para fazer a memória parecer
infinitamente grande e, ainda assim, o mais rápida possível (o Apêndice B fornece material
introdutório sobre caches para leitores sem formação nem muita experiência nisso). Como
nos capítulos mais adiante, veremos que a cooperação entre hardware e software se tornou
essencial para os sistemas de memória de alto desempenho, assim como para os pipelines
de alto desempenho. Este capítulo também aborda as máquinas virtuais, uma técnica de
proteção cada vez mais importante.
No Capítulo 3, analisaremos o paralelismo em nível de instrução (Instruction-Level Parallelism — ILP), cuja forma mais simples e mais comum é o pipelining. A exploração do ILP
é uma das técnicas mais importantes para a criação de uniprocessadores de alta velocidade.
A presença de dois capítulos reflete o fato de que existem várias técnicas para a exploração
do ILP e que essa é uma tecnologia importante e amadurecida. O Capítulo 3 começa com
uma longa apresentação dos conceitos básicos que o prepararão para a grande gama de
ideias examinadas nos dois capítulos anteriores. Ele utiliza exemplos disseminados há
cerca de 40 anos, abarcando desde um dos primeiros supercomputadores (IBM 360/91)
até os processadores mais rápidos do mercado em 2011. Além disso, enfatiza a técnica
para a exploração do ILP, chamada dinâmica ou em tempo de execução. Também focaliza os
limites e as extensões das ideias do ILP e apresenta o multithreading, que será detalhado
nos Capítulos 4 e 5. O Apêndice C é um material introdutório sobre pipelining para os
leitores sem formação ou muita experiência nesse assunto. (Esperamos que ele funcione
como uma revisão para muitos leitores, incluindo os do nosso texto introdutório, Computer
Organization and Design: The Hardware/Software Interface.)
O Capítulo 4 foi escrito para esta edição e explica três modos de explorar o paralelismo em
nível de dados. A abordagem clássica, mais antiga, é a arquitetura vetorial, e começamos
por ela para estabelecer os princípios do projeto SIMD (o Apêndice G apresenta detalhes
sobre as arquiteturas vetoriais). Em seguida, explicamos as extensões de conjunto de instruções encontradas na maioria dos microprocessadores desktops atuais. A terceira parte
1.12
é uma explicação aprofundada de como as unidades de processamento gráfico (GPUs)
modernas funcionam. A maioria das descrições de GPU é feita da perspectiva do programador, que geralmente oculta o modo como o computador realmente funciona. Essa
seção explica as GPUs da perspectiva de alguém “de dentro”, incluindo um mapeamento
entre os jargões de GPU e os termos de arquitetura mais tradicionais.
O Capítulo 5 enfoca como obter alto desempenho usando múltiplos processadores ou
multiprocessadores. Em vez de usar o paralelismo para sobrepor instruções individuais,
o multiprocessamento o utiliza para permitir que vários fluxos de instruções sejam executados simultaneamente em diferentes processadores. Nosso foco recai sobre a forma
dominante dos multiprocessadores, os multiprocessadores de memória compartilhada,
embora também apresentemos outros tipos e discutamos os aspectos mais amplos que
surgem em qualquer multiprocessador. Aqui, mais uma vez, exploramos diversas técnicas,
focalizando as ideias importantes apresentadas inicialmente nas décadas de 1980 e 1990.
O Capítulo 6 também foi criado para esta edição. Apresentamos os clusters e, depois, tratamos detalhadamente dos computadores de escala warehouse (warehouse-scale computers
— WSCs) que os arquitetos de computadores ajudam a projetar. Os projetistas de WSCs
são os descendentes profissionais dos pioneiros dos supercomputadores, como Seymour
Cray, pois vêm projetando computadores extremos. Eles contêm dezenas de milhares de
servidores, e seu equipamento e sua estrutura custam cerca de US$ 200 milhões. Os problemas de preço-desempenho e eficiência energética abordados nos primeiros capítulos
aplicam-se aos WSCs, assim como a abordagem quantitativa de tomada de decisões.
Este livro conta com grande quantidade de material on-line (ver mais detalhes no Prefácio),
tanto para reduzir o custo quanto para apresentar aos leitores diversos tópicos avançados.
A Figura 1.21 apresenta todo esse material. Os Apêndices A, B e C, incluídos neste volume,
funcionarão como uma revisão para muitos leitores.
No Apêndice D, nos desviaremos de uma visão centrada no processador e examinaremos questões sobre sistemas de armazenamento. Aplicamos um enfoque quantitativo
semelhante, porém baseado em observações do comportamento do sistema, usando
uma técnica de ponta a ponta para a análise do desempenho. Ele focaliza a importante
questão de como armazenar e recuperar dados de modo eficiente usando principalmente
FIGURA 1.21 Lista de apêndices.
Comentários finais
53
54
CAPÍTULO 1 :
Fundamentos do projeto e análise quantitativos
as tecnologias de armazenamento magnético de menor custo. Nosso foco recai sobre o
exame do desempenho dos sistemas de armazenamento de disco para cargas de trabalho
típicas de E/S, como os benchmarks OLTP que vimos neste capítulo. Exploramos bastante
os tópicos avançados nos sistemas baseados em RAID, que usam discos redundantes para
obter alto desempenho e alta disponibilidade. Finalmente, o capítulo apresenta a teoria
de enfileiramento, que oferece uma base para negociar a utilização e a latência.
O Apêndice E utiliza um ponto de vista de computação embarcada para as ideias de cada
um dos capítulos e apêndices anteriores.
O Apêndice F explora o tópico de interconexão de sistemas mais abertamente, incluindo
WANs e SANs, usadas para permitir a comunicação entre computadores.
Ele também descreve os clusters, que estão crescendo em importância, devido à sua
adequação e eficiência para aplicações de banco de dados e servidor Web.
O Apêndice H revê hardware e software VLIW, que, por contraste, são menos populares
do que quando o EPIC apareceu em cena, um pouco antes da última edição.
O Apêndice I descreve os multiprocessadores em grande escala para uso na computação
de alto desempenho.
O Apêndice J é o único que permanece desde a primeira edição. Ele abrange aritmética
de computador.
O Apêndice K é um estudo das arquiteturas de instrução, incluindo o 80x86, o IBM 360,
o VAX e muitas arquiteturas RISC, como ARM, MIPS, Power e SPARC.
Descreveremos o Apêndice L mais adiante.
1.13
PERSPECTIVAS HISTÓRICAS E REFERÊNCIAS
O Apêndice L (disponível on-line) inclui perspectivas históricas sobre as principais ideias
apresentadas em cada um dos capítulos deste livro. Essas seções de perspectiva histórica
nos permitem rastrear o desenvolvimento de uma ideia por uma série de máquinas ou descrever projetos significativos. Se você estiver interessado em examinar o desenvolvimento
inicial de uma ideia ou máquina, ou em ler mais sobre o assunto, são dadas referências ao
final de cada história. Sobre este capítulo, consulte a Seção L.2, “O desenvolvimento inicial
dos computadores”, para obter uma análise do desenvolvimento inicial dos computadores
digitais e das metodologias de medição de desempenho.
Ao ler o material histórico, você logo notará que uma das maiores vantagens da juventude
da computação, em comparação com vários outros campos da engenharia, é que muitos dos
pioneiros ainda estão vivos — podemos aprender a história simplesmente perguntando a eles!
ESTUDOS DE CASO E EXERCÍCIOS POR DIANA FRANKLIN
Estudo de caso 1: custo de fabricação de chip
Conceitos ilustrados por este estudo de caso
j
j
j
Custo de fabricação
Rendimento da fabricação
Tolerância a defeitos pela redundância
Existem muitos fatores envolvidos no preço de um chip de computador. Tecnologia nova e
menor oferece aumento no desempenho e uma queda na área exigida para o chip. Na tecno-
Estudos de caso e exercícios por Diana Franklin
FIGURA 1.22 Fatores de custo de manufatura para vários processadores modernos.
logia menor, pode-se manter a área pequena ou colocar mais hardware no chip, a fim de obter
mais funcionalidade. Neste estudo de caso, exploramos como diferentes decisões de projeto
envolvendo tecnologia de fabricação, superfície e redundância afetam o custo dos chips.
1.1
1.2
1.3
[10/10] <1.6> A Figura 1.22 contém uma estatística relevante de chip,
que influencia o custo de vários chips atuais. Nos próximos exercícios, você
vai explorar as escolhas envolvidas para o IBM Power5.
a. [10] <1.6> Qual é o rendimento para o IBM Power5?
b. [10] <1.6> Por que o IBM Power5 tem uma taxa de defeitos pior
do que o Niagara e o Opteron?
[20/20/20/20] <1,6> Custa US$ 1 bilhão montar uma nova instalação
de fabricação. Você vai produzir diversos chips nessa fábrica e precisa decidir
quanta capacidade dedicar a cada chip. Seu chip Woods terá uma área de 150 mm2,
vai lucrar US$ 20 por chip livre de defeitos. Seu chip Markon terá 250 mm2 e vai
gerar um lucro de US$ 225 por chip livre de defeitos. Sua instalação de fabricação
será idêntica àquela do Power5. Cada wafer tem 300 mm de diâmetro.
a. [20] <1.6> Quanto lucro você obterá com cada wafer do chip Woods?
b. [20] <1.6> Quanto lucro você obterá com cada wafer do chip Markon?
c. [20] <1.6> Que chip você deveria produzir nessa instalação?
d. [20] <1.6> Qual é o lucro em cada novo chip Power5? Se sua demanda
é de 50.000 chips Woods por mês e 25.000 chips Markon por mês,
e sua instalação pode fabricar 150 wafers em um mês, quantos wafers
de cada chip você deveria produzir?
[20/20] <1.6> Seu colega na AMD sugere que, já que o rendimento é tão pobre,
você poderia fabricar chips mais rapidamente se colocasse um núcleo extra no
die e descartasse somente chips nos quais os dois processadores tivessem falhado.
Vamos resolver este exercício vendo o rendimento como a probabilidade
de não ocorrer nenhum defeito em certa área, dada a taxa de defeitos. Calcule
as probabilidades com base em cada núcleo Opteron separadamente
(isso pode não ser inteiramente preciso, já que a equação do rendimento
é baseada em evidências empíricas, e não em um cálculo matemático
relacionando as probabilidades de encontrar erros em partes diferentes do chip).
a. [20] <1.6> Qual é a probabilidade de um defeito ocorrer em somente
um dos núcleos?
b. [10] <1.6> Se o chip antigo custar US$ 20 por unidade, qual será o custo
do novo chip, levando em conta a nova área e o rendimento?
Estudo de caso 2: consumo de potência nos sistemas de computador
Conceitos ilustrados por este estudo de caso
j
j
j
j
Lei de Amdahl
Redundância
MTTF
Consumo de potência
55
56
CAPÍTULO 1 :
Fundamentos do projeto e análise quantitativos
O consumo de potência nos sistemas modernos depende de uma série de fatores, incluindo
a frequência de clock do chip, a eficiência, a velocidade da unidade de disco, a utilização
da unidade de disco e a DRAM. Os exercícios a seguir exploram o impacto sobre a potência
que tem diferentes decisões de projeto e/ou cenários de uso.
1.4
[20/10/20] <1.5> A Figura 1.23 apresenta o consumo de potência de vários
componentes do sistema de computador. Neste exercício, exploraremos como
o disco rígido afeta o consumo de energia para o sistema.
FIGURA 1.23 Consumo de potência de vários componentes do computador.
1.5
1.6
a. [20] <1.5> Considerando a carga máxima para cada componente
e uma eficiência da fonte de alimentação de 80%, que potência, em watts,
a fonte de alimentação do servidor precisa fornecer a um sistema com um chip
Intel Pentium 4 com DRAM Kingston de 2 GB e 240 pinos, e duas unidades
de disco rígido de 7.200 rpm?
b. [10] <1.5> Quanta potência a unidade de disco de 7.200 rpm consumirá
se estiver ociosa aproximadamente 60% do tempo?
c. [20] <1.5> Dado que o tempo de leitura de dados de um drive de disco
de 7.200 rpm será aproximadamente 75% do de um disco de 5.400 rpm,
com qual tempo de inatividade do disco de 7.200 rpm o consumo de energia
será igual, na média, para os dois discos?
[10/10/20] <1.5> Um fator crítico no cálculo de potência de um conjunto
de servidores é o resfriamento. Se o calor não for removido do computador
com eficiência, os ventiladores devolverão ar quente ao computador em vez de ar
frio. Veremos como diferentes decisões de projeto afetam o resfriamento necessário e,
portanto, o preço de um sistema. Use a Figura 1.23 para fazer os cálculos de potência.
a. [10] <1.5> Uma porta de resfriamento para um rack custa US$ 4.000 e
dissipa 14 KW (na sala; um custo adicional é necessário para que saia da sala).
Quantos servidores com processador Intel Pentium 4, DRAM de 1 GB em 240
pinos e um único disco rígido de 7.200 rpm você pode resfriar com uma porta
de resfriamento?
b. [10] <1.5> Você está considerando o fornecimento de tolerância a falhas para a
sua unidade de disco rígido. O RAID 1 dobra o número de discos (Cap. 6). Agora,
quantos sistemas você pode colocar em um único rack com um único cooler?
c. [20] <1.5> Conjunto de servidores típicos pode dissipar no máximo 200 W
por pé quadrado. Dado que um rack de servidor requer 11 pés quadrados
(incluindo espaços na frente e atrás), quantos servidores da parte (a) podem se
colocados em um único rack e quantas portas de resfriamento são necessárias?
[Discussão] <1.8> A Figura 1.24 oferece uma comparação da potência e do
desempenho para vários benchmarks considerando dois servidores: Sun Fire
T2000 (que usa o Niagara) e IBM x346 (que usa processadores Intel Xeon). Essa
Estudos de caso e exercícios por Diana Franklin
FIGURA 1.24 Comparação de potência/desempenho do Sun, conforme informado seletivamente pela Sun.
1.7
informação foi reportada em um site da Sun. Existem duas informações reportadas:
potência e velocidade em dois benchmarks. Para os resultados mostrados, o Sun
Fire T2000 é claramente superior. Que outros fatores poderiam ser importantes
a ponto de fazer alguém escolher o IBM x346 se ele fosse superior nessas áreas?
[20/20/20/20] <1.6, 1.9> Os estudos internos da sua empresa mostram
que um sistema de único núcleo é suficiente para a demanda na sua capacidade
de processamento. Porém, você está pesquisando se poderia economizar potência
usando dois núcleos.
a. [20] <1.9> Suponha que sua aplicação seja 80% paralelizável. Por quanto
você poderia diminuir a frequência e obter o mesmo desempenho?
b. [20] <1.6> Considere que a voltagem pode ser diminuída linearmente com
a frequência. Usando a equação na Seção 1.5, quanta potência dinâmica
o sistema de dois núcleos exigiria em comparação com o sistema de único núcleo?
c. [20] <1.6, 1.9> Agora considere que a tensão não pode cair para menos
de 25% da voltagem original. Essa tensão é conhecida como “piso de tensão”,
e qualquer voltagem inferior a isso perderá o estado. Que porcentagem
de paralelização lhe oferece uma tensão no piso de tensão?
d. [20] <1.6, 1.9> Usando a equação da Seção 1.5, quanta potência dinâmica
o sistema de dois núcleos exigiria em comparação com o sistema de único
núcleo, levando em consideração o piso de tensão?
Exercícios
1.8
[10/15/15/10/10] <1.1,1.5> Um desafio para os arquitetos é que o projeto criado
hoje vai requerer muitos anos de implementação, verificação e testes antes
de aparecer no mercado. Isso significa que o arquiteto deve projetar, muitos anos
antes, o que a tecnologia será. Às vezes, isso é difícil de fazer.
a. [10] <1.4> De acordo com a tendência em escala de dispositivo observada
pela lei de Moore, o número de transistores em 2015 será quantas vezes
o número de transistores em 2005?
b. [10] <1.5> Um dia, o aumento nas frequências de clock acompanhou essa
tendência. Se as frequências de clock tivessem continuado a crescer na mesma
taxa que nos anos 1990, aproximadamente quão rápidas seriam as frequências
de clock em 2015?
c. [15] <1.5> Na taxa de aumento atual, quais são as frequências de clock
projetadas para 2015?
d. [10] <1.4> O que limitou a taxa de aumento da frequência de clock
e o que os arquitetos estão fazendo com os transistores adicionais
para aumentar o desempenho?
e. [10] <1.4> A taxa de crescimento para a capacidade da DRAM também
diminuiu. Por 20 anos, a capacidade da DRAM aumentou em 60% por ano.
Essa taxa caiu para 40% por ano e hoje o aumento é de 25-40% por ano.
57
58
CAPÍTULO 1 :
Fundamentos do projeto e análise quantitativos
Se essa tendência continuar, qual será a taxa de crescimento aproximada para
a capacidade da DRAM em 2020?
1.9 [10/10] <1.5> Você está projetando um sistema para uma aplicação em tempo real
na qual prazos específicos devem ser atendidos. Terminar o processamento mais rápido
não traz nenhum benefício. Você descobre que, na pior das hipóteses, seu sistema
pode executar o código necessário duas vezes mais rápido do que o necessário.
a. [10] <1.5> Quanta energia você economizará se executar na velocidade atual
e desligar o sistema quando o processamento estiver completo?
b. [10] <1.5> Quanta energia você economizará se configurar a voltagem
e a frequência para a metade das atuais?
1.10 [10/10/20/10] <1.5> Conjunto de servidores, como as do Google e do Yahoo!,
fornece capacidade computacional suficiente para a maior taxa de requisições
do dia. Suponha que, na maior parte do tempo, esses servidores operem a 60%
da capacidade. Suponha também que a potência não aumente linearmente com
a carga, ou seja, quando os servidores estão operando a 60% de capacidade,
consomem 90% da potência máxima. Os servidores poderiam ser desligados,
mas levariam muito tempo para serem reiniciados em resposta a mais carga. Foi
proposto um novo sistema, que permite um reinício rápido, mas requer 20%
da potência máxima durante esse estado “quase vivo”.
a. [10] <1.5> Quanta economia de energia seria obtida desligando
60% dos servidores?
b. [10] <1.5> Quanta economia de energia seria obtida colocando
60% dos servidores no estado “quase vivo”?
c. [20] <1.5> Quanta economia de energia seria obtida reduzindo a tensão
em 20% e a frequência em 40%?
d. [20] <1.5> Quanta economia de energia seria obtida colocando
30% dos servidores no estado “quase vivo” e desligando 30%?
1.11 [10/10/20] <1.7> Disponibilidade é a consideração mais importante
para o projeto de servidores, seguida de perto pela escalabilidade e pelo throughput.
a. [10] <1.7> Temos um único processador com falhas no tempo (FIT)
de 100. Qual é o tempo médio para a falha (MTTF) desse sistema?
b. [10] <1.7> Se levar um dia para fazer o sistema funcionar de novo, qual será
a disponibilidade desse sistema?
c. [20] <1.7> Imagine que, para reduzir custos, o governo vai construir um
supercomputador a partir de computadores baratos em vez de computadores
caros e confiáveis. Qual é o MTTF para um sistema com 1.000 processadores?
Suponha que, se um falhar, todos eles falharão.
1.12 [20/20/20] <1.1, 1.2, 1.7> Em conjunto de servidores como os usadas pela
Amazon e pelo eBay, uma única falha não faz com que todo o sistema deixe
de funcionar. Em vez disso, ela vai reduzir o número de requisições que podem
ser satisfeitas em dado momento.
a. [20] <1.7> Se uma companhia tem 10.000 computadores, cada qual
com um MTTF de 35 dias, e sofre uma falha catastrófica somente quando
1/3 dos computadores falham, qual é o MTTF do sistema?
b. [20] <1.1, 1.7> Se uma companhia tivesse US$ 1.000 adicionais,
por computador, para dobrar o MTTF, essa seria uma boa decisão de negócio?
Mostre seu trabalho.
c. [20] <1.2> A Figura 1.3 mostra a média dos custos dos tempos de paralisação,
supondo que o custo é igual durante o ano todo. Para os varejistas, entretanto,
a época de Natal é a mais lucrativa (e, portanto, a mais prejudicada pela perda
de vendas). Se um centro de vendas por catálogo tiver duas vezes mais tráfego
Estudos de caso e exercícios por Diana Franklin
1.13
1.14
1.15
1.16
no quarto trimestre do que em qualquer outro, qual será o custo médio
do tempo de paralisação por hora no quarto trimestre e no restante do ano?
[10/20/20] <1.9> Suponha que sua empresa esteja tentando decidir
entre adquirir o Opteron e adquirir o Itanium 2. Você analisou as aplicações
da sua empresa e notou que em 60% do tempo ela estará executando
aplicações similares ao wupwise, em 20% do tempo aplicações similares
ao ammp e em 20% do tempo aplicações similares ao apsi.
a. [10] Se você estivesse escolhendo somente com base no desempenho
SPEC geral, qual seria a escolha e por quê?
b. [20] Qual é a média ponderada das taxas de tempo de execução para esse mix
de aplicações para o Opteron e o Itanium 2?
c. [20] Qual é o ganho de velocidade do Opteron sobre o Itanium 2?
[20/10/10/10/15] <1.9> Neste exercício, suponha que estejamos considerando
melhorar uma máquina adicionando a ela hardware vetorial. Quando
um processamento é executado em modo vetor nesse hardware, é 10 vezes mais
rápido do que o modo original de execução. Chamamos porcentagem de vetorização
a porcentagem de tempo que seria gasta usando o modo vetorial. Vetores serão
discutidos no Capítulo 4, mas você não precisa saber nada sobre como eles
funcionam para responder a esta questão!
a. [20] <1.9> Trace um gráfico que plote o ganho de velocidade como uma
porcentagem do processamento realizada em modo vetor. Chame o eixo y
de “Ganho médio de velocidade” e o eixo x de “Porcentagem de vetorização”.
b. [10] <1.9> Que porcentagem de vetorização é necessária para atingir um
ganho de velocidade de 2?
c. [10] <1.9> Que porcentagem do tempo de execução do processamento será
gasto no modo vetorial se um ganho de velocidade de 2 for alcançado?
d. [10] <1.9> Que porcentagem de vetorização é necessária para atingir metade
do ganho de velocidade que pode ser obtido usando o modo vetorial?
e. [15] <1.9> Suponha que você tenha descoberto que a porcentagem
de vetorização do programa é de 70%. O grupo de projeto de hardware estima que
pode acelerar ainda mais o hardware vetorial com significativo investimento
adicional. Você imagina, em vez disso, que a equipe do compilador poderia
aumentar a porcentagem de vetorização. Que porcentagem de vetorização
a equipe do compilador precisa atingir para igualar uma adição de 2x no ganho
de velocidade na unidade vetorial (além dos 10x iniciais)?
[15/10] <1.9> Suponha que tenha sido feita uma melhoria em um computador
que aumente algum modo de execução por um fator de 10. O modo melhorado
é usado em 50% do tempo e medido como uma porcentagem do tempo
de execução quando o modo melhorado está em uso. Lembre-se de que a lei de Amdahl
depende da fração de tempo de execução original e não melhorado que poderia
fazer uso do modo melhorado. Assim, não podemos usar diretamente
essa medida de 50% para calcular o ganho de velocidade com a lei de Amdahl.
a. [15] <1.9> Qual é o ganho de velocidade que obtemos do modo rápido?
b. [10] <1.9> Que porcentagem do tempo de execução original foi convertida
para o modo rápido?
[20/20/15] <1.9> Muitas vezes, ao fazermos modificações para otimizar parte
de um processador, o ganho de velocidade em um tipo de instrução ocorre
à custa de reduzir a velocidade de algo mais. Por exemplo, se adicionarmos uma
complicada unidade de ponto flutuante que ocupe espaço e algo tiver de ser
afastado do centro para acomodá-la, adicionar um ciclo extra de atraso para atingir
essa unidade. A equação básica da lei de Amdahl não leva em conta essa troca.
59
60
CAPÍTULO 1 :
Fundamentos do projeto e análise quantitativos
a. [20] <1.9> Se a nova unidade rápida de ponto flutuante acelerar as operações
do ponto flutuante numa média de 2x e as operações de ponto flutuante
ocuparem 20% do tempo de execução do programa original, qual será
o ganho geral de velocidade (ignorando a desvantagem de quaisquer outras
instruções)?
b. [20] <1.9> Agora suponha que a aceleração da unidade de ponto flutuante
reduziu a velocidade dos acessos à cache de dados, resultando em uma
redução de velocidade de 1,5x (ou em ganho de velocidade de 2/3).
Os acessos à cache de dados consomem 10% do tempo de execução.
Qual é o ganho geral de velocidade agora?
c. [15] <1.9> Depois de implementar as novas operações de ponto flutuante,
que porcentagem do tempo de execução é gasto em operações desse tipo? Que
porcentagem é gasta em acessos à cache de dados?
1.17 [10/10/20/20] <1.10> Sua empresa acabou de comprar um novo processador
Intel Core i5 e você foi encarregado de otimizar seu software para esse
processador. Você executará duas aplicações nesse Pentium dual, mas os requisitos
de recursos não são iguais. A primeira aplicação precisa de 80% dos recursos e
a outra de apenas 20% dos recursos. Suponha que, quando você paraleliza uma
parte do programa, o ganho de velocidade para essa parte seja de 2.
a. [10] <1.10> Se 40% da primeira aplicação fosse paralelizável, quanto ganho
de velocidade você obteria com ela se fosse executada isoladamente?
b. [10] <1.10> Se 99% da segunda aplicação fosse paralelizável, quanto ganho
de velocidade ela observaria se fosse executada isoladamente?
c. [20] <1.10> Se 40% da primeira aplicação fosse paralelizável, quanto ganho
de velocidade geral do sistema você observaria se a paralelizasse?
d. [20] <1.10> Se 99% da segunda aplicação fosse paralelizável, quanto ganho
de velocidade geral do sistema você obteria?
1.18 [10/20/20/20/25] <1.10> Ao paralelizar uma aplicação, o ganho de velocidade
ideal é feito pelo número de processadores. Isso é limitado por duas coisas:
a porcentagem da aplicação que pode ser paralelizada e o custo da comunicação.
A lei de Amdahl leva em conta a primeira, mas não a segunda.
a. [10] <1.10> Qual será o ganho de velocidade com N processadores se 80%
da aplicação puder ser paralelizada, ignorando o custo de comunicação?
b. [20] <1.10> Qual será o ganho de velocidade com oito processadores se,
para cada processador adicionado, o custo adicional de comunicação
for de 0,5% do tempo de execução original?
c. [20] <1.10> Qual será o ganho de velocidade com oito processadores se, cada
vez que o número de processadores for dobrado, o custo adicional
de comunicação for aumentado em 0,5% do tempo de execução original?
d. [20] <1.10> Qual será o ganho de velocidade com N processadores se, cada
vez que o número de processadores for dobrado, o custo adicional
de comunicação for aumentado em 0,5% do tempo de execução original?
e. [25] <1.10> Escreva a equação geral que resolva esta questão: qual
é o número de processadores com o maior ganho de velocidade em uma
aplicação na qual P% do tempo de execução original é paralelizável e,
para cada vez que o número de processadores for dobrado, a comunicação
será aumentada em 0,5% do tempo de execução original?
CAP ÍTULO 2
Projeto de hierarquia de memória
O ideal seria uma capacidade de memória indefinidamente grande, de modo que
qualquer palavra em particular […] pudesse estar imediatamente disponível […]
Somos […] forçados a reconhecer a possibilidade de construir uma hierarquia de
memórias, cada qual com maior capacidade que a anterior, porém com acesso
mais lento que a outra.
A. W. Burks, H. H. Goldstine e J. von Neumann, Preliminary Discussion of the Logical
Design of na Eletronic Computing Instrument (1946)
2.1 Introdução .............................................................................................................................................61
2.2 Dez otimizações avançadas de desempenho de cachê .....................................................................67
2.3 Tecnologia de memória e otimizações ................................................................................................83
2.4 Proteção: memória virtual e máquinas virtuais .................................................................................91
2.5 Questões cruzadas: o projeto de hierarquias de memória ................................................................97
2.6 Juntando tudo: hierarquia de memória no ARM Cortex-A8 e Intel Core i7 ....................................98
2.7 Falácias e armadilhas .........................................................................................................................107
2.8 Comentários finais: olhando para o futuro .......................................................................................113
2.9 Perspectivas históricas e referências ................................................................................................114
Estudos de caso com exercícios por Norman P. Jouppi, Naveen Muralimanohar e Sheng Li.............114
2.1
INTRODUÇÃO
Os pioneiros do computador previram corretamente que os programadores desejariam
uma quantidade ilimitada de memória rápida. Uma solução econômica para isso é a
hierarquia de memória, que tira proveito da localidade e da relação custo-desempenho
das tecnologias de memória. O princípio da localidade, apresentado no Capítulo 1, afirma
que a maioria dos programas não acessa todo o código ou dados uniformemente. A
localidade ocorre no tempo (localidade temporal) e no espaço (localidade espacial). Esse
princípio, junto com a noção de que um hardware menor pode se tornar mais rápido, levou às hierarquias baseadas em memórias de diferentes velocidades e tamanhos.
A Figura 2.1 mostra uma hierarquia de memória multinível, incluindo os valores típicos
do tamanho e da velocidade de acesso.
Como a memória rápida também é cara, uma hierarquia de memória é organizada em
vários níveis — cada qual menor, mais rápido e mais caro por byte do que o nível inferior
seguinte. O objetivo é oferecer um sistema de memória com custo por unidade quase tão
baixo quanto o nível de memória mais barato e velocidade quase tão rápida quanto o
nível mais rápido. Na maioria dos casos (mas nem sempre), os dados contidos em um
nível inferior são um subconjunto do nível superior seguinte. Essa propriedade, chamada
61
62
CAPÍTULO 2 :
Projeto de hierarquia de memória
FIGURA 2.1 Os níveis em uma hierarquia de memória em um servidor (a) e em um dispositivo pessoal móvel (PMD) (b).
À medida que nos distanciamos do processador, a memória no nível abaixo se torna mais lenta e maior. Observe que as unidades de tempo mudam por
fatores de 109 — de picossegundos para milissegundos — e que as unidades de tamanho mudam por fatores de 1012 — de bytes para terabytes.
O PMD tem uma taxa de clock menor e as caches e a memória principal menores. Uma diferença-chave é que os servidores e desktops usam
armazenamento de disco como o nível mais baixo na hierarquia, enquanto os PMDs usam memória Flash, construída a partir de tecnologia EEPROM.
propriedade de inclusão, é sempre necessária para o nível mais baixo da hierarquia, que
consiste na memória principal, no caso das caches, e na memória de disco, no caso da
memória virtual.
A importância da hierarquia de memória aumentou com os avanços no desempenho dos
processadores. A Figura 2.2 ilustra as projeções do desempenho do processador contra a
melhoria histórica de desempenho no tempo para acessar a memória principal. A linha
do processador mostra o aumento na média das requisições de memória por segundo (ou
seja, o inverso da latência entre referências de memória), enquanto a linha da memória
mostra o aumento nos acessos por segundo à DRAM (ou seja, o inverso da latência de
acesso à DRAM). Na verdade, a situação em um uniprocessador é um pouco pior, uma
vez que o pico na taxa de acesso à memória é mais rápido do que a taxa média, que é o
que é mostrado.
Mais recentemente, processadores de alto nível progrediram para múltiplos núcleos,
aumentando mais os requisitos de largura de banda em comparação com os núcleos
únicos. De fato, o pico de largura de banda agregada essencialmente aumenta conforme o
número de núcleos aumenta. Um processador de alto nível moderno, como o Intel Core
i7, pode gerar duas referências de memória de dados por núcleo a cada ciclo de clock.
Com quatro núcleos em uma taxa de clock de 3,2 GHz, o i7 pode gerar um pico de 25,6
bilhões de referências a dados de 64 bits por segundo, além de um pico de demanda de
instruções de cerca de 12,8 bilhões de referências a instruções de 128 bits. Isso é um pico
2.1
FIGURA 2.2 Começando com o desempenho de 1980 como uma linha base, a distância do desempenho,
medida como a diferença entre os requisitos da memória dos processadores (para um uniprocessador
ou para um core) e a latência de um acesso à DRAM, é desenhada contra o tempo.
Observe que o eixo vertical precisa estar em uma escala logarítmica para registrar o tamanho da diferença
de desempenho processador-DRAM. A linha-base da memória é de 64 KB de DRAM em 1980, com uma
melhoria de desempenho de 1,07 por ano na latência (Fig. 2.13, na página 85). A linha do processador pressupõe uma
melhoria de 1,25 por ano até 1986, uma melhoria de 1,52 até 2000, uma melhoria de 1,20 entre 2000 e 2005,
e nenhuma mudança no desempenho do processador (tendo por base um núcleo por core) entre 2005 e 2010
(Fig. 1.1, no Cap. 1).
de largura de banda total de 409,6 GB/s! Essa incrível largura de banda é alcançada pelo
multiporting e pelo pipelining das caches; pelo uso de níveis múltiplos de caches, usando
caches de primeiro — e às vezes segundo — nível separados por núcleo; e pelo uso de
caches de dados e instruções separados no primeiro nível. Em contraste, o pico de largura
de banda para a memória principal DRAM é de somente 6% desse valor (25 GB/s).
Tradicionalmente, os projetistas de hierarquias de memória se concentraram em otimizar
o tempo médio de acesso à memória, que é determinado pelo tempo de acesso à cache,
taxa de falta e penalidade por falta. Mais recentemente, entretanto, a potência tornou-se
uma importante consideração. Em microprocessadores de alto nível, pode haver 10 MB
ou mais de cache no chip, e uma grande cache de segundo — ou terceiro — nível vai
consumir potência significativa, tanto como fuga, quando ele não está operando (chamada
potência estática) quanto como potência ativa quando uma leitura ou gravação é realizada
(chamada potência dinâmica), como descrito na Seção 2.3. O problema é ainda mais
sério em processadores em PMDs, nos quais a CPU é menos agressiva e a necessidade de
potência pode ser 20-50 vezes menor. Nesses casos, as caches podem ser responsáveis por
25-50% do consumo total de potência. Assim, mais projetos devem considerar a relação
de desempenho e da potência, que serão examinados neste capítulo.
O básico das hierarquias de memória: uma revisão rápida
O tamanho crescente e, portanto, a importância dessa diferença levou à migração dos
fundamentos de hierarquia de memória para os cursos de graduação em arquitetura de
computador e até mesmo para cursos de sistemas operacionais e compiladores. Assim, começaremos com uma rápida revisão das caches e sua operação. Porém, este capítulo descreve
inovações mais avançadas, que focam a diferença de desempenho processador-memória.
Quando uma palavra não é encontrada na cache, ela precisa ser recuperada de um nível
inferior na hierarquia (que pode ser outra cache ou a memória principal) e colocada na
Introdução
63
64
CAPÍTULO 2 :
Projeto de hierarquia de memória
cache antes de continuar. Múltiplas palavras, chamadas bloco (ou linha), são movidas por
questões de eficiência, e porque elas provavelmente serão necessárias em breve, devido
à localização espacial. Cada bloco da I-cache inclui uma tag para ver a qual endereço de
memória ela corresponde.
Uma decisão de projeto importante é em que parte da cache os blocos (ou linhas) podem
ser colocados. O esquema mais popular é a associação por conjunto (set associative), em que
um conjunto é um grupo de blocos na cache. Primeiro, um bloco é mapeado para um
conjunto; depois pode ser colocado em qualquer lugar dentro desse conjunto. Encontrar
um bloco consiste primeiramente em mapear o endereço do bloco para o conjunto e
depois em examinar o conjunto — normalmente em paralelo — para descobrir o bloco.
O conjunto é escolhido pelo endereço dos dados:
(Endereçodo bloco)MOD(Númerode conjuntos da cache)
Se houver n blocos em um conjunto, o posicionamento da cache será denominado associativo por conjunto com n vias (n-way set associative). As extremidades da associatividade
do conjunto têm seus próprios nomes. Uma cache mapeada diretamente tem apenas um
bloco por conjunto (de modo que um bloco sempre é colocado no mesmo local), e uma
cache totalmente associativa tem apenas um conjunto (de modo que um bloco pode ser
colocado em qualquer lugar).
O caching de dados que são apenas lidos é fácil, pois as cópias na cache e na memória são
idênticas. O caching de escritas é mais difícil: como a cópia na cache e na memória pode
ser mantida consistente? Existem duas estratégias principais. A write-through, que atualiza
o item na cache e também escreve na memória principal, para atualizá-la. A write-back só
atualiza a cópia na cache. Quando o bloco está para ser atualizado, ele é copiado de volta
na memória. As duas estratégias de escrita podem usar um buffer de escrita para permitir
que a cache prossiga assim que os dados forem colocados no buffer, em vez de esperar a
latência total para escrever os dados na memória.
Uma medida dos benefícios de diferentes organizações de cache é a taxa de falta. A taxa
de falta (miss rate) é simplesmente a fração de acessos à cache que resulta em uma falta,
ou seja, o número de acessos em que ocorre a falta dividido pelo número total de acessos.
Para entender as causas das altas taxas de falta, que podem inspirar projetos de cache
melhores, o modelo dos três C classifica todas as faltas em três categorias simples:
j
j
j
Compulsória. O primeiro acesso a um bloco não pode estar na cache, de modo
que o bloco precisa ser trazido para a cache. As faltas compulsórias são aquelas
que ocorrem mesmo que se tenha uma I-cache infinita.
Capacidade. Se a cache tiver todos os blocos necessários durante a execução
de≈um programa, as faltas por capacidade (além das faltas compulsórias) ocorrerão
porque os blocos são descartados e mais tarde recuperados.
Conflito. Se a estrutura de colocação do bloco não for totalmente associativa, faltas
por conflito (além das faltas compulsórias e de capacidade) ocorrerão porque um
bloco pode ser descartado e mais tarde recuperado se os blocos em conflito forem
mapeados para o seu conjunto e os acessos aos diferentes blocos forem intercalados.
As Figuras B.8 e B.9, nas páginas B-21 e B-22, mostram a frequência relativa das faltas de
cache desmembradas pelos “três C”. Como veremos nos Capítulos 3 e 5, o multithreading
e os múltiplos núcleos acrescentam complicações para as caches, tanto aumentando o
potencial para as faltas de capacidade quanto acrescentando um quarto C para as faltas de
coerência advindas de esvaziamentos de cache, a fim de manter múltiplas caches coerentes
em um multiprocessador. Vamos considerar esses problemas no Capítulo 5.
2.1
Infelizmente, a taxa de falta pode ser uma medida confusa por vários motivos. Logo,
alguns projetistas preferem medir as faltas por instrução em vez das faltas por referência de
memória (taxa de falta). Essas duas estão relacionadas:
Taxas de perdas × Acessos à memória
Acessos à memória
Perdas
=
= Taxa de perda ×
Instrução
Instrução
Contagemde instruções
(Normalmente são relatadas como faltas por 1.000 instruções, para usar inteiros no lugar
de frações.)
O problema com as duas medidas é que elas não levam em conta o custo de uma falta.
Uma medida melhor é o tempo de acesso médio à memória:
Tempode acesso médioà memória = Tempode acerto + Taxa de falta × Penalidade de falta
onde tempo de acerto é o tempo de acesso quando o item acessado está na cache e penalidade
de falta é o tempo para substituir o bloco de memória (ou seja, o custo de uma falta). O
tempo de acesso médio à memória ainda é uma medida indireta do desempenho; embora
sendo uma medida melhor do que a taxa de falta, ainda não é um substituto para o tempo
de execução. No Capítulo 3, veremos que os processadores especulativos podem executar
outras instruções durante uma falta, reduzindo assim a penalidade efetiva de falta. O uso
de multithreading (apresentado no Cap. 3) também permite que um processador tolere
faltas sem ser forçado a ficar inativo. Como veremos em breve, para tirar vantagem de tais
técnicas de tolerância de latência, precisamos de caches que possam atender requisições
e, ao mesmo tempo, lidar com uma falta proeminente.
Se este material é novo para você ou se esta revisão estiver avançando muito rapidamente,
consulte o Apêndice B. Ele aborda o mesmo material introdutório com profundidade e
inclui exemplos de caches de computadores reais e avaliações quantitativas de sua eficácia.
A Seção B.3, no Apêndice B, também apresenta seis otimizações de cache básicas, que
revisamos rapidamente aqui. O apêndice oferece exemplos quantitativos dos benefícios
dessas otimizações.
1. Tamanho de bloco maior para reduzir a taxa de falta. O modo mais simples de reduzir
a taxa de falta é tirar proveito da proximidade espacial e aumentar o tamanho
do bloco. Blocos maiores reduzem as faltas compulsórias, mas também aumentam
a penalidade da falta. Já que blocos maiores diminuem o número de tags, eles
podem reduzir ligeiramente a potência estática. Blocos de tamanhos maiores
também podem aumentar as faltas por capacidade ou conflito, especialmente
em caches menores. Selecionar o tamanho de bloco correto é uma escolha
complexa que depende do tamanho da cache e da penalidade de falta.
2. Caches maiores para reduzir a taxa de falta. O modo óbvio de reduzir as faltas
por capacidade é aumentar a capacidade da cache. As desvantagens incluem
o tempo de acerto potencialmente maior da memória de cache maior, além de custo
e consumo de potência mais altos. Caches maiores aumentam tanto a potência
dinâmica quanto a estática.
3. Associatividade mais alta para reduzir a taxa de falta. Obviamente, aumentar
a associatividade reduz as faltas por conflito. Uma associatividade maior
pode ter o custo de maior tempo de acerto. Como veremos em breve,
a associatividade também aumenta o potência.
4. Caches multiníveis para reduzir a penalidade de falta. Uma decisão difícil é a de tornar
o tempo de acerto da cache rápido, para acompanhar a taxa de clock crescente dos
processadores ou tornar a cache grande, para contornar a grande diferença entre o
Introdução
65
66
CAPÍTULO 2 :
Projeto de hierarquia de memória
FIGURA 2.3 Os tempos de acesso geralmente aumentam conforme o tamanho da cache e a
associatividade aumentam.
Esses dados vêm do CACTI modelo 6.5 de Tarjan, Thoziyoor e Jouppi (2005). O dado supõe um tamanho característico
de 40 nm (que está entre a tecnologia usada nas versões mais rápida e segunda mais rápida do Intel i7 e igual
à tecnologia usada nos processadores AMD embutidos mais velozes), um único banco e blocos de 64 bytes.
As suposições sobre o leiaute da cache e as escolhas complexas entre atrasos de interconexão (que dependem
do tamanho do bloco de cache sendo acessado) e o custo de verificações de tag e multiplexação levaram a resultados
que são ocasionalmente surpreendentes, como o menor tempo de acesso de uma associatividade por conjunto de
duas vias com 64 KB em comparação com o mapeamento direto. De modo similar, os resultados com associatividade
por conjunto de oito vias gera um comportamento incomum conforme o tamanho da cache aumenta. Uma vez que tais
observações são muito dependentes da tecnologia e suposições detalhadas de projeto, ferramentas como o CACTI
servem para reduzir o espaço de busca, e não para uma análise precisa das opções.
processador e a memória principal. A inclusão de outro nível de cache entre a cache
original e a memória simplifica a decisão (Fig. 2.3). A cache de primeiro nível pode
ser pequena o suficiente para combinar com um tempo de ciclo de clock rápido,
enquanto a cache de segundo nível pode ser grande o suficiente para capturar
muitos acessos que iriam para a memória principal. O foco nas faltas nas caches
de segundo nível leva a blocos maiores, capacidade maior e associatividade mais
alta. Se L1 e L2 se referem, respectivamente, às caches de primeiro e segundo níveis,
podemos redefinir o tempo de acesso médio à memória:
TempoacertoL1 + Taxa falta L1 × (TempoacertoL2 + Taxa falta L2 × Penalidade falta L2 )
5. Dar prioridade às faltas de leitura, em vez de escrita, para reduzir a penalidade de falta.
Um buffer de escrita é um bom lugar para implementar essa otimização. Os buffers
de escrita criam riscos porque mantêm o valor atualizado de um local necessário
em uma falta de leitura, ou seja, um risco de leitura após escrita pela memória. Uma
solução é verificar o conteúdo do buffer de escrita em uma falta de leitura. Se não
houver conflitos e se o sistema de memória estiver disponível, o envio da leitura
antes das escritas reduzirá a penalidade de falta. A maioria dos processadores dá
prioridade às leituras em vez de às escritas. Essa escolha tem pouco efeito sobre
o consumo de potência.
2.2
Dez otimizações avançadas de desempenho da cache
6. Evitar tradução de endereço durante a indexação da cache para reduzir o tempo de acerto.
As caches precisam lidar com a tradução de um endereço virtual do processador
para um endereço físico para acessar a memória (a memória virtual é explicada
nas Seções 2.4 e B.4). Uma otimização comum é usar o offset de página — a parte
idêntica nos endereços virtual e físico — para indexar a cache, como descrito no
Apêndice B, página B-34. Esse método de índice virtual/tag físico introduz algumas
complicações de sistema e/ou limitações no tamanho e estrutura da cache L1, mas
as vantagens de remover o acesso ao translation buffer lookaside (TLB) do caminho
crítico supera as desvantagens.
Observe que cada uma dessas seis otimizações possui uma desvantagem em potencial, que
pode levar a um tempo de acesso médio à memória ainda maior em vez de diminuí-lo.
O restante deste capítulo considera uma familiaridade com o material anterior e os detalhes apresentados no Apêndice B. Na seção “Juntando tudo”, examinamos a hierarquia
de memória para um microprocessador projetado para um servidor de alto nível, o Intel
Core i7, além de um projetado para uso em um PMD, o Arm Cortex-A8, que é a base para
o processador usado no Apple iPad e diversos smartphones de alto nível. Dentro de cada
uma dessas classes existe significativa diversidade na abordagem, devido ao uso planejado
do computador. Embora o processador de alto nível usado no servidor tenha mais núcleos e caches maiores do que os processadores Intel projetados para usos em desktop,
os processadores têm arquiteturas similares. As diferenças são guiadas pelo desempenho
e pela natureza da carga de trabalho. Computadores desktop executam primordialmente
um aplicativo por vez sobre um sistema operacional para um único usuário, enquanto
computadores servidores podem ter centenas de usuários rodando dúzias de aplicações ao
mesmo tempo. Devido a essas diferenças na carga de trabalho, os computadores desktop
geralmente se preocupam mais com a latência média da hierarquia de memória, enquanto
os servidores se preocupam também com a largura de banda da memória. Mesmo dentro
da classe de computadores desktop, existe grande diversidade entre os netbooks, que vão
desde os de baixo nível com processadores reduzidos mais similares aos encontrados
em PMDs de alto nível até desktops de alto nível cujos processadores contêm múltiplos
núcleos e cuja organização lembra a de um servidor de baixo nível.
Em contraste, os PMDs não só atendem a um usuário, mas geralmente também têm sistemas operacionais menores, geralmente menos multitasking (a execução simultânea de
diversas aplicações) e aplicações mais simples. Em geral, os PMDs também usam memória
Flash no lugar de discos, e a maioria considera tanto o desempenho quanto o consumo
de energia, que determina a vida da bateria.
2.2 DEZ OTIMIZAÇÕES AVANÇADAS DE DESEMPENHO
DA CACHE
A fórmula do tempo médio de acesso à memória, dada anteriormente, nos oferece três
medidas para otimizações da cache: tempo de acerto, taxa de falta e penalidade de falta.
Dadas as tendências atuais, adicionamos largura de banda da cache e consumo de potência
a essa lista. Podemos classificar as 10 otimizações avançadas de cache que vamos examinar
em cinco categorias baseadas nessas medidas:
1. Reduzir o tempo de acerto: caches de primeiro nível pequenas e simples e previsão
de vias (way-prediction). Ambas as técnicas diminuem o consumo de potência.
2. Aumentar a largura de banda da cache: caches em pipeline, caches em multibanco
e caches de não bloqueio. Essas técnicas têm impacto variado sobre o consumo
de potência.
67
68
CAPÍTULO 2 :
Projeto de hierarquia de memória
3. Reduzir a penalidade de falta: primeira palavra crítica e utilização de write buffer
merges. Essas otimizações têm pouco impacto sobre a potência.
4. Reduzir a taxa de falta: otimizações do compilador. Obviamente, qualquer melhoria
no tempo de compilação melhora o consumo de potência.
5. Reduzir a penalidade de falta ou a taxa de falta por meio do paralelismo: pré-busca
do hardware e pré-busca do compilador. Essas otimizações geralmente aumentam
o consumo de potência, principalmente devido aos dados pré-obtidos que não são
usados.
Em geral, a complexidade do hardware aumenta conforme prosseguimos por essas otimizações. Além disso, várias delas requerem uma tecnologia complexa de compiladores.
Concluíremos com um resumo da complexidade da implementação e os benefícios das
10 técnicas (Fig. 2.11, na página 82) para o desempenho. Uma vez que algumas delas são
bastantes diretas, vamos abordá-las rapidamente. Outras, contudo, requerem descrições
mais detalhadas.
Primeira otimização: caches pequenas e simples
para reduzir o tempo de acerto e a potência
A pressão de um ciclo de clock rápido e das limitações de consumo de potência encoraja o
tamanho limitado das caches de primeiro nível. Do mesmo modo, o uso de níveis menores
de associatividade pode reduzir tanto o tempo de acerto quanto a potência, embora tais
relações sejam mais complexas do que aquelas envolvendo o tamanho.
O caminho crítico de tempo em um acerto de cache é um processo, em três etapas,
de endereçar a memória de tag usando a parte do índice do endereço, comparar o valor de
tag de leitura ao endereço e configurar o multiplexador para selecionar o item de dados
correto se a cache for configurada como associativa. Caches mapeadas diretamente podem
sobrepor a verificação de tag à transmissão dos dados, reduzindo efetivamente o tempo
de acerto. Além do mais, níveis inferiores de associatividade geralmente vão reduzir o
consumo de potência, porque menos linhas de cache devem ser acessadas.
Embora a quantidade de cache no chip tenha aumentado drasticamente com as novas
gerações de microprocessadores, graças ao impacto da taxa de clock devido a uma cache L1
maior, recentemente o tamanho das caches aumentou muito pouco ou nada. Em muitos
processadores recentes, os projetistas optaram por maior associatividade em vez de caches
maiores. Uma consideração adicional na escolha da associatividade é a possibilidade de
eliminar as instâncias de endereços (address aliases). Vamos discutir isto em breve.
Uma técnica para determinar o impacto sobre o tempo de acerto e a potência antes da
montagem de um chip é a utilização de ferramentas CAD. O CACTI é um programa
para estimar o tempo de acesso e a potência de estruturas de cache alternativas nos
microprocessadores CMOS, dentro de 10% das ferramentas de CAD mais detalhadas.
Para determinada característica de tamanho mínimo, o CACTI estima o tempo de acerto
das caches quando variam o tamanho da cache, a associatividade e o número de portas
de leitura/escrita e parâmetros mais complexos. A Figura 2.3 mostra o impacto estimado
sobre o tempo de acerto quando o tamanho da cache e a associatividade são variados.
Dependendo do tamanho da cache, para esses parâmetros o modelo sugere que o tempo
de acerto para o mapeamento direto é ligeiramente mais rápido do que a associatividade
por conjunto com duas vias, que a associatividade por conjunto com duas vias é 1,2 vez
mais rápida do que com quatro vias, e com quatro vias é 1,4 vez mais rápido do que a
associatividade com oito vias. Obviamente, essas estimativas dependem da tecnologia e
do tamanho da cache.
2.2
Exemplo
Resposta
Dez otimizações avançadas de desempenho da cache
Usando os dados da Figura B.8, no Apêndice B, e da Figura 2.3, determine se
uma cache L1 de 32 KB, associativa por conjunto com quatro vias tem tempo
de acesso à memória mais rápido do que uma cache L1 de 32 KB, associativa
por conjunto com quatro vias. Suponha que a penalidade de falta para a
cache L2 seja 15 vezes o tempo de acesso para a cache L1 mais rápido. Ignore
as faltas além de L2. Qual é o tempo médio de acesso à memória mais rápido?
Seja o tempo de acesso para cache com associatividade por conjunto de duas
vias igual a 1. Então, para a cache de duas vias:
Tempodeacesso médioà memória 2vias = Tempodeacerto + Tempode falta × Penalidadede falta
= 1 + 0,038 × 15 = 1,38
Para a cache de quatro vias, o tempo de clock é 1,4 vez maior. O tempo gasto
da penalidade de falta é 15/1,4 = 10,1. Por simplicidade, assuma que ele é
igual a 10:
Tempodeacesso médioà memória 4vias = Tempodeacerto2vias × 1,4 + Taxa de perda × Penalidade de falta
= 1,4 + 0,037 × 10 = 1,77
Obviamente, a maior associatividade parece uma troca ruim. Entretanto,
uma vez que o acesso à cache nos processadores modernos muitas vezes é
pipelined, o impacto exato sobre o tempo do ciclo de clock é difícil de avaliar.
O consumo de energia também deve ser considerado na escolha tanto do tamanho da cache
como na da associatividade, como mostra a Figura 2.4. O custo energético da maior associatividade varia desde um fator de mais de 2 até valores irrelevantes, em caches de 128 KB ou
256 KB, indo do mapeado diretamente para associatividade por conjunto de duas vias.
Em projetos recentes, três fatores levaram ao uso de maior associatividade nas caches
de primeiro nível: 1) muitos processadores levam pelo menos dois ciclos de clock para
acessar a cache, por isso o impacto de um período de tempo mais longo pode não ser
FIGURA 2.4 O consumo de energia por leitura aumenta conforme aumentam o tamanho da cache e a
associatividade.
Como na figura anterior, o CACTI é usado para o modelamento com os mesmos parâmetros tecnológicos. A grande
penalidade para as caches associativas por conjunto de oito vias é decorrente do custo de leitura de oito tags e aos
dados correspondentes em paralelo.
69
70
CAPÍTULO 2 :
Projeto de hierarquia de memória
crítico; 2) para manter o TLB fora do caminho crítico (um atraso que seria maior do que
aquele associado à maior associatividade), embora todas as caches L1 devam ser indexadas
virtualmente. Isso limita o tamanho da cache para o tamanho da página vezes a associatividade, porque somente os bits dentro da página são usados para o índice. Existem outras
soluções para o problema da indexação da cache antes que a tradução do endereço seja
completada, mas aumentar a associatividade, que também tem outros benefícios, é a mais
atraente; 3) com a introdução do multithreading (Cap. 3), as faltas de conflito podem
aumentar, tornando a maior associatividade mais atraente.
Segunda otimização: previsão de via para reduzir
o tempo de acesso
Outra técnica reduz as faltas por conflito e ainda mantém a velocidade de acerto da cache
mapeada diretamente. Na previsão de via, bits extras são mantidos na cache para prever a
via ou o bloco dentro do conjunto do próximo acesso à cache. Essa previsão significa que
o multiplexador é acionado mais cedo para selecionar o bloco desejado, e apenas uma
comparação de tag é realizada nesse ciclo de clock, em paralelo com a leitura dos dados
da cache. Uma falta resulta na verificação de outros blocos em busca de combinações no
próximo ciclo de clock.
Acrescentados a cada bloco de uma cache estão os bits de previsão de bloco. Os bits
selecionam quais dos blocos experimentar no próximo acesso à cache. Se a previsão for
correta, a latência de acesso à cache será o tempo de acerto rápido. Se não, ele tentará
o outro bloco, mudará o previsor de via e terá uma latência extra de um ciclo de clock.
As simulações sugeriram que a exatidão da previsão de conjunto excede 90% para um
conjunto de duas vias e em 80% para um conjunto de quatro vias, com melhor precisão
em caches de instruções (I-caches) do que em caches de dados (D-caches). A previsão de
via gera menos tempo médio de acesso à memória para um conjunto de duas vias se ele
for pelo menos 10% mais rápido, o que é muito provável. A previsão de via foi usada
pela primeira vez no MIPS R10000 em meados dos anos 1990. Ela é muito popular em
processadores que empregam associatividade por conjunto de duas vias e é usada no ARM
Cortex-A8 com caches associativas por conjunto de quatro vias. Para processadores muito
rápidos, pode ser desafiador implementar o atraso de um ciclo que é essencial para manter
uma penalidade pequena de previsão de via.
Uma forma estendida de previsão de via também pode ser usada para reduzir o consumo
de energia usando os bits de previsão de via para decidir que bloco de cache acessar na
verdade (os bits de previsão de via são essencialmente bits de endereço adicionais). Essa
abordagem, que pode ser chamada seleção de via, economiza energia quando a previsão de
via está correta, mas adiciona um tempo significativo a uma previsão incorreta de via, já
que o acesso, e não só a comparação e a seleção de tag, deve ser repetida. Tal otimização
provavelmente faz sentido somente em processadores de baixa potência. Inoue, Ishihara
e Muramaki (1999) estimaram que o uso da técnica da seleção de via em uma cache associativas por conjunto aumenta o tempo médio de acesso para a I-cache em 1,04 e em
1,13 para a D-cache, nos benchmarks SPEC95, mas gera um consumo médio de energia
de cache de 0,28 para a I-cache e 0,35 para a D-cache. Uma desvantagem significativa da
seleção de via é que ela torna difícil o pipeline do acesso à cache.
Exemplo
Suponha que os acessos à D-cache sejam a metade dos acessos à I-cache,
e que a I-cache e a D-cache sejam responsáveis por 25% e 15% do consumo
de energia do processador em uma implementação normal associativa por
conjunto de quatro vias. Determine que seleção de via melhora o desempenho por watt com base nas estimativas do estudo anterior.
2.2
Resposta
Dez otimizações avançadas de desempenho da cache
Para a I-cache, a economia é de 25 × 0,28 = 0,07 potência total, enquanto para
a D-cache é de 15 × 0,35 = 0,05 para uma economia total de 0,12. A versão
com previsão de via requer 0,88 do requisito de potência da cache padrão de
quatro vias. O aumento no tempo de acesso à cache é o aumento no tempo
médio de acesso à I-cache, mais metade do aumento do tempo de acesso à
D-cache, ou 1,04 + 0,5 × 0,13 = 1,11 vezes mais demorado. Esse resultado
significa que a seleção de via tem 0,090 do desempenho de uma cache padrão de quatro vias. Assim, a seleção de via melhora muito ligeiramente o
desempenho por joule por uma razão de 0,90/0,88 = 1,02. Essa otimização é
mais bem usada onde a potência, e não o desempenho, é o objetivo principal.
Terceira otimização: acesso à cache em pipeline para aumentar
a largura de banda da cache
Essa otimização serve simplesmente para possibilitar o acesso pipeline à cache, de modo
que a latência efetiva de um acerto em uma cache de primeiro nível possa ser de múltiplos ciclos de clock, gerando rápidos ciclos de clock e grande largura de banda, mas com
acertos lentos. Por exemplo, em meados dos anos 1990, o pipeline para o processador
Intel Pentium usava um ciclo de clock para acessar a cache de instruções; de meados dos
anos 1990 até o ano 2000, levava dois ciclos para o Pentium Pro ao Pentium III; para
o Pentium 4, lançado em 2000, e o Intel Core i7 atual leva quatro ciclos de clock. Essa
mudança aumenta o número de estágios da pipeline, levando a uma penalidade maior
nos desvios mal previstos e mais ciclos de clock entre o carregamento e o uso dos dados
(Cap. 3), mas isso torna mais fácil incorporar altos graus de associatividade.
Quarta otimização: caches sem bloqueio para aumentar
a largura de banda da cache
Para os computadores com pipeline que permitem a execução fora de ordem (discutidos
no Cap. 3), o processador não precisa parar (stall) em uma falta na cache de dados, esperando o dado. Por exemplo, o processador pode continuar buscando instruções da
cache de instruções enquanto espera que a cache de dados retorne os dados que faltam.
Uma cache sem bloqueio ou cache sem travamento aumenta os benefícios em potencial de tal
esquema, permitindo que a cache de dados continue a fornecer acertos de cache durante
uma falta. Essa otimização de “acerto sob falta” reduz a penalidade de falta efetiva, sendo
útil durante uma falta, em vez de ignorar as solicitações do processador. Uma opção sutil
e complexa é que a cache pode reduzir ainda mais a penalidade de falta efetiva se puder
sobrepor múltiplas faltas: uma otimização “acerto sob múltiplas faltas” ou “falta sob falta”.
A segunda opção só será benéfica se o sistema de memória puder atender a múltiplas faltas.
A maior parte dos processadores de alto desempenho (como o Intel Core i7) geralmente
suporta ambos, enquanto os processadores de baixo nível, como o ARM A8, fornecem
somente suporte limitado sem bloqueio no L2.
Para examinar a eficiência das caches sem bloqueio na redução da penalidade de falta de
caches, Farkas e Jouppi (1994) realizaram um estudo assumindo caches de 8 KB com uma
penalidade de falta de 14 ciclos. Eles observaram uma redução na penalidade efetiva de
falta de 20% para os benchmarks SPECINT92 e de 30% para os benchmarks SPECFP92
ao permitir um acerto sob falta.
Recentemente, Li, Chen, Brockman e Jouppi (2011) atualizaram esse estudo para usar
uma cache multiníveis, suposições mais modernas sobre as penalidades de falta, além dos
benchmarks SPEC2006, maiores e mais exigentes. O estudo foi feito supondo um modelo
baseado em um único núcleo de um Intel i7 (Seção 2.6) executando os benchmarks
SPC2006. A Figura 2.5 mostra a redução na latência de acesso à cache de dados quando
71
72
CAPÍTULO 2 :
Projeto de hierarquia de memória
FIGURA 2.5 A eficácia de uma cache sem bloqueio é avaliada, permitindo 1, 2 ou 64 acertos sob uma falta
de cache com os benchmarks 9 SPCINT (à esquerda) e 9 SECFP (à direita).
O sistema de memória de dados modelado com base no Intel i7 consiste em uma cache L1 de 32 KB, com latência de
acesso de quatro ciclos. A cache L2 (compartilhada com instruções) é de 256 KB, com uma latência de acesso de 10
ciclos de clock. O L3 tem 2 MB e uma latência de acesso de 32 ciclos. Todos as caches são associativas por conjunto
de oito vias e têm tamanho de bloco de 64 bytes. Permitir um acerto sob falta reduz a penalidade de falta em 9% para
os benchmarks inteiros e de 12,5% para os de ponto flutuante. Permitir um segundo acerto melhora esses resultados
para 10% e 16%, e permitir 64 resulta em pouca melhoria adicional.
permitimos 1, 2 e 64 acertos sob uma falta. A legenda apresenta mais detalhes sobre o sistema de memória. As caches maiores e a adição de uma cache L3 desde o estudo anterior
reduziram os benefícios com os benchmarks SPECINT2006, mostrando uma redução média
na latência da cache de cerca de 9%, e com os benchmarks SPECFP2006, de cerca de 12,5%.
Exemplo
Resposta
O que é mais importante para os programas de ponto flutuante: associatividade por conjunto em duas vias ou acerto sob uma falta? E para os programas de inteiros? Considere as taxas médias de falta a seguir para caches
de dados de 32 KB: 5,2% para programas de ponto flutuante com cache de
mapeamento direto, 4,9% para esses programas com cache com associatividade por conjunto em duas vias, 3,5% para programas de inteiros com cache
mapeado diretamente e 3,2% para programas de inteiros com cache com
associatividade por conjunto em duas vias. Considere que a penalidade de
falta para L2 é de 10 ciclos e que as faltas e acertos do L2 sejam os mesmos.
Para programas de ponto flutuante, os tempos de stall médios da memória são:
Taxa de falta DM × Penalidadede falta = 5,2% × 10 = 0,52
Taxa de falta 2 vias × Penalidadede falta = 4,9% × 10 = 0,49
A latência de acesso à cache (incluindo as paradas — stalls) para associatividade de duas é 0,49/0,52 ou 94% da cache mapeada diretamente. A legenda
da Figura 2.5 revela que o acerto sob uma falta reduz o tempo médio de stall da
memória para 87,5% de uma cache com bloqueio. Logo, para programas
de ponto flutuante, a cache de dados com mapeamento direto com suporte
para acerto sob uma falta oferece melhor desempenho do que uma cache
com associatividade por conjunto em duas vias, que bloqueia em uma falta.
Para programas inteiros, o cálculo é:
Taxa de falta DM × Penalidadede falta = 3,5% × 10 = 0,35
Taxa de falta 2 vias × Penalidadede falta = 3,2% × 10 = 0,32
A latência de acesso à cache com associatividade por conjunto de duas vias
é, assim, 0,32/0,35 ou 91% de cache com mapeamento direto, enquanto a
redução na latência de acesso, quando permitimos um acerto sob falta, é de
9%, tornando as duas opções aproximadamente iguais.
2.2
Dez otimizações avançadas de desempenho da cache
A dificuldade real com a avaliação do desempenho das caches de não bloqueio é que uma
falta de cache não causa necessariamente um stall no processador. Nesse caso, é difícil
julgar o impacto de qualquer falta isolada e, portanto, é difícil calcular o tempo de acesso
médio à memória. A penalidade de falta efetiva não é a soma das faltas, mas o tempo não
sobreposto em que o processador é adiado. O benefício das caches de não bloqueio é
complexo, pois depende da penalidade de falta quando ocorrem múltiplas faltas, do
padrão de referência da memória e de quantas instruções o processador puder executar
com uma falta pendente.
Em geral, os processadores fora de ordem são capazes de ocultar grande parte da penalidade
de falta de uma falta de cache de dados L1 na cache L2, mas não são capazes de ocultar
uma fração significativa de uma falta de cache de nível inferior. Decidir quantas faltas
pendentes suportar depende de diversos fatores:
j
j
j
j
A localidade temporal e espacial no fluxo da falta, que determina se uma falta pode
iniciar um novo acesso a um nível inferior da cache ou à memória.
A largura da banda da resposta da memória ou da cache.
Permitir mais faltas pendentes no nível mais baixo da cache (onde o tempo de falta
é o mais longo) requer pelo menos o suporte do mesmo número de faltas em um
nível mais alto, já que a falta deve se iniciar na cache de nível mais alto.
A latência do sistema de memória.
O exemplo simplificado a seguir mostra a ideia principal.
Exemplo
Resposta
Considere um tempo de acesso à memória principal de 36 ns e um sistema
de memória capaz de uma taxa sustentável de transferência de 16 GB/s. Se
o tamanho do bloco for de 64 bytes, qual será o número máximo de faltas
pendentes que precisamos suportar, supondo que possamos manter o pico
de largura de banda, dado o fluxo de requisições, e que os acessos nunca
entram em conflito? Se a probabilidade de uma referência colidir com uma
das quatro anteriores for de 50% e supondo-se que o acesso tenha que esperar até que o acesso anterior seja completado, estime o número máximo
de referências pendentes. Para simplificar, ignore o tempo entre as faltas.
No primeiro caso, supondo que possamos manter o pico de largura de banda,
o sistema de memória pode suportar (16 × 10)9/64 = 250 milhões de referências por segundo. Uma vez que cada referência leva 36 ns, podemos suportar
250 × 106 × 36 × 10−9 = nove referências. Se a probabilidade de uma colisão
for maior do que 0, então precisamos de mais referências pendentes, uma
vez que não podemos começar a trabalhar nessas referências. O sistema de
memória precisa de mais referências independentes, não de menos! Para
aproximar isso, podemos simplesmente supor que é preciso que a metade
das referências de memória não seja enviada para a memória. Isso quer
dizer que devemos suportar duas vezes mais referências pendentes, ou 18.
Em seu estudo, Li, Chen, Brosckman e Jouppi descobriram que a redução na CPI para os
programas de inteiros foi de cerca de 7% para um acerto sob falta e cerca de 12,7% para
64. Para os programas de ponto flutuante, as reduções foram de 12,7% para um acerto
sob falta e de 17,8% para 64. Essas reduções acompanham razoavelmente de perto as
reduções na latência no acesso à cache de dados mostrado na Figura 2.5.
Quinta otimização: caches multibanco para aumentar a largura
de banda da cache
Em vez de tratar a cache como um único bloco monolítico, podemos dividi-lo em bancos
independentes que possam dar suporte a acessos simultâneos. Os bancos foram usados
73
74
CAPÍTULO 2 :
Projeto de hierarquia de memória
FIGURA 2.6 Bancos de caches intercalados em quatro vias, usando o endereçamento de bloco.
Considerando 64 bytes por bloco, cada um desses endereços seria multiplicado por 64 para obter o endereçamento do
byte.
originalmente para melhorar o desempenho da memória principal e agora são usados
tanto nos modernos chips de DRAM como nas caches. O Arm Cortex-A8 suporta 1-4
bancos em sua cache L2; o Intel Core i7 tem quatro bancos no L1 (para suportar até dois
acessos de memória por clock), e o L2 tem oito bancos.
Obviamente, o uso de bancos funciona melhor quando os acessos se espalham naturalmente por eles, de modo que o mapeamento de endereços a bancos afeta o comportamento do sistema de memória. Um mapeamento simples, que funciona bem, consiste em
espalhar os endereços do bloco sequencialmente pelos bancos, algo chamado intercalação
sequencial. Por exemplo, se houver quatro bancos, o banco 0 terá todos os blocos cujo
endereço módulo 4 é igual a 0; o banco 1 terá todos os blocos cujo endereço módulo 4
é 1, e assim por diante. A Figura 2.6 mostra essa intercalação. Bancos múltiplos também
são um modo de reduzir o consumo de energia, tanto nas caches quanto na DRAM.
Sexta otimização: palavra crítica primeiro e reinício antecipado
para reduzir a penalidade da falta
Essa técnica é baseada na observação de que o processador normalmente precisa de apenas
uma palavra do bloco de cada vez. Essa estratégia é a da impaciência: não espere até que
o bloco inteiro seja carregado para então enviar a palavra solicitada e reiniciar o processador. Aqui estão duas estratégias específicas:
j
j
Palavra crítica primeiro. Solicite primeiro a palavra que falta e envie-a para o
processador assim que ela chegar; deixe o processador continuar a execução
enquanto preenche o restante das palavras no bloco.
Reinício antecipado. Busque as palavras na ordem normal, mas, assim que a palavra
solicitada chegar, envie-a para o processador e deixe que ele continue a execução.
Geralmente, essas técnicas só beneficiam projetos com grandes blocos de cache, pois o
benefício é baixo, a menos que os blocos sejam grandes. Observe que, normalmente, as
caches continuam a satisfazer os acessos a outros blocos enquanto o restante do bloco
está sendo preenchido.
Infelizmente, dada a proximidade espacial, existe boa chance de que a próxima referência
sirva para o restante do bloco. Assim como as caches de não bloqueio, a penalidade de
falta não é simples de calcular. Quando existe uma segunda solicitação da palavra crítica
primeiro, a penalidade de falta efetiva é o tempo não sobreposto da referência até que a
segunda parte chegue. Os benefícios da palavra crítica primeiro e de reinício antecipado
dependem do tamanho do bloco e da probabilidade de outro acesso à parte do bloco
que ainda não foi acessada.
Sétima otimização: write buffer merge de escrita para reduzir
a penalidade de falta
Caches write-through contam com buffers de escrita, pois todos os armazenamentos
precisam ser enviados para o próximo nível inferior da hierarquia. Até mesmo as caches
2.2
Dez otimizações avançadas de desempenho da cache
write-back utilizam um buffer simples quando um bloco é substituído. Se o buffer de
escrita estiver vazio, os dados e o endereço completo serão escritos no buffer, e a escrita
será terminada do ponto de vista do processador; o processador continuará trabalhando
enquanto o buffer de escrita se prepara para escrever a palavra na memória. Se o buffer tiver
outros blocos modificados, os endereços poderão ser verificados para saber se o endereço
desses novos dados combina com o endereço de uma entrada válida no buffer de escrita.
Então, os novos dados serão combinados com essa entrada. A mesclagem de escrita é o nome
dessa otimização. O Intel Core i7, entre muitos outros, utiliza a mesclagem de escrita.
Se o buffer estiver cheio e não houver combinação de endereço, a cache (e o processador)
precisará esperar até que o buffer tenha uma entrada vazia. Essa otimização utiliza a
memória de modo mais eficiente, pois as escritas multipalavras normalmente são mais
rápidas do que as escritas realizadas uma palavra de cada vez. Skadron e Clark (1997)
descobriram que aproximadamente 5-10% do desempenho era perdido devido a stalls
em um buffer de escrita de quatro entradas.
A otimização também reduz os stalls devido ao fato de o buffer de escrita estar cheio. A
Figura 2.7 mostra um buffer de escrita com e sem a mesclagem da escrita. Suponha que
tenhamos quatro entradas no buffer de escrita e que cada entrada possa manter quatro
palavras de 64 bits. Sem essa otimização, quatro armazenamentos nos endereços sequenciais preencheriam o buffer em uma palavra por entrada, embora essas quatro palavras,
quando mescladas, caibam exatamente dentro de uma única entrada do buffer de escrita.
Observe que os registradores do dispositivo de entrada/saída são então mapeados para o
espaço de endereços físico. Esses endereços de E/S não podem permitir a mesclagem da
escrita, pois registradores de E/S separados podem não atuar como um array de palavras
FIGURA 2.7 Para ilustrar a mesclagem da escrita, o buffer de escrita de cima não a utiliza, enquanto o
buffer de escrita de baixo a utiliza.
As quatro escritas são mescladas em uma única entrada de buffer com mesclagem de escrita; sem ela, o buffer fica
cheio, embora 3/4 de cada entrada sejam desperdiçados. O buffer possui quatro entradas, e cada entrada mantém
quatro palavras de 64 bits. O endereço para cada entrada está à esquerda, com um bit de válido (V) indicando se os
próximos oito bytes sequenciais nessa entrada são ocupados. (Sem a mesclagem da escrita, as palavras à direita na
parte superior da figura não seriam usadas para instruções que escrevessem múltiplas palavras ao mesmo tempo.)
75
76
CAPÍTULO 2 :
Projeto de hierarquia de memória
na memória. Por exemplo, eles podem exigir um endereço e uma palavra de dados por
registrador, em vez de escritas multipalavras usando um único endereço. Esses efeitos
colaterais costumam ser implementados marcando as páginas como requerendo escrita
sem mesclagem pelas caches.
Oitava otimização: otimizações de compilador para reduzir
a taxa de falta
Até aqui, nossas técnicas têm exigido a mudança do hardware. Essa próxima técnica reduz
as taxas de falta sem quaisquer mudanças no hardware.
Essa redução mágica vem do software otimizado — a solução favorita do projetista de
hardware! A diferença de desempenho cada vez maior entre os processadores e a memória
principal tem inspirado os projetistas de compiladores a investigar a hierarquia de memória
para ver se as otimizações em tempo de compilação podem melhorar o desempenho.
Mais uma vez, a pesquisa está dividida entre as melhorias nas faltas de instrução e as
melhorias nas faltas de dados. As otimizações apresentadas a seguir são encontradas em
muitos compiladores modernos.
Permuta de loop
Alguns programas possuem loops aninhados que acessam dados na memória na ordem
não sequencial. Simplesmente trocar o aninhamento dos loops pode fazer o código acessar os dados na ordem em que são armazenados. Considerando que os arrays não cabem
na cache, essa técnica reduz as faltas, melhorando a localidade espacial; a reordenação
maximiza o uso de dados em um bloco de cache antes que eles sejam descartados. Por
exemplo, se x for um array bidimensional de tamanho [5.000,100] alocado de modo que
x[i,j] e x[i,j + 1] sejam adjacentes (uma ordem chamada ordem principal de linha, já que o
array é organizado em linhas), então os códigos a seguir mostram como os acessos podem
ser otimizados:
O código original saltaria pela memória em trechos de 100 palavras, enquanto a versão
revisada acessa todas as palavras em um bloco de cache antes de passar para o bloco
seguinte. Essa otimização melhora o desempenho da cache sem afetar o número de instruções executadas.
Bloqueio
Essa otimização melhora a localidade temporal para reduzir as faltas. Novamente, estamos
lidando com múltiplos arrays, com alguns arrays acessados por linhas e outros por colunas.
Armazenar os arrays linha por linha (ordem principal de linha) ou coluna por coluna (ordem
principal de coluna) não resolve o problema, pois linhas e colunas são usadas em cada
iteração do loop. Esses acessos ortogonais significam que transformações como permuta
de loop ainda possuem muito espaço para melhoria.
Em vez de operar sobre linhas ou colunas inteiras de um array, os algoritmos bloqueados
operam sobre submatrizes ou blocos. O objetivo é maximizar os acessos aos dados carregados
2.2
Dez otimizações avançadas de desempenho da cache
na cache antes que eles sejam substituídos. O exemplo de código a seguir, que realiza a
multiplicação de matriz, ajuda a motivar a otimização:
Os dois loops interiores leem todos os elementos N por N de z, leem os mesmos N elementos em uma linha de y repetidamente e escrevem uma linha de N elementos de x. A
Figura 2.8 apresenta um instantâneo dos acessos aos três arrays. O tom escuro indica acesso
recente, o tom claro indica acesso mais antigo e o branco significa ainda não acessado.
O número de faltas de capacidade depende claramente de N e do tamanho da cache. Se ele
puder manter todas as três matrizes N por N, tudo está bem, desde que não existam conflitos de
cache. Se a cache puder manter uma matriz N por N e uma linha de N, pelo menos a i-ésima linha de y e o array z podem permanecer na cache. Menos do que isso e poderão ocorrer faltas para
x e z. No pior dos casos, haverá 2N 3 + N 2 palavras de memória acessadas para N3 operações.
Para garantir que os elementos acessados podem caber na cache, o código original é
mudado para calcular em uma submatriz de tamanho B por B. Dois loops internos agora
calculam em passos de tamanho B, em vez do tamanho completo de x e z. B é chamado
de fator de bloqueio (considere que x é inicializado com zero).
FIGURA 2.8 Um instantâneo dos três arrays x, y e z quando N = 6 e i = 1.
Os acessos, distribuídos no tempo, aos elementos do array são indicados pelo tom: branco significa ainda não tocado,
claro significa acessos mais antigos e escuro significa acessos mais recentes. Em comparação com a Figura 2.9, os
elementos de y e z são lidos repetidamente para calcular novos elementos de x. As variáveis i, j e k aparecem ao longo
das linhas ou colunas usadas para acessar os arrays.
77
78
CAPÍTULO 2 :
Projeto de hierarquia de memória
FIGURA 2.9 Acessos, distribuídos no tempo, aos arrays x, y e z quando B = 3.
Observe, em comparação com a Figura 2.8, o número menor de elementos acessados.
A Figura 2.9 ilustra os acessos aos três arrays usando o bloqueio. Vendo apenas as perdas
de capacidade, o número total de palavras acessadas da memória é 2N 3/B + N 2. Esse
total é uma melhoria por um fator de B. Logo, o bloqueio explora uma combinação de
localidade espacial e temporal, pois y se beneficia com a localidade espacial e z se beneficia
com a localidade temporal.
Embora tenhamos visado reduzir as faltas de cache, o bloqueio também pode ser usado
para ajudar na alocação de registradores. Selecionando um pequeno tamanho de bloqueio,
de modo que o bloco seja mantido nos registradores, podemos minimizar o número de
carregamentos e armazenamentos no programa.
Como veremos na Seção 4.8 do Capítulo 4, o bloqueio de cache é absolutamente necessário para obter bom desempenho de processadores baseados em cache, executando
aplicações que usam matrizes como estrutura de dados primária.
Nona otimização: a pré-busca de pelo hardware das instruções
e dados para reduzir a penalidade de falta ou a taxa de falta
As caches de não bloqueio reduzem efetivamente a penalidade de falta, sobrepondo a
execução com o acesso à memória. Outra técnica é fazer a pré-busca (prefetch) dos itens
antes que o processador os solicite. Tanto as instruções quanto os dados podem ter sua
busca antecipada, seja diretamente nas caches, seja diretamente em um buffer externo,
que pode ser acessado mais rapidamente do que a memória principal.
Frequentemente, a pré-busca de instrução é feita no hardware fora da cache. Em geral,
o processador apanha dois blocos em uma falta: o bloco solicitado e o próximo bloco
consecutivo. O bloco solicitado é colocado na cache de instruções, e o bloco cuja busca
foi antecipada é colocado no buffer do fluxo de instruções. Se o bloco solicitado estiver
presente no buffer do fluxo de instruções, a solicitação de cache original será cancelada,
o bloco será lido do buffer de fluxo e a próxima solicitação de pré-busca será emitida.
Uma técnica semelhante pode ser aplicada aos acessos a dados (Jouppi, 1990). Palacharla e Kessler (1994) examinaram um conjunto de programas científicos e consideraram múltiplos buffers
de fluxo que poderiam tratar tanto instruções como dados. Eles descobriram que oito buffers
de fluxo poderiam capturar 50-70% de todas as faltas de um processador com duas caches de
64 KB associativas por conjunto com quatro vias, um para instruções e os outros para dados.
O Intel Core i7 pode realizar a pré-busca de dados no L1 e L2 com o caso de pré-busca
mais comum sendo o acesso à próxima linha. Alguns processadores anteriores da Intel
usavam uma pré-busca mais agressiva, mas isso resultou em desempenho reduzido para
algumas aplicações, fazendo com que alguns usuários sofisticados desativassem o recurso.
2.2
Dez otimizações avançadas de desempenho da cache
A Figura 2.10 mostra a melhoria de desempenho geral para um subconjunto dos programas
SPEC2000 quando a pré-busca de hardware está ativada. Observe que essa figura inclui
apenas dois de 12 programas inteiros, enquanto inclui a maioria dos programas SPEC de
ponto flutuante.
A pré-busca conta com o uso de uma largura de banda de memória que, de outra forma,
seria inutilizada, mas, se interferir nas perdas de demanda, pode realmente reduzir o
desempenho. A ajuda de compiladores pode reduzir a pré-busca inútil. Quando a pré-busca funciona bem, seu impacto sobre a consumo de energia é irrelevante. Quando dados
pré-obtidos não são usados ou dados úteis estão fora do lugar, a pré-busca pode ter um
impacto muito negativo no consumo de energia.
Décima otimização: pré-busca controlada por compilador para
reduzir a penalidade de falta ou a taxa de falta
Uma alternativa à pré-busca de hardware é que o compilador insira instruções de pré-busca
para solicitar dados antes que o processador precise deles. Existem dois tipos de pré-busca:
j
j
A pré-busca de registrador, que carrega o valor em um registrador.
A pré-busca de cache, que carrega os dados apenas na cache, e não no registrador.
Qualquer uma delas pode ser com falta ou sem falta, ou seja, o endereço causa ou não uma
exceção para faltas de endereço virtuais e violações de proteção. Usando essa terminologia,
uma instrução de carregamento normal poderia ser considerada uma “instrução de pré-busca de registrador com falta”. As pré-buscas sem falta simplesmente se transformarão
em no-ops se normalmente resultarem em uma exceção, que é o que queremos.
A pré-busca mais efetiva é “semanticamente invisível” a um programa: ela não muda o
conteúdo dos registradores e da memória, e não pode causar faltas de memória virtual.
Hoje a maioria dos processadores oferece pré-buscas de cache sem falta. Esta seção considera a pré-busca de cache sem falta, também chamada pré-busca sem vínculo.
FIGURA 2.10 Ganho de velocidade devido à pré-busca de hardware no Intel Pentium 4 com a pré-busca
de hardware ativada para dois dos 12 benchmarks SPECint2000 e nove dos 14 benchmarks SPECfp2000.
Somente os programas que se beneficiam mais com a pré-busca são mostrados; a pré-busca agiliza os 15
benchmarks SPEC restantes em menos de 15% (Singhal, 2004).
79
80
CAPÍTULO 2 :
Projeto de hierarquia de memória
A pré-busca só faz sentido se o processador puder prosseguir enquanto realiza a pré-busca
dos dados, ou seja, as caches não param, mas continuam a fornecer instruções e dados
enquanto esperam que os dados cujas buscas foram antecipadas retornem. Como era de
esperar, a cache de dados para esses computadores normalmente é sem bloqueio.
Assim como na pré-busca controlada pelo hardware, o objetivo é sobrepor a execução com
a pré-busca de dados. Os loops são os alvos importantes, pois servem para otimizações de
pré-busca. Se a penalidade de falta for pequena, o compilador simplesmente desdobrará
o loop uma ou duas vezes e escalonará as pré-buscas com a execução. Se a penalidade de
falta for grande, ele usará o pipelining de software (Apêndice H) ou desdobrará muitas
vezes para realizar a pré-busca de dados para uma iteração futura.
Entretanto, emitir instruções de pré-busca contrai um overhead de instrução, de modo
que os compiladores precisam tomar cuidado para garantir que tais overheads não sejam
superiores aos benefícios. Concentrando-se em referências que provavelmente serão faltas
de cache, os programas podem evitar pré-buscas desnecessárias enquanto melhoram bastante o tempo médio de acesso à memória.
Exemplo
Para o código a seguir, determine quais acessos provavelmente causarão
faltas de cache de dados. Em seguida, insira instruções de pré-busca para
reduzir as faltas. Finalmente, calcule o número de instruções de pré-busca
executadas e as faltas evitadas pela pré-busca. Vamos supor que tenhamos
uma cache de dados mapeado diretamente de 8 KB com blocos de 16 bytes e
ela seja uma cache write-back que realiza alocação de escrita. Os elementos
de a e b possuem oito bytes, pois são arrays de ponto flutuante de precisão
dupla. Existem três linhas e 100 colunas para a e 101 linhas e três colunas
para b. Vamos supor também que eles não estejam na cache no início do
programa
Resposta
O compilador primeiro determinará quais acessos provavelmente causarão
faltas de cache; caso contrário, perderemos tempo emitindo instruções de
pré-busca para dados que seriam acertos. Os elementos de a são escritos na
ordem em que são armazenados na memória, de modo que a se beneficiará
com a proximidade espacial: os valores pares de j serão faltas, e os valores
ímpares serão acertos. Como a possui três linhas e 100 colunas, seus acessos
levarão a 3 × (100/2), ou 150 faltas.
O array b não se beneficia com a proximidade espacial, pois os acessos
não estão na ordem em que são armazenados. O array b se beneficia duas
vezes da localidade temporal: os mesmos elementos são acessados para
cada iteração de i, e cada iteração de j usa o mesmo valor de b que a última
iteração. Ignorando faltas de conflito em potencial, as faltas devidas a b
serão para b[j+1][0] acessos quando i = 0, e também o primeiro acesso a b[j]
[0] quando j = 0. Como j vai de 0 a 99 quando i = 0, os acessos a b levam a
100 + 1 ou 101 faltas.
Assim, esse loop perderá a cache de dados aproximadamente 150 vezes para
a mais 101 vezes para b ou 251 faltas.
Para simplificar nossa otimização, não nos preocuparemos com a pré-busca
dos primeiros acessos do loop. Eles já podem estar na cache ou pagaremos
a penalidade de falta dos primeiros poucos elementos de a ou b. Também
não nos preocuparemos em suprimir as pré-buscas ao final do loop, que
tentam buscar previamente além do final de a (a[i][100] … a[i][106]) e o
final de b (b[101][0] … b[107][0]). Se essas pré-buscas fossem com falta,
2.2
Dez otimizações avançadas de desempenho da cache
não poderíamos ter esse luxo. Vamos considerar que a penalidade de falta
é tão grande que precisamos começar a fazer a pré-busca pelo menos sete
iterações à frente (em outras palavras, consideramos que a pré-busca não
tem benefício até a oitava iteração). Sublinhamos as mudanças feitas no
código anterior, necessárias para realizar a pré-busca.
Esse código revisado realiza a pré-busca de a[i][7] até a[i][99] e de b[7][0]
até b[100][0], reduzindo o número de faltas de não pré-busca para
j
j
j
j
Sete faltas para os elementos b[0][0], b[1][0], …, b[6][0] no primeiro loop
Quatro faltas ([7/2]) para os elementos a[0][0], a[0][1], …, a[0][6] no
primeiro loop (a proximidade espacial reduz as faltas para uma por bloco
de cache de 16 bytes)
Quatro faltas ([7/2]) para os elementos a[1][0], a[1][1], …, a[1][6] no
segundo loop.
Quatro faltas ([7/2]) para os elementos a[2][0], a[2][1], …, a[2][6] no
segundo loop
ou um total de 19 faltas de não pré-busca. O custo de evitar 232 faltas de
cache é a execução de 400 instruções de pré-busca, provavelmente uma
boa troca.
Exemplo
Resposta
Calcule o tempo economizado no exemplo anterior. Ignore faltas da cache
de instrução e considere que não existem faltas por conflito ou capacidade
na cache de dados. Suponha que as pré-buscas possam se sobrepor umas
às outras com faltas de cache, transferindo, portanto, na largura de banda
máxima da memória. Aqui estão os principais tempos de loop ignorando
as faltas de cache: o loop original leva sete ciclos de clock por iteração, o
primeiro loop de pré-busca leva nove ciclos de clock por iteração e o segundo
loop de pré-busca leva oito ciclos de clock por iteração (incluindo o overhead
do loop for externo). Uma falta leva 100 ciclos de clock.
O loop original duplamente aninhado executa a multiplicação 3 × 100 ou
300 vezes. Como o loop leva sete ciclos de clock por iteração, o total é de
300 × 7 ou 2.100 ciclos de clock mais as faltas de cache. As faltas de cache
aumentam 251 × 100 ou 25.100 ciclos de clock, gerando um total de 27.200
ciclos de clock. O primeiro loop de pré-busca se repete 100 vezes; a nove
ciclos de clock por iteração, o total é de 900 ciclos de clock mais as faltas de
cache. Elas aumentam 11 × 100 ou 1.100 ciclos de clock para as faltas de
cache, gerando um total de 2.000. O segundo loop é executado 2 × 100 ou
200 vezes, e a nove ciclos de clock por iteração; isso leva 1.600 ciclos de clock
mais 8 × 100 ou 800 ciclos de clock para as faltas de cache. Isso gera um
total de 2.400 ciclos de clock. Do exemplo anterior, sabemos que esse código
executa 400 instruções de pré-busca durante os 2.000 + 2.400 ou 4.400 ciclos
de clock para executar esses dois loops. Se presumirmos que as pré-buscas
são completamente sobrepostas com o restante da execução, então o código
da pré-busca é 27.200/4.400 ou 6,2 vezes mais rápido.
81
82
CAPÍTULO 2 :
Projeto de hierarquia de memória
Embora as otimizações de array sejam fáceis de entender, os programas modernos provavelmente utilizam ponteiros. Luk e Mowry (1999) demonstraram que a pré-busca baseada
em compilador às vezes também pode ser estendida para ponteiros. Dos 10 programas
com estruturas de dados recursivas, a pré-busca de todos os ponteiros quando um nó é
visitado melhorou o desempenho em 4-31% na metade dos programas. Por outro lado, os
programas restantes ainda estavam dentro de 2% de seu desempenho original. A questão
envolve tanto se as pré-buscas são para dados já na cache quanto se elas ocorrem cedo o
suficiente para os dados chegarem quando forem necessários.
Muitos processadores suportam instruções para pré-busca de cache e, muitas vezes, processadores de alto nível (como o Intel Core i7) também realizam algum tipo de pré-busca
automática no hardware.
Resumo de otimização de cache
As técnicas para melhorar o tempo de acerto, a largura de banda, a penalidade de falta
e a taxa de falta geralmente afetam os outros componentes da equação de acesso médio
à memória, além da complexidade da hierarquia de memória. A Figura 2.11 resume
essas técnicas e estima o impacto sobre a complexidade, com + significando que a
técnica melhora o fator, – significando que ela prejudica esse fator, e um espaço significando que ela não tem impacto. Geralmente, nenhuma técnica ajuda mais de uma
categoria.
FIGURA 2.11 Resumo das 10 otimizações de cache avançadas mostrando o impacto sobre o desempenho da cache e a complexidade.
Embora geralmente uma técnica só ajude um fator, a pré-busca pode reduzir as faltas se for feita suficientemente cedo; se isso não ocorrer, ela pode
reduzir a penalidade de falta. + significa que a técnica melhora o fator, – significa que ela prejudica esse fator, e um espaço significa que ela não tem
impacto. A medida de complexidade é subjetiva, com 0 sendo o mais fácil e 3 sendo um desafio.
2.3
2.3
Tecnologia de memória e otimizações
TECNOLOGIA DE MEMÓRIA E OTIMIZAÇÕES
[…] o único desenvolvimento isolado que colocou os computadores na linha foi
a invenção de uma forma confiável de memória, a saber, a memória de núcleo
[…] Seu custo foi razoável, ela era confiável e, por ser confiável, poderia se tornar
grande com o passar do tempo. (p. 209)
Maurice Wilkes
Memoirs of a Computer Pioneer (1985)
A memória principal é o próximo nível abaixo na hierarquia. A memória principal satisfaz
as demandas das caches e serve de interface de E/S, pois é o destino da entrada e também
a origem da saída. As medidas de desempenho da memória principal enfatizam tanto a
latência quanto a largura de banda. Tradicionalmente, a latência da memória principal
(que afeta a penalidade de falta de cache) é a principal preocupação da cache, enquanto a
largura de banda da memória principal é a principal preocupação dos multiprocessadores
e da E/S.
Embora as caches se beneficiem da memória de baixa latência, geralmente é mais fácil
melhorar a largura de banda da memória com novas organizações do que reduzir a latência.
A popularidade das caches de segundo nível e de seus tamanhos de bloco maiores torna
a largura de banda da memória principal importante também para as caches. Na verdade,
os projetistas de cache aumentam o tamanho de bloco para tirar proveito da largura de
banda com alto desempenho.
As seções anteriores descrevem o que pode ser feito com a organização da cache para
reduzir essa diferença de desempenho processador-DRAM, mas simplesmente tornar as
caches maiores ou acrescentar mais níveis de caches não elimina a diferença. Inovações
na memória principal também são necessárias.
No passado, a inovação era o modo de organizar os muitos chips de DRAM que compunham a memória principal, assim como os múltiplos bancos de memória. Uma largura de
banda maior está disponível quando se usam bancos de memória, tornando a memória e
seu barramento mais largos ou fazendo ambos. Ironicamente, à medida que a capacidade
por chip de memória aumenta, existem menos chips no sistema de memória do mesmo
tamanho, reduzindo as possibilidades para sistemas de memória mais largos com a mesma capacidade.
Para permitir que os sistemas de memória acompanhem as demandas de largura de banda
dos processadores modernos, as inovações de memória começaram a acontecer dentro dos
próprios chips de DRAM. Esta seção descreve a tecnologia dentro dos chips de memória
e essas organizações inovadoras, internas. Antes de descrever as tecnologias e as opções,
vamos examinar as medidas de desempenho.
Com a introdução das memórias de transferência pelo modo burst, hoje amplamente
usadas em memórias Flash e DRAM, a latência da memória é calculada usando duas
medidas: tempo de acesso e tempo de ciclo. O tempo de acesso é o tempo entre a solicitação
de uma leitura e a chegada da palavra desejada, enquanto tempo de ciclo é o tempo mínimo
entre as solicitações e a memória.
Quase todos os computadores desktops ou servidores utilizam, desde 1975, DRAMs para
a memória principal e quase todos utilizam SRAMs para cache, com 1-3 níveis integrados
no chip do processador com a CPU. Em PMDs, a tecnologia de memória muitas vezes
equilibra consumo de energia e velocidade com sistemas de alto nível, usando tecnologia
de memória rápida e com grande largura de banda.
83
84
CAPÍTULO 2 :
Projeto de hierarquia de memória
Tecnologia de SRAM
A primeira letra de SRAM significa estática (static, em inglês). A natureza dinâmica dos
circuitos na DRAM exige que os dados sejam escritos de volta após serem lidos — daí a
diferença entre o tempo de acesso e o tempo de ciclo, além da necessidade de atualização
(refresh). As SRAMs não precisam ser atualizadas e, portanto, o tempo de acesso é muito
próximo do tempo de ciclo. As SRAMs normalmente utilizam seis transistores por bit
para impedir que a informação seja modificada quando lida. A SRAM só precisa de um
mínimo de energia para reter a carga no modo de stand-by.
No princípio, a maioria dos sistemas de desktops e servidores usava chip de SRAM para
suas caches primárias, secundárias ou terciárias. Hoje, os três níveis de caches são integrados
no chip do processador. Atualmente, os maiores caches de terceiro nível no chip são de
12 MB, enquanto o sistema de memória para tal processador provavelmente terá de 4-16
GB de DRAM. Em geral, os tempos de acesso para grandes caches de terceiro nível no chip
são 2-4 vezes os de uma cache de segundo nível, que ainda é 3-5 vezes mais rápido do
que acessar uma memória DRAM.
Tecnologia de DRAM
À medida que as primeiras DRAMs cresciam em capacidade, o custo de um pacote com
todas as linhas de endereço necessárias se tornava um problema. A solução foi multiplexar as linhas de endereço, reduzindo assim o número de pinos de endereço ao meio.
A Figura 2.12 mostra a organização básica da DRAM. Metade do endereço é enviada
primeiro, algo chamado RAS — Row Access Strobe. A outra metade do endereço, enviada
durante o CAS — Column Access Strobe —, vem depois. Esses nomes vêm da organização
interna do chip, pois a memória é organizada como uma matriz retangular endereçada
por linhas e colunas.
Um requisito adicional da DRAM deriva da propriedade indicada pela primeira letra, D,
de dinâmica. Para compactar mais bits por chips, as DRAMs utilizam apenas um único
transistor para armazenar um bit. A leitura desse bit destrói a informação, de modo que
ela precisa ser restaurada. Esse é um motivo pelo qual o tempo de ciclo da DRAM é muito
maior que o tempo de acesso. Além disso, para evitar perda de informações quando um bit
não é lido ou escrito, o bit precisa ser “restaurado” periodicamente (refresh). Felizmente,
todos os bits em uma linha podem ser renovados simultaneamente apenas pela leitura
FIGURA 2.12 Organização interna de uma DRAM.
As DRAMs modernas são organizadas em bancos, em geral quatro por DDR3. Cada banco consiste em uma série
de linhas. Enviar um comando PRE (pré-carregamento) abre ou fecha um banco. Um endereço de linha é enviado
com um ACT (ativar), que faz com que a linha seja transferida para um buffer. Quando a linha está no buffer,
ela pode ser transferida por endereços de coluna sucessivos em qualquer largura da DRAM (geralmente 4, 8
ou 16 bits em DDR3) ou especificando uma transferência de bloco e o endereço de início. Cada comando, assim
como as transferências de bloco, é sincronizado com um clock.
2.3
Tecnologia de memória e otimizações
dessa linha. Logo, cada DRAM do sistema de memória precisa acessar cada linha dentro
de certa janela de tempo, como 8 ms. Os controladores de memória incluem o hardware
para refresh das DRAMs periodicamente.
Esse requisito significa que o sistema de memória está ocasionalmente indisponível, pois
está enviando um sinal que diz a cada chip para ser restaurado. O tempo para um refresh
normalmente é um acesso completo à memória (RAS e CAS) para cada linha da DRAM.
Como a matriz de memória em uma DRAM é conceitualmente quadrada, em geral o
número de etapas em um refresh é a raiz quadrada da capacidade da DRAM. Os projetistas
de DRAM tentam manter o tempo gasto na restauração menor que 5% do tempo total.
Até aqui, apresentamos a memória principal como se ela operasse como um trem suíço,
entregando suas mercadorias de modo consistente, exatamente de acordo com o horário.
O refresh contradiz essa analogia, pois alguns acessos levam muito mais tempo do que
outros. Assim, o refresh é outro motivo para a variabilidade da latência da memória e,
portanto, da penalidade de falta da cache.
Amdahl sugeriu uma regra prática de que a capacidade da memória deverá crescer linearmente com a velocidade do processador para manter um sistema equilibrado, de modo
que um processador de 1.000 MIPS deverá ter 1.000 MB de memória. Os projetistas de
processador contam com as DRAMs para atender a essa demanda: no passado, eles esperavam uma melhoria que quadruplicasse a capacidade a cada três anos ou 55% por ano.
Infelizmente, o desempenho das DRAMs está crescendo em uma taxa muito mais lenta.
A Figura 2.13 mostra a melhora de desempenho no tempo de acesso da linha, que está
relacionado com a latência, de cerca de 5% por ano. O CAS, ou tempo de transferência de
dados, relacionado com a largura de banda está crescendo em mais de duas vezes essa taxa.
FIGURA 2.13 Tempos de DRAMs rápidas e lentas a cada geração.
(O tempo de ciclo foi definido na página 83.) A melhoria de desempenho do tempo de acesso de linha é de cerca de 5% por ano. A melhoria por um fator
de 2 no acesso à coluna em 1986 acompanhou a troca das DRAMs NMOS para DRAMs CMOS. A introdução de vários modos burst de transferência,
em meados dos anos 1990, e as SDRAMs, no final dos anos 1990, complicaram significativamente o cálculo de tempo de acesso para blocos de dados.
Vamos discutir isso de modo aprofundado nesta seção quando falarmos sobre tempo de acesso e potência da SDRAM. O lançamento dos projetos DDR4
está previsto para o segundo semestre de 2013. Vamos discutir essas diversas formas de DRAM nas próximas páginas.
85
86
CAPÍTULO 2 :
Projeto de hierarquia de memória
Embora estejamos falando de chips individuais, as DRAMs normalmente são vendidas
em pequenas placas, chamadas módulos de memória em linha dupla (Dual Inline Memory
Modules — DIMMs). Os DIMMs normalmente contêm 4-16 DRAMs e são organizados
para ter oito bytes de largura (+ ECC) para sistemas desktops.
Além do empacotamento do DIMM e das novas interfaces para melhorar o tempo de transferência de dados, discutidos nas próximas subseções, a maior mudança nas DRAMs tem
sido uma redução no crescimento da capacidade. As DRAMs obedeceram à lei de Moore
por 20 anos, gerando um novo chip com capacidade quadruplicada a cada três anos.
Devido aos desafios de fabricação de uma DRAM de bit único, novos chips só dobraram de
capacidade a cada dois anos a partir de 1998. Em 2006, o ritmo foi reduzido ainda mais,
com o período de 2006 a 2010 testemunhando somente uma duplicação na capacidade.
Melhorando o desempenho da memória dentro
de um chip de DRAM
À medida que a lei de Moore continua a fornecer mais transistores e a diferença entre
processador-memória aumenta a pressão sobre o desempenho da memória, as ideias da
seção anterior se encaminharam para dentro do chip da DRAM. Geralmente, a inovação
tem levado a maior largura de banda, às vezes ao custo da maior latência. Esta subseção
apresenta técnicas que tiram proveito da natureza das DRAMs.
Como já mencionamos, um acesso à DRAM é dividido em acessos de linha e acessos de
coluna. As DRAMs precisam colocar uma linha de bits no buffer dentro da DRAM para o
acesso de coluna, e essa linha normalmente é a raiz quadrada do tamanho da DRAM — por
exemplo, 2 Kb para uma DRAM de 4 Mb. Conforme as DRAMs cresceram, foram adicionadas estruturas adicionais e diversas oportunidades para aumentar a largura de banda.
Primeiramente, as DRAMs adicionaram a temporização de sinais que permitem acessos repetidos ao buffer de linha sem outro tempo de acesso à linha. Tal buffer vem naturalmente
quando cada array colocará de 1.024 a 4.096 bits no buffer para cada acesso. Inicialmente,
endereços separados de coluna tinham de ser enviados para cada transferência com um
atraso depois de cada novo conjunto de endereços de coluna.
Originalmente, as DRAMs tinham uma interface assíncrona para o controlador de memória
e, portanto, cada transferência envolvia o overhead para sincronizar com o controlador. A
segunda principal mudança foi acrescentar um sinal de clock à interface DRAM, de modo
que as transferências repetidas não sofram desse overhead. DRAM síncrona (SDRAM) é o
nome dessa otimização. Normalmente, as SDRAMs também tinham um registrador programável para manter o número de bytes solicitados e, portanto, podiam enviar muitos
bytes por vários ciclos por solicitação. Em geral, oito ou mais transferências de 16 bits
podem ocorrer sem enviar novos endereços colocando a DRAM em modo de explosão.
Esse modo, que suporta transferências de palavra crítica em primeiro lugar, é o único em
que os picos de largura de banda mostrados na Figura 2.14 podem ser alcançados.
Em terceiro lugar, para superar o problema de obter um grande fluxo de bits da memória
sem ter de tornar o sistema de memória muito grande, conforme a densidade do sistema
de memória aumenta, as DRAMs se tornaram mais largas. Inicialmente, elas ofereciam um
modo de transferência de 4 bits. Em 2010, as DRAMs DDR2 e DDR3 tinham barramentos
de até 16 bits.
A quarta inovação importante da DRAM para aumentar a largura de banda é transferir
dados tanto na borda de subida quanto na borda de descida no sinal de clock da DRAM,
dobrando assim a taxa de dados de pico. Essa otimização é chamada taxa de dados dupla
(Double Data Rate — DDR).
2.3
Tecnologia de memória e otimizações
FIGURA 2.14 Taxas de clock, largura de banda e nomes de DRAMs e DIMMs DDR em 2010.
Observe o relacionamento numérico entre as colunas. A terceira coluna é o dobro da segunda, e a quarta usa o número da terceira coluna no nome
do chip DRAM. A quinta coluna é oito vezes a terceira coluna, e uma versão arredondada desse número é usada no nome do DIMM. Embora não
aparecendo nessa figura, as DDRs também especificam a latência em ciclos de clock. Por exemplo, o DDR3-2000 CL 9 tem latências de 9-9-9-28.
O que isso significa? Com um clock de 1 ns (o ciclo de clock é metade da taxa de transferência), isso indica 9 ns para endereços de linha
para coluna (tempo RAS), 9 ns para acesso de coluna aos dados (tempo CAS) e um tempo de leitura mínimo de 28 ns. Fechar a linha leva 9 ns
para pré-carregamento, mas ocorre somente quando as leituras da linha foram completadas. Em modo de explosão, o pré-carregamento não é necessário
até que toda a linha seja lida. O DDR4 será produzido em 2013 e espera-se que atinja taxas de clock de 1.600 MHz em 2014, quando se espera
que o DDR5 assuma. Os exercícios exploram esses detalhes.
Para fornecer algumas das vantagens do interleaving, além de ajudar no gerenciamento de
energia, as SDRAMs também introduziram os bancos, afastando-se de uma única SDRAM
para 2-8 blocos (nas DRAMs DDR3 atuais), que podem operar independentemente (já
vimos bancos serem usados em caches internas; muitas vezes, eles são usados em grandes
memórias principais). Criar múltiplos bancos dentro de uma DRAM efetivamente adiciona
outro segmento ao endereço, que agora consiste em número do banco, endereço da linha
e endereço da coluna. Quando é enviado um endereço que designa um novo banco, esse
banco deve ser aberto, incorrendo em um atraso adicional. O gerenciamento de bancos
e buffers de linha é manipulado completamente por interfaces modernas de controle de
memória, de modo que, quando um acesso subsequente especifica a mesma linha por
um banco aberto, o acesso pode ocorrer rapidamente, enviando somente o endereço da
coluna.
Quando as SDRAMs DDR são montadas como DIMMs, são rotuladas pela largura de banda
DIMM de pico, confusamente. Logo, o nome do DIMM PC2100 vem de 133 MHz × 2 × 8
bytes ou 2.100 MB/s. Sustentando a confusão, os próprios chips são rotulados como
número de bits por segundo, em vez da sua taxa de clock, de modo que um chip DDR de
133 MHz é chamado de DDR266. A Figura 2.14 mostra o relacionamento entre a taxa de
clock, as transferências por segundo por chip, nome de chip, largura de banda de DIMM
e nome de DIMM.
A DDR agora é uma sequência de padrões. A DDR2 reduz a potência diminuindo a
voltagem de 2,5 volts para 1,8 volt e oferece maiores taxas de clock: 266 MHz, 333 MHz
e 400 MHz. A DDR3 reduz a voltagem para 1,5 volt e tem velocidade de clock máxima de
800 MHz. A DDR4, prevista para entrar em produção em 2014, diminui a voltagem para
1-1,2 volts e tem taxa de clock máxima esperada de 1.600 MHz. A DDR5 virá em 2014
ou 2015. (Como discutiremos na próxima seção, a GDDR5 é uma RAM gráfica e baseada
nas DRAMs DDR3.)
87
88
CAPÍTULO 2 :
Projeto de hierarquia de memória
RAMs de dados gráficos
GDRAMs ou GSDRAMs (DRAMs gráficas ou gráficas síncronas) são uma classe especial
de DRAMs baseadas nos projetos da SDRAM, mas ajustadas para lidar com as exigências
maiores de largura de banda das unidades de processamento gráfico. A GDDR5 é baseada
na DDR3, com as primeiras GDDRs baseadas na DDR2. Uma vez que as unidades de
processamento gráfico (Graphics Processor Units — GPUs; Cap. 4) requerem mais
largura de banda por chip de DRAM do que as CPUs, as GDDRs têm várias diferenças
importantes:
1. As GDDRs têm interfaces mais largas: 32 bits contra 4, 8 ou 16 nos projetos atuais.
2. As GDDRs têm taxa máxima de clock nos pinos de dados. Para permitir taxa
de transferência maior sem incorrer em problemas de sinalização, em geral as
GDRAMS se conectam diretamente à GPU e são conectadas por solda à placa, ao
contrário das DRAMs, que costumam ser colocadas em um array barato de DIMMs.
Juntas, essas características permitem às GDDRs rodar com 2-5 vezes a largura de banda
por DRAM em comparação às DRAMs DDR3, uma vantagem significativa para suportar as
GPUs. Devido à menor distância entre as requisições de memória em um GPU, o modo
burst geralmente é menos útil para uma GPU, mas manter múltiplos bancos de memória
abertos e gerenciar seu uso melhora a largura de banda efetiva.
Reduzindo o consumo de energia nas SDRAMs
O consumo de energia em chips de memória dinâmica consiste na potência dinâmica
usada em uma leitura ou escrita e na potência estática ou de stand-by. As duas dependem
da voltagem de operação. Nos SDRAMs DDR3 mais avançados, a voltagem de operação
caiu para 1,35-1,5 volt, reduzindo significativamente o consumo de energia em relação
às SDRAMs DDR2. A adição dos bancos também reduziu o consumo de energia, uma vez
que somente a ilha em um único banco é lida e pré-carregada.
Além dessas mudanças, todas as SDRAMs recentes suportam um modo de power down,
que é iniciado dizendo à DRAM para ignorar o clock. O modo power down desabilita a
SDRAM, exceto pela atualização interna automática (sem a qual entrar no modo power
down por mais tempo do que o tempo de atualização vai fazer o conteúdo da memória ser
perdido). A Figura 2.15 mostra o consumo de energia em três situações em uma SDRAM
FIGURA 2.15 Consumo de energia para uma SDRAM DDR3 operando sob três condições: modo de
baixa potência (shutdown), modo de sistema típico (a DRAM está ativa 30% do tempo para leituras e 15%
para escritas) e modo totalmente ativo, em que a DRAM está continuamente lendo ou escrevendo quando
não está em pré-carregamento.
Leituras e escritas assumem burts de oito transferências. Esses dados são baseados em um Micron 1,5 V de 2 Gb
DDR3-1066.
2.3
Tecnologia de memória e otimizações
DDR3 de 2 GB. O atraso exato necessário para retornar do modo de baixa potência depende
da SDRAM, mas um tempo típico da atualização do modo de baixa potência é de 200
ciclos de clock. Pode ser necessário tempo adicional para resetar o registrador de modo
antes do primeiro comando.
Memória Flash
A memória Flash é um tipo de EEPROM (Electronically Erasable Programmable Read-Only
Memory), que normalmente se presta somente à leitura, mas pode ser apagada. A outra
propriedade-chave da memória Flash é que ela mantém seu conteúdo sem qualquer
alimentação.
A memória Flash é usada como armazenamento de backup nos PMDs do mesmo modo
que um disco em um laptop ou servidor. Além disso, uma vez que a maioria dos PMDs
tem quantidade limitada de DRAM, a memória Flash também pode agir como um nível
da hierarquia de memória, muito mais do que precisaria ser no desktop ou no servidor
com uma memória principal que pode ser de 10-100 vezes maior.
A memória Flash usa uma arquitetura muito diferente e tem propriedades diferentes da
DRAM padrão. As diferenças mais importantes são:
1. A memória Flash deve ser apagada (por isso o nome Flash, do processo “flash”
de apagar) antes que seja sobrescrita, e isso deve ser feito em blocos (em memórias
Flash de alta densidade, chamadas NAND Flash, usadas na maioria das aplicações
de computador) em vez de bytes ou palavras individuais. Isso significa que, quando
dados precisam ser gravados em uma memória Flash, todo um bloco deve ser
montado, seja como um bloco de dados novos, seja mesclando os dados a serem
gravados e o resto do conteúdo do bloco.
2. A memória Flash é estática (ou seja, ela mantém seu conteúdo mesmo quando
não é aplicada energia) e consome significativamente menos energia quando
não se está lendo ou gravando (de menos de metade em modo stand-by a zero
quando completamente inativa).
3. A memória Flash tem número limitado de ciclos de escrita em qualquer bloco (em
geral, pelo menos 100.000). Ao garantir a distribuição uniforme dos blocos escritos
por toda a memória, um sistema pode maximizar o tempo de vida de um sistema
de memória Flash.
4. Memórias Flash de alta densidade são mais baratas do que a SDRAM, porém são
mais caras do que discos: aproximadamente US$ 2/GB para Flash, US$ 20-40/GB
para SDRAM e US$ 0,09/GB para discos magnéticos.
5. A memória Flash é muito mais lenta do que a SDRAM, porém muito mais rápida
do que um disco. Por exemplo, uma transferência de 256 bytes de uma memória
Flash típica de alta densidade leva cerca de 6,5 ms (usando uma transferência em
modo de explosão similar, porém mais lenta do que a usada na SDRAM). Uma
transferência comparável de uma SDRAM DDR leva cerca de um quarto desse
tempo, e para um disco é cerca de 1.000 vezes mais demorada. Para escritas, a
diferença é consideravelmente maior, com a SDRAM sendo pelo menos 10 vezes
e no máximo 100 vezes mais rápida do que a memória Flash, dependendo das
circunstâncias.
As rápidas melhorias na memória Flash de alta densidade na década passada tornaram
a tecnologia uma parte viável das hierarquias de memória em dispositivos móveis e
também como substitutos dos discos. Conforme a taxa de aumento na densidade da
DRAM continua a cair, a memória Flash pode ter um papel cada vez maior nos sistemas
89
90
CAPÍTULO 2 :
Projeto de hierarquia de memória
de memória futuros, agindo tanto como um substituto para os discos rígidos quanto como
armazenamento intermediário entre a DRAM e o disco.
Aumentando a confiabilidade em sistemas de memória
Caches e memórias principais grandes aumentam significativamente a possibilidade
de erros ocorrerem tanto durante o processo de fabricação quanto dinamicamente,
principalmente resultantes de raios cósmicos que atingem a célula de memória. Esses
erros dinâmicos, que são mudanças no conteúdo de uma célula, e não uma mudança
nos circuitos, são chamados soft errors. Todas as DRAMs, memórias Flash e muitas SRAMs
são fabricadas com linhas adicionais para que pequeno número de defeitos de fabricação
possa ser acomodado, programando a substituição de uma linha defeituosa por uma
linha adicional. Além de erros de fabricação que podem ser reparados no momento da
configuração, também podem ocorrer na operação os hard errors, que são mudanças permanentes na operação de uma ou mais células de memória.
Erros dinâmicos podem ser detectados por bits de paridades, detectados e corrigidos
pelo uso de códigos de correção de erro (Error Correcting Codes — ECCs). Uma vez
que as caches de instrução são somente para leitura, a paridade é o suficiente. Em
caches de dados maiores e na memória principal, ECCs são usados para permitir que
os erros sejam detectados e corrigidos. A paridade requer somente um bit de overhead
para detectar um único erro em uma sequência de bits. Já que um erro multibit não
seria detectado com paridade, os números de bits protegidos por um bit de paridade
devem ser limitados. Um bit de paridade para 8 bits de dados é uma razão típica. ECCs
podem detectar dois erros e corrigir um único erro com um custo de 8 bits de overhead
para 64 bits de dados.
Em sistema grandes, a possibilidade de múltiplos erros além da falha completa em
um único chip de memória se torna importante. O Chipkill foi lançado pela IBM para
solucionar esse problema, e vários sistemas grandes, como servidores IBM e SUN e os
Google Clusters, usam essa tecnologia (a Intel chama sua versão de SDDC). Similar em
natureza à técnica RAID usada para discos, o Chipkill distribui os dados e informações
de ECC para que a falha completa de um único chip de memória possa ser tratada de
modo a dar suporte à reconstrução dos dados perdidos a partir dos chips de memória
restantes. Usando uma análise da IBM e considerando um servidor de 10.000 processadores com 4 GB por processador, gera as seguintes taxas de erros irrecuperáveis em três
anos de operação:
j
j
j
Somente paridade — cerca de 90.000 ou uma falha irrecuperável (ou não detectada)
a cada 17 minutos.
Somente ECC — cerca de 3.500 ou cerca de uma falha não detectada ou
irrecuperável a cada 7,5 horas.
Chipkill — 6 ou cerca de uma falha não detectada ou irrecuperável a cada dois
meses.
Outro modo de ver isso é verificar o número máximo de servidores (cada um com 4 GB)
que pode ser protegido, ao mesmo tempo que temos a mesma taxa de erros demonstrada
para o Chipkill. Para a paridade, mesmo um servidor com um único processador terá uma
taxa de erro irrecuperável maior do que um sistema de 10.000 servidores protegido por
Chipkill. Para a ECC, um sistema de 17 servidores teria aproximadamente a mesma taxa
de falhas que um sistema Chipkill com 10.000 servidores. Portanto, o Chipkill é uma
exigência para os servidores de 50.000-100.000 em computadores em escala warehouse
(Seção 6.8, no Cap. 6).
2.4
Proteção: memória virtual e máquinas virtuais
2.4 PROTEÇÃO: MEMÓRIA VIRTUAL E MÁQUINAS
VIRTUAIS
Uma máquina virtual é levada a ser uma duplicata eficiente e isolada da máquina
real. Explicamos essas noções por meio da ideia de um monitor de máquina
virtual (Virtual Machine Monitor — VMM) … um VMM possui três características
essenciais: 1) oferece um ambiente para programas que é basicamente idêntico
ao da máquina original; 2) os programas executados nesse ambiente mostram,
no pior dos casos, apenas pequeno decréscimo na velocidade; 3) está no controle
total dos recursos do sistema.
Gerald Popek e Robert Goldberg
“Formal requirements for virtualizable third generation architectures”,
Communications of the ACM (julho de 1974).
Segurança e privacidade são dois dos desafios mais irritantes para a tecnologia da informação em 2011. Roubos eletrônicos, geralmente envolvendo listas de números de cartão de
crédito, são anunciados regularmente, e acredita-se que muitos outros não sejam relatados.
Logo, tanto pesquisadores quanto profissionais estão procurando novas maneiras de
tornar os sistemas de computação mais seguros. Embora a proteção de informações não
seja limitada ao hardware, em nossa visão segurança e privacidade reais provavelmente
envolverão a inovação na arquitetura do computador, além dos sistemas de software.
Esta seção começa com uma revisão do suporte da arquitetura para proteger os processos
uns dos outros, por meio da memória virtual. Depois, ela descreve a proteção adicional
fornecida a partir das máquinas virtuais, os requisitos de arquitetura das máquinas virtuais
e o desempenho de uma máquina virtual. Como veremos no Capítulo 6, as máquinas
virtuais são uma tecnologia fundamental para a computação em nuvem.
Proteção via memória virtual
A memória virtual baseada em página, incluindo um TLB (Translation Lookaside Buffer),
que coloca em cache as entradas da tabela de página, é o principal mecanismo que protege
os processos uns dos outros. As Seções B.4 e B.5, no Apêndice B, revisam a memória
virtual, incluindo uma descrição detalhada da proteção via segmentação e paginação
no 80x86. Esta subseção atua como uma breve revisão; consulte essas seções se ela for
muito rápida.
A multiprogramação, pela qual vários programas executados simultaneamente compartilhariam um computador, levou a demandas por proteção e compartilhamento entre
programas e ao conceito de processo. Metaforicamente, processo é o ar que um programa
respira e seu espaço de vida, ou seja, um programa em execução mais qualquer estado
necessário para continuar executando-o. A qualquer instante, deve ser possível passar de
um processo para outro. Essa troca é chamada de troca de processo ou troca de contexto.
O sistema operacional e a arquitetura unem forças para permitir que os processos compartilhem o hardware, sem interferir um com o outro. Para fazer isso, a arquitetura precisa
limitar o que um processo pode acessar ao executar um processo do usuário, permitindo
ainda que um processo do sistema operacional acesse mais. No mínimo, a arquitetura
precisa fazer o seguinte:
1. Oferecer pelo menos dois modos, indicando se o processo em execução
é do usuário ou do sistema operacional. Este último processo, às vezes, é chamado
de processo kernel ou processo supervisor.
91
92
CAPÍTULO 2 :
Projeto de hierarquia de memória
2. Oferecer uma parte do status do processador que um processo do usuário pode
usar, mas não escrever. Esse status inclui bit(s) de modo usuário/supervisor, bit de
ativar/desativar exceção e informações de proteção de memória. Os usuários são
impedidos de escrever nesse status, pois o sistema operacional não poderá controlar
os processos do usuário se eles puderem se dar privilégios de supervisor, desativar
exceções ou alterar a proteção da memória.
3. Oferecer mecanismos pelos quais o processador pode ir do modo usuário para o
modo supervisor e vice-versa. A primeira direção normalmente é alcançada por
uma chamada do sistema, implementada como uma instrução especial que transfere
o controle para um local determinado no espaço de código do supervisor. O PC
é salvo a partir do ponto da chamada do sistema, e o processador é colocado no
modo supervisor. O retorno ao modo usuário é como um retorno de sub-rotina,
que restaura o modo usuário/supervisor anterior.
4. Oferecer mecanismos para limitar os acessos à memória a fim de proteger o estado
da memória de um processo sem ter de passar o processo para o disco em uma troca
de contexto.
O Apêndice A descreve vários esquemas de proteção de memória, mas o mais popular
é, sem dúvida, a inclusão de restrições de proteção a cada página da memória virtual. As
páginas de tamanho fixo, normalmente com 4 KB ou 8 KB de extensão, são mapeadas a
partir do espaço de endereços virtuais para o espaço de endereços físicos, por meio de uma
tabela de página. As restrições de proteção estão incluídas em cada entrada da tabela de
página. As restrições de proteção poderiam determinar se um processo do usuário pode
ler essa página, se um processo do usuário pode escrever nessa página e se o código pode
ser executado a partir dessa página. Além disso, um processo não poderá ler nem escrever
em uma página se não estiver na tabela de página. Como somente o SO pode atualizar a
tabela de página, o mecanismo de paginação oferece proteção de acesso total.
A memória virtual paginada significa que cada acesso à memória usa logicamente pelo
menos o dobro do tempo, com um acesso à memória para obter o endereço físico e um
segundo acesso para obter os dados. Esse custo seria muito alto. A solução é contar com
o princípio da localidade, se os acessos tiverem proximidade; então, as traduções de acesso
para os acessos também precisam ter proximidade. Mantendo essas traduções de endereço
em uma cache especial, um acesso à memória raramente requer um segundo acesso para
traduzir os dados. Essa cache de tradução de endereço especial é conhecida como Translation Lookaside Buffer (TLB).
A entrada do TLB é como uma entrada de cache em que a tag mantém partes do endereço
virtual e a parte de dados mantém um endereço de página físico, campo de proteção,
bit de validade e normalmente um bit de utilização e um bit de modificação. O sistema
operacional muda esses bits alterando o valor na tabela de página e depois invalidando
a entrada de TLB correspondente. Quando a entrada é recarregada da tabela de página, o
TLB apanha uma cópia precisa dos bits.
Considerando que o computador obedece fielmente às restrições nas páginas e mapeia os
endereços virtuais aos endereços físicos, isso pode parecer o fim. As manchetes de jornal
sugerem o contrário.
O motivo pelo qual isso não é o fim é que dependemos da exatidão do sistema operacional
e também do hardware. Os sistemas operacionais de hoje consistem em dezenas de milhões de linhas de código. Como os bugs são medidos em número por milhares de linhas
de código, existem milhares de bugs nos sistemas operacionais em produção. As falhas
no SO levaram a vulnerabilidades que são exploradas rotineiramente.
2.4
Proteção: memória virtual e máquinas virtuais
Esse problema e a possibilidade de que não impor a proteção poderia ser muito mais custoso do que no passado têm levado algumas pessoas a procurarem um modelo de proteção
com uma base de código muito menor do que o SO inteiro, como as máquinas virtuais.
Proteção via máquinas virtuais
Uma ideia relacionada à com a memória virtual que é quase tão antiga é a de máquinas
virtuais (Virtual Machines — VM). Elas foram desenvolvidas no final da década de 1960
e continuaram sendo uma parte importante da computação por mainframe durante anos.
Embora bastante ignoradas no domínio dos computadores monousuários nas décadas de
1980 e 1990, recentemente elas ganharam popularidade devido:
j
j
j
j
à crescente importância do isolamento e da segurança nos sistemas modernos;
às falhas na segurança e confiabilidade dos sistemas operacionais padrão;
ao compartilhamento de um único computador entre muitos usuários não
relacionados;
aos aumentos drásticos na velocidade bruta dos processadores, tornando o overhead
das VMs mais aceitável.
A definição mais ampla das VMs inclui basicamente todos os métodos de emulação
que oferecem uma interface de software-padrão, como a Java VM. Estamos interessados
nas VMs que oferecem um ambiente completo em nível de sistema, no nível binário da
arquitetura do conjunto de instruções (Instruction Set Architecture — ISA). Muitas vezes,
a VM suporta a mesma ISA que o hardware nativo. Entretanto, também é possível suportar
uma ISA diferente, e tais técnicas muitas vezes são empregadas quando migramos entre
ISAs para permitir que o software da ISA original seja usado até que possa ser transferido
para a nova ISA. Nosso foco será em VMs onde a ISA apresentada pela VM e o hardware
nativo combinam. Essas VMs são chamadas máquinas virtuais do sistema (operacional).
IBM VM/370, VMware ESX Server e Xen são alguns exemplos. Elas apresentam a ilusão de
que os usuários de uma VM possuem um computador inteiro para si mesmos, incluindo
uma cópia do sistema operacional. Um único computador executa várias VMs e pode
dar suporte a uma série de sistemas operacionais (SOs) diferentes. Em uma plataforma
convencional, um único SO “possui” todos os recursos de hardware, mas com uma VM
vários SOs compartilham os recursos do hardware.
O software que dá suporte às VMs é chamado monitor de máquina virtual (Virtual Machine
Monitor — VMM) ou hipervisor; o VMM é o coração da tecnologia de máquina virtual.
A plataforma de hardware subjacente é chamada de hospedeiro (host), e seus recursos são
compartilhados entre as VMs de convidadas (guests). O VMM determina como mapear os
recursos virtuais aos recursos físicos: um recurso físico pode ser de tempo compartilhado,
particionado ou até mesmo simulado no software. O VMM é muito menor do que um SO
tradicional; a parte de isolamento de um VMM talvez tenha apenas 10.000 linhas de código.
Em geral, o custo de virtualização do processador depende da carga de trabalho. Os programas voltados a processador em nível de usuário, como o SPEC CPU2006, possuem
zero overhead de virtualização, pois o SO raramente é chamado, de modo que tudo é
executado nas velocidades nativas. Em geral, cargas de trabalho com uso intenso de E/S
também utilizam intensamente o SO, que executa muitas chamadas do sistema e instruções privilegiadas, o que pode resultar em alto overhead de virtualização. O overhead
é determinado pelo número de instruções que precisam ser simuladas pelo VMM e pela
lentidão com que são emuladas. Portanto, quando as VMs convidadas executam a mesma
ISA que o host, conforme presumimos aqui, o objetivo da arquitetura e da VMM é executar
quase todas as instruções diretamente no hardware nativo. Por outro lado, se a carga de
93
94
CAPÍTULO 2 :
Projeto de hierarquia de memória
trabalho com uso intensivo de E/S também for voltada para E/S, o custo de virtualização
do processador pode ser completamente ocultado pela baixa utilização do processador,
pois ele está constantemente esperando pela E/S.
Embora nosso interesse aqui seja nas VMs para melhorar a proteção, elas oferecem dois
outros benefícios que são comercialmente significativos:
1. Gerenciamento de software. As VMs oferecem uma abstração que pode executar um
conjunto de software completo, incluindo até mesmo sistemas operacionais antigos,
como o DOS. Uma implantação típica poderia ser algumas VMs executando SOs
legados, muitas executando a versão atual estável do SO e outras testando a próxima
versão do SO.
2. Gerenciamento de hardware. Um motivo para múltiplos servidores é ter cada
aplicação executando com a versão compatível do sistema operacional em
computadores separados, pois essa separação pode melhorar a dependência.
As VMs permitem que esses conjuntos separados de software sejam executadas
independentemente, embora compartilhem o hardware, consolidando assim o
número de servidores. Outro exemplo é que alguns VMMs admitem a migração
de uma VM em execução para um computador diferente, seja para balancear
a carga seja para abandonar o hardware que falha.
É por essas duas razões que os servidores baseados na nuvem, como os da Amazon, contam
com máquinas virtuais.
Requisitos de um monitor de máquina virtual
O que um monitor de VM precisa fazer? Ele apresenta uma interface de software para o
software convidado, precisa isolar o status dos convidados uns dos outros e proteger-se
contra o software convidado (incluindo SOs convidados). Os requisitos qualitativos são:
j
j
O software convidado deve se comportar em uma VM exatamente como se
estivesse rodando no hardware nativo, exceto pelo comportamento relacionado com o
desempenho ou pelas limitações dos recursos fixos compartilhados por múltiplas VMs.
O software convidado não deverá ser capaz de mudar a alocação dos recursos reais
do sistema diretamente.
Para “virtualizar” o processador, o VMM precisa controlar praticamente tudo — acesso ao
estado privilegiado, tradução de endereço, E/S, exceções e interrupções —, embora a VM
e o SO convidados em execução os estejam usando temporariamente.
Por exemplo, no caso de uma interrupção de timer, o VMM suspenderia a VM convidada
em execução, salvaria seu status, trataria da interrupção, determinaria qual VM convidada
será executada em seguida e depois carregaria seu status. As VMs convidadas que contam
com interrupção de timer são fornecidas com um timer virtual e uma interrupção de timer
simulada pelo VMM.
Para estar no controle, o VMM precisa estar em um nível de privilégio mais alto do que
a VM convidada, que geralmente executa 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 àqueles para a memória virtual
paginada, que listamos anteriormente.
j
j
Pelo menos dois modos do processador, sistema e usuário.
Um subconjunto privilegiado de instruções, que está disponível apenas no modo
do sistema, resultando em um trap se for executado no modo usuário. Todos os
recursos do sistema precisam ser controláveis somente por meio dessas instruções.
2.4
Proteção: memória virtual e máquinas virtuais
(Falta de) Suporte à arquitetura de 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 precisam ser executadas por um VMM e o tempo necessário
para simulá-las. Uma arquitetura que permite que a VM execute diretamente no hardware
ganha o título de virtualizável, e a arquitetura IBM 370 orgulhosamente ostenta este título.
Infelizmente, como as VMs só foram consideradas para aplicações desktop e servidor
baseado em PC muito recentemente, a maioria dos conjuntos de instruções foi criada sem
que se pensasse na virtualização. Entre esses culpados incluem-se o 80x86 e a maioria das
arquiteturas RISC.
Como o VMM precisa garantir que o sistema convidado só interaja com recursos virtuais, um
SO convidado convencional é executado como um programa no modo usuário em cima do
VMM. Depois, se um SO convidado tentar acessar ou modificar informações relacionadas
com os recursos do software por meio de uma instrução privilegiada — por exemplo, lendo
ou escrevendo o ponteiro da tabela de página —, ele gerará um trap para o VMM. O VMM
pode, então, efetuar as mudanças apropriadas aos recursos reais correspondentes.
Logo, se qualquer instrução tentar ler ou escrever essas informações sensíveis ao trap,
quando executada no modo usuário, a VMM poderá interceptá-la e dar suporte a uma
versão virtual da informação sensível, como o SO convidado espera.
Na ausência de tal suporte, outras medidas precisam ser tomadas. Um VMM deve tomar
precauções especiais para localizar todas as instruções problemáticas e garantir que se
comportem corretamente quando executadas por um SO convidado, aumentando assim
a complexidade do VMM e reduzindo o desempenho da execução da VM.
As Seções 2.5 e 2.7 oferecem exemplos concretos de instruções problemáticas na arquitetura 80x86.
Impacto das máquinas virtuais sobre a memória virtual e a E/S
Outro desafio é a virtualização da memória virtual, pois cada SO convidado 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 memória física (que normalmente são tratadas como
sinônimos) e torna a memória real um nível separado, intermediário entre a memória
virtual e a memória física (alguns usam os nomes memória virtual, memória física e memória de máquina para indicar os mesmos três níveis). O SO convidado mapeia a memória
virtual à memória real por meio de suas tabelas de página, e as tabelas de página do VMM
mapeiam a memória real dos convidados à memória física. A arquitetura de memória
virtual é especificada por meio de tabelas de página, como no IBM VM/370 e no 80x86,
ou por meio da estrutura de TLB, como em muitas arquiteturas RISC.
Em vez de pagar um nível extra de indireção em cada acesso à memória, o VMM mantém
uma tabela de página de sombra (shadow page table), que é mapeada diretamente do espaço
de endereço virtual do convidado ao espaço do hardware. Detectando todas as modificações à tabela de página do convidado, o VMM pode garantir que as entradas da tabela de
página de sombra sendo usadas pelo hardware para traduções correspondam àquelas do
ambiente do SO convidado, com a exceção das páginas físicas corretas substituídas pelas
páginas reais nas tabelas convidadas. Logo, o VMM precisa interceptar qualquer tentativa
do SO convidado de alterar sua tabela de página ou de acessar o ponteiro da tabela de
página. Isso normalmente é feito protegendo a escrita das tabelas de página convidadas
e interceptando qualquer acesso ao ponteiro da tabela de página por um SO convidado.
95
96
CAPÍTULO 2 :
Projeto de hierarquia de memória
Conforme indicamos, o último acontecerá naturalmente se o acesso ao ponteiro da tabela
de página for uma operação privilegiada.
A arquitetura IBM 370 solucionou o problema da tabela de página na década de 1970 com
um nível adicional de indireção que é gerenciado pelo VMM. O SO convidado mantém
suas tabelas de página como antes, de modo que as páginas de sombra são desnecessárias.
A AMD propôs um esquema semelhante para a sua revisão pacífica do 80x86.
Para virtualizar o TLB arquitetado em muitos computadores RISC, o VMM gerencia
o TLB real e tem uma cópia do conteúdo do TLB de cada VM convidada. Para liberar
isso, quaisquer instruções que acessem o TLB precisam gerar traps. Os TLBs com
tags Process ID podem aceitar uma mistura de entradas de diferentes VMs e o VMM,
evitando assim o esvaziamento do TBL em uma troca de VM. Nesse meio-tempo, em
segundo plano, o VMM admite um mapeamento entre os Process IDs virtuais da VM
e os Process IDs reais.
A última parte da arquitetura para virtualizar é a E/S. Essa é a parte mais difícil da virtualização do sistema, devido ao número crescente de dispositivos de E/S conectados ao
computador e à diversidade crescente de tipos de dispositivo de E/S. Outra dificuldade é
o compartilhamento de um dispositivo real entre múltiplas VMs, e outra ainda vem do
suporte aos milhares de drivers de dispositivo que são exigidos, especialmente se diferentes
OS convidados forem admitidos no mesmo sistema de VM. A ilusão da 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 o tratamento da E/S real.
O método para mapear um dispositivo de E/S virtual para físico depende do tipo de dispositivo. Por exemplo, os discos físicos normalmente são particionados pelo VMM para
criar discos virtuais para as VMs convidadas, e o VMM mantém o mapeamento de trilhas
e setores virtuais aos equivalentes físicos. As interfaces de rede normalmente são compartilhadas entre as VMs em fatias de tempo muito curtas, e a tarefa do VMM é registrar as
mensagens para os endereços de rede virtuais a fim de garantir que as VMs convidadas
recebam apenas mensagens enviadas para elas.
Uma VMM de exemplo: a máquina virtual Xen
No início do desenvolvimento das VMs, diversas ineficiências se tornaram aparentes. Por
exemplo, um SO convidado gerencia seu mapeamento de página, mas esse mapeamento
é ignorado pelo VMM, que realiza o mapeamento real para as páginas físicas. Em outras
palavras, quantidade significativa de esforço desperdiçado é gasta apenas para satisfazer
o SO convidado. Para reduzir tais ineficiências, os desenvolvedores de VMM decidiram
que pode valer a pena permitir que o SO convidado esteja ciente de que está rodando
em uma VM. Por exemplo, um SO convidado poderia pressupor uma memória real tão
grande quanto sua memória virtual, de modo que nenhum gerenciamento de memória
será exigido pelo SO convidado.
A permissão de pequenas modificações no SO convidado para simplificar a virtualização
é conhecida como paravirtualização, e o VMM Xen, de fonte aberto, é um bom exemplo disso. O VMM Xen oferece a um SO convidado uma abstração de máquina virtual
semelhante ao hardware físico, mas sem muitas das partes problemáticas. Por exemplo,
para evitar o esvaziamento do TBL, o Xen é mapeado nos 64 MB superiores do espaço
de endereços de cada VM. Ele permite que o SO convidado aloque páginas, apenas
cuidando para que não infrinja as restrições de proteção. Para proteger o SO convidado
contra programas do usuário na VM, o Xen tira proveito dos quatro níveis de proteção
disponíveis no 80x86. O VMM Xen é executado no mais alto nível de privilégio (0), o SO
2.5
Questões cruzadas: o projeto de hierarquias de memória
convidado é executado no próximo nível de privilégio (1) e as aplicações são executadas
no nível de privilégio mais baixo (3). A maioria dos SOs para o 80x86 mantém tudo nos
níveis de privilégio 0 ou 3.
Para que as sub-redes funcionem corretamente, o Xen modifica o SO convidado para
não usar partes problemáticas da arquitetura. Por exemplo, a porta do Linux para o Xen
alterou cerca de 3.000 linhas, ou cerca de 1% do código específico do 80x86. Porém, essas
mudanças não afetam as interfaces binárias da aplicação do SO convidado.
Para simplificar o desafio de E/S das VMs, recentemente o Xen atribuiu máquinas virtuais
privilegiadas a cada dispositivo de E/S de hardware. Essas VMs especiais são chamadas
domínios de driver (o Xen chama suas VMs de “domínios”). Os domínios de driver executam
os drivers do dispositivo físico, embora as interrupções ainda sejam tratadas pela VMM
antes de serem enviadas para o domínio de driver apropriado. As VMs regulares, chamadas
domínios de convidado, executam drivers de dispositivo virtuais simples, que precisam se
comunicar com os drivers de dispositivo físicos nos domínios de driver sobre um canal para
acessar o hardware de E/S físico. Os dados são enviados entre os domínios de convidado
e driver pelo remapeamento de página.
2.5 QUESTÕES CRUZADAS: O PROJETO
DE HIERARQUIAS DE MEMÓRIA
Esta seção descreve três tópicos abordados em outros capítulos que são fundamentais para
as hierarquias de memória.
Proteção e arquitetura de conjunto de instruções
A proteção é um esforço conjunto de arquitetura e sistemas operacionais, mas os arquitetos
tiveram de modificar alguns detalhes esquisitos 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 alterar a bem-sucedida arquitetura
do conjunto de instruções do IBM 360, que havia sido anunciada seis anos antes. Ajustes
semelhantes estão sendo feitos hoje para acomodar as máquinas virtuais.
Por exemplo, a instrução POPF do 80x86 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 mudará todos os flags,
exceto IE. No modo do sistema, ela não mudará o IE. Como um SO convidado é executado
no modo usuário dentro de uma VM, isso é um problema, pois ele espera ver o IE alterado.
Extensões da arquitetura 80x86 para suportar a virtualização eliminaram esse problema.
Historicamente, o hardware de mainframe IBM e o VMM utilizavam três passos para melhorar o desempenho das máquinas virtuais:
1. Reduzir o custo da virtualização do processador.
2. Reduzir o custo de overhead de interrupção devido à virtualização.
3. Reduzir o custo de interrupção direcionando as interrupções para a VM apropriada
sem invocar o VMM.
A IBM ainda é o padrão dourado da tecnologia de máquina virtual. Por exemplo, um
mainframe IBM executava milhares de VMs Linux em 2000, enquanto o Xen executava
25 VMs, em 2004 (Clark et al., 2004). Versões recentes de chipsets adicionaram instruções
especiais para suportar dispositivos em uma VM, para mascarar interrupções em níveis
inferiores de cada VM e para direcionar interrupções para a VM apropriada.
97
98
CAPÍTULO 2 :
Projeto de hierarquia de memória
Consistência dos dados em cache
Os dados podem ser encontrados na memória e na cache. Desde que um processador
seja o único dispositivo a alterar ou ler os dados e a cache fique entre o processador e a
memória, haverá pouco perigo de o processador ver a cópia antiga ou desatualizada (stale).
Conforme mencionaremos no Capítulo 4, múltiplos processadores e dispositivos de E/S
aumentam a oportunidade de as cópias serem inconsistentes e de lerem a cópia errada.
A frequência do problema de coerência de cache é diferente para multiprocessadores e
para E/S. Múltiplas cópias de dados são um evento raro para E/S — que deve ser evitado
sempre que possível —, mas um programa em execução em múltiplos processadores
desejará ter cópias dos mesmos dados em várias caches. O desempenho de um programa
multiprocessador depende do desempenho do sistema ao compartilhar dados.
A questão de coerência da cache de E/S é esta: onde ocorre a E/S no computador — entre o
dispositivo de E/S e a cache ou entre o dispositivo de E/S e a memória principal? Se a entrada colocar dados na cache e a saída ler dados da cache, tanto a E/S quanto o processador
verão os mesmos dados. A dificuldade dessa técnica é que ela interfere com o processador e
pode fazer com que o processador pare para a E/S. A entrada também pode interferir com
a cache, deslocando alguma informação com dados novos que provavelmente serão acessados em breve.
O objetivo para o sistema de E/S em um computador com cache é impedir o problema dos
dados desatualizados, enquanto interfere o mínimo possível. Muitos sistemas, portanto,
preferem que a E/S ocorra diretamente na memória principal, com a memória principal
atuando como um buffer de E/S. Se uma cache write-through for usada, a memória terá
uma cópia atualizada da informação e não haverá o problema de dados passados para a
saída (esse benefício é um motivo para os processadores usarem a cache write-through).
Infelizmente, hoje o write-through normalmente é encontrado apenas nas caches de dados
de primeiro nível, apoiados por uma cache L2 que use write-back.
A entrada requer algum trabalho extra. A solução de software é garantir que nenhum bloco
do buffer de entrada esteja na cache. Uma página contendo o buffer pode ser marcada
como não passível de cache (noncachable), e o sistema operacional sempre poderá entrar em tal página. Como alternativa, o sistema operacional pode esvaziar os endereços
de buffer da cache antes que ocorra a entrada. Uma solução de hardware é verificar os
endereços de E/S na entrada para ver se eles estão na cache. Se houver uma correspondência
de endereços de E/S na cache, as entradas de cache serão invalidadas para evitar dados passados. Todas essas técnicas também podem ser usadas para a saída com caches write-back.
A consistência da cache do processador é um assunto essencial na era dos processadores
multicore, e vamos examiná-la em detalhes no Capítulo 5.
2.6 JUNTANDO TUDO: HIERARQUIA DE MEMÓRIA
NO ARM CORTEX-A8 E INTEL CORE I7
Esta seção desvenda as hierarquias de memória do ARM Cortex-A8 (daqui em diante chamado Cortex-A8) e do Intel Core i7 (daqui em diante chamado i7) e mostra o desempenho
de seus componentes para um conjunto de benchmarks de thread único. Nós examinamos
o Cortex-A8 primeiro porque ele tem um sistema de memória mais simples. Vamos entrar
em mais detalhes sobre o i7 detalhando uma referência de memória. Esta seção supõe
que os leitores estejam familiarizados com a organização de uma hierarquia de cache de
dois níveis usando caches indexadas virtualmente. Os elementos básicos de tal sistema
de memória são explicados em detalhes no Apêndice B, e os leitores que não estão acos-
2.6
Juntando tudo: hierarquia de memória no ARM Cortex-A8 e Intel Core i7
tumados com a organização desses sistemas são enfaticamente aconselhados a revisar o
exemplo do Opteron no Apêndice B. Após a compreensão da organização do Opteron,
a breve explicação sobre o sistema Cortex-A8, que é similar, será fácil de acompanhar.
O ARM Cortex-A8
O Cortex-A8 é um núcleo configurável que dá suporte à arquitetura de conjunto de instruções ARMv7. Ele é fornecido como um núcleo IP (propriedade intelectual). Os núcleos
IP são a forma dominante de entrega de tecnologia nos mercados dos embarcados, PMD e
relacionados. Bilhões de processadores ARM e MIPS foram criados a partir desses núcleos
IP. Observe que eles são diferentes dos núcleos no Intel i7 ou AMD Athlon multicores.
Um núcleo IP (que pode ser, ele próprio, um multicore) é projetado para ser incorporado
com outras lógicas (uma vez que ele é o núcleo de um chip), incluindo processadores de
aplicação específica (como um codificador ou um decodificador de vídeo), interfaces de
E/S e interfaces de memória, e então fabricados para gerar um processador otimizado para
uma aplicação em particular. Por exemplo, o núcleo Cortex-A8 IP é usado no Apple iPad
e smartphones de diversos fabricantes, incluindo Motorola e Samsung. Embora o núcleo
do processador seja quase idêntico, os chips resultantes têm muitas diferenças.
Geralmente, os núcleos IP têm dois tipos: núcleos hard são otimizados para um fornecedor
particular de semicondutores e são caixas-pretas com interfaces externas (mas ainda no
chip). Em geral, permitem a parametrização somente da lógica fora do núcleo, como
tamanhos de cache L2, sendo que o núcleo IP não pode ser modificado. Normalmente
núcleos soft são fornecidos em uma forma que usa uma biblioteca-padrão de elementos
lógicos. Um núcleo soft pode ser compilado para diferentes fornecedores de semicondutores e também pode ser modificado, embora modificações extensas sejam difíceis, devido
à complexidade dos núcleos IP modernos. Em geral, núcleos hard apresentam melhor
desempenho e menor área de substrato, enquanto os núcleos soft permitem o atendimento
a outros fornecedores e podem ser modificados mais facilmente.
O Cortex-A8 pode enviar duas instruções por clock a taxas de clock de até 1 GHz. Ele
pode suportar uma hierarquia de cache de dois níveis, com o primeiro nível sendo um
par de caches (para I & D), cada uma com 16 KB ou 32 KB organizados como associativos
por conjuntos de quatro vias e usando previsão de via e susbstituição aleatória. O objetivo é ter latência de acesso de ciclo único para as caches, permitindo que o Cortex-A8
mantenha um atraso de carregamento para uso de um ciclo, busca de instruções mais
simples e menor penalidade por busca de instrução correta quando uma falta de desvio
faz com que a instrução errada seja lida na pré-busca. A cache de segundo nível opcional,
quando presente, é um conjunto associativo de oito vias e pode ser configurado com
128 KB até 1 MB. Ela é organizada em 1-4 bancos para permitir que várias transferências
de memória ocorram ao mesmo tempo. Um barramento externo de 64-128 bits trata as
requisições de memória. A cache de primeiro nível é indexada virtualmente e taggeada
fisicamente, e a cache de segundo nível é indexada e taggeada fisicamente. Os dois níveis
usam um tamanho de bloco de 64 bytes. Para a D-cache de 32 KB e um tamanho de
página de 4 KB, cada página física pode mapear dois endereços de cache diferentes. Tais
instâncias são evitadas por detecção de hardware em uma falta, como na Seção B.3 do
Apêndice B.
O gerenciamento de memória é feito por um par de TLBs (I e D), cada um dos quais é
totalmente associativo com 32 entradas e tamanho de página variável (4 KB, 16 KB, 64
KB, 1 MB e 16 MB). A substituição no TLB é feita por um algoritmo round robin. As faltas
do TLB são tratadas no hardware, que percorre uma estrutura de tabela de página na
memória. A Figura 2.16 mostra como o endereço virtual de 32 bits é usado para indexar
99
100
CAPÍTULO 2 :
Projeto de hierarquia de memória
FIGURA 2.16 Endereço virtual, endereço físico, índices, tags e blocos de dados para as caches de dados e TLB
de dados do ARM Cortex A-8.
Uma vez que as hierarquias de instrução e dados são simétricas, mostramos somente uma. A TLB (instrução ou dados)
é totalmente associativa com 32 entradas. A cache L1 é associativa por conjunto de quatro vias com blocos de 64 bytes
e capacidade de 32 KB. A cache L2 é associativa por conjunto com oito vias com blocos de 64 bytes e capacidade de 1 MB.
Esta figura não mostra os bits de validade e bits de proteção para as caches e TLB nem o uso de bits de modo de predição
que ditam o banco de predição da cache L1.
a TLB e as caches, supondo caches primárias de 32 KB e uma cache secundária de 512 KB
com tamanho de página de 16 KB.
Desempenho da hierarquia de memória do ARM Cortex-A8
A hierarquia de memória do Cortex-A8 foi simulada com caches primárias de 32 KB e uma
cache L2 associativa por conjunto de oito vias de 1 MB, usando os benchmarks inteiros
Minnespec (KleinOswski e Lilja, 2002). O Minnespec é um conjunto de benchmarks que
consiste nos benchmarks SPEC2000, porém com entradas diferentes que reduzem os tempos de execução em várias ordens de magnitude. Embora o uso de entradas menores não
mude o mix de instruções, ele afeta o comportamento da cache. Por exemplo, em mcf, o
benchmark inteiro mais pesado em termos de memória do SPEC2000, o Minnespec, tem
taxa de falta, para uma cache de 32 KB, de somente 65% da taxa de falta para a versão
SPEC completa. Para uma cache de 1 MB, a diferença é um fator de 6! Em muitos outros
benchmarks, as taxas são similares àquelas do mcf, mas as taxas de falta absolutas são
muito menores. Por essa razão, não é possível comparar os benchmarks Minnespec com
os benchmarks SPEC2000. Em vez disso, os dados são úteis para a análise do impacto
relativo às faltas em L1 e L2 e na CPI geral, como faremos no próximo capítulo.
As taxas de falta da cache de instrução para esses benchmarks (e também para as versões
completas do SPEC2000, nas quais o Minnespec se baseia) são muito pequenas, mesmo
2.6
Juntando tudo: hierarquia de memória no ARM Cortex-A8 e Intel Core i7
para o L1: perto de zero para a maioria e abaixo de 1% para todos eles. Essa taxa baixa
provavelmente resulta da natureza computacionalmente intensa dos programas SPEC e da
cache associativa por conjunto de quatro vias, que elimina a maioria das faltas por conflito.
A Figura 2.17 mostra os resultados da cache de dados, que tem taxas de falta significativas
para L1 e L2. A penalidade de falta do L1 para um Cortex-A8 de 1 GHz é de 11 ciclos de
clock, enquanto a penalidade de falta do L2 é de 60 ciclos de clock, usando SDRAMs
DDR como memória principal. Usando essas penalidades de falta, a Figura 2.18 mostra
a penalidade média por acesso aos dados. No Capítulo 3, vamos examinar o impacto das
faltas de cache na CPI geral.
O Intel Core i7
O i7 suporta a arquitetura de conjunto de instruções x86-64, uma extensão de 64 bits da
arquitetura 80x86. O i7 é um processador de execução fora de ordem que inclui quatro
núcleos. Neste capítulo, nos concentramos no projeto do sistema de memória e desempenho do ponto de vista de um único núcleo. O desempenho do sistema dos projetos de
multiprocessador, incluindo o i7 multicore, será examinado em detalhes no Capítulo 5.
Cada núcleo em um i7 pode executar até quatro instruções 80x86 por ciclo de clock,
usando um pipeline de 16 estágios, dinamicamente escalonados, que descreveremos em
detalhes no Capítulo 3. O i7 pode também suportar até dois threads simultâneos por
processador, usando uma técnica chamada multithreading simultâneo, que será descrita
FIGURA 2.17 A taxa de falta de dados para o ARM com uma L1 de 32 KB e a taxa de falta de dados
globais de uma L2 de 1 MB usando os benchmarks inteiros do Minnespec é afetada significativamente pelas
aplicações.
As aplicações com necessidades de memória maiores tendem a ter taxas de falta maiores, tanto em L1 quanto em L2.
Note que a taxa de L2 é a taxa de falta global, que considera todas as referências, incluindo aquelas que acertam em
L1. O Mcf é conhecido como cache buster.
101
102
CAPÍTULO 2 :
Projeto de hierarquia de memória
FIGURA 2.18 A penalidade média de acesso à memória por referência da memória de dados vindo de L1
e L2 é mostrada para o processador ARM executando o Minnespec.
Embora as taxas de falta para L1 sejam significativamente maiores, a penalidade de falta de L2, que é mais de cinco
vezes maior, significa que as faltas de L2 podem contribuir significativamente.
no Capítulo 4. Em 2010, o i7 mais rápido tinha taxa de clock de 3,3 GHz, que gera um
pico de taxa de execução de instruções de 13,2 bilhões de instruções por segundo, ou mais
de 50 bilhões de instruções por segundo para o projeto de quatro núcleos.
O i7 pode suportar até três canais de memória, cada qual consistindo em um conjunto de
DIMMs separados, e cada um dos quais pode transferir em paralelo. Usando DDR3-1066
(DIMM PC8500), o i7 tem pico de largura de banda de memória pouco acima de 25 GB/s.
O i7 usa endereços virtuais de 48 bits e endereços físicos de 36 bits, gerando uma memória
física máxima de 36 GB. O gerenciamento de memória é tratado com um TLB de dois
níveis (Apêndice B, Seção B.3), resumido na Figura 2.19.
A Figura 2.20 resume a hierarquia de cache em três níveis do i7. As caches de primeiro nível
são indexadas virtualmente e taggeadas fisicamente (Apêndice B, Seção B.3), enquanto as
caches L2 e L3 são indexadas fisicamente. A Figura 2.21 mostra os passos de um acesso à
hierarquia de memória. Primeiro, o PC é enviado para a cache de instruções. O índice da
cache de instruções é
2Índice =
Tamanho da cache
32K
=
= 128 = 27
Tamanho do bloco × Associabilidade do conjunto 64 × 4
ou 7 bits. A estrutura de página do endereço da instrução (36 = 48 − 12 bits) é enviada para
o TLB de instrução (passo 1). Ao mesmo tempo, o índice de 7 bits (mais 2 bits adicionais
para o offset do bloco para selecionar os 16 bytes apropriados, a quantidade de busca
de instrução) do endereço virtual é enviado para a cache de instrução (passo 2). Observe
2.6
Juntando tudo: hierarquia de memória no ARM Cortex-A8 e Intel Core i7
FIGURA 2.19 Características da estrutura de TLB do i7, que tem TLBs de primeiro nível de instruções e
dados separadas, as duas suportadas, em conjunto, por um TLB de segundo nível.
Os TLBs de primeiro nível suportam o tamanho-padrão de página de 4 KB, além de ter número limitado de entradas de
páginas grandes de 2-4 MB. Somente as páginas de 4 KB são suportadas no TLB de segundo nível.
FIGURA 2.20 Características da hierarquia de cache em três níveis no i7.
Os três caches usam write-back e tamanho de bloco de 64 bytes. As caches L1 e L2 são separadas para cada núcleo,
enquanto a cache L3 é compartilhada entre os núcleos em um chip e tem um total de 2 MB por núcleo. As três caches
não possuem bloqueio e permitem múltiplas escritas pendentes. Um write buffer merge é usado para a cache L1, que
contém dados no evento de que a linha não está presente em L1 quando ela é escrita (ou seja, a falta de escrita em
L1 não faz com que a linha seja alocada). L3 é inclusivo de L1 e L2; exploramos essa propriedade em detalhes quando
explicamos as caches multiprocessador. A substituição é por uma variante na pseudo-LRU: no caso de L3, o bloco
substituído é sempre a via de menor número cujo bit de acesso esteja desligado. Isso não é exatamente aleatório, mas
é fácil de computar.
que, para a cache de instrução associativa de quatro vias, 13 bits são necessários para o
endereço de cache: 7 bits para indexar a cache, mais 6 bits de offset de bloco para bloco
de 64 bytes, mas o tamanho da página é de 4 KB = 212, o que significa que 1 bit do índice
de cache deve vir do endereço virtual. Esse uso de 1 bit de endereço virtual significa que
o bloco correspondente poderia, na verdade, estar em dois lugares diferentes da cache,
uma vez que o endereço físico correspondente poderia ser um 0 ou 1 nesse local. Para
instruções, isso não é um problema, uma vez que, mesmo que uma instrução apareça
na cache em dois locais diferentes, as duas versões devem ser iguais. Se tal duplicação de
dados, ou aliasing, for permitida, a cache deverá ser verificada quando o mapa da página
for modificado, o que é um evento pouco frequente. Observe que um uso muito simples da
colorização de página (Apêndice B, Seção B.3) pode eliminar a possibilidade desses aliases.
Se páginas virtuais de endereço par forem mapeadas para páginas físicas de endereço par
(e o mesmo ocorrer com as páginas ímpares), esses aliases poderão não ocorrer, porque
os bits de baixa ordem no número das páginas virtual e física serão idênticos.
A TBL de instrução é acessada para encontrar uma correspondência entre o endereço e
uma entrada de tabela de página (Page Table Entry — PTE) válida (passos 3 e 4). Além de
traduzir o endereço, a TBL verifica se a PTE exige que esse acesso resulte em uma exceção,
devido a uma violação de acesso.
103
104
CAPÍTULO 2 :
Projeto de hierarquia de memória
FIGURA 2.21 A hierarquia de memória do Intel i7 e os passos no acesso às instruções e aos dados.
Mostramos somente as leituras de dados. As escritas são similares, no sentido de que começam com uma leitura (uma vez que as
caches são write-back). Faltas são tratadas simplesmente colocando os dados em um buffer de escrita, uma vez que a cache L1 não
é alocado para escrita.
Uma falta de TLB de instrução primeiro vai para a TLB L2, que contém 512 PTEs com
tamanho de página de 4 KB, e é associativa por conjunto com quatro vias. Ela leva dois
ciclos de clock para carregar a TLB L1 da TLB L2. Se a TLB L2 falhar, um algoritmo de
hardware será usado para percorrer a tabela da página e atualizar a entrada da TLB. No
pior caso, a página não estará na memória, e o sistema operacional recuperará a página
do disco. Uma vez que milhões de instruções podem ser executadas durante uma falha
2.6
Juntando tudo: hierarquia de memória no ARM Cortex-A8 e Intel Core i7
de página, o sistema operacional vai realizar outro processo se um estiver esperando para
ser executado. Se não houver exceção de TLB, o acesso à cache de instrução continuará.
O campo de índice do endereço é enviado para os quatro bancos da cache de instrução
(passo 5). A tag da cache de instrução tem 36 − 7 bits (índice) – 6 bits (offset de bloco),
ou 23 bits. As quatro tags e os bits válidos são comparados à estrutura física da página a
partir da TLB de instrução (passo 6). Como o i7 espera 16 bytes a cada busca de instrução,
2 bits adicionais são usados do offset de bloco de 6 bits para selecionar os 16 bytes apropriados. Portanto, 7 + 2 ou 9 bits são usados para enviar 16 bytes de instruções para o
processador. A cache L1 é pipelining e a latência de um acerto é de quatro ciclos de clock
(passo 7). Uma falta vai para a cache de segundo nível.
Como mencionado, a cache de instrução é virtualmente endereçada e fisicamente taggeada.
Uma vez que as caches de segundo nível são endereçadas fisicamente, o endereço físico
da página da TLB é composta com o offset de página para criar um endereço para acessar
a cache L2. O índice L2 é
2Índice =
Tamanho da cache
256K
=
= 512 = 29
Tamanho do bloco × Associabilidade do conjunto 64 × 8
então o endereço de bloco de 30 bits (endereço físico de 36 bits – offset de bloco de 6 bits)
é dividido em uma tag de 21 bits e um índice 9 bits (passo 8). Uma vez mais, o índice e a
tag são enviados para os oitos bancos da cache L2 unificada (passo 9), que são comparados
em paralelo. Se um corresponder e for válido (passo 10), é retornado ao bloco em ordem
sequencial após a latência inicial de 10 ciclos a uma taxa de 8 bytes por ciclo de clock.
Se a cache L2 falhar, a cache L3 será acessada. Para um i7 de quatro núcleos, que tem uma
L3 de 8 MB, o tamanho do índice é
2Índice =
Tamanho da cache
8M
=
= 8.192 = 213
Tamanho do bloco × Associabilidade do conjunto 64 × 16
O índice de 13 bits (passo 11) é enviado para os 16 bancos de L3 (passo 12). A tag L3,
que tem 36 – (13 + 6) = 17 bits, é comparada com o endereço físico da TLB (passo 13).
Se ocorrer um acerto, o bloco é retornado depois de uma latência inicial a uma taxa de 16
bytes por clock e colocado em L1 e L3. Se L3 falhar, um acesso de memória será iniciado.
Se a instrução não for encontrada na cache L3, o controlador de memória do chip deverá
obter o bloco da memória principal. O i7 tem três canais de memória de 64 bits, que
podem agir como um canal de 192 bits, uma vez que existe somente um controlador de
memória e o mesmo endereço é enviado nos dois canais (passo 14). Transferências amplas ocorrem quando os dois canais têm DIMMs idênticos. Cada canal pode suportar até
quatro DIMMs DDR (passo 15). Quando os dados retornam, são posicionados em L3 e
L1 (passo 16), pois L3 é inclusiva.
A latência total da falta de instrução atendida pela memória principal é de aproximadamente 35 ciclos de processador para determinar que uma falta de L3 ocorreu, mais a
latência da DRAM para as instruções críticas. Para uma SDRAM DDR1600 de banco único
e uma CPU de 3,3 GHz, a latência da DRAM é de cerca de 35 ns ou 100 ciclos de clock
para os primeiros 16 bytes, levando a uma penalidade de falta total de 135 ciclos de clock.
O controlador de memória preenche o restante do bloco de cache de 64 bytes a uma taxa
de 16 bits por ciclo de clock de memória, o que leva mais 15 ns ou 45 ciclos de clock.
Uma vez que a cache de segundo nível é uma cache write-back, qualquer falta pode levar
à reescrita de um bloco velho na memória. O i7 tem um write buffer merge de 10 entradas
que escreve linhas modificadas de cache quando o próximo nível da cache não é usado
105
106
CAPÍTULO 2 :
Projeto de hierarquia de memória
para uma leitura. O buffer de escrita é pesquisado em busca de qualquer falta para ver ser
a linha de cache existe no buffer; em caso positivo, a falta é preenchida a partir do buffer.
Um buffer similar é usado entre as caches L1 e L2.
Se essa instrução inicial for um load, o endereço de dados será enviado para a cache de
dados e TLBs de dados, agindo de modo muito similar a um acesso de cache de instrução
com uma diferença-chave. A cache de dados de primeiro nível é um conjunto associativo de
oito vias, o que significa que o índice é de 6 bits (contra 7 da cache de instrução) e o
endereço usado para acessar a cache é o mesmo do offset de página. Portanto, aliases na
cache de dados não são um problema.
Suponha que a instrução seja um store em vez de um load. Quando o store é iniciado,
ele realiza uma busca na cache de dados, assim como um load. Uma falta faz com que o
bloco seja posicionado em um buffer de escrita, uma vez que a cache L1 não aloca o bloco
em uma falta de escrita. Em um acerto, o store não atualiza a cache L1 (ou L2) até mais
tarde, depois que se sabe que ele é não especulativo. Durante esse tempo, o store reside
em uma fila load-store, parte do mecanismo de controle fora de ordem do processador.
O i7 também suporta pré-busca para L1 e L2 do próximo nível na hierarquia. Na maioria
dos casos, a linha pré-obtida é simplesmente o próximo bloco da cache. Ao executar a pré-busca somente para L1 e L2, são evitadas as buscas caras e desnecessárias na memória.
Desempenho do sistema de memória do i7
Nós avaliamos o desempenho da estrutura de cache do i7 usando 19 dos benchmarks SPEC
CPU2006 (12 inteiros e sete de ponto flutuante), que foram descritos no Capítulo 1. Os
dados desta seção foram coletados pelo professor Lu Peng e pelo doutorando Ying Zhang,
ambos da Universidade do Estado da Louisiana.
Começamos com a cache L1. A cache de instrução associativa por conjunto com quatro
vias leva a uma taxa de falta de instrução muito baixa, especialmente porque a pré-busca
de instrução no i7 é bastante efetiva. Obviamente, avaliar a taxa de falta é um pouco complicado, já que o i7 não gera requisições individuais para unidades de instrução únicas,
mas, em vez disso, pré-busca 16 bytes de dados de instrução (em geral, 4-5 instruções).
Se, por simplicidade, examinarmos a taxa de falta da cache de instrução como tratamos
as referências de instrução únicas, a taxa de falta de cache de instrução do L1 variará entre
0,1-1,8%, com uma média pouco acima de 0,4%. Essa taxa está de acordo com outros
estudos do comportamento da cache de instrução para os benchmarks SPEC CPU2006,
que mostraram baixas taxas de falta da cache de instrução.
A cache de dados L1 é mais interessante e também a mais complicada de avaliar por três razões:
1. Como a cache de dados L1 não é alocada para escrita, as escritas podem acertar mas
nunca errar de verdade, no sentido de que uma escrita que não acerta simplesmente
coloca seus dados no buffer de escrita e não registra uma falha.
2. Como, às vezes, a especulação pode estar errada (veja discussão detalhada no Cap. 3),
existem referências à cache de dados L1 que não correspondem a loads ou stores que
eventualmente completam a execução. Como tais faltas deveriam ser tratadas?
3. Por fim, a cache de dados L1 realiza pré-busca automática. As pré-buscas que falham
deveriam ser contadas? Caso afirmativo, como?
Para tratar desses problemas e ao mesmo tempo manter uma quantidade de dados
razoável, a Figura 2.22 mostra as faltas de cache de dados L1 de dois modos: 1) relativas
ao número de loads que realmente são completados (muitas vezes chamados graduação
ou aposentadoria) e 2) relativas a todos os acessos a cache de dados L1 por qualquer fonte.
2.7
FIGURA 2.22 A taxa de falta da cache de dados L1 para 17 benchmarks SPEC CPU2006 é mostrada
de dois modos: relativa às cargas reais que completam com sucesso a execução e relativa a todas as
referências a L1, que também inclui pré-buscas, cargas especulativas que não são completadas, e escritas,
que contam como referências, mas não geram faltas.
Esses dados, como o resto desta seção, foram coletados pelo professor Lu Peng e pelo doutorando Ying Zhang, ambos
da Universidade do Estado da Louisiana, com base em estudos anteriores do Intel Core Duo e outros processadores
(Peng et al., 2008).
Como veremos, a taxa de faltas, quando medida em comparação somente com os loads
completos, é 1,6 vez maior (uma média de 9,5% contra 5,9%). A Figura 2.23 mostra os
mesmos dados em forma de tabela.
Com as taxas de falta da cache de dados L1 sendo de 5-10%, e às vezes mais alta, a importância das caches L2 e L3 deve ser óbvia. A Figura 2.24 mostra as taxas de falta das caches
L2 e L3 contra o número de referências de L1 (e a Fig. 2.25 mostra os dados em forma
de tabela). Uma vez que o custo de uma falta para a memória é de mais de 100 ciclos e a
taxa média de falta de dados em L2 é de 4%, L3 é obviamente crítico. Sem L3 e supondo
que cerca de metade das instruções é de loads ou stores, as faltas da cache L2 poderiam
adicionar dois ciclos por instrução para a CPI! Em comparação, a taxa de falta de dados em
L3, de 1%, ainda é significativa, mas quatro vezes menor do que a taxa de falta de L2 e seis
vezes menor do que a taxa de falta de L1. No Capítulo 3, vamos examinar o relacionamento
entre a CPI do i7 e as faltas de cache, assim como outros efeitos de pipeline.
2.7
FALÁCIAS E ARMADILHAS
Como a mais naturalmente quantitativa das disciplinas da arquitetura de computador,
a hierarquia de memória poderia parecer menos vulnerável a falácias e armadilhas. Entretanto, fomos limitados aqui não pela falta de advertências, mas pela falta de espaço!
Falácias e armadilhas
107
108
CAPÍTULO 2 :
Projeto de hierarquia de memória
FIGURA 2.23 As faltas da cache de dados primários são mostradas em comparação com todos
os carregamentos que são completados e todas as referências (que incluem requisições especulativas
e pré-buscas).
Falácia. Prever o desempenho da cache de um programa a partir de outro.
A Figura 2.26 mostra as taxas de falta de instrução e as taxas de falta de dados para três
programas do pacote de benchmark SPEC2000 à medida que o tamanho da cache varia.
Dependendo do programa, as faltas de dados por mil instruções para uma cache de
4.096 KB é de 9, 2 ou 90, e as faltas de instrução por mil instruções para uma cache
de 4 KB é de 55, 19 ou 0,0004. Programas comerciais, como os bancos de dados,
terão taxas de falta significativas até mesmo em grandes caches de segundo nível,
o que geralmente não é o caso para os programas SPEC. Claramente, generalizar o
desempenho da cache de um programa para outro não é sensato. Como a Figura 2.24
nos lembra, há muita variação, e as previsões sobre as taxas de falta relativas de programas pesados em inteiros e ponto flutuante podem estar erradas, como o mcf eo
sphinx3 nos lembram!
Armadilha. Simular instruções suficientes para obter medidas de desempenho precisas da
hierarquia de memória.
Existem realmente três armadilhas aqui. Uma é tentar prever o desempenho de uma
cache grande usando um rastreio pequeno. Outra é que o comportamento da localidade
de um programa não é constante durante a execução do programa inteiro. A terceira
é que o comportamento da localidade de um programa pode variar de acordo com a
entrada.
2.7
FIGURA 2.24 As taxas de falta das caches de dados L2 e L3 para 17 benchmarks SPEC CPU2006 são
mostradas em relação às referências a L1, que também incluem pré-buscas, carregamentos especulativos
que não são completados e carregamentos e armazenamentos gerados por programa.
Esses dados, como o resto desta seção, foram coletados pelo professor Lu Peng e pelo doutorando Ying Zhang, ambos
da Universidade do Estado da Louisiana.
A Figura 2.27 mostra as faltas de instrução médias acumuladas por mil instruções para
cinco entradas em um único programa SPEC2000. Para essas entradas, a taxa de falta
média para o primeiro 1,9 bilhão de instruções é muito diferente da taxa de falta média
para o restante da execução.
Armadilha. Não oferecer largura de banda de memória alta em um sistema baseado em cache.
As caches ajudam na latência média de memória cache, mas não podem oferecer grande
largura de banda de memória para uma aplicação que precisa ir até a memória principal.
O arquiteto precisa projetar uma memória com grande largura de banda por trás da cache
para tais aplicações. Vamos revisitar essa armadilha nos Capítulos 4 e 5.
Armadilha. Implementar um monitor de máquina virtual em uma arquitetura de conjunto de
instruções que não foi projetado para ser virtualizável.
Nas décadas de 1970 e 1980, muitos arquitetos não tinham o cuidado de garantir que
todas as instruções de leitura ou escrita de informações relacionadas com a informações
de recursos de hardware fossem privilegiadas. Essa atitude laissez-faire causa problemas
para os VMMs em todas essas arquiteturas, incluindo 80x86, que usamos aqui como
exemplo.
A Figura 2.28 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
Falácias e armadilhas
109
110
CAPÍTULO 2 :
Projeto de hierarquia de memória
FIGURA 2.25 Taxas de falta de L2 e L3 mostradas em forma de tabela em comparação com o número de
requisições de dados.
FIGURA 2.26 Faltas de instruções e dados por 1.000 instruções à medida que o tamanho da cache varia
de 4 KB a 4.096 KB.
As faltas de instruções para gcc são 30.000-40.000 vezes maiores do que para o lucas e, reciprocamente, as faltas
de dados para o lucas são 2-60 vezes maiores do que para o gcc. Os programas gap, gcc e lucas são do pacote
de benchmark SPEC2000.
j
j
leem registradores de controle no modo usuário, que revela que o sistema
operacional está rodando em uma máquina virtual (como POPF, mencionada
anteriormente); e
verificam a proteção exigida pela arquitetura segmentada, mas presumem que o
sistema operacional está rodando no nível de privilégio mais alto.
2.7
FIGURA 2.27 Faltas de instrução por 1.000 referências para cinco entradas no benchmark perl do
SPEC2000.
Existem poucas variações nas faltas e poucas diferenças entre as cinco entradas para o primeiro 1,9 bilhão de
instruções. A execução até o término mostra como as faltas variam durante a vida do programa e como elas dependem
da entrada. O gráfico superior mostra as faltas médias de execução para o primeiro 1,9 bilhão de instruções, que
começa em cerca de 2,5 e termina em cerca de 4,7 faltas por 1.000 referências para todas as cinco entradas.
O gráfico inferior mostra as faltas médias de execução para executar até o término, que ocupa 16-41 bilhões de
instruções, dependendo da entrada. Após o primeiro 1,9 bilhão de instruções, as faltas por 1.000 referências variam
de 2,4-7,9, dependendo da entrada. As simulações foram para o processador Alpha usando caches L1 separadas
para instruções e dados, cada qual de 64 KB em duas vias com LRU, e uma cache L2 unificada de 1 MB, mapeada
diretamente.
A memória virtual também é desafiadora. Como os TLBs do 80x86 não admitem tags de
ID (identificação) de processo, assim como a maioria das arquiteturas RISC, é mais dispendioso para o VMM e os SOs convidados compartilhar o TLB; cada mudança de espaço
de endereço normalmente exige um esvaziamento do TLB.
A virtualização da E/S também é um desafio para o 80x86, em parte porque ele admite
E/S mapeada na memória e possui instruções de E/S separadas, e em parte — o que é mais
importante — porque existe um número muito grande e enorme variedade de tipos de
dispositivos e drivers de dispositivo para PCs, para o VMM tratar. Os vendedores terceiros
fornecem seus próprios drivers, e eles podem não virtualizar corretamente. Uma solução
Falácias e armadilhas
111
112
CAPÍTULO 2 :
Projeto de hierarquia de memória
FIGURA 2.28 Resumo das 18 instruções do 80x86 que causam problemas para a virtualização (Robin e
Irvine, 2000).
As cinco primeiras instruções do grupo de cima permitem que um programa no modo usuário leia um registrador
de controle como os registradores da tabela de descritor, sem causar um trap. A instrução pop 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 80x86 é a ruína do grupo de baixo, pois cada uma dessas
instruções verifica o nível de privilégio implicitamente como parte da execução da instrução quando lê um registrador
de controle. A verificação pressupõe que o SO precisa estar no nível de privilégio mais alto, que não é o caso para VMs
convidadas. Somente o MOVE para o registrador de segmento tenta modificar o estado de controle, e a verificação de
proteção é prejudicada.
para as implementações convencionais de uma VM é carregar os drivers de dispositivo
reais diretamente no VMM.
Para simplificar as implementações de VMMs no 80x86, tanto a AMD quanto a Intel
propuseram extensões à arquitetura. O VT-x da Intel oferece um novo modo de execução
para executar VMs, uma definição arquitetada do estado da VM, instruções para trocar
VMs rapidamente e um grande conjunto de parâmetros para selecionar as circunstâncias
em que um VMM precisa ser invocado. Em conjunto, o VT-x acrescenta 11 novas instruções para o 80x86. A Secure Virtual Machine (SVM) da AMD tem uma funcionalidade
similar.
Depois de ativar o modo que habilita o suporte do VT-x (por meio da instrução VMXON),
o VT-x oferece quatro níveis de privilégio para o SO convidado, que são inferiores em
prioridade aos quatro originais. O VT-x captura todo o estado de uma máquina virtual no
Virtual Machine Control State (VMCS) e depois oferece instruções indivisíveis para salvar
e restaurar um VMCS. Além do estado crítico, o VMCS inclui informações de configuração
para determinar quando invocar o VMM e, depois, especificamente, o que causou a invocação do VMM. Para reduzir o número de vezes que o VMM precisa ser invocado, esse
modo acrescenta versões de sombra de alguns registradores sensíveis e acrescenta máscaras que verificam se os bits críticos de um registrador sensível serão alterados antes da
interceptação. Para reduzir o custo da virtualização da memória virtual, a SVM da AMD
acrescenta um nível de indireção adicional, chamado tabelas de página aninhadas. Isso torna
as tabelas de página de sombra desnecessárias.
2.8
2.8
Comentários finais: olhando para o futuro
COMENTÁRIOS FINAIS: OLHANDO PARA O FUTURO
Ao longo dos últimos trinta anos tem havido diversas previsões do fim eminente
[sic] da taxa de melhoria do desempenho dos computadores. Todas essas
previsões estavam erradas, pois foram articuladas sobre suposições derrubadas
por eventos subsequentes. Então, por exemplo, a falha em prever a mudança
dos componentes discretos para os circuitos integrados levou a uma previsão
de que a velocidade da luz limitaria a velocidade dos computadores a várias
ordens de magnitude a menos do que as velocidades atuais. Provavelmente,
nossa previsão sobre a barreira de memória também está errada, mas ela sugere
que precisamos começar a pensar “fora da caixa”.
Wm. A. Wulf e Sally A. McKee
Hitting the Memory Wall: Implications of the Obvious
Departamento de Ciência da Computação, Universidade da Virginia (dezembro
de 1994); (Esse artigo introduziu o nome memory wall — barreira de memória).
A possibilidade de usar uma hierarquia de memória vem desde os primeiros dias dos computadores digitais de uso geral, no final dos anos 1940 e começo dos anos 1950. A memória
virtual foi introduzida nos computadores de pesquisa, no começo dos anos 1960, e nos mainframes IBM, nos anos 1970. As caches apareceram na mesma época. Os conceitos básicos
foram expandidos e melhorados ao longo do tempo para ajudar a diminuir a diferença do
tempo de acesso entre a memória e os processadores, mas os conceitos básicos permanecem.
Uma tendência que poderia causar mudança significativa no projeto das hierarquias de
memória é uma redução contínua de velocidade, tanto em densidade quanto em tempo
de acesso nas DRAMs. Na última década, essas duas tendências foram observadas. Embora
algumas melhorias na largura de banda de DRAM tenham sido alcançadas, diminuições
no tempo de acesso vieram muito mais lentamente parcialmente porque, para limitar o
consumo de energia, os níveis de voltagem vêm caindo. Um conceito que vem sendo explorado para aumentar a largura de banda é ter múltiplos acessos sobrepostos por banco.
Isso fornece uma alternativa para o aumento do número de bancos e permite maior largura
de banda. Desafios de manufatura para o projeto convencional de DRAM, que usa um
capacitor em cada célula, tipicamente posicionada em uma lacuna profunda, também
levaram a reduções na taxa de aumento na densidade. Já existem DRAM que não utilizam
capacitores, acarretando a continuidade da melhoria da tecnologia DRAM.
Independentemente das melhorias na DRAM, a memória Flash provavelmente terá um
papel maior, devido às possíveis vantagens em termos de potência e densidade. Obviamente, em PMDs, a memória Flash já substituiu os drives de disco e oferece vantagens, como
“ativação instantânea”, que muitos computadores desktop não fornecem. A vantagem
potencial da memória Flash sobre as DRAMs — a ausência de um transistor por bit para
controlar a escrita — é também seu calcanhar de Aquiles. A memória Flash deve usar
ciclos de apagar-reescrever em lotes consideravelmente mais lentos. Como resultado,
diversos PMDs, como o Apple iPad, usam uma memória principal SDRAM relativamente
pequena combinada com Flash, que age como sistema de arquivos e como sistema de
armazenamento de página para tratar a memória virtual.
Além disso, diversas abordagens totalmente novas à memória estão sendo exploradas. Elas
incluem MRAMs, que usam armazenamento magnético de dados, e RAMs de mudança
de fase (conhecidas como PCRAM, PCME e PRAM), que usam um vidro que pode mudar
entre os estados amorfo e cristalino. Os dois tipos de memória são não voláteis e oferecem densidades potencialmente maiores do que as DRAMs. Essas ideias não são novas;
113
114
CAPÍTULO 2 :
Projeto de hierarquia de memória
tecnologias de memória magnetorresistivas e memórias de mudança de fase estão por aí há
décadas. Qualquer uma dessas tecnologias pode tornar-se uma alternativa à memória Flash
atual. Substituir a DRAM é uma tarefa muito mais difícil. Embora as melhorias nas DRAMs
tenham diminuído, a possibilidade de uma célula sem capacitor e outras melhorias em
potencial tornam difícil apostar contra as DRAMs pelo menos durante a década seguinte.
Por alguns anos, foram feitas várias previsões sobre a chegada da barreira de memória (veja
artigo citado no início desta seção), que levaria a reduções fundamentais no desempenho do
processador. Entretanto, a extensão das caches para múltiplos níveis, esquemas mais sofisticados
de recarregar e pré-busca, maior conhecimento dos compiladores e dos programadores sobre a
importância da localidade, e o uso de paralelismo para ocultar a latência que ainda existir, vêm
ajudando a manter a barreira de memória afastada. A introdução de pipelines fora de ordem
com múltiplas faltas pendentes permitiu que o paralelismo em nível de instrução disponível
ocultasse a latência de memória ainda existente em um sistema baseado em cache. A introdução
do multithreading e de mais paralelismo no nível de thread levou isso além, fornecendo mais
paralelismo e, portanto, mais oportunidades de ocultar a latência. É provável que o uso de
paralelismo em nível de instrução e thread seja a principal ferramenta para combater quaisquer
atrasos de memória encontrados em sistemas de cache multinível modernos.
Uma ideia que surge periodicamente é o uso de scratchpad controlado pelo programador
ou outras memórias de alta velocidade, que veremos ser usadas em GPUs. Tais ideias
nunca se popularizaram por várias razões: 1) elas rompem com o modelo de memória,
introduzindo espaços de endereço com comportamento diferente; 2) ao contrário das
otimizações de cache baseadas em compilador ou em programador (como a pré-busca),
transformações de memória com scratchpads devem lidar completamente com o remapeamento a partir do espaço de endereço da memória principal para o espaço de endereço
do scratchpad. Isso torna tais transformações mais difíceis e limitadas em aplicabilidade.
No caso das GPUs (Cap. 4), onde memórias scratchpad locais são muito usadas, o peso
de gerenciá-las atualmente recai sobre o programador.
Embora seja necessário muito cuidado quanto a prever o futuro da tecnologia da computação, a história mostrou que o uso de caches é uma ideia poderosa e altamente ampliável
que provavelmente vai nos permitir continuar construindo computadores mais rápidos
e garantindo que a hierarquia de memória entregue as instruções e os dados necessários
para manter tais sistemas funcionando bem.
2.9
PERSPECTIVAS HISTÓRICAS E REFERÊNCIAS
Na Seção L.3 (disponível on-line), examinaremos a história das caches, memória virtual
e máquinas virtuais. A IBM desempenha um papel proeminente na história dos três. As
referências para leitura adicional estão incluídas nessa seção.
Estudos de caso e exercícios por Norman P. Jouppi
ESTUDOS DE CASO COM EXERCÍCIOS POR NORMAN P.
JOUPPI, NAVEEN MURALIMANOHAR E SHENG LI
Estudo de caso 1: otimizando o desempenho da cache por meio
de técnicas avançadas
Conceitos ilustrados por este estudo de caso
j
j
Caches sem bloqueio
Otimizações de compilador para as caches
Estudos de caso com exercícios por Norman P. Jouppi, Naveen Muralimanohar e Sheng Li
Pré-busca de software e hardware
Cálculo de impacto do desempenho da cache sobre processadores mais complexos
j
j
A transposição de uma matriz troca suas linhas e colunas e é ilustrada a seguir:
A11
A 21
A31
A 41
A12
A 22
A32
A 42
A13
A 23
A33
A 43
A14
A 24
A34
A 44
A11
A12
⇒ A13
A14
A 21
A 22
A 23
A 24
A31
A32
A33
A34
A 41
A 42
A 43
A 44
Aqui está um loop simples em C para mostrar a transposição:
Considere que as matrizes de entrada e saída sejam armazenadas na ordem principal de
linha (ordem principal de linha significa que o índice de linha muda mais rapidamente).
Suponha que você esteja executando uma transposição de precisão dupla de 256 × 256
em um processador com cache de dados de 16 KB totalmente associativa (de modo que
não precise se preocupar com conflitos de cache) nível 1 por substituição LRU, com
blocos de 64 bytes. Suponha que as faltas de cache de nível 1 ou pré-buscas exijam 16
ciclos, sempre acertando na cache de nível 2, e a cache de nível 2 possa processar uma
solicitação a cada dois ciclos de processador. Suponha que cada iteração do loop interno
acima exija quatro ciclos se os dados estiverem presentes na cache de nível 1. Suponha
que a cache tenha uma política escrever-alocar-buscar na escrita para as faltas de escrita.
Suponha, de modo não realista, que a escrita de volta dos blocos modificados de cache
exija 0 ciclo.
2.1
2.2
[10/15/15/12/20] <2.2> Para a implementação simples mostrada anteriormente,
essa ordem de execução seria não ideal para a matriz de entrada. Porém, a
aplicação de uma otimização de troca de loops criaria uma ordem não ideal
para a matriz de saída. Como a troca de loops não é suficiente para melhorar seu
desempenho, ele precisa ser bloqueado.
a. [10] <2.2> Que tamanho de bloco deve ser usado para preencher
completamente a cache de dados com um bloco de entrada e saída?
b. [15] <2.2> Como os números relativos de faltas das versões bloqueada
e não bloqueada podem ser comparados se a cache de nível 1 for mapeada
diretamente?
c. [15] <2.2> Escreva um código para realizar transposição com um parâmetro
de tamanho de bloco B que usa B × B blocos.
d. [12] <2.2> Qual é a associatividade mínima requerida da cache L1
para desempenho consistente independentemente da posição dos dois arrays
na memória?
e. [20] <2.2> Tente as transposições bloqueada e não bloqueada de uma matriz
de 256 × 256 em um computador. Quanto os resultados se aproximam
de suas expectativas com base no que você sabe sobre o sistema de memória
do computador? Explique quaisquer discrepâncias, se possível.
[10] <2.2> Suponha que você esteja reprojetando um hardware de pré-busca para
o código de transposição de matriz não bloqueado anterior. O tipo mais simples de
115
116
CAPÍTULO 2 :
Projeto de hierarquia de memória
2.3
hardware de pré-busca só realiza a pré-busca de blocos de cache sequenciais após
uma falta. Os hardwares de pré-busca de “passos (strides) não unitários” mais
complicados podem analisar um fluxo de referência de falta e detectar e pré-buscar
passos não unitários. Ao contrário, a pré-busca via software pode determinar
passos não unitários tão facilmente quanto determinar os passos unitários.
Suponha que as pré-buscas escrevam diretamente na cache sem nenhuma
“poluição” (sobrescrever dados que precisam ser usados antes que os dados sejam
pré-buscados). No estado fixo do loop interno, qual é o desempenho (em ciclos
por iteração) quando se usa uma unidade ideal de pré-busca de passo não unitário?
[15/20] <2.2> Com a pré-busca via software, é importante ter o cuidado de fazer
com que as pré-buscas ocorram em tempo para o uso, mas também minimizar
o número de pré-buscas pendentes, a fim de viver dentro das capacidades da
microarquitetura e minimizar a poluição da cache. Isso é complicado pelo fato de
os diferentes processadores possuírem diferentes capacidades e limitações.
a. [15] <2.2> Crie uma versão bloqueada da transposição de matriz com
pré-busca via software.
b. [20] <2.2> Estime e compare o desempenho dos códigos bloqueado e não
bloqueado com e sem pré-busca via software.
Estudo de caso 2: juntando tudo: sistemas de memória
altamente paralelos
Conceito ilustrado por este estudo de caso
j
Questões cruzadas: O projeto de hierarquias de memória
O programa apresentado na Figura 2.29 pode ser usado para avaliar o comportamento
de um sistema de memória. A chave é ter temporização precisa e depois fazer com que o
programa corra pela memória para invocar diferentes níveis da hierarquia. A Figura 2.29
mostra o código em C. A primeira parte é um procedimento que usa um utilitário-padrão
para obter uma medida precisa do tempo de CPU do usuário; talvez esse procedimento
tenha de mudar para funcionar em alguns sistemas. A segunda parte é um loop aninhado
para ler e escrever na memória em diferentes passos e tamanhos de cache. Para obter tempos
de cache precisos, esse código é repetido muitas vezes. A terceira parte temporiza somente o
overhead do loop aninhado, de modo que possa ser subtraído dos tempos medidos em geral
para ver quanto tempo os acessos tiveram. Os resultados são enviados para o formato de
arquivo .csv, para facilitar a importação em planilhas. Você pode ter de mudar CACHE_MAX,
dependendo da pergunta a que estiver respondendo e do tamanho da memória no sistema
que estiver medindo. A execução do programa no modo monousuário ou pelo menos sem
outras aplicações ativas dará resultados mais coerentes. O código mostrado na Figura 2.29
foi derivado de um programa escrito por Andrea Dusseau, da U.C. Berkeley, baseado em uma
descrição detalhada encontrada em Saavedra-Barrera (1992). Ele foi modificado para resolver
uma série de problemas com máquinas mais modernas e para executar sob o Microsoft
Visual C++. Ele pode ser baixado em <www.hpl.hp.com/research/cacti/aca_ch2_cs2.c>.
O programa mostrado anteriormente considera que os endereços de memória rastreiam os
endereços físicos, o que é verdadeiro em algumas máquinas que usam caches endereçadas
virtualmente, como o Alpha 21264. Em geral, os endereços virtuais costumam acompanhar os endereços físicos logo depois da reinicialização, de modo que você pode ter de
reinicializar a máquina a fim de conseguir linhas suaves nos seus resultados. Para fazer os
exercícios, considere que os tamanhos de todos os componentes da hierarquia de memória
sejam potências de 2. Considere ainda que o tamanho da página é muito maior do que o
tamanho de um bloco em uma cache de segundo nível (se houver uma) e que o tamanho
Estudos de caso com exercícios por Norman P. Jouppi, Naveen Muralimanohar e Sheng Li
FIGURA 2.29 Programa em C para avaliar os sistemas de memória.
117
118
CAPÍTULO 2 :
Projeto de hierarquia de memória
de um bloco de cache de segundo nível é maior ou igual ao tamanho de um bloco em uma
cache de primeiro nível. Um exemplo da saída do programa é desenhado na Figura 2.30,
com a chave listando o tamanho do array que é exercitado.
2.4
2.5
2.6
[12/12/12/10/12] <2.6> Usando os resultados do programa de exemplo na
Figura 2.30:
a. [12] <2.6> Quais são o tamanho geral e o tamanho de bloco da cache de
segundo nível?
b. [12] <2.6> Qual é a penalidade de falta da cache de segundo nível?
c. [12] <2.6> Qual é a associatividade da cache de segundo nível?
d. [10] <2.6> Qual é o tamanho da memória principal?
e. [12] <2.6> Qual será o tempo de paginação se o tamanho da página for de 4 KB?
[12/15/15/20] <2.6> Se necessário, modifique o código na Figura 2.29 para
medir as seguintes características do sistema. Desenhe os resultados experimentais
com o tempo decorrido no eixo y e o stride da memória no eixo x. Use escalas
logarítmicas para os dois eixos e desenhe uma linha para cada tamanho de cache.
a. [12] <2.6> Qual é o tamanho de página do sistema?
b. [15] <2.6> Quantas entradas existem no translation lookaside buffer (TLB)?
c. [15] <2.6> Qual é a penalidade de falta para o TLB?
d. [20] <2.6> Qual é a associatividade do TLB?
[20/20] <2.6> Em sistemas de memória de multiprocessadores, níveis inferiores
da hierarquia de memória podem não ser capazes de ser saturados por um único
processador, mas devem ser capazes de ser saturados por múltiplos processadores
trabalhando juntos. Modifique o código na Figura 2.29 e execute múltiplas cópias
ao mesmo tempo. Você pode determinar:
a. [20] <2.6> Quantos processadores reais estão no seu sistema de computador
e quantos processadores de sistema são só contextos mutithread adicionais?
b. [20] <2.6> Quantos controladores de memória seu sistema tem?
FIGURA 2.30 Resultados de exemplo do programa da Figura 2.29.
Estudos de caso com exercícios por Norman P. Jouppi, Naveen Muralimanohar e Sheng Li
2.7
[20] <2.6> Você pode pensar em um modo de testar algumas das características
de uma cache de instrução usando um programa? Dica: O compilador pode gerar
grande número de instruções não óbvias de um trecho de código. Tente usar
instruções aritméticas simples de comprimento conhecido da sua arquitetura de
conjunto de instruções (ISA).
Exercícios
2.8 [12/12/15] <2.2> As perguntas a seguir investigam o impacto de caches pequenas
e simples usando CACTI e presumindo uma tecnologia de 65 nm (0,065 mm). (O
CACTI está disponível on-line em <http://quid.hpl.hp.com:9081/cacti/>).
a. [12] <2.2> Compare os tempos de acesso das caches de 64 KB com blocos
de 64 bytes em um único banco. Quais são os tempos de acesso relativos de
caches associativas por conjunto de duas e quatro vias em comparação com uma
organização mapeada de modo diferente?
b. [12] <2.2 Compare os tempos de acesso de caches associativas por conjunto
de quatro vias com blocos de 64 bytes e um único banco. Quais são os tempos
relativos de acesso de caches de 32 KB e 64 KB em comparação com uma cache
de 16 KB?
c. [15] <2.2> Para uma cache de 64 KB, encontre a associatividade de cache entre
1 e 8 com o menor tempo médio de acesso à memória, dado que as faltas por
instrução para certa carga de trabalho é de 0,00664 para mapeamento direto,
0,00366 para associativas por conjunto de duas vias, 0,000987 para associativas
por conjunto de quatro vias e 0,000266 para cache de associativas por conjunto
de oito vias. Em geral existem 0,3 referência de dados por instrução. Suponha
que as faltas de cache levem 10 ns em todos os modelos. Para calcular o tempo
de acerto em ciclos, suponha a saída de tempo de ciclo usando CACTI, que
corresponde à frequência máxima em que uma cache pode operar sem “bolhas”
no pipeline.
2.9 [12/15/15/10] <2.2> Você está investigando os possíveis benefícios de uma
cache L1 com previsão de via. Considere que a cache de dados L1 com 64
KB, associativas por conjunto com duas vias e único banco, seja atualmente
o limitador do tempo de ciclo. Como organização de cache alternativa, você
está considerando uma cache com previsão de via modelado como uma cache
mapeada diretamente de 64 KB, com 80% de exatidão na previsão. A menos que
indicado de outra forma, considere um acesso à via mal previsto que chegue à
cache utilizando mais um ciclo. Considere as taxas de falta e as penalidades de
falta da Questão 2.8, item (c).
a. [12] <2.2> Qual é o tempo médio de acesso à memória da cache atual (em
ciclos) contra a cache com previsão de via?
b. [15] <2.2> Se todos os outros componentes pudessem operar com o tempo
de ciclo de clock com previsão de via mais rápido (incluindo a memória
principal), qual seria o impacto sobre o desempenho de usar a cache com
previsão de via?
c. [15] <2.2> As caches com previsão de via normalmente só têm sido usadas
para caches de instrução que alimentam uma fila de instrução ou buffer.
Imagine que você deseje experimentar a previsão de via em uma cache
de dados. Suponha que você tenha 80% de exatidão na previsão e que as
operações subsequentes (p. ex., acesso da cache de dados de outras instruções,
dependentes das operações) sejam emitidas pressupondo uma previsão de via
correta. Assim, um erro de previsão de via necessita de um esvaziamento de
pipe e interceptação da repetição, o que exige 15 ciclos. A mudança no tempo
119
120
CAPÍTULO 2 :
Projeto de hierarquia de memória
médio de acesso à memória por instrução de carregamento com previsão
de via na cache de dados é positiva ou negativa? Quanto?
d. [10] <2.2> Como alternativa à previsão de via, muitas caches associativas
grandes L2 serializam o acesso a tags e dados, de modo que somente o array
do conjunto de dados exigido precisa ser ativado. Isso economiza energia,
mas aumenta o tempo de acesso. Use a interface Web detalhada do CACTI
para uma cache associativa por conjunto de quatro vias, com 1 MB e processo
de 0,065 mm com blocos de 64 bytes, 144 bits lidos, um banco, somente
uma porta de leitura/escrita e tags de 30 bits. Quais são a razão de energias
de leitura dinâmica total por acesso e a razão dos tempos de acesso para
serializar o acesso a tags e dados em comparação com o acesso paralelo?
2.10 [10/12] <2.2> Você recebeu a tarefa de investigar o desempenho relativo de uma cache
de dados nível 1 em banco contra outro em pipeline para um novo microprocessador.
Considere uma cache associativa por conjunto de duas vias e 64 KB, com blocos
de 64 bytes. A cache em pipeline consistiria em dois estágios de pipe, semelhante à
cache de dados do Alpha 21264. Uma implementação em banco consistiria em dois
bancos associativos por conjunto de duas vias e 32 KB. Use o CACTI e considere uma
tecnologia de 90 nm (0,09 mm) na resposta às perguntas a seguir.
a. [10] <2.2> Qual é o tempo de ciclo da cache em comparação com o seu
tempo de acesso? Quantos estágios de pipe a cache ocupará (até duas casas
decimais)?
b. [12] <2.2> Compare a energia de leitura dinâmica total e de área por acesso
do projeto com pipeline com o projeto com bancos. Diga qual ocupa menos
área e qual requer mais potência, e explique o porquê.
2.11 [12/15] <2.2> Considere o uso de palavra crítica primeiro e o reinício antecipado
em faltas de cache L2. Suponha uma cache L2 de 1 MB com blocos de 64 bytes
e uma via de recarregar com 16 bytes de largura. Suponha que o L2 possa ser
escrito com 16 bytes a cada quatro ciclos de processador, o tempo para receber
o primeiro bloco de 16 bytes do controlador de memória é de 120 ciclos, cada
bloco adicional de 16 bytes da memória principal requer 16 ciclos, e os dados
podem ser enviados diretamente para a porta de leitura da cache L2. Ignore
quaisquer ciclos para transferir a requisição de falta para a cache L2 e os dados
requisitados para a cache L1.
a. [12] <2.2> Quantos ciclos levaria para atender uma falta de cache L2
com e sem palavra crítica primeiro e reinício antecipado?
b. [15] <2.2> Você acha que a palavra crítica primeiro e o reinício antecipado
seriam mais importantes para caches L1 e L2? Que fatores contribuiriam para
sua importância relativa?
2.12 [12/12] <2.2> Você está projetando um buffer de escrita entre uma cache
write-through L1 e uma cache write-back L2. O barramento de dados de escrita
da cache de L2 tem 16 bytes de largura e pode realizar escrita em um endereço
de cache independente a cada quatro ciclos do processador.
a. [12] <2.2> Quantos bytes de largura cada entrada do buffer de escrita deverá ter?
b. [15] <2.2> Que ganho de velocidade poderia ser esperado no estado
constante usando um write buffer merge em vez de um buffer sem mesclagem
quando a memória estiver sendo zerada pela execução de armazenamentos
de 64 bits, se todas as outras instruções puderem ser emitidas em paralelo
com os armazenamentos e os blocos estiverem presentes na cache L2?
c. [15] <2.2> Qual seria o efeito das possíveis faltas em L1 no número de
entradas necessárias de buffer de escrita para sistemas com caches com blocos
e sem blocos?
Estudos de caso com exercícios por Norman P. Jouppi, Naveen Muralimanohar e Sheng Li
2.13 [10/10/10] <2.3> Considere um sistema de desktop com um processador
conectado a uma DRAM de 2 GB com código de correção de erro (ECC). Suponha
que exista somente um canal de memória com largura de 72 bits para 64 bits para
dados e 8 bits para ECC.
a. [10] <2.3> Quantos chips DRAM estão na DIMM, se forem usados chips
de DRAM 1 GB, e quantas E/S de dados cada DRAM deve ter se somente
uma DRAM se conecta a cada pino de dados da DIMM?
b. [10] <2.3> Que duração de burst é necessária para suportar blocos de cache
L2 de 32 KB?
c. [10] <2.3> Calcule o pico de largura de banda para as DIMMs DDR2-667
e DDR2-533 para leituras de uma página ativa excluindo o overhead do ECC.
2.14 [10/10] <2.3> Um exemplo de diagrama de temporização SDRAM DDR2 aparece
na Figura 2.31. tRCD é o tempo exigido para ativar uma linha em um banco,
enquanto a latência CAS (CL) é o número de ciclos exigidos para ler uma coluna
em uma linha. Considere que a RAM esteja em um DIMM DDR2 com ECC tendo
72 linhas de dados. Considere também extensões de burst de 8 que leem 8 bits
por linha de dados, ou um total de 64 bytes do DIMM. Considere tRCD = CAS
(ou CL)* frequência_clock e frequência_clock = transferências_por_segundo/2.
A latência no chip em uma falta de cache através dos níveis 1 e 2 e de volta,
sem incluir o acesso à DRAM, é de 20 ns.
a. [10] <2.3> Quanto tempo é necessário da apresentação do comando de
ativação até que o último bit de dados solicitado das transições de DRAM
de válido para inválido para a DIMM DDR2-667 de 1 GB CL-5? Suponha
que, para cada requisição, fazemos a pré-busca automaticamente de outra
linha de cache adjacente na mesma.
b. [10] <2.3> Qual é a latência relativa quando usamos a DIMM DDR2-667 de
uma leitura requerendo um banco ativo em vez de um para uma página já aberta,
incluindo o tempo necessário para processar a falta dentro do processador?
2.15 [15] <2.3> Considere que um DIM DDR2-667 de 2 GB com CL = 5 esteja
disponível por US$ 130 e uma DIMM DDR2-533 de 2 GB com CL = 4 esteja
disponível por US$ 100. Considere o desempenho do sistema usando as DIMMs
DDR2-667 e DDR2-533 em uma carga de trabalho com 3,33 faltas em L2 por
1 K instruções, e suponha que 80% de todas as leituras de DRAM exijam uma
ativação. Qual é o custo-desempenho de todo o sistema quando usamos as
diferentes DIMMs, presumindo que somente uma falta em L2 seja pendente
em dado momento e um núcleo em ordem com uma CPI de 1,5 não inclua
tempo de acesso à memória para falta de cache?
2.16 [12] <2.3> Você está provisionando um servidor com CMP de oito núcleos
de 3 GHz, que pode executar uma carga de trabalho com uma CPI geral de 2,0
(supondo que os recarregamentos de falta de cache L2 não sejam atrasados). O
tamanho de linha da cache L2 é de 32 bytes. Supondo que o sistema use DIMMs
FIGURA 2.31 Diagrama de temporização da SDRAM DDR2.
121
122
CAPÍTULO 2 :
Projeto de hierarquia de memória
2.17
2.18
2.19
2.20
DDR2-667, quantos canais independentes de memória devem ser provisionados
para que o sistema não seja limitado pela largura de banda da memória se a
largura de banda necessária for algumas vezes o dobro da média? As cargas de
trabalho incorrem, em média, em 6,67 faltas de L2 por 1 K instruções.
[12/12] <2.3> Grande quantidade (mais de um terço) de potência de DRAM
pode ser devida à ativação da página (<http://download.micron.com/pdf/
technotes/ddr2/TN4704.pdf> e <www.micron.com/systemcalc>). Suponha que
você esteja montando um sistema com 2 GB de memória usando DRAMs DDR2
de 2 GB x8 com oito bancos ou DRAMs de 1 GB × 8 com oito bancos, as duas
com a mesma classe de velocidade. Ambas utilizam tamanho de página de 1 KB,
e o tamanho da linha de cache do último nível é de 64 bytes. Suponha que as
DRAMs que não estão ativas estejam em stand-by pré-carregado e dissipem uma
potência insignificante. Suponha que o tempo para a transição de stand-by para
ativo não seja significativo.
a. [12] <2.3> Qual tipo de DRAM você acha que resultaria em menor potência?
Explique o porquê.
b. [12] <2.3> Como uma DIMM de 2GB composta de DRAMs DDR2 de 1
GB x8 se compara em termos de potência com uma DIMM com capacidade
similar composta DRAM DDR2 de 1 GB x4?
[20/15/12] <2.3> Para acessar dados de uma DRAM típica, primeiro temos
de ativar a linha apropriada. Suponha que isso traga uma página inteira com
tamanho de 8 KB para o buffer de linha. Então, nós selecionamos determinada
coluna do buffer de linha. Se acessos subsequentes à DRAM forem feitos à mesma
página, poderemos pular o passo da ativação. Caso contrário, precisaremos fechar
a página atual e pré-carregar as linhas de bit para a próxima ativação. Outra
política popular de DRAM é fechar proativamente uma página e pré-carregar
linhas de bits assim que um acesso for encerrado. Suponha que todas as leituras
ou escritas para a DRAM sejam de 64 bytes e a latência do barramento DDR
(dados de saída na Figura 2.30) para enviar 512 bits seja Tddr.
a. [20] <2.3> Considerando a DDR2-667, se ela levar cinco ciclos para pré-carregar,
cinco ciclos para se ativar e quatro ciclos para ler uma coluna, para que valor da
taxa de acerto do buffer de linha (r) você vai escolher uma política no lugar da
outra para obter o melhor tempo de acesso? Suponha que cada acesso à DRAM
seja separado por tempo suficiente para terminar um novo acesso aleatório.
b. [15] <2.3> Se 10% dos acessos totais à DRAM acontecessem back to back
ou continuamente, sem intervalo de tempo, como sua decisão mudaria?
c. [12] <2.3> Calcule a diferença na energia média da DRAM por acesso
entre as duas políticas usando a taxa de acerto de buffer de linhas calculada
anteriormente. Suponha que o pré-carregamento requeira 2 nJ e a ativação
requeira 4 nJ, e que 100 pJ/bit sejam necessários para ler ou escrever a partir
do buffer de linha.
[15] <2.3> Sempre que um computador está inativo, podemos colocá-lo em
stand-by (onde a DRAM ainda está ativa) ou deixá-lo hibernar. Suponha que, para
a hibernação, tenhamos que copiar somente o conteúdo da DRAM para um meio
não volátil, como uma memória Flash. Se ler ou escrever em uma linha de cache
de tamanho 64 bytes para Flash requerer 2,56 mJ e a DRAM requerer 0,5 nJ, e se a
potência em estado inativo para a DRAM for de 1,6 W (para 8 GB), quanto tempo
um sistema deverá permanecer inativo para se beneficiar da hibernação? Suponha
uma memória principal com 8 GB de tamanho.
[10/10/10/10/10] <2.4> As máquinas virtuais (VMs) possuem o potencial de
incluir muitas capacidades benéficas aos sistemas de computador, resultando, por
Estudos de caso com exercícios por Norman P. Jouppi, Naveen Muralimanohar e Sheng Li
exemplo, em custo total da posse (Total Cost of Ownership — TCO) melhorado
ou disponibilidade melhorada. As VMs poderiam ser usadas para fornecer as
capacidades a seguir? Caso afirmativo, como elas poderiam facilitar isso?
a. [10] <2.4> Testar aplicações em ambientes de produção usando máquinas de
desenvolvimento?
b. [10] <2.4> Reimplementação rápida de aplicações em caso de desastre ou
falha?
c. [10] <2.4> Desempenho mais alto nas aplicações com uso intensivo das E/S?
d. [10] <2.4> Isolamento de falha entre aplicações diferentes, resultando em
maior disponibilidade dos serviços?
e. [10] <2.4> Realizar manutenção de software nos sistemas enquanto as
aplicações estão sendo executadas sem interrupção significativa?
2.21 [10/10/12/12] <2.4> As máquinas virtuais podem perder desempenho devido
a uma série de eventos, como a execução de instruções privilegiadas, faltas de
TLB, traps e E/S. Esses eventos normalmente são tratados no código do sistema.
Assim, um modo de estimar a lentidão na execução sob uma VM é a porcentagem
de tempo de execução da aplicação no sistema contra o modo usuário. Por
exemplo, uma aplicação gastando 10% de sua execução no modo do sistema
poderia retardar em 60% quando fosse executada em uma VM. A Figura 2.32
lista o desempenho inicial de diversas chamadas de sistema sob execução
nativa, virtualização pura e paravirtualização para LMbench usando Xen em um
sistema Itanium com tempos medidos em microssegundos (cortesia de Matthew
Chapman da Universidade de New South Wales).
a. [10] <2.4> Que tipos de programa poderiam ter maior lentidão quando
executados sob VMs?
b. [10] <2.4> Se a lentidão fosse linear, como função do tempo do sistema, dada
a lentidão anterior, quão mais lentamente um programa será executado se
estiver gastando 20% de sua execução no tempo do sistema?
c. [12] <2.4> Qual é a lentidão média das funções na tabela sob a virtualização
pura e paravirtualização?
d. [12] <2.4> Quais funções da tabela possuem os menores atrasos? Qual você
acha que poderia ser a causa disso?
2.22 [12] <2.4> A definição de uma máquina virtual de Popek e Goldberg estabelecia
que ela seria indistinguível de uma máquina real, exceto por seu desempenho. Neste
exercício, usaremos essa definição para descobrir se temos acesso à execução nativa
FIGURA 2.32 Desempenho inicial de diversas chamadas do sistema sob execução nativa, virtualização
pura e paravirtualização.
123
124
CAPÍTULO 2 :
Projeto de hierarquia de memória
em um processador ou se estamos executando em uma máquina virtual. A tecnologia
VT-x da Intel, efetivamente, oferece um segundo conjunto de níveis de privilégio para
o uso da máquina virtual. O que uma máquina virtual sendo executada sobre outra
máquina virtual precisaria fazer, considerando a tecnologia VT-x?
2.23 [20/25] <2.4> Com a adoção do suporte à virtualização na arquitetura x86, as
máquinas virtuais estão ativamente evoluindo e se popularizando. Compare
e contraste a virtualização das tecnologias Intel VT-x e da AMD AMD-V.
(Informações sobre a AMD-V podem ser encontradas em <http://sites.amd.com/
us/business/it-solutions/virtualization/Pages/resources.aspx>.)
a. [20] <2.4> Qual delas proporcionaria maior desempenho para aplicações
intensas no uso da memória com grande necessidade de memória?
b. [25] <2.4> Informações sobre o suporte IOMMU para E/S virtualizadas da
AMD podem ser encontradas em <http://developer.amd.com/documentation/
articles/pages/892006101.aspx>. O que a tecnologia de virtualização e uma
unidade de gerenciamento de entrada/saída (IOMMU) podem fazer para
melhorar o desempenho das E/S virtualizadas?
2.24 [30] <2.2, 2.3> Como o paralelismo em nível de instrução também pode
ser explorado efetivamente nos processadores superescalares em ordem e
VLIWs com especulação, um motivo importante para montar um processador
superescalar fora de ordem (out-of-order-OOO) é a capacidade de tolerar
a latência de memória imprevisível causada por faltas de cache. Logo, você
pode pensar no hardware que suporta a emissão OOO como fazendo parte do
sistema de memória! Veja a planta baixa do Alpha 21264 na Figura 2.33 para
descobrir a área relativa das filas de emissão e mapeadores de inteiros e ponto
flutuante contra as caches. As filas programam as instruções por emissão, e
FIGURA 2.33 Planta baixa do Alpha 21264 (Kessler, 1999).
Estudos de caso com exercícios por Norman P. Jouppi, Naveen Muralimanohar e Sheng Li
os mapeadores renomeiam os especificadores de registrador. Logo, estes são
acréscimos necessários para dar suporte à emissão OOO. O 21264 só possui
caches de dados e instruções de nível 1 no chip, e ambos são associativas por
conjunto com duas vias e 64 KB. Use um simulador superescalar OOO, como
o Simplescalar (www.cs.wisc.edu/∼mscalar/simplescalar.html) nos benchmarks
com uso intensivo de memória para descobrir quanto desempenho é perdido
se a área das filas de emissão e mapeadores for usada para a área da cache de
dados de nível 1 adicional em um processador superescalar em ordem, em vez da
emissão OOO em um modelo do 21264. Certifique-se de que os outros aspectos
da máquina sejam os mais semelhantes possíveis para tornar a comparação justa.
Ignore qualquer aumento no tempo de acesso ou ciclo a partir de caches maiores
e os efeitos da cache de dados maior na planta baixa do chip. (Observe que essa
comparação não será totalmente justa, pois o código não terá sido programado
para o processador fora de ordem pelo compilador.)
2.25 [20/20/20] <2.6> O analisador de desempenho VTune da Intel pode ser usado
para realizar muitas medições do comportamento da cache. Uma versão gratuita
para avaliação do VTune para Windows e Linux pode ser encontrada em <http://
software.intel.com/enus/articles/intel-vtune-amplifier-xe/>. O programa (aça.ch2.
cs2.c) usado no Estudo de Caso 2 foi modificado para funcionar prontamente
com o VTune em Microsoft Visual C++. O programa pode ser obtido em <www.
hpl.hp.com/research/cacti/aca_ch2_cs2_vtune.c>. Foram adicionadas funções
especiais do VTunes para excluir a inicialização e o overhead de loop durante
o processo de análise de desempenho. Instruções detalhadas da configuração
do VTunes são dadas na seção README do programa. O programa permanece
em loop por 20 segundos para cada configuração. No experimento a seguir
você poderá descobrir os efeitos do tamanho dos dados sobre a cache e sobre
o desempenho geral do processador. Execute o programa no VTube em um
processador Intel com os tamanhos de conjunto de dados de 8 KB, 128 KB, 4 MB
e 32 MB, e mantenha um passo de 64 bytes (um passo de uma linha de cache nos
processadores Intel i7). Colete estatísticas sobre o desempenho geral e das caches
de dados L1, L2 e L3.
a. [20] <2.6> Liste o número de faltas por 1 K instruções da cache de dados L1,
L2 e L3 para cada tamanho de conjunto de dados e seu modelo e velocidade
de processador. Com base nos resultados, o que você pode dizer sobre os
tamanhos de cache de dados L1, caches L2 e L3 do seu processador? Explique
suas observações.
b. [20] <2.6> Liste as instruções por clock (IPC) para cada tamanho de conjunto
de dados e seu modelo e velocidade de processador. Com base nos resultados,
o que você pode dizer sobre as penalidades de falta de L1, L2 e L3 do seu
processador? Explique suas observações.
c. [20] <2.6> Execute o programa no VTune com tamanho de conjunto de
dados de 8 KB e 125 KB em um processador OOO Intel. Liste o número
de faltas da cache de dados L1 e cache L2 por 1 K instruções e a CPI para
as duas configurações. O que você pode dizer sobre a eficácia das técnicas
de ocultamento de latência da memória em processadores OOO de alto
desempenho? Dica: Você precisa encontrar a latência de falta da cache de
dados L1 do seu processador. Para processadores Intel i7 recentes, ela é de
aproximadamente 11 ciclos.
125
CAP ÍTULO 3
Paralelismo em nível de instrução e sua
exploração
“Quem é o primeiro?”
“América.”
“Quem é o segundo?”
“Senhor, não existe segundo.”
Diálogo entre dois observadores da corrida de veleiro chamada “Copa da
América”, realizada de alguns em alguns anos — a inspiração para John Cocke
nomear o processador em pesquisa da IBM como “América”.
Esse processador foi o precursor da série RS/6000 e o primeiro microprocessador
superescalar.
3.1 Paralelismo em nível de instrução: conceitos e desafios ................................................................127
3.2 Técnicas básicas de compilador para expor o ILP ...........................................................................135
3.3 Redução de custos com previsão de desvio avançado ....................................................................140
3.4 Contornando hazards de dados com o escalonamento dinâmico ...................................................144
3.5 Escalonamento dinâmico: exemplos e algoritmo.............................................................................152
3.6 Especulação baseada em hardware ..................................................................................................158
3.7 Explorando o ILP com múltiplo despacho e escalonamento estático .............................................167
3.8 Explorando o ILP com escalonamento dinâmico, múltiplo despacho e especulação ....................170
3.9 Técnicas avançadas para o despacho de instruções e especulação ..............................................175
3.10 Estudos das limitações do ILP .........................................................................................................185
3.11 Questões cruzadas: técnicas de ILP e o sistema de memória ......................................................192
3.12 Multithreading: usando suporte do ILP para explorar o paralelismo
em nível de thread ............................................................................................................................193
3.13 Juntando tudo: Intel Core i7 e o ARM Cortex-A8 ..........................................................................202
3.14 Falácias e armadilhas .......................................................................................................................209
3.15 Comentários finais: o que temos à frente?.....................................................................................213
3.16 Perspectivas históricas e referências ..............................................................................................215
Estudos de caso e exercícios por Jason D. Bakos e Robert P. Colwell ..................................................215
3.1 PARALELISMO EM NÍVEL DE INSTRUÇÃO:
CONCEITOS E DESAFIOS
Desde cerca de 1985, todos os processadores utilizam pipelining para sobrepor a execução
de instruções e melhorar o desempenho. Essa potencial sobreposição das instruções é
chamada paralelismo em nível de instrução (Instruction-Level Parallelism — ILP), pois as
127
128
CAPÍTULO 3 :
Paralelismo em nível de instrução e sua exploração
instruções podem ser avaliadas em paralelo. Neste capítulo e no Apêndice H, veremos
grande variedade de técnicas para ampliar os conceitos básicos de pipelining, aumentando
a quantidade de paralelismo explorada entre as instruções.
Este capítulo está em um nível consideravelmente mais avançado do que o material básico
sobre pipelining, no Apêndice C. Se você não estiver acostumado com as ideias desse
apêndice, deverá revê-lo antes de se aventurar por este capítulo.
Começaremos o capítulo examinando a limitação imposta pelos hazards de dados e
hazards de controle, e depois passaremos para o tópico relacionado com o aumento da
capacidade do compilador e do processador de explorar o paralelismo. Essas seções introduzirão grande quantidade de conceitos, que acumulamos no decorrer deste capítulo e
do Capítulo 4. Embora parte do material mais básico deste capítulo pudesse ser entendida
sem todas as ideias das duas primeiras seções, esse material básico é importante para
outras seções deste capítulo.
Existem duas abordagens altamente separáveis para explorar o ILP: 1) uma que conta com
o hardware para ajudar a descobrir e explorar o paralelismo dinamicamente e 2) uma
que conta com a tecnologia de software para encontrar o paralelismo, estaticamente, no
momento da compilação. Os processadores usando a abordagem dinâmica, baseada no
hardware, incluindo a série Core da Intel, dominam os mercados de desktop e servidor.
No mercado de dispositivos pessoais móveis, no qual muitas vezes a eficiência energética
é o objetivo principal, os projetistas exploram níveis inferiores de paralelismo em nível
de instrução. Assim, em 2011, a maior parte dos processadores para o mercado de PMDs
usa abordagens estáticas, como veremos no ARM Cortex-A8. Entretanto, processadores
futuros (p. ex., o novo ARM Cortex-A9) estão usando abordagens dinâmicas. Abordagens
agressivas baseadas em compilador foram tentadas diversas vezes desde os anos 1980 e
mais recentemente na série Intel Itanium. Apesar dos enormes esforços, tais abordagens
não obtiveram sucesso fora da estreita gama de aplicações científicas.
Nos últimos anos, muitas das técnicas desenvolvidas para uma abordagem têm sido
exploradas dentro de um projeto que conta basicamente com a outra. Este capítulo introduz os conceitos básicos e as duas abordagens. Uma discussão sobre as limitações das
abordagens ILP é incluída neste capítulo, e foram tais limitações que levaram diretamente
ao movimento para o multicore. Entender as limitações ainda é importante para equilibrar
o uso de ILP e paralelismo em nível de thread.
Nesta seção, discutiremos recursos de programas e processadores que limitam a quantidade
de paralelismo que pode ser explorada entre as instruções, além do mapeamento crítico
entre a estrutura do programa e a estrutura do hardware, que é a chave para entender se uma
propriedade do programa realmente limitará o desempenho e em quais circunstâncias.
O valor do CPI (ciclos por instruções) para um processador em pipeline é a soma do CPI
base e todas as contribuições de stalls:
CPI de pipeline = CPI de pipeline ideal + Stalls estruturais
+ Stalls de hazard de dados + Stalls de controle
O CPI de pipeline ideal é uma medida do desempenho máximo que pode ser obtida pela
implementação. Reduzindo cada um dos termos do lado direito, minimizamos o CPI de
pipeline geral ou, como alternativa, aumentamos o valor do IPC (instruções por clock).
A equação anterior nos permite caracterizar o uso de diversas técnicas que permitem a
redução dos componentes do CPI geral. A Figura 3.1 mostra as técnicas que examinaremos
neste capítulo e no Apêndice H, além dos tópicos abordados no material introdutório do
3.1
Paralelismo em nível de instrução: conceitos e desafios
FIGURA 3.1 As principais técnicas examinadas no Apêndice C, no Capítulo 3 ou no Apêndice H aparecem com o componente da equação
do CPI afetado pela técnica.
Apêndice C. Neste capítulo, veremos que as técnicas introduzidas para diminuir o CPI de
pipeline ideal podem aumentar a importância de lidar com os hazards.
O que é paralelismo em nível de instrução?
Todas as técnicas deste capítulo exploram o paralelismo entre as instruções. A quantidade
de paralelismo disponível dentro de um bloco básico — uma sequência de código em linha reta, sem desvios para dentro, exceto na entrada, e sem desvios para fora, exceto na
saída — é muito pequena. Para os programas MIPS típicos, a frequência média de desvio
dinâmico normalmente fica entre 15-25%, significando que 3-6 instruções são executadas
entre um par de desvios. Como essas instruções provavelmente dependem umas das
outras, a quantidade de sobreposição que podemos explorar dentro de um bloco básico
provavelmente será menor que o tamanho médio desse bloco. Para obter melhorias de
desempenho substanciais, temos que explorar o ILP entre os diversos blocos básicos.
A maneira mais simples e mais comum de aumentar o ILP é explorar o paralelismo entre
iterações de um loop. Esse tipo de paralelismo normalmente é chamado paralelismo em
nível de loop. A seguir damos um exemplo simples de loop, que soma dois arrays de 1.000
elementos e é completamente paralelo:
Cada iteração do loop pode sobrepor qualquer outra iteração, embora dentro de cada
uma delas exista pouca ou nenhuma oportunidade para sobreposição.
Existem diversas técnicas que examinaremos para converter esse paralelismo em nível de
loop em paralelismo em nível de instrução. Basicamente, essas técnicas funcionam desdobrando o loop estaticamente pelo compilador (como na seção seguinte) ou dinamicamente pelo hardware (como nas Seções 3.5 e 3.6).
Um método alternativo importante para explorar o paralelismo em nível de loop é o uso
de SIMD tanto em processadores vetoriais quanto em unidades de processamento gráfico
129
130
CAPÍTULO 3 :
Paralelismo em nível de instrução e sua exploração
(GPUs), ambos abordados no Capítulo 4. Uma instrução SIMD explora o paralelismo
em nível de dados, operando sobre um número pequeno a moderado de itens de dados
em paralelo (geralmente 2-8). Uma instrução vetorial explora o paralelismo em nível de
dados, operando sobre itens de dados em paralelo. Por exemplo, a sequência de código
anterior, que de forma simples requer sete instruções por iteração (dois loads, um ass,
um store, dois adress updates e um brach) para um total de 7.000 instruções, poderia ser
executada com um quarto das instruções em algumas arquiteturas SIMD, onde quatro itens
de dados são processados por instrução. Em alguns processadores vetoriais, a sequência
poderia usar somente quatro instruções: duas instruções para carregar os vetores x e y da
memória, uma instrução para somar os dois vetores e uma instrução para armazenar o
vetor de resultado. Naturalmente, essas instruções seriam canalizadas em um pipeline e
teriam latências relativamente longas, mas essas latências podem ser sobrepostas.
Dependências de dados e hazards
Determinar como uma instrução depende de outra é fundamental para determinar quanto
paralelismo existe em um programa e como esse paralelismo pode ser explorado. Particularmente, para explorar o paralelismo em nível de instrução, temos de determinar quais
instruções podem ser executadas em paralelo. Se duas instruções são paralelas, elas podem
ser executadas simultaneamente em um pipeline de qualquer profundidade sem causar
quaisquer stalls, supondo que o pipeline tenha recursos suficientes (logo, não existem
hazards estruturais). Se duas instruções forem dependentes, elas não serão paralelas e
precisam ser executadas em ordem, embora normalmente possam ser parcialmente sobrepostas. O segredo, nos dois casos, é determinar se uma instrução é dependente de outra.
Dependências de dados
Existem três tipos diferentes de dependência: dependências de dados (também chamadas
dependências de dados verdadeiras), dependências de nome e dependências de controle. Uma
instrução j é dependente de dados da instrução i se um dos seguintes for verdadeiro:
j
j
a instrução i produz um resultado que pode ser usado pela instrução j; ou
a instrução j é dependente de dados da instrução k, e a instrução k é dependente de
dados da instrução i.
A segunda condição afirma simplesmente que uma instrução é dependente de outra se
houver uma cadeia de dependências do primeiro tipo entre as duas instruções. Essa cadeia
de dependência pode ter o tamanho do programa inteiro. Observe que uma dependência
dentro de uma única instrução (como ADDD R1,R1,R1) não é considerada dependência.
Por exemplo, considere a sequência de código MIPS a seguir, que incrementa um vetor de
valores na memória (começando com 0(R1), e com o último elemento em 8(R2)), por
um escalar no registrador F2 (para simplificar, no decorrer deste capítulo nossos exemplos
ignoram os efeitos dos delayed branches).
As dependências de dados nessa sequência de código envolvem tanto dados de ponto
flutuante
3.1
Paralelismo em nível de instrução: conceitos e desafios
quanto dados inteiros:
As duas sequências dependentes anteriores, conforme mostrado pelas setas, têm cada instrução dependendo da anterior. As setas aqui e nos exemplos seguintes mostram a ordem
que deve ser preservada para a execução correta. A seta sai de uma instrução que deve
preceder a instrução para a qual ela aponta.
Se duas instruções forem dependentes de dados, elas não poderão ser executadas simultaneamente nem ser completamente sobrepostas. A dependência implica que haverá uma cadeia
de um ou mais hazards de dados entre as duas instruções (ver no Apêndice C uma rápida
descrição dos hazards de dados, que definiremos com exatidão mais adiante). A execução
simultânea das instruções fará um processador com pipeline interlock (e uma profundidade
de pipeline maior que a distância entre as instruções em ciclos) detectar um hazard e parar
(stall), reduzindo ou eliminando assim a sobreposição. Em um processador sem interlock,
que conta com o escalonamento do compilador, o compilador não pode escalonar instruções dependentes de modo que elas sejam totalmente sobrepostas, pois o programa não
será executado corretamente. A presença de dependência de dados em uma sequência de
instruções reflete uma dependência de dados no código-fonte a partir do qual a sequência
de instruções foi gerada. O efeito da dependência de dados original precisa ser preservado.
As dependências são uma propriedade dos programas. Se determinada dependência resulta
em um hazard real sendo detectado e se esse hazard realmente causa um stall, essas são
propriedades da organização do pipeline. Essa diferença é essencial para entender como o
paralelismo em nível de instrução pode ser explorado.
Uma dependência de dados transmite três coisas: 1) a possibilidade de um hazard; 2) a
ordem em que os resultados podem ser calculados; e 3) um limite máximo de paralelismo
a ser explorado. Esses limites serão explorados em detalhes na Seção 3.10 e no Apêndice H.
Como uma dependência de dados pode limitar a quantidade de paralelismo em nível
de instrução que podemos explorar, um foco importante deste capítulo é contornar essas
limitações. Uma dependência pode ser contornada de duas maneiras diferentes: mantendo
a dependência, mas evitando o hazard, e eliminando uma dependência, transformando
o código. O escalonamento do código é o método principal utilizado para evitar hazards
sem alterar uma dependência, e esse escalonamento pode ser feito tanto pelo compilador
quanto pelo hardware.
Um valor de dados pode fluir entre as instruções ou por registradores ou por locais da
memória. Quando o fluxo de dados ocorre em um registrador, a detecção da dependência
é direta, pois os nomes dos registradores são fixos nas instruções, embora ela fique mais
complicada quando os desvios intervêm e questões de exatidão forçam o compilador ou
o hardware a ser conservador.
As dependências que fluem pelos locais da memória são mais difíceis de se detectar, pois
dois endereços podem referir-se ao mesmo local, mas podem aparecer de formas diferentes.
Por exemplo, 100(R4) e 20(R6) podem ser endereços de memória idênticos. Além disso,
o endereço efetivo de um load ou store pode mudar de uma execução da instrução para
outra (de modo que 20(R4) e 20(R4) podem ser diferentes), complicando ainda mais a
detecção de uma dependência.
Neste capítulo, examinaremos o hardware para detectar as dependências de dados que
envolvem locais de memória, mas veremos que essas técnicas também possuem limitações.
131
132
CAPÍTULO 3 :
Paralelismo em nível de instrução e sua exploração
As técnicas do compilador para detectar essas dependências são críticas para desvendar o
paralelismo em nível de loop.
Dependências de nome
O segundo tipo de dependência é uma dependência de nome. Uma dependência de nome ocorre
quando duas instruções usam o mesmo registrador ou local de memória, chamado nome, mas
não existe fluxo de dados entre as instruções associadas a ele. Existem dois tipos de dependências de nome entre uma instrução i que precede uma instrução j na ordem do programa:
1. Uma antidependência entre a instrução i e a instrução j ocorre quando a instrução j
escreve, em um registrador ou local de memória, que a instrução i lê. A ordenação
original precisa ser preservada para garantir que i leia o valor correto. No exemplo das
páginas 130 e 131, existe uma antidependência entre S.D e DADDIU no registrador R1.
2. Uma dependência de saída ocorre quando a instrução i e a instrução j escrevem no
mesmo registrador ou local de memória. A ordenação entre as instruções precisa ser
preservada para garantir que o valor finalmente escrito corresponda à instrução j.
As antidependências e as dependências de saída são dependências de nome, ao contrário
das verdadeiras dependências de dados, pois não existe valor sendo transmitido entre as
instruções. Como uma dependência de nome não é uma dependência verdadeira, as instruções envolvidas em uma dependência de nome podem ser executadas simultaneamente
ou ser reordenadas se o nome (número de registrador ou local de memória) usado nas
instruções for alterado de modo que as instruções não entrem em conflito.
Essa renomeação pode ser feita com mais facilidade para operandos registradores, quando é
chamada renomeação de registrador. A renomeação de registrador pode ser feita estaticamente
por um compilador ou dinamicamente pelo hardware. Antes de descrever as dependências
que surgem dos desvios, vamos examinar o relacionamento entre as dependências e os
hazards de dados do pipeline.
Hazards de dados
Um hazard é criado sempre que existe uma dependência entre instruções, e elas estão
próximas o suficiente para que a sobreposição durante a execução mude a ordem de
acesso ao operando envolvido na dependência. Devido à dependência, temos de preservar
a chamada ordem do programa, ou seja, a ordem em que as instruções seriam executadas
se executadas sequencialmente uma de cada vez, conforme determinado pelo programa
original. O objetivo do nosso software e das técnicas de hardware é explorar o paralelismo,
preservando a ordem do programa somente onde afeta o resultado do programa. A detecção
e a prevenção dos hazards garantem a preservação da ordem necessária do programa.
Os hazard de dados, que são descritos informalmente no Apêndice C, podem ser classificados em um de três tipos, dependendo da ordem de acessos de leitura e escrita nas
instruções. Por convenção, os hazards são nomeados pela ordenação no programa, que
precisa ser preservada pelo pipeline. Considere duas instruções i e j, com i precedendo j
na ordem do programa. Os hazards de dados possíveis são:
j
j
RAW (Read After Write — leitura após escrita) — j tenta ler um fonte antes que i
escreva nele, de modo que j apanha incorretamente o valor antigo. Esse hazard é o
tipo mais comum e corresponde a uma dependência de dados verdadeira. A ordem
do programa precisa ser preservada para garantir que j recebe o valor de i.
WAW (Write After Write — escrita após escrita) — j tenta escrever um operando
antes que ele seja escrito por i. As escritas acabam sendo realizadas na ordem errada,
deixando o valor escrito por i em vez do valor escrito por j no destino. Esse hazard
3.1
j
Paralelismo em nível de instrução: conceitos e desafios
corresponde a uma dependência de saída. Hazards WAW estão presentes apenas
em pipelines que escrevem em mais de um estágio de pipe ou permitem que uma
instrução prossiga mesmo quando uma instrução anterior é parada.
WAR (Write After Read — escrita após leitura) — j tenta escrever um destino antes
que seja lido por i, de modo que i incorretamente apanha o valor novo. Esse hazard
surge de uma antidependência. Hazards WAR não podem ocorrer na maioria
dos pipelines de despacho estático — até mesmo os pipelines mais profundos
ou pipelines de ponto flutuante —, pois todas as leituras vêm cedo (em ID) e
todas as escritas vêm tarde (em WB) (para se convencer, Apêndice A). Um hazard
WAR ocorre quando existem algumas instruções que escrevem resultados cedo no
pipeline de instruções e outras instruções que leem um fonte tarde no pipeline ou
quando as instruções são reordenadas, como veremos neste capítulo.
Observe que o caso RAR (Read After Read — leitura após leitura) não é um hazard.
Dependências de controle
O último tipo de dependência é uma dependência de controle. Uma dependência de controle determina a ordenação de uma instrução i com relação a uma instrução de desvio,
de modo que essa instrução seja executada na ordem correta do programa e somente
quando precisar. Cada instrução, exceto aquelas no primeiro bloco básico do programa, é
dependente de controle em algum conjunto de desvios e, em geral, essas dependências de
controle precisam ser preservadas para preservar a ordem do programa. Um dos exemplos
mais simples de uma dependência de controle é a dependência das instruções na parte
“then” de uma instrução “if” no desvio. Por exemplo, no segmento de código
S1 é dependente de controle de p1, e S2 é dependente de controle de p2, mas não de p1.
Em geral, existem duas restrições impostas pelas dependências de controle:
1. Uma instrução que é dependente de controle em um desvio não pode ser movida
antes do desvio, de modo que sua execução não é mais controlada por ele. Por
exemplo, não podemos apanhar uma instrução da parte then de uma instrução if e
movê-la para antes da instrução if.
2. Uma instrução que não é dependente de controle em um desvio não pode ser movida
para depois do desvio, de modo que sua execução é controlada pelo desvio. Por exemplo,
não podemos apanhar uma instrução antes da instrução if e movê-la para a parte then.
Quando os processadores preservam a ordem estrita do programa, eles garantem que as
dependências de controle também sejam preservadas. Porém, podemos estar querendo
executar instruções que não deveriam ter sido executadas, violando assim as dependências
de controle, se pudermos fazer isso sem afetar a exatidão do programa. A dependência
de controle não é a propriedade crítica que precisa ser preservada. Em vez disso, as duas
propriedades críticas à exatidão do programa — e normalmente preservadas mantendo-se
a dependência de dados e o controle — são o comportamento de exceção e o fluxo de dados.
A preservação do comportamento de exceção significa que quaisquer mudanças na ordem
de execução da instrução não deverão mudar o modo como as exceções são geradas no
programa. Normalmente, isso significa que a reordenação da execução da instrução não
deverá causar quaisquer novas exceções no programa. Um exemplo simples mostra como a
133
134
CAPÍTULO 3 :
Paralelismo em nível de instrução e sua exploração
manutenção das dependências de controle e dados pode impedir tais situações. Considere
essa sequência de código:
Nesse caso, é fácil ver que, se não mantivermos a dependência de dados envolvendo R2,
poderemos alterar o resultado do programa. Menos óbvio é o fato de que, se ignorarmos a
dependência de controle e movermos as instruções load para antes do desvio, elas poderão
causar uma exceção de proteção de memória. Observe que nenhuma dependência de dados
nos impede de trocar o BEQZ e o LW; essa é apenas a dependência de controle. Para permitir que reordenemos essas instruções (e ainda preservemos a dependência de dados),
gostaríamos apenas de ignorar a exceção quando o desvio for tomado. Na Seção 3.6,
veremos uma técnica de hardware, a especulação, que nos permite contornar esse problema
de exceção. O Apêndice H examina as técnicas de software para dar suporte à especulação.
A segunda propriedade preservada pela manutenção das dependências de dados e das
dependências de controle é o fluxo de dados. O fluxo de dados é o fluxo real dos valores
de dados entre as instruções que produzem resultados e aquelas que os consomem.
Os desvios tornam o fluxo de dados dinâmico, pois permitem que a fonte de dados
para determinada instrução venha de muitos pontos. Em outras palavras, é insuficiente
apenas manter dependências de dados, pois uma instrução pode ser dependente de dados
em mais de um predecessor. A ordem do programa é o que determina qual predecessor
realmente entregará um valor de dados a uma instrução. A ordem do programa é garantida
mantendo-se as dependências de controle.
Por exemplo, considere o seguinte fragmento de código:
Neste exemplo, o valor de R1 usado pela instrução OR depende de o desvio ser tomado
ou não. A dependência de dados sozinha não é suficiente para preservar a exatidão. A
instrução OR é dependente de dados nas instruções DADDU e DSUBU, mas somente
preservar essa ordem é insuficiente para a execução correta.
Em vez disso, quando as instruções são executadas, o fluxo de dados precisa ser preservado:
se o desvio não for tomado, o valor de R1 calculado pelo DSUBU deve ser usado pelo OR
e, se o desvio for tomado, o valor de R1 calculado pelo DADDU deve ser usado pelo OR.
Preservando a dependência de controle do OR no desvio, impedimos uma mudança ilegal
no fluxo dos dados. Por motivos semelhantes, a instrução DSUBU não pode ser movida
para cima do desvio. A especulação, que ajuda com o problema de exceção, também nos
permite suavizar o impacto da dependência de controle enquanto ainda mantém o fluxo
de dados, conforme veremos na Seção 3.6.
Às vezes, podemos determinar que a violação da dependência de controle não pode afetar o
comportamento da exceção ou o fluxo de dados. Considere a sequência de código a seguir:
3.2
Técnicas básicas de compilador para expor o ILP
Suponha que saibamos que o destino do registrador da instrução DSUBU (R4) não
foi usado depois da instrução rotulada com skip (a propriedade que informa se um
valor será usado por uma instrução vindoura é chamada de liveness). Se R4 não fosse
utilizado, a mudança do valor de R4 imediatamente antes do desvio não afetaria o fluxo
de dados, pois R4 estaria morto (em vez de vivo) na região do código após skip. Assim,
se R4 estivesse morto e a instrução DSUBU existente não pudesse gerar uma exceção
(outra além daquelas das quais o processador retoma o processo), poderíamos mover
a instrução DSUBU para antes do desvio, pois o fluxo de dados não poderia ser afetado
por essa mudança.
Se o desvio for tomado, a instrução DSUBU será executada e não terá utilidade, mas
não afetará os resultados do programa. Esse tipo de escalonamento de código também
é uma forma de especulação, normalmente chamada de especulação de software, pois o
compilador está apostando no resultado do desvio; nesse caso, a aposta é que o desvio
normalmente não é tomado. O Apêndice H discute mecanismos mais ambiciosos de especulação do compilador. Normalmente ficará claro, quando dissermos especulação ou
especulativo, se o mecanismo é um mecanismo de hardware ou software; quando isso
não for claro, é melhor dizer “especulação de hardware” ou “especulação de software”.
A dependência de controle é preservada pela implementação da detecção de hazard de
controlar um stall de controle. Stalls de controle podem ser eliminados ou reduzidos por
diversas técnicas de hardware e software, que examinaremos na Seção 3.3.
3.2 TÉCNICAS BÁSICAS DE COMPILADOR PARA EXPOR
O ILP
Esta seção examina o uso da tecnologia simples de compilação para melhorar a capacidade
de um processador de explorar o ILP. Essas técnicas são cruciais para os processadores
que usam despacho estático e escalonamento estático. Armados com essa tecnologia de
compilação, examinaremos rapidamente o projeto e o desempenho de processadores
usando despacho estático. O Apêndice H investigará esquemas mais sofisticados de
compilação e hardware associado, projetados para permitir que um processador explore
mais o paralelismo em nível de instrução.
Escalonamento básico de pipeline e desdobramento de loop
Para manter um pipeline cheio, o paralelismo entre as instruções precisa ser explorado
encontrando-se sequências de instruções não relacionadas que possam ser sobrepostas
no pipeline. Para evitar stall de pipeline, uma instrução dependente precisa ser separada
da instrução de origem por uma distância em ciclos de clock igual à latência do pipeline
dessa instrução de origem. A capacidade de um compilador de realizar esse escalonamento
depende da quantidade de ILP disponível no programa e das latências das unidades
funcionais no pipeline. A Figura 3.2 mostra as latências da unidade de PF que consideramos neste capítulo, a menos que latências diferentes sejam indicadas explicitamente.
Consideramos o pipeline de inteiros-padrão de cinco estágios, de modo que os desvios
possuem um atraso de um ciclo de clock. Consideramos que as unidades funcionais são
totalmente pipelined ou replicadas (tantas vezes quanto for a profundidade do pipeline),
de modo que uma operação de qualquer tipo possa ser enviada em cada ciclo de clock e
não haja hazards estruturais.
Nesta subseção, examinaremos como o compilador pode aumentar a quantidade de
ILP disponível transformando loops. Esse exemplo serve tanto para ilustrar uma técnica
importante quanto para motivar as transformações de programa mais poderosas, descritas
135
136
CAPÍTULO 3 :
Paralelismo em nível de instrução e sua exploração
FIGURA 3.2 Latências de operações de PF usadas neste capítulo.
A última coluna é o número de ciclos de clock interferindo, necessários para evitar um stall. Esses números são
semelhantes às latências médias que veríamos em uma unidade de PF. A latência de um load de ponto flutuante para
um store é 0, pois o resultado do load pode ser contornado sem protelar o store. Vamos continuar considerando uma
latência de load de inteiros igual a 1 e uma latência de operação da ALU igual a 0.
no Apêndice H. Vamos nos basear no seguinte segmento de código, que acrescenta um
valor escalar a um vetor:
Podemos ver que esse loop é paralelo, observando que o corpo de cada iteração é independente. Formalizaremos essa noção no Apêndice H, descrevendo como podemos testar
se as iterações do loop são independentes no momento da compilação. Primeiro, vejamos
o desempenho desse loop, mostrando como podemos usar o paralelismo para melhorar
seu desempenho para um pipeline MIPS com as latências indicadas antes.
O primeiro passo é traduzir o segmento anterior para a linguagem assembly MIPS. No
segmento de código a seguir, R1 é inicialmente o endereço do elemento no array com o
endereço mais alto, e F2 contém o valor escalar s. O registrador R2 é pré-calculado, de
modo que 8(R2) é o endereço do último elemento a ser processado.
O código MIPS direto, não escalonado para o pipeline, se parece com este:
Vamos começar vendo como esse loop funcionará quando programado em um pipeline
simples para MIPS com as latências da Figura 3.2.
Exemplo
Resposta
Mostre como o loop ficaria no MIPS, escalonado e não escalonado, incluindo quaisquer stalls ou ciclos de clock ociosos. Escalone para os atrasos das operações de
ponto flutuante, mas lembre-se de que estamos ignorando os delayed branches.
Sem qualquer escalonamento, o loop será executado da seguinte forma,
usando nove ciclos:
Ciclo de clock emitido
1
2
3
4
5
6
7
8
9
3.2
Técnicas básicas de compilador para expor o ILP
Podemos escalonar o loop para obter apenas dois stalls e reduzir o tempo
para sete ciclos:
Os stalls após ADD.D são para uso do S.D.
No exemplo anterior, completamos uma iteração de loop e armazenamos um elemento
do array a cada sete ciclos de clock, mas o trabalho real de operar sobre o elemento do
array leva apenas três (load, add e store) desses sete ciclos de clock. Os quatro ciclos de
clock restantes consistem em overhead do loop — o DADDUI e o BNE — e dois stalls.
Para eliminar esses quatro ciclos de clock, precisamos apanhar mais operações relativas
ao número de instruções de overhead.
Um esquema simples para aumentar o número de instruções relativas às instruções de
desvio e overhead é o desdobramento de loop. O desdobramento simplesmente replica o
corpo do loop várias vezes, ajustando o código de término do loop.
O desdobramento de loop também pode ser usado para melhorar o escalonamento.
Por eliminar o desvio, ele permite que instruções de diferentes iterações sejam escalonadas juntas. Nesse caso, podemos eliminar os stalls de uso de dados criando
instruções independentes adicionais dentro do corpo do loop. Se simplesmente
replicássemos as instruções quando desdobrássemos o loop, o uso resultante dos
mesmos registradores poderia nos impedir de escalonar o loop com eficiência. Assim,
desejaremos usar diferentes registradores para cada iteração, aumentando o número
de registradores exigidos.
Exemplo
Resposta
Mostre nosso loop desdobrado de modo que haja quatro cópias do corpo do
loop, considerando que R1 – R2 (ou seja, o tamanho do array) é inicialmente
um múltiplo de 32, o que significa que o número de iterações do loop é um
múltiplo de 4. Elimine quaisquer cálculos obviamente redundantes e não
reutilize qualquer um dos registradores.
Aqui está o resultado depois de mesclar as instruções DADDUI e remover as
operações BNE desnecessárias que são duplicadas durante o desdobramento.
Observe que agora R2 precisa ser definido de modo que 32(R2) seja o endereço
inicial dos quatro últimos elementos.
137
138
CAPÍTULO 3 :
Paralelismo em nível de instrução e sua exploração
Eliminamos três desvios e três decrementos de R1. Os endereços nos loads
e stores foram compensados para permitir que as instruções DADDUI em
R1 sejam mescladas. Essa otimização pode parecer trivial, mas não é; ela
exige substituição simbólica e simplificação. A substituição simbólica e a
simplificação rearrumarão expressões de modo a permitir que constantes
sejam reduzidas, possibilitando que uma expressão como “((i + 1) + 1)” seja
reescrita como “(i + (1 + 1))” e depois simplificada para “(i + 2)”. Veremos as
formas mais gerais dessas otimizações que eliminam cálculos dependentes
no Apêndice H.
Sem o escalonamento, cada operação no loop desdobrado é seguida por uma
operação dependente e, assim, causará um stall. Esse loop será executado em
27 ciclos de clock — cada LD tem um stall, cada ADDD tem dois, o DADDUI
tem um mais 14 ciclos de despacho de instrução — ou 6,75 ciclos de clock para
cada um dos quatro elementos, mas ele pode ser escalonado para melhorar
significativamente o desempenho. O desdobramento do loop normalmente
é feito antes do processo de compilação, de modo que cálculos redundantes
podem ser expostos e eliminados pelo otimizador.
Em programas reais, normalmente não sabemos o limite superior no loop. Suponha que
ele seja n e que gostaríamos de desdobrar o loop para criar k cópias do corpo. Em vez de
um único loop desdobrado, geramos um par de loops consecutivos. O primeiro executa
(n mod k) vezes e tem um corpo que é o loop original. O segundo é o corpo desdobrado,
cercado por um loop externo que repete (n/k) vezes (como veremos no Capítulo 4, essa
técnica é similar a uma técnica chamada strip mining, usada em compiladores para processadores vetoriais). Para valores grandes de n, a maior parte do tempo de execução será
gasta no corpo do loop desdobrado.
No exemplo anterior, o desdobramento melhora o desempenho desse loop, eliminando
as instruções de overhead, embora aumente o tamanho do código substancialmente.
Como o loop desdobrado funcionará quando for escalonado para o pipeline descrito
anteriormente?
Exemplo
Mostre o loop desdobrado no exemplo anterior após ter sido escalonado para
o pipeline com as latências mostradas na Figura 3.2.
Resposta
O tempo de execução do loop caiu para um total de 14 ciclos de clock ou 3,5
ciclos de clock por elemento, em comparação com os nove ciclos por elemento
antes de qualquer desdobramento ou escalonamento e sete ciclos quando
escalonado, mas não desdobrado.
O ganho vindo do escalonamento no loop desdobrado é ainda maior que no loop original.
Esse aumento surge porque o desdobramento do loop expõe mais computação que pode
ser escalonada para minimizar os stalls; o código anterior não possui stalls. Dessa forma, o
3.2
Técnicas básicas de compilador para expor o ILP
escalonamento do loop necessita da observação de que os loads e stores são independentes
e podem ser trocados.
Resumo do desdobramento e escalonamento de loop
No decorrer deste capítulo e no Apêndice H, veremos uma série de técnicas de hardware
e software que nos permitirão tirar proveito do paralelismo em nível de instrução para
utilizar totalmente o potencial das unidades funcionais em um processador. A chave para
a maioria dessas técnicas é saber quando e como a ordenação entre as instruções pode ser
alterada. No nosso exemplo, fizemos muitas dessas mudanças, que, para nós, como seres
humanos, eram obviamente permissíveis. Na prática, esse processo precisa ser realizado
em um padrão metódico, seja por um compilador, seja pelo hardware. Para obter o código
desdobrado final, tivemos de tomar as seguintes decisões e transformações:
j
j
j
j
j
Determinar que o desdobramento do loop seria útil descobrindo que as iterações
do loop eram independentes, exceto para o código de manutenção do loop.
Usar diferentes registradores para evitar restrições desnecessárias que seriam
forçadas pelo uso dos mesmos registradores para diferentes cálculos.
Eliminar as instruções extras de teste e desvio, e ajustar o código de término
e iteração do loop.
Determinar que os loads e stores no loop desdobrado podem ser trocados,
observando que os loads e stores de diferentes iterações são independentes. Essa
transformação requer analisar os endereços de memória e descobrir que eles não
se referem ao mesmo endereço.
Escalonar o código preservando quaisquer dependências necessárias para gerar
o mesmo resultado do código original.
O requisito-chave por trás de todas essas transformações é o conhecimento de como uma
instrução depende de outra e como as instruções podem ser alteradas ou reordenadas
dadas as dependências.
Existem três tipos de limite diferentes para os ganhos que podem ser alcançados pelo
desdobramento do loop: 1) diminuição na quantidade de overhead amortizado com
cada desdobramento; 2) limitações de tamanho de código e 3) limitações do compilador.
Vamos considerar primeiro a questão do overhead do loop. Quando desdobramos o loop
quatro vezes, ele gerou paralelismo suficiente entre as instruções em que o loop poderia
ser escalonado sem ciclos de stall. De fato, em 14 ciclos de clock, somente dois ciclos
foram overhead do loop: o DADDUI, que mantém o valor de índice, e o BNE, que termina
o loop. Se o loop for desdobrado oito vezes, o overhead será reduzido de 1/2 ciclo por
iteração original para 1/4.
Um segundo limite para o desdobramento é o consequente crescimento no tamanho do
código. Para loops maiores, o crescimento no tamanho do código pode ser um problema,
particularmente se causar aumento na taxa de falha da cache de instruções.
Outro fator normalmente mais importante que o tamanho do código é o déficit em
potencial de registradores, que é criado pelo desdobramento e pelo escalonamento agressivo. Esse efeito secundário que resulta do escalonamento de instruções em segmentos
de código grandes é chamado pressão de registradores. Ele surge porque escalonar o código
para aumentar o ILP faz com que o número de valores vivos seja aumentado. Talvez
depois do escalonamento de instrução agressivo não seja possível alocar todos os valores
vivos aos registradores. O código transformado, embora teoricamente mais rápido, pode
perder parte de sua vantagem ou toda ela, pois gera uma escassez de registradores. Sem
desdobramento, o escalonamento agressivo é suficientemente limitado pelos desvios, de
139
140
CAPÍTULO 3 :
Paralelismo em nível de instrução e sua exploração
modo que a pressão de registradores raramente é um problema. Entretanto, a combinação
de desdobramento e escalonamento agressivo pode causar esse problema. O problema
se torna especialmente desafiador nos processadores de múltiplo despacho, que exigem
a exposição de mais sequências de instruções independentes, cuja execução pode ser
sobreposta. Em geral, o uso de transformações de alto nível sofisticadas, cujas melhorias
em potencial são difíceis de medir antes da geração de código detalhada, levou a aumentos
significativos na complexidade dos compiladores modernos.
O desdobramento de loop é um método simples, porém útil, para aumentar o tamanho
dos fragmentos de código direto que podem ser escalonados com eficiência. Essa transformação é útil em diversos processadores, desde pipelines simples, como aqueles que
examinamos até aqui, até os superescalares e VLIWs de múltiplo despacho, explorados
mais adiante neste capítulo.
3.3 REDUÇÃO DE CUSTOS COM PREVISÃO DE DESVIO
AVANÇADO
Devido à necessidade de forçar as dependências de controle por meio dos hazards de dados
e stalls, os desvios atrapalharão o desempenho do pipeline. O desdobramento de loop é
uma forma de reduzir o número de hazards de desvio; também podemos reduzir as perdas
de desempenho prevendo como elas se comportarão. O comportamento dos desvios pode
ser previsto estaticamente no momento da compilação e dinamicamente pelo hardware
no momento da execução. As previsões de desvio estático às vezes são usadas nos processadores em que a expectativa é de que o comportamento do desvio seja altamente previsível
no momento da compilação; a previsão estática também pode ser usada para auxiliar na
previsão dinâmica.
Correlacionando esquemas de previsão de desvio
Os esquemas de previsão de 2 bits utilizam apenas o comportamento recente de um único
desvio para prever o comportamento futuro desse desvio. Talvez seja possível melhorar a
exatidão da previsão se também virmos o comportamento recente dos outros desvios em vez
de vermos apenas o desvio que estamos tentando prever. Considere um pequeno fragmento
de código, do benchmark eqntott, um membro dos primeiros pacotes de benchmark SPEC
que exibiam comportamento de previsão de desvio particularmente ruins:
Aqui está o código MIPS que normalmente geraríamos para esse fragmento de código,
considerando que aa e bb são atribuídos aos registradores R1 e R2:
Vamos rotular esses desvios como b1, b2 e b3. A principal observação é que o comportamento do desvio b3 é correlacionado com o comportamento dos desvios b1 e b2. Obviamente, se os desvios b1 e b2 não forem tomados (ou seja, se as condições forem avaliadas
3.3
Redução de custos com previsão de desvio avançado
como verdadeira e aa e bb receberem o valor 0), então b3 será tomado, pois aa e bb são
nitidamente iguais. Um esquema de previsão que utiliza o comportamento de um único
desvio para prever o resultado desse desvio nunca poderá capturar esse comportamento.
Os esquemas de previsão de desvio que usam o comportamento de outros desvios para fazer
uma previsão são chamados previsores de correlação ou previsores de dois níveis. Os previsores
de correlação existentes acrescentam informações sobre o comportamento da maioria dos
desvios recentes para decidir como prever determinado desvio. Por exemplo, um esquema
de previsão (1,2) utiliza o comportamento do último desvio para escolher dentre um par de
previsores de desvio de 2 bits na previsão de determinado desvio. No caso geral, um esquema
de previsão (m,n) utiliza o comportamento dos últimos m desvios para escolher dentre 2m
previsores de desvio, cada qual sendo um previsor de n bits para um único desvio. A atração
desse tipo de previsor de desvio de correlação é que ele pode gerar taxas de previsão mais altas
do que o esquema de 2 bits e exige apenas uma quantidade trivial de hardware adicional.
A simplicidade do hardware vem de uma observação simples: a história global dos m desvios mais recentes pode ser registrada em um registrador de desvio de m bits, onde cada
bit registra se o desvio foi tomado ou não. O buffer de previsão de desvio pode, então, ser
indexado usando uma concatenação dos bits de baixa ordem a partir do endereço de desvio
com um histórico global de m bits. Por exemplo, em um buffer (2,2) com 64 entradas no
total, os 4 bits de endereço de baixa ordem do desvio (endereço de palavra) e os 2 bits
globais representando o comportamento dos dois desvios executados mais recentemente
formam um índice de 6 bits que pode ser usado para indexar os 64 contadores.
Quão melhor os previsores de desvio de correlação funcionam quando comparados
com o esquema-padrão de 2 bits? Para compará-los de forma justa, temos de comparar
os previsores que utilizam o mesmo número de bits de status. O número de bits em um
previsor de (m,n) é
2m × n × Número de entradas de previsão selecionadas pelo endereço de desvio
Um esquema de previsão de 2 bits sem histórico global é simplesmente um previsor (0,2).
Exemplo
Resposta
Quantos bits existem no previsor de desvio (0,2) com 4 K entradas? Quantas
entradas existem em um previsor (2,2) com o mesmo número de bits?
O previsor com 4 K entradas possui
20 × 2 × 4 K = 8K bits
Quantas entradas selecionadas de desvio existem em um previsor (2,2) que
tem um total de 8 K bits no buffer de previsão? Sabemos que
22 × 2 × Númerodeentradasde previsãoselecionadaspelodesvio = 8K
Logo, o número de entradas de previsão selecionadas pelo desvio = 1 K.
A Figura 3.3 compara as taxas de erro de previsão do previsor anterior (0,2) com 4 K
entradas e um previsor (2,2) com 1 K entrada. Como você pode ver, esse previsor de
correlação não apenas ultrapassa o desempenho de um previsor simples de 2 bits com o
mesmo número total de bits de status, mas normalmente é superior a um previsor de 2
bits com um número ilimitado de entradas.
Previsores de torneio: combinando previsores locais e globais
adaptativamente
A principal motivação para correlacionar previsores de desvio veio da observação de que
o previsor de 2 bits padrão usando apenas informações locais falhou em alguns desvios
141
142
CAPÍTULO 3 :
Paralelismo em nível de instrução e sua exploração
FIGURA 3.3 Comparação de previsores de 2 bits.
Primeiro previsor não correlacionado para 4.096 bits, seguido por um previsor não correlacionado de 2 bits com
entradas ilimitadas e um previsor de 2 bits com 2 bits de histórico global e um total de 1.024 entradas. Embora
esses dados sejam para uma versão mais antiga de SPEC, dados para benchmarks SPEC mais recentes mostrariam
diferenças similares em precisão.
importantes e que, acrescentando informações globais, esse desempenho poderia ser melhorado. Os previsores de torneio levam essa compreensão para o próximo nível, usando
vários previsores, normalmente um baseado em informações globais e outro em informações locais, e combinando-os com um seletor. Os previsores de torneio podem conseguir
melhor exatidão em tamanhos médios (8-32 K bits) e também utilizar números muito
grandes de bits de previsão com eficiência. Os previsores de torneio existentes utilizam um
contador de saturação de 2 bits por desvio para escolher entre dois previsores diferentes
com base em qual previsor (local, global ou até mesmo alguma mistura) foi mais eficaz
nas previsões recentes. Assim como no previsor de 2 bits simples, o contador de saturação
requer dois erros de previsão antes de alterar a identidade do previsor preferido.
A vantagem de um previsor de torneio é a sua capacidade de selecionar o previsor certo
para determinado desvio, o que é particularmente crucial para os benchmarks de inteiros.
Um previsor de torneio típico selecionará o previsor global em quase 40% do tempo para
os benchmarks de inteiros SPEC e em menos de 15% do tempo para os benchmarks de
PF SPEC. Além dos processadores Alpha, que foram pioneiros dos previsores de torneio,
processadores AMD recentes, incluindo o Opteron e o Phenom, vêm usando previsores
no estilo previsor de torneio.
A Figura 3.4 examina o desempenho de três previsores diferentes (um previsor local de
2 bits, um de correlação e um de torneio) para diferentes quantidades de bits usando
o SPEC89 como benchmark. Como vimos anteriormente, a capacidade de previsão do
3.3
Redução de custos com previsão de desvio avançado
FIGURA 3.4 Taxa de erro de previsão para três previsores diferentes no SPEC89 à medida que o número
total de bits é aumentado.
Os previsores são um previsor de 2 bits local, um previsor de correlação, que é idealmente estruturado em seu uso de
informações globais e locais em cada ponto no gráfico, e um previsor de torneio. Embora esses dados sejam para uma
versão mais antiga do SPEC, os dados para benchmarks SPEC mais recentes mostrariam comportamento semelhante,
talvez convergindo para o limite assintótico em tamanhos de previsores ligeiramente maiores.
previsor local não melhora além de certo tamanho. O previsor de correlação mostra
uma melhoria significativa, e o previsor de torneio gera um desempenho ligeiramente
melhor. Para versões mais recentes do SPEC, os resultados seriam semelhantes, mas o
comportamento assintomático não seria alcançado até que houvesse previsores de tamanho ligeiramente maior.
O previsor local consiste em um previsor de dois níveis. O nível superior é uma tabela de
histórico consistindo em 1.024 entradas de 10 bits; cada entrada de 10 bits corresponde
aos 10 resultados de desvio mais recentes para a entrada. Ou seja, se o desvio foi tomado
10 ou mais vezes seguidas em uma linha, a entrada na tabela local de histórico, serão
todos 1s. Se o desvio for alternadamente tomado e não tomado, a entrada no histórico
consistirá em 0s e 1s alternados. Esse histórico de 10 bits permite que padrões de até 10
desvios sejam descobertos e previstos. A entrada selecionada da tabela de histórico local
é usada para indexar uma tabela de 1 K entradas consistindo em contadores de saturação
de 3 bits, que oferecem a previsão local. Essa combinação, que usa um total de 29 K bits,
leva a alta precisão na previsão do desvio.
O previsor de desvio Intel Core i7
A Intel liberou somente informações limitadas sobre o previsor de desvio do Core i7,
que se baseia em previsores anteriores usados no chip Core Duo. O i7 usa um previsor
de dois níveis que tem um previsor menor de primeiro nível, projetado para atender às
restrições de ciclo de previsão de um desvio a cada ciclo de clock, e um previsor maior
de segundo nível como backup. Cada previsor combina três previsores diferentes: 1) um
previsor simples de dois bits, que foi apresentado no Apêndice C (e usado no previsor
de torneio discutido anteriormente); 2) um previsor de histórico global, como aqueles
que acabamos de ver; e 3) um previsor de saída de loop. O previsor de saída de loop
usa um contador para prever o número exato de desvios tomados (que é o número de
iterações de loop) para um desvio que é detectado como um desvio de loop. Para cada
143
144
CAPÍTULO 3 :
Paralelismo em nível de instrução e sua exploração
FIGURA 3.5 A taxa de erro de previsão para 19 dos benchmarks SPEC CPU2006 em comparação com o
número de desvios removidos com sucesso em média para os benchmarks inteiros para PF (4% versus 3%).
O mais importante é que ela é muito mais alta para poucos benchmarks.
desvio, a melhor previsão é selecionada entre os três previsores rastreando a precisão de
cada previsão, como um previsor de torneio. Além desse previsor multinível principal,
uma unidade separada prevê endereços-alvo para desvios indiretos e é usada também uma
pilha para prever endereços de retorno.
Como em outros casos, a especulação cria alguns desafios na avaliação do previsor, uma
vez que um desvio previsto de modo incorreto pode facilmente levar a busca e interpretação incorretas de outro desvio. Para manter a simplicidade, examinamos o número
de previsões incorretas como uma porcentagem do número de desvios completados
com sucesso (aqueles que não foram resultado de especulação incorreta). A Figura 3.5
mostra esses dados para 19 dos benchmarks SPEC CPU2006. Esses benchmarks são
consideravelmente maiores do que o SPEC89 ou o SPEC2000, implicando que as taxas
de previsão incorreta sejam ligeiramente maiores do que aquelas na Figura 3.4, mesmo
com uma combinação mais elaborada de previsores. Uma vez que a previsão incorreta de
desvios leva à especulação ineficaz, ela contribui para o trabalho perdido, como veremos
mais adiante neste capítulo.
3.4 CONTORNANDO HAZARDS DE DADOS
COM O ESCALONAMENTO DINÂMICO
Um pipeline simples escalonado estaticamente carrega uma instrução e a envia, a menos
que haja uma dependência de dados entre uma instrução já no pipeline e a instrução
carregada, que não pode ser escondida com o bypassing ou o encaminhamento (a lógica
de encaminhamento reduz a latência efetiva do pipeline, de modo que certas dependências
não resultam em hazards). Se houver uma dependência de dados que não possa ser escondida, o hardware de detecção de hazard forçará um stall no pipeline, começando com
a instrução que usa o resultado. Nenhuma instrução nova é carregada ou enviada até que
a dependência seja resolvida.
3.4
Contornando hazards de dados com o escalonamento dinâmico
Nesta seção, exploramos o escalonamento dinâmico, em que o hardware reorganiza a
execução da instrução para reduzir os stalls enquanto mantém o fluxo de dados e o
comportamento da exceção. O escalonamento dinâmico oferece diversas vantagens:
ele permite o tratamento de alguns casos quando as dependências são desconhecidas
durante a compilação (p. ex., podem envolver uma referência à memória) e simplifica
o compilador. E, talvez, o mais importante: ele permite que o processador tolere atrasos
imprevistos, como falhas de cache, executando outro código enquanto espera que a
falha seja resolvida. Quase tão importante, o escalonamento dinâmico permite que o
código compilado com um pipeline em mente seja executado de forma eficiente em um
pipeline diferente. Na Seção 3.6, exploraremos a especulação de hardware, uma técnica
com vantagens significativas no desempenho, que é baseada no escalonamento dinâmico.
Conforme veremos, as vantagens do escalonamento dinâmico são obtidas à custa de um
aumento significativo na complexidade do hardware.
Embora um processador dinamicamente escalonado não possa mudar o fluxo de dados,
ele tenta evitar os stalls quando as dependências estão presentes. Ao contrário, o escalonamento estático do pipeline pelo compilador (explicado na Seção 3.2) tenta minimizar os
stalls separando instruções dependentes de modo que não levem a hazards. Naturalmente,
o escalonamento de pipeline do compilador também pode ser usado no código destinado
a executar em um processador com um pipeline escalonado dinamicamente.
Escalonamento dinâmico: a ideia
Uma limitação importante das técnicas de pipelining simples é que elas utilizam o despacho e a execução de instruções em ordem: as instruções são enviadas na ordem do
programa e, se uma instrução for protelada no pipeline, nenhuma instrução posterior
poderá prosseguir. Assim, se houver uma dependência entre duas instruções próximas no
pipeline, isso levará a um hazard e ocorrerá um stall. Se houver várias unidades funcionais,
essas unidades poderão ficar ociosas. Se a instrução j depender de uma instrução de longa
execução i, em execução no pipeline, então todas as instruções depois de j precisarão ser
proteladas até que i termine e j possa ser executada. Por exemplo, considere este código:
A instrução SUB.D não pode ser executada, porque a dependência de ADD.D em DIV.D
faz com que o pipeline fique em stall; mesmo assim, SUB.D não é dependente de dados
de qualquer coisa no pipeline. Esse hazard cria uma limitação de desempenho que pode
ser eliminada por não exigir que as instruções sejam executadas na ordem do programa.
No pipeline clássico em cinco estágios, os hazards estruturais e de dados poderiam ser
verificados durante a decodificação da instrução (ID): quando uma instrução pudesse ser
executada sem hazards, ela seria enviada pela ID sabendo que todos os hazards de dados
foram resolvidos.
Para que possamos começar a executar o SUB.D no exemplo anterior, temos de dividir o
processo em duas partes: verificar quaisquer hazards estruturais e esperar pela ausência de
um hazard de dados. Ainda assim usamos o despacho de instruções na ordem (ou seja,
instruções enviadas na ordem do programa), mas queremos que uma instrução comece
sua execução assim que seus operandos de dados estiverem disponíveis. Esse pipeline
realiza a execução fora de ordem, que implica em término fora de ordem.
A execução fora de ordem introduz a possibilidade de hazards WAR e WAW, que não
existem no pipeline de inteiros de cinco estágios, e sua extensão lógica a um pipeline de
145
146
CAPÍTULO 3 :
Paralelismo em nível de instrução e sua exploração
ponto flutuante em ordem. Considere a sequência de códigos de ponto flutuante MIPS
a seguir:
Existe uma antidependência entre o ADD.D e o SUB.D e, se o pipeline executar o SUB.D
antes do ADD.D (que está esperando por DIV.D), ele violará a antidependência, gerando
um hazard WAR. De modo semelhante, para evitar violar as dependências de saída, como
a escrita de F6 por MUL.D, os hazards WAW precisam ser tratados. Conforme veremos,
esses dois hazards são evitados pelo uso da renomeação de registradores.
O término fora de ordem também cria complicações importantes no tratamento de
exceções. O escalonamento dinâmico com o término fora de ordem precisa preservar o
comportamento da exceção no sentido de que exatamente as exceções que surgiriam se o
programa fosse executado na ordem estrita do programa realmente surjam. Processadores
escalonados dinamicamente preservam o comportamento de exceção garantindo que
nenhuma instrução possa gerar uma exceção até que o processador saiba que a instrução
que levanta a exceção será executada; veremos brevemente como essa propriedade pode
ser garantida.
Embora o comportamento de exceção tenha de ser preservado, os processadores escalonados dinamicamente podem gerar exceções imprecisas. Uma exceção é imprecisa se
o estado do processador quando uma exceção for levantada não se parecer exatamente
como se as instruções fossem executadas sequencialmente na ordem estrita do programa.
Exceções imprecisas podem ocorrer devido a duas possibilidades:
1. O pipeline pode ter instruções já completadas, que estão mais adiante na ordem do
programa do que a instrução que causa a exceção.
2. O pipeline pode ainda não ter completado algumas instruções, que estão mais atrás na
ordem do programa do que a instrução que causa a exceção.
As exceções imprecisas dificultam o reinício da execução após uma exceção. Em vez de
resolver esses problemas nesta seção, discutiremos na Seção 3.6 uma solução que oferece
exceções precisas no contexto de um processador com especulação. Para exceções de ponto
flutuante, outras soluções foram usadas, conforme discutiremos no Apêndice J.
Para permitir a execução fora de ordem, basicamente dividimos o estágio ID do nosso
pipeline simples de cinco estágios em dois estágios:
1. Despacho. Decodificar instruções, verificar hazards estruturais.
2. Leitura de operandos. Esperar até que não haja hazards de dados, depois ler
operandos.
Um estágio de load de instrução precede o estágio de despacho e pode carregar tanto de
um registrador de instrução quanto de uma fila de instruções pendentes; as instruções são
então enviadas a partir do registrador ou da fila. O estágio EX segue o estágio de leitura
de operandos, assim como no pipeline de cinco estágios. A execução pode levar vários
ciclos, dependendo da operação.
Distinguimos quando uma instrução inicia a execução e quando ela termina a execução;
entre os dois momentos, a instrução está em execução. Nosso pipeline permite que várias
instruções estejam em execução ao mesmo tempo e, sem essa capacidade, uma vantagem
importante do escalonamento dinâmico é perdida. Ter várias instruções em execução ao
mesmo tempo exige várias unidades funcionais, unidades funcionais pipelined ou ambas.
3.4
Contornando hazards de dados com o escalonamento dinâmico
Como essas duas capacidades — unidades funcionais pipelined e múltiplas unidades
funcionais — são essencialmente equivalentes para fins de controle de pipeline, vamos
considerar que o processador tem várias unidades funcionais.
Em um pipeline escalonado dinamicamente, todas as instruções passam de maneira
ordenada pelo estágio de despacho (despacho na ordem); porém, elas podem ser proteladas ou contornadas entre si no segundo estágio (leitura de operandos) e, assim,
entrar na execução fora de ordem. O scoreboarding é uma técnica para permitir que as instruções sejam executadas fora de ordem quando houver recursos suficientes e nenhuma
dependência de dados; recebu esse nome após o CDC 6600 scoreboard, que desenvolveu
essa capacidade, e nós a discutiremos no Apêndice A. Aqui, enfocamos uma técnica mais
sofisticada, chamada algoritmo de Tomasulo, que apresenta várias melhorias importantes em
relação ao scoreboarding. Adicionalmente, o algoritmo de Tomasulo pode ser estendido
para lidar com especulação, uma técnica para reduzir o efeito das dependências de controle
prevendo o resultado de um desvio, executando instruções no endereço de destino previsto
e realizando ações de previsão quando a previsão estiver incorreta. Embora provavelmente
o uso de scoreboarding seja suficiente para suportar um superescalar simples de dois níveis
como o ARM A8, um processador mais agressivo, como o Intel i7 de quatro despachos,
se beneficia do uso da execução fora de ordem.
Escalonamento dinâmico usando a técnica de Tomasulo
A unidade de ponto flutuante IBM 360/91 usava um esquema sofisticado para permitir a
execução fora de ordem. Esse esquema, inventado por Robert Tomasulo, verifica quando
os operandos para as instruções estão disponíveis, para minimizar os hazards RAW e introduz a renomeação de registrador para minimizar os hazards WAW e WAR. Nos processadores modernos existem muitas variações desse esquema, embora os principais conceitos
do rastreamento de dependência de instrução — para permitir a execução assim que os
operandos estiverem disponíveis e a renomeação de registradores para evitar os hazards
WAR e WAW — sejam características comuns.
O objetivo da IBM foi conseguir alto desempenho de ponto flutuante a partir de um
conjunto de instruções e de compiladores projetados para toda a família de computadores
360, em vez de compiladores especializados para os processadores de ponta. A arquitetura
360 tinha apenas quatro registradores de ponto flutuante de precisão dupla, o que limita
a eficácia do escalonamento do compilador; esse fato foi outra motivação para a técnica
de Tomasulo. Além disso, o IBM 360/91 tinha longos acessos à memória e longos atrasos
de ponto flutuante, o que o algoritmo de Tomasulo foi projetado para contornar. Ao final
desta seção, veremos que o algoritmo de Tomasulo também pode admitir a execução
sobreposta de várias iterações de um loop.
Explicamos o algoritmo, que enfoca a unidade de ponto flutuante e a unidade de load-store, no contexto do conjunto de instruções do MIPS. A principal diferença entre o MIPS
e o 360 é a presença das instruções registrador-memória na segunda arquitetura. Como
o algoritmo de Tomasulo utiliza uma unidade funcional de load, nenhuma mudança
significativa é necessária para acrescentar os modos de endereçamento registrador-memória. O IBM 360/91 também tinha unidades funcionais pipelined, em vez de múltiplas
unidades funcionais, mas descrevemos o algoritmo como se houvesse múltiplas unidades
funcionais. Essa é uma extensão conceitual simples para também utilizar o pipeline nessas
unidades funcionais.
Conforme veremos, os hazards RAW são evitados executando-se uma instrução apenas
quando seus operandos estiverem disponíveis. Hazards WAR e WAW, que surgem das
dependências de nomes, são eliminados pela renomeação de registrador. A renomeação
147
148
CAPÍTULO 3 :
Paralelismo em nível de instrução e sua exploração
de registradores elimina esses hazards renomeando todos os registradores de destino,
incluindo aqueles com leitura ou escrita pendente de uma instrução anterior, de modo
que a escrita fora de ordem não afeta quaisquer instruções que dependam de um valor
anterior de um operando.
Para entender melhor como a renomeação de registradores elimina os hazards WAR e
WAW, considere o exemplo de sequência de código a seguir, que inclui um hazard WAR
e um WAW em potencial:
Existe uma antidependência entre o ADD.D e o SUB.D e uma dependência de saída entre
o ADD.D e o MUL.D, levando a dois hazards possíveis, um hazard WAR no uso de F8
por ADD.D e um hazard WAW, pois o ADD.D pode terminar depois do MUL.D. Também
existem três dependências de dados verdadeiras: entre o DIV.D e o ADD.D, entre o SUB.D
e o MUL.D, e entre o ADD.D e o S.D.
Essas duas dependências de nome podem ser eliminadas pela renomeação de registrador.
Para simplificar, considere a existência de dois registradores temporários, S e T. Usando S
e T, a sequência pode ser reescrita sem quaisquer dependências como:
Além disso, quaisquer usos subsequentes de F8 precisam ser substituídos pelo registrador
T. Nesse segmento de código, o processo de renomeação pode ser feito estaticamente pelo
compilador. A descoberta de quaisquer usos de F8 que estejam mais adiante no código
exige análise sofisticada do compilador ou suporte do hardware, pois podem existir desvios entre o segmento de código anterior e um uso posterior de F8. Conforme veremos,
o algoritmo de Tomasulo pode lidar com a renomeação entre desvios.
No esquema de Tomasulo, a renomeação de registrador é fornecida por estações de reserva,
que colocam em buffer os operandos das instruções esperando para serem enviadas. A
ideia básica é que uma estação de reserva apanhe e coloque um operando em um buffer
assim que ele estiver disponível, eliminando a necessidade de carregar o operando de um
registrador. Além disso, instruções pendentes designam a estação de reserva que fornecerá
seu suporte. Finalmente, quando escritas sucessivas em um registrador forem superpostas
na execução, somente a última será realmente utilizada para atualizar o registrador. À
medida que as instruções forem enviadas, os especificadores de registrador para operandos
pendentes serão trocados para os nomes da estação de reserva, que oferecerá renomeação
de registrador.
Como pode haver mais estações de reserva do que registradores reais, a técnica pode
até mesmo eliminar hazards que surgem das dependências de nome que não poderiam
ser eliminadas por um compilador. À medida que explorarmos os componentes do esquema de Tomasulo, retornaremos ao tópico de renomeação de registradores e veremos
exatamente como ocorre a renomeação e como ela elimina os hazards WAR e WAW.
O uso de estações de reserva, em vez de um banco de registradores centralizado, leva a
duas outras propriedades importantes: 1) a detecção de hazard e o controle de execução
3.4
Contornando hazards de dados com o escalonamento dinâmico
são distribuídos: a informação mantida nas estações de reserva em cada unidade funcional
determina quando uma instrução pode iniciar a execução nessa unidade; 2) os resultados
são passados diretamente para as unidades funcionais a partir das estações de reserva,
onde são mantidos em buffer em vez de passarem pelos registradores. Esse bypass é feito
com um barramento de resultados comum, que permite que todas as unidades esperando
por um operando sejam carregadas simultaneamente (no 360/91, isso é chamado de
barramento de dados comum ou CDB). Em pipelines com múltiplas unidades de execução e
enviando múltiplas instruções por clock, será necessário o uso de mais de um barramento
de resultados.
A Figura 3.6 mostra a estrutura básica de um processador baseado em Tomasulo, incluindo
a unidade de ponto flutuante e a unidade de load-store; nenhuma das tabelas do controle
de execução aparece. Cada estação de reserva mantém uma instrução que foi enviada e
está esperando a execução em uma unidade funcional e outros valores operando para essa
instrução, se já tiverem sido calculados, ou então os nomes das estações de reserva que
oferecerão os valores de operando.
FIGURA 3.6 Estrutura básica de uma unidade de ponto flutuante MIPS usando o algoritmo de Tomasulo.
As instruções são enviadas da unidade de instrução para a fila de instruções, da qual são enviadas na ordem FIFO.
As estações de reserva incluem a operação e os operandos reais, além das informações usadas para detectar e
resolver hazards. Buffers de load possuem três funções: manter os componentes do endereço efetivo até que ele seja
calculado, rastrear loads pendentes que estão aguardando na memória e manter os resultados dos loads completados
que estão esperando pelo CDB. De modo semelhante, os buffers de store possuem três funções: 1) manter os
componentes do endereço efetivo até que ele seja calculado; 2) manter os endereços de memória de destino dos
stores pendentes que estão aguardando pelo valor de dado para armazenar; e 3) manter o endereço e o valor a
armazenar até que a unidade de memória esteja disponível. Todos os resultados das unidades de PF ou da unidade
de load são colocados no CDB, que vai para o banco de registradoreses de PF e também para as estações de reserva
e buffers de store. Os somadores de PF implementam adição e subtração, e os multiplicadores de PF realizam a
multiplicação e a divisão.
149
150
CAPÍTULO 3 :
Paralelismo em nível de instrução e sua exploração
Os buffers de load e os buffers de store mantêm dados ou endereços vindo e indo para
a memória, comportando-se quase exatamente como estações de reserva, de modo que
os distinguimos somente quando necessário. Os registradores de ponto flutuante estão
conectados por um par de barramentos para as unidades funcionais e por um único
barramento para os buffers de store. Todos os resultados das unidades funcionais e da
memória são enviados no barramento de dados comum, que vai para toda parte, exceto
para o buffer de load. Todas as estações de reserva possuem campos de tag, empregados
pelo controle do pipeline.
Antes de descrevermos os detalhes das estações de reserva e do algoritmo, vejamos as etapas
pelas quais uma instrução passa. Existem apenas três etapas, embora cada uma possa usar
um número arbitrário de ciclos de clock:
1. Despacho. Carregue a próxima instrução do início da fila de instrução, que é mantida
na ordem FIFO para garantir a manutenção do fluxo de dados correto. Se houver
determinada estação de reserva que esteja vazia, a instrução será enviada para a
estação com os valores de operando, se estiverem nos registradores. Se não houver
uma estação de reserva vazia, haverá um hazard estrutural, e a instrução ficará em
stall até que uma estação ou um buffer seja liberado. Se os operandos não estiverem
nos registradores, registre as unidades funcionais que produzirão os operandos. Essa
etapa renomeia registradores, eliminando os hazards WAR e WAW. (Esse estágio, às
vezes, é chamado de despacho em um processador com escalonamento dinâmico.)
2. Execução. Se um ou mais operandos ainda não estiver disponível, monitore o
barramento de dados comum enquanto espera que ele seja calculado. Quando um
operando estiver disponível, ele será colocado em qualquer estação de reserva que
o esperar. Quando todos os operandos estiverem disponíveis, a operação poderá ser
executada na unidade funcional correspondente. Adiando a execução da instrução
até que os operandos estejam disponíveis, os hazards RAW serão evitados. (Alguns
processadores com escalonamento dinâmico chamam essa etapa de “despacho”,
mas usamos o termo “execução”, que foi usado no primeiro processador com
escalonamento dinâmico, o CDC 6600.)
Observe que várias instruções poderiam ficar prontas no mesmo ciclo de clock para
a mesma unidade funcional. Embora as unidades funcionais independentes possam
iniciar a execução no mesmo ciclo de clock para diferentes instruções, se mais de uma
instrução estiver pronta para uma única unidade funcional a unidade terá de escolher
entre elas. Para as estações de reserva de ponto flutuante, essa escolha pode ser feita
arbitrariamente; porém, loads e stores apresentam uma complicação adicional.
Loads e stores exigem um processo de execução em duas etapas. A primeira etapa
calcula o endereço efetivo quando o registrador de base estiver disponível, e então o
endereço efetivo é colocado no buffer de load ou store. Loads no buffer de load são
executados assim que a unidade de memória está disponível. Stores no buffer de store
esperam pelo valor a ser armazenado antes de serem enviados à unidade de memória.
Loads e stores são mantidos na ordem do programa por meio do cálculo do endereço
efetivo, que ajudará a impedir problemas na memória, conforme veremos em breve.
Para preservar o comportamento da exceção, nenhuma instrução tem permissão para
iniciar sua execução até que todos os desvios que precedem a instrução na ordem do
programa tenham sido concluídos. Essa restrição garante que uma instrução que causa
uma exceção durante a execução realmente tenha sido executada. Em um processador
usando a previsão de desvio (como é feito em todos os processadores com escalonamento dinâmico), isso significa que o processador precisa saber que a previsão de desvio
3.4
Contornando hazards de dados com o escalonamento dinâmico
estava correta antes de permitir o início da execução de uma instrução após o desvio. Se
o processador registrar a ocorrência da exceção, mas não a tratar de fato, uma instrução
poderá iniciar sua execução mas não ser protelada até que entre na escrita do resultado.
Conforme veremos, a especulação oferece um método mais flexível e mais completo
para lidar com as exceções. Por isso, deixaremos essa melhoria para depois, a fim de
mostrarmos como a especulação trata desse problema.
3. Escrita do resultado. Quando o resultado estiver disponível, escreva-o no CDB e,
a partir daí, nos registradores e em quaisquer estações de reserva (incluindo os
buffers de store) esperando por esse resultado. Os stores são mantidos no buffer de
store até que o valor a ser armazenado e o endereço do store estejam disponíveis, e
depois o resultado é escrito assim que a unidade de memória ficar livre.
As estruturas de dados que detectam e eliminam os hazards estão conectadas às estações
de reserva, ao banco de registradoreses e aos buffers de load e store com informações ligeiramente diferentes conectadas a diferentes objetos. Essas tags são essencialmente nomes
para um conjunto estendido de registradores virtuais usados para renomeação. Em nosso
exemplo, o campo de tag é uma quantidade de 4 bits que indica uma das cinco estações
de reserva ou um dos cinco buffers de load. Como veremos, isso produz o equivalente
a 10 registradores que podem ser designados como registradores de resultado (ao contrário dos quatro registradores de precisão dupla que a arquitetura 360 contém). Em um
processador com mais registradores reais, desejaríamos que a renomeação fornecesse um
conjunto ainda maior de registradores virtuais. O campo de tag descreve qual estação de
reserva contém a instrução que produzirá um resultado necessário como operandos-fonte.
Quando uma instrução tiver sido enviada e estiver aguardando um operando-fonte, ela
se referirá ao operando pelo número da estação de reserva, atribuída à instrução que escreverá no registrador. Valores não usados, como zero, indicam que o operando já está
disponível nos registradores. Como existem mais estações de reserva do que números de
registrador reais, os hazards WAW e WAR são eliminados pela renomeação de resultados
usando números de estações de reserva. Embora no esquema de Tomasulo as estações
de reserva sejam usadas como registradores virtuais estendidos, outras técnicas poderiam
usar um conjunto de registradores com registradores adicionais ou uma estrutura como
o buffer de reordenação, que veremos na Seção 3.6.
No esquema de Tomasulo, além dos métodos subsequentes que veremos para dar suporte
à especulação, os resultados são transmitidos por broadcast a um barramento (o CDB),
que é monitorado pelas estações de reserva. A combinação do barramento de resultados
comum e da recuperação dos resultados do barramento pelas estações de reserva implementa os mecanismos de encaminhamento e bypass usados em um pipeline escalonado
estaticamente. Porém, ao fazer isso, um esquema escalonado dinamicamente introduz
um ciclo de latência entre a fonte e o resultado, pois a combinação de um resultado e seu
uso não pode ser feita antes do estágio de escrita do resultado. Assim, em um pipeline
escalonado dinamicamente, a latência efetiva entre uma instrução produzindo e uma
instrução consumindo é pelo menos um ciclo maior que a latência da unidade funcional
que produz o resultado.
É importante lembrar que as tags no esquema de Tomasulo se referem ao buffer ou unidade
que vai produzir um resultado. Os nomes de registrados são descartados quando uma
instrução envia para uma estação de reserva. (Essa é uma das diferenças-chave entre o esquema de Tomasulo e o scoreboarding: no scoreboarding, os operandos permanecem nos
registradores e somente são lidos depois da instrução que os produziu ser completada e
da instrução que vai consumi-lo estar pronta para ser executada.)
151
152
CAPÍTULO 3 :
Paralelismo em nível de instrução e sua exploração
Cada estação de reserva possui sete campos:
j
j
j
j
j
Op. A operação a ser realizada sobre os operandos-fonte S1 e S2.
Qj, Qk. As estações de reserva que produzirão o operandos-fonte correspondentes;
um valor zero indica que o operando-fonte já está disponível em Vj ou Vk, ou é
desnecessário. (O IBM 360/91 os chama SINKunit e SOURCEunit.)
Vj, Vk. O valor dos operandos-fonte. Observe que somente o campo V ou o campo
Q é válido para cada operando. Para loads, o campo Vk é usado para manter o
campo de offset. (Esses campos são chamados SINK e SOURCE no IBM 360/91.)
A. Usado para manter informações para o cálculo de endereço de memória para
um load ou store. Inicialmente, o campo imediato da instrução é armazenado aqui;
após o cálculo do endereço, o endereço efetivo é armazenado aqui.
Busy. Indica que essa estação de reserva e sua resepctiva unidade funcional estão
ocupadas.
O banco de registradores possui um campo, Qi:
j
Qi. O número da estação de reserva que contém a operação cujo resultado deve
ser armazenado nesse registrador. Se o valor de Qi estiver em branco (ou 0),
nenhuma instrução atualmente ativa está calculando um resultado destinado a esse
registrador, significando que o valor é simplesmente o conteúdo do registrador.
Os buffers de load e store possuem um campo cada, A, que mantém o resultado do
endereço efetivo quando a primeira etapa da execução tiver sido concluída.
Na próxima seção, primeiro vamos considerar alguns exemplos que mostram como
funcionam esses mecanismos e depois examinaremos o algoritmo detalhado.
3.5 ESCALONAMENTO DINÂMICO:
EXEMPLOS E ALGORITMO
Antes de examinarmos o algoritmo de Tomasulo com detalhes, vamos considerar alguns
exemplos que ajudarão a ilustrar o modo como o algoritmo funciona.
Exemplo
Mostre como se parecem as tabelas de informação para a sequência de código a
seguir quando somente o primeiro load tiver sido concluído e seu resultado escrito:
Resposta
A Figura 3.7 mostra o resultado em três tabelas. Os números anexados aos
nomes add, mult e load indicam a tag para a estação de reserva — Add1 é a
tag para o resultado da primeira unidade de soma. Além disso, incluímos uma
tabela de status de instrução. Essa tabela foi incluída apenas para ajudá-lo a
entender o algoritmo; ela não faz parte do hardware. Em vez disso, a estação
de reserva mantém o status de cada operação que foi enviada.
O esquema de Tomasulo oferece duas vantagens importantes e mais simples em relação aos
esquemas anteriores: 1) a distribuição da lógica de detecção de hazard e 2) a eliminação
de stalls para hazards WAW e WAR.
A primeira vantagem surge das estações de reserva distribuídas e do uso do Common Data
Bus (CDB). Se várias instruções estiverem aguardando um único resultado e cada instrução
3.5
Escalonamento dinâmico: exemplos e algoritmo
FIGURA 3.7 Estações de reserva e tags de registradores mostradas quando todas as instruções forem enviadas, mas somente a primeira
instrução load tiver sido concluída e seu resultado escrito no CDB.
O segundo load concluiu o cálculo do endereço efetivo, mas está esperando na unidade de memória. Usamos o array Registros[ ] para nos referirmos ao
banco de registradores, e o array Mem[ ] para nos referirmos à memória. Lembre-se de que um operando é especificado por um campo Q ou um campo V a
qualquer momento. Observe que a instrução ADD.D, que tem um hazard WAR no estágio WB, foi enviada e poderia ser concluída antes que o DIV.D se inicie.
já tiver seu outro operando, as instruções poderão ser liberadas simultaneamente por
broadcast do resultado no CDB. Se um banco de registradores centralizado fosse utilizado,
as unidades teriam de ler seus resultados dos registradores quando os barramentos de
registrador estivessem disponíveis.
A segunda vantagem, a eliminação de hazards WAW e WAR, é obtida renomeando-se os
registradores por meio das estações de reserva e pelo processo de armazenar operandos
na estação de reserva assim que estiverem disponíveis.
Por exemplo, a sequência de código na Figura 3.7 envia o DIV.D e o ADD.D, embora
exista um hazard WAR envolvendo F6. O hazard pode ser eliminado de duas maneiras.
Primeiro, se a instrução oferecendo o valor para o DIV.D tiver sido concluída, Vk
armazenará o resultado, permitindo que DIV.D seja executado independentemente do
ADD.D (esse é o caso mostrado). Por outro lado, se o L.D não tivesse sido concluído,
Qk apontaria para a estação de reserva Load1 e a instrução DIV.D seria independente
do ADD.D. Assim, de qualquer forma, o ADD.D pode ser enviado e sua execução
iniciada. Quaisquer usos do resultado do DIV.D apontariam para a estação de reserva,
153
154
CAPÍTULO 3 :
Paralelismo em nível de instrução e sua exploração
permitindo que o ADD.D concluísse e armazenasse seu valor nos registradores sem
afetar o DIV.D.
Veremos um exemplo da eliminação de um hazard WAW em breve. Mas primeiro vejamos
como nosso exemplo anterior continua a execução. Nesse exemplo, e nos exemplos
seguintes dados neste capítulo, consideramos estas latências: load usa um ciclo de clock,
uma adição usa dois ciclos de clock, multiplicação usa seis ciclos de clock e divisão usa
12 ciclos de clock.
Exemplo
Resposta
Usando o mesmo segmento de código do exemplo anterior (página 152),
mostre como ficam as tabelas de status quando o MUL.D está pronto para
escrever seu resultado.
O resultado aparece nas três tabelas da Figura 3.8. Observe que ADD.D foi
concluída, porque os operandos de DIV.D foram copiados, contornando assim
o hazard WAR. Observe que, mesmo que o load de F6 fosse adiado, o add em
F6 poderia ser executado sem disparar um hazard WAW.
Algoritmo de Tomasulo: detalhes
A Figura 3.9 especifica as verificações e etapas pelas quais cada instrução precisa passar.
Como já dissemos, loads e stores passam por uma unidade funcional para cálculo de
endereço efetivo antes de prosseguirem para buffers de load e store independentes. Os
loads usam uma segunda etapa de execução para acessar a memória e depois passar para
FIGURA 3.8 Multiplicação e divisão são as únicas instruções não terminadas.
3.5
Escalonamento dinâmico: exemplos e algoritmo
FIGURA 3.9 Etapas no algoritmo e o que é exigido para cada etapa.
Para a instrução sendo enviada, rd é o destino, rs e rt são os números dos registrador fontes, imm é o campo imediato com extensão de sinal e r é a
estação de reserva ou buffer ao qual a instrução está atribuída. RS é a estrutura de dados da estação de reserva. O valor retornado por uma unidade
de PF ou pela unidade de load é chamado de result. RegisterStat é a estrutura de dados de status do registrador (não o banco de registradores, que é
Regs[ ]). Quando uma instrução é enviada, o registrador de destino tem seu campo Qi definido com o número do buffer ou da estação de reserva à qual a
instrução é enviada. Se os operandos estiverem disponíveis nos registradores, eles serão armazenados nos campos V. Caso contrário, os campos Q serão
definidos para indicar a estação de reserva que produzirá os valores necessários como operandos-fontes. A instrução espera na estação de reserva até
que seus dois operandos estejam disponíveis, indicado por zero nos campos Q. Os campos Q são definidos com zero quando essa instrução é enviada
ou quando uma instrução da qual essa instrução depende é concluída e realiza sua escrita de volta. Quando uma instrução tiver terminado sua execução
e o CDB estiver disponível, ela poderá realizar sua escrita de volta. Todos os buffers, registradores e estações de reserva cujo valor de Qj ou Qk é igual à
estação de reserva concluída atualizam seus valores pelo CDB e marcam os campos Q para indicar que os valores foram recebidos. Assim, o CDB pode
transmitir seu resultado por broadcast para muitos destinos em um único ciclo de clock e, se as instruções em espera tiverem seus operandos, elas
podem iniciar sua execução no próximo ciclo de clock. Loads passam por duas etapas na Execução, e os stores funcionam um pouco diferente durante
a escrita de resultados, podendo ter de esperar pelo valor a armazenar. Lembre-se de que, para preservar o comportamento da exceção, as instruções
não têm permissão para serem executadas se um desvio anterior na ordem do programa não tiver sido concluído. Como qualquer conceito de ordem de
programa não é mantido após o estágio de despacho, essa restrição normalmente é implementada impedindo-se que qualquer instrução saia da etapa
de despacho, se houver um desvio pendente já no pipeline. Na Seção 3.6, veremos como o suporte à especulação remove essa restrição.
o estágio de escrita de resultados, a fim de enviar o valor da memória para o banco de
registradores e/ou quaisquer estações de reserva aguardando. Os stores completam sua
execução no estágio de escrita de resultados, que escreve o resultado na memória. Observe
que todas as escritas ocorrem nesse estágio, seja o destino um registrador ou a memória.
Essa restrição simplifica o algoritmo de Tomasulo e é fundamental para a sua extensão
com especulação na Seção 3.6.
155
156
CAPÍTULO 3 :
Paralelismo em nível de instrução e sua exploração
Algoritmo de Tomasulo: exemplo baseado em loop
Para entender o poder completo da eliminação de hazards WAW e WAR por meio da
renomeação dinâmica de registradores, temos de examinar um loop. Considere a sequência
simples ab seguir para multiplicar os elementos de um array por um escalar em F2:
Se prevermos que os desvios serão tomados, o uso de estações de reserva permitirá que
várias execuções desse loop prossigam ao mesmo tempo. Essa vantagem é obtida sem
mudar o código — de fato, o loop é desdobrado dinamicamente pelo hardware, usando
as estações de reserva obtidas pela renomeação para atuar como registradores adicionais.
Vamos supor que tenhamos enviado todas as instruções em duas iterações sucessivas do
loop, mas nenhum dos loads-stores ou operações de ponto flutuante tenham sido concluídas. A Figura 3.10 mostra as estações de reserva, tabelas de status de registrador e buffers
de load e store nesse ponto (a operação da ALU com inteiros é ignorada e considera-se
FIGURA 3.10 Duas iterações ativas do loop sem qualquer instrução concluída.
As entradas nas estações de reserva do multiplicador indicam que os loads pendentes são as fontes. As estações de reserva do store indicam que o
destino da multiplicação é a fonte do valor a armazenar.
3.5
Escalonamento dinâmico: exemplos e algoritmo
que o desvio foi previsto como sendo tomado). Quando o sistema alcança esse estado,
duas cópias do loop poderiam ser sustentadas com um CPI perto de 1,0, desde que as
multiplicações pudessem ser completadas em quatro ciclos de clock. Com uma latência
de seis ciclos, iterações adicionais terão de ser processadas antes que o estado seguro possa
ser alcançado. Isso exige mais estações de reserva para manter as instruções que estão em
execução.
Conforme veremos mais adiante neste capítulo, quando estendida com o múltiplo despacho de instruções, a técnica de Tomasulo pode sustentar mais de uma instrução por
clock.
Um load e um store podem seguramente ser feitos fora de ordem, desde que acessem
diferentes endereços. Se um load e um store acessarem o mesmo endereço, então:
j
j
o load vem antes do store na ordem do programa e sua inversão resulta em um
hazard WAR ou
o store vem antes do load na ordem do programa e sua inversão resulta em um
hazard RAW.
De modo semelhante, a inversão de dois stores para o mesmo endereço resulta em um
hazard WAW.
Logo, para determinar se um load pode ser executado em certo momento, o processador
pode verificar se qualquer store não concluído que precede o load na ordem do programa
compartilha o mesmo endereço de memória de dados que o load. De modo semelhante,
um store precisa esperar até que não haja loads ou stores não executados que estejam antes,
na ordem do programa, e que compartilham o mesmo endereço de memória de dados.
Consideramos um método para eliminar essa restrição na Seção 3.9.
Para detectar tais hazards, o processador precisa ter calculado o endereço de memória de
dados associado a qualquer operação de memória anterior. Uma maneira simples, porém
não necessariamente ideal, de garantir que o processador tenha todos esses endereços é
realizar os cálculos de endereço efetivo na ordem do programa. (Na realidade, só precisamos manter a ordem relativa entre os stores e outras referências de memória, ou seja, os
loads podem ser reordenados livremente.)
Vamos considerar a situação de um load primeiro. Se realizarmos o cálculo de endereço
efetivo na ordem do programa, quando um load tiver completado o cálculo de endereço
efetivo poderemos verificar se existe um conflito de endereço examinando o campo A de
todos os buffers de store ativos. Se o endereço de load combinar com o endereço de quaisquer entradas ativas no buffer de store, essa instrução load não será enviada ao buffer de
load até que o store em conflito seja concluído. (Algumas implementações contornam
o valor diretamente para o load a partir de um store pendente, reduzindo o atraso para
esse hazard RAW.)
Os stores operam de modo semelhante, exceto pelo fato de que o processador precisa
verificar os conflitos nos buffers de load e nos buffers de store, pois os stores em conflito
não podem ser reordenados com relação a um load ou a um store.
Um pipeline com escalonamento dinâmico pode gerar um desempenho muito alto,
desde que os desvios sejam previstos com precisão — uma questão que resolveremos
na última seção. A principal desvantagem dessa técnica é a complexidade do esquema
de Tomasulo, que exige grande quantidade de hardware. Em particular, cada estação de
reserva deve conter um buffer associativo, que precisa ser executado em alta velocidade,
além da lógica de controle complexa. O desempenho também pode ser limitado pelo
157
158
CAPÍTULO 3 :
Paralelismo em nível de instrução e sua exploração
único CDB. Embora CDBs adicionais possam ser incluídos, cada CDB precisa interagir
com cada estação de reserva, e o hardware de verificação de tag associativo precisará ser
duplicado em cada estação para cada CDB.
No esquema de Tomasulo, duas técnicas diferentes são combinadas: a renomeação dos
registradores de arquitetura para um conjunto maior de registradores e a manutenção
em buffer dos operandos-fonte a partir do banco de registradores. A manutenção em
buffer de operandos-fonte resolve os hazards WAR que surgem quando o operando
está disponível nos registradores. Como veremos mais adiante, também é possível
eliminar os hazards WAR renomeando um registrador junto com a manutenção de
um resultado em buffer até que não haja mais qualquer referência pendente à versão
anterior do registrador. Essa técnica será usada quando discutirmos sobre a especulação
de hardware.
O esquema de Tomasulo ficou sem uso por muitos anos após o 360/91, mas nos anos
1990 foi bastante adotado nos processadores de múltiplo despacho, por vários motivos:
1. Embora o algoritmo de Tomasulo fosse projetado antes das caches, a presença de
caches, com os atrasos inerentemente imprevisíveis, tornou-se uma das principais
motivações para o escalonamento dinâmico. A execução fora de ordem permite que
os processadores continuem executando instruções enquanto esperam o término
de uma falta de cache, escondendo o, assim, toda a penalidade da falta de cache ou
parte dela.
2. À medida que os processadores se tornam mais agressivos em sua capacidade
de despacho e os projetistas se preocupam com o desempenho de código difícil
de escalonamento (como a maioria dos códigos não numéricos), as técnicas
como renomeação de registradores e escalonamento dinâmico se tornam mais
importantes.
3. Ele pode alcançar alto desempenho sem exigir que o compilador destine o código
a uma estrutura de pipeline específica, uma propriedade valiosa na era do software
“enlatado” para o mercado em massa.
3.6
ESPECULAÇÃO BASEADA EM HARDWARE
À medida que tentamos explorar mais paralelismo em nível de instrução, a manutenção
de dependências de controle se torna um peso cada vez maior. A previsão de desvio reduz
os stalls diretos atribuíveis aos desvios, mas, para um processador executando múltiplas
instruções por clock, apenas prever os desvios com exatidão pode não ser suficiente para
gerar a quantidade desejada de paralelismo em nível de instrução. Um processador de alta
capacidade de despacho pode ter de executar um desvio a cada ciclo de clock para manter
o desempenho máximo. Logo, a exploração de mais paralelismo requer que contornemos
a limitação da dependência de controle.
Contornar a dependência de controle é algo feito especulando o resultado dos desvios
e executando o programa como se nossas escolhas fossem corretas. Esse mecanismo
representa uma extensão sutil, porém importante, em relação à previsão de desvio com
escalonamento dinâmico. Em particular com a especulação, buscamos, enviamos e
executamos instruções, como se nossas previsões de desvio sempre estivessem corretas; o
escalonamento dinâmico só busca e envia essas instruções. Naturalmente, precisamos de
mecanismos para lidar com a situação em que a especulação está incorreta. O Apêndice H
discute uma série de mecanismos para dar suporte à especulação pelo compilador. Nesta
seção, exploraremos a especulação do hardware, que estende as ideias do escalonamento
dinâmico.
3.6
Especulação baseada em hardware
A especulação baseada no hardware combina três ideias fundamentais: 1) previsão dinâmica de desvio para escolher quais instruções executar; 2) especulação para permitir
a execução de instruções antes que as dependências de controle sejam resolvidas (com a
capacidade de desfazer os efeitos de uma sequência especulada incorretamente); e 3)
escalonamento dinâmico para lidar com o escalonamento de diferentes combinações
de blocos básicos. (Em comparação, o escalonamento dinâmico sem especulação só
sobrepõe parcialmente os blocos básicos, pois exige que um desvio seja resolvido antes
de realmente executar quaisquer instruções no bloco básico seguinte.)
A especulação baseada no hardware segue o fluxo previsto de valores de dados para escolher
quando executar as instruções. Esse método de executar programas é essencialmente uma
execução de fluxo de dados: as operações são executadas assim que seus operandos ficam
disponíveis.
Para estender o algoritmo de Tomasulo para dar suporte à especulação, temos de separar
o bypass dos resultados entre as instruções, que é necessário para executar uma instrução
especulativamente, desde o término real de uma instrução. Fazendo essa separação,
podemos permitir que uma instrução seja executada e enviar seus resultados para outras
instruções, sem possibilitar a ela realizar quaisquer atualizações que não possam ser desfeitas, até sabermos que essa instrução não é mais especulativa.
Usar o valor bypassed é como realizar uma leitura especulativa de registrador, pois não
sabemos se a instrução que fornece o valor do registrador-fonte está fornecendo o resultado correto até que a instrução não seja mais especulativa. Quando uma instrução não
é mais especulativa, permitimos que ela atualize o banco de registradores ou a memória;
chamamos essa etapa adicional na sequência de execução da instrução de confirmação de
instrução (instruction commit).
A ideia central por trás da implementação da especulação é permitir que as instruções
sejam executadas fora de ordem, mas forçando-as a serem confirmadas em ordem e impedir
qualquer ação irrevogável (como atualizar o status ou apanhar uma exceção) até que uma
instrução seja confirmada. Logo, quando acrescentamos a especulação, precisamos separar os
processos de concluir a execução e confirmar a instrução, pois as instruções podem terminar
a execução consideravelmente antes de estarem prontas para confirmar. A inclusão dessa fase
de confirmação na sequência de execução da instrução requer um conjunto adicional de
buffers de hardware que mantenham os resultados das instruções que terminaram a execução
mas não foram confirmadas. Esse buffer de hardware, que chamamos buffer de reordenação,
também é usado para passar resultados entre instruções que podem ser especuladas.
O buffer de reordenação (ROB) oferece registradores adicionais da mesma forma que as
estações de reserva no algoritmo de Tomasulo estendem o conjunto de registradores. O
ROB mantém o resultado de uma instrução entre o momento em que a operação associada
à instrução termina e o momento em que a instrução é confirmada. Logo, o ROB é a fonte
dos operandos para as instruções, assim como as estações de reserva oferecem operandos
no algoritmo de Tomasulo. A principal diferença é que, no algoritmo de Tomasulo, quando
uma instrução escreve seu resultado, quaisquer instruções enviadas depois disso encontram
o resultado no banco de registradores. Com a especulação, o banco de registradores não
é atualizado até que a instrução seja confirmada (e nós sabemos que a instrução deverá
ser executada); assim, o ROB fornece operandos no intervalo entre o término da execução
da instrução e a confirmação da instrução. O ROB é semelhante ao buffer de store no
algoritmo de Tomasulo, e integramos a função do buffer de store no ROB para simplificar.
Cada entrada no ROB contém quatro campos: o tipo de instrução, o campo de destino,
o campo de valor e o campo de pronto (ready). O campo de tipo de instrução indica
159
160
CAPÍTULO 3 :
Paralelismo em nível de instrução e sua exploração
se a instrução é um desvio (e não possui resultado de destino), um store (que tem um
endereço de memória como destino) ou uma operação de registrador (operação da ALU
ou load, que possui como destinos registradores). O campo de destino fornece o número
do registrador (para loads e operações da ALU) ou o endereço de memória (para stores)
onde o resultado da instrução deve ser escrito. O campo de valor é usado para manter o
valor do resultado da instrução até que a instrução seja confirmada. Veremos um exemplo
de entradas ROB em breve. Finalmente, o campo de pronto indica que a instrução completou sua execução, e o valor está pronto.
A Figura 3.11 mostra a estrutura de hardware do processador incluindo o ROB. O ROB
substitui os buffers de store. Os stores ainda são executados em duas etapas, mas a segunda
etapa é realizada pela confirmação da instrução. Embora a função restante das estações
de reserva seja substituída pelo ROB, ainda precisamos de um lugar (buffer) para colocar
operações (e operandos) entre o momento em que são enviadas e o momento em que
iniciam sua execução. Essa função ainda é fornecida pelas estações de reserva. Como cada
instrução tem uma posição no ROB até que seja confirmada, identificamos um resultado
FIGURA 3.11 Estrutura básica de uma unidade de PF usando o algoritmo de Tomasulo e estendida para
lidar com a especulação.
Comparando esta figura com a Figura 3.6, na página 149, que implementava o algoritmo de Tomasulo, as principais
mudanças são o acréscimo do ROB e a eliminação do buffer de store, cuja função está integrada ao ROB. Esse
mecanismo pode ser estendido para o múltiplo despacho, tornando o CDB mais largo para permitir múltiplos términos
por clock.
3.6
Especulação baseada em hardware
usando o número de entrada do ROB em vez do número da estação de reserva. Essa
marcação exige que o ROB atribuído para uma instrução seja rastreado na estação de
reserva. Mais adiante nesta seção, exploraremos uma implementação alternativa que usa
registradores extras para renomeação e o ROB apenas para rastrear quando as instruções
podem ser confirmadas.
Aqui estão as quatro etapas envolvidas na execução da instrução:
1. Despacho. Apanhe uma instrução da fila de instruções. Envie a instrução se
houver uma estação de reserva vazia e um slot vazio no ROB; envie os operandos
à estação de reserva se eles estiverem disponíveis nos registradores ou no ROB.
Atualize as entradas de controle para indicar que os buffers estão em uso. O
número da entrada do ROB alocada para o resultado também é enviado à estação
de reserva, de modo que o número possa ser usado para marcar o resultado
quando ele for colocado no CDB. Se todas as reservas estiverem cheias ou o ROB
estiver cheio, o despacho de instrução é adiado até que ambos tenham entradas
disponíveis.
2. Execução. Se um ou mais dos operandos ainda não estiver disponível, monitore
o CDB enquanto espera que o registrador seja calculado. Essa etapa verifica os
hazards RAW. Quando os dois operandos estiverem disponíveis em uma estação
de reserva, execute a operação. As instruções podem levar vários ciclos de clock
nesse estágio, e os loads ainda exigem duas etapas nesse estágio. Os stores só
precisam ter o registrador de base disponível nessa etapa, pois a execução para um
store nesse ponto é apenas o cálculo do endereço efetivo.
3. Escrita de resultado. Quando o resultado estiver disponível, escreva-o no CDB
(com a tag ROB enviada quando a instrução for enviada) e do CDB para o ROB,
e também para quaisquer estações de reserva esperando por esse resultado.
Marque a estação de reserva como disponível. Ações especiais são necessárias para
armazenar instruções. Se o valor a ser armazenado estiver disponível, ele é escrito
no campo Valor da entrada do ROB para o store. Se o valor a ser armazenado
ainda não estiver disponível, o CDB precisa ser monitorado até que esse valor
seja transmitido, quando o campo Valor na entrada do ROB para o store
é atualizado. Para simplificar, consideramos que isso ocorre durante o estágio
de escrita de resultado de um store; mais adiante, discutiremos o relaxamento
desse requisito.
4. Confirmação (commit). Esse é o estágio final para o término de uma instrução, após
o qual somente seu resultado permanece (alguns processadores chamam essa
fase de “término” ou “graduação”). Existem três sequências de ações diferentes
na confirmação, dependendo da instrução confirmando ser um desvio com uma
previsão incorreta, um store ou qualquer outra instrução (confirmação normal).
O caso da confirmação normal ocorre quando uma instrução alcança o início do
ROB e seu resultado está presente no buffer; nesse ponto, o processador atualiza
o registrador com o resultado e remove a instrução do ROB. A confirmação de um
store é semelhante, exceto que a memória é atualizada, em vez de um registrador
de resultado. Quando um desvio com previsão incorreta atinge o início do ROB,
isso indica que a especulação foi errada. O ROB é esvaziado e a execução
é reiniciada no sucessor correto do desvio. Se o desvio foi previsto corretamente,
ele será terminado.
Quando uma instrução é confirmada, sua entrada no ROB é reclamada, e o registrador
ou destino da memória é atualizado, eliminando a necessidade da entrada do ROB. Se
o ROB se encher, simplesmente paramos de enviar instruções até que haja uma entrada
161
162
CAPÍTULO 3 :
Paralelismo em nível de instrução e sua exploração
disponível. Agora, vamos examinar como esse esquema funcionaria com o mesmo exemplo
que usamos para o algoritmo de Tomasulo.
Exemplo
Vamos considerar as mesmas latências para as unidades funcionais de ponto
flutuante que nos exemplos anteriores: adição usa dois ciclos de clock, multiplicação usa seis ciclos de clock e divisão usa 12 ciclos de clock. Usando o
segmento de código a seguir, o mesmo que usamos para gerar a Figura 3.8,
mostre como ficariam as tabelas de status quando o MUL.D estiver pronto
para a confirmação.
Resposta
A Figura 3.12 mostra o resultado nas três tabelas. Observe que, embora a
instrução SUB.D tenha completado sua execução, ela não é confirmada até
que o MUL.D seja confirmado. As estações de reserva e o campo de status do
registrador contêm a mesma informação básica que eles tinham no algoritmo
de Tomasulo (ver descrição desses campos na página 152). A diferença é que
os números de estação de reserva são substituídos por números de entrada
ROB nos campos Qj e Qk, e também nos campos de status de registrador, e
acrescentamos o campo Dest às estações de reserva. O campo Dest designa a
entrada do ROB, que é o destino para o resultado produzido por essa entrada
da estação de reserva.
O exemplo ilustra a importante diferença-chave entre um processador com especulação e
um processador com escalonamento dinâmico. Compare o conteúdo da Figura 3.12 com
o da Figura 3.8, na página 154, que mostra a mesma sequência de código em operação em
um processador com o algoritmo de Tomasulo. A principal diferença é que, no exemplo
anterior, nenhuma instrução após a instrução mais antiga não completada (MUL.D acima)
tem permissão para concluir. Ao contrário, na Figura 3.8, as instruções SUB.D e ADD.D
também foram concluídas.
Uma implicação dessa diferença é que o processador com o ROB pode executar código
dinamicamente enquanto mantém um modelo de interrupção preciso. Por exemplo, se
a instrução MUL.D causasse uma interrupção, poderíamos simplesmente esperar até que
ela atingisse o início do ROB e apanhar a interrupção, esvaziando quaisquer outras instruções pendentes do ROB. Como a confirmação da instrução acontece em ordem, isso
gera uma exceção precisa.
Ao contrário, no exemplo usando o algoritmo de Tomasulo, as instruções SUB.D e ADD.D
poderiam ser concluídas antes que o MUL.D levantasse a exceção. O resultado é que os
registradores F8 e F6 (destinos das instruções SUB.D e ADD.D) poderiam ser sobrescritos
e a interrupção seria imprecisa.
Alguns usuários e arquitetos decidiram que as exceções de ponto flutuante imprecisas
são aceitáveis nos processadores de alto desempenho, pois o programa provavelmente
terminará; veja no Apêndice J uma discussão aprofundada desse assunto. Outros tipos
de exceção, como falhas de página, são muito mais difíceis de acomodar quando são
imprecisas, pois o programa precisa retomar a execução transparentemente depois de
tratar de tal exceção.
O uso de um ROB com a confirmação de instrução em ordem oferece exceções precisas,
além de dar suporte à exceção especulativa, como mostra o exemplo seguinte.
3.6
Especulação baseada em hardware
FIGURA 3.12 No momento em que o MUL.D está pronto para ser confirmado, somente as duas instruções L.D foram confirmadas, embora
várias outras tenham completado sua execução.
O MUL.D está no início do ROB, e as duas instruções L.D estão lá somente para facilitar a compreensão. As instruções SUB.D e ADD.D não serão confirmadas
até que a instrução MUL.D seja confirmada, embora os resultados das instruções estejam disponíveis e possam ser usados como fontes para outras instruções.
O DIV.D está em execução, mas ainda não concluiu unicamente devido à sua latência maior do que MUL.D. A coluna Valor indica o valor sendo mantido; o
formato #X é usado para se referir a um campo de valor da entrada X do ROB. Os buffers de reordenação 1 e 2 estão realmente concluídos, mas aparecem para
fins informativos. Não mostramos as entradas para a fila load-store, mas essas entradas são mantidas em ordem.
Exemplo
Resposta
Considere o exemplo de código utilizado para o algoritmo de Tomasulo e
mostrado na Figura 3.10 em execução:
Considere que tenhamos enviado todas as instruções no loop duas vezes.
Vamos também considerar que o L.D e o MUL.D da primeira iteração foram
confirmados e todas as outras instruções terminaram a execução. Normalmente, o store esperaria no ROB pelo operando de endereço efetivo (R1 neste
exemplo) e pelo valor (F4 neste exemplo). Como só estamos considerando o
pipeline de ponto flutuante, suponha que o endereço efetivo para o store seja
calculado no momento em que a instrução é enviada.
A Figura 3.13 mostra o resultado em duas tabelas.
163
164
CAPÍTULO 3 :
Paralelismo em nível de instrução e sua exploração
Como nem os valores de registradores nem quaisquer valores de memória são realmente
escritos até que uma instrução seja confirmada, o processador poderá facilmente desfazer suas ações especulativas quando um desvio for considerado mal previsto. Suponha
que o desvio BNE não seja tomado pela primeira vez na Figura 3.13. As instruções antes
do desvio simplesmente serão confirmadas quando cada uma alcançar o início do ROB;
quando o desvio alcançar o início desse buffer, o buffer será simplesmente apagado e o
processador começará a apanhar instruções do outro caminho.
Na prática, os processadores que especulam tentam se recuperar o mais cedo possível após
um desvio ser mal previsto. Essa recuperação pode ser feita limpando-se o ROB para todas
as entradas que aparecem após o desvio mal previsto, permitindo que aquelas que estão
antes do desvio no ROB continuem, reiniciando a busca no sucesso correto do desvio.
Nos processadores especulativos, o desempenho é mais sensível à previsão do desvio, pois
o impacto de um erro de previsão é mais alto. Assim, todos os aspectos do tratamento
de desvios — exatidão da previsão, latência da detecção de erro de previsão e tempo de
recuperação do erro de previsão — passam a ter mais importância.
As exceções são tratadas pelo seu não reconhecimento até que estejam prontas para serem
confirmadas. Se uma instrução especulada levantar uma exceção, a exceção será registrada no
ROB. Se um erro de previsão de desvio surgir e a instrução não tiver sido executada, a exceção
será esvaziada junto com a instrução quando o ROB for apagado. Se a instrução atingir o
início do ROB, saberemos que ela não é mais especulativa, e a exceção deverá realmente ser
tomada. Também poderemos tentar tratar das exceções assim que elas surgirem e todos os
desvios anteriores forem resolvidos, porém isso é mais desafiador no caso das exceções do
que para o erro de previsão de desvio, pois ocorre com menos frequência e não é tão crítico.
FIGURA 3.13 Somente as instruções L.D e MUL.D foram confirmadas, embora todas as outras tenham a execução concluída.
Logo, nenhuma estação de reserva está ocupada e nenhuma aparece. As instruções restantes serão confirmadas o mais rápido possível. Os dois primeiros buffers
de reordenação serão confirmados o mais rápido possível. Os dois primeiros buffers de reordenação estão vazios, mas aparecem para completar a figura.
3.6
Especulação baseada em hardware
A Figura 3.14 mostra as etapas da execução para uma instrução, além das condições que
devem ser satisfeitas a fim de prosseguir para a etapa e as ações tomadas. Mostramos o
caso em que os desvios mal previstos não são resolvidos antes da confirmação. Embora a
especulação pareça ser um acréscimo simples ao escalonamento dinâmico, uma comparação da Figura 3.14 com a figura comparável para o algoritmo de Tomasulo na Figura 3.9
mostra que a especulação acrescenta complicações significativas ao controle. Além disso,
lembre-se de que os erros de previsão de desvio também são um pouco mais complexos.
Existe uma diferença importante no modo como os stores são tratados em um processador
especulativo e no algoritmo de Tomasulo. No algoritmo de Tomasulo, um store pode
atualizar a memória quando alcançar a escrita de resultado (o que garante que o endereço
efetivo foi calculado) e o valor de dados a armazenar estiver disponível. Em um processador
especulativo, um store só atualiza a memória quando alcança o início do ROB. Essa diferença
garante que a memória não seja atualizada até que uma instrução não seja mais especulativa.
A Figura 3.14 apresenta uma simplificação significativa para stores, que é desnecessária na
prática. Ela exige que os stores esperem no estágio de escrita de resultado pelo registrador
operando-fonte cujo valor deve ser armazenado; o valor é, então, movido do campo Vk
da estação de reserva do store para o campo Valor da entrada de store do ROB. Porém, na
realidade, o valor a ser armazenado não precisa chegar até imediatamente antes do store ser
confirmado, e pode ser colocado diretamente na entrada de store do ROB, pela instrução
de origem. Isso é realizado fazendo com que o hardware acompanhe quando o valor de
origem a ser armazenado estará disponível na entrada de store do ROB e pesquisando o
ROB a cada término de instrução para procurar stores dependentes.
Esse acréscimo não é complicado, mas sua inclusão tem dois efeitos: precisaríamos acrescentar um campo no ROB, e a Figura 3.14, que já está com uma fonte pequena, seria ainda
maior! Embora a Figura 3.14 faça essa simplificação, em nossos exemplos permitiremos
que o store passe pelo estágio de escrita de resultado e simplesmente espere que o valor
esteja pronto quando for confirmado.
Assim como o algoritmo de Tomasulo, temos de evitar hazards na memória. Hazards WAW
e WAR na memória são eliminados com a especulação, pois a atualização real da memória
ocorre em ordem, quando um store está no início do ROB e, portanto, nenhum load ou store
anterior poderá estar pendente. Os hazards RAW na memória são mantidos por duas restrições:
1. Não permitir que um load inicie a segunda etapa de sua execução se qualquer
entrada ROB ativa ocupada por um store tiver um campo Destino que corresponda
ao valor do campo A do load e
2. Manter a ordem do programa para o cálculo de endereço efetivo de um load com
relação a todos os stores anteriores.
Juntas, essas duas restrições garantem que nenhum load que acesse um local da memória
escrito por um store anterior poderá realizar o acesso à memória até que o store tenha escrito os dados. Alguns processadores especulativos realmente contornarão o valor do store
para o load diretamente, quando ocorrer esse hazard RAW. Outra técnica é prever colisões
em potencial usando uma forma de previsão de valor; consideraremos isso na Seção 3.9.
Embora essa explicação da execução especulativa tenha focalizado o ponto flutuante, as
técnicas se estendem facilmente para registradores inteiros e unidades funcionais, conforme
veremos na seção “Juntando tudo”. Na realidade, a especulação pode ser mais útil em programas de inteiros, pois esses programas costumam ter um código em que o comportamento
do desvio é menos previsível. Além disso, essas técnicas podem ser estendidas para que
funcionem em um processador de múltiplo despacho, permitindo que várias instruções
165
166
CAPÍTULO 3 :
Paralelismo em nível de instrução e sua exploração
FIGURA 3.14 Etapas no algoritmo e o que é necessário para cada etapa.
Para as instruções enviadas, rd é o destino, rs e rt são os fontes, r é a estação de reserva alocada, b é a entrada do ROB atribuída e h é a entrada inicial
do ROB. RS é a estrutura de dados da estação de reserva. O valor retornado por uma estação de reserva é chamado de resultado. RegisterStat é a
estrutura de dados do registrador, Regs representa os registradores reais e ROB é a estrutura de dados do buffer de reordenação.
3.7
Explorando o ILP com múltiplo despacho e escalonamento estático
sejam enviadas e confirmadas a cada clock. Na verdade, a especulação provavelmente é mais
interessante nesses processadores, pois talvez técnicas menos ambiciosas possam explorar
um ILP suficiente dentro dos blocos básicos quando auxiliado por um compilador.
3.7 EXPLORANDO O ILP COM MÚLTIPLO DESPACHO
E ESCALONAMENTO ESTÁTICO
As técnicas das seções anteriores podem ser usadas para eliminar stalls de dados e controle
e alcançar um CPI ideal de 1. Para melhorar ainda mais o desempenho, gostaríamos de
diminuir o CPI para menos de 1. Mas o CPI não pode ser reduzido para menos de 1 se
enviarmos apenas uma instrução a cada ciclo de clock.
O objetivo dos processadores de múltiplo despacho, discutidos nas próximas seções, é permitir que múltiplas instruções sejam enviadas em um ciclo de clock. Os processadores de
múltiplo despacho podem ser de três tipos principais:
1. Processadores superescalares escalonados estaticamente.
2. Processadores VLIW (Very Long Instruction Word).
3. Processadores superescalares escalonados dinamicamente.
Os dois tipos de processadores superescalares enviam números variados de instruções por
clock e usam a execução em ordem quando são estaticamente escalonados ou a execução
fora da ordem quando são dinamicamente escalonados.
Processadores VLIW, ao contrário, enviam um número fixo de instruções formatadas como
uma instrução grande ou como um pacote de instrução fixo com o paralelismo entre instruções indicado explicitamente pela instrução. Processadores VLIW são inerentemente escalonados de forma estática pelo compilador. Quando a Intel e a HP criaram a arquitetura
IA-64, descrita no Apêndice H, também introduziram o nome EPIC (Explicitly Parallel
Instruction Computer) para esse estilo de arquitetura.
Embora os superescalares escalonados estaticamente enviem um número variável e não
um número fixo de instruções por clock, na verdade, em conceito, eles estão mais próximos
aos VLIWs, pois as duas técnicas contam com o compilador para escalonar o código para
o processador. Devido às vantagens cada vez menores de um superescalar escalonado estaticamente à medida que a largura de despacho aumenta, os superescalares escalonados estaticamente são usados principalmente para larguras de despacho estreitas, geralmente apenas
com duas instruções. Além dessa largura, a maioria dos projetistas escolhe implementar um
VLIW ou um superescalar escalonado dinamicamente. Devido às semelhanças no hardware
e tecnologia de compilador exigida, nesta seção enfocamos os VLIWs. Os conhecimentos
desta seção são facilmente extrapolados para um superescalar escalonado estaticamente.
A Figura 3.15 resume as técnicas básicas para o múltiplo despacho e suas características
distintas, mostrando os processadores que usam cada técnica.
A técnica VLIW básica
Os VLIWs utilizam múltiplas unidades funcionais independentes. Em vez de tentar enviar múltiplas instruções independentes para as unidades, um VLIW empacota as múltiplas operações
em uma instrução longa ou exige que as instruções no pacote de despacho satisfaçam as mesmas restrições. Como não existe diferença fundamental nas duas técnicas, assumiremos apenas
que múltiplas operações são colocadas em uma instrução, como na técnica VLIW original.
Como a vantagem de um VLIW aumenta à medida que a taxa de despacho máxima cresce,
enfocamos um processador com largura de despacho maior. Na realidade, para proces-
167
168
CAPÍTULO 3 :
Paralelismo em nível de instrução e sua exploração
FIGURA 3.15 As cinco técnicas principais em uso para processadores de múltiplo despacho e as principais características que os distinguem.
Este capítulo enfoca as técnicas com uso intensivo de hardware, que são todas de alguma forma de superescalar. O Apêndice G enfoca as técnicas
baseadas em compilador. A técnica EPIC, incorporada à arquitetura IA-64, estende muitos dos conceitos das primeiras técnicas VLIW, oferecendo uma
mistura de técnicas estáticas e dinâmicas.
sadores simples de despacho com largura dois, o overhead de um superescalar provavelmente é mínimo. Muitos projetistas provavelmente argumentariam que um processador
de despacho quádruplo possui overhead controlável, mas, como veremos mais adiante
neste capítulo, o crescimento no overhead é um fator importante que limita os processadores com larguras de despacho maiores.
Vamos considerar um processador VLIW com instruções que contêm cinco operações, incluindo uma operação com inteiros (que também poderia ser um desvio), duas operações
de ponto flutuante e duas referências à memória. A instrução teria um conjunto de campos
para cada unidade funcional — talvez 16-24 bits por unidade, gerando um tamanho de
instrução de algo entre 80-120 bits. Por comparação, o Intel Itanium 1 e o 2 contêm seis
operações de instruções por pacote (ou seja, eles permitem o despacho concorrente de
dois conjuntos de três instruções, como descreve o Apêndice H).
Para manter as unidades funcionais ocupadas, é preciso haver paralelismo suficiente em
uma sequência de código para preencher os slots de operação disponíveis. Esse paralelismo
é descoberto desdobrando os loops e escalonando o código dentro do único corpo de
loop maior. Se o desdobramento gerar código sem desvios ou loops (straight-line code),
as técnicas de escalonamento local, que operam sobre um único bloco básico, podem ser utilizadas. Se a localização e a exploração do paralelismo exigirem escalonamento de código
entre os desvios, um algoritmo de escalonamento global substancialmente mais complexo
terá de ser usado. Os algoritmos de escalonamento global não são apenas mais complexos
em estrutura, mas também precisam lidar com escolhas significativamente mais complicadas em otimização, pois a movimentação de código entre os desvios é dispendiosa.
No Apêndice H, discutiremos o escalonamento de rastreio, uma dessas técnicas de escalonamento global desenvolvidas especificamente para VLIWs; também exploraremos o suporte especial
de hardware, que permite que alguns desvios condicionais sejam eliminados, estendendo a
utilidade do escalonamento local e melhorando o desempenho do escalonamento global.
Por enquanto, contaremos com o desdobramento do loop para gerar sequências de código
longas, straight-line, a fim de podermos usar o escalonamento local para montar instruções
VLIW e explicar o quanto esses processadores operam bem.
3.7
Exemplo
Resposta
Explorando o ILP com múltiplo despacho e escalonamento estático
Suponha que tenhamos um VLIW que possa enviar duas referências à
memória, duas operações de PF e uma operação com inteiros ou desvio a
cada ciclo de clock. Mostre uma versão desdobrada do loop x[i] = x[i] + s
(ver código MIPS na página 136) para tal processador. Desdobre tantas
vezes quantas forem necessárias para eliminar quaisquer stalls. Ignore os
desvios adiados.
A Figura 3.16 mostra o código. O loop foi desdobrado para fazer sete cópias
do corpo, o que elimina quaisquer stalls (ou seja, ciclos de despacho completamente vazios), sendo executado em nove ciclos. Esse código gera
uma taxa de execução de sete resultados em nove ciclos ou 1,29 ciclo por
resultado, quase o dobro da rapidez do superescalar de despacho duplo da
Seção 3.2, que usava código desdobrado e escalonado.
Para o modelo VLIW original, havia problemas técnicos e logísticos que tornavam a
técnica menos eficiente. Os problemas técnicos são o aumento no tamanho do código
e as limitações da operação de bloqueio. Dois elementos diferentes são combinados
para aumentar o tamanho do código substancialmente para um VLIW. Primeiro, a
geração de operações suficientes em um fragmento de código straight-line requer desdobramento de loops de ambiciosos (como nos exemplos anteriores), aumentando
assim o tamanho do código. Segundo, sempre que as instruções não forem cheias,
as unidades funcionais não usadas se traduzem em bits desperdiçados na codificação
de instrução. No Apêndice H, examinamos as técnicas de escalonamento de software,
como o pipelining de software, que podem alcançar os benefícios do desdobramento
sem muita expansão do código.
Para combater esse aumento no tamanho do código, às vezes são utilizadas codificações
inteligentes. Por exemplo, pode haver apenas um campo imediato grande para uso por
qualquer unidade funcional. Outra técnica é compactar as instruções na memória principal e expandi-las quando forem lidas para a cache ou quando forem decodificadas. No
Apêndice H, mostramos outras técnicas, além de documentar a significativa expansão do
código vista no IA-64.
FIGURA 3.16 Instruções VLIW que ocupam o loop interno e substituem a sequência desdobrada.
Esse código usa nove ciclos, considerando que não haja atraso de desvio; normalmente, o atraso de desvio também precisaria ser escalonado. A taxa de
despacho é de 23 operações em nove ciclos de clock ou 2,5 operações por ciclo. A eficiência, ou a porcentagem de slots disponíveis que continuam uma
operação, é de cerca de 60%. Para conseguir essa taxa de despacho, é preciso um número maior de registradores do que o MIPS normalmente usaria
nesse loop. A sequência de código VLIW acima requer pelo menos oito registradores FP, enquanto a mesma sequência de código para o processador MIPS
básico pode usar desde dois até cinco registradores de PF, quando desdobrada e escalonada.
169
170
CAPÍTULO 3 :
Paralelismo em nível de instrução e sua exploração
Os primeiros VLIWs operavam em bloqueio; não havia hardware de detecção de hazard
algum. Essa estrutura ditava que um stall em qualquer pipeline de unidade funcional
precisa fazer com que o processador inteiro pare e espere, pois todas as unidades
funcionais precisam ser mantidas em sincronismo. Embora um compilador possa
ser capaz de escalonar as unidades funcionais determinísticas para evitar os stalls, é
muito difícil prever quais acessos aos dados causarão um stall de cache e escaloná-los.
Logo, as caches precisavam de bloqueio, fazendo com que todas as unidades funcionais
protelassem. À medida que a taxa de despacho e o número de referências à memória
se tornava grande, essa restrição de sincronismo se tornou inaceitável. Em processadores mais recentes, as unidades funcionais operam de forma mais independente,
e o compilador é usado para evitar hazards no momento do despacho, enquanto as
verificações de hardware permitem a execução não sincronizada quando as instruções
são enviadas.
A compatibilidade do código binário também tem sido um problema logístico importante
para os VLIWs. Em uma técnica VLIW estrita, a sequência de código utiliza a definição do
conjunto de instruções e a estrutura de pipeline detalhada, incluindo as unidades funcionais e suas latências. Assim, diferentes quantidades de unidades funcionais e latências de
unidade exigem diferentes versões do código. Esse requisito torna a migração entre implementações sucessivas, ou entre implementações com diferentes larguras de despacho,
mais difícil do que para um projeto superescalar. Naturalmente, a obtenção de desempenho melhorado a partir de um novo projeto de superescalar pode exigir recompilação.
Apesar disso, a capacidade de executar arquivos binários antigos é uma vantagem prática
para uma técnica superescalar.
A técnica EPIC, da qual a arquitetura IA-64 é o principal exemplo, oferece soluções para
muitos dos problemas encontrados nos primeiros projetos VLIW, incluindo extensões
para uma especulação de software mais agressiva e métodos para contornar a limitação
da dependência do hardware enquanto preserva a compatibilidade binária.
O principal desafio para todos os processadores de múltiplo despacho é tentar explorar
grande quantidade de ILP. Quando o paralelismo vem do desdobramento de loops simples em programas de PF, provavelmente o loop original não foi executado de forma
eficiente em um processador vetorial (descrito no Capítulo 4). Não é claro que um processador de múltiplo despacho seja preferido em relação a um processador vetorial para tais
aplicações; os custos são semelhantes, e o processador vetorial normalmente tem a mesma
velocidade ou é mais rápido. As vantagens em potencial de um processador de múltiplo
despacho versus um processador vetorial são sua capacidade de extrair algum paralelismo
do código menos estruturado e sua capacidade de facilmente colocar em cache todas
as formas de dados. Por esses motivos, as técnicas de múltiplo despacho se tornaram o
método principal para tirar proveito do paralelismo em nível de instrução, e os vetores se
tornaram principalmente uma extensão desses processadores.
3.8 EXPLORANDO O ILP COM ESCALONAMENTO
DINÂMICO, MÚLTIPLO DESPACHO E ESPECULAÇÃO
Até aqui, vimos como funcionam os mecanismos individuais do escalonamento dinâmico, múltiplo despacho e especulação. Nesta seção, juntamos os três, o que gera uma
microarquitetura muito semelhante àquelas dos microprocessadores modernos. Para
simplificar, consideramos apenas uma taxa de despacho de duas instruções por clock,
mas os conceitos não são diferentes dos processadores modernos, que enviam três ou
mais instruções por clock.
3.8
Explorando o ILP com escalonamento dinâmico, múltiplo despacho e especulação
Vamos considerar que queremos estender o algoritmo de Tomasulo para dar suporte a
um pipeline superescalar de despacho duplo, com uma unidade de inteiros e de ponto
flutuante separadas, cada qual podendo iniciar uma operação a cada clock. Não queremos enviar instruções fora de ordem para as estações de reserva, pois isso levaria a
uma violação da semântica do programa. Para tirar proveito total do escalonamento
dinâmico, permitiremos que o pipeline envie qualquer combinação de duas instruções
em um clock, usando o hardware de escalonamento para realmente atribuir operações à
unidade de inteiros e à de ponto flutuante. Como a interação das instruções de inteiros
e de ponto flutuante é crucial, também estendemos o esquema de Tomasulo para lidar
com as unidades funcionais e os registradores de inteiros e de ponto flutuante, além de
incorporar a execução especulativa. Como a Figura 3.17 mostra, a organização básica é
similar àquela de um processador com especulação com um despacho por clock, exceto
pelo fato de que a lógica de despacho e conclusão deve ser melhorada para permitir
múltiplas instruções por clock.
Emitir instruções múltiplas por clock em um processador escalonado dinamicamente
(com ou sem especulação) é muito complexo pela simples razão de que as múltiplas
FIGURA 3.17 A organização básica de um processador de múltiplos despachos com especulação.
Neste caso, a organização poderia permitir uma multiplicação de PF, uma soma de PF, inteiros e load/store simultaneamente para todos os despachos
(supondo um despacho por clock por unidade funcional). Observe que diversos caminhos de dados devem ser alargados para suportar múltiplos
despachos: o CDB, os barramentos de operando e, essencialmente, a lógica de despacho de instrução, que não é mostrada nesta figura. O último é um
problema difícil, como discutiremos no texto.
171
172
CAPÍTULO 3 :
Paralelismo em nível de instrução e sua exploração
instruções podem depender umas das outras. Por isso, as tabelas devem ser atualizadas
para as instruções em paralelo. Caso contrário, ficarão incorretas ou a dependência
poderá ser perdida.
Duas técnicas diferentes foram usadas para enviar múltiplas instruções por clock em um
processador escalonado dinamicamente, e ambas contam com a observação de que a chave
é atribuir uma estação de reserva e atualizar as tabelas de controle de pipeline. Uma técnica
é executar essa etapa na metade de um ciclo de clock, de modo que duas instruções possam
ser processadas em um ciclo de clock. Infelizmente, essa técnica não pode ser estendida
facilmente para lidar com quatro instruções por clock.
Uma segunda alternativa é montar a lógica necessária para lidar com duas instruções
ao mesmo tempo, incluindo quaisquer dependências possíveis entre as instruções. Os
modernos processadores superescalares que enviam quatro ou mais instruções por clock
normalmente incluem ambas as técnicas: ambas utilizam uma lógica de despacho de
largura grande e com o pipeline. Uma observação importante é que não podemos eliminar
o problema com pipelining. Ao fazer com que os despachos de instrução levem múltiplos
clocks, porque novas instruções são enviadas a cada ciclo de clock, devemos ser capazes de
definir a estação de reserva e atualizar as tabelas de pipeline, de modo que uma instrução
dependente enviada no próximo clock possa usar as informações atualizadas.
Esse passo de despacho é um dos funis mais fundamentais em superscalares escalonados
dinamicamente. Para ilustrar a complexidade desse processo, a Figura 3.18 mostra a lógica
de despacho para um caso: enviar um load seguido por uma operação FP dependente. A
lógica se baseia na da Figura 3.14, na página 166, mas representa somente um caso. Em
um superescalar moderno, todas as combinações possíveis de instruções dependentes que
se podem enviar em um clock, o passo do despacho é um gargalo provável para tentativas
de ir além de quatro instruções por clock.
Podemos generalizar os detalhes da Figura 3.18 para descrever a estratégia básica para
atualizar a lógica de despacho e as tabelas de reserva em um superescalar escalonado
dinamicamente com até n despachos por clock como a seguir:
1. Definir uma estação de reserva e um buffer de reordenação para todas as instruções
que podem ser enviadas no próximo conjunto de despacho. Essa definição pode ser
feita antes que os tipos de instruções sejam conhecidos, simplesmente pré-alocando
as entradas do buffer de reordenação e garantindo que estejam disponíveis estações
de reserva o suficiente para enviar todo o conjunto, independentemente do que
ele contenha. Ao limitar o número de instruções de uma dada classe (digamos, um
FP, um inteiro, um load, um store), as estações de reserva necessárias podem ser
pré-alocadas. Se estações de reserva suficientes não estiverem disponíveis (como
quando as próximas instruções do programa são todas do mesmo tipo), o conjunto
será quebrado, e somente um subconjunto das instruções, na ordem do programa
original, será enviado. O restante das instruções no conjunto poderá ser colocado
no próximo conjunto para despacho em potencial.
2. Analisar todas as dependências entre as instruções no conjunto de despacho.
3. Se uma instrução no conjunto depender de uma instrução anterior no conjunto, use
o número do buffer de reorganização atribuído para atualizar a tabela de reserva
para a instrução dependente. Caso contrário, use a tabela de reserva existente e
a informação do buffer de reorganização para atualizar as entradas da tabela de
reservas para as instruções enviadas.
Obviamente, o que torna isso muito complicado é o fato de que tudo é feito em paralelo
em um único ciclo de clock!
3.8
Explorando o ILP com escalonamento dinâmico, múltiplo despacho e especulação
FIGURA 3.18 Passos de despacho para um par de instruções dependentes (chamadas 1 e 2), onde a instrução 1 é um load de PF e a
instrução 2 é uma operação de PF cujo primeiro operando é o resultado da instrução de load; r1 e r2 são as estações de reserva designadas
para as instruções; b1 e b2 são as entradas de buffer de reordenação designadas.
Para as instruções enviadas, rd1 e rd2 são os destinos, rs1, rs2 e rt2 são as fontes (o load tem somente uma fonte); r1 e r2 são as estações de reserva alocadas;
b1 e b são as entradas ROB designadas. RS é a estrutura de dados da estação de reserva. RegisterStat é a estrutura de dados de registrador, Regs representa os
registradores reais e ROB é a estrutura de dados do buffer de reorganização. Observe que precisamos ter entradas de buffer de reorganização designadas para
que essa lógica opere corretamente e lembrar que todas essas atualizações ocorrem em um único ciclo de clock em paralelo, não sequencialmente!
Na extremidade final do pipeline, devemos ser capazes de completar e emitir múltiplas
instruções por clock. Esses passos são um pouco mais fáceis do que os problemas de despacho, uma vez que múltiplas instruções que podem realmente ser emitidas no mesmo
ciclo de clock já devem ter sido tratadas e quaisquer dependências resolvidas. Como
veremos mais adiante, os projetistas descobriram como lidar com essa complexidade: o
Intel i7, que examinaremos na Seção 3.13, usa essencialmente o esquema que descrevemos
para múltiplos despachos especulativos, incluindo grande número de estações de reserva,
um buffer de reorganização e um buffer de load e store, que também é usado para tratar
faltas de cache sem bloqueio.
Do ponto de vista do desempenho, podemos mostrar como os conceitos se encaixam
com um exemplo.
173
174
CAPÍTULO 3 :
Paralelismo em nível de instrução e sua exploração
Exemplo
Resposta
Considere a execução do loop a seguir, que incrementa cada elemento de
um array inteiro, em um processador de duplo despacho, uma vez sem especulação e uma com especulação:
Suponha que existam unidades funcionais separadas de inteiros para cálculo
eficaz de endereço, para operações de ALU e para avaliação de condição de
desvios. Crie uma tabela para as três primeiras iterações desse loop para
os dois processadores. Suponha que até duas instruções de qualquer tipo
possam ser emitidas por clock.
As Figuras 3.19 e 3.20 mostram o desempenho para um processador escalonado dinamicamente com duplo despacho, sem e com especulação. Nesse caso,
onde um desvio pode ser um limitador crítico do desempenho, a especulação
ajuda significativamente. O terceiro desvio em que o processador é executado
no ciclo de clock 13, enquanto no pipeline não especulativo, ele é executado no
ciclo de clock 19. Já que a taxa de conclusão no pipeline não especulativo está
ficando rapidamente para trás da taxa de despacho, o pipeline não especulativo
vai sofrer stall quando algumas poucas iterações a mais forem enviadas. O desempenho do processador não especulativo poderia ser melhorado permitindo
que as instruções load completassem o cálculo do endereço efetivo antes de
um desvio ser decidido, mas sem que sejam permitidos acessos especulativos
à memória, essa melhoria vai ganhar somente um clock por interação.
FIGURA 3.19 Tempo de despacho, execução e escrita de resultado para uma versão de despacho duplo do nosso pipeline sem especulação.
Observe que o LD seguido pelo BNE não pode iniciar a execução antes, pois precisa esperar até que o resultado do desvio seja determinado. Esse tipo
de programa, com desvios dependentes de dados que não podem ser resolvidos anteriormente, mostra a força da especulação. Unidades funcionais
separadas para o cálculo do endereço, operações com a ALU e avaliação da condição de desvio permitem que várias instruções sejam executadas no
mesmo ciclo. A Figura 3.20 mostra esse exemplo com especulação.
3.9
Técnicas avançadas para o despacho de instruções e especulação
FIGURA 3.20 Tempo de despacho, execução e escrita de resultado para uma versão de despacho duplo de nossos pipelines com especulação.
Observe que o LD seguido do BNE pode iniciar a execução mais cedo, pois é especulativo.
Esse exemplo mostra claramente como a especulação pode ser vantajosa quando existem
desvios dependentes dos dados, que de outro modo limitariam o desempenho. Entretanto, essa vantagem depende da previsão precisa de desvios. A especulação incorreta não
melhora o desempenho. Na verdade, geralmente ela prejudica o desempenho e, como
veremos, diminui drasticamente a eficiência energética.
3.9 TÉCNICAS AVANÇADAS PARA O DESPACHO
DE INSTRUÇÕES E ESPECULAÇÃO
Em um pipeline de alto desempenho, especialmente um com múltiplo despacho, prever
bem os desvios não é suficiente; na realidade, temos de ser capazes de entregar um fluxo
de instrução com uma grande largura de banda. Nos processadores recentes de múltiplo
despacho, isso significa resolver 4-8 instruções a cada ciclo de clock. Primeiro veremos os
métodos para aumentar a largura de banda de despacho de instrução. Depois, passaremos
a um conjunto de questões fundamentais na implementação de técnicas avançadas de
especulação, incluindo o uso de renomeação de registrador versus buffers de reordenação,
a agressividade da especulação e uma técnica chamada previsão de valor, que poderia
melhorar ainda mais o ILP.
Aumentando a largura de banda da busca de instruções
(instruction fetch)
Um processador de múltiplo despacho exigirá que o número médio de instruções buscadas a cada ciclo de clock seja pelo menos do mesmo tamanho do throughput médio.
Naturalmente, buscar essas instruções exige caminhos largos o suficiente para a cache
175
176
CAPÍTULO 3 :
Paralelismo em nível de instrução e sua exploração
da instrução, mas o aspecto mais difícil é lidar com desvios. Nesta seção, veremos dois
métodos para lidar com desvios e depois discutiremos como os processadores modernos
integram as funções de previsão e pré-fetch de instrução.
Buffers de destino de desvio
Para reduzir a penalidade do desvio para o nosso pipeline simples de cinco estágios, além
dos pipelines mais profundos, temos de saber se a instrução ainda não decodificada é um
desvio e, se for, qual deverá ser o próximo PC. Se a instrução for um desvio e soubermos
qual deve ser o próximo PC, podemos ter uma penalidade de desvio de zero. Uma cache
de previsão de desvio que armazena o endereço previsto para a próxima instrução após um
desvio é chamada de buffer de destino de desvio ou cache de destino de desvio. A Figura 3.21
mostra um buffer de destino de desvio.
Como o buffer de destino de desvio prevê o endereço da próxima instrução e o envia antes
de decodificar a instrução, precisamos saber se a instrução lida é prevista como um desvio
tomado. Se o PC da instrução lida combinar com um PC no buffer de instrução, o PC
previsto correspondente será usado como próximo PC. O hardware para esse buffer de
destino de desvio é essencialmente idêntico ao hardware para a cache.
Se uma entrada correspondente for encontrada no buffer de destino de desvio, a busca
começará imediatamente no PC previsto. Observe que, diferentemente de um buffer
de previsão de desvio, a entrada da previsão precisa corresponder a essa instrução, pois
o PC previsto será enviado antes de se saber sequer se essa instrução é um desvio. Se o
FIGURA 3.21 Um buffer de destino de desvio.
O PC da instrução sendo lida é combinado com um conjunto de endereços de instrução armazenados na primeira
coluna; estes representam os endereços de desvios conhecidos. Se o PC for correspondente a uma dessas entradas,
a instrução sendo lida será um desvio tomado, e o segundo campo, o PC previsto, conterá a previsão para o próximo
PC após o desvio. A busca começa imediatamente nesse endereço. O terceiro campo, que é opcional, pode ser usado
para os bits extras de status de previsão.
3.9
Técnicas avançadas para o despacho de instruções e especulação
processador não verificasse se a entrada corresponde a esse PC, o PC errado seria enviado
para instruções que não fossem desvios, resultando em um processador mais lento. Só
precisamos armazenar os desvios tomados previstos no buffer de destino de desvio, pois
um desvio não tomado deve simplesmente apanhar a próxima instrução sequencial como
se não fosse um desvio.
A Figura 3.22 mostra as etapas detalhadas quando se usa um buffer de destino de desvio para um pipeline simples de cinco estágios. A partir disso, podemos ver que não
haverá atraso de desvio se uma entrada de previsão de desvio for encontrada no buffer
e a previsão estiver correta. Caso contrário, haverá uma penalidade de pelo menos dois
ciclos de clock. Lidar com erros de previsão e faltas é um desafio significativo, pois
normalmente teremos de interromper a busca da instrução enquanto reescrevemos a
entrada do buffer. Assim, gostaríamos de tornar esse processo rápido para minimizar
a penalidade.
Para avaliar se um buffer de destino de desvio funciona bem, primeiro temos de determinar
as penalidades em todos os casos possíveis. A 3 contém essa informação para o pipeline
simples de cinco estágios.
FIGURA 3.22 Etapas envolvidas no tratamento de uma instrução com um buffer de destino de desvio.
177
178
CAPÍTULO 3 :
Paralelismo em nível de instrução e sua exploração
Exemplo
Resposta
Determine a penalidade de desvio total para um buffer de destino de desvio,
considerando os ciclos de penalidade para os erros de previsão individuais
da Figura 3.23. Faça as seguintes suposições sobre a precisão de previsão
e taxa de acerto:
A precisão de previsão é de 90% (para instruções no buffer).
A taxa de acerto no buffer é de 90% (para desvios previstos tomados).
Calculamos a penalidade verificando a probabilidade de dois eventos: o desvio tem previsão de ser tomado, mas acaba não sendo tomado, e o desvio é
tomado mas não é encontrado no buffer. Ambos carregam uma penalidade
de dois ciclos.
Probabilidade(desviono buffer,masnão tomado realmente) = Percentual de taxa deacerto do buffer
× Porcentagem de previsõesincorretas
= 90% × 10% = 0,09
Probabilidade(desvionão no buffer,mastomado) = 10%
Penalidadededesvio = (0,09 + 0,10) × 2
Penalidadededesvio = 0,38
Essa penalidade se compara com uma penalidade de desvio para os desvios
adiados, que avaliaremos no Apêndice C, de cerca de meio ciclo de clock
por desvio. Lembre-se, no entanto, de que a melhoria da previsão de desvio
dinâmico crescerá à medida que crescer o tamanho do pipeline, portanto, o
atraso do desvio; além disso, previsores melhores gerarão uma vantagem
de desempenho maior. Os processadores modernos de alto desempenho
apresentam atrasos de previsão incorreta de desvio da ordem de 15 ciclos
de clock. Obviamente, uma previsão precisa é essencial!
Uma variação no buffer de destino de desvio é armazenar uma ou mais instruções-alvo no
lugar do endereço-alvo previsto ou adiconalmente a ele. Essa variação possui duas vantagens
em potencial: 1) ela permite que o acesso ao buffer de desvio leve mais tempo do que
o tempo entre buscas sucessivas de instruções, possivelmente permitindo um buffer de
destino de desvio maior; 2) o armazenamento das instruções-alvo reais em buffer permite
que realizemos uma otimização chamada branch folding. O branch folding pode ser usado
para obter desvios incondicionais de 0 ciclo, e às vezes os desvios condicionais de 0 ciclo.
Considere um buffer de destino de desvio que coloca instruções no buffer a partir do
caminho previsto e está sendo acessado com o endereço de um desvio incondicional. A
única função do desvio incondicional é alterar o PC. Assim, quando o buffer de destino
de desvio sinaliza um acerto e indica que o desvio é incondicional, o pipeline pode simplesmente substituir a instrução do buffer de destino de desvio no lugar da instrução que
FIGURA 3.23 Penalidades para todas as combinações possíveis de que o desvio está no buffer e o que ele
realmente faz supondo que armazenemos apenas os desvios tomados no buffer.
Não existe penalidade de desvio se tudo for previsto corretamente e o desvio for encontrado no buffer de desvio.
Se o desvio não for previsto corretamente, a penalidade será igual a um ciclo de clock para atualizar o buffer com a
informação correta (durante o que uma instrução não poderá ser apanhada) e um ciclo de clock, se for preciso, para
reiniciar a busca da próxima instrução correta para o desvio. Se o desvio não for encontrado e tomado, ocorrerá uma
penalidade de dois ciclos enquanto o buffer for atualizado.
3.9
Técnicas avançadas para o despacho de instruções e especulação
é retornada da cache (que é o desvio incondicional). Se o processador estiver enviando
múltiplas instruções por ciclo, o buffer precisará fornecer múltiplas instruções para obter o
máximo de benefício. Em alguns casos, talvez seja possível eliminar o custo de um desvio.
Previsões de endereço de retorno
Ao tentarmos aumentar a oportunidade e a precisão da especulação, encararemos o desafio de
prever saltos indiretos, ou seja, saltos cujo endereço de destino varia durante a execução. Embora os programas em linguagem de alto nível gerem esses saltos para chamadas de procedimento
indiretas, instruções select, case e os gotos do FORTRAN, a grande maioria dos saltos indiretos
vem de retornos de procedimento. Por exemplo, para os benchmarks SPEC95, esses retornos
são responsáveis por mais de 15% dos desvios e pela grande maioria dos saltos indiretos na
média. Para linguagens orientadas a objeto, como C + + e Java, os retornos de procedimento
são ainda mais frequentes. Assim, focar nos retornos de procedimento parece apropriado.
Embora os retornos de procedimento possam ser previstos com um buffer de destino de desvio,
a exatidão dessa técnica de previsão pode ser baixa se o procedimento for chamado de vários
locais e as chamadas de um local não forem agrupadas no tempo. Por exemplo, no SPEC
CPU95, um previsor de desvio agressivo consegue uma precisão de menos de 60% para tais
desvios de retorno. Para contornar esse problema, alguns projetos usam um pequeno buffer de
endereços de retorno operando como uma pilha. Essa estrutura coloca em cache os endereços
de retorno mais recentes: colocando um endereço de retorno na pilha em uma chamada e
retirando-o em um retorno. Se a cache for suficientemente grande (ou seja, do mesmo tamanho
da profundidade máxima de chamada), vai prever os retornos perfeitamente. A Figura 3.24
FIGURA 3.24 Exatidão da previsão para um buffer de endereço de retorno operado como pilha em uma
série de benchmarks SPEC CPU95.
A precisão é a fração dos endereços de retorno previstos corretamente. Um buffer de 0 entrada implica que a
previsão-padrão de desvio seja utilizada. Como as profundidades de chamadas normalmente são muito grandes, com
algumas exceções, um buffer modesto funciona bem. Esse dado vem de Skadron et al. (1999) e utiliza um mecanismo
de reparo para impedir a adulteração dos endereços de retorno em cache.
179
180
CAPÍTULO 3 :
Paralelismo em nível de instrução e sua exploração
mostra o desempenho de um buffer de retorno desse tipo com 0-16 elementos para uma série de
benchmarks SPEC CPU95. Usaremos um previsor de retorno semelhante quando examinarmos
os estudos do ILP na Seção 3.10. Tanto os processadores Intel Core quanto os processadores
AMD Phenom têm previsores de endereço de retorno.
Unidades integradas de busca de instrução
Para atender as demandas dos processadores de múltiplo despacho, muitos projetistas
recentes escolheram implementar uma unidade integrada de busca de instrução, como
uma unidade autônoma separada, que alimenta instruções para o restante do pipeline.
Basicamente, isso significa reconhecer que não é mais válido caracterizar a busca de instruções como um único estágio da pipe, dadas as complexidades do múltiplo despacho.
Em vez disso, os projetos recentes usaram uma unidade integrada de busca de instrução
que abrange diversas funções:
1. Previsão integrada de desvio. O previsor de desvio torna-se parte da unidade de busca
de instrução e está constantemente prevendo desvios, de modo a controlar a busca
no pipeline.
2. Pré-busca de instrução. Para oferecer múltiplas instruções por clock, a unidade de
busca de instrução provavelmente precisará fazer a busca antecipadamente. A
unidade controla autonomamente a pré-busca de instruções (ver uma discussão das
técnicas para fazer isso no Capítulo 2), integrando com a previsão de desvio.
3. Acesso à memória de instruções e armazenamento em buffer. Ao carregar múltiplas
instruções por ciclo, diversas complexidades são encontradas, incluindo a
dificuldade de que a busca de múltiplas instruções pode exigir o acesso a múltiplas
linhas de cache. A unidade de busca de instrução contorna essa complexidade,
usando a pré-busca para tentar esconder o custo de atravessar blocos de cache.
A unidade de busca de instrução também oferece o uso de buffer, basicamente
atuando como uma unidade sob demanda, para oferecer instruções ao estágio de
despacho conforme a necessidade e na quantidade necessária.
Hoje, quase todos os processadores sofisticados usam uma unidade de busca de instrução
separada conectada ao resto do pipeline por um buffer contendo instruções pendentes.
Especulação: problemas de implementação e extensões
Nesta seção, exploraremos três ideias que envolvem a implementação da especulação,
começando com o uso da renomeação de registradores, a técnica que substituiu quase
totalmente o uso de um buffer de reordenação. Depois, discutiremos uma extensão possível importante para a especificação no fluxo de controle: uma ideia chamada previsão
de valor.
Suporte à especulação: renomeação de registrador versus
buffers de reordenação
Uma alternativa ao uso de um buffer de reordenação (ROB) é o uso explícito de um
conjunto físico maior de registradores, combinado com a renomeação de registradores.
Essa técnica se baseia no conceito de renomeação usado no algoritmo de Tomasulo e o
estende. No algoritmo de Tomasulo, os valores dos registradores arquitetonicamente visíveis
a arquitetura (R0, ..., R31 e F0, ..., F31) estão contidos, em qualquer ponto na execução,
em alguma combinação do conjunto de registradores e a estações de reserva. Com o acréscimo da especulação, os valores de registrador também podem residir temporariamente
no ROB. De qualquer forma, se o processador não enviar novas instruções por um período
de tempo, todas as instruções existentes serão confirmadas, e os valores dos registradores
3.9
Técnicas avançadas para o despacho de instruções e especulação
aparecerão no banco de registradores, que corresponde diretamente aos registradores
visíveis arquitetonicamente.
Na técnica de renomeação de registrador, um conjunto estendido de registradores físicos
é usado para manter os registradores arquitetonicamente visíveis e também valores temporários. Assim, os registradores estendidos substituem a função do ROB e das estações de
reserva. Durante o despacho de instrução, um processo de renomeação mapeia os nomes
dos registradores da arquitetura para os números dos registradores físicos no conjunto de
registradores estendido, alocando um novo registrador não usado para o destino. Hazards
WAW e WAR são evitados renomeando-se o registrador de destino, e a recuperação da especulação é tratada, porque um registrador físico, mantido como um destino de instrução
não se torna o registrador da arquitetura até que a instrução seja confirmada. O mapa de
renomeação é uma estrutura de dados simples que fornece o número de registrador físico
do registrador correspondente ao registrador da arquitetura especificado. Essa estrutura é
semelhante em estrutura e função à tabela de status de registrador no algoritmo de Tomasulo. Quando uma instrução é confirmada, a tabela restante é atualizada permanentemente
para indicar que um registrador físico corresponde ao registrador de arquitetura real,
finalizando efetivamente a atualização ao status do processador. Embora um ROB não
seja necessário com a renomeação de registrador, o hardware deve rastrear instruções em
uma estrutura similar à de uma fila e atualizar a tabela de renomeação em ordem estrita.
Uma vantagem da técnica de renomeação versus a técnica ROB é que a confirmação de instrução é simplificada, pois exige apenas duas ações simples: 1) registrar que o mapeamento
entre um número de registrador da arquitetura e o número do registrador físico não é mais
especulativo e 2) liberar quaisquer registradores físicos sendo usados para manter o valor
“mais antigo” do registrador da arquitetura. Em um projeto com estações de reserva, uma
estação é liberada quando a instrução que a utiliza termina a execução, e uma entrada
ROB é liberada quando a instrução correspondente é confirmada.
Com a renomeação de registrador, a desalocação de registradores é mais complexa, pois,
antes de liberarmos um registrador físico, temos de saber se ele não corresponde mais a
um registrador da arquitetura e se nenhum outro uso do registrador físico está pendente.
Um registrador físico corresponde a um registrador da arquitetura até que o registrador
da arquitetura seja reescrito, fazendo com que a tabela de renomeação aponte para outro
lugar. Ou seja, se nenhuma entrada restante apontar para determinado registrador físico,
ela não corresponderá mais a um registrador da arquitetura. Porém, ainda poderá haver
usos pendentes do registrador físico. O processador poderá determinar se esse é o caso
examinando os especificadores de registrador de origem de todas as instruções nas filas
da unidade funcional. Se determinado registrador físico não aparecer como origem e não
for designado como registrador da arquitetura, ele poderá ser reclamado e realocado.
Como alternativa, o processador pode simplesmente esperar até que seja confirmada outra
instrução que escreva no mesmo registrador da arquitetura. Nesse ponto, pode não haver
mais usos para o valor pendente antigo. Embora esse método possa amarrar um registrador físico por um pouco mais de tempo do que o necessário, ele é fácil de implementar
e, portanto, é usado em vários superescalares recentes.
Uma pergunta que você pode estar fazendo é: “Como sabemos quais registradores são de
arquitetura se eles estão constantemente mudando?” Na maior parte do tempo em que um
programa está executando, isso não importa. Porém, existem casos em que outro processo,
como o sistema operacional, precisa ser capaz de saber exatamente onde reside o conteúdo
de certo registrador de arquitetura. Para entender como essa capacidade é fornecida,
considere que o processador não envia instruções por algum período de tempo. Por fim,
181
182
CAPÍTULO 3 :
Paralelismo em nível de instrução e sua exploração
todas as instruções no pipeline serão confirmadas, e o mapeamento entre os registradores
arquitetonicamente visíveis e os registradores físicos se tornará estável. Nesse ponto, um
subconjunto dos registradores físicos contém os registradores arquitetonicamente visíveis,
e o valor de qualquer registrador físico não associado a um registrador de arquitetura é desnecessário. Então, é fácil mover os registradores de arquitetura para um subconjunto fixo
de registradores físicos, de modo que os valores possam ser comunicados a outro processo.
Tanto a renomeação de registrador quanto os buffers de reorganização continuam a ser
usados em processadores sofisticados, que hoje podem ter em ação até 40-50 instruções
(incluindo loads e stores aguardando na cache). Seja usando a renomeação, seja usando
um buffer de reorganização, o principal gargalo para a complexidade de um superescalar escalonado dinamicamente continua sendo o despacho de conjuntos de instruções
com dependências dentro do conjunto. Em particular, instruções dependentes em um
conjunto de despacho devem ser enviadas com os registradores virtuais designados das
instruções das quais eles dependem. Uma estratégia para despacho de instrução com
renomeação de registrador similar ao usado para múltiplo despacho com buffers de
reorganização (página 157) pode ser empregada como a seguir:
1. A lógica de despacho pré-reserva registradores físicos suficientes para todo o
conjunto de despachos (digamos, quatro registradores para um conjunto de quatro
instruções com, no máximo, um resultado de registrador por instrução).
2. A lógica de despacho determina quais dependências existem dentro do conjunto. Se
não existir nenhuma dependência dentro do conjunto, a estrutura de renomeação
de registrador será usada para determinar o registrador físico que contém, ou vai
conter, o resultado do qual a instrução depende. Quando não existe nenhuma
dependência dentro do conjunto, o resultado é um conjunto de despachos anterior,
e a tabela de renomeação de registrador terá o número de registrador correto.
3. Se uma instrução depender de uma instrução anterior no conjunto, o registrador
físico pré-reservado no qual o resultado será colocado será usado para atualizar a
informação para a instrução que está enviando.
Observe que, assim como no caso do buffer de reorganização, a lógica de despacho deve
determinar as dependências dentro do conjunto e atualizar as tabelas de renomeação em
um único clock e, como antes, a complexidade de fazer isso para grande número de instruções por clock torna-se uma grande limitação na largura do despacho.
Quanto especular
Uma das vantagens significativas da especulação é a sua capacidade de desvendar eventos
que, de outra forma, fariam com que o pipeline ficasse em stall mais cedo, como as falhas de cache. Porém, essa vantagem em potencial possui uma significativa desvantagem
em potencial. A especulação não é gratuita: ela gasta tempo e energia, e a recuperação da
especulação incorreta reduz ainda mais o desempenho. Além disso, para dar suporte à
taxa mais alta de execução de instrução, necessária para se tirar proveito da especulação,
o processador precisa ter recursos adicionais, que exigem área de silício e energia. Finalmente, se a especulação levar a um evento excepcional, como a falhas de cache ou TLB,
o potencial para uma perda significativa de desempenho aumentará se esse evento não
tiver ocorrido sem especulação.
Para manter a maior parte da vantagem enquanto minimiza as desvantagens, a maioria
dos pipelines com especulação só permite que eventos excepcionais de baixo custo (como
uma falha de cache de primeiro nível) sejam tratados no modo especulativo. Se houver
um evento excepcional dispendioso, como falha de cache de segundo nível ou falha do
buffer de TLB, o processador vai esperar até que a instrução que causa o evento deixe de ser
3.9
Técnicas avançadas para o despacho de instruções e especulação
especulativa, antes de tratar dele. Embora isso possa degradar ligeiramente o desempenho
de alguns programas, evita perdas de desempenho significativa em outros, especialmente
naqueles que sofrem com a alta frequência de tais eventos, acoplado com uma previsão
de desvio menos que excelente.
Na década de 1990, as desvantagens em potencial da especulação eram menos óbvias. Com
a evolução dos processadores, os custos reais da especulação se tornaram mais aparentes, e
as limitações do despacho mais amplo e a especulação se tornaram óbvias. Retomaremos
essa questão em breve.
Especulação por desvios múltiplos
Nos exemplos que consideramos neste capítulo, tem sido possível resolver um desvio
antes de ter de especular outro. Três situações diferentes podem beneficiar-se com a especulação em desvios múltiplos simultaneamente: 1) frequência de desvio muito alta; 2)
agrupamento significativo de desvios; e 3) longos atrasos nas unidades funcionais. Nos dois
primeiros casos, conseguir um desempenho alto pode significar que múltiplos desvios são
especulados e até mesmo tratar de mais de um desvio por clock. Os programas de banco
de dados, e outras computações com inteiros menos estruturadas, geralmente exigem essas
propriedades, tornando a especulação em desvios múltiplos mais importante. De modo
semelhante, longos atrasos nas unidades funcionais podem aumentar a importância da
especulação em desvios múltiplos como um meio de evitar stalls a partir de atrasos de
pipeline mais longos.
A especulação em desvios múltiplos complica um pouco o processo de recuperação da
especulação, mas é simples em outros aspectos. Em 2011, nenhum processador havia
combinado especulação completa com a resolução de múltiplos desvios por ciclo, e é
improvável que os custos de fazer isso fossem justificados em termos de desempenho
versus complexidade e energia.
Especulação e o desafio da eficiência energética
Qual é o impacto da especulação sobre a eficiência energética? À primeira vista, pode-se
argumentar que usar a especulação sempre diminui a eficiência energética, já que sempre
que a especulação está errada ela consome energia em excesso de dois modos:
1. As instruções que foram especuladas e aquelas cujos resultados não foram
necessários geraram excesso de trabalho para o processador, desperdiçando energia.
2. Desfazer a especulação e restaurar o status do processador para continuar a execução
no endereço apropriado consome energia adicional que não seria necessária sem
especulação.
Certamente, a especulação vai aumentar o consumo de energia e, se pudermos controlar a
especulação, será possível medir o custo (ou pelo menos o custo da potência dinâmica).
Mas, se a especulação diminuir o tempo de execução mais do que aumentar o consumo
médio de energia, a energia total consumida pode ser menor.
Assim, para entender o impacto da especulação sobre a eficiência energética, precisamos
examinar com que frequência a especulação leva a um trabalho desnecessário. Se número
significativo de instruções desnecessárias for executado, é improvável que a especulação
melhore em comparação com o tempo de execução! A Figura 3.25 mostra a fração de
instruções executadas a partir da especulação incorreta. Como podemos ver, essa fração
é pequena em códigos científicos e significativa (cerca de 30% em média) em códigos
inteiros. Assim, é improvável que a especulação seja eficiente em termos de energia para
aplicações de números inteiros. Os projetistas devem evitar a especulação, tentar reduzir
183
184
CAPÍTULO 3 :
Paralelismo em nível de instrução e sua exploração
FIGURA 3.25 Fração de instruções que são executadas como resultado de especulação incorreta para
programas inteiros (os cinco primeiros) em comparação a programas de PF (os cinco últimos).
a especulação incorreta ou pensar em novas abordagens, como somente especular em
desvios altamente previsíveis.
Previsão de valor
Uma técnica para aumentar a quantidade de ILP disponível em um programa é a previsão
de valor. A previsão de valor tenta prever o valor que será produzido por uma instrução.
Obviamente, como a maioria das instruções produz um valor diferente toda vez que é
executada (ou, pelo menos, um valor diferente a partir de um conjunto de valores), a
previsão de valor só pode ter sucesso limitado. Portanto, existem certas instruções para
as quais é mais fácil prever o valor resultante — por exemplo, loads que carregam de um
pool constante ou que carregam um valor que muda com pouca frequência. Além disso,
quando uma instrução produz um valor escolhido a partir de um pequeno conjunto de
valores em potencial, é possível prever o valor resultante correlacionando-o com outros
comportamentos do programa.
A previsão de valor é útil quando aumenta significativamente a quantidade de ILP disponível. Isso é mais provável quando um valor é usado como origem de uma cadeia de
computações dependentes, como em um load. Como a previsão de valor é usada para
melhorar especulações e a especulação incorreta tem impacto prejudicial no desempenho,
a exatidão da previsão é essencial.
Embora muitos pesquisadores tenham se concentrado na previsão de valor nos últimos
10 anos, os resultados nunca foram atraentes o suficiente para justificar sua incorporação
em processadores reais. Em vez disso, uma ideia simples e mais antiga, relacionada com a
previsão de valor, tem sido adotada: a previsão de aliasing de endereço. Previsão de aliasing
de endereço é uma técnica simples que prevê se dois stores ou um load e um store se referem
ao mesmo endereço de memória. Se duas referências desse tipo não se referirem ao mesmo
endereço, elas poderão ser seguramente trocadas. Caso contrário, teremos de esperar até
que sejam conhecidos os endereços de memória acessados pelas instruções. Como não
precisamos realmente prever os valores de endereço somente se tais valores entram em
3.10
Estudos das limitações do ILP
conflito, a previsão é mais estável e mais simples. Essa forma limitada de especulação de
valor de endereço tem sido usada em diversos processadores e pode tornar-se universal
no futuro.
3.10
ESTUDOS DAS LIMITAÇÕES DO ILP
A exploração do ILP para aumentar o desempenho começou com os primeiros processadores com pipeline na década de 1960. Nos anos 1980 e 1990, essas técnicas foram
fundamentais para conseguir rápidas melhorias de desempenho. A questão de quanto ILP
existe foi decisiva para a nossa capacidade de aumentar, a longo prazo, o desempenho
a uma taxa que ultrapassasse o aumento na velocidade da tecnologia básica do circuito
integrado. Em uma escala mais curta, a questão crítica do que é necessário para explorar
mais ILP é crucial para os projetistas de computadores e de compiladores. Os dados
apresentados nesta seção também nos oferecem um modo de examinar o valor das ideias
que introduzimos no capítulo anterior, incluindo a falta de ambiguidade de memória,
renomeação de registrador e especulação.
Nesta seção, vamos rever um dos estudos feitos sobre essas questões (baseados no estudo
de Wall, em 1993). Todos esses estudos do paralelismo disponível operam fazendo um
conjunto de suposições e vendo quanto paralelismo está disponível sob essas suposições. Os
dados que examinamos aqui são de um estudo que faz o mínimo de suposições; na verdade,
é provável que o modelo de hardware definitivo não seja realizável. Apesar disso, todos esses
estudos consideram certo nível de tecnologia de compilador, e algumas dessas suposições
poderiam afetar os resultados, apesar do uso de um hardware incrivelmente ambicioso.
Como veremos, para modelos de hardware que tenham custo razoável, é improvável que
os custos de uma especulação muito agressiva possam ser justificados. As ineficiências
energéticas e o uso de silício são simplesmente muito altos. Enquanto muitos na comunidade de pesquisa e os principais fabricantes de processadores estavam apostando em
maior exploração do ILP, e foram inicialmente relutantes em aceitar essa possibilidade,
em 2005 eles foram forçados a mudar de ideia.
O modelo de hardware
Para ver quais poderiam ser os limites do ILP, primeiro precisamos definir um processador
ideal. Um processador ideal é aquele do qual são removidas todas as restrições sobre o
ILP. Os únicos limites sobre o ILP em tal processador são aqueles impostos pelos fluxos
de dados reais, pelos registradores ou pela memória.
As suposições feitas para um processador ideal ou perfeito são as seguintes:
1. Renomeação infinita de registrador. Existe um número infinito de registradores virtuais
à disposição e, por isso, todos os hazards WAW e WAR são evitados e um número
ilimitado de instruções pode iniciar a execução simultaneamente.
2. Previsão perfeita de desvio. A previsão de desvio é perfeita. Todos os desvios
condicionais são previstos com exatidão.
3. Previsão perfeita de salto. Todos os saltos (incluindo o salto por registrador, usado
para retorno e saltos calculados) são perfeitamente previstos. Quando combinado
com a previsão de desvio perfeita, isso é equivalente a ter um processador com
especulação perfeita e um buffer ilimitado de instruções disponíveis para execução.
4. Análise perfeita de alias de endereço de memória. Todos os endereços de memória são
conhecidos exatamente, e um load pode ser movido antes de um store, desde que os
endereços não sejam idênticos. Observe que isso implementa a análise de alias de
endereço perfeita.
185
186
CAPÍTULO 3 :
Paralelismo em nível de instrução e sua exploração
5. Caches perfeitas. Todos os endereços de memória utilizam um ciclo de clock.
Na prática, os processadores superescalares normalmente consumirão grande
quantidade de ILP ocultando falhas de cache, tornando esses resultados altamente
otimistas.
As suposições 2 e 3 eliminam todas as dependências de controle. De modo semelhante,
as suposições 1 e 4 eliminam todas menos as dependências de dados verdadeiras. Juntas, essas
quatro suposições significam que qualquer instrução na execução do programa pode ser
escalonada no ciclo imediatamente após a execução da predecessora da qual depende.
É possível ainda, sob essas suposições, que a última instrução executada dinamicamente
no programa seja sempre escalonada no primeiro ciclo de clock! Assim, esse conjunto
de suposições substitui a especulação de controle e endereço e as implementa como se
fossem perfeitas.
Inicialmente, examinamos um processador que pode enviar um número ilimitado de instruções ao mesmo tempo, olhando arbitrariamente para o futuro da computação. Para
todos os modelos de processador que examinamos, não existem restrições sobre quais
tipos de instruções podem ser executadas em um ciclo. Para o caso de despacho ilimitado,
isso significa que pode haver um número ilimitado de loads ou stores enviados em um
ciclo de clock. Além disso, todas as latências de unidade funcional são consideradas como
tendo um ciclo, de modo que qualquer sequência de instruções dependentes pode ser
enviada em ciclos sucessivos. As latências maiores do que um ciclo diminuiriam o número
de despachos por ciclo, embora não o número de instruções em execução em qualquer
ponto (as instruções em execução em qualquer ponto normalmente são chamadas de
instruções in flight).
É evidente que esse processador está às margens do irrealizável. Por exemplo, o IBM
Power7 (ver Wendell et al., 2010) é o processador superescalar mais avançado anunciado
até o momento. O Power7 envia até seis instruções por clock e inicia a execução em
até 8-12 unidades de execução (somente duas das quais são unidades de load/store),
suporta um grande conjunto de registradores renomeados (permitindo centenas de
instruções no ato), usa um previsor de desvio grande e agressivo, e emprega a desambiguação de memória dinâmica. O Power7 continuou o movimento rumo ao uso de mais
paralelismo no nível de thread, aumentando a largura do suporte para multithreading
simultâneo (SMT) para quatro threads por núcleo e o número de núcleos por chip
para oito. Depois de examinar o paralelismo disponível para o processador perfeito,
examinaremos o que pode ser alcançado em qualquer processador que seja projetado
em futuro próximo.
Para medir o paralelismo disponível, um conjunto de programas foi compilado e otimizado com os compiladores de otimização MIPS padrão. Os programas foram instrumentados e executados para produzir um rastro das referências de instruções e dados.
Cada uma dessas instruções é então escalonada o mais cedo possível, limitada apenas
pelas dependências de dados. Como um rastro é usado, a previsão de desvio perfeita e a
análise de alias perfeita são fáceis de fazer. Com esses mecanismos, as instruções podem
ser escalonadas muito mais cedo do que poderiam ser de outra forma, movendo grande
quantidade de instruções, cujos dados não são dependentes, incluindo desvios, que são
perfeitamente previstos.
A Figura 3.26 mostra a quantidade média de paralelismo disponível para seis dos benchmarks SPEC92. Por toda esta seção, o paralelismo é medido pela taxa de despacho de
instrução média. Lembre-se de que todas as instruções possuem uma latência de um
ciclo; uma latência maior reduziria o número médio de instruções por clock. Três desses
3.10
Estudos das limitações do ILP
FIGURA 3.26 ILP disponível em um processador perfeito para seis dos benchmarks SPEC92.
Os três primeiros programas são programas de inteiros, o os últimos três são programas de ponto flutuante.
Os programas de ponto flutuante são intensos em termos de loop e têm grande quantidade de paralelismo em nível
de loop.
benchmarks (fpppp, doduc e tomcatv) utilizam intensamente números de ponto flutuante,
e os outros três são programas para inteiros. Dois dos benchmarks de ponto flutuante
(fpppp e tomcatv) possuem paralelismo extensivo, que poderia ser explorado por um
computador vetorial ou por um multiprocessador (porém, a estrutura do fpppp é muito
confusa, pois algumas transformações foram feitas manualmente no código). O programa
doduc possui extenso paralelismo, mas esse paralelismo não ocorre em loops paralelos
simples, assim como no fpppp e tomcatv. O programa li é um interpretador LISP que
possui dependências muito curtas.
Limitações do ILP para processadores realizáveis
Nesta seção examinaremos o desempenho de processadores com níveis ambiciosos de
suporte ao hardware igual ou melhor que o disponível em 2011 ou dados os eventos e
lições da última década, que provavelmente estarão disponíveis num futuro próximo. Em
particular, supomos os seguintes atributos fixos:
1. Até 64 despachos de instrução por clock sem restrições de despacho ou cerca
de 10 vezes a amplitude de despacho total do maior processador em 2011.
Conforme examinaremos mais adiante, as implicações práticas de grandes
amplitudes de despacho na frequência do clock, complexidade lógica e potência
podem ser a limitação mais importante da exploração do ILP.
2. Um previsor de torneio com 1 K entradas e um previsor de retorno de 16 entradas.
Esse previsor é bastante comparável aos melhores previsores em 2011; o previsor
não é um gargalo importante.
3. A desambiguidade perfeita de referências de memória feita dinamicamente
é bastante ambiciosa, mas talvez seja possível para pequenos tamanhos de janela
(e, portanto, pequenas taxas de despacho e buffers de load-store) ou através de um
previsor de dependência de memória.
4. Renomeação de registradorres com 64 registradores adicionais de inteiros
e 64 de ponto flutuante, o que é ligeiramente menor do que o processador
mais agressivo em 2011. O Intel Core i7 tem 128 entradas em seu buffer de
reordenação, embora eles não sejam divididos entre inteiros e ponto flutuante,
enquanto o IBM Power7 tem quase 200. Observe que supomos uma latência
de pipeline de um ciclo que reduz significativamente a necessidade de entradas
de buffer de reorganização. Tanto o Power7 quanto o i7 têm latências de
10 ciclos ou mais.
187
188
CAPÍTULO 3 :
Paralelismo em nível de instrução e sua exploração
A Figura 3.27 mostra o resultado para essa configuração à medida que variamos o
tamanho da janela. Essa configuração é mais complexa e dispendiosa do que quaisquer implementações existentes, sobretudo em termos do número de despachos de
instrução, que é mais de 10 vezes maior que o maior número de despachos disponíveis
em qualquer processador em 2011. Apesar disso, oferece um limite útil sobre o que as
implementações futuras poderiam alcançar. Os dados nessa figura provavelmente são
muito otimistas por outro motivo. Não existem restrições de despacho entre as 64 instruções: todas elas podem ser referências de memória. Ninguém sequer contemplaria
essa capacidade em um processador no futuro próximo. Infelizmente, é muito difícil
vincular o desempenho de um processador com restrições de despacho razoáveis; não
só o espaço de possibilidades é muito grande, mas a existência de restrições de despacho
exige que o paralelismo seja avaliado com um escalonador de instrução preciso, o que
torna muito dispendioso o custo de estudo de processadores com grande quantidade
de despachos.
FIGURA 3.27 A quantidade de paralelismo disponível em comparação ao tamanho da janela para diversos
programas de inteiros e ponto flutuante com até 64 despachos arbitrários de instruções por clock.
Embora haja menos registradores restantes do que o tamanho da janela, o fato de que todas as operações tenham
latência de um ciclo e o número de registradores restantes seja igual à largura de despacho permite ao processador
explorar o paralelismo dentro de toda a janela. Na implementação real, o tamanho da janela e o número de registradores
restantes devem ser equilibrados para impedir que um desses fatores restrinja demais a taxa de despacho.
3.10
Estudos das limitações do ILP
Além disso, lembre-se de que, na interpretação desses resultados, as falhas de cache e
as latências não unitárias não foram levadas em consideração, e esses dois efeitos terão
impacto significativo!
A observação mais surpreendente da Figura 3.27 é que, mesmo com as restrições
de processador realista listadas, o efeito do tamanho da janela para os programas de
inteiros não é tão severo quanto para programas de ponto flutuante. Esse resultado
aponta para a principal diferença entre esses dois tipos de programas. A disponibilidade
do paralelismo em nível de loop em dois dos programas de ponto flutuante significa
que a quantidade de ILP que pode ser explorado é mais alta, mas que, para programas
de inteiros, outros fatores — como a previsão de desvio, a renomeação de registrador
e menos paralelismo, para começar — são limitações importantes. Essa observação é
crítica, devido à ênfase aumentada no desempenho para inteiros nos últimos anos.
Na realidade, a maior parte do crescimento do mercado na última década — processamento de transação, servidores Web e itens semelhantes — dependeu do desempenho
para inteiros, em vez do ponto flutuante. Conforme veremos na seção seguinte, para
um processador realista em 2011, os níveis de desempenho reais são muito inferiores
àqueles mostrados na Figura 3.27.
Dada a dificuldade de aumentar as taxas de instrução com projetos de hardware realistas,
os projetistas enfrentam um desafio na decisão de como usar melhor os recursos limitados
disponíveis em um circuito integrado. Uma das escolhas mais interessantes é entre processadores mais simples com caches maiores e taxas de clock mais altas versus mais ênfase no
paralelismo em nível de instrução com clock mais lento e caches menores. O exemplo a
seguir ilustra os desafios, e no Capítulo 4 veremos uma técnica alternativa para explorar
o paralelismo fino na forma de GPUs.
Exemplo
Resposta
Considere os três processadores hipotéticos (mas não atípicos) a seguir, nos
quais vamos executar o benchmark gcc do SPEC:
1. Um pipe estático MIPS de duplo despacho rodando a uma frequência de
clock de 4 GHz e alcançando um CPI de pipeline de 0,8. Esse processador
tem um sistema de cache que gera 0,005 falha por instrução.
2. Uma versão fortemente canalizada de um processador MIPS com duplo
despacho com cache ligeiramente menor e frequência de clock de 5 GHz.
O CPI de pipeline do processador é 1,0, e as caches menores geram 0,0055
falha por instrução, em média.
3. Um superescalar especulativo com uma janela de 64 entradas. Ele alcança
metade da taxa de despacho ideal medida para esse tamanho de janela
(usar dados da Figura 3.27). Esse processador tem as menores caches,
que levam a 0,01 falha por instrução, mas oculta 25% da penalidade de
falha a cada falha através de escalonamento dinâmico. Esse processador
tem um clock de 2,5 GHz.
Suponha que o tempo da memória principal (que estabelece a penalidade de
falha) seja de 50 ns. Determine o desempenho relativo desses três processadores.
Primeiro, usaremos as informações de penalidade de falha e taxa de falha
para calcular a contribuição para o CPI a partir das falhas de cache para cada
configuração. Faremos isso com a seguinte fórmula:
CPI de cache = Falhas por instrução × Penalidade de falha
Precisamos calcular as penalidades de falha para cada sistema:
Penalidade de falha =
Tempo de acesso à memória
Ciclo de clock
189
190
CAPÍTULO 3 :
Paralelismo em nível de instrução e sua exploração
Os tempos de ciclo de clock são de 250 ps, 200 ps e 400 ps, respectivamente.
Portanto, as penalidades de falha são
Penalidade de falha1 =
50 ns
= 200 ciclos
250 ps
Penalidade de falha2 =
50 ns
= 250 ciclos
200 ps
Penalidade de falha3 =
0,75 × 50 ns
= 94 ciclos
400 ps
Aplicando isso a cada cache:
CPI de cache1 = 0,005 × 200 = 1,0
CPI de cache2 = 0,0055 × 250 = 1,4
CPI de cache3 = 0,01 × 94 = 0,94
Conhecemos a contribuição do CPI de pipeline para tudo, exceto para o
processador 3. Esse CPI de pipeline é dado por:
CPI de pipeline3 =
1
1
1
=
=
= 0,22
Taxa de despacho 9 × 0,5 4,5
Agora podemos encontrar o CPI para cada processador adicionando as contribuições de CPI do pipeline e cache.
CPI1 = 0,8 + 1,0 = 1,8
CPI2 = 1,0 + 1,4 = 2,4
CPI3 = 0,22 + 0,94 = 1,16
Uma vez que essa é a mesma arquitetura, podemos comparar as taxas de
execução de instrução em milhões de instruções por segundo (MIPS) para
determinar o desempenho relativo:
CR
CPI
4.000 MHz
Taxa de execução de instrução1 =
= 2.222 MIPS
1,8
Taxa de execução de instrução =
Taxa de execução de instrução2 =
5.000 MHz
= 2.083 MIPS
2,4
Taxa de execução de instrução3 =
2.500 MHz
= 2.155 MIPS
1,16
Neste exemplo, o superescalar estático simples de duplo despacho parece
ser o melhor. Na prática, o desempenho depende das suposições de CPI e
frequência do clock.
Além dos limites deste estudo
Como em qualquer estudo de limite, o estudo que examinaremos nesta seção tem suas
próprias limitações. Nós as dividimos em duas classes: 1) limitações que surgem até
mesmo para o processador especulativo perfeito e 2) limitações que surgem de um ou
mais modelos realistas. Naturalmente, todas as limitações na primeira classe se aplicam
à segunda. As limitações mais importantes que se aplicam até mesmo ao modelo perfeito são:
1. Hazards WAW e WAR através da memória. O estudo eliminou hazards WAW e WAR
por meio da renomeação de registrador, mas não no uso da memória. Embora a
princípio tais circunstâncias possam parecer raras (especialmente os hazards WAW),
elas surgem devido à alocação de frames de pilha. Uma chamada de procedimento
3.10
Estudos das limitações do ILP
reutiliza os locais da memória de um procedimento anterior na pilha, e isso pode
levar a hazards WAW e WAR, que são desnecessariamente limitadores. Austin e Sohi
(1992) examinam essa questão.
2. Dependências desnecessárias. Com um número infinito de registradores, todas as
dependências, exceto as verdadeiras dependências de dados de registrador, são
removidas. Porém, algumas dependências surgem de recorrências ou de convenções
de geração de código que introduzem dependências de dados verdadeiras
desnecessárias. Um exemplo disso é a dependência da variável de controle em um
simples loop for. Como a variável de controle é incrementada a cada iteração do
loop, ele contém pelo menos uma dependência. Como mostraremos no Apêndice
H, o desdobramento de loop e a otimização algébrica agressiva podem remover
essa computação dependente. O estudo de Wall inclui quantidade limitada dessas
otimizações, mas sua aplicação de forma mais agressiva poderia levar a maior
quantidade de ILP. Além disso, certas convenções de geração de código introduzem
dependências desnecessárias, em particular o uso de registradores de endereço
de retorno e de um registrador para o ponteiro de pilha (que é incrementado
e decrementado na sequência de chamada/retorno). Wall remove
o efeito do registrador de endereço de retorno, mas o uso de um ponteiro de pilha
na convenção de ligação pode causar dependências “desnecessárias”. Postiff et al.
(1999) exploraram as vantagens de remover essa restrição.
3. Contornando o limite de fluxo de dados. Se a previsão de valor funcionou com alta
precisão, ela poderia contornar o limite do fluxo de dados. Até agora, nenhum
dos mais de 100 trabalhos sobre o assunto conseguiu melhoria significativa no
ILP usando um esquema de previsão realista. Obviamente, a previsão perfeita do
valor de dados levaria ao paralelismo efetivamente infinito, pois cada valor de cada
instrução poderia ser previsto a priori.
Para um processador menos que perfeito, várias ideias têm sido propostas e poderiam expor mais ILP. Um exemplo é especular ao longo de vários caminhos. Essa ideia foi tratada
por Lam e Wilson (1992) e explorada no estudo abordado nesta seção. Especulando sobre
caminhos múltiplos, o custo da recuperação incorreta é reduzido e mais paralelismo pode
ser desvendado. Só faz sentido avaliar esse esquema para um número limitado de desvios,
pois os recursos de hardware exigidos crescem exponencialmente. Wall (1993) oferece
dados para especular nas duas direções em até oito desvios. Dados os custos de perseguir
os dois caminhos mesmo sabendo que um deles será abandonado (e a quantidade crescente de computação inútil, na medida em que tal processo é seguido por desvios múltiplos), cada projeto comercial, em vez disso, dedicou um hardware adicional para melhor
especulação sobre o caminho correto.
É fundamental entender que nenhum dos limites mencionados nesta seção é fundamental
no sentido de que contorná-los exige uma mudança nas leis da Física! Em vez disso, eles
são limitações práticas que implicam a existência de algumas barreiras formidáveis para
a exploração do ILP adicional. Essas limitações — sejam elas tamanho de janela, sejam
detecção de alias ou previsão de desvio — representam desafios para projetistas e pesquisadores contornarem.
Tentativas de romper esses limites nos primeiros cinco anos deste século resultaram
em frustrações. Algumas técnicas levaram a pequenas melhorias, porém muitas vezes
com significativos aumentos em complexidade, aumentos no ciclo de clock e aumentos
desproporcionais na potência. Em resumo, os projetistas descobriram que tentar extrair
mais ILP era simplesmente ineficiente. Vamos retomar essa discussão nos “Comentários
Finais”.
191
192
CAPÍTULO 3 :
Paralelismo em nível de instrução e sua exploração
3.11 QUESTÕES CRUZADAS: TÉCNICAS DE ILP
E O SISTEMA DE MEMÓRIA
Especulação de hardware versus especulação de software
As técnicas de uso intenso de hardware para a especulação, mencionadas no Capítulo 2,
e as técnicas de software do Apêndice H oferecem enfoques alternativos à exploração do
ILP. Algumas das escolhas e suas limitações para esses enfoques aparecem listadas a seguir:
j
j
j
j
j
j
Para especular extensivamente, temos de ser capazes de tirar a ambiguidade das
referências à memória. Essa capacidade é difícil de fazer em tempo de compilação
para programas de inteiros que contêm ponteiros. Em um esquema baseado no
hardware, a eliminação da ambiguidade dos endereços de memória em tempo
de execução dinâmica é feita com o uso das técnicas que vimos para o algoritmo
de Tomasulo. Essa desambiguidade nos permite mover loads para depois de stores
em tempo de execução. O suporte para referências de memória especulativas pode
contornar o conservadorismo do compilador, mas, a menos que essas técnicas
sejam usadas cuidadosamente, o overhead dos mecanismos de recuperação poderá
sobrepor as vantagens.
A especulação baseada em hardware funciona melhor quando o fluxo de controle
é imprevisível e quando a previsão de desvio baseada em hardware é superior à
previsão de desvio baseada em software, feita em tempo de compilação. Essas
propriedades se mantêm para muitos programas de inteiros. Por exemplo, um
bom previsor estático tem taxa de erro de previsão de cerca de 16% para quatro
principais programas SPEC92 de inteiros, e um previsor de hardware tem taxa de
erro de previsão de menos de 10%. Como as instruções especuladas podem atrasar
a computação quando a previsão é incorreta, essa diferença é significativa. Um
resultado dessa diferença é que mesmo os processadores escalonados estaticamente
normalmente incluem previsores de desvio dinâmicos.
A especulação baseada em hardware mantém um modelo de exceção
completamente preciso, até mesmo para instruções especuladas. As técnicas recentes
baseadas em software têm acrescentado suporte especial para permitir isso também.
A especulação baseada em hardware não exige código de compensação ou de
manutenção, que é necessário para mecanismos ambiciosos de especulação de
software.
Técnicas baseadas em compilador podem se beneficiar com a capacidade de ver
adiante na sequência de código, o que gera melhor escalonamento de código do
que uma técnica puramente controlada pelo hardware.
A especulação baseada em hardware com escalonamento dinâmico não exige
sequências de código diferentes para conseguir bom desempenho para diferentes
implementações de uma arquitetura. Embora essa vantagem seja a mais difícil de
quantificar, ela pode ser a mais importante com o passar do tempo. Interessante é
que essa foi uma das motivações para o IBM 360/91. Por outro lado, arquiteturas
explicitamente paralelas mais recentes, como IA-64, acrescentaram uma flexibilidade
que reduz a dependência de hardware inerente em uma sequência de código.
As principais desvantagens do suporte à especulação no hardware são a complexidade e
os recursos de hardware adicionais exigidos. Esse custo de hardware precisa ser avaliado
contra a complexidade de um compilador, para uma técnica baseada em software, e a
quantidade e a utilidade das simplificações, em um processo que conte com tal compilador.
Alguns projetistas tentaram combinar as técnicas dinâmica e baseada em compilador para
conseguir o melhor de cada uma. Essa combinação pode gerar interações interessantes e
3.12
Multithreading: usando suporte do ILP para explorar o paralelismo em nível de thread
obscuras. Por exemplo, se moves condicionais forem combinados com a renomeação de
registrador, aparecerá um efeito colateral sutil. Um move condicional que é anulado ainda
precisa copiar um valor para o registrador de destino, pois foi renomeado no pipeline de
instruções. Essas interações sutis complicam o processo de projeto e verificação, e também
podem reduzir o desempenho.
O processador Intel Itanium foi o computador mais ambicioso já projetado com base no
suporte de software ao ILP e à especulação. Ele não concretizou as esperanças dos projetistas, especialmente para códigos de uso geral e não científicos. Conforme as ambições dos
projetistas para explorar o ILP foram reduzidas à luz das dificuldades discutidas na Seção
3.10, a maioria das arquiteturas se estabeleceu em mecanismos baseados em hardware
com taxas de despacho de três ou quatro instruções por clock.
Execução especulativa e o sistema de memória
Inerente a processadores que suportam execução especulativa ou instruções condicionais é
a possibilidade de gerar endereços inválidos que não existiriam sem execução especulativa.
Não só isso seria um comportamento incorreto se fossem tomadas exceções de proteção,
mas os benefícios da execução especulativa também seriam sobrepujados pelo overhead
de exceções falsas. Portanto, o sistema de memória deve identificar instruções executadas
especulativamente e instruções executadas condicionalmente e suprimir a exceção correspondente.
Seguindo um raciocínio similar, não podemos permitir que tais instruções façam com
que a cache sofra stall em uma falha, porque stalls desnecessários poderiam sobrepujar
os benefícios da especulação. Portanto, esses processadores devem ser associados a caches
sem bloqueio.
Na verdade, a penalidade em uma falha em L2 é tão grande que, normalmente, os compiladores só especulam sobre falhas em L1. A Figura 2.5, na página 72, mostra que para alguns
programas científicos bem comportados o compilador pode sustentar múltiplas falhas
pendentes em L2 a fim de cortar efetivamente a penalidade de falha de L2. Novamente, para
que isso funcione, o sistema de memória por trás da cache deve fazer a correspondência
entre os objetivos do compilador em número de acessos simultâneos à memória.
3.12 MULTITHREADING: USANDO SUPORTE DO ILP
PARA EXPLORAR O PARALELISMO EM NÍVEL DE THREAD
O tópico que cobrimos nesta seção, o multithreading, é na verdade um tópico cruzado,
uma vez que tem relevância para o pipelining e para os superescalares, para unidades de
processamento gráfico (Cap. 4) e para multiprocessadores (Cap. 5). Apresentaremos o
tópico aqui e exploraremos o uso do multithreading para aumentar o throughput de uniprocessador usando múltiplos threads para ocultar as latências de pipeline e memória. No
Capítulo 4, vamos ver como o multithreading fornece as mesmas vantagens nas GPUs e,
por fim, o Capítulo 5 vai explorar a combinação de multithreading e multiprocessamento.
Esses tópicos estão firmemente interligados, já que o multithreading é uma técnica primária
usada para expor mais paralelismo para o hardware. Num senso estrito, o multithreading
usa paralelismo em nível de thread e, por isso, é o assunto do Capítulo 5, mas seu papel
tanto na melhoria da utilização de pipelines quanto nas GPUs nos motiva a introduzir
aqui esse conceito.
Embora aumentar o desempenho com o uso do ILP tenha a grande vantagem de ser
razoavelmente transparente para o programador, como já vimos, o ILP pode ser bastante
193
194
CAPÍTULO 3 :
Paralelismo em nível de instrução e sua exploração
limitado ou difícil de explorar em algumas aplicações. Em particular, com taxas razoáveis
de despacho de instrução, as falhas de cache que vão para a memória ou caches fora do
chip provavelmente não serão ocultadas por um ILP disponível. Obviamente, quando o
processador está paralisado esperando por uma falha de cache, a utilização das unidades
funcionais cai drasticamente.
Uma vez que tentativas de cobrir paralisações longas de memória com mais ILP tem eficácia
limitada, é natural perguntar se outras formas de paralelismo em uma aplicação poderiam
ser usadas para ocultar atrasos de memória. Por exemplo, um sistema de processamento
on-line de transações tem paralelismo natural entre as múltiplas pesquisas e atualizações
que são apresentadas pelas requisições. Sem dúvida, muitas aplicações científicas contêm
paralelismo natural, uma vez que muitas vezes modelam a estrutura tridimensional,
paralela, da natureza, e essa estrutura pode ser explorada usando threads separados.
Mesmo aplicações de desktop que usam sistemas operacionais modernos baseados no
Windows muitas vezes têm múltiplas aplicações ativas sendo executadas, proporcionando
uma fonte de paralelismo.
O multithreading permite que vários threads compartilhem as unidades funcionais de um
único processador em um padrão superposto. Em contraste, um método mais geral de
explorar o paralelismo em nível de thread (TLP) é com um multiprocessador que tenha múltiplos threads independentes operando ao mesmo tempo e em paralelo. O multithreading,
entretanto, não duplica todo o processador, como ocorre em um multiprocessador. Em
vez disso, o multithreading compartilha a maior parte do núcleo do processador entre
um conjunto de threads, duplicando somente o status privado, como os registradores e
o contador de programa. Como veremos no Capítulo 5, muitos processadores recentes
incorporam múltiplos núcleos de processador em um único chip e também fornecem
multithreading dentro de cada núcleo.
Duplicar o status por thread de um núcleo de processador significa criar um banco de
registradores separado, um PC separado e uma tabela de páginas separada para cada
thread. A própria memória pode ser compartilhada através dos mecanismos de memória
virtual, que já suportam multiprogramação. Além disso, o hardware deve suportar a
capacidade de mudar para um thread diferente com relativa rapidez. Em particular,
uma mudança de thread deve ser mais eficiente do que uma mudança de processador,
que em geral requer de centenas a milhares de ciclos de processador. Obviamente,
para o hardware em multithreading atingir melhoras de desempenho, um programa
deve conter múltiplos threads (às vezes dizemos que a aplicação é multithreaded) que
possam ser executados de modo simultâneo. Esses threads são identificados por um
compilador (em geral, a partir de uma linguagem com construções para paralelismo)
ou pelo programador.
Existem três técnicas principais para o multithreading. O multithreading de granularidade
fina alterna os threads a cada clock, fazendo com que a execução de múltiplos threads seja
intercalada. Essa intercalação normalmente é feita em um padrão round-robin, pulando
quaisquer threads que estejam em stall nesse momento. Para tornar o multithreading fino
prático, a CPU precisa ser capaz de trocar de threads a cada ciclo de clock. A principal
vantagem do multithreading de granularidade fina é que ele pode esconder as falhas de
throughput que surgem de stalls curtos e longos, pois as instruções de outros threads
podem ser executadas quando um thread está em stall. A principal desvantagem do multithreading de granularidade fina é que ele atrasa a execução dos threads individuais, pois
um thread que estiver pronto para ser executado sem stalls será atrasado pelas instruções
de outros threads. Ela troca um aumento no throughput do multithreading por uma perda
no desempenho (como medido pela latência) de um único thread. O processador Sun
3.12
Multithreading: usando suporte do ILP para explorar o paralelismo em nível de thread
Niagara, que vamos examinar a seguir, usa multithreading de granularidade fina, assim
como as GPUs Nvidia, que examinaremos no Capítulo 4.
O multithreading de granularidade grossa foi inventado como alternativa para o multithreading de granularidade fina. O multithreading de granularidade grossa troca de threads
somente em stalls dispendiosos, como as falhas de cache de nível 2. Essa troca alivia a
necessidade da comutação de threads ser essencialmente livre e é muito menos provável
que atrase o processador, pois as instruções de outros threads só serão enviadas quando
um thread encontrar um stall dispendioso.
Contudo, o multithreading de granularidade grossa apresenta uma grande desvantagem:
sua capacidade de contornar as falhas de throughput, especialmente de stalls mais curtos,
é limitada. Essa limitação advém dos custos de partida do pipeline de multithreading de
granularidade grossa. Como uma CPU com multithreading de granularidade grossa envia
instruções de um único thread quando ocorre um stall, o pipeline precisa ser esvaziado ou
congelado. O novo thread que inicia a execução após o stall precisa preencher o pipeline
antes que as instruções sejam capazes de terminar. Devido a esse overhead de partida, o
multithread de granularidade grossa é muito mais útil para reduzir as penalidades dos
stalls de alto custo, quando o preenchimento do pipeline é insignificante em comparação
com o tempo do stall. Muitos projetos de pesquisa têm explorado o multithreading de
granularidade grossa, mas nenhum grande processador atual usa essa técnica.
A implementação mais comum do multithreading é chamada multithreading simultâneo
(SMT). O multithreading simultâneo é uma variação do multithreading de granularidade
grossa que surge naturalmente quando é implementado em um processador de múltiplo
despacho, escalonado dinamicamente. Assim como ocorre com outras formas de multithreading, o SMT usa o paralelismo em nível de thread para ocultar eventos com grande
latência em um processador, aumentando o uso das unidades funcionais. A principal
característica do SMT é que a renomeação de registrador e o escalonamento dinâmico
permitem que múltiplas instruções de threads independentes sejam executadas sem
considerar as dependências entre eles; a resolução das dependências pode ser manipulada
pela capacidade de escalonamento dinâmico.
A Figura 3.28 ilustra conceitualmente as diferenças na capacidade de um processador
explorar os recursos de um superescalar para as seguintes configurações de processador:
j
j
j
j
Um superescalar sem suporte para multithreading
Um superescalar com multithreading de granularidade grossa
Um superescalar com multithreading de granularidade fina
Um superescalar com multithreading simultâneo
No superescalar sem suporte para multithreading, o uso dos slots de despacho é limitado
pela falha de ILP, incluindo ILP para ocultar a memória de latência. Devido ao comprimento das falhas das caches L2 e L3, grande parte do processador pode ficar ociosa.
No superescalar com multithreading de granularidade grossa, os stalls longos são parcialmente escondidos pela troca por outro thread que usa os recursos do processador.
Embora isso reduza o número de ciclos de clock completamente ociosos, dentro de cada
ciclo de clock as limitações do ILP ainda levam a ciclos ociosos. Além do mais, como em
um processador com multithreading de granularidade grossa a troca de thread só ocorre
quando existe um stall e o novo thread possui um período de partida, provavelmente restarão alguns ciclos totalmente ociosos.
No caso de granularidade fina, a intercalação de threads elimina slots totalmente vazios.
Além disso, já que o thread que envia instruções é mudado a cada ciclo de clock, operações
195
196
CAPÍTULO 3 :
Paralelismo em nível de instrução e sua exploração
FIGURA 3.28 Como quatro técnicas diferentes utilizam os slots de despacho de um processador
superescalar.
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 de ciclos de clock. Uma caixa vazia (branca) indica que o slot de despacho
correspondente não é usado nesse ciclo de clock. Os tons de cinza e preto correspondem a quatro threads
diferentes nos processadores de multithreading. Preto também é usado para indicar os slots de despacho
ocupados no caso do superescalar sem suporte para multithreading. Sun T1 e T2 (também conhecidos como
Niagara) são processadores multithread de granularidade fina, enquanto os processadores Intel Core i7 e IBM
Power7 usam SMT. O T2 possuiu oito threads, o Power7 tem quatro, e o Intel i7 tem dois. Em todos os SMTs
existentes, as instruções são enviadas de um thread por vez. A diferença do SMT é que a decisão subsequente
de executar uma instrução é desacoplada e pode executar as operações vindo de diversas instruções diferentes
no mesmo ciclo de clock.
com maior latência podem ser ocultadas. Como o despacho de instrução e a execução
estão conectados, um thread só pode enviar as instruções que estiverem prontas. Em uma
pequena largura de despacho isso não é um problema (um ciclo está ocupado ou não),
porque o multithreading de granularidade fina funciona perfeitamente para um processador de despacho único e o SMT não faria sentido. Na verdade, no Sun T2 existem duplos
despachos por clock, mas eles são de threads diferentes. Isso elimina a necessidade de
implementar a complexa técnica de escalonamento dinâmico e, em vez disso, depende
de ocultar a latência com mais threads.
Se um threading de granularidade fina for implementado sobre um processador com
escalonamento dinâmico, de múltiplos despachos, o resultado será SMT. Em todas as
implementações SMT existentes, todos os despachos vêm de um thread, embora instruções
de threads diferentes possam iniciar sua execução no mesmo ciclo, usando o hardware de
escalonamento dinâmico para determinar que instruções estão prontas. Embora a Figura 3.28 simplifique bastante a operação real desses processadores, ela ilustra as vantagens
de desempenho em potencial do multithreading em geral e do SMT em particular, em
processadores escalonáveis dinamicamente.
Multithreading simultâneos usam a característica de que um processador escalonado
dinamicamente já tem muitos dos mecanismos de hardware necessários para dar suporte
ao mecanismo, incluindo um grande conjunto de registradores virtual. O multithreading
pode ser construído sobre um processador fora de ordem, adicionando uma tabela de
renomeação por thread, mantendo PCs separados e fornecendo a capacidade de confirmar
instruções de múltiplos threads.
3.12
Multithreading: usando suporte do ILP para explorar o paralelismo em nível de thread
Eficácia do multithreading de granularidade fina no Sun T1
Nesta seção, usaremos o processador Sun T1 para examinar a capacidade do multithreading de ocultar a latência. O T1 é um multiprocessador multicore com multithreading
de granularidade fina introduzido pela Sun em 2005. O que torna o T1 especialmente
interessante é que ele é quase totalmente focado na exploração do paralelismo em nível
de thread (TLP) em vez do paralelismo em nível de instrução (ILP). O T1 abandonou o
foco em ILP (pouco depois os processadores ILP mais agressivos serem introduzidos),
retornou à estratégia simples de pipeline e focou na exploração do TLP, usando tanto
múltiplos núcleos como multithreading para produzir throughput.
Cada processador T1 contém oito núcleos de processador, cada qual dando suporte a quatro threads. Cada núcleo de processador consiste em um pipeline simples de seis estágios
e despacho único (um pipeline RISC padrão de cinco estágios, como aquele do Apêndice
C, com um estágio a mais para a comutação de threads). O T1 utiliza o multithreading de
granularidade fina (mas não SMT), passando para um novo thread a cada ciclo de clock, e
os threads que estão ociosos por estarem esperando devido a um atraso no pipeline ou falha
de cache são contornados no escalonamento. O processador fica ocioso somente quando
os quatro threads estão ociosos ou em stall. Tanto loads quanto desvios geram um atraso
de três ciclos, que só pode ser ocultado por outros threads. Um único conjunto de unidades
funcionais de ponto flutuante é compartilhado pelos oito núcleos, pois o desempenho de
ponto flutuante não foi o foco para o T1. A Figura 3.29 resume o processador T1.
Desempenho do multithreading do T1 de núcleo único
O T1 faz do TLP o seu foco, através do multithreading em um núcleo individual, e também
através do uso de muitos núcleos simples em um único substrato. Nesta seção, vamos
examinar a eficácia do T1 em aumentar o desempenho de um único núcleo através de
multithreading de granularidade fina. No Capítulo 5, vamos voltar a examinar a eficácia
de combinar multithreading com múltiplos núcleos.
Examinamos o desempenho do T1 usando três benchmarks orientados para servidor:
TPC-C, SPECJBB (o SPEC Java Business Benchmark) e SPECWeb99. Já que múltiplos
threads aumentam as demandas de memória de um único processador, eles poderiam
sobrecarregar o sistema de memória, levando a reduções no ganho em potencial do
multithreading. A Figura 3.30 mostra o aumento relativo na taxa de falha e a latência
quando rodamos com um thread por núcleo em comparação a rodar quatro threads por
núcleo para o TPC-C. As taxas de falha e as latências de falha aumentam, devido à maior
FIGURA 3.29 Resumo do processador T1.
197
198
CAPÍTULO 3 :
Paralelismo em nível de instrução e sua exploração
FIGURA 3.30 Mudança relativa nas taxas de falha e latências de falha ao executar com um thread por
núcleo contra quatro threads por núcleo no benchmark TPC-C.
As latências são o tempo real para retornar os dados solicitados após uma falha. No caso de quatro threads, a
execução dos outros threads poderia ocultar grande parte dessa latência.
contenção no sistema de memória. O aumento relativamente pequeno na latência de falha
indica que o sistema de memória ainda tem capacidade não utilizada.
Ao examinarmos o comportamento de uma média de threads, podemos entender a interação
entre multithreading e processamento paralelo. A Figura 3.31 mostra a porcentagem de ciclos
para os quais um thread está executando, pronto mas não executando e não pronto. Lembre-se de que “não pronto” não significa que o núcleo com esse thread está em stall; somente
quando todos os quatro threads estiverem não prontos é que o núcleo gerará um stall.
Os threads podem estar não prontos devido a falhas de cache, atrasos de pipeline (surgindo
de instruções de longa latência, como desvios, loads, ponto flutuante ou multiplicação/
divisão de inteiros) e uma série de efeitos menores. A Figura 3.32 mostra a frequência
relativa dessas várias causas. Os efeitos de cache são responsáveis pelo thread não estando
pronto em 50-75% do tempo, com falhas de instrução L1, falhas de dados L1 e falhas
FIGURA 3.31 Desmembramento de um thread mediano.
“Executando” indica que o thread envia uma instrução nesse ciclo. “Pronto, não escolhido” significa que ele poderia
ser enviado, mas outro thread foi escolhido, e “não pronto” indica que o thread está esperando o término de um evento
(um atraso de pipeline ou falha de cache, por exemplo).
3.12
Multithreading: usando suporte do ILP para explorar o paralelismo em nível de thread
FIGURA 3.32 Desmembramento de causas para um thread não pronto.
A contribuição para a categoria “outros” varia. No TPC-C, o buffer de armazenamento cheio é o contribuinte maior; no
SPEC-JBB, as instruções indivisíveis são o contribuinte maior; e, no SPECWeb99, ambos os fatores contribuem.
L2 contribuindo de forma aproximadamente igual. Os atrasos em potencial do pipeline
(chamados “atrasos de pipeline”) são mais severos no SPECJBB e podem surgir de sua
frequência de desvio mais alta.
A Figura 3.33 mostra o CPI por thread e por núcleo. Já que o T1 é um processador multithreaded de granularidade fina com quatro threads por núcleo, com paralelismo suficiente,
o CPI ideal por thread seria quatro, uma vez que isso significaria que cada thread estaria
consumindo um a cada quatro ciclos. O CPI ideal por núcleo seria um. Em 2005, o IPC
para esses benchmarks, sendo executados em núcleos ILP agressivos, seria similar ao visto
em um núcleo T1. Entretanto, o núcleo T1 tinha tamanho muito modesto em comparação
com os núcleos ILP agressivos de 2005, porque o T1 tinha oito núcleos em comparação aos
dois a quatro oferecidos por outros processadores da mesma época. Como resultado,
em 2005, quando foi lançado, o processador Sun T1 tinha o mesmo desempenho em
aplicações de número inteiro com TLP extensivo e desempenho de memória exigente,
como o SPECJBB e cargas de trabalho de processamento de transação.
Eficácia no multithreading simultâneo em processadores
superescalares
Uma pergunta-chave é: “Quanto desempenho pode ser ganho com a implementação do
SMT?” Quando essa pergunta foi explorada em 2000-2001, os pesquisadores presumiram
que os superescalares dinâmicos ficariam maiores nos cinco anos seguintes, admitindo
6-8 despachos por clock com escalonamento dinâmico especulativo, muitos loads e stores
FIGURA 3.33 O CPI por thread, o CPI por núcleo, o CPI efetivo para oito núcleos e o IPC eficiente (inverso
da CPI) para o processador T1 de oito núcleos.
199
200
CAPÍTULO 3 :
Paralelismo em nível de instrução e sua exploração
simultâneos, grandes caches primárias e 4-8 contextos com busca simultânea de múltiplos
contextos. Mas nenhum processador chegou perto desse nível.
Em consequência, os resultados de pesquisa de simulação que mostraram ganhos para
cargas de trabalho multiprogramadas de duas ou mais vezes são irrealistas. Na prática,
as implementações existentes do SMT oferecem dois contextos com busca de apenas um,
além de capacidades de despacho mais modestas. O resultado disso é que o ganho do
SMT também é mais modesto.
Por exemplo, no Pentium 4 Extreme, implementado nos servidores HP-Compaq, o uso
do SMT gera melhoria de desempenho de 1,01 quando executa o benchmark SPECintRate
e cerca de 1,07 quando executa o benchmark SPECfpRate. Em um estudo separado, Tuck
e Tullsen (2003) observam que os benchmarks paralelos SPLASH informam ganhos de
velocidade de 1,02-1,67, com ganho médio de velocidade de cerca de 1,22.
Com a disponibilidade de medidas completas e esclarecedoras recém-feitas por Esmaeilzadeh et al. (2011), podemos examinar os benefícios de desempenho e energia de
empregar SMT em um único núcleo i7 usando um conjunto de aplicações multithreaded.
Os benchmarks que usamos consistem em uma coleção de aplicações científicas paralelas e
um conjunto de programas Java multithreaded dos conjuntos DaCapo e SPEC Java, como
resumido na Figura 3.34, que mostra a taxa de desempenho e a taxa de eficiência energética
dos benchmarks executados em um núcleo do i7 com o SMT desligado e ligado.Figura 3.35
FIGURA 3.34 Benchmarks paralelos usados aqui para examinar multithreading e também no Capítulo 5 para examinar o
multiprocessamento com um i7.
A metade superior da figura consiste em benchmarks PARSEC coletados por Biena et al. (2008). Os benchmarks PARSEC foram criados para indicar
aplicações paralelas intensas em termos de computação, que seriam apropriadas para processadores multicore. A metade inferior consiste em benchmarks
Java multithreaded do conjunto DaCapo (ver Blackburn et al., 2006) e pjbb2005 da SPEC. Todos esses benchmarks contêm algum paralelismo. Outros
benchmarks Java nas cargas de trabalho DaCapo e SPEC Java usam threads múltiplos, mas têm pouco ou nenhum paralelismo e, portanto, não são usados
aqui. Ver informações adicionais sobre as características desses benchmarks em relação às medidas aqui e no Capítulo 5 (Esmaeilzadeh et al., 2011).
3.12
Multithreading: usando suporte do ILP para explorar o paralelismo em nível de thread
FIGURA 3.35 O ganho de velocidade usando multithreading em um núcleo de um processador i7 é,
em média, de 1,28 para os benchmarks Java e de 1,31 para os benchmarks PARSEC (usando uma média
harmônica não ponderada, que implica uma carga de trabalho em que o tempo total gasto executando cada
benchmark no conjunto-base de thread único seja o mesmo).
A eficiência energética tem médias de 0,99 e 1,07, respectivamente (usando a média harmônica). Lembre-se de que
qualquer coisa acima de 1,0 para a eficiência energética indica que o recurso reduz o tempo de execução mais do que
aumenta a potência média. Dois dos benchmarks Java experimentam pouco ganho de velocidade e, por causa disso,
têm efeito negativo sobre a eficiência energética. O Turbo Boost está desligado em todos os casos. Esses dados foram
coletados e analisados por Esmaeilzadeh et al. (2011) usando o build Oracle (Sun) Hotspot 16.3-b01 Java 1.6.0 Virtual
Machine e o compilador nativo gcc v4.4.1.
(Nós plotamos a taxa de eficiência energética, que é o inverso do consumo de energia, de
modo que, assim como no ganho de velocidade, uma taxa maior seja melhor.)
A média harmônica do ganho de velocidade para o Benchmark Java é 1,28, apesar de os dois
benchmarks verificarem pequenos ganhos. Esses dois benchmarks, pjbb2055 e tradebeans,
embora multithreaded, têm paralelismo limitado. Eles são incluídos porque são típicos de
um benchmark multithreaded que pode ser executado em um processador SMT com a esperança de extrair algum desempenho, que eles obtêm de modo limitado. Os benchmarks
PARSEC obtêm ganhos de velocidade um pouco melhores do que o conjunto completo de
benchmarks Java (média harmônica de 1,31). Se o tradebeans e o pjbb2005 fossem omitidos, na verdade a carga de trabalho Java teria um ganho de velocidade significativamente
melhor (1,39) do que os benchmarks PARSEC. (Ver discussão sobre a implicação de usar
a média harmônica para resumir os resultados na legenda da Figura 3.36.)
O consumo de energia é determinado pela combinação do ganho de energia e pelo
aumento no consumo de potência. Para os benchmarks Java os SMT proporcionam,
em média, a mesma eficiência energética que os não SMT (média de 1,0), mas essa
eficiência é reduzida pelos dois benchmarks de desempenho ruim. Sem o tradebeans
e o pjbb2005, a eficiência energética média para os benchmarks Java é de 1,06, o que
é quase tão bom quanto os benchmarks PARSEC. Nos benchmarks PARSEC, o SMT
reduz a energia em 1 – (1/1,08) = 7%. Tais melhorias de desempenho de redução de
201
202
CAPÍTULO 3 :
Paralelismo em nível de instrução e sua exploração
FIGURA 3.36 A estrutura básica do pipeline A8 é de 13 estágios.
São usados três ciclos para a busca de instruções e quatro para a decodificação de instruções, além de um pipeline
inteiro de cinco ciclos. Isso gera uma penalidade de erro de previsão de desvio de 13 ciclos. A unidade de busca de
instruções tenta manter a fila de instruções de 12 entradas cheia.
energia são muito difíceis de encontrar. Obviamente, a potência estática associada ao SMT
é paga nos dois casos, por isso os resultados provavelmente exageram um pouco nos
ganhos de energia.
Esses resultados mostram claramente que o SMT em um processador especulativo agressivo
com suporte extensivo para SMT pode melhorar o desempenho em eficiência energética, o
que as técnicas ILP mais agressivas não conseguiram. Em 2011, o equilíbrio entre oferecer
múltiplos núcleos mais simples e menos núcleos mais sofisticados mudou em favor de
mais núcleos, com cada núcleo sendo um superescalar com 3-4 despachos com SMT
suportando 2-4 threads. De fato, Esmaeilzadeh et al. (2011) mostram que as melhorias
de energia derivadas do SMT são ainda maiores no Intel i5 (um processador similar ao i7,
mas com caches menores e taxa menor de clock) e o Intel Atom (um processador 80x86
projetado para o mercado de netbooks e descrito na Seção 3.14).
3.13 JUNTANDO TUDO: O INTEL CORE I7 E O ARM
CORTEX-A8
Nesta seção exploraremos o projeto de dois processadores de múltiplos despachos: o ARM
Cortex-A8, que é usado como base para o processador Apple A9 no iPad, além de ser o
processador no Motorola Droid e nos iPhones 3GS e 4, e o Intel Core i7, um processador
sofisticado especulativo, escalonado dinamicamente, voltado para desktops sofisticados
e aplicações de servidor. Vamos começar com o processador mais simples.
O ARM Cortex-A8
O A8 é um superescalar com despacho duplo, escalonado estaticamente com detecção
dinâmica de despacho que permite ao processador enviar uma ou duas instruções por
clock. A Figura 3.36 mostra a estrutura básica do pipeline de 13 estágios.
O A8 usa um previsor dinâmico de desvio com um buffer associativo por conjunto de
duas vias com 512 entradas para alvos de desvio e um buffer global de histórico de 4 K
entradas, que é indexado pelo histórico de desvios e pelo PC atual. Caso o buffer de alvo
de desvio erre, uma previsão será obtida do buffer global de histórico, que poderá ser usado
para calcular o endereço do desvio. Além disso, uma pilha de retorno de oito entradas é
mantida para rastrear os endereços de retorno. Uma previsão incorreta resulta em uma
penalidade de 13 ciclos quando o pipeline é descartado.
3.13
Juntando tudo: O Intel Core i7 e o ARM Cortex-A8
A Figura 3.37 mostra o pipeline de decodificação de instrução. Até duas instruções por
clock podem ser enviadas usando um mecanismo de despacho em ordem. Uma simples
estrutura de scoreboard é usada para rastrear quando uma instrução pode ser enviada. Um
par de instruções dependentes pode ser processado através da lógica de despacho, mas,
obviamente, elas serão serializadas no scoreboard, a menos que possam ser enviadas para
que os ganhos de avanço possam resolver a dependência.
A Figura 3.38 mostra o pipeline de execução para o processador A8. A instrução 1 ou
a instrução 2 pode ir para o pipeline de load/store. O contorno total é suportado entre
os pipelines. O pipeline do ARM Cortex-A8 usa um superescalar simples escalonado
estaticamente de dois despachos para permitir uma frequência do clock razoavelmente
alta com menor potência. Em contraste, o i7 usa uma estrutura de pipeline especulativa
razoavelmente agressiva, escalonada dinamicamente com quatro despachos.
FIGURA 3.37 Decodificação de instruções em cinco estágios do A8.
No primeiro estágio, um PC produzido pela unidade de busca (seja do buffer de alvo de desvio seja do incrementador
de PC) é usado para atingir um bloco de 8 bytes da cache. Até duas instruções são decodificadas e colocadas na fila
de decodificação. Se nenhuma instrução for um desvio, o PC é incrementado para a próxima busca. Uma vez na fila de
decodificação, a lógica de scoreboard decide quando as instruções podem ser enviadas. No despacho, os registradores
operandos são lidos. Lembre-se de que em um scoreboard simples os operandos sempre vêm dos registradores. Os
registradores operandos e o opcode são enviados para a parte de execução de instruções do pipeline.
FIGURA 3.38 Decodificação de cinco estágios do A8.
Operações de multiplicação são sempre realizadas no pipeline 0 da ALU.
203
204
CAPÍTULO 3 :
Paralelismo em nível de instrução e sua exploração
Desempenho do pipeline do A8
O A8 tem um CPI ideal de 0,5, devido a sua estrutura de despacho duplo. Os stalls de
pipeline podem surgir de três fontes:
1. Hazards funcionais, que ocorrem porque duas instruções adjacentes selecionadas
simultaneamente para despacho usam o mesmo pipeline funcional. Como o A8
é escalonado estaticamente, é tarefa do compilador tentar evitar tais conflitos.
Quando eles não podem ser evitados, o A8 pode enviar, no máximo, uma instrução
nesse ciclo.
2. Hazard de dados, que são detectados precocemente no pipeline e podem causar o
stall das duas instruções (se a primeira não puder ser enviada, a segunda sempre
sofrerá stall) ou da segunda instrução de um par. O compilador é responsável por
impedir tais stalls sempre que possível.
3. Hazards de controle que surgem somente quando desvios são previstos
incorretamente.
Além dos stalls de pipeline, as falhas de L1 e L2 causam stalls.
A Figura 3.39 mostra uma estimativa dos fatores que podem contribuir para o CPI real
dos benchmarks Minnespec, que vimos no Capítulo 2. Como podemos ver, os atrasos
FIGURA 3.39 A composição estimada do CPI no ARM A8 mostra que os stalls de pipeline são a principal
adição ao CPI base.
O eon merece menção especial, já que realiza cálculos de gráficos baseados em números inteiros (rastreamento de
raio) e tem poucas falhas de cache. Ele é computacionalmente intenso, com uso pesado de multiplicação, e o único
pipeline de multiplicação se torna um grande gargalo. Essa estimativa é obtida usando as taxas de falha e penalidades
de L1 e L2 para calcular os stalls de L1 e L2 gerados por instrução. Estas são subtraídas do CPI medido por um
simulador detalhado para obter os stalls de pipeline. Todos esses stalls incluem as três ameaças e efeitos menores,
como erro de previsão de trajeto.
3.13
Juntando tudo: O Intel Core i7 e o ARM Cortex-A8
de pipeline, e não os stalls de memória, são os maiores contribuidores para o CPI. Esse
resultado se deve parcialmente ao efeito de o Minnespec ter uma pegada menor de cache
do que o SPEC completo ou outros programas grandes.
A compreensão de que os stalls de pipeline criaram perdas de desempenho significativas
provavelmente teve um papel importante na decisão de fazer do ARM Cortex-A8 um
superescalar escalonado dinamicamente. O A9, como o A8, envia até duas instruções por
clock, mas usa escalonamento dinâmico e especulação. Até quatro instruções pendentes
(duas ALUs, um load/store ou PF/multimídia e um desvio) podem começar sua execução
em um ciclo de clock. O A9 usa um previsor de desvio mais poderoso, pré-fetch na cache
de instrução e uma cache de dados L1 sem bloqueio. A Figura 3.40 mostra que o A9
tem desempenho melhor que o A8 por um fator de 1,28, em média, supondo a mesma
frequência do clock e configurações de cache quase idênticas.
O Intel Core i7
O i7 usa uma microestrutura especulativa agressiva fora de ordem com pipelines razoavelmente profundos com o objetivo de atingir alto throughput de instruções combinando
múltiplos despachos e altas taxas de clock. A Figura 3.41 mostra a estrutura geral do
pipeline do i7. Vamos examinar o pipeline começando com a busca de instruções e
continuando rumo à confirmação de instrução, seguindo os passos mostrados na figura.
1. Busca de instruções. O processador usa um buffer de endereços-alvo de desvio
multiníveis para atingir um equilíbrio entre velocidade e precisão da previsão. Há
também uma pilha de endereço de retorno para acelerar o retorno de função. Previsões
FIGURA 3.40 A taxa de desempenho do A9, comparada à do A8, ambos usando um clock de 1 GHz
e os mesmos tamanhos de cache para L1 e L2, mostra que o A9 é cerca de 1,28 vez mais rápido.
Ambas as execuções usam cache primária de 32 KB e cache secundária de 1 MB, que é associativo de conjunto
de oito vias para o A8 e 16 vias para o A9. Os tamanhos de bloco nas caches são de 64 bytes para o A8 e de 32
bytes para o A9. Como mencionado na legenda da Figura 3.39, o eon faz uso intenso de multiplicação de inteiros,
e a combinação de escalonamento dinâmico e um pipeline de multiplicação mais rápido melhora significativamente
o desempenho no A9. O twolf experimenta ligeira redução de velocidade, provavelmente devido ao fato de que seu
comportamento de cache é pior com o tamanho menor de bloco de L1 do A9.
205
206
CAPÍTULO 3 :
Paralelismo em nível de instrução e sua exploração
FIGURA 3.41 A estrutura de pipeline do Intel Core i7 mostrada com os componentes do sistema de memória.
A profundidade total do pipeline é de 14 estágios, com erros de previsão de desvio custando 17 ciclos. Existem
48 buffers de carregamento e 32 de armazenamento. As seis unidades funcionais independentes podem começar
a execução de uma micro-op no mesmo ciclo.
incorretas causam uma penalidade de cerca de 15 ciclos. Usando os endereços
previstos, a unidade de busca de instrução busca 16 bytes da cache de instrução.
2. Os 16 bytes são colocados no buffer de pré-decodificação de instrução — nesse
passo, um processo chamado fusão macro-op é realizado. A fusão macro-op toma as
combinações de instrução como comparação, seguido por um desvio, e as funde
em uma única operação. O estágio de pré-decodificação também quebra os 16 bytes
em instruções x86 individuais. Essa pré-decodificação não é trivial, uma vez que o
tamanho de uma instrução x86 pode ser de 1-17 bytes e o pré-decodificador deve
examinar diversos bytes antes de saber o comprimento da instrução. Instruções
x86 individuais (incluindo algumas instruções fundidas) são colocadas na fila de
instruções de 18 entradas.
3.13
Juntando tudo: O Intel Core i7 e o ARM Cortex-A8
3. Decodificação micro-op. Instruções x86 individuais são traduzidas em micro-ops.
Micro-ops são instruções simples, similares às do MIPS, que podem ser executadas
diretamente pelo pipeline. Essa técnica de traduzir o conjunto de instruções x86 em
operações simples mais fáceis de usar em pipeline foi introduzida no Pentium Pro
em 1997 e tem sido usada desde então. Três dos decodificadores tratam instruções
x86 que as traduzem diretamente em uma micro-op. Para instruções x86 que têm
semântica mais complexa, existe um máquina de microcódigo que é usada para
produzir a sequência de micro-ops. Ela pode produzir até quatro micro-ops a cada
ciclo e continua até que a sequência de micro-ops necessária tenha sido gerada. As
micro-ops são posicionadas de acordo com a ordem das instruções x86 no buffer
de micro-ops de 28 entradas.
4. O buffer de micro-op realiza detecção e microfusão. Se houver uma sequência
pequena de instruções (menos de 28 instruções ou 256 bytes de comprimento)
que contenham um loop, o detector de fluxo de loop vai encontrar o loop e enviar
diretamente as micro-ops do buffer, eliminando a necessidade de ativar os estágios
de busca de instrução e decodificação da instrução. A microfusão combina pares de
instruções, como load/operação ALU e a operação ALU/store, e as envia para uma
única estação de reserva (onde elas ainda podem ser enviadas independentemente),
aumentando assim o uso do buffer. Em um estudo da arquitetura do Intel Core, que
também incorpora microfusão e macrofusão, Bird et al. (2007) descobriram que a
microfusão tinha pouco impacto no desempenho, enquanto a macrofusão parece
ter um impacto positivo modesto no desempenho com inteiros e pouco impacto
sobre o desempenho com ponto flutuante.
5. Realizar o despacho da instrução básica. Buscar a localização do registrador nas
tabelas de registro, renomear os registradores, alocar uma entrada no buffer
de reordenação e buscar quaisquer resultados dos registradores ou do buffer de
reordenação antes de enviar as micro-ops para as estações de reserva.
6. O i7 usa uma estação de reserva de 36 entradas centralizada compartilhada por
seis unidades funcionais. Até seis micro-ops podem ser enviadas para as unidades
funcionais a cada ciclo de clock.
7. As micro-ops são executadas pelas unidades funcionais individuais e então os
resultados são enviados de volta para qualquer estação de reserva, além da unidade
de remoção de registrador, onde elas vão atualizar o status do registrador, uma vez
que se saiba que a instrução não é mais especulativa. A entrada correspondente à
instrução no buffer de reordenação é marcada como completa.
8. Quando uma ou mais instruções no início do buffer de reordenação são marcadas
como completas, as gravações pendentes na unidade de remoção de registrador são
executadas e as instruções são removidas do buffer de reordenação.
Desempenho do i7
Em seções anteriores, nós examinamos o desempenho do previsor de desvio do i7 e
também o desempenho do SMT. Nesta seção, examinaremos o desempenho do pipeline
de thread único. Por causa da presença de especulação agressiva e de caches sem bloqueio,
é difícil avaliar com precisão a distância entre o desempenho idealizado e o desempenho
real. Como veremos, poucos stalls ocorrem, porque as instruções não podem enviar. Por
exemplo, somente cerca de 3% dos loads são atrasados, porque nenhuma estação de
reserva está disponível. A maioria das falhas vem de previsões incorretas de desvio ou
de falhas de cache. O custo de uma previsão incorreta de desvio é de 15 ciclos, enquanto
o custo de uma falha de L1 é de cerca de 10 ciclos. As falhas de L2 são cerca de três vezes
mais caras do que uma falha de L1, e as falhas de L3 custam cerca de 13 vezes o custo
de uma falha de L1 (130-135 ciclos)! Embora o processador tente encontrar instruções
207
208
CAPÍTULO 3 :
Paralelismo em nível de instrução e sua exploração
alternativas para executar para falhas de L3 e algumas falhas de L2, é provável que alguns
dos buffers sejam preenchidos antes de a falha se completar, fazendo o processador parar
de enviar instruções.
Para examinar o custo de previsões e especulações incorretas, a Figura 3.42 mostra a fração
do trabalho (medida pelos números de micro-ops enviadas para o pipeline) que não são
removidas (ou seja, seus resultados são anulados) em relação a todos os despachos de
micro-op. Para o sjeng, por exemplo, 25% do trabalho é desperdiçado, já que 25% das
micro-ops enviadas nunca são removidas.
Observe que, em alguns casos, o trabalho perdido é muito próximo das taxas de previsão
incorreta mostradas na Figura 3.5, na página 144, mas em vários outros, como o mcf,
o trabalho perdido parece relativamente maior do que a taxa de previsão incorreta. Em
tais casos, uma explicação provável está no comportamento da memória. Com as taxas
muito altas de falha de cache de dados, o mcf vai enviar muitas instruções durante uma
especulação incorreta enquanto houver estações de reserva suficientes disponíveis para
as referências de memória que sofreram stall. Quando a previsão incorreta de desvio é
detectada, as micro-ops correspondentes a essas instruções são descartadas, mas ocorre
um congestionamento nas caches, conforme as referências especuladas à memória tentam
ser completadas. Não há modo simples de o processador interromper tais requisições de
cache depois que elas são iniciadas.
A Figura 3.43 mostra o CPI geral para os 19 benchmarks SPEC CPU2006. Os benchmarks
inteiros têm um CPI de 1,06 com variância muito grande (0,67 de desvio-padrão). O
MCF e o OMNETTP são as principais discrepâncias, ambos tendo um CPI de mais de 2,0,
enquanto a maioria dos outros benchmarks estão próximos de 1,0 ou são menores do
que isso (o gcc, o segundo maior, tem 1,23). Essa variância é decorrente de diferenças na
precisão da previsão de desvio e nas taxas de falha de cache. Para os benchmarks inteiros,
FIGURA 3.42 A quantidade de “trabalho perdido” é plotada tomando a razão entre micro-ops enviadas que
não são graduadas e todas as micro-ops enviadas.
Por exemplo, a razão é de 25% para o sjeng, significando que 25% das micro-ops enviadas e executadas são jogadas
fora. Os dados apresentados nesta seção foram coletados pelo professor Lu Peng e pelo doutorando Ying Zhang,
ambos da Universidade do Estado da Louisiana.
3.14
FIGURA 3.43 O CPI para os 19 benchmarks SPECCPU2006 mostra um CPI médio de 0,83 para
os benchmarks PF e de inteiro, embora o comportamento seja bastante diferente.
No caso dos inteiros, os valores de CPI variam de 0,44-2,66, com desvio-padrão de 0,77, enquanto no caso de PF a
variação é de 0,62-1,38, com desvio-padrão de 0,25. Os dados nesta seção foram coletados pelo professor Lu Peng e
pelo doutorando Ying Zhang, ambos da Universidade do Estado da Louisiana.
a taxa de falha de L2 é o melhor previsor de CPI, e a taxa de falhas de L3 (que é muito
pequeno) praticamente não tem efeito.
Os benchmarks de PF atingem desempenho maior com CPI médio menor (0,89) e desvio-padrão menor (0,25). Para os benchmarks de PF, L1 e L2 são igualmente importantes
para determinar o CPI, enquanto o L3 tem um papel menor mais significativo. Embora o
escalonamento dinâmico e as capacidades de não bloqueio do i7 possam ocultar alguma
latência de falha, o comportamento da memória de cache ainda é responsável por uma
grande contribuição. Isso reforça o papel do multithreading como outro modo de ocultar
a latência de memória.
3.14
FALÁCIAS E ARMADILHAS
Nossas poucas falácias se concentram na dificuldade de prever o desempenho e a eficiência
energética e de extrapolar medidas únicas, como frequência do clock ou CPI. Mostraremos
também que diferentes técnicas de arquiteturas podem ter comportamentos radicalmente
diferentes para diferentes benchmarks.
Falácia. É fácil prever o desempenho e a eficiência energética de duas versões diferentes da mesma
arquitetura de conjunto de instruções, se mantivermos a tecnologia constante.
A Intel fabrica um processador para a utilização final em netbooks e PDMs que é muito
similar ao ARM A8 na sua microarquitetura: é o chamado Atom 230. É interessante que
o Atom 230 e o Core i7 920 tenham sido fabricados com a mesma tecnologia de 45 nm
da Intel. A Figura 3.44 resume o Intel Core i7, o ARM Cortex-A8 e o Intel Atom 230. Essas
similaridades proporcionam uma rara oportunidade de comparar diretamente duas microarquiteturas radicalmente diferentes para o mesmo conjunto de instruções e, ao mesmo
Falácias e armadilhas
209
210
CAPÍTULO 3 :
Paralelismo em nível de instrução e sua exploração
FIGURA 3.44 Visão geral do Intel i7 920 de quatro núcleos, exemplo de um processador ARM A8 (com um L2 de 256 MB, L1 de 32 KB e sem
ponto flutuante) e o Intel ARM 230 mostrando claramente a diferença em termos de filosofia de projeto entre um processador voltado
para PMD (no caso do ARM) ou espaço de netbook (no caso do Atom) e um processador para uso em servidores e desktops sofisticados.
Lembre-se de que o i7 inclui quatro núcleos, cada qual várias vezes mais alto em desempenho do que o A8 ou o Atom de um núcleo. Todos esses
processadores são implementados em uma tecnologia comparável de 45 nm.
tempo, manter constante a tecnologia de fabricação fundamental. Antes de fazermos a
comparação, precisamos contar um pouco mais sobre o Atom 230.
Os processadores Atom implementam a arquitetura x86 em instruções similares às RISC
(como toda implementação x86 tem feito desde meados dos anos 1990). O Atom usa uma
micro-operação ligeiramente mais poderosa, que permite que uma operação aritmética
seja pareada com um carregamento ou armazenamento. Isso significa que, em média,
para um mix típico de instruções, somente 4% das instruções requerem mais de uma
micro-operação. As micro-operações são executadas em um pipeline com profundidade
de 16, capaz de enviar duas instruções por clock, em ordem, como no ARM A8. Há duas
ALUs de inteiro duplo, pipelines separados para soma de PF e outras operações de PF, e
dois pipelines de operação de memória suportando execução dupla, mais geral do que
o ARM A8, porém ainda limitadas pela capacidade de despacho em ordem. O Atom 230
tem cache de instrução de 32 KB e cache de dados de 24 KB, ambas suportadas por um L2
de 512 KB no mesmo substrato. (O Atom 230 também suporta multithreading com dois
threads, mas vamos considerar somente comparações de um único thread.) A Figura 3.46
resume os processadores i7, A8 e Atom e suas principais características.
3.14
Falácias e armadilhas
Podemos esperar que esses dois processadores, implementados na mesma tecnologia
e com o mesmo conjunto de instruções, apresentassem comportamento previsível em
termos de desempenho relativo e consumo de energia, o que significa que a potência e o
desempenho teriam uma escala próxima à linearidade. Examinamos essa hipótese usando
três conjuntos de benchmarks. Os primeiros conjuntos são um grupo de benchmarks Java
de thread único que vêm dos benchmarks DaCapo, e dos benchmarks SPEC JVM98 (ver
discussão sobre os benchmarks e medidas em Esmaeilzadeh et al., 2011). O segundo e o
terceiro conjuntos são do SPEC CPU2006 e consistem, respectivamente, nos benchmarks
para inteiros e PF.
Como podemos ver na Figura 3.45, o i7 tem desempenho significativamente maior do
que o do Atom. Todos os benchmarks são pelo menos quatro vezes mais rápidos no i7,
dois benchmarks SPECFP são mais de 10 vezes mais rápidos, e um benchmark SPECINT
é executado mais de oito vezes mais rápido!
Como a razão das taxas de clock desses dois processadores é de 1,6, a maior parte da
vantagem vem de um CPI muito menor para o i7: um fator de 2,8 para os benchmarks
Java, um fator de 3,1 para os benchmarks SPECINT e um fator de 4,3 para os benchmarks
SPECFP.
Mas o consumo médio de energia para o i7 está pouco abaixo de 43 W, enquanto o consumo médio de energia do Atom é de 4,2 W, ou cerca de um décimo da energia! Combinar
o desempenho e a energia leva a uma vantagem na eficiência energética para o Atom, que
FIGURA 3.45 O desempenho relativo e a eficiência energética para um conjunto de benchmarks de thread único mostram que o i7 920
é 4-10 vezes mais rápido do que o Atom 230, porém cerca de duas vezes menos eficiente em termos de potência, em média!
O desempenho é mostrado nas colunas como o i7 em relação ao Atom, como tempo de execução (i7)/tempo de execução (Atom). A energia é mostrada
pela linha como energia(Atom)/energia(i7). O i7 nunca vence o Atom em eficiência energética, embora seja essencialmente tão bom em quatro
benchmarks, três dos quais são de ponto flutuante. Os dados mostrados aqui foram coletados por Esmaelizadeh et al. (2011). Os benchmarks SPEC foram
compilados com otimização sobre o uso do compilador-padrão Intel, enquanto os benchmarks Java usam o Sun (Oracle) Hotspot Java VM. Somente
um núcleo está ativo no i7, e o resto está em modo de economia de energia profunda. O Turbo Boost é usado no i7, que aumenta sua vantagem
de desempenho mas diminui levemente sua eficiência energética relativa.
211
212
CAPÍTULO 3 :
Paralelismo em nível de instrução e sua exploração
costuma ser mais de 1,5-2 vezes melhor! Essa comparação de dois processadores usando
a mesma tecnologia fundamental torna claro que as vantagens do desempenho de um
superescalar agressivo com escalonamento dinâmico e especulação vêm com significativa
desvantagem em termos de eficiência energética.
Falácia. Processadores com CPIs menores sempre serão mais rápidos.
Falácia. Processadores com taxas de clock mais rápidas sempre serão mais rápidos.
A chave é que o produto da CPI e a frequência do clock determinam o desempenho. Com
alta frequência do clock obtida por um pipelining longo, a CPU deve manter um CPI baixo
para obter o benefício total do clock mais rápido. De modo similar, um processador simples com alta frequência do clock mas CPI baixo pode ser mais lento.
Como vimos na falácia anterior, o desempenho e a eficiência energética podem divergir
significativamente entre processadores projetados para ambientes diferentes, mesmo
quando eles têm o mesmo ISA. Na verdade, grandes diferenças em desempenho podem
aparecer até dentro de uma família de processadores da mesma companhia, projetados
todos para aplicações de alto nível. A Figura 3.46 mostra o desempenho para inteiros e
PF de duas implementações diferentes da arquitetura x86 da Intel, além de uma versão
da arquitetura Itanium, também da Intel.
O Pentium 4 foi o processador com pipeline mais agressivo já construído pela Intel. Ele
usava um pipeline com mais de 20 estágios, tinha sete unidades funcionais e micro-ops
em cache no lugar de instruções x86. Seu desempenho relativamente inferior, dada a
implementação agressiva, foi uma indicação clara de que a tentativa de explorar mais ILP
(podia haver facilmente 50 operações em ação) havia falhado. O consumo de energia do
Pentium era similar ao do i7, embora sua contagem de transistores fosse menor, já que
as caches primárias tinham metade do tamanho das do i7, e incluía somente uma cache
secundária de 2 MB, sem cache terciária.
O Intel Itanium é uma arquitetura no estilo VLIW que, apesar da redução potencial em
complexidade em comparação aos superescalares escalonados dinamicamente, nunca
atingiu taxas de clock competitivas versus os processadores x86 da linha principal (embora
ele pareça alcançar um CPI geral similar ao do i7). Ao examinar esses resultados, o leitor
deve ter em mente que eles usam diferentes tecnologias de implementação, o que dá ao i7
uma vantagem em termos de velocidade de transistor, portando frequência do clock para
um processador com pipeline equivalente. Mesmo assim, a grande variação no desempenho — mais de três vezes entre o Pentium e o i7 — é surpreendente. A próxima armadilha
explica de onde vem uma significativa parte dessa vantagem.
Armadilha. Às vezes maior e mais “burro” é melhor.
No início dos anos 2000, grande parte da atenção estava voltada para a construção de
processadores agressivos na exploração de ILP, incluindo a arquitetura Pentium 4, que
FIGURA 3.46 Três diferentes processadores da Intel variam muito.
Embora o processador Itanium tenha dois núcleos e o i7 tenha quatro, somente um núcleo é usado nesses benchmarks.
3.15
Comentários finais: o que temos à frente?
usava o pipeline mais longo já visto em um microprocessador, e o Intel Itanium, que tinha
a mais alta taxa de pico de despacho por clock já vista. O que se tornou rapidamente claro
foi que, muitas vezes, a maior limitação em explorar ILP era o sistema de memória. Embora
pipelines especulativos fora de ordem fossem razoavelmente bons em ocultar uma fração
significativa das penalidades de falha, 10-15 ciclos para uma falha de primeiro nível, muitas
vezes eles faziam muito pouco para ocultar as penalidades para uma falha de segundo nível,
que, quando ia para a memória principal, provavelmente era de 50-100 ciclos de clock.
O resultado foi que esses projetos nunca chegaram perto de atingir o pico de throughput
de instruções, apesar do grande número de transistores e das técnicas extremamente sofisticadas e inteligentes. A próxima seção discute esse dilema e o afastamento dos esquemas
de ILP mais agressivos em favor dos núcleos múltiplos, mas houve outra mudança, que é
um exemplo dessa armadilha. Em vez de tentar ocultar ainda mais latências de memória
com ILP, os projetistas simplesmente usaram os transistores para construir caches muito
maiores. O Itanium 2 e o i7 usam caches de três níveis em comparação à cache de dois
níveis do Pentium 4. Não é necessário dizer que construir caches maiores é muito mais
fácil do que projetar o pipeline com mais de 20 estágios do Pentium 4 e, a partir dos dados
da Figura 3.46, parece ser também mais eficaz.
3.15
COMENTÁRIOS FINAIS: O QUE TEMOS À FRENTE?
No início de 2000, o foco era explorar o paralelismo em nível de instrução. A Intel estava prestes a lançar o Itanium, processador escalonado estaticamente com alta taxa de
despacho que contava com uma técnica semelhante à VLIW, com suporte intensivo do
compilador. Os processadores MIPS, Alpha e IBM, com execução especulativa escalonada
dinamicamente, estavam na segunda geração e se tornando maiores e mais rápidos. O
Pentium 4, que usava escalonamento especulativo, também havia sido anunciado naquele
ano com sete unidades funcionais e um pipeline com mais de 20 estágios de comprimento.
Mas havia algumas nuvens pesadas no horizonte.
Pesquisas como a abordada na Seção 3.10 mostravam que levar o ILP muito adiante seria
extremamente difícil, e, embora as taxas de throughput de pico de instruções tivessem
aumentado em relação aos primeiros processadores especulativos de 3-5 anos antes, as
taxas sustentáveis de execução de instrução estavam aumentando muito mais lentamente.
Os cinco anos seguintes disseram muito. O Itanium acabou sendo um bom processador
de PF, mas apenas um processador de inteiros medíocre. A Intel ainda produz a linha, mas
não há muitos usuários; a frequência do clock está aquém da frequência dos processadores
da linha principal da Intel, e a Microsoft não suporta mais o conjunto de instruções. O
Intel Pentium 4, embora tivesse bom desempenho, acabou sendo ineficiente em termos
de desempenho/watt (ou seja, uso de energia), e a complexidade do processador tornou
improvável que mais avanços fossem possíveis aumentando a taxa de despacho. Havia
chegado o fim de uma estrada de 20 anos atingindo novos níveis de desempenho em microprocessadores explorando o ILP. O Pentium 4 foi amplamente reconhecido como tendo
ido além do ponto, e a agressiva e sofisticada microarquitetura Netburst foi abandonada.
Em 2005, a Intel e os demais fabricantes principais haviam renovado sua abordagem
para se concentrar em núcleos múltiplos. Um desempenho maior seria alcançado através
do paralelismo em nível de thread, em vez de paralelismo em nível de instrução, e a responsabilidade por usar o processador com eficiência mudaria bastante do hardware para
o software e para o programador. Essa mudança foi a mais significativa na arquitetura de
processador desde os primeiros dias do pipelining e do paralelismo em nível de instrução,
cerca de 25 anos antes.
213
214
CAPÍTULO 3 :
Paralelismo em nível de instrução e sua exploração
No mesmo período, os projetistas começaram a explorar o uso de um paralelismo em
nível de dados como outra abordagem para obter desempenho. As extensões SIMD permitiram aos microprocessadores de desktops e servidores atingir aumentos moderados
de desempenho para gráficos e funções similares. E o que é mais importante: as GPUs
buscaram o uso agressivo de SIMD, atingindo vantagens significativas de desempenho
para aplicações com grande paralelismo em nível de dados. Para aplicações científicas, tais
técnicas representam uma alternativa viável para o mais geral — porém menos eficiente
— paralelismo em nível de thread explorado nos núcleos múltiplos. O Capítulo 4 vai
explorar esses desenvolvimentos no uso de paralelismo em nível de dados.
Muitos pesquisadores anteviram uma grande redução no uso de ILP, prevendo que processadores superescalares com dois despachos e números maiores de núcleos seriam o futuro.
Entretanto, as vantagens de taxas de despacho ligeiramente maiores e a capacidade de
escalonamento dinâmico especulativo para lidar com eventos imprevisíveis, como falhas
de cache de primeiro nível, levaram um ILP moderado a ser o principal bloco de construção nos projetos de núcleos múltiplos. A adição do SMT e a sua eficácia (tanto em
desempenho quanto em eficiência energética) concretizaram ainda mais a posição das
técnicas especulativas, fora de ordem, de despacho moderado. De fato, mesmo no mercado
de embarcados, os processadores mais novos (como o ARM Cortex-A9) introduziram escalonamento dinâmico, especulação e taxas maiores de despacho.
É muito improvável que os futuros processadores tentem melhorar significativamente
a largura de despacho. É simplesmente muito ineficiente, tanto do ponto de vista da
utilização de silício quanto da eficiência energética. Considere os dados da Figura 3.47,
que mostra os quatro processadores mais recentes da série IBM Power. Ao longo da década
passada, houve uma modesta melhoria no suporte a ILP nos processadores Power, mas a
parte dominante do aumento no número de transistores (um fator de quase 7 do Power4
para o Power7), aumentou as caches e o número de núcleos por die. Até mesmo a expansão no suporte a SMT parece ser mais um foco do que um aumento no throughput
de ILP. A estrutura ILP do Power4 para o Power7 foi de cinco despachos para seis, de oito
unidades funcionais para 12 (mas sem aumento das duas unidades de carregamento/
armazenamento originais), enquanto o suporte a SMT foi de não existente para quatro
threads/processador. Parece claro que, mesmo para o processador ILP mais avançado em
2011 (o Power7), o foco foi deslocado para além do paralelismo em nível de instrução.
FIGURA 3.47 Características de quatro processadores IBM Power.
Todos foram escalonados dinamicamente, exceto o Power6, que é estático, e em ordem, e todos os processadores suportam dois pipelines de
carregamento/armazenamento. O Power6 tem as mesmas unidades funcionais que o Power5, exceto por uma unidade decimal. O Power7 usa DRAM para
a cache L3.
Estudos de caso e exercícios por Jason D. Bakos e Robert P. Colwell
Os Capítulos 4 e 5 enfocam técnicas que exploram o paralelismo em nível de dados e o
paralelismo em nível de thread.
3.16
PERSPECTIVAS HISTÓRICAS E REFERÊNCIAS
A Seção L.5 (disponível on-line) contém uma análise sobre o desenvolvimento do pipelining e do paralelismo em nível de instrução. Apresentamos diversas referências para leitura
adicional e exploração desses tópicos. A Seção L.5 cobre o Capítulo 3 e o Apêndice H.
ESTUDOS DE CASO E EXERCÍCIOS POR JASON D. BAKOS
E ROBERT P. COLWELL
Estudo de caso: explorando o impacto das técnicas
de microarquiteturas
Conceitos ilustrados por este estudo de caso
j
j
j
j
j
Escalonamento básico de instrução, reordenação, despacho
Múltiplo despacho e hazards
Renomeação de registrador
Execução fora de ordem e especulativa
Onde gastar recursos fora de ordem
Você está encarregado de projetar uma nova microarquitetura de processador e tentando
descobrir como alocar melhor seus recursos de hardware. Quais das técnicas de hardware
e software aprendidas neste capítulo deverá aplicar? Você tem uma lista de latências para
as unidades funcionais e para a memória, além de algum código representativo. Seu chefe
foi um tanto vago com relação aos requisitos de desempenho do seu novo projeto, mas
você sabe, por experiência, que, com tudo o mais sendo igual, mais rápido geralmente é
melhor. Comece com o básico. A Figura 3.48 apresenta uma sequência de instruções e a
lista de latências.
3.1
[10] <1.8, 3.1, 3.2> Qual seria o desempenho de referência (em ciclos, por
iteração do loop) da sequência de código da Figura 3.48 se nenhuma nova
execução de instrução pudesse ser iniciada até que a execução da instrução
anterior tivesse sido concluída? Ignore a busca e a decodificação de front-end.
FIGURA 3.48 Código e latências para os Exercícios 3.1 a 3.6.
215
216
CAPÍTULO 3 :
Paralelismo em nível de instrução e sua exploração
3.2
3.3
3.4
3.5
3.6
Considere, por enquanto, que a execução não fique em stall por falta da próxima
instrução, mas somente uma instrução/ciclo pode ser enviada. Considere que o
desvio é tomado e que existe um slot de atraso de desvio de um ciclo.
[10] <1.8, 3.1, 3.2> Pense no que realmente significam os números de latência
— eles indicam o número de ciclos que determinada função exige para produzir
sua saída, e nada mais. Se o pipeline ocasionar stalls para os ciclos de latência de
cada unidade funcional, pelo menos você terá a garantia de que qualquer par de
instruções de ponta a ponta (um “produtor” seguido por um “consumidor”) será
executado corretamente. Contudo, nem todos os pares de instruções possuem um
relacionamento produtor/consumidor. Às vezes, duas instruções adjacentes não
têm nada a ver uma com a outra. Quantos ciclos o corpo do loop na sequência de
código da Figura 3.48 exigiria se o pipeline detectasse verdadeiras dependências
de dados e só elas ficassem em stall, em vez de tudo ficar cegamente em stall
só porque uma unidade funcional está ocupada? Mostre o código com <stall>
inserido onde for necessário para acomodar as latências proteladas. (Dica: Uma
instrução com latência “ + 2” precisa que dois ciclos de <stall> sejam inseridos
na sequência de código. Pense desta maneira: uma instrução de um ciclo possui
latência 1 + 0, significando zero estado de espera extra. Assim, a latência 1 + 1
implica um ciclo de stall; latência 1 + N possui N ciclos de stall extras.)
[15] <3.6, 3.7> Considere um projeto de múltiplo despacho. Suponha que você
tenha dois pipelines de execução, cada qual capaz de iniciar a execução de uma
instrução por ciclo, além de largura de banda de busca/decodificação suficiente
no front-end, de modo que sua execução não estará em stall. Considere que os
resultados podem ser encaminhados imediatamente de uma unidade de execução
para outra ou para si mesma. Considere, ainda, que o único motivo para um
pipeline de execução protelar é observar uma dependência de dados verdadeira.
Quantos ciclos o loop exigiria?
[10] <3.6, 3.7> No projeto de múltiplo despacho do Exercício 3.3, você pode ter
reconhecido algumas questões sutis. Embora os dois pipelines tenham exatamente
o mesmo repertório de instruções, elas não são idênticas nem intercambiáveis,
pois existe uma ordenação implícita entre elas que precisa refletir a ordenação das
instruções no programa original. Se a instrução N + 1 iniciar sua execução na pipe
de execução 1 ao mesmo tempo que a instrução N iniciar na pipe 0, e N + 1 exigir
uma latência de execução mais curta que N, então N + 1 será concluída antes de
N (embora a ordenação do programa tivesse indicado de outra forma). Cite pelo
menos duas razões pelas quais isso poderia ser arriscado e exigiria considerações
especiais na microarquitetura. Dê um exemplo de duas instruções do código da
Figura 3.48 que demonstrem esse hazard.
[20] <3.7> Reordene as instruções para melhorar o desempenho do código da
Figura 3.48. Considere a máquina de dois pipelines do Exercício 3.3 e que os
problemas de término fora de ordem do Exercício 3.4 foram tratados com sucesso.
Por enquanto, preocupe-se apenas em observar as dependências de dados verdadeiras
e as latências da unidade funcional. Quantos ciclos o seu código reordenado utiliza?
3.6 [10/10/10] <3.1, 3.2> Cada ciclo que não inicia uma nova operação em um
pipeline é uma oportunidade perdida, no sentido de que seu hardware não está
“acompanhando seu potencial”.
a. [10] <3.1, 3.2> Em seu código reordenado do Exercício 3.5, que fração de
todos os ciclos, contando ambos os pipelines, foi desperdiçada (não iniciou
uma nova operação)?
b. [10] <3.1, 3.2> O desdobramento de loop é uma técnica-padrão do
compilador para encontrar mais paralelismo no código, a fim de minimizar as
Estudos de caso e exercícios por Jason D. Bakos e Robert P. Colwell
3.7
3.8
oportunidades perdidas para desempenho. Desdobre duas iterações do loop
em seu código reordenado do Exercício 3.5.
c. [10] <3.1, 3.2> Que ganho de velocidade você obteve? (Neste exercício,
basta colorir as instruções da iteração N + 1 de verde para distingui-las das
instruções da iteração N; se você estivesse realmente desdobrando o loop, teria
de reatribuir os registradores para impedir colisões entre as iterações.)
[15] <3.1> Os computadores gastam a maior parte do tempo nos loops,
de modo que as iterações de loop são ótimos locais para encontrar
especulativamente mais trabalho para manter os recursos da CPU ocupados.
Porém, nada é tão fácil; o compilador emitiu apenas uma cópia do código
desse loop, de modo que, embora múltiplas iterações estejam tratando dados
distintos, elas parecerão usar os mesmos registradores. Para evitar a colisão de
uso de registrador por múltiplas iterações, renomeamos seus registradores. A
Figura 3.49 mostra o código de exemplo que gostaríamos que nosso hardware
renomeasse. Um compilador poderia ter simplesmente desdobrado o loop e
usado registradores diferentes para evitar conflitos, mas, se esperarmos que nosso
hardware desdobre o loop, ele também terá de fazer a renomeação de registrador.
Como? Considere que seu hardware tenha um pool de registradores temporários
(vamos chamá-los de registradores T e considerar que existam 64 deles, de T0
a T63) que ele pode substituir por registradores designados pelo compilador.
Esse hardware de renomeação é indexado pela designação do registrador de
origem, e o valor na tabela é o registrador T do último destino que designou esse
registrador. (Pense nesses valores de tabela como produtores e nos registradores
de origem como consumidores; não importa muito onde o produtor coloca
seu resultado, desde que seus consumidores possam encontrá-lo.) Considere a
sequência de código na Figura 3.49. Toda vez que você encontrar um registrador
de destino no código, substitua o próximo T disponível, começando com T9.
Depois atualize todos os registradores de origem adequadamente, de modo que as
dependências de dados verdadeiras sejam mantidas. Mostre o código resultante.
(Dica: Ver a Figura 3.50).
[20] <3.4> O Exercício 3.7 explorou a renomeação simples de registradores:
quando o renomeador de registrador do hardware vê um registrador de
origem, substitui o registrador T de destino da última instrução a ter designado
esse registrador de origem. Quando a tabela de renomeação encontra um
FIGURA 3.49 Exemplo de código para prática de renomeação de registrador.
FIGURA 3.50 Dica: Saída esperada do renomeamento de registrador.
217
218
CAPÍTULO 3 :
Paralelismo em nível de instrução e sua exploração
3.9
registrador de destino, ela o substitui pelo próximo T disponível. Mas os
projetos superescalares precisam lidar com múltiplas instruções por ciclo
de clock em cada estágio na máquina, incluindo a renomeação de registrador.
Um processador escalar simples, portanto, pesquisaria os mapeamentos de
registrador de origem para cada instrução e alocaria um novo mapeamento de
destino por ciclo de clock. Os processadores superescalares precisam ser capazes
de fazer isso também, mas teriam de garantir que quaisquer relacionamentos
destino para origem entre as duas instruções concorrentes fossem tratados
corretamente. Considere a sequência de código de exemplo na Figura 3.51 e
que gostaríamos de renomear simultaneamente as duas primeiras instruções.
Considere ainda que os próximos dois registradores T disponíveis a serem usados
sejam conhecidos no início do ciclo de clock em que essas duas instruções
estão sendo renomeadas. Conceitualmente, o que queremos é que a primeira
instrução faça suas pesquisas na tabela de renomeação e depois atualize a tabela
por seu registrador T de destino. Depois, a segunda instrução faria exatamente
a mesma coisa e, portanto, qualquer dependência entre instruções seria tratada
corretamente. Mas não existe tempo suficiente para escrever essa designação de
registrador T na tabela de renomeação e depois pesquisá-la novamente para a
segunda instrução, tudo no mesmo ciclo de clock. Em vez disso, essa substituição
de registrador precisa ser feita ao vivo (em paralelo com a atualização da tabela
de renomeação de registrador). A Figura 3.52 mostra um diagrama de circuito
usando multiplexadores e comparadores que conseguirá fazer a renomeação
de registrador necessária no ato. Sua tarefa é mostrar o estado ciclo por ciclo da
tabela de renomeação para cada instrução do código mostrado na Figura 3.51.
Considere que a tabela começa com cada entrada igual ao seu índice (T0 = 0;
T1 = 1, ...).
[5] <3.4> Se você já se confundiu com relação ao que um renomeador de
registrador precisa fazer, volte ao código assembly que está executando e pergunte
a si mesmo o que deve acontecer para que o resultado correto seja obtido. Por
exemplo, considere uma máquina superescalar de três vias renomeando estas três
instruções simultaneamente:
3.10 [20] <3.4, 3.9> Projetistas de palavras de instrução muito longas (VLIW)
têm algumas escolhas básicas a fazer com relação a regras de arquitetura para
uso de registrador. Suponha que um VLIW seja projetado com pipelines de
execução com autodrenagem: quando uma operação for iniciada, seus resultados
aparecerão no registrador de destino no máximo L ciclos mais tarde (onde L
é a latência da operação). Nunca existem registradores suficientes, de modo
que há uma tentativa de espremer o uso máximo de registradores que existem.
Considere a Figura 3.53. Se os loads tiverem uma latência de 1 + 2 ciclos,
FIGURA 3.51 Exemplo de código para renomeação de registrador superescalar.
Estudos de caso e exercícios por Jason D. Bakos e Robert P. Colwell
FIGURA 3.52 Tabela de renomeação e lógica de substituição em ação para máquinas superescalares.
(Observe que src é a fonte e dest é o destino.)
FIGURA 3.53 Código VLIW de exemplo com dois adds, dois loads e dois stalls.
desdobre esse loop uma vez e mostre como um VLIW capaz de dois loads e dois
adds por ciclo pode usar o número mínimo de registradores, na ausência de
quaisquer interrupções ou stalls no pipeline. Dê exemplo de um evento que, na
presença de pipelines de autodrenagem, possa romper essa canalização e gerar
resultados errados.
3.11 3.11 [10/10/10] <3.3> Considere uma microarquitetura de único pipeline em
cinco estágios (load, decodificação, execução, memória, escrita) e o código na
Figura 3.54. Todas as operações são de um ciclo, exceto LW e SW, que são de 1 + 2
ciclos, e os desvios são de 1 + 1 ciclo. Não existe adiantamento. Mostre as fases de
cada instrução por ciclo de clock para uma iteração do loop.
a. [10] <3.3> Quantos ciclos de clock por iteração do loop são perdidos para o
overhead de desvio?
219
220
CAPÍTULO 3 :
Paralelismo em nível de instrução e sua exploração
FIGURA 3.54 Código de loop para o Exercício 3.11.
b. [10] <3.3> Considere um previsor de desvio estático capaz de reconhecer um
desvio ao contrário no estágio de decodificação. Quantos ciclos de clock são
desperdiçados no overhead de desvio?
c. [10] <3.3> Considere um previsor de desvio dinâmico. Quantos ciclos são
perdidos em uma previsão correta?
3.12 [15/20/20/10/20] <3.4, 3.7, 3.14> Vamos considerar o que o escalonamento
dinâmico poderia conseguir aqui. Considere uma microarquitetura como a da
Figura 3.55. Suponha que as ALUs possam fazer todas as operações aritméticas
(MULTD, DIVD, ADDD, ADDI, SUB) e desvios, e que a estação de reserva (RS)
possa enviar no máximo uma operação para cada unidade funcional por ciclo
(uma operação para cada ALU mais uma operação de memória para a unidade
de LD/ST).
a. [15] <3.4> Suponha que todas as instruções da sequência na Figura 3.48
estejam presentes no RS, sem que qualquer renomeação precise ser feita.
Destaque quaisquer instruções no código onde a renomeação de registrador
melhoraria o desempenho. (Dica: Procure hazards RAW e WAW. Considere
as mesmas latências de unidade funcional da Figura 3.48.)
b. [20] <3.4> Suponha que a versão com registrador renomeado do código
do item a esteja residente na RS no ciclo de clock N, com latências conforme
indicado na Figura 3.48. Mostre como a RS deverá enviar essas instruções
fora de ordem, clock por clock, para obter o desempenho ideal nesse código.
(Considere as mesmas restrições de RS do item a. Considere também que
os resultados precisam ser escritos na RS antes que estejam disponíveis para
uso, ou seja, sem bypassing.) Quantos ciclos de clock a sequência de código
utiliza?
c. [20] <3.4> O item b permite que a RS tente escalonar essas instruções de
forma ideal. Mas, na realidade, a sequência de instruções inteira — em que
FIGURA 3.55 Microarquitetura fora de ordem.
Estudos de caso e exercícios por Jason D. Bakos e Robert P. Colwell
estamos interessados — normalmente não está presente na RS. Em vez disso,
diversos eventos apagam a RS e, quando novos fluxos de sequência de código
entram no decodificador, a RS precisa enviar o que ela tem. Suponha que
a RS esteja vazia. No ciclo 0, as duas primeiras instruções dessa sequência
com registrador renomeado aparecem na RS. Considere que é necessário um
ciclo de clock para enviar qualquer operação e que as latências da unidade
funcional sejam como apareceram no Exercício 3.2. Considere ainda que o
front-end (decodificador/renomeador de registrador) continuará a fornecer
duas novas instruções por ciclo de clock. Mostre a ordem, ciclo por ciclo,
de despacho da RS. Quantos ciclos de clock essa sequência de código exige
agora?
d. [10] <3.14> Se você quisesse melhorar os resultados do item c, quais teriam
ajudado mais: 1) outra ALU; 2) outra unidade de LD/ST; 3) bypassing total
de resultados da ALU para operações subsequentes; 4) cortar a latência mais
longa ao meio? Qual é o ganho de velocidade?
e. [20] <3.7> Agora vamos considerar a especulação, o ato de apanhar,
decodificar e executar além de um ou mais desvios condicionais. Nossa
motivação para fazer isso é dupla: o escalonamento de despacho que vimos
no item c tinha muitas nops, e sabemos que os computadores gastam a
maior parte do seu tempo executando loops (implicando que o desvio
de volta ao topo do loop é bastante previsível). Os loops nos dizem onde
encontrar mais trabalho a fazer; nosso escalonamento de despacho escasso
sugere que temos oportunidades para fazer algum trabalho mais cedo do que
antes. No item d, você descobriu o caminho crítico através do loop. Imagine
gerar uma segunda cópia desse caminho no escalonamento que você obteve
no item b. Quantos ciclos de clock a mais seriam necessários para realizar o
trabalho de dois loops (supondo que todas as instruções estejam residentes
na RS)? (Considere que todas as unidades funcionais sejam totalmente
canalizadas.)
Exercícios
3.13 [25] <3.13> Neste exercício, você vai explorar os trade-offs de desempenho
entre três processadores que empregam diferentes tipos de multithreading.
Cada um desses processadores é superescalar, o uso pipelines em ordem requer
um stall fixo de três ciclos seguindo todos os loads e desvios, e tem caches L1
idênticas. Instruções do mesmo thread enviados no mesmo ciclo são lidas na
ordem do programa e não devem conter quaisquer dependências de dados ou
controle.
j
O processador A é uma arquitetura superescalar SMT capaz de enviar até duas
instruções por ciclo de dois threads.
j
O processador B é uma arquitetura MT fina capaz de enviar até quatro
instruções por ciclo de um único thread e muda de thread a qualquer stall de
pipeline.
j
O processador C é uma arquitetura MT grossa capaz de enviar até oito
instruções por ciclo de um thread único e muda de thread a cada falha de
cache L1.
Nossa aplicação é um buscador de lista que verifica uma região de memória à
procura de um valor específico em R9, na faixa de endereços especificadas em R16
e R17. Ela é paralelizada dividindo o espaço de busca em quatro blocos contíguos
de tamanho igual e designando um thread de busca para cada bloco (gerando
221
222
CAPÍTULO 3 :
Paralelismo em nível de instrução e sua exploração
quatro threads). A maior parte do runtime de cada thread é gasto no seguinte
corpo de loop:
Suponha o seguinte:
j
É usada uma barreira para garantir que todos os threads comecem
simultaneamente.
j
A primeira falha de cache L1 ocorre depois de duas iterações do loop.
j
Nenhum dos desvios BEQAL é tomado.
j
O BLT é sempre tomado.
j
Todos os três processadores escalonam threads de modo round-robin.
Determine quantos ciclos são necessários para cada processador completar as duas
primeiras iterações do loop.
3.14 [25/25/25] <3.2, 3.7> Neste exercício, examinamos como técnicas de software
podem extrair paralelismo de nível de instrução (ILP) em um loop comum
vetorial. O loop a seguir é o chamado loop DAXPY (aX mais Y de precisão dupla)
e é a operação central na eliminação gaussiana. O código a seguir implementa a
operação DAPXY, Y = aX + Y, para um vetor de comprimento 100. Inicialmente,
R1 é configurado para o endereço de base do array X e R2 e configurado para o
endereço de base de Y:
Considere as latências de unidade funcional mostradas na tabela a seguir.
Considere também um desvio atrasado de um ciclo que se resolve no estágio ID
e que os resultados são totalmente contornados.
Estudos de caso e exercícios por Jason D. Bakos e Robert P. Colwell
Instrução produzindo o resultado
Instrução usando
o resultado
Latência em ciclos
de clock
Multiplicação de PF
Op ALU PF
6
Soma de PF
Op ALU PF
4
Multiplicação de PF
Store de PF
5
Soma de PF
Store de PF
4
Operações com inteiros
e todos os loads
Any
2
a. [25] <3.2> Considere um pipeline de despacho único. Mostre como
seria o loop não escalonado pelo compilador e depois escalonado pelo
compilador, tanto para operação de ponto flutuante como para atrasos
de desvio, incluindo quaisquer stalls ou ciclos de clock ociosos. Qual
é o tempo de execução (em ciclos) por elemento do vetor resultante,
Y, não escalonado e escalonado? Quão mais rápido o clock deveria ser
para que o hardware do processador pudesse igualar sozinho a melhoria
de desempenho atingida pelo compilador de escalonamento? (Ignore
possíveis efeitos da maior velocidade de clock sobre o desempenho do
sistema.)
b. [25] <3.2> Considere um pipeline de despacho único. Expanda o loop
quantas vezes forem necessárias para escaloná-lo sem nenhum stall, ocultando
as instruções de overhead do loop. Quantas vezes o loop deve ser expandido?
Mostre o escalonamento de instruções. Qual é o tempo de execução por
elemento do resultado?
c. [25] <3.7> Considere um processador VLIW com instruções que contêm
cinco operações, como mostrado na Figura 3.16. Vamos comparar dois graus
de expansão de loop. Primeiro, expanda o loop seis vezes para extrair ILP e
escaloná-lo sem nenhum stall (ou seja, ciclos de despacho completamente
vazios), ocultando as instruções de overhead de loop. Então, repita o processo,
mas expanda o loop 10 vezes. Ignore o slot de atraso de desvio. Mostre os
dois escalonamentos. Qual é o tempo de instrução, por elemento, do vetor
resultado para cada escalonamento? Que porcentagem dos slots de operação
é usada em cada escalonamento? Em quanto o tamanho do código difere
nos dois escalonamentos? Qual é a demanda total do registrador para esses
escalonamentos?
3.15 [20/20] <3.4, 3.5, 3.7, 3.8> Neste exercício, vamos examinar como variações no
algoritmo de Tomasulo se comportam quando executam o loop do Exercício 3.14.
As unidades funcionais (FUs) são descritas na tabela a seguir.
Tipo de FU
Ciclos em EX
Número de FUs
Número de estações
de reserva
Inteiro
1
1
5
Somador de PF
10
1
3
Multiplicador de PF
15
1
2
Considere o seguinte:
j
As unidades funcionais não são pipelined.
j
Não há adiandamento entre as unidades funcionais; os resultados são
comunicados pelo barramento comum de dados (CDB).
223
224
CAPÍTULO 3 :
Paralelismo em nível de instrução e sua exploração
O estágio de execução (EX) realiza o cálculo efetivo de endereço e os acessos à
memória para loads e stores. Assim, o pipeline é IF/ID/IS/EX/WB.
j
Loads requerem um ciclo de clock.
j
Os estágios de resultados de despacho (IS) e write-back (WB) requerem um
ciclo de clock cada um.
j
Há cinco slots de buffer de carregamento e cinco slots de buffer de
armazenamento.
j
Considere que a instrução Branch on Not Equal do Zero (BNEZ) requer um
ciclo de clock.
a. [20] <3.4, 3.5> Para este problema, use o pipeline MIPS de Tomasulo
de despacho único da Figura 3.6 com as latências de pipeline da tabela
anterior. Mostre o número de ciclos de stall para cada instrução e em que
ciclo de clock cada uma delas começa a ser executada (ou seja, entra no seu
primeiro ciclo EX) para três iterações do loop. Quantos ciclos cada iteração
de loop leva? Dê sua resposta em forma de tabela com os seguintes títulos
de coluna:
j Iteração (número da iteração do loop)
j Instrução
j Envia (ciclo em que a instrução é enviada)
j Executa (ciclo em que a instrução é executada)
j Acesso à memória (ciclo em que a memória é acessada)
j CDB de gravação (ciclo em que o resultado é gravado no CDB)
j Comentário (descrição de qualquer evento que a instrução esteja
aguardando)
Mostre três iterações do loop na sua tabela. Você pode ignorar a primeira
instrução.
b. [20] <3.7, 3.8> Repita o procedimento do item a, mas desta vez considere um
algoritmo de Tomasulo de dois despachos e uma unidade de ponto flutuante
totalmente pipelined (FPU).
3.16 [10] <3.4> O algoritmo de Tomasulo apresenta uma desvantagem: somente
um resultado pode ser computado por clock por CDB. Use a configuração de
hardware e latências da questão anterior e encontre uma sequência de código de
não mais de 10 instruções onde o algoritmo de Tomasulo deve sofrer stall, devido
à contenção de CDB. Indique onde isso ocorre na sua sequência.
3.17 [20] <3.3> Um previsor de desvio de correlação (m,n) usa o comportamento
dos m desvios executados mais recentemente para escolher entre 2m previsores,
cada qual previsor de n bits. Um previsor local de dois níveis funciona de modo
similar, mas só rastreia o comportamento passado de cada desvio individual para
prever o comportamento futuro.
Existe um trade-off de projeto envolvido com tais previsores. Previsores de
correlação requerem pouca memória para histórico, o que permite a eles manter
previsores de 2 bits para um grande número de desvios individuais (reduzindo a
probabilidade das instruções de desvio reutilizarem o mesmo previsor), enquanto
previsores locais requerem substancialmente mais memória para manter um
histórico e, assim, são limitados a rastrear um número relativamente menor de
instruções de desvio. Neste exercício, considere um previsor de correlação (1,2)
que pode rastrear quatro desvios (requerendo 16 bits) em comparação com um
previsor local (1.2) que pode rastrear dois desvios usando a mesma quantidade de
memória. Para os resultados de desvio a seguir, forneça cada previsão, a entrada
de tabela usada para realizar a previsão, quaisquer atualizações na tabela como
resultado da previsão e a taxa final de previsões incorretas de cada previsor.
j
Estudos de caso e exercícios por Jason D. Bakos e Robert P. Colwell
Suponha que todos os desvios até este ponto tenham sido tomados. Inicialize
cada previsor com o seguinte:
Previsor de correlação
Entrada
Desvio
Último resultado
Previsão
0
0
T
T com uma previsão incorreta
1
0
NT
NT
2
1
T
NT
3
1
NT
T
4
2
T
T
5
2
NT
T
6
3
T
NT com uma previsão
incorreta
7
3
NT
NT
Previsor local
Entrada
Desvio
Últimos dois resultados
(o da direita é o mais recente)
Previsão
0
0
T,T
T com uma previsão incorreta
1
0
T,NT
NT
2
0
NT,T
NT
3
0
NT
T
4
1
T,T
T
5
1
T,NT
T com uma previsão incorreta
6
1
NT,T
NT
7
1
NT,NT
NT
Desvio PC (endereço de palavra)
Resultado
454
T
543
NT
777
NT
543
NT
777
NT
454
T
777
NT
454
T
543
T
3.18 [10] <3.9> Considere um processador altamente pipelined para o qual
tenhamos implementado um buffer de alvos de desvio somente para os desvios
condicionais. Considere que a penalidade de previsão incorreta é sempre de cinco
ciclos e a penalidade de falha de buffer é sempre de três ciclos. Considere uma
taxa de acerto de 90%, precisão de 90% e frequência de desvio de 15%. Quão
mais rápido é o processador com o buffer de alvo de desvio comparado a um
225
226
CAPÍTULO 3 :
Paralelismo em nível de instrução e sua exploração
processador que tenha uma penalidade de desvio fixa de dois ciclos? Considere
um ciclo-base de clock por instrução (CPI) sem stalls de desvio de um.
3.19 [10/5] <3.9> Considere um buffer de alvos de desvio que tenha penalidades
de zero, dois e dois ciclos de clock para previsão correta de desvio condicional,
previsão incorreta e uma falha de buffer, respectivamente. Considere também
um projeto de buffer de alvo de desvio que distingue desvios condicionais e não
condicionais, armazenando os endereços de alvo para um desvio condicional e a
instrução-alvo para um desvio não condicional.
a. [10] <3.9> Qual é a penalidade em ciclos de clock quando um desvio não
condicional é encontrado no buffer?
b. [10] <3.9> Determine a melhoria da dobra de desvios para desvios não
condicionais. Suponha uma taxa de acerto de 90%, uma frequência de desvio
não condicional de 5% e uma penalidade de dois ciclos para uma falha de
buffer. Quanta melhoria é obtida por essa modificação? Quão alta deve ser a
taxa de acerto para essa melhoria gerar um ganho de desempenho?
CAP ÍTULO 4
Paralelismo em nível de dados em arquiteturas
vetoriais, SIMD e GPU1
Chamamos esses algoritmos de paralelismo de dados porque seu paralelismo
vem de operações simultâneas através de grandes conjuntos de dados,
em vez de múltiplos threads de controle.
W. Daniel Hillis e Guy L. Steele
“Data Parallel Algorithms”, Comm. ACM (1986).
Se você estivesse arando um campo, o que preferiria usar: dois bois fortes
ou 1.024 galinhas?
Seymour Cray, Pai do Supercomputador,
(defendendo dois poderosos processadores
vetoriais em vez de vários processadores simples).
4.1 Introdução ...........................................................................................................................................227
4.2 Arquitetura vetorial ............................................................................................................................229
4.3 Extensões de conjunto de instruções SIMD para multimídia .........................................................246
4.4 Unidades de processamento gráfico .................................................................................................251
4.5 Detectando e melhorando o paralelismo em nível de loop..............................................................274
4.6 Questões cruzadas..............................................................................................................................282
4.7 Juntando tudo: GPUs móveis versus GPUs servidor Tesla versus Core i7 ...................................284
4.8 Falácias e armadilhas .........................................................................................................................290
4.9 Considerações finais...........................................................................................................................291
4.10 Perspectivas históricas e referências ..............................................................................................293
Estudo de caso e exercícios por Jason D. Bakos .....................................................................................293
4.1
INTRODUÇÃO
Uma questão para a arquitetura de simples instrução e múltiplos dados (SIMD), apresentada no Capítulo 1, é de que a largura do conjunto de aplicações tem paralelismo significativo em nível de dados (DLP). Cinquenta anos depois, a resposta não são só os cálculos
orientados para a matriz da computação científica, mas também para o processamento de
imagens e sons orientado para a mídia. Além disso, como uma única instrução pode lançar
muitas operações de dados, o SIMD é potencialmente mais eficiente em termos de energia
do que múltiplas instruções e múltiplos dados (MIMD), que precisam buscar e executar
uma instrução por operação de dados. Essas duas respostas tornam o SIMD atraente para
dispositivos pessoais móveis. Por fim, talvez a maior vantagem do SIMD em comparação
ao MIMD seja que o programador continua a pensar sequencialmente e, ainda assim,
atinge um ganho de velocidade ao realizar operações de dados paralelas.
227
228
CAPÍTULO 4:
Paralelismo em nível de dados em arquiteturas vetoriais, SIMD e GPU
Este capítulo abrange três variações do SIMD: arquiteturas vetoriais, extensões de conjunto
de instrução SIMD para multimídia e unidades de processamento gráfico (GPUs).1
A primeira variação, que antecede as outras duas em mais de 30 anos, significa essencialmente a execução em pipeline de muitas operações de dados. Essas arquiteturas vetoriais
são mais fáceis de entender e compilar do que outras variações de SIMD, mas até bem
recentemente eram consideradas muito caras para os microprocessadores. Parte desse
custo era referente a transistores e parte à largura de banda suficiente para a DRAM, dada
a dependência generalizada das caches para atender às demandas de desempenho de
memória em microprocessadores convencionais.
A segunda variação SIMD pega esse nome emprestado para representar operações simultâneas de dados paralelos (Simultaneous Parallel Data Operations) e é encontrada
na maioria das arquiteturas de conjunto de instruções atuais que suportam aplicações
multimídia. Para arquiteturas ×86, as extensões de instruções SIMD começaram com
o MMX (extensões multimídia) em 1996, seguidas por diversas versões SSE (extensões
SIMD para streaming) na década seguinte e continuam com as AVX (extensões vetoriais
avançadas). Muitas vezes, para obter a maior taxa de computação de um computador ×86,
você precisa usar essas instruções SIMD, especialmente para programas de ponto flutuante.
A terceira variação do SIMD vem da comunidade GPU, oferecendo maior desempenho
potencial do que o encontrado nos computadores multicore tradicionais de hoje. Embora
as GPUs compartilhem características com as arquiteturas vetoriais, elas têm suas próprias
características, em parte devido ao ecossistema no qual evoluíram. Esse ambiente tem
um sistema de processador e um sistema de memória, além da GPU e de sua memória
gráfica. De fato, para reconhecer essas distinções, a comunidade GPU se refere a esse tipo
de arquitetura como heterogênea.
Por problemas com muito paralelismo de dados, as três variações de SIMD compartilham
a vantagem de serem mais fáceis para os programadores do que a clássica programação
MIMD. Para colocar em perspectiva a importância do SIMD versus o MIMD, a Figura 4.1
plota o número de núcleos para o MIMD versus o número de operações de 32 bits e 64 bits
por ciclo de clock no modo SIMD para computadores ×86 ao longo do tempo.
Para os computadores ×86, esperamos ver dois núcleos adicionais por chip a cada dois
anos e a largura SIMD dobrar a cada quatro anos. Dadas essas suposições, ao longo da
próxima década, o ganho potencial de velocidade do paralelismo SIMD será duas vezes o
do paralelismo MIMD. Portanto, é igualmente importante entender o paralelismo SIMD
como paralelismo MIMD, embora recentemente o último tenha recebido muito mais
destaque. Para aplicações com paralelismo em nível de dados e paralelismo em nível de
thread, o ganho potencial de velocidade em 2020 terá magnitude maior do que hoje.
O objetivo deste capítulo é fazer que os arquitetos entendam por que os vetores são mais
gerais do que o SIMD de multimídia, assim como as similaridades e diferenças entre as arquiteturas vetoriais e as de GPU. Como as arquiteturas vetoriais são superconjuntos das instruções
SIMD multimídia, incluindo um modelo melhor para compilação, e as GPUs compartilham
diversas similaridades com as arquiteturas vetoriais, começamos com arquiteturas vetoriais
para estabelecer a base para as duas seções a seguir. A seção seguinte apresenta as arquiteturas
vetoriais, e o Apêndice G vai muito mais fundo no assunto.
1
Este capítulo se baseia em material do Apêndice F, “Processadores Vetoriais”, de Krste Asanovic,
e do Apêndice G, “Hardware e Software para VLIW e EPIC” da 4a edição deste livro; em material
do Apêndice A, “Graphics and Computing GPUs”, de John Nickolls e David Kirk, da 4a edição de Computer
Organization and Design; e, em menor escala, em material de “Embracing and Extending 20th-Century
Instruction Set Architectures”, de Joe Gebis e David Patterson, IEEE Computer, abril de 2007.
4.2
FIGURA 4.1 Ganho de vista potencial através de paralelismo de MIMD, SIMD e tanto MIMD quanto SIMD
ao longo do tempo para computadores ×86.
Esta figura supõe que dois núcleos por chip para MIMD serão adicionados a cada dois anos e o número de operações
para SIMD vai dobrar a cada quatro anos.
4.2
ARQUITETURA VETORIAL
O modo mais eficiente de executar uma aplicação vetorizável é um processador
vetorial.
Jim Smith
International Symposium on Computer Architecture (1994)
As arquiteturas vetoriais coletam conjuntos de elementos de dados espalhados pela
memória, os colocam em arquivos de registradores sequenciais, operam sobre dados
nesses arquivos de registradores e então dispersam os resultados de volta para a memória.
Uma única instrução opera sobre vetores de dados, que resulta em dúzias de operações
registrador-registrador em elementos de dados independentes.
Esses grandes arquivos de registradores agem como buffers controlados pelo compilador,
tanto para ocultar a latência de memória quanto para aproveitar a largura de banda da
memória. Como carregamentos e armazenamentos vetorias são fortemente pipelined, o
programa paga pela grande latência de memória somente uma vez por carregamento ou
armazenamento vetorial em comparação a uma vez por elemento, amortizando assim a
latência ao longo de cerca de 64 elementos. De fato, os programas vetoriais lutam para
manter a memória ocupada.
VMIPS
Começamos com um processador vetorial que consiste nos principais componentes mostrados na Figura 4.2. Esse processador, que é livremente baseado no Cray-1, é o alicerce
para a discussão por quase toda esta seção. Nós o chamaremos de VMIPS; sua parte escalar
Arquitetura vetorial
229
230
CAPÍTULO 4:
Paralelismo em nível de dados em arquiteturas vetoriais, SIMD e GPU
é MIPS e sua parte vetorial é a extensão vetorial lógica do MIPS. O restante desta seção
examina a forma como a arquitetura básica do VMIPS está relacionada com os outros
processadores.
Os principais componentes da arquitetura do conjunto de instruções do VMIPS são os
seguintes:
j
j
j
j
Registradores vetorial. Cada registrador vetorial é um banco de tamanho fixo
mantendo um único vetor. O VMIPS possui oito registradores vetoriais, cada qual
com 64 elementos. O registrador vetorial precisa fornecer portas suficientes para
alimentar todas as unidades funcionais vetoriais. Essas portas vão permitir alto grau
de sobreposição entre as operações vetoriais para diferentes registradores vetoriais.
As portas de leitura e escrita, que totalizam pelo menos 16 portas de leitura e oito
portas de escrita, estão conectadas às entradas ou saídas de unidade funcional por
um par de matrizes de chaveamento crossbars.
Unidades funcionais vetoriais. Cada unidade é totalmente pipelined e pode iniciar
uma nova operação a cada ciclo de clock. Uma unidade de controle é necessária
para detectar os riscos, sejam riscos estruturais para unidades funcionais, sejam
riscos de dados em acessos de registradores. A Figura 4.2 mostra que o VMIPS
possui cinco unidades funcionais. Para simplificar, focalizaremos exclusivamente
as unidades funcionais de ponto flutuante.
Unidade carregamento-armazenamento vetorial. Essa é uma unidade de memória
vetorial que carrega ou armazena um vetor na memória. Os carregamentos e
armazenamentos vetoriais do VMIPS são totalmente pipelined, de modo que as
palavras podem ser movidas entre os registradores vetoriais e memória com largura
de banda de uma palavra por ciclo de clock, após uma latência inicial. Normalmente,
essa unidade trataria também de carregamentos e armazenamentos de escalares.
Um conjunto de registradores escalares. Os registradores escalares também podem
oferecer dados como entrada para as unidades funcionais vetoriais, além de calcular
endereços para passar para a unidade load/store vetorial. Esses são os 32 registradores
de uso geral normais e 32 registradores de ponto flutuante do MIPS. Uma entrada
da unidade funcional vetorial trava valores escalares como lidos do banco de
registradoreses escalares.
A Figura 4.3 lista as instruções vetoriais do VMIPS. No VMIPS, as operações vetoriais
utilizam os mesmos nomes das operações do MIPS, mas com as letras “VV” anexadas.
Assim, ADDVV.D é uma adição de dois vetores de precisão dupla. As instruções vetoriais
têm como entrada um par de registradores vetoriais (ADDVV.D) ou um registrador vetorial
e um registrador escalar, designado pelo acréscimo de “VS” (ADDVS.D). Neste último
caso, o valor no registrador escalar será usado como entrada para todas as operações — a
operação ADDVS.D acrescentará o conteúdo de um registrador escalar a cada elemento
em um registrador vetorial. O valor escalar será copiado para a unidade funcional vetorial
no momento da emissão. A maioria das operações vetoriais possui um registrador de destino vetorial, embora algumas (contagem de elementos) produzam um valor escalar, que
é armazenado em um registrador escalar.
Os nomes LV e SV indicam load vetorial e store vetorial, e carregam ou armazenam um vetor
de dados inteiros de precisão dupla. Um operando é o registrador vetorial a ser carregado
ou armazenado; o outro operando, que é um registrador de uso geral do MIPS, é o endereço
inicial do vetor na memória. Como veremos, além dos registradores vetoriais, precisamos
de dois registradores adicionais de uso especial: os registradores de comprimento vetorial
e de máscara vetorial. O primeiro é usado quando o tamanho natural do vetor não é 64, e
o último é usado quando os loops envolvem declarações IF.
4.2
FIGURA 4.2 Estrutura básica de uma arquitetura vetorial, VMIPS.
Esse processador possui uma arquitetura escalar, assim como o MIPS. Há também oito registradores vetoriais
de 64 elementos, e todas as unidades funcionais são unidades funcionais vetoriais. Instruções especiais vetoriais são definidas,
neste capítulo, tanto para aritmética quanto para acessos à memória. A figura mostra as unidades vetoriais para operações
lógicas e de inteiros, fazendo com que o VMIPS se pareça com um processador vetorial padrão, que normalmente as
inclui. Porém, não vamos discutir essas unidades, exceto nos exercícios. Os registradores vetoriais e escalares têm número
significativo de portas de leitura e escrita para permitir várias operações vetoriais simultâneas. Essas portas estão conectadas
às entradas e saídas das unidades funcionais vetoriais por um conjunto de swichtes crossbars (mostrados em linhas cinza
grossas) conectadas a essas portas de entradas e saídas das unidades funcionais vetoriais.
A barreira da potência levou os arquitetos a avaliarem arquiteturas que possam apresentar
alto desempenho sem os custos de energia e a complexidade de processadores superescalares com processamento fora de ordem. As instruções vetoriais são um parceiro natural
para essa tendência, já que os arquitetos podem usá-las para aumentar o desempenho de
simples processadores escalares em ordem sem aumentar muito as demandas de energia
e complexidade de projeto. Na prática, os desenvolvedores podem expressar muitos
dos programas que funcionavam bem em projetos complexos fora de ordem com mais
eficiência como paralelismo em nível de dados na forma de instruções vetoriais, tal como
mostrado por Kozyrakis e Patterson (2002).
Com uma instrução vetorial, o sistema pode realizar as operações sobre os elementos de
dados do vetor de muitas maneiras, incluindo como operar em muitos elementos simultaneamente. Essa flexibilidade permite aos projetos vetoriais usar unidades de execução
lentas, porém largas, sem realizar custosas verificações adicionais de dependência, como
exigem os processadores superescalares.
Os vetores acomodam naturalmente tamanhos variáveis de dados. Portanto, uma interpretação de um tamanho de registrador vetorial é de 64 elementos de 64 bits, mas 128 elementos
Arquitetura vetorial
231
232
CAPÍTULO 4:
Paralelismo em nível de dados em arquiteturas vetoriais, SIMD e GPU
FIGURA 4.3 Instruções vetoriais do VMIPS.
Apenas as operações de PF de precisão dupla aparecem. Além dos registradores vetoriais, existem dois registradores especiais, VLR (discutido
na Seção F.3) e VM (discutido na Seção F.4). Esses registradores especiais são considerados como vivendo no espaço do coprocessador
1 do MIPS, junto com os registradores FPU. As operações com passo serão explicadas na Seção F.3, e os usos da criação de índice e operações
carregamento-armazenamento indexadas serão explicadas mais adiante.
de 32 bits, 256 elementos de 16 bits e até mesmo 512 elementos de 8 bits são interpretações
igualmente válidas. Essa multiplicidade de hardware é o motivo de uma arquitetura vetorial
ser útil para aplicações multimídia e científicas.
Como os processadores vetoriais funcionam: exemplo
Um processador vetorial pode ser mais bem entendido examinando-se um loop vetorial no
VMIPS. Vamos usar um problema típico vetorial, que será usado no decorrer desta seção:
Y=a×X+Y
X e Y são vetores, inicialmente residentes na memória, e a é um escalar. Esse é o chamado
loop SAXPY ou DAXPY, que forma o loop interno do benchmark Linpack. (SAXPY é a sigla
para single-precision a × X plus Y; DAXPY é a sigla para double-precision a × X plus Y.)
4.2
Linpack é uma coleção de rotinas da álgebra linear, e o benchmark Limpack consite em
rotinas que realizam a eliminação gaussiana. A rotina DAXPY, que implementa o loop
anterior, representa uma pequena fração do código-fonte do benchmark Linpack, mas
considera a maior parte do tempo de execução para esse benchmark.
Por enquanto, vamos considerar que o número de elementos, ou tamanho de um registrador vetorial (64), corresponde ao tamanho da operação vetorial em que estamos interessados (essa restrição será removida brevemente).
Exemplo
Resposta
Mostre o código para MIPS e VMIPS para o loop DAXPY. Considere que os
endereços iniciais de X e Y estão em Rx e Ry, respectivamente.
Aqui está o código MIPS.
Aqui está o código VMIPS para o loop DAXPY.
A diferença mais importante é que o processador vetorial reduz bastante a largura de banda
de instrução dinâmica, executando apenas seis instruções contra quase 600 para MIPS.
Essa redução ocorre tanto porque as operações vetoriais trabalham sobre 64 elementos
quanto porque as instruções de overhead que constituem quase metade do loop no MIPS
não estão presentes no código VMIPS. Quando o compilador produz instruções vetoriais
para essa sequência e o código passa grande parte do tempo sendo executado em modo
vetorial, diz-se que o código está vetorizado ou vetorizável. Loops podem ser vetorizados
quando não têm dependências entre as suas iterações loop, as quais são chamadas
dependências loop-carried (Seção 4.5).
Outra diferença importante entre MIPS e VMIPS é a frequência dos interbloqueios do
pipeline. 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 vetorial, cada instrução vetorial sofrerá stall
somente para o primeiro elemento em cada vetor, e depois os elementos subsequentes
fluirão suavemente pelo pipeline. Assim, stalls de pipeline são exigidos apenas uma
vez por operação vetorial, e não uma vez por elemento do vetor. Os arquitetos vetoriais
chamam o adiantamento de operações de elementos dependentes de encadeamento,
onde as operações dependentes formam uma “corrente” ou “cadeia”. Neste exemplo, a
frequência de stall do pipeline no MIPS será cerca de 64 vezes maior do que no VMIPS. Os
stalls de pipeline podem ser eliminados no MIPS usando pipelining de software ou desdobramento de loop (descrito no Apêndice H). Contudo, a grande diferença na largura de
banda de instrução não pode ser reduzida.
Arquitetura vetorial
233
234
CAPÍTULO 4:
Paralelismo em nível de dados em arquiteturas vetoriais, SIMD e GPU
Tempo de execução vetorial
O tempo de execução de uma sequência de operações vetoriais depende principalmente
de três fatores: 1) o tamanho dos vetores do operando; 2) os riscos estruturais entre as
operações; e 3) as dependências de dados. Dados o tamanho do vetor e a taxa de iniciação,
que é a velocidade com que uma unidade vetorial consome novos operandos e produz
novos resultados, podemos calcular o tempo para uma única instrução vetorial. Todos os
supercomputadores modernos têm unidades funcionais vetoriais com múltiplos pipelines
paralelos (ou pistas) que podem produzir dois ou mais resultados por ciclo de clock,
mas também têm algumas unidades funcionais que não são totalmente pipelineds. Por
simplicidade, nossa implementação VMIPS tem uma pista com taxa de iniciação de um
elemento por ciclo de clock para operações individuais. Assim, o tempo de execução para
uma única instrução vetorial é aproximadamente o tamanho do vetor.
Para simplificar a discussão sobre a execução do vetor e seu tempo, usaremos a noção
de comboio, que é o conjunto de instruções vetoriais que podem iniciar a execução juntas
em um período de clock. (Embora o conceito de comboio seja usado em compiladores
vetoriais, não existe uma terminologia-padrão. Por isso, criamos o termo comboio.) As instruções em um comboio não podem conter quaisquer riscos estruturais ou de dados; se
esses riscos estivessem presentes, as instruções no comboio em potencial precisariam ser
seriadas e iniciadas em diferentes comboios. Para manter a análise simples, consideramos
que um comboio de instruções precisa completar a execução antes que quaisquer outras
instruções (escalares ou vetoriais) possam iniciar a execução.
Pode parecer que, além das sequências de instruções vetoriais com riscos estruturais, seria: as
sequências de leitura com riscos de dependência leitura após gravação também deveriam estar
em comboios diferentes, mas o encadeamento permite que elas estejam no mesmo comboio.
O encadeamento permite que uma operação vetorial comece assim que os elementos
individuais do operando-fonte desse vetor fiquem disponíveis: os resultados da primeira
unidade funcional na cadeia são “adiantados” para a segunda unidade funcional. Na
prática, muitas vezes implementamos o encadeamento permitindo que o processador
leia e grave um registrador vetorial particular ao mesmo tempo, embora para diferentes
elementos. As primeiras implementações de encadeamento funcionavam do mesmo
modo que o adiantamento em pipelines escalares, mas isso restringia a temporização
dos fontes e destinos das instruções na cadeia. Implementações recentes usam o encadeamento flexível, que permite que uma instrução vetorial seja encadeada para qualquer
outra instrução vetorial ativa, supondo que isso não gere um risco estrutural. Todas as
arquiteturas vetoriais modernas suportam encadeamento flexível, que vamos considerar
neste capítulo.
Para converter comboios em tempo de execução precisamos de uma medida de temporização para estimar o tempo de um comboio. Ela é chamada de chime, que é a unidade de
tempo necessária para executar um comboio. Assim, uma sequência vetorial que consiste
em m comboios é executada em m chimes, e, para um tamanho vetorial de n, isso é aproximadamente m × n ciclos de clock. Uma aproximação do chime ignora alguns overheads
específicos do processador, muitos dos quais dependem do tamanho do vetor. Logo, medir
o tempo em chimes é uma aproximação melhor para vetores longos. Usaremos a medida
do chime em vez de ciclos de clock por resultado para indicar explicitamente que certos
overheads estão sendo ignorados.
Se soubermos o número de comboios em uma sequência vetorial, saberemos o tempo de
execução em chimes. Uma fonte de overhead ignorada na medição de chimes é qualquer
limitação na iniciação de múltiplas instruções vetoriais em um ciclo de clock. Se apenas
4.2
uma instrução vetorial puder ser iniciada em um ciclo de clock (a realidade na maioria dos
processadores vetoriais), a contagem de chime subestimará o tempo de execução real de
um comboio. Como o tamanho do vetor normalmente é muito maior que o número
de instruções no comboio, simplesmente consideraremos que o comboio é executado
em um chime.
Exemplo Mostre como a sequência de código a seguuir é disposta em comboios,
considerando uma única cópia de cada unidade funcional vetorial:
De quantos chimes essa sequência vetorial precisa? Quantos ciclos por
FLOP (operação de ponto flutuante) são necessários, ignorando o overhead
da emissão da instrução vetorial?
Resposta O primeiro comboio é ocupado pela primeira instrução LV. O MULVS.D
depende do primeiro LV, de modo que não pode estar no mesmo comboio.
A segunda instrução LV pode estar no mesmo comboio de MULVS.D. O
ADDV.D é dependente do segundo LV, de modo que precisa vir em um
terceiro comboio, e por fim o SV depende do ADDVV.D, de modo que precisa
vir em um comboio seguinte. Isso leva ao seguinte leiaute de instruções
vetoriais nos comboios:
A sequência exige três comboios. Como a sequência usa um total de três
chimes e existem duas operações de ponto flutuante por resultado, o número
de ciclos por FLOP é 1,5 (ignorando qualquer overhead de emissão de instrução vetorial). Observe que, embora permitíssemos que MULVS.D e LV
fossem executadas no primeiro comboio, a maioria das máquinas vetoriais
usará dois ciclos de clock para iniciar as instruções.
Este exemplo mostra que a aproximação chime é razoavelmente precisa
para vetores longos. Por exemplo, para vetores de 64 elementos, o tempo
em chimes é 3, então a sequência levaria cerca de 64 × 3 ou 192 ciclos de
clock. O overhead de despachar comboios em dois ciclos de clock separados
seria pequeno.
Outra fonte de overhead é muito mais significativa do que a limitação de despacho.
A fonte mais importante de overhead, ignorada pelo modelo de chime, é o tempo de
início do vetor. O tempo de início vem da latência de pipelining da operação vetorial e
é determinado principalmente pela profundidade do pipeline para a unidade funcional
utilizada. Para VMIPS, vamos usar as mesmas profundidades de pipeline do Cray-1,
embora as latências em processadores mais modernos tenham aumentado, especialmente
para carregamentos vetoriais. Todas as unidades funcionais são totalmente pipelined.
As profundidades de pipeline são de seis ciclos de clock por soma de ponto flutuante,
sete para multiplicação de ponto flutuante, 20 para divisão de ponto flutuante e 12 para
carregamento vetorial.
Arquitetura vetorial
235
236
CAPÍTULO 4:
Paralelismo em nível de dados em arquiteturas vetoriais, SIMD e GPU
Dados esses conceitos básicos vetoriais, as próximas subseções vão dar otimizações que
melhoram o desempenho ou diminuem os tipos de programas que podem ser bem
executados em arquiteturas vetoriais. Em particular, elas vão responder às questões:
j
j
j
j
j
j
j
Como um processador vetorial executa um único vetor mais rápido do que um
elemento por ciclo de clock? Múltiplos elementos por ciclo de clock melhoram
o desempenho.
Como um processador vetorial trata programas em que os comprimentos dos
vetores não são iguais ao comprimento do registrador vetorial (64 para VMIPS)?
Como a maioria dos vetores de aplicação não corresponde ao comprimento de
vetor da arquitetura, precisamos de uma solução eficiente para esse caso comum.
O que acontece quando existe uma declaração IF dentro do código a ser vetorizado?
Mais código pode ser vetorizado se pudermos lidar eficientemente com declarações
condicionais.
O que um processador vetorial precisa do sistema de memória? Sem largura de
banda de memória suficiente, a execução vetorial pode ser fútil.
Como um processador vetorial lida com matrizes multidimensionais? Essa popular
estrutura de dados deve ser vetorizada para arquiteturas vetoriais para funcionar bem.
Como um processador vetorial lida com matrizes dispersas? Essa popular estrutura
de dados também deve ser vetorizada.
Como você programa um computador vetorial? Inovações arquiteturais que não
correspondam à tecnologia de compilador podem não ser amplamente utilizadas.
O restante desta seção apresentará cada uma dessas otimizações da arquitetura vetorial,
e o Apêndice G mostrará mais detalhes.
Múltiplas pistas: além de um elemento por ciclo de clock
Uma das maiores vantagens de um conjunto de instruções vetoriais é que ele permite que
o software passe uma grande quantidade de trabalho paralelo para o hardware usando
uma única instrução curta. Uma única instrução vetorial pode incluir entre dezenas e
centenas de operações independentes, porém ser codificada com o mesmo número de bits
de uma instrução escalar convencional. A semântica paralela de uma instrução vetorial permite que uma implementação execute essas operações elementares usando uma unidade
funcional de pipeline profundo, como na implementação VMIPS que estudamos até aqui,
ou usando um array de unidades funcionais paralelas ou uma combinação de unidades
funcionais paralelas e em pipeline. A Figura 4.4 ilustra como o desempenho do vetor pode
ser melhorado usando pipelines paralelos para executar uma instrução de adição vetorial.
O conjunto de instruções VMIPS foi projetado com a propriedade de que todas as instruções de aritmética vetorial só permitem que o elemento N de um registrador vetorial
tome parte das operações com o elemento N de outros registradores vetoriais. Isso simplifica bastante a construção de uma unidade vetorial altamente paralela, que pode ser
estruturada como múltiplas pistas paralelas. Assim como em uma rodovia de trânsito,
podemos aumentar a vazão de pico de uma unidade vetorial acrescentando pistas.
A estrutura de uma unidade vetorial de quatro pistas aparece na Figura 4.5. Assim, mudar
de uma pista para quatro pistas reduz o número de clocks de um chime de 64 para 16.
Para que múltiplas pistas sejam vantajosas, as aplicações e a arquitetura devem suportar
vetores longos. Caso contrário, elas serão executadas tão rapidamente que você vai ficar
sem largura de banda de instrução, requerendo técnicas de ILP (Cap. 3) para fornecer
instruções vetoriais suficientes.
Cada pista contém uma parte do banco de registradores vetoriais e um pipeline de execução
de cada unidade funcional vetorial. Cada unidade funcional vetorial executa instruções
4.2
FIGURA 4.4 Uso de múltiplas unidades funcionais para melhorar o desempenho de uma única instrução
de adição vetorial, C = A + B.
A máquina mostrada em (a) tem um pipeline de adição única e pode completar uma adição por ciclo. A máquina
mostrada em (b) possui quatro pipelines de adição e pode completar quatro adições por ciclo. Os elementos dentro
de uma única instrução de adição vetorial são intercalados pelos quatro pipelines. O conjunto de elementos que se
movem pelos pipelines juntos é chamado grupo de elementos. (Reproduzido com permissão de Asanovic, 1998.)
vetoriais na velocidade de um grupo de elementos por ciclo usando múltiplos pipelines,
um por pista. A primeira pista mantém o primeiro elemento (elemento 0) para todos os
registradores vetoriais, por isso o primeiro elemento em qualquer instrução vetorial terá
seus próprios operandos-fonte e destino localizados na primeira pista. Essa alocação permite que o pipeline aritmético local à pista termine a operação sem se comunicar com
outras pistas. O entrelaçamento de fios entre as pistas só é necessário para o acesso à
memória principal. Essa falta de comunicação entre as pistas reduz o custo de fiação e as
portas do banco de registradores exigidas para a montagem de uma unidade de execução
altamente paralela, e ajuda a explicar por que os supercomputadores vetoriais atuais
podem completar até 64 operações por ciclo (duas unidades aritméticas e duas unidades
carregamento-armazenamento por 16 pistas).
A adição de múltiplas pistas é uma técnica popular para melhorar o desempenho do vetor,
pois exige pouco aumento na complexidade de controle e não exige mudanças no código
de máquina existente. Isso também permite aos projetistas reduzir a área do substrato, a
taxa de clock, a voltagem e a energia sem sacrificar o desempenho de pico. Se a taxa de
clock de um processador vetorial for reduzida pela metade, dobrar o número de pistas vai
manter o mesmo desempenho potencial.
Arquitetura vetorial
237
238
CAPÍTULO 4:
Paralelismo em nível de dados em arquiteturas vetoriais, SIMD e GPU
FIGURA 4.5 Estrutura de uma unidade vetorial contendo quatro pistas.
O armazenamento do registrador vetorial é dividido pelas pistas, com cada pista mantendo cada quarto
elementos de cada registrador vetorial. São mostradas três unidades funcionais vetoriais, uma de adição de PF,
uma de multiplicação de PF e uma unidade carregamento-armazenamento. Cada uma das unidades aritméticas
vetoriais contém quatro pipelines de execução, uma por pista, que atuam em conjunto para completar uma única
instrução vetorial. Observe como cada seção do banco de registradores vetorial só precisa oferecer portas suficientes
para os pipelines locais à sua pista; isso reduz drasticamente o custo de fornecer múltiplas portas aos registradores
vetoriais. O caminho para oferecer o operando escalar para as instruções escalares vetoriais não aparece nesta figura,
mas o valor escalar precisa ser enviado a todas as pistas por broadcast.
Registradores de tamanho do vetor: tratando loops
diferentes de 64
Um processador de registrador vetorial tem o tamanho vetorial natural determinado pelo
número de elementos em cada registrador vetorial. Esse tamanho, que é de 64 para VMIPS,
provavelmente não combina com o tamanho de vetor real em um programa. Além do
mais, em um programa real, o tamanho de determinada operação vetorial normalmente
é desconhecido durante a compilação. Na verdade, um único pedaço de código pode exigir
diferentes tamanhos de vetores. Por exemplo, considere este código:
O tamanho de todas as operações vetoriais depende de n, que pode nem ser conhecido
antes da execução! O valor de n também poderia ser um parâmetro para um procedimento
contendo o loop acima e, portanto, estar sujeito a mudanças durante a execução.
A solução para esses problemas é criar um registrador de tamanho de vetor (VLR). O VLR
controla o tamanho de qualquer operação vetorial, incluindo carregamento ou armazenamento vetorial. O valor no VLR, porém, não pode ser maior que o tamanho dos registradores vetoriais. Isso resolve nosso problema desde que o tamanho real seja menor ou
4.2
igual ao tamanho máximo de vetor (MVL). O MVL determina o tamanho de elementos de
dados em um vetor de uma arquitetura. Esse parâmetro significa que o comprimento dos
registradores vetoriais pode crescer em gerações futuras de computadores sem mudar o
conjunto de instruções. Como veremos na próxima seção, extensões SIMD multimídia
não têm equivalente em MVL, então elas mudam o conjunto de instruções sempre que
aumentam seu comprimento de vetor.
E se o valor de n não for conhecido durante a execução e, assim, puder ser maior que o
MVL? Para enfrentar o segundo problema, em que o vetor é maior que o tamanho máximo,
é usada uma técnica chamada strip mining. Strip mining é a geração de código de modo
que cada operação vetorial seja feita para um tamanho menor ou igual ao MVL. Criamos
um loop que trata de qualquer número de iterações, que seja um múltiplo do MVL, e
outro loop que trate de quaisquer iterações restantes, que precisam ser menores que o
MVL. Na prática, os compiladores normalmente criam um único loop strip-mined que é
parametrizado para lidar com as duas partes, alterando o tamanho. Mostramos a versão
strip-mined do loop DAXPY em C:
O termo n/MVL representa o truncamento da divisão de inteiros. O efeito desse loop
é bloquear o vetor em segmentos, que são então processados pelo loop interno.
O tamanho do primeiro segmento é (n % MVL), e todos os segmentos subsequentes são de tamanho MVL. A Figura 4.6 mostra como dividir um vetor longo em
segmentos.
O loop interno do código anterior é vetorizável com tamanho VL, que é igual a (n % MVL)
ou MVL. O registrador VLR precisa ser definido duas vezes — uma em cada lugar onde a
variável VL no código é atribuída.
Registradores de máscara vetorial: lidando com declarações
IF em loops vetoriais
Pela lei de Amdahl, sabemos que o ganho de velocidade nos programas com níveis de
vetorização baixo a moderado será muito limitado. Dois motivos pelos quais os níveis
FIGURA 4.6 Um vetor de tamanho arbitrário processado com strip mining.
Todos os blocos, exceto o primeiro, são de tamanho MVL, utilizando a capacidade total do processador vetorial. Nesta
figura, a variável m é usada para a expressão (n % MVL). (O operador C % é módulo.)
Arquitetura vetorial
239
240
CAPÍTULO 4:
Paralelismo em nível de dados em arquiteturas vetoriais, SIMD e GPU
de vetorização mais altos não são alcançados são a presença de condicionais (instruções
if) dentro dos loops e o uso de matrizes dispersas. Os programas que contêm instruções if
nos loops não podem ser executados no modo vetorial usando as técnicas que discutimos
até aqui, pois as instruções if introduzem dependências de controle em um loop. De modo
semelhante, as matrizes dispersas não podem ser implementadas eficientemente usando
qualquer uma das capacidades que vimos até agora. Discutiremos aqui as estratégias
para lidar com a execução condicional, deixando a discussão das matrizes dispersas para
a próxima subseção.
Considere o seguinte loop escrito em C:
Esse loop normalmente não pode ser vetorizado, devido à execução condicional do corpo;
porém, se o loop interno pudesse ser executado para as iterações para as quais X(i) ≠ 0,
então a subtração poderia ser vetorizada.
A extensão que normalmente é usada para essa capacidade é o controle de máscara vetorial.
Os registradores de máscara fornecem, essencialmente, a execução condicional de cada
operação de elemento em uma instrução vetorial. O controle de máscara vetorial usa um
vetor booleano para controlar a execução de uma instrução vetorial, assim como as instruções executadas condicionalmente utilizam uma condição booleana para determinar
se uma instrução é executada. Quando o registrador de máscara vetorial é habilitado,
quaisquer instruções vetoriais executadas operam somente sobre os elementos vetoriais
cujas entradas correspondentes no registrador de máscara vetorial são 1. As entradas no
registrador vetorial de destino que correspondem a um 0 no registrador de máscara não
são afetadas pela operação vetorial. Limpar o registrador de máscara vetorial o define
com todos os bits iguais a 1, fazendo com que as instruções vetoriais subsequentes
operem sobre todos os elementos do vetor. Agora o código a seguir pode ser usado para
o loop anterior, supondo que os endereços de partida de X e Y estejam em Rx e Ry, respectivamente:
Os projetistas de compiladores chamam a transformação para mudar uma declaração
IF em uma sequência de código de linha direta usando execução condicional de
conversão if.
Entretanto, o uso de um registrador de máscara vetorial apresenta desvantagens. Quando
examinamos instruções executadas condicionalmente, vemos que essas instruções ainda
exigem tempo de execução quando a condição não é satisfeita. Apesar disso, a eliminação
de um desvio e as dependências de controle associadas podem tornar uma instrução
condicional mais rápida, ainda que às vezes realize trabalho inútil. Da mesma forma, as
4.2
instruções executadas com uma máscara vetorial tomam tempo de execução mesmo para
os elementos onde a máscara é 0. Da mesma forma, mesmo com um número significativo
de 0s na máscara, o uso do controle de máscara vetorial pode ser significativamente mais
rápido do que o do modo escalar.
Como veremos na Seção 4.4, uma diferença entre os processadores vetoriais e os GPUs é
o modo como eles lidam com declarações condicionais. Processadores vetoriais tornam os
registradores de máscara parte do estado da arquitetura e dependem dos compiladores para
manipular explicitamente os registradores de máscara. Em contraste, as GPUs obtêm o mesmo
efeito usando o hardware para manipular registradores de máscara internos, que são invisíveis
para o software da GPU. Nos dois casos, o hardware usa o tempo para executar um elemento
vetorial quando a máscara é 0 ou 1, então a taxa GFLOPS cai quando máscaras são usadas.
Bancos de memória: fornecendo largura de banda para unidades
de carregamento/armazenamento vetoriais
O comportamento da unidade vetorial de carregamento-armazenamento é significativamente mais complicado do que o das unidades funcionais aritméticas. O tempo inicial para
um carregamento é o tempo para levar a primeira palavra da memória para um registrador.
Se o restante do vetor puder ser fornecido sem stalls, então a taxa de iniciação do vetor
será igual à taxa em que novas palavras são buscadas ou armazenadas. Diferentemente das
unidades funcionais mais simples, a taxa de iniciação pode não ser necessariamente um
ciclo de clock, pois os stalls do banco de memória podem reduzir o throughput efetivo.
Normalmente, as penalidades para os inícios em unidades carregamento-armazenamento
são mais altas do que aquelas para as unidades funcionais aritméticas — mais de 100 ciclos
de clock em alguns processadores. Para o VMIPS, consideramos um tempo de início de
12 ciclos de clock, o mesmo do Cray-1 (computadores vetoriais mais recentes usam as
caches para reduzir a latência dos carregamentos e os armazenamentos vetoriais).
Para manter uma taxa de iniciação de uma palavra buscada ou armazenada por clock, o
sistema de memória precisa ser capaz de produzir ou aceitar essa quantidade de dados.
Isso normalmente é feito espalhando-se os acessos por vários bancos de memória independentes. Conforme veremos na próxima seção, dispor de um número significativo
de bancos é útil para lidar com carregamentos ou armazenamentos vetoriais que acessam
linhas ou colunas de dados.
A maioria dos processadores vetoriais utiliza bancos de memória em vez de simples intercalação por três motivos principais:
1. Muitos computadores vetoriais admitem múltiplos carregamentos ou
armazenamentos por clock, e normalmente o tempo do ciclo de banco de memória
é muitas vezes maior do que o tempo do ciclo de CPU. Para dar suporte a múltiplos
acessos simultâneos, o sistema de memória precisa ter múltiplos bancos e ser capaz
de controlar os endereços para os bancos de forma independente.
2. Conforme veremos na próxima seção, muitos processadores vetoriais admitem a
capacidade de carregar ou armazenar palavras de dados que não são sequenciais.
Nesses casos, é necessário haver endereçamento independente do banco em vez de
intercalação.
3. Muitos computadores vetoriais admitem múltiplos processadores compartilhando
o mesmo sistema de memória, por isso cada processador estará gerando seu próprio
fluxo independente de endereços.
Em combinação, esses recursos levam a um grande número de bancos de memória independentes, como mostramos no exemplo a seguir.
Arquitetura vetorial
241
242
CAPÍTULO 4:
Paralelismo em nível de dados em arquiteturas vetoriais, SIMD e GPU
Exemplo
Resposta
A maior configuração do Cray T90 (Cray T932) possui 32 processadores,
cada qual capaz de gerar quatro carregamentos e dois armazenamentos
por ciclo. O ciclo de clock de CPU é de 2,167 ns, enquanto o tempo de ciclo
das SRAMs usadas no sistema de memória é de 15 ns. Calcule o número
mínimo de bancos de memória necessários para permitir que todas as CPUs
executem na largura de banda total da memória.
O número máximo de referências de memória de cada ciclo é 192 (32 CPUs
vezes seis referências por CPU). Cada banco de SRAM está ocupado por
15/2,167 = 6,92 ciclos de clock, que arredondamos para sete ciclos de clock.
Portanto, exigimos um mínimo de 192 × 7 = 1.344 bancos de memória!
O Cray T932 na realidade tem 1.024 bancos de memória, e por isso os primeiros modelos não poderiam sustentar a largura de banda total para todas
as CPUs simultaneamente. Um upgrade de memória subsequente substituiu as SRAMs assíncronas de 15 ns por SRAMs síncronas em pipeline,
que dividiram ao meio o tempo de ciclo de memória, fornecendo assim uma
largura de banda suficiente.
Assumindo uma perspectiva de alto nível, as unidades de carregamento/armazenamento
vetorial têm um papel similar ao das unidades pré-busca em processadores escalares, no
sentido de que os dois tentam obter proporcional largura de banda de dados fornecendo
streams de dados para os processadores.
Stride: manipulando arrays multidimensionais em arquiteuras
vetoriais
A posição na memória dos elementos adjacentes em um vetor pode não ser sequencial.
Considere o código C simples para multiplicação de matriz:
Poderíamos vetorizar a multiplicação de cada linha de B com cada coluna de D e realizar
o strip mining do loop interno tendo k como variável de índice.
Para fazer isso, temos que considerar o modo como os elementos adjacentes em B
e os elementos adjacentes em D são endereçados. Quando um array é alocado, ele é
linearizado e precisa ser disposto em ordem de linha (em C) ou de coluna (como em
Fortran). Essa linearização significa que os elementos na linha ou os elementos na
coluna não estão adjacentes na memória. Por exemplo, o código C (anterior) aloca em
ordem de linha, então os elementos de D acessados por iterações no loop interno são
separados pelo tamanho da linha vezes 8 (número de bytes por entrada) para um total
de 800 bytes. No Capítulo 2, vimos que o bloqueio poderia ser usado para melhorar
a localidade nos sistemas baseados em cache. Para os processadores vetoriais sem caches, precisamos de outra técnica para buscar os elementos de um vetor que não estão
adjacentes na memória.
Essa distância que separa os elementos que devem ser reunidos em um único registrador é chamada passo. Nesse exemplo, a matriz D tem um passo de 100 palavras
duplas (800 bytes).
4.2
Quando um vetor é carregado em um registrador vetorial, atua como se tivesse elementos
logicamente adjacentes. Assim, um processador vetorial pode tratar de passos maiores
que 1, chamados passos não unitários, usando apenas operações carregamento e armazenamento vetorial com capacidade de passo. Essa capacidade de acessar locais de memória
não sequenciais e remodelá-los em uma estrutura densa é uma das principais vantagens
de um processador vetorial em relação a um processador baseado em cache. As caches
lidam inerentemente com dados de passo unitários, de modo que, embora aumentar o
tamanho de bloco possa ajudar a reduzir as taxas de perda para grandes conjuntos de
dados científicos com passo unitário, aumentar o tamanho de bloco pode ter um efeito
negativo para os dados que são acessados com passo não unitário. Embora as técnicas de
bloqueio possam solucionar alguns desses problemas (Cap. 2), a capacidade de associar
de modo eficaz dados que não são contíguos continua sendo uma vantagem para os
processadores vetoriais em certos problemas, como veremos na Seção 4.7.
No VMIPS, em que a unidade endereçável é um byte, o passo para o nosso exemplo seria
800. O valor deve ser calculado dinamicamente, pois o tamanho da matriz pode não ser
conhecido durante a compilação ou — assim como o tamanho do vetor — pode mudar
para diferentes execuções da mesma instrução. O passo vetorial, como o endereço inicial
do vetor, pode ser colocado em um registrador de uso geral. Depois, a instrução LVWS
(Load Vector With Stride) do VMIPS pode ser usada para buscar o vetor em um registrador vetorial. De modo semelhante, quando um vetor de passo não unitário está sendo
armazenado, SVWS (Store Vector With Stride) pode ser usada.
Complicações no sistema de memória podem ocorrer a partir do suporte a passos maiores
que 1. Quando os passos não unitários são introduzidos, torna-se possível solicitar os
acessos a partir do mesmo banco. Quando múltiplos acessos disputam um banco, ocorre
um conflito de banco de memória e um acesso precisa ser adiado. Um conflito de banco
e, portanto, um stall, ocorrerá se
Número de bancos
< Tempo de ocupação do banco
Mínimo múltiplo comum(Stride,Número de bancos)
Exemplo Suponha que tenhamos oito bancos de memória com um tempo de ocupação
do banco com seis clocks e uma latência de memória total de 12 ciclos. Quanto
tempo será preciso para completar um carregamento vetorial de 64 elementos
com um passo de 1? E com um passo de 32?
Resposta Como o número de bancos é maior que o tempo de ocupação do banco, para um
stride de 1 o carregamento usará 12 + 64 = 76 ciclos de clock ou 1,2 clock por elemento. O pior passo possível é um valor que seja múltiplo do número de bancos
de memória, como nesse caso, com um passo de 32 e oito bancos de memória.
Cada acesso à memória (depois do primeiro) colidirá com o acesso anterior e terá
que esperar pelo tempo de ocupado do banco de seis ciclos de clock. O tempo
total será 12 + 1 + 6 * 63 = 391 ciclos de clock ou 6,1 clocks por elemento.
Gather-Scatter: lidando com matrizes dispersas em arquiteturas
vetoriais
Como mencionado, matrizes dispersas são comuns e, por isso, é importante dispor de
técnicas para permitir aos programas com matrizes dispersas a execução em modo vetorial.
Em uma matriz dispersa, normalmente os elementos de um vetor são armazenados em
alguma forma compactada e depois acessados indiretamente. Considerando uma estrutura
dispersa simplificada, poderíamos ver um código semelhante a este:
Arquitetura vetorial
243
244
CAPÍTULO 4:
Paralelismo em nível de dados em arquiteturas vetoriais, SIMD e GPU
Esse código implementa uma soma vetorial dispersa sobre os arrays A e C, usando vetores
de índice K e M para designar os elementos diferentes de zero de A e C. (A e C precisam
ter o mesmo número de elementos diferentes de zero — n deles —, então K e M são do
mesmo tamanho.)
O principal mecanismo para dar suporte a matrizes dispersas são operações scatter-gatter
usando vetores de índice. O objetivo de tais operações é dar suporte à movimentação entre
uma representação densa (ou seja, zeros não são incluídos) e a representação normal (ou
seja, os zeros são incluídos) de uma matriz dispersa. Uma operação gather pega um vetor
de índice e busca o vetor cujos elementos estão nos endereços dados pela adição de um
endereço de base para os deslocamentos dados no vetor de índice. O resultado é um vetor
não disperso em um registrador vetorial. Depois que esses elementos são operados em
uma forma densa, o vetor disperso pode ser armazenado em forma expandida por um
armazenamento scatter, usando o mesmo vetor de índice. O suporte do hardware para tais
operações é chamado de scatter-gather e aparece em quase todos os processadores vetoriais
modernos. As instruções LVI (Load Vector Indexed) e SVI (Store Vector Indexed) oferecem
essas operações no VMIPS. Por exemplo, supondo que Ra, Rc, Rk e Rm contenham os
endereços iniciais dos vetores na sequência anterior, o loop interno da sequência pode
ser codificado com instruções vetoriais como:
Essa técnica permite que o código com matrizes dispersas seja executado no modo vetor. Um compilador de vetorização simples não poderia vetorizar automaticamente o
código-fonte anterior, pois o compilador não saberia que os elementos de K são valores
distintos e, portanto, que não existem dependências. Em vez disso, uma diretiva do programador diria ao compilador que ele poderia executar o loop no modo vetorial.
Embora carregamentos e armazenamentos indexados (gather e scatter) possam ser usados
em pipeline, em geral eles rodam muito mais lentamente do que carregamentos e armazenamentos não indexados, uma vez que os bancos de memória não são conhecidos no
início da instrução. Cada elemento tem um endereço individual, então eles não podem ser
tratados em grupos, e pode haver conflitos em muitos locais pelos sistemas de memória.
Assim, cada acesso individual incorre em latência significativa. Entretanto, como mostra a
Seção 4.7, um sistema de memória pode ter melhor desempenho sendo projetado para esse
caso e usando mais recursos de hardware em comparação aos casos em que os arquitetos
têm uma atitude laissez-faire em relação a tais acessos.
Como veremos na Seção 4.4, todos os carregamentos são gathers e todos os armazenamentos são scatters nas GPUs. Para evitar a execução lenta no frequente caso dos passos
unitários, cabe ao programador da GPU garantir que todos os endereços em um gather
ou scatter sejam em locais adjacentes. Além disso, o hardware da GPU deve reconhecer a
sequência desses endereços durante a execução para transformar os gathers e scatters no
mais eficiente acesso de passo unitário à memória.
4.2
Programando arquiteturas vetoriais
Uma vantagem das arquiteturas vetoriais é que os compiladores podem dizer aos programadores, no momento da compilação, se uma seção de código será ou não vetorizada,
muitas vezes dando dicas sobre o motivo de ele não ter sido vetorizado no código. Esse
modelo de execução direta permite aos especialistas em outros domínios aprender como
melhorar o desempenho revisando seu código ou dando dicas para o compilador quando
é correto suportar a independência entre operações, como nas transferências gather-scatter
de dados. É esse diálogo entre o compilador e o programador, com cada lado dando
dicas para o outro sobre como melhorar o desempenho, que simplifica a programação
de computadores vetoriais.
Hoje, o principal fator que afeta o sucesso com que um programa é executado em modo
vetorial é a estrutura desse programa: os loops têm dependências reais de dados (Seção 4.5)
ou podem ser reestruturados para não ter tantas dependências? Esse fator é influenciado
pelos algoritmos escolhidos e, até certo ponto, pelo modo como eles são codificados.
Como indicação do nível de vetorização que pode ser alcançado nos programas científicos, vejamos os níveis de vetorização observados para os benchmarks Perfect Club. Esses
benchmarks são aplicações científicas grandes e reais. A Figura 4.7 mostra a porcentagem das
operações executadas no modo vetor para duas versões do código executando no Cray Y-MP.
A primeira versão é aquela obtida apenas com otimização do compilador no código original,
enquanto a segunda foi otimizada manualmente por uma equipe de programadores da
Cray Research. A grande variação no nível de vetorização do compilador foi observada por
vários estudos do desempenho das aplicações em processadores vetoriais.
As versões ricas em dicas mostram ganhos significativos no nível de vetorização para
códigos que o compilador não poderia vetorizar bem por si só, com todos os códigos
acima de 50% de vetorização. A vetorização média melhorou de aproximadamente 70%
para cerca de 90%.
FIGURA 4.7 Nível de vetorização entre os benchmarks Perfect Club quando executados no Cray Y-MP
(Vajapeyam, 1991).
A primeira coluna mostra o nível de vetorização obtido com o compilador, enquanto a segunda coluna mostra os resultados
depois que os códigos tiverem sido otimizados à mão por uma equipe de programadores da Cray Research.
Arquitetura vetorial
245
246
CAPÍTULO 4:
Paralelismo em nível de dados em arquiteturas vetoriais, SIMD e GPU
FIGURA 4.8 Resumo do suporte SIMD típico a multimídia para operações de 256 bits.
Observe que o padrão IEEE 754-2008 para ponto flutuante adicionou operações de ponto flutuante de meia precisão
(16 bits) e um quarto de precisão (128 bits).
4.3 EXTENSÕES DE CONJUNTO DE INSTRUÇÕES SIMD
PARA MULTIMÍDIA
Extensões SIMD multimídia começaram com a simples observação de que muitas aplicações de mídia operam com tipos de dados mais restritos do que aqueles para os quais os
processadores de 32 bits foram otimizados. Muitos sistemas gráficos usavam 8 bits para
representar cada uma das três cores primárias e 8 bits para transparências. Dependendo da
aplicação, amostras de áudio geralmente são representadas com 8 ou 16 bits. Particionando
as cadeias de carry dentro de um somador de 256 bits, um processador poderia realizar
operações simultâneas em vetores curtos de 32 operandos de 8 bits, 16 operandos de 16
bits, oito operandos de 32 bits ou quatro operandos de 64 bits. O custo adicional de tais
somadores particionados era baixo. A Figura 4.8 resume as instruções SIMD multimídia
típicas. Como as instruções vetoriais, uma instrução SIMD especifica a mesma operação
em vetores de dados, ao contrário das máquinas vetoriais com grandes arquivos de registradores, como o registrador vetorial VMIPS, que pode conter até 64 elementos de 64 bits
em cada um dos registradores vetoriais. As instruções SIMD tendem a especificar menos
operandos e, portanto, usam arquivos de registradores muito menores.
Em contraste com as arquiteturas vetoriais, que oferecem um conjunto elegante de instruções destinado a ser o alvo de um compilador vetorizador, as extensões SIMD têm três
grandes omissões:
j
j
j
As extensões multimídia SIMD fixaram o número de operandos de dados no
opcode, o que levou à adição de centenas de instruções nas extensões MMX, SSE e
AVX da arquitetura x86. Arquiteturas vetoriais têm um registrador de comprimento
vetorial que especifica o número de operandos para a operação atual. Esses
registradores vetoriais de comprimento variável acomodam facilmente programas
que têm, naturalmente, vetores mais curtos do que o tamanho máximo que a
arquitetura suporta. Além do mais, a arquitetura vetorial tem um comprimento
máximo vetorial implícito na arquitetura, que, combinado com o registrador de
comprimento vetorial, evita o uso de muitos opcodes.
A SIMD multimídia não oferece os modos de endereçamento mais sofisticados das
arquiteturas vetoriais, especificamente acessos por passo e acessos gather-scatter.
Esses recursos aumentam o número de programas que um compilador vetorial pode
vetorizar com sucesso (Seção 4.7).
A SIMD multimídia geralmente não oferece os registradores de máscara para
suportar a execução condicional de elementos, como nas arquiteturas vetoriais.
Essas omissões tornam mais difícil, para o compilador, gerar código SIMD e aumentam
a dificuldade de programar em linguagem assembly SIMD.
4.3
Extensões de conjunto de instruções SIMD para multimídia
Para a arquitetura x86, as instruções MMX adicionadas em 1996 modificaram o objetivo dos
registradores de ponto flutuante de 64 bits, então as instruções básicas poderiam realizar oito
operações de 8 bits ou quatro operações de 16 bits simultaneamente. As operações MAX e MIN
se juntaram a elas, além de ampla variedade de instruções condicionais e de mascaramento,
operações tipicamente encontradas em processadores de sinais digitais e instruções ad hoc
que se acreditava serem úteis nas bibliotecas de mídia importantes. Observe que o MMX
reutilizou as instruções de transferência de dados de ponto flutuante para acessar a memória.
Em 1999, as extensões SIMD para streaming (Streaming SIMD Extensions — SSE) adicionaram registradores separados que tinham 128 bits de largura, então as instruções
poderiam realizar simultaneamente 16 operações de 8 bits, oito operações de 16 bits ou
quatro operações de 32 bits. Ela também realizava aritmética paralela de ponto flutuante
de precisão simples. Como a SSE tinha registradores separados, precisava de instruções
separadas de transferência de dados. A Intel logo adicionou tipos de dados de ponto
flutuante SIMD de precisão dupla através do SSE2 em 2001, SSE3 em 2004 e SSE4 em 2007.
Instruções com quatro operações de ponto flutuante de precisão simples ou duas operações
paralelas de precisão dupla aumentariam o pico de desempenho de ponto flutuante nos
computadores ×86, contanto que os programadores colocassem os operandos lado a
lado. A cada nova geração, eles também adicionaram instruções ad hoc cujo objetivo era
acelerar funções multimídia específicas consideradas importantes.
As extensões vetoriais avançadas (Advanced Vector Extensions — AVX) adicionadas em
2010 dobraram a largura dos registradores novamente para 256 bits e, portanto, ofereceram
instruções que dobraram o número de operações em todos os tipos de dados mais estreitos. A Figura 4.9 mostra instruções AVX úteis para cálculos de ponto flutuante com
precisão dupla. A AVX inclui preparações para estender a largura para 512 bits e 1.024 bits
nas futuras gerações da arquitetura.
Em geral, o objetivo dessas extensões tem sido acelerar bibliotecas cuidadosamente escritas em vez de o compilador gerá-las (Apêndice H), mas compiladores ×86 recentes
estão tentando gerar código particularmente para aplicações intensas em termos de ponto
flutuante.
FIGURA 4.9 Instruções AVX para arquitetura ×86 úteis em programas de ponto flutuante de precisão dupla.
Duplo pacote para AVX de 256 bits significa quatro operandos de 64 bits executados em modo SIMD. Conforme a largura aumenta com o AVX, é cada vez
mais importante adicionar instruções de permutação de dados que permitam combinações de operandos estreitos de diferentes partes dos registradores
largos. O AVX inclui instruções que deslocam operandos de 32 bits, 64 bits ou 128 bits dentro de um registrador de 256 bits. Por exemplo, o BROADCAST
replica um operando de 64 bits quatro vezes em um registrador AVX. O AVX também inclui grande variedade de instruções multiplica-soma/subtrai
fundidas. Nós mostramos somente duas aqui.
247
248
CAPÍTULO 4:
Paralelismo em nível de dados em arquiteturas vetoriais, SIMD e GPU
Dados esses pontos fracos, por que as extensões SIMD multimídia são tão populares? 1)
Elas são fáceis de adicionar à unidade aritmética padrão e de implementar; 2) elas requerem
pouco estado extra em comparação com as arquiteturas vetoriais, que são sempre uma
preocupação para os tempos de troca de contexto; 3) você precisa de muita largura de
banda de memória para suportar uma arquitetura vetorial, o que muitos computadores
não têm; 4) a SIMD não tem de lidar com problemas de memória virtual quando uma
única instrução que pode gerar 64 acessos de memória pode ter uma falha de página no
meio do vetor. Extensões SIMD usam transferências de dados separadas por grupo de
operandos SIMD que estejam alinhados na memória, então não podem ultrapassar os
limites da página. Outra vantagem dos “vetores” de tamanho fixo curtos do SIMD é que é
fácil introduzir instruções que podem ajudar com novos padrões de mídia, como as que realizam permutações ou as que consomem menos ou mais operandos do que os vetores podem
produzir. Por fim, havia preocupação sobre quão bem as arquiteturas vetoriais podem trabalhar
com caches. Arquiteturas vetoriais mais recentes têm tratado de todos esses problemas, mas
o legado de falhas passadas moldou a atitude cética dos arquitetos com relação aos vetores.
Exemplo Para dar uma ideia de como são as instruções multimídia, suponha que tenhamos adicionado instruções multimídia SIMD de 256 bits ao MIPS. Neste
exemplo, nos concentramos em ponto flutuante. Nós adicionamos o sufixo
“4D” às instruções que operam com quatro operandos de precisão dupla por
vez. Como nas arquiteturas vetoriais, você pode pensar em um processador
SIMD como tendo pistas, quatro neste caso. O MIPS SIMD vai reutilizar os
registradores de ponto flutuante como operandos em instruções 4D, assim
como a precisão dupla reutilizou registradores de precisão simples no MIPS
original. Este exemplo mostra um código MIPS para o loop DAXPY. Suponha
que os endereços de início de X e Y sejam, respectivamente, Rx e Ry. Sublinhe
as mudanças no código MIPS para SIMD.
Resposta Aqui está o código MIPS:
As mudanças consistiram em substituir cada instrução MIPS de precisão dupla
pelo seu equivalente 4D, aumentando o incremento de oito para 32, e mudar
os registradores de F2 para F4 e de F4 para F8 para obter espaço suficiente no
banco de registradores para quatro operandos sequenciais de precisão dupla.
Para que cada pista SIMD tivesse sua própria cópia do escalar a, nós copiamos
o valor de F0 para os registradores F1, F2 e F3 (extensões de instruções SIMD
reais têm uma instrução para transmitir um valor para todos os outros registradores em um grupo). Assim, a multiplicação realiza F4*F0, F5*F1, F6*F2 e
F7*F3. Embora não tão drástico quanto a redução de 100x em largura de banda
de instrução dinâmica do VMIPS, o SIMD MIPS obtém uma redução de 4x: 149
versus 578 instruções executadas pelo MIPS.
4.3
Extensões de conjunto de instruções SIMD para multimídia
FIGURA 4.10 Intensidade aritmética especificada como o número de operações de ponto flutuante a serem
executadas no programa dividido pelo número de bytes acessados na memória principal (Williams et al., 2009).
Alguns kernels têm uma intensidade aritmética que aumenta com o tamanho do problema, como uma matriz densa,
mas há muitos kernels com intensidades aritméticas independentes do tamanho do problema.
Programando arquiteturas multimídia SIMD
Dada a natureza ad hoc das extensões multimídia SIMD, o modo mais fácil de usar essas
instruções é por meio de bibliotecas ou escrevendo em linguagem assembly.
Extensões recentes se tornaram mais regulares, dando ao compilador um alvo mais razoável.
Pegando emprestadas técnicas dos compiladores vetorizadores, os compiladores estão
começando a produzir automaticamente instruções SIMD. Por exemplo, os compiladores
avançados de hoje podem gerar instruções SIMD de ponto flutuante para gerar em códigos
científicos com grande desempenho. Entretanto, os programadores devem ter a certeza de
alinhar todos os dados na memória à largura da unidade SIMD na qual o código é executado
para impedir que o compilador gere instruções escalares para código que pode ser vetorizado.
O modelo Roofline de desempenho visual
Um modo visual e intuitivo de comparar o desempenho potencial de ponto flutuante
das variações da arquitetura SIMD é o modelo Roofline (Williams et al., 2009). Ele liga o
desempenho de ponto flutuante, o desempenho de memória e a intensidade aritmética
em um gráfico bidimensional. Intensidade aritmética é a razão das operações de ponto
flutuante por byte de memória acessado. Ela pode ser calculada tomando-se o número
total de operações de ponto flutuante de 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 4.10 mostra a intensidade aritmética relativa de diversos kernels de exemplo.
O pico do desempenho de ponto flutuante pode ser encontrado usando as especificações de
hardware. Muitos dos kernels nesse estudo de caso são caches integradas no chip, então o pico
de desempenho da memória é definido pelo sistema de memória por trás das caches. Observe
que nós precisamos do pico de largura de banda de memória que está disponível para os
processadores, não só nos pinos da DRAM como na Figura 4.27, na página 285. Um modo
de encontrar o pico de desempenho de memória (entregue) é executar o benchmark Stream.
A Figura 4.11 mostra o modelo Roofline para o processador vetorial NEC SX-9 à esquerda
e o computador multicore Intel Core i7 920 à direita. O eixo Y vertical é o desempenho
de ponto flutuante alcançável de 2-256 GFLOP/s. O eixo X horizontal é a intensidade
aritmética, variando de 1/8 FLOP/bytes da DRAM acessados a 16 FLOP/bytes da DRAM
acessados, em ambos os gráficos. Observe que o gráfico está em escala logarítmica e que
os Rooflines são feitos somente uma vez por computador.
Para um dado kernel, podemos encontrar um ponto no eixo X, baseado na sua intensidade
aritmética. Se traçarmos uma linha vertical através desse ponto, o desempenho do kernel
249
250
CAPÍTULO 4:
Paralelismo em nível de dados em arquiteturas vetoriais, SIMD e GPU
FIGURA 4.11 Modelo Roofline para um processador vetorial NEC SX-9 à esquerda e um computador
multicore Intel i7 920 com extensões SIMD à direita (Williams et al., 2009).
Esse Roofline é para acessos de memória de passo unitário e desempenho de ponto flutuante de precisão dupla. O NEC
SX-9 é um supercomputador vetorial anunciado em 2008 que custa milhões de dólares. Ele tem um pico de desempenho
PF PD de 102,4 GFLOP/s e um pico de largura de banda de memória de 162 GBytes/s do benchmark Stream. O Core i7
920 tem um pico de desempenho PF PD de 42,66 GFLOP/s e um pico de largura de banda de memória de 16,4 GBytes/s.
As linhas tracejadas verticais na intensidade aritmética de 4 FLOP/byte mostram que os dois processadores operam no
pico de desempenho. Nesse caso, o SX-9 a 102,4 FLOP/s é 2,4× mais rápido do que o Core i7 a 42,66 GFLOP/s. A uma
intensidade aritmética de 0,25 FLOP/byte, o SX-9 é 10x mais rápido, a 40,5 FLOP/s contra 4,1 GFLOP/s para o Core i7.
nesse computador deverá estar em algum ponto ao longo dessa linha. Podemos traçar uma
linha horizontal mostrando o pico de desempenho de ponto flutuante do computador.
Obviamente, o real desempenho de ponto flutuante não pode ser maior do que a linha
horizontal, uma vez que esse é um limite de hardware.
Como podemos traçar o pico de desempenho de memória? Uma vez que o eixo X é FLOP/
byte e o eixo Y é FLOP/s, bytes/s é só uma linha diagonal com ângulo de 45 graus nessa
figura. Portanto, podemos traçar uma terceira linha que nos dê o máximo desempenho
de ponto flutuante que o sistema de memória desse computador pode suportar para dada
intensidade aritmética. Podemos expressar os limites como uma fórmula para traçar essas
linhas nos gráficos na Figura 4.11:
GFLOPs/ s atingível = Min(pico de memória BW × intensidade aritmética,
pico de desempenho de ponto flutuante)
As linhas horizontais e verticais dão o nome a esse modelo2 simples e indicam seu valor.
O “Roofline” estabelece um limite superior sobre o desempenho de um kernel, dependendo da sua intensidade aritmética. Se pensarmos na intensidade aritmética como um
mastro que atinge o telhado, ele atinge a parte plana no telhado, o que significa que o
desempenho é limitado computacionalmente, ou atinge a parte inclinada do telhado,
o que significa que o desempenho é limitado pela largura de banda da memória. Na
Figura 4.11, a linha tracejada vertical à direita (intensidade aritmética de 4) é um exemplo
do primeiro, e a linha tracejada vertical à esquerda (intensidade aritmética de 1/4) é um
exemplo do segundo. Dado um modelo Roofline de um computador, você pode aplicá-lo
repetidamente, uma vez que ele não varia com o kernel.
Nota da Tradução: “Roofline” é uma expressão sem tradução direta em português; equivale ao “horizonte”
formado pelos telhados de casas e prédios.
2
4.4
Unidades de processamento gráfico
Observe que o “ponto limite” onde os telhados diagonal e horizontal se encontram
oferece um conhecimento interessante sobre o computador. Se ele estiver muito à direita,
somente kernels com intensidade aritmética muito alta poderão atingir o desempenho
máximo desse computador. Se estiver muito à esquerda, praticamente qualquer kernel
poderá atingir o desempenho máximo. Como veremos, esse processador vetorial tem uma
largura de banda de memória muito maior e um ponto limite muito à esquerda, quando
comparado com outros processadores SIMD.
A Figura 4.11 mostra que o pico de desempenho computacional do SX-9 é 2,4 vezes mais
rápido do que o do Core i7, mas o desempenho de memória é 10 vezes maior. Para programas com intensidade aritmética de 0,25, o SX-9 é 10 vezes mais rápido (40,5 contra
4,1 GFLOP/s). A maior largura de banda de memória move o ponto limite de 2,6 no Core
i7 para 0,6 no SX-9, o que significa que muito mais programas podem atingir o pico do
desempenho computacional no processador vetorial.
4.4
UNIDADES DE PROCESSAMENTO GRÁFICO
Por poucas centenas de dólares, qualquer um pode comprar uma GPU com centenas de
unidades paralelas de ponto flutuante, que tornam a computação de alto desempenho
mais acessível. O interesse em computação GPU aumentou quando seu potencial foi
combinado com uma linguagem de programação que tornou as GPUs mais fáceis de
programar. Portanto, hoje muitos programadores de aplicações científicas e de multimídia
estão ponderando se usam GPUs ou CPUs.
As GPUs e CPUs não têm um ancestral comum na genealogia das arquiteturas de computador. Não há elo perdido que explique as duas. Como descrito pela Seção 4.10, os
primeiros ancestrais das GPUS são os aceleradores gráficos, já que criar gráficos bem é a
razão pela qual elas existem. Embora as GPUs estejam rumando para corrente principal
da computação, não podem abandonar sua responsabilidade de continuarem sendo excelentes com gráficos. Assim, seu projeto pode fazer mais sentido quando os arquitetos perguntam, dado o hardware investido para criar bons gráficos: como podemos suplementá-lo
para melhorar o desempenho de uma gama maior de aplicações?
Observe que esta seção se concentra no uso das GPUs para computação. Para ver como
a computação por GPU combina com o papel tradicional da aceleração de gráficos, leia
“Graphics and Computing GPUs”, de John Nickolls e David Kirk (Apêndice A da 4a edição
de Computer Organization and Design, também de nossa autoria).
Como a terminologia e alguns recursos de hardware são muito diferentes entre arquiteturas
vetoriais e SIMD, acreditamos que será mais fácil se começarmos com o modelo de programação simplificada para GPUs antes de descrevermos a arquitetura.
Programando a GPU
A CUDA é uma solução elegante para o problema de representar paralelismo
em algoritmos, não em todos os algoritmos, mas o suficiente para ser importante.
Ela parece ressonar, de algum modo, com o modo como pensamos e codificamos,
permitindo uma expressão mais fácil e natural do paralelismo além do nível de tarefa.
Vincent Natol
“Kudos for CUDA”, HPC Wire (2010)
O desafio do programador de GPU não consiste simplesmente em obter bons desempenhos da GPU, mas também em coordenar o escalonamento da computação no
251
252
CAPÍTULO 4:
Paralelismo em nível de dados em arquiteturas vetoriais, SIMD e GPU
processador do sistema e a GPU, assim como a transferência de dados entre a memória
do sistema e a memória da GPU. Além do mais, como veremos mais adiante nesta
seção, as GPUs têm quase todos os tipos de paralelismo que podem ser capturados
pelo ambiente de programação: multithreading, MIMD, SIMD e mesmo em nível de
instrução.
A NVIDIA decidiu desenvolver uma linguagem semelhante a C e um ambiente de
programação que melhorariam a produtividade dos programadores de GPU, atacando
os desafios da computação heterogênea e do paralelismo multifacetado. O nome
do seu sistema é CUDA, de Arquitetura de Computação de Dispositivo Unificado
(Compute Unified Device Architecture). A CUDA produz C/C++ para o processador do sistema (host) e um dialeto C e C + + para a GPU (dispositivo, daí o D em
CUDA). Uma linguagem de programação similar é a OpenCL, que muitas empresas
estão desenvolvendo para oferecer uma linguagem independente de fornecedor para
múltiplas plataformas.
A NVIDIA decidiu que o tema unificador de todas essas formas de paralelismo é o Thread
CUDA. Usando esse nível mais baixo de paralelismo como a primitiva de programação, o
compilador e o hardware podem reunir milhares de threads CUDA para utilizar os diversos
estilos de paralelismo dentro de uma GPU: multithreading, MIMD, SIMD e em nível de
instrução. Assim, a NVIDIA classifica o modelo de programação CUDA como instrução
única, múltiplos threads (Single Instruction, Multiple Thread — SIMT). Por motivos que
veremos em breve, esses threads são reunidos em bloco e executados em grupos de 32
threads, chamados bloco de thread. O hardware que executa um bloco inteiro de threads é
chamado processador SIMD multithreaded.
Precisamos somente de alguns detalhes antes de dar um exemplo de um programa CUDA:
j
j
j
Para distinguir entre funções da GPU (dispositivo) e funções do processador do sistema
(host), a CUDA usa _device_ ou _global_ para o primeiro e _host_ para o segundo.
As variáveis CUDA declaradas como nas funções _device_ ou _global_ são alocadas
na memória da GPU (ver adiante), que é acessível por todos os processadores SIMD
multithreaded.
A sintaxe da função estendida de chamada para a função name que é executada na GPU é
onde dimGrid e dimBlock especificam as dimensões do código (em blocos) e as dimensões
de um bloco (em threads).
j
Além do identificador de blocos (blockIdx) e do identificador de threads
por bloco (threadIdx), a CUDA fornece um indentificador para o número de
threads por bloco (blockDim), que vem do parâmetro dimBlock do item anterior.
Antes de ver o código CUDA, vamos começar com um código C convencional para o loop
DAXPY da Seção 4.2:
4.4
Unidades de processamento gráfico
A seguir está a versão CUDA. Lançamos n threads, um por elemento de vetor, com 256
threads CUDA por bloco de threads em um processador SIMD multithreaded. A função
da GPU começa calculando-se o índice de elemento correspondente i, baseado no ID
do bloco, o número de threads por bloco e o ID do thread. Enquanto esse índice estiver
dentro do array (i < n), realiza a multiplicação e adição.
Comparando os códigos C e CUDA, vemos um padrão comum para paralelizar o
código CUDA com dados paralelos. A versão C tem um loop em que cada iteração é
independente das outras. Isso permite que o loop seja transformado diretamente em
um código paralelo no qual cada iteração de loop se torna um thread independente.
(Como mencionado antes e descrito em detalhes na Seção 4.5, compilação vetorial
também depende de uma falta de dependências entre as iterações de um loop, que são
chamadas dependências carregadas em loop.) O programador determina o paralelismo
em CUDA explicitamente, especificando as dimensões do grid e o número de threads
por processador SIMD. Considerando um único thread para cada elemento, não há
necessidade de sincronização entre os threads quando se gravam os resultados na
memória.
O hardware da GPU suporta a execução em paralelo e o gerenciamento de threads. Isso
não é feito por aplicações ou pelo sistema operacional. Para simplificar o escalonamento
pelo hardware, a CUDA requer que blocos de threads sejam capazes de ser executados
independentemente e em qualquer ordem. Diferentes blocos de threads não podem
se comunicar diretamente, embora possam se coordenar usando operações atômicas de
memória na memória global.
Como veremos em breve, muitos conceitos de hardware de GPU não são óbvios em
CUDA. Isso é uma coisa boa da perspectiva da produtividade de um programador, mas a
maioria dos programadores está usando GPUs em vez de CPUs para obter desempenho.
Os programadores de desempenho devem ter o hardware da GPU em mente quando
escrevem em CUDA. Por motivos que explicaremos em breve, eles sabem que precisam
manter juntos grupos de 32 threads no fluxo de controle para obter o melhor desempenho
dos processadores SIMD multithreaded e criar muitos outros threads por processador
SIMD multithreaded para ocultar a latência da DRAM. Eles também precisam manter
os endereços de dados localizados em um ou alguns blocos de memória para obter o
desempenho de memória esperado.
Como muitos sistemas paralelos, um compromisso entre a produtividade e o desempenho constitui a inclusão de intrínsecos na CUDA para dar aos programadores o controle
explícito do hardware. Muitas vezes, a luta entre garantir a produtividade e permitir ao
253
254
CAPÍTULO 4:
Paralelismo em nível de dados em arquiteturas vetoriais, SIMD e GPU
programado expressar qualquer coisa que o hardware possa fazer acontece na computação
paralela. Vai ser interessante ver como a linguagem evolui nessa clássica batalha entre
produtividade e desempenho, além de ver se a CUDA se torna popular para outras GPUS
ou mesmo outros estilos arquitetônicos.
Estruturas computacionais da GPU NVIDIA
A herança incomum mencionada ajuda a explicar por que as GPUs têm seu próprio estilo
de arquitetura e sua própria terminologia, independentemente das CPUs. Um obstáculo
para entender as GPUs tem sido o jargão, pois alguns termos têm até nomes enganosos.
Esse obstáculo tem sido surpreendentemente difícil de superar, como podem atestar as
muitas vezes que este capítulo foi reescrito. Para tentar fazer a ponte entre os objetivos
gêmeos de tornar a arquitetura das GPUs compreensível e aprender os muitos termos de
GPU com definições não tradicionais, nossa solução final foi usar a terminologia CUDA
para software, mas usar inicialmente termos mais descritivos para o hardware, às vezes
pegando emprestados termos usados pelo OpenCL. Depois de explicarmos a arquitetura
de GPU em nossos termos, vamos relacioná-los ao jargão oficial das GPUs NVIDIA.
Da esquerda para a direita, a Figura 4.12 lista os termos mais descritivos usados nesta
seção, os termos mais próximos da corrente principal da computação, os termos oficiais
da GPU NVIDIA para o caso de você estar interessado e, depois, uma rápida descrição
dos termos. O restante desta seção explica as características microarquiteturais das GPUs
usando os termos descritivos da esquerda da figura.
Usamos sistemas NVIDIA como exemplo porque eles são representativos das arquiteturas
de GPU. Seguimos especificamente a terminologia da linguagem de programação paralela
CUDA acima e usamos a arquitetura de Fermi como exemplo (Seção 4.7).
Como as arquiteturas vetoriais, as GPUs funcionam bem somente com problemas paralelos em nível de dados. Os dois estilos possuem transferências de dados gather-scatter
e registradores de máscara, e os processadores de GPU têm ainda mais registradores do
que os processadores vetoriais. Como não possuem um processador escalar próximo, às
vezes as GPUs implementam em tempo de execução, no hardware, um recurso que os
computadores vetoriais implementam em tempo de compilação, em software. Ao contrário da maioria das arquiteturas vetoriais, as GPUs também dependem de multithreading
dentro de um único processador SIMD multithreaded para ocultar a latência de memória
(Caps. 2 e 3). Entretanto, o código eficiente tanto para arquiteturas vetoriais quanto para
GPUs requer que os programadores pensem em grupos de operações SIMD.
Um grid é o código executado em uma GPU que consiste em um conjunto de blocos de
threads. A Figura 4.12 traça a analogia entre um grid e um loop vetorizado e entre um
bloco de threads e um corpo desse loop (depois que ele foi expandido, de modo que seja
um loop computacional completo). Para dar um exemplo concreto, vamos supor que
queiramos multiplicar dois vetores, cada qual com 8.192 elementos. Vamos retomar esse
exemplo ao longo desta seção. A Figura 4.13 mostra o relacionamento entre ele e os dois
primeiros termos de GPU. O código de GPU que trabalha sobre toda a multiplicação dos
8.192 elementos é chamado grid (ou loop vetorizado). Para que possa ser dividido em
partes mais gerenciáveis, um grid é composto de blocos de threads (ou corpo de um loop
vetorizado), cada qual com até 512 elementos. Observe que uma instrução SIMD executa
32 elementos por vez. Com 8.192 elementos nos vetores, esse exemplo tem 16 blocos de
threads, já que 16 = 8.192/512. O grid e o bloco de threads são abstrações de programação
implementadas no hardware da GPU que ajudam os programadores a organizar seu código
CUDA. (O bloco de threads é análogo a um loop vetorial expandido com comprimento
vetorial de 32.)
4.4
Unidades de processamento gráfico
FIGURA 4.12 Guia rápido para os termos de GPU usados neste capítulo.
Usamos a primeira coluna para os termos de hardware. Quatro grupos reúnem esses 11 termos. De cima para baixo: abstrações de programa, objetos de máquina,
hardware de processamento e hardware de memória. A Figura 4.21, na página 270, associa os termos vetoriais com os termos mais próximos aqui, e a Figura 4.24,
na página 275, e a Figura 4.25, na página 276, revelam os termos e definições oficiais CUDA/NVIDIA e AMD, juntamente com os termos usados pelo OpenCL.
255
256
CAPÍTULO 4:
Paralelismo em nível de dados em arquiteturas vetoriais, SIMD e GPU
FIGURA 4.13 Mapeamento de um grid (loop vetorizável), blocos de thread (blocos SIMD básicos) e threads
de instruções SIMD para uma multiplicação vetor-vetor, com cada vetor tendo 8.192 elementos.
Cada thread de instruções SIMD calcula 32 elementos por instrução, e neste exemplo cada bloco de threads contém
16 threads de instruções SIMD e o grid contém 16 blocos de threads. O hardware escalonador de bloco de threads
atribui blocos de threads a processadores SIMD multithreaded e o hardware escalonador de thread seleciona qual thread
de instruções SIMD executar a cada ciclo de clock em um processador SIMD. Somente threads SIMD no mesmo bloco de
threads podem se comunicar através da memória local. (O número máximo de threads SIMD que podem ser executados
simultaneamente por bloco de thread é 16 para GPUS da geração Tesla e 32 para as GPUs geração Fermi mais recentes.)
Um bloco de threads é designado para um processador que executa esse código, que chamamos processador SIMD multithreaded, pelo escalonador de bloco de threads. Esse escalonador
tem algumas similaridades com um processador de controle em uma arquitetura vetorial.
Ele determina o número de blocos de threads necessários para o loop e continua a alocá-los
para diferentes processadores SIMD multithreaded até que o loop seja completado. Neste
4.4
Unidades de processamento gráfico
FIGURA 4.14 Diagrama de blocos simplificados de um processador SIMD multithreaded.
Ele tem 16 pistas SIMD. O escalonador de threads SIMD tem cerca de 48 threads independentes de instruções SIMD,
que ele escalona com uma tabela de 48 PCs.
exemplo, ele enviaria 16 blocos de threads para processadores SIMD multithreaded a fim
de calcular os 8.192 elementos desse loop.
A Figura 4.14 mostra um diagrama de blocos simplificado de um processador SIMD multithreaded. Ele é similar a um processador vetorial, mas tem muitas unidades funcionais paralelas, em vez de poucas que são fortemente pipelined, como em um processador vetorial.
No exemplo de programação dado na Figura 4.13, cada processador SIMD multithreaded
recebe 512 elementos dos vetores para trabalhar. Os processadores SIMD são processadores
completos com PCs separados e programados usando threads (Cap. 3).
O hardware de GPU contém, portanto, uma coleção de processadores SIMD multithreaded
que executam um grid de blocos de threads (corpos de loop vetorizado). Ou seja, uma
GPU é um multiprocessador composto de processadores SIMD multithreaded.
As quatro primeiras implementações da arquitetura Fermi tinham sete, 11, 14 e 15 processadores SIMD multithreaded. Versões futuras poderão ter somente duas ou quatro. Para
fornecer escalabilidade transparente entre modelos de GPUs com números diferentes de
processadores SIMD multithreaded, o escalonador de blocos de threads designa blocos
de thread (corpos de um loop vetorizado) a processadores SIMD multithreaded. A Figura 4.15 mostra a planta da implementação GTX 480 da arquitetura Fermi.
257
258
CAPÍTULO 4:
Paralelismo em nível de dados em arquiteturas vetoriais, SIMD e GPU
FIGURA 4.15 Planta da GPU Fermi GTX 480.
Este diagrama mostra 16 processadores SIMD multithreaded. O escalonador de bloco de threads é destacado
à esquerda. O GTX 480 tem seis portas GDDR5, cada uma com 64 bits de largura, suportando até 6 GB de capacidade.
A interface de host é PCI Express 2.0 × 16. Giga Thread é o nome do escalonador que distribui blocos de threads
para múltiplos processadores, cada qual com seu próprio escalonador de threads SIMD.
Descendo mais um nível de detalhes, o objeto de máquina que o hardware cria, gerencia,
escalona e executa é um thread de instruções SIMD. Ele é um thread tradicional que contém
exclusivamente instruções SIMD. Esses threads de instruções SIMD têm seus próprios
PCs e são executados em um processador SIMD multithreaded. O escalonador de thread
SIMD inclui um scoreboard que lhe permite saber quais threads de instruções SIMD estão
prontas para serem executadas; então ele as envia para uma unidade de despacho para
serem executadas no processador SIMD multithreaded. Ele é idêntico a um escalonador
de thread de hardware em um processador multithreaded tradicional (Cap. 3), já que está
escalonando threads de instruções SIMD. Assim, o hardware de GPU tem dois níveis de
escalonadores de hardware: 1) o escalonador de bloco de threads que designa blocos
de threads (corpos de loops vetorizados) a processadores SIMD multithreaded, o que
garante que os blocos de threads sejam designados para os processadores cujas memórias
locais tenham os dados correspondentes; 2) o escalonador de threads SIMD dentro de
um processador SIMD, que escalona quando os threads de instruções SIMD devem ser
executados.
As instruções SIMD desses threads têm 32 de largura, então cada thread de instruções
SIMD neste exemplo calcularia 32 dos elementos do cálculo. Neste exemplo, os blocos
de threads conteriam 512/32 = 16 threads SIMD (Fig. 4.13).
Uma vez que o thread consiste em instruções SIMD, o processador SIMD deve ter unidades
funcionais paralelas para realizar a operação. Nós as chamamos pistas SIMD, e elas são
bastante similares às pistas vetoriais da Seção 4.2.
4.4
Unidades de processamento gráfico
O número de pistas por processador SIMD varia de acordo com as gerações de GPU. Com
a Fermi, cada thread de instruções SIMD com 32 de largura é mapeado em 16 pistas SIMD
físicas, então cada instrução SIMD em um thread de instruções SIMD leva dois ciclos de
clock para ser completada. Cada thread de instruções SIMD é executado em lock step e
escalonado somente no início. Continuando com a analogia de um processador SIMD
como processador vetorial, você poderia dizer que ele tem 16 pistas, o comprimento do
vetor seria de 32 e o chime de dois ciclos de clock. (Essa natureza ampla mas rasa é o
porquê de usarmos o termo processador SIMD em vez de processador vetorial, já que ele
é mais descritivo.)
Já que, por definição, os threads de instruções SIMD são independentes, o escalonador
de threads SIMD pode escolher qualquer thread de instruções SIMD que esteja pronto, e
não precisa se prender à próxima instrução SIMD na sequência dentro de um thread. O
escalonador de threads SIMD inclui um scoreboard (Cap. 3) para rastrear até 48 threads
de instruções SIMD a fim de verificar qual instrução SIMD está pronta. Esse scoreboard é
necessário porque as instruções de acesso à memória podem ocupar um número imprevisível de ciclos de clock, devido a conflitos de banco de memória, por exemplo. A
Figura 4.16 mostra o escalonador de threads SIMD selecionando threads de instruções SIMD em ordem diferente ao longo do tempo. A suposição dos arquitetos
de GPU é de que as aplicações de GPU têm tantos threads de instruções SIMD
que o multithreading pode tanto ocultar a latência da DRAM quanto aumentar
a utilização de processadores SIMD multithreaded. Entretanto, para cobrir as
apostas deles, a recente GPU NVIDIA Fermi inclui um cache L2 (Seção 4.7).
Continuando com nosso exemplo de multiplicação de vetores, cada processador
SIMD multithreaded deve carregar 32 elementos de dois vetores da memória
em registradores, realizando a multiplicação lendo e gravando registradores, e
armazenando o produto dos registradores de volta para a memória. Para conter
esses elementos de memória, um processador SIMD tem impressionantes 32.768
registradores de 32 bits. Assim como um processador vetorial, esses registradores
são divididos logicamente ao longo das pistas vetoriais ou, nesse caso, pistas
SIMD. Cada thread SIMD é limitado a não mais de 64 registradores, então você
pode pensar em um thread SIMD como tendo até 64 registradores vetoriais, com
cada registrador tendo 32 elementos e cada elemento tendo 32 bits de largura.
(Como os operandos de ponto flutuante com precisão dupla usam dois registradores adjacentes de 32 bits, uma visão alternativa é que cada thread SIMD tem
32 registradores vetoriais de 32 elementos, cada qual com 64 bits de largura.)
Como o Fermi tem 16 pistas SIMD físicas, cada uma contém 2.048 registradores. (Em vez de tentar projetar registradores de hardware com muitas portas de leitura
e portas de gravação por bit, as GPUs vão usar estruturas de memória mais simples, porém
dividindo-as em bancos para obter largura de banda suficiente, assim como fazem os
processadores vetoriais.) Cada thread CUDA obtém um elemento de cada um dos registradores vetoriais. Para lidar com os 32 elementos de cada thread de instruções SIMD com
16 pistas SIMD, os threads CUDA de um bloco de threads podem usar coletivamente até
metade dos 2.048 registradores.
Para ser capaz de executar vários threads de instruções SIMD, cada conjunto de registradores é alocado dinamicamente em um processador SIMD; threads de instruções SIMD
são criados e liberados quando existe o thread SIMD.
Observe que um thread CUDA é somente um corte vertical de um thread de instruções
SIMD, correspondendo a um elemento executado por uma pista SIMD. Saiba que threads
FIGURA 4.16
Escalonamento de threads
de instruções SIMD.
O escalonador seleciona
um thread de instruções
SIMD pronto e despacha
uma instrução sincronizada
para todas as pistas SIMD,
executando o thread SIMD.
Já que os threads de instruções
SIMD são independentes,
o escalonador pode selecionar
um thread SIMD diferente
a cada vez.
259
260
CAPÍTULO 4:
Paralelismo em nível de dados em arquiteturas vetoriais, SIMD e GPU
CUDA são muito diferentes de threads POSIX. Você não pode fazer chamadas arbitrárias
do sistema a partir de um thread CUDA.
Agora estamos prontos para ver como são as instruções de GPU.
Arquitetura de conjunto de instruções da GPU NVIDIA
A contrário da maioria dos processadores de sistema, o alvo do conjunto de instruções dos
compiladores NVIDIA é uma abstração do conjunto de instruções do hardware. A execução de
threads em paralelo (Parallel Thread Exectution — PTX) fornece um conjunto estável de instruções, além de compatibilidade ao longo das gerações de GPUs. O conjunto de instruções
de hardware é ocultado do programador. As instruções PTX descrevem as operações em
um único thread CUDA e, geralmente, têm correspondência um a um com as instruções de
hardware, mas um PTX pode ser expandido para muitas instruções de máquina e vice-versa.
A PTX usa registradores virtuais, o compilador descobre de quantos registradores físicos
vetoriais um thread SIMD precisa e, então, um otimizador divide o armazenamento em
um registrador disponível entre os threads SIMD. Esse otimizador também elimina o
código morto, reúne instruções e calcula locais onde os desvios podem divergir e locais
onde caminhos divergentes podem convergir.
Embora haja alguma similaridade entre as microarquiteturas ×86 e o PTX, no sentido de
que as duas se traduzem em uma forma interna (microinstruções para o ×86), a diferença
é que essa tradução acontece no hardware em runtime durante a execução no ×86 e no
software e no tempo de carregamento em uma GPU.
O formato de uma instrução PTX é
onde d é o operando de destino, a, b e c são os operandos de origem, e o tipo (type) é
um dos seguintes:
Tipo
Especificador do tipo
Bits sem tipo de 8, 16, 32 e 64 bits
Inteiro sem sinal de 8, 16, 32 e 64 bits
Inteiro com sinal de 8, 16, 32 e 64 bits
Ponto flutuante de 16, 32 e 64 bits
Os operandos de origem são registradores de 32 ou 64 bits ou um valor constante. Destinos são registradores, exceto para instruções de armazenamento.
A Figura 4.17 mostra o conjunto básico de instruções PTX. Todas as instruções podem
ser previstas por 1 bit dos registradores de predicado, que pode ser configurado por uma
instrução de predicado de conjunto (setp). As instruções de fluxo de controle são funções
de chamada e retorno, saída de thread, desvio e sincronização de barreira para threads
dentro de um bloco de threads (bar.sync). Colocar um predicado em frente a uma instrução de desvio nos dá desvios condicionais. O compilador ou programador PTX declara
registradores virtuais como valores de 32 bits ou 64 bits com tipo ou sem tipo. Por exemplo, R0, R1, ... são para valores de 32 bits, e RD0, RD1,... são para registradores de 64 bits.
Lembre-se de que a designação de registradores virtuais para registradores físicos ocorre
no momento do carregamento no PTX.
A sequência de instruções PTX a seguir é para uma iteração do nosso loop DAXPY, da
página 252:
4.4
FIGURA 4.17 Instruções de thread básicas da GPI PTX.
Unidades de processamento gráfico
261
262
CAPÍTULO 4:
Paralelismo em nível de dados em arquiteturas vetoriais, SIMD e GPU
Como demonstrado, o modelo de programação CUDA designa um thread CUDA para
cada iteração de loop e oferece um número identificador único para cada bloco de threads
(blockIdx) e um para cada thread CUDA dentro de um bloco (threadIdx). Assim, ele
cria 8.192 threads CUDA e usa o número único para endereçar cada elemento no array,
de modo que não há código de incremento ou desvio. As três primeiras instruções PTX
calculam o offset de byte desse elemento único em R8, que é somado à base dos arrays.
As instruções PTX a seguir carregam dois operandos de ponto flutuante de precisão dupla,
os multiplicam e somam, e armazenam a soma. (Vamos descrever o código PTX correspondente ao código CUDA “if (i < n)” a seguir.)
Observe que, ao contrário das arquiteturas vetoriais, as GPUs não têm instruções separadas para transferências sequenciais de dados, transferências de dados com passo e transferências de dados gather-scatter. Todas as transferências de dados são gather-scatter!
Para recuperar a eficiência das transferências de dados sequenciais (passo unitário),
as GPUs incluem hardware de aglutinação de endereço para reconhecer quando as
pistas SIMD dentro de um thread de instruções SIMD estão enviando coletivamente
endereços sequenciais. Então, esse hardware de runtime notifica a unidade de interface
de memória para requerer uma transferência de bloco de 32 palavras sequenciais.
Para obter essa importante melhoria no desempenho, o programador da GPU deve
garantir que threads CUDA adjacentes acessem endereços próximos ao mesmo tempo
que podem ser aglutinados em um ou poucos blocos da memória ou da cache, o que
nosso exemplo faz.
Desvios condicionais em GPUs
Assim como no caso das transferências de dados de passo unitário, existem fortes similaridades entre o modo como as arquiteturas vetoriais e as GPUs lidam com declarações
IF: as primeiras implementam o mecanismo principalmente em software com suporte
limitado de hardware e as segundas com o uso de mais hardware. Como veremos, além
de usar registradores explícitos de predicados, o hardware de desvio de GPU usa máscaras
internas, uma pilha de sincronização de desvio e marcadores de instrução para gerenciar
quando um desvio diverge em múltiplos caminhos de execução e quando os caminhos
convergem.
No nível de assembler do PTX, o fluxo de controle de um thread CUDA é descrito pelas
instruções PTX branch, call, return e exit, e pelo uso de predicados individuais por pista de
thread em cada instrução, especificados pelo programador com registradores de predicado
com 1 bit por pista de thread. O assembler PTX analisa o gráfico de desvios PTX e o otimiza
para a sequência de instruções de hardware GPU mais rápida.
No nível de instrução de hardware de GPU, o fluxo de controle inclui desvios, saltos,
saltos indexados, chamadas, chamadas indexadas, retornos, saídas e instruções especiais
que gerenciam a pilha de sincronização de desvio. O hardware da GPU fornece a cada
thread SIMD sua própria pilha. Uma entrada de pilha contém um token identificador,
4.4
Unidades de processamento gráfico
um endereço de instrução-alvo e uma máscara-alvo de thread ativo. Existem instruções
especiais de GPU que empilham entradas de pilha para um thread SIMD e instruções especiais e marcadores de instrução que desempilham uma entrada de pilha ou retornam
a pilha para uma entrada específica e a desviam para o endereço da instrução-alvo com a
máscara-alvo de thread ativo. As instruções de hardware de GPU também possuem
predicados individuais por pista (habilita/desabilita) especificados com um registrador
de predicado com 1 bit para cada pista.
O assembler PTX geralmente otimiza uma simples declaração IF/THEN/ELSE de nível externo, codificada com instruções de desvio PTX em instruções de GPU com predicados, sem
quaisquer instruções de desvio de GPU. Um fluxo de controle mais complexo geralmente
resulta em uma mistura de predicados e instruções de desvio de GPU com instruções especiais e marcadores que usam a pilha de sincronização de desvio para empilhar uma
entrada de pilha, quando algumas pistas desviam para o endereço-alvo enquanto outras
caem. A NVIDIA diz que um desvio diverge quando isso acontece. Essa mistura também
é usada quando uma pista SIMD executa um marcador de sincronização ou converge, que
desempilha uma entrada de pilha e a desvia para o endereço de entrada de pilha com a
máscara de thread ativo em nível de pilha.
O assembler PTX identifica desvios de loop e gera instruções de desvio de GPU que desviam para o topo do loop, além de instruções especiais de pilha para lidar com pistas individuais saindo do loop e convergindo as pistas SIMD quando todas tiverem completado
o loop. Instruções de salto indexado e chamada indexada de GPU empilham entradas na
pilha para que, quando todas as pistas completarem a declaração de troca ou chamada
de função, o thread SIMD convirja.
Uma instrução de GPU configura predicado (setp na figura anterior), avaliando a parte
não condicional da declaração IF. Por isso, a instrução de desvio PTX depende desse
predicado. Se o assembler PTX gerar instruções com predicado sem instruções de desvio
de GPU, ele usará um registrador de predicado por pista para habilitar ou desabilitar uma
pista SIMD para cada instrução. As instruções SIMD nos threads dentro da parte THEN
da declaração IF transmitem as operações para todas as pistas SIMD. Essas pistas com o
conjunto de predicados configurado em 1 realizam a operação e armazenam o resultado,
e as outras pistas SIMD não realizam uma operação ou armazenam um resultado. Para a
declaração ELSE, as instruções usam o complemento do predicado (relativo à declaração
THEN), então as pistas SIMD que estavam ociosas realizam a operação e armazenam o
resultado, enquanto suas irmãs, que antes estavam ativas, não o fazem. No final da declaração ELSE, as instruções não possuem predicados, portanto o cálculo original pode
prosseguir. Assim, para caminhos de comprimento igual, um IF-THEN-ELSE opera com
50% de eficiência.
Declarações IF podem ser aninhadas, daí o uso de uma pilha, e o assembler PTX geralmente
gera uma mistura de instruções com predicado e desvios de GPU e instruções especiais de
sincronização para controle complexo de fluxo. Observe que o aninhamento profundo
pode significar que a maioria das pistas SIMD está ociosa durante a execução de declarações
condicionais aninhadas. Assim, declarações IF duplamente aninhadas com caminhos
de mesmo tamanho rodam com 25% de eficiência, triplamente aninhadas a 12,5% de
eficiência, e assim por diante. O caso análogo seria o de um processador vetorial operando
onde somente alguns dos bits de máscara são 1.
Descendo um nível de detalhe, o assembler PTX configura um marcador de “sincronização
de desvio” em instruções condicionais apropriadas, que empilham a máscara ativa atual em
uma pilha dentro de cada thread SIMD. Se o desvio condicional diverge (algumas pistas
263
264
CAPÍTULO 4:
Paralelismo em nível de dados em arquiteturas vetoriais, SIMD e GPU
tomam o desvio, outras não), empilha uma entrada de pilha e configura a máscara interna
ativa atual com base na condição. Um marcador de sincronização de desvio desempilha a
entrada de desvio que divergiu e alterna os bits de máscara antes da porção ELSE. No final
da declaração IF, o assembler PTX adiciona outro marcador de sincronizador de desvio,
que transfere a máscara ativa anterior da pilha para a máscara ativa atual.
Se todos os bits de máscara forem configurados para 1, então a instrução de desvio no
final do THEN pula as instruções na parte ELSE. Existe uma otimização similar para a
parte THEN no caso de todos os bits de máscara serem zero, já que o desvio condicional
salta sobre as instruções THEN. Muitas vezes, declarações IF paralelas e desvios PTX
usam condições de desvio unânimes (todas as pistas concordam em seguir o mesmo
caminho), de modo que o thread SIMD não diverge em fluxos diferentes de controle de
pista individual. O assembler PTX otimiza tais desvios para pular blocos de instruções
que não são executados por qualquer pista de um thread SIMD. Essa otimização é útil na
verificação de condição de erro, por exemplo, em que o teste deve ser feito mas raramente
o desvio é tomado.
O código para uma declaração condicional similar à da Seção 4.2 é
Essa declaração IF poderia ser compilada com as seguintes instruções PTX (supondo que
R8 já tenha o ID escalado do thread), com *Push, *Comp, *Pop indicando os marcadores
de sincronização de desvio inseridos pelo assembler PTX que empilham a máscara velha,
complementam a máscara atual e desempilham para restaurar a máscara velha:
Normalmente, todas as instruções na declaração IF-THEN-ELSE são executadas por um
processador SIMD. Apenas algumas das pistas SIMD são habilitadas para as instruções
THEN e outras para as instruções ELSE. Como mencionado, no caso surpreendentemente
comum de as pistas individuais concordarem quanto ao desvio com predicado — como
desviar com base em um parâmetro que é o mesmo para todas as pistas, de modo que os
bits de máscara ativa sejam todos 0 ou todos 1 —, o desvio pula as instruções THEN ou
as instruções ELSE.
Essa flexibilidade faz parecer que um elemento tem seu próprio contador de programa.
Entretanto, no caso mais lento, somente uma lista SIMD poderia armazenar seu resultado
4.4
Unidades de processamento gráfico
a cada dois ciclos de clock, com o restante ocioso. Um caso mais lento análogo para
arquiteturas vetoriais é operar com um bit de máscara configurado para 1. Por causa dessa
flexibilidade, programadores de GPU ingênuos podem apresentar um desempenho ruim,
mas ela pode ser útil nos primeiros estágios do desenvolvimento de programas. Entretanto,
tenha em mente que as únicas opções para uma pista SIMD em um ciclo de clock é realizar
a operação especificada ou estar ocioso. Duas pistas SIMD não podem executar instruções
diferentes ao mesmo tempo.
Tal flexibilidade também ajuda a explicar o nome thread CUDA dado a cada elemento em
um thread de instruções SIMD, já que cria a ilusão de ação independente. Um programador
inexperiente pode achar que essa abstração de thread significa que a GPU lida com desvios condicionais com mais graça. Alguns threads vão em uma direção, o restante vai em
outra, o que parecerá ser verdadeiro enquanto você não estiver com pressa. Cada thread
CUDA está executando a mesma instrução que qualquer outro thread no bloco de threads
ou está ocioso. Essa sincronização torna mais fácil tratar loops com desvios condicionais,
uma vez que o recurso de máscara pode desligar pistas SIMD e detectar automaticamente
o fim do loop.
Às vezes, o desempenho resultante desmente essa simples abstração. Escrever programas
que operam pistas SIMD nesse modo MIMD altamente independente é como escrever
programas que usam grande parte do espaço de endereços virtuais em um computador
com memória física menor. Os dois estão corretos, mas podem rodar tão lentamente que
o programador pode ficar insatisfeito com o resultado.
Os compiladores vetoriais poderiam fazer os mesmos truques com registradores de máscara que as GPU fazem com hardware, mas isso envolveria instruções escalares para salvar,
complementar e restaurar registradores de máscara. A execução condicional é um caso em
que as GPUS fazem em tempo de execução de hardware o que as arquiteturas vetoriais
fazem em tempo de compilação. Uma otimização disponível em tempo de execução para
as GPUs, mas não em tempo de compilação para arquiteturas vetoriais, é pular as partes
THEN ou ELSE quando os bits de máscara são todos 0 ou todos 1.
Assim, a eficiência com que as GPUs executam declarações condicionais se resume à
frequência com que os desvios divergem. Por exemplo, um cálculo de autovalores tem
aninhamento condicional profundo, mas medições do código mostram que cerca de
82% da emissão de ciclo de clock têm entre 29 e 32 dos 32 bits de máscara configurados
para 1; então, as GPUs executam esse código com mais eficiência do que se poderia
esperar.
Observe que o mesmo mecanismo trata o desdobramento de loops vetorial — quando
o número de elementos não corresponde perfeitamente ao hardware. O exemplo do
início desta seção mostra que uma declaração IF verifica se esse número de elementos de
pista SIMD (armazenado em R8 no exemplo anterior) é menor do que o limite (i < n) e
configura as máscaras de acordo com essa informação.
Estruturas de memória da GPU NVIDIA
A Figura 4.18 mostra as estruturas de memória de uma GPU NVIDIA. Cada pista SIMD
em um processador SIMD multithreaded recebe uma seção privada de uma DRAM fora do
chip, que chamamos memória privada. Ela é usada para a estrutura de pilha, para espalhamento de registradores e para variáveis privadas que não se encaixam nos registradores.
As pistas SIMD não compartilham memórias privadas. As GPUS recentes armazenam
temporariamente essa memória privada nas caches L1 e L2 para auxiliar o espalhamento
de registradores e para acelerar as chamadas de função.
265
266
CAPÍTULO 4:
Paralelismo em nível de dados em arquiteturas vetoriais, SIMD e GPU
FIGURA 4.18 Estruturas de memória de GPU.
A memória de GPU é compartilhada por todos os grids (loops vetorizados), a memória local é compartilhada por todos
os threads de instruções SIMD dentro de um bloco de threads (corpo de um loop vetorizado), e a memória privada é
privativa de um único thread CUDA.
Chamamos memória local a memória no chip de cada processador SIMD multithreaded.
Ela é compartilhada pelas pistas SIMD dentro de um processador SIMD multithreaded, mas
não é compartilhada entre processadores SIMD multithread. O processador SIMD
multithreaded aloca dinamicamente partes da memória local para um bloco de threads
que cria o bloco de thread, liberando a memória quando todos os threads do bloco de
threads saírem. Essa porção da memória local é privativa a esse bloco de thread.
Finalmente, chamamos memória de GPU a DRAM fora do chip compartilhada por toda
a GPU e todos os blocos de threads. Nosso exemplo de multiplicação de vetores usou
somente a memória da GPU.
O processador do sistema, chamado host, pode ler ou gravar na memória da GPU. A
memória local não está disponível para o host, já que é privativa para cada processador
SIMD multithread. Memórias privadas também não estão disponíveis para o host.
Em vez de depender de grandes caches para conter todos os conjuntos funcionais de uma
aplicação, as GPUs tradicionalmente têm caches de streaming menores e dependem de
intenso multithreading de threads de instruções SIMD para ocultar a grande latência da
DRAM, uma vez que seus conjuntos funcionais podem ter centenas de megabytes. Dado o
uso de multithreading para ocultar a latência de DRAM, em vez disso a área do chip usada
para caches em processadores de sistema é gasta com recursos computacionais e no grande
número de registradores para conter o estado de muitos threads de instruções SIMD. Em
contraste, como mencionado, carregamentos e armazenamentos vetoriais amortizam a
latência através de muitos elementos, já que eles somente pagam uma vez pela latência e
então utilizam pipeline no restante dos acessos.
4.4
Unidades de processamento gráfico
Embora ocultar a latência de memória seja a filosofia fundamental, observe que as GPUs
e os processadores vetoriais mais recentes têm caches adicionais. Por exemplo, a recente
arquitetura Fermi tem caches adicionais, mas elas são consideradas filtros de largura de
banda para reduzir as demandas sobre a memória de GPU ou aceleradoras para as poucas
variáveis cuja latência não pode ser ocultada por multithreading. Assim, a memória local
para estruturas de pilha, chamadas de função e espalhamento de registradores é uma boa
correspondência para as caches, já que a latência é importante quando se chama uma
função. As caches também economizam energia, já que os acessos à cache no chip gastam
muito menos energia do que acessos a múltiplos chips DRAM externos.
Para melhorar a largura de banda da memória e reduzir o overhead, como mencionado,
as instruções de transferência de dados PTX reúnem requisições paralelas de thread do
mesmo thread SIMD em uma única requisição de bloco de memória quando os endereços
estão no mesmo bloco. Essas restrições são colocadas no programa da GPU, de modo
análogo às orientações para programas do processador do sistema realizarem a pré-busca
de hardware (Cap. 2). O controlador de memória da GPU também vai conter requisições
e enviar requisições juntas para a mesma página aberta a fim de melhorar a largura de
banda de memória (Seção 4.6). O Capítulo 2 descreve a DRAM em detalhes suficientes
para que se compreendam os benefícios potenciais de agrupar endereços relacionados.
Inovações na arquitetura da GPU Fermi
O processador SIMD multithreaded da Fermi é mais complicado do que a versão simplificada na Figura 4.14. Para aumentar a utilização de hardware, cada processador SIMD
tem dois escalonadores de thread SIMD e duas unidades de despacho de instruções. O
escalonador de thread SIMD duplo seleciona dois threads de instruções SIMD e envia uma
instrução de cada para dois conjuntos de 16 pistas SIMD, 16 unidades de carregamento/
armazenamento ou quatro unidades de função especial. Assim, dois threads de instruções
SIMD são escalonados a cada dois ciclos de clock para qualquer uma dessas coleções.
Como os threads são independentes, não há necessidade de verificar se há dependências
de dados no fluxo de instruções. Essa inovação é análoga a um processador vetorial
multithreaded, que pode despachar instruções vetoriais de dois threads independentes.
A Figura 4.19 mostra o escalonador duplo enviando instruções, e a Figura 4.20 mostra o
diagrama de blocos do processador SIMD multithreaded de uma GPU Fermi.
FIGURA 4.19 Diagrama de blocos de um escalonador de thread SIMD duplo.
Compare esse projeto ao projeto de thread SIMD único na Figura 4.16.
267
268
CAPÍTULO 4:
Paralelismo em nível de dados em arquiteturas vetoriais, SIMD e GPU
FIGURA 4.20 Diagrama de blocos do processador SIMD multithreaded de uma GPU Fermi.
Cada pista SIMD tem uma unidade de ponto flutuante pipelined, uma unidade de inteiros pipelined, alguma lógica
para despacho de instruções e operandos para essas unidades, e uma fila para conter os resultados. As quatro
unidades de função especial (SFUs) calculam funções como raízes quadradas, recíprocos, senos e cossenos.
A Fermi introduz diversas inovações para trazer as GPUs muito mais perto dos processadores de sistema mais populares do que a Tesla e as gerações anteriores de arquiteturas
de GPU:
j
j
Aritmética rápida de ponto flutuante de precisão dupla. A Fermi iguala a velocidade de
precisão dupla dos processadores convencionais em cerca de metade da velocidade
da precisão simples contra um décimo da velocidade da precisão simples na geração
Tesla anterior. Ou seja, não há tentação, em qualquer ordem de magnitude, em usar
a precisão simples quando a exatidão pede precisão dupla. O pico de desempenho
de precisão dupla aumentou de 78 GFLOP/s na GPU anterior para 515 GFLOP/s
com o uso de instruções multiplicação-soma.
Caches para memória de GPU. Embora a filosofia da GPU seja ter threads suficientes
para ocultar a latência da DRAM, existem variáveis que são necessárias para vários
threads, como as variáveis locais mencionadas. A Fermi inclui uma cache de dados
L1 e uma cache de instruções L1 para cada processador SIMD multithreaded e
uma única cache L2 de 768 KB compartilhada por todos os processadores SIMD
4.4
j
j
j
j
Unidades de processamento gráfico
multithreaded na GPU. Como mencionado, além de reduzir a pressão sobre
a largura de banda da memória GPU, as caches podem economizar mais energia
por estar no próprio chip do que as DRAM, que não estão no mesmo chip. A cache
L1, na verdade, coabita a mesma SRAM da memória local. A arquitetura Fermi tem
um bit, de modo que oferece a escolha de usar 64 KB de SRAM como uma cache
L1 de 16 KB com 48 KB de memória local ou como uma cache L1 de 48 KB com
16 KB de memória local. Observe que o GTX 480 tem uma hierarquia de memória
invertida: o tamanho do banco de registradores agregado é 2 MB, o tamanho
de todas as caches L1 está entre 0,25-0,75 MB (dependendo de elas serem de 16 KB
ou 48 KB), e o tamanho da cache L2 é de 0,75 MB. Será interessante ver o impacto
dessa razão invertida sobre as aplicações de GPU.
Endereçamento de 64 bits e um espaço de endereços unificado para todas as memórias
da GPU. Essa inovação torna muito mais fácil fornecer os ponteiros necessários
para C e C + +.
Códigos de correção de erro (ECC) para detectar e corrigir erros na memória
e nos registradores (Cap. 2). Para tornar aplicações de execução longa confiáveis
em milhares de servidores, o ECC é uma norma nos centros de dados (Cap. 6).
Troca rápida de contexto. Dado o grande estado de um processador SIMD
multithreaded, a arquitetura Fermi tem suporte de hardware para trocar contextos
muito mais rapidamente. Ela pode trocar em menos de 25 microssegundos, cerca
de 10× mais rápido do que seu predecessor.
Instruções atômicas mais rápidas. Incluídas pela primeira vez na arquitetura Tesla,
a arquitetura Fermi melhora o desempenho das instruções atômicas em 5-20× ,
para poucos microssegundos. Uma unidade de hardware especial associada
com a cache L2, e não situada dentro dos processadores SIMD multithreaded,
lida com instruções atômicas.
Similaridades e diferenças entre arquiteturas vetoriais e GPUs
Como vimos, na verdade existem muitas similaridades entre arquiteturas vetoriais e GPUs.
Além do jargão peculiar das GPUs, essas similaridades contribuíram para a confusão nos
círculos arquitetônicos sobre quão novas as GPUs realmente são. Agora que você viu o
que está por baixo dos capôs dos computadores vetoriais e das GPUs, pode apreciar tanto
as similaridades quanto as diferenças. Já que as duas arquiteturas são projetadas para
executar programas paralelos em nível de dados, mas tomam caminhos diferentes, essa
comparação é feita em profundidade para termos melhor compreensão do que é necessário para o hardware DLP. A Figura 4.21 mostra primeiro o termo vetorial e depois o
equivalente mais próximo em uma GPU.
Um processador SIMD é como um processador vetorial. Os múltiplos processadores SIMD
nas GPUs agem como núcleos MIMD independentes, assim como muitos computadores
vetoriais possuem múltiplos processadores vetoriais. Esse ponto de vista considera o NVIDIA GTX 480 uma máquina de 15 núcleos com suporte de hardware para multithreading,
em que cada núcleo tem 16 pistas. A maior diferença é o multithreading, fundamental
para as GPUs e ausente na maioria dos processadores vetoriais.
Examinando os registradores nas duas arquiteturas, o arquivo de registradores VMIPS
contém vetores completos, ou seja, um bloco contíguo de 64 duplos. Em contraste, um
único vetor em uma GPU seria distribuído através dos registradores de todas as pistas
SIMD. Um processador VMIPS tem oito registradores vetoriais com 64 elementos, ou
512 elementos no total. Um thread GPU de instruções SIMD tem até 64 registradores
com 32 elementos cada, ou 2.048 elementos. Esses registradores extras de GPU suportam
multithreading.
269
270
CAPÍTULO 4:
Paralelismo em nível de dados em arquiteturas vetoriais, SIMD e GPU
FIGURA 4.21 Equivalente de GPU para termos vetoriais.
4.4
Unidades de processamento gráfico
FIGURA 4.22 Processador vetorial com quatro pistas à esquerda e processador SIMD multithreaded de uma GPU com quatro pistas SIMD
à direita. (As GPUs geralmente têm 8-16 pistas SIMD.)
O processador de controle fornece operandos escalares para operações escalar-vetor, incrementa o endereçamento para acessos por passo unitário
e não unitário à memória e realiza outras operações do tipo contagem. O pico de desempenho de memória ocorre somente em uma GPU, quando a
unidade de junção de endereços pode descobrir o endereçamento localizado. De modo similar, o pico de desempenho computacional ocorre quando todos
os bits de máscara são configurados de modo idêntico. Observe que o processador SIMD tem um PC por thread SIMD para ajudar com o multithreading.
A Figura 4.22 é um diagrama de bloco das unidades de execução de um processador vetorial
à esquerda e de um processador SIMD multithreaded de uma GPU à direita. Para fins
pedagógicos, consideramos que o processador vetorial tem quatro pistas e o processador
SIMD multithreaded também tem quatro pistas SIMD. Essa figura mostra que as quatro
pistas SIMD agem de modo semelhante a uma unidade vetorial de quatro pistas e que um
processador SIMD age de modo semelhante a um processador vetorial.
Na verdade, existem muito mais pistas nas GPUs, então os “chimes” de GPU são menores.
Enquanto um processador vetorial pode ter 2-8 pistas e um comprimento vetorial de 32,
por exemplo — gerando um chime de 4-16 ciclos —, um processador SIMD multithreaded
pode ter oito ou 16 pistas. Um thread SIMD tem 32 elementos de largura, então um chime
de GPU seria de somente dois ou quatro ciclos de clock. Essa diferença é o motivo de
usarmos “processador SIMD” como o nome mais descritivo: ele é mais próximo de um
projeto SIMD do que de um projeto tradicional de processador vetorial.
O termo de GPU mais próximo de um loop vetorizado é grid, e uma instrução PTX é o
mais próximo de uma instrução vetorial, uma vez que um thread SIMD transmite uma
instrução PTX para todas as pistas SIMD.
Com relação às instruções de acesso de memória nas duas arquiteturas, todos os carregamentos da GPU são instruções gather e todos os armazenamentos de GPU são instruções scatter. Se os endereços de dados dos threads CUDA se referirem a endereços
271
272
CAPÍTULO 4:
Paralelismo em nível de dados em arquiteturas vetoriais, SIMD e GPU
próximos que estão no mesmo bloco de cache/memória ao mesmo tempo, a unidade
de junção de endereço da GPU vai garantir alta largura de banda de memória. As instruções de carregamento e armazenamento de passo unitário explícito das arquiteturas
vetoriais versus o passo unitário implícito da programação de GPU são o motivo pelo
qual escrever um código de GPU eficiente requer que os programadores pensem em
termos de operações SIMD, embora o modelo de programação CUDA se pareça com
MIMD. Como os threads CUDA geram seus próprios endereços, tanto com passo como
com gather-scatter, os vetores de endereçamento são encontrados tanto nas arquiteturas
vetoriais quanto nas GPUs.
Como mencionamos muitas vezes, as duas arquiteturas têm abordagens muito diferentes
para ocultar a latência de memória. Arquiteturas vetoriais as amortizam para todos
os elementos do vetor, tendo um acesso fortemente pipelined para que você pague a
latência somente uma vez por carregamento ou armazenamento vetorial. Portanto,
carregamentos e armazenamentos vetoriais são como uma transferência de bloco entre
a memória e os registradores vetoriais. Em contraste, as GPUs ocultam a latência de
memória usando multithreading. (Alguns pesquisadores estão investigando como
adicionar multithreading às arquiteturas vetoriais para tentar capturar o melhor dos
dois mundos.)
Com relação às instruções de desvio condicional, as duas arquiteturas as implementam
usando registradores de máscara. Os dois caminhos de desvio condicional ocupam
tempo e/ou espaço mesmo quando não armazenam um resultado. A diferença é que
o compilador vetorial gerencia registradores de máscara explicitamente no software,
enquanto o hardware e o assembler da GPU os gerenciam implicitamente usando
marcadores de sincronização de desvio e uma pilha interna para salvar, complementar
e restaurar máscaras.
Como mencionado, o mecanismo de desvio condicional das GPUS trata graciosamente
o problema de desdobramento de loops das arquiteturas vetoriais. Quando o comprimento do vetor é desconhecido no momento da compilação, o programa deve calcular
o módulo do comprimento do vetor de aplicação e o comprimento máximo do vetor,
e armazená-lo no registrador de comprimento de vetor. Então, o loop expandido deve
resetar o registrador de comprimento de vetor para o comprimento máximo do vetor
pelo resto do loop. Esse caso é mais simples com as GPUs, uma vez que elas somente
repetem o loop até que todas as pistas SIMD atinjam o limite do loop. Na última
iteração, algumas pistas SIMD serão mascaradas e restauradas depois que o loop for
completado.
O processador de controle de um computador vetorial tem um papel importante na
execução de instruções vetoriais. Ele transmite operações para todas as pistas vetoriais e
um valor de registrador escalar para operações vetor-escalar. Além disso, realiza cálculos
implícitos que são explícitos nas GPUs, como incrementar automaticamente endereços de
memória para carregamentos e armazenamentos de passo unitário e não unitário. A GPU
não possui processador de controle. A analogia mais próxima é o escalonador de bloco
de threads, que designa blocos de threads (corpos do loop do vetor) para processadores
SIMD multithreaded. Os mecanismos de tempo de execução de hardware em uma GPU
que gera endereços e então descobre se eles são adjacentes, o que é comum em muitas
aplicações de DLP, provavelmente são menos eficientes em termos de energia do que um
processador de controle.
O processador escalar em um computador vetorial executa as instruções escalares de um
programa vetorial, ou seja, realiza operações que seriam muito lentas para serem feitas
4.4
Unidades de processamento gráfico
na unidade vetorial. Embora o processador de sistema associado com uma GPU seja
a analogia mais próxima para um processador escalar em uma arquitetura vetorial, os
espaços de endereços separados e a transferência por um barramento PCIe significam
milhares de ciclos de clock de overhead para usá-las em conjunto. O processador escalar
pode ser mais lento do que um processador vetorial para cálculos de ponto flutuante
em um computador vetorial, mas não na mesma razão de um processador do sistema
versus um processador SIMD multithreaded (dado o overhead).
Portanto, cada “unidade vetorial” em uma GPU deve realizar cálculos que você esperaria
realizar em um processador escalar em um computador vetorial. Ou seja, em vez de
calcular no processador de sistema e comunicar os resultados, pode ser mais rápido
desabilitar todas as pistas SIMD menos uma usando os registradores de predicado e máscaras embutidas, realizando o trabalho escalar com uma pista SIMD. É provável que o
processador escalar em um computador vetorial, relativamente simples, seja mais rápido
e mais eficiente em termos de energia do que a solução da GPU. Se os processadores de
sistema e a GPU se tornarem mais intimamente ligados no futuro, será interessante ver se
os processadores de sistema poderão ter o mesmo papel que os processadores escalares
têm para as arquiteturas vetoriais e SIMD multimídia.
Similaridades e diferenças entre computadores SIMD multimídia
e GPUs
Em um nível mais alto, os computadores multicore com extensões de instruções SIMD
multimídia têm algumas similaridades com as GPUs. A Figura 4.23 resume as similaridades
e diferenças.
Os dois são multiprocessadores cujos processadores usam múltiplas pistas SIMD, embora as
GPUs tenham mais processadores e muitas outras pistas. Os dois usam multithreading de
hardware para melhorar a utilização do processador, embora as GPUs tenham suporte
de hardware para muitos outros threads. Inovações recentes nas GPUs significam que agora
os dois têm taxas melhores de desempenho entre aritmética de ponto flutuante de precisão
simples e precisão dupla. Os dois usam caches, embora as GPUs usem caches de streaming
menores e os computadores multicore usem grandes caches multinível, que tentam conter
completamente conjuntos funcionais inteiros. Os dois usam um espaço de endereços de 64 bits,
FIGURA 4.23 Similaridades e diferenças entre multicore com extensões SIMD multimídia e GPUs recentes.
273
274
CAPÍTULO 4:
Paralelismo em nível de dados em arquiteturas vetoriais, SIMD e GPU
embora a memória física principal seja muito menor nas GPUs. As GPUs suportam proteção
de memória em nível de página, mas não suportam paginação de demanda.
Além das grandes diferenças numéricas nos processadores, pistas SIMD, suporte de
hardware a thread e tamanhos de cache, existem muitas diferenças entre as arquiteturas.
As instruções de processador escalar e SIMD multimídia são fortemente integradas em
computadores tradicionais. Elas são separadas por um barramento de E/S nas GPUs
e têm memórias principais separadas. Os múltiplos processadores SIMD em uma GPU
usam um único espaço de endereços, mas as caches não são coerentes como nos computadores multicore tradicionais. Ao contrário das GPUs, as instruções SIMD multimídia não
suportam acessos gather-scatter à memória, o que a Seção 4.7 mostra ser uma omissão
significativa.
Resumo
Agora que o véu foi levantado, podemos ver que, na verdade, as GPUs são somente
processadores SIMD multithreaded, embora tenham mais processadores, mais pistas
por processador e mais hardware de multithreading do que os computadores multicore
tradicionais. Por exemplo, o Fermi GTX 480 tem 15 processadores SIMD com 16 pistas por
processador e suporte de hardware a 32 threads SIMD. O Fermi até abraça o paralelismo
em nível de instrução despachando instruções de dois threads SIMD para dois conjuntos
de pistas SIMD. Eles também têm menos memória de cache — o cache L2 do Fermi é de
0,75 megabyte — e não são coerentes com o processador escalar distante.
O modelo de programação CUDA reúne todas essas formas de paralelismo ao redor de
uma única abstração, o thread CUDA. Assim, o programador CUDA pode pensar em
programar milhares de threads, embora na verdade eles estejam executando cada bloco
de 32 threads nas muitas pistas dos muitos processadores SIMD. O programador CUDA
que quer bom desempenho tem em mente que esses threads são bloqueados e executados
32 por vez e que os endereços precisam ser adjacentes para se obter bom desempenho
do sistema de memória.
Embora tenhamos usado o CUDA e a GPU NVIDIA nesta seção, fique tranquilo, pois as
mesmas ideias são encontradas na linguagem de programação OpenCL e em GPUs de
outras empresas.
Agora que você entende melhor como as GPUs funcionam, vamos revelar o verdadeiro
jargão. As Figuras 4.24 e 4.25 fazem a correspondência entre os termos descritivos e
definições desta seção com os termos e definições oficiais da CUDA-NVIDIA e AMD.
Nós incluímos também os termos OpenCL. Acreditamos que a curva de aprendizado
de GPU seja acentuada em parte pelo fato de usarmos nomes como “multiprocessador de streaming” para o processador SIMD, “processador de thread” para a pista SIMD
e “memória compartilhada” para a memória local — especialmente porque a memória
local não é compartilhada entre processadores SIMD! Esperamos que essa abordagem
em duas etapas faça você aumentar essa curva mais rapidamente, embora ela seja um
pouco indireta.
4.5 DETECTANDO E MELHORANDO O PARALELISMO
EM NÍVEL DE LOOP
Loops em programas são a origem de muitos dos tipos de paralelismo que discutimos
aqui e veremos no Capítulo 5. Nesta seção, abordaremos a tecnologia de compilador
para descobrir quanto paralelismo podemos explorar em um programa, além do suporte
4.5
Detectando e melhorando o paralelismo em nível de loop
FIGURA 4.24 Conversão de termos usados neste capítulo para o jargão oficial NVIDIA/CUDA e AMD.
Os nomes OpenCL são dados na definição do livro.
de hardware para essas técnicas de compilador. Nós definimos precisamente quando um
loop é paralelo (ou vetorizável), como a dependência pode impedir que um loop seja
paralelo e técnicas para eliminar alguns tipos de dependência. Encontrar e manipular o
paralelismo em nível de loop é essencial para explorar DLP e TLP, além das técnicas mais
agressivas de ILP estático (p. ex., VLIW) que vamos examinar no Apêndice H.
O paralelismo em nível de loop normalmente é analisado no nível de fonte ou perto disso,
enquanto a maior parte da análise do ILP é feita depois que as instruções são geradas pelo
compilador. A análise em nível de loop envolve determinar quais dependências existem
entre os operandos em um loop durante as iterações desse loop. Por enquanto, vamos
considerar apenas as dependências de dados, que surgem quando um operando é escrito
em algum ponto e lido em um ponto posterior. As dependências de nome também existem
e podem ser removidas com técnicas de renomeação, como aquelas que exploramos no
Capítulo 3.
A análise do paralelismo em nível de loop visa determinar se os acessos aos dados nas próximas iterações são dependentes dos valores de dados produzidos nas iterações anteriores;
essa dependência é chamada dependência transportada por loop. A maioria dos exemplos
275
276
CAPÍTULO 4:
Paralelismo em nível de dados em arquiteturas vetoriais, SIMD e GPU
FIGURA 4.25 Conversão de termos usados neste capítulo para o jargão oficial NVIDIA/CUDA e AMD.
Observe que nossa descrição utiliza os nomes “memória local” e “memória privada” usados na terminologia OpenCL. O NVIDIA utiliza SIMT, instruções
únicas-múltiplos threads (single-instruction multiple-thread), em vez de SIMD, para descrever um multiprocessador de streaming. O SIMT é preferido
no lugar do SIMD, e o fluxo de controle é diferente de qualquer máquina SIMD.
4.5
Detectando e melhorando o paralelismo em nível de loop
que consideramos nos Capítulos 2 e 3 não possui dependências transportadas por loop
e, portanto, é paralela em nível de loop. Para ver se um loop é paralelo, vamos primeiro
examinar a representação-fonte:
Nesse loop existe uma dependência entre os dois usos de x[i], mas ela está dentro
de uma única iteração e não é transportada pelo loop. Existe uma dependência entre
os usos sucessivos de i em iterações diferentes que é transportada pelo loop, mas ela
envolve uma variável de indução e pode ser facilmente reconhecida e eliminada. Vimos
exemplos de como eliminar dependências envolvendo variáveis de indução durante o
desdobramento do loop na Seção 2.2 do Capítulo 2 e veremos exemplos adicionais
mais adiante.
Como localizar o paralelismo em nível de loop envolve reconhecer estruturas como
loops, referências de array e cálculos de variável de indução, o compilador pode fazer essa
análise mais facilmente no nível de fonte ou quase isso, ao contrário do nível de código
de máquina. Vejamos um exemplo mais complexo.
Exemplo
Resposta
Considere um loop como este:
Suponha que A, B e C sejam arrays distintos, não sobrepostos. (Na prática,
os arrays podem ser iguais ou se sobrepor. Como podem ser passados como
parâmetros a um procedimento que inclui esse loop, determinar se os arrays
se sobrepõem ou se são idênticos normalmente exige uma análise sofisticada
entre os procedimentos do programa.) Quais são as dependências de dados
entre as instruções S1 e S2 no loop?
Existem duas dependências diferentes:
1. S1 utiliza um valor calculado por S1 em uma iteração anterior, pois a
iteração i calcula A[i + 1], que é lido na iteração i + 1. O mesmo acontece
com S2 para B[i] e B[i + 1].
2. S2 usa o valor A[i + 1], calculado por S1 na mesma iteração.
Essas duas dependências são diferentes e possuem efeitos distintos.
Para ver como elas diferem, vamos supor que haja somente uma dessas
dependências de cada vez. Como a dependência da instrução S1 é sobre
uma iteração anterior de S1, essa dependência é transportada pelo loop.
Essa dependência força iterações sucessivas desse loop a serem executadas
em série.
A segunda dependência (S2 dependendo de S1) ocorre dentro de uma
iteração e não é transportada pelo loop. Assim, se essa fosse a única
dependência, múltiplas iterações do loop poderiam ser executadas em
paralelo, desde que cada par de instruções em uma iteração fosse mantido
em ordem. Vimos esse tipo de dependência em um exemplo na Seção
2.2, em que o desdobramento foi capaz de expor o paralelismo. Essas
dependências intraloop são comuns. Por exemplo, uma sequência de instruções vetoriais que usam encadeamento exibe claramente esse tipo de
dependência.
Também é possível ter uma dependência transportada pelo loop que não
impeça o paralelismo, como veremos no próximo exemplo.
277
278
CAPÍTULO 4:
Paralelismo em nível de dados em arquiteturas vetoriais, SIMD e GPU
Exemplo
Considere um loop como este:
Quais são as dependências entre S1 e S2? Esse loop é paralelo? Se não,
mostre como torná-lo paralelo.
Resposta
A instrução S1 usa o valor atribuído na iteração anterior pela instrução S2, de
modo que existe uma dependência transportada pelo loop entre S2 e S1. Apesar dessa dependência, esse loop pode se tornar paralelo. Diferentemente do
loop anterior, tal dependência não é circular: nenhuma instrução depende
de si mesma e, embora S1 dependa de S2, S2 não depende de S1. Um loop
será paralelo se puder ser escrito sem um ciclo nas dependências, pois a
ausência de um ciclo significa que as dependências dão uma ordenação
parcial nas instruções.
Embora não haja dependências circulares nesse loop, ele precisa ser transformado para estar de acordo com a ordenação parcial e expor o paralelismo.
Duas observações são fundamentais para essa transformação:
1. Não existe dependência de S1 para S2. Se houvesse, haveria um ciclo
nas dependências e o loop não seria paralelo. Como essa ordem de
dependência é ausente, o intercâmbio das duas instruções não afetará
a execução de S2.
2. Na primeira iteração do loop, a instrução S1 depende do valor de B[0]
calculado antes do início do loop.
Essas duas observações nos permitem substituir o loop pela sequência de
código a seguir:
A dependência entre as duas instruções não é mais transportada pelo loop,
de modo que as iterações do loop podem ser sobrepostas desde que as instruções em cada iteração sejam mantidas em ordem.
Nossa análise precisa começar encontrando todas as dependências transportadas pelo loop.
Essa informação de dependência é inexata, no sentido de que nos diz que tal dependência
pode existir. Considere o exemplo a seguir:
A segunda referência a A nesse exemplo não precisa ser traduzida para uma instrução
load, pois sabemos que o valor é calculado e armazenado pela instrução anterior;
logo, a segunda referência a A pode ser simplesmente uma referência ao registrador
no qual A foi calculado. Realizar essa otimização requer saber que as duas referências
4.5
Detectando e melhorando o paralelismo em nível de loop
são sempre para o mesmo endereço de memória e que não existe acesso intermediário
ao mesmo local. Normalmente, a análise de dependência de dados só diz que uma
referência pode depender de outra; é preciso haver uma análise mais complexa para
determinar que duas referências precisam ser para o mesmo exato endereço. No exemplo
anterior, uma versão simples dessa análise é suficiente, pois as duas referências estão
no mesmo bloco básico.
Normalmente, as dependências transportadas pelo loop ocorrem na forma de uma
recorrência. Uma recorrência ocorre quando uma variável é definida com base no valor
dessa variável em uma iteração anterior, muitas vezes a imediatamente anterior, como no
fragmento de código a seguir:
Detectar uma recorrência pode ser importante por duas razões: algumas arquiteturas (especialmente as de computadores vetoriais) têm suporte especial para executar recorrências
e, em um contexto ILP, talvez seja possível explorar uma boa quantidade de paralelismo.
Encontrando dependências
Obviamente, encontrar as dependências em um programa é importante tanto para
determinar quais loops poderiam conter paralelismo quanto para eliminar dependências
de nome. A complexidade da análise de dependência surge devido à presença de arrays
e ponteiros em linguagens como C ou C + +, ou passagem de parâmetro por referência
em FORTRAN. Como as referências de variável escalar se referem explicitamente a um
nome, em geral podem ser facilmente analisadas com rapidez com o aliasing porque
os ponteiros e os parâmetros de referência causam algumas complicações e incertezas
na análise.
Como o compilador detecta dependências em geral? Quase todos os algoritmos de
análise de dependência trabalham na suposição de que os índices de array são afins. Em
termos mais simples, um índice de array unidimensional será afim se puder ser escrito na
forma a × i + b, onde a e b são constantes e i é a variável de índice do loop. O índice de
um array multidimensional será afim se o índice em cada dimensão for afim. Os acessos
a array disperso, que normalmente têm a forma x[y[i]], são um dos principais exemplos
de acessos não afim.
Determinar se existe uma dependência entre duas referências para o mesmo array em um
loop é, portanto, equivalente a determinar se duas funções afins podem ter o mesmo valor
para diferentes índices entre os limites do loop. Por exemplo, suponha que tenhamos
armazenado em elemento de array com valor de índice a × i + b e carregado do mesmo
array com o valor de índice c × i + d, onde i é a variável de índice do loop for que vai de
m até n. Existirá uma dependência se duas condições forem atendidas:
1. Existem dois índices de iteração, j e k, ambos dentro dos limites do loop-for. Ou
seja, m ≤ j ≤ n, m ≤ k ≤ n.
2. O loop armazena um elemento do array indexado por a × j + b e depois apanha
esse mesmo elemento de array quando ele for indexado por c × k + d. Ou seja,
a × j + b = c × k + d.
Em geral, não podemos determinar se existe uma dependência em tempo de compilação.
Por exemplo, os valores de a, b, c e d podem não ser conhecidos (podem ser valores em
279
280
CAPÍTULO 4:
Paralelismo em nível de dados em arquiteturas vetoriais, SIMD e GPU
outros arrays), o que torna impossível saber se existe uma dependência. Em outros casos,
o teste de dependência pode ser muito dispendioso, mas decidido no momento da
compilação. Por exemplo, os acessos podem depender dos índices de iteração de vários
loops aninhados. Contudo, muitos programas contêm principalmente índices simples,
em que a, b, c e d são constantes. Para esses casos, é possível criar alguns testes em tempo
de compilação para a dependência.
Para exemplificar, um teste simples e satisfatório para a ausência de dependência é o teste
do maior divisor comum (MDC). Ele é baseado na observação de que, se existir uma dependência transportada pelo loop, o MDC (c,a) precisa ser divisível por (d – b). (Lembre-se
de que um inteiro, x, será divisível por outro inteiro, y, se obtivermos um quociente inteiro
quando realizarmos a divisão y/x e não houver resto.)
Exemplo
Use o teste do MDC para determinar se existem dependências no loop a
seguir:
Resposta
Dados os valores a = 2, b = 3, c = 2 e d = 0, então o MDC(a,c) = 2, e d −
b = − 3. Como 2 não é divisível por −3, nenhuma dependência é possível.
O teste do MDC é suficiente para garantir que não existe dependência; porém, existem
casos em que o teste do MDC tem sucesso, mas não existe dependência. Isso pode
surgir, por exemplo, porque o teste do MDC não leva em consideração os limites
do loop.
Em geral, determinar se uma dependência realmente existe é incompleto. Na prática,
porém, muitos casos comuns podem ser analisados com precisão a baixo custo. Recentemente, técnicas que usam uma hierarquia de testes exatos aumentando em generalidade
e custos foram consideradas precisas e eficientes. (Um teste é exato se ele determina com
precisão se existe uma dependência. Embora o caso geral seja incompleto, existem testes
exatos para situações restritas que são muito mais baratos.)
Além de detectar a presença de uma dependência, um compilador deseja classificá-la
quanto ao tipo. Essa classificação permite que um compilador reconheça as dependências
de nome e as elimine durante a compilação, renomeando e copiando.
Exemplo O loop a seguir possui múltiplos tipos de dependência. Encontre todas as
dependências verdadeiras, as dependências de saída e as antidependências,
e elimine as dependências de saída e as antidependências pela renomeação.
4.5
Detectando e melhorando o paralelismo em nível de loop
Resposta As dependências a seguir existem entre as quatro instruções:
1. Existem dependências verdadeiras de S1 para S3 e de S1 para S4, devido
a Y[i]. Estas não são transportadas pelo loop, de modo que não impedem
que o loop seja considerado paralelo. Essas dependências forçarão S3 e S4
a esperar que S1 termine.
2. Existe uma antidependência de S1 para S2, com base em X[i].
3. Existe uma antidependência de S3 para S4 para Y[i].
4. Existe uma dependência de saída de S1 para S4, com base em Y[i].
A versão do loop a seguir elimina essas falsas (ou pseudo) dependências.
Após o loop, a variável X foi renomeada para X1. No código seguinte ao loop,
o compilador pode simplesmente substituir o nome X por X1. Nesse caso, a
renomeação não exige uma operação de cópia real, mas pode ser feita substituindo nomes ou pela alocação de registrador. Porém, em outros casos, a
renomeação exigirá a cópia.
A análise de dependência é uma tecnologia essencial para explorar o paralelismo, assim
como para o bloqueio similar da transformação abordada no Capítulo 2. A análise de
dependência é a ferramenta básica para detectar o paralelismo em nível de loop. Compilar
efetivamente os programas para computadores vetoriais, computadores SIMD ou multiprocessadores é algo que depende criticamente dessa análise. A principal desvantagem
da análise de dependência é que ela só se aplica sob um conjunto limitado de circunstâncias, a saber, entre referências dentro de um único aninhamento de loop e usando
funções de índice afins. Portanto, há uma grande variedade de situações em que a análise
de dependência orientada a array não pode nos dizer o que poderíamos querer saber, por
exemplo, analisar acessos realizados com ponteiros, e não com índices de array, pode ser
muito mais difícil. (Essa é uma das razões pelas quais o FORTRAN é preferido ao C e C + +
em muitas aplicações científicas projetadas para computadores paralelos.) Da mesma
forma, analisar referências através de chamadas de procedimento é extremamente difícil.
Assim, embora a análise de código escrito em linguagens sequenciais continue sendo
importante, precisamos também de técnicas como OpenMP e CUDA, que escrevem loops
explicitamente paralelos.
Eliminando cálculos dependentes
Como mencionado, uma das formas mais importantes de cálculo dependente é uma
recorrência. Um produto de ponto é um exemplo perfeito de uma recorrência:
Esse loop não é recorrente, porque possui uma dependência carregada pelo loop na variável
sum. Entretanto, podemos transformá-lo em um conjunto de loops: um deles é completamente paralelo e o outro pode ser parcialmente paralelo. O primeiro loop executará
a parte completamente paralela desse loop. Ele é assim:
281
282
CAPÍTULO 4:
Paralelismo em nível de dados em arquiteturas vetoriais, SIMD e GPU
Observe que sum foi expandido de um escalar para um vetor (uma transformação chamada
expansão escalar) e que essa transformação torna o novo loop completamente paralelo.
Quando acabamos, entretanto, precisamos realizar o passo de redução, que soma os
elementos do vetor. Ele é assim:
Embora esse loop não seja paralelo, tem uma estrutura bastante específica chamada
redução. Reduções são comuns em Álgebra Linear e, como veremos no Capítulo 6, são
também parte importante da primitiva de paralelismo primário MapReduce, usada em
computadores em escala warehouse. Em geral, qualquer função pode ser usada como
operador de redução, e casos comuns incluem operadores como max e min.
Às vezes, as reduções são tratadas por um hardware especial em um vetor e arquitetura
SIMD, que permite que o passo de redução seja realizado muito mais rapidamente do
que seria em modo escalar. Elas funcionam implementando uma técnica similar ao que
pode ser feito em um ambiente de multiprocessador. Embora a transformação geral
funcione com qualquer número de processadores, para simplificar suponha que tenhamos
10 processadores. No primeiro passo, que se refere a reduzir a soma, cada processador
executa o seguinte (com p como o número de processador, indo de 0 a 9):
Esse loop, que soma 1.000 elementos em cada um dos 10 processadores, é completamente
paralelo. Assim, um loop escalar simples pode completar o somatório das últimas 10
somas. Abordagens similares são usadas em processadores vetoriais e SIMD.
É importante observar que essa transformação depende da associatividade da adição.
Embora a aritmética com alcance e precisão ilimitados seja associativa, a aritmética dos
computadores não é associativa, seja aritmética de inteiros, pelo alcance limitado, seja
aritmética de ponto flutuante, pelo alcance e pela precisão. Assim, às vezes usar essas técnicas de reestruturação pode levar a um comportamento errôneo, embora tais ocorrências
sejam raras. Por esse motivo, a maioria dos compiladores requer que as otimizações que
dependem da associatividade sejam habilitadas explicitamente.
4.6
QUESTÕES CRUZADAS
Energia e DLP: lento e largo versus rápido e estreito
Uma vantagem energética fundamental das arquiteturas paralelas em nível de dados vem
da equação energética do Capítulo 1. Como consideramos amplo o paralelismo em nível
de dados, o desempenho será o mesmo se reduzirmos a taxa de clock à metade e dobrarmos
os recursos de execução: duas vezes o número de pistas para um computador vetorial,
registradores e ALUs mais largos para SIMD multimídia e mais pistas SIMD para GPUs.
Se pudermos reduzir a voltagem e, ao mesmo tempo, reduzir a taxa de clock, poderemos
realmente reduzir a energia tanto quanto a potência para a computação, mantendo o
mesmo pico de desempenho. Portanto, os processadores DLP tendem a ter taxas de clock
4.6
menores do que os processadores de sistema, cujo desempenho depende de altas taxas
de clock (Seção 4.7).
Comparados aos processadores fora de ordem, os processos DLP podem ter lógica de
controle mais simples para lançar um grande número de operações por ciclo de clock.
Por exemplo, o controle é idêntico para todas as pistas em processadores vetoriais, e não
existe lógica para decidir entre despacho múltiplo de instrução e lógica de execução especulativa. As arquiteturas vetoriais também podem tornar mais fácil desligar partes não
utilizadas do chip. Cada instrução vetorial descreve explicitamente todos os recursos de
que precisa para um número de ciclos quando a instrução é despachada.
Memória em banco e memória gráfica
A Seção 4.2 destacou a importância da largura de banda de memória significativa em arquiteturas vetoriais para suportar passo unitário, passo não unitário e acessos gather-scatter.
Para atingir alto desempenho, as GPUs também requerem largura de banda substancial de
memória. Chips DRAM especiais projetados somente para GPUs, chamados GDRAM,
de DRAM gráfica (Graphics DRAM), ajudam a fornecer essa largura de banda. Chips
GDRAM têm maior largura de banda, muitas vezes com capacidade menor do que chips
DRAM convencionais. Para fornecer essa largura de banda, muitas vezes os chips GDRAM
são soldados diretamente na mesma placa da GPU, em vez de serem colocados em
módulos DIMM, que são inseridos em slots em uma placa, como é o caso da memória
de sistema. Os módulos DIMM permitem capacidade muito maior e atualização do sistema, ao contrário da GDRAM. Essa capacidade limitada — cerca de 4 GB em 2011 — está
em conflito com o objetivo de executar problemas maiores, que é um uso natural da
capacidade computacional maior das GPUs.
Para apresentar o melhor desempenho possível, as GPUs tentam levar em conta todos
os recursos das GDRAMs. Em geral são organizados internamente em 4-8 bancos, com
número de linhas sendo uma potência de 2 (tipicamente 16.384) e uma potência de 2 de
bits por linha (tipicamente 8.192). O Capítulo 2 descreve os detalhes do comportamento
da DRAM que as GPUs tentam igualar.
Dadas todas as demandas potenciais das tarefas de cálculo e de aceleração gráfica sobre
as GDRAMs, o sistema de memória poderia deparar com grande número de requisições
não correlacionadas. Infelizmente, essa diversidade prejudica o desempenho de memória.
Para lidar com isso, o controlador de memória da GPU mantém listas separadas de tráfego
limitadas para diferentes bancos de GDRAM, aguardando até que haja tráfego suficiente
para justificar abrir uma linha e transferir todos os dados requisitados de uma vez. Esse
atraso melhora a largura de banda, mas aumenta a latência, e o controlador deve garantir
que nenhuma unidade de processamento fique com fome enquanto espera por dados,
caso contrário, os processadores vizinhos poderão ficar ociosos. A Seção 4.7 mostra que
as técnicas gather-scatter e as de acesso ciente dos bancos de memória podem apresentar
aumentos substanciais no desempenho em comparação com as arquiteturas convencionais
baseadas em cache.
Acessos por passo e perdas de TLB
Um problema com acessos por passo é como eles interagem com o buffer lookaside de
tradução (TLB) para a memória virtual em arquiteturas vetoriais ou GPUs. (As GPUs usam
as TLBs para mapeamento de memória.) Dependendo de como a TLB é organizada e do
tamanho do array sendo acessado na memória, é possível até conseguir uma perda de TLB
para cada acesso a um elemento no array!
Questões cruzadas
283
284
CAPÍTULO 4:
Paralelismo em nível de dados em arquiteturas vetoriais, SIMD e GPU
FIGURA 4.26 Características principais das GPUs para clientes móveis e servidores.
O Tegra 2 é a plataforma de referência para o OS Android e é encontrado no telefone celular LG Optimus 2X.
4.7 JUNTANDO TUDO: GPUs MÓVEIS VERSUS GPUs
SERVIDOR TESLA VERSUS CORE i7
Dada a popularidade das aplicações gráficas, hoje as GPUs são encontradas em clientes
móveis, além de servidores tradicionais ou computadores desktop para trabalho pesado.
A Figura 4.26 lista as principais características do NVIDIA Tegra 2 para clientes móveis,
que é usado no LG Optimus 2X e roda o SO Android, e a GPU Fermi para servidores. Os
engenheiros de GPUs servidor esperam ser capazes de realizar animação ao vivo dentro
de cinco anos depois de um filme ser lançado. Os engenheiros de GPUs móveis, por sua
vez, querem que em mais cinco anos um cliente móvel possa fazer tudo o que um servidor
ou console de games faz hoje. Mais concretamente, o objetivo geral é que a qualidade dos
gráficos de um filme como Avatar seja atingida em tempo real em uma GPU servidor em
2015 e na sua GPU móvel em 2020.
O NVIDIA Tegra 2 para dispositivos móveis fornece tanto o processador de sistema quanto
a GPU em um único chip usando uma única memória física. O processador de sistema
é um ARM Cortex-A9 dual core, com cada núcleo usando execução fora de ordem e despacho duplo de instruções. Cada núcleo inclui a unidade de ponto flutuante opcional.
A GPU possui hardware para sombreamento programável de pixel, vértice e iluminação
programáveis e gráficos 3-D, mas não inclui os recursos computacionais de GPU necessários para executar programas CUDA ou OpenCL.
O tamanho do die é de 57 mm2 (7,5 × 7,5 mm) em um processo TSMC de 40 nm e
contém 242 milhões de transistores. Ele usa 1,5 watt.
O NVIDIA 480, na Figura 4.26, é a primeira implementação da arquitetura Fermi. A taxa
de clock é de 1,4 GHz e inclui 15 processadores SIMD. O próprio chip tem 16, mas,
para melhorar o rendimento, somente 15 deles precisam funcionar para esse produto.
O caminho para a memória GDDR5 tem 384 (6 × 64) bits de largura e interface com o
clock a 1,84 GHz, oferecendo um pico de largura de banda de memória de 177 GBytes/s e
4.7
Juntando tudo: GPUs móveis versus GPUs servidor Tesla versus Core i7
FIGURA 4.27 Especificações do Intel Core i7-960, NVIDIA GTX 280 e GTX 480.
As colunas à direita mostram as razões entre o GTX 280 e GTX 480 e Core i7. Para FLOPS SIMD de precisão simples no GTX 280, a velocidade mais alta
(933) vem de um caso muito raro de despacho duplo de multiplicação-soma e multiplicação fundidos. O mais razoável é 622 para multiplicações-somas
únicas fundidas. Embora o estudo de caso compare o 280 e o i7, incluímos o 480 para mostrar seu relacionamento com o 280, já que ele é descrito neste
capítulo. Observe que essas larguras de banda de memória são maiores do que as apresentadas na Figura 4.28, porque são larguras de banda de pinos
DRAM, e as apresentadas na Figura 4.28 são os processadores como medidos por um programa de benchmark. (Da Tabela 2 em Lee et al., 2010.)
transferindo nas duas bordas do clock da memória de taxa dupla de dados. Ele se conecta
com o processador do sistema host e a memória através de um link PCI Express 2.0 × 16,
que tem um pico de taxa bidirecional de 12 GBytes/s.
Todas as características físicas do die GTX 480 são impressionantemente grandes: ele contém 30 bilhões de transistores, o tamanho do substrato é de 520 mm2 (22,8 × 22,8 mm),
em um processo TSMC de 40 nm, e a potência típica é de 167 watts. O módulo todo
é de 250 watts, e inclui GPU, GDRAMs, ventoinhas, reguladores de potência etc.
Comparação entre uma GPU e uma MIMD com SIMD multimídia
Um grupo de pesquisadores da Intel publicou um artigo (Lee et al., 2010) comparando um
Intel i7 quadcore (Cap. 3) com extensões SIMD multimídia e GPU da geração anterior, a
Tesla GTX 280. A Figura 4.27 lista as características dos dois sistemas. Ambos os produtos
foram comprados no outono de 2009. O Core i7 usa a tecnologia semicondutora de
45 nm da Intel, enquanto a GPU usa a tecnologia de 65 nanômetros da TSMC. Embora
possa ter sido mais justo fazer uma comparação com uma parte neutra ou as duas partes
interessadas, o objetivo desta seção não é determinar o quanto um produto é mais rápido
do que o outro, mas tentar entender o valor relativo dos recursos desses dois estilos
arquitetônicos contrastantes.
Os rooflines do Core i7 920 e GTX 280, na Figura 4.28, ilustram as diferenças nos computadores. O 920 tem uma taxa de clock mais lenta do que o 960 (2,66 GHz versus 3,2 GHz),
mas o resto do sistema é o mesmo. Não só o GTX tem largura de banda de memória
285
286
CAPÍTULO 4:
Paralelismo em nível de dados em arquiteturas vetoriais, SIMD e GPU
FIGURA 4.28 Modelo roofline (Williams et al., 2009).
Esses rooflines mostram o desempenho de ponto flutuante de precisão dupla na linha superior e o desempenho de precisão simples na linha inferior.
(O teto de desempenho de PF PD também está na linha inferior, para dar uma perspectiva.) O Core i7 920, à esquerda, tem um pico de desempenho
PF PD de 42,66 GFLOP/s, um pico de PF PD de 85,33 GBytes/s e um pico de largura de banda de memória de 16,4 GBytes/s. O NVIDIA GTX 280 tem
um pico de PF PD de 78 GFLOP/s, um pico de PF PS de 624 GBytes/s e um pico de largura de banda de memória de 127 GBytes/s. A linha vertical
tracejada à esquerda representa uma intensidade aritmética de 0,5 FLOP/byte. Ela é limitada pela largura de banda de memória de não mais de 8 PD
de GFLOP/s ou 8 PS GLOP/s no Core i7. A linha vertical tracejada à direita tem uma intensidade aritmética de 4 FLOP/byte. Ela é limitada somente
computacionalmente a 42,66 PD GLOP/s e 64 PS GFLOP/s no Core i7 e 78 PD GLOP/s e 512 PD GFLOP/s no GTX 280. Para atingir a maior taxa
computacional no Core i7 você precisa usar os quatro núcleos e instruções SSE com um número igual de multiplicações e somas. Para o GTX 280,
você precisa usar instruções multiplicação-soma fundidas em todos os processadores SIMD multithreaded. Guz et al. (2009) têm um modelo analítico
interessante para essas duas arquiteturas.
e desempenho de ponto flutuante de precisão dupla muito maiores, como seu ponto
limite de precisão dupla também está consideravelmente à esquerda. Como mencionado,
quanto mais à esquerda estiver o ponto limite do roofline, mais fácil será atingir o pico
do desempenho computacional. O ponto limite de precisão dupla é 0,6 para o GTX 280
versus 2,6 para o Core i7. Para desempenho de precisão simples, o ponto limite se move
para a direita, pois é muito mais difícil atingir o “telhado” do desempenho de precisão
4.7
Juntando tudo: GPUs móveis versus GPUs servidor Tesla versus Core i7
FIGURA 4.29 Características de throughput computacional de kernel (da Tabela 1 em Lee et al., 2010).
Os nomes entre parênteses identificam o nome do benchmark nesta seção. Os autores sugerem que os códigos para as duas máquinas têm igual esforço
de otimização.
simples, já que ele é muito mais alto. Observe que a intensidade aritmética do kernel se
baseia nos bytes que vão para a memória principal, não nos que vão para a memória de
cache. Assim, o uso da cache pode mudar a intensidade aritmética de um kernel em um
computador em particular, considerando que a maioria das referências realmente vai para
a cache. O roofline ajuda a explicar o desempenho relativo neste estudo de caso. Observe
também que essa largura de banda é para acessos de passo unitário nas duas arquiteturas.
Endereços gather-scatter reais que não são fundidos são mais lentos no GTX 280 e no Core
i7, como podemos ver.
Os pesquisadores dizem que selecionaram os programas de benchmark analisando as
características computacionais e de memória dos quatro sites recém-propostos de benchmark, “formulando o conjunto de kernels de throughput computacional que capturam essas
características”. A Figura 4.29 descreve esses 14 kernels, e a Figura 4.30 mostra os resultados
de desempenho com números maiores significando maior velocidade.
Dado que as especificações de desempenho bruto do GTX 280 variam de 2,5× mais lento
(taxa de clock) a 7,5× mais rápido (núcleos por chip), enquanto o desempenho varia de
2,0× mais lento (Solv) a 15,2× mais rápido (GJK), os pesquisadores da Intel exploraram
as razões para essas diferenças:
j
Largura de banda de memória. A GPU tem 4,4× a largura de banda de memória,
o que ajuda a explicar por que o LBM e o SAXPY rodam 5,0× e 5,3× mais rápido,
respectivamente. Seus conjuntos funcionais têm centenas de megabytes e, portanto,
não se encaixam no cache do Core i7. (Para acessar intensamente a memória,
287
288
CAPÍTULO 4:
Paralelismo em nível de dados em arquiteturas vetoriais, SIMD e GPU
FIGURA 4.30 Desempenhos brutos e relativos medidos para as duas plataformas.
Nesse estúdio, o SAXPY é usado somente como medida de largura de banda de memória, então a unidade correta é
GBytes/s, e não GFLOP/s. (Baseado na Tabela 3 em Lee et al., 2010.)
j
j
eles não usam bloqueio de cache no SAXPY.) Assim, a rampa dos rooflines explica
seu desempenho. O SpMV tem também um grande conjunto funcional, mas
roda a somente 1,9 ×, porque o ponto flutuante de precisão dupla da GTX 280
é somente 1,5× mais rápida do que o Core i7. (Lembre-se de que a precisão dupla
da Fermi GTX 480 é 4× mais rápida do que a Tesla GTX 280.)
Largura de banda computacional. Cinco dos kernels restantes são limitados
computacionalmente: SGEMM, Conv, FFT, MC e Bilat. A GTX é mais rápida 3,9,
2,8, 3,0, 1,8 e 5,7, respectivamente. Os três primeiros usam aritmética de ponto
flutuante de precisão simples, e a precisão simples da GTX 280 é 3× a 6× mais rápida.
(O “9× mais rápido que o Core i7”, como mostrado na Figura 4.27, ocorre somente
em um caso muito especial em que a GTX 280 pode despachar um multiplicação-soma
fundida e uma multiplicação por ciclo de clock.) O MC usa precisão dupla, o que
explica por que ele é somente 1,8× mais rápido, já que o desempenho de PD é
somente 1,5× mais rápido. Bilat usa funções transcendentais, que a GTX 280 suporta
diretamente (Fig. 4.17). O Core i7 passa dois terços do seu tempo calculando funções
transcendentais, então a GTX 280 é 5,7× mais rápida. Essa observação ajuda a destacar
o valor do suporte de hardware para operações que ocorrem na carga de trabalho:
ponto flutuante de precisão dupla e, talvez, até mesmo transcendentais.
Benefícios de cache. A emissão de raios (RC) é somente 1,6× mais rápida na GTX
porque o bloqueio de caches nas caches do Core i7 o impede de se tornar limitado
pela largura de banda de memória, como as GPUs. O bloqueio de cache pode
ajudar também na busca. Se as árvores de índices forem pequenas de modo que se
encaixem na cache, o Core i7 é duas vezes mais rápido. Árvores maiores de índices
as tornam limitadas pela largura de banda de memória. No geral, a GTX 280 executa
buscas 1,8× mais rapidamente. O bloqueio de cache também auxilia na ordenação
(sort). Enquanto a maioria dos programadores não executaria a ordenação em um
processador SIMD, ela pode ser escrita com uma primitiva de ordenação de 1 bit
chamada split. Entretanto, o algoritmo split executa muito mais instruções do que
4.7
j
j
Juntando tudo: GPUs móveis versus GPUs servidor Tesla versus Core i7
uma organização escalar. Como resultado, a GTX roda somente 0,8× mais rápido
do que o Core i7. Observe que as caches também ajudam outros kernels no Core i7,
já que o bloqueio de cache permite que SGEMM, FFT e SpMV se tornem limitados
computacionalmente. Essa observação reenfatiza a importância das otimizações
de bloqueio de cache no Capítulo 2. (Seria interessante ver como as caches da Fermi
GTX 480 vão afetar os seis kernels mencionados neste parágrafo.)
Gather/Scatter. As extensões SIMD multimídia serão de pouca ajuda se os dados
estiverem espalhados pela memória principal. O desempenho ótimo vem somente
quando os dados estão alinhados em limites de 16 bytes. Assim, GJK obtém
pouco benefício do SIMD no Core i7. Como mencionado, as GPUs oferecem
endereçamento gather-scatter, que é encontrado em uma arquitetura vetorial
mas omitido nas extensões SIMD. A unidade de união de endereço também ajuda
combinando acessos à mesma linha DRAM, reduzindo assim o número de gathers
e scatters. O controlador de memória também reúne acessos à mesma página
DRAM. Essa combinação significa que a GTX 280 executa o GJK 15,2× mais rápido
do que o Core i7, que é mais do que qualquer parâmetro físico na Figura 4.27.
Essa observação reforça a importância do gather-scatter para arquiteturas vetoriais
e GPU, que está ausente nas extensões SIMD.
Sincronização. A sincronização de desempenho é limitada pelas atualizações
atômicas, que são responsáveis por 28% do tempo de execução total no Core
i7, apesar de ele ter uma instrução busca e incremento de hardware. Assim, Hist
é somente 1,7× mais rápido na GTX 280. Como mencionado, as atualizações
atômicas da Fermi GTX 480 são 5× a 20× mais rápidas do que aquelas na Tesla
GTX 280, então novamente seria interessante executar Hist na GPU mais nova.
Solv soluciona um conjunto de restrições independentes com pouca computação,
seguida por uma sincronização de barreira. O Core i7 se beneficia das instruções
atômicas e um modelo de consistência de memória que garante os resultados
corretos mesmo que nem todos os acessos à hierarquia de memória tenham sido
completados. Sem o modelo de consistência de memória, a versão da GTX 280
lança alguns conjuntos do processador de sistema, o que leva a GTX 280 a rodar
0,5× mais rápido do que o Core i7. Essa observação destaca como o desempenho
de sincronização pode ser importante para alguns problemas de dados paralelos.
É surpreendente com que frequência os pontos fracos na Tesla GTX 280 que foram descobertos por kernels selecionados pelos pesquisadores da Intel já haviam sido endereçados na
arquitetura que sucedeu a Tesla. A Fermi tem desempenho de ponto flutuante de precisão
dupla mais rápida, operações atômicas e caches. Em um estudo relacionado, pesquisadores
da IBM fizeram a mesma observação (Bordawekar, 2010). É interessante também que o
suporte gather-scatter das arquiteturas vetoriais, décadas mais antigo que as instruções
SIMD, era tão importante para as eficazes utilidades dessas extensões SIMD que algumas
pessoas haviam feito uma previsão antes dessa comparação (Gebis e Patterson, 2007).
Os pesquisadores da Intel observaram que seis dos 14 kernels explorariam melhor o SIMD
com suporte mais eficiente a gather-scatter no Core i7. Esse estudo certamente estabelece
também a importância do bloqueio de cache. Será interessante ver se gerações futuras do
hardware, compiladores e bibliotecas multicore e de GPU respondem com recursos que
melhoram o desempenho de tais kernels.
Esperamos que haja mais dessas comparações multicore/GPU. Observe que um importante
recurso ausente dessa comparação foi descrever o nível de esforço para obter os resultados
para os dois sistemas. Idealmente, as comparações futuras liberariam o código usado nos
dois sistemas para que outros pudessem recriar os mesmos experimentos em plataformas
de hardware diferentes e, possivelmente, melhorar os resultados.
289
290
CAPÍTULO 4:
Paralelismo em nível de dados em arquiteturas vetoriais, SIMD e GPU
4.8
FALÁCIAS E ARMADILHAS
Enquanto paralelismo em nível de dados é a forma mais fácil de paralelismo após ILP
da perspectiva do programador, e plausível a facilidade do ponto de vista dos arquitetos,
ainda tem muitas falácias e armadilhas.
Falácia. As GPUs sofrem por serem coprocessadores.
Embora a divisão entre a memória principal e a memória de GPU apresente desvantagens,
existem vantagens em estar distante da CPU.
Por exemplo, as PTX existem em parte por causa da natureza de dispositivo de E/S das GPUs.
Esse nível de indireção entre o compilador e o hardware dá aos arquitetos de GPU muito mais
flexibilidade do que os arquitetos de sistema têm. Muitas vezes é difícil saber com antecedência se uma inovação de arquitetura será bem suportada pelos compiladores e bibliotecas e
se será importante para as aplicações. Às vezes, um novo mecanismo vai se mostrar útil para
uma ou duas gerações e então decair em importância, conforme o mundo de TI mudar. As
PTX permitem aos arquitetos de GPU tentar inovações especulativamente e abandoná-las em
gerações subsequentes se elas forem um desapontamento ou perderem importância, o que
encoraja a experimentação. A justificativa para a inclusão é compreensivelmente muito maior
para processadores de sistema — e, portanto, muito menos experimentação pode ocorrer, já
que distribuir código binário de máquina normalmente implica que novos recursos devem
ser suportados por todas as gerações futuras daquela arquitetura.
Uma demonstração do valor das PTX é que a arquitetura Fermi mudou radicalmente
o conjunto de instruções de hardware — de orientado à memória, como o ×86, para
orientado a registrados, como no MIPS, além de dobrar o tamanho de endereço para 64
bits sem perturbar a pilha de software NVIDIA.
Armadilha. Concentrar-se no pico de desempenho em arquiteturas vetoriais e ignorar o overhead
de inicialização.
Os primeiros processadores vetoriais memória-memória, como o TI ASC e o CDC STAR-100,
têm longos tempos de inicialização. Para alguns problemas vetoriais, os vetores devem ter
mais de 100 elementos para que o código vetorial seja mais rápido do que o código escalar!
No CYBER 205 — derivado do STAR 100 —, o overhead de inicialização para o DAZPY
é de 158 ciclos de clock, o que aumenta substancialmente o ponto de break-even. Se as
taxas de clock do Cray-1 e do CYBER 205 fossem idênticas, o Cray-1 seria mais rápido até
que o número de elementos no vetor fosse maior do que 64. Já que o clock do Cray-1 era
também mais rápido (embora o 205 fosse mais novo), o ponto de cruzamento era um
comprimento de vetor de mais de 100.
Armadilha. Aumentar o desempenho vetorial sem aumentos comparáveis em desempenho escalar.
Esse desequilíbrio foi um problema em muitos dos primeiros processadores vetoriais, e
um ponto em que Seymour Cray (o arquiteto dos computadores Cray) reescreveu as regras.
Muitos dos primeiros processadores vetoriais tinham unidades escalares comparativamente
pequenas (além de grandes overheads de inicialização). Mesmo hoje, um processador
com desempenho vetorial menor mas com desempenho escalar melhor pode ter melhor desempenho do que um processador com maior pico de desempenho vetorial. Um
bom desempenho escalar mantém os custos de overhead baixos (p. ex., strip mining)
e reduz o impacto da lei de Amdahl.
Um bom exemplo disso vem na comparação de um processador escalar rápido com um
processador vetorial com desempenho escalar menor. Os kernels Livermore Fortran são
uma coleção de 24 kernels científicos com graus variáveis de vetorização. A Figura 4.31
4.9
FIGURA 4.31 Medidas de desempenho para os kernels Livermore Fortran em dois processadores
diferentes.
O MIPS M/120-5 e o Stardent-1500 (anteriormente o Ardent Titan-1) usam um chip MIPS R2000 de 16,7 MHz como CPU
principal. O Stardent-1500 usa sua unidade vetorial para PF escalar e tem cerca de metade do desempenho escalar (como
medido pela taxa mínima) do MIPS M/120-5, que usa o chip MIPS R2010 FP. O processador vetorial é mais de 2,5× mais
rápido para um loop altamente vetorizável (taxa máxima). Entretanto, o desempenho escalar menor do Stardent-1500 nega
o desempenho maior vetorial quando o desempenho total é medido pela média harmônica em todos os 24 loops.
mostra o desempenho de dois processadores diferentes nesse benchmark. Apesar do maior
pico de desempenho do processador vetorial, seu baixo desempenho escalar o torna mais
lento do que um processador escalar veloz, como medido pela média harmônica.
Hoje, o outro lado desse perigo é aumentar o desempenho vetorial 0, por exemplo,
aumentando o número de pistas sem aumentar o desempenho escalar. Tal miopia é outro
caminho para um computador desequilibrado.
A próxima falácia se relaciona intimamente com esta.
Falácia. Você pode obter um bom desempenho vetorial sem fornecer largura de banda de memória.
Como vimos no loop DAXPY e no modelo de Roofline, a largura de memória é muito
importante para todas as arquiteturas SIMD. O DAXPY requer 1,5 referência de memória
por operação de ponto flutuante, e essa taxa é típica para muitos códigos científicos. Mesmo se as operações de ponto flutuante não ocupassem tempo, um Cray-1 não poderia
aumentar o desempenho da sequência vetorial usada, já que ela é limitada pela memória.
O desempenho do Cray-1 em Linpack saltou quando o compilador usou bloqueio para
mudar o cálculo de modo que os valores fossem mantidos nos registradores vetoriais.
Essa abordagem diminuiu o número de referências de memória por FLOP e melhorou o
desempenho por um fator de quase duas vezes! Assim, a largura de banda de memória
no Cray-1 tornou-se suficiente para um loop que antes requeria mais largura de banda.
Falácia. Nas GPUs, simplesmente adicione mais threads se não tiver desempenho de memória
suficiente.
As GPUs usam muitos threads CUDA para ocultar a latência da memória principal. Se os
acessos de memória estiverem espalhados, mas não correlacionados entre threads CUDA, o
sistema de memória vai ficar progressivamente mais lento em responder a cada requisição
individual. Eventualmente, nem mesmo vários threads vão cobrir a latência. Para que a
estratégia “mais threads CUDA” funcione, não só você precisará de muitos threads CUDA,
como os próprios threads CUDA deverão se comportar bem em termos de localidade de
acessos de memória.
4.9
CONSIDERAÇÕES FINAIS
O paralelismo em nível de dados está aumentando em importância para dispositivos pessoais móveis, dada a popularidade de aplicações mostrando a importância de áudio, vídeo
e jogos nesses dispositivos. Quando combinados com um modelo mais fácil de programar
do que o paralelismo em nível de tarefa e eficiência energética potencialmente melhor, é
Considerações finais
291
292
CAPÍTULO 4:
Paralelismo em nível de dados em arquiteturas vetoriais, SIMD e GPU
fácil prever uma renascença do paralelismo em nível de dados na próxima década. De fato,
já podemos ver essa ênfase em produtos, já que tanto as GPUs quanto os processadores
tradicionais vêm aumentando o número de pistas SIMD tão rapidamente quanto estão
adicionando processadores (Fig. 4.1 na página 229).
Portanto, estamos vendo os processadores de sistema adquirirem mais das características
da GPUs e vice-versa. Uma das maiores diferenças entre o desempenho dos processadores convencionais e as GPUs tem sido o endereçamento gather-scatter. Arquiteturas
vetoriais tradicionais mostram como adicionar tal endereçamento a instruções SIMD,
e esperamos ver mais ideias das comprovadas arquiteturas vetoriais adicionadas às extensões SIMD ao longo do tempo.
Como dissemos nas páginas iniciais da Seção 4.4, a questão das GPUs não é simplesmente
o fato de a arquitetura ser melhor, mas, dado o investimento em hardware para realizar
gráficos bem, como ela pode ser melhorada para suportar computação mais geral. Embora
no papel as arquiteturas vetoriais tenham muitas vantagens, ainda precisa ser provado
que as arquiteturas vetoriais podem ser uma base tão boa para gráficos quanto as GPUS.
Os processadores SIMD das GPUs e computadores ainda têm um projeto relativamente
simples. Técnicas mais agressivas provavelmente serão introduzidas ao longo do tempo
para melhorar a utilização das GPUs, já que as aplicações de GPU para cálculo estão
começando a ser desenvolvidas. Ao estudar esses novos programas, os projetistas de GPUs
certamente vão descobrir e implementar novas otimizações de máquina. Uma questão
é se os processadores escalares (ou processadores de controle), que servem para poupar
hardware e energia em processadores vetoriais, vão aparecer dentro das GPUs.
A arquitetura Fermi já incluía muitos recursos encontrados em processadores convencionais para tornar as GPUs mais populares, mas ainda há outras necessárias para fechar
a lacuna. Aqui estão algumas que esperamos ver endereçadas em futuro próximo.
j
j
j
j
GPUs virtualizáveis. A virtualização se provou importante para os servidores
e é a base da computação em nuvem (Cap. 6). Para que as GPUs sejam incluídas
na nuvem, elas vão precisar ser tão virtualizáveis quanto os processadores e memórias
a que estão ligadas.
Tamanho relativamente pequeno da memória das GPUs. Um uso comum
de computação mais rápida é solucionar problemas maiores, e problemas maiores
muitas vezes têm uma “pegada” maior na memória. A inconsistência das GPUs
entre velocidade e tamanho pode ser endereçada com mais capacidade de memória.
O desafio é manter uma grande largura de banda e, ao mesmo tempo, aumentar
a capacidade.
E/S diretas para a memória da GPU. Programas reais usam E/S para dispositivos
de armazenamento, assim como para buffers de quadro, e programas grandes
podem exigir muitas E/S, além de uma memória de tamanho considerável.
Os sistemas de GPU atuais devem transferir entre dispositivos de E/S e a memória
de sistema e então entre a memória de sistema e a memória da GPU. Esse passo
extra diminui significativamente o desempenho de E/S em alguns programas,
tornando as GPUs menos atrativas. A lei de Amdahl nos alerta do que acontece
quando se negligencia uma parte da tarefa enquanto se aceleram outras. Esperamos
que as GPUs futuras tornem todos cidadãos de primeira classe de E/S, assim
como fazem hoje para E/S com frame buffer.
Memórias físicas unificadas. Uma solução alternativa para os dois itens anteriores
é ter uma única memória física para o sistema e a GPU, assim como algumas GPUs
baratas fazem para PMDs e laptops. A arquitetura AMD Fusion, anunciada quando
Estudo de caso e exercícios por Jason D. Bakos
esta edição estava sendo finalizada, é uma fusão inicial entre as GPUs tradicionais
e as CPUs tradicionais. A NVIDIA também anunciou o Projeto Denver, que combina
um processador escalar ARM com GPUs NVIDIA em um único espaço de endereços.
Quando esses sistemas forem comercializados, será interessante aprender quão
integrados eles são, além do impacto da integração no desempenho e na energia
de aplicações com paralelismo de dados e gráficos.
Tendo coberto as muitas versões de SIMD, o Capítulo 5 entrará no reino do MIMD.
4.10
PERSPECTIVAS HISTÓRICAS E REFERÊNCIAS
A Seção L.6 (disponível on-line) apresenta uma discussão sobre o Illiac IV (um representante das primeiras arquiteturas SIMD) e o Cray-1 (um representante das arquiteturas
vetoriais). Nós também examinamos as extensões SIMD multimídia e a história das GPUs.
ESTUDO DE CASO E EXERCÍCIOS POR JASON D. BAKOS
Estudo de caso: implementando um kernel vetorial em um
processador vetorial e GPU
Conceitos ilustrados neste estudo de caso
j
j
j
Programação de processadores vetorial
Programação de GPUs
Estimativa de desempenho
MrBayes é uma popular e conhecida aplicação computacional para biologia para inferir
os históricos evolucionários entre um conjunto de espécies de entrada com base nos seus
dados de sequência de DNA multiplamente alinhados de comprimento n. O MrBayes
funciona realizando uma busca heurística nos espaço de todas as topologias de árvore
binária, na qual as entradas são as folhas. Para avaliar uma árvore em particular, a aplicação
deve calcular uma tabela de probabilidade n × 4 (chamada cIP) para cada nó interno. A
tabela é uma função das tabelas de probabilidade condicionais dos dois nós descendentes
do nó (clL e clR, ponto flutuante de precisão simples) e suas tabelas de probabilidade
de transição associadas n × 4 × 4 (tiPL e tiPR, ponto flutuante de precisão simples).
Um dos kernels dessa aplicação é o cálculo dessa tabela de probabilidade condicional e
é mostrado a seguir:
293
294
CAPÍTULO 4:
Paralelismo em nível de dados em arquiteturas vetoriais, SIMD e GPU
FIGURA 4.32 Constantes e valores para o estudo de caso.
4.1
[25] <4.2, 4.3> Considere as constantes mostradas na Figura 4.32. Mostre o código para
MIPS e VMIPS. Considere que não podemos usar carregamentos ou armazenamentos
scatter-gather. Considere que os endereços iniciais de tiPL, tiPR, cIL, cIP e CIP
estão em RtiPL, RtiPR, RclR e RclP, respectivamente. Considere que o comprimento
do registrador VMIPS é programável pelo usuário e pode ser designado configurando
o registrador especial VL (p. ex., li VL 4). Para facilitar as reduções de adição vetorial,
considere que adicionamos as seguintes instruções ao VIMPS:
Esta instrução realiza uma redução de somatório em um registrador vetorial Vs,
gravando a soma no registrador escalar Fd.
4.2 [5] <4.2, 4.3> Considerando seq_length == 500, qual é a contagem de instruções
dinâmicas para as duas implementações?
4.3 [25] <4.2, 4.3> Considere que a instrução de redução vetorial seja executada
na unidade fundamental vetorial, similar a uma instrução soma vetorial. Mostre
como a sequência de código estabelece comboios supondo uma única instância de
cada unidade funcional vetorial. Quantos chimes o código vai requerer? Quantos ciclos
por FLOP são necessários, ignorando o overhead de despacho de instrução vetorial?
4.4 [15] <4.2, 4.3> Agora considere que possamos usar carregamentos e
armazenamentos scatter-gather (LVI e SVI). Considere que tiPL, tiPR, clL, clR
e clP são posicionados consecutivamente na memória. Por exemplo, se if seq_
length = =500, o array tiPR começaria 500*4 bytes depois do array tiPL. Como
isso afeta o modo como você pode gravar o código VMIPS para esse kernel?
Considere que você possa inicializar os registradores vetoriais usando a seguinte
técnica que, por exemplo, inicializaria o registrador vetorial V1 com os valores
(0,0;2000;2000):
4.5
Considere que o comprimento máximo vetorial é 64. Há algum modo como
o desempenho pode ser melhorado usando carregamentos gather-scatter? Se sim,
quanto?
[25] <4.4> Agora considere que queremos implementar o kernel MrBayes em uma
GPU usando um único bloco de threads. Reescreva o código C para o kernel usando
CUDA. Considere que os ponteiros para as tabelas de probabilidade condicional
Estudo de caso e exercícios por Jason D. Bakos
FIGURA 4.33 Árvore de amostras.
4.6
4.7
4.8
e probabilidade de transição são especificados como parâmetros para o kernel.
Invoque um thread para cada iteração do loop. Carregue quaisquer valores
reutilizados na memória compartilhada antes de realizar operações sobre eles.
[15] <4.4> Com CUDA podemos usar paralelismo de granularidade grossa
no nível de bloco para calcular as probabilidades condicionais de múltiplos
nós em paralelo. Considere que queremos calcular as probabilidades condicionais
da base da árvore para cima. Considere que os arrays de probabilidade condicional
e probabilidade de transição são organizados na memória como descrito na questão
4, e o grupo de tabelas para cada um dos 12 nós de folha é armazenado em posições
de memória consecutivas na ordem do número de nó. Considere ainda que
queremos calcular a probabilidade condicional para os nós 12 a 17, como
mostrado na Figura 4.33. Mude o método como você calcula os índices de array
em sua resposta para o Exercício 4.5 a fim de incluir o número de bloco.
[15] <4.4> Converta seu código do Exercício 4.6 em código PTX. Quantas
instruções são necessárias para o kernel?
[10] <4.4> Quão bem você espera que esse código seja realizado em uma GPU?
Explique sua resposta.
Exercícios
4.9
[10/20/20/15/15] <4.2> Considere o código a seguir, que multiplica dois vetores
que contêm valores complexos de precisão simples:
Considere que o processador roda a 700 MHz e tem um comprimento máximo
de vetor de 64. A unidade de carregamento/armazenamento tem um overhead de
inicialização de 15 ciclos, a unidade de multiplicação, oito ciclos, e a unidade
de soma/subtração, cinco ciclos.
a. [10] <4.2> Qual é a intensidade aritmética desse kernel? Justifique sua resposta.
b. [20] <4.2> Converta esse loop em código assembly VMIPS usando strip mining.
295
296
CAPÍTULO 4:
Paralelismo em nível de dados em arquiteturas vetoriais, SIMD e GPU
c. [20] <4.2> Considere encadeamento e um único pipeline de memória.
Quantos chimes são necessários? Quantos ciclos de clock são requeridos por
valor de resultado complexo, incluindo overhead de inicialização?
d. [15] <4.2> Se a sequência vetorial for encadeada, quantos ciclos de clocks
serão necessários por valor de resultado complexo, incluindo o overhead?
e. [15] <4.2> Agora suponha que o processador tenha três pipelines de memória e encadeamento. Se não houver conflitos de banco nos acessos de loop,
quantos ciclos de clock serão necessários por resultado?
4.10 [30] <4.4> Neste problema, vamos comparar o desempenho de um processador
vetorial com um sistema híbrido que contém um processador escalar e um
coprocessador baseado em GPU. No sistema híbrido, o processador host tem
um desempenho escalar superior ao da GPU, então nesse caso todo o código escalar
é executado no processador host, enquanto todo o código vetorial é executado na
GPU. Vamos nos referir ao primeiro sistema como computador vetorial e ao segundo
sistema como computador híbrido. Considere que sua aplicação-alvo contém um
kernel vetorial com intensidade aritmética de 0,5 FLOP por byte da DRAM acessado.
Entretanto, a aplicação também tem um componente escalar que deve ser realizado
antes e depois do kernel para preparar os vetores de entrada e os vetores de saída,
respectivamente. Para um conjunto de dados de amostra, a porção escalar do código
requer 400 ms de tempo de execução no processador vetorial e no processador host
no sistema híbrido. O kernel lê vetores de entrada consistindo em 200 MB de dados
e tem dados de saída consistindo em dados de 100 MB. O processador vetorial tem
um pico de largura de banda de memória de 30 GB/s e a GPU tem um pico de largura
de banda de memória de 150 GB/s. O sistema híbrido tem um overhead adicional
que requer todos os vetores de entrada a serem transferidos entre a memória
do host e a memória local da GPU antes e depois do kernel ser invocado. O sistema
híbrido tem uma largura de banda de acesso direto à memória (DMA) de 10 GB/s
e uma latência média de 10 ms. Considere que o processador vetorial e a GPU
têm desempenho limitado pela largura de banda da memória. Calcule o tempo
de execução requerido pelos dois computadores para esta aplicação.
4.11 [15/25/25] <4.4, 4.5> A Seção 4.5 discutiu a operação de redução que reduz
um vetor para um escalar por aplicação repetida de uma operação. Uma redução
é um tipo especial de recorrência de loop. Mostramos a seguir um exemplo:
Um compilador de vetorização pode aplicar uma transformação chamada
expansão escalar, que expande um escalar em um vetor e divide o loop de modo
que a multiplicação possa ser realizada com uma operação vetorial, deixando
a redução como uma operação escalar separada:
Como mencionado na Seção 4.5, se permitirmos que a soma de ponto flutuante
seja associativa, existem diversas técnicas disponíveis para paralelizar a redução.
a. [15] <4.4, 4.5> Uma técnica é chamada a dobrar a recorrência, que soma
sequências de vetores progressivamente mais curtos (p. ex., dois vetores de 32
elementos, então dois vetores de 16 elementos, e assim por diante). Mostre
como seria o código C para executar o segundo loop desse modo.
b. [25] <4.4, 4.5> Em alguns processadores vetoriais, os elementos individuais
dentro dos registradores vetoriais são endereçáveis; nesse caso, os operandos
Estudo de caso e exercícios por Jason D. Bakos
de uma operação vetorial podem ter duas partes diferentes do mesmo registrador vetorial. Isso permite outra solução para a redução chamada somas parciais.
A ideia é reduzir o vetor a m somas, em que m é a latência total na unidade
funcional vetorial, incluindo os tempos de leitura e gravação de operandos.
Considere que os registradores vetoriais VMIPS sejam endereçáveis
(p. ex., você pode iniciar uma operação vetorial com o operando V1(16),
indicando que o operando de entrada começa com o elemento 16). Considere
também que a latência total para somas, incluindo a leitura e operandos
e a gravação do resultado, é de oito ciclos. Escreva uma sequência de código
VMIPS que reduza o conteúdo de V1 para oito somas parciais.
c. [25] <4.4, 4.5> Ao realizar uma redução em uma GPU, um thread é associado a cada elemento no vetor de entrada. O primeiro passo é cada thread
gravar seu valor correspondente na memória compartilhada. A seguir, cada
thread entra em um loop que soma cada par de valores de entrada. Isso
reduz o número de elementos à metade após cada iteração, significando que
o número de threads ativos também é reduzido à metade a cada iteração.
Para maximizar o desempenho da redução, o número de warps totalmente
preenchidos deve ser maximizado durante o curso do loop. Em outras palavras, os threads ativos devem ser contíguos. Além disso, cada thread deve
indexar o array compartilhado de tal modo que evite conflitos de banco com
a memória compartilhada. O loop a seguir viola somente a primeira dessas
instruções e também usa o operador módulo, muito dispendioso para as
GPUS:
Reescreva o loop para atender a essas regras e elimine o uso do operador módulo.
Considere que existam 32 threads por warp e um conflito de banco ocorre sempre
que dois ou mais threads do mesmo warp referenciam um índice cujos módulos
por 32 sejam iguais.
4.12 [10/10/10/10] <4.3> O kernel a seguir realiza uma porção do método finite-difference
time-domain (FDTD) para calcular as equações de Maxwell em um espaço
tridimensional para um dos benchmarks SPEC06fp:
297
298
CAPÍTULO 4:
Paralelismo em nível de dados em arquiteturas vetoriais, SIMD e GPU
Considere que dH1, dH2, Hy, Hz, dy, dz, Ca, Cb e Ex sejam arrays de ponto
flutuante de precisão simples. Considere também que IDx é um array de inteiros
sem sinal.
a. [10] <4.3> Qual é a intensidade aritmética desse kernel?
b. [10] <4.3> Esse kernel é adequado para execução vetorial ou SIMD? Por quê?
c. [10] <4.3> Considere que esse kernel será executado em um processador que
tem largura de banda de memória de 30 GB/s. Esse kernel será limitado pela
memória ou computacionalmente?
d. [10] <4.3> Desenvolva um modelo roofline para esse processador considerando que ele tenha um pico de throughput computacional de 85 GFLOP/s.
4.13 [10/15] <4.4> Considere uma arquitetura de GPU que contenha 10
processadores SIMD. Cada instrução SIMD tem uma largura de 32 e cada
processador SIMD contém oito (pistas para aritmética de precisão simples
e instruções de carregamento/armazenamento, significando que cada
instrução SIMD não divergida pode produzir 32 resultados a cada quatro
ciclos). Considere um kernel que tenha desvios divergentes que faça com
que uma média de 80% dos threads estejam ativos. Considere que 70%
de todas as instruções SIMD executadas sejam aritméticas de precisão simples
e 20% sejam carregamento/armazenamento. Uma vez que nem todas as
latências de memória são cobertas, considere uma taxa média de despacho
de instruções SIMD de 0,85. Considere que a GPU tem uma velocidade
de clock de 1,5 GHz.
a. [10] <4.4> Calcule o throughput, em GFLOP/s, para esse kernel nessa GPU.
b. [15] <4.4> Considere que você tem as seguintes opções:
– Aumentar o número de pistas de precisão simples para 16.
– Aumentar o número de processadores SIMD para 15 (considere que essa
mudança não afeta outras medidas de desempenho e que o código está
em proporção com os processadores adicionais).
– Adicionar um cache que vai efetivamente reduzir a latência de memória
em 40%, o que vai aumentar a taxa de despacho de instruções para 0,95.
Qual é o ganho de velocidade em throughput para cada uma dessas melhorias?
4.14 [10/15/15] <4.5> Neste exercício, vamos examinar diversos loops e analisar seu
potencial para paralelização.
a. [10] <4.5> O loop a seguir possui uma dependência carregada pelo loop?
b. [15] <4.5> No loop a seguir, encontre todas as dependências reais, dependências de saída e antidependências. Elimine as dependências de saída e antidependências por renomeação.
Estudo de caso e exercícios por Jason D. Bakos
c. [15] <4.5> Considere o seguinte loop:
Existem dependências entre S1 e S2? Esse loop é paralelo? Se não, mostre
como torná-lo paralelo.
4.15 [10] <4.4> Liste e descreva pelo menos quatro fatores que influenciam
o desempenho dos kernels de GPU. Em outras palavras, que comportamento
de tempo de execução causado pelo código do kernel provoca uma redução
na utilização de recursos durante a execução do kernel?
4.16 [10] <4.4> Considere uma GPU hipotética com as seguintes características:
Taxa de clock de 1,5 GHz
Contém 16 processadores SIMD, cada qual contendo 16 unidades de ponto
flutuante de precisão simples
j
Possui 100 GB/s de largura de banda de memória fora do chip
Sem considerar a largura de banda de memória, qual é o pico de throughput
de ponto flutuante de precisão simples para essa GPU em GFLOP/s, considerando
que todas as latências de memória podem ser ocultadas? Esse throughput
é sustentável, dada a limitação em largura de banda de memória?
4.17 [60] <4.4> Para este exercício de programação, você vai escrever e caracterizar
o comportamento de um kernel CUDA que contenha muito paralelismo em nível
de dados, mas também comportamento de execução condicional. Use o toolkit
NVIDIA CUDA juntamente com o GPU-SIM da Universidade da Colúmbia
Britânica (www.ece.ubc.ca/∼aamodt/gpgpu-sim/) ou o CUDA Profiler para escrever e
compilar um kernel CUDA que realize 100 iterações do Conway's Game of Life para
um tabuleiro de 256 × 256 e retorne ao status final do tabuleiro de jogo para o
host. Considere que o tabuleiro é inicializado pelo host. Associe um thread a cada
célula. Tenha a certeza de adicionar uma barreira após cada iteração do jogo. Use as
seguintes regras de jogo:
j
j
Qualquer célula viva com menos de dois vizinhos vivos morre.
Qualquer célula viva com dois ou três vizinhos vivos vai para a próxima geração.
j
Qualquer célula viva com mais de três vizinhos vivos morre.
j
Qualquer célula morta com exatamente três vizinhos vivos se torna uma célula viva.
Depois de acabar o kernel, responda às seguintes perguntas:
a. [60] <4.4> Compile seu código usando a opção –ptx e inspecione a representação PTC do seu kernel. Quantas instruções PTX compõem a implementação
PTX do seu kernel? As seções condicionais do seu kernel incluem instruções
de desvio ou somente instruções sem desvio com predicado?
b. [60] <4.4> Depois de executar seu código no simulador, qual é a contagem
dinâmica de instruções? Quais são as instruções por ciclo (IPC) alcançado ou taxa de despacho de instruções? Qual é o detalhamento dinâmico de instruções
em termos de instruções de controle, instruções da unidade lógico-aritmética
(ALU) e instruções de memória? Existem conflitos de banco de memória
compartilhados? Qual é a largura de banda de memória efetiva fora do chip?
c. [60] <4.4> Implemente uma versão melhorada do seu kernel em que
as referências à memória fora do chip sejam reunidas e observe as diferenças
em desempenho de tempo de execução.
j
j
299
CAP ÍTULO 5
Paralelismo em nível
de thread
A virada da organização convencional veio em meados da década de 1960,
quando a lei dos rendimentos decrescentes começou a ter efeito sobre o esforço
de aumentar a velocidade operacional de um computador […] Os circuitos
eletrônicos são, por fim, limitados em sua velocidade de operação pela velocidade
da luz […] e muitos dos circuitos já estavam operando na faixa do nanossegundo.
W. Jack Bouknight et al., The Illiac IV System (1972)
Dedicamos todo o futuro desenvolvimento de produtos aos projetos multicore.
Acreditamos que esse seja um ponto-chave de inflexão para o setor.
Paul Otellini, presidente da Intel, ao descrever os futuros rumos da empresa
no Intel Developers Forum, em 2005
5.1 Introdução ...........................................................................................................................................301
5.2 Estruturas da memória compartilhada centralizada........................................................................308
5.3 Desempenho de multiprocessadores simétricos de memória compartilhada ...............................321
5.4 Memória distribuída compartilhada e coerência baseada em diretório .........................................332
5.5 Sincronismo: fundamentos ................................................................................................................339
5.6 Modelos de consistência de memória: uma introdução ..................................................................343
5.7 Questões cruzadas..............................................................................................................................347
5.8 Juntando tudo: processadores multicore e seu desempenho.........................................................350
5.9 Falácias e armadilhas .........................................................................................................................355
5.10 Comentários finais ............................................................................................................................359
5.11 Perspectivas históricas e referências ..............................................................................................361
Estudos de caso com exercícios por Amr Zaky e David A. Wood..........................................................361
5.1
INTRODUÇÃO
Conforme mostram as citações que abrem este capítulo, a visão de que os avanços na arquitetura de uniprocessador chegavam a um fim tem sido mantida por alguns pesquisadores
há muitos anos. Obviamente, essas visões foram prematuras; na verdade, durante o período
de 1986 a 2003, o crescimento do desempenho do uniprocessador cresceu, conduzido
pelos microprocessadores, atingindo a taxa mais alta desde os primeiros computadores
transistorizados, no final da década de 1950 e início da década de 1960.
Apesar disso, a importância dos multiprocessadores aumentou por toda a década de 1990,
enquanto os projetistas buscavam um meio de criar servidores e supercomputadores
que alcançassem desempenho mais alto do que um único microprocessador, enquanto
exploravam as tremendas vantagens no custo-desempenho dos microprocessadores como
301
302
CAPÍTULO 5 :
Paralelismo em nível de thread
commodity. Conforme discutimos nos Capítulos 1 e 3, o atraso no desempenho do uniprocessador que surgiu dos rendimentos decrescentes na exploração do ILP, combinado
com a crescente preocupação com a potência, está levando a uma nova era na arquitetura
de computador — uma era na qual os multiprocessadores desempenham um papel
importante. A segunda citação captura esse evidente ponto de inflexão.
Essa tendência em direção a mais confiança no multiprocessamento é reforçada por outros fatores:
j
j
j
j
j
j
As eficiências drasticamente menores no uso de silício e energia que foram
encontradas entre 2000 e 2005, quando os projetistas tentaram encontrar e explorar
mais ILP, o que se mostrou ineficiente, já que os custos da energia e do silício
cresceram mais do que o desempenho. Além do ILP, o único modo escalável e de uso
geral que conhecemos para aumentar o desempenho mais rápido do que a tecnologia
básica permite (de uma perspectiva de chaveamento) é o multiprocessamento.
Um crescente interesse por servidores de alto nível, conforme a computação
em nuvem e o “software sob demanda” se tornam mais importantes.
Um crescimento nas aplicações orientadas a computação intensiva
de dados, pela disponibilidade de grande quantidade de dados na internet.
A percepção de que o desempenho crescente no desktop é menos importante
(fora dos gráficos, pelo menos), seja porque o desempenho atual é aceitável,
seja porque aplicações altamente pesadas computacionalmente ou em termos
de dados estão sendo feitas na nuvem.
Compreensão melhorada de como usar multiprocessadores de modo eficiente,
especialmente nos ambientes de servidor em que existe significativo paralelismo
natural, vindo de grandes conjuntos de dados, paralelismo natural (que ocorre
em códigos científicos) ou paralelismo entre grande número de requisições
independentes (paralelismo em nível de requisição).
As vantagens de aproveitar um investimento de projeto pela replicação, em vez de um
projeto exclusivo — todos os projetos de multiprocessador oferecem tal aproveitamento.
Neste capítulo, nos concentraremos em explorar o paralelismo em nível de thread (TLP).
O TLP implica a existência de múltiplos contadores de programa e, portanto, é explorado
primeiramente através de MIMDs. Embora as MIMDs estejam por aí há décadas, o movimento do paralelismo de nível de thread para o primeiro plano de toda a computação,
de aplicações embarcadas a servidores de alto desempenho, é relativamente recente. Do
mesmo modo, o uso extensivo de paralelismo em nível de thread para aplicações de uso
geral no lugar de aplicações científicas é relativamente novo.
Nosso foco, neste capítulo, recairá sobre os multiprocessadores, que definimos como computadores consistindo em processadores fortemente acoplados cuja coordenação e cujo uso costumam ser controlados por um único sistema operacional que compartilha memória através
de um espaço de endereços compartilhado. Tais sistemas exploram o paralelismo em nível de
thread através de dois modelos de software diferentes. O primeiro é a execução de um conjunto
de threads fortemente acoplados colaborando em uma única tarefa, em geral chamado processamento paralelo. O segundo é a execução de múltiplos processos relativamente independentes
que podem se originar de um ou mais usuários, o que é uma forma de paralelismo em nível de
requisição, embora em uma escala muito menor do que a que exploraremos no Capítulo 6.
O paralelismo em nível de requisição pode ser explorado por uma única aplicação sendo
executada em múltiplos processadores, como uma base de dados respondendo a pesquisas, ou
múltiplas aplicações rodando independentemente, muitas vezes chamado multiprogramação.
Os multiprocessadores que vamos examinar neste capítulo costumam variar em tamanho,
indo desde dois processadores até dúzias de processadores, e que se comunicam e se
coordenam através do compartilhamento de memória. Embora compartilhar através da
memória implique um espaço de endereços compartilhado, não significa necessariamente
5.1
que exista uma única memória física. Tais multiprocessadores incluem sistemas de chip
único com múltiplos núcleos, chamados multicore1 e computadores consistindo em múltiplos chips, cada qual podendo ter um projeto multicore.
Além dos multiprocessadores reais, vamos retornar ao tópico do multithreading, uma
técnica que suporta múltiplos threads sendo executados de modo interligado em um único
processador de despacho múltiplo. Muitos processadores multicore também incluem
suporte para multithreading.
No Capítulo 6, consideraremos computadores de ultraescala construídos a partir de um
grande número de processadores. Esses sistemas de larga escala costumam ser usados para
computação em nuvem com um modelo que supõe grande número de requisições independentes ou tarefas computacionais paralelas e intensas. Quando esses clusters aumentam
para dezenas de milhares de servidores, nós os chamamos computadores em escala warehouse.
Além dos multiprocessadores que estudaremos aqui e os sistemas em escala warehouse do
Capítulo 6, existe uma gama especial de sistemas multiprocessadores de grande escala, às vezes
chamados multicomputadores. Eles não são tão fortemente acoplados quanto os multiprocessadores examinados neste capítulo, mas são mais fortemente acoplados do que os sistemas
em escala warehouse do próximo. Tais multicomputadores são usados principalmente em
cálculos científicos de alto nível. Muitos outros livros, como o de Culler, Singh e Gupta (1999),
abordam esses sistemas em detalhes. Devido à natureza grande e mutante do campo do multiprocessamento (o livro citado tem mais de 1.000 páginas e trata apenas de microprocessadores!),
nós decidimos concentrar nossa atenção no que consideramos as partes mais importantes e
de uso mais geral do espaço de computação. O Apêndice I aborda alguns dos problemas que
surgem na construção desses computadores no contexto de aplicações científicas de alta escala.
Assim, nosso foco recairá sobre os multiprocessadores com número pequeno a médio de
processadores (2 a 32). Esses projetos dominam em termos de unidades e valores monetários. Só daremos um pouco de atenção de projeto de multiprocessador em escala maior
(33 ou mais processadores) principalmente no Apêndice I, que abrange mais aspectos do
projeto desses processadores, além do desempenho de comportamento para cargas de
trabalho científicas paralelas, uma classe primária de aplicações para multiprocessadores
em grande escala. Nos multiprocessadores em grande escala, as redes de interconexão são
uma parte crítica do projeto; o Apêndice F aborda esse tópico.
Arquitetura de multiprocessadores: problemas e abordagem
Para tirar proveito de um multiprocessador MIMD com n processadores, precisamos ter
pelo menos n threads ou processos para executar. Os threads independentes dentro de um
único processo geralmente são identificados pelo programador ou criados pelo compilador. Os threads podem vir de processos em grande escala, independentes, escalonados
e manipulados pelo sistema operacional. No outro extremo, um thread pode consistir
em algumas dezenas de iterações de um loop, geradas por um compilador paralelo que
explora o paralelismo de dados no loop. Embora a quantidade de computação atribuída
a um thread, chamada tamanho de granularidade, seja importante na consideração de
como explorar de forma eficiente o paralelismo em nível de thread, a distinção qualitativa
importante entre o paralelismo e o nível de instrução é que o paralelismo em nível de
thread é identificado em alto nível pelo sistema de software e os threads consistem em
centenas a milhões de instruções que podem ser executadas em paralelo.
Os threads também podem ser usados para explorar o paralelismo em nível de dados,
embora o overhead provavelmente seja mais alto do que seria visto em um computador
Nota da Tradução: A tradução do termo multicore seria “multinúcleos”. Porém, como esse tipo de
processador ficou conhecido comercialmente como multicore, resolvemos manter o termo em inglês.
1
Introdução
303
304
CAPÍTULO 5 :
Paralelismo em nível de thread
SIMD ou em uma GPU. Esse overhead significa que a granularidade precisa ser suficientemente grande para explorar o paralelismo de modo eficiente. Por exemplo, embora um
processador vetorial (Cap. 4) possa ser capaz de colocar operações em paralelo de forma
eficiente em vetores curtos, a granularidade resultante quando o paralelismo é dividido
entre muitos threads pode ser tão pequena a ponto de o overhead tornar a exploração do
paralelismo proibitivamente dispendiosa em um MIMD.
Os multiprocessadores MIMD existentes estão em duas classes, dependendo do número
de processadores envolvidos, que, por sua vez, dita uma organização de memória e uma
estratégia de interconexão. Vamos nos referir aos multiprocessadores por sua organização
de memória, pois o que constitui um número pequeno ou grande de processadores
provavelmente mudará com o tempo.
O primeiro grupo, que chamamos multiprocessadores simétricos (memória compartilhada) (SMPs)
ou multiprocessadores centralizados de memória compartilhada, tem um número pequeno de núcleos, geralmente oito ou menos. Para multiprocessadores com pouca quantidade de processadores, é possível que os processadores compartilhem uma única memória centralizada à
qual todos os processadores tenham igual acesso; daí o termo simétrico. Em chips multicore,
a memória é efetivamente compartilhada de modo centralizado entre os núcleos, e todos os
multicores existentes são SMPs. Quando mais de um multicore é conectado, existem memórias separadas para cada multicore, então a memória é distribuída em vez de centralizada.
Às vezes, as arquiteturas SMP também são chamadas multiprocessadores de acesso uniforme
à memória (UMA), advindo do fato de que todos os processadores possuem uma latência
de memória uniforme, mesmo que essa memória seja organizada em múltiplos bancos. A
Figura 5.1 mostra como são esses multiprocessadores. A arquitetura dos SMPs é o assunto
da Seção 5.2, em que explicaremos a técnica no contexto de um multicore.
A técnica de projeto alternativa consiste em multiprocessadores com memória fisicamente
distribuída, chamada memória compartilhada distribuída (distributed shared memory —
DSM). A Figura 5.2 mostra como se parecem esses multiprocessadores. Para dar suporte
a uma grande quantidade de processadores, a memória precisa ser distribuída entre os
processadores em vez de centralizada; caso contrário, o sistema de memória não será capaz
de dar suporte às demandas de largura de banda de um número maior de processadores
sem incorrer em uma latência de acesso excessivamente longa. Com o rápido aumento
no desempenho do processador e associado ao aumento nos requisitos de largura de
banda da memória de um processador, o tamanho de um multiprocessador para o qual a
memória distribuída é preferida continua a diminuir. O grande número de processadores
também aumenta a necessidade de uma interconexão com alta largura de banda, da qual
veremos exemplos no Apêndice F. Tanto as redes diretas (ou seja, switches) quanto as redes
indiretas (normalmente malhas multidimensionais) são usadas.
A distribuição da memória entre os nós aumenta a largura de banda e reduz a latência da
memória. Um multiprocessador DSM é também chamado NUMA (acesso não uniforme
à memória), já que o tempo de acesso depende da localização de uma palavra de dados
na memória. As desvantagens principais para um DSM são que comunicar dados entre
processadores se torna um pouco mais complexo e que um DSM requer mais esforço
no software para tirar vantagem da largura de banda de memória maior gerada pelas
memórias distribuídas. Já que todos os multiprocessadores multicore com mais de um
chip processador (ou soquete) usam memória distribuída, vamos explicar a operação dos
multiprocessadores de memória distribuída a partir desse ponto de vista.
Nas arquiteturas SMP e DSM, a comunicação entre threads ocorre através de um espaço
de endereços compartilhado, o que significa que uma referência de memória pode ser
feita por qualquer processador para qualquer local na memória, supondo que ele tenha
5.1
FIGURA 5.1 Estrutura básica de um multiprocessador de memória compartilhada centralizada.
Subsistemas de cache de múltiplos processadores compartilham a mesma memória física, normalmente conectada
por um ou mais barramentos ou um switch. A principal propriedade da arquitetura é o tempo de acesso uniforme
a toda a memória a partir de todos os processadores. Em uma versão multichip de cache compartilhada seria omitida,
e o barramento ou rede de interconexão conectando os processadores à memória seria executado entre chips, em vez
de dentro de um único chip.
FIGURA 5.2 A arquitetura básica de um multiprocessador de memória distribuída em 2011 consiste
em um chip de multiprocessador multicore com memória e possivelmente E/S anexas e uma interface
para uma rede de interconexão que conecta todos os nós.
Cada núcleo de processador compartilha toda a memória, embora o tempo de acesso para a memória anexada
ao chip do núcleo seja muito mais rápido do que o tempo de acesso às memórias remotas.
Introdução
305
306
CAPÍTULO 5 :
Paralelismo em nível de thread
os direitos de acesso corretos. O nome memória compartilhada, associado tanto ao SMP
quanto ao DSM, se refere ao fato de que o espaço de endereços é compartilhado.
Em contraste, os clusters e computadores em escala warehouse do Capítulo 6 se parecem
com computadores individuais conectados por uma rede, e a memória de um processador não pode ser acessada por outro processador sem a assistência de protocolos de
software sendo executados nos dois processadores. Em tais projetos, protocolos de envio
de mensagem são usados para comunicar dados entre os processadores.
Desafios do processamento paralelo
A aplicação dos multiprocessadores varia desde a execução de tarefas independentes, essencialmente sem comunicação, até a execução de programas paralelos em que os threads
precisam se comunicar para completar a tarefa. Dois obstáculos importantes, ambos explicáveis pela lei de Amdahl, tornam o processamento paralelo desafiador. O grau pelo qual esses
obstáculos são difíceis ou fáceis é determinado tanto pela aplicação quanto pela arquitetura.
O primeiro obstáculo tem a ver com o paralelismo limitado disponível nos programas, e o
segundo surge do custo relativamente alto das comunicações. As limitações no paralelismo
disponível tornam difícil alcançar bons ganhos de velocidade em qualquer processador
paralelo, como mostra nosso primeiro exemplo.
Suponha que você queira alcançar um ganho de velocidade de 80 com 100
processadores. Que fração da computação original pode ser sequencial?
Resposta Conforme vimos no Capítulo 1, a lei de Amdahl é
Exemplo
Ganhode velocidade =
1
Fração melhorado
+ (1 − Fração melhorado )
Ganho de velocidade melhorado
Para simplificar, considere que o programa opere em apenas dois modos: paralelo
com todos os processadores totalmente usados, que é o modo avançado, e serial, com
apenas um processador em uso. Com essa simplificação, o ganho de velocidade no
modo avançado é simplesmente o número de processadores, enquanto a fração do
modo avançado é o tempo gasto no modo paralelo. Substituindo na equação anterior:
80 =
1
Fração paralelo
+ (1 − Fração paralelo )
100
Simplificando essa equação, temos:
0,8 × Fração paralelo + 80 × (1 − Fraçãoparalelo ) = 1
80 − 79,2 × Fração paralelo = 1
Fração paralelo =
80 − 1
79,2
Fração paralelo = 0,9975
Assim, para alcançar um ganho de velocidade de 80 com 100 processadores, apenas
0,25% da computação original pode ser sequencial. Naturalmente, para conseguir
ganho de velocidade linear (ganho de velocidade de n com n processadores), o programa inteiro precisa ser paralelo, sem partes seriais. Na prática, os programas não
só operam no modo totalmente paralelo ou sequencial, como normalmente utilizam
menos do que o complemento total de processadores ao executar no modo paralelo.
O segundo desafio importante no processamento paralelo envolve a grande latência do acesso
remoto em um processador paralelo. Nos multiprocessadores de memória compartilhada
existentes, a comunicação de dados entre os processadores pode custar 35-50 ciclos de clock
(para múltiplos núcleos) até mais de 1.000 ciclos de clock (para multiprocessadores em grande
escala), dependendo do mecanismo de comunicação, do tipo de rede de interconexão e da escala do multiprocessador. O efeito de longos atrasos de comunicação é claramente substancial.
Vamos considerar um exemplo simples.
5.1
Exemplo
Resposta
Suponha uma aplicação executando em um multiprocessador com 32
processadores, que possui um tempo de 200 ns para lidar com a referência
a uma memória remota. Para essa aplicação, considere que todas as referências, exceto aquelas referentes à comunicação, atingem a hierarquia
de memória local, que é ligeiramente otimista. Os processadores ficam
em stall em uma solicitação remota, e a frequência do processador é de
3,3 GHz. Se o CPI de base (considerando que todas as referências atingem
a cache) é 0,5, quão mais rápido será o multiprocessador se não houver
comunicação versus se 0,2% das instruções envolverem uma referência
de comunicação remota?
É mais simples calcular primeiro o CPI. O CPI efetivo para o multiprocessador
com 0,2% de referências remotas é
CPI = CPI base + taxa de solicitação remota × custo de solicitação remota
= 0,5 + 0,2% × custo de solicitação remota
O custo de solicitação remota é
Custodeacesso remoto 200ns
=
= 666ciclos
Tempodeciclo
0,3ns
Logo, podemos calcular o CPI:
CPI = 0,5 + 1,2 = 1,7
O multiprocessador com todas as referências locais é 1,7/0,5 = 3,4 vezes
mais rápido. Na prática, a análise de desempenho é muito mais complexa,
pois alguma fração das referências que não são de comunicação se perderá na hierarquia local e o tempo de acesso remoto não terá um único
valor constante. Por exemplo, o custo de uma referência remota poderia ser
muito pior, pois a disputa causada por muitas referências que tentam usar
a interconexão global poderia ocasionar atrasos maiores.
Esses problemas — paralelismo insuficiente e comunicação remota com longa latência — são os dois maiores desafios para o desempenho no uso dos microprocessadores.
O problema do paralelismo de aplicação inadequado precisa ser atacado sobretudo
no software com novos algoritmos que podem ter melhor desempenho paralelo, além
de sistemas de software que maximizem o tempo gasto na execução com todos os
processadores. A redução do impacto da longa latência remota pode ser atacada pela
arquitetura e pelo programador. Por exemplo, podemos reduzir a frequência dos acessos remotos com mecanismos de hardware, como o caching de dados compartilhados
ou com mecanismos de software, como a reestruturação dos dados para termos mais
acessos locais. Podemos tentar tolerar a latência usando o multithreading (tratado
mais adiante neste capítulo) ou a pré-busca (um tópico que abordamos extensivamente
no Capítulo 2).
Grande parte deste capítulo enfoca as técnicas para reduzir o impacto da longa latência
de comunicação remota. Por exemplo, as Seções 5.2 a 5.4 discutirão como o caching
pode ser usado para reduzir a frequência de acesso remoto enquanto mantém uma visão
coerente da memória. A Seção 5.5 discute o sincronismo, que, por envolver inerentemente
a comunicação entre processadores e também limitar o paralelismo, é um importante
gargalo em potencial. A Seção 5.6 abrange as técnicas de ocultação de latência e modelos
consistentes de memória para a memória compartilhada. No Apêndice I, focalizamos
principalmente os multiprocessadores em grande escala, que são usados predominantemente para o trabalho científico. Nesse apêndice, examinaremos a natureza de tais
aplicações e os desafios de alcançar um ganho de velocidade com dezenas a centenas de
processadores.
Introdução
307
308
CAPÍTULO 5 :
Paralelismo em nível de thread
5.2 ESTRUTURAS DA MEMÓRIA COMPARTILHADA
CENTRALIZADA
A observação de que o uso de grandes caches com múltiplos níveis pode reduzir substancialmente as demandas de largura de banda de memória de um processador é a principal
percepção que motiva os multiprocessadores de memória centralizada. Originalmente,
esses processadores eram todos de núcleo único e muitas vezes ocupavam uma placa inteira, e a memória estava localizada em um barramento compartilhado. Com processadores de alto desempenho mais recentes, as demandas de memória superaram a capacidade
de barramentos razoáveis, e os microprocessadores recentes se conectam diretamente à
memória, dentro de um único chip, o que às vezes é chamado barramento backside ou de
memória, para distingui-lo do barramento usado para a conexão com as E/S. Acessar a
memória local de um chip, seja para uma operação de E/S, seja para um acesso de outro
chip, requer passar pelo chip “proprietário” dessa memória. Assim, o acesso à memória é
assimétrico: mais rápido para a memória local e mais lento para a memória remota. Em
um multicore, essa memória é compartilhada por todos os núcleos em um único chip, mas
o acesso assimétrico à memória de um multicore a partir da memória de outro permanece.
As máquinas simétricas de memória compartilhada normalmente admitem o caching de dados
compartilhados e privados. Os dados privados são usados por um único processador, enquanto os
dados compartilhados são usados por múltiplos processadores, basicamente oferecendo comunicação entre os processadores através de leitura e escrita dos dados compartilhados. Quando um
item privado é colocado na cache, seu local é migrado para a cache, reduzindo o tempo de acesso
médio e também a largura de banda de memória exigida. Como nenhum outro processador
usa os dados, o comportamento do programa é idêntico ao de um uniprocessador. Quando
os dados compartilhados são colocados na cache, o valor compartilhado pode ser replicado
em múltiplas caches. Além da redução na latência de acesso e largura de banda de memória
exigida, essa replicação oferece uma redução na disputa que pode existir pelos itens de dados
compartilhados que estão sendo lidos por múltiplos processadores simultaneamente. Contudo,
o caching dos dados compartilhados introduz um novo problema: coerência da cache.
O que é coerência de cache de multiprocessador?
Infelizmente, o caching de dados compartilhados introduz um novo problema, pois a
visão da memória mantida por dois processadores diferentes se dá através de seus caches
individuais, que, sem quaisquer precauções adicionais, poderiam acabar vendo dois valores
diferentes. A Figura 5.3 ilustra o problema e mostra como dois processadores diferentes
podem ter dois valores diferentes para o mesmo local. Essa dificuldade geralmente é conhecida como problema de coerência de cache. Observe que o problema da coerência existe
porque temos um estado global, definido primeiramente pela memória principal, e um
estado local, definido pelas caches individuais, que são privativos para cada núcleo de
processador. Assim, em um multicore em que algum nível de cache pode ser compartilhado (p. ex., um L3), embora alguns níveis sejam privados (p. ex., L1 e L2), o problema
da coerência ainda existe e deve ser solucionado.
Informalmente, poderíamos afirmar que um sistema de memória será 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 simplista; a realidade é muito mais
complexa. Essa definição simples contém dois aspectos diferentes do comportamento do
sistema de memória, ambos essenciais para a escrita de programas corretos de memória
compartilhada. O primeiro aspecto, chamado coerência, define quais valores podem ser
retornados por uma leitura. O segundo aspecto, chamado consistência, determina quando
um valor escrito será retornado por uma leitura. Vejamos primeiro a coerência.
5.2
Estruturas da memória compartilhada centralizada
FIGURA 5.3 O problema de coerência de cache para um único local de memória (X), lido e escrito
por dois processadores (A e B).
Inicialmente, consideramos que nenhuma cache contém a variável e que X tem o valor 1. Também consideramos
uma cache write-through; uma cache write-back acrescenta algumas complicações adicionais, porém semelhantes.
Depois que o valor de X tiver sido escrito por A, a cache de A e a memória contêm o novo valor, mas não a cache
de B e, se B ler o valor de X, ele receberá 1!
Um sistema de memória é coerente se
1. Uma leitura por um processador P a um local X, que acompanha uma escrita
por P a X, sem escrita de X por outro processador ocorrendo entre
a escrita e a leitura de P, sempre retorna o valor escrito por P.
2. Uma leitura por um processador ao local X, que acompanha uma escrita
por outro processador a X, retornará o valor escrito se a leitura e a escrita forem
suficientemente separadas no tempo e nenhuma outra escrita em X ocorrer entre
os dois acessos.
3. As escritas no mesmo local são serializadas, ou seja, duas escritas ao mesmo
local por dois processadores quaisquer são vistas na mesma ordem por todos
os processadores. Por exemplo, se os valores 1 e depois 2 forem escritos
em um local, os processadores não poderão jamais ler o valor
do local como 2 e depois como 1.
A primeira propriedade simplesmente preserva a ordem do programa — esperamos que essa propriedade seja verdadeira mesmo em uniprocessadores. 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, diríamos que a memória estava incoerente.
A necessidade de serialização de escrita é mais sutil, mas igualmente importante. Suponha
que não realizássemos as escritas em série e o processador P1 escrevesse no local X seguido
por P2 escrevendo no local X. A serialização das escritas garante que cada processador verá
a escrita feita por P2 no mesmo ponto. Se não realizássemos as escritas em série, algum
processador poderia ver primeiro a escrita de P2 e depois 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.
Embora as três propriedades recém-descritas sejam suficientes para garantir a coerência,
a questão de quanto um valor escrito será visto também é importante. Para ver por que,
observe que não podemos exigir que uma leitura de X 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 por um tempo muito pequeno, talvez seja
impossível garantir que a leitura retorne o valor dos dados escritos, pois os dados escritos
podem nem sequer ter deixado o processador nesse ponto. A questão de exatamente quando
309
310
CAPÍTULO 5 :
Paralelismo em nível de thread
um valor de escrita deve ser visto por um leitor é definida por um modelo de consistência
de memória — um tópico discutido na Seção 5.6.
Coerência e consistência são complementares: a coerência define o comportamento de
leituras e escritas no mesmo local da memória, enquanto a consistência define o comportamento de leituras e escritas com relação aos acessos a outros locais da memória. Por
enquanto, considere as duas suposições a seguir: 1) uma escrita não termina (e permite
que a escrita seguinte ocorra) até que todos os processadores tenham visto o efeito dessa
escrita; 2) 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 A seguido pelo local B, qualquer processador que vê o novo valor de B também
precisa ver o novo valor de A. Essas restrições permitem que o processador reordene
as leituras, mas forçam o processador a terminar uma escrita na ordem do programa.
Contaremos com essa suposição até chegarmos à Seção 5.6, na qual veremos exatamente
as implicações dessa definição, além das alternativas.
Esquemas básicos para impor a coerência
O problema de coerência para multiprocessadores e E/S, embora semelhante na origem,
possui diferentes características que afetam a solução apropriada. Ao contrário 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 que executa em múltiplos processadores normalmente terá cópias dos
mesmos dados em várias caches. Em um multiprocessador coerente, as caches oferecem
migração e replicação dos itens de dados compartilhados.
Caches coerentes oferecem migração, pois um item de dados pode ser movido para uma
cache local e usado ali em um padrão transparente. Essa migração reduz tanto a latência
para acessar um item de dados compartilhado alocado remotamente quanto a demanda
de largura de banda na memória compartilhada.
Caches coerentes também oferecem replicação para dados compartilhados que estão sendo
lidos simultaneamente, pois as caches criam uma cópia do item de dados na cache local.
A replicação reduz tanto a latência de acesso quanto a disputa por um item de dados de
leitura compartilhada. O suporte para essa migração e replicação é fundamental para o
desempenho no acesso aos dados compartilhados. Assim, em vez de tentar solucionar
o problema evitando-o no software, multiprocessadores em pequena escala adotam uma
solução de hardware, introduzindo um protocolo para manter caches coerentes.
Os protocolos para manter coerência para múltiplos processadores são chamados protocolos de
coerência de cache. A chave para implementar um protocolo de coerência de cache é rastrear o
estado de qualquer compartilhamento de um bloco de dados. Existem duas classes de protocolos em uso, que empregam diferentes técnicas para rastrear o estado de compartilhamento:
j
j
Baseado em diretório. O estado de compartilhamento de um bloco de memória
física é mantido em apenas um local, chamado diretório. Existem dois tipos muito
diferentes de coerência de cache baseada em diretório. Em um SMP, podemos usar
um diretório centralizado, associado à memória ou a algum outro tipo de ponto
único de centralização, como a cache mais externa de um multicore. Em um DSM
não faz sentido ter um diretório único, uma vez que isso criaria um único ponto de
contenção e tornar difícil escalar para muitos chips multicore, dadas as demandas
de memória de multicores com oito ou mais núcleos. Diretórios distribuídos são mais
complexos do que um diretório único, e tais projetos serão assunto da Seção 5.4.
Snooping. Em vez de manter o estado do compartilhamento em um único diretório,
cada cache que tem uma cópia dos dados de um bloco da memória física pode
5.2
Estruturas da memória compartilhada centralizada
rastrear o estado de compartilhamento do bloco. Em um SMP, geralmente as caches
são acessíveis por meio de algum meio de broadcast (p. ex., um barramento
que conecta as caches por núcleo à cache ou à memória compartilhada) e todos
os controladores de cache monitoram ou bisbilhotam (snoop) o meio para determinar
se elas têm uma cópia de um bloco que é solicitado em um acesso ao barramento
ou a um switch. O snooping também pode ser usado como protocolo de coerência
para um multiprocessador multichip, e alguns projetos suportam um protocolo
de snooping no topo de um protocolo de diretório dentro de cada multicore!
Os protocolos snooping tornaram-se populares com multiprocessadores que usam microprocessadores (núcleo único) e caches anexados a uma única memória compartilhada por
um barramento. Esse barramento proporcionava um meio de transmissão conveniente
para implementar os protocolos de snooping. As arquiteturas multicore mudaram significativamente a situação, uma vez que todos os multicores compartilham algum nível de
cache no chip. Assim, alguns projetos mudaram para usar protocolos de diretório, uma vez
que o overhead era pequeno. Para permitir ao leitor se familiarizar com os dois tipos de
protocolo, nos concentraremos em um protocolo snooping e discutiremos um protocolo
de diretório quando chegarmos às arquiteturas DSM.
Protocolos de coerência por snooping
Existem duas maneiras de manter o requisito de coerência descrito na subseção anterior.
Uma delas é garantir que um processador tenha acesso exclusivo a um item de dados antes
que escreva nesse item. Esse estilo de protocolo é chamado protocolo de invalidação de escrita,
pois invalida outras cópias em uma escrita. De longe, é o protocolo mais comum para
os esquemas de snooping e de diretório. O acesso exclusivo garante que nenhuma outra
cópia de um item que possa ser lida ou escrita existirá quando houver a escrita: todas as
outras cópias do item na cache serão invalidadas.
A Figura 5.4 mostra um exemplo de 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
FIGURA 5.4 Exemplo de um protocolo de invalidação que trabalha em barramento de snooping para um único bloco de cache (X)
com caches write-back.
Consideramos que nenhuma cache mantém inicialmente X e que o valor de X na memória é 0. O processador e o conteúdo da memória mostram o valor
depois que as atividades do processador e do barramento tiverem sido concluídas. Um espaço em branco indica nenhuma atividade ou nenhuma cópia na
cache. Quando ocorrer uma segunda falta por B, o processador A responderá com o valor cancelando a resposta da memória. Além disso, tanto o conteúdo
da cache de B quanto o conteúdo da memória de X são atualizados. Essa atualização da memória, que ocorre quando um bloco se torna compartilhado,
simplifica o protocolo, mas só será possível rastrear a propriedade e forçar a escrita de volta se o bloco for substituído. Isso exige a introdução de um
estado adicional, chamado “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 alterar o bloco ou substituí-lo. Se um multicore usa uma cache compartilhada (p. ex., L3), toda
a memória é vista através da cache compartilhada. L3 age como a memória nesse exemplo, e a coerência deve ser tratada para os L1 e L2 provados
de cada núcleo. Foi essa observação que levou alguns projetistas a optarem por um protocolo de diretório dentro do multicore. Para fazer isso funcionar,
a cache L3 deve ser inclusiva (página 348).
311
312
CAPÍTULO 5 :
Paralelismo em nível de thread
uma escrita seguida por uma leitura por outro processador: como a escrita exige acesso
exclusivo, qualquer cópia mantida pelo processador que está lendo precisa ser invalidada
(daí o nome do protocolo). Assim, quando ocorre a leitura, ocorre um miss na cache e ela
é forçada a buscar uma nova cópia dos dados. Para uma escrita, exigimos que o processador
que está escrevendo tenha acesso exclusivo, para evitar que qualquer outro processador seja
capaz de escrever simultaneamente. Se dois processadores tentarem escrever os mesmos
dados simultaneamente, um deles vencerá a corrida (veremos como decidir quem vence
em breve), 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
tem de conter o valor atualizado. Portanto, esse protocolo impõe a serialização da escrita.
A alternativa a um protocolo de invalidação é atualizar todas as cópias na cache de um
item de dados quando esse item é escrito. Esse tipo de protocolo é chamado protocolo
de atualização de escrita ou broadcast de escrita. Como um protocolo de atualização de escrita precisa transmitir por broadcast todas as escritas nas linhas da cache compartilhada,
consome muito mais largura de banda. Por esse motivo, todos os multiprocessadores
recentes optaram por implementar um protocolo de invalidação de escrita; no restante
deste capítulo enfocaremos apenas os protocolos de invalidação.
Técnicas básicas de implementação
A chave para a implementação de um protocolo de invalidação em um multiprocessador
em pequena escala é o uso do barramento ou de outro meio de broadcast para realizar as
invalidações. Em multiprocessadores de múltiplos chips mais antigos, o barramento usado
para coerência era o barramento de acesso à memória compartilhada. Em um multicore,
o barramento pode ser a conexão entre as caches privadas (L1 e L2 no Intel Core i7) e
a cache externa compartilhada (L3 no i7). Para realizar uma invalidação, o processador
simplesmente adquire acesso ao barramento e transmite o endereço a ser invalidado no
barramento por broadcast. Todos os processadores realizam um snoop continuamente
no barramento, observando os endereços. Os processadores verificam se o endereço no barramento está em sua cache. Se estiver, os dados correspondentes na cache serão invalidados.
Quando ocorre uma escrita em um bloco que é compartilhado, o processador que está
escrevendo precisa adquirir o acesso ao barramento para enviar sua invalidação por broadcast. Se dois processadores tentarem escrever em blocos compartilhados ao mesmo tempo,
suas tentativas de enviar uma operação de invalidação por broadcast serão serializadas
quando disputarem o barramento. O primeiro processador a obter acesso ao barramento
fará com que quaisquer outras cópias do bloco que estiver escrevendo sejam invalidadas.
Se os processadores estiverem tentando escrever no mesmo bloco, a serialização imposta
pelo barramento também serializará suas escritas. Uma implicação desse esquema é que
uma escrita em um item de dados compartilhado não pode realmente ser concluída até
obter acesso ao barramento. Todos os esquemas de coerência exigem algum método de
serializar os acessos ao mesmo bloco de cache, seja serializando o acesso ao meio de
comunicação, seja serializando outra estrutura compartilhada.
Além de invalidar cópias pendentes de um bloco de cache que está sendo escrito, também
precisamos localizar um item de dados quando ocorre uma cache miss. Em uma cache
write-through, é fácil encontrar o valor recente de um item de dados, pois todos os dados
escritos são enviados à memória, na qual o valor mais recente de um item de dados sempre
pode ser apanhado. (Os buffers de escrita podem levar a algumas complexidades adicionais,
por isso devem efetivamente ser tratados como entradas adicionais de cache.)
Para uma cache write-back, o problema de encontrar os valores de dados mais recentes é mais
difícil, pois o valor mais recente de um item de dados pode estar em uma cache privada e
5.2
Estruturas da memória compartilhada centralizada
não em uma cache compartilhada ou na memória. Felizmente, as caches write-back podem
usar o mesmo esquema de snooping, tanto para cache miss quanto para escritas: cada processador bisbilhota cada endereço colocado no barramento. Se um processador descobrir que
possui uma cópia modificada do bloco de cache solicitado, oferecerá esse bloco de cache em
resposta à solicitação de leitura e fará com que o acesso à memória (ou L3) seja abortado.
A complexidade adicional advém do fato de ser necessário recuperar o bloco de cache de
uma cache privada de outro processador (L1 ou L2), o que normalmente demorará mais
tempo do que recuperá-lo da L3. Como as caches write-back geram requisitos inferiores para
largura de banda de memória, eles podem admitir maior quantidade de processadores mais
rápidos, e essa tem sido a técnica escolhida na maioria dos multiprocessadores, apesar da
complexidade adicional de manter a coerência. Portanto, examinaremos a implementação
da coerência com as caches write-back.
As tags de cache normais podem ser usadas para implementar o processo de snooping, e o
bit de validade para cada bloco torna a invalidação fácil de implementar. Faltas de leitura,
sejam elas geradas por invalidação, seja por algum outro evento, também são diretas, pois
simplesmente contam com a capacidade de snooping. Para escritas, gostaríamos de saber
se quaisquer outras cópias do bloco estão na cache porque, se não houver outras cópias na
cache, a escrita não precisará ser colocada no barramento em uma cache write-back. Não
enviar a escrita reduz tanto o tempo gasto pela escrita quanto a largura de banda exigida.
Para rastrear se um bloco de cache é ou não compartilhado, podemos acrescentar um bit
de estado extra associado a cada bloco de cache, assim como temos um bit de validade e
um bit de modificação. Acrescentando um bit para indicar se o bloco é compartilhado,
podemos decidir se uma escrita precisa gerar invalidação. Quando ocorre escrita em um
bloco no estado compartilhado, a cache gera invalidação no barramento e marca o bloco
como exclusivo. Nenhuma outra invalidação será enviada por esse processador para esse
bloco. O processador com a única cópia de um bloco de cache normalmente é chamado
proprietário (owner) do bloco de cache.
Quando uma invalidação é enviada, o estado do bloco de cache do proprietário é trocado
de compartilhado para não compartilhado (ou exclusivo). Se outro processador mais tarde
exigir esse bloco de cache, o estado precisará se tornar compartilhado novamente. Como
nossa cache snooping também vê quaisquer misses, ele sabe quando o bloco de cache
exclusivo foi solicitado por outro processador e o estado deve tornar-se compartilhado.
Cada transação do barramento precisa verificar as tags de endereço de cache, que poderiam
interferir com os acessos à cache do processador. Uma maneira de reduzir essa interferência
é duplicar as tags. Outra abordagem é usar um diretório na cache L3 compartilhada. O
diretório indica se um dado bloco é compartilhado e que núcleos podem ter cópias. Com a
informação de diretório, invalidações podem ser direcionadas somente para as caches com
cópias do bloco de cache. Isso requer que L3 tenha sempre uma cópia de qualquer item de
dados em L1 ou L2, uma propriedade chamada inclusão, a qual retomaremos na Seção 5.7.
Um exemplo de protocolo
Um protocolo de coerência snooping normalmente é implementado pela incorporação
de um controlador de estados finitos em cada nó. Esse controlador responde a solicitações
do processador e do barramento (ou de outro meio de broadcast), alterando o estado do
bloco de cache selecionado e também usando o barramento para acessar os dados ou
invalidá-los. Logicamente, você pode pensar em um controlador separado estando associado a cada bloco, ou seja, as operações de snooping ou solicitações de cache para
diferentes blocos podem prosseguir independentemente. Nas implementações reais, um
único controlador permite que múltiplas operações para blocos distintos prossigam de
313
314
CAPÍTULO 5 :
Paralelismo em nível de thread
um modo intercalado (ou seja, uma operação pode ser iniciada antes que outra seja concluída, embora somente um acesso à cache ou um acesso ao barramento seja permitido de
cada vez). Além disso, lembre-se de que, embora estejamos nos referindo a um barramento
na descrição a seguir, qualquer rede de interconexão que admita um broadcast a todos os
controladores de coerência e suas caches associadas poderá ser usada para implementar
o snooping.
O protocolo simples que consideramos possui três estados: inválido, compartilhado e
modificado. O estado compartilhado indica que o bloco é potencialmente compartilhado,
enquanto o estado modificado indica que o bloco foi atualizado na cache; observe que
o estado modificado implica que o bloco é exclusivo. A Figura 5.5 mostra as solicitações
geradas pelo módulo de cache do processador em um nó (na metade superior da tabela),
além daquelas que vêm do barramento (na metade inferior da tabela). Esse protocolo
é para uma cache write-back, mas é facilmente alterado para trabalhar para uma cache
write-through, reinterpretando o estado modificado como um estado exclusivo e atualizando a cache nas escritas no padrão normal para uma cache write-through. A extensão mais
comum desse protocolo básico é o acréscimo de um estado exclusivo, que descreve um
bloco que não é modificado, mas mantido em apenas uma cache privada. Descreveremos
essa e outras extensões nas páginas 317 e 318.
Quando uma invalidação ou uma falta de escrita é colocada no barramento, quaisquer
núcleos cujas caches privadas têm cópias do bloco de cache a invalidam. Para falta de
escrita em uma cache write-back, se o bloco for exclusivo em apenas uma cache, essa
cache também escreverá de volta no bloco; caso contrário, os dados podem ser lidos
da cache compartilhada ou memória.
A Figura 5.6 mostra um diagrama de transição de estados finitos para um único bloco
de cache que usa um protocolo de invalidação de escrita e uma cache write-back. Para
simplificar, os três estados do protocolo são duplicados para representar transições com
base nas solicitações do processador (à esquerda, que corresponde à metade superior da
tabela na Figura 5.5), ao contrário das transições baseadas nas solicitações de barramento
(à direita, que corresponde à metade inferior da tabela na Figura 5.5). O texto em negrito
é usado para distinguir as ações do barramento, ao contrário das condições em que uma
transição de estado depende. O estado em cada nó representa o estado do bloco de cache
selecionado, especificado pela solicitação de processador ou de barramento.
Todos os estados nesse protocolo de cache seriam necessários em uma cache de uniprocessador, onde corresponderiam aos estados inválido, válido (e limpo) e modificado. A
maioria das mudanças de estados indicadas pelos arcos na metade esquerda da Figura 5.6
seria necessária em uma cache de uniprocessador write-back, exceto a invalidação em um
acerto de escrita para um bloco compartilhado. As mudanças de estados representadas
pelos arcos na metade direita da Figura 5.6 são necessárias apenas por coerência, e não
apareceriam de forma alguma em um controlador de cache de uniprocessador.
Conforme mencionamos, existe apenas uma máquina de estados finitos por cache, com
estímulos vindos do processador associado ou do barramento. A Figura 5.7 mostra como
as transições de estados na metade direita da Figura 5.6 são combinadas com as da metade
esquerda da figura para formar um único diagrama de estados para cada bloco de cache.
Para entender por que esse protocolo funciona, observe que qualquer bloco de cache válido
está no estado compartilhado em uma ou mais caches ou no estado exclusivo, exatamente
em uma cache. Qualquer transição para o estado exclusivo (que é exigido para que um
processador escreva no bloco) exige que uma invalidação ou falta de escrita seja colocada
no barramento, fazendo com que todas as caches locais tornem o bloco inválido. Além
5.2
Estruturas da memória compartilhada centralizada
FIGURA 5.5 O mecanismo de coerência de cache recebe solicitações tanto do processador quanto do barramento e as responde com base
no tipo de solicitação, se ela acerta ou falta na cache local, e o estado do bloco de cache especificado na solicitação.
A quarta coluna descreve o tipo de ação de cache como acerto ou falta normal (o mesmo que uma cache de uniprocessador veria), substituição (uma falta
de substituição, uma falta de cache do uniprocessador) ou coerência (exigida para manter a coerência da cache); uma ação normal ou de substituição pode
causar uma ação de coerência, dependendo do estado do bloco em outras caches. Para falta de leitura, faltas, faltas de escrita ou invalidações monitoradas
do barramento, uma ação é necessária somente se os endereços de leitura ou escrita corresponderem a um bloco na cache e o bloco for válido.
disso, se alguma outra cache local tiver o bloco no estado exclusivo, essa cache local gera
um write-back, que fornece o bloco com o endereço desejado. Finalmente, se houver falta
de leitura no barramento para um bloco no estado exclusivo, a cache local com a cópia
exclusiva mudará seu estado para compartilhado.
As ações em cinza na Figura 5.7, que tratam de miss de leitura e de escrita no barramento,
são essencialmente o componente snooping do protocolo. Outra propriedade que é preservada nesse e na maioria dos outros protocolos é que qualquer bloco de memória no estado
compartilhado sempre está atualizado em relação à cache externa compartilhada (L2 ou
L3 ou memória, e não existir cache compartilhada), o que simplifica a implementação.
315
316
CAPÍTULO 5 :
Paralelismo em nível de thread
FIGURA 5.6 Protocolo de invalidação de escrita, coerência de cache, para uma cache privada write-back, mostrando os estados
e as transições de estados para cada bloco na cache.
Os estados da cache aparecem em círculos, com qualquer acesso permitido pelo processador local sem transição de estado sendo mostrado entre
parênteses, sob o nome do estado. O estímulo que causa uma mudança de estado aparece nos arcos de transição em tipo normal, e quaisquer ações
do barramento geradas como parte da transição de estado aparecem no arco de transição em negrito. As ações de estímulo se aplicam a um bloco
na cache, e não a um endereço específico na cache. Logo, uma falta de leitura para um bloco no estado compartilhado é um miss para esse bloco de cache,
mas para um endereço diferente. O lado esquerdo do diagrama mostra as transições de estado com base nas ações do processador associados a essa
cache; o lado direito mostra as transições com base nas operações sobre o barramento. Falta de leitura no estado exclusivo ou compartilhado
e falta de escrita no estado exclusivo ocorrem quando o endereço solicitado pelo processador não combina com o endereço no bloco de cache. Tal falta
é uma falta de substituição de cache-padrão. Uma tentativa de escrever um bloco no estado compartilhado gera invalidação. Sempre que ocorre uma
transação no barramento, todas as caches que contêm o bloco de cache especificado na transação do barramento tomam a ação indicada pela metade
direita do diagrama. O protocolo considera que a memória (ou a cache compartilhada) oferece dados em uma falta de leitura para um bloco que é limpo
em todas as caches. Nas implementações reais, esses dois conjuntos de diagramas de estado são combinados. Na prática, existem muitas variações sutis
nos protocolos de invalidação, incluindo a introdução do estado não modificado exclusivo quanto ao fato de um processador ou memória oferecer dados
em uma falta. Em um chip multicore, a cache compartilhada (geralmente L3, mas às vezes L2) age como o equivalente da memória, e o barramento
é o barramento entre as caches privadas de cada núcleo e a cache compartilhada, que, por sua vez, tem interfaces com a memória.
Na verdade, não importa se o nível fora das caches privadas é uma cache ou memória
compartilhada. A chave é que todos os acessos dos núcleos passam por esse nível.
Embora nosso protocolo de cache simples esteja correto, ele omite uma série de complicações
que tornam a implementação muito mais complicada. A mais importante delas é que o
protocolo considera que as operações são atômicas — ou seja, uma operação pode ser feita de
modo que nenhuma operação intermediária possa ocorrer. Por exemplo, o protocolo descrito
considera que as faltas de escrita podem ser detectadas, que o barramento pode ser tomado e
que uma resposta possa ser dada como uma única ação indivisível. Na realidade, isso não é
verdade. De fato, mesmo um miss de leitura pode não ser indivisível. Depois de detectar um
miss no L2 de um multicore, o núcleo deve decidir entre acessar o barramento que o conecta
à cache compartilhada L3. Ações não indivisíveis introduzem a possibilidade de o protocolo
sofrer deadlock, significando que ele chega a um estado em que não pode continuar. Em breve,
exploraremos essas complicações nesta seção, quando examinarmos projetos DSM.
Com os processadores multicore, a coerência entre os núcleos do processador é toda
implementada no chip, usando um protocolo snooping ou diretório central simples.
Muitos chips com dois processadores, incluindo o Intel Xeon e AMD Opteron, suportavam
5.2
Estruturas da memória compartilhada centralizada
FIGURA 5.7 Diagrama de estado de coerência de cache com as transições de estado induzidas
pelo processador local, mostradas em preto, e pelas atividades de barramento, mostradas em cinza.
Assim como na Figura 5.6, as atividades em uma transição aparecem em negrito.
multiprocessadores com múltiplos chips que poderiam ser construídos conectando uma
interface de alta velocidade (chamadas Quickpath ou Hypertranspor, respectivamente).
Esses tipos de interconexões não são apenas extensões do barramento compartilhado, mas
usam uma abordagem diferente para multicores interconectados.
Um multiprocessador construído com múltiplos chips multicore terá uma arquitetura
de memória compartilhada e precisará de um mecanismo de coerência interna ao chip,
acima e além daquela existente dentro do chip. Na maioria dos casos, alguma forma
de esquema de diretório é usada.
Extensões do protocolo básico de coerência
O protocolo de coerência que acabamos de descrever é um simples protocolo de três
estados e, muitas vezes, é chamado pela primeira letra dos estados, fazendo dele um
protocolo MSI (Modificado, Compartilhado, Inválido — Modified, Shared, Invalid). Existem
muitas extensões para esse protocolo básico, que mencionamos nas legendas desta seção.
Essas extensões são criadas pela adição de estados e transações, que otimizam certos
comportamentos, possivelmente resultando em melhor desempenho. Duas das extensões
mais comuns são:
1. MESI adiciona o estado Exclusivo ao protocolo básico MSI para indicar quando
um bloco de cache é residente somente em uma cache única, mas está limpo.
317
318
CAPÍTULO 5 :
Paralelismo em nível de thread
Se um bloco estiver no estado E, ele pode ser gravado sem gerar nenhuma
invalidação, o que otimiza o caso em que um bloco é lido por uma única cache
antes de ser escrito por ele. Obviamente, quando um miss de leitura para um bloco
no estado E ocorre, o bloco deve ser modificado para o estado S para manter
a coerência. Uma vez que todos os acessos subsequentes são monitorados,
é possível manter a coerência desse estado. Em particular, se outro processador
despachar um miss de leitura, o estado é mudado de exclusivo para compartilhado.
A vantagem de adicionar esse estado é que uma gravação subsequente
para um bloco no estado exclusivo pelo mesmo núcleo não precisa obter
acesso ao barramento ou gerar uma invalidação, já que se sabe que o bloco está
exclusivamente nessa cache local. O processador simplesmente muda o estado
para modificado. Esse estado é adicionado facilmente usando o bit que codifica
o estado coerente como um estado exclusivo e usando o bit modificado para indicar
que um bloco foi modificado. O popular protocolo MESI, que recebe o nome
dos quatro estados que ele inclui (Modificado, Exclusivo, Compartilhado
e Inválido), usa essa estrutura. O Intel i7 usa uma variação de um protocolo MESI,
chamada MESIF, que adiciona um estado (Forward) para designar que o processador
que está compartilhando deve responder a uma requisição. Isso é projetado
para aumentar o desempenho em organizações de memória distribuída.
2. MOESI adiciona o estado Owned (proprietário) para o protocolo MESI para indicar
que o bloco associado é de propriedade daquela cache e está desatualizado
na memória. Em protocolos MSI e MESI, quando há uma tentativa de compartilhar
um bloco no estado Modificado, o estado é mudado para Compartilhado
(tanto na cache original quanto na que agora está compartilhada), e o bloco
deve ser escrito de volta na memória. Em um protocolo MOESI, o bloco pode ser
mudado do estado Modificado para o estado Owned na cache original sem gravá-lo
na memória. Outras caches, que agora estão compartilhando o bloco, mantêm
o bloco no estado Compartilhado. O estado O, que somente a cache original
mantém, indica que a cópia na memória principal está desatualizada e que a cache
designada é a proprietária. A proprietária do bloco deve fornecê-lo no caso
de uma falta, já que a memória não está atualizada e deve gravar o bloco de volta
na memória se ele for substituído. O AMD Opteron usa o protocolo MOESI.
A próxima seção examinará o desempenho desses protocolos para nossas cargas de trabalho paralelas e multiprogramadas. O valor dessas extensões para um protocolo básico
ficará claro quando examinarmos o desempenho. Mas, antes de fazermos isso, vamos dar
uma rápida olhada nas limitações no uso de uma estrutura simétrica de memória e um
esquema de coerência snooping.
Limitações nos multiprocessadores simétricos
de memória compartilhada e protocolos de snooping
À medida que o número de processadores em um multiprocessador cresce ou as demandas
de memória de cada processador aumentam, qualquer recurso centralizado no sistema
pode se tornar um gargalo. Usando a maior conexão de largura de banda disponível no
chip e uma cache L3 compartilhada, que é mais rápido do que a memória, os projetistas
têm conseguido suportar quatro a oito núcleos de alto desempenho de modo simétrico.
Tal abordagem provavelmente não vai muito além de oito núcleos, e não vai funcionar
quando múltiplos multicores forem combinados.
A largura de banda de snooping nas caches também pode se tornar um problema, já
que cada cache deve examinar cada falta colocada no barramento. Como mencionamos,
duplicar as tags é uma solução. Outra abordagem, que tem sido adotada em alguns
5.2
Estruturas da memória compartilhada centralizada
multicores recentes, é colocar um diretório no nível da cache mais externa. O diretório indica explicitamente as caches em que o processador tem cópias de cada item da cache mais
externa. Essa é a abordagem que a Intel usa nas séries i7 e Xeon 7000. Observe que o uso
desse diretório não elimina o gargalo devido a um barramento e uma L3 compartilhados
entre os processadores, mas é muito mais simples de implementar do que os esquemas
de diretório distribuído que vamos examinar na Seção 5.4.
Como um projetista poderia aumentar a largura de banda da memória para dar suporte
a mais processadores e a processadores mais rápidos? Para aumentar a largura de banda
de comunicação entre os processadores e a memória, os projetistas têm usado vários
barramentos e também redes de interconexão, como crossbars ou pequenas redes ponto a
ponto. Nesses projetos, o sistema de memória pode ser configurado em múltiplos bancos
físicos, de modo a aumentar a largura de banda efetiva da memória enquanto retém o
tempo de acesso uniforme à memória. A Figura 5.8 mostra essa técnica, que representa um
ponto intermediário entre as duas técnicas que discutimos no início do capítulo: memória
compartilhada centralizada e memória compartilhada distribuída.
O AMD Opteron representa outro ponto intermediário no espectro entre um protocolo
snooping e de diretório. A memória está conectada diretamente a cada chip multicore, e
até quatro chips multicore podem estar conectados. O sistema é NUMA, já que a memória
local é um pouco mais rápida. O Opteron implementa seu protocolo de coerência usando
os links ponto a ponto para transmitir por broadcast a até três outros chips. Como os
links entre os processadores não são compartilhados, a única maneira de um processador
saber quando uma operação inválida foi concluída é uma confirmação explícita. Assim,
o protocolo de coerência utiliza um broadcast para encontrar cópias potencialmente
FIGURA 5.8 Multiprocessador com acesso uniforme à memória que usa bancos cache compartilhada
e rede de interconexão em vez de barramento.
319
320
CAPÍTULO 5 :
Paralelismo em nível de thread
compartilhadas, como um protocolo snooping, mas usa as confirmações para ordenar as
operações, como um protocolo de diretório. Já que a memória local é só um pouco mais
rápida do que a memória remota na implementação Opteron, alguns softwares tratam
um multiprocessador Opteron como tendo acesso uniforme à memória.
Um protocolo de coerência de cache snooping pode ser usado sem barramento centralizado, mas ainda exigir que um broadcast seja feito para monitorar as caches individuais
em cada falta em um potencial bloco de cache compartilhado. Esse tráfego de coerência
de cache cria outro limite na escala e na velocidade dos processadores. Como o tráfego de
coerência não é afetado por caches maiores, os processadores mais rápidos inevitavelmente
sobrecarregarão a rede e a capacidade de cada cache responderá a solicitações de snoop de
todas as outras caches. Na Seção 5.4, examinaremos os protocolos baseados em diretório,
que eliminam a necessidade de broadcast para todas as outras caches, em uma falta. À
medida que aumentam as velocidades de processador e o número de núcleos por processador, mais projetistas provavelmente optam por tais protocolos para evitar o limite de
broadcast de um protocolo snooping.
Implementando coerência snooping de cache
O diabo está nos detalhes.
Provérbio clássico
Quando escrevemos a primeira edição deste livro, em 1990, nossa seção final “Juntando
tudo” foi um multiprocessador de 30 processadores e único barramento, usando a coerência baseada em snoop; o barramento tinha uma capacidade pouco acima de 50 MB/s, que
em 2011 não seria largura de banda de barramento suficiente para dar suporte nem mesmo
a um Intel i7! Quando escrevemos a segunda edição deste livro, em 1995, os primeiros
multiprocessadores de coerência de cache com mais de um barramento tinham aparecido
recentemente; acrescentamos um apêndice para descrever a implementação do snooping em um sistema com múltiplos barramentos. Em 2011, a maioria dos processadores
multicore que suportavam somente um multiprocessador de chip único optou por usar
uma estrutura de barramento compartilhado conectada a uma memória compartilhada ou
a uma cache compartilhada. Em contraste, todos os sistemas multiprocessadores multicore
que suportam 16 ou mais núcleos não utilizam uma interconexão de único barramento, e
os projetistas precisam encarar o desafio de implementar o snooping sem a simplificação
de um barramento para colocar os eventos em série.
Como já dissemos, a principal complicação para implementar o protocolo de coerência
snooping que descrevemos é que as faltas de escrita e de atualização não são indivisíveis
em qualquer multiprocessador recente. As etapas de detecção de uma falta de escrita ou
de atualização, comunicação com outros processadores e com a memória, obtenção do
valor mais recente para uma falta de escrita e garantia de que quaisquer invalidações são
processadas, e a atualização da cache não pode ser feita como se utilizassem um único ciclo.
Em um único chip multicore, essas etapas podem se tornar efetivamente indivisíveis
apanhando o barramento primeiro (antes de alterar o estado da cache) e não liberando
o barramento até que todas as ações sejam concluídas. Como o processador pode saber
quando todas as invalidações foram concluídas? Em alguns multicore, uma única linha
é usada para sinalizar quando todas as invalidações necessárias foram recebidas e estão
sendo processadas. Após esse sinal, o processador que gerou a falta pode liberar o barramento sabendo que quaisquer ações exigidas serão concluídas antes de qualquer atividade
relacionada à próxima falta. Mantendo o barramento exclusivamente durante essas etapas,
o processador efetivamente torna as etapas individuais indivisíveis.
5.3
Desempenho de multiprocessadores simétricos de memória compartilhada
Em um sistema sem barramento, temos que encontrar algum outro método para tornar
as etapas indivisíveis em caso de falta. Em particular, temos que garantir que dois processadores que tentam escrever no mesmo bloco ao mesmo tempo, uma situação chamada
corrida, sejam estritamente ordenados: uma escrita é processada e prossegue antes que a
próxima seja iniciada. Não importa qual das duas escritas vença a corrida, apenas que
haja uma única vencedora, cujas ações de coerência sejam completadas primeiro. Em um
sistema snooping, fazer com que uma corrida tenha apenas um vencedor é algo garantido
pelo uso do broadcast para todas as faltas, além de algumas propriedades básicas da rede
de interconexão. Essas propriedades, junto com a capacidade de reiniciar o tratamento de
falta do perdedor de uma corrida, são a chave para implementar a coerência snooping
de cache sem barramento. Explicaremos os detalhes no Apêndice I.
É possível combinar snooping e diretórios, e muitos projetistas usam snooping dentro
de um multicore e diretórios entre múltiplos chips ou vice-versa, diretórios dentro de um
multicore e snooping entre múltiplos chips.
5.3 DESEMPENHO DE MULTIPROCESSADORES
SIMÉTRICOS DE MEMÓRIA COMPARTILHADA
Em um multiprocessador que usa protocolo de coerência snoopy, diversos fenômenos
diferentes são combinados para determinar o desempenho. Em particular, o desempenho geral da cache é uma combinação do comportamento do tráfego de cache miss do
uniprocessador e do tráfego causado pela comunicação, que resulta em invalidações e
subsequentes cache miss. A mudança da quantidade de processadores, do tamanho da
cache e do tamanho do bloco pode afetar esses dois componentes da taxa de falta (miss
rate) de diferentes maneiras, levando a um comportamento geral do sistema que é uma
combinação dos dois efeitos.
O Apêndice B desmembra a taxa de falta do uniprocessador na classificação dos três C
(capacity, compulsory e conflict) e oferece percepções tanto para o comportamento da
aplicação como para possíveis melhorias no projeto da cache. De modo semelhante,
as faltas que surgem da comunicação entre processadores, que normalmente são chamadas
faltas de coerência, podem ser desmembradas em duas origens separadas.
A primeira origem são as chamadas faltas de compartilhamento verdadeiros, que surgem
da comunicação dos dados pelo mecanismo de coerência de cache. Em um protocolo
baseado em invalidação, a primeira escrita por um processador a um bloco de cache
compartilhado causa invalidação para estabelecer a posse desse bloco. Além disso, quando
outro processador tenta ler uma palavra modificada nesse bloco de cache, ocorre uma
falta e o bloco resultante é transferido. Essas duas faltas são classificadas como faltas de
compartilhamento verdadeiras, pois surgem diretamente do compartilhamento de dados
entre os processadores.
O segundo efeito, chamado compartilhamento falso, surge do uso de um algoritmo de coerência baseado em invalidação com um único bit de validade por bloco de cache. O compartilhamento falso ocorre quando um bloco é invalidado (e uma referência subsequente causa
uma falta), pois alguma palavra no bloco, fora a que está sendo lida, é escrita. Se a palavra
escrita for realmente usada pelo processador que recebeu a invalidação, então a referência
foi uma referência de compartilhamento verdadeira e teria causado uma falta, independentemente do tamanho do bloco. Porém, se a palavra que está sendo escrita e a palavra
lida forem diferentes e a invalidação não fizer com que um novo valor seja comunicado,
apenas causando uma falta de cache extra, então ela será uma falta de compartilhamento
321
322
CAPÍTULO 5 :
Paralelismo em nível de thread
falso. Em uma falta de compartilhamento falso, o bloco é compartilhado, mas nenhuma
palavra na cache é realmente compartilhada, e a falta não ocorreria se o tamanho do bloco
fosse uma única palavra. O exemplo a seguir esclarece os padrões de compartilhamento.
Exemplo
Suponha que as palavras x1 e x2 estejam no mesmo bloco de cache, que está
no estado compartilhado nas caches de P1 e P2. Considerando a sequência de
eventos a seguir, identifique cada falta como uma falta de compartilhamento
verdadeiro, uma falta de compartilhamento falso ou um acerto. Qualquer
falta que ocorrer, se o tamanho de bloco for de uma palavra, será designada
como falta de compartilhamento verdadeira.
Tempo
P1
1
Escreve x1
2
3
Lê x2
Escreve x1
4
5
Resposta
P2
Escreve x2
Lê x2
Aqui estão as classificações por etapa no tempo:
1. Esse evento é uma falta de compartilhamento verdadeira,
pois x1 foi lido por P2 e precisa ser invalidado de P2.
2. Esse evento é uma falta de compartilhamento falsa, pois x2
foi invalidado pela escrita de x1 em P1, mas esse valor de x1
não é usado em P2.
3. Esse evento é uma falta de compartilhamento falsa, pois o
bloco contendo x1 é marcado como compartilhado, devido à
leitura em P2, mas P2 não leu x1. O bloco de cache contendo
x1 estará no estado compartilhado depois da leitura por P2;
uma falta de escrita será necessária para obter acesso exclusivo ao bloco. Em alguns protocolos, isso será tratado
como uma solicitação de upgrade, que gera invalidação do
barramento, mas não transfere o bloco de cache.
4. Esse evento é uma falta de compartilhamento falsa, pelo
mesmo motivo da etapa 3.
5. Esse evento é uma falta de compartilhamento verdadeira,
pois o valor sendo lido foi escrito por P2.
Embora vejamos os efeitos das faltas de compartilhamento verdadeiros e falsos nas cargas
de trabalho comerciais, o papel das faltas de coerência é mais significativo para aplicações
fortemente acopladas, que compartilham quantidades significativas de dados do usuário.
Examinaremos seus efeitos com detalhes no Apêndice I, quando considerarmos o desempenho de uma carga de trabalho científica paralela.
Uma carga de trabalho comercial
Nesta seção, examinaremos o comportamento do sistema de memória para um multiprocessador de memória compartilhada com quatro processadores ao rodar uma carga
de trabalho comercial de uso geral. O estudo que examinaremos foi feito em 1998, em
um sistema Alpha de quatro processadores, mas continua sendo o mais completo e esclarecedor para o desempenho de um multiprocessador para tais cargas de trabalho. Os
resultados foram colhidos em um AlphaServer 4100 ou usando um simulador configurável
modelado no AlphaServer 4100. Cada processador no AlphaServer 4100 é um Alpha 21164,
que despacha até quatro instruções por clock e trabalha em 300 MHz. Embora a frequência
do processador Alpha nesse sistema seja consideravelmente mais lenta do que os processadores nos sistemas projetados em 2011, a estrutura básica do sistema, consistindo em
5.3
Desempenho de multiprocessadores simétricos de memória compartilhada
FIGURA 5.9 Características da hierarquia de cache do Alpha 21164 usado neste estudo e no Intel i7.
Embora os tamanhos sejam maiores e a associabilidade seja maior no i7, as penalidades de falta também são maiores,
então o comportamento pode diferir muito pouco. Por exemplo, do Apêndice B, podemos estimar as taxas de falta
da cache L1 menor do Alpha como 4,9% e 3% para a cache L1 maior do i7, então a penalidade de falta média de L1
por referência é 0,34 para o Alpha e 0,30 para o i7. Os dois sistemas têm uma penalidade alta (125 ciclos ou mais)
para uma transferência requerida a partir de uma cache privada. O i7 também compartilha L3 entre todos os núcleos.
um processador de quatro despachos e uma hierarquia de cache de três níveis, é muito
similar à do multicore Intel i7 e outros processadores, como mostrado na Figura 5.9. Em
particular, as caches do Alpha são um pouco menores, mas os tempos de falta (miss times)
também são menores que os de um i7. Assim, o comportamento do sistema Alpha deve
fornecer percepções interessantes sobre o comportamento dos projetos multicore modernos.
A carga de trabalho usada para esse estudo consiste em três aplicações:
1. Uma carga de trabalho de processamento de transação on-line (OLPT) modelada no
TPC-B (que tem comportamento de memória semelhante ao seu primo mais novo,
o TPC-C, descrito no Capítulo 1) e usando Oracle 7.3.2 como sistema de banco
de dados. A carga de trabalho consiste em um conjunto de processos clientes que
geram solicitações e um conjunto de servidores que os tratam.
Os processos servidores consomem 85% do tempo do usuário, com o restante
indo para os clientes. Embora a latência de E/S seja escondida pelo ajuste cuidadoso
e por solicitações suficientes para manter a CPU ocupada, os processos servidores
normalmente são bloqueados para E/S após cerca de 25.000 instruções.
2. Uma carga de trabalho de um sistema de apoio à decisão (decision support system —
DSS) baseada no TPC-D e também usando Oracle 7.3.2 como sistema de banco de
dados. A carga de trabalho inclui apenas seis das 17 consultas de leitura no TPC-D,
embora as seis consultas examinadas no benchmark se espalhem por toda a gama
de atividades do benchmark inteiro. Para encobrir a latência de E/S, o paralelismo
é explorado tanto dentro das consultas, em que o paralelismo é detectado durante
um processo de formulação de consulta, quanto entre as consultas. As chamadas de
bloqueio são muito menos frequentes do que no benchmark OLTP; as seis consultas
têm,