(PDF) Arquitetura de Computadores - Uma Abordagem Quantitativa - David A. Patterson e John L. Hennessy | Alexandre Luis Fajardo Machado - Academia.edu
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฀é฀o฀décimo฀presidente฀da฀Universidade฀de฀Stanford,฀onde฀é฀membro฀ do฀corpo฀docente฀desde฀1977,฀nos฀departamentos฀de฀Engenharia฀Elétrica฀e฀Ciência฀da฀ Computação.฀Hennessy฀é฀membro฀do฀IEEE฀e฀ACM,฀membro฀da฀Academia฀Nacional฀de฀ Engenharia฀e฀da฀Sociedade฀Americana฀de฀Filosoia฀e฀membro฀da฀Academia฀Americana฀ de฀Artes฀e฀Ciências.฀Entre฀seus฀muitos฀prêmios฀estão฀o฀Prêmio฀Eckert-Mauchly฀de฀2001,฀ por฀suas฀contribuições฀para฀a฀tecnologia฀RISC,฀o฀Prêmio฀Seymour฀Cray฀de฀Engenharia฀da฀ Computação฀de฀2001฀e฀o฀Prêmio฀John฀von฀Neumann฀de฀2000,฀que฀ele฀dividiu฀com฀David฀ Patterson.฀Ele฀também฀recebeu฀sete฀doutorados฀honorários. Em฀1981,฀ele฀iniciou฀o฀Projeto฀MIPS,฀em฀Stanford,฀com฀um฀grupo฀de฀estudantes฀de฀ pós-graduação.฀Depois฀de฀completar฀o฀projeto฀em฀1984,฀tirou฀licença฀da฀universidade฀ para฀co-fundar฀a฀MIPS฀Computer฀Systems฀(hoje฀MIPS฀Technologies),฀que฀desenvolveu฀ um฀dos฀primeiros฀microprocessadores฀RISC฀comerciais.฀Em฀2006,฀mais฀de฀2฀bilhões฀de฀ microprocessadores฀MIPS฀foram฀vendidos฀em฀dispositivos,฀variando฀de฀video฀games฀e฀ computadores฀palmtop฀a฀impressoras฀laser฀e฀switches฀de฀rede.฀Em฀seguida,฀Hennessy฀ liderou฀o฀projeto฀DASH฀(Director฀Architeture฀for฀Shared฀Memory฀–฀Arquitetura฀Diretora฀ para฀Memória฀Compartilhada),฀que฀criou฀o฀protótipo฀do฀primeiro฀microprocessador฀ com฀cache฀coerente฀escalável.฀Muitas฀das฀ideias-chave฀desse฀projeto฀foram฀adotadas฀em฀ multiprocessadores฀modernos.฀Além฀de฀suas฀atividades฀técnicas฀e฀responsabilidades฀na฀ universidade,฀ele฀continuou฀a฀trabalhar฀com฀diversas฀empresas฀startup฀como฀conselheiro฀ nos฀estágios฀iniciais฀e฀como฀investidor. David A. Patterson฀ensina฀arquitetura฀de฀computadores฀na฀Universidade฀da฀Califórnia,฀ em฀Berkeley,฀desde฀que฀se฀juntou฀ao฀corpo฀docente฀em฀1977,฀onde฀ele฀ocupa฀a฀Cadeira฀ Pardee฀de฀Ciência฀da฀Computação.฀Sua฀docência฀foi฀honrada฀com฀o฀Prêmio฀de฀Ensino฀ Notável฀da฀Universidade฀da฀Califórnia,฀o฀Prêmio฀Karlstrom฀da฀ACM,฀a฀Medalha฀Mulligan฀ de฀Educação฀e฀o฀Prêmio฀de฀Ensino฀Universitário฀do฀IEEE.฀Patterson฀recebeu฀o฀Prêmio฀de฀ Realização฀Técnica฀do฀IEEE฀e฀o฀Prêmio฀Eckert-Mauchly฀por฀contribuições฀para฀o฀RISC฀e฀ dividiu฀o฀Prêmio฀Johnson฀de฀Armazenamento฀de฀Informações฀por฀contribuições฀para฀o฀ RAID.฀Ele฀também฀dividiu฀a฀Medalha฀John฀von฀Neumann฀do฀IEEE฀e฀o฀Prêmio฀C&C฀com฀ John฀Hennessy.฀Como฀seu฀coautor,฀Patterson฀é฀membro฀da฀Academia฀Americana฀de฀Artes฀ e฀Ciências,฀do฀Museu฀da฀História฀dos฀Computadores,฀ACM฀e฀IEEE,฀e฀foi฀eleito฀para฀a฀Academia฀Nacional฀de฀Engenharia,฀Academia฀Nacional฀de฀Ciências฀e฀para฀o฀Hall฀da฀Fama฀da฀ Engenharia฀do฀Vale฀do฀Silício.฀Ele฀atuou฀no฀Comitê฀Consultivo฀de฀Tecnologia฀da฀Informação฀do฀presidente฀dos฀Estados฀Unidos,฀como฀presidente฀da฀divisão฀de฀CS฀no฀departamento฀ EECS฀em฀Berkeley,฀como฀presidente฀da฀Associação฀de฀Pesquisa฀em฀Computação฀e฀como฀ Presidente฀da฀ACM.฀Este฀histórico฀levou฀a฀prêmios฀de฀Serviço฀Destacado฀da฀ACM฀e฀CRA. Em฀Berkeley,฀Patterson฀liderou฀o฀projeto฀e฀a฀implementação฀do฀RISC฀I,฀provavelmente฀o฀ primeiro฀computador฀com฀conjunto฀reduzido฀de฀instruções฀VLSI,฀e฀a฀fundação฀da฀arquitetura฀comercial฀SPARC.฀Ele฀foi฀líder฀do฀projeto฀Arrays฀Redundantes฀de฀Discos฀Baratos฀ (Redundant฀Array฀of฀Inexpensive฀Disks฀–฀RAID),฀que฀levou฀a฀sistemas฀de฀armazenamento฀ v vi Sobre os Autores coniáveis฀para฀muitas฀empresas.฀Ele฀também฀se฀envolveu฀no฀projeto฀Rede฀de฀Workstations฀(Network฀of฀Workstations฀–฀NOW),฀que฀levou฀à฀tecnologia฀de฀clusters฀usada฀ pelas฀empresas฀de฀Internet฀e,฀mais฀tarde,฀à฀computação฀em฀nuvem.฀Esses฀projetos฀valeram฀ três฀prêmios฀de฀dissertação฀da฀ACM.฀Seus฀projetos฀de฀pesquisa฀atuais฀são฀o฀Laboratório฀ Algoritmo-Máquina-Pessoas฀e฀o฀Laboratório฀de฀Computação฀Paralela,฀onde฀ele฀é฀o฀diretor.฀ O฀objetivo฀do฀Laboratório฀AMP฀é฀desenvolver฀algoritmos฀de฀aprendizado฀de฀máquina฀escaláveis,฀modelos฀de฀programação฀amigáveis฀para฀computadores฀em฀escala฀de฀depósito฀e฀ ferramentas฀de฀crowd-sourcing฀para฀obter฀rapidamente฀insights฀valiosos฀de฀muitos฀dados฀ na฀nuvem.฀O฀objetivo฀do฀laboratório฀Par฀é฀desenvolver฀tecnologias฀para฀entregar฀softwares฀ escaláveis,฀portáveis,฀eicientes฀e฀produtivos฀para฀dispositivos฀pessoais฀móveis฀paralelos. 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,