Estructura Y Diseno De Computad - Patterson, David A.; Hennessy, .pdf [z06o9yn9dj0x]

Estructura Y Diseno De Computad - Patterson, David A.; Hennessy, .pdf

  • Uploaded by: Sasmir
  • 0
  • 0
  • December 2020
  • PDF

This document was uploaded by user and they confirmed that they have the permission to share it. If you are author or own the copyright of this book, please report to us by using this DMCA report form. Report DMCA


Overview

Download & View Estructura Y Diseno De Computad - Patterson, David A.; Hennessy, .pdf as PDF for free.

More details

  • Words: 374,521
  • Pages: 914
ESTRUCTURA Y DISEÑO DE COMPUTADORES LA INTERFAZ HARDWARE / SOFTWARE

DAVID A. PATTERSON JOHN L. HENNESSY

Cuarta edición original

1

Datos de referencia de

MIPS

NÚCLEO DEL REPERTORIO DE INSTRUCCIONES NOMBRE

Suma Suma inmediata Suma inm. sin signo Suma sin signo And And inmediato Salto si igual

MNE- FORMO- MATÉC- TO NICO add R addi I addiu I addu R and R andi I beq I

Salto si distinto

bne

I

Salto incondicional Saltar y enlazar

j

J J

jal

OPERACIÓN (en verilog)

R[rd] = R[rs] + R[rt] R[rt] = R[rs] + ExtSignInm R[rt] = R[rs] + ExtSignInm R[rd] = R[rs] + R[rt] R[rd] = R[rs] & R[rt] R[rt] = R[rs] & ExtSignInm if(R[rs] = R[rt] PC = PC + 4 + DirSalto if(R[rs] != R[rt] PC = PC + 4 + DirSalto PC = DirSaltoInc R[31] = PC +8; PC = DirSaltoInc PC = R[rs] R[rt] = {24`b0, M[R[rs] + ExtSignInm](7:0) R[rt] = {16`b0, M[R[rs] + ExtSignInm](15:0) R[rt] = M[Rs[rs] + ExtSignInm] R[rt] = {inm, 16`b0} R[rt] = M[Rs[rs] + ExtSignInm] R[rd] = ~(R[rs] | R[rt]) R[rd] = R[rs] | R[rt] R[rd] = R[rs] | ExtCeroInm R[rd] = (R[rs] < R[rt]) ? 1:0 R[rd] = (R[rs] < ExtSignInm) ? 1:0 R[rd] = (R[rs] < ExtSignInm) ? 1:0 R[rd] = (R[rs] < R[rt]) ? 1:0

COD OP/ FUNC (Hex) 1 0/20hex 1,2 8hex 2 9hex 0/21hex 0/24hex 3 chex 4 4hex 4

5hex

5 5

2hex 3hex

jr R 0/08hex Salto con registro I 2 24hex Carga de un byte sin lbu signo lhu I 2 25hex Carga de media palabra sin signo ll I 2,7 30hex Carga enlazada lui I fhex Carga superior inm. I 2 23hex Carga de una palabra lw nor R 0/27hex Nor or R 0/25hex Or ori I 3 dhex Or inmediato slt R 0/2ahex Fijar si menor que slti I 2 ahex Fijar si menor que inm. sltiu I 2,6 bhex Fijar si menor que inm. sin signo 6 0/2bhex Fijar si menor que sin sltu R signo sll R R[rd] = R[rt] << desplaz 0/00hex Desplazamiento lógico a la izquierda srl R R[rd] = R[rt] >> desplaz 0/02hex Desplazamiento a lógico a la derecha I M[R[rs] + ExtSignInm](7:0) = 2 28hex Almacenamiento de sb R[rt](7:0) un byte sc I M[R[rs] + ExtSignInm] = R[rt]; 2,7 38hex Almacenamiento R[rt] =(atomic) ? 1:0 condicional I M[R[rs] + ExtSignInm](15:0) = 2 29hex Almacenamiento de sh R[rt](15:0) media palabra I M[R[rs] + ExtSignInm] = R[rt] 2 2bhex Almacenamiento de sw una palabra sub R R[rd] = R[rs] - R[rt] 1 0/22hex Resta subu R R[rd] = R[rs] - R[rt] 0/23hex Resta sin signo (1) Puede producirse una excepción de desbordamiento (2) ExtSignImn = {16{inmediato[15], inmediato} (3) ExtCeroInm = {16{1b'0}, inmediato} (4) DirSalto = {14{inmediato[15], inmediato, 2`b0} (5) DirSaltoCond = {PC+4[31:28], direccion, 2`b0} (6) Los operandos se consideran números sin signo (7) Pareja atómica comprobar y fijar; R[rt]=1si pareja atómica 0 si no atómica

FORMATOS BÁSICOS DE INSTRUCCIÓN R

cod oper 31

I

cod oper 31

R

26 25

cod oper 31

rs

26 25

26 25

rt 21 20

rs

rd 16 15

desplaz 11 10

rt 21 20

func 6 5

0

inmediato 16 15

2 NÚCLEO ARITMÉTICO DEL REPERTORIO DE INSTRUCCIONES NOMBRE MNE- FOR- OPERACIÓN COD OP/ MOMAFMT/FT/ TÉC- TO FUNC NICO (Hex) bclt FI If (FPcond) PC = PC + 4 + DirSalto 4 11/8/1/-Salto si FP cierto bclf FI If (FPcond) PC = PC + 4 + DirSalto 4 11/8/0/-Salto si FP falso div R Lo = R[rs]/R[rt]; Hi = R[rs]%R[rt] 0/--/--/1a División divu R Lo = R[rs]/R[rt]; Hi = R[rs]%R[rt] 6 11/--/--/1b División sin signo add.s FR F[fd] = F[fs] + F[ft] 11/10/--/0 Suma FP simple add.d FR {F[fd],F[fd+1]} = {F[fs], F[fs+1]} 11/11/--/0/ Suma FP doble + {F[ft], F[ft+1]} FPcond = (F[fs] op F[ft]) ? 1:0 11/10/--/y Comparación FP simple c.x.s* FR Fpcond = ({F[fs], F[fs+1]} op 11/11/--/y Comparación FP doble c.x.d* FR {F[ft], F[ft+1]} ? 1:0 *(x es eq, lt o le) (op es ==, < o <=) (y es 32, 3c o 3e) div.s FR F[fd] = F[fs]/F[ft] 11/10/--/3 División FP simple div.d FR {F[fd],F[fd+1]} = {F[fs], F[fs+1]}/ 11/11/--/3 División FP doble {F[ft], F[ft+1]} F[fd] = F[fs] * F[ft] 11/10/--/2 Multiplicación FP simple mul.s FR {F[fd],F[fd+1]} = {F[fs], F[fs+1]} * 11/11/--/2 Multiplicación FP doble mul.d FR {F[ft], F[ft+1]} sub.s FR F[fd] = F[fs] - F[ft] 11/10/--/1 Resta FP simple sub.d FR {F[fd],F[fd+1]} = {F[fs], F[fs+1]} 11/11/--/1 Resta FP doble {F[ft], F[ft+1]} lwcl I F[rt] = M[R[rs] + ExtSignInm] 2 31/--/--/-Carga FP simple ldcl 2 35/--/--/-I F[rt] = M[R[rs] + ExtSignInm] Carga FP doble F[rt+1] = M[R[rs] + ExtSignInm +4] mfhi R R[rd] = Hi 0/--/--/10 Mover de parte alta mflo R R[rd] = Lo 0/--/--/12 Mover de parte baja mfc0 R R[rd] = CR[rs] 10/0/--/0 Mover de control mult R {Hi, Lo} = R[rs] * R[rt] 0/--/--/18 Multiplicación {Hi, Lo} = R[rs] * R[rt] 6 0/--/--/19 Multiplicación sin signo multu R Desplazamiento R R[rd] = R[rt ] >>> desplaz 0/--/--/3 aritmético a la derecha sra Almacenamiento de FP I M[R[rs]+ExtSignInm] = F[rt] 2 39/--/--/-simple swcl M[R[rs]+ExtSignInm] = F[rt]; Almacenamiento de FP I M[R[rs]+ExtSignInm+4] = F[rt+1] 2 3d/--/--/-doble sdcl FORMATOS DE INSTRUCCIÓN PUNTO FLOTANTE FR cod oper fmt ft fs 31

FI

26 25

cod oper 31

0

Copyright 2005 by Elsevier, Inc., All rights reserved. From Patterson and Hennessy, Computer Organization and Design, 3rd ed.

fmt

26 25

16 15

11 10

ft 21 20

fd

func 6 5

0

inmediato 16 15

0

REPERTORIO DE PSEUDOINSTRUCCIONES NOMBRE Salto si menor que Salto si mayor que Salto si menor que o igual Salto si mayor que o igual Carga inmediata Mover

MNEMOTÉCNICO OPERACIÓN blt If (R[rs] < R[rt]) PC = etiqueta bgt If (R[rs] > R[rt]) PC = etiqueta ble If (R[rs] <= R[rt]) PC = etiqueta bge If (R[rs] >= R[rt]) PC = etiqueta li R[rd] = inmediato move R[rd] = R[rs]

NOMBRE DE REGISTRO, NÚMERO, USO Y CONVENIO DE LLAMADA NOMBRE NÚMERO $zero $at $v0-$v1

0 1 2-3

$a0-$a3 $t0-$t7 $s0-$s7 $t8-$t9 $k0-$k1

4-7 8 - 15 16 - 23 24 - 25 26 - 27

0

dirección

21 20

$gp $sp $fp $ra

28 29 30 31

USO Valor constante 0 Ensamblador temporal Valores de resultado de funciones y evaluación de expresiones Argumentos Temporales Temporales almacenados Temporales Reservados para el núcleo del Sistema Operativo Puntero global Puntero de pila Puntero de marco Dirección de retorno

¿SE CONSERVA EN UNA LLAMADA? No disponible No No No No Sí No No Sí Sí Sí Sí

CÓDIGOS DE OPERACIÓN, CONVERSIÓN DE BASE, SÍMBOLOS ASCII MIPS MIPS cod oper cod oper (31:26) (31:26) (1) j jal oeq bne blez bgtz add i addiu slti sltiu andi ori xori lui (2)

Ib lh lwl lw lbu lhu lwr sb sh sw

swr cache 11 lwcl lwc2 pref

ldcl ldc2 sc swcl swc2

sdcl sdc2

sil

(2)MIPS func (5:0) add.f sub.f mul.f div.f

Binario

00 0000 00 0001 srl 00 0010 sra 00 0011 sllv sqrt.f 00 0100 abs.f 00 0101 srlv mov.f 00 0110 srav neg.f 00 0111 jl 00 1000 jslr 00 1001 movz 00 1010 movn 00 1011 syscall round.w.f 00 1100 break trunc.w.f 00 1101 cell.v.f 00 1110 sync floor.w.f 00 1111 rafhi 01 0000 mthi 01 0001 mflo movz.f 01 0010 mtlo movn.f 01 0011 01 0100 01 0101 01 0110 01 0111 mult 01 1000 mul tu 01 1001 div 01 1010 divu 01 1011 01 1100 01 1101 01 1110 01 1111 add cvt.s.f 10 0000 addu cvt.d.f 10 0001 sub 10 0010 subu 10 0011 and cvt. w.f 10 0100 or 10 0101 xor 10 0110 nor 10 0111 10 1000 10 1001 slt 10 1010 sltu 10 1011 10 1100 10 1101 10 1110 10 1111 tge c.f.f 11 0000 tgeu c.un.f 11 0001 tlt c.eq.f 11 0010 tltu c.ueq.f 11 0011 teq c,olt.f 11 0100 c.ult.f 11 0101 tne c.ole.f 11 0110 c.ule.f 11 0111 c.sf.f 11 1000 c.ngle.f 11 1001 c.seq.f 11 1010 c.ng1.f 11 1011 c.lt.f 11 1100 c.nge.f 11 1101 c.le.f 11 1110 c.ngt.f 11 1111

3

Deci- Hexa- Carác- Deci- Hexa- Carácmal deciter mal deciter mal ASCII mal ASCII 0 0 NUL 64 40 @ 1 1 SOH 65 41 A 2 2 STX 66 42 B 3 3 ETX 67 43 C 4 4 EOT 68 44 D 5 5 BNQ 69 45 E 6 6 ACK 70 46 F 7 7 BEL 71 47 G 8 8 BS 72 48 H 9 9 HT 73 49 I 10 a LF 74 4a J 11 b VT 75 4b K 12 c FF 76 4c L 13 d CR 77 4d M 14 e SO 78 4e N 15 f SI 79 4f O 10 10 DLE 80 50 P 17 11 DC1 81 51 Q 18 12 DC2 82 52 R 19 13 DC3 83 53 S 20 14 DC4 84 54 T 21 15 NAK 85 55 U 22 16 SYN 86 56 V 23 17 ETB 87 57 W 24 18 CAN 88 58 X 25 19 EM 89 59 Y 26 1a SUB 90 5a Z 27 1b ESC 91 5b [ 28 1e FS 92 5c \ 29 1d GS 93 5d ] 30 1e RS 94 5e ^ 31 1f US 95 5f 32 20 Space 96 60 ‘ 33 21 ! 97 61 a 34 22 98 62 b 35 23 # 99 63 c 36 24 $ 100 64 d 37 25 % 101 65 c 38 26 & 102 66 f 39 27 ‘ 103 67 g 40 28 ( 104 68 h 41 29 ) 105 69 i 42 2a * 106 6a j 43 2b + 107 6b k 44 2c , 108 6c 1 45 2d 109 6d m 46 2e . 110 6e n 47 2f / 111 6f o 48 30 0 112 70 p 49 31 1 113 71 q 50 32 2 114 72 r 51 33 3 115 73 s 52 34 4 116 74 t 53 35 5 117 75 u 54 36 6 118 76 v 55 37 7 119 77 w 56 38 8 120 78 x 57 39 9 121 79 y 58 3a : 122 7a z 59 3b ; 123 7b { 60 3c < 124 7c 61 3d = 125 7d } 62 3e > 126 7e 63 3f ? 127 7f DEL

(1) cod oper(31:26) == 0 (2) cod oper(31:26) ==17dec (11hex); si fmt(25:21)==16dec (10hex) f=s (simple) si fmt(52:21)==17dec (11hex) f=d (doble)

Símbolos IEEE 754 Fracción Objeto 0 ±0 |0 ±Denormal Cualquiera ±Número punto flotante =0 ±h |0 NaN

Exponente 0 0 1 a MAX – 1 MAX MAX

(–1)5 × (1 + Fracción) × 2(exponente – sesgo) donde Sesgo de Precisión Simple = 127 Sesgo de Precisión Doble = 1023

S.P.MAx = 255. D.P.MAX = 2047 Formatos de Precisión Simple y Precisión Doble de IEEE S 31

Exponente 30

S 63

Fracción 23 22

0

Exponente

Fracción

62

52 51

0

ASIGNACIÓN DE MEMORIA

$sp

CUADRO DE PILA Direcciones Argumento 6 de memoria Argumento 5 más altas Registros salvados La pila Variables locales crece

Pila

7fff fffehex

$fp $gp

1000 8000hex 1000 0000hex

pc

Datos dinámicos Datos estáticos Texto

0040 0000hex

$sp

Direcciones de memoria más bajas

Reservado

0hex

ALINEAMIENTO DE DATOS Doble Palabra Palabra Palabra Media palabra Media palabra Media palabra Media palabra Byte Byte Byte Byte Byte Byte Byte Byte 1

0

2

3

4

5

6

7

Valor de los tres bits menos significativos del byte de la dirección (Big Endian)

REGISTROS DE CONTROL DE EXCEPCIONES: CAUSA Y ESTADO B D

Máscara de Interrupción

31

15

Código de Excepción 8

6

2

Interrupción Pendiente 15

U M 8

E I L E

4

1

0

BD: Retraso de Salto (Branch Delay); UM: Modo Usuario (User Mode); EL: Nivel de Excepción (Excepción Level); IE: Habilitación de Interrupción (Interrupt Enable) CÓDIGOS DE EXCEPCIÓN Nº Nombre Causa de la excepción 0 Int Interrupción (hardware) 4

AdEL

5

AdES

6

IBE

7

DBE

8

Sys

Excepción de Error de Dirección (carga o búsqueda de instrucción) Excepción de Error de Dirección (almacenamiento) Error de Bus en Búsqueda de Instrucción Error de Bus en Carga o Almcenamiento Excepción de Llamada al Sistema

Nº Nombre Causa de la excepción 9 Bp Excepción de Punto de Ruptura 10 RI Instrucción de Excepción Reservada 11

CpU

12

Ov

13

Tr

Coprocesador no Implementado Excepción de Desbordamiento Aritmético Trap

15

FPE

Excepción de Punto Flotante

TAMAÑOS DE PREFIJOS (10x para discos y comunicaciones; 2x para memoria) TAMAÑO PREFIJO

103, 210

Kilo-

106, 220

Mega-

9

10 , 2 Copyright 2005 by Elsevier, Inc., All rights reserved. From Patterson and Hennessy, Computer Organization and Design, 3rd ed.

4

ESTÁNDAR DE PUNTO FLOTANTE IEEE 754

12

30 40

10 , 2

TAMAÑO PREFIJO

TAMAÑO PREFIJO

1015, 250 Peta1018, 260 Exa-

10–3

mili-

10–15

femto-

10–6

micro-

10–18

atto-

Giga-

21

70

10 , 2

Zetta-

Tera-

24

80

Yotta-

10 , 2

10

–9

nano-

10

–12

pico-

TAMAÑO PREFIJO

10

–21

zepto-

10

–24

yocto-

El símbolo de cada prefijo es justamente su primera letra, excepto R que se utiliza para micro.

Estructura y diseño de computadores L A

I N T E R FA Z

S O F T W A R E / H A R D W A R E

TRADUCCIÓN DE LA CUARTA EDICIÓN EN LENGUA INGLESA

Estructura y diseño de computadores L A

I N T E R FA Z

S O F T W A R E / H A R D W A R E

TRADUCCIÓN DE LA CUARTA EDICIÓN EN LENGUA INGLESA

David A. Patterson University of California, Berkeley John L. Hennessy Stanford University Con contribuciones de Perry Alexander The University of Kansas

David Kaeli Northeastern University

Kevin Lim Hewlett-Packard

Peter J. Ashenden Ashenden Designs Pty Ltd

Nicole Kaiyan University of Adelaide

John Nickolls NVIDIA

Javier Bruguera Universidade de Santiago de Compostela

David Kirk NVIDIA

John Oliver Cal Poly, San Luis Obispo

Jichuan Chang Hewlett-Packard

James R. Larus Microsoft Research

Milos Prvulovic Georgia Tech

Matthew Farrens University of California, Davis

Jacob Leverich Hewlett-Packard

Partha Ranganathan Hewlett-Packard

Barcelona - Bogotá - Buenos Aires - Caracas - México

Registro bibliográfico (ISBD) PATTERSON, DAVID A. [Computer Organization and Design. Español] Estructura y diseño de computadores / David A. Patterson, John L. Hennessy; versión española por: Dr. Javier Díaz Bruguera. –Barcelona : Reverté. D.L. 2011 XXV, 703 p., [184] p. : il. col. ; 24 cm Ed. orig.: Computer organization and design: the hardware/software interface. 4.ª ed. Burlington: Elsevier Inc., cop. 2007. – Índice. DL B-15281-2011. – ISBN 978-84-291-2620-4 1. Estructura y diseño de computadores. I. Hennessy, John L., coaut. II. Díaz Bruguera, Javier, trad. III. Título.

Título de la obra original: Computer Organization and Design. The Hardware / Software Interface. Fourth Edition

Edición original en lengua inglesa publicada por: ELSEVIER INC of 200 Wheeler Road, 6th floor, Burlington, MA 01803, USA Copyright © 2009 by Elsevier Inc. All Rights Reserved

Edición en español: © Editorial Reverté, S. A., 2011

Edición en papel ISBN: 978-84-291-2620-4

Edición e-book (PDF) ISBN: 978-84-291-9418-0

Versión española por: Prof. Dr. Javier Díaz Bruguera Catedrático de Universidad en el área de arquitectura y tecnología de computadores Universidad de Santiago de Compostela Maquetación: REVERTÉ-AGUILAR, SL

Propiedad de: EDITORIAL REVERTÉ, S. A. Loreto, 13-15, Local B 08029 Barcelona – España Tel: (34) 93 419 33 36 Fax: (34) 93 419 51 89 [email protected] www.reverte.com Reservados todos los derechos. La reproducción total o parcial de esta obra, por cualquier medio o procedimiento, comprendidos la reprografía y el tratamiento informático, queda rigurosamente prohibida, salvo excepción prevista en la ley. Asimismo queda prohibida la distribución de ejemplares mediante alquiler o préstamo públicos, la comunicación pública y la transformación de cualquier parte de esta publicación (incluido el diseño de la cubierta) sin la previa autorización de los titulares de la propiedad intelectual y de la Editorial. La infracción de los derechos mencionados puede ser constitutiva de delito contra la propiedad intelectual (arts. 270 y siguientes del Código Penal). El Centro Español de Derechos Reprográficos (CEDRO) vela por el respeto a los citados derechos. # 1361

A Linda, que ha sido, es y siempre será el amor de mi vida

Elogios a Computer Organization and Design. The Hardware / Software Interface, fourth edition “Patterson y Hennessy no sólo mejoran la pedagogía del material tradicional sobre procesadores segmentados y jerarquía de memoria, sino que también extienden enormemente la cobertura de los sistemas multiprocesador para incluir arquitecturas emergentes como los procesadores multinúcleo y las GPUs. La cuarta edición de Estructura y Diseño de Computadores es un nuevo punto de referencia con el cual deben compararse los restantes libros sobre arquitectura de computadores.” —David A. Wood, University of Wisconsin-Madison “Patterson y Hennessy han mejorado enormemente lo que ya era el estándar de referencia de los libros de texto. En un campo en continua y rápida evolución como el de la arquitectura de los computadores, han entrelazado un número impresionante de casos reales y cuestiones actuales en un marco de fundamentos suficientemente comprobados.” —Fer Chong, University of California at Santa Barbara “Desde la publicación de la primera edición en 1994, Estructura y diseño de computadores ha iniciado en el campo de la arquitectura de los computadores a una generación de estudiantes de informática e ingeniería. Ahora, muchos de aquellos estudiantes son los líderes en este campo. La tradición continúa en la universidad, porque los profesores utilizan las últimas ediciones del libro que los inspiró para captar a la siguiente generación. Con la cuarta edición, los lectores se preparan para la próxima era de la computación.” —David I. August, Princeton University

“La nueva cobertura de los sistemas multiprocesador y el paralelismo está a la altura de este excelente clásico. Los nuevos temas se introducen de forma progresiva y bien argumentada, y además se proporcionan muchos ejemplos y detalles extraídos del hardware actual.” —John Greiner, Rice University “A medida que la arquitectura de los computadores se mueve de los monoprocesadores a los sistemas multinúcleo, los entornos de programación paralela usados para aprovecharlos serán un punto clave para el éxito de estos nuevos sistemas. En los sistemas multinúcleo, la interfaz entre el hardware y el software adquiere una importancias especial. Esta nueva edición de Estructura y diseño de computadores es obligatoria para cualquier estudiante que quiera comprender la arquitectura de los multinúcleo, incluida la interfaz entre la programación y la arquitectura.” —Jesse Fang, Director of Programming System Lab at Intel “La cuarta edición de Estructura y diseño de computadores sigue mejorando los altos estándares de calidad de las ediciones anteriores. Los nuevos contenidos, tendencias que están cambiando los ordenadores incluyendo los multinúcleo, memorias flash, GPUs, etc., hacen de esta edición un libro que todos aquellos que hemos crecido con las anteriores ediciones debemos leer.” —Parthasarathy Ranganathan, Principal Research Scientist, HP Labs

A G R A D E C I M I E N T O S Figuras 1.7 y 1.8. Gentileza de Other World Computing (www.macsales.com). Figuras 1.9, 1.19 y 5.37. Gentileza de AMD. Figura 1.10. Gentileza de Storage Technology Corp. Figuras 1.10.1, 1.10.2 y 4.15.2. Gentileza del Charles Babbage. Institute, University of Minnesota Libraries, Minneapolis. Figuras 1.10.3, 4.15.1, 4.15.3, 5.12.3 y 6.14.2. Gentileza de IBM. Figura 1.10.4. Gentileza de Cray Inc. Figura 1.10.5. Gentileza de Apple Computer Inc.

Figura 1.10.6. Gentileza del Computer History Museum. Figuras 5.12.1 y 5.12.2. Gentileza de Museum of Science. Boston. Figura 5.12.4. Gentileza de MIPS Technologies Inc. Figuras 6.15, 6.16 y 6.17. Gentileza de Sun Microsystems Inc. Figura 6.4. © Peg Skorpinski. Figura 6.14.1. Gentileza de Computer Museum of America. Figura 6.14.3. Gentileza de Commercial Computing Museum. Figuras 7.13.1. Gentileza de NASA Ames Research Center.

Contenido Prefacio xiii C A P Í T U L O S

1

Abstracciones y tecnología de los computadores Introducción 3 Bajo los programas 10 Bajo la cubierta 13 Prestaciones 26 El muro de la potencia 39 El gran cambio: el paso de monoprocesadores a multiprocesadores 41 1.7 Casos reales: fabricación y evaluación del AMD Opteron x4 1.8 Falacias y errores habituales 51 1.9 Conclusiones finales 54 1.10 Perspectiva histórica y lecturas recomendadas 55 1.11 Ejercicios 56

2

1.1 1.2 1.3 1.4 1.5 1.6

2

Instrucciones: el lenguaje del computador 2.1 2.2 2.3 2.4 2.5 2.6 2.7 2.8 2.9 2.10 2.11 2.12 2.13

44

74

Introducción 76 Operaciones del hardware del computador 77 Operandos del hardware del computador 80 Números con signo y sin signo 87 Representación de instrucciones en el computador 94 Operaciones lógicas 102 Instrucciones para la toma de decisiones 105 Apoyo a los procedimientos en el hardware del computador 112 Comunicarse con la gente 122 Direcciones y direccionamiento inmediato MIPS para 32 bits 128 Paralelismo e instrucciones: sincronización 137 Traducción e inicio de un programa 139 Un ejemplo de ordenamiento en C para verlo todo junto 149

Nota importante: En la presente edición en castellano, los contenidos del CD incluidos en la edición original son accesibles (en lengua inglesa) a través de la página web www.reverte.com/microsites/pattersonhennessy. Aunque en la presente edición no se proporciona un CD-ROM físico, a lo largo de todo el texto se menciona el CD y se utiliza el icono que lo representa para hacer referencia a su contenido.

viii

Contenido

2.14 2.15 2.16 2.17 2.18 2.19 2.20 2.21

3

Aritmética para computadores 3.1 3.2 3.3 3.4 3.5 3.6 3.7 3.8 3.9 3.10 3.11

4

Tablas frente a punteros 157 Perspectiva histórica y lecturas recomendadas Caso real: instrucciones ARM 161 Casos reales: instrucciones x86 165 Falacias y errores habituales 174 Conclusiones finales 176 Perspectiva histórica y lecturas recomendadas Ejercicios 179

4.11 4.12

4.13 4.14 4.15 4.16

179

222

Introducción 224 Suma y resta 224 Multiplicación 230 División 236 Punto flotante 242 Paralelismo y aritmética del computador: asociatividad Caso real: punto flotante en el x86 272 Falacias y errores habituales 275 Conclusiones finales 280 Perspectiva histórica y lecturas recomendadas 283 Ejercicios 283

El procesador 4.1 4.2 4.3 4.4 4.5 4.6 4.7 4.8 4.9 4.10

161

270

298

Introducción 300 Convenios de diseño lógico 303 Construcción de un camino de datos 307 Esquema de una implementación simple 316 Descripción general de la segmentación 330 Camino de datos segmentados y control de la segmentación 344 Riesgos de datos: anticipación frente a bloqueos 363 Riesgos de control 375 Excepciones 384 Paralelismo y paralelismo a nivel de instrucciones avanzado 391 Casos reales: El pipeline del AMD Opteron X4 (Barcelona) 404 Tema avanzado: una introducción al diseño digital utilizando un lenguaje de descripción hardware para describir y modelar un pipeline y más figuras sobre segmentación 406 Falacias y errores habituales 407 Conclusiones finales 408 Perspectiva histórica y lecturas recomendadas 409 Ejercicios 409

Contenido

5

Grande y rápida: aprovechamiento de la jerarquía de memoria 450 5.1 5.2 5.3 5.4 5.5 5.6 5.7 5.8 5.9 5.10 5.11 5.12 5.13 5.14

6

Almacenamiento y otros aspectos de la E/S 6.1 6.2 6.3 6.4 6.5 6.6 6.7 6.8 6.9 6.10 6.11 6.12 6.13 6.14 6.15

7

Introducción 452 Principios básicos de las caches 457 Evaluación y mejora de las prestaciones de la cache 457 Memoria virtual 492 Un marco común para las jerarquías de memoria 518 Máquinas virtuales 525 Utilización de una máquina de estados finitos para el control de una cache sencilla 529 Paralelismo y jerarquías de memoria: coherencia de cache 534 Material avanzado: implementación de controladores de cache 538 Casos reales: las jerarquías de memoria del AMD Opteron X4 (Barcelona) y del Intel Nehalem 539 Falacias y errores habituales 543 Conclusiones finales 547 Perspectiva histórica y lecturas recomendadas 548 Ejercicios 548

Introducción 570 Confiabilidad, fiabilidad y disponibilidad 573 Almacenamiento en disco 575 Almacenamiento Flash 580 Conexión entre procesadores, memoria y dispositivos de E/S 582 Interfaz de los dispositivos de E/S al procesador, memoria y sistema operativo 586 Medidas de las prestaciones de la E/S: ejemplos de discos y sistema de ficheros 596 Diseño de un sistema de E/S 598 Paralelismo y E/S: conjuntos redundantes de discos económicos 599 Casos reales: servidor Sun Fire x4150 606 Aspectos avanzados: redes 612 Falacias y errores habituales 613 Conclusiones finales 617 Perspectiva histórica y lecturas recomendadas 618 Ejercicios 619

Multinúcleos, multiprocesadores y clústeres 7.1 7.2 7.3 7.4

568

630

Introducción 632 La dificultad de crear programas de procesamiento paralelo 634 Multiprocesadores de memoria compartida 638 Clústeres y otros multiprocesadores de paso de mensajes 641

ix

x

Contenido

7.5 7.6 7.7 7.8 7.9 7.10 7.11 7.12 7.13 7.14 7.15

Ejecución multihilo en hardware 645 SISD, MIMD, SIMD, SPMD y procesamiento vectorial 648 Introducción a las unidades de procesamiento gráfico 654 Introducción a las topologías de redes para multiprocesadores 660 Programas de prueba para multiprocesadores 664 Roofline: un modelo de prestaciones sencillo 667 Casos reales: evaluación de cuatro multinúcleos con el modelo Roofline 675 Falacias y errores habituales 684 Conclusiones finales 686 Perspectiva histórica y lecturas recomendadas 688 Ejercicios 688

A P É N D I C E S

A

GPUs para gráficos y cálculo A.1 A.2 A.3 A.4 A.5 A.6 A.7 A.8 A.9 A.10 A.11

B

A-2

Introducción A-3 Arquitecturas del sistema de la GPU A-7 Programación de las GPU A-12 Arquitectura multiprocesador con ejecución multihilo A-25 Sistema de memoria paralelo A-36 Aritmética punto flotante A-41 Casos reales: NVIDIA GeForce 8800 A-46 Casos reales: Implementación de aplicaciones en la GPU A-55 Falacias y errores habituales A-72 Conclusiones finales A-76 Perspectiva histórica y lecturas recomendadas A-77

Ensambladores, enlazadores y el simulador SPIM B-2 B.1 B.2 B.3 B.4 B.5 B.6 B.7 B.8 B.9 B.10 B.11 B.12

Índice I-1

Introducción B-3 Ensambladores B-10 Enlazadores B-18 Cargador B-19 Utilización de la memoria B-20 Convenio de llamada a procedimiento B-22 Excepciones e interrupciones B-33 Entrada y salida B-38 SPIM B-40 Lenguaje ensamblador MIPS R2000 B-45 Conclusiones finales B-81 Ejercicios B-82

Contenido

C O N T E N I D O S

C

E L

C D

*

Conceptos clásicos de diseño lógico C-2 C.1 C.2 C.3 C.4 C.5 C.6 C.7 C.8 C.9 C.10 C.11 C.12 C.13 C.14

D

E N

Introducción C-3 Puertas, tablas de verdad y ecuaciones lógicas C-4 Lógica combinacional C-9 Lenguajes de descripción hardware C-20 Una unidad aritmético-lógica básica C-26 Sumas más rápidas: acarreo adelantado C-38 Relojes C-48 Elementos de memoria: biestables, cerrojos y registros Elementos de memoria: SRAM y DRAM C-58 Máquinas de estados finitos C-67 Metodologías de temporización C-72 Dispositivos programables por campos C-78 Conclusiones finales C-79 Ejercicios C-80

C-50

Implementación del control en hardware D-2 D.1 Introducción D-3 D.2 Implementación de unidades de control combinacionales D-4 D.3 Implementación de un control basado en máquinas de estados finitos D-8 D.4 Implementación de la función Estado-siguiente con un secuenciador D-22 D.5 Traducción de un microprograma a hardware D-28 D.6 Conclusiones finales D-32 D.7 Ejercicios D-33

E

Estudio de arquitecturas RISC para ordenadores de sobremesa, servidores y sistemas empotrados E-2 E.1 E.2 E.3 E.4

Introducción E-3 Modos de direccionamiento y formatos de instrucciones E-5 Instrucciones: El subconjunto del núcleo MIPS E-9 Instrucciones: Extensiones multimedia de los servidores y ordenadores de sobremesa RISC E-16

* Nota importante: En la presente edición en castellano, los contenidos del CD incluido en la edición original (en inglés) son accesibles a través de la página web www.reverte.com/microsites/pattersonhennessy. Aunque en la presente edición no se proporciona un CD-ROM físico, a lo largo de todo el texto se menciona el CD y se utiliza el icono que lo representa para hacer referencia a su contenido.

xi

xii

Contenido

E.5 E.6 E.7 E.8 E.9 E.10 E.11 E.12 E.13 E.14 E.15 E.16 E.17 Glosario

Instrucciones: Extensiones para procesado digital de señales de sistemas RISC empotrados E-19 Instrucciones: Extensiones habituales del núcleo MIPS E-20 Instrucciones específicas del MIPS-64 E-25 Instrucciones específicas del Alpha E-27 Instrucciones específicas del Sparc v.9 E-29 Instrucciones específicas del PowerPC E-32 Instrucciones específicas del PA-RISC 2.0 E-34 Instrucciones específicas del ARM E-36 Instrucciones específicas del Thumb E-38 Instrucciones específicas del SuperH E-39 Instrucciones específicas del M32R E-40 Instrucciones específicas del MIPS-16 E-40 Conclusiones finales E-43 G-1

Lecturas recomendadas FR-1

Prefacio Lo más bello que podemos experimentar es el misterio. Es la fuente de todo el arte verdadero y la ciencia. Albert Einstein, What I Believe, 1930

Sobre este libro Creemos que la enseñanza de la ciencia e ingeniería de computadores debería reflejar el estado actual de este campo, así como introducir los principios que dan forma a la computación. También opinamos que los lectores de cualquier especialidad de la computación necesitan conocer los paradigmas de organización que determinan las capacidades, prestaciones y, en definitiva, el éxito de los sistemas informáticos. La tecnología de los computadores moderna necesita que los profesionales de todas las especialidades de la informática conozcan el hardware y el software. La interacción entre estos dos aspectos a diferentes niveles ofrece, al mismo tiempo, un entorno para la compresión de los fundamentos de la computación. Independientemente de que su interés principal sea el hardware o el software, la informática o la electrónica, las ideas centrales de la estructura y el diseño del computador son las mismas. Por lo tanto, hemos centrado este libro en mostrar la relación entre el hardware y el software y en los conceptos básicos de los computadores actuales. El paso reciente de los monoprocesadores a los microprocesadores multinúcleo ha confirmado la solidez de esta perspectiva, establecida ya en la primera edición. El tiempo en el que los programadores podían ignorar este aviso y confiar en los arquitectos de ordenadores, diseñadores de compiladores e ingenieros electrónicos para que sus programas se ejecutasen cada vez más rápido sin introducir cambio alguno, ha pasado. Para que los programas se ejecuten más rápido debe introducirse paralelismo. El objetivo de muchos investigadores de introducir el paralelismo sin que los programadores tengan que preocuparse de la naturaleza paralela del hardware que están programando, todavía tardará muchos años en hacerse realidad. Según nuestra visión, durante al menos la próxima década, la mayoría de los programadores van a necesitar conocer la interfaz entre el hardware y el software para que sus programas se ejecuten eficientemente en los computadores paralelos. La audiencia de este libro incluye tanto a aquellos que, con poca experiencia en lenguaje ensamblador o diseño lógico, necesitan entender la organización básica de un computador, como a los que, con conocimientos de lenguaje ensamblador y/o diseño lógico, quieren aprender como se diseña un ordenador o entender como trabaja un sistema y por qué se comporta como lo hace.

xiv

Prefacio

Sobre el otro libro Algunos lectores seguramente están familiarizados con el libro Arquitectura de Computadores: un Enfoque Cuantitativo, conocido popularmente como Hennessy y Patterson. (Este libro, por el contrario, a menudo se llama Patterson y Hennessy). Nuestra motivación al escribir ese libro era describir los principios de la arquitectura de computadores haciendo uso de fundamentos sólidos de ingeniería y compromisos coste/prestaciones cuantitativos. Utilizamos un enfoque que combinaba ejemplos y medidas, basados en sistemas comerciales, para crear experiencias de diseño realistas. Nuestro objetivo fue demostrar que la arquitectura de los computadores se podía aprender con una metodología cuantitativa en lugar de un enfoque descriptivo. El libro estaba dirigido a los profesionales de la informática que querían adquirir un conocimiento detallado de los computadores. La mayoría de los lectores de este libro no tienen previsto convertirse en arquitectos de computadores. Sin embargo, las prestaciones y la eficiencia energética de los sistemas software, en el futuro, dependerán drásticamente de la adecuada comprensión de las técnicas básicas del hardware por parte de los diseñadores de software. De este modo, los diseñadores de compiladores, los diseñadores de sistemas operativos, programadores de bases de datos y la mayor parte de los ingenieros del software necesitan un conocimiento firme de los principios presentados en este libro. De manera similar, los diseñadores del hardware deben comprender claramente los efectos de su trabajo sobre las aplicaciones software. Por lo tanto, sabíamos que este libro tenía que ser mucho más que un subconjunto del material incluido en el libro Arquitectura de Computadores, y el material se revisó ampliamente para adecuarse a una audiencia diferente. Quedamos tan satisfechos del resultado que se revisaron las siguientes ediciones de Arquitectura de Computadores para eliminar la mayor parte del material de introducción; así, hay mucho menos solape entre los dos libros ahora que en las primeras ediciones.

Cambios en la cuarta edición Teníamos cinco objetivos principales para la cuarta edición de Estructura y diseño de computadores: dada la revolución multinúcleo de los microprocesadores, resaltar los aspectos paralelos del hardware y el software a lo largo del libro; racionalizar el material existente para hacer hueco a los aspectos relacionados con el paralelismo; mejorar la pedagogía en general; actualizar el contenido técnico para reflejar los cambios producidos en la industria desde la publicación de la tercera edición en 2004; restablecer la utilidad de los ejercicios en la era de internet. Antes de discutir estos objetivos con más detalle, echemos un vistazo a la tabla de la página siguiente. Muestra los caminos del hardware y el software a lo largo del libro. Los capítulos 1, 4, 5 y 7 están en ambos caminos, sin importar la experiencia o el enfoque. El capítulo 1 es un introducción nueva que incluye una discusión sobre la importancia de la potencia y como ha alentado el paso de los sistemas con un núcleo a los microprocesadores multinúcleo. Incluye también material sobre prestaciones y evaluación mediante programas de prueba, material que en la tercera edición estaba incluido en un capítulo aparte. El capítulo 2 es más probable que esté orientado hacia el hardware, pero es una lectura esencial para los lectores orientados al software, especialmente para

Prefacio

Capítulo o apéndice

Secciones

Enfoque software Enfoque hardware

1.1 a 1.9

1. Abstracciones y tecnología de los computadores

1.10 (Historia) 2.1 a 2.14 2.15 (Compiladores y java)

2. Instrucciones: el lenguaje del computador

2.16 a 2.19 2.20 (Historia)

E. Arquitecturas de repertorio de instrucciones RISC

E.1 a E.9 3.1 a 3.9

3. Aritmética para computadores

3.10 (Historia)

C. Conceptos básicos de diseño lógico

C.1 a C.13 4.1 (Visión general) 4.2 (Convenciones lógicas) 4.3 a 4.4 (Implementación simple) 4.5 (Introducción a segmentación) 4.6 (Camino de datos segmentado)

4. El procesador

4.7 a 4.9 (Riesgos, excepciones) 4.10 a 4.11 (Paralelo, caso real) 4.12 (Control segmentado en Verilog) 4.13 a 4.14 (Falacias) 4.15 (Historia)

D. Implementación del control en hardware

D.1 a D.6 5.1 a 5.8

5. Grande y rápida: aprovechamiento de la jerarquía de memoria

5.9 (Controlador de cache en Verilog) 5.10 a 5.12 5.13 (Historia) 6.1 a 6.10

6. Almacenamiento y otros aspectos de la E/S

6.11 (Redes) 6.12 a 6.13 6.14 (Historia)

7. Multinúcleo, multiprocesadores y clústeres A. Unidades de procesamiento gráfico B. Ensambladores, enlazadores

7.1 a 7.3 7.14 (Historia) A.1 a A.12 B.1 a B.12

y el simulador SPIM

Leer detenidamente

Leer si se dispone de tiempo

Revisar o leer

Leer por cultura

xv

Referencia

xvi

Prefacio

aquellos interesados en aprender más sobre compiladores y lenguajes de programación orientada a objetos. Incluye material del capítulo 3 de la tercera edición, de forma que toda la arquitectura MIPS, excepto las instrucciones de punto flotante, se describe ahora en un único capítulo. El capítulo 3 está dirigido a los lectores interesados en el diseño de un camino de datos o en aprender algo más sobre aritmética de punto flotante. Sin embargo, algunos lectores podrán saltarse este capítulo, bien porque no lo necesitan, bien porque es una revisión. El capítulo 4, donde se explica el procesador segmentado, es una combinación de dos capítulos de la tercera edición. Las secciones 4.1, 4.5 y 4.10 proporcionan una visión general para aquellos lectores interesados en los aspectos software. Sin embargo, constituye el material central para los interesados en los aspectos hardware; dependiendo de sus conocimientos previos, estos lectores pueden necesitar o querer leer antes el apéndice C sobre diseño lógico. El capítulo 6, sobre sistemas de almacenamiento, es de importancia crucial para los lectores con un enfoque software y los restantes lectores deberían leerlo también si disponen de tiempo suficiente. El último capítulo sobre multinúcleos, multiprocesadores y clústeres es mayoritariamente material nuevo y debería ser leído por todos. El primer objetivo fue hacer del paralelismo un ciudadano de primera clase en esta edición, cuando en la anterior edición era un capítulo aparte en el CD. El ejemplo más evidente es el capítulo 7. En este capítulo se introduce el modelo Roofline para la evaluación de las prestaciones y se muestra su validez con la evaluación de cuatro microprocesadores multinúcleo recientes con dos núcleos computacionales. Podría demostrarse que este modelo puede ser tan intuitivo para la evaluación de los multinúcleos como el modelo de las 3Cs para las caches. Dada la importancia del paralelismo, no sería inteligente esperar hasta el último capítulo para abordarlo, por lo tanto hay una sección sobre paralelismo en cada uno de los seis primeros capítulos: ■

Capítulo 1: Paralelismo y potencia. Se muestra como los límites impuestos por el consumo de potencia han forzado a los principales fabricantes a mirar hacia el paralelismo, y como el paralelismo ayuda en este problema.



Capítulo 2: Paralelismo e instrucciones: Sincronización. Esta sección analiza los bloqueos para variables compartidas, de forma específica las instrucciones MIPS carga enlazada y almacenamiento condicional.



Capítulo 3. Paralelismo y aritmética del computador: Asociatividad punto flotante. En esta sección se analizan los retos en precisión numérica y en las operaciones punto flotante.



Capítulo 4. Paralelismo y paralelismo a nivel de instrucciones avanzado. Trata el paralelismo a nivel de instrucciones (ILP) avanzado —superescalares, especulación, VLIW, desenrollamiento de lazos y OOO— así como la relación entre la profundidad del procesador segmentado y el consumo de potencia.



Capítulo 5. Paralelismo y jerarquías de memoria: Coherencia de cache. Introduce los protocolos de coherencia, consistencia y fisgoneo (snooping) de cache.



Capítulo 6. Paralelismo y E/S: Conjuntos redundantes de discos económicos (RAID). Describe los sistemas RAID como un sistema paralelo de E/S, así como un sistema ICO de alta disponibilidad.

Prefacio

El capítulo 7 concluye con varias razones para ser optimista con respecto a que esta incursión en el paralelismo debería tener más éxito que las que se realizaron anteriormente. Particularmente, estoy entusiasmado con la incorporación de un apéndice sobre Unidades de Procesamiento Gráfico escrito por el científico jefe de NVIDIA, David Kirk, y el líder del grupo de arquitectos, John Nicolls. En este sentido, el apéndice A es la primera descripción detallada de una GPU, un nuevo e interesante empuje para la arquitectura de los computadores. El apéndice articula los temas paralelos de esta edición para presentar un estilo de computación que permite al programador pensar en un sistema MIMD aunque el hardware ejecute los programas como un SIMD cuando es posible. Como las GPUs son baratas y están disponibles en casi cualquier sistema —incluso en computadores portátiles— y sus entornos de programación son de acceso libre, proporciona una plataforma hardware paralela con la que experimentar. El segundo objetivo ha sido racionalizar el libro para hacer sitio a nuevo material sobre paralelismo. El primer paso fue mirar con lupa todos los párrafos presentes en las tres ediciones anteriores para determinar si seguían siendo necesarios. Los cambios de grano grueso fueron la combinación de capítulos y la eliminación de temas. Mark Hill sugirió eliminar la implementación multiciclo de los procesadores y añadir, en su lugar, un controlador de cache multiciclo en el capítulo dedicado a la jerarquía de memoria. Este cambio permitió presentar el procesador en un único capítulo en lugar de en los dos capítulos de ediciones anteriores, mejorando el material sobre el procesador por omisión. Los contenidos del capítulo sobre prestaciones de la tercera edición se añadieron al primer capítulo. El tercer objetivo fue mejorar los aspectos pedagógicos del libro. El capítulo 1 es ahora más jugoso, e incluye material sobre prestaciones, circuitos integrados y consumo de potencia, y crea el marco para el resto del libro. Los capítulos 2 y 3 originalmente estaban escritos con un estilo evolutivo, comenzando con una arquitectura sencilla y terminando con la arquitectura completa del MIPS al final del capítulo 3. Sin embargo, este estilo pausado no le gusta a los lectores modernos. Por lo tanto, en esta edición se junta toda la descripción del repertorio de instrucciones enteras en el capítulo 2 —haciendo el capítulo 3 opcional para muchos lectores— y ahora cada sección es independiente, de modo que el lector no necesita ya leer todas la secciones anteriores. Así, el capítulo 2 es mejor como referencia ahora que en ediciones anteriores. El capítulo 4 está mejor organizado ahora porque se dedica un único capítulo al procesador, ya que la implementación multiciclo no es de utilidad actualmente. El capítulo 5 tiene una nueva sección sobre diseño de controladores de cache y el código Verilog para este controlador se incluye en una sección del CD.1 El CD-ROM que acompaña al libro, que se introdujo en la tercera edición, nos permitió reducir el coste del libro, que tenía menos páginas, y profundizar en algunos temas que eran de interés sólo para algunos lectores, pero no para todos. Desafortunadamente, 1Nota importante: En la presente edición en castellano, los contenidos del CD incluido en la edición original (en inglés) son accesibles a través de la página web www.reverte.com/microsites/pattersonhennessy. Aunque en la presente edición no se proporciona un CD-ROM físico, a lo largo de todo el texto se menciona el CD y se utiliza el icono que lo representa para hacer referencia a su contenido.

xvii

xviii

Prefacio

como consecuencia de nuestro entusiasmo por reducir el número de páginas del libro, el lector tenía que ir del libro al CD y del CD al libro más veces de las que le gustaría. Esto no ocurre en esta edición. Cada capítulo tiene la sección de Perspectivas históricas en el CD y, además, todos los ejercicios están en el libro impreso. Por lo tanto, en esta edición será muy raro que el lector tenga que ir del libro al CD y del CD al libro. Para aquellos que se sorprenden de que incluyamos un CD-ROM con el libro, la respuesta es sencilla: el CD contiene material que hemos considerado que debería ser de acceso fácil e inmediato sin importar el lugar en el que se encuentre el lector. Si usted está interesado en los contenidos avanzados o le gustaría leer una guía práctica de VHDL (por ejemplo), está en el CD, lista para que la utilice. El CD-ROM incluye también una característica que debería mejorar significativamente el acceso al material: un motor de búsqueda que permite hacer búsquedas de cualquier cadena de texto en el CD o en el libro. Si está buscando algún contenido que podría no estar incluido en el índice del libro, simplemente indicando el texto que se quiere buscar el motor devuelve el número de la página en la que aparece ese texto. Esta característica es muy útil y esperamos que usted haga uso de ella frecuentemente. Este es un campo en continua evolución, y como ocurre siempre que se lanza una nueva edición, uno de los objetivos prioritarios es actualizar los contenidos técnicos. El procesador Opteron X4 2356 (nombre en clave “Barcelona”) de AMD se utiliza como ejemplo guía a lo largo de todo el libro, concretamente para los capítulos 1, 4, 5 y 7. Los capítulos 1 y 6 añaden los resultados obtenidos con los programa de prueba para la evaluación del consumo de potencia de SPEC. El capítulo 2 incorpora una sección sobre la arquitectura ARM, que actualmente es la ISA (arquitectura del repertorio de instrucciones) de 32 bits más popular en todo el mundo. El capítulo 5 tiene una sección sobre Máquinas Virtuales, que están adquiriendo una importancia creciente. En el capítulo 5 se analizan detalladamente las medidas de rendimiento de la cache en el microprocesador multinúcleo Opteron X4 y se proporcionan algunos datos, menos detallados, sobre su rival, el procesador Nehalem de Intel, que no será anunciando antes de que se publique esta edición. En el capítulo 6 se describen por primera vez las memorias Flash así como un destacado servidor de Sun, que empaqueta 8 núcleos, 16 DIMMs y 8 discos en una única unidad 1U. También se incluyen resultados recientes sobre fallos de discos a largo plazo. El capítulo 7 cubre un gran número de temas relacionados con el paralelismo —ejecución multihilo, SIMD, procesamiento vectorial, modelos de prestaciones, programas de prueba, redes de multiprocesadores, entre otros— y describe tres microprocesadores multinúcleo, además del Opteron X4: Intel Xeon e5345 (Clovertown), IBM Cell QS20 y Sun Microsystems T2 5120 (Niagara). El objetivo final fue intentar que los ejercicios fuesen útiles a los profesores en esta era de Internet, porque los ejercicios que los estudiantes tienen que hacer en su propia casa han sido desde hace mucho tiempo una forma importante de aprender el material contenido en el libro. Desafortunadamente, hoy en día las soluciones se cuelgan en la web casi al mismo tiempo que el libro sale a la venta. Tenemos una propuesta en dos partes. Primero, colaboradores expertos han desarrollado ejercicios totalmente nuevos para todos los capítulos del libro. Segundo, la mayoría de los ejercicios tiene una descripción cualitativa apoyada en una tabla que proporciona varios parámetros cuantitativos alternativos que son necesarios para resolver las preguntas. El elevado número de ejercicios

Prefacio

unidos a la flexibilidad que tiene el profesor para introducir variaciones hará difícil que el estudiante pueda encontrar las soluciones en la red. Los profesores podrán cambiar estos parámetros cuantitativos como quieran, haciendo que aquellos estudiantes que confiaban en internet para encontrar las soluciones de un conjunto de ejercicios estático y fijo se sientan frustrados. Creemos que este enfoque es un nuevo valor añadido al libro; por favor, le pedimos que nos haga saber si este enfoque funciona correctamente, ¡tanto como estudiante o como profesor! Hemos mantenido elementos útiles de las anteriores ediciones del libro. Para conseguir que el libro se mejore como libro de referencia, mantenemos las definiciones de los nuevos términos en los márgenes de las páginas en las que aparecen por primera vez. Los elementos del libro que hemos llamado secciones “Comprender las prestaciones de los programas” ayudan al lector a comprender las prestaciones de sus programas y como mejorarlas, del mismo modo que los elementos llamados “Interfaz Hardware/Software” ayudan a entender las soluciones de compromiso que se adoptan en esta interfaz. Las secciones “Idea clave” se han mantenido para que el lector vea el bosque a pesar de los árboles. Las secciones “Autoevaluación” ayudan a confirmar la comprensión del material estudiado, ya que las respuestas se incluyen al final de cada capítulo. Esta edición incluye también la tarjeta de referencia MIPS, inspirada en la “Tarjeta Verde” del IBM System/360. Esta tarjeta ha sido actualizada y debería ser una referencia accesible al escribir programas en lenguaje ensamblador MIPS.

Apoyo a los profesores Hemos recopilado una gran cantidad de material de ayuda a los profesores que utilicen este libro en sus cursos. Tienen a su disposición soluciones a los ejercicios, puntos críticos de cada capítulo, figuras del libro, notas, dispositivas, etc., en la web de los editores www.reverte.com/microsites/pattersonhennessy

Conclusiones finales Si usted lee la sección de agradecimientos que viene a continuación, verá que hemos hecho un gran esfuerzo para corregir los posibles errores. Dado que un libro pasa por muchas etapas, hemos tenido la oportunidad de hacer aún más correcciones. Si usted descubre algún error que se nos ha resistido, por favor contacte con el editor a través de correo electrónico en la dirección [email protected] o por correo ordinario a la dirección de la página de copyright. Esta edición marca una ruptura en la colaboración entre Hennessy y Patterson, que empezó en 1989 y se mantuvo durante muchos años. La oportunidad de dirigir una de las mayores universidades del mundo implica que el Rector Hennessy no podrá asumir en adelante la responsabilidad de crear una nueva edición. El otro autor se siente como un malabarista que siempre ha trabajado con un socio y de repente es empujado a actuar en solitario. En consecuencia, todos aquellos incluidos en los agradecimientos y los colegas de Berkeley han jugado un papel aun más importante en darle forma a los contenidos de este libro. Sin embargo, esta vez hay un único autor al que culpar por el nuevo material que va a leer.

xix

xx

Prefacio

Agradecimientos de la cuarta edición Me gustaría dar las gracias a David Kirk, John Nicholls y sus colegas de NVIDIA (Michael Garland, John Montrym, Dough Voorhies, Lars Nyland, Erik Lindholm, Paulius Micikevicius, Massimiliano Fatica, Stuart Oberman y Vasily Volkov) por escribir el primer apéndice que analiza en profundidad las GPUs. Quiero expresar otra vez mi agradecimientoa Jim Larus de Microsoft Research por su buena disposición a contribuir como experto en programación en lenguaje ensamblador y por alentar a los lectores de este libro a utilizar el simulador que él mismo desarrolló y mantiene. También estoy muy agradecido por las contribuciones de muchos expertos que desarrollaron los nuevos ejercicio de esta edición. Escribir buenos ejercicios no es una tarea fácil y todos los colaboradores tuvieron que trabajar mucho y duro para plantear unos problemas exigentes y atractivos. ■

Capítulo 1: Javier Bruguera (Universidade de Santiago de Compostela)



Capítulo 2: John Oliver (Cal Poly, San Luis Obispo), con contribuciones de Nicole Kaiyan (University of Adelaide) y Milos Prvulovic (Georgia Tech)



Capítulo 3: Matthew Farrens (University of California, Davis)



Capítulo 4: Milos Prvulovic (Georgia Tech)



Capítulo 5: Jichuan Chang, Jacob Leverich, Kevin Lim y Partha Ranganathan (todos de Hewlett-Packard), con contribuciones de Nicole Kaiyan (University of Adelaide)



Capítulo 6: Perry Alexander (The University of Kansas)



Capítulo 7: David Kaeli (Northeastern University)

Peter Asheden llevó a cabo la hercúlea tarea de editar y evaluar todos los nuevos ejercicios. Además, incluso asumió la elaboración del CD que acompaña al libro y de las diapossitivas para las clases. Gracias a David August y Prakash Prabhu de Princenton University por su trabajo sobre los concursos de los capítulos que están disponibles para los profesores en la Web de los editores. Conté con el apoyo de mis colegas del Silicon Valley para la elaboración de gran parte del material técnico del libro: ■

AMD, por los detalles y medidas del Opteron X4 (Barcelona): William Brantley, Vasileios Liaskovitis, Chuck Moore y Brian Waldecker.



Intel, por la información prelanzamiento del Intel Nehalem: Faye Briggs.



Micron, por la información sobre memorias Flash del capítulo 6: Dean Klein.



Sun Microsystems, por las medidas de mezclas de instrucciones de los programas de prueba SPEC2006 en el capítulo 2 y los detalles y medidas del Sun Server x4150 en el capítulo 6: Yan Fisher, John Fowler, Darryl Gove, Paul Joyce, Shenik Mehta, Pierre Reynes, Dimitry Stuve, Durgam Vahia y David Weaver.

Prefacio



U.C. Berkeley, Krste Asanovic (que proporcionó la idea para la concurrencia software frente a paralelismo hardware del capítulo 7), James Demmel y Velvel Kahan (con sus comentarios sobre paralelismo y cálculos punto flotante), Zhangxi Tan (que diseñó en controlador de cache y escribió el código Verilog para el mismo en el capítulo 5), Sam Williams (que proporcionó el modelo Roofline y las medidas para los multinúcleos en el capítulo 7) y el resto de mis colegas en el Par Lab que hicieron sugerencias y comentarios para los distintos aspectos del paralelismo que se pueden encontrar en este libro.

Estoy agradecido a los muchos profesores que contestaron los cuestionarios enviados por los editores, revisaron nuestras propuestas y asistieron a los grupos de discusión para analizar y responder a nuestros planes para esta edición. Esto incluye a las siguientes personas: Grupo de discusión: Mark Hill (University of Wisconsin, Madison), E.J. Kim (Texas A&M University), Jihong Kim (Seoul National University), Lu Peng (Louisiana State University), Dean Tullsen (UC San Diego), Ken Vollmar (Missouri State University), David Wood (University of Wisconsin, Madison), Ki Hwan Yum (University of Texas, San Antonio); Revisiones: Mahmoud Abou-Nasr (Wayne State University), Perry Alexander (The University of Kansas), Hakan Aydin (George Mason University), Hussein Badr (State University of New York at Stony Brook), Mac Baker (Virginia Military Institute), Ron Barnes (George Mason University), Douglas Blough (Georgia Institute of Technology), Kevin Bolding (Seattle Pacifi c University), Miodrag Bolic (University of Ottawa), John Bonomo (Westminster College), Jeff Braun (Montana Tech), Tom Briggs (Shippensburg University), Scott Burgess (Humboldt State University), Fazli Can (Bilkent University), Warren R. Carithers (Rochester Institute of Technology), Bruce Carlton (Mesa Community College), Nicholas Carter (University of Illinois at Urbana-Champaign), Anthony Cocchi (The City University of New York), Don Cooley (Utah State University), Robert D. Cupper (Allegheny College), Edward W. Davis (North Carolina State University), Nathaniel J. Davis (Air Force Institute of Technology), Molisa Derk (Oklahoma City University), Derek Eager (University of Saskatchewan), Ernest Ferguson (Northwest Missouri State University), Rhonda Kay Gaede (The University of Alabama), Etienne M. Gagnon (UQAM), Costa Gerousis (Christopher Newport University), Paul Gillard (Memorial University of Newfoundland), Michael Goldweber (Xavier University), Georgia Grant (College of San Mateo), Merrill Hall (The Master’s College), Tyson Hall (Southern Adventist University), Ed Harcourt (Lawrence University), Justin E. Harlow (University of South Florida), Paul F. Hemler (Hampden-Sydney College), Martin Herbordt (Boston University), Steve J. Hodges (Cabrillo College), Kenneth Hopkinson (Cornell University), Dalton Hunkins (St. Bonaventure University), Baback Izadi (State University of New York—New Paltz), Reza Jafari, Robert W. Johnson (Colorado Technical University), Bharat Joshi (University of North Carolina, Charlotte), Nagarajan Kandasamy (Drexel University), Rajiv Kapadia, Ryan Kastner (University of California, Santa Barbara), Jim Kirk (Union University), Geoffrey S. Knauth (Lycoming College), Manish M. Kochhal (Wayne State), Suzan Koknar-Tezel (Saint Joseph’s University), Angkul Kongmunvattana (Columbus State University), April Kontostathis (Ursinus

xxi

xxii

Prefacio

College), Christos Kozyrakis (Stanford University), Danny Krizanc (Wesleyan University), Ashok Kumar, S. Kumar (The University of Texas), Robert N. Lea (University of Houston), Baoxin Li (Arizona State University), Li Liao (University of Delaware), Gary Livingston (University of Massachusetts), Michael Lyle, Douglas W. Lynn (Oregon Institute of Technology), Yashwant K Malaiya (Colorado State University), Bill Mark (University of Texas at Austin), Ananda Mondal (Clafl in University), Alvin Moser (Seattle University), Walid Najjar (University of California, Riverside), Danial J. Neebel (Loras College), John Nestor (Lafayette College), Joe Oldham (Centre College), Timour Paltashev, James Parkerson (University of Arkansas), Shaunak Pawagi (SUNY at Stony Brook), Steve Pearce, Ted Pedersen (University of Minnesota), Gregory D Peterson (The University of Tennessee), Dejan Raskovic (University of Alaska, Fairbanks) Brad Richards (University of Puget Sound), Roman Rozanov, Louis Rubinfi eld (Villanova University), Md Abdus Salam (Southern University), Augustine Samba (Kent State University), Robert Schaefer (Daniel Webster College), Carolyn J. C. Schauble (Colorado State University), Keith Schubert (CSU San Bernardino), William L. Schultz, Kelly Shaw (University of Richmond), Shahram Shirani (McMaster University), Scott Sigman (Drury University), Bruce Smith, David Smith, Jeff W. Smith (University of Georgia, Athens), Philip Snyder (Johns Hopkins University), Alex Sprintson (Texas A&M), Timothy D. Stanley (Brigham Young University), Dean Stevens (Morningside College), Nozar Tabrizi (Kettering University), Yuval Tamir (UCLA), Alexander Taubin (Boston University), Will Thacker (Winthrop University), Mithuna Thottethodi (Purdue University), Manghui Tu (Southern Utah University), Rama Viswanathan (Beloit College), Guoping Wang (Indiana-Purdue University), Patricia Wenner (Bucknell University), Kent Wilken (University of California, Davis), David Wolfe (Gustavus Adolphus College), David Wood (University of Wisconsin, Madison), Mohamed Zahran (City College of New York), Gerald D. Zarnett (Ryerson University), Nian Zhang (South Dakota School of Mines & Technology), Jiling Zhong (Troy University), Huiyang Zhou (The University of Central Florida), Weiyu Zhu (Illinois Wesleyan University). Me gustaría dar las gracias especialmente a la gente de Berkeley que dio una retroalimentación clave para el capítulo 7 y el apéndice A, que fueron las dos partes de este libro más complicadas de escribir: Krste Asanovic, Christopher Batten, Rastilav Bodik, Bryan Catanzaro, Jike Chong, Kaushik Data, Greg Giebling, Anik Jain, Jae Lee, Vasily Volkov y Samuel Williams. Un agradecimiento especial también para Mark Smotherman por sus múltiples revisiones para encontrar errores técnicos y de escritura, que contribuyeron a mejorar significativamente la calidad de esta edición. Su papel ha sido incluso más importante esta vez puesto que esta edición se hizo en solitario. Queremos agradecer a la familia de Morgan Kaufmann por estar de acuerdo en la publicación de este libro otra vez bajo la dirección de Denise Penrose. Nathaniel McFadden fue el editor de desarrollo (developmental editor) para esta edición y trabajó codo con codo conmigo en los contenidos del libro. Kimberlee Honjo coordinó las encuestas de los usuarios y sus respuestas. Dawnmarie Simpson llevó a cabo la supervisión del proceso de producción del libro. También queremos dar las gracias a muchos agentes por cuenta propia (fre-

Prefacio

elance vendors) que contribuyeron a que este volumen vea la luz, especialmente a Alan Rose de Multiscience Press y diacriTech, nuestro corrector (compositor). Las contribuciones de las casi 200 personas mencionadas aquí han ayudado a hacer de esta cuarta edición lo que espero sea nuestro mejor libro hasta el momento. ¡Gracias a todos! David A. Patterson

xxiii

1 La civilización avanza extendiendo el número de operaciones importantes que se pueden hacer sin pensar en ellas Alfred North Whitehead An Introduction to Mathematics, 1911

Abstracciones y tecnología de los computadores 1.1

Introducción 3

1.2

Bajo los programas 10

1.3

Bajo la cubierta 13

1.4

Prestaciones 26

1.5

El muro de la potencia 39

1.6

El gran cambio: el paso de monoprocesadores a multiprocesadores 41

1.7

Casos reales: fabricación y evaluación del AMD Opteron x4 44

1.8

Falacias y errores habituales 51

1.9

Conclusiones finales 54

1.10

Perspectiva histórica y lecturas recomendadas 55

1.11

Ejercicios 56

Nota importante: En la presente edición en castellano, los contenidos del CD incluido en la edición original (en inglés) son accesibles a través de la página web www.reverte.com/microsites/pattersonhennessy. Aunque en la presente edición no se proporciona un CD-ROM físico, a lo largo de todo el texto se menciona el CD y se utiliza el icono que lo representa para hacer referencia a su contenido.

1.1

Introducción

1.1

¡Bienvenido a este libro! Estamos encantados de tener esta oportunidad de transmitir el entusiasmo del mundo de los computadores. Éste no es un campo árido y aburrido, donde el progreso es glacial y donde las nuevas ideas se atrofian por negligencia. ¡No! Los computadores son el producto de la increíblemente vibrante industria de las tecnologías de la información, que en su conjunto es responsable como mínimo del 10% del producto nacional bruto de los Estados Unidos y cuya economía se ha vuelto, en parte, dependiente de las rápidas mejoras en tecnologías de la información prometidas por la ley de Moore. Esta insólita industria impulsa la innovación a una velocidad asombrosa. En los últimos 25 años aparecieron varios computadores nuevos cuya introducción parecía que iba a revolucionar la industria de la computación; estas revoluciones duraban poco tiempo simplemente porque alguien construía un computador aun mejor. Esta carrera por innovar condujo a un progreso sin precedentes desde el inicio de la computación electrónica en los últimos años de la década de 1940. Si los medios de transporte hubiesen ido a la par con la industria de la computación, por ejemplo, hoy en día se podría viajar de Nueva York a Londres en aproximadamente un segundo por unos pocos céntimos. Pensemos un momento cómo tal adelanto habría cambiado la sociedad (vivir en Tahití y trabajar en San Francisco, ir a Moscú para ver el Ballet Bolshoi) y podremos apreciar las implicaciones de tal cambio.

4

Capítulo 1

Abstracciones y tecnología de los computadores

Los computadores nos han llevado a una tercera revolución de la civilización, la revolución de la información, que se sitúa a la par de las revoluciones agrícola e industrial. El resultado de la multiplicación de la potencia y el alcance intelectual de la humanidad ha afectado profundamente a nuestras vidas cotidianas y también ha cambiado la manera de obtener nuevos conocimientos. Hay ahora una nueva forma de investigación científica, en la cual científicos informáticos trabajan junto a científicos teóricos y experimentales en la exploración de nuevas fronteras en astronomía, biología, química y física entre otras. La revolución de los computadores continúa. Cada vez que el coste de la computación se mejora en un factor 10, las oportunidades para los computadores se multiplican. Aplicaciones que eran económicamente inviables repentinamente se convierten en factibles. Hasta hace muy poco, las siguientes aplicaciones eran “ciencia ficción computacional”. ■

Computadores en los coches: hasta que los microprocesadores mejoraron drásticamente en precio y prestaciones a principios de la década de 1980, el control por computador en los coches era risible. Hoy en día los computadores reducen la contaminación y mejoran las prestaciones del combustible vía controles en el motor, y también mejoran la seguridad al prevenir peligrosos patinazos e inflando los air bags para proteger a los ocupantes en caso de colisión.



Teléfonos portátiles: ¿quién habría podido soñar que los avances en sistemas de computación llevarían al desarrollo de teléfonos móviles, permitiendo una comunicación persona-a-persona casi en cualquier parte del mundo?



Proyecto genoma humano: el coste del equipamiento informático para cartografiar y analizar las secuencias del ADN humano es de centenares de millones de dólares. Es improbable que alguien hubiese considerado este proyecto si los costes de los computadores hubiesen sido entre 10 y 100 veces mayores, tal y como eran hace 10 o 20 años. Además, el coste continúa bajando; podríamos ser capaces de adquirir nuestro propio genoma, permitiendo que los cuidados médicos se adapten a nosotros.



World Wide Web (La telaraña mundial): la World Wide Web, que no existía en la primera edición de este libro, ha cambiado nuestra sociedad. Para muchos, la www ha reemplazado a las bibliotecas.



Motores de búsqueda: Dado que el contenido de la WWW ha crecido en tamaño y valor, encontrar información relevante es cada vez más importante. En la actualidad, mucha gente confía tanto en los motores de búsqueda que se verían en apuros si no pudiesen utilizarlos.

Claramente, los avances en esta tecnología influyen hoy en día en casi todos los aspectos de nuestra sociedad. Los avances en el hardware han permitido a los programadores crear programas maravillosamente útiles, y explican por qué los computadores son omnipresentes. Lo que hoy es ciencia ficción, serán las aplicaciones normales en el futuro: ya hay mundos virtuales, reconocimiento de voz y asistencia médica personalizada.

1.1

5

Introducción

Tipos de aplicaciones de computador y sus características Aunque se usan un conjunto común de tecnologías hardware (presentadas en las secciones 1.3 y 1.4) que van desde los electrodomésticos caseros inteligentes a los teléfonos móviles o celulares o los mayores supercomputadores, estas aplicaciones diferentes tienen diversos requerimientos de diseño y utilizan las tecnologías hardware de manera diferente. Grosso modo, los computadores se utilizan en tres clases diferentes de aplicaciones. Los computadores de sobremesa son posiblemente la forma más conocida de computación y están representados por el computador personal, que muchos lectores de este libro habrán usado extensamente. Los computadores de sobremesa se caracterizan por dar buenas prestaciones a bajo coste a único usuario, y a menudo se usan para ejecutar programas de terceros, también llamado software estándar. La evolución de muchas tecnologías es impulsada por este tipo de computadores, ¡que sólo tiene 30 años de antigüedad! Los servidores son la versión moderna de lo que fueron los computadores centrales, los minicomputadores y los supercomputadores, y generalmente solo se accede a ellos vía una red. Los servidores están pensados para soportar grandes cargas de trabajo, que pueden consistir en una única aplicación compleja, generalmente científica o de ingeniería, o en muchos trabajos pequeños, como ocurre en un servidor Web. Estas aplicaciones están basadas en programas de otras fuentes (tales como una base de datos o un sistema de simulación), pero frecuentemente se modifican o adaptan a una función concreta. Los servidores se construyen con la misma tecnología básica que los computadores de sobremesa, pero permiten una mayor ampliación de su capacidad tanto de computación como de entrada/salida. En general, los servidores también ponen gran énfasis en la confiabilidad, puesto que un fallo es generalmente más costoso que en un computador de sobremesa de un único usuario. Los servidores abarcan la mayor gama de costes y capacidades. En el extremo más bajo, un servidor puede ser un poco más que una máquina de sobremesa sin pantalla ni teclado y con un coste un poco mayor. Estos servidores de gama baja se usan típicamente para almacenar archivos o ejecutar pequeñas aplicaciones de empresas o un servicio web sencillo (véase la sección 6.10). En el otro extremo están los supercomputadores, que en la actualidad disponen de cientos de miles de procesadores, y generalmente terabytes de memoria y petabytes de almacenamiento, y cuestan de millones a cientos de millones de dólares. Los supercomputadores se utilizan para cálculos científicos y de ingeniería de alta calidad, tales como predicción del clima, prospección petrolífera, determinación de la estructura de proteínas, y otros problemas de gran envergadura. Aunque estos supercomputadores representan el pico de la capacidad de computación, en términos relativos constituyen una pequeña fracción del número total de servidores y también del total del mercado de los computadores en términos de facturación. Aunque no se les llama supercomputadores, los centros de datos de internet utilizados por compañías como eBay y Google disponen también de miles de procesadores, terabytes de memoria y petabytes de almacenamiento. Habitualmente se consideran como grandes clústeres de computadores (véase capítulo 7).

Computador de sobremesa: computador diseñado para un único usuario, y que incorpora una pantalla, un teclado y un ratón.

Servidor: computador que se utiliza para ejecutar grandes programas para muchos usuarios, a menudo simultáneamente; generalmente sólo se accede a él vía un red. Supercomputador: computador con la capacidad de computación y coste más altos; se configuran como servidores y generalmente su coste es de millones de dólares.

Terabyte: originalmente son 1 099 511 627 776 (240) bytes, aunque algunos sistemas de comunicaciones y de almacenamiento secundario lo han redefinido como 1 000 000 000 000 (1012) bytes.

Petabyte: 1000 o 1024 terabytes.

Centro de datos: una habitación o edificio diseñado con todo lo que se necesita para un número elevado de servidores: alimentación y potencia eléctrica, aire acondicionado y red.

6

Capítulo 1

Computadores empotrados: computador que

Los computadores empotrados constituyen la clase de computadores más amplia y son los que tienen la gama más amplia de aplicaciones y prestaciones. Los computadores empotrados incluyen los microprocesadores que se encuentran en los coches, los computadores de los teléfonos móviles o de los computadores de los videojuegos o de los televisores digitales, y las redes de procesadores que controlan los aviones modernos o los barcos de carga. Los sistemas de computación empotrada se diseñan para ejecutar una aplicación o un conjunto de aplicaciones relacionadas, que normalmente están integradas con el hardware y se proporcionan como un único sistema; así, a pesar del gran número de computadores empotrados, ¡muchos usuarios nunca ven realmente que están usando un computador! La figura 1.1. muestra que durante los últimos años el incremento de los teléfonos móviles, que dependen de los computadores empotrados, ha sido mucho más rápido que el incremento de los computadores de sobremesa. Obsérvese que las televisiones digitales, coches, cámaras digitales, reproductores de música, videojuegos y otros muchos dispositivos de consumo incorporan también computadores empotrados, lo que incrementa aún más la diferencia entre el número de computadores empotrados y computadores de sobremesa. Las aplicaciones empotradas a menudo tienen un único requisito de aplicación que combina unas prestaciones mínimas con fuertes limitaciones en coste o consumo de potencia. Por ejemplo, pensemos en un reproductor de música: El procesador necesita solamente tener la velocidad suficiente para llevar a cabo esta función limitada, y a partir de aquí, los objetivos más importantes son reducir el coste y el consumo de potencia. A pesar de su coste reducido, los computadores

Móviles

PCs

TVs

98 19 99 20 00 20 01 20 02 20 03 20 04 20 05 20 06 20 07

19

97

1200 1100 1000 900 800 700 600 500 400 300 200 100 0 19

se encuentra dentro de otro dispositivo y que se utiliza para ejecutar una aplicación predeterminada o un conjunto de aplicaciones relacionadas.

Abstracciones y tecnología de los computadores

FIGURA 1.1 El número de teléfonos móviles, computadores personales y televisores fabricados cada año entre 1997 y 2007. (Solo hay datos de televisores del año 2004). Más de mil millones de nuevos teléfonos móviles se vendieron en 2006. En 1997, la venta de teléfonos móviles superaba a la de PC en un factor 1.4; y este factor creció hasta 4.5 en 2007. En 2004, se estimaba que había 2000 millones de televisores en uso, 1800 millones de teléfonos móviles y 800 millones de PCs. Cómo la población mundial era 6400 millones de personas, había aproximadamente 1 PC, 2.2 teléfonos móviles y 2.5 televisores por cada 8 habitantes del planeta. En 2006, un estudio estimó que en Estados Unidos había una media de 12 dispositivos por familia, incluyendo 3 televisiones, 2 PCs y otros aparatos como consolas para videojuegos, reproductores de MP3 y teléfonos móviles.

1.1

Introducción

empotrados tienen a menudo menor tolerancia a fallos, porque las consecuencias de un fallo pueden variar desde ser molestas (cuando nuestra televisión nueva se estropea) a devastadoras (algo que puede ocurrir cuando falla el computador de un avión o de un barco de carga). En aplicaciones empotradas orientadas al consumo, como por ejemplo electrodomésticos digitales, la funcionalidad se obtiene principalmente a través de la sencillez; se intenta hacer una función tan perfectamente como sea posible. En grandes sistemas empotrados, se emplean a menudo técnicas de redundancia desarrolladas para el mundo de los servidores (véase sección 6.9). Aunque este libro se centra en los computadores de propósito general, la mayoría de los conceptos son aplicables directamente, o con pocas modificaciones, a los computadores empotrados. Extensión: Las extensiones son secciones cortas que se usan a lo largo del texto para proporcionar un mayor detalle sobre algún tema en particular, que puede ser de interés. Los lectores que no están interesados en una extensión, pueden saltársela, ya que el material siguiente nunca dependerá de su contenido. Muchos procesadores empotrados se diseñan usando núcleos de procesador, una versión de un procesador escrito en un lenguaje de descripción hardware, tal como Verilog o VHDL (véase capítulo 4). El núcleo permite a un diseñador integrar otro hardware específico de la aplicación con el núcleo del procesador para fabricar un único circuito integrado.

Qué puede aprender en este libro Los programadores de éxito siempre han estado comprometidos con las prestaciones de sus programas, porque proporcionar de forma rápida resultados al usuario es fundamental para crear software de éxito. En las décadas de 1960 y 1970, la principal restricción para las prestaciones era el tamaño de la memoria del computador. Así, los programadores frecuentemente seguían una sencilla regla: minimizar el espacio de la memoria para hacer programas rápidos. En la última década, los avances en el diseño de los computadores y en la tecnología de la memoria redujeron drásticamente la importancia del pequeño tamaño de la memoria en muchas aplicaciones, excepto las de los sistemas de computación empotrada. Los programadores interesados en las prestaciones necesitan ahora conocer las cuestiones que han reemplazado el modelo de memoria simple de la década de 1960: la naturaleza jerárquica de las memorias y la naturaleza paralela de los procesadores. Los programadores que buscan construir versiones competitivas de los compiladores, los sistemas operativos, las bases de datos, e incluso de aplicaciones tendrán consecuentemente que incrementar su conocimiento de la organización de los computadores. Nosotros tenemos la oportunidad de explicar qué hay dentro de esta máquina revolucionaria, desentrañando el software bajo su programa y el hardware bajo las cubiertas de su computador. Cuando complete este libro, creemos que será capaz de contestar a las siguientes preguntas: ■

¿Cómo se escriben los programas en un lenguaje de alto nivel, tales como C o Java, cómo se traducen al lenguaje del hardware, y cómo ejecuta el hardware

7

8

Capítulo 1

Abstracciones y tecnología de los computadores

el programa resultante? Comprender estos conceptos forma la base para entender los aspectos tanto del hardware y como del software que afectan a las prestaciones de los programas.

Microprocesador multinúcleo: procesador que contiene varios procesadores o núcleos en un único circuito integrado.

Acrónimo: palabra construida tomando las letras iniciales de cada una de las palabras de una frase; por ejemplo, RAM es un acrónimo de Random Access Memory (memoria de acceso aleatorio), y CPU es un acrónimo de for Central Processing Unit (unidad central de proceso).



¿Cuál es la interfaz entre el software y el hardware, y cómo el software instruye al hardware para realizar las funciones necesarias? Estos conceptos son vitales para comprender cómo se escriben muchos tipos de software.



¿Qué determina las prestaciones de un programa, y cómo un programador puede mejorarlo? Como veremos, esto depende del programa original, el software de traducción de este programa al lenguaje del computador, y de la efectividad del hardware al ejecutar el programa.



¿Qué técnicas pueden usar los diseñadores de hardware para mejorar las prestaciones? Este libro introducirá los conceptos básicos del diseño de los computadores actuales. El lector interesado encontrará mucho más material sobre este tema en nuestro libro avanzado, Arquitectura del Computador: Una aproximación cuantitativa.



¿Cuáles son las razones y consecuencias del reciente paso del procesamiento secuencial al paralelo? En este libro se indican las motivaciones, se describe el hardware disponible actualmente para dar soporte al paralelismo y se revisa la nueva generación de microprocesadores multinúcleo (véase capítulo 7).

Sin comprender las respuestas a estas preguntas, mejorar las prestaciones de sus programas en un computador moderno, o evaluar qué características podrían hacer un computador mejor que otro para una aplicación particular, sería un complejo proceso de prueba y error, en lugar de un procedimiento científico conducido por la comprensión y el análisis. El primer capítulo contiene los fundamentos para el resto del libro. Introduce las ideas básicas y las definiciones, pone en perspectiva los mayores componentes del hardware y del software, muestra como evaluar prestaciones y potencia e introduce los circuitos integrados, la tecnología que alimenta la revolución de los computadores y explica la evolución hacia los multinúcleos. En este capítulo, y otros posteriores, probablemente usted verá un montón de nuevas palabras, o palabras que puede haber oído, pero que no está seguro de lo que significan. ¡No se preocupe! Sí, hay un montón de terminología especial que se usa en la descripción de los computadores modernos, pero la terminología realmente es una ayuda que nos permite describir con precisión una función o capacidad. Además, a los diseñadores de computadores (incluidos a los autores de este libro) les encanta usar acrónimos, ¡que son más fáciles de comprender cuando se sabe lo que significa cada una de sus letras! Para ayudarle a recordar y localizar los términos, hemos incluido una definición resaltada de cada uno de ellos la primera vez que aparece en el texto. Después de un breve periodo trabajando con la terminología, usted estará habituado, y sus amigos se quedarán impresionados de la manera tan correcta como usa palabras como BIOS, CPU, DIMM, DRAM, PCIE, SATA y tantas otras. Para reforzar la comprensión de cómo los sistemas hardware y software que se usan para ejecutar un programa afectan a las prestaciones, a lo largo del libro usamos la sección especial “Comprender las prestaciones de los programas”. A

1.1

Introducción

9

continuación veremos la primera. Estas secciones resumen detalles importantes de las prestaciones de los programas.

Las prestaciones de un programa dependen de una combinación de efectividad de Comprender los algoritmos usados en el programa, de los sistemas de software usados para las prestaciones crear y traducir el programa en instrucciones máquina y de la efectividad del de los programas computador al ejecutar esas instrucciones, las cuales pueden incluir operaciones de entrada y salida (E/S). La siguiente tabla resume cómo afectan a las prestaciones tanto el hardware como el software. Componente Hardware o software

Cómo afecta este componente a las prestaciones

Dónde se cubre este tema

Algoritmo

Determina el número de sentencias de alto nivel y ¡Otros libros! el número de operaciones de E/S que se ejecutarán

Lenguaje de programación, compilador y arquitectura

Determina el número de instrucciones máquina que capítulos 2 y 3 se ejecutarán por cada sentencia de alto nivel

Procesador y sistema de memoria

Determina cuán rápido se pueden ejecutar las instrucciones

capítulos 4, 5 y 7

Sistema de E/S (hardware y sistema operativo)

Determina cuán rápido se pueden ejecutar las operaciones de E/S

capítulo 6

Las secciones “Autoevaluación” se han diseñado para ayudar a los lectores a valorar si han comprendido los conceptos principales introducidos en un capítulo y han entendido las implicaciones de los mismos. Algunas preguntas de estas secciones “Autoevaluación” tienen respuestas sencillas; otras son para debate en grupo. Las respuestas a las cuestiones específicas se pueden encontrar al final del capítulo. Las preguntas de “Autoevaluación” aparecen sólo al final de una sección, de manera que es fácil saltarlas si usted está seguro de que entiende el material. 1. La sección 1.1 mostraba que el número de procesadores empotrados vendidos cada año supera significativamente el número de procesadores de computadores de sobremesa. ¿Puede confirmar o negar esta percepción basándose en su propia experiencia? Intente contar el número de procesadores empotrados que hay en su casa. ¿Cómo es este número comparado con el número de computadores de sobremesa que hay en su casa? 2. Tal y como se mencionó antes, tanto el hardware como el software afectan a las prestaciones de un programa. ¿Puede pensar ejemplos dónde cada uno de los ítems siguientes sea el lugar adecuado para buscar un cuello de botella de las prestaciones? ■ ■ ■ ■ ■

El algoritmo elegido El lenguaje de programación o el compilador El sistema operativo El procesador El sistema de E/S y los dispositivos

Autoevaluación

Software de sistemas: software que proporciona servicios que habitualmente son útiles, entre ellos los sistemas operativos, los compiladores y los ensambladores.

Sistema operativo: programa de supervisión que gestiona los recursos de un computador para provecho de los programas que se ejecutan en esa máquina.

1.2

Abstracciones y tecnología de los computadores

Bajo los programas

1.2

Una aplicación típica, tal como un procesador de textos o un gran sistema de base de datos, puede consistir de cientos de miles o millones de líneas de código y depender de sofisticadas bibliotecas de software que implementan funciones complejas de soporte a la aplicación. Tal y como veremos, el hardware de un computador sólo puede ejecutar instrucciones extremadamente simples de bajo nivel. Para ir de una aplicación compleja hasta las instrucciones simples se ven involucradas varias capas de software que interpretan o trasladan las operaciones de alto nivel en instrucciones simples del computador. Estas capas de software están organizadas principalmente en forma jerárquica, donde las aplicaciones son el anillo más externo y una variedad de software de sistemas se coloca entre el hardware y las aplicaciones software, como muestra la figura 1.2. Hay muchos tipos de software de sistemas, pero actualmente hay dos tipos que son fundamentales para todos los computadores: un sistema operativo y un compilador. Un sistema operativo interactúa entre el programa del usuario y el hardware y proporciona una variedad de servicios y funciones de supervisión. Entre sus funciones más importantes están: ■

manejo de las operaciones básicas de entrada y salida



asignación de espacio de almacenamiento y de memoria



facilitar la compartición del computador entre múltiples aplicaciones simultáneas

Ejemplos de sistemas operativos en uso hoy en día son Windows, Linux y MacOS.

mas de aplica ció gra o r d s e a sis am te gr

n

P

En París sólo me miraban fijamente cuando les hablaba en francés; nunca conseguí hacer entender a esos idiotas su propio idioma. Mark Twain, The Innocents Abroad, 1869

Capítulo 1

ma

Pro

10

Circuitería

FIGURA 1.2 Vista simplificada del hardware y el software como capas jerárquicas, mostradas como círculos concéntricos con el hardware en el centro y el software de las aplicaciones en el exterior. En aplicaciones complejas frecuentemente se encuentran múltiples capas software. Por ejemplo, un sistema de base de datos puede ejecutarse sobre el software de sistemas que aloja una aplicación, el cual a su vez se ejecuta sobre la base de datos.

1.2

11

Bajo los programas

Los compiladores realizan otra función vital: la traducción de un programa escrito en un lenguaje de alto nivel, como C, C++, Java o Visual Basic a instrucciones que el hardware puede ejecutar. Dada la sofisticación de los modernos lenguajes de programación y las instrucciones simples ejecutadas por el hardware, la traducción desde un programa en un lenguaje de alto nivel a instrucciones hardware es compleja. Daremos una breve visión general del proceso y volveremos a este tema en el capítulo 2 y apéndice B.

Compiladores: programa que traduce sentencias en un lenguaje de alto nivel a sentencias en lenguaje ensamblador.

Del lenguaje de alto nivel al lenguaje del hardware Para hablar realmente a una máquina electrónica es necesario enviar señales eléctricas. Las señales eléctricas más fáciles de entender para las máquinas son encendido (on) y apagado (off), y por lo tanto el alfabeto de la máquina tiene sólo dos letras. Del mismo modo que las 26 letras del alfabeto inglés no limitan cuánto se puede escribir, las dos letras del alfabeto de los computadores no limitan lo que los éstos pueden hacer. Los dos símbolos para estas letras son los números 0 y 1 y habitualmente pensamos en el lenguaje de las máquinas como números en base 2, o números binarios. Nos referimos a cada “letra” como dígito binario o bit.1 Los computadores son esclavos de nuestras órdenes. De ahí que el nombre para una orden individual sea instrucción. Las instrucciones, que son simples colecciones de bits que el computador puede comprender, se pueden pensar como números. Por ejemplo, los bits 1000110010100000

indican a un computador que sume dos números. En el capítulo 3 explicamos por qué usamos números para instrucciones y datos; no queremos adelantar acontecimientos, pero el uso de números tanto para las instrucciones como para los datos es de importancia capital para la informática. Los primeros programadores se comunicaban con los computadores mediante números binarios, pero eso era tan laborioso que rápidamente inventaron nuevas notaciones más próximas a la forma de pensar de los humanos. Al principio estas notaciones se traducían a binario a mano, pero ese proceso aún era fatigoso. Usando la propia máquina para ayudar a programar la máquina, los pioneros inventaron programas para traducir de notación simbólica a binario. El primero de estos programas fue llamado ensamblador. Este programa traduce la versión simbólica de una instrucción a su versión binaria. Por ejemplo, el programador escribiría

Dígito binario: también llamado bit. Uno de los dos números en base 2 (0 ó 1) que son los componentes de la información.

Instrucción: orden que el hardware del computador entiende y obedece.

Ensamblador: programa que traduce una versión simbólica de las instrucciones a su versión binaria.

add A, B

y el ensamblador traduciría esta notación a 1000110010100000

Esta instrucción indica al computador que sume los números A y B. El nombre acuñado para este lenguaje simbólico, aún usado hoy en día, es lenguaje ensamblador. Por el contrario, el lenguaje binario que entiende el computador se llama lenguaje máquina. 1. Originariamente, contracción inglesa de binary digit. (N. del T.)

Lenguaje ensamblador: representación simbólica de las instrucciones de la máquina.

Lenguaje máquina: representación binaria de las instrucciones máquina.

12

Lenguaje de programación de alto nivel: lenguaje transportable tal como C, Fortran o Java compuesto por palabras y notación algebraica que un compilador puede traducir en lenguaje ensamblador.

Capítulo 1

Abstracciones y tecnología de los computadores

Aunque sea una enorme mejora, el lenguaje ensamblador todavía queda muy lejos de la notación que le gustaría usar a un científico para simular el flujo de fluidos o la que podría usar un contable para hacer balance de sus cuentas. El lenguaje ensamblador requiere que el programador escriba una línea para cada instrucción que desee que la máquina ejecute; por tanto, este lenguaje fuerza al programador a pensar como la máquina. El reconocimiento de que se podía escribir un programa para traducir un lenguaje más potente a instrucciones del computador fue uno de los grandes avances en los primeros días de la computación. Los programadores de hoy en día deben su productividad (y su cordura) a la creación de los lenguajes de programación de alto nivel y los compiladores que traducen los programas en tales lenguajes en instrucciones. La figura 1.3 muestra la relación entre estos programas y lenguajes.

Programa en lenguaje de alto nivel (en C)

swap(int v[], int k) {int temp; temp = v[k]; v[k] = v[k+1]; v[k+1] = temp; }

Compilador de C

Programa en lenguaje ensamblador (para MIPS)

swap: muli add lw lw sw sw jr

$2, $5,4 $2, $4,$2 $15, 0($2) $16, 4($2) $16, 0($2) $15, 4($2) $31

Ensamblador

Programa en lenguaje máquina binario (para MIPS)

00000000101000010000000000011000 00000000000110000001100000100001 10001100011000100000000000000000 10001100111100100000000000000100 10101100111100100000000000000000 10101100011000100000000000000100 00000011111000000000000000001000

FIGURA 1.3 Programa en C compilado a lenguaje ensamblador y luego ensamblado a lenguaje máquina binario. Aunque la traducción del lenguaje de alto nivel al lenguaje máquina binario se muestra en dos pasos, algunos compiladores eliminan el paso intermedio y producen lenguaje máquina binario directamente. En el capítulo 2 se examinan con más detalle estos lenguajes y este programa.

1.3

Bajo la cubierta

Un compilador permiten que un programador escriba esta expresión en lenguaje de alto nivel: A + B

El compilador compilaría esto en esta sentencia de lenguaje ensamblador: add A, B

El ensamblador traduciría esto en la instrucción binaria que indica al computador que sume los números A y B. Los lenguajes de programación de alto nivel ofrecen varias ventajas importantes. Primero, permiten al programador pensar en un lenguaje más natural, usando palabras inglesas y notación algebraica, dando lugar a programas con un aspecto mucho más parecido a un texto que a una tabla de símbolos crípticos (véase figura 1.3). Por otro lado, permiten que los lenguajes sean diseñados de acuerdo con su intención de uso. De este modo, el Fortran fue diseñado para computación científica, Cobol para el procesado de datos comerciales, Lisp para manipulación simbólica, etc. Hay también lenguajes de dominio específico, incluso para pequeños grupos de usuarios, como por ejemplo los interesados en simulación de fluidos. La segunda ventaja de los lenguajes de programación es la mejora de la productividad del programador. Una de las pocas cuestiones con amplio consenso en el desarrollo de programas es que toma menos tiempo desarrollar programas cuando se escriben en lenguajes que requieren menos líneas para expresar una idea. La concisión es una ventaja clara de los lenguajes de alto nivel sobre el lenguaje ensamblador. La ventaja final es que los lenguajes de programación permiten a los programas ser independientes del computador sobre el que se desarrollan, ya que los compiladores y ensambladores pueden traducir programas en lenguaje de alto nivel a las instrucciones binarias de cualquier máquina. Estas tres ventajas son tan decisivas que hoy en día se programa muy poco en lenguaje ensamblador.

1.3

Bajo la cubierta

1.3

Ahora que hemos mirado debajo de los programas para descubrir la programación subyacente, abramos la cubierta del computador para aprender sobre la circuitería que hay debajo. El hardware de cualquier computador lleva a cabo las mismas funciones básicas: introducción de datos, extracción de resultados, procesamiento de datos y almacenamiento de datos. El tema principal de este libro es explicar cómo se realizan estas funciones, y los capítulos siguientes tratan las diferentes partes de estas cuatro tareas. Cuando llegamos a un punto importante en este libro, un punto tan importante que desearíamos que lo recordase para siempre, lo enfatizamos identificándolo con

13

14

Capítulo 1

Abstracciones y tecnología de los computadores

un ítem de “Idea clave”. Tenemos cerca de una docena de Ideas clave en este libro, siendo la primera los cinco componentes de un computador que realizan las tareas de entrada, salida, proceso y almacenamiento de datos.

IDEA clave

Los cinco componentes clásicos de un computador son entrada, salida, memoria, camino de datos y control, donde las dos últimas a veces están combinadas y se llaman el procesador. La figura 1.4 muestra la organización estándar de un computador. Esta organización es independiente de la tecnología del hardware: se puede colocar cada parte de cada computador, pasado y presente, en una de estas cinco categorías. Para ayudarle a tener todo esto en perspectiva, los cinco componentes de un computador se muestran en la primera página de los capítulos siguientes, con la parte de interés para ese capítulo resaltada.

FIGURA 1.4 La organización de un computador, mostrando los cinco componentes clásicos. El procesador toma las instrucciones y datos de la memoria. La entrada escribe datos en la memoria y la salida lee datos de la memoria. El control envía señales que determinan las operaciones del camino de datos, la memoria, la entrada y la salida.

1.3

Bajo la cubierta

15

FIGURA 1.5 Un computador de sobremesa. La pantalla de cristal líquido (LCD) es el dispositivo de salida principal, y el teclado y el ratón son los dispositivos de entrada principales. En la parte derecha se puede ver un cable de Ethernet que conecta el portátil con la red y la web. El portátil tiene procesador, memoria y dispositivos de E/S. Este computador es un portátil Macbook Pro 15” conectado a un monitor externo.

Dispositivo de entrada:

La figura 1.5 muestra un computador de sobremesa típico, con teclado, ratón y pantalla. Esta fotografía revela dos de los componentes clave de un computador: dispositivos de entrada, como el teclado y el ratón, y dispositivos de salida, como la pantalla. Tal como sugiere su nombre, la entrada alimenta al computador y la salida es el resultado de la computación enviado al usuario. Algunos dispositivos, tales como las redes y discos, ofrecen al computador tanto entrada como salida. El capítulo 6 describe los dispositivos de entrada/salida (E/S) con mayor detalle, pero ahora, como introducción, haremos un breve recorrido por la circuitería del computador, empezando por los dispositivos externos de E/S.

mecanismo, como el teclado o el ratón, mediante el cual se introduce información al computador.

Dispositivos de salida: mecanismo que comunica el resultado de la computación a un usuario o a otro computador.

16

Capítulo 1

Se me ocurrió la idea del ratón mientras estaba en una conferencia de computadores. El conferenciante era tan aburrido que empecé a soñar despierto y di con la idea. Doug Engelbart En las pantallas de los computadores he hecho aterrizar un avión en la cubierta de un portaaviones en movimiento, he visto el impacto de una partícula nuclear con un átomo, he volantemplado un computador revelando todas sus tareas más internas.

Anatomía de un ratón

Ivan Sutherland, “padre” de los gráficos por computador, cita de “Computer Software for Graphics”; Scientific American, 1984.

Abstracciones y tecnología de los computadores

Aunque muchos usuarios no se imaginarían ahora un computador sin ratón, la idea de un dispositivo para señalar, como es el ratón, fue mostrada por primera vez por Engelbart usando un prototipo de investigación en 1967. El Alto, que fue la inspiración para todas las estaciones de trabajo (workstations) y para los sistemas operativos Macintosh y Windows, incluía un ratón como dispositivo para señalar en 1973. En la década de 1990, todos los computadores de sobremesa incluían este dispositivo y se hicieron populares nuevas interfaces basadas en presentaciones gráficas y ratones. El primer ratón era electromecánico y usaba una bola grande que cuando rodaba a lo largo de una superficie hacía que unos contadores x e y se incrementasen. La cantidad de incremento de cada contador decía cuánto se había movido el ratón. La mayoría de los ratones electromecánicos fueron reemplazados por el novedoso ratón óptico, que en realidad es un procesador óptico en miniatura que incluye un diodo luminoso (Light Emitting Diode, LED) para proporcionar iluminación, una minúscula cámara en blanco y negro y un procesador óptico sencillo. El LED ilumina la superficie que hay debajo del ratón; la cámara toma 1500 fotografías de muestra por segundo de la zona iluminada. Las sucesivas fotografías se envían al procesador óptico que compara las imágenes y determina si el ratón se ha movido y en qué medida. La sustitución del ratón electromecánico por el ratón electro-óptico es una ilustración de un fenómeno común: los costes decrecientes y la mayor fiabilidad de la electrónica hace que una solución electrónica reemplace a una solución electromecánica más antigua. En la página 22 veremos otro ejemplo: la memoria flash.

A través del cristal de observación Pantalla de panel plano, pantalla de cristal líquido: tecnología de pantalla que usa una capa fina de polímeros de líquidos que se pueden usar para transmitir o bloquear la luz según si se aplica una carga eléctrica o no.

Pantalla de panel plano, Pantalla de matriz activa: pantalla de cristal líquido que usa un transistor para controlar la transmisión de luz a cada píxel individual. Píxel: el elemento individual más pequeño de la imagen. Las pantallas están formadas de millones de píxels organizados como una matriz.

El dispositivo de E/S más fascinante es probablemente la pantalla gráfica. Los computadores portátiles y los de mano, las calculadoras, los teléfonos móviles o celulares y casi todos los computadores de sobremesa usan una pantalla de cristal líquido (liquid crystal display, LCD) o de panel plano, para conseguir una pantalla delgada y de bajo consumo. La diferencia principal es que el píxel del LCD no es la fuente de luz; en su lugar controla la transmisión de la luz. Un LCD típico consiste en moléculas en forma de bastoncillos suspendidos en un líquido. Estas moléculas forman una hélice retorcida que refractan la luz que entra en la pantalla, habitualmente de una fuente de luz situada detrás de ella o, menos frecuentemente, de una luz reflejada. Las barras se alinean cuando se aplica una corriente y dejan de refractar la luz; puesto que el material del cristal líquido está entre dos pantallas polarizadas a 90 grados, la luz no puede pasar si no es refractada. Hoy en día, muchas pantallas de LCD usan una matriz activa que tiene un minúsculo transistor que actúa como interruptor en cada píxel para controlar con precisión la corriente y así formar imágenes más nítidas. La intensidad de los tres colores —rojo, verde, azul— de la imagen final se obtiene a partir de una máscara roja-verde-azul asociada a cada punto de la pantalla; en una matriz activa LCD de color, hay tres transistores en cada punto. La imagen se compone de una matriz de elementos, o píxels,1 que se pueden representar en una matriz de bits, llamada mapa de bits (bit map). Dependiendo de la 1. Originariamente, contracción de “picture element”, elemento de imagen. Dada la extensión de su uso, se ha optado por mantener el término. (N. del T.)

1.3

Bajo la cubierta

17

medida de la pantalla y de la resolución, la matriz varía en tamaño desde 640 × 480, hasta 2560 × 1600 píxeles en 2008. Una pantalla en color puede usar 8 bits para cada uno de los tres colores primarios (rojo, azul y verde), 24 bits por píxel en total, y permite ver millones de colores diferentes en la pantalla. El soporte del hardware del computador para gráficos consiste principalmente en un búfer de refresco, o búfer de pantalla, para almacenar el mapa de bits. La imagen que se va a representar se almacena en el búfer de pantalla y el patrón de bits de cada píxel se leen hacia la pantalla gráfica a la velocidad de refresco. La figura 1.6 muestra un búfer de pantalla con 4 bits por píxel.

búfer de pantalla pantalla CRT

Y0 Y1

0

11 0 1

X0 X1

01 1

Y0 Y1

X0 X1

FIGURA 1.6 Cada coordenada en el búfer de pantalla de la izquierda determina el sombreado de la correspondiente coordenada de la pantalla CRT de la derecha. El píxel (X0, Y0) contiene el patrón de bits 0011, el cual es un tono de gris más claro en pantalla que el patrón de bits 1101 del píxel (X1, Y1).

El objetivo del mapa de bits es representar fielmente lo que está en la pantalla. Los problemas en sistemas gráficos aparecen debido a que el ojo humano es muy bueno detectando cambios sutiles.

Apertura de la caja Si se abre la caja del computador, se ve una placa fascinante de plástico verde delgado, cubierta con docenas de pequeños rectángulos grises o negros. En la figura 1.7 se puede ver el contenido del computador portátil de la figura 1.5. En la parte superior de la foto se muestra la placa base. En la parte frontal hay dos bahías de disco, el disco duro a la derecha y el DVD a la izquierda. El hueco en el medio es para albergar la batería del portátil. Los pequeños rectángulos de la placa base son los dispositivos que impulsan nuestra avanzada tecnología, los circuitos integrados o chips. La placa consta de tres partes: la parte que conecta con los dispositivos de E/S antes mencionados, la memoria y el procesador. La memoria es el lugar donde se guardan los programas mientras se ejecutan; también contiene los datos requeridos por éstos. En la figura 1.8, la memoria se encuentra en las dos placas pequeñas, y cada pequeña placa de memoria contiene ocho circuitos integrados. La memoria de la figura 1.10 está construida con chips

Placa base: placa de plástico que contiene empaquetados de circuitos integrados o chips, incluyendo al procesador, cache, memoria y conectores para los dispositivos de E/S tales como redes o discos. Circuito integrado: también llamado chip. Dispositivo que combina desde docenas a millones de transistores. Memoria: área de almacenamiento en la cual se almacenan los programas cuando se están ejecutando y que contiene los datos que necesitan esos mismos programas.

18

Capítulo 1

Disco duro

Procesador

Abstracciones y tecnología de los computadores

Ventilador con cubierta

Espacio para DIMMs de memoria

Espacio para baterías

Placa base

Ventilador con cubierta

Lector de DVD

FIGURA 1.7 El portátil de la figura 1.5 por dentro. La caja delgada con la etiqueta blanca de la esquina izquierda es un disco duro SATA de 100GB, y la caja delgada metálica de la esquina derecha es un DVD. La batería se alojará en el hueco entre ellos. El hueco pequeño encima del hueco de la batería es para la memoria DIMM. La figura 1.8 muestra una visión más cercana de los DIMMs que se insertan en este portátil. En la parte superior de la batería y el DVD está la placa base, un circuito impreso que contiene la mayor parte de la electrónica del computador. Los dos círculos delgados de la mitad superior son dos ventiladores con sus cubiertas. El procesador es el rectángulo que puede verse justo debajo del ventilador de la izquierda. Foto por gentileza de OtherWorldComputing.com

Bajo la cubierta

19

DRAM. La DRAM es la memoria dinámica de acceso aleatorio (dynamic random access memory). Se usan conjuntamente varias DRAMs para contener las instrucciones y los datos de los programas. En contraste con las memorias de acceso secuencial, como las cintas magnéticas, la parte RAM del término DRAM significa que los accesos a memoria toman el mismo tiempo independientemente de la posición de memoria que se lea.

Memoria de acceso aleatorio dinámica (DRAM): memoria cons-

1.3

truida como un circuito integrado, que provee acceso aleatorio a cualquier posición.

DIMM (módulo de memoria de dos líneas): pequeña tarjeta que contiene chips DRAM en ambas caras. Los SIMMs tienen DRAMs en una sola cara.

FIGURA 1.8 La memoria se aprecia con una vista cercana de la parte inferior del portátil. La memoria principal se aloja en una o más pequeñas tarjetas mostradas a la izquierda. El hueco para la batería se puede apreciar a la derecha. Los chips de DRAM se colocan en esas tarjetas (llamadas DIMM, dual inline memory modules o modulos de memoria en línea duales) y se enchufan a los conectores. Foto por gentileza de OtherWorldComputing.com

El procesador es la parte activa de la placa y se encarga de seguir las instrucciones de un programa al pie de la letra. Suma números, comprueba números, indica la activación de dispositivos de E/S, etc. El procesador es el cuadrado grande que se encuentra debajo del ventilador y está cubierto por un disipador térmico, en la parte izquierda de la figura 1.7. A veces, al procesador se le llama CPU, porque el término suena más “oficial”: unidad central de proceso (central processing unit, CPU). Adentrándonos aún más en el hardware, la figura 1.9 revela detalles del procesador. El procesador comprende dos componentes principales: el camino de datos (datapath) y el control, la fuerza y el cerebro del procesador, respectivamente. El camino de datos realiza las operaciones aritméticas. El control indica al camino de datos, a la memoria y a los dispositivos de E/S lo que deben hacer, de acuerdo con la voluntad de las instrucciones del programa. El capítulo 4 explica el camino de datos y el control para obtener un diseño con mejores prestaciones. Introducirse en las profundidades de cualquier componente del hardware supone comprender lo que hay en la máquina. Dentro del procesador hay otro tipo de memoria: la memoria cache. La memoria cache es una memoria pequeña

Unidad central de proceso (CPU): también llamada procesador. Es la parte activa de un computador; contiene los caminos de datos y el control que suma y comprueba números, indica a los dispositivos de E/S que se activen, etc.

Camino de datos: componente del procesador que ejecuta las operaciones aritméticas. Control: componente del procesador que gobierna el camino de datos, la memoria y los dispositivos de E/S según a las instrucciones del programa. Memoria cache: memoria rápida y pequeña que actúa como un búfer para otra memoria mayor y más lenta.

20

Capítulo 1

Abstracciones y tecnología de los computadores

HT PHY, enlace 1 E/S lenta Fusibles

HT PHY, enlace 2

FPU de 128 bits

Cache L3 compartida de 2 MB

Carga/ almace- Cache L1 Cache namiento de datos L2 de Ejecución Control 512KB de la L2 Captura/de codificación Cache L1 de /saltos instr.

Núcleo 2

D D R

Puente norte

HT PHY, enlace 3

P H Y

Núcleo 4

Núcleo 3

HT PHY, enlace 4 E/S lenta Fusibles

FIGURA 1.9 Interior del chip del microprocesador AMD Barcelona. La parte izquierda es una microfotografía de un chip procesador AMD Barcelona y la parte derecha muestra los principales bloques del procesador. Este chip tiene 4 procesadores o núcleos. El microprocesador del portátil de la figura 1.7 tiene dos núcleos, Intel Core 2 Duo.

Memoria de acceso aleatorio estática (SRAM): también es una memoria construida como un circuito integrado, pero es más rápida y menos densa que la DRAM.

Abstracción: modelo que oculta temporalmente los detalles de menor nivel de los sistemas de computadores para facilitar el diseño de sistemas sofisticados. Arquitectura del repertorio de instrucciones (también llamada arquitectura): en una máquina, es una interfaz abstracta entre el hardware y el nivel más bajo del software que incluye toda la información necesaria para escribir un programa en lenguaje máquina que se ejecutará correctamente, e incluye continúa...

y rápida que actúa como un búfer para la memoria DRAM. (Esta definición no técnica de cache es una buena forma de ocultar detalles que se verán posteriormente.) La memoria cache se construye usando una tecnología de memoria diferente, memoria de acceso aleatorio estático (static random acces memory, SRAM). La SRAM es más rápida pero menos densa, y por lo tanto, más cara que la DRAM (véase capítulo 5). Se observa fácilmente un aspecto común en las descripciones tanto del software como del hardware: penetrar en las profundidades revela más información, o a la inversa, los detalles de bajo nivel se ocultan para ofrecer un modelo más simple a los niveles más altos. El uso de estos niveles o abstracciones es una técnica fundamental para diseñar computadores muy complejos. Una de las abstracciones más importantes es la interfaz entre el hardware y el nivel más bajo del software. Por su importancia recibe un nombre especial: arquitectura del repertorio de instrucciones (instruction set architecture), o simplemente arquitectura, de una máquina. La arquitectura del repertorio de instrucciones incluye todo lo que los programadores puedan necesitar para construir un programa correcto en lenguaje máquina binario, incluidas las instrucciones, los dispositivos de E/S, etc. Típicamente, el sistema operativo se encarga de realizar los detalles de las operaciones de E/S, de asignación de memoria y de otras funciones de bajo nivel del sistema, de manera que los programadores de aplicaciones no necesitan preocuparse de

1.3

21

Bajo la cubierta

esos detalles. La combinación del repertorio de instrucciones básico y la interfaz del sistema operativo proporcionados para los programadores de aplicaciones se llama la interfaz binaria de las aplicaciones (application binary interface, ABI). Una arquitectura del repertorio de instrucciones permite a los diseñadores de computadores hablar de las funciones independientemente del hardware que las lleva a cabo. Por ejemplo, se puede hablar de las funciones de un reloj digital (mantener y mostrar la hora, establecer alarmas) independientemente de la circuitería del reloj (cristal de cuarzo, pantalla LED, botones de plástico). Los diseñadores de computadores hacen una distinción entre arquitectura e implementación de una arquitectura: una implementación es el hadware que obedece a una abstracción arquitectónica. Estas ideas nos llevan a otra Idea clave.

...continuación las instrucciones, los registros, el acceso a memoria, la E/S, etc.

Interfaz binaria de aplicación (ABI): es la porción del repertorio de instrucciones correspondiente al usuario más la interfaz del sistema operativo que usan los programadores de aplicaciones. Define un estándar para la portabilidad binaria entre computadores.

Implementación: Hardware que cumple la abstracción de una arquitectura.

Tanto el hardware como el software están estructurados en niveles jerárquicos, cada uno de los cuales oculta detalles al nivel superior. Mediante este principio de abstracción los diseñadores de hardware y de software pueden enfrentarse a la complejidad de los computadores. Una interfaz clave entre los niveles de abstracción es la arquitectura del repertorio de instrucciones: la interfaz entre el hardware y el software de bajo nivel. Esta interfaz abstracta permite muchas implementaciones de costes y prestaciones diferentes para ejecutar programas idénticos.

Un lugar seguro para los datos Hasta ahora hemos visto cómo se pueden introducir datos, hacer cálculos con ellos y mostrarlos en pantalla. No obstante, si se fuera la corriente del computador, lo perderíamos todo porque su memoria interna es volátil; esto es, cuando se va la corriente, “olvida” su contenido. En cambio, un DVD no pierde la película grabada cuando se apaga el aparato, porque es una tecnología de memoria no volátil. Para distinguir entre la memoria usada para almacenar programas mientras se ejecutan y esta memoria no volátil, usada para guardar programas entre ejecuciones, se utiliza el término memoria principal para la primera y memoria secundaria para la última. Las DRAMs predominan como memoria principal desde 1975,

IDEA clave Memoria volátil: almacenamiento, tal como la DRAM, que solo guarda los datos si está recibiendo alimentación eléctrica. Memoria no volátil: forma de memoria que guarda los datos incluso en ausencia de alimentación eléctrica y que se usa para almacenar los programas entre ejecuciones. Los discos magnéticos son memoria no volátil, mientras que la memoria DRAM no lo es.

Memoria primaria (memoria principal): es la memoria volátil que se usa para almacenar los programas cuando se están ejecutando; típicamente se compone de DRAM en los computadores actuales.

Memoria secundaria: memoria no volátil usada para almacenar los continúa...

22

Capítulo 1

...continuación programas y datos entre ejecuciones; típicamente se compone de discos magnéticos en los computadores actuales.

pero los discos magnéticos predominan como memoria secundaria desde 1965. La memoria Flash, una memoria semiconductora no volátil, es utilizada en sustitución de discos móviles en dispositivos tales como teléfonos móviles y está reemplazando a gran velocidad a los discos en reproductores de música e incluso en portátiles. Tal como muestra la figura 1.10, un disco duro magnético consiste en una colección de platos que giran a una velocidad de entre 5400 y 15 000 revoluciones por minuto. Los discos de metal están recubiertos de material magnético grabable en ambas caras, similar al material de las cintas de casete o de vídeo. Para leer y escribir información en un disco duro, sobre cada superficie hay un brazo móvil que en su extremo tiene una pequeña bobina electromagnética llamada cabezal de lectura/escritura. El disco entero está sellado permanentemente para controlar el ambiente de su interior y, además, para permitir que a los cabezales del disco estén mucho más cerca de la superficie del disco. Hoy en día, los diámetros de los discos duros varían aproximadamente en un factor de 3, desde menos de 1 pulgada hasta 3.5 pulgadas, y su tamaño se ha ido reduciendo a lo largo de los años para caber en nuevos aparatos; las estaciones de trabajo servidoras, los computadores personales, los portátiles, los computadores de bolsillo y las cámaras digitales han inspirado nuevos formatos de discos. Tradicionalmente, los discos más grandes tienen mejores prestaciones, mientras que los más pequeños tienen un menor coste; sin embargo, el mejor

Disco magnético (disco duro): forma de memoria secundaria no volátil compuesta de platos rotatorios recubiertos con un material de grabación magnético. Memoria Flash: memoria semiconductora no volátil. Es más barata y más lenta que la DRAM, pero más cara y más rápida que los discos magnéticos.

Abstracciones y tecnología de los computadores

FIGURA 1.10

Disco donde se muestran 10 platos y los cabezales de escritura/lectura.

Bajo la cubierta

23

coste por gigabyte varía. Aunque la mayoría de los discos duros están dentro del computador (como en la figura 1.7), también pueden estar conectados mediante interfaces externas tales como USB. El uso de componentes mecánicos hace que el tiempo de acceso de los discos magnéticos sea mucho más lento que el de las DRAMs. Típicamente, el tiempo de acceso de los discos es de unos 5 a 20 milisegundos, mientras que el de las DRAMs está entre los 50 y los 70 nanosegundos, por lo que éstas últimas son unas 100 000 veces más rápidas que los primeros. En contrapartida, el coste por megabyte de los discos es muy inferior al de las DRAMs, ya que los costes de producción de una determinada capacidad en disco son más bajos que para la misma capacidad en circuitos integrados. En 2008, el coste por megabyte de los discos era unas 30 a 100 veces más barato que el de las DRAMs. Por lo tanto, hay tres diferencias principales entre discos magnéticos y memoria principal: los discos son no volátiles porque son magnéticos, tienen un tiempo de acceso más lento porque son mecánicos y son más baratos por megabyte porque tienen una gran capacidad de almacenaje con un coste moderado. Ha habido muchos intentos de desarrollar una tecnología más barata que la DRAM y más cara que los discos para llenar el hueco entre ambos, pero la mayoría han fracasado. Los contendientes no fueron nunca capaces de hacer llegar el producto al mercado en el momento adecuado. Cuando un nuevo producto podía estar listo para comercializarse, DRAMs y discos habían continuado su evolución, los costes habían disminuido y el producto rival quedó obsoleto inmediatamente. Sin embargo, la memoria Flash es un serio competidor. Esta memoria semiconductora es no volátil, al igual que los discos, tiene aproximadamente el mismo ancho de banda y la latencia es 100 a 1000 veces más rápida que la del disco. A pesar de que en 2008 el coste por gigabyte de una memoria Flash era de 6 a 10 veces superior al del disco, este tipo de memoria se ha vuelto muy popular en cámaras y reproductores de música portátiles porque viene con capacidades mucho menores, es más dura y es más eficiente en cuanto a consumo de potencia que los discos. Al contrario que en las DRAM y los discos, la memoria Flash se desgasta después de 100 000 a 1 000 000 escrituras. Así, el sistema de ficheros debe llevar cuenta del número de escrituras y debe tener una estrategia para evitar la pérdida de lo almacenado, como por ejemplo, copiar en otro soporte los datos más importantes. Las memorias Flash se describen con mayor detalle en el capítulo 6. Aunque los discos duros no son extraíbles (de quita y pon), existen varias tecnologías de almacenamiento en uso que sí lo son, entre ellas las siguientes:

Gigabyte: tradicionalmente 1 073 741 824 (230) bytes, aunque algunos sistemas de almacenamiento secundario y de comunicaciones lo han redefinido a 1 000 000 000 (109) bytes. De forma similar, un megabyte puede ser 220 o 106 dependiendo del contexto.

1.3



Los discos ópticos, que incluyen tanto discos compactos (CDs) como discos de vídeo digital (DVDs), constituyen la forma más común de almacenamiento extraíble. El disco óptico Blu-Ray (BD) es el heredero natural del DVD.



Las tarjetas de memoria extraíbles basadas en memoria FLASH se conectan mediante una conexión USB (Universal Serial Bus, Bus serie universal) y se usan para transferir archivos.



Las cintas magnéticas sólo proporcionan un acceso en serie lento y se han usado para realizar copias de seguridad de discos, pero actualmente esta técnica es reemplazada habitualmente por la duplicación de discos.

24

Capítulo 1

Abstracciones y tecnología de los computadores

La tecnología de los discos ópticos funciona de una manera totalmente diferente a la de los discos duros. En un CD, los datos se graban en forma de espiral, y los bits individuales se graban quemando pequeños hoyos —de aproximadamente 1 micra (10-6 metros) de diámetro— en la superficie del disco. El disco se lee radiando una luz láser en la superficie del CD y determinando si hay un hoyo o una superficie plana (reflectante) cuando se examina la luz reflejada. Los DVDs usan el mismo método para hacer rebotar un rayo láser sobre una serie de hoyos y superficies planas; pero además, el rayo láser se puede enfocar sobre múltiples capas y el tamaño de cada bit es mucho menor, lo cual incrementa significativamente la capacidad. En Blu-Ray se utiliza un láser con longitud de onda más corta que disminuye el tamaño de los bits y, por lo tanto, incrementa la capacidad. Las grabadoras de CD y DVD de los computadores personales usan un láser para hacer los hoyos en la capa de grabación de la superficie del CD o del DVD. Este proceso de escritura es relativamente lento: desde unos minutos (para un CD completo) hasta cerca de 10 minutos (para un DVD completo). Por ello, para grabar cantidades grandes se usa una técnica de planchado diferente, que sólo cuesta unos pocos céntimos por cada CD o DVD. Los CDs y DVDs regrabables tienen una superficie de grabación diferente, constituida por un material reflectante cristalino; los hoyos se forman de manera que no son reflectantes, al igual que en los CDs o DVDs de una sola escritura. Para borrar un CD o DVD regrabable, la superficie se calienta y enfría lentamente, permitiendo un proceso de templado que devuelve a la capa de grabación su estructura cristalina. Los discos regrabables son más caros que los de una sola escritura; para discos de solo lectura —usados para distribuir software, música o películas— tanto el coste del disco como el de la grabación son mucho menores.

Comunicación con otros computadores Ya hemos explicado cómo se pueden introducir, calcular, mostrar y guardar datos, pero aún nos queda por ver un elemento presente en los computadores de hoy en día: las redes de computadores. Del mismo modo que el procesador de la figura 1.4 se conecta con la memoria y los dispositivos de E/S, las redes conectan computadores enteros y permiten que los usuarios aumenten su capacidad de computación mediante la comunicación. Las redes se han hecho tan populares que se han convertido en la columna vertebral de los sistemas informáticos actuales. Una máquina nueva sin una interfaz para red sería ridícula. Los computadores en red tienen algunas ventajas importantes: ■ Comunicación: la información se intercambia entre computadores a gran velocidad. ■ Compartición de recursos: en lugar de tener dispositivos de E/S para cada computador, los computadores de la red pueden compartir sus dispositivos. ■ Acceso no local: conectando computadores separados a grandes distancias, los usuarios no necesitan estar cerca del computador que están usando. Las redes varían en longitud y rendimiento, y el coste de comunicación aumenta en función de la velocidad de comunicación y de la distancia a la que debe viajar la información. Quizá la red más popular sea Ethernet. Su longitud está limitada a un kilómetro y transfiere hasta 10 gigabytes por segundo. Por su longitud y velocidad,

1.3

Ethernet es útil para conectar computadores de un mismo edificio, y de ahí que sea un buen ejemplo de lo que genéricamente se llaman red de área local, o LAN (local area network). Las redes de área local se interconectan mediante conmutadores que pueden proporcionar también servicios de encaminamiento y de seguridad. Las redes de área extensa, o WAN (wide area network), que cruzan continentes, son la espina dorsal de Internet, que soporta la World Wide Web; se basan típicamente en fibras ópticas y son gestionadas por empresas de telecomunicaciones. Las redes han cambiado la fisonomía de la computación en los últimos 25 años, tanto por haberse extendido por todas partes como por su expectacular incremento en prestaciones. En la década de 1970, muy pocas personas tenían acceso al correo electrónico, e Internet y la Web no existían; así, el envío físico de cintas magnéticas era la principal manera de transferir grandes cantidades de datos entre dos sitios. En esa misma década, las redes de área local eran casi inexistentes, y las pocas redes de gran ámbito que existían tenían una capacidad limitada y eran de acceso restringido. A medida que la tecnología de las redes mejoraba, se hizo mucho más barata y su capacidad aumentó. Por ejemplo, la primera tecnología de red de área local estándar desarrollada hace 25 años era una versión de Ethernet que tenía una capacidad máxima (también llamado ancho de banda) de 10 millones de bits por segundo, y típicamente era compartida por decenas, si no cientos, de computadores. Hoy, la tecnología de las redes de área local ofrecen una capacidad de entre 100 millones de bits por segundo a 10 gigabites por segundo, y es frecuentemente compartida por unos pocos computadores como mucho. La tecnología de las comunicaciones ópticas permitió un crecimiento similar en la capacidad de las redes de área extensa desde cientos de kilobits a gigabits, y desde cientos de computadores conectados a millones de computadores conectados entre sí formando una red mundial. El espectacular incremento en la utilización de la redes junto con el incremento en su capacidad han hecho que la tecnología de red sea central en la revolución de la información de los últimos 25 años. Recientemente, otra innovación en las redes está dando nuevas formas a la manera en que los computadores se comunican. La tecnología inalámbrica se está utilizando ampliamente, y actualmente muchos computadores portátiles incorporan esta tecnología. La capacidad de hacer una transmisión por radio con la misma tecnología de semiconductores de bajo coste (CMOS) que se usa para la memoria y los microprocesadores ha posibilitado una mejora significativa en coste, lo que ha conducido a una explosión en su utilización. Las tecnologías inalámbricas, llamadas 802.11 según el estándar IEEE, permiten tasas de transmisión entre 1 y casi 100 millones de bits por segundo. La tecnología inalámbrica es bastante diferente de la redes basadas en cables, puesto que todos los usuarios de una misma área comparten las ondas radiofónicas. ■

25

Bajo la cubierta

Red de área local (LAN): red diseñada para transportar datos en un área confinada geográficamente, típicamente dentro de un mismo edificio.

Red de área extensa (WAN): red que se extiende a lo largo de cientos de kilómetros y que puede abarcar un continente.

La memoria DRAM semiconductora y el almacenamiento en disco difieren Autoevaluación significativamente. Describa la diferencia fundamental para cada uno de los siguientes aspectos: volatibilidad, tiempo de acceso y coste.

Tecnologías para construir procesadores y memorias Los procesadores y la memoria han mejorado a un ritmo increíble porque los diseñadores de computadores han aprovechado la tecnología electrónica más avanzada para intentar ganar la carrera de diseñar un computador mejor. La figura 1.11

26

Capítulo 1

Abstracciones y tecnología de los computadores

muestra las tecnologías que se han ido usando a lo largo del tiempo, con una estimación relativa de las prestaciones por unidad de coste de cada una de ellas. La sección 1.7 explora la tecnología que ha alimentado la industria de los computadores desde 1975 y que presumiblemente lo continuará haciendo en el futuro. Como esta tecnología determina lo que los computadores podrán hacer y la rapidez con que evolucionarán, creemos que todos los profesionales del sector deberían estar familiarizados con los fundamentos de los circuitos integrados.

Válvula de vacío: componente electrónico, predecesor del transistor, que consiste en tubo de vidrio hueco de entre 5 y 10 cm de longitud, en el cual se ha eliminado la mayor cantidad de aire posible, y que usa un haz de electrones para transferir datos. Transistor: interruptor de encendido/apagado controlado por una señal eléctrica.

Circuito integrado a muy gran escala (VLSI): dispositivo que contiene cientos de miles a millones de transistores.

Año

Tecnología usada en los computadores

Prestaciones relativas por unidad de coste

1951

Válvula de vacío

1965

Transistor

0,000,001 0,000,035

1975

Circuito integrado

0,000,900

1995

Circuito integrado a muy gran escala

2 400 000

2005

Circuito integrado a ultra gran escala

6 200 000 000

FIGURA 1.11 Prestaciones relativas por unidad de coste de las tecnologías usadas en los computadores a lo largo del tiempo. Fuente: Museo de los computadores, Boston, donde 2005 ha sido extrapolado por los autores.

Un transistor es simplemente un interruptor de encendido/apagado, controlado eléctricamente. El circuito integrado combina desde docenas hasta cientos de ellos en un solo chip. Para describir el enorme incremento de integración desde cientos a millones de transistores, se utiliza el adjetivo a muy gran escala, que da lugar a la expresión circuito a muy gran escala de integración, o circuito VLSI (very large scale of integration). Esta velocidad de aumento de la integración ha sido notablemente estable. La figura 1.12 muestra el incremento de la capacidad de las DRAM desde 1977. La industria, prácticamente, ha cuadruplicado la capacidad cada 3 años, ¡un incremento total que excede las 16 000 veces en unos 20 años! Este incremento en el número de transistores en un circuito integrado se conoce popularmente como la ley de Moore, que establece que la capacidad en transistores se dobla cada 18 ó 24 meses. La ley de Moore es el resultado de una predicción del crecimiento en la capacidad de los circuitos integrados hecha por Gordon Moore, uno de los fundadores de Intel, durante los años sesenta del siglo pasado. El mantenimiento de esta tasa de progreso durante al menos 40 años ha requerido increíbles innovaciones en las técnicas de manufacturación. En la sección 1.7 abordamos cómo se fabrican los circuitos integrados.

1.4

Prestaciones

1.4

Evaluar las prestaciones de un sistema puede ser un desafío. La envergadura y complejidad de los sistemas software modernos, junto con la amplia variedad de técnicas para mejorar las prestaciones empleadas por los diseñadores de hardware, han hecho que su evaluación sea mucho más difícil.

1.4

Prestaciones

1 000 000

Capacidad en Kbits

100 000 16M 10 000

128M

256M 512M

64M

4M 1M

1000

256K 64K

100 16K

10 1976 1978 1980 1982 1984 1986 1988 1990 1992 1994 1996 1998 2000 2002 Año de introducción

FIGURA 1.12 Crecimiento de la capacidad por chip DRAM a lo largo del tiempo. El eje y se mide en Kbits, donde K = 1024 (210). La industria DRAM ha cuadriplicado la capacidad casi cada 3 años, un incremento del 60% por año, durante 20 años. En años recientes, la tasa se ha frenado un poco y está más cerca de doblarse cada dos años o cuadruplicarse cada cuatro años.

Por supuesto, a la hora de elegir entre diferentes computadores, las prestaciones son casi siempre un atributo importante. Unas medidas precisas y una comparación entre diferentes máquinas son decisivas para los compradores y, por lo tanto, para los diseñadores. Los vendedores de computadores también conocen este hecho. A menudo, los vendedores desean que los compradores vean las mejores cualidades de su máquina, independientemente de si esas cualidades reflejan las necesidades de las aplicaciones de los compradores. El resto de esta sección describe diferentes maneras de determinar las prestaciones. Después se describen las métricas para medir las prestaciones desde el punto de vista del usuario y del diseñador de computadores. También se analiza cómo estas métricas están relacionadas y se presenta la ecuación clásica sobre las prestaciones del procesador que será utilizada a lo largo del texto.

Definición de prestaciones Cuando se dice que un computador tiene mejores prestaciones que otro, ¿qué se quiere decir? A pesar de que esta cuestión podría parecer simple, una analogía con los pasajeros de un avión muestra lo sutil que puede ser la cuestión de las prestaciones. La figura 1.13 muestra algunos aviones típicos de pasajeros, junto con su velocidad de crucero, autonomía y capacidad. Si se quisiera saber cuál de los aviones de esa tabla tiene las mejores prestaciones, primero se tendría que definir lo que se entiende por prestaciones. Por ejemplo, considerando diferentes medidas de las prestaciones, se observa que el avión con la mayor velocidad de crucero es el Concorde, el avión con la mayor autonomía es el DC-8, y el avión con la mayor capacidad es el 747. Supongamos que se definen las prestaciones en términos de velocidad. Esto aún deja dos posibles definiciones. Se podría definir el avión más rápido como aquel que posea la velocidad de crucero más alta, es decir, el que lleve a un pasajero desde un punto a otro en el mínimo tiempo. Sin embargo, si se estuviera inte-

27

28

Capítulo 1

Abstracciones y tecnología de los computadores

Capacidad

Autonomía

Velocidad de crucero (km/h)

Productividad pasajeros (pasajeros x km/h)

Boeing 777

375

4630

980

367 500

Boeing 747

470

4150

980

460 600

BAC/Sud Concorde

132

4000

2175

287 100

Douglas DC-8-50

146

8720

875

127 750

Avión

FIGURA 1.13 Capacidad, autonomía y velocidad de algunos aviones comerciales. La última columna muestra la razón a la que el avión transporta los pasajeros, que es la capacidad multiplicada por la velocidad de crucero (ignorando la autonomía y los tiempos de despegue y aterrizaje).

Tiempo de respuesta (tiempo de ejecución): tiempo total requerido por un computador para completar una terea, incluidos los accesos a disco, los accesos a memoria, las operaciones E/S, sobrecarga del sistema operativo, tiempo de ejecución, etc.

Productividad, también llamada ancho de banda: Otra medida de las prestaciones, se define como el número de tareas que se completan por unidad de tiempo.

resado en transportar 450 pasajeros desde un punto a otro, el 747 sería claramente el más rápido, como muestra la última columna de la figura. Análogamente, las prestaciones de un computador se pueden definir de varias maneras diferentes. Si se estuviera ejecutando un programa en dos estaciones de trabajo diferentes, se podría decir que la más rápida es la que acaba el trabajo primero. Si se estuviera dirigiendo un centro de computación que tiene dos grandes computadores de tiempo compartido, que ejecutan trabajos enviados por diferentes usuarios, se diría que el computador más rápido es aquel que ha completado más trabajos durante el día. Los usuarios individuales de computadores están más interesados en reducir el tiempo de respuesta —el tiempo entre el inicio y el final de una tarea— también referido como tiempo de ejecución. Los directivos de los centros de computación están habitualmente interesados en incrementar la productividad, que es la cantidad total de trabajo hecho en un cierto tiempo. Por lo tanto, en la mayoría de los casos, se necesitarán diferentes métricas de prestaciones, así como diferentes conjuntos de aplicaciones para probar las prestaciones de los computadores de sobremesa frente a los servidores y los computadores empotrados, que están más orientados al tiempo de respuesta, frente a los servidores, más orientados a la productividad.

Productividad y tiempo de respuesta

EJEMPLO

RESPUESTA

Si en un sistema informático se realizan los siguientes cambios, ¿qué ocurre: aumenta la productividad, se reduce el tiempo de respuesta o ambos a la vez? 1. Se reemplaza el procesador de un computador por una versión más rápida. 2. Se añaden procesadores a un sistema que usa múltiples procesadores para diferentes tareas, por ejemplo, buscar en la World Wide Web. Reducir el tiempo de respuesta de un sistema casi siempre mejora la productividad. Así, en el primer caso, ambos mejoran. En el segundo caso, ninguna de las tareas consigue que su trabajo se haga más rápido, por lo tanto sólo la productividad se verá incrementada. Sin embargo, si la demanda de procesadores en el segundo caso fuera casi tan alta como la productividad, el sistema podría verse forzado a poner en cola algunas peticiones. En este caso, el incremento de la productividad podría mejorar también el tiempo de respuesta, ya que eso reduciría el tiempo de espera en la cola. Por lo tanto, en muchos sistemas informáticos reales, cambiar el tiempo de ejecución o la productividad afecta a menudo al otro factor.

1.4

29

Prestaciones

En nuestra exposición de las prestaciones de los computadores, en los primeros capítulos nos ocuparemos principalmente del tiempo de respuesta. Para maximizar las prestaciones, lo que se desea es minimizar el tiempo de respuesta o tiempo de ejecución de alguna tarea. Por lo tanto, las prestaciones y el tiempo de ejecución de un computador X se pueden relacionar de la siguiente manera: 1 Prestaciones X = ----------------------------------------------------Tiempo de ejecución X Así, si las prestaciones de una máquina X son mayores que las prestaciones de una máquina Y, se tiene: Prestaciones X > Prestaciones Y 1 1 ----------------------------------------------------- > ----------------------------------------------------Tiempo de ejecución X Tiempo de ejecución Y Tiempo de ejecución Y > Tiempo de ejecución X Esto significa que el tiempo de ejecución de Y es mayor que el de X, si X es más rápido que Y. Al tratar sobre el diseño de un computador, a menudo se desea relacionar cuantitativamante las prestaciones de dos máquinas diferentes. Usaremos la frase “X es n veces más rápida que Y” para indicar que: Prestaciones --------------------------------X- = n Prestaciones Y Si X es n veces más rápida que Y, entonces el tiempo de ejecución de Y es n veces mayor que el de X: Prestaciones X Tiempo de ejecución Y --------------------------------- = ----------------------------------------------------= n Prestaciones Y Tiempo de ejecución X

Prestaciones relativas

Si una máquina A ejecuta un programa en 10 segundos y una máquina B ejecuta el mismo programa en 15 segundos, ¿cuánto más rápida es A respecto de B? Se sabe que A es n veces más rápida que B si Prestaciones A

Tiempo de ejecución ------------------------------- = --------------------------------------------------B- = n Prestaciones B Tiempo de ejecución A

EJEMPLO RESPUESTA

30

Capítulo 1

Abstracciones y tecnología de los computadores

Así, la relación de las prestaciones es 15

----- = 1.5 10 y, por lo tanto, A es 1.5 veces más rápida que B. En el ejemplo anterior se podría decir también que la máquina B es 1.5 veces más lenta que la máquina A, ya que Prestaciones --------------------------------A- = 1.5 Prestaciones B lo que significa que Prestaciones --------------------------------A- = Prestaciones B 1.5 Por simplicidad, usaremos normalmente el término más rápido que cuando intentemos comparar máquinas cuantitativamente. Ya que las prestaciones y el tiempo de ejecución son recíprocos, aumentar las prestaciones requiere reducir el tiempo de ejecución. Para evitar la confusión potencial entre los términos aumentar y reducir, diremos normalmente “mejorar las prestaciones” o “mejorar el tiempo de ejecución” cuando queramos decir “aumentar las prestaciones” o “reducir el tiempo de ejecución”.

Medición de las prestaciones

Tiempo de ejecución de CPU (tiempo de CPU): tiempo real que la CPU emplea en computar una tarea específica.

Tiempo de CPU del usuario: tiempo de CPU empleado en el propio programa.

Tiempo de CPU del sistema: tiempo que la CPU emplea en realizar tareas del sistema operativo para el programa.

El tiempo es la medida de las prestaciones de un computador: el computador que ejecuta la misma cantidad de trabajo en el menor tiempo es el más rápido. El tiempo de ejecución de un programa se mide en segundos. Pero el tiempo puede ser definido de maneras diferentes, dependiendo de lo que se cuente. La definición más sencilla de tiempo se llama tiempo de reloj (wall clock time), tiempo de respuesta (response time) o tiempo transcurrido (elapsed time). Estos términos se refieren al tiempo total que tarda una tarea en completarse, e incluye los accesos a disco, los accesos a memoria, las actividades de entrada/salida (E/S) y la sobrecarga introducida por el sistema operativo. Sin embargo, a menudo los computadores son de tiempo compartido, y un procesador podría trabajar en diferentes programas simultáneamente. En estos casos, el sistema podría intentar optimizar la productividad más que tratar de minimizar el tiempo de ejecución de un programa concreto. Por lo tanto, a menudo se querrá distinguir entre el tiempo transcurrido y el tiempo que un procesador está trabajando para nosotros. El tiempo de ejecución de CPU, o simplemente tiempo de CPU, el cual reconoce esta distinción, es el tiempo que la CPU dedica a ejecutar una tarea concreta y no incluye el tiempo perdido en las actividades de E/S o en la ejecución de otros programas. (Sin embargo, hay que recordar que el tiempo de respuesta que percibirá un usuario será el tiempo transcurrido para el programa, no el tiempo de CPU). Además, el tiempo de CPU puede ser dividido en el tiempo de CPU consumido por el programa, llamado tiempo de CPU del usuario, y el tiempo de CPU consumido por el sistema operativo, llamado tiempo de CPU del sistema. La diferenciación entre los tiempos de CPU del usuario y del sistema es difícil de realizar de una manera precisa,

1.4

Prestaciones

31

ya que a menudo es complicado asignar responsabilidades a las actividades del sistema operativo para un programa de usuario más que a otro, y además los sistemas operativos presentan diferentes funcionalidades. Por coherencia, mantendremos la distinción entre las prestaciones basadas en el tiempo transcurrido y las basadas en el tiempo de ejecución de CPU. Usaremos el término prestaciones del sistema para referirnos al tiempo transcurrido en un sistema sin carga, y usaremos las prestaciones de la CPU para referirnos al tiempo de CPU. Nos centraremos en las prestaciones de la CPU, aunque nuestras argumentaciones sobre la manera de exponer las prestaciones pueden ser aplicadas para el tiempo total de ejecución o para el tiempo de CPU. Diferentes aplicaciones son sensibles a diferentes aspectos de las prestaciones de un Comprender computador. Muchas aplicaciones, especialmente aquellas que se ejecutan en servidolas prestaciones res, dependen en gran medida de las prestaciones de E/S, que a su vez depende tanto del hardware como del software, y el tiempo total transcurrido medido en tiempo de reloj de los programas (wall clock time) es la medida de interés. En algunos entornos de aplicación, el usuario puede preocuparse por la productividad, el tiempo de respuesta, o una compleja combinación de ambos (p. ej., la máxima productividad con un tiempo de respuesta en el peor caso). Para mejorar las prestaciones de un programa hay que tener una clara definición de cuáles son las métricas de las prestaciones importantes y entonces proceder a identificar problemas de prestaciones midiendo la ejecución del programa y buscando probables cuellos de botella. En los capítulos siguientes se describe cómo se buscan los cuellos de botella y se mejoran las prestaciones en varias partes del sistema. Aunque como usuarios de computadores nos importa el tiempo, cuando se examinan los detalles de una máquina es conveniente considerar las prestaciones según otras medidas. En particular, es posible que los diseñadores de computadores quieran considerar una máquina utilizando una medida relacionada con la rapidez con que el hardware realiza determinadas funciones básicas. Casi todos los computadores tienen un reloj que funciona a una frecuencia concreta y determina el momento en que tienen lugar los sucesos en el hardware. Estos intervalos discretos de tiempo se llaman ciclos de reloj (o tics, tics de reloj, periodos de reloj o ciclos de reloj). Los diseñadores llaman a la longitud del periodo de reloj tanto el tiempo de un ciclo de reloj completo (p. ej. 0.25 nanosegundos, 0.25 ns, 250 picosegundos, o 250 ps) como la frecuencia de reloj (p. ej. 4 gigahercios o 4 GHz), que es el inverso del periodo de reloj. En la siguiente sección formalizaremos la relación entre los ciclos de reloj de los diseñadores de circuitos y los segundos de los usuarios de computadores. 1. Suponga que se sabe que una aplicación que utiliza un cliente local y un servidor remoto está limitada por las prestaciones de la red. Para los cambios siguientes, determine si sólo mejora la productividad, si mejoran tanto el tiempo de respuesta como la productividad, o si no mejora ninguna de las dos. a. Entre el cliente y el servidor se añade un canal de red extra que aumenta la productividad total de la red y reduce el retardo para obtener acceso a la red (ya que ahora hay dos canales).

Ciclo de reloj: también llamado tic, tic de reloj, periodo de reloj, ciclo de reloj. El tiempo de un periodo de reloj, usualmente del reloj del procesador, avanza a velocidad constante. Periodo de reloj: longitud de cada ciclo de reloj.

Autoevaluación

32

Capítulo 1

Abstracciones y tecnología de los computadores

b. Se mejora el software de red, de modo que se reduce el retardo de comunicación en la red, pero no se incrementa la productividad. c. Se añade más memoria al computador. 2. Las prestaciones del computador C es 4 veces mejor que el del computador B, que ejecuta una aplicación dada en 28 segundos. ¿Cuánto tiempo emplea el computador C en ejecutar la aplicación?

Prestaciones de la CPU y sus factores Frecuentemente, diseñadores y usuarios miden las prestaciones usando métricas diferentes. Si se pudieran relacionar estas métricas, se podría determinar el efecto de un cambio en el diseño sobre las prestaciones observadas por el usuario. Como nos estamos restringiendo a las prestaciones de la CPU, la medida base de prestaciones será el tiempo de ejecución de la CPU. Una fórmula sencilla que relaciona las métricas más básicas (ciclos de reloj y tiempo del ciclo de reloj) con el tiempo de CPU es la siguiente: Tiempo de ejecución de = Ciclos de reloj de la × Tiempo del CPU para un programa CPU para el programa ciclo del reloj

Alternativamente, ya que la frecuencia de reloj es la inversa del tiempo de ciclo, de reloj de la CPU para el programa Tiempo de ejecución de = Ciclos ------------------------------------------------------------------------------------------------------CPU para un programa Frecuencia de reloj Esta fórmula pone de manifiesto que el diseñador de hardware puede mejorar las prestaciones reduciendo la longitud del ciclo de reloj o el número de ciclos de reloj requeridos por un programa. Además, como se verá en otros capítulos, el diseñador a menudo se enfrenta a un compromiso entre el número de ciclos requerido por un programa y la longitud de cada ciclo. Muchas de las técnicas que reducen el número de ciclos incrementan también su longitud.

Mejora de las prestaciones

EJEMPLO

Nuestro programa favorito se ejecuta en 10 segundos en el computador A, que tiene un reloj de 2 GHz. Intentaremos ayudar al diseñador de computadores a que construya una máquina B que ejecute el programa en 6 segundos. El diseñador ha determinado que es posible un incremento sustancial en la frecuencia de reloj, pero que este incremento afectará al resto del diseño de la CPU, haciendo que la máquina B requiera 1.2 veces los ciclos de reloj que la máquina A necesitaba para ejecutar el programa. ¿Qué frecuencia de reloj debería ser el objetivo del diseñador?

1.4

Prestaciones

Primero se calcularán los ciclos de reloj requeridos por el programa A:

33

RESPUESTA

Ciclos de reloj de CPU Tiempo de CPU A = ---------------------------------------------------------AFrecuencia de reloj A Ciclos del reloj de CPU 10 segundos = -----------------------------------------------------------A9 ciclos 2 × 10 -------------------segundo 9 ciclos 9 Ciclos de reloj de CPU A= 10 segundos × 2 × 10 -------------------- = 20 × 10 ciclos segundo

El tiempo de CPU para B se puede calcular utilizando la misma fórmula: 1.2 × Ciclos de reloj de CPU Tiempo de CPU B = -----------------------------------------------------------------------AFrecuencia de reloj B 1.2 × 20 × 10 ciclos 6 segundos = -----------------------------------------------Frecuencia de reloj B 9

× 20 × 10 ciclos 4 × 10 ciclos Frecuencia de reloj B = 1.2 ------------------------------------------------ = ------------------------------- = 4 GHz 6 segundos segundo 9

9

Por lo tanto, la máquina B necesitará el doble de la frecuencia de reloj de la máquina A para ejecutar el programa en 6 segundos.

Prestaciones de las instrucciones Las ecuaciones de los ejemplos previos no incluyen ninguna referencia al número de instrucciones ejecutadas por el programa. En el siguiente capítulo veremos qué aspecto tienen las instrucciones que forman un programa. Sin embargo, debido a que el compilador evidentemente genera las instrucciones que se deben ejecutar, y la máquina ha de ejecutarlas para que el programa funcione, el tiempo de ejecución debe depender del número de instrucciones del programa: una manera de pensar en el tiempo de ejecución es que éste es igual al número de instrucciones ejecutadas multiplicado por el tiempo medio por instrucción. Por lo tanto, el número de ciclos de reloj requerido por un programa puede ser representado como: Ciclos de reloj de CPU = Instrucciones de un programa × Media de ciclos por instrucción El término ciclos de reloj por instrucción, que es el número medio de ciclos de reloj que una instrucción necesita para ejecutarse, es a menudo abreviado como CPI (clock cycles per instruction). Como instrucciones diferentes podrían

Ciclos de reloj por instrucción (CPI): número medio de ciclos de reloj por instrucción para un programa o fragmento de programa.

34

Capítulo 1

Abstracciones y tecnología de los computadores

necesitar un número de ciclos diferente dependiendo de lo que hacen, el CPI es una media de todas las instrucciones ejecutadas por el programa. El CPI proporciona una manera de comparar dos realizaciones diferentes de la misma arquitectura del repertorio de instrucciones, ya que el número de instrucciones (o número total de instrucciones) requeridas por un programa será, obviamente, el mismo.

Utilización de la ecuación de las prestaciones

EJEMPLO

RESPUESTA

Supongamos que se tienen dos realizaciones de la misma arquitectura de repertorio de instrucciones. La máquina A tiene un tiempo de ciclo de reloj de 250 ps y un CPI de 2.0 para un programa concreto, mientras que la máquina B tiene un tiempo de ciclo de 500 ps y un CPI de 1.2 para el mismo programa. ¿Qué máquina es más rápida para este programa? ¿Cuánto más rápida es?

Sabemos que cada máquina ejecuta el mismo número de instrucciones para el programa, y a este número le llamaremos I. Primero, hallaremos el número de ciclos para cada máquina. Ciclos de reloj de CPU A = I × 2.0 Ciclos de reloj de CPU B = I × 1.2 Ahora calcularemos el tiempo de CPU para cada máquina: Tiempo de CPU A = Ciclos de reloj de CPU A × Tiempo de ciclo A = I × 2.0 × 250 ps = 500 × I ps De la misma forma, para B: Tiempo de CPU B= I × 1.2 × 500 ps = 600 × I ps Claramente, la máquina A es más rápida. La diferencia en rapidez viene dada por la relación entre los tiempos de ejecución. Prestaciones CPU A Tiempo ejecución 600 × I ps ---------------------------------------------- = ---------------------------------------------B- = ------------------------ = 1.2 Prestaciones CPU B Tiempo ejecución A 500 × I ps Se puede concluir que, para este programa, la máquina A es 1.2 veces más rápida que la máquina B.

Prestaciones

35

Ahora se puede escribir la ecuación básica de las prestaciones en términos del número de instrucciones (número de instrucciones ejecutadas por el programa), del CPI y del tiempo de ciclo:

Número de instrucciones: número de instruc-

Tiempo de ejecución = Número de instrucciones × CPI × Tiempo de ciclo

ciones ejecutadas por el programa.

1.4

La ecuación clásica de las prestaciones de la CPU

o bien, dado que la frecuencia es el inverso del tiempo de ciclo: Número de instrucciones × CPI Tiempo de ejecución = -------------------------------------------------------------------------Frecuencia de reloj Estas fórmulas son especialmente útiles porque distinguen los tres factores claves que influyen en las prestaciones. Estas fórmulas se pueden utilizar para comparar dos realizaciones diferentes o para evaluar un diseño alternativo si se conoce el impacto en estos tres parámetros.

Comparación de segmentos de código

Un diseñador de compiladores está intentando decidir entre dos secuencias de código para una máquina en particular. Los diseñadores del hardware le han proporcionado los siguientes datos: CPI para instrucciones de esta clase

CPI

A

B

C

1

2

3

Para una declaración particular de un lenguaje de alto nivel, el diseñador del compilador está considerando dos secuencias de código que requieren el siguiente número de instrucciones: Total de instrucciones por clase Secuencia de código

A

B

C

1

2

1

2

2

4

1

1

¿Qué secuencia de código ejecuta el mayor número de instrucciones? ¿Cuál será la más rápida? ¿Cuál es el CPI para cada secuencia?

EJEMPLO

36

Capítulo 1

RESPUESTA

Abstracciones y tecnología de los computadores

La secuencia 1 ejecuta 2 + 1 + 2 = 5 instrucciones. La secuencia 2 ejecuta 4 + 1 + 1 = 6 instrucciones. Por lo tanto la secuencia 1 ejecuta el menor número de instrucciones. Se puede usar la ecuación para los ciclos de reloj de la CPU basada en el número de instrucciones y el CPI para encontrar el número total de ciclos para cada secuencia: n

Ciclos de reloj de la CPU =

¨ ( CPI × C ) i

i

i=1

Y se obtiene Ciclos de reloj de la CPU1 = ( 2 × 1 ) + ( 1 × 2 ) + ( 2 × 3 ) = 2 + 2 + 6 = 10 ciclos Ciclos de reloj de la CPU 2 = ( 4 × 1 ) + ( 1 × 2 ) + ( 1 × 3 ) = 4 + 2 + 3 = 9 ciclos

Por lo tanto, la segunda secuencia de código es más rápida, aun cuando ejecuta una instrucción más. Debido a que la secuencia de código 2 necesita menos ciclos totales, pero tiene más instrucciones, debe tener un CPI menor. Los valores del CPI pueden ser calculados así: Ciclos de reloj de la CPUCPI = --------------------------------------------------------------Número de instrucciones Ciclos de reloj de la CPU 10 CPI 1 = --------------------------------------------------------------1- = ------ = 2 5 Número de instrucciones 1 Ciclos de reloj de la CPU CPI 2 = --------------------------------------------------------------2- = 9 --- = 1.5 6 Número de instrucciones 2

IDEA clave

La figura 1.14 muestra las medidas básicas a diferentes niveles del computador y lo que está siendo medido en cada caso. Se puede ver cómo se combinan estos factores para obtener el tiempo de ejecución medido en segundos Segundos Instrucciones Ciclos de reloj Tiempo = Segundos ----------------------- = -------------------------------- × ----------------------------------- × -------------------------------Instrucción Ciclo de reloj Programa Programa Se ha de tener siempre en cuenta que la única medida completa y fiable de las prestaciones de un computador es el tiempo. Por ejemplo, cambiar el repertorio de instrucciones para disminuir el número total de las mismas podría llevar a una organización con un ciclo de reloj mayor que contrarrestara la mejora en el número de instrucciones. Igualmente, ya que el CPI depende de la mezcla de instrucciones, el código que ejecuta el menor número de instrucciones podría no ser el más rápido.

1.4

Componentes de las prestaciones

Unidades de medida

Tiempo de ejecución de CPU de un programa

Segundos por programa

Número de instrucciones

Número de instrucciones ejecutadas por el programa

Ciclos por instrucción (CPI)

Número medio de ciclos por instrucción

Tiempo de ciclo del reloj

Segundos por ciclo de reloj

FIGURA 1.14 de ellos.

37

Prestaciones

Componentes básicos de las prestaciones y cómo se mide cada uno

¿Cómo podemos determinar el valor de estos factores en la ecuación de las prestaciones? El tiempo de CPU se puede medir ejecutando un programa, y el tiempo de ciclo normalmente forma parte de la documentación de una máquina. El número de instrucciones y el CPI pueden ser más difíciles de obtener. Evidentemente, si la frecuencia de reloj y el tiempo de ejecución de CPU son conocidos, sólo necesitaremos conocer o el número de instrucciones, o el CPI, para determinar el otro parámetro que falta. El número de instrucciones se puede determinar utilizando herramientas que analizan la ejecución o usando un simulador de la arquitectura. Alternativamente, se pueden usar contadores hardware, los cuales forman parte de la mayoría de los procesadores con el objetivo de guardar una gran variedad de medidas, incluido el número de instrucciones ejecutadas por el programa, el CPI medio y, a menudo, las fuentes de pérdida de prestaciones. Ya que el número de instrucciones depende de la arquitectura, pero no de la implementación exacta, se puede medir el número de instrucciones sin conocer todos los detalles sobre la implementación. Sin embargo, el CPI depende de una amplia variedad de detalles del diseño de la máquina, que incluyen tanto el sistema de memoria como la estructura del procesador (como se verá en los capítulos 4 y 5), así como de la mezcla de tipos de instrucciones ejecutadas en una aplicación. Por lo tanto, el CPI varía según la aplicación, así como entre las diferentes realizaciones con el mismo repertorio de instrucciones. El ejemplo anterior muestra el peligro de usar un solo factor (número de instrucciones) para evaluar las prestaciones. Cuando se comparan dos máquinas, se deben considerar los tres componentes, los cuales se combinan para formar el tiempo de ejecución. Si algunos de los factores son idénticos, como la frecuencia de reloj en el ejemplo anterior, las prestaciones pueden determinarse comparando los factores diferentes. Ya que la mezcla de instrucciones modifica el CPI, tanto éste como el número de instrucciones deben ser comparados, aunque las frecuencias de reloj sean idénticas. En varios ejercicios se le pedirá que estudie una serie de mejoras en el computador y el compilador que afectan a la frecuencia de reloj, el CPI y el número de instrucciones. En la sección 1.8 se examinará una medida de las prestaciones habitual que no incorpora todos los términos y que por lo tanto puede inducir a error.

Mezcla de instrucciones: medida de la frecuencia dinámica de las instrucciones a lo largo de uno o varios programas.

38

Capítulo 1

Abstracciones y tecnología de los computadores

Comprender Las prestaciones de un programa depende del algoritmo, del lenguaje, del compilador, las prestaciones de la arquitectura y del hardware real. La siguiente tabla resume de qué manera estos de los programas componentes afectan a los distintos factores de la ecuación de las prestaciones. Componente hardware o software

¿A qué afecta? ¿Cómo?

Algoritmo

El algoritmo determina el número de instrucciones del programa Número de fuente ejecutadas y por lo tanto el número de instrucciones del instrucciones, posiblemente CPI procesador ejecutadas. El algoritmo puede también afectar al CPI, favoreciendo instrucciones más lentas o más rápidas. Por ejemplo, si el algoritmo utiliza más operaciones en punto flotante, tenderá a tener un mayor CPI.

Lenguaje de programación

Número de El lenguaje de programación afecta al número de instrucciones, instrucciones, CPI ya que las sentencias del lenguaje son traducidas a instrucciones del procesador, lo cual determina el número de instrucciones. Las características del lenguaje también pueden afectar al CPI; por ejemplo, un lenguaje con soporte para datos abstractos (p. ej. Java) requerirá llamadas indirectas, las cuales utilizarán instrucciones con un CPI mayor.

Compilador

Número de La eficiencia del compilador afecta tanto al número de instrucciones instrucciones, CPI como al promedio de los ciclos por instrucción, ya que el compilador determina la traducción de las instrucciones del lenguaje fuente a instrucciones del computador. El papel del compilador puede ser muy complejo y afecta al CPI de formas complejas.

Arquitectura del repertorio de instrucciones

Número de instrucciones, frecuencia de reloj, CPI

La arquitectura del repertorio de instrucciones afecta a los tres aspectos de las prestaciones de la CPU, ya que afecta a las instrucciones necesarias para realizar una función, al coste en ciclos de cada instrucción, y a la frecuencia del reloj del procesador.

Extensión: Aunque se podría esperar que el valor mínimo para el CPI es 1, como veremos en el capítulo 4, algunos procesadores buscan y ejecutan varias instrucciones en cada ciclo de reloj; para reflejar este hecho, alguno diseñadores invierten el CPI para obtener el IPC, instrucciones por ciclo. Si un procesador ejecuta una media de 2 instrucciones por ciclo, el IPC es 2 y, por lo tanto, el CPI es 0.5.

Autoevaluación

Una aplicación dada escrita en Java se ejecuta durante 15 segundos en un procesador. Se dispone de un nuevo compilador Java que requiere 0.6 de las instrucciones que necesitaba el viejo compilador. Desafortunadamente, el CPI se incrementa en 1.1. ¿Cuánto tardará la aplicación en ejecutarse utilizando el nuevo compilador? × 0.6 a. 15 ------------------ = 8.2 s2 1.1

b. 15 × 0.6 × 1.1 × 1.1 c. 15 -----------------------0.6

=

=

9.9 s 2

27.5 s2

1.5

1.5

El muro de la potencia

El muro de la potencia

1.5

La figura 1.15 ilustra el incremento en la frecuencia de reloj y el consumo de potencia de ocho generaciones de microprocesadores de Intel en los últimos 25 años. Tanto la frecuencia de reloj como el consumo de potencia crecieron de forma rápida durante décadas, y de forma más moderada recientemente. La razón de este crecimiento conjunto es que están correlacionados, y la razón de un crecimiento moderado más recientemente es que se han alcanzado los límites prácticos de disipación de potencia en los microprocesadores corrientes. La tecnología dominante en la fabricación de circuitos integrados es la tecnología CMOS (complementary metal oxide semiconductor). En CMOS, la fuente principal de disipación de potencia es la llamada potencia dinámica; es decir, potencia consumida en las transiciones. La disipación de potencia dinámica depende de la carga capacitiva de cada transistor, del voltaje aplicado y de la frecuencia de conmutación del transistor: Potencia = carga capacitiva × voltaje2 × frecuencia de conmutación La frecuencia de conmutación depende de la frecuencia de la señal de reloj. La carga capacitiva por transistor es una función del número de transistores conectados a una salida (llamado fanout) y de la tecnología, que determina la capacitancia de las conexiones y los transistores. ¿Cómo es posible que la frecuencia de la señal de reloj crezca un factor 1000 mientras la potencia crece sólo un factor 30? La potencia puede reducirse disminuyendo el voltaje, algo que ha ocurrido con cada nueva generación de la tecnología, ya que depende de forma cuadrática del voltaje. Típicamente, el voltaje se ha reducido un 3600

2000

2667

120 100

103

1000 Frecuencia de reloj 200

95

75.3

66

100 12.5

16

4.1

60

25 40

Potencia

10 3.3

80

4.9

10.1

29.1

Potencia (Vatios)

Frecuencia de reloj (MHz)

10000

20 0

Core 2 Kentsfield (2007)

Pentium 4 Prescott (2004)

Pentium 4 Willamette (2001)

Pentium Pro (1997)

Pentium (1993)

80486 (1989)

80386 (1985)

80286 (1982)

1

FIGURA 1.15 Variación de la frecuencia de reloj y la potencia en las últimas 8 generaciones en 25 años de microprocesadores x86. La arquitectura Pentium 4 dio un salto muy importante en la frecuencia de reloj y la potencia, pero menos importante en las prestaciones. La línea Pentium 4 se abandonó debido a los problemas térmicos del Prescott. La línea Core 2 vuelve a una segmentación más simple con frecuencias de reloj más bajas y varios procesadores por chip.

39

40

Capítulo 1

Abstracciones y tecnología de los computadores

15% con cada nueva generación. En 20 años, el voltaje ha disminuido desde 5V a 1V; este el motivo que explica que la potencia sólo se haya incrementado 30 veces.

Potencia relativa

EJEMPLO

RESPUESTA

Supongamos que desarrollamos un nuevo procesador más sencillo, cuya carga capacitiva es el 85% de la carga de un procesador anterior más complejo. Supongamos, además, que el voltaje es ajustable de modo que puede reducirse en un 15% respecto al procesador anterior, lo que ocasiona una disminución del 15% en la frecuencia de la señal de reloj. ¿Cuál es el impacto en la potencia dinámica? Potencia nuevo -------------------------------- = Potencia antiguo ( carga capacitiva × 0.85 ) × ( voltaje × 0.85 ) 2 × ( frecuencia × 0.85 ) ----------------------------------------------------------------------------------------------------------------------------------------------carga capacitiva × voltaje 2 × fremcuencia Así, la relación de potencias es 0.854 = 0.52 Por lo tanto, el nuevo procesador utiliza aproximadamente la mitad de potencia que el procesador antiguo. El problema actualmente es que disminuciones adicionales en el voltaje hacen que el transistor tenga demasiadas fugas, como grifos de agua que no cierran perfectamente. Hoy en día, aproximadamente el 40% del consumo de potencia es debido a la fuga de corriente. Si las pérdidas de los transistores continuasen creciendo, el proceso completo se volvería difícil de controlar. En un intento de afrontar el problema de la potencia, los diseñadores ya han adosado grandes dispositivos para incrementar la refrigeración, y las partes del chip que no se utilizan en un ciclo de reloj determinado se apagan. Aunque hay otras técnicas para refrigerar los chips y, en consecuencia, aumentar la potencia, por ejemplo a 300 W, estas son demasiado caras para poder ser incluidas en los computadores de sobremesa. Desde que los diseñadores de computadores se encontraron con el muro de la potencia, han necesitado una nueva forma de afrontarlo. Han elegido una forma de diseñar los microprocesadores diferente de la que se utilizó en los primeros 30 años. Extensión: Aunque la potencia dinámica es la principal fuente de disipación de potencia en CMOS, hay una disipación estática debida a las fugas de corriente que se producen incluso cuando el transistor está inactivo. Como se ha mencionado anteriormente, en 2008, la fuga de corriente es la responsable del 40% de la disipación de potencia. De este modo, un aumento en el número de transistores aumenta la potencia disipada, incluso si los transistores estuviesen siempre inactivos. Se están empleando diversas técnicas e innovaciones tecnológicas para controlar la fuga de corriente, pero es difícil, conseguir disminuciones adicionales del voltaje.

1.6

1.6

El gran cambio: el paso de monoprocesadores a multiprocesadores

El gran cambio: el paso de monoprocesadores a multiprocesadores

1.6

El muro de potencia ha forzado un cambio dramático en el diseño de microprocesadores. La figura 1.16 muestra la mejora en el tiempo de respuesta de programas para microprocesadores de computadores de sobremesa en los últimos años. Desde 2002, el factor de aumento ha disminuido desde un factor 1.5 por año hasta un factor menor que 1.2 por año. En lugar de continuar con la reducción del tiempo de respuesta de un único programa ejecutándose en un único procesador, desde 2006 todas las compañías de computadores personales y servidores están incorporando microprocesadores con varios procesadores por chip, con los cuales el beneficio es, a menudo, más patente en la productividad que en el tiempo de respuesta. Para eliminar la confusión entre las palabras procesador y microprocesador, los fabricantes utilizan el término “núcleo (core)” para referirse a un procesador, y a estos microprocesadores se les denomina habitualmente microprocesadores multinúcleo (multicore). Así, un microprocesador quadcore, es un chip que contiene cuatro procesadores o cuatro núcleos. La figura 1.17 muestra el número de procesadores (núcleos), potencia y frecuencias de reloj de varios microprocesadores recientes. La previsión de muchos fabricantes es doblar el número de núcleos por microprocesador en cada generación de tecnología de semiconductores, es decir, cada dos años (véase capítulo 7). En el pasado, los programadores contaban con las innovaciones en el hardware, la arquitectura y los compiladores para doblar las prestaciones de sus programas cada 18 meses sin tener que cambiar ninguna línea de código. En la actualidad, para obtener mejoras significativas en tiempos de respuesta, los programadores deben reescribir sus programas para extraer las ventajas de disponer de múltiples procesadores. Además, para alcanzar los beneficios históricos de ejecuciones más rápidas en nuevos microprocesadores, los programadores tendrán que continuar mejorando las prestaciones de sus códigos a medida que el número de núcleos de duplica. Para enfatizar la manera en que hardware y software cooperan, a lo largo del libro usamos la sección especial interfaz hardware/software, la primera de las cuales se incluye a continuación. Estos elementos resumen visiones importantes de esta interfaz crítica. El paralelismo siempre ha sido un elemento crítico para las prestaciones en computación, pero a menudo ha estado oculto. El capítulo 4 aborda la segmentación, una técnica elegante que permite ejecutar más rápidamente los programas mediante el solapamiento de la ejecución de las instrucciones. Este es un ejemplo del paralelismo a nivel de instrucciones, donde la naturaleza paralela del hardware está tan oculta que el programador y el compilador creen que el hardware ejecuta las instrucciones secuencialmente. Conseguir que los programadores estén pendientes del hardware paralelo y reescriban sus programas explícitamente para ser paralelos había sido la “tercera vía” de la arquitectura de computadores, para aquellas compañías que el pasado dependieron de este cambio de conducta fallido (véase sección 7.14 en el CD). Desde esta perspectiva histórica, resulta asombroso que la industria de las Tecnologías de la

41

Hasta ahora, la mayor parte del software ha sido como la música escrita para un solista; con la actual generación de microprocesadores, estamos teniendo una pequeña experiencia con duetos y cuartetos y con otros grupos de pocos intérpretes; pero conseguir un trabajo para una gran orquesta y un coro es un reto diferente. Brian Hayes, Computing in a Parallel Universe, 2007.

Interfaz hardware software

42

Capítulo 1

Abstracciones y tecnología de los computadores

Información (TI) al completo haya apostado porque los programadores finalmente se habituarán a una programación explícitamente paralela.

10 000

Intel Xeon, 3.6 GHz 64-bit Intel Xeon, 3.6 GHz 6505 AMD Opteron, 2.2 GHz 5764 Intel Pentium 4, 3.0 GHz 5364 4195 AMD Athlon, 1.6 GHz 2584 Intel Pentium III, 1.0 GHz Alpha 21264A, 0.7 GHz 1779 1267 Alpha 21264, 0.6 GHz

Performance (vs.VAX-11/780)

1000

993 Alpha 21164, 0.6 GHz 649 Alpha 21164, 0.5 GHz 481 Alpha 21164, 0.3 GHz 280 Alpha 21064A, 0.3 GHz 183 PowerPC 604, 0.1GHz 117 Alpha 21064, 0.2 GHz 80 HP PA-RISC, 0.05 GHz 51

100

IBM RS6000/540 MIPS M2000 MIPS M/120 10

Sun-4/260 VAX 8700

VAX-11/780

25%/año

20%

52%/año

24

18

13

9

5

1.5, VAX-11/785

0 1978

1980

1982

1984

1986

1988

1990

1992

1994

1996

1998

2000

2002

2004

2006

FIGURA 1.16 Aumento de las prestaciones de los procesadores desde la década de 1980. Esta gráfica muestra las prestaciones relativas al VAX 11/780 obtenidas con los programas de prueba SPECint (véase sección 1.8). Antes de mediados de la década de 1980 el aumento de las prestaciones se debía principalmente a la tecnología y de media suponía un 25% cada año. A partir de entonces hay un incremento del 52% durante varios años, atribuible a avances y nuevas ideas en la arquitectura y la organización. En 2002, este aumento había supuesto multiplicar por 7 las prestaciones. Las prestaciones de aplicaciones y cálculos orientados a punto flotante era todavía mayor. Desde 2002, el aumento de las prestaciones de un monoprocesador se redujo a un 20% anual debido a los límites en la disipación de potencia, el paralelismo a nivel de instrucciones disponible y la latencia de la memoria.

Producto

AMD Opteron X4 (Barcelona)

Sun Ultra SPARC T2 (Niagara 2)

Intel Nehalem

IBM Power 6

4

02

8

4.7 GHz

1.4 GHz

Núcleos por chip

4

Frecuencia de reloj

2.5 GHz

~ 2.5 GHz ?

Potencia microprocesador

120 W

~ 100 W ?

~ 100 W ?

94 W

FIGURA 1.17 Número de núcleos por chip, frecuencia de reloj y potencia en varios microprocesadores multinúcleo de 2008.

¿Por qué ha sido tan difícil conseguir que los programadores escribiesen programas explícitamente paralelos? La primera razón es que la programación paralela es por definición programación de prestaciones, que presenta una mayor dificultad. No solamente obliga a que el programa sea correcto, resuelva un problema importante, y proporcione una interfaz útil a los usuarios o a otros progra-

1.6

El gran cambio: el paso de monoprocesadores a multiprocesadores

madores que lo utilicen, el programa debe ser también rápido. Por el contrario, si no se necesitan prestaciones, es más sencillo escribir un programa secuencial. La segunda razón es que rápido para un hardware paralelo significa que el programador debe dividir un aplicación de forma que todos los procesadores tengan que hacer, a grandes rasgos, la misma cantidad de trabajo en el mismo tiempo, y que la sobrecarga debido a la planificación y la coordinación no dilapide los beneficios potenciales del paralelismo. Podemos utilizar la siguiente analogía. Supongamos la tarea de escribir un artículo para un periódico. Ocho periodistas trabajando en el mismo artículo podrían, potencialmente, escribir el artículo ocho veces más rápido. Para alcanzar este aumento de velocidad, sería necesario dividir la tarea para que cada periodista tuviese algo que hacer en todo momento. Por lo tanto, deberíamos planificar las subtareas. Si algo estuviese mal planificado y un periodista necesitase más tiempo que los otros siete, podrían perderse los beneficios de disponer de ocho escritores. Así, para obtener la aceleración deseada se debe equilibrar la carga equitativamente. Otro peligro sería que los periodistas tuviesen que perder demasiado tiempo hablando entre sí para escribir sus secciones. Por otra parte, se podría fracasar en alcanzar ese objetivo si una parte del artículo, por ejemplo las conclusiones, no pudiesen escribirse hasta que todas las demás partes estuviese terminadas. Así, se debe poner especial atención en reducir la sobrecarga de las comunicaciones y la sincronización. Tanto para esta analogía como para la programación paralela, los retos son la planificación, el equilibrio de la carga, el tiempo de sincronización y la sobrecarga de las comunicaciones entre colaboradores. Como se puede adivinar, el reto es todavía mayor con más periodistas para un artículo y más procesadores para programación paralela. Para analizar la influencia de este gran cambio en la industria, cada uno de los siguientes cinco capítulos de esta edición del libro tienen una sección sobre las implicaciones de la revolución paralela sobre los conceptos de ese capítulo. ■

Capítulo 2, sección 2.11: Paralelismo e instrucciones: Sincronización. Habitualmente, es necesario coordinar las tareas paralelas independientes en un cierto instante de tiempo, por ejemplo para indicar cuando han terminado su trabajo. En este capítulo se explican las instrucciones de sincronización de tareas utilizadas en los procesadores multinúcleo.



Capítulo 3, sección 3.6: Paralelismo y aritmética del computador: Asociatividad. A menudo los programadores paralelos toman como punto de partida un programa secuencial ya operativo. La pregunta natural para determinar si versión paralela funciona correctamente es “¿se obtiene el mismo resultado?”. Si la respuesta es no, la conclusión lógica es que la versión nueva tiene algún error. Esta lógica está asumiendo que la aritmética del computador es asociativa: el resultado de la suma de un millón de números es el mismo independientemente del orden en que se sumen. En este capítulo se explica que este argumento es válido para números enteros, pero no para número punto flotante.



Capítulo 4, sección 4.10: Paralelismo y paralelismo a nivel de instrucciones avanzado. Dada la dificultad de la programación paralela explícita, desde comienzos de la década de 1990 se ha hecho un gran esfuerzo en el desarrollo de hardware y compiladores que aprovechen el paralelismo implícito. En este capítulo se describen

43

44

Capítulo 1

Abstracciones y tecnología de los computadores

algunas de estas técnicas agresivas, incluida la búsqueda y ejecución simultánea de varias instrucciones, la predicción en los resultados de las decisiones y la ejecución especulativa de instrucciones.

Pensé (que el computador) sería una idea aplicable universalmente, tal y como lo es un libro. Pero no pensé que se desarrollaría tan rápido como lo ha hecho, porque no imaginé que fuésemos capaces de poner tantos componentes en un chip tal y como finalmente se ha conseguido. El transistor llegó de manera inesperada. Todo sucedió mucho más rápido de lo que esperábamos. J. Persper Eckert, coinventor del ENIAC, hablando en 1991.



Capítulo 5, sección 5.8: Paralelismo y jerarquía de memoria: Coherencia de la cache. Una forma para reducir el coste de las comunicaciones es hacer que todos los procesadores usen el mismo espacio de direcciones, para que cualquier procesador pueda leer y escribir cualquier dato. Dado que todos los procesadores actuales usan caches para mantener una copia temporal de los datos en una memoria rápida cercana al procesador, es fácil darse cuenta de que la programación paralela sería incluso más dificultosa si las caches asociadas a cada procesador tuviesen valores incoherentes de los datos compartidos. En este capítulo se describen los mecanismos que permiten mantener valores coherentes de los datos en todas las caches.



Capítulo 6, sección 6.9: Paralelismo y E/S: Conjuntos redundantes de discos baratos. Si nos olvidamos de la entrada y salida en esta revolución paralela, la consecuencia imprevista de la programación paralela podría ser que el programa paralelo desperdicia la mayor parte de su tiempo esperando la E/S. En este capítulo se describen los RAID, una técnica para mejorar las prestaciones en los accesos a los dispositivos de almacenamiento. RAID pone de manifiesto otro beneficio potencial del paralelismo: si se disponen de muchas copias de un recurso, el sistema puede continuar prestando el servicio a pesar de un fallo en alguno de los recursos. De esta forma, RAID puede mejorar tanto las prestaciones de E/S como la disponibilidad.

Además de estas secciones, hay un capítulo entero dedicado al procesamiento paralelo. El capítulo 7 profundiza en los retos de la programación paralela; presenta dos alternativas de comunicación a través de memoria compartida y envío explícito de mensajes; describe un modelo restringido de paralelismo fácil de programar; discute la dificultad de evaluación de los procesadores paralelos; introduce un nuevo y simple modelo de prestaciones para los microprocesadores multinúcleo y finalmente describe y evalúa cuatro ejemplos de microprocesadores multinúcleo usando este modelo. El apéndice A es nuevo en esta edición del libro. En él se describe un componente hardware, incluido en los computadores personales, que cada vez es más popular: la unidad de procesamiento gráfico (GPU). Desarrolladas inicialmente para acelerar las aplicaciones con gráficos, las GPU se están convirtiendo en plataformas de programación. Como es de esperar en estos tiempos, las GPU son altamente paralelas. En el apéndice A se describe la GPU de NVIDIA y algunas partes de su entorno de programación paralela.

1.7

Casos reales: fabricación y evaluación del AMD Opteron x4

1.7

Cada capítulo tiene una sección llamada “Casos reales” que enlaza los conceptos del libro con el computador que uno puede usar a diario. Estas secciones informan sobre la tecnología usada en los compuadores modernos. En este primer “Caso real”, estudiamos cómo se fabrican los circuitos integrados y cómo se miden las prestaciones y la potencia, tomando el AMD Opteron X4 como ejemplo.

1.7

45

Casos reales: fabricación y evaluación del AMD Opteron x4

Empecemos por el principio. La fabricación de un chip empieza con el silicio, sustancia que se encuentra en la arena. Debido a que el silicio no es un buen conductor, se le llama semiconductor. Con un proceso químico especial, es posible añadir al silicio materiales que permiten transformar áreas muy pequeñas en uno de los tres dispositivos siguientes: ■

Conductores excelentes de la electricidad (utilizando cables microscópicos de cobre o aluminio).



Excelentes aislantes de la electricidad (como el plástico o vidrio).



Áreas que pueden conducir o ser aislantes en determinadas condiciones (como un interruptor).

Los transistores están en la última categoría. Así, un circuito VLSI no es más que miles de millones de conductores, aislantes e interruptores fabricados en un único bloque muy pequeño. El proceso de fabricación de circuitos integrados es crucial para el coste de los chips y, por lo tanto, importante para los diseñadores de computadores. La figura 1.18 muestra este proceso, que comienza con un lingote de cristal de silicio, cuyo aspecto es el de una salchicha gigante. Hoy en día, estas barras miden de unos 15 a 30 cm de diámetro y entre 30 y 60 cm de longitud. Las barras se rebanan muy finamente en obleas de menos de 0.25 cm de grosor. A estas obleas se les aplica un proceso de varias etapas en las cuales sobre cada una de ellas se estampan los patrones de productos quí-

Silicio: elemento natural que es semiconductor.

Semiconductor: sustancia que no conduce bien la electricidad.

Lingote de cristal de silicio: barra compuesta de cristal de silicio que tiene entre 15 y 30 cm de diámetro y entre 30 y 60 cm de longitud.

Oblea: rebanada de Obleas vírgenes

Lingote de silicio Corte en obleas Dados verificados

De 20 a 30 etapas de proceso

Dados sueltos (una oblea) Verificador de dados

Conexión del dado al empaquetado

Obleas estampadas Corte en dados

Dados empaquetados verificados

Dados encapsulados Verificador de componentes

Envío a los consumidores

FIGURA 1.18 Proceso de fabricación de chips. Después de cortarse del lingote de silicio, las obleas vírgenes pasan por un proceso que tiene de 20 a 40 etapas para crear obleas estampadas (véase figura 1.9). Estas obleas estampadas se comprueban con un comprobador de obleas y se hace un mapa de las partes correctas. A continuación, las obleas se cortan en dados (véase figura 1.9). En esta figura, una oblea produce 20 dados, de los cuales 17 pasaron con éxito la comprobación. (X significa que el dado es defectuoso.) El factor de producción de dados correctos en este caso es de 17/20, o un 85%. Estos dados correctos se unen a los empaquetados y se comprueban una vez más antes de enviar los chips empaquetados a los clientes. En esta comprobación final se encontró un empaquetado defectuoso.

un lingote de silicio, de grosor no mayor que 0.25 cm, que se usa para fabricar chips.

46

Defecto: imperfección microscópica en una oblea o en uno de los pasos de estampación que provoca que falle el dado que lo contiene. Dado: sección rectangular individual que se corta de una oblea, más informalmente conocido como chip.

Factor de producción: porcentaje de dados correctos del total de dados de la oblea.

Capítulo 1

Abstracciones y tecnología de los computadores

micos que crean los transistores, conductores y aislantes mencionados anteriormente. Los circuitos integrados actuales contienen una sola capa de transistores, pero podrían tener de dos a ocho niveles de conductor metal, separados por capas de aislante. Una sola imperfección en la propia oblea o en cualquiera de las docenas de etapas de patronaje puede causar un mal funcionamiento en esa área de la oblea. Estos defectos, así se llaman, hacen prácticamente imposible fabricar una oblea perfecta. Para resolver este problema, se han usado varias estrategias, pero la más sencilla es poner muchos componentes independientes sobre una única oblea. La oblea estampada se corta en esos componentes, llamados dados (die/dices), más informalmente conocidos como chips. La figura 1.19 es una fotografía de una oblea que contiene microprocesadores Pentium 4 antes de que se hayan cortado; antes, en la figura 1.9 de la página 21, mostrábamos un dado individual de Pentium y sus principales componentes. El corte en dados permite descartar solamente aquellos que tengan defectos, en lugar de la oblea entera. Este concepto se cuantifica como factor de producción (yield factor) del proceso, que se define como el porcentaje de dados buenos del total de dados de una oblea. El coste de un circuito integrado se eleva muy deprisa a medida que aumenta el tamaño del dado, tanto por el menor factor de producción como por el menor número de dados (de mayor tamaño) que caben en una oblea. Para reducir el coste, los dados grandes se “encogen” mediante un proceso de nueva generación que incorpora transistores y conductores de menor tamaño. Esto mejora el factor de producción y el número de dados por oblea. Una vez que se dispone de los dados buenos, se conectan a las patas (pins) de entrada/salida del encapsulado (package) mediante un proceso de conexión (bonding). Los componentes encapsulados (packaged) se verifican por última vez, ya que también se pueden producir errores durante este proceso y, finalmente, se envían a los consumidores. Otra restricción de diseño de importancia creciente es la alimentación eléctrica. La alimentación es un reto. Esto es así por dos razones. En primer lugar, se debe llevar y distribuir electricidad a todo el chip; los microprocesadores modernos usan cientos de patas para las conexiones eléctricas y a tierra. Asimismo, se usan varios niveles de interconexión solamente para proporcionar electricidad y conexiones a tierra a determinadas a porciones del chip. En segundo lugar, la energía se disipa como calor que debe ser eliminado. ¡Un AMD Opteron X4 modelo 2352 a 2.0 GHz consume 120 vatios, que deben ser extraídos de un chip que tiene poco más de 1 cm2 de superficie!

Extensión: El coste de un circuito integrado se puede expresar con tres ecuaciones simples: coste por oblea coste por dado = ------------------------------------------------------------------------------------------dado por oblea × factor de producción de la oblea dados por oblea = área ------------------------------------área del dado 1 factor de producción = --------------------------------------------------------------------------------------------------------( 1 + ( defectos por área × área del dado/2 ) ) 2

1.7

Casos reales: fabricación y evaluación del AMD Opteron x4

FIGURA 1.19 Un oblea de 12 pulgadas (300 mm) que contiene procesadores AMD Opteron X2, el predecesor del Opteron X4 (cortesía de AMD). Con un factor de producción del 100%, hay 117 dados en cada oblea. Las docenas de chips parcialmente redondeados que están en el borde la oblea no son útiles; se incluyen porque así es más fácil crear las máscaras para estampar el silicio. Estos dados se han fabricado con una tecnología de 90 nanómetros, lo que el significa que el tamaño del transistor más pequeño es aproximadamente de 90 nanómetros, aunque típicamente son algo más pequeños que el tamaño característico real, el cual se refiere al tamaño de los transistores cuando se dibujan y no al tamaño real fabricado.

La primera ecuación se obtiene de forma directa y sencilla. La segunda es una aproximación, puesto que no resta el área del borde de la oblea circular que no puede aprovecharse para dados (véase figura 1.19). La última ecuación se basa en observaciones empíricas del factor de producción en fábricas de circuitos integrados, y el exponente está relacionado con el número de pasos críticos del proceso de fabricación. De este modo, generalmente los costes son no lineales con el área de dado, dependiendo del número de defectos y del tamaño del dado y de la oblea.

47

48

Capítulo 1

Abstracciones y tecnología de los computadores

Evaluación de la CPU con programas de prueba SPEC

Carga de trabajo (workload): conjunto de programas que se están ejecutando en un computador que son o bien un conjunto real de aplicaciones de usuario, o bien un conjunto construido artificialmente a partir de fragmentos de programas reales. Una carga de trabajo típica especifica tanto los programas como su frecuencia relativa.

Programa de prueba (benchmark): programa utilizado para comparar las prestaciones de un computador.

Un usuario que está ejecutando los mismos programas día tras día sería un candidato perfecto para evaluar un nuevo computador; y el conjunto de programas utilizados formarían la carga de trabajo. Para llevar a cabo la evaluación de dos sistemas nuevos, el usuario simplemente tendría que comparar la carga de trabajo en los dos computadores. Sin embargo, la mayoría de los usuarios no se encuentran en esta situación. Por el contrario, tienen que depender de otros métodos de medida de las prestaciones de un computador candidato, esperando que estos métodos reflejen como se va a comportar el computador con la carga de trabajo del usuario. Con esta alternativa, se utiliza un conjunto de programas de prueba, especialmente seleccionados, para la evaluación de las prestaciones. Los programas de prueba forman una carga de trabajo que debe predecir las prestaciones con una carga de trabajo real. SPEC (System Performance Evaluation Cooperative) es una organización fundada y financiada por un numeroso conjunto de vendedores de computadores para la creación de conjuntos estándar de programas de prueba para los computadores modernos. En 1989, originalmente SPEC creó un conjunto de programas de prueba enfocados hacia la evaluación de las prestaciones de procesadores (llamado ahora SPEC89), que evolucionó a lo largo de cinco generaciones. La última es SPEC CPU2006, y consiste en un conjunto de 12 programas de prueba de enteros (CINT2006) y 17 programas de prueba de punto flotante (CFP2006). Los programas de prueba de enteros varían desde parte de un compilador de C a un programa de ajedrez o a un código de simulación de un computador cuántico. Los programas de prueba de punto flotante incluyen códigos de mallas estructurados para el modelado de elementos finitos, códigos de métodos de partículas de dinámica molecular y álgebra lineal dispersa para dinámica de fluidos. La figura 1.20 describe los programas de prueba de enteros y los tiempos de ejecución en el Opteron X4, y muestra los factores que explican el tiempo de ejecución: número de instrucciones, CPI y tiempo de ciclo del reloj. Observe que el CPI varía en un factor 13. Para simplificar la comercialización de computadores, SPEC decidió resumir los 12 programas de prueba de enteros en un único número. En primer lugar, se normalizan los tiempos de ejecución medidos dividiendo el tiempo de ejecución en un procesador de referencia entre el tiempo de ejecución del computador que se está evaluando; obteniéndose así la razón SPEC (SPECratio), que tiene la ventaja de que cuanto mayor es el número mayores son las prestaciones (es decir, la razón SPEC es inversamente proporcional al tiempo de ejecución). Haciendo la media geométrica de las razones SPEC se obtiene una medida resumen de los programas de prueba CINT2006 o CF2006. Extensión: Cuando se comparan dos computadores utilizando razones SPEC, es conveniente utilizar la media geométrica porque nos da la misma respuesta relativa sin importar qué computador se utiliza como referencia para normalizar los tiempos. Por el contrario, si utilizásemos la media aritmética de los tiempos de ejecución, los resultados variarían dependiendo del computador utilizado como referencia. La fórmula de la media geométrica es la siguiente n

n

˜ Relaciones de tiempos de ejecucióni i=1

1.7

Descripción

49

Casos reales: fabricación y evaluación del AMD Opteron x4

Nombre

Nº instr. × 109

CPI

Tiempo de ciclo del reloj (seg. × 109)

Tiempo de ejecución (segundos)

Tiempo de referencia (segundos)

Razón SPEC

Procesamiento de secuencias de caracteres Compresión block-sorting

perl

2118

0.75

0.4

637

9770

15.3

bzip2

2389

0.85

0.4

817

9650

11.8

Compilador C de GNU

gcc

1050

1.72

0.4

724

8050

11.1

Optimización combinatorial

mc

336

10.00

0.4

1345

9120

6.8

Juego Go (IA)

go

1658

1.09

0.4

721

10 490

14.6

Búsqueda en secuencia de genes

hmmer

2783

0.80

0.4

890

9330

10.5

Juego de ajedrez (AI)

sjeng

2176

0.96

0.4

837

12 100

14.5

Simulación ordenador cuántico

libquantum

1623

1.61

0.4

1047

20 720

19.8

Compresión de vídeo

h264avc

3102

0.80

0.4

993

22 130

22.3

Librería de simulación de eventos discretos

omnetpp

587

2.94

0.4

690

6250

9.1

Juego path finding

astar

1082

1.79

0.4

773

7020

9.1

Análisis sintáctico de XML

xalancbmk

1058

2.70

0.4

1143

6900

6.0

Media geométrica

11.7

FIGURA 1.20 Programas de prueba SPECINT2006 en un AMD Opteron X4 modelo 2356 (Barcelona). Como se ha explicado en las ecuaciones de la página 35, el tiempo de ejecución es el producto de tres factores de la tabla; número de instrucciones, ciclos por instrucción (CPI) y tiempo de ciclo del reloj. Razón SPEC es el tiempo de referencia, proporcionado por SPEC, dividido entre el tiempo de ejecución medido. El número ofrecido como resultado del SPECINT2006 es la media geométrica de todas las razones SPEC. La figura 5.40 en la página 542 muestra que el CPI de mcf, libquantum, omnetpp y xalancbmk es relativamente alto debido a las elevadas relaciones de fallos de cache.

siendo relaciones de tiempos de ejecucióni el tiempo de ejecución normalizado al del computador de referencia para el programa i de un total de n en la carga de trabajo y n

˜ ai significa el producto a1 × a2 × … × an i=1

Evaluación de la potencia con programas de prueba SPEC En la actualidad, SPEC ofrece una docena de conjuntos de programas de prueba destinados al chequeo de una amplia variedad de ambientes de computación, usando aplicaciones reales y con una especificación estricta de las reglas de ejecución y los requerimientos de información. El más reciente es SPECpower, que proporciona datos sobre consumo de potencia de servidores con diferentes niveles de cargas de trabajo, divididos en tramos de incrementos del 10%, en un periodo de tiempo dado. La figura 1.21 muestra los resultados de un servidor con el procesador Barcelona. El comienzo de SPECpower fueron los programas de prueba SPEC para aplicaciones de negocios codificadas en Java (SPECJBB2005), que evaluaban el procesador, la memoria principal y la cache, así como la máquina virtual de Java, el compilador, recolector de basura (garbage collector) y partes del sistema operativo. La medida de las prestaciones es la productividad y las unidades son operaciones de negocio por segundo. Una vez más, para facilitar la comercialización de los

50

Capítulo 1

Abstracciones y tecnología de los computadores

Carga objetivo

Prestaciones (ssj_ops)

Potencia media (vatios)

100%

231 867

295

90%

211 282

286

80%

185 803

275

70%

163 427

266

60%

140 160

256

50%

118 324

246

40%

92 035

233

30%

70 500

222

20%

47 126

206

10%

23 066

180

0%

0

141

1 283 590

2605

Suma global

¨ ssj_ops/ ¨ potencia

493

FIGURA 1.21 SPECpower_ssj2008 en un AMD Opteron x4 2356 (Barcelona) con soporte para dos procesadores, 16 GB de memoria RAM dinámica DDR2-667 y un disco de 500 GB.

computadores, SPEC reduce estas medidas a un único número, llamado ssj_ops global por vatio. La fórmula para esta métrica de resumen es © ¹ © ¹ potencia iº ssj_ops global por vatio = ª ssj_ops iº / ª «i = 0 » «i = 0 » 10

¨

10

¨

siendo ssj_opsi las prestaciones en cada incremento del 10% de la carga y potenciai el consumo de potencia en cada nivel.

Autoevaluación Un factor clave para determinar el coste de un circuito integrado es el volumen de fabricación. ¿Cuáles de las siguientes razones pueden hacer que el coste de un chip disminuya con el volumen de fabricación? 1. Con volúmenes elevados, el proceso de fabricación puede adaptarse a un diseño determinado, incrementando las prestaciones. 2. Es más fácil diseñar una parte que va a ser fabricada en grandes volúmenes que una parte que va a ser fabricada en volúmenes pequeños. 3. Las máscaras necesarias para la fabricación de los chips son caras, por lo tanto el coste por chip es menor para mayores volúmenes de fabricación. 4. Los costes de desarrollo de ingeniería son elevados y fuertemente dependientes del volumen; así, el coste de desarrollo por dado es menor para grandes volúmenes de fabricación. 5. Las partes que van a ser fabricada en grandes volúmenes usualmente tienen dados más pequeños que las partes que van a ser fabricada en volúmenes pequeños y, por lo tanto, tienen un mayor rendimiento por oblea.

1.8

1.8

Falacias y errores habituales

Falacias y errores habituales

1.8

51

La ciencia debe empezar con mitos, y la crítica de los mitos. Sir Karl Popper, The Philosophy of Science, 1957

El objetivo de una sección sobre falacias y errores más comunes, que podremos encontrar en cada capítulo, es explicar algunos errores de concepto comúnmente aceptados. A estos errores conceptuales los llamaremos falacias. Cada vez que se discuta una falacia, se intentará dar un contraejemplo. También discutiremos los errores más comunes. A menudo estos errores son generalizaciones de principios que son ciertos en un contexto limitado. El objetivo de estas secciones es ayudar a que estos errores no se reproduzcan en los computadores que el lector pueda diseñar o utilizar. Las falacias y errores más comunes relacionados con el coste/prestaciones han atrapado a muchos diseñadores de computadores, incluyéndonos a nosotros. Por lo tanto, en esta sección no faltarán ejemplos relevantes. Empezamos con un error que comenten muchos diseñadores y revela importantes relaciones en el diseño de computadores. Error: Esperar que la mejora de un aspecto de un computador incremente las prestaciones globales en una cantidad proporcional a dicha mejora. Este error ha sido cometido por diseñadores de hardware y diseñadores de software. Para ilustrarlo, proponemos un problema simple de diseño. Supongamos que un programa se ejecuta en 100 segundos en un computador dado, de los cuales 80 segundos se dedican a varias multiplicaciones. ¿Cuánto debo mejorar la velocidad de la multiplicación si quiero que mi programa se ejecute cinco veces más rápido? El tiempo de ejecución del programa después de hacer las mejoras viene dado por la siguiente ecuación simple, conocida como ley de Amdahl: tiempo de ejecución después de las mejoras = tiempo de ejecución por la mejora -------------------------------------------------------------------------- + tiempo de ejecución no afectado cantidad de mejora Para este ejemplo: 80 segundos tiempo de ejecución por la mejora = --------------------------- + ( 100 – 80 segundos ) n como queremos que la ejecución sea cinco veces más rápida, el nuevo tiempo de ejecución debe ser 20 segundos, entonces 80 segundos 20 segundos = --------------------------- + 20 segundos n 80 segundos 0 = --------------------------n Es decir, no es posible mejorar la multiplicación para alcanzar un aumento en las prestaciones que implique que la ejecución es cinco veces más rápida, siempre y cuando la multiplicación suponga el 80% de la carga de trabajo.

Ley de Amdhal: regla que establece que el aumento posible de las prestaciones con una mejora determinada está limitado por la cantidad en que se usa la mejora. Esta es una versión cuantitativa de la ley de rendimiento decreciente en economía.

52

Capítulo 1

Abstracciones y tecnología de los computadores

El aumento posible de las prestaciones con una mejora determinada está limitado por la cantidad en que se usa la mejora. Este concepto también conduce a lo que llamamos ley de rendimiento decreciente en economía y en la vida diaria. Podemos utilizar la ley de Amdahl para estimar el aumento de las prestaciones cuando se conoce el tiempo consumido por una función y su aceleración potencial. La ley de Amdahl, junto con las ecuaciones de prestaciones de la CPU, es una herramienta útil para la evaluación de mejoras potenciales. En los ejercicios se profundizará en la utilización de la ley de Amdahl. Un corolario de la ley de Amdahl es de uso común en el diseño de hardware: Hacer el caso más frecuente rápido. Esta idea simple nos recuerda que en muchos casos un evento puede ocurrir mucho más frecuentemente que otro. La ley de Amdahl nos dice que la oportunidad para conseguir una mejora se ve afectada por el tiempo que este suceso consume. Por lo tanto, hacer rápido el caso más frecuente tenderá a que la mejora de las prestaciones sea mayor que optimizando el caso menos frecuente. Irónicamente, el caso más frecuente es a menudo más simple que el caso menos frecuente y así es a menudo más fácil de mejorar. La ley de Amdahl se usa también para justificar los límites prácticos en el número de procesadores paralelos. Esto se examinará en la sección de falacias y errores más comunes del capítulo 7. Falacia: Computadores con una utilización baja consumen poca potencia. La eficiencia energética es importante con una baja utilización porque la carga de trabajo de los servidores puede variar. El uso de CPU para los servidores en Google, por ejemplo, está comprendida entre el 10% y el 50% la mayor parte del tiempo, y es del 100% menos del 1% del tiempo. La figura 1.22 muestra la potencia consumida por varios servidores con los mejores resultados del SPECpower con una carga del 100%, del 50%, 10% e inactivos. Incluso cuando los servidores están ocupados al 10%, la potencia consumida es dos tercios del consumo pico. Dado que la carga de trabajo de los servidores varía pero consumen una parte elevada de la potencia pico, Luiz Barroso y Urs Hözle [2007] dicen que se debería rediseñar el hardware para alcanzar una “computación energeticamente proporcional”. Si lo servidores utilizasen, por ejemplo, el 10% de la potencia pico cuando tienen una carga de trabajo del 10%, se reduciría el consumo de electricidad de un centro de datos y se comportarían como buenos ciudadanos en una época de grandes preocupaciones por la emisiones de CO2.

Fabricante del servidor

Poten- Carga Potencia Número Presta- Poten- Poten- Carga cia a cia a 50%/ cia a 10%/ inactivo/ Frede ciones Micro100% de 50% de potencia 10% de potencia Potencia potencia núcleos/ cuencia pico carga 100% carga 100% procesador zócalos de reloj (ssj_ops) carga inactivo 100%

HP

Xeon E5440

8/2

Dell

Xeon E5440

8/2

Fujitsu Seimens Xeon X3220

4/1

3.0 GHz

308 022

269 W

227 W

84%

174 W

65%

160 W

2.8 GHz

305 413

276 W

230 W

83%

2.4 GHz

143 742

132 W

110 W

83%

59%

173 W

63%

157 W

57%

85 W

65%

80 W

60%

FIGURA 1.22 Resultados de SPECPower para tres servidores con el mejor ssj_ops por vatio en el último cuarto de 2007. El ssj_ops por vatio de los tres servidores es 698, 682 y 667, respectivamente. La memoria de los dos primeros servidores es 16 GB y del último es 8 GB.

1.8

Falacias y errores habituales

53

Error: Usar un subconjunto de las ecuaciones de prestaciones como una métrica de las prestaciones. Se ha mostrado anteriormente la falacia resultante de predecir las prestaciones basándose únicamente en la frecuencia de la señal de reloj, o en el número de instrucciones, o en el CPI. Otro error muy corriente se produce cuando se utilizan sólo dos de los tres factores para comparar las prestaciones. Aunque esto puede ser válido en un contexto limitado, es fácil cometer errores. De hecho, casi todas las alternativas para usar el tiempo como una métrica de las prestaciones han dado lugar a conclusiones engañosas, resultados distorsionados o interpretaciones incorrectas. Una alternativa al tiempo de ejecución son los MIPS (millones de instrucciones por segundo). Para un programa dado, número de instrucciones MIPS = -------------------------------------------------------6 tiempo de ejecución × 10 MIPS es una especificación de las prestaciones inversamente proporcional al tiempo de ejecución: los computadores más rápidos tienen valores mayores para MIPS. Por lo tanto, la ventaja de MIPS es que es una métrica fácil de entender e intuitiva, ya que computadores más rápidos tienen MIPS más elevados. Sin embargo, cuando se utiliza como una medida para comparar computadores aparecen tres problemas. Primero, MIPS especifica la razón de ejecución de instrucciones pero no tiene en cuenta lo que hace cada instrucción. No es posible comparar computadores con diferentes conjuntos de instrucciones usando MIPS, porque el número de instrucciones será diferente. Segundo, MIPS varía entre diferentes programas ejecutándose en el mismo computador; así, un computador no puede tener un único valor de MIPS. Por ejemplo, sustituyendo el tiempo de ejecución, obtenemos las relación entre MIPS, frecuencia del reloj y CPI:

Millones de instrucciones por segundo (MIPS): medida de la velocidad de ejecución de un programa basada en el número de instrucciones. MIPS está definido como el número de instrucciones dividido por el producto del tiempo de ejecución por 106.

número de instrucciones frecuencia de reloj MIPS = --------------------------------------------------------------------------------- = --------------------------------------número de instrucciones × CPI CPI × 10 6 6 10 × -------------------------------------------------------------------frecuencia de reloj Tal como se mostró anteriormente, el CPI varía en un factor 13 con los programas del SPEC2006 en un Opteron X4, por lo tanto MIPS varía de la misma forma. Por último, y más importante, las variaciones de MIPS pueden ser independientes de las prestaciones. Considere las siguientes medidas de las prestaciones de un programa: Medida

Computador A

Computador B

Número de instrucciones

10 billones

8 billones

4 Ghz

4 Ghz

1.0

1.1

Frecuencia del reloj CPI

a. ¿Qué computador tiene un MIPS más elevado? b. ¿Qué computador es más rápido?

Autoevaluación

54

Capítulo 1

Mientras el ENIAC está equipado con 18 000 válvulas de vacío y pesa 30 toneladas, los computadores del futuro pueden tener 1000 válvulas y quizás pesen sólo 1 tonelada y media. Popular Mechanics, Marzo de 1949

1.9

Abstracciones y tecnología de los computadores

Conclusiones finales

1.9

Aunque es difícil predecir exactamente la relación coste/prestaciones que tendrán los computadores en el futuro, afirmar que será mucho mejor que la actual es una apuesta segura. Para participar en estos avances, los diseñadores de computadores y programadores deben entender una gran variedad de cuestiones. Los diseñadores de hardware y los de software construyen sus sistemas en niveles jerárquicos, donde cada nivel oculta sus detalles a los niveles superiores. Este principio de abstracción es fundamental para comprender los computadores de hoy en día, pero no significa que los diseñadores se puedan limitar al conocimiento de una sola tecnología. El ejemplo de abstracción más importante es quizá la interfaz entre el hardware y el software de más bajo nivel, llamada arquitectura del repertorio de instrucciones. Mantener la arquitectura del repertorio de instrucciones constante permite muchas implementaciones de esa arquitectura (diferentes en coste y prestaciones) capaces de ejecutar los mismos programas. En el lado negativo, la arquitectura puede imposibilitar la introducción de innovaciones que requieran cambios en esa interfaz. Hay un método fiable para determinar las prestaciones utilizando como métrica el tiempo de ejecución de programas reales. Este tiempo de ejecución está relacionado con otras métricas importantes mediante la siguiente ecuación: segundos instrucciones ciclos de reloj segundos --------------------- = ----------------------------- × ----------------------------- × ----------------------------programa programa instrucción ciclos de reloj Esta ecuación y los factores que en intervienen en ella serán utilizados en muchas ocasiones en el libro. Es importante tener presente que estos factores, de forma individual, no determinan las prestaciones, sólo el producto de ellos, que es igual al tiempo de ejecución, es una medida fiable de las prestaciones.

IDEA clave

La única métrica totalmente válida para evaluar las prestaciones es el tiempo de ejecución. Se han propuesto otras métricas pero no han dado la talla. A veces, estas métricas tenían ya fallos desde el mismo concepto, por no reflejar el tiempo de ejecución. En otras ocasiones, métricas que eran válidas en un contexto limitado se utilizaron en un contexto más amplio sin las aclaraciones necesarias para poder aplicarlas sin errores.

La tecnología clave de los procesadores modernos es el silicio. Para entender la tecnología de los circuitos integrados es igual de importante comprender las tasas de cambio tecnológico esperadas. Mientras el silicio alimenta el rápido avance del hardware, nuevas ideas sobre la organización de los computadores han mejorado la relación precio/prestaciones. Dos de las ideas clave son la explotación del paralelismo en

1.10

Perspectiva histórica y lecturas recomendadas

55

el procesador, típicamente vía la utilización de varios procesadores, y la explotación de la localidad de los accesos a la jerarquía de memoria, típicamente vía caches. El área de silicio ha sido reemplazado por el consumo de potencia como recurso más crítico del diseño de microprocesadores. Aumentar las prestaciones manteniendo la potencia ha forzado que la industria del hardware cambie a los microprocesadores multinúcleo; en consecuencia, la industria del software se ha visto forzada ha moverse hacia la programación de hardware paralelo. Los diseños de computadores siempre se han medido por su coste y sus prestaciones, y por otros factores también importantes, como la potencia, fiabilidad, propiedad y escalabilidad. Aunque en este capítulo nos hemos centrado en el coste, las prestaciones y la potencia, los mejores diseños mantendrán un equilibrio adecuado entre todos estos factores, para un segmento de mercado concreto.

Mapa de carreteras de este libro En el nivel más bajo de estas abstracciones están los cinco elementos clásicos del computador: camino de datos, control, memoria, entrada y salida (véase la figura 1.5). Estos cinco componentes también sirven como marco para el resto de los capítulos de este libro: ■ Camino de datos: capítulos 3, 4, 7 y apéndice A ■ Control: capítulos 4, 7 y apéndice A ■ Memoria: capítulo 5 ■ Entrada: capítulo 6 ■ Salida: capítulo 6 Como se ha dicho anteriormente, el capítulo 4 describe como el procesador explota el paralelismo implícito, el capítulo 7 describe el corazón de la revolución paralela, los microprocesadores multinúcleo con paralelismo explícito, y el apéndice A describe los altamente paralelos procesadores gráficos. El capítulo 5 describe como la jerarquía de memoria explota la localidad. El capítulo 2 describe los repertorios de instrucciones —la interfaz entre los compiladores y la máquina— y enfatiza el papel de los compiladores y lenguajes de programación en el uso de las características del repertorio de instrucciones. El apéndice B proporciona una referencia para el repertorio de instrucciones del capítulo 2. El capítulo 3 describe como los computadores manipulan datos aritméticos. El apéndice C, en el CD, introduce los conceptos básicos del diseño lógico.

1.10

Perspectiva histórica y lecturas recomendadas

1.10

En el CD que acompaña a este libro se puede encontrar una sección dedicada a la perspectiva histórica relacionada con cada capítulo. Podemos exponer el desarrollo de una idea a través de una serie de máquinas o describir algún proyecto importante y dar referencias para aquellos lectores interesados en profundizar más.

Un campo activo de la ciencia es como un inmenso hormiguero. El individuo casi desaparece en una masa de intelectos que tropiezan unos con otros, que transportan información de un sitio a otro y la hacen circular a la velocidad de la luz. Lewis Thomas, “Natural Science”; en The Lives of a Cell, 1974.

56

Capítulo 1

Abstracciones y tecnología de los computadores

La perspectiva histórica para este capítulo proporciona el trasfondo de algunas de las ideas clave presentadas en él. Su propósito es mostrar la historia humana que hay detrás de los avances tecnológicos y poner los descubrimientos en su contexto histórico. Gracias a la comprensión del pasado se pueden entender mejor las líneas maestras que guiarán el futuro. Cada sección de perspectiva histórica del CD acaba con sugerencias para lecturas adicionales, las cuales están recogidas separadamente en el CD bajo la sección “Lecturas adicionales”. El resto de esta sección 1.10 está en el CD.

1.11

Ejercicios

1.11

Contribución de Javier Bruguera, Universidad de Santiago de Compostela

La mayor parte de los ejercicios de esta edición se han diseñado de modo que constan de una descripción cualitativa apoyada en una tabla que proporciona diferentes parámetros cuantitativos alternativos. Estos parámetros se necesitan para resolver las cuestiones que se plantean en el ejercicio. Las cuestiones individuales pueden resolverse utilizando alguno o todos los parámetros; el número de parámetros que se necesitan para cualquiera de los ejercicios es decisión suya. Por ejemplo, es posible encontrarnos con que se dice “completar la cuestión 4.1.1 utilizando los parámetros de la fila A de la tabla”. Alternativamente, los profesores pueden adaptar estos ejercicios para crear nuevas soluciones reemplazando los parámetros dados por otros valores. El número de ejercicios cuantitativos varía de un capítulo a otro en función de los aspectos tratados en ese capítulo. Cuando esta aproximación cuantitativa no encaja perfectamente, se proponen ejercicios más convencionales. El tiempo relativo necesario para resolver estos ejercicios se indica entre corchetes detrás del número del ejercicio. De media, un ejercicio catalogado como [10] necesitará el doble de tiempo que uno catalogado como [5]. Por otra parte, la sección que debería haberse leído antes de intentar resolver un ejercicio se indica mediante los signos <>; por ejemplo, <1.3> significa que deberías haber leído la sección 1.3, para resolver este ejercicio.

Ejercicio 1.1 En la lista que se muestra a continuación, encuentre la palabra o frase que mejor se adapte a la descripción de las cuestiones siguientes. Indique la respuesta utilizando los números que aparecen a la izquierda de las palabras. Cada palabra sólo se utiliza una vez. 1.1.1 [2]<1.1> Computador utilizado para ejecutar programas muy complejos y al que se accede habitualmente a través de una red.

1.11

1

Mundos virtuales

14

Sistema operativo

2

Computadores personales

15

Compilador

3

Servidores

16

Bit

4

Servidores de gama baja

17

Instrucciones

5

Supercomputadores

18

Lenguaje ensamblador

6

Terabyte

19

Lenguaje máquina

7

Petabyte

20

C

8

Centro de datos

21

Ensamblador

9

Computadores empotrados

22

Lenguaje de alto nivel

10

Procesadores multinúcleo

23

Software del sistema

11

VHDL

24

Software de aplicación

12

RAM

25

Cobol

13

CPU

26

Fortran

Ejercicios

1.1.2 [2]<1.1> 1015 o 250 bytes. 1.1.3 [2]<1.1> Computador formado por cientos o miles de procesadores con varios terabytes de memoria. 1.1.4 [2] <1.1> Aplicaciones que actualmente son ciencia ficción pero que probablemente serán reales en un futuro próximo. 1.1.5 [2]<1.1> Un tipo de memoria llamado memoria de acceso aleatorio (ran-

dom access memory). 1.1.6 [2]<1.1> La parte del computador llamada unidad central de procesamiento (central processor unit). 1.1.7 [2]<1.1> Miles de procesadores formando un gran clúster. 1.1.8 [2]<1.1> Un microprocesador compuesto de varios procesadores en el mismo chip. 1.1.9 [2]<1.1> Computador personal sin monitor ni teclado al que se accede ha-

bitualmente a través de una red. 1.1.10 [2]<1.1> Sistema diseñado para ejecutar una aplicación concreta o un

conjunto de aplicaciones software. 1.1.11 [2]<1.1> Lenguaje utilizado para la descripción de componentes hardware.

57

58

Capítulo 1

Abstracciones y tecnología de los computadores

1.1.12 [2]<1.1> Computador que proporciona buenas prestaciones a un único usuario y con bajo coste. 1.1.13 [2]<1.2> Programa que traduce sentencias de un lenguaje de alto nivel a lenguaje ensamblador. 1.1.14 [2]<1.2> Programa traduce instrucciones simbólicas a instrucciones binarias. 1.1.15 [2]<1.2> Lenguaje de alto nivel para aplicaciones del ámbito de los negocios y la economía. 1.1.16 [2]<1.2> Lenguaje binario que es capaz de entender un procesador. 1.1.17 [2]<1.2> Órdenes que entiende e interpreta el procesador. 1.1.18 [2]<1.2> Lenguaje de alto nivel para aplicaciones científicas. 1.1.19 [2]<1.2> Representación simbólica de las instrucciones máquina. 1.1.20 [2]<1.2> Interfaz entre los programas de usuario y el hardware, que proporciona varios servicios y funciones de supervisión. 1.1.21 [2]<1.2> Software o programas desarrollados por los usuarios. 1.1.22 [2]<1.2> Dígito binario (valor 0 o 1) 1.1.23 [2]<1.2> Capa software entre el software de aplicación y el hardware, que incluye el sistema operativo y los compiladores. 1.1.24 [2]<1.2> Lenguaje de alto nivel utilizado para escribir software de sistema y de aplicaciones. 1.1.25 [2]<1.2> Lenguaje compuesto de palabras y expresiones algebraicas que deben ser traducidas a lenguaje ensamblador antes de ejecutarse en un computador. 1.1.26 [2]<1.2> 1012 o 240 bytes.

Ejercicio 1.2 1.2.1 [10]<1.3> En un monitor de color en el que se utilizan 8 bits por píxel para cada color primario (rojo, verde, azul) y con una resolución de 1280 × 800 píxeles, ¿cuál es el tamaño mínimo (en bytes) del búfer de pantalla necesario para almacenar un cuadro? 1.2.2 [5]<1.3> Un computador con una memoria principal de 2GB, ¿cuántos cuadros puede almacenar si consideramos que no almacena ninguna otra información? 1.2.3 [5]<1.3> Si un computador conectado a una red ethernet de 1 gigabit tiene que enviar un fichero de 256 Kbytes, ¿cuánto tiempo tardará?

1.11

Ejercicios

1.2.4 [5]<1.3> Teniendo en cuenta que una memoria cache es 10 veces más rápida que una memoria DRAM, que la memoria DRAM es 100 000 veces más rápida que un disco magnético y que la memoria flash es 1000 veces más rápida que el disco, determine cuánto tiempo se necesita para un leer un fichero de una DRAM, un disco y una memoria flash si se necesitan 2 microsegundos para leerlo de la memoria cache.

Ejercicio 1.3 Suponga que se dispone de tres procesadores diferentes, P1, P2 y P3, con las frecuencias de reloj y CPI mostrados en la tabla y que ejecutan el mismo conjunto de instrucciones.

Procesador

Frecuencia de reloj

CPI

P1

2 GHz

1.5

P2

15 GHz

1

P3

3 GHz

2.5

1.3.1 [5]<1.4> ¿Qué procesador tiene mejores prestaciones? 1.3.2 [5]<1.4> Si cada procesador ejecuta un programa que dura 10 segundos, calcule el número de ciclos y el número de instrucciones para cada uno. 1.3.3 [10]<1.4> Se quiere reducir el tiempo de ejecución en un 30%, pero esto sólo se consigue a costa de un incremento del 20% en el CPI. ¿Qué frecuencia de reloj es necesaria para alcanzar esa reducción en el tiempo de ejecución?

Para los siguientes problemas utilice la información de la tabla que se muestra a continuación

Procesador

Frecuencia de reloj

Nº instrucciones

Tiempo

P1

2 GHz

2 × 1010

7s

P2

1.5 GHz

30 × 109

10 s

P3

3 GHz

90 × 109

9s

1.3.4 [10]<1.4> Calcule el IPC (instrucciones por ciclo) de cada procesador. 1.3.5 [10]<1.4> Calcule la frecuencia de reloj de P2 necesaria para que el tiempo de ejecución sea el de P1 1.3.6 [10]<1.4> Calcule cuántas instrucciones debería ejecutarse en P2 para que el tiempo de ejecución sea el de P3.

59

60

Capítulo 1

Abstracciones y tecnología de los computadores

Ejercicio 1.4 Considere dos implementaciones diferentes de un mismo repertorio de instrucciones, con cuatro tipos de instrucciones, A, B, C y D. La frecuencia de reloj y el CPI de cada implementación se muestran en la siguiente tabla. Frec. reloj

CPI tipo A

CPI tipo B

CPI tipo C

CPI tipo D

P1

1.5 GHz

1

2

3

4

P2

2 GHz

2

2

2

2

1.4.1 [10]<1.4> Dado un programa con 106 instrucciones repartidas de la

siguiente forma: 10% de tipo A, 20% de tipo B, 50% de tipo C y 20% de tipo D, ¿qué implementación es más rápida? 1.4.2 [5]<1.4> ¿Cuál es el CPI global de cada implementación? 1.4.3 [5]<1.4> Determine el número de ciclos de reloj en ambos casos.

La siguiente tabla muestra el número y tipo de instrucciones de un programa Aritméticas

Almacenamiento

Carga

Saltos

Total

500

50

100

50

700

P1

1.4.4 [5]<1.4> Suponiendo que las instrucciones aritméticas necesitan 1 ciclo

para ejecutarse, las de carga y almacenamiento 5 ciclos y los saltos 2 ciclos, ¿cuál es el tiempo de ejecución en un procesador a 2 GHz? 1.4.5 [5]<1.4> Determine el CPI del programa. 1.4.6 [10]<1.4> Si el número de instrucciones de carga se reduce a la mitad, ¿cuál es la aceleración y el nuevo CPI?

Ejercicio 1.5 Considere dos implementaciones diferentes, P1 y P2, de un mismo repertorio de instrucciones, con cinco tipos de instrucciones, A, B, C, D y E. La frecuencia de reloj y el CPI de cada implementación se muestran en la siguiente tabla.

a.

b.

Frec. reloj

CPI tipo A

CPI tipo B

CPI tipo C

CPI tipo D

CPI tipo E

P1

1.0 GHz

1

2

3

4

3

P2

1.5 GHz

2

2

2

4

4

P1

1.0 GHz

1

1

2

3

2

P2

1.5 GHz

1

2

3

4

3

1.11

Ejercicios

1.5.1 [5]<1.4> Se define la prestación pico como la velocidad más elevada a la que un computador puede ejecutar cualquier secuencia de instrucciones. ¿Cuáles son las prestaciones pico de P1 y P2 expresadas en instrucciones por segundo? 1.5.2 [5]<1.4> Supongamos que las instrucciones de un programa se reparten equitativamente entre todos los tipos de instrucciones, excepto para las instrucciones de tipo A, que representan el doble de las instrucciones de cualquiera de los otros tipos. ¿Qué computador es más rápido? ¿Cuál es el aumento de velocidad? 1.5.3 [5]<1.4> Suponga que las instrucciones de un programa se reparten equitativamente entre todos los tipos de instrucciones, excepto para las instrucciones de tipo E, que representan el doble de las instrucciones de cualquiera de los otros tipos. ¿Qué computador es más rápido? ¿Cuál es el aumento de velocidad?

La siguiente tabla muestra el número de instrucciones de varios tipos de instrucciones en dos programas diferentes. Utilizando estos datos, explorar las variaciones de las prestaciones que se producen al aplicar diferentes cambios a un procesador MIPS.

Nº de instrucciones Cálculo

Carga

Almacenamiento

Salto

Total

a.

Programa 1

1000

400

100

50

1550

b.

Programa 2

1500

300

100

100

2000

1.5.4 [5]<1.4> Suponiendo que las instrucciones de cálculo necesitan 1 ciclo, las de carga y almacenamiento 10 ciclos y los saltos 3 ciclos, determine el tiempo de ejecución de cada programa en un procesador MIPS a 3 GHz. 1.5.5 [5]<1.4> Suponiendo que las instrucciones de cálculo necesitan 1 ciclo, las de carga y almacenamiento 2 ciclos y los saltos 3 ciclos, determine el tiempo de ejecución de cada programa en un procesador MIPS a 3 GHz. 1.5.6 [5]<1.4> Suponiendo que las instrucciones de cálculo necesitan 1 ciclo, las de carga y almacenamiento 10 ciclos y los saltos 3 ciclos, ¿cuál es la aceleración de un programa si el número de instrucciones de cálculo se reduce a la mitad?

Ejercicio 1.6 Los compiladores tienen un fuerte impacto sobre las prestaciones de una aplicación que se ejecuta en un determinado procesador. Este ejercicio analiza el impacto de los compiladores sobre el tiempo de ejecución.

61

62

Capítulo 1

Abstracciones y tecnología de los computadores

Compilador A

Compilador B

Nº de instrucciones Tiempo de ejecución Nº de instrucciones Tiempo de ejecución a.

1.00E+09

1s

1.20E+09

1.4 s

b.

1.00E+09

0.8 s

1.20E+09

0.7 s

1.6.1 [5]<1.4> Se utilizan dos compiladores diferentes para un mismo programa. La tabla anterior muestra los tiempos de ejecución para los dos programas. Determine el CPI medio para cada programa considerando que el ciclo de la señal de reloj del procesador es 1 ns. 1.6.2 [5]<1.4> Suponga el CPI medio obtenido en 1.6.1, pero que los programas compilados se ejecutan en dos procesadores diferentes. Si los tiempos de ejecución en los dos procesadores son iguales, determine la relación entre las frecuencias de reloj del procesador ejecutando el programa compilado con el compilador A y del procesador ejecutando el programa compilado con el compilador B. 1.6.3 [5]<1.4> Se desarrollada un nuevo compilador que solamente genera 600 millones de instrucciones y tiene un CPI medio de 1.1. ¿Cuál es la aceleración obtenida al utilizar este nuevo compilador con respecto a los compiladores A y B de 1.6.1?

Considere dos implementaciones diferentes, P1 y P2, del mismo repertorio de instrucciones con 5 tipos de instrucciones (A, B, C, D y E). La frecuencia de reloj de P1 es 4 GHz y la de P2, 6 GHz. El número medio de ciclos para cada tipo de instrucción se indica en la siguiente tabla.

a.

b.

Tipo

CPI en P1

CPI en P2

A

1

2

B

2

2

C

3

2

D

4

4

E

5

4

Tipo

CPI en P1

CPI en P2

A

1

2

B

1

2

C

1

2

D

4

4

E

5

4

1.11

Ejercicios

1.6.4 [5]<1.4> Se definen las prestaciones pico como la velocidad más elevada a la que un computador puede ejecutar cualquier secuencia de instrucciones. ¿Cuáles son las prestaciones pico de P1 y P2 expresadas en instrucciones por segundo? 1.6.5 [5]<1.4> Suponga que las instrucciones de un programa se reparten equitativamente entre todos los tipos de instrucciones, excepto para las instrucciones de tipo A, que representan el doble de las instrucciones de cualquiera de los otros tipos. ¿Cuál es la aceleración de P2 respecto a P1? 1.6.6 [5]<1.4> ¿A qué frecuencia las prestaciones de P2 se igualan a las de P1 para el reparto de instrucciones dado en 1.6.5?

Ejercicio 1.7 La siguiente tabla muestra el aumento de la frecuencia del reloj y potencia disipada para ocho generaciones de procesadores Intel en los últimos 28 años.

Procesador

Frecuencia de reloj

Potencia

80286 (1982)

12.5 MHz

3.3 W

80386 (1985)

16 MHz

4.1 W

80486 (1989)

25 MHz

4.9 W

Pentium (1993)

66 MHz

10.1 W

Pentium Pro (1997)

200 MHz

29.1 W

2 GHz

75.3 W

Pentium 4 Willamette (2001) Pentium 4 Prescott (2004) Core 2 Ketsfield (2007)

3.6 GHz 2667 GHz

103 W 95 W

1.7.1 [5]<1.5> Determine la media geométrica de las relaciones entre generaciones consecutivas, tanto para la frecuencia como para la potencia. (La media geométrica se define en la sección 1.7). 1.7.2 [5]<1.5> Indique cuál es el mayor incremento relativo en frecuencia y potencia entre generaciones. 1.7.3 [5]<1.5> ¿Cuál es el aumento de la frecuencia y la potencia de la última generación respecto la primera?

63

64

Capítulo 1

Abstracciones y tecnología de los computadores

En los siguientes ejercicios se utilizan los valores de voltaje indicados en la siguiente tabla. Procesador

Voltaje

80286 (1982)

5

80386 (1985)

5

80486 (1989)

5

Pentium (1993)

5

Pentium Pro (1997)

3.3

Pentium 4 Willamette (2001)

1.75

Pentium 4 Prescott (2004)

1.25

Core 2 Ketsfield (2007)

1.1

1.7.4 [5]<1.5> Determine las cargas capacitivas medias, suponiendo que el consumo estático de potencia es despreciable. 1.7.5 [5]<1.5> Determine el mayor incremento relativo en voltaje entre generaciones. 1.7.6 [5]<1.5> Determine la media geométrica de las relaciones de voltaje entre generaciones consecutivas desde el Pentium.

Ejercicio 1.8. Suponga que se han desarrollado varias versiones de un procesador con las siguientes características Voltaje

Frecuencia de reloj

versión 1

5V

0.5 GHz

versión 2

3.3 V

1 GHz

1.8.1 [5]<1.5> ¿Cuánto se ha reducido la carga capacitiva entre versiones si la potencia dinámica se ha reducido un 10%? 1.8.2 [5]<1.5> ¿Cuánto se ha reducido la potencia dinámica si la carga capacitiva no ha cambiado? 1.8.3 [5]<1.5> Suponiendo que la carga capacitiva de la versión 2 es el 80% de la carga capacitiva de la versión 1, determine el voltaje de la versión 2 si su potencia dinámica es tal que se ha reducido un 40% con respecto a la de la versión 1.

1.11

Ejercicios

Suponga que las tendencias actuales en la industria son que, con cada nueva generación del proceso de fabricación, las características de un procesador escalan según los datos en la siguiente tabla. Capacitancia

Voltaje

Frecuencia de reloj

Área

1

2-1/4

21/2

2-1/2

1.8.4 [5]<1.5> Determine el factor de escalado de la potencia dinámica. 1.8.5 [5]<1.5> Determine el factor de escalado de la capacitancia por unidad de área. 1.8.6 [5]<1.5> Utilizando los datos del ejercicio 1.7, determine el voltaje y la frecuencia de reloj del procesador Core 2 para la nueva generación del proceso de fabricación.

Ejercicio 1.9 Aunque la potencia dinámica es la fuente principal de disipación de potencia en tecnología CMOS, la corriente de fuga ocasiona una disipación de potencia estática, V × Ifuga. Cuanto más pequeñas son las dimensiones del chip, más significativa es la potencia estática. Utilice los datos de la siguiente tabla sobre disipación estática y dinámica de potencia en varias generaciones de procesadores para resolver los siguientes problemas. Tecnología

Potencia dinámica (W)

Potencia estática (W)

Voltaje (V)

a.

250 nm

49

1

3.3

b.

90 nm

75

45

1.1

1.9.1 [5]<1.5> Determine que porcentaje de la potencia disipada corresponde a potencia estática. 1.9.2 [5]<1.5> La potencia estática depende de la corriente de fuga, Pest = V × Ifuga. Determine la corriente de fuga para cada tecnología. 1.9.3 [5]<1.5> Determine la relación entre potencia estática y potencia dinámica para cada tecnología.

Considere ahora la disipación de potencia dinámica en diferentes versiones de un procesador para los tres voltajes dados en la siguiente tabla. 1.2 V

1.0 V

0.8 V

a.

80 W

70 W

40 W

b.

65 W

55 W

30 W

65

66

Capítulo 1

Abstracciones y tecnología de los computadores

1.9.4 [5]<1.5> Determine la potencia estática a 0.8 V de cada versión considerando que la relación entre potencia estática y dinámica es 0.6. 1.9.5 [5]<1.5> Determine la corriente de fuga a 0.8 V para cada versión. 1.9.6 [5]<1.5> Calcule la mayor corriente de fuga a 1.0 V y 1.2 V considerando

que la relación entre potencia estática y dinámica es 1.7.

Ejercicio 1.10 La siguiente tabla muestra los distintos tipos de instrucciones de una aplicación dada que se ejecutan en 1, 2, 4 u 8 procesadores. Utilizando esta información se explorará la aceleración de las aplicaciones en procesadores paralelos. Nº instrucciones por procesador CPI Procesadores Aritméticas Carga/almacen. Salto Aritméticas Carga/almacen. Salto a.

1

2560

1280

256

1

4

2

2

1280

640

128

1

4

2

4

640

320

64

1

4

2

8

320

160

32

1

4

2

Nº instrucciones por procesador CPI Procesadores Aritméticas Carga/almacen. Salto Aritméticas Carga/almacen. Salto a.

1

2560

1280

256

1

4

2

2

1350

800

128

1

6

2

4

800

600

64

1

9

2

8

600

500

32

1

13

2

1.10.1 [5]<1.4, 1.6> En la tabla anterior se muestra el número de instrucciones por procesador necesarias para la ejecución de un programa en un multiprocesador con 1, 2 4 u 8 procesadores. ¿Cuántas instrucciones se han ejecutado en cada procesador? ¿Cuántas instrucciones adicionales se han ejecutado en cada configuración del multiprocesador? 1.10.2 [5]<1.4, 1.6> Con los valores de CPI de la tabla, determine el tiempo total de ejecución para este programa con 1, 2, 4 y 8 procesadores, suponiendo que la frecuencia del reloj es 2 GHz. 1.10.3 [5]<1.4, 1.6> Si el CPI de las instrucciones aritméticas se duplicase, ¿cuál sería el impacto en los tiempos de ejecución con 1, 2, 4 y 8 procesadores?

1.11

Ejercicios

La siguiente tabla muestra el número de instrucciones por núcleo en un procesador multinúcleo, junto con el CPI medio de la ejecución de un programa en 1, 2, 4 y 8 núcleos. Utilizando esta información exploraremos la aceleración de una aplicación en procesadores multinúcleo.

Núcleos por procesador

Instrucciones por núcleo

CPI medio

1

1.00E+10

1.2

2

5.00E+09

1.3

4

2.50E+09

1.5

8

1.25E+09

1.8

Núcleos por procesador

Instrucciones por núcleo

CPI medio

1

1.00E+10

1.2

2

5.00E+09

1.2

4

2.50E+09

1.2

8

1.25E+09

1.2

a.

b.

1.10.4 [5]<1.4, 1.6> Suponiendo que la frecuencia de la señal de reloj es 3 GHz, ¿cuál es el tiempo de ejecución del programa utilizando 1, 2, 4 y 8 núcleos? 1.10.5 [5]<1.5, 1.6> Suponga que el consumo de potencia de un núcleo está dado por la siguiente ecuación

5.0 mA Potencia = --------------- Voltaje 2 MHz siendo el voltaje 1 Voltaje = -- Frecuencia + 0.4 5 con la frecuencia medida en GHz. Es decir, a 5 GHz, el voltaje será 1.4 V. Determine el consumo de potencia del programa cuando se ejecuta en 1, 2, 4 y 8 núcleos si cada núcleo opera a 3 GHz. Asimismo, determine el consumo de potencia del programa cuando se ejecuta en 1, 2, 4 y 8 núcleos si cada núcleo está operando a 500 MHz. 1.10.6 [5]<1.5, 1.6> Determine la energía necesaria para ejecutar el programa en 1, 2, 4 y 8 núcleos suponiendo que la frecuencia de reloj de cada núcleo es 3 GHz y 500 MHz. Utilice las ecuaciones de consumo de potencia de 1.10.5.

67

68

Capítulo 1

Abstracciones y tecnología de los computadores

Ejercicio 1.11 La siguiente tabla muestra los datos de fabricación para varios procesadores. Diámetro de la oblea a.

Dados por oblea

15 cm

b.

25 cm

Defectos por unidad de área

Coste por oblea

90

0.018 defectos/cm2

10

140

2

20

0.024 defectos/cm

1.11.1 [10]<1.7> Determine el factor de producción. 1.11.2 [10]<1.7> Determine el coste por dado. 1.11.3 [10]<1.7> Si el número de dados por oblea se incrementa en un 10% y los defectos por unidad de área en un 15% , determine el área del dado y el factor de producción.

Suponga que el factor de producción varía con la evolución de la tecnología de fabricación de dispositivos electrónicos según la siguiente tabla,

factor de producción

T1

T2

T3

T4

0.85

0.89

0.92

0.95

1.11.4 [10] <1.7> Determine los defectos por unidad de área de cada tecnología para un área de dado de 200 mm2. 1.11.5 [5] <1.7> Represente gráficamente la variación del factor de producción y del número de defectos por unidad de área.

Ejercicio 1.12 La siguiente tabla muestra algunos resultados de la ejecución de los programas de prueba SPEC2006 en el procesador AMD Barcelona. Nombre

Nº intr. × 109

Tiempo de ejecución (s)

Tiempo de referencia (s)

a.

perl

2118

500

9770

b.

mcf

336

1200

9120

1.12.1 [5] <1.7> Determine el CPI si el ciclo del reloj es 0.333 ns. 1.12.2 [5] <1.7> Determine la razón SPEC. 1.12.3 [5] <1.7> Determine la media geométrica para estos dos programas de prueba.

1.11

Ejercicios

1.12.4 La siguiente tabla muestra algunos resultados adicionales. Nombre

CPI

Frecuencia de reloj

Razón SPEC

a.

sjeng

0.96

4 GHz

14.5

b.

omnetpp

2.94

4 GHz

9.1

1.12.5 [5] <1.7> Calcule el aumento del tiempo de CPU cuando el número de instrucciones de los programas de prueba aumenta un 10% sin afectar al CPI. 1.12.6 [5] <1.7> Calcule el aumento del tiempo de CPU cuando el número de instrucciones de los programas de prueba aumenta un 10% y el CPI un 5%. 1.12.7 [5] <1.7> Determine la razón SPEC con los datos de 1.12.5.

Ejercicio 1.13 Suponga que está desarrollando una nueva versión del procesador AMD Barcelona con un reloj de 4 GHz. Ha incorporado algunas instrucciones adicionales al repertorio de instrucciones de modo que el número de instrucciones de los programas de prueba del ejercicio 1.12 se ha reducido un 15%. Los tiempos de ejecución obtenidos se muestran en la siguiente tabla. Nombre

Tiempo de ejecución (s)

Tiempo de referencia (s)

Razón SPEC

a.

perl

450

9770

21.7

b.

mcf

1150

9120

7.9

1.13.1 [10] <1.8> Calcule el nuevo CPI. 1.13.2 [10] <1.8> En general, los CPI obtenidos son mayores que los obtenidos en ejercicios anteriores, debido a las frecuencias de reloj consideradas, 3 GHz y 4 GHz. Compruebe si el incremento del CPI es similar al incremento de la frecuencia. Si son diferentes, explique la causa. 1.13.3 [5] <1.8> ¿Cuál ha sido la reducción en el CPI?

La siguiente tabla muestra más resultados. Nombre

Tiempo de ejecución (s)

CPI

Frecuencia de reloj

a.

sjeng

820

0.96

3 GHz

b.

omnetpp

580

2.94

3 GHz

1.13.4 [10] <1.8> Si el tiempo de ejecución se reduce un 10% adicional sin afectar al CPI, determine el número de instrucciones para una frecuencia de 4GHz.

69

70

Capítulo 1

Abstracciones y tecnología de los computadores

1.13.5 [10] <1.8> Determine la frecuencia de reloj para obtener una reducción adicional del 10% en el tiempo de ejecución manteniendo sin cambios el número de instrucciones y el CPI. 1.13.6 [10] <1.8> Determine la frecuencia de reloj si el CPI se reduce un 15% y

el tiempo de CPU un 20%., manteniendo constante el número de instrucciones.

Ejercicio 1.14 En la sección 1.8 se pone como ejemplo de errores habituales la utilización de un subconjunto de las ecuaciones de prestaciones como métrica de las prestaciones. Para ilustrar este error, considere los datos de ejecución de una secuencia de 106 instrucciones en diferentes procesadores. Procesador

Frecuencia de reloj

CPI

P1

4 GHz

1.25

P2

3 GHz

0.75

1.14.1 [5] <1.8> Una falacia habitual es considerar que el computador con la frecuencia más elevada es el que tiene mejores prestaciones. Compruebe si esto es cierto para P1y P2. 1.14.2 [10] <1.8> Otra falacia es creer que el procesador que ejecuta un mayor número de instrucciones necesita más tiempo de CPU. Considerando que el procesador P1 está ejecutando una secuencia de 106 instrucciones y que el CPI de P1 y P2 no cambia, calcule el número de instrucciones que P2 puede ejecutar en el mismo tiempo que P1 necesita para ejecutar 106 instrucciones 1.14.3 [10] <1.8> Otra falacia habitual se produce al utilizar MIPS (Millones de instrucciones por segundo) para comparar las prestaciones de dos procesadores, y considerar que el procesador con un MIPS más elevado es el que tiene las mejores prestaciones. Compruebe si esto es cierto para P1y P2.

Otra medida habitual de prestaciones es MFLOPS (million floating-point operations per second), que se define como MFLOPS=Nº operaciones PF / tiempo de ejecución × 106 La utilización de esta métrica presenta unos problemas parecidos a los de la utilización de MIPS. Consideremos los programas de la tabla siguiente en un procesador con frecuencia de reloj de 3 GHz. Nº instr.

Instr. L/S

Instr. PF

Instr. salto

CPI(L/S)

CPI(PF)

CPI(salto)

a.

106

50%

40%

10%

0.75

1

1.5

b.

3 × 10

40%

40%

20%

1.25

0.70

1.25

6

1.11

71

Ejercicios

1.14.4 [10] <1.8> Calcule los MFLOPS de los programas. 1.14.5 [10] <1.8> Calcule los MIPS de los programas. 1.14.6 [10] <1.8> Calcule las prestaciones de los programas y compárelas con los valores de MIPS y MFLOPS.

Ejercicio 1.15 Otro error habitual citado en la sección 1.8 es pretender mejorar las prestaciones globales de un computador mejorando únicamente un aspecto concreto. Esto puede ser cierto en algunas ocasiones, pero no siempre. Considere un computador que ejecuta programas con los tiempos mostrados en la tabla.

Instr. PF

Instr. ENT

Instr. L/S

Instr. salto

Tiempo total

a.

35 ns

85 ns

50 ns

30 ns

200 ns

b.

50 ns

80 ns

50 ns

30 ns

210 ns

1.15.1 [5] <1.8> Calcule cuánto se reduce el tiempo total si el tiempo de las operaciones punto flotante (PF) se reduce un 20%. 1.15.2 [5] <1.8> Calcule cuánto se reduce el tiempo de las operaciones enteras (ENT) si el tiempo total se reduce un 20%. 1.15.3 [5] <1.8> Determine si el tiempo total se puede reducir un 20% reduciendo únicamente el tiempo de las instrucciones de salto.

La siguiente tabla muestra los distintos tipos de instrucciones por procesador de una aplicación dada que se ejecuta en números diferentes de procesadores.

a. b.

Nº proc.

Instr. PF

Instr. ENT

Instr. L/S

Instr. salto

1

560 × 106

2000 × 106

1280 × 106

8

80 × 10

240 × 10

160 × 10

6

6

6

CPI (PF)

CPI (ENT)

CPI (L/S)

CPI (salto)

256 × 106

1

1

4

2

32 × 10

1

1

4

2

6

Suponga que la frecuencia de reloj de cada procesador es 2 GHz. 1.15.4 [10] <1.8> ¿Cuánto hay que mejorar el CPI de las instrucciones PF si se quiere que el programa se ejecute el doble de rápido? 1.15.5 10] <1.8> ¿Cuánto hay que mejorar el CPI de las instrucciones L/S si se quiere que el programa se ejecute el doble de rápido?

72

Capítulo 1

Abstracciones y tecnología de los computadores

1.15.6 [5] <1.8> ¿Cuánto se mejora el tiempo de ejecución si el CPI de las ins-

trucciones PF y ENT se reduce un 40% y el CPI de las instrucciones L/S y salto se reduce un 30%?

Ejercicio 1.16 Otro error común está relacionado con la ejecución de programas en un sistema multiprocesador, cuando se intenta mejorar las prestaciones mejorando únicamente el tiempo de ejecución de parte de las rutinas. La siguiente tabla indica el tiempo de ejecución de cinco rutinas de un programa cuando se ejecutan en varios procesadores.

Nº proc. Rutina A (ms) Rutina B (ms) Rutina C (ms) Rutina D (ms) Rutina E (ms) a.

2

20

80

10

70

5

b.

16

4

14

2

12

2

1.16.1 [10] <1.8> Obtenga el tiempo de ejecución total y cuánto se reduce si el tiempo de las rutinas A, C y E se reduce un 15%. 1.16.2 [10] <1.8> ¿Cuánto se mejora el tiempo total si el tiempo de la rutina B se reduce un 10%? 1.16.3 [10] <1.8> ¿Cuánto se mejora el tiempo total si el tiempo de la rutina D se reduce un 10%?

El tiempo de ejecución en un sistema multiprocesador puede dividirse en tiempo de cálculo de la rutinas y tiempo de comunicaciones empleado en el envío de datos de un procesador a otro. Considere los tiempos de cálculo y de comunicación de la tabla. En este caso, el tiempo de comunicaciones es significativo.

Nº proc.

Rutina A (ms)

Rutina B (ms)

Rutina C (ms)

Rutina D (ms)

Rutina E (ms)

Comun. (ms)

2

20

78

9

65

4

11

4

12

44

4

34

2

13

8

1

23

3

19

3

17

16

4

13

1

10

2

22

32

2

5

1

5

1

23

64

1

3

0.5

1

1

26

1.11

Ejercicios

73

1.16.4 [10] <1.8> Determine las relación entre los tiempos de cálculo y los tiempos de comunicación cada vez que se dobla el número de procesadores. 1.16.5 [5] <1.8> Utilizando la media geométrica de las relaciones, extrapole el tiempo de cálculo y el tiempo de comunicación para un sistema con 128 procesadores. 1.16.6 [10] <1.8> Determine el tiempo de cálculo y el tiempo de comunicación para un sistema con un procesador.

1.1, página 9. Cuestiones de discusión: Son válidas varias respuestas. Respuestas a las 1.3, página 25. Memoria en disco: no volátil, tiempo de acceso elevado (milise- autoevaluaciones gundos) y coste entre 0.2 y 2.0 dólares/GB. Memoria semiconductor: volátil, tiempo de acceso reducido (naosegundos) y coste entre 20 y 75 dólares/GB 1.4, página 31. 1.a ambos, b: latencia, c: ninguno, 2.7 segundos 1.4, página 38: b 1.7, página 50. 1, 3 y 4 son razones válidas. Respuesta 5 es generalmente cierta porque el elvado volumen de fabricación puede hacer que la inversión extra para reducir el tamaño del dado, por ejemplo un 10%, sea una buena decisión económica, pero no tiene por qué ser cierto. 1.8, página 53. a. El computador con mayor MIPS es el A. b. El computador más rápido es el B.

2 Instrucciones: el lenguaje del computador Yo hablo castellano con Dios, italiano con las mujeres, francés con los hombres y alemán con mi caballo. Carlos V, rey de Francia 1337–1380

2.1

Introducción 76

2.2

Operaciones del hardware del computador 77

2.3

Operandos del hardware del computador 80

2.4

Números con signo y sin signo 87

2.5

Representación de instrucciones en el computador 94

2.6

Operaciones lógicas 102

2.7

Instrucciones para la toma de decisiones 105

2.8

Apoyo a los procedimientos en el hardware del computador 112

2.9 2.10

Comunicarse con la gente 122 Direcciones y direccionamiento inmediato MIPS para 32 bits 128

2.11 2.12

Paralelismo e instrucciones: sincronización 137 Traducción e inicio de un programa 139

2.13 2.14 2.15

Un ejemplo de ordenamiento en C para verlo todo junto 149 Tablas frente a punteros 157 Perspectiva histórica y lecturas recomendadas 161

2.16 2.17

Caso real: instrucciones ARM 161 Casos reales: instrucciones x86 165

2.18 2.19 2.20 2.21

Falacias y errores habituales 174 Conclusiones finales 176 Perspectiva histórica y lecturas recomendadas 179 Ejercicios 179

Nota importante: En la presente edición en castellano, los contenidos del CD incluido en la edición original (en inglés) son accesibles a través de la página web www.reverte.com/microsites/pattersonhennessy. Aunque en la presente edición no se proporciona un CD-ROM físico, a lo largo de todo el texto se menciona el CD y se utiliza el icono que lo representa para hacer referencia a su contenido.

Los cinco componentes clásicos de un computador

76

Capítulo 2

2.1

Repertorio de instrucciones: vocabulario de comandos entendidos por una arquitectura concreta.

Instrucciones: el lenguaje del computador

Introducción

2.1

Para dominar el hardware de un computador debemos hablar su lenguaje. Las palabras del lenguaje del computador se denominan instrucciones, y su vocabulario se denomina repertorio de instrucciones. En este capítulo veremos el repertorio de instrucciones de un computador real, en la forma escrita por los humanos y en la forma leída por el computador. Introduciremos instrucciones desde el nivel alto hasta el nivel bajo (top-down): comenzando desde una notación que parece un lenguaje de programación limitado, la refinaremos paso a paso hasta que veamos el lenguaje real de un computador real. El capítulo 3 continúa en el mismo orden descendente, y muestra la representación de números enteros y en punto flotante y el hardware que opera con ellos. Podríamos pensar que los lenguajes de los computadores son tan diversos como los distintos idiomas de los humanos, pero en realidad son bastante similares, más parecidos a dialectos regionales que a lenguas independientes. Por tanto, una vez que se aprende uno, es fácil aprender otros. Esta similitud ocurre porque todos los computadores utilizan tecnologías hardware similares basadas en principios similares y porque hay un conjunto de operaciones básicas que todos los computadores deben proporcionar. Por otra parte, los diseñadores de computadores comparten una meta común: encontrar un lenguaje que haga fácil construir el hardware y el compilador, a la vez que se maximicen las prestaciones y se minimice el coste y la potencia consumida. Esta meta ha sido una constante a lo largo del tiempo. La siguiente cita fue escrita antes de que se pudiera comprar un computador, y es tan verdad hoy como lo fue en 1947: Es fácil ver mediante métodos lógico-formales que existen ciertos [repertorios de instrucciones] que son adecuados en abstracto para controlar y ejecutar cualquier secuencia de operaciones… Las consideraciones realmente decisivas, desde el punto de vista actual, al seleccionar un [repertorio de instrucciones] son más bien de naturaleza práctica: la simplicidad del equipo pedido por el [repertorio de instrucciones] y la claridad de su aplicación a los problemas realmente importantes, junto con la velocidad en el manejo de esos problemas. Burks, Goldstine y von Neumann, 1947

La “simplicidad del equipo” es una consideración tan valiosa para los computadores de los años 2000 como lo fue para los de los años 50. La finalidad de este capítulo es enseñar un repertorio de instrucciones que siga esta premisa: mostrar cómo se representa en el hardware y la relación entre el lenguaje de programación de alto nivel y el lenguaje del computador más primitivo. En nuestros ejemplos utilizaremos el lenguaje de programación C. La sección 2.15 en el CD muestra cómo éstos cambiarían para un lenguaje orientado a objetos tipo Java.

2.2

Operaciones del hardware del computador

Al aprender a representar instrucciones también descubriremos el secreto de la computación: el concepto de programa almacenado. Además, ejercitaremos nuestra habilidad con un “idioma extranjero” escribiendo programas en el lenguaje del computador y ejecutándolos en el simulador que proporcionamos con este libro. También veremos el impacto de los lenguajes de programación y la optimización del compilador en las prestaciones. Concluiremos con un vistazo a la evolución histórica del repertorio de instrucciones y con un repaso de otros dialectos del computador. El repertorio de instrucciones elegido proviene de MIPS, un repertorio de instrucciones típico diseñado a partir de 1980. Más adelante, daremos una visión rápida de otros dos repertorios de instrucciones muy populares. ARM es bastante similar a MIPS y en 2008 se han vendido más de 3000 millones de procesadores ARM en dispositivos empotrados. El otro ejemplo, Intel x86, está en el interior de la mayoría de los 330 millones de PCs fabricados en 2008. Mostraremos el repertorio de instrucciones MIPS por partes, una cada vez, relacionándola con la estructura del computador. Este tutorial top-down relaciona paso a paso los componentes con sus explicaciones, haciendo el lenguaje ensamblador más digerible. La figura 2.1 da una visión general del repertorio de Instrucciones de este capítulo.

2.2

Operaciones del hardware del computador

2.2

Todo computador debe ser capaz de realizar cálculos aritméticos. La notación del lenguaje ensamblador MIPS: add a, b, c

indica a un computador que sume las variables b y c y que coloque el resultado en a. Esta notación es rígida en el sentido de que cada instrucción aritmética MIPS lleva a cabo sólo una operación y debe tener siempre exactamente tres variables. Por ejemplo, Suponga que queremos colocar la suma de las variables b, c, d y e en la variable a (en esta sección se está siendo deliberadamente vago sobre lo que es una “variable” ; en la sección siguiente lo explicaremos en detalle). La siguiente secuencia de instrucciones suma las cuatro variables: add a, b, c add a, a, d add a, a, e

# la suma de b y c se coloca en a # la suma de b, c y d está ahora en a # la suma de b, c, d y e está ahora en a

Por tanto, se necesitan tres instrucciones para realizar la suma de cuatro variables. Las palabras a la derecha del símbolo sostenido (#) en cada una de las líneas anteriores son comentarios para el lector y son ignorados por el computador. Observe que, a diferencia de otros lenguajes de programación, cada línea de este lenguaje puede contener como mucho una instrucción. Otra diferencia con el lenguaje C es que los comentarios siempre terminan al final de una línea.

77

Concepto de programa almacenado: la idea de que instrucciones y datos de diferentes tipos se pueden almacenar en memoria como números nos lleva al concepto de computador de programa almacenado.

“Ciertamente, deben existir instrucciones para realizar las operaciones aritméticas fundamentales.” Burks, Goldstine y von Neumann, 1947

78

Capítulo 2

Instrucciones: el lenguaje del computador

Operandos MIPS Nombre

Ejemplo

Comentarios

32 registros

$s0–$s7, $t0–$t9, $zero, $a0–$a3, $v0–$v1, $gp, $fp, $sp, $ra, $at

Localizaciones rápidas para los datos. En MIPS, los datos deben estar en los registros para realizar operaciones aritméticas. El registro MIPS $zero es siempre igual a 0. El registro $at está reservado por el ensamblador para manejar constantes grandes.

Memory[0], 230 palabras Memory[4], . . . , de memoria Memory[4294967292]

Accesibles solamente por instrucciones de transferencia de datos. MIPS utiliza direcciones de byte, de modo que las direcciones de palabras consecutivas se diferencian en 4. La memoria guarda las estructuras de datos, las tablas y los registros desbordados (guardados).

Categoría

Instrucción

Ejemplo

add

add

$s1,$s2,$s3 $s1 = $s2 + $s3

Tres operandos; datos en registros

Aritmética

subtract

sub

$s1,$s2,$s3 $s1 = $s2 – $s3

Tres operandos; datos en registros

Lenguaje ensamblador MIPS Significado

Comentarios

add immediate

addi $s1,$s2,100 $s1 = $s2 + 100

load word

lw

$s1,100($s2) $s1 = Memory[$s2 + 100] Palabra de memoria a registro

store word

sw

$s1,100($s2) Memory[$s2 + 100] = $s1

Palabra de registro a memória

load half

lh

$s1,100($s2) $s1 = Memory[$s2 + 100]

Media palabra de memoria a registro Media palabra de registro a memoria

Usado para sumar constantes

Transferencia store half de dato load byte

sh

$s1,100($s2) Memory[$s2 + 100] = $s1

lb

$s1,100($s2) $s1 = Memory[$s2 + 100]

Byte de memoria a registro

store byte

sb

$s1,100($s2) Memory[$s2 + 100] = $s1

Byte de registro a memoria

load upper immed.

lui

$s1,100

and

and

$s1,$s2,$s3 $s1 = $s2 & $s3

or

or

$s1,$s2,$s3 $s1 = $s2 | $s3

Tres registros operandos; OR bit-a-bit

nor

nor

$s1,$s2,$s3 $s1 = ~ ($s2 |$s3)

Tres registros operandos; NOR bit-a-bit

and immediate

andi $s1,$s2,100 $s1 = $s2 & 100

AND Bit-a-bit registro con constante

or immediate

ori

$s1,$s2,100 $s1 = $s2 | 100

OR Bit-a-bit registro con constante

shift left logical

sll

$s1,$s2,10

$s1 = $s2 << 10

Desplazamiento a la izquierda por constante

shift right logical

srl

$s1,$s2,10

$s1 = $s2 >> 10

Desplazamiento a la derecha por constante

branch on equal

beq

$s1,$s2,L

if ($s1 == $s2) go to L PC + 4 + 100

Comprueba igualdad y bifurca relativo al PC

branch on not equal

bne

$s1,$s2,L

if ($s1 != $s2) go to L PC + 4 + 100

Comprueba si no igual y bifurca relativo al PC

set on less than

slt

$s1,$s2,$s3

if ($s2 < $s3) $s1 = 1; else $s1 = 0

Compara si es menor que; usado con beq, bne

set on less than immediate

slt

$s1,$s2,100

if ($s2 < 100) $s1 = 1; else $s1 = 0

Compara si es menor que una constante

Lógica

Salto condicional

jump Salto jump register incondicional jump and link

$s1 = 100 * 216

Cargar constante en los 16 bits de mayor peso Tres registros operandos; AND bit-a-bit

j

2500

go to 10000

Salto a la dirección destino

jr

$ra

go to $ra

Para retorno de procedimiento

jal

2500

$ra = PC + 4; go to 10000 Para llamada a procedimiento

FIGURA 2.1 Lenguaje ensamblador MIPS tratado en este capítulo. Esta información se encuentra también en la Tarjeta de Datos de Referencia de MIPS, que se incluye con este libro.

2.2

79

Operaciones del hardware del computador

El número natural de operandos para una operación como la suma es tres: los dos números que se suman juntos y el lugar donde se coloca el resultado de la suma. El hecho de que cada instrucción requiera tener exactamente tres operandos, ni más ni menos, está de acuerdo con la filosofía de mantener el hardware sencillo: el hardware para un número de operandos variable es más complejo que el hardware para un número fijo. Esta situación ilustra el primero de los cuatro principios fundamentales del diseño de hardware: Principio de diseño 1: la simplicidad favorece la regularidad. Ahora podemos mostrar, en los dos ejemplos siguientes, la relación que existe entre los programas escritos en lenguaje de programación de alto nivel y los programas escritos en esta notación más primitiva.

Compilación de dos sentencias de asignación C en MIPS

Este segmento de un programa en C contiene cinco variables: a, b, c, d y e. Puesto que Java se desarrolló a partir de C, este ejemplo y los siguientes son válidos para ambos lenguajes de programación de alto nivel.

EJEMPLO

a = b + c; d = a – e;

La traducción de C a instrucciones del lenguaje ensamblador MIPS la realiza el compilador. Mostrar el código MIPS producido por un compilador.

Una instrucción MIPS trabaja con dos operandos fuente y coloca el resultado en un operando destino. Por tanto, las dos sentencias simples anteriores se compilan directamente en estas dos instrucciones del lenguaje ensamblador MIPS:

RESPUESTA

add a, b, c sub d, a, e

Compilación de una sentencia C compleja en MIPS

Una sentencia un tanto compleja contiene cinco variables: f, g, h, i y j: donde: f = (g + h) – (i + j);

¿Qué debería producir un compilador de C?

EJEMPLO

80

Capítulo 2

RESPUESTA

Instrucciones: el lenguaje del computador

El compilador debe fragmentar esta sentencia en varias instrucciones de ensamblador, puesto que se realiza sólo una operación por cada instrucción MIPS. La primera instrucción MIPS calcula la suma de g y h. Debemos colocar el resultado en algún lugar, por tanto el compilador crea una variable temporal llamada t0: add t0,g,h # la variable temporal t0 contiene g + h

Aunque la siguiente operación en C es una resta, necesitamos calcular la suma de i y j antes de poder restar. Por tanto, la segunda instrucción coloca la suma de i y j en otra variable temporal creada por el compilador llamada t1: add t1,i,j # la variable temporal t1 contiene i + j

Finalmente, la instrucción de restar resta la segunda suma de la primera y coloca la diferencia en la variable f, completando el código compilado: sub f,t0,t1 # f obtiene t0 – t1, que es (g + h)–(i + j)

Autoevaluación Para una función dada, ¿qué lenguaje de programación lleva probablemente más líneas de código? Poner las tres representaciones siguientes en orden: 1. Java 2. C 3. Lenguaje ensamblador MIPS Extensión: Para incrementar la portabilidad, Java fue originalmente concebido como dependiente de un software intérprete. El repertorio de instrucciones de este intérprete se denomina Java bytecodes (véase sección 2.15 en el CD), y es completamente distinto del repertorio de instrucciones MIPS. Para conseguir unas prestaciones cercanas al programa C equivalente, los sistemas Java típicos actuales compilan los Java bytecodes en un repertorio de instrucciones nativo parecido a MIPS. Debido a que esta compilación se hace normalmente mucho más tarde que en los programas C, tales compiladores Java son frecuentemente llamados compiladores Just-In-Time (JIT). La sección 2.12 muestra cómo los compiladores JIT se utilizan más tarde que los compiladores C en el proceso de arranque del programa, y la sección 2.13 muestra las consecuencias en las prestaciones de compilación frente a las de interpretación de los programas Java.

2.3

Operandos del hardware del computador

2.3

A diferencia de los programas en lenguajes de alto nivel, los operandos de las instrucciones aritméticas tienen restricciones; debemos usar un número limitado de posiciones especiales construidas directamente en hardware llamados registros.

2.3

81

Operandos del hardware del computador

Los registros son las piezas para la construcción de un computador: son elementos básicos usados en el diseño hardware que son también visibles al programador cuando el computador está terminado. El tamaño de un registro en la arquitectura MIPS es de 32 bits; estos grupos de 32 bits son tan frecuentes que se les ha dado el nombre de palabra. Una diferencia importante entre las variables de un lenguaje de programación y los registros es su número limitado, usualmente 32, en los computadores actuales como MIPS. (Véase la sección 2.20 en el CD para conocer la historia del número de registros.) Así, continuando de arriba hacia abajo (top-down) con el desarrollo de la representación simbólica del lenguaje MIPS, en esta sección hemos añadido la restricción de que cada uno de los tres operandos de las instrucciones aritméticas MIPS debe elegirse a partir de uno de los 32 registros de 32 bits. La razón del límite de 32 registros se puede encontrar en el segundo de nuestros cuatro principios fundamentales del diseño de hardware:

Palabra: unidad natural de acceso en un computador; normalmente un grupo de 32 bits; corresponde al tamaño de un registro en la arquitectura MIPS.

Principio de diseño 2: Cuanto más pequeño, más rápido. Un número muy grande de registros puede aumentar la duración del ciclo de reloj, simplemente porque se necesitan señales electrónicas más largas cuando deben viajar más lejos. Directrices tales como “cuanto más pequeño, más rápido” no son absolutas; 31 registros pueden no ser más rápidos que 32. Aun así, la verdad que hay detrás de tales observaciones obliga a los diseñadores de computadores a tomarlas en serio. En este caso, el diseñador debe equilibrar el ansia de programar con más registros con el deseo de mantener el ciclo de reloj rápido. Otra razón para no usar más de 32 registros es el número de bits que ocuparía en el formato de instrucción, como muestra la sección 2.5. El capítulo 4 muestra el importante papel de los registros en la construcción del hardware. Como veremos en este capítulo, el uso eficaz de los registros es clave para las prestaciones de los programas. Aunque podríamos escribir instrucciones simplemente usando números para los registros, de 0 a 31, el convenio para MIPS es utilizar nombres de dos caracteres precedidos por un signo de dólar para representar un registro. En la sección 2.8 explicaremos las razones que hay detrás de esta nomenclatura. Por ahora, utilizaremos $s0, $s1, . . . para los registros que corresponden a variables en los programas en C y en Java, y $t0, $t1, . . . para los registros temporales usados para compilar en instrucciones MIPS.

Compilación de una sentencia de asignación en C usando registros

Es trabajo del compilador asociar las variables del programa a los registros. Tomemos, por ejemplo, la sentencia de asignación de nuestro ejemplo anterior: f = (g + h) – (i + j);

Las variables f, g, h, i y j se asignan a los registros $s0, $s1, $s2, $s3 y $s4, respectivamente. ¿Cuál es el código MIPS compilado?

EJEMPLO

82

Capítulo 2

RESPUESTA

Instrucciones: el lenguaje del computador

El programa compilado es muy similar al ejemplo anterior, excepto que ahora reemplazamos las variables por los nombres de registros mencionados anteriormente más los dos registros temporales $t0 y $t1, que corresponden a las variables temporales add $t0,$s1,$s2 # el registro $t0 contiene g + h add $t1,$s3,$s4 # el registro $t1 contiene i + j sub $s0,$t0,$t1 # f se carga con $t0 – $t1, que es # (g + h)–(i + j)

Operandos en memoria

Instrucciones de transferencia de datos: comandos que mueven datos entre memoria y registros.

Los lenguajes de programación tienen variables simples que contienen elementos con datos únicos, como en los ejemplos anteriores; pero también tienen estructuras de datos más complejas, como tablas (arrays) y estructuras. Estas estructuras complejas pueden contener muchos más datos que el número de registros que hay en un computador. ¿Cómo puede un computador representar y acceder a estructuras tan grandes? Recordemos los cinco componentes de un computador introducidos en el capítulo 1 y representados en la página 75. El procesador puede mantener solamente una cantidad pequeña de datos en los registros, pero la memoria del computador contiene millones de datos. Por tanto, las estructuras de datos (tablas y estructuras) se guardan en memoria. Según lo explicado anteriormente, las operaciones aritméticas se producen sólo entre registros en las instrucciones MIPS. Así, MIPS debe incluir instrucciones que transfieran datos entre la memoria y los registros. Tales instrucciones son llamadas instrucciones de transferencia de datos. Para acceder a una palabra en memoria, la instrucción debe proporcionar la dirección de memoria. La memoria es simplemente una gran tabla unidimensional, y la dirección actúa como índice de esa tabla y empieza por 0. Por ejemplo, en la figura 2.2 la dirección del tercer elemento de datos es 2, y el valor de Memoria[2] es 10:

Dirección: valor usado para señalar la posición de un elemento de datos específico dentro de una memoria.

Procesador

3

100

2

10

1

101

0

1

Dirección

Datos

Memoria

FIGURA 2.2 Direcciones de memoria y contenidos de memoria en esas posiciones. Esto es una simplificación del direccionamiento MIPS. La figura 2.3 muestra el direccionamiento MIPS real para direcciones de palabras secuenciales en memoria.

2.3

83

Operandos del hardware del computador

La instrucción de transferencia de datos que copia datos de la memoria a un registro se llama tradicionalmente load (carga). El formato de la instrucción de carga (load) es el nombre de la operación seguido por el registro que se cargará, una constante y el registro usado para acceder a la memoria. La dirección de memoria se forma sumando la parte constante de la instrucción y el contenido del segundo registro. El nombre MIPS real para esta instrucción es lw, contracción de load word (cargar palabra).

Compilación de una sentencia de asignación cuando un operando está en memoria

Suponga que A es una tabla de 100 palabras y que, como antes, el compilador ha asociado las variables g y h a los registros $s1 y $s2. Suponga también que la dirección de comienzo o dirección base de la tabla está en $s3. Compilar esta sentencia de asignación en C:

EJEMPLO

g = h + A[8];

Aunque hay una sola operación en esta sentencia de asignación, uno de los operandos está en memoria, así que debemos hacer primero la transferencia de A[8] a un registro. La dirección de este elemento de la tabla es la suma de la base de la tabla A, que se encuentra en el registro $s3, más el número para seleccionar el elemento 8. El dato debe colocarse en un registro temporal para usarlo en la instrucción siguiente. De acuerdo con la figura 2.2, la primera instrucción compilada es: lw $t0,8($s3)# el registro temporal $t0 toma el valor # de A[8]

(En la página siguiente haremos un pequeño ajuste a esta instrucción, pero por ahora utilizaremos esta versión simplificada.) La instrucción siguiente puede operar con el valor $t0 (que contiene A[8]), puesto que está en un registro. La instrucción debe sumar h (contenido en $s2) con A[8] ($t0) y colocar el resultado en el registro que corresponde a g (asociado con $s1): add

$s1,$s2,$t0 # g = h + A[8]

La constante en una instrucción de transferencia de datos se llama desplazamiento, y el registro añadido para formar la dirección se llama registro base.

RESPUESTA

84

Capítulo 2

Interfaz hardware software

Restricción de la alineación: requisito de que los datos se alineen en memoria en límites naturales.

Instrucciones: el lenguaje del computador

Además de asociar variables a los registros, el compilador asigna las estructuras de datos, tales como tablas y estructuras, a posiciones de memoria. Con esto el compilador puede situar la dirección de comienzo adecuada en las instrucciones de transferencia de datos. Puesto que los bytes (8 bits) se utilizan en muchos programas, la mayoría de las arquitecturas pueden direccionar bytes individuales. Por tanto, la dirección de una palabra coincide con la dirección de uno de sus 4 bytes. Así pues, las direcciones de palabras consecutivas difieren en 4 unidades. Por ejemplo, la figura 2.3 muestra las direcciones MIPS reales para la figura 2.2; la dirección del byte de la tercera palabra es 8. En MIPS, las palabras deben comenzar en direcciones múltiplos de 4. Este requisito se llama restricción de la alineación, y muchas arquitecturas la tienen. (El capítulo 4 explica por qué la alineación facilita transferencias de datos más rápidas). Los computadores se dividen en aquellos que utilizan la dirección del byte del extremo izquierdo como la dirección de la palabra —el extremo mayor o big end— y aquellos que utilizan el byte de más a la derecha —el extremo menor o little end—. MIPS usa el extremo mayor (es un Big Endian). (El apéndice B, muestra las dos opciones de la numeración de los bytes en una palabra). El direccionamiento de byte también afecta al índice de la tabla (array). Para conseguir la dirección apropiada del byte en el código anterior, el desplazamiento que se añadirá al registro base $s3 debe ser 4 × 8, ó 32, de modo que la dirección cargada sea A[8] y no A[8/4]. (Véase la trampa de la sección 2.18, en la página 175.)

Procesador

12

100

8

10

4

101

0

1

Dirección

Datos

Memoria

FIGURA 2.3 Direcciones de memoria reales MIPS y contenido de la memoria para dichas palabras. Las direcciones que han cambiado están resaltadas para comparar con las de la figura 2.2. Puesto que MIPS direcciona cada byte, las direcciones de las palabras son múltiplos de cuatro, ya que hay 4 bytes en cada palabra.

2.3

85

Operandos del hardware del computador

La instrucción complementaria de load (cargar) se ha llamado tradicionalmente store (almacenar), y copia los datos de un registro en la memoria. El formato de store es similar al de load: el nombre de la operación, seguido por el registro a almacenar, el desplazamiento para seleccionar el elemento de la tabla y finalmente el registro base. De nuevo, la dirección de MIPS se especifica en parte por una constante y en parte por el contenido de un registro. El nombre MIPS real es sw, contracción de store word (almacenar palabra).

Compilación usando load y store

Suponga que la variable h está asociada al registro $s2 y la dirección base de la tabla A está en $s3. ¿Cuál es el código ensamblador MIPS para la siguiente sentencia C de asignación?

EJEMPLO

A[12] = h + A[8];

Aunque hay una sola operación en esta declaración de C, dos de los operandos están en memoria, así que necesitamos aún más instrucciones MIPS. Las dos primeras instrucciones son las mismas que en el ejemplo anterior, excepto que esta vez utilizamos el desplazamiento apropiado para el direccionamiento de byte en la instrucción de load word para seleccionar A[8], y la instrucción add coloca la suma en $t0: lw

$t0,32($s3)# el registro temporal $t0 toma el # valor de A[8]

add $t0,$s2,$t0 # el registro temporal $t0 toma el # valor de h + A[8]

La instrucción final almacena la suma en A[12], utilizando 48 (4 × 12) como desplazamiento y $s3 como registro base. sw

$t0,48($s3) # almacena h + A[8] en A[12]

Cargar palabra y almacenar palabra son las instrucciones que permiten copiar palabras entre memoria y registros en la arquitectura MIPS. Otros computadores tienen otras instrucciones además de cargar y almacenar para transferir datos. Una de estas arquitecturas es la Intel x86, que se describe en la sección 2.17.

RESPUESTA

86

Capítulo 2

Interfaz hardware software

Instrucciones: el lenguaje del computador

Muchos programas tienen más variables que registros tienen los computadores. Por tanto, el compilador intenta mantener las variables más frecuentemente usadas en registros y coloca el resto en memoria, y utiliza load y store para mover variables entre los registros y la memoria. El proceso de poner las variables usadas con menos frecuencia (o aquellas que necesitaremos más tarde) en memoria se llama spilling (desbordar los registros). El principio hardware que relaciona tamaño y velocidad sugiere que la memoria debe ser más lenta que los registros, puesto que los registros son de menor tamaño. Éste es, efectivamente, el caso: los accesos a los datos son más rápidos si los datos están en los registros en vez de en la memoria. Además, el dato es más útil cuando está en un registro. Una instrucción aritmética MIPS puede leer dos registros, operar con ellos y escribir el resultado. Una instrucción de transferencia de datos MIPS lee o escribe solamente un operando, sin operar con él. Así, los registros MIPS necesitan menos tiempo de acceso y tienen mayor productividad que la memoria (una rara combinación), al hacer que sea más rápido acceder a los datos de los registros y que éstos sean más fáciles de usar. Para alcanzar las mayores prestaciones los compiladores deben utilizar eficientemente los registros.

Operandos constantes o inmediatos Muchas veces un programa utilizará una constante en una operación —por ejemplo, incrementando un índice para señalar el elemento siguiente de una tabla—. De hecho, más de la mitad de las instrucciones aritméticas MIPS tienen una constante como operando cuando ejecutan los programas de prueba (benchmark) SPEC2006. Si usamos solamente las instrucciones que hemos visto hasta ahora, tendríamos que cargar alguna constante de la memoria para poder utilizar una (las constantes habrían sido puestas en memoria cuando el programa fue cargado). Por ejemplo, para sumar la constante 4 al registro $s3 podríamos utilizar el código: lw $t0, AddrConstant4($s1)# $t0 = constante 4 add $s3,$s3,$t0 # $s3 = $s3 + $t0 ($t0 == 4)

suponiendo que AddrConstant4 es la dirección de memoria de la constante 4. Un alternativa que evita la instrucción de carga es ofrecer versiones de las instrucciones aritméticas en las cuales un operando es una constante. Esta instrucción de suma rápida con un operando constante se llama suma inmediata (add immediate) o addi. Para sumar 4 al registro $s3 escribimos: addi

$s3,$s3,4

# $s3 = $s3 + 4

Las instrucciones inmediatas ilustran el tercer principio del diseño de hardware, citado anteriormente en los errores y trampas del capítulo 1: Principio de diseño 3: Hacer rápido lo común (lo más frecuente).

2.4

Números con signo y sin signo

87

Los operandos-constante son muy frecuentes, y al incluir las constantes en la operación aritmética, las operaciones son mucho más rápidas y requieren menos energía que si las constantes fuesen cargadas desde memoria. El valor constante cero juega otro papel: ofrecer variaciones útiles para simplificar el repertorio de instrucciones. Por ejemplo, la operación mover es simplemente una suma con un operando igual a cero. Por lo tanto, MIPS tiene un registro, $zero, con el valor cero fijado por hardware. (Como es de esperar, es el registro número 0.) Dada la importancia de los registros, ¿cuál es el coeficiente de incremento en el Autoevaluación número de registros en un chip con relación al tiempo? 1. Muy rápido: aumentan tan rápido como la ley de Moore, que predice que se dobla el número de transistores en un chip cada 18 meses. 2. Muy lento: puesto que los programas generalmente se distribuyen en el lenguaje del computador, existe cierta inercia en la arquitectura del repertorio de instrucciones, y por eso el número de registros aumenta sólo tan rápido como los nuevos repertorios de instrucciones llegan a ser viables. Extensión: Aunque los registros MIPS en este libro son de 32 bits de ancho hay una versión del repertorio de instrucciones de MIPS con 32 registros de 64 bits de ancho. Para distinguirlos claramente son llamados oficialmente MIPS-32 y MIPS-64. En este capítulo utilizamos un subconjunto de MIPS-32. El apéndice E en el CD muestra las diferencias entre MIPS-32 y MIPS-64. El modo de direccionamiento MIPS, registro base más desplazamiento, es una combinación excelente para tratar tanto las estructuras como las tablas, ya que el registro puede señalar el inicio de la estructura y el desplazamiento puede seleccionar el elemento deseado. Veremos un ejemplo de esto en la sección 2.13. El registro, en las instrucciones de transferencia de datos, fue inventado originalmente para mantener un índice de una tabla con el desplazamiento usado para la dirección inicial de la tabla. Por eso el registro base se llama también registro índice. Las memorias actuales son mucho más grandes y el modelo de programación para la asignación de los datos es más sofisticado, así que la dirección base de la tabla normalmente se pasa a un registro, puesto que no cabría en el desplazamiento, tal y como veremos. La sección 2.4 explica que, puesto que MIPS soporta constantes negativas, no hay necesidad de resta inmediata en lenguaje MIPS.

2.4

Números con signo y sin signo

2.4

En primer lugar daremos un vistazo a como se representan los número en el computador. Los seres humanos pensamos en base 10, pero los números pueden representarse en cualquier base. Por ejemplo, 123 en base 10 = 1111011 en base 2. Los números se almacenan en el computador como una serie de señales eléctricas, alta o baja, y por lo tanto se consideran números en base 2. (Del mismo modo que los números en base 10 se llaman números decimales, los números en base 2 se llaman números binarios.) Así, un dígito de un número binario es el “átomo” de la computación, puesto que toda la información se compone de dígitos binarios o bits. Este bloque de cons-

Dígito binario o bit: uno de los dos números en base 2, 0 ó 1, que son los componentes de la información.

88

Capítulo 2

Instrucciones: el lenguaje del computador

trucción fundamental puede tomar dos valores, que pueden verse desde diferentes perspectivas: alto o bajo, activo (on) o inactivo (off), verdadero o falso, 1 o 0. En cualquier base, el valor del i-ésimo digito d es d × basei donde i empieza en 0 y se incrementa de derecha a izquierda. Esto conduce a una manera obvia de numerar los bits de una palabra: simplemente se usa la potencia de la base para ese bit. Indicamos los números decimales con el subíndice diez y los números binarios con el subíndice dos. Por ejemplo, 1011dos

representa (1 × 23) + (0 × 22) + (1 × 21) + (1 × 20)diez = (1 × 8)+ (0 × 4) + (1 × 2) + (1 × 1)diez = 8 + 0 + 2 + 1diez = 11diez

Aquí los bits de la palabra se numeran 0, 1, 2, 3, … de derecha a izquierda. A continuación se muestra la numeración de los bits dentro de una palabra MIPS y la colocación del número 1011dos: 31 30 29 28 27 26 25 24 23 22 21 20 19 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0

0 0 0 0 0 0 0 1 0 1 1

(anchura de 32 bits)

Bit menos significativo: es el bit de más a la derecha en una palabra MIPS.

Bit más significativo: es el bit de más a la izquierda en una palabra MIPS.

Puesto que las palabras se escriben tanto verticalmente como horizontalmente, puede que el extremo izquierdo y el derecho no estén claros. Por eso, la frase el bit menos significativo se usa para referirse al bit situado más a la derecha (bit 0 el dibujo anterior) y el bit más significativo para el bit situado más a la izquierda (bit 31). La palabra MIPS tiene una longitud de 32 bits, de manera que podemos representar 232 patrones de 32 bits diferentes. Es natural que estas combinaciones representen los números del 0 a 232 – 1 (4 294 967 295diez): 0000 0000 0000 0000 0000 0000 0000 0000dos = 0diez 0000 0000 0000 0000 0000 0000 0000 0001dos = 1diez 0000 0000 0000 0000 0000 0000 0000 0010dos = 2diez ... ... 1111 1111 1111 1111 1111 1111 1111 1101dos = 4 294 967 293diez 1111 1111 1111 1111 1111 1111 1111 1110dos = 4 294 967 294diez 1111 1111 1111 1111 1111 1111 1111 1111dos = 4 294 967 295diez

Es decir, los números binarios de 32 bits se pueden representar en términos del valor del bit multiplicado por la potencia de 2 (donde xi significa el i-ésimo bit de x) (x31 × 231) + (x30 × 230) + (x29 × 229) + . . . + (x1 × 21) + (x0 × 20)

2.4

Números con signo y sin signo

Tenga en cuenta que los patrones de bits binarios anteriores son simples representaciones de números. Los números, en realidad, tienen un número infinito de cifras, donde casi todas son 0 excepto unas pocas situadas más a la derecha. Simplemente, no mostramos los 0s delanteros. El hardware se puede diseñar para sumar, restar, multiplicar y dividir estos patrones de bits binarios. Si el número que es el resultado correcto de estas operaciones no se puede representar por esos bits del hardware situados más a la derecha, se dice que se ha producido desbordamiento. Es responsabilidad del sistema operativo y del programa determinar qué hacer si se produce un desbordamiento. Los programas de computador operan tanto con números positivos como con negativos, de manera que necesitamos una representación que distinga el positivo del negativo. La solución más obvia es añadir un signo por separado, que puede ser convenientemente representado por un único bit; el nombre de esta representación es signo y magnitud. Desgraciadamente, la representación signo y magnitud tiene diversas limitaciones. Primera, no es obvio dónde poner el bit de signo: ¿a la derecha?, ¿a la izquierda? Los primeros computadores eligieron ambas formas. Segundo, los sumadores de signo y magnitud pueden necesitar un paso extra para establecer el signo, porque no podemos saber por adelantado cuál será el signo correcto. Finalmente, un signo separado significa que la representación signo y magnitud tiene tanto un cero positivo como negativo, lo cual puede causar problemas a los programadores poco cuidadosos. Como resultado de estas limitaciones, la representación signo y magnitud fue pronto abandonada. En la búsqueda de una alternativa más atractiva, se planteó cuál debería ser el resultado para números sin signo si intentásemos restar un número grande a otro menor. La respuesta es que se intentaría tomar prestado de la cadena de 0s iniciales, de modo que el resultado tendría una cadena de 1s delanteros. Dado que no había una alternativa obvia mejor, la solución final fue elegir la representación que hacía el hardware más simple: los 0s del inicio significan positivo, los 1s del inicio significan negativo. Esta convención para representar los números binarios con signo se llama representación en complemento a dos: 0000 0000 0000 0000 0000 0000 0000 0000dos = 0diez 0000 0000 0000 0000 0000 0000 0000 0001dos = 1diez 0000 0000 0000 0000 0000 0000 0000 0010dos = 2diez ... ... 0111 1111 1111 1111 1111 1111 1111 1101dos 0111 1111 1111 1111 1111 1111 1111 1110dos 0111 1111 1111 1111 1111 1111 1111 1111dos 1000 0000 0000 0000 0000 0000 0000 0000dos 1000 0000 0000 0000 0000 0000 0000 0001dos 1000 0000 0000 0000 0000 0000 0000 0010dos ...

= = = = = =

2 147 483 645diez 2 147 483 646diez 2 147 483 647diez –2 147 483 648diez –2 147 483 647diez –2 147 483 646diez ...

1111 1111 1111 1111 1111 1111 1111 1101dos = –3diez 1111 1111 1111 1111 1111 1111 1111 1110dos = –2diez 1111 1111 1111 1111 1111 1111 1111 1111dos = –1diez

89

90

Capítulo 2

Instrucciones: el lenguaje del computador

La mitad positiva de los números, desde 0 hasta 2 147 483 647diez (231 – 1), usa la misma representación que antes. El siguiente patrón de bits (1000 … 0000dos) representa el número más negativo –2 147 483 648diez (231). Le siguen un conjunto decreciente de números negativos –2 147 483 647diez (1000 … 0001dos) hasta –1 diez (1111…1111dos). La representación en complemento a dos tiene un número negativo, –2 147 483 648diez, que no tiene su correspondiente número positivo. Tal desequilibrio es una preocupación para el programador poco atento, pero el signo y magnitud tenía problemas tanto para el programador como para el diseñador del hardware. Consecuentemente, todos los computadores hoy en día usan representaciones en complemento a dos para los números con signo. La representación en complemento a dos tiene la ventaja de que todos los números negativos tienen un 1 en el bit más significativo. Consecuentemente, el hardware sólo necesita comprobar este bit para saber si un número es positivo o negativo (el cero se considera positivo). Este bit se llama frecuentemente bit de signo. Reconociendo el papel de este bit de signo, podemos representar los números de 32 bits positivos y negativos en términos del valor del bit multiplicado por la potencia de 2: (x31 × –231) + (x30 × 230) + (x29 × 229) + . . . + (x1 × 21) + (x0 × 20) El bit de signo se multiplica por –231, y el resto de bits se multiplican por la versión positiva de sus valores de base respectivos.

Conversión de binario a decimal

EJEMPLO

¿Cuál es el valor decimal de este número de 32 bits en complemento a dos? 1111 1111 1111 1111 1111 1111 1111 1100dos

RESPUESTA

Sustituyendo los valores de los bits del número en la fórmula anterior: (1 × –231) + (1 × 230) + (1 × 229) + . . . + (1 × 22) + (0 × 21) + (0 × 20) = –231 + 230 + 229 + . . . + 22 + 0 + 0 = –2 147 483 648diez + 2 147 483 644diez = – 4diez Pronto veremos un atajo para simplificar esta conversión. Del mismo modo que una operación con números sin signo puede desbordar la capacidad del hardware para representar el resultado, así puede suceder con una operación con números en complemento a dos. El desbordamiento ocurre cuando el bit situado más a la izquierda guardado del patrón de bits binario no es el mismo que el valor de los infinitos dígitos de la izquierda (el bit de signo es incorrecto): un 0 a la izquierda del patrón de bits cuando el número es negativo o un 1 cuando el número es positivo.

2.4

91

Números con signo y sin signo

A diferencia de los números tratados anteriormente, las direcciones de memoria empiezan de manera natural en 0 y continúan hasta la dirección más alta. Dicho de otra manera, las direcciones negativas no tienen sentido. Así, los programas a veces deben trabajar con números que pueden ser positivos o negativos y a veces con números que sólo pueden ser positivos. Algunos lenguajes de programación reflejan esta situación. El lenguaje C, por ejemplo, nombra a los primeros enteros integer (declarados como int en el programa) y a los últimos unsigned integers, (unsigned int). Algunas guías de estilo de C incluso recomiendan declarar los primeros como signed int (enteros con signo), para marcar la diferencia claramente.

Interfaz hardware software

Veamos unos cuantos atajos cuando trabajamos con números en complemento a dos. El primer atajo es una manera rápida de negar un número binario en complemento a dos. Simplemente se invierte cada 0 a 1 y cada 1 a 0 y sumando 1 al resultado. Este atajo se basa en la observación de que la suma de un número y su inverso debe ser 111 . . . 111dos, lo cual representa –1. Puesto que x + x } – 1 , por lo tanto x + x + 1 = 0 o x + 1= –x.

Atajo de negación

Negar 2diez, y entonces comprobar el resultado negando –2diez.

2diez = 0000 0000 0000 0000 0000 0000 0000 0010dos

Negando este número invirtiendo los bits y añadiendo uno, sumando 1, tenemos,

+

1111 1111 1111 1111 1111 1111 1111 1101dos 1dos

= =

1111 1111 1111 1111 1111 1111 1111 1110dos –2diez

EJEMPLO

RESPUESTA

92

Capítulo 2

Instrucciones: el lenguaje del computador

Y, en sentido contrario, 1111 1111 1111 1111 1111 1111 1111 1110dos

primero invertimos y luego incrementamos: +

0000 0000 0000 0000 0000 0000 0000 0001dos 1dos

= =

0000 0000 0000 0000 0000 0000 0000 0010dos 2diez

El segundo atajo nos dice cómo convertir un número binario representado con n bits en un número representado con más de n bits. Por ejemplo, el campo inmediato en las instrucciones de carga, almacenamiento, bifurcación, suma y activar si menor que contiene un número de 16 bits en complemento a dos, que representa desde –32 768diez (–215) hasta 32 767 diez (215–1). Para sumar el campo inmediato a un registro de 32 bits, el computador debe convertir este número de 16 bits a su equivalente de 32 bits. El atajo es tomar el bit más significativo de la cantidad menor –el bit de signo– y replicarlo para rellenar los nuevos bits de la cantidad mayor. Los bits antiguos se copian simplemente en la parte derecha de la nueva palabra. Este atajo comúnmente se llama extensión de signo.

Atajo de la extensión de signo

EJEMPLO

RESPUESTA

Convertir las versiones binarias de 16 bits de 2diez y –2diez a números binarios de 32 bits.

La versión binaria de 16 bits del número 2 es 0000 0000 0000 0010dos = 2diez

Se convierte a un número de 32 bits haciendo 16 copias del valor en el bit más significativo (0) y colocándolas en la mitad izquierda de la palabra. La mitad derecha recibe el valor anterior: 0000 0000 0000 0000 0000 0000 0000 0010dos = 2diez

2.4

Números con signo y sin signo

93

Neguemos la versión de 16 bits usando el atajo anterior. Así, 0000 0000 0000 0010dos

se convierte en 1111 1111 1111 1101dos + 1dos = 1111 1111 1111 1110dos

Crear una versión de 32 bits del número negativo significa copiar el signo 16 veces y colocarlo a la izquierda del número: 1111 1111 1111 1111 1111 1111 1111 1110dos = –2diez

Este truco funciona porque los números positivos en complemento a dos realmente tienen un número infinito de 0s a la izquierda y los que son negativos en complemento a dos tienen infinitos 1s. El patrón de bits binarios que representa un número oculta los bits delanteros para encajar con la anchura del hardware; la extensión de signo simplemente recupera algunos de ellos.

Resumen El punto principal de esta sección es que necesitamos tanto enteros positivos como enteros negativos en una palabra del computador, y aunque cualquier opción tiene pros y contras, la opción inmensamente mayoritaria desde 1965 ha sido el complemento a dos. ¿Cuál es el valor decimal de este número de 64 bits en complemento a 2? 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1000dos 1) -4diez 2) -8diez 3) -16diez 4) 18 446 744 073 709 551 609diez

Extensión: El complemento a dos toma su nombre de la regla según la cual la suma de un número de n-bits y su negativo da 2n; por tanto, el complemento o negación de un número x en complemento a dos es 2n – x.

Autoevaluación

94

Capítulo 2

Complemento a 1:

Una tercera representación alternativa es el complemento a uno. El negativo de un número representado en complemento a uno se halla invirtiendo cada bit, de 0 a 1 y de 1 a 0, lo cual ayuda a explicar su nombre, porque el complemento de x es 2n – x – 1. Fue también un intento de mejorar la solución signo magnitud, y varios computadores científicos usaron esta notación. Esta representación es similar al complemento a dos excepto en que también tiene dos ceros: 00 . . . 00dos es el cero positivo y 11 . . . 11dos es el cero negativo. El número más negativo 10 . . . 000 dos representa –2 147 483 647diez, y de esta manera los positivos y los negativos están equilibrados. Los sumadores en complemento a uno necesitan un paso extra para restar un número, y por esto el complemento a dos domina hoy en día. Una notación final, que veremos cuando hablemos del punto flotante, es representar el valor más negativo por 00…000 dos y el valor más positivo por 11…11dos, donde el 0 típicamente tiene el valor 10…00 dos. Esta forma se llama notación sesgada, puesto que sesga el número de manera que el número más el sesgo tiene una representación no negativa.

notación que representa el valor más negativo con 10...000dos y el valor más positivo con 01...11dos, con igual número de valores positivos y negativos pero con dos ceros, uno positivo (00...00dos) y otro negativo (11...11dos). Este término se utiliza también para la inversión de los bits de una secuencia de bits: cambiar 0 por 1 y 1 por 0.

Instrucciones: el lenguaje del computador

Notación sesgada: notación que representa el valor más negativo por 00 . . . 000dos y el valor más positivo por 11 . . . 11dos, donde el 0 típicamente tiene el valor 10 . . . 000dos, de modo que sega el número de forma que el número más el sesgo tiene una representación no negativa.

Extensión: Para números decimales con signo usamos “–” para representar negativos, porque no hay límites en el tamaño de un número decimal. Dado un tamaño de palabra fijo, las cadenas de bits binarias o hexadecimales pueden incluir la codificación del signo, y por ello normalmente no usamos “+” o “–” con la notación binaria o hexadecimal.

2.5

Representación de instrucciones en el computador

2.5

Ahora ya podemos explicar la diferencia entre la manera en que los seres humanos dan instrucciones a los computadores y la manera en que los computadores ven dichas instrucciones. Las instrucciones se guardan en el computador como series de señales electrónicas altas y bajas y se pueden representar como números. De hecho, cada parte de una instrucción se puede considerar un número individual, y juntando estos números se forma la instrucción. Puesto que los registros son parte de casi todas las instrucciones debe haber una convención para relacionar nombres de registro con números. En el lenguaje ensamblador MIPS los registros de $s0 a $s7 se corresponden con los registros 16 a 23, y los registros de $t0 a $t7 se corresponden con los registros 8 a 15. Por tanto, $s0 significa registro 16, $s1 significa registro 17, $s2 significa registro 18…, $t0 se coloca en el registro 8, $t1 en el registro 9, y así sucesivamente. Describiremos la convención para el resto de los 32 registros en las secciones siguientes.

2.5

95

Representación de instrucciones en el computador

Traducción de una instrucción ensamblador MIPS a una instrucción máquina

Hagamos el siguiente paso en el refinamiento del lenguaje MIPS como ejemplo. Mostraremos la versión del lenguaje MIPS real de la instrucción, representada simbólicamente como:

EJEMPLO

add $t0,$s1,$s2

primero como una combinación de números decimales y después de números binarios. La representación decimal es: 0

17

RESPUESTA 18

8

0

32

Cada uno de estos segmentos de una instrucción se llama campo. El primero y el último campo (que contienen 0 y 32 en este caso) combinados indican al computador MIPS que esta instrucción realiza una suma. El segundo campo indica el número de registro que es el primer operando fuente de la operación suma (17 = $s1), y el tercer campo indica el otro operando fuente para la suma (18 = $s2). El cuarto campo contiene el número de registro que va a recibir la suma (8 = $t0). El quinto campo no se utiliza en esta instrucción, así que se fija en 0. Así pues, esta instrucción suma el registro $s1 al registro $s2 y coloca el resultado en el registro $t0. Esta instrucción también puede representarse como campos de números binarios como opuestos a los decimales: 000000

10001

10010

01000

00000

100000

6 bits

5 bits

5 bits

5 bits

5 bits

6 bits

Esta disposición de la instrucción se llama el formato de instrucción. Como podemos ver al contar el número de bits, esta instrucción MIPS ocupa exactamente 32 bits —el mismo tamaño que una palabra de datos—. De acuerdo con nuestro principio de diseño en el que la simplicidad favorece la regularidad, todas las instrucciones MIPS tienen una longitud de 32 bits. Para distinguirlo del lenguaje ensamblador, llamaremos a la versión numérica de las instrucciones lenguaje máquina, y a una secuencia de tales instrucciones código máquina. Puede parecer que ahora debiéramos estar leyendo y escribiendo largas y tediosas cadenas de números binarios. Evitamos ese tedio usando una base más alta que la binaria que se convierta fácilmente en código binario. Dado que casi todos los tamaños de los datos del computador son múltiplos de 4, los números hexadecimales (en base 16) son idóneos. Puesto que la base 16 es una potencia de 2, podemos hacer la conversión fácilmente, sustituyendo cada grupo de cuatro dígitos binarios por un único dígito hexadecimal y viceversa. La figura 2.4 convierte de hexadecimal a binario y viceversa.

Formato de instrucción: forma de representación de una instrucción compuesta por campos de números binarios. Lenguaje máquina: representación binaria de las instrucciones usada para la comunicación dentro de un sistema informático. Hexadecimal: números en base 16.

96

Capítulo 2

Instrucciones: el lenguaje del computador

Hexadecimal

Binario

Hexadecimal

Binario

Hexadecimal

Binario

Hexadecimal

Binario

0hex

0000dos

4hex

0100dos

8hex

1hex

0001dos

5hex

0101dos

9hex

1000dos

chex

1100dos

1001dos

dhex

2hex

0010dos

6hex

0110dos

1101dos

ahex

1010dos

ehex

3hex

0011dos

7hex

0111dos

1110dos

bhex

1011dos

fhex

1111dos

FIGURA 2.4 Tabla de conversión hexadecimal-binario. Simplemente reemplazamos un dígito hexadecimal por los cuatro dígitos binarios correspondientes, y viceversa. Si la longitud del número binario no es múltiplo de 4 va de derecha a izquierda.

Debido a que con frecuencia tratamos con diversas bases de números, para evitar confusiones utilizaremos el subíndice diez con los números decimales, dos con los números binarios y hex con los números hexadecimales. (Si no hay subíndice, son base 10 por defecto). C y Java utilizan la notación 0xnnnn para los números hexadecimales.

Binario a hexadecimal y viceversa

EJEMPLO

Convertir los números hexadecimales y binarios siguientes a la otra base: eca8 6420hex 0001 0011 0101 0111 1001 1011 1101 1111dos

Utilizando la figura 2.4, la respuesta es una búsqueda en una tabla en una dirección: eca8 6420hex

1110 1100 1010 1000 0110 0100 0010 0000dos

Y en la otra dirección también: 0001 0011 0101 0111 1001 1011 1101 1111dos

1357 9bdfhex

Campos MIPS A los campos MIPS se les da nombre para identificarlos más fácilmente: op

rs

rt

rd

shamt

funct

6 bits

5 bits

5 bits

5 bits

5 bits

6 bits

2.5

Representación de instrucciones en el computador

97

Este es el significado de cada uno de los nombres de los campos de las instrucciones MIPS: ■

op: operación básica de la instrucción, tradicionalmente llamada código de operación u opcode.



rs: el registro del primer operando fuente.



rt: el registro del segundo operando fuente.



rd: el registro del operando destino, donde se almacena el resultado de la operación.



shamt: cantidad de desplazamientos (shift amount). (La sección 2.6 explica instrucciones de desplazamiento y este término, que no será utilizado hasta entonces y por tanto el campo contiene cero).



funct: función. Selecciona la variante específica de la operación en el campo op y a veces es llamado código de función.

Puede haber un problema cuando una instrucción necesita campos más largos que los mostrados arriba. Por ejemplo, la instrucción cargar palabra (load word) debe especificar dos registros y una constante. Si la dirección tuviera que utilizar uno de los campos de 5 bits del formato anterior, la constante de la instrucción cargar palabra (load word) estaría limitada a, como máximo, 25 ó 32 bytes. Esta constante se utiliza para seleccionar elementos de las tablas o estructuras de datos, y a menudo se necesita que sea mucho mayor de 32. Este campo de 5 bits es demasiado pequeño para ser útil. Por tanto, tenemos un conflicto entre el deseo de mantener todas las instrucciones con la misma longitud y el deseo de tener un único formato de instrucción. Esto nos conduce al último principio del diseño de hardware: Principio de diseño 4: el buen diseño exige buenos compromisos. El compromiso elegido por los diseñadores de MIPS es mantener todas las instrucciones con la misma longitud, que requieren diferentes clases de formatos de instrucción para diferentes clases de instrucciones. Por ejemplo, el formato anterior es llamado Rtype (de registro) o R-format. Un segundo tipo de formato de instrucción se llama I-type (de inmediato) o I-format, y es utilizado por las instrucciones de transferencia de datos y las instrucciones con direccionamiento inmediato. Los campos del I-format son: op

rs

rt

constante o dirección

6 bits

5 bits

5 bits

16 bits

Los 16 bits de la dirección significan que una instrucción cargar palabra (load word) puede cargar cualquier palabra de hasta ±215 ó 32 768 bytes (±213 o 8192 palabras) de la dirección del registro base rs. De la misma forma, la suma inmediata (add immediate) está limitada a las constantes no más grandes que ±215. Vemos que más de 32 registros serían difíciles en este formato, ya que los campos rs y rt necesitarían cada uno otro bit, complicando más hacer caber todo en una palabra. Miremos la instrucción cargar palabra (load word) de la página 83: lw

$t0,32($s3)

# reg temporal $t0 toma el valor de A[8]

Opcode: campo que indica la operación y el formato de una instrucción.

98

Capítulo 2

Instrucciones: el lenguaje del computador

En este caso, en el campo rs se coloca 19 (de $s3), en el campo rt se coloca 8 (de $t0) y en el campo de la dirección se coloca 32. Obsérvese que el significado del campo rt ha cambiado para esta instrucción: en una instrucción cargar palabra (load word) el campo rt especifica el registro destino, que recibe el resultado de la carga. Aunque tener múltiples formatos complica el hardware podemos reducir la complejidad manteniendo los formatos similares. Por ejemplo, los tres primeros campos de los formatos R-type e I-type son del mismo tamaño y tienen los mismos nombres; el cuarto campo del I-type es igual a la longitud de los tres últimos campos del R-type. Por si se lo está preguntando, los formatos se distinguen por los valores del primer campo: a cada formato se le asigna un conjunto distinto de valores en el primer campo (op), de modo que el hardware sepa si debe tratar la última mitad de la instrucción como tres campos (R-type) o como un solo campo (I-type). La figura 2.5 muestra los números usados en cada campo para las instrucciones MIPS vistas a lo largo de esta sección.

Instrucción

Formato

op

rs

rt

rd

shamt

funct

dirección

add

R

0

reg

reg

reg

0

32diez

n.a.

sub (subtract)

R

0

reg

reg

reg

0

34diez

n.a.

add immediate

I

8diez

reg

reg

n.a.

n.a.

n.a.

constante

lw (load word)

I

35diez

reg

reg

n.a.

n.a.

n.a.

dirección

sw (store word)

I

43diez

reg

reg

n.a.

n.a.

n.a.

dirección

FIGURA 2.5 Codificación de instrucciones en MIPS. En la tabla, “reg” significa un número de registro entre 0 y 31, “dirección” significa una dirección de 16 bits, y “n.a.” (no aplicable) significa que ese campo no aparece en este formato. Observe que las instrucciones add y sub tienen el mismo valor en el campo op; el hardware usa el campo funct para decidir la variante de una operación: add (32) o sub (34) .

Traducción de lenguaje ensamblador MIPS a lenguaje máquina

EJEMPLO

Ahora podemos mostrar un ejemplo hasta el final, de lo que escribe el programador a lo que ejecuta el computador. Si $t1 tiene la base de la tabla A y $s2 se corresponde con h, la sentencia de asignación: A[300] = h + A[300];

se compila en: lw $t0,1200($t1) add $t0,$s2,$t0 sw $t0,1200($t1)

# reg temporal t0 toma el valor A[300] # reg temporal t0 toma el valor h + A[300] # h + A[300] se almacena de nuevo en A[300]

¿Cuál es el código de lenguaje máquina MIPS para estas tres instrucciones?

2.5

Por conveniencia, representaremos primero las instrucciones de lenguaje máquina usando números decimales. De la figura 2.5 podemos determinar las tres instrucciones de lenguaje máquina:

op

rs

99

Representación de instrucciones en el computador

rt

35

9

8

0

18

8

43

9

8

rd

dirección/ shamt

funct

1200 8

0

32

1200

La instrucción lw se identifica con 35 (véase figura 2.5) en el primer campo (op). El registro base 9 ($t1) se especifica en el segundo campo (rs), y el registro de destino 8 ($t0) se especifica en el tercer campo (rt). El desplazamiento para seleccionar A[300] (1200 = 300 × 4) se encuentra en el campo final (dirección). La instrucción add que va a continuación se especifica con 0 en el primer campo (op) y 32 en el último campo (funct). Los tres operandos en registro (18, 8 y 8) se encuentran en el segundo, tercer y cuarto campos, y corresponden a $s2, $t0 y $t0. La instrucción sw se identifica con 43 en el primer campo. El resto de esta última instrucción es idéntico a la instrucción lw. El equivalente binario a la forma decimal es el siguiente (1200 en base 10 es 0000 0100 1011 0000 en base 2):

100011

01001

01000

000000

10010

01000

101011

01001

01000

0000 0100 1011 0000 01000

00000

100000

0000 0100 1011 0000

Obsérvese la semejanza de las representaciones binarias de la primera y de la última instrucción. La única diferencia está en el tercer bit desde la izquierda.

La figura 2.6 resume las partes del lenguaje ensamblador MIPS descritas en esta sección. Tal y como veremos en el capítulo 4, la semejanza de las representaciones binarias de instrucciones relacionadas simplifica el diseño del hardware. Estas instrucciones son otro ejemplo de la regularidad en la arquitectura MIPS.

RESPUESTA

100

Capítulo 2

Instrucciones: el lenguaje del computador

Lenguaje máquina MIPS Nombre

Formato

Ejemplo

Comentarios

add

R

0

18

19

17

0

32

add $s1,$s2,$s3

sub

R

0

18

19

17

0

34

sub $s1,$s2,$s3

addi

I

8

18

17

100

addi $s1,$s2,100

lw

I

35

18

17

100

lw $s1,100($s2)

sw

I

Tamaño de cada campo

43

18

17

6 bits

5 bits

5 bits

5 bits rd

R-format

R

op

rs

rt

I-format

I

op

rs

rt

100

sw $s1,100($s2)

5 bits

6 bits

Todas las instrucciones MIPS tienen 32 bits

shamt

funct

Formato de instrucciones aritméticas

address

Formato de instrucciones de transferencia de datos

FIGURA 2.6 Arquitectura MIPS mostrada en la sección 2.5. Los dos formatos de las instrucciones MIPS vistos hasta ahora son R e I. Los primeros 16 bits son los mismos: ambos contienen un campo op que indica la operación base, un campo rs que da uno de los operandos fuentes, y un campo rt que especifica el otro operando fuente, a excepción de la instrucción de cargar palabra (load word), donde se especifica el registro destino. El R-format divide los últimos 16 bits en un campo rd, que indica el registro destino, el campo shamt (cantidad de desplazamientos), que se explica en la sección 2.5, y el campo funct, que especifica la operación de las instrucciones del R-format. El I-format guarda los últimos 16 bits como un único campo de dirección.

IDEA clave

Actualmente, los computadores se construyen basados en dos principios clave: 1. Las instrucciones se representan como números. 2. Los programas son almacenados en memoria para ser leídos o escritos también como números. Estos principios conducen al concepto de programa almacenado (stored-program). Su invención abrió la caja de Pandora de la computación. La figura 2.7 muestra el potencial de este concepto; concretamente, la memoria puede contener el código fuente para un programa editor, el correspondiente código máquina compilado, el texto que el programa compilado está utilizando e incluso el compilador que generó el código máquina. Una consecuencia de tratar las instrucciones como números es que a menudo los programas son enviados como ficheros de números binarios. La implicación comercial que ello supone es que los computadores pueden heredar software previamente preparado si son compatibles con un repertorio de instrucciones existente. Tal “compatibilidad binaria” con frecuencia lleva a la industria a centrarse alrededor de una pequeña cantidad de arquitecturas del repertorio de instrucción.

2.5

Representación de instrucciones en el computador

101

Memoria Programa de contabilidad (código máquina) Programa editor (código máquina) Compilador C (código máquina)

Procesador

Datos de la nómina

Texto de un libro Código fuente en C del programa editor

FIGURA 2.7 El concepto de programa almacenado. Los programas almacenados permiten a un computador que hace contabilidad convertirse, en un abrir y cerrar de ojos, en un computador que ayuda a un autor a escribir un libro. El cambio se lleva a cabo simplemente cargando la memoria con programas y datos y después indicando al computador que comience a ejecutar desde una posición de memoria concreta. Tratar las instrucciones de la misma forma que los datos simplifica el hardware de la memoria y el software de los sistemas informáticos. Concretamente, la tecnología de memoria que se necesita para los datos se puede utilizar también para los programas, y programas como los compiladores, por ejemplo, pueden traducir el código escrito en una notación lejana pero más conveniente para los seres humanos al código que el computador puede comprender.

¿Qué instrucción MIPS se está representando? Escoja una de la cuatro opciones.

Op

Rs

Rt

Rd

Shamt

Funct

0

8

9

10

0

34

1. add $s0, $s1, $s2 2. add $s2, $s0, $s1 3. add $s2, $s1, $s0 4. sub $s2, $s0, $s1

Autoevaluación

102

“¡Por el contrario!” —continuó Tweedledee— “si fue así, podría serlo; y si así fuera, entonces lo sería; pero como no lo es, entonces no es. ¡Es lógico!” Lewis Carroll, Alicia a través del espejo, 1871.

Capítulo 2

2.6

Instrucciones: el lenguaje del computador

Operaciones lógicas

2.6

Aunque los primeros computadores se centraron en palabras completas, pronto se vio claro que era útil operar con campos de bits dentro de una palabra o incluso con bits individuales. Examinar los caracteres de una palabra, cada uno de los cuales se almacena como 8 bits, es un ejemplo de este tipo de operación (véase sección 2.9). Entonces se añadieron instrucciones para simplificar, entre otras cosas, las operaciones de empaquetar y desempaquetar los bits en palabras. Estas instrucciones se llaman operaciones lógicas. La figura 2.8 muestra operaciones lógicas en C, en Java y en MIPS.

Operaciones lógicas

Operadores C

Operadores Java

Instrucciones MIPS

Shift left (desplazamiento

<<

<<

sll

>>

>>>

srl

AND (bit-a-bit)

&

&

and, andi

OR (bit-a-bit)

|

|

or, ori

NOT (bit-a-bit)

~

~

nor

a la izquierda) Shift right (desplazamiento a la derecha)

FIGURA 2.8 Operadores lógicos de C y de Java y sus instrucciones MIPS correspondientes. MIPS implementa la operación NOT con NOR con uno de sus operandos igual a cero.

La primera clase de este tipo de operaciones se llama desplazamiento (shift). Mueve todos los bits de una palabra a la izquierda o a la derecha, rellenando con ceros los bits vacíos. Por ejemplo, si el registro $s0 contenía 0000 0000 0000 00000 000 0000 0000 0000 1001dos = 9diez

y se ejecutó la instrucción de desplazar 4 posiciones a la izquierda, el nuevo valor se parecería a esto: 0000 0000 0000 0000 0000 0000 0000 1001 0000dos= 144diez

El complementario de un desplazamiento a la izquierda es un desplazamiento a la derecha. El nombre real de las dos instrucciones de desplazamiento MIPS es desplazamiento lógico a la izquierda (shift left logical, sll) y desplazamiento lógico a

2.6

Operaciones lógicas

103

la derecha (shift right logical, srl). La siguiente instrucción realiza la operación anterior, suponiendo que el resultado debería ir al registro $t2: sll

$t2,$s0,4

# reg $t2 = reg $s0 << 4 bits

Hemos retrasado la explicación del campo shamt en el formato de R-type hasta ahora. Éste representa la cantidad de desplazamiento (shift amount) y se utiliza en instrucciones de desplazamiento. Por tanto, la versión en lenguaje máquina de la instrucción anterior es:

op

rs

rt

rd

shamt

funct

0

0

16

10

4

0

La codificación de sll es 0 en los campos op y funct, rd contiene 10 (registro $t2), rt contiene 16 (registro $s0) y shamt contiene 4. El campo rs no se utiliza, y se fija en 0. El desplazamiento lógico a la izquierda proporciona una ventaja adicional. El desplazamiento a la izquierda de i bits nos da el mismo resultado que multiplicar por 2i, igual que desplazar un número decimal i dígitos es equivalente a multiplicar por 10i. Por ejemplo, el anterior sll de 4 arroja el mismo resultado que multiplicar por 24 ó 16. La primera configuración de bits anterior representa 9, y el valor de la segunda configuración de bits es 9 × 16 = 144. Otra operación útil que aísla campos es AND (Y). (Pondremos en inglés la palabra para evitar la confusión entre la operación y la conjunción.) AND es una operación bit-a-bit que deja un 1 en el resultado solamente si ambos bits de los operandos son 1. Por ejemplo, si el registro $t2 contiene 0000 0000 0000 0000 0000 1101 0000 0000dos

y el registro $t1 contiene 0000 0000 0000 0000 0011 1100 0000 0000dos

entonces, después de ejecutar la instrucción MIPS and $t0,$t1,$t2

# reg $t0 = reg $t1 & reg $t2

el valor del registros $t0 sería 0000 0000 0000 0000 0000 1100 0000 0000dos

AND: operación lógica bit-a-bit con dos operandos que genera un 1 solamente si los dos operandos son 1.

104

OR: Operación lógica bit-a-bit con dos operandos que genera un 1 si hay un 1 en cualquiera de los dos operandos.

Capítulo 2

Instrucciones: el lenguaje del computador

Como podemos ver, AND puede aplicar una configuración de bits a un conjunto de bits para forzar ceros allí donde hay un 0 en la configuración de bits. Tradicionalmente, a dicha configuración de bits en conjunción con AND se le llama una máscara, puesto que la máscara “oculta” algunos bits. Para poner un valor a uno de estos mares de ceros usamos la operación complementaria de AND, llamada OR (O). Es una operación bit-a-bit que coloca un 1 en el resultado si cualquier bit del operando es un 1. Es decir, si no cambiamos los registros $t1 y $t2 del ejemplo anterior, el resultado de la instrucción MIPS or $t0,$t1,$t2 # reg $t0 = reg $t1 | reg $t2

deja el siguiente valor en el registro $t0: 0000 0000 0000 0000 0011 1101 0000 0000dos NOT: operación lógica bit-a-bit con un operando que invierte los bits; es decir, que sustituye cada 1 por un 0 y cada 0 por un 1. NOR: operación lógica bit-a-bit con dos operandos que calcula el NOT (x OR y) de los dos operandos.

La última operación lógica es un complementario. NOT (NO) toma un operando y coloca un 1 en el resultado si el bit del operando es un 0, y viceversa. En consonancia con el formato de los dos operandos, los diseñadores de MIPS decidieron incluir la instrucción NOR (NOT OR) en vez de NOT. Si un operando es cero, entonces es equivalente a NOT. Por ejemplo, A NOR 0 = NOT (A OR 0) = NOT (A). Si el registro $t1 del ejemplo anterior no cambia y el registro $t3 tiene valor 0, el resultado de la instrucción MIPS nor $t0,$t1,$t3 # reg $t0 = ~ (reg $t1 | reg $t3)

deja el siguiente valor en el registro $t0: 1111 1111 1111 1111 1100 0011 1111 1111dos

La figura 2.8 anterior muestra la relación entre los operadores de C y de Java y las instrucciones MIPS. Las constantes son útiles tanto en las operaciones lógicas AND y OR como en operaciones aritméticas, así que MIPS también proporciona las instrucciones and inmediato (andi) y or inmediato (ori). Las constantes son raramente usadas para NOR, puesto que su uso principal es invertir los bits de un de único operando; así pues, el hardware no tiene ninguna versión con el direccionamiento inmediato.

Extensión: El repertorio de instrucciones de MIPS incluye también la operación “o exclusiva” (XOR), que pone el bit del resultado a 1 si los correspondientes bits de los operandos son diferentes, y a 0 si son iguales. El lenguaje C permite definir campos de bits o campos dentro de las palabras, tanto para permitir que varios objetos se empaqueten en una palabra como para ajustarse a una interfaz impuesta externa-

2.7

105

Instrucciones para la toma de decisiones

mente, como un dispositivo de E/S. Todos los campos deben caber dentro de una palabra, son enteros sin signo e incluso pueden tener solamente 1 bit. El compilador de C inserta y extrae los campos mediante instrucciones lógicas de MIPS: and, or, sll y srl.

Autoevaluación

¿Qué operaciones pueden aislar un campo en una palabra? 1.

AND

2.

Un desplazamiento a la izquierda seguido de un desplazamiento a la derecha

2.7

Instrucciones para la toma de decisiones

2.7

Lo que distingue un computador de una calculadora simple es su capacidad de tomar decisiones. Basándose en los datos de entrada y en los valores creados durante el cómputo, se ejecutan diferentes instrucciones. La toma de decisiones se representa comúnmente en los lenguajes de programación utilizando la sentencia if (si condicional), combinado a veces con sentencias go to (ir a) y etiquetas. El lenguaje ensamblador MIPS incluye dos instrucciones de toma de decisiones, similares a las sentencias if y go to. La primera instrucción es:

beq registro1, registro2, L1

La utilidad de un computador automático radica en la posibilidad de usar repetidamente una secuencia de instrucciones dada; el número de veces que se itera puede ser dependiente de los resultados del cómputo. Cuando la repetición se completa, debe seguirse una secuencia diferente de [instrucciones], por lo tanto debemos, en la mayoría de los casos, dar dos series paralelas de [instrucciones] precedidas por una instrucción que indica qué rutina debe seguirse. Esta opción puede realizarse en función del signo de un número (el cero se considera positivo para este propósito). Consecuentemente, introducimos una [instrucción] (la [instrucción] de transferencia condicional) que, dependiendo del signo de un número dado, determina que se ejecute la rutina adecuada de dos posibles. Burks, Goldstine y von Neumann, 1947.

Esta instrucción significa ir a la sentencia etiquetada con L1 si el valor en registro1 es igual al valor en registro2. El mnemónico beq significa salta si es igual (branch if equal). La segunda instrucción es:

bne registro1, registro2, L1

Esta instrucción significa ir a la sentencia etiquetada con L1 si el valor en registro1 no es igual al valor en registro2. El mnemónico bne significa salta si no es igual (branch if not equal). Tradicionalmente, estas dos instrucciones son llamadas bifurcaciones o saltos condicionales.

Salto condicional (o bifurcación condicional): instrucción que requiere la comparación de dos valores y que permite una posterior transferencia del control a una nueva dirección en el programa, basada en el resultado de la comparación.

106

Capítulo 2

Instrucciones: el lenguaje del computador

Compilación de una sentencia if-then-else (si-entonces-si no) en un salto condicional

EJEMPLO

En el segmento de código siguiente, f, g, h, i y j son variables. Si las cinco variables de f a j corresponden a cinco registros de $s0 a $s4, ¿cuál es el código MIPS compilado para esta sentencia if en C? if (i == j) f = g + h; else f = g – h;

RESPUESTA

La figura 2.9 es un diagrama de flujo de lo que debería hacer el código MIPS. La primera expresión compara la igualdad, por tanto parece que usaríamos beq. En general, el código será más eficiente si comprobamos la condición opuesta para saltar sobre el código que realiza la parte then (entonces) que sigue al if (si); la etiqueta Else (si no) está definida a continuación: bne $s3,$s4,Else

# ir a Else si i | j

La siguiente sentencia de asignación realiza una sola operación y, si todos los operandos están asignados a registros, es simplemente una instrucción: add $s0,$s1,$s2

# f = g + h (se omite si i | j)

Ahora necesitamos ir al final de la sentencia if. Este ejemplo introduce otra clase de bifurcación o salto, frecuentemente llamado salto incondicional. Esta instrucción dice que el procesador sigue siempre el salto. Para distinguir entre los saltos condicionales e incondicionales, el nombre MIPS para este tipo de instrucción es jump (salto), abreviado como j –la etiqueta Salida (Exit) está definida a continuación–. j Exit

# ir a Salida

La sentencia de asignación en la parte del else (si no) de la sentencia if (si) puede traducirse o compilarse de nuevo en una sola instrucción. Ahora necesitamos añadir la etiqueta Si no (Else) a esta instrucción. También mostramos la etiqueta Salida que está después de esta instrucción, y vemos el final del código compilado if-then-else (si-entonces-si no): Else:sub $s0,$s1,$s2 Exit:

# f = g – h (se omite si i = j)

2.7

i=j

107

Instrucciones para la toma de decisiones

i==j?

i|j

Else: f=g–h

f=g+h

Exit: FIGURA 2.9 Ilustración de las opciones de la sentencia if anterior. La caja de la izquierda corresponde a la parte then (entonces) de la sentencia if, y la caja de la derecha corresponde a la parte else (si no).

Observe que el ensamblador libera al compilador y al programador del lenguaje ensamblador del tedio de calcular las direcciones para las bifurcaciones, de forma similar a como lo hace para calcular las direcciones de los datos para cargas y almacenamientos (véase la sección 2.12).

Los compiladores crean con frecuencia bifurcaciones y etiquetas cuando no aparecen en el lenguaje de programación. Evitar el agobio de la escritura de las etiquetas y bifurcaciones explícitas es uno de los beneficios de escribir en lenguajes de programación de alto nivel, y es una razón por la cual la codificación es más rápida a ese nivel.

Interfaz hardware software

Lazos Las decisiones son importantes tanto para elegir entre dos alternativas —las que aparecen en las sentencias if (si)— como para iterar un cómputo —que aparece en los lazos—. Las mismas instrucciones ensamblador son las unidades básicas en ambos casos.

Compilación de un lazo while (mientras) en C

Disponemos de un lazo tradicional en C while (guardar[i] == k) i += 1;

Suponga que i y k corresponden a los registros $s3 y $s5 y la base de la tabla guarda está en $s6. ¿Cuál es el código ensamblador MIPS correspondiente a este segmento de C?

EJEMPLO

108

Capítulo 2

RESPUESTA

Instrucciones: el lenguaje del computador

El primer paso es cargar guardar[i] en un registro temporal. Antes de que podamos hacerlo necesitamos tener su dirección, y antes de que podamos sumar i a la base de la tabla guardar para formar la dirección debemos multiplicar el índice i por 4 debido al problema del direccionamiento de byte. Afortunadamente, podemos utilizar el desplazamiento lógico a la izquierda, puesto que desplazando a la izquierda 2 bits multiplicamos por 4 (véase la página 103 en la sección 2.5). Necesitamos añadir la etiqueta Lazo (Loop) de modo que podamos saltar de nuevo a esta instrucción al final del mismo: Loop:sll

$t1,$s3,2

# reg temporal $t1 = 4 * i

Para conseguir la dirección de guardar[i] necesitamos sumar $t1 y la base de guardar en $s6: add $t1,$t1,$s6 # $t1 = dirección de guardar[i]

Ahora podemos utilizar esa dirección para cargar guardar[i] en un registro temporal: lw

$t0,0($t1) # reg temporal $t0 = guardar[i]

La instrucción siguiente realiza el test del lazo, y va a Salida (Exit) si guardar[i] | k: bne

$t0,$s5, Exit # ir a Salida si guardar[i] | k

La instrucción siguiente suma 1 a i: add

$s3,$s3,1 # i = i + 1

El final del lazo salta de nuevo hacia atrás hasta el test del while (mientras) en la parte superior del lazo. Únicamente añadimos la etiqueta Salida justo detrás, de la forma siguiente: j Exit:

Loop

# ir a Lazo (Loop)

(Véanse los ejercicios para una optimización de esta secuencia).

Interfaz hardware software

Estas secuencias de instrucciones que terminan con una bifurcación son tan fundamentales para la compilación que tienen su propio nombre: un bloque básico es una secuencia de instrucciones sin bifurcaciones, excepto posiblemente al final, y sin ningún destino de saltos o etiquetas de saltos, excepto posiblemente al comienzo. Una de las primeras fases de la compilación es dividir el programa en bloques básicos

Bloque básico: secuencia de instrucciones sin bifurcaciones (excepto posiblemente al final) y sin destinos de saltos o etiquetas de saltos (excepto posiblemente al comienzo).

La prueba de la igualdad o la desigualdad es probablemente el test más habitual, pero algunas veces es útil ver si el valor de una variable es menor que el de otra variable. Por ejemplo, para un lazo for (para) se puede desear comprobar si la variable índice es menor que 0. Este tipo de comparaciones se realizan en el lenguaje ensamblador MIPS con una instrucción que compara dos registros y asigna a un tercer regis-

2.7

109

Instrucciones para la toma de decisiones

tro un 1 si el primero es menor que el segundo; si no es así, le asigna un 0. La instrucción MIPS se llama set on less than (activar si es menor que) o slt. Por ejemplo slt

$t0, $s3, $s4

significa que el registro $t0 se actualiza a 1 si el valor en el registro $s3 es menor que el valor en el registro $s4; si no, el registro $t0 se actualiza a 0. Las constantes como operandos son frecuentes en las comparaciones. Puesto que el registro $zero siempre contiene 0, podemos comparar directamente con 0. Para comparar con otros valores hay una versión de la instrucción set on less than (activar si menor que) con valor inmediato. Para comprobar si el registro $s2 es menor que la constante 10 podemos escribir: slti

$t0,$s2,10

# $t0 = 1 if $s2 < 10

Los compiladores del MIPS utilizan slt, slti, beq, bne y el valor fijo 0 (siempre disponible leyendo el registro $zero) para crear todas las condiciones relativas: igual, no igual, menor que, menor o igual que, mayor que, mayor o igual que.

Interfaz hardware software

Teniendo en cuenta la advertencia de Von Neumann sobre la simplicidad del “equipo”, la arquitectura MIPS no incluye la instrucción de bifurcación si es menor que, porque es demasiado complicada: o bien alargaría la duración de ciclo de reloj o la instrucción necesitaría ciclos de reloj extras. Dos instrucciones más rápidas son más útiles.

Las instrucciones de comparación deben abordar la dicotomía entre números con y sin signo. En ocasiones, una secuencia de bits con un 1 en el bit más significativo representa un número negativo y, por supuesto, es menor que cualquier número positivo, que debe tener su bit más significativo igual a 0. Por otra parte, si los números son sin signo, un número con su bit más significativo igual a 1 es mayor que cualquier otro número que comience con 0. (Más adelante tendremos en cuenta este doble significado del bit más significativo para reducir el coste de la comprobación de los límites de un vector.) MIPS ofrece dos versiones de la comparación poner a 1 cuando menor que (set on less than) para contemplar estas dos alternativas. Set on less than (slt) y set on less than inmediate (slti) operan con números con signo. Las comparaciones de números sin signo se realizan con set on less than unsigned (sltu) y set on less than inmediate unsigned (sltiu).

Interfaz hardware software

110

Capítulo 2

Instrucciones: el lenguaje del computador

Comparaciones con signo frente a comparaciones sin signo

EJEMPLO

Supongamos que el contenido del registro $s0 es el número binario 1111 1111 1111 1111 1111 1111 1111 1111dos

y el del registro $s1 es el número binario 0000 0000 0000 0000 0000 0000 0000 0001dos

¿Qué valores tendremos en los registros $t0 y $t1 después de las dos siguientes instrucciones? slt

RESPUESTA

$t0, $s0, $s1

# comparación con signo

sltu $t1, $s0, $s1

# comparación sin signo

El valor en el registro $s0 es –1diez si se está representando un entero y 4 294 967 295diez si es un entero sin signo. El valor en el registro $s1 representa el valor 1diez en ambos casos. Entonces, en $t0 tendremos el valor 1, porque –1diez < 1diez, y en $t1 el valor 0 porque 4 294 967 295diez > 1diez. Considerar los números con signo como si fuesen números sin signo nos proporciona una manera de comprobar si 0 f x < y de bajo coste, que coincide con el chequeo de un índice fuera-de-límites de los vectores. La clave es que los enteros negativos en representación binaria de complemento a 2 son similares a número muy altos en una notación sin signo; esto es, el bit más significativo es el bit de signo en el primer caso y una gran parte del número en el segundo caso. Así, la comparación sin signo x < y también comprueba si x es negativo al mismo tiempo que si x es menor que y.

Atajo para la comprobación de límites

EJEMPLO

Utilice este atajo para simplificar la comprobación de si un índice está fuera de los límites: salto a IndexOutOfBounds si $s1 v $t2 o si $s1 es negativo.

RESPUESTA

El código de comprobación utiliza sltu para ambos tests: sltu $t0, $s1, $t2 # $t0 = 0 si $s1 >= longitud o $s1<0 beq $t0, $zero, IndexOutOfBounds # si malo, salto a Error

2.7

111

Instrucciones para la toma de decisiones

La sentencia case / switch La mayoría de los lenguajes de programación tienen una sentencia alternativa case o switch, que permite que el programador seleccione una de muchas alternativas dependiendo de un único valor. La manera más simple de implementar switch (cambio) es a través de una secuencia de pruebas condicionales, convirtiendo la sentencia switch en una cadena de sentencias if-then-else (si-entonces-si no). A veces, las alternativas se pueden codificar eficientemente como una tabla con las direcciones de las secuencias de instrucciones alternativas, llamada tabla de direcciones de salto, en la que el programa necesita solamente indexar en la tabla y después saltar a la secuencia apropiada. Así, la tabla de salto es simplemente una tabla de palabras que contiene direcciones que se corresponden con las etiquetas en el código. El programa carga la entrada apropiada de la tabla de salto en un registro. A partir de este momento, para saltar necesita la dirección en el registro. Para permitir este tipo de situaciones, los computadores como MIPS incluyen una instrucción jump register (registro de salto) o jr, que significa un salto incondicional a la dirección especificada en un registro. El programa carga la entrada apropiada de la tabla de saltos en un registro, y luego salta a la dirección adecuada utilizando un registro de salto. Esta instrucción está descrita en la sección siguiente.

Aunque hay muchas sentencias para decisiones y lazos en los lenguajes de programación como C y Java, la sentencia básica que las pone en ejecución en el nivel inferior siguiente es la bifurcación condicional.

Tabla de direcciones de salto (jump address table): también llamada tabla de salto. Es una tabla de direcciones de las secuencias de instrucciones alternativas.

Interfaz hardware software

Extensión: Si ha oído hablar de los saltos retardados, tratados en el capítulo 4, no se preocupe: el ensamblador MIPS los hace invisibles al programador del lenguaje ensamblador.

I. C tiene muchas sentencias para decisiones y lazos, mientras que MIPS tiene Autoevaluación pocas. ¿Cuál de las afirmaciones siguientes explica o no este desequilibrio? ¿Por qué? 1. La mayoría de las sentencias de decisión hacen que el código sea más fácil de leer y de comprender. 2. Menos sentencias de decisión simplifican la tarea del nivel inferior que es responsable de la ejecución. 3. Más sentencias de decisión significan menos líneas de código, lo que reduce generalmente el tiempo de codificación. 4. Más sentencias de decisión significan pocas líneas de código, lo que da lugar generalmente a la ejecución de pocas operaciones.

112

Capítulo 2

Instrucciones: el lenguaje del computador

II. ¿Por qué C proporciona dos grupos de operadores para AND (& y &&) y dos grupos de operadores para OR (| y ||), mientras que las instrucciones MIPS no lo hacen? 1. Las operaciones lógicas AND y OR usan & y |, mientras que las bifurcaciones condicionales usan && y || en la ejecución. 2. La afirmación anterior tiene su inversa: && y || corresponden a las operaciones lógicas, mientras que & y | corresponden a las bifurcaciones condicionales. 3. Son redundantes y significan la misma cosa: && y || son simplemente herederos del lenguaje de programación B, el precursor de C.

2.8 Procedimiento: subrutina almacenada que realiza una tarea específica basada en los parámetros que le son proporcionados.

Apoyo a los procedimientos en el hardware del computador

2.8

Un procedimiento o una función (o subrutina) es una herramienta que usan los programadores de C o de Java para estructurar programas, a fin de hacerlos más fáciles de entender y de permitir que el código sea reutilizado. Los procedimientos permiten al programador concentrarse en una sola parte de la tarea en un momento determinado, usando los parámetros como barrera entre el procedimiento y el resto del programa y los datos, lo que permite que pasen los valores y que devuelva resultados. Describimos el equivalente en Java en la sección 2.15 en el CD, pero Java necesita todo lo que C necesita de un computador. Podemos pensar en un procedimiento como un espía que funciona con un plan secreto, que adquiere recursos, realiza la tarea, borra sus pistas y después retorna al punto original con el resultado deseado. Ninguna otra cosa debería verse perturbada una vez que la misión se ha completado. Por otra parte, un espía opera solamente con una “necesidad de saber” básica, así que no puede hacer suposiciones sobre su jefe. Análogamente, en la ejecución de un procedimiento el programa debe seguir estos seis pasos: 1. Colocar los parámetros en un lugar donde el procedimiento pueda acceder a ellos. 2. Transferir el control al procedimiento. 3. Adquirir los recursos del almacenamiento necesarios para el procedimiento. 4. Realizar la tarea deseada. 5. Colocar el valor del resultado en un lugar donde el programa que lo ha llamado pueda tener acceso. 6. Retornar el control al punto del origen, puesto que un procedimiento puede ser llamado desde varios puntos de un programa.

2.8

Apoyo a los procedimientos en el hardware del computador

113

Como ya hemos dicho, los registros son el lugar más rápido para situar datos en un computador; por tanto, procuramos utilizarlos tanto como sea posible. Los programas MIPS siguen la convención siguiente en la asignación de sus 32 registros para llamar a un procedimiento: ■ $a0-$a3: cuatro ■ $v0-$v1: dos

registros de argumentos para pasar parámetros.

registros de valores para retornar valores.

■ $ra: un registro con la dirección de retorno para volver al punto del origen.

Además de asignar estos registros, el lenguaje ensamblador MIPS incluye una instrucción sólo para los procedimientos: salta a una dirección y simultáneamente guarda la dirección de la instrucción siguiente en el registro $ra. La instrucción de saltar-y-enlazar o jal (jump-and-link) se escribe simplemente: jal ProcedureAddress

La parte de enlace (link) en el nombre de la instrucción significa que se construye una dirección o enlace que apunta al punto desde el que se hace la llamada para permitir que el procedimiento retorne a la dirección apropiada. Este enlace almacenado en el registro $ra se llama dirección de retorno. Esta dirección de retorno o remitente es necesaria porque el mismo procedimiento se podría llamar desde varias partes del programa. Para trabajar en tales situaciones, los computadores como MIPS utilizan la instrucción salto según registro o jr (jump register), que realiza un salto incondicional a la dirección especificada en un registro: jr

$ra

Los saltos de la instrucción salto según registro (jump register, jr) saltan a la dirección almacenada en el registro $ra —lo cual es justo lo que deseamos—. Así, el programa que llama o invoca, es decir, el invocador o llamador (caller), pone los valores de los parámetros en $a0-$a3 y utiliza jalX para saltar al procedimiento X —a veces conocido por invocado o llamado (callee)—. El procedimiento llamado realiza entonces los cálculos, pone los resultados en $v0-$v1 y devuelve el control al llamador utilizando el jr $ra. Implícita en la idea de programa almacenado está la necesidad de tener un registro para guardar la dirección de la instrucción actual que está siendo ejecutada. Por razones históricas, este registro casi siempre se llama contador de programas, abreviado como PC (program counter) en la arquitectura MIPS, aunque un nombre más acorde habría sido registro de dirección de instrucción. La instrucción jal guarda PC + 4 en el registro $ra para enlazar con la instrucción siguiente y establecer el retorno del procedimiento.

Instrucción saltar-y-enlazar: instrucción que salta a una dirección y simultáneamente guarda la dirección de la instrucción siguiente en un registro ($ra en MIPS).

Dirección de retorno: enlace al sitio que llama que permite que un procedimiento retorne a la dirección apropiada; en MIPS se almacena en el registro $ra.

Invocador (o llamador): programa que convoca un procedimiento y proporciona los valores de los parámetros necesarios.

Invocado (o llamado): procedimiento que ejecuta una serie de instrucciones almacenadas basadas en los parámetros proporcionados por el llamador y después le devuelve el control.

Contador de programas (PC): registro que contiene la dirección de la instrucción que está siendo ejecutada en el programa.

114

Capítulo 2

Instrucciones: el lenguaje del computador

Utilización de más registros

Pila: estructura de datos para los registros volcados organizados como una cola tipo “último en entrar-primero en salir”. Puntero de pila: valor que indica la dirección más recientemente puesta en la pila y que muestra dónde deberían ser volcados los registros o dónde se pueden encontrar los valores antiguos de los registros. Apilar: añadir un elemento a una pila. Desapilar: sacar un elemento de una pila.

Supongamos que un compilador necesita más registros para un procedimiento que los cuatro registros de argumentos y los dos registros de retorno de valores. Puesto que debemos borrar nuestras pistas después de completar nuestra misión, cualquier registro usado por el llamador debe ser restaurado con los valores que contenía antes de que el procedimiento fuera invocado. Esta situación es un ejemplo en el cual necesitamos volcar los registros a la memoria, tal y como se mencionó en la sección Interfaz hardware software. La estructura de datos ideal para volcar registros es una pila (stack), una cola tipo “el último en entrar-el primero en salir”. Una pila necesita un puntero hacia la dirección más recientemente usada en la pila para mostrar al procedimiento siguiente donde debería colocar los registros que se volcarán, o donde se encuentran los valores antiguos de los registros. El puntero de pila (stack pointer) se ajusta a una palabra cada vez que se guarda o restaura un registro. MIPS reserva el registro 29 para el puntero de pila, dándole el nombre obvio de $sp (viene de stack pointer). Las pilas son tan utilizadas que las tranferencias de datos a ellas y desde ellas tienen sus propios nombres: poner datos en la pila se llama apilar (push), y sacar datos de la pila se llama desapilar (pop). Por razones históricas, la pila “crece” desde las direcciones más altas a las más bajas. Esta convención significa que ponemos valores sobre la pila restando del puntero de pila. Sumar al puntero de pila reduce el apilado, quitando de ese modo valores de la pila.

Compilación de un procedimiento C que no llama a otro procedimiento

EJEMPLO

Convirtamos el ejemplo de la página 79 de la sección 2.2 en un procedimiento C: int ejemplo_hoja (int g, int h, int i, int j) { int f; f = (g + h) – (i + j); return f; }

¿Cuál es el código ensamblador MIPS compilado?

RESPUESTA

Los parámetros variables g, h, i y j se corresponden con los registros de argumento $a0, $a1, $a2 y $a3, y f se corresponde con $s0. El programa compilado comienza con la etiqueta del procedimiento: ejemplo_hoja:

2.8

Apoyo a los procedimientos en el hardware del computador

El paso siguiente es guardar los registros usados por el procedimiento. La sentencia de asignación de C en el cuerpo del procedimiento es idéntica al ejemplo de la página 79, que utiliza dos registros temporales. De aquí que necesitemos guardar tres registros: $s0, $t0 y $t1. “Apilamos” los valores antiguos sobre la pila creando el espacio para tres palabras en la pila y después las almacenamos: addi $sp,$sp,-12 sw $t1, 8($sp) sw $t0, 4($sp) sw $s0, 0($sp)

# # # # # # # #

ajusta la pila para 3 campos guardar el registro posteriormente guardar el registro posteriormente guardar el registro posteriormente

hacer sitio para $t1 para usarlo $t0 para usarlo $s0 para usarlo

La figura 2.10 muestra la pila antes, durante y después de la llamada al procedimiento. Las tres sentencias siguientes corresponden al cuerpo del procedimiento, que sigue el ejemplo de la página 79: add $t0,$a0,$a1 # registro $t0 contiene g + h add $t1,$a2,$a3 # registro $t1 contiene i + j sub $s0,$t0,$t1 # f = $t0 – $t1, que es (g + h)–(i + j)

Para retornar el valor de f, lo copiamos en un registro de retorno del valor: add $v0,$s0,$zero # returns f ($v0 = $s0 + 0)

Antes del retorno, restauramos los tres valores antiguos de los registros que guardamos “desapilándolos” de la pila: lw $s0, 0($sp) lw $t0, 4($sp) lw $t1, 8($sp) addi $sp,$sp,12

# # # #

restaura registros $s0 para el llamador restaura registros $t0 para el llamador restaura registros $t1 para el llamador ajusta la pila para eliminar 3 campos

El procedimiento termina con una instrucción de salto indirecto según registro (jump register) que utiliza la dirección de retorno: jr $ra

# salto de retorno a la rutina de llamada

En el ejemplo anterior utilizamos los registros temporales y suponemos que sus valores antiguos deben ser guardados y restaurados. Para evitar guardar y restaurar un registro cuyo valor nunca se utiliza, cosa que puede suceder con un registro temporal, el software MIPS separa 18 de los registros en dos grupos: ■ $t0-$t9: 10 registros temporales que no son preservados por el procedimiento llamado en una llamada al procedimiento. ■ $s0-$s7: 8 registros guardados que se deben preservar en una llamada a procedimiento (si fueran utilizados, el procedimiento llamado los guardaría y restauraría). Esta simple convención reduce el volcado de registros. En el ejemplo anterior, puesto que el llamador (procedimiento que hace la llamada) no espera que los registros $t0

115

116

Capítulo 2

Instrucciones: el lenguaje del computador

y $t1 sean preservados a lo largo de una llamada al procedimiento, podemos evitar dos almacenamientos y dos cargas en el código. Todavía debemos guardar y restaurar $s0, ya que el llamado debe suponer que el llamador necesita su valor. Direcciones superiores

$sp

$sp Contenido del registro $t1 Contenido del registro $t0 $sp

Direcciones inferiores

a.

Contenido del registro $s0

b.

c.

FIGURA 2.10 Los valores del puntero de pila y la pila (a) antes, (b) durante y (c) después de la llamada al procedimiento. El puntero de pila siempre apunta a la “cabeza” de la pila, o a la última palabra de la pila en este dibujo.

Procedimientos anidados Los procedimientos que no llaman a otros se llaman los procedimientos hoja (leaf). La vida sería sencilla si todos los procedimientos fueran procedimientos hoja, pero no lo son. De la misma forma que un espía puede emplear a otros espías para cumplir parte de una misión, que a su vez pueden utilizar a otros espías, así los procedimientos invocan a otros procedimientos. Aún más, los procedimientos recurrentes o recursivos incluso invocan a “clones” de sí mismos. Del mismo modo que necesitamos tener cuidado cuando se usan registros en los procedimientos, aún más cuidado debemos tener cuando se invocan procedimientos no-hoja. Por ejemplo, supongamos que el programa principal llama al procedimiento A con un argumento de 3, pone el valor 3 en el registro $a0 y después usa jalA. Supongamos entonces que el procedimiento A llama al procedimiento B haciendo jalB con un argumento de 7, también colocado en $a0. Puesto que A todavía no tiene terminada su tarea hay un conflicto con el uso del registro $a0. Análogamente, existe un conflicto con la dirección de retorno en el registro $ra, puesto que ahora tiene la dirección de retorno para el B. A menos que tomemos medidas para prevenir el problema, este conflicto eliminará la capacidad del procedimiento A para volver a su llamador. Una solución es poner en la pila todos los otros registros que se deban preservar, tal y como hicimos con los registros guardados. El llamador pone en la pila cualquier registro de argumentos de la sentencia ($a0-$a3) o registro temporal ($t0$t9) que le sea necesario después de la llamada. El llamado pone en la pila el registro de dirección de retorno $ra y cualquier registro guardado ($s0- $s7) usado por el llamado. El puntero de pila $sp se ajusta para contar el número de registros puestos en la pila. Antes del retorno, los registros se restauran desde la memoria y se reajusta el puntero de pila.

2.8

117

Apoyo a los procedimientos en el hardware del computador

Compilación de un procedimiento recursivo C, mostrando el encadenamiento de procedimientos anidados

Veamos el procedimiento recursivo que calcula el factorial:

EJEMPLO

int fact (int n) { if (n < 1) return (1); else return (n * fact(n-1)); }

¿Cuál es el código ensamblador MIPS? El parámetro n variable se corresponde con el registro de argumentos $a0. El programa compilado comienza con la etiqueta del procedimiento y después guarda dos registros en la pila, la dirección de retorno y $a0: fact: addi sw sw

$sp,$sp,-8 # ajustar la pila para 2 elementos $ra, 4($sp) # guardar la dirección de retorno $a0, 0($sp) # guardar el argumento n

La primera vez que se llama al procedimiento fact la instrucción sw guarda una dirección del programa que ha llamado a fact. Las dos instrucciones siguientes comprueban si n es menor que 1, y se va a L1 si n v 1. slti $t0,$a0,1 beq $t0,$zero,L1

# test para n < 1 # si n >= 1, ir a L1

Si n es menor que 1, fact devuelve 1 y coloca 1 en un registro de valor: se suma 1 con 0 y se sitúa el resultado de la suma en $v0. Después se eliminan dos valores guardados de la pila y salta a la dirección de retorno: addi $v0,$zero,1 # devolver 1 addi $sp,$sp,8 # eliminar 2 elementos de la pila jr $ra # retornar al llamador

Antes de quitar los dos elementos de la pila, habríamos podido cargar $a0 y $ra. Puesto que $a0 y $ra no cambian cuando n es menor que 1, nos saltamos esas instrucciones. Si n no es menor que 1, se disminuye el argumento n y entonces fact se llama otra vez con el valor disminuido: L1: addi $a0,$a0,-1 # n >= 1: el argument toma el valor (n – 1) jal fact # llamar a fact con (n – 1)

RESPUESTA

118

Capítulo 2

Instrucciones: el lenguaje del computador

La siguiente instrucción es donde fact retorna. La dirección de retorno antigua y el argumento antiguo se restauran, junto con el puntero de pila: lw $a0, 0($sp) # retorno de jal:restaura el argumento n lw $ra, 4($sp) # restaura la dirección de retorno addi $sp, $sp,8 # ajusta el puntero de pila para eliminar # 2 elementos

A continuación, el registro de valor $v0 toma el valor del producto del argumento antiguo $a0 y del valor actual del registro de valor. Suponemos que se dispone de una instrucción de multiplicar, aunque no será tratada hasta el capítulo 3: mul

$v0,$a0,$v0

# devuelve n * fact (n – 1)

Finalmente, fact salta de nuevo a la dirección de retorno: jr

Interfaz hardware software

Puntero global: registro que se reserva para apuntar a datos estáticos.

$ra

# retorna al llamador

Una variable C es una posición de almacenamiento, y su interpretación depende de su tipo y clase de almacenamiento. Los ejemplos incluyen números enteros y caracteres (véase sección 2.9). C tiene dos clases de almacenamiento: automático y estático. Las variables automáticas son locales para un procedimiento y se descartan cuando el procedimiento finaliza. Las variables estáticas existen para todos los procedimientos, desde que comienzan hasta que acaban. Las variables de C declaradas fuera de todos los procedimientos se consideran estáticas, al igual que cualquier variable declarada que usa la palabra clave static. El resto es automático. Para simplificar el acceso a los datos estáticos el software MIPS reserva otro registro, llamado el puntero global o $gp. La figura 2.11 resume qué se preserva a lo largo de una llamada a un procedimiento. Obsérvese que varios sistemas conservan la pila garantizando que el llamador obtendrá, al cargar desde la pila, los mismos datos de retorno que puso en la pila mediante un almacenamiento. La pila por encima de $sp se protege simplemente cerciorándose que el llamado no escribe sobre $sp; $sp es a su vez preservado por el llamado añadiéndole exactamente la misma cantidad de elementos que se le restaron, y los otros registros son conservados salvándolos en la pila (si se utilizan) y restaurándolos desde allí. Preservado

No preservado

Registros guardados: $s0–$s7

Registros temporales: $t0–$t9

Registro puntero de la pila: $sp

Registros de argumentos: $a0–$a3

Registro dirección de retorno: $ra

Registros de valor de retorno: $v0–$v1

Pila por encima del puntero de pila

Pila por debajo del puntero de pila

FIGURA 2.11 Qué es y qué no es preservado a lo largo de una llamada a un procedimiento. Si el programa cuenta con un registro de puntero de bloque de activación o de registro de puntero global, tratados en las siguientes secciones, también se preservan.

2.8

Apoyo a los procedimientos en el hardware del computador

119

Asignación del espacio para los nuevos datos en la pila La última complicación es que la pila también se utiliza para almacenar las variables locales del procedimiento que no caben en los registros, tales como tablas o estructuras locales. El segmento de la pila que contiene los registros guardados del procedimiento y las variables locales se llama estructura del procedimiento o bloque de activación. La figura 2.12 muestra el estado de la pila antes, durante y después de la llamada a un procedimiento. Algunos programas MIPS utilizan un puntero de estructura (frame pointer) llamado $fp para señalar a la primera palabra de la estructura de un procedimiento. Un puntero de pila podría cambiar durante el procedimiento, y por tanto las referencias a una variable local en memoria podrían tener diversos desplazamientos dependiendo de donde estuvieran en el procedimiento, haciendo que éste sea más difícil de entender. Como alternativa, un puntero de estructura ofrece un registro base estable dentro de un procedimiento para las referencias locales de la memoria. Obsérvese que un bloque de activación aparece en la pila se haya utilizado o no un puntero de estructura explícito. Hemos estado evitando $fp al evitar cambios en $sp dentro de un procedimiento: en nuestros ejemplos, la pila se ajusta solamente a la entrada y la salida del procedimiento.

Direcciones superiores

$fp

$fp $sp

$sp $fp

Registros de argumentos salvados (si hay) Dirección de retorno salvada Registros salvados salvados (si hay)

$sp Direcciones inferiores

a.

Tablas y estructuras locales (si hay)

b.

c.

FIGURA 2.12 IIlustración de la asignación de la pila (a) antes, (b) durante y (c) después de la llamada al procedimiento. El puntero de estructura (frame pointer) o $fp señala hacia la primera palabra del bloque de activación, habitualmente un registro de argumento guardado, y el puntero de pila o $sp apunta al comienzo de la pila. La pila se ajusta para hacer sitio a todos los registros guardados y a toda variable local residente en la memoria principal. Puesto que el puntero de pila puede cambiar durante la ejecución del programa, es más fácil que los programadores se refieran a variables vía el puntero de estructura, aunque podría hacerse sólo con el puntero de pila y una pequeña aritmética de direcciones. Si no hay variables locales en la pila dentro de un procedimiento, el compilador ahorrará tiempo si no ajusta y no restaura el puntero de estructura. Cuando se utiliza un puntero de estructura se inicializa usando la dirección en $sp de la llamada, y se restaura $sp usando $fp. Esta información se encuentra también en la columna 4 de la Tarjeta de Datos de Referencia de MIPS, que se incluye con este libro.

Estructura del procedimiento (bloque de activación): segmento de la pila que contiene los registros guardados y las variables locales de un procedimiento.

Puntero de estructura (frame pointer): valor que indica la posición de los registros guardados y de las variables locales para un procedimiento dado.

120

Capítulo 2

Instrucciones: el lenguaje del computador

Asignación del espacio para los nuevos datos sobre el montón (heap)

Segmento de texto: segmento de un fichero objeto de Unix que contiene el código en lenguaje máquina para las rutinas en el archivo fuente.

Además de las variables automáticas que son locales para los procedimientos, los programadores de lenguaje C necesitan espacio en la memoria para las variables estáticas y para las estructuras de datos dinámicas. La figura 2.13 muestra el convenio de MIPS para la asignación de la memoria. La pila comienza en el límite superior de la memoria y crece hacia abajo. La primera parte del extremo inferior de la memoria está reservada, y va seguida por el código máquina original de MIPS, tradicionalmente llamado segmento de texto. Encima del código está el segmento de datos estáticos, donde se almacenan las constantes y otras variables estáticas. Aunque las tablas tienden a ser de longitud fija y entonces son una buena combinación para el segmento de datos estáticos, las estructuras de datos como listas encadenadas tienden a crecer y a contraerse durante su ciclo de vida. Tradicionalmente, el segmento para tales estructuras de datos se llama el montón (heap) y se coloca después en memoria. Obsérvese que esta asignación permite a la pila y al montón crecer el uno hacia el otro, posibilitando el uso eficiente de la memoria mientras los dos segmentos incrementan y disminuyen. C asigna y libera el espacio en el montón con funciones explícitas: malloc( ) asigna espacio en el montón y retorna un puntero a él, y free( ) libera espacio en el montón al cual apunta el puntero. Los programas en C controlan la asignación de memoria C y es la fuente de muchos errores (bugs) comunes y difíciles. Olvidarse de liberar espacio conduce a una “pérdida de memoria”, que puede llegar a “colgar” (crash) el sistema operativo. Liberar espacio demasiado pronto puede conducir a “punteros colgados” (dangling pointers), y provoca que algunos punteros apunten a donde el programa no tiene intención de apuntar. Para evitar estos errores, Java utiliza una asignación de manera automática y “garbage collection”. $sp

7fff fffchex

Pila

Datos dinámicos $gp

1000 8000hex 1000 0000hex

pc

0040 0000hex

Datos estáticos Texto

0

Reservado

FIGURA 2.13 Asignación de memoria MIPS para el programa y los datos. Estas direcciones son solamente una convención del software y no parte de la arquitectura MIPS. Comenzando de arriba a abajo, el puntero de pila se inicializa en 7fff fffchex y crece hacia abajo, hacia el segmento de datos. En el otro extremo, el código del programa (“texto”) empieza en 0040 0000hex. Los datos estáticos empiezan en 1000 0000hex. Los datos dinámicos, asignados por malloc en C y por new en Java, van a continuación y crecen hacia arriba, hacia la pila, en un área llamada el montón (heap). El puntero global, $gp, se fija a una dirección para hacer fácil el acceso a los datos. Se inicializa en 1000 8000hex, de modo que pueda tener acceso a partir de 1000 0000hex hasta 1000 ffffhex usando desplazamientos de 16 bits positivos y negativos desde $gp. Esta información se encuentra también en la columna 4 de la Tarjeta de Datos de Referencia de MIPS, que se incluye con este libro.

2.8

Apoyo a los procedimientos en el hardware del computador

La figura 2.14 resume las convenciones de los registros para el lenguaje ensamblador MIPS. Nombre

Número de registros

$zero $v0–$v1 $a0–$a3 $t0–$t7 $s0–$s7 $t8–$t9 $gp $sp $fp $ra

0 2–3 4–7 8–15 16–23 24–25 28 29 30 31

Preservado en llamada

Uso El valor constante 0 Valores para los resultados y evaluación de la expresión Argumentos Temporales Guardados Más temporales Puntero global Puntero de la pila Puntero de estructura Dirección de retorno

n.a. no no no sí no sí sí sí sí

FIGURA 2.14 Convenciones de registro MIPS. El registro 1, llamado $at, está reservado para el ensamblador (véase la sección 2.12), y los registros 26-27, llamados $k0-$k1, están reservados para el sistema operativo. Esta información se encuentra también en la columna 2 de la Tarjeta de Datos de Referencia de MIPS que se incluye con este libro.

Extensión: ¿Qué pasa si hay más de cuatro parámetros? La convención MIPS es poner los parámetros adicionales en la pila justo por encima del puntero de estructura. El procedimiento espera entonces que los cuatro primeros parámetros estén en los registros $a0 a $a3 y el resto en la memoria, direccionables a través del puntero de estructura (puntero del bloque de activación). Según lo mencionado en el pie de la figura 2.12, el puntero de estructura es conveniente porque todas las referencias a las variables en la pila dentro de un procedimiento tendrán el mismo desplazamiento. Sin embargo, el puntero de estructura no es necesario. El compilador de C del GNU para MIPS utiliza un puntero de estructura, pero el compilador de C de MIPS no lo hace; utiliza el registro 30 como otro registro guardado ($s8).

Extensión: Algunos procedimientos recursivos pueden implementarse iterativamente sin usar una recursión. De este modo se pueden mejorar las prestaciones de forma significativa porque se elimina el sobrecoste asociado a la llamada al procedimiento. Por ejemplo, consideremos un procedimiento que acumula una suma:

int sum(in n, int acc){ if (n>0) return sum(n-1,acc+n); else return acc; } Si la llamada es sum(3,0), recursivamente se llama a sum(2,3), sum(1,5) y sum(0,6), y el resultado es 6, que será devuelto cuatro veces. Esta llamada recursiva a suma se conoce como llamada de cola (tail call), y este ejemplo puede implementarse eficientemente (suponga que $a0 = n y $a1 = acc):

sum: slti$a0, 1 beq$a0, $zero, sum_exit add$a1, $a1, $a0

# comprueba si n <= 0 # salto a sum_exit si n <= 0 # suma n a acc

121

122

Capítulo 2

Instrucciones: el lenguaje del computador

addi$a0, $a0, -1 j sum sum_exit: add$v0, $a1, $zero jr $ra

# decrementa n # salto a sum # devuelve el valor acc # retorno de rutina

Autoevaluación ¿Cuáles de las siguientes declaraciones sobre C y Java son generalmente ciertas? 1. Los programadores de C manipulan datos explícitamente, mientras que en Java es automático. 2. C es más propenso a errores de punteros y pérdidas de memoria que Java. !(@ | = > (la lengüeta abierta de la ululación en la barra es grande) Cuarta línea del teclado- poema “Hatless Atlas,” 1991 (algunos dan nombres a los caracteres ASCII: “!” es la ululación, “(” está abierto, “|” es la barra, y así sucesivamente).

2.9

Comunicarse con la gente

2.9

Los computadores fueron inventados para hacer muchos cálculos, pero tan pronto como llegaron a ser comercialmente viables se utilizaron para procesar texto. La mayoría de los computadores utilizan hoy bytes de 8 bits para representar caracteres, con el código American Standard Code for Information Interchange (ASCII), que es la representación que casi todos siguen. La figura 2.15 resume el ASCII.

Valor ASCII

Carácter

Valor ASCII

32

espacio

48

33

!

49

34

"

50

35

#

51

Carácter

Valor ASCII

Carácter

Valor ASCII

Carácter

Valor ASCII

Carácter

Valor ASCII

Carácter

0

64

@

80

P

096

`

112

p

1

65

A

81

Q

097

a

113

q

2

66

B

82

R

098

b

114

r

3

67

C

83

S

099

c

115

s

36

$

52

4

68

D

84

T

100

d

116

t

37

%

53

5

69

E

85

U

101

e

117

u

38

&

54

6

70

F

86

V

102

f

118

v

39

'

55

7

71

G

87

W

103

g

119

w

40

(

56

8

72

H

88

X

104

h

120

x

41

)

57

9

73

I

89

Y

105

i

121

y

42

*

58

:

74

J

90

Z

106

j

122

z

43

+

59

;

75

K

91

[

107

k

123

{

44

,

60

<

76

L

92

\

108

l

124

|

45

-

61

=

77

M

93

]

109

m

125

}

46

.

62

>

78

N

94

^

110

n

126

~

47

/

63

?

79

O

95

_

111

o

127

SUPR

FIGURA 2.15 Representación ASCII de los caracteres. Obsérvese que las letras mayúsculas y minúsculas difieren exactamente en 32; esta observación nos puede ayudar a hacer simplificaciones a la hora de comprobar o cambiar mayúsculas y minúsculas. Los valores no mostrados corresponden a caracteres de formato. Por ejemplo, 8 representa la tecla de retroceso, 9 representa un carácter de tabulación y 13 un retorno del carro. Otro valor útil es 0 para nulo (falta de información), que es el valor que el lenguaje C de programación utiliza para marcar el final de una cadena. Esta información se encuentra también en la columna 3 de la Tarjeta de Datos de Referencia de MIPS que se incluye con este libro.

2.9

123

Comunicarse con la gente

La base 2 no es natural para el ser humano; tenemos diez dedos y por lo tanto encontramos la base 10 natural. ¿Por qué los computadores no usan una representación decimal? De hecho, el primer computador comercial ofrecía una aritmética decimal. El problema era que este computador todavía usaba señales “on” y “off”, de modo que un dígito decimal se representaba por varios dígitos binarios. La representación decimal se mostró tan ineficiente que los computadores siguientes representaban todo en binario, convirtiendo a base 10 sólo las operaciones de entrada/salida, relativamente infrecuentes.

Interfaz hardware software

ASCII frente a números binarios

Se podrían representar los números como secuencias de dígitos ASCII en lugar de como enteros. ¿Cuánto aumentaría el almacenamiento si el número mil millones se representa como ASCII en lugar de como un entero de 32 bits? Mil millones, 1 000 000 000, necesitaría 10 dígitos ASCII de 8 bits cada uno. De este modo, el aumento en el almacenamiento sería (10 × 8)/32 = 2.5. Además, el hardware para sumar, restar, multiplicar y dividir números decimales es complejo. Estas dificultades explican por qué los profesionales de la computación creen que la representación binaria es natural y por qué consideran extraña la utilización de la representación decimal.

Una serie de instrucciones puede extraer un byte de una palabra, así que las instrucciones de cargar palabra (load word) y almacenar palabra (store word) son suficientes para la transferencia tanto de bytes como de palabras. No obstante, debido a la popularidad del procesamiento de texto en algunos programas, MIPS proporciona instrucciones para mover bytes. Cargar byte (load byte, lb) carga un byte desde la memoria y lo coloca en los 8 bits más a la derecha de un registro. Almacenar byte (store byte, sb) toma un byte de los 8 bits más a la derecha de un registro y lo escribe en la memoria. Así, copiamos un byte con la secuencia lb $t0,0($sp)

# leer byte de la fuente

sb $t0,0($gp)

# escribir byte en el destino

EJEMPLO

RESPUESTA

124

Capítulo 2

Interfaz hardware software

Instrucciones: el lenguaje del computador

La dicotomía con signo-sin signo se aplica también a las cargas, además de a la aritmética. La función de una carga con signo es copiar el signo repetitivamente hasta completar el resto del registro, llamado extensión de signo, pero su propósito es cargar una representación correcta del número en el registro. La carga sin signo rellena con ceros los bits a la izquierda del dato, de modo que el número representado por la secuencia de bits no tiene signo. Cuando se carga una palabra de 32 bits en un registro de 32 bits, este aspecto es dudoso; las cargas con y sin signo son idénticas. MIPS ofrece dos variantes para la carga de un byte: carga un byte (lb) trata el byte como un número con signo y hace extensión de signo para completar los 24 bits más a la izquierda del registro, mientras que carga un byte sin signo (lbu) lo trata como un entero sin signo. Dado que los programas en C casi siempre usan bytes para representar caracteres en lugar de considerar los bytes como enteros con signo muy cortos, lbu se usa casi exclusivamente para la carga de bytes. Los caracteres normalmente se combinan en cadenas, que tienen un número variable de ellos. Hay tres opciones para representar una cadena: (1) la primera posición de la cadena se reserva para indicar la longitud o tamaño de la cadena, (2) una variable complementaria que tiene la longitud de la cadena (como en una estructura), o (3) la última posición de una cadena se indica con un carácter de fin de cadena. C utiliza la tercera opción: termina una cadena con un byte cuyo valor es 0 —denominado nulo (null) en ASCII—. Así, la cadena “Cal” se representa en C con los 4 bytes siguientes, mostrados como números decimales: 67, 97, 108, 0.

Compilación de un procedimiento de copia de cadenas que muestra cómo se utilizan las cadenas en C

EJEMPLO

El procedimiento strcpy copia la cadena y en la cadena x usando la convención de terminación con byte nulo de C: void strcpy (char x[], char y[]) { int i; i = 0; while ((x[i] = y[i]) != ‘\0’) /* copia y comprueba el byte */ i += 1; }

¿Cuál es el código ensamblador MIPS?

2.9

Comunicarse con la gente

A continuación mostramos el fragmento de código básico de ensamblador MIPS. Suponemos que las direcciones base de las tablas x e y se encuentran en $a0 y $a1, mientras que i estará en $s0. strcpy ajusta el puntero de pila y luego guarda el registro $s0 en la pila: strcpy: addi $sp,$sp,-4 sw $s0,0($sp)

# ajusta la pila para un elemento más # guardar $s0

Para inicializar i a 0, la siguiente instrucción actualiza $s0 a 0 sumando 0 a 0 y colocando esa suma en $s0: add

$s0,$zero,$zero

# i = 0 + 0

éste es el principio del lazo. La dirección de y[i] se forma primero sumando i a y[]: L1: add

$t1,$s0,$a1

# dirección de y[i] en $t1

Observe que no tenemos que multiplicar i por 4 puesto que y es una tabla de bytes y no de palabras, como en ejemplos anteriores. Para cargar el carácter en y[i] utilizamos cargar byte (load byte), que pone el carácter en $t2: lb

$t2, 0($t1)

# $t2 = y[i]

Un cálculo de dirección similar coloca la dirección de x[i] en $t3, y entonces el carácter en $t2 se almacena en esa dirección. add sb

$t3,$s0,$a0 $t2, 0($t3)

# dirección de x[i] en $t3 # x[i] = y[i]

A continuación salimos del lazo si el carácter es 0; es decir, si es el último carácter de la cadena: beq

$t2,$zero,L2 # si y[i] == 0, ir a L2

si no, incrementamos i y se vuelve hacia atrás al lazo: addi $s0,$s0,1 j L1

# i = i + 1 # ir a L1

125

RESPUESTA

126

Capítulo 2

Instrucciones: el lenguaje del computador

si no se salta hacia atrás, porque era el último carácter de la cadena, restauramos $s0 y el puntero de pila y después se retorna. L2: lw

$s0,0($sp)

addi $sp,$sp,4 jr $ra

# # # #

y[i] == 0: fin de cadena; restaura el antiguo $s0 elimina una palabra de la pila retorna

En C, el copiado de cadenas utiliza generalmente punteros en vez de tablas, para evitar las operaciones con i en el código anterior. Véase la sección 2.15 para una explicación comparativa de tablas versus punteros. Puesto que el procedimiento strcpy anterior es un procedimiento hoja, el compilador podría asignar i a un registro temporal y evitar tener que guardar y restaurar $s0. Por tanto, en vez de considerar los registros $t simplemente como lugares temporales, podemos pensar en ellos como registros que el llamado debe utilizar siempre que crea conveniente. Cuando un compilador encuentra un procedimiento hoja, utiliza todos los registros temporales antes de usar los registros que debe guardar.

Caracteres y cadenas en Java Unicode es una codificación universal de los alfabetos de la mayoría de los idiomas humanos. La figura 2.16 es una lista de los alfabetos de Unicode; existen tantos alfabetos en Unicode como símbolos útiles hay en ASCII. Para ser más inclusivos, Java utiliza Unicode para los caracteres. Por defecto, utiliza 16 bits para representar un carácter. El repertorio de instrucción MIPS tiene instrucciones explícitas para cargar y para almacenar cantidades de 16 bits llamadas medias palabras (halfwords). Cargar media (load half, lh) carga media palabra de la memoria, y la coloca en los 16 bits de más a la derecha de un registro. De forma similar a la carga (load), la instrucción de carga mitad (load half, lh) considera la media palabra como un número con signo y utiliza la extensión de signo para completar los 16 bits de la izquierda del registro; por otra parte, la instrucción carga mitad sin signo (load half unsigned, lhu) considera enteros sin signo. Esto hace que lhu sea la más popular de las dos. Almacenar media (store half, sh) toma la mitad de una palabra, los 16 bits de la parte de más a la derecha de un registro, y la escribe en la memoria. Copiamos una media palabra con la secuencia: lhu $t0,0($sp) # lee media palabra (16 bits) desde una fuente sh $t0,0($gp) # escribe media palabra (16 bits) a un destino

Las cadenas son una clase estándar de Java con una ayuda especial incorporada y métodos predefinidos para el encadenamiento, la comparación y la conversión. A diferencia de C, Java incluye una palabra que da la longitud de la cadena, similar a las tablas de Java.

2.9

Comunicarse con la gente

127

Extensión: Los programas MIPS intentan mantener la pila alineada a direcciones de palabras, permitiendo al programa utilizar solamente lw y sw (que deben estar alineados) para tener acceso a la pila. Esta convención significa que una variable char (tipo carácter) asignada a la pila ocupa 4 bytes, aunque necesite menos. Sin embargo, una variable de cadena de C o una tabla de bytes empaquetará 4 bytes por palabra, y una variable de cadena de Java o una tabla de cortos empaquetará 2 medias palabras por palabra. Latino

Malayalam

Tagbanwa

Puntuación general

Griego

Sinhala

Khmer

Letras modificadoras de espacio

Cirílico

Tailandés

Mongol

Símbolos actuales

Armenio

Lao

Limbu

Combinar marcas diacríticas

Hebreo

Tibetano

Tai Le

Combinar las marcas para los símbolos

Árabe

Myanmar

Kangxi Radicals

Superíndices y subíndices

Sirio

Georgiano

Hiragana

Formas de número

Thaana

Hangul coreano

Katakana

Operadores matemáticos

Devanagari

Ethiopic

Bopomofo chino

Símbolos alfanuméricos matemáticos

Bengalí

Cheroqui

Kanbun

Patrones de Braille

Gurmukhi

Sílabas aborígenes

Shavian

Reconocimiento de caracteres ópticos

canadienses Guyarate

Ogham

Osmanya

Símbolos musicales Byzantine

Oriya

Runic

Cypriot Syllabary

Símbolos musicales

Tamil

Tagalog

Tai Xuan Jing Symbols

Flechas

Telugu

Hanunoo

Yijing Hexagram Symbols Caja de dibujo

Kannada

Buhid

Aegean Numbers

Formas geométricas

FIGURA 2.16 Ejemplos de alfabetos en Unicode. La versión 4.0 de Unicode tiene más de 160 “bloques”, que es el nombre que usa para una colección de símbolos. Cada bloque es un múltiplo de 16. Por ejemplo, el griego empieza en 0370hex, y el cirílico en 0400hex. Las primeras tres columnas muestran 48 bloques que corresponden a idiomas humanos en aproximadamente el orden numérico de Unicode. La última columna tiene 16 bloques que son multilingües y no están en orden. Una codificación de 16 bits, llamada UTF-16, es la básica por defecto. Una codificación de longitud variable, llamada UTF-8, guarda el subconjunto ASCII como 8 bits y utiliza 16-32 bits para los otros caracteres. UTF-32 utiliza 32 bits por carácter. Para aprender más, véase www.unicode.org.

I. ¿Cuáles de las siguientes sentencias sobre caracteres y cadenas en C y en Java Autoevaluación son verdaderas? 1. Una cadena en C ocupa aproximadamente la mitad de memoria que la misma cadena en Java. 2. “Cadena” es precisamente un nombre informal para las tablas de una sola dimensión de caracteres en C y en Java. 3. En C y en Java se usa el carácter nulo (0) para marcar el final de una cadena. 4. Las operaciones con cadenas, así como su longitud, son más rápidas en C que en Java. II. ¿Qué tipo de variable que puede contener el valor 1 000 000 000diez necesita más espacio de memoria? 1. int de C 2. string de C 3. string de Java

128

Capítulo 2

2.10

Instrucciones: el lenguaje del computador

Direcciones y direccionamiento inmediato MIPS para 32 bits

2.10

Aunque mantener todas las instrucciones MIPS con un tamaño de 32 bits simplifica todo el hardware, en ocasiones sería conveniente tener una constante de 32 bits o una dirección de 32 bits. Esta sección comienza con la solución general para las constantes grandes y después muestra las optimizaciones para las direcciones de instrucción usadas en saltos condicionales (bifurcaciones) e incondicionales.

Operandos inmediatos de 32 bits Aunque las constantes son con frecuencia cortas y caben en campos de 16 bits, algunas veces son más grandes. El repertorio de instrucciones MIPS incluye específicamente la instrucción carga superior inmediata (load upper immediate, lui) para fijar los 16 bits superiores de una constante en un registro, permitiendo a una instrucción posterior especificar los 16 bits inferiores de la constante. La figura 2.17 muestra la operación de lui.

Carga de una constante de 32 bits

EJEMPLO

¿Cuál es el código ensamblador MIPS para cargar la siguiente constante de 32 bits en el registro $s0? 0000 0000 0011 1101 0000 1001 0000 0000

RESPUESTA

Primero cargaríamos los 16 bits de mayor peso, que es 61 en decimal, usando lui: lui $s0, 61 # 61 en decimal = 0000 0000 0011 1101 binario

El valor del registro $s0 después de esto es: 0000 0000 0011 1101 0000 0000 0000 0000

El paso siguiente es sumar los 16 bits de menor peso, cuyo valor decimal es 2304: ori $s0, $s0, 2304 # 2304 decimal = 0000 1001 0000 0000

El valor final del registro $s0 es el valor deseado: 0000 0000 0011 1101 0000 1001 0000 0000

2.10

Direcciones y direccionamiento inmediato MIPS para 32 bits

La versión del lenguaje máquina de lui $t0, 255 001111

00000

# $t0 es el registro 8:

01000

0000 0000 1111 1111

Contenido del registro $t0 después de ejecutar lui $t0, 255: 0000 0000 1111 1111

0000 0000 0000 0000

FIGURA 2.17 El efecto de la instrucción lui. La instrucción lui transfiere el valor del campo constante inmediato de 16 bits a los 16 bits del extremo izquierdo del registro, y rellena los 16 bits más bajos con ceros.

Tanto el compilador como el ensamblador deben descomponer las constantes grandes en partes y después volverlas a montar en un registro. Como es de esperar, la restricción del tamaño del campo inmediato puede ser un problema para las direcciones de memoria en cargas y almacenamientos, igual que para las constantes en instrucciones inmediatas. Si este trabajo recae en el ensamblador, como pasa en los programas MIPS, éste debe tener un registro temporal disponible para crear los valores largos. Ésta es la razón por la que el registro $at se reserva para el ensamblador. De aquí que la representación simbólica del lenguaje máquina MIPS no esté limitada por el hardware, sino por lo que el creador del ensamblador elige incluir (véase la sección 2.12). Nos quedamos cerca del hardware para explicar la arquitectura del computador, atentos a cuándo utilizamos el lenguaje mejorado del ensamblador que no se encuentra en el procesador. Extensión: Hay que tener cuidado en la creación de constantes de 32 bits. La instrucción addi copia el bit de mayor peso del campo inmediato de 16 bits de la instrucción en los 16 bits de mayor peso de una palabra. La instrucción or lógico inmediato (ori) de la sección 2.6 carga ceros en los 16 bits de mayor peso y por tanto es utilizado por el ensamblador conjuntamente con lui para crear constantes de 32 bits.

Direccionamiento en saltos condicionales (bifurcaciones) e incondicionales Las instrucciones de salto MIPS tienen el direccionamiento más simple. Utilizan el formato de instrucción MIPS final, llamado de J-type, que consiste en 6 bits para el campo de la operación y el resto para el campo de la dirección. Así, j

1000

# ir a la posición 10000

podría ser montado en este formato (en realidad es un poco más complicado, como veremos en la página siguiente): 2

10000

6 bits

26 bits

donde el valor del código de operación del salto es 2 y la dirección del salto es 10000.

Interfaz hardware software

129

130

Capítulo 2

Instrucciones: el lenguaje del computador

A diferencia de la instrucción de salto incondicional, la instrucción de bifurcación condicional debe especificar dos operandos además de la dirección de bifurcación. Así pues, # ir a Salida si $s0 | $s1

bne $s0,$s1,Exit

se ensambla en esta instrucción, y deja solamente 16 bits para la dirección de bifurcación: 5

16

17

Salida

6 bits

5 bits

5 bits

16 bits

Si las direcciones del programa tuvieran que caber en este campo de 16 bits, significaría que ningún programa podría ser más grande que 216, cosa que está lejos de ser una opción realista hoy en día. Una alternativa sería especificar un registro que se añadiera siempre a la dirección de bifurcación, de modo que una instrucción de bifurcación calcularía lo siguiente: Contador de programa = registro + dirección de bifurcación

Direccionamiento relativo al PC: régimen de direccionamiento en el cual la dirección es la suma del contador del programa (PC) y de una constante en la instrucción.

Esta suma permite al programa alcanzar los 232 bytes y aún ser capar de utilizar las bifurcaciones condicionales, solucionando el problema del tamaño de la dirección de bifurcación. La pregunta entonces es: ¿qué registro? La respuesta viene dada al considerar cómo se utilizan las bifurcaciones condicionales. Los saltos condicionales se encuentran en lazos y en sentencias if, por tanto tienden a bifurcar una instrucción cercana. Por ejemplo, casi la mitad de todas las bifurcaciones condicionales en los programas de prueba SPEC van a parar a posiciones no más allá de 16 instrucciones. Puesto que el contador de programa (PC) contiene la dirección de la instrucción actual, podemos saltar dentro del rango de palabras ±215 desde la instrucción actual si utilizamos el PC como el registro a ser añadido a la dirección. Casi todos los lazos y sentencias if son mucho más pequeños de ±216 palabras, así que el PC es la elección ideal. Esta forma de direccionamiento de las bifurcaciones o saltos condicionales se llama direccionamiento relativo al PC. Tal y como veremos en el capítulo 4, es conveniente que el hardware incremente el PC pronto para apuntar a la instrucción siguiente. Por tanto, la dirección MIPS hace referencia en realidad a la dirección de la instrucción siguiente (PC + 4) en lugar de a la instrucción actual (PC). Como la mayoría de los computadores recientes, MIPS utilizan el modo de direccionamiento relativo al PC para todas las bifurcaciones o saltos condicionales porque el destino de estas instrucciones es probable que esté cerca de la instrucción de bifurcación. Por otra parte, las instrucciones de saltar-y-enlazar (jump-and-link) invocan procedimientos que no tienen ninguna razón para estar cerca de la llamada, así que normalmente utilizan otros modos de direccionamiento. Por tanto, la arquitectura MIPS permite direcciones largas en las llamadas a procedimientos al utilizar el formato J-type para las instrucciones de salto y de saltar-y-enlazar.

2.10

131

Direcciones y direccionamiento inmediato MIPS para 32 bits

Puesto que todas las instrucciones MIPS tienen 4 bytes de largo, MIPS alarga la distancia de las bifurcaciones teniendo el direccionamiento relativo al PC referido al número de palabras de la instrucción siguiente, en vez de al número de bytes. Así, el campo de 16 bits puede bifurcar cuatro veces más lejos si interpreta el campo como una dirección relativa a la palabra en lugar de como una dirección relativa al byte. Análogamente, el campo de 26 bits en instrucciones de salto es también una dirección de palabra, así que representa una dirección byte de 28 bits. Extensión: Puesto que el PC es de 32 bits, 4 bits deben venir de alguna otra parte. La instrucción de salto MIPS reemplaza solamente los 28 bits de menor peso del PC, y deja los 4 bits de mayor peso sin cambios. El cargador y el enlazador (sección 2.12) deben tener cuidado de evitar colocar un programa que cruce una dirección límite de 256 MB (64 millones de instrucciones); de lo contrario, un salto se debe reemplazar por una instrucción de salto indirecto sobre registro precedida por otras instrucciones para cargar la dirección completa de 32 bits en un registro.

Muestra del desplazamiento de una bifurcación o salto condicional en lenguaje máquina

El lazo while en la página 107-108 se ha compilado en este código ensamblador MIPS Loop:sll $t1,$s3,2 add $t1,$t1,$s6 lw $t0,0($t1) bne $t0,$s5,Salida addi $s3,$s3,1 j Loop Exit:

# # # # # #

EJEMPLO

registro temporal $t1 = 4 * i $t1 = dirección de guardar[i] registro temporal $t0 = guardar[i] ir a Salida si guardar[i] _ k i = i + 1 ir a Lazo

Si suponemos que colocamos el lazo comenzando en la posición 80000 en memoria, ¿cuál es el código máquina MIPS para este lazo? Las instrucciones ensambladas y sus direcciones tienen el siguiente aspecto: 80000

0

0

19

9

4

0

80004

0

9

22

9

0

32

80008

35

9

8

0

80012

5

8

21

2

80016

8

19

19

80020

2

80024

...

1 20000

RESPUESTA

132

Capítulo 2

Instrucciones: el lenguaje del computador

Recuérdese que las instrucciones MIPS utilizan direcciones de byte; por tanto, las direcciones de palabras consecutivas se diferencian en 4, el número de bytes en una palabra. La instrucción bne en la cuarta línea añade 2 palabras o 8 bytes a la dirección de la instrucción siguiente (80016), especificando así el destino de la bifurcación o salto condicional en relación con la siguiente instrucción (8 + 80016), en vez de en relación con la instrucción actual de bifurcación (12 + 80012) o usando la dirección de destino completa (80024). La instrucción de salto (jump) en la última línea utiliza la dirección completa (20000 × 4 = 80000) correspondiente a la etiqueta Lazo.

Interfaz hardware software

Casi todas las bifurcaciones o saltos condicionales están en una localización próxima, pero ocasionalmente se bifurca o salta más lejos, más allá de lo que puede ser representado con los 16 bits de la instrucción de bifurcación o salto condicional. El ensamblador viene al rescate tal y como lo hizo con las constantes o direcciones grandes: inserta un salto incondicional al destino de la bifurcación e invierte la condición, de modo que la bifurcación o salto condicional decida si esquiva el salto.

Bifurcación a distancia

EJEMPLO

Dada una bifurcación condicional que se produce si el registro $s0 es igual al registro $s1, beq

$s0,$s1, L1

substituirlo por un par de instrucciones que ofrezcan una distancia de bifurcación mucho mayor.

RESPUESTA

Estas instrucciones pueden ser reemplazadas por la bifurcación o salto condicional con direccionamiento corto: bne j

$s0,$s1, L2 L1

L2:

Resumen de los modos de direccionamiento de MIPS Modo de direccionamiento: uno de varios regímenes de direccionamiento delimitados por el uso variado de operandos y de direcciones.

Las múltiples formas de direccionamiento genéricamente se llaman modos de direccionamiento (addressing modes). La figura 2.18 muestra cómo se identifican los operandos para cada modo de direccionamiento. Los modos de direccionamiento de MIPS son los siguientes: 1. Direccionamiento inmediato: cuando el operando es una constante que aparece en la misma instrucción. 2. Direccionamiento a registro: cuando el operando está en un registro.

2.10

Direcciones y direccionamiento inmediato MIPS para 32 bits

1. Direccionamiento inmediato: op

rs

rt

Immediato

2. Direccionamiento a registro op

rs

rt

rd

. . . funct

Registros registro

3. Direccionamiento base op

rs

rt

dirección

registro

Memoria +

Byte

media palabra

palabra

4. Direccionamiento relativo a PC op

rs

rt

dirección

PC

Memoria +

palabra

5. Direccionamiento pseudodirecto op

dirección

Memoria

PC

palabra

FIGURA 2.18 Ilustración de los cinco modos de direccionamiento MIPS. Los operandos aparecen resaltados. El operando del modo 3 está en memoria, mientras que el operando del modo 2 es un registro. Observe que las versiones de carga y almacenamiento acceden a bytes, medias palabras o palabras. En el modo 1 el operando está en los 16 bits de la misma instrucción. Los modos 4 y 5 se usan para direccionar instrucciones en memoria: en el modo 4 se suman los 16 bits de la dirección desplazados 2 bits a la izquierda al PC, y en el modo 5 se concatenan una dirección de 26 bits desplazados 2 bits a la izquierda con los 4 bits de mayor peso del PC.

3. Direccionamiento base o desplazamiento: cuando el operando está en la posición de memoria cuya dirección es la suma de un registro y de una constante en la instrucción. 4. Direccionamiento relativo al PC: cuando la dirección es la suma del PC y de una constante en la instrucción. 5. Direccionamiento pseudodirecto: cuando la dirección de salto es los 26 bits de la instrucción concatenados con los bits de mayor peso del PC.

133

134

Capítulo 2

Interfaz hardware software

Instrucciones: el lenguaje del computador

Aunque mostramos la arquitectura MIPS con direcciones de 32 bits, casi todos los microprocesadores (MIPS incluido) tienen extensiones de direcciones de 64 bits (véase el apéndice E). Estas extensiones nacieron como respuesta a las necesidades de software para programas más grandes. El proceso de ampliación del repertorio de instrucciones permite crecer a las arquitecturas de manera que se mantenga la compatibilidad de los programas hasta la siguiente generación de la arquitectura. Obsérvese que una operación simple puede utilizar más de un modo de direccionamiento. La instrucción sumar (add), por ejemplo, usa tanto el direccionamiento inmediato (addi) como el direccionamiento a registro (add).

Descodificación de lenguaje máquina A veces nos vemos forzados a aplicar técnicas de ingeniería inversa al lenguaje máquina para reconstruir el lenguaje ensamblador del original. Por ejemplo, al examinar un volcado de memoria. La figura 2.19 muestra la codificación MIPS de los campos para el lenguaje máquina MIPS. Esta figura ayuda a traducir “a mano” el lenguaje ensamblador al lenguaje máquina y viceversa.

Descodificación de código máquina

EJEMPLO

¿Cuál es el lenguaje ensamblador que corresponde a esta instrucción de máquina? 00af8020hex

RESPUESTA

El primer paso es convertir de hexadecimal a binario para encontrar los campos op: (Bits:

31 28 26

5

2

0)

0000 0000 1010 1111 1000 0000 0010 0000

Miramos en el campo op para determinar la operación. Basándonos en la figura 2.25, cuando los bits 31-29 son 000 y los bits 28-26 son 000 se trata de una instrucción de R-format. Así pues, la instrucción binaria se reconstruye a partir de los campos de R-format listados en la figura 2.26: op

rs

rt

rd

shamt

funct

000000

00101

01111

10000

00000

100000

La porción inferior de la figura 2.19 determina la operación de una instrucción de R-format. En este caso, los bits 5-3 son 100 y los bits 2-0 son 000, lo que significa que este patrón binario representa la instrucción de la suma (add). Descodificamos el resto de la instrucción mirando los valores de los campos. Los valores decimales son 5 para el campo rs, 15 para el rt y 16 para el rd (el shamt no se usa). La figura 2.14 indica que estos números representan los registros $a1, $t7 y $s0. Ahora podemos mostrar la instrucción de ensamblador: add $s0,$a1,$t7

2.10

135

Direcciones y direccionamiento inmediato MIPS para 32 bits

op(31:26) 28–26

0(000)

1(001)

2(010)

3(011)

4(100)

5(101)

6(110)

7(111)

31–29 0(000)

R-format

Bltz/gez

jump

jump & link branch eq branch ne blez

bgtz

1(001)

add immediate

addiu

set less than imm.

sltiu

andi

ori

xori

load upper imm

2(010)

TLB

FlPt

lbu

lhu

lwr

3(011) 4(100)

load byte

load half lwl

load word

5(101)

store byte

store half

store word

6(110)

lwc0

lwc1

7(111)

swc0

swc1

swl

swr

op(31:26)=010000 (TLB), rs(25:21) 23–21

0(000)

1(001)

2(010)

3(011)

4(100)

5(101)

6(110)

7(111)

25–24 0(00)

mfc0

cfc0

mtc0

ctc0

1(01) 2(10) 3(11)

op(31:26)=000000 (R-format), funct(5:0) 2–0

0(000)

1(001)

2(010)

3(011)

4(100)

5(101)

6(110)

7(111)

5–3 0(000)

shift left logical

1(001)

jump reg.

jalr

2(010)

mfhi

3(011) 4(100) 5(101)

shift right logical

sra

mthi

mflo

mtlo

mult

multu

div

divu

add

addu

subtract

subu

set l.t.

sltu

sllv syscall

break

and

or

srlv

srav

xor

not or (nor)

6(110) 7(111)

FIGURA 2.19 Codificación de las instrucciones MIPS. Esta notación da el valor de un campo por fila y por columna. Por ejemplo, la parte superior de la figura muestra cargar palabra load word en la fila número 4 (100dos para los bits 31-29 de la instrucción) y la columna número 3 (011dos para los bits 28-26 de la instrucción), así que el valor correspondiente del campo op (bits 31-26) es 100011 dos. El resaltado significa que el campo está utilizado en alguna otra parte. Por ejemplo, el R-format en la fila 0 y la columna 0 (op = 000000dos) está definido en la parte inferior de la figura. Por tanto, restar subtract en la fila 4 y la columna 2 de la sección inferior significa que el campo funct (bits 5-0) de la instrucción es 100010dos y el campo op (bits 31-26) es 000000dos. El valor punto flotante en la fila 2 y columna 1 se define en la figura 3.18 en el capítulo 3. Bltz/gez es el código de operación (opcode) para cuatro instrucciones que se encuentran en el apéndice B: bltz, bgez, bltzal y bgezal. Este capítulo describe las instrucciones dadas con el nombre completo coloreado, mientras que el capítulo 3 describe las instrucciones dadas con mnemónicos coloreados. El apéndice B trata todas las instrucciones.

136

Capítulo 2

Nombre

Instrucciones: el lenguaje del computador

Campos

Comentarios

Tamaño del campo

6 bits

5 bits

5 bits

5 bits

5 bits

6 bits

R-format

op

rs

rt

rd

shamt

funct

I-format

op

rs

rt

J-format

op

FIGURA 2.20

address/immediate target address

Todas las instrucciones MIPS tienen 32 bits Formato de instrucciones aritméticas Formato para transferencias, bifurcaciones, imm. Formato de las instrucciones de salto incondicional

Formatos de instrucción MIPS.

La figura 2.20 muestra todos los formatos de instrucción MIPS. La figura 2.1 en la página 78 muestra el lenguaje ensamblador de MIPS tratado en este capítulo; la parte no estudiada de instrucciones MIPS se ocupa principalmente de la aritmética, que veremos en el capítulo siguiente.

Autoevaluación I. ¿Cuál es el rango de direcciones para las bifurcaciones o saltos condicionales en MIPS (K = 1024)? 1. Direcciones entre 0 y 64K – 1. 2. Direcciones entre 0 y 256K – 1. 3. Direcciones desde aproximadamente 32K antes de la bifurcación hasta 32K después. 4. Direcciones desde aproximadamente 128K antes de la bifurcación hasta 128K después. II. ¿Cuál es el rango de direcciones para saltar y saltar-y-enlazar en MIPS (M = 1024K)? 1. Direcciones entre 0 y 64M – 1. 2. Direcciones entre 0 y 256M – 1. 3. Direcciones desde aproximadamente 32M antes del salto hasta aproximadamente 32M después. 4. Direcciones desde aproximadamente 128 M antes del salto hasta aproximadamente 128M después. 5. Cualquiera dentro de un bloque de direcciones de 64M donde el PC suministra los 6 bits de mayor peso. 6. Cualquiera dentro de un bloque de direcciones de 256M donde el PC suministra los 4 bits de mayor peso. III. ¿Cuál es la instrucción del lenguaje ensamblador MIPS correspondiente a la instrucción máquina con valor 0000 0000hex? 1. 2. 3. 4. 5. 6.

j R-format addi sll mfc0

El opcode está indefinido: no hay instrucción legal que corresponda a 0.

2.11

2.11

Paralelismo e instrucciones: sincronización

Paralelismo e instrucciones: sincronización

137

2.11

La programación paralela es más fácil cuando las tareas son independientes, pero a menudo es necesaria una cooperación entre ellas. Cooperación significa habitualmente que algunas tareas están escribiendo nuevos valores que otra tareas necesitan leer. Para saber cuando una tarea ha finalizado la escritura y, por lo tanto, otra tarea ya puede hacer una lectura segura, las tareas necesitan sincronizarse. Si no hay esta sincronización existe el peligro de una carrera de datos (data race), en la que los resultados de un programa pueden variar dependiendo del orden en el que ocurran ciertos sucesos. Por ejemplo, pensemos otra vez en la analogía de los ocho periodistas escribiendo un artículo periodístico que ya hemos utilizado en la página 43 del capítulo 1. Suponga que un periodista necesita leer todas las secciones anteriores antes de escribir las conclusiones. Así, necesita saber cuando los otros periodistas han terminado sus secciones, para no tener que preocuparse de cambios que puedan producirse posteriormente. Es decir, tienen que sincronizar la escritura y lectura de cada sección para que las conclusiones sean coherentes con lo que se ha escrito en las anteriores secciones. En computación, los mecanismos de sincronización se construyen, típicamente, con rutinas software a nivel de usuario que se apoyan en instrucciones de sincronización de bajo nivel (hardware-supplied). En esta sección nos centraremos en la implementación de las operaciones de sincronización lock (bloquear) y unlock (desbloquear). Estas operaciones pueden utilizarse de forma directa para crear regiones a las que sólo puede acceder un procesador, llamadas regiones de exclusión mutua, o para implementar mecanismos de sincronización más complejos. Para implementar la sincronización en un sistema multiprocesador se requiere, como capacidad crítica, un conjunto de primitivas hardware que nos permitan leer y modificar posiciones de memoria de forma atómica. Es decir, nada se puede interponer entre la lectura y la escritura de la posición de memoria. Sin esta capacidad, la construcción de las primitivas básicas de sincronización tendría un coste demasiado elevado que además aumentaría con el número de procesadores. Hay muchas formulaciones alternativas de las primitivas hardware básicas y en todas ellas se dispone de la capacidad de lectura y modificación atómica de una posición de memoria, junto con alguna forma de indicar si la lectura y la escritura se realizaron de forma atómica. En general, los diseñadores no esperan que los usuarios empleen las primitivas hardware básicas pero, por el contrario, esperan que los programadores de sistemas utilicen estas primitivas para construir bibliotecas de sincronización; este proceso es complejo y difícil. Comenzaremos con la descripción de una de estas primitivas hardware y mostraremos cómo puede utilizarse para la construcción de una primitiva básica de sincronización. Una operación típicamente utilizada en la construcción de operaciones de sincronización es el intercambio atómico, que intercambia lo valores de un registro y una posición de memoria.

Carrera de datos: Dos accesos a memoria producen una carrera de datos si provienen de dos hilos (threads) diferentes, los accesos son a la misma posición de memoria, al menos uno es una escritura y ocurren uno después del otro.

138

Capítulo 2

Instrucciones: el lenguaje del computador

Como ejemplo de utilización de esta operación para construir una primitiva básica de sincronización, se diseñará un bloqueo simple donde el valor 0 se usa para indicar que el acceso a la variable protegida está libre (bloqueo = 0) y 1 para indicar que no lo está (bloqueo = 1). Un procesador intenta poner el valor 1 en el bloqueo intercambiando un registro con el valor 1 y la posición de memoria correspondiente al bloqueo. Si algún otro procesador ya ha fijado el valor del bloqueo a 1, el intercambio devuelve el valor 1, y en caso contrario devuelve el valor 0. En este último caso además el bloqueo se pone a 1 para evitar que cualquier otro procesador pueda poner a 1 el bloqueo. Por ejemplo, consideremos que dos procesadores intentan hacer el intercambio simultáneamente; uno de los procesadores hará el intercambio en primer lugar, devolviendo un 0 como resultado, y cuando el segundo intenta hacer el intercambio devolverá como resultado un 1. La clave para que la primitiva de intercambio pueda ser usada para implementar la sincronización es que la operación es atómica: el intercambio es indivisible, y cuando se solicitan dos intercambios simultáneos el hardware se encarga de reordenarlos. Es imposible que, cuando dos procesadores intentan poner a 1 la variable de sincronización usando el intercambio, ambos crean que han puesto la variable a 1. La implementación de una operación de memoria atómica plantea algunos retos en el diseño del procesador, porque se necesita que tanto la lectura como la escritura sea una instrucción única e ininterrumpible. Un alternativa es disponer de una pareja de instrucciones en donde la segunda instrucción devuelve un valor que indica si la pareja fue ejecutada de forma atómica. La pareja de instrucciones es efectivamente atómica si todas las operaciones restantes ejecutadas por cualquier procesador se ha hecho antes o después de la ejecución de la pareja. De esta forma, cuando un pareja de instrucciones es efectivamente atómica, ningún otro procesador puede cambiar el valor entre las instrucciones de la pareja. En el MIPS esta pareja de instrucciones está formada por una instrucción de carga y almacenamiento especiales, carga enlazada (load linked) y almacenamiento condicional (store conditional), respectivamente. Estas instrucciones se utilizan en secuencia: el almacenamiento condicional falla si los contenidos de la posición de memoria especificada en la carga enlazada se cambian antes de la instrucción de almacenamiento condicional. El almacenamiento condicional guarda el valor de un registro en memoria y además pone el valor 1 en este registro si la operación termina con éxito, o el valor 0 si falla. Dado que la carga devuelve el valor inicial y el almacenamiento el valor 1 sólo si tiene éxito, la siguiente secuencia de instrucciones implementa un intercambio atómico en la posición de memoria especificada por el contenido de $s1: try: add $t0, $zer0, $s4 ll $t1, 0($s1) sc $t0, 0($s1) beq $t0, $zero, try add $s4, $zero, $t1

; ; ; ; ;

copia valor de intercambio carga enlazada almacenamiento condicional salto, almacenamiento fallido llevar valor cargado a $s4

El resultado de esta secuencia es un intercambio atómico entre los contenidos de $s4 y de la posición de memoria especificada por $s4. Si cualquier otro procesador interviene y modifica el valor de la memoria entre las instrucciones ll y sc, la instrucción sc devuelve el valor 0 en $t0 y la secuencia de código se ejecuta otra vez.

2.12

Traducción e inicio de un programa

139

Extensión: Aunque el intercambio atómico se ha presentado en un entorno de sincronización de multiprocesadores, es útil también para la sincronización, por parte del sistema operativo, de procesos en un sistema con un único procesador. Para asegurarse de que nada interfiere en el procesador, el almacenamiento condicional falla, también, si se produce un cambio de contexto del procesador entre las dos instrucciones (véase capítulo 5). Dado que el almacenamiento condicional falla si otro procesador intenta ejecutar una instrucción de almacenamiento con la dirección de la carga enlazada o si se produce una excepción, se deben elegir cuidadosamente qué instrucciones se insertan entre las dos instrucciones. En particular, solamente son seguras las instrucciones registro-registro; con cualquier otro tipo de instrucciones es posible que se creen situaciones de punto muerto (deadlock) y que el procesador no pueda completar nunca la instrucción sc debido a fallos de página repetitivos. Además, el número de instrucciones entre la carga enlazada y el almacenamiento condicional debe ser pequeño con el objetivo de minimizar la probabilidad de que, o bien un suceso no relacionado, o bien otro procesador hagan que el almacenamiento condicional falle frecuentemente. Una ventaja del mecanismo carga enlazada/almacenamiento condicional es que puede utilizarse para construir otras primitivas de sincronización, tales como comparación e intercambio atómico o búsqueda-e-incremento atómico, que se utilizan en algunos modelos de programación paralela. Estas primitivas tienen más instrucciones entre ll y sc.

¿Cuándo se utilizan primitivas como carga enlazada y almacenamiento condicional? Autoevaluación 1. Cuando los hilos cooperantes de un programa paralelo necesitan sincronizarse para leer y escribir datos compartidos correctamente 2. Cuando los procesos cooperantes de un monoprocesador necesitan sincronizarse para leer y escribir datos compartidos.

2.12

Traducción e inicio de un programa

2.12

Esta sección describe los cuatro pasos necesarios para transformar un programa en C que se encuentre en un fichero de un disco en un programa ejecutable en un computador. La figura 2.21 muestra la secuencia de traducción. Algunos sistemas combinan estos pasos para reducir el tiempo de la traducción, pero éstas son las cuatro fases lógicas que todos los programas necesitan. Esta sección sigue esta jerarquía de traducción.

Compilador El compilador transforma el programa en C en un programa en lenguaje ensamblador, una forma simbólica de lo que entiende la máquina. Los programas en lenguaje de alto nivel utilizan bastantes menos líneas de código que el lenguaje ensamblador, por lo que la productividad del programador es mucho más alta. En 1975, muchos sistemas operativos y ensambladores fueron escritos en lenguaje ensamblador porque las memorias eran pequeñas y los compiladores eran ineficientes. El aumento de 500 000 veces la capacidad de memoria por cada chip de DRAM ha reducido las preocupaciones por el tamaño del programa, y los actuales compiladores que optimizan pueden producir hoy programas en lenguaje ensamblador prácticamente tan buenos como los de un experto en lenguaje ensamblador, e incluso a veces mejores en el caso de programas grandes.

Lenguaje ensamblador: lenguaje simbólico que se puede traducir a binario.

140

Capítulo 2

Instrucciones: el lenguaje del computador

Programa C Compilador Programa en lenguaje ensamblador Ensamblador Objeto: módulo en lenguaje máquina Objeto: bibliotecas de rutinas (lenguaje máquina) Enlazador Ejecutable: programa en lenguaje máquina

Cargador Memoria FIGURA 2.21 Una jerarquía de traducción para C. Un programa en lenguaje de alto-nivel es primero compilado en un programa del lenguaje ensamblador y después se ensambla en un módulo objeto en lenguaje máquina. El enlazador (linker) combina los múltiples módulos con rutinas de la biblioteca para resolver todas las referencias. Luego el cargador pone el código máquina en las posiciones de memoria apropiadas para la ejecución por el procesador. Para acelerar el proceso de la traducción, algunos pasos se saltan o se combinan. Algunos compiladores producen módulos objeto directamente, y algunos sistemas utilizan cargadores enlazadores que realizan los dos últimos pasos. Para identificar el tipo de fichero, UNIX usa una convención de sufijos para los ficheros: los ficheros fuente de C se denominan x.c, y los ficheros en ensambador son x.s, los ficheros objeto se denominan x.o, las rutinas de la biblioteca enlazadas estáticamente son x.a, las rutas de la biblioteca dinámicamente enlazadas son x.so, y los ficheros ejecutables por defecto se llaman a.out. El MSDOS utiliza las extensiones.C, .ASM, .OBJ, .LIB, .DLL y .EXE para el mismo efecto.

Ensamblador

Pseudoinstrucciones: variación común de las instrucciones del lenguaje ensamblador, generalmente tratadas como si fueran instrucciones de pleno derecho.

Puesto que el lenguaje ensamblador es la interfaz para programas de alto nivel, el ensamblador puede también tratar variaciones comunes de las instrucciones del lenguaje máquina como si fueran instrucciones de pleno derecho. El hardware no necesita implementar estas instrucciones; sin embargo, su aparición en el lenguaje ensamblador simplifica la traducción y la programación. Tales instrucciones se llaman pseudoinstrucciones. Como se ha mencionado anteriormente, el hardware del MIPS asegura que el registro $zero tiene siempre el valor 0. Es decir, siempre que se utilice el registro $zero éste proporciona un 0, y el programador no puede cambiar este valor. El registro $zero se utiliza para crear la instrucción de lenguaje ensamblador move (mover) que copia el contenido de un registro a otro. Así, el ensamblador MIPS acepta esta instrucción aunque no se encuentre en la arquitectura MIPS: move $t0,$t1

# el registro $t0 toma el valor del # registro $t1

2.12

141

Traducción e inicio de un programa

El ensamblador convierte esta instrucción de lenguaje ensamblador en la instrucción del lenguaje máquina equivalente a la instrucción siguiente: add $t0,$zero,$t1

# el registro $t0 toma el valor 0 + # registro $t1

El ensamblador de MIPS también convierte blt (bifurcación en menor que, branch on less than) en las dos instrucciones slt y bne mencionadas en el ejemplo de la página 128. Otros ejemplos incluyen bgt, bge y ble. También convierte bifurcaciones a posiciones lejanas en una bifurcación o salto condicional y un salto incondicional. Tal y como hemos mencionado anteriormente, el ensamblador de MIPS puede incluso permitir constantes de 32 bits para ser cargadas en un registro, a pesar del límite de los 16 bits de las instrucciones inmediatas. Resumiendo, las pseudoinstrucciones dan a MIPS un repertorio de instrucciones de lenguaje ensamblador más rico que aquellas que implementa el hardware. El único coste es la reserva de un registro, $at, para ser usado por el ensamblador. Si se desea escribir programas en ensamblador es conveniente utilizar pseudoinstrucciones para simplificar la tarea. De todos modos, para comprender el hardware de MIPS y para estar seguro de obtener las mejores prestaciones será mejor estudiar las instrucciones MIPS reales que aparecen en las figuras 2.1 y 2.19. Los ensambladores también aceptan números en una gran variedad de bases. Además de binario y de decimal, aceptan una base que es más compacta que la binaria y más fácil de convertir en patrones de bits. Los ensambladores del MIPS utilizan la hexadecimal. Este tipo de características son oportunas, pero la principal tarea de un ensamblador es ensamblar en código máquina. El ensamblador convierte el programa en lenguaje ensamblador en un fichero objeto, que es una combinación de instrucciones en lenguaje máquina, datos e información necesaria para colocar las instrucciones correctamente en memoria. Para producir la versión binaria de cada instrucción del programa en lenguaje ensamblador, el ensamblador debe determinar las direcciones que corresponden a todas las etiquetas. Los ensambladores mantienen pistas de las etiquetas usadas en bifurcaciones o saltos condicionales y en instrucciones de transferencia de datos en una tabla de símbolos. Como es de esperar, la tabla contiene pares de símbolos y direcciones. El fichero objeto para sistemas UNIX contiene típicamente seis partes diferenciadas: ■

La cabecera del fichero objeto describe el tamaño y la posición de las otras partes del fichero objeto.



El segmento de texto contiene el código del lenguaje máquina.



El segmento de datos estáticos contiene los datos asignados para la duración del programa. (UNIX permite que los programas utilicen tanto datos estáticos, que son asignados a lo largo del programa, como datos dinámicos, que pueden crecer o contraerse según lo requerido por el programa. Véase figura 2.13.)



La información de reubicación identifica instrucciones y palabras de datos que dependen de direcciones absolutas cuando el programa se carga en memoria.

Tabla de símbolos: tabla que relaciona o empareja nombres de etiquetas con las direcciones de las palabras de la memoria que ocupan las instrucciones.

142

Capítulo 2

Instrucciones: el lenguaje del computador



La tabla de símbolos contiene las etiquetas restantes que no están definidas, como las referencias externas. ■ La información de depuración contiene una descripción concisa de cómo fueron compilados los módulos, para que el depurador pueda asociar las instrucciones máquina con ficheros fuente en C y hacer las estructuras de datos legibles. Las siguiente subsecciones muestran cómo utilizar aquellas rutinas que ya han sido ensambladas, por ejemplo las rutinas de biblioteca.

Enlazador (linker)

Enlazador (también llamado editor de enlaces o, a veces, montador): programa de sistemas que combina independientemente programas en lenguaje máquina ensamblados y que resuelve todas las etiquetas indefinidas en un fichero ejecutable.

Fichero ejecutable: programa funcional ejecutable en el formato de un fichero objeto que no contiene ninguna referencia no resuelta y sí contiene la información de la reubicación, la tabla de símbolos o la información para la depuración.

Lo que hemos presentado hasta ahora sugiere que un simple cambio en una línea de un procedimiento requiere compilar y ensamblar el programa completo. La retraducción completa es una pérdida terrible de recursos de computación. Esta repetición es particularmente costosa para las rutinas estándares de biblioteca, porque los programadores deberían compilar y ensamblar rutinas que por definición casi nunca cambian. Una alternativa es compilar y ensamblar cada procedimiento independientemente, de modo que un cambio en una línea implicara compilar y ensamblar solamente un procedimiento. Esta alternativa requiere un nuevo programa de sistemas, denominado enlazador o editor de enlaces (linker o link editor), que toma todos los programas en lenguaje máquina ensamblados independientemente y los une. Hay tres pasos que debe seguir el enlazador: 1. Colocar simbólicamente en memoria módulos de código y de datos. 2. Determinar las direcciones de los datos y de las etiquetas de las instrucciones. 3. Arreglar tanto las referencias internas como las externas. El enlazador utiliza la información de reubicación y la tabla de símbolos de cada módulo objeto para resolver todas las etiquetas indefinidas. Este tipo de referencias ocurren en instrucciones de bifurcaciones o salto condicional, instrucciones de salto incondicional y direcciones de datos. Por tanto, el trabajo de este programa se parece al de un editor: encuentra las direcciones antiguas y las sustituye por las nuevas direcciones. En inglés, el término link editor (editor de enlaces) o, abreviadamente, linker (enlazador) viene de editing. La razón por la que tiene sentido un enlazador es que es mucho más rápido unir código que volverlo a compilar y ensamblar. Si todas las referencias externas quedan resueltas, el enlazador determina a continuación las posiciones de memoria que ocupará cada módulo. La figura 2.13 en la página 120 muestra la convención MIPS para la asignación del programa y de los datos en memoria. Debido a que los ficheros se ensamblan aisladamente, el ensamblador no podría saber dónde se situarían las instrucciones y los datos de un módulo y cómo serían puestos en relación a otros módulos. Cuando el enlazador coloca un módulo en memoria todas las referencias absolutas, es decir, direcciones de memoria que no son relativas a un registro, se deben volver a reubicar para reflejar su localización verdadera. El enlazador produce un fichero ejecutable que se puede ejecutar en un computador. Típicamente este fichero tiene el mismo formato que un fichero objeto, salvo que no contiene ninguna referencia sin resolver. Es posible tener ficheros parcialmente enlazados (montados), tales como rutinas de biblioteca, que todavía contengan direcciones sin resolver y que por tanto aparezcan como ficheros objeto.

2.12

143

Traducción e inicio de un programa

Edición de enlaces de ficheros objeto

Enlazar los dos ficheros objeto de abajo. Mostrar las direcciones actualizadas de las primeras instrucciones del fichero ejecutable terminado. Mostramos las instrucciones en lenguaje ensamblador simplemente para hacer el ejemplo comprensible; en realidad las instrucciones serían números. Obsérvese que en los ficheros objeto hemos resaltado las direcciones y los símbolos que deben ser actualizados en el proceso de enlace: las instrucciones que se refieren a las direcciones de los procedimientos A y B y las instrucciones que se refieren a las direcciones de las palabras de datos X e Y. Cabecera de fichero objeto Nombre Tamaño texto Segmento de texto

20hex

Dirección

Instrucción

0

lw $a0, 0($gp) jal 0 … (X)



Información de reubicación

Tabla de símbolos

100hex

Tamaño datos

4 Segmento de datos

Procedimiento A

0 …



Dirección

Tipo de instrucción

Dependencia

0 4

lw jal

B

Etiqueta

Dirección

X



B



Nombre

Procedimiento B

Tamaño de texto

200hex

X

Cabecera de fichero objeto

Segmento de texto

Tamaño de datos

30hex

Dirección

Instrucción

0 …

sw $a1, 0($gp) jal 0 …

0

(Y)

4 Segmento de datos Información de reubicación

Tabla de símbolos





Dirección

Tipo de instrucción

Dependencia

0

sw

Y

4

jal

A

Etiqueta

Dirección

Y



A



EJEMPLO

144

RESPUESTA

Capítulo 2

Instrucciones: el lenguaje del computador

El procedimiento A necesita encontrar la dirección de la variable etiquetada como X para ponerla en la instrucción de carga y buscar la dirección del procedimiento B para colocarla en la instrucción jal. El procedimiento B necesita la dirección de la variable etiquetada como Y para la instrucción de almacenamiento y la dirección del procedimiento A para su instrucción jal. De la figura 2.13, en la página 120, sabemos que el segmento de texto empieza en la dirección 40 0000hex y el segmento de datos en 1000 0000hex. El texto del procedimiento A se coloca en la primera dirección y sus datos en la segunda. La cabecera del fichero objeto del procedimiento A indica que su texto son 100hex bytes y sus datos son 20hex bytes, así que la dirección de inicio para el texto del procedimiento B son 40 0100hex y sus datos empiezan a partir de 1000 0020hex. Cabecera del fichero ejecutable Tamaño del texto

300hex

Tamaño de los

50hex

datos Segmento de texto

Segmento de datos

Dirección

Instrucción

0040 0000hex

lw $a0, 8000hex($gp)

0040 0004hex

jal 40 0100hex





0040 0100hex

sw $a1, 8020hex($gp)

0040 0104hex

jal 40 0000hex





Dirección 1000 0000hex

(X)





1000 0020hex

(Y)

Ahora el enlazador actualiza los campos de dirección de las instrucciones. Éste utiliza el campo de tipo de instrucción para saber el formato de la dirección que debe ser editada. Vemos dos tipos aquí: 1. Las instrucciones jal son fáciles porque utilizan un modo de direccionamiento pseudodirecto. A la instrucción jal de la dirección 40 0004hex le ponemos 40 0100hex (la dirección del procedimiento B) en su campo de dirección, y a la instrucción jal de 40 0104hex le ponemos 40 0000hex (la dirección del procedimiento A) en su campo de dirección. 2. Las direcciones de carga y almacenamiento son más difíciles porque son relativas a un registro base. En este ejemplo utilizamos el puntero global como registro base. La figura 2.17 muestra que $gp está inicializado a 1000 8000hex . Para obtener la dirección 1000 0000hex (la dirección de la palabra X) colocamos 8000hex en el campo de dirección de lw en la dirección 40 0000hex . El capítulo 3 explica la aritmética del computador en complemento a dos para 16 bits, por la cual 8000hex en el campo de dirección produce 1000 0000hex como dirección. Similarmente, colocamos 8020hex en el campo de dirección de la instrucción sw en la dirección 40 0100hex para obtener la dirección 1000 0020hex (la dirección de la palabra Y).

2.12

Traducción e inicio de un programa

145

Extensión: Reiteramos que las instrucciones MIPS se alinean por palabra, así jal descarta los dos bits de la derecha para aumentar el rango de direcciones de la

instrucción. De este modo, utiliza 26 bits para formar una dirección de 28 bits. La dirección real en los 26 bits de la instrucción jal del ejemplo es 10 0040hex en lugar de 40 0100hex.

Cargador Una vez que el fichero ejecutable está en disco, el sistema operativo lo lee para pasarlo a la memoria y lo inicia. En los sistemas UNIX el cargador sigue estos pasos: 1. Lee la cabecera del fichero ejecutable para determinar el tamaño de los segmentos de texto y de datos. 2. Crea un espacio de direcciones suficientemente grande para el texto y los datos. 3. Copia las instrucciones y los datos del fichero ejecutable en memoria. 4. Copia los parámetros del programa principal (si los hay) sobre la pila. 5. Inicializa los registros de la máquina y actualiza el puntero de pila con la primera posición libre. 6. Salta a una rutina de inicio, que copia los parámetros en los registros de argumento y llama a la rutina principal del programa. Cuando la rutina principal retorna, la rutina de inicio (start-up) termina el programa con una llamada de sistema de salida (exit). Las secciones B.3 y B.4 del apéndice B describen enlazadores y cargadores más detalladamente.

Bibliotecas enlazadas (montadas) dinámicamente La primera parte de esta sección describe el enfoque tradicional para enlazar bibliotecas antes de que se ejecute el programa. Aunque este acercamiento estático es la manera más rápida para llamar rutinas de biblioteca, tiene algunas desventajas: ■

Las rutinas de biblioteca se convierten en parte del código ejecutable. Si se lanza una nueva versión de la biblioteca que arregla errores o que soporta nuevos dispositivos hardware, el programa estáticamente enlazado sigue usando la vieja versión.



Carga la biblioteca entera aunque no se utilice toda cuando el programa se ejecuta. La biblioteca puede ser grande con relación al programa; por ejemplo, la biblioteca estándar de C es de 2.5 MB.

Cargador: programa de sistema que coloca un programa objeto en la memoria principal de modo que esté listo para ejecutarse.

146

Capítulo 2

Bibliotecas enlazadas dinámicamente (DLL):

Estas desventajas nos conducen a las bibliotecas enlazadas (montadas) dinámicamente (DLLs), donde las rutinas de biblioteca no se enlazan ni se cargan hasta que el programa se ejecuta. Las rutinas de biblioteca y de programa albergan información extra sobre la posición de los procedimientos no locales y de sus nombres. En la versión inicial de DLLs, el cargador ejecutaba un enlazador dinámico, que usaba la información extra en el fichero para encontrar las bibliotecas apropiadas y actualizar todas las referencias externas. El inconveniente de la versión inicial de DLLs era que todavía enlazaba todas las rutinas de la biblioteca que pueden ser llamadas en lugar de enlazar sólo aquellas que se llaman durante la ejecución del programa. Esta observación condujo a la versión de las DLLs de procedimiento de enlazado tardío, donde cada rutina se enlaza (“linka”) solamente después de ser llamada. Como muchas veces ocurre en nuestro campo, este truco cuenta con un nivel de indirección. La figura 2.22 nos muestra la técnica. Comienza con las rutinas no locales que llaman a un sistema de rutinas simuladas (dummy) al final del programa, con una entrada por rutina no local. Cada una de estas entradas simuladas contiene un salto indirecto. La primera vez que se llama a la rutina de biblioteca, el programa llama a la entrada simulada y sigue el salto indirecto. Apunta al código que pone un número en un registro para identificar la rutina de biblioteca deseada y después salta al enlazador-cargador dinámico. El enlazador-cargador busca la rutina deseada, la recoloca (remap) y cambia la dirección en la posición del salto indirecto para apuntar a esa rutina. Entonces salta a ella. Cuando la rutina termina vuelve al sitio original del programa invocador o llamador. A partir de entonces, salta indirectamente a la rutina sin los saltos adicionales. Resumiendo, las DLLs requieren espacio extra para la información que necesitan para el enlace dinámico, pero no requieren copiar o enlazar bibliotecas enteras. Tienen mucha sobrecarga la primera vez que se llama a la rutina, pero después solamente deben ejecutar un único salto indirecto. Obsérvese que ningún retorno de biblioteca tiene sobrecarga extra. Windows de Microsoft confía extensamente en bibliotecas dinámicamente enlazadas tardías, y es también la manera normal de ejecutar programas sobre sistemas UNIX.

procedimientos de una biblioteca que se enlazan durante la ejecución del programa.

Instrucciones: el lenguaje del computador

Poner en marcha un programa Java La discusión anterior parte del modelo tradicional de ejecutar un programa, donde el énfasis se pone en la rapidez de ejecución para un programa destinado a una arquitectura del repertorio de instrucciones específica, o incluso a una implementación específica de dicha arquitectura. De hecho, es posible ejecutar los programas de Java de un modo parecido a los de C. Sin embargo, Java fue inventado con una serie de objetivos diferentes. Uno era poder ejecutar rápidamente sin incidentes en cualquier computador, aunque se ralentizara el tiempo de ejecución. La figura 2.23 muestra la traducción típica y los pasos de la ejecución para Java. Más que compilar el lenguaje ensamblador de un computador destino, Java primero compila instrucciones que son fáciles de interpretar: el repertorio de ins-

2.12

Traducción e inicio de un programa

147

Texto

Texto jal ...

jal ...

lw jr ...

lw jr ...

Datos

Datos

Texto ... li j ...

ID

Texto Enlazador/cargador dinámico Rutina DLL recolocada

j

...

Datos/texto Rutina DLL

... jr

(a) Primera llamada a la rutina DLL

Texto Rutina DLL

... jr

(b) Siguiente llamada a la ruina DLL

FIGURA 2.22 Biblioteca enlazada (montada) dinámicamente mediante un procedimiento de enlazado tardío. (a) Pasos para la primera vez que se llama a la rutina DLL. (b) Los pasos para buscar la rutina, recolocarla y enlazarla se saltan en las llamadas siguientes. Como veremos en el capítulo 5, el sistema operativo puede evitar copiar la rutina deseada si la recoloca usando el gestor de memoria virtual.

trucciones Java bytecode (véase sección 2.15 en el CD). Este repertorio de instrucciones se diseña para que sea cercano al lenguaje Java de modo que este paso de la compilación sea trivial. No se realiza prácticamente ninguna optimización. Como el compilador de C, el compilador de Java comprueba los tipos de datos y produce la operación apropiada para cada uno. Los programas de Java se distribuyen en la versión binaria de estos bytecodes. Un software intérprete, llamado máquina virtual de Java (Java Virtual Machine, JVM), puede ejecutar los Java bytecodes. Un intérprete es un programa que simula una arquitectura del repertorio de instrucciones. Por ejemplo, el simulador de MIPS usado con este libro es un intérprete. No hay necesidad de un paso de ensamblaje aparte, puesto que la traducción es tan simple que el compilador rellena las direcciones o la JVM las busca durante la ejecución.

Java bytecode: instrucciones de un repertorio de instrucciones diseñado para interpretar los programas de Java. Máquina virtual de Java: programa que interpreta los Java bytecodes.

148

Capítulo 2

Instrucciones: el lenguaje del computador

Programa Java Compilador Fichero de clases (Java bytecode) Rutinas de biblioteca de Java (lenguaje máquina) Compilador Just-In-Time

Máquina virtual de Java

Métodos Java compilados (lenguaje máquina)

FIGURA 2.23 Una secuencia de traducción para Java. Un programa Java se compila primero en una versión binaria de código Java, con todas las direcciones definidas por el compilador. El programa Java está entonces listo para ejecutarse en un software intérprete, llamado máquina virtual de Java (JVM). La JVM enlaza con los métodos deseados en la biblioteca de Java mientras el programa se está ejecutando. Para lograr mejores prestaciones, la JVM puede invocar el compilador Just–In-Time (JIT), el cual compila selectivamente métodos en el lenguaje máquina nativo de la máquina en la que se está ejecutando.

Compiladores Just-InTime: nombre dado

normalmente a un compilador que funciona durante el tiempo de ejecución, traduciendo los segmentos de código interpretados al código nativo del computador.

Lo bueno de la interpretación directa es la portabilidad. La disponibilidad del software de la máquina virtual de Java significó que muchos podrían escribir y ejecutar programas Java poco después que Java fuera dado a conocer. Las máquinas virtuales de Java se encuentran hoy en millones de dispositivos de todo tipo, desde los teléfonos móviles a los buscadores de Internet. La desventaja de esta traducción es que se obtienen unas prestaciones menores. La increíble mejora de las prestaciones en las décadas de 1980 y 1990 hicieron la interpretación viable para muchos usos importantes, pero la disminución de las prestaciones en un factor de 10 cuando eran comparados con los programas de C tradicionalmente compilados hicieron a Java poco atractivo para algunas aplicaciones. Para preservar la portabilidad y mejorar la velocidad de ejecución, la siguiente fase del desarrollo de Java fue lograr que los compiladores tradujeran mientras el programa funcionara. Tales compiladores Just-In-Time (JIT) hacen el perfil del programa ejecutado para buscar donde están los puntos “calientes” y los compilan en el repertorio de instrucciones nativo en el que la máquina virtual está funcionando. La parte compilada se guarda para la próxima vez que se ejecute el programa, de modo que pueda funcionar más rápido cada vez que se ejecute. Este reequilibrio entre traducción y compilación se desarrolla con el tiempo, de modo que los programas Java que se ejecutan frecuentemente sufren poco retraso debido a la interpretación. A medida que los computadores son más rápidos y los compiladores pueden hacer más, y a medida que los investigadores inventan mejores modos de compilar Java improvisadamente, las diferencias en prestaciones entre Java y C o C++ van disminuyendo. La sección 2.15 en el CD trata con una profundidad mucho mayor la implementación de compiladores Java, de los Java bytecodes, de JVM y de JIT.

2.13

Un ejemplo de ordenamiento en C para verlo todo junto

149

¿Cuál de las ventajas de un intérprete sobre un traductor cree usted que fue más Autoevaluación importante para los diseñadores de Java? 1. Facilidad para escribir un intérprete. 2. Mensajes de error mejores. 3. Un código de objeto más pequeño. 4. Independencia de la máquina.

2.13

Un ejemplo de ordenamiento en C para verlo todo junto

2.13

Uno de los riesgos de mostrar fragmentos de código del lenguaje ensamblador es que no tenemos ninguna idea de cómo es un programa completo en lenguaje ensamblador. En esta sección deducimos el código MIPS a partir de dos procedimientos escritos en C: uno para intercambiar elementos de una tabla y otro para ordenarlos. void swap(int v[], int k) { int temp; temp = v[k]; v[k] = v[k+1]; v[k+1] = temp; } FIGURA 2.24 Procedimiento C que intercambia dos posiciones en memoria. La subsección siguiente utiliza este procedimiento en un ejemplo de ordenación (sort).

El procedimiento intercambio (swap) Comencemos con el código para el procedimiento de intercambio (swap) de la figura 2.24. Este procedimiento intercambia simplemente dos posiciones de memoria. Al traducir de C al lenguaje ensamblador a mano seguimos estos pasos generales: 1. Asignar los registros a las variables del programa. 2. Producir el código para el cuerpo del procedimiento. 3. Preservar los registros a lo largo de la invocación del procedimiento. Esta sección describe el procedimiento de intercambio (swap) en estas tres partes, y concluye juntándolas. Asignación de registro para el intercambio (swap)

Según lo mencionado en la página 112-113, la convención MIPS para el paso de parámetros es utilizar los registros $a0, $a1, $a2 y $a3. Puesto que el intercambio tiene sólo dos parámetros, v y k, éstos se situarán en los registros $a0 y $a1.

150

Capítulo 2

Instrucciones: el lenguaje del computador

Únicamente se necesita otra variable, temp, que asociamos al registro $t0 puesto que el intercambio es un procedimiento hoja (véase la página 116). Esta asignación de registros se corresponde con las declaraciones de variables de la primera parte del procedimiento de intercambio de la figura 2.24. Código para el cuerpo del procedimiento de intercambio (swap)

Las líneas restantes del código C del intercambio son: temp = v[k]; v[k] = v[k+1]; v[k+1] = temp;

Recordemos que la dirección de memoria para MIPS se refiere a la dirección del byte, y por tanto que las palabras están realmente separadas por 4 bytes. Por tanto necesitamos multiplicar el índice k por 4 antes de sumarle la dirección. Olvidar que las direcciones de palabras secuenciales difieren de 4 en vez de 1 es un error común en la programación en lenguaje ensamblador. Por tanto, el primer paso es conseguir la dirección de v[k] multiplicando k por 4; mediante un desplazamiento a la izquierda de 2 posiciones. sll add

$t1, $a1,2 # reg $t1 = k * 4 $t1, $a0,$t1 # reg $t1 = v + (k * 4) # reg $t1 tiene la dirección de v[k]

Ahora cargamos v[k] utilizando $t1, y luego v[k+1] añadiendo 4 a $t1: lw lw

$t0, 0($t1) $t2, 4($t1)

# reg $t0 (temp) = v[k] # reg $t2 = v[k + 1] # se refiere al siguiente elemento de v

Luego almacenamos $t0 y $t2 en las direcciones intercambiadas: sw sw

$t2, 0($t1) $t0, 4($t1)

# v[k] = reg $t2 # v[k+1] = reg $t0 (temp)

Ya hemos asignado los registros y hemos escrito el código para realizar las operaciones del procedimiento. Lo único que falta es el código para preservar los registros guardados usados dentro del intercambio. Puesto que no estamos utilizando los registros guardados en este procedimiento hoja, no hay nada que preservar. El procedimiento completo de intercambio (swap)

Ahora estamos listos para la rutina completa, que incluye la etiqueta del procedimiento y el retorno del salto. Para hacerlo más fácil de seguir, identificamos en la figura 2.25 cada bloque del código con su propósito en el procedimiento.

El procedimiento de ordenación (sort) Para asegurar que apreciamos el rigor de la programación en lenguaje ensamblador escogemos un segundo ejemplo más largo. En este caso, construiremos una rutina que llame al procedimiento de intercambio. Este programa ordena una tabla de números enteros usando la burbuja o la ordenación por intercambio, que

2.13

Un ejemplo de ordenamiento en C para verlo todo junto

Cuerpo del procedimiento swap: sll add

$t1, $a1, 2 $t1, $a0, $t1

lw lw

$t0, 0($t1) $t2, 4($t1)

sw sw

$t2, 0($t1) $t0, 4($t1)

# reg $t1 = k * 4 # reg $t1 = v + (k * 4) # reg $t1 t1 tiene la dirección de v[k] # reg $t0 (temp) = v[k] # reg $t2 = v[k + 1] # referencia al siguiente elemento de v # v[k] = reg $t2 # v[k+1] = reg $t0 (temp)

Retorno del procedimiento jr

FIGURA 2.25 figura 2.24.

$ra

# retorna a la rutina que lo llamó

Código ensamblador MIPS del procedimiento intercambio (swap) en la

es una de las más simples aunque no la ordenación más rápida. La figura 2.26 muestra la versión C del programa. De nuevo, presentamos este procedimiento en varios pasos y concluimos con el procedimiento completo.

void sort (int v[], int n) { int i, j; for (i = 0; i < n; i += 1) { for (j = i – 1; j >= 0 && v[j] > v[j + 1]; j -= 1) { swap(v,j); } } } FIGURA 2.26

Procedimiento C que realiza la ordenación (sort) de una tabla v.

Asignación de registros para la ordenación (sort)

Los dos parámetros del procedimiento de ordenación (sort), v y n, están en los registros $a0 y $a1 y asignamos el registro $s0 a i y el registro $s1 a j. Código del cuerpo del procedimiento de ordenación (sort)

El cuerpo del procedimiento consiste en dos lazos anidados for y una llamada al procedimiento de intercambio (swap) que incluye parámetros. El código se descompone del exterior hacia dentro. El primer paso de la traducción es el primer lazo for: for (i = 0; i < n; i += 1) {

Recordemos que la sentencia for de C tiene tres partes: inicialización, comprobación del lazo e incremento de iteración. Solo hace falta una instrucción para inicializar i a 0, la primera parte de la sentencia for: move

$s0, $zero

# i = 0

151

152

Capítulo 2

Instrucciones: el lenguaje del computador

(Recuérdese también que move es una pseudoinstrucción proporcionada por el ensamblador para ayudar al programador del lenguaje ensamblador; véase la página 141). De igual modo, solo se necesita instrucción incrementar i, la última parte de la sentencia for: addi

$s0, $s0, 1 # i += 1

El lazo debería finalizar si i < n no se cumple o, dicho de otra manera, debería salir si i v n. La instrucción activar si menor que activa el registro $t0 a 1 si $s0 < $a1, y a 0 en cualquier otro caso. Puesto que deseamos probar si $s0 v $a1, bifurcamos si el registro $t0 es 0. Esta prueba necesita dos instrucciones: for1tst:slt $t0, $s0, $a1 # reg $t0 = 0 si $s0 v $a1 (ivn) beq $t0, $zero,exit1 # ir a exit1 si $s0v$a1 (ivn)

La parte final del lazo salta hacia atrás hacia la prueba de lazo: j

for1tst

# salta para probar el lazo exterior

exit1:

El esquema del código del primer lazo for es por tanto: move $s0, $zero # i = 0 for1tst:slt $t0, $s0, $a1 # reg $t0 = 0 si $s0 v $a1 (ivn) beq $t0, $zero,exit1 # ir a exit1 si $s0v$a1 (ivn) ... (cuerpo del primer lazo for) ... addi $s0, $s0, 1 # i += 1 j for1tst # salta para probar el lazo exterior exit1:

¡Voilà! (En los ejercicios se explora la escritura de un código más rápido para lazos similares). El segundo lazo for se parece a esto en C: for (j = i – 1; j >= 0 && v[j] > v[j + 1]; j -= 1) {

La parte de inicialización de este lazo es de nuevo una instrucción: addi

$s1, $s0, –1 # j = i – 1

El decremento de j al final del lazo es también una instrucción: addi

$s1, $s1, –1 # j -= 1

La prueba del lazo tiene dos partes. Salimos del lazo si cualquier condición falla, por tanto, el primer test debe salir del lazo si falla (j < 0): for2tst:slti$t0, $s1, 0 # reg $t0 = 1 si $s1 < 0 (j < 0) bne $t0, $zero, exit2 # ir a exit2 si $s1<0 (j < 0)

Esta bifurcación pasará por alto el segundo test de condición. Si no lo salta, entonces j v 0.

2.13

Un ejemplo de ordenamiento en C para verlo todo junto

La segunda prueba sale del lazo si v[j] > v[j + 1] no es verdad, o bien si v[j] f v[j + 1]. Primero creamos la dirección multiplicando j por 4 (puesto que necesitamos una dirección del byte) y la sumamos la dirección base de v: sll add

$t1, $s1,2 # reg $t1 = j * 4 $t2, $a0,$t1 # reg $t2 = v + (j * 4)

Ahora cargamos v[j]: lw

$t3, 0($t2) # reg $t3

= v[j]

Puesto que sabemos que el segundo elemento es justo la palabra siguiente, sumamos 4 a la dirección en el registro $t2 para conseguir v[j + 1]: lw

$t4, 4($t2) # reg $t4

= v[j + 1]

El test de v[j] f v[j + 1] es igual que el v[j + 1] v v[j]. Por tanto, las dos instrucciones de la prueba de salida son: slt beq

$t0, $t4, $t3 $t0, $zero,exit2

# reg $t0 = 0 si $t4 v $t3 # ir a exit2 si $t4 v $t3

La parte final del lazo salta de nuevo atrás hacia el test del lazo interno: j

for2tst

# salta al test del lazo interno

Combinando las piezas conjuntamente, el esquema del segundo lazo for aparece como: addi for2tst: slti bne sll add lw lw slt beq

$s1, $s0, –1 # j = i – 1 $t0, $s1, 0 # reg $t0 = 1 si $s1 < 0 (j<0) $t0, $zero,exit2 # ir a exit2 si $s1<0 (j<0) $t1, $s1,2 # reg $t1 = j * 4 $t2, $a0,$t1 # reg $t2 = v + (j * 4) $t3, 0($t2) # reg $t3 = v[j] $t4, 4($t2) # reg $t4 = v[j + 1] $t0, $t4, $t3 # reg $t0 = 0 si $t4 v $t3 $t0, $zero,exit2 # ir a exit2 si $t4 v $t3 ... (Cuerpo del segundo lazo for) ... addi $s1, $s1, –1 # j -= 1 j for2tst # salta al test del lazo interno

exit2:

La llamada de procedimiento en ordenación (sort)

El siguiente paso es el cuerpo del segundo lazo for: swap(v,j);

La llamada al procedimiento de intercambio (swap) es bastante fácil: jal

swap

153

154

Capítulo 2

Instrucciones: el lenguaje del computador

El paso de parámetros en ordenación (sort)

El problema aparece cuando deseamos pasar parámetros porque el procedimiento de ordenación necesita los valores en los registros $a0 y $a1 y el procedimiento de intercambio necesita tener sus parámetros puestos en esos mismos registros. Una solución es copiar los parámetros para la ordenación (sort) en otros registros al principio del procedimiento, de modo que los registros $a0 y $a1 estén disponibles para la llamada del intercambio (swap). (Esta copia es más rápida que guardar y restaurar de la pila). Primero se copia $a0 y $a1 en $s2 y $s3 durante el procedimiento: move move

$s2, $a0 $s3, $a1

# copia el parámetro $a0 en $s2 # copia el parámetrr $a1 en $s3

Luego se pasan los parámetros al procedimiento de intercambio (swap) con las dos instrucciones siguientes move

$a0, $s2

move

$a1, $s1

# # # #

el primer parámetro de intercambio (swap) es v el segundo parámetro de intercambio (swap) es j

Preservación de registros en ordenación (sort)

El único código que nos falta tratar es guardar y restaurar los registros. Claramente, debemos guardar la dirección de retorno en el registro $ra, puesto que la ordenación (sort) es un procedimiento y es llamado como tal. El procedimiento de ordenación también utiliza los registros guardados $s0, $s1, $s2 y $s3, así que deben ser guardados. El prólogo del procedimiento de ordenación es el siguiente: addi sw sw sw sw sw

$sp,$sp,–20 $ra,16($sp) $s3,12($sp) $s2, 8($sp) $s1, 4($sp) $s0, 0($sp)

# # # # # #

reserva guardar guardar guardar guardar guardar

espacio en la pila para 5 reg. $ra en la pila $s3 en la pila $s2 en la pila $s1 en la pila $s0 en la pila

El final del procedimiento invierte simplemente todas estas instrucciones, y después añade un jr para retornar. El procedimiento de ordenación (sort) completo

Ahora ponemos todas las piezas juntas en la figura 2.27, teniendo cuidado de reemplazar las referencias a los registros $a0 y $a1 en los lazos for por referencias a los registros $s2 y $s3. De nuevo, para hacer el código más fácil de seguir, identificamos cada bloque del código con su propósito en el procedimiento. En este ejemplo, 9 líneas del procedimiento de ordenación (sort) en C se convirtieron en 35 líneas de lenguaje ensamblador MIPS.

2.13

Un ejemplo de ordenamiento en C para verlo todo junto

155

Extensión: Una optimización que funcionaría con este ejemplo es el procedimiento en línea (inlining). En vez de pasar argumentos en parámetros y de invocar el código con una instrucción jal, el compilador copiaría el código del cuerpo del procedimiento de intercambio (swap) donde la llamada al procedimiento de intercambio apareciera en el código. La inclusión (inlining) evitaría cuatro instrucciones en este ejemplo. La desventaja de la optimización por inclusión (inlining) es que el código compilado sería más grande si el procedimiento en línea se llamara desde distintos lugares. Este tipo de extensión del código podría suponer menores prestaciones si incrementara la tasa de fallos de la memoria caché; véase el capítulo 5. Guardar registros sort:

addi sw sw sw sw sw

$sp,$sp, –20 $ra, 16($sp) $s3,12($sp) $s2, 8($sp) $s1, 4($sp) $s0, 0($sp)

# reserva espacio en la pila para 5 registros # guardar $ra en la pila # guardar $s3 en la pila # guardar $s2 en la pila # guardar $s1 en la pila # guardar $s0 en la pila

Cuerpo del procedimiento Copiar parámetros Lazo exterior

Lazo interior

move move move for1tst: slt beq addi for2tst: slti bne sll add lw lw slt beq move

Paso de parámetros y llamada Lazo interior Lazo exterior

exit2:

move jal addi j addi j

$s2, $a0 $s3, $a1 $s0, $zero $t0, $s0, $s3 $t0, $zero, exit1 $s1, $s0, –1 $t0, $s1, 0 $t0, $zero, exit2 $t1, $s1, 2 $t2, $s2, $t1 $t3, 0($t2) $t4, 4($t2) $t0, $t4, $t3 $t0, $zero, exit2 $a0, $s2

# copia el parámetro $a0 en $s2 (g uardar $a0) # copia el parámetro $a1 en $s3 (guardar $a1) #i=0 # reg $t0 = 0 si $s0 v $s3 (i v n) # ir a exit1 si $s0 v $s3 (i v n) #j=i–1 # reg $t0 = 1 si $s1 < 0 (j < 0) # ir a exit2 si $s1 < 0 (j < 0) # reg $t1 = j * 4 # reg $t2 = v + (j * 4) # reg $t3 = v[j] # reg $t4 = v[j + 1] # reg $t0 = 0 si $t4 v $t3 # ir a exit2 si $t4 v $t3 # el primer parámetro de intercambio (swap) es v # (antiguo $a0) # el segundo parámetro de intercambio (swap) es j # swap código mostrado en la figura 2.25 # j -= 1 # salta al test del lazo interior

$a1, $s1 swap $s1, $s1, –1 for2tst # i += 1 s0, $s0, 1 for1tst # salta al test del lazo exterior

Restauración de registros exit1:lw lw lw lw lw addi

$s0, 0($sp) $s1, 4($sp) $s2, 8($sp) $s3,12($sp) $ra,16($sp) $sp,$sp, 20

# recupera $s0 de la pila # recupera $s1 de la pila # recupera $s2 de la pila # recupera $s3 de la pila # recupera $ra de la pila # recupera el puntero de la pila

Retorno del procedimiento jr

FIGURA 2.27

$ra

# retorno a la rutina llamadora

Versión ensamblada MIPS del procedimiento de ordenación (sort) de la figura 2.26.

156

Capítulo 2

Instrucciones: el lenguaje del computador

Comprender La figura 2.28 muestra el impacto de la optimización del compilador en las prestalas prestaciones ciones, en el tiempo de compilación, en los ciclos de reloj, en el recuento de insde los programas trucciones y en el CPI del procedimiento de ordenación. Obsérvese que el código

no optimizado tiene mejor CPI y la optimización O1 tiene menor número de instrucciones, pero O3 es la más rápida, y nos recuerda que el tiempo es la única medida exacta de las prestaciones del programa. La figura 2.29 compara el impacto de los lenguajes de programación, compilación frente a interpretación, y de los algoritmos en las prestaciones de los procedimientos de ordenación. La cuarta columna muestra que el programa de C no optimizado es 8.3 veces más rápido que el código de Java interpretado para la ordenación por burbuja (bubble sort). El uso del compilador Just-In-Time de Java hace que el código Java sea 2.1 veces más rápido que el C no optimizado, y alrededor de un 1.13 más que el código de optimización de C más alto. (La sección 2.15 en el CD da más detalles de la interpretación versus la compilación de Java y del código para la ordenación por burbuja Java y MIPS). Los ratios no están tan cerca para Quicksort en la columna 5, probablemente porque es más difícil amortizar el coste de la compilación en tiempo de ejecución (runtime) en el tiempo de ejecución más corto. La última columna muestra el efecto de un algoritmo mejor, que proporciona un incremento de las prestaciones de tres órdenes de magnitud para una ordenación de 100 000 elementos. Incluso comparando el Java interpretado en la columna 5 con el compilador de C en la optimización de grado más alto en la columna 4, Quicksort gana a la ordenación por burbuja (bubble sort) por un factor de 50 (0.05 × 2468, o 123 contra 2.41).

Extensión: Los compiladores MIPS siempre reservan espacio en la pila para los argumentos en el caso de que necesiten ser almacenados, así que en realidad siempre disminuyen $sp en 16 para reservar espacio para los cuatro registros de argumentos (16 bytes). Una razón es que C proporciona una opción vararg que permite a un puntero tomar, por ejemplo, el tercer argumento de un procedimiento. Cuando el compilador encuentra el poco frecuente vararg, copia los cuatro registros de argumentos en la pila en las cuatro posiciones reservadas.

Optimización gcc

Prestaciones relativas

Ciclos de reloj (millones)

Número de instrucciones (millones)

CPI

ninguna

1.00

158 615

114 938

1.38

O1 (media)

2.37

66 990

37 470

1.79

O2 (completa)

2.38

66 521

39 993

1.66

O3 (integración del

2.41

65 747

44 993

1.46

procedimiento)

FIGURA 2.28 Comparación de las prestaciones, del número de instrucciones y del CPI usando las optimizaciones del compilador para la ordenación por el método de la burbuja (bubble sort). Los programas ordenaron 100 000 palabras con la tabla iniciada con valores al azar. Esos programas fueron ejecutados en un Pentium 4 con una frecuencia de reloj de 3.06 GHz y un bus del sistema de 533 MHz con 2 GB de memoria SDRAM PC2100 DDR. Se utilizó la versión 2.4.20 de Linux.

2.14

Lenguaje C

Java

Método de ejecución compilador compilador compilador compilador intérprete compilador Jus-In-Time

Prestaciones relativas Optimización Ordenación por burbuja ninguna O1 O2 O3 — —

1.00 2.37 2.38 2.41 0.12 2.13

157

Tablas frente a punteros

Prestaciones relativas Quicksort

Ventaja de Quicksort frente a la ordenación por burbuja

1.00 1.50 1.50 1.91 0.05 0.29

2468 1562 1555 1955 1050 338

FIGURA 2.29 Prestaciones de dos algoritmos de ordenación en C y en Java usando interpretación y optimización de compiladores en relación con la versión de C no optimizada. La última columna muestra la ventaja en velocidad de Quicksort sobre la ordenación por burbuja para cada lenguaje y opción de ejecución. Estos programas fueron ejecutados en el mismo sistema que el utilizado en la figura 2.28. La JVM es la versión 1.3.1 de Sun, y el JIT es la versión 1.3.1 de Hotspot de Sun.

2.14

Tablas frente a punteros

2.14

Un desafío típico para cualquier programador novel es comprender los punteros. La comparación del código ensamblador que utiliza tablas y los índices a tablas con el código ensamblador que utiliza punteros ofrece elementos de comprensión de los punteros. Esta sección muestra las versiones de dos procedimientos en C y en lenguaje ensamblador MIPS para inicializar una secuencia de palabras en memoria: una utilizando índices de tablas y otra utilizando punteros. La figura 2.30 muestra los dos procedimientos en C. El propósito de esta sección es mostrar cómo se corresponden los punteros con instrucciones MIPS, y no promover un estilo de programación anticuado. Veremos el impacto de la optimización de un compilador moderno en estos dos procedimientos al final de la sección.

Versión de iniciar (clear) con tablas Comencemos con la versión con tablas, clear1, centrándonos en el cuerpo del lazo e ignorando el código de enlace de procedimientos. Suponemos que los dos parámetros tabla y tamaño se encuentran en los registros $a0 y $a1, y que i está asignado al registro $t0. La iniciación de i, la primera parte del lazo for, es directa: move

$t0,$zero

# i = 0 (registro $t0 = 0)

Para iniciar tabla[i] a 0, primero debemos conseguir su dirección. Se comienza multiplicando i por 4 para conseguir la dirección del byte: loop1:sll

$t1,$t0,2

# $t1 = i * 4

Puesto que la dirección inicial de la tabla está en un registro, debemos añadirlo al índice para conseguir la dirección de tabla[i] usando una instrucción de suma: add

$t2,$a0,$t1

# $t2 = dirección de tabla[i]

158

Capítulo 2

Instrucciones: el lenguaje del computador

Finalmente, podemos almacenar 0 en esa dirección: clear1(int tabla[], int tamaño) { int i; for (i = 0; i < tamaño; i += 1) tabla[i] = 0; } clear2(int *tabla, int tamaño) { int *p; for (p = &tabla[0]; p < &tabla[tamaño]; p = p + 1) *p = 0; } FIGURA 2.30 Dos procedimientos de C para iniciar una tabla a ceros. Clear1 utiliza índices, mientras que clear2 utiliza punteros. El segundo procedimiento necesita una cierta explicación para los que no están familiarizados con C. La dirección de una variable se indica con &, y la referencia al objeto apuntado por un puntero se indica con *. Las sentencias dicen que tabla y p son punteros a números enteros. La primera parte del lazo for en clear2 asigna la dirección del primer elemento de tabla al puntero p. La segunda parte del lazo for comprueba si el puntero está señalando más allá del último elemento de tabla. El incremento de un puntero en uno, en la última parte secuencial del lazo for, significa mover el puntero al siguiente objeto secuencial teniendo en cuenta su tamaño declarado. Puesto que p es un puntero a números enteros, el compilador generará instrucciones MIPS para incrementar p en cuatro, el número de bytes de un número entero MIPS. La asignación en el lazo coloca un 0 en el objeto señalado por p.

sw

$zero, 0($t2)

# tabla[i] = 0

Esta instrucción está al final del cuerpo del lazo. Por tanto, el paso siguiente es incrementar i: addi

$t0,$t0,1

# i = i + 1

El test del lazo comprueba si i es menor que tamaño: slt bne

$t3,$t0,$a1 $t3,$zero,loop1

# $t3 = (i < tamaño) # si (i < tamaño) ir a loop1

Ahora hemos visto todas las piezas del procedimiento. Éste es el código MIPS para inicializar una tabla usando índices: move loop1:sll add sw addi slt bne

$t0,$zero $t1,$t0,2 $t2,$a0,$t1 $zero, 0($t2) $t0,$t0,1 $t3,$t0,$a1 $t3,$zero,loop1

# # # # # # #

i = 0 $t1 = i * 4 $t2 = dirección de tabla[i] tabla[i] = 0 i = i + 1 $t3 = (i < tamaño) si (i < tamaño) ir a loop1

(Este código es válido mientras tamaño sea mayor que 0; ANSI C requiere comprobar el tamaño antes del lazo, pero aquí omitiremos esta cuestión.)

2.14

Tablas frente a punteros

Versión de iniciar (clear) con punteros El segundo procedimiento, que utiliza punteros, asigna los dos parámetros tabla y tamaño a los registros $a0 y $a1 y coloca p en el registro $t0. El código para el segundo procedimiento comienza asignando el indicador p a la dirección del primer elemento de la tabla: move

$t0,$a0

# p = dirección de tabla[0]

El código siguiente es el cuerpo del lazo for, que almacena simplemente 0 en la posición p: loop2:sw

$zero,0($t0)

# Memoria[p] = 0

Esta instrucción implementa el cuerpo del lazo, así que el código siguiente es el incremento de la iteración, que cambia p para apuntar a la palabra siguiente: addi

$t0,$t0,4

# p = p + 4

El incremento de un puntero en 1 significa mover el puntero al siguiente objeto secuencial en C. Puesto que p es un puntero a números enteros, que utilizan 4 bytes, el compilador incrementa p en 4. El test del lazo es el siguiente. El primer paso es calcular la dirección del último elemento de tabla. Se comienza multiplicando tamaño por 4 para conseguir su dirección byte: sll

$t1,$a1,2

# $t1 = tamaño * 4

y entonces sumamos el producto a la dirección inicial de la tabla para conseguir la dirección de la primera palabra después de la tabla: add

$t2,$a0,$t1

# $t2 = direccion de tabla[tamaño]

El test del lazo es simplemente comprobar si p es menor que el último elemento de la tabla: slt bne

$t3,$t0,$t2 $t3,$zero,loop2

# $t3 = (p<&tabla[tamaño]) # si (p<&tabla[tamaño]) ir a loop2

Con todas las piezas terminadas, podemos mostrar una versión completa del código con punteros para inicializar una tabla a cero: move $t0,$a0 loop2:sw$zero,0($t0) addi $t0,$t0,4 add $t1,$a1,$a1 add $t1,$t1,$t1 add $t2,$a0,$t1 slt $t3,$t0,$t2 bne $t3,$zero,loop2

# # # # # # # #

p = dirección de tabla[0] Memoria[p] = 0 p = p + 4 $t1 = tamaño * 2 $t1 = tamaño * 4 $t2 = dirección de tabla[tamaño] $t3 = (p<&tabla[tamaño]) si (p<&tabla[tamaño]) ir a loop2

Como en el primer ejemplo, este código supone que tamaño es mayor que 0.

159

160

Capítulo 2

Instrucciones: el lenguaje del computador

Obsérvese que este programa calcula la dirección del final de la tabla en cada iteración del lazo, a pesar de que no cambia. Una versión más rápida del código traslada este cálculo fuera del lazo: move sll add loop2:sw addi slt bne

$t0,$a0 $t1,$a1,2 $t2,$a0,$t1 $zero,0($t0) $t0,$t0,4 $t3,$t0,$t2 $t3,$zero,loop2

# # # # # # #

p = direcciones de tabla[0] $t1 = tamaño * 4 $t2 = dirección de tabla[tamaño] Memoria[p] = 0 p = p + 4 $t3 = (p<&tabla[tamaño]) si (p<&tabla[tamaño]) ir a loop2

Comparación de las dos versiones de iniciar a cero (clear) Comparar las dos secuencias de código, una al lado de la otra, ilustra la diferencia entre índices de tablas y de punteros (se resaltan los cambios introducidos por la versión con punteros):

move $t0,$zero

# i = 0

move

$t0,$a0

# p = & tabla[0]

$t1,$t0,2

# $t1 = i * 4

sll

$t1,$a1,2

# $t1 = tamaño * 4

add

$t2,$a0,$t1

# $t2 = &tabla[i]

add

$t2,$a0,$t1

# $t2 = &tabla[tamaño]

sw

$zero, 0($t2) # tabla[i] = 0

$zero,0($t0)

# Memoria[p] = 0

loop1: sll

loop2: sw

addi $t0,$t0,1

# i = i + 1

addi

$t0,$t0,4

# p = p + 4

slt

$t3,$t0,$a1

# $t3 = (i < tamaño)

slt

$t3,$t0,$t2

# $t3=(p<&tabla[tamaño])

bne

$t3,$zero,loop1# si () ir a loop1

bne

$t3,$zero,loop2# si () ir a loop2

La versión de la izquierda debe tener la “multiplicación” y la suma dentro del lazo porque se incrementa i y cada dirección se debe recalcular para el nuevo índice; la versión con punteros a memoria de la derecha incrementa el puntero p directamente. La versión con punteros mueve estas operaciones fuera del lazo, reduciendo así las instrucciones ejecutadas por iteración de 6 a 4. Esta optimización manual corresponde a la optimización del compilador de la reducción de la intensidad (desplazar en vez de multiplicar) y de la eliminación de variables de inducción (que elimina cálculos de dirección de la tabla dentro de lazos). La sección 2.15 en el CD describe estas dos optimizaciones y muchas otras.

Extensión: El compilador de C añadiría una prueba para estar seguro de que tamaño es mayor que 0. Una forma sería añadir un salto justo antes de la instrucción de la primera instrucción del lazo hacia la instrucción slt.

2.16

Caso real: instrucciones ARM

161

Se solía enseñar a utilizar punteros en C para conseguir mayor eficiencia que la dispo- Comprender nible con tablas: “Utilicen los punteros, aunque no puedan comprender el código”. las prestaciones Los compiladores de optimización modernos pueden producir código igual de bueno de los programas para la versión de tablas. La mayoría de los programadores prefieren hoy que el compilador haga la parte pesada.

2.15

2.15

Perspectiva histórica y lecturas recomendadas

2.15

En esta sección se da una visión general de como trabaja el compilador de C y de como se ejecuta java. La comprensión de la tecnología de los compiladores es crítica para entender las prestaciones de un computador, puesto que el compilador afecta de forma significativa a las prestaciones. Téngase en cuenta que la construcción de un compilador es materia para un curso de uno o dos semestres, por lo tanto, nuestra introducción no puede ir más allá de los aspectos básicos. La segunda parte de esta sección está destinada a aquellos lectores interesados en conocer como se ejecuta un lenguaje orientado a objetos, como Java, en una arquitectura MIPS. Muestra las instrucciones de Java usadas para la interpretación y los códigos MIPS para la versión Java de algunos de segmentos en C de secciones anteriores, incluyendo el Ordenamiento de Burbujas. Cubre la máquina virtual de Java y los compiladores JIT. El resto de esta sección está en el CD.

2.16

Caso real: instrucciones ARM

2.16

La arquitectura del conjunto de instrucciones para dispositivos empotrados más popular es ARM, con más de tres mil millones de dispositivos por año utilizando esta arquitectura. Desarrollada originalmente para el procesador Acorn RISC Machine, que después se llamó Advanced RISC Machine, ARM se lanzó el mismo año que MIPS y siguen filosofías similares. La figura 2.31 muestra estas similitudes. La principal diferencia entre MIPS y ARM es que MIPS tiene más registros y ARM tiene más modos de direccionamiento. El núcleo de instrucciones aritmético-lógicas y de transferencia de datos es muy similar en MIPS y en ARM, como se muestra en la figura 2.32. Modos de direccionamiento

Los modos de direccionamiento de datos soportados por ARM se muestran en la figura 2.33. A diferencia de lo que ocurre con el MIPS, en ARM no hay un registro reservado con el valor 0. En ARM hay 9 modos de direccionamiento de datos

Lenguaje orientado a objetos: lenguaje de programación que está orientado a los objetos en vez de a las acciones. O dicho de otra forma, datos frente a lógica.

162

Capítulo 2

Instrucciones: el lenguaje del computador

Fecha de lanzamiento Tamaño de la instrucción (bits) Espacio de direcciones (tamaño, modelo) Alineamiento de datos Modos de direccionamiento Registros de enteros (número, modelo, tamaño) E/S

FIGURA 2.31

ARM

MIPS

1985 32 32 bits, plano Alineado 9 15 GPR x 32 bits

1985 32 32 bits, plano Alineado 3 31 GPR x 32 bits

Proyectada (mapped) en memoria

Proyectada (mapped) en memoria

Similitudes entre los conjuntos de instrucciones de ARM y MIPS. Nombre de la instrucción

Registro-registro

Transferencia de datos

ARM

MIPS

Suma

add

Suma (excepción si desbordamiento)

adds; swivs

addu, addlu add

Resta

sub

subu

Resta (excepción si desbordamiento)

subs; swivs

sub

Multiplicación

mul

mult, multu

División



div, divu

And

and

and

Or

orr

or

Or exclusiva

eor

xor

Carga parte más significativa del registro

_

lui

Desplazamiento lógico a la izquierda

lsl1

sllv, sll

Desplazamiento lógico a la derecha

lsr1

srlv, srl

Desplazamiento aritmético a la derecha

asr1

srav, sra

Comparación

cmp, cmn, tst, teq

slt/i, slt/iu

Carga de un byte con signo

ldrsb

lb

Carga de un byte sin signo

ldrb

lbu

Carga de media palabra con signo

ldrsh

lh

Carga de media palabra sin signo

ldrh

lhu

Carga de una palabra

ldr

lw

Almacenamiento de un byte

strb

sb

Almacenamiento de media palabra

strh

sh

Almacenamiento de una palabra

str

sw

Lectura, escritura de registros especiales

mrs, msr

move

Intercambio atómico

swp, swpb

ll;sc

FIGURA 2.32 Instrucciones registro-registro y de transferencia de datos de ARM equivalentes a instrucciones MIPS. El guión significa que la operación no está disponible en esa arquitectura o que ha sido sintetizada en una pocas instrucciones. Si hay varias instrucciones que pueden reemplazar al núcleo MIPS, éstas se separan por comas. ARM incluye desplazamientos como parte de cualquier instrucción de operación de datos, por lo tanto, los desplazamientos con superíndice 1 son simplemente una variación de una instrucciones de transferencia, por ejemplo lsr1. Observa que ARM no tiene instrucciones de división.

(véase figura 2.18), algunos de los cuales incluyen cálculos bastante complejos, mientras que en MIPS hay sólo tres. Por ejemplo, uno de los modos de direccionamiento de ARM puede hacer un desplazamiento de cualquier cantidad del contenido de un registro, sumarlo al contenido de otro registro para formar la dirección y actualizar un registro con esta dirección.

2.16

Modo de direccionamiento

Caso real: instrucciones ARM

ARM v.4

MIPS

Operando en registro

X

X

Operando inmediato

X

X

Registro + desplazamiento (desplazamiento o basado)

X

X

Registro + registro (indexado)

X



Registro + registro escalado (escalado)

X



Registro + desplazamiento y actualización de registro

X



Registro + registro y actualización de registro

X



Autoincremento, autodecremento

X



Dato relativo al PC

X



FIGURA 2.33 Resumen de modos de direccionamiento de datos. ARM tiene modos de direccionamiento indirecto a registro y registro + desplazamiento diferenciados, en lugar de poner un desplazamiento igual a 0 en este último. Para disponer de un mayor rango de direccionamiento, ARM desplaza el desplazamiento a la izquierda 1 o 2 bits cuando el tamaño del dato es media palabra o una palabra.

Comparación y salto condicional

En MIPS se utiliza el contenido de registros para la evaluación de los saltos condicionales. Por el contrario, en ARM se utilizan los tradicionales cuatro bits de código de condición almacenados en la palabra de estado: negativo, cero, acarreo (carry) y desbordamiento (overflow). Estos bits pueden ser activados con cualquier instrucción aritmética o lógica y, a diferencia de arquitecturas anteriores, esta activación es opcional para cada instrucción. Esta selección explícita hace que la implementación segmentada tenga menos problemas. En los saltos condicionales de ARM se utilizan los códigos de condición para determinar todas la posibles relaciones con y sin signo. La instrucción CMP resta dos operandos y el resultado determina el valor de los bits del código de condición. La instrucción Compara negativo (CMN) suma dos operandos y el resultado determina el valor del código de condición. La instrucción TST hace el AND lógico entre los dos operandos y pone el valor de los bits del código de condición excepto del bit de desbordamiento; de modo similar, TEQ realiza la operación OR exclusiva y fija el valor de los tres primeros bits del código de condición. Las instrucciones de ARM incorporan una característica poco usual: todas instrucciones tienen la opción de ejecutarse condicionalmente dependiendo de los bits del código de condición. Cada instrucción comienza con un campo de cuatro bits que indica, dependiendo del código de condición, si actúa como una instrucción de no operación (nop) o como una instrucción real. De este modo, los saltos condicionales pueden ser considerados como una ejecución condicional de una instrucción de salto incondicional. La ejecución condicional permite eliminar las instrucciones de salto que se introducen para saltar únicamente una instrucción; es más eficiente desde el punto de vista del tamaño del código y tiempo de ejecución simplemente ejecutar condicionalmente una instrucción. Los formatos de las instrucciones ARM y MIPS se muestran en la figura 2.34. Las principales diferencias son que cada instrucción ARM incorpora un campo de ejecución condicional de 4 bits y que el campo de registro es más reducido, porque ARM tiene la mitad de los registros de MIPS.

163

164

Capítulo 2

Instrucciones: el lenguaje del computador

31

Registro-a-registro

28 27 8

26 25 6

Op

31

28 27

Op

Salto condicional

28 27 Opx

4

0

0

5

Const

16

0

21 20

24

16 15

0

Opx5/Rs25

Const16

24 23

0

4

Const24

Op

31

0 Opx6

Const12

Const

Rs15

28 27 4

Opx

12 11

24 23

26 25

31

6 5 5

16 15 Rd

0 4

Rs2

Const

Rd4

4

Op6

ARM

8

11 10 5

Op

31 MIPS

Salto incondicional/ llamada a procedimiento MIPS

Rs1

4 3 Opx

16 15

21 20 5

4

Rd

Rs14

26 25

31

5

20 19 8

6

ARM

Rs2

Op

31 MIPS

5

12 11 Rd

16 15

21 20 Rs1

Opx4

ARM

16 15 4

Rs1

Op

31 MIPS

Transferencia de datos

20 19

Opx4

ARM

0

26 25 6

Op

Const

Código de operación

Registro

26

Constante

FIGURA 2.34 Formatos de instrucciones de ARM y MIPS. Las diferencias están en el número de registros de la arquitectura, 16 o 32.

Características únicas de ARM

Las instrucciones aritmético-lógicas de ARM que no tienen equivalente en MIPS se indican en la figura 2.35. Como ARM no tiene un registro especial con el valor 0, dispone de códigos de operación diferenciados para algunas operaciones que en MIPS se pueden hacer con $zero. Además, en ARM se dispone de soporte para aritmética multipalabra. El campo inmediato de 12 bits de ARM tiene una interpretación novedosa. Los ocho bits menos significativos se extienden con ceros hasta 32 bits, y después se rotan a la derecha el número de bits especificado en los primeros cuatro bits del campo multiplicado por dos. La ventaja de este esquema es que se pueden representar todas las potencias de dos en una palabra de 32 bits. Sería interesante analizar si esta partición realmente nos permite representar más valores inmediatos que un campo convencional de 12 bits. El desplazamiento de operandos no está limitado únicamente a los inmediatos. Se dispone de la opción de desplazar el contenido del segundo registro de todas las operaciones aritméticas y lógicas antes de realizar la operación. Los desplazamientos pueden ser desplazamiento lógico a la izquierda, desplazamiento lógico a la derecha, desplazamiento aritmético a la derecha y rotación a la derecha.

2.17

Nombre

Definición

ARM v.4

MIPS

Carga inmediata

Rd = Imm

mov

addi, $0,

Negación

Rd = ~(Rs1)

mvn

nor, $0,

Mover

Rd = Rs1

mov

or, $0,

Rotación a la derecha

Rd = Rs i>> i Rd0...i-1 = Rs31-i...31

ror

And negado

Rd = Rs1 & ~(Rs2)

blc

Resta negada

Rd = Rs2 - Rs1

rsb, rsc

Apoyo para suma entera multipalabra

CarryOut, Rd = Rd + Rs1 + OldCarryOut

adcs

_

Apoyo para resta entera multipalabra

CarryOut, Rd = Rd - Rs1 + OldCarryOut

sbcs

_

FIGURA 2.35

165

Casos reales: instrucciones x86

Instrucciones aritméticas y lógicas de ARM no disponibles en MIPS.

En ARM también se dispone de instrucciones para almacenar grupos de registros, llamados cargas y almacenamiento por bloques (block load, block store). Utilizando como control una máscara de 16 bits incluida en la instrucción, cualesquiera de los 16 registros pueden cargarse o almacenarse en memoria en una única instrucción. Estas instrucciones puede utilizarse para salvar y restaurar registros en las llamadas y retornos de un procedimiento. Sin embargo, el uso actual más importante de estas instrucciones es la copia de bloques de memoria.

2.17

La belleza está en los ojos del que mira.

Casos reales: instrucciones x86

2.17

Los diseñadores del repertorio de instrucciones proporcionan a veces operaciones más potentes que las que aparecen en ARM o MIPS. La meta es generalmente reducir el número de instrucciones ejecutadas por un programa. El peligro está en que esta reducción puede hacerse a costa de la simplicidad, y se aumenta el tiempo que tardan los programas en ejecutarse porque las instrucciones son más lentas. Esta lentitud puede ser el resultado de un ciclo de reloj más lento o de que se requiere más ciclos de reloj que una secuencia más simple. El camino hacia la mayor complejidad de las operaciones está así plagado de peligros. Para evitar estos problemas los diseñadores se han decantado por instrucciones más simples. La sección 2.18 muestra las trampas de la complejidad.

Evolución del Intel x86 ARM y MIPS fueron el resultado de la visión que tuvieron en 1985 un pequeño grupo de personas; las piezas de esta arquitectura encajan fácilmente unas con otras y la arquitectura entera se puede describir de forma sucinta. Pero este no es el caso del x86; éste es el producto de varios grupos independientes que desarrollaron la arquitectura a lo largo de casi 30 años, agregando nuevas características al repertorio de instrucciones original como el que añade ropa a una maleta. Aquí están los hitos más importantes del x86:

Margaret Wolfe Hungerford, Molly Bawn, 1877

166

Capítulo 2



Registro de propósito general (GPR): registro que se puede utilizar para las direcciones o para los datos con virtualmente cualquier instrucción.















Instrucciones: el lenguaje del computador

1978: La arquitectura del Intel 8086 fue anunciada como una extensión compatible en lenguaje ensamblador del exitoso Intel 8080, un microprocesador de 8 bits. El 8086 es una arquitectura de 16 bits, con todos los registros internos de un tamaño de 16 bits. A diferencia de MIPS, los registros tienen usos específicos, y por tanto los 8086 no está considerados una arquitectura de registro de propósito general. 1980: Se anuncia el coprocesador de punto flotante Intel 8087. Esta arquitectura amplía los 8086 con cerca de 60 instrucciones de punto flotante. En lugar de usar los registros, se utiliza la pila (véase la sección 2.20 en el CD y la sección 3.7). 1982: El 80286 amplía la arquitectura 8086 aumentando el espacio de direcciones a 24 bits, creando una distribución de memoria y un modelo de protección elaborado (véase el capítulo 5) y agregando algunas instrucciones más para completar el repertorio de instrucciones y para manipular el modelo de protección. 1985: Los 80386 ampliaron la arquitectura 80286 a 32 bits. Además de la arquitectura de 32 bits con registros de 32 bits y un espacio de dirección de 32 bits, el 80386 añade nuevos modos de direccionamiento y nuevas operaciones. Las instrucciones añadidas hacen de los 80386 casi una máquina de registros de propósito general. El 80386 también añade soporte de paginación además del direccionamiento segmentado (véase el capítulo 5). Como los 80286, los 80386 tienen un modo para ejecutar programas 8086 sin hacer ningún cambio. 1989-95: Los subsiguientes 80486 de 1989, el Pentium en 1992 y el Pentium Pro en 1995 tenían el propósito de aumentar las prestaciones, con solamente cuatro instrucciones añadidas al repertorio de instrucciones visible por el usuario: tres para ayudar con el multiproceso (capítulo 7) y una instrucción de movimiento condicional. 1997: Después de lanzar el Pentium y el Pentium Pro, Intel anunció que ampliaría las arquitecturas Pentium y Pentium Pro con MMX (eXtensiones Multi Media). Este nuevo repertorio de 57 instrucciones utilizaba la pila del punto flotante para acelerar aplicaciones multimedia y de comunicaciones. Las instrucciones MMX funcionan típicamente con múltiples datos pequeños a la vez, siguiendo la tradición de las arquitecturas de flujo único de instrucciones-múltiples flujos de datos (SIMD) (véase el capítulo 7). El Pentium II no introdujo ninguna instrucción nueva. 1999: Intel añadió otras 70 instrucciones, etiquetadas SSE (extensiones de flujo SIMD, Streaming SIMD Extensions) como parte del Pentium III. Los principales cambios eran añadir ocho registros separados, doblar su anchura a 128 bits y agregar un tipo de datos de punto flotante de precisión simple. Por tanto, cuatro operaciones de punto flotante de 32 bits podían realizarse en paralelo. Para mejorar las prestaciones de la memoria, SSE incluía instrucciones de prebúsqueda de cache más las instrucciones de almacenamiento de flujo que puentean las cache y escriben directamente en la memoria. 2001: Intel agregó otras 144 instrucciones, esta vez etiquetadas SSE2. Este nuevo tipo de datos es de aritmética de doble precisión, que permite pares de operaciones de punto flotante de 64-bit en paralelo. Casi todas estas

2.17











Casos reales: instrucciones x86

144 instrucciones eran versiones de las instrucciones existentes de MMX y de SSE que funcionan en 64 bits de datos en paralelo. Este cambio no sólo permitía más operaciones multimedia, también daba al compilador un objetivo diferente para las operaciones de punto flotante que la arquitectura de pila única. Los compiladores podían elegir utilizar los ocho registros de SSE como registros de punto flotante del tipo de los encontrados en otros computadores. Este cambio estimuló las prestaciones del punto flotante en el Pentium 4, el primer microprocesador en incluir las instrucciones SSE2. 2003: Otra compañía además de Intel aumentó la arquitectura x86. AMD anunció un conjunto de extensiones arquitectónicas para aumentar el espacio de dirección de 32 bits a 64. De forma similar a la transición del espacio de dirección de 16 a 32 bits en 1985 con los 80386, AMD64 amplía todos los registros a 64 bits. También aumenta el número de registros a 16 y el número de registros de 128 bits SSE a 16. El cambio principal del ISA (arquitectura del repertorio de instrucciones) viene de añadir un nuevo modo llamado el modo largo (long mode), que redefine la ejecución de todas las instrucciones x86 con direcciones y datos de 64 bits. Para abordar este número más grande de registros, agrega un nuevo prefijo a las instrucciones. Dependiendo de cómo contemos, el modo largo también añade de 4 a 10 nuevas instrucciones y quita unas 27 viejas. El direccionamiento de datos relativo al PC es otra extensión. AMD64 todavía tiene un modo que es idéntico a x86 (modo de herencia, legacy mode) más un modo que restringe que el usuario programe en x86 pero que permite que los sistemas operativos utilicen AMD64 (modo de compatibilidad). Estos modos permiten una transición más agradable al direccionamiento de 64 bits que la arquitectura de HP/Intel IA-64. 2004: Intel capitula y adopta AMD64, y la reetiqueta Tecnología de Memoria Extendida 64 (EM64T). La diferencia principal es que Intel añadió una instrucción atómica de 128 bits de comparación e intercambio que probablemente se debía haber incluido en AMD64. Al mismo tiempo, Intel anuncia otra generación de las extensiones multimedia. SSE3 añade 13 instrucciones para soportar aritmética compleja, operaciones con gráficos en tablas de estructuras, codificación vídeo, conversión punto flotante y la sincronización de hilos (thread) (véase la sección 2.11). AMD ofrecerá SSE3 en los próximos chips y agregará a AMD64 la mayoría de las instrucciones y la instrucción atómica que falta del intercambio para mantener compatibilidad binaria con Intel. 2006: Intel añade otras 54 instrucciones, como extensión de su repertorio de instrucciones y lo llama SSE4. Esta extensión incluye variaciones como la suma de diferencias absolutas, producto escalar para tablas de estructuras, extensión de signo o extensión con ceros de datos con pocos bits a anchos mayores, etc. Se añadió también apoyo para máquinas virtuales (véase capítulo 5) 2007: AMD anuncia 170 nuevas instrucciones que forman parte de la extensión SSE5. Se incluyen 46 instrucciones del repertorio de instrucciones básico pero con tres operandos, como en MIPS. 2008: Intel anuncia el Advanced Vector Extension, que expande el ancho del registro SSE de 128 a 256 bits, redefine 250 instrucciones y añade 128 instrucciones nuevas.

167

168

Capítulo 2

Instrucciones: el lenguaje del computador

Esta historia ilustra el impacto de la “esposas de oro” de la compatibilidad en el x86, pues la base del software existente en cada paso era demasiado importante para hacerla peligrar con cambios significativos en la arquitectura. Si se analiza la vida del x86, veremos que de media se ha añadido ¡una instrucción cada mes! A pesar de los fallos artísticos del x86, tengamos presente que hay más copias de esta familia arquitectónica en computadores de sobremesa que de cualquier otra arquitectura, con un aumento de 250 millones por año. Sin embargo, estos altibajos han conducido a una arquitectura que es difícil de explicar e imposible de apreciar. ¡Prepárese para lo que está a punto de ver! No intente leer esta sección con el cuidado que necesitaría para escribir los programas x86, pues la intención es familiarizarnos con las ventajas e inconvenientes de la arquitectura de computadores de sobremesa más popular del mundo. Más que mostrar entero el repertorio de instrucciones de 16 y de 32 bits, en esta sección nos centramos en el subconjunto de instrucciones de 32 bits que se originaron con los 80386, pues es esta parte de la arquitectura la que se usa en la actualidad. Comenzamos nuestra explicación con los registros y los modos de direccionamiento, seguimos por las operaciones de números enteros y concluimos con un examen de la codificación de las instrucciones.

Registros y modos de direccionamiento x86

Los registros del 80386 muestran la evolución del repertorio de instrucciones (figura 2.36). Los 80386 ampliaron todos los registros de 16 bits a 32 bits (excepto los registros de segmento) y añadieron el prefijo E a su nombre para distinguir la versión de 32 bits. Nos referiremos a ellos genéricamente como GPRs (registros de propósito general). El 80386 contiene solamente ocho GPRs. Esto significa que los programas MIPS y ARM pueden utilizar hasta cuatro veces y dos veces más registros, respectivamente. Las instrucciones aritméticas, lógicas y de transferencia de datos son instrucciones de dos operandos que permiten las combinaciones mostradas en la figura 2.37. Hay dos diferencias importantes aquí. Las instrucciones aritméticas y lógicas del x86 deben tener un operando que actúe a la vez como fuente y destino; ARM y MIPS permiten registros separados para fuente y destino. Esta restricción añade más presión a los limitados registros, puesto que un registro fuente puede ser modificado. La segunda diferencia importante es que uno de los operandos puede estar en memoria. Así, virtualmente cualquier instrucción puede tener un operando en memoria, a diferencia de MIPS y de ARM. Los modos de direccionamiento a memoria, descritos detalladamente más adelante, ofrecen dos tamaños de direcciones en las instrucciones. Éstos, que podemos llamar también desplazamientos, pueden ser de 8 o de 32 bits. Aunque un operando de memoria puede utilizar cualquier modo de direccionamiento, hay restricciones sobre los registros que se pueden utilizar en un modo. La figura 2.38 muestra los modos de direccionamiento del x86 y qué GPRs no se pueden utilizar con ese modo, además de cómo se obtendría el mismo efecto usando instrucciones MIPS.

2.17

Nombre

Casos reales: instrucciones x86

Uso 31

0

EAX

GPR 0

ECX

GPR 1

EDX

GPR 2

EBX

GPR 3

ESP

GPR 4

EBP

GPR 5

ESI

GPR 6

EDI

GPR 7 CS

Puntero de segmento de código

SS

Puntero de segmento de pila (parte superior de la pila)

DS

Puntero de segmento de datos 0

ES

Puntero de segmento de datos 1

FS

Puntero de segmento de datos 2

GS

Puntero de segmento de datos 3

EIP

Puntero de instrucción (PC)

EFLAGS

Códigos de condición

FIGURA 2.36 Conjunto de registros del 80386. A partir del 80386, los 8 registros de arriba fueron extendidos a 32 bits y se pudieron usar como registros de propósito general.

Tipos de operando fuente/destino

Segundo operando fuente

Registro

Registro

Registro

Immediato

Registro

Memoria

Memoria

Registro

Memoria

Immediato

FIGURA 2.37 Tipos de instrucciones para las instrucciones aritméticas, lógicas y de transferencia de datos. El x86 permite las combinaciones mostradas. La única restricción es la ausencia de un modo memoria-memoria. Los immediatos pueden ser de 8, 16 o 32 bits de longitud; un registro es cualquiera de los 14 registros principales de la figura 2.40 (no EIP o EFLAGS).

169

170

Capítulo 2

Modo Registro indirecto Modo base con desplazamiento de 8 o de 32 bits Base más índice escalado

Instrucciones: el lenguaje del computador

Descripción

Restricciones de registros

La dirección está en un registro

ni ESP ni EBP

La dirección es un registro base más un desplazamiento La dirección es Base + (2escala × Índice)

Base más índice escalado con desplazamiento de 8 o de 32 bits

donde escala tiene el valor 0, 1, 2 o 3. La dirección es Base + (2escala × Índice) + desplazamiento

donde escala tiene el valor 0, 1, 2 o 3.

Equivalente MIPS lw $s0,0($s1)

lw $s0,100($s1) # f16-bit # desplazamiento Base: cualquier GPR mul $t0,$s2,4 Índice: no ESP add $t0,$t0,$s1 lw $s0,0($t0) Base: cualquier GPR mul $t0,$s2,4 Índice: no ESP add $t0,$t0,$s1 lw $s0,100($t0) # f16-bit # desplazamiento ni ESP ni EBP

FIGURA 2.38 Modos de direccionamiento de 32 bits del x86 con restricciones de registros y con el código equivalente MIPS. El modo de direccionamiento base más índice escalado no se encuentra en MIPS o en ARM, se incluye para evitar la multiplicación por 4 (factor de escalar de 2) para convertir un índice guardado en un registro en una dirección byte (véase las figuras 2.25 y 2.27). Para datos de 16 bits se utiliza un factor de escala de 1, y para 64 bits de 3. El factor escala 0 significa que la dirección no está escalada. Si el desplazamiento es más largo de 16 bits en el segundo o el cuarto modo, entonces el modo equivalente de MIPS necesitaría dos instrucciones más: una lui para cargar los 16 bits de mayor peso del desplazamiento y una add para sumar la parte superior de la dirección con el registro base $s1. (Intel da dos nombres diferentes a lo que se llama modo de direccionamiento base –base e indexado–, pero son esencialmente idénticos y aquí los combinamos).

Operaciones de enteros x86 Los 8086 proporcionan soporte para tipos de datos de 8 bits (byte) y de 16 bits (palabra). Los 80386 agregan direcciones y datos de 32 bits (doble palabra) en el x86. (AMD64 añade direcciones y datos de 64 bits, llamados palabras cuadrúples, quad word; en esta sección seguiremos con el 80386.) Los distintos tipos de datos se aplican a las operaciones con registro, así como a los accesos a memoria. Casi todas las operaciones trabajan tanto con datos de 8 bits como con un tamaño de datos mayor. Este tamaño se determina por el modo y es de 16 o de 32 bits. Algunos programas desean claramente funcionar con datos de los tres tamaños, así que los arquitectos del 80386 proporcionan una manera conveniente de especificar cada caso sin tener que extender el tamaño del código significativamente. Decidieron que en mucho programas dominarían los datos de 16 o de 32 bits, y por tanto parecía tener sentido poder fijar un tamaño grande por defecto. Este tamaño de datos por defecto es fijado mediante un bit en el registro de segmento de código. Para invalidar el tamaño de los datos por defecto se concatena a la instrucción un prefijo de 8 bits para decir a la máquina que debe utilizar el otro tamaño grande para esta instrucción. La solución del prefijo se tomó prestada de los 8086, que permiten múltiples prefijos para modificar el comportamiento de la instrucción. Los tres prefijos originales invalidan el registro del segmento por defecto, bloquean el bus para soportar sincronización (véase la sección 2.11), o repiten la instrucción siguiente hasta que el registro ECX decrementa hasta 0. Este último prefijo fue pensado para ser emparejado con una instrucción de movimiento de byte, para mover un número variable de bytes. Los 80386 también añadieron un prefijo para invalidar el tamaño de la dirección por defecto. Las operaciones de enteros x86 se pueden dividir en cuatro clases importantes: 1. Instrucciones de movimiento de datos, que incluyen mover (move), introducir (push) y sacar (pop). 2. Instrucciones de aritméticas y lógicas, que incluyen operaciones aritméticas de test, número entero y decimal.

2.17

Casos reales: instrucciones x86

3. Instrucciones para controlar el flujo, que incluyen bifurcaciones o saltos condicionales, saltos incondicionales, llamadas y retornos. 4. Instrucciones de cadenas, que incluyen movimiento y comparación de cadenas. Las dos primeras categorías no son relevantes, no tienen nada especial, salvo que las instrucciones de operaciones aritméticas y lógicas permiten que el destino sea tanto un registro como una posición de memoria. La figura 2.39 muestra algunas instrucciones típicas x86 y sus funciones. Las bifurcaciones o saltos condicionales en el x86 se basan en códigos de condición o indicadores (flags). Los códigos de condición se fijan como un efecto secundario de una operación; la mayoría se utilizan para comparar el valor de un resultado con 0. Luego las bifurcaciones o saltos condicionales comprueban los códigos de condición. Las direcciones de bifurcaciones o saltos condicionales relativas al PC se deben especificar en bytes, dado que, a diferencia de ARM y MIPS, las instrucciones 80386 no son todas de 4 bytes de longitud. Las instrucciones de manipulación de cadenas son parte del 8080 anterior al x86 y no se ejecutan en la mayoría de los programas. Son a menudo más lentas que rutinas equivalentes del software (véase la falacia en la página 174). La figura 2.40 enumera algunas de las instrucciones x86 de enteros. Muchas de las instrucciones están disponibles tanto en el formato byte como en el formato palabra. Instrucción

Función

JE name

si igual(código de condición) {EIP=nombre}; EIP–128 f nombre < EIP+128

JMP name

EIP=nombre

CALL name

SP=SP–4; M[SP]=EIP+5; EIP=nombre;

MOVW EBX,[EDI+45]

EBX=M[EDI+45]

PUSH ESI

SP=SP–4; M[SP]=ESI

POP EDI

EDI=M[SP]; SP=SP+4

ADD EAX,#6765

EAX= EAX+6765

TEST EDX,#42

Actualiza los códigos de condición (flags) con EDX y 42

movsl

M[EDI]=M[ESI]; EDI=EDI+4; ESI=ESI+4

FIGURA 2.39 Algunas instrucciones típicas x86 y sus funciones. En la figura 2.40 aparece una lista de operaciones frecuentes. La instrucción CALL guarda el EIP de la siguiente instrucción en la pila. (EIP es el PC de Intel).

Codificación de las instrucciones en x86 Hemos guardado lo peor para el final: la codificación de instrucciones en los 80836 es compleja, con muchos formatos de instrucción diversos. Las instrucciones del 80386 pueden variar a partir de 1 byte, cuando no hay operandos, hasta 15 bytes. La figura 2.41 muestra el formato de instrucciones para varias de las instrucciones del ejemplo de la figura 2.39. El byte del código de operación generalmente contiene un bit que indica si el operando es de 8 bits o de 32 bits. Para algunas instrucciones, el código de operación puede incluir el modo de direccionamiento y el registro; esto es cierto en muchas instrucciones que tienen la forma “registro =

171

172

Capítulo 2

Instrucciones: el lenguaje del computador

Instrucción

Significado

Control

Saltos condicionales e incondicionales

JNZ, JZ

Salta si se cumple la condición a EIP + 8 bits de desplazamiento; JNE (por JNZ) y JE (por JZ) son nombres alternativos

JMP

Salto incondicional–desplazamiento de 8 bits o de 16 bits

CALL

Llamada a subrutina–desplazamiento de 16 bits; deja la dirección de retorno en la pila

RET

Saca la dirección de retorno de la pila y salta a ella

LOOP

Salto del lazo–decremento de ECX; salta a EIP + 8 bits de desplazamiento si ECX | 0

Transferencia de datos

Mueven datos entre registros o entre registros y memoria

MOV

Mueven datos entre dos registros o entre registros y memoria

PUSH, POP

Push: guarda el operando fuente en la pila; pop: saca el operando de la parte alta de la pila y lo pone en un registro

LES

Carga ES y unos de los registros GPRs de memoria

Aritmética, lógica Operaciones aritméticas y lógicas utilizando registros y memoria ADD, SUB

Suma la fuente al destino; resta la fuente del destino; formato registro-memoria

CMP

Compara fuente y destino; formato registro-memoria

SHL, SHR, RCR

Desplazamiento a la izquierda; desplazamiento lógico a la derecha; rota a la derecha dando entrada al código de condición de acarreo

CBW

Convierte el byte de los 8 bits más a la derecha de EAX en una palabra de 16 bits a la derecha de EAX

TEST

AND lógico de fuente y destino, activa los códigos de condición

INC, DEC

Incrementa el destino, decrementa el destino

OR, XOR

OR lógico; OR exclusivo; formato registro-memoria

Cadenas

Mueven datos entre operandos tipo cadena, con la longitud dada por un prefijo de repetición

MOVS

Copia la cadena desde la fuente al destino incrementando ESI y EDI; se puede repetir

LODS

Carga un byte, una palabra o una doble palabra de una cadena en el registro EAX

FIGURA 2.40 Algunas operaciones típicas en el x86. Muchas operaciones utilizan el formato de registro-memoria, donde tanto el operando fuente como el destino pueden estar en memoria y el otro puede ser un registro o un operando inmediato.

registro operación inmediato”. Otras instrucciones utilizan un postbyte o byte de código de operación adicional, etiquetado “mod, reg, r/m”, que contiene la información del modo de direccionamiento. Este postbyte se utiliza para muchas de las instrucciones que tratan con la memoria. El modo base más índice escalado utiliza un segundo postbyte, etiquetado con “sc, índice, base”. La figura 2.42 muestra la codificación de los dos especificadores de dirección postbyte posibles para los modos de 16 bits y de 32 bits. Desafortunadamente, para entender completamente qué registro se coloca y qué modos de dirección están disponibles necesitamos ver la codificación de todos los modos de direccionamiento y a veces incluso la codificación de las instrucciones.

Conclusión sobre x86 Intel tenía un microprocesador de 16 bits dos años antes que las arquitecturas más elegantes de sus competidores, tales como el Motorola 68000, y esta delantera le llevó a tener ventaja en la elección de los 8086 como la CPU para el PC de IBM. Los ingenieros

2.17

Casos reales: instrucciones x86

a. JE EIP + Desplazamiento 4 4 8 Condi- Desplazamiento ción

JE

b. CALL 8

32

CALL

Desplazamiento

c. MOV 6

EBX, [EDI + 45] 1 1 8

MOV

d w

r/m Postbyte

8 Desplazamiento

d. PUSH ESI 5

3

PUSH

Reg

e. ADD EAX, #6765 4 3 1 ADD

32

Reg w

f. TEST EDX, #42 7 1 TEST

w

Inmediato

8

32

Postbyte

Inmediato

FIGURA 2.41 Formatos de instrucción típicos x86. La figura 2.42 muestra la codificación del postbyte. Muchas instrucciones contienen el campo w de 1 bit, que indica si la operación es un byte o una doble palabra. El campo d en MOV se utiliza en las instrucciones que pueden mover a o desde memoria y muestran la dirección del movimiento. La instrucción ADD requiere 32 bits para el campo inmediato porque en modos de 32 bits los immediatos son tanto de 8 bits como de 32 bits. El campo inmediato en TEST es de 32 bits de longitud porque no hay inmediatos de 8 bits para probar en modos de 32 bits. En general, las instrucciones pueden variar de 1 a 17 bytes de longitud. La longitud larga viene de prefijos adicionales de 1 byte, teniendo un inmediato de 4 bytes y una dirección de desplazamiento de 4 bytes, usando un código de operación de 2 bytes y utilizando un especificador del modo índice escalado, que agrega otro byte.

de Intel reconocen generalmente que el x86 es más difícil de construir que máquinas como MIPS, pero un mercado mayor significa que AMD e Intel pueden permitirse más recursos para ayudar a superar la complejidad añadida. Lo que le falta al x86 en estilo se compensa con la cantidad, haciéndolo bonito desde una perspectiva correcta. El mérito excepcional es que los componentes arquitectónicos más frecuentemente usados en x86 no son demasiado difíciles de implementar, tal como AMD e Intel han demostrado rápidamente mejorando desde 1978 el funcionamiento de los programas de enteros. Para conseguir esas prestaciones, los compiladores deben evitar las partes de la arquitectura que son difíciles de poner en ejecución con rapidez.

173

174

Capítulo 2

w=1

r/m

Instrucciones: el lenguaje del computador

reg

w=0

mod = 0

mod = 1

mod = 2

mod = 3

16b

32b

16b

32b

16b

32b

16b

32b

0

AL

AX

EAX

0

addr=BX+SI

=EAX

igual

igual

igual

igual

1

CL

CX

ECX

1

addr=BX+DI

=ECX

dir que

dir que

dir que

dir que

2

DL

DX

EDX

2

addr=BP+SI

=EDX

mod=0

mod=0

mod=0

mod=0

3

BL

BX

EBX

3

addr=BP+SI

=EBX

+ disp8

+ disp8

+ disp16

+ disp32

4

AH

SP

ESP

4

addr=SI

=(sib)

SI+disp8

(sib)+disp8

si+disp8

(sib)+disp32

5

CH

BP

EBP

5

addr=DI

=disp32

DI+disp8

EBP+disp8

DI+disp16

EBP+disp32



6

DH

SI

ESI

6

addr=disp16

=ESI

BP+disp8

ESI+disp8

BP+disp16

ESI+disp32



7

BH

DI

EDI

7

addr=BX

=EDI

BX+disp8

EDI+disp8

BX+disp16

EDI+disp32



igual dir que reg campo “

FIGURA 2.42 La codificación del primer especificador de la dirección del x86, “mod, registro, r/m”. Las primeras cuatro columnas muestran la codificación del campo registro de 3 bits, que depende del bit w del código de operación y de si la máquina está en el modo de 16 bits (8086) o en el modo de 32 bits (80386). Las columnas restantes explican los campos mod y r/m. El significado del campo r/m de 3 bits depende del valor del campo mod de 2 bits y del tamaño de la dirección. Básicamente, los registros usados en el cálculo de la dirección se enumeran en la sexta y la séptima columnas, bajo mod = 0 y con mod = 1 que añade un desplazamiento de 8 bits y mod = 2 que añade un desplazamiento de 16 bits o de 32 bits, dependiendo del modo de direccionamiento. Las excepciones son r/m = 6 cuando mod = 1 o mod = 2 en modo de 16 bits selecciona BP más el desplazamiento; r/m = 5 cuando mod = 1 o mod = 2 en modo de 32 bits selecciona EBP más el desplazamiento; y r/m = 4 en modo de 32 bits cuando mod | 3, donde (sib) significa utilizar el modo índice escalado mostrado en la figura 2.38. Cuando mod = 3, el campo r/m indica un registro, usando la misma codificación que el campo del registro combinado con el bit w.

2.18

Falacias y errores habituales

2.18

Falacia: instrucciones más potentes implican unas prestaciones mayores. Una parte del potencial del Intel x86 son los prefijos que pueden modificar la ejecución de la instrucción siguiente. Un prefijo puede repetir la instrucción siguiente hasta que un contador, que se va decrementando, llegue. Así, para mover datos en memoria parece que la secuencia de instrucción natural es utilizar move con el prefijo de la repetición para realizar movimientos de memoria-a-memoria de 32 bits. Un método alternativo, que utiliza las instrucciones estándares que tienen todos los computadores, es cargar los datos en los registros y después almacenar los registros de nuevo en memoria. Esta segunda versión del programa, con el código replicado para reducir la sobrecarga del lazo, copia aproximadamente 1.5 veces más rápido. Una tercera versión, que utiliza los registros de punto flotante en vez de los registros de enteros del x86, copia aproximadamente 2.0 veces más rápido que la instrucción compleja. Falacia: escribir en lenguaje ensamblador para obtener prestaciones más altas. Hace un tiempo, los compiladores para los lenguajes de programación producían secuencias de instrucciones sencillas; el aumento de la sofisticación de los compiladores implica que la distancia entre el código compilado y el código producido a mano se está acortando rápidamente. De hecho, para competir con los compiladores actuales el programador de lenguaje ensamblador necesita comprender a fondo los conceptos de los capítulos 4 y 5 (segmentación –pipeline– del procesador y jerarquía de memoria).

2.18

Falacias y errores habituales

Esta batalla entre los compiladores y los codificadores del lenguaje ensamblador enmarca una situación en la cual los seres humanos están perdiendo terreno. Por ejemplo, C ofrece al programador una oportunidad para echarle una mano al compilador diciéndole qué variables se deberían mantener en registros frente a las volcadas en memoria. Cuando los compiladores eran pobres en la asignación de registros, este tipo de ayudas eran vitales para las prestaciones. De hecho, algunos libros de texto de C emplean bastante tiempo en dar ejemplos que utilizan con eficacia estas ayudas. Los compiladores C actuales generalmente ignoran tales ayudas porque el compilador hace un trabajo mejor en cuanto a la asignación que el programador. Aunque el resultado de escribirlo a mano fuera un código más rápido, los peligros de escribir en lenguaje ensamblador son un mayor tiempo de codificación y depuración, la pérdida en portabilidad y la dificultad de mantener tal código. Uno de los pocos axiomas extensamente aceptados por la ingeniería del software es que la codificación lleva más tiempo si se escriben más líneas, y claramente se requieren muchas más líneas para escribir un programa en lenguaje ensamblador que en C o Java. Por otra parte, una vez codificado, el siguiente riesgo es que se convierta en un programa popular. Tales programas siempre viven un tiempo más largo que el esperado, y esto significa que alguien tendrá que poner al día el código al cabo de varios años y lo tendrá que hacer trabajar con las nuevas versiones de sistemas operativos y los nuevos modelos de máquinas. El escribir en lenguaje de alto nivel en vez hacerlo en lenguaje ensamblador no sólo permite a los compiladores futuros adaptar el código a las máquinas futuras, también hace más fácil mantener el software y permite que el programa funcione en más tipos de computadores. Falacia: la importancia de la compatibilidad binaria comercial implica que los repertorios de instrucciones exitosos no cambian. La compatibilidad binaria hacia atrás, a versiones anteriores del software, es sacrosanta, pero la figura 2.43 muestra que la arquitectura del x86 ha crecido significativamente. ¡La media es más de instrucción por mes en sus 30 años de vida! Error: olvidar que las direcciones secuenciales de las palabras en máquinas con direccionamiento byte no se diferencian en uno. Muchos programadores de lenguaje ensamblador han tenido que trabajar mucho sobre errores cometidos por suponer que la dirección de la palabra siguiente se puede encontrar incrementando la dirección en un registro, de uno en uno, en lugar del tamaño de la palabra en bytes. ¡El que avisa no es traidor! Error: usar un puntero a una variable automática fuera de la definición de su procedimiento. Un error común al trabajar con punteros es pasar un resultado de un procedimiento que incluye el puntero a una tabla declarada local a ese procedimiento. Siguiendo la disciplina de la pila en la figura 2.12, la memoria que contiene la tabla local será reutilizada tan pronto como el procedimiento retorne. Los punteros a las variables automáticas pueden conducir al caos.

175

176

Capítulo 2

Instrucciones: el lenguaje del computador

1000 Número de instrucciones

900 800 700 600 500 400 300 200 100 0

78 80 82 84 86 88 90 92 94 96 98 00 02 04 06 08 19 19 19 19 19 19 19 19 19 19 19 20 20 20 20 20 Año FIGURA 2.43 Aumento del repertorio de instrucciones x86 con el tiempo. Aunque hay una justificación técnica clara para alguna de estas extensiones, este aumento tan rápido también aumenta las dificultades que encuentran otros fabricantes para desarrollar procesadores compatibles.

Menos es más. Robert Browning, Andrea del Sarto, 1855

2.19

Conclusiones finales

2.19

Los dos principios del computador de programa almacenado (stored-program) son el uso de instrucciones que son indistinguibles de los números y el uso de la memoria alterable para los programas. Estos principios permiten que una sola máquina ayude a científicos ambientales, a consejeros financieros y a novelistas en sus especialidades. La selección de un repertorio de instrucciones que la máquina pueda entender exige un equilibrio delicado entre el número de instrucciones necesario para ejecutar un programa, el número de ciclos de reloj necesarios para cada instrucción y la velocidad del reloj. Cuatro principios de diseño guían a los autores de repertorios de instrucciones a la hora de encontrar este delicado equilibrio: 1. La simplicidad favorece la regularidad. La regularidad es el origen de muchas características del repertorio de instrucciones MIPS: mantiene todas las instrucciones de un mismo tamaño, siempre requiere tres registros de operandos para las instrucciones aritméticas y mantiene los campos de registros en el mismo lugar en cada formato de instrucción.

2.19

Conclusiones finales

2. Cuanto más pequeño, más rápido. El deseo de velocidad es la razón por la que MIPS tiene 32 registros y no muchos más. 3. Hacer rápido lo común (lo más frecuente). Ejemplos de hacer rápido acciones MIPS comunes incluyen el direccionamiento relativo al PC para las bifurcaciones o saltos condicionales y el direccionamiento inmediato para los operandos constante. 4. El buen diseño exige buenos compromisos. Un ejemplo para MIPS fue el compromiso entre permitir direcciones y constantes más grandes en instrucciones y mantener todas las instrucciones de la misma longitud. Por encima de este nivel máquina está el lenguaje ensamblador, un lenguaje que los humanos podemos leer. El ensamblador lo traduce a números binarios que las máquinas puedan entender e incluso “amplían” el repertorio de instrucciones, creando instrucciones simbólicas que no están en el hardware. Por ejemplo, las constantes o las direcciones que son demasiado grandes están partidas en trozos del tamaño apropiado, las variaciones comunes de instrucciones tienen su propio nombre, y así sucesivamente. La figura 2.44 enumera las instrucciones MIPS que hemos estudiado hasta ahora, tanto las reales como las pseudoinstrucciones. Cada categoría de instrucciones MIPS se asocia a las construcciones que aparecen en lenguajes de programación: ■

Las instrucciones aritméticas se corresponden con las operaciones basadas en sentencias de asignación.



Las instrucciones de transferencia de datos son más frecuentes cuando se trabaja con estructuras de datos como tablas o estructuras.



Las bifurcaciones o saltos condicionales se utiliza en sentencias si (if) y en lazos.



Los saltos incondicionales se utilizan en llamadas y retornos de procedimiento y para las sentencias case/switch.

Estas instrucciones no nacen iguales; la popularidad de unas cuantas domina a las otras. Por ejemplo, la figura 2.45 muestra la frecuencia de aparición de cada clase de instrucciones para SPEC2006. La distinta popularidad de unas instrucciones respecto a otras desempeña un papel importante en los capítulos sobre el camino de datos (datapath), control y segmentación. Después de explicar la aritmética del computador en el capítulo 3, analizaremos más la arquitectura del repertorio de instrucciones MIPS.

177

178

Capítulo 2

Instrucciones: el lenguaje del computador

Instrucciones MIPS

Nombre

Formato

add

add

R

subtract

sub

add immediate

Nombre

Formato

move

move

R

R

multiply

mult

R

addi

I

multiply immediate

multi

I

load word

lw

I

load immediate

li

I

store word

sw

I

branch less than

blt

I

load half

lh

I

branch less than or equal

ble

I

store half

sh

I

branch greater than

bgt

I

load byte

lb

I

branch greater than or equal

bge

I

store byte

sb

I

load upper immediate

lui

I

and

and

R

or

or

R

nor

nor

R

and immediate

andi

I

or immediate

ori

I

shift left logical

sll

R

shift right logical

srl

R

branch on equal

beq

I

branch on not equal

bne

I

set less than

slt

R

set less than

slti

I

jump

j

J

jump register

jr

R

jump and link

jal

J

Pseudo MIPS

immediate

FIGURA 2.44 El repertorio de instrucciones MIPS visto hasta ahora, con las instrucciones reales MIPS a la izquierda y las pseudoinstrucciones a la derecha. El apéndice B (sección B.10) describe la arquitectura completa MIPS. La figura 2.1 muestra más detalles de la arquitectura MIPS tratada en este capítulo. La información se encuentra también en las columnas 1 y 2 de la Tarjeta de Datos de Referencia MIPS, que se incluye con este libro.

2.21

179

Ejercicios

Frecuencia Tipo de Instrucción

Ejemplos MIPS

Correspondencia con el lenguaje de alto nivel (HLL)

Enteros

Pt. fl.

Aritmética

add, sub, addi

Operaciones en sentencias de asignación

24%

48%

Transferencia de datos

lw, sw, lb, sb, lui

Referencias a estructuras de datos, tales como tablas

36%

39%

Lógica

and, or, nor, andi, ori, sll, srl

Operaciones en sentencias de asignación

18%

4%

Bifurcación condicional

beq, bne, slt, slti

Sentencias si (if) y lazos

18%

6%

Salto incondicional

j, jr, jal

Llamadas a procedimientos, retornos y sentencias case/switch

3%

0%

FIGURA 2.45 Tipos de instrucción MIPS, ejemplos, correspondencia con construcciones de lenguaje de programación de alto nivel y porcentaje de instrucciones MIPS ejecutadas por categoría para el promedio del SPEC2006. La figura 3.26 en el capítulo 3 muestra el porcentaje de las instrucciones MIPS ejecutadas individualmente.

2.20

2.20

Perspectiva histórica y lecturas recomendadas

2.20

Esta sección examina la historia de las líneas maestras del repertorio de instrucciones a lo largo del tiempo, y damos un repaso de los lenguajes de programación y de los compiladores. Los ISAs incluyen arquitecturas con acumulador, arquitecturas con registros de propósito general, arquitecturas con pila y una breve historia del ARM y x86. También revisamos los temas polémicos de las arquitecturas de computador con lenguajes de alto nivel y de las arquitecturas de computador con repertorios de instrucciones reducidos. La historia de los lenguajes de programación incluye Fortran, Lisp, Algol, C, Cobol, Pascal, Simula, Smaltalk, C++ y Java, y la historia de los compiladores incluye los hitos clave y a los pioneros que los alcanzaron. El resto de esta sección está en el CD.

2.21

Ejercicios

2.21

Contribución de John Oliver, Cal Poly, San Luis Obispo, con la colaboración de Nicole Kaiyan (University of Adelaide) y Milos Prvulovic (Georgia Tech)

El simulador MIPS, que es útil para estos ejercicios, se describe en el apéndice B. Aunque el simulador acepta pseudoinstrucciones, es mejor no utilizarlas en los ejercicios que piden desarrollar un código MIPS. El objetivo debe ser aprender el repertorio real de instrucciones de MIPS, y si se pide contar instrucciones, la cuenta debe reflejar el número real de instrucciones que se van ejecutar y no las pseudoinstrucciones. Las pseudoinstrucciones pueden ser utilizadas en algunos casos (por ejemplo, la instrucción la cuando no se conoce el valor real en tiempo de ensamblaje). En

180

Capítulo 2

Instrucciones: el lenguaje del computador

muchos casos, son convenientes y producen un código más legible (por ejemplo, las instrucciones li y move). Si se elige utilizar pseudoinstrucciones por estos motivos, por favor añádase un par de frases explicando qué pseudoinstrucciones se han usado y por qué.

Ejercicio 2.1 Los siguientes problemas abordan la traducción de C a MIPS. Suponga que las variables g, h, i, json conocidas y son enteros de 32 bits, tal como se declaran en C. a.

f = g + h + i + j;

b.

f = g + (h + 5);

2.1.1 [5]<2.2> ¿Cuál es el código MIPS para las instrucciones C de la tabla? Utilice el número mínimo posible de instrucciones ensamblador de MIPS. 2.1.2 [5]<2.2> ¿Cuántas instrucciones ensamblador de MIPS resultan de la traducción de las instrucciones C? 2.1.3 [5]<2.2> Si las variables f, g, h, i y j tienen valores 1, 2, 3, 4 y 5, respecti-

vamente, ¿cuál es valor final de f? Los siguientes problemas abordan la traducción de MIPS a C. Suponga que las variables g, h, i y j son conocidas y son enteros de 32 bits, tal como se declaran en C. a.

add

f, g, h

b.

addi add

f, f, 1 f, g, h

2.1.4 [5]<2.2> ¿Cuál es la sentencia C equivalente para las instrucciones MIPS de la tabla? 2.1.5 [5]<2.2> Si las variables f, g, h e itienen valores 1, 2, 3 y 4, respectivamente,

¿cuál es valor final de f?

Ejercicio 2.2 Los siguientes problemas abordan la traducción de C a MIPS. Suponga que las variables g, h, i y j son conocidas y son enteros de 32 bits, tal como se declaran en C.

2.21

a.

f = f + f + i;

b.

f = g + (j + 2);

Ejercicios

2.2.1 [5]<2.2> ¿Cuál es el código MIPS para las instrucciones C de la tabla? Utilice el número mínimo posible de instrucciones ensamblador de MIPS. 2.2.2 [5]<2.2> ¿Cuántas instrucciones ensamblador de MIPS resultan de la traducción de las instrucciones C? 2.2.3 [5]<2.2> Si las variables f, g, h e itienen valores 1, 2, 3 y 4, respectivamente, ¿cuál es valor final de f?

Los siguientes problemas abordan la traducción de MIPS a C. Suponga que las variables g, h, i y j son conocidas y son enteros de 32 bits, tal como se declaran en C. a.

add

f, f, h

b.

sub addi

f, $0, f f, f, 1

2.2.4 [5]<2.2> ¿Cuál es la sentencia C equivalente para las instrucciones MIPS de la tabla? 2.2.5 [5]<2.2> Si las variables f, g, h e i tienen valores 1, 2, 3 y 4, respectivamente, ¿cuál es valor final de f?

Ejercicio 2.3 Los siguientes problemas abordan la traducción de C a MIPS. Suponga que las variables g, h, i y j son conocidas y son enteros de 32 bits, tal como se declaran en C. a.

f = f + g + h + i + j + 2;

b.

f = g - (f + 5);

2.3.1 [5]<2.2> ¿Cuál es el código MIPS para las instrucciones C de la tabla? Utilice el número mínimo posible de instrucciones ensamblador de MIPS.

181

182

Capítulo 2

Instrucciones: el lenguaje del computador

2.3.2 [5]<2.2> ¿Cuántas instrucciones ensamblador de MIPS resultan de la traducción de las instrucciones C? 2.3.3 [5]<2.2> Si las variables f, g, h, i y j tienen valores 1, 2, 3, 4 y 5, respecti-

vamente, ¿cuál es valor final de f? Los siguientes problemas abordan la traducción de MIPS a C. Suponga que las variables g, h, i y j son conocidas y son enteros de 32 bits, tal como se declaran en C.

a.

add

f, -g, h

b.

addi sub

h, f, 1 f, g, h

2.3.4 [5]<2.2> ¿Cuál es la sentencia C equivalente para las instrucciones MIPS de la tabla? 2.3.5 [5]<2.2> Si las variables f, g, h e itienen valores 1, 2, 3 y 4, respectivamente, ¿cuál es valor final de f?

Ejercicio 2.4 Los siguientes problemas abordan la traducción de C a MIPS. Suponga que las variables f, g, h, i y j se han asignado a los registros $s0, $s1, $s2,$s3 y $s4, respectivamente. Suponga que las direcciones base de los vectores A y B están en los registros $s6 y $s7, respectivamente.

a.

f = g + h + B[4];

b.

f = g - A[B[4]];

2.4.1 [10]<2.2, 2.3> ¿Cuál es el código MIPS para las instrucciones C de la tabla? Utilice el número mínimo posible de instrucciones ensamblador de MIPS. 2.4.2 [5]<2.2, 2.3> ¿Cuántas instrucciones ensamblador de MIPS resultan de la traducción de las instrucciones C? 2.4.3 [5]<2.2, 2.3> ¿Cuántos registros diferentes es necesario utilizar?

2.21

Ejercicios

Los siguientes problemas abordan la traducción de MIPS a C. Suponga que las variables f, g, h, i y j se han asignado a los registros $s0, $s1, $s2, $s3 y $s4, respectivamente. Suponga que las direcciones base de los vectores A y B están en los registros $s6 y $s7, respectivamente. a.

add add add add

$s0, $s0, $s0, $s0,

$s0, $s0, $s0, $s0,

$s1 $s2 $s3 $s4

b.

lw

$s0, 4($s6)

2.4.4 [5]<2.2, 2.3> ¿Cuál es la sentencia C equivalente para las instrucciones MIPS de la tabla? 2.4.5 [5]<2.2, 2.3> Reescriba el código ensamblador para minimizar (si es posible) el número de instrucciones MIPS necesarias para llevar a cabo la misma operación de la tabla. 2.4.6 [5]<2.2, 2.3> ¿Cuántos registros diferentes es necesario utilizar tal como está escrito el código anterior? Con el nuevo código, ¿cuál es el número mínimo de registros necesario?

Ejercicio 2.5 En los siguientes problemas se estudian las operaciones de memoria en el contexto de un procesador MIPS. Los valores de un vector almacenado en memoria se muestran en la tabla. a.

Dirección 12 8 4 0

Valor 1 6 4 2

b.

Dirección 16 12 8 4 0

Valor 1 2 3 4 5

2.5.1 [10]<2.2, 2.3> Para las posiciones de memoria mostradas en la tabla, escriba el código C que ordene los valores de menor a mayor, situando el menor valor en la posición de memoria más pequeña de las mostradas. Suponer que los datos se han representado en un vector de enteros de C de nombre array, que el procesador es direccionable por bytes y que una palabra está formada por 4 bytes.

183

184

Capítulo 2

Instrucciones: el lenguaje del computador

2.5.2 [10]<2.2, 2.3> Para las posiciones de memoria mostradas en la tabla, escriba el código MIPS que ordene los valores de menor a mayor, situando el menor valor en la posición de memoria más pequeña de las mostradas. Utilice el número mínimo de instrucciones MIPS. Suponga que la dirección base de la tabla está en el registro $s6. 2.5.3 [5]<2.2, 2.3> ¿Cuántas instrucciones ensamblador de MIPS son necesarias para el ordenamiento? Si no se pudiese utilizar un campo inmediato en las instrucciones lw y sw, ¿cuántas instrucciones se necesitarían?

En los siguientes problemas se estudia la conversión de números hexadecimales a otros formatos.

a.

0x12345678

b.

0xbeadf00d

2.5.4 [5]<2.3> Convierta los número hexadecimales de la tabla a decimales. 2.5.5 [5]<2.3> Indique cómo se almacenarían los datos de la tabla en un procesador little-endian o uno big-endian. Suponga que los datos se almacenan a partir de la dirección 0.

Ejercicio 2.6 Los siguientes problemas abordan la traducción de C a MIPS. Suponga que las variables f, g, h, i, y j se han asignado a los registros $s0, $s1, $s2, $s3 y $s4, respectivamente. Suponga que las direcciones base de los vectores A y B están en los registros $s6 y $s7, respectivamente.

a.

f = -g + h + B[1];

b.

f = A[B[g]+1];

2.6.1 [10]<2.2, 2.3> ¿Cuál es el código MIPS para las instrucciones C de la tabla? Utilice el número mínimo posible de instrucciones ensamblador de MIPS. 2.6.2 [5]<2.2, 2.3> ¿Cuántas instrucciones ensamblador de MIPS resultan de la traducción de las instrucciones C? 2.6.3 [5]<2.2, 2,3> ¿Cuántos registros diferentes es necesario utilizar?

2.21

Ejercicios

Los siguientes problemas abordan la traducción de MIPS a C. Suponga que las variables f, g, h, i y j se han asignado a los registros $s0, $s1, $s2, $s3 y $s4, respectivamente. Suponga que las direcciones base de los vectores A y B están en los registros $s6 y $s7, respectivamente.

a.

add add add

$s0, $s0, $s1 $s0, $s3, $s2 $s0, $s0, $s3

b.

addi $s6, $s6, -20 add $s6, $s6, $s1 lw $s0, 8($s6)

2.6.4 [5]<2.2, 2.3> ¿Cuál es la sentencia C equivalente para las instrucciones MIPS de la tabla? 2.6.5 [5]<2.2, 2.3> Suponga que los valores contenidos en los registros $s0, $s1, $s2 y $s3 son, respectivamente, 10, 20, 30 y 40, que el registro $s6 tiene el valor 256 y que en la memoria están los siguientes valores:

Dirección

Valor

256

100

260

200

264

300

Determine el valor en $s0 al terminar el código ensamblador. 2.6.6 [10]<2.3, 2.5> Indique el valor de los campos op, rs y rt para cada instrucción MIPS. Indique el valor del campo inmediato para las instrucciones de tipo I, y del campo rd para las instrucciones de tipo R.

Ejercicio 2.7 En los siguientes problemas se estudia la conversión de números binarios con y sin signo a número decimales.

a.

1010

1101

0001

0000

0000

0000

0000

0010dos

b.

1111

1111

1111

1111

1011

0011

0101

0011dos

185

186

Capítulo 2

Instrucciones: el lenguaje del computador

2.7.1 [5]<2.4> Suponiendo que los números de la tabla son enteros en complemento a 2, ¿qué números decimales representan? 2.7.2 [5]<2.4> Suponiendo que los números de la tabla son enteros sin signo, ¿qué números decimales representan? 2.7.3 [5]<2.4> ¿Qué números hexadecimales representan?

En los siguientes ejercicios se estudia la conversión de números decimales a binarios con y sin signo.

a.

2147483647diez

b.

1000diez

2.7.4 [5]<2.4> Convierta los números de la tabla en una representación binaria en complemento a 2. 2.7.5 [5]<2.4> Convierta los números de la tabla en una representación hexadecimal en complemento a 2. 2.7.6 [5]<2.4> Convierta los negativos de los números de la tabla en una representación hexadecimal en complemento a 2.

Ejercicio 2.8 En los siguientes problemas se estudia la extensión de signo y el desbordamiento. El contenido de los registros $s0 y $s1 son los valores mostrados en la tabla. Se le pedirá que haga algunas operaciones MIPS sobre estos registros y que muestre los resultados. a.

$s0 = 70000000dieciseis,

$s1 = 0x0FFFFFFFdieciseis

b.

$s0 = 0x40000000dieciseis,

$s1 = 0x40000000dieciseis

2.8.1 [5]<2.4> Determine el valor de $t0 después de la siguiente instrucción: add $t0, $s0, $s1

¿Se ha producido desbordamiento?

2.21

Ejercicios

2.8.2 [5]<2.4> Determine el valor de $t0 después de la siguiente instrucción: sub $t0, $s0, $s1

¿Se ha producido desbordamiento? 2.8.3 [5]<2.4> Determine el valor de $t0 después de la siguiente instrucción: add $t0, $s0, $s1 add $t0, $t0, $s0

¿Se ha producido desbordamiento? En los siguientes problemas deberá realizar operaciones MIPS sobre los registros $s0 y $s1. Dados los valores de $s0 y $s1 en cada una de las preguntas, diga si se produce desbordamiento.

a.

add

$s0, $s0, $s1

b.

sub sub

$s0, $s0, $s1 $s0, $s0, $s1

2.8.4 [5]<2.4> Si los contenidos de los registros son $s0 = 0x70000000 y $s1 =

0x10000000, ¿se produce desbordamiento? 2.8.5 [5]<2.4> Si los contenidos de los registros son $s0 = 0x40000000 y $s1 =

0x20000000, ¿se produce desbordamiento? 2.8.6 [5]<2.4> Si los contenidos de los registros son $s0 = 0x8FFFFFFF y $s1 =

0xD0000000, ¿se produce desbordamiento?

Ejercicio 2.9 En la tabla se muestran varios valores para el registro $s1. Determine si se produce desbordamiento para ciertas operaciones.

a.

$s1 = 2147483647diez

b.

$s1 = 0xD0000000dieciseis

187

188

Capítulo 2

Instrucciones: el lenguaje del computador

2.9.1 [5]<2.4> Suponga que el contenido del registro $s0 es $s0 = 0x70000000. ¿Se produce desbordamiento al hacer la operación add $s0, $s0, $s1? 2.9.2 [5]<2.4> Suponga que el contenido del registro $s0 es $s0 = 0x80000000. ¿Se produce desbordamiento al hacer la operación sub $s0, $s0, $s1? 2.9.3 [5]<2.4> Suponga que el contenido del registro $s0 es $s0 = 0x7FFFFFFF. ¿Se produce desbordamiento al hacer la operación add $s0, $s0, $s1?

En la tabla se muestran varios valores para el registro $s1. Determine si se produce desbordamiento para ciertas operaciones.

a.

$s1 = 1010

1101

b.

$s1 = 1111

1111

0001 1111

0000 1111

0000 1011

0000 0011

0000

0010dos

0101

0011dos

2.9.4 [5]<2.4> Suponga que el contenido del registro $s0 es $s0 = 0x70000000. ¿Se produce desbordamiento al hacer la operación add $s0, $s0, $s1? 2.9.5 [5]<2.4> Suponga que el contenido del registro $s0 es $s0 = 0x70000000. ¿Cuál es el resultado en hexadecimal de la operación add $s0, $s0, $s1? 2.9.6 [5]<2.4> Suponga que el contenido del registro $s0 es $s0 = 0x70000000.

¿Cuál es el resultado en base diez de la operación add $s0, $s0, $s1?

Ejercicio 2.10 La tabla muestra la codificación binaria de dos instrucciones. Tradúzcalas a ensamblador y averigüe qué instrucción MPS representan.

a.

1010

1110

0000

1011

0000

0000

0000

0100dos

b.

1000

1101

0000

1000

0000

0000

0100

0000dos

2.10.1 [5]<2.5> ¿Qué instrucciones representan los valores de la tabla? 2.10.2 [5]<2.5> ¿Qué tipo de instrucciones (tipo I, tipo R) representan los valores de la tabla?

2.21

Ejercicios

2.10.3 [5]<2.5> Si los valores de la tabla fuesen datos en lugar de instrucciones, ¿qué número hexadecimal representan?

La tabla muestra dos instrucciones MIPS. Tradúzcalas y determine el formato. a.

add

$t0, $t0, $zero

b.

lw

$t1, 4($s3)

2.10.4 [5]<2.4, 2.5> Indique la representación hexadecimal de las dos instrucciones. 2.10.5 [5]<2.5> ¿Qué tipo de instrucciones (tipo I, tipo R) son las instrucciones de la tabla? 2.10.6 [5]<2.5> ¿Cuál es representación hexadecimal del código de operación, y los campos rs y rt en esta instrucción? ¿Cuál es representación hexadecimal de los campos rd y func en las instrucciones tipo R? ¿Cuál es representación hexadecimal del campo inmediato en las instrucciones tipo I?

Ejercicio 2.11 La tabla muestra la representación del código de operación de dos instrucciones. Se le pedirá que las traduzca a ensamblador y averigüe qué instrucción MIPS representan. a.

0xAE0BFFFC

b.

0x8D08FFC0

2.11.1 [5]<2.4, 2.5> ¿Cuál es la representación binaria de los números hexadecimales de la tabla? 2.11.2 [5]<2.4, 2.5> ¿Cuál es la representación decimal de los números hexadecimales de la tabla? 2.11.3 [5]<2.5> ¿Qué instrucciones representan los números hexadecimales de la tabla?

La tabla contiene los valores de varios campos de instrucciones MIPS. Se le pedirá que averigüe qué instrucciones son y que determine el formato MIPS. a.

op=0, rs=1, rt=2, rd=3, shamt=0, funct=32

b.

op=0x2B, rs=0x10, rt=0x5, const=0x4

189

190

Capítulo 2

Instrucciones: el lenguaje del computador

2.11.4 [5]<2.5> ¿Qué tipo de instrucción (tipo I, tipo R) representan los valores de la tabla? 2.11.5 [5]<2.5> ¿Cuáles son las instrucciones MIPS que representan los valores de la tabla? 2.11.6 [5]<2.4, 2.5> ¿Cuál es la representación binaria de estas instrucciones?

Ejercicio 2.12 La tabla contiene varias modificaciones que podrían hacerse a la arquitectura del repertorio de instrucciones de MIPS. Deberá investigar el impacto de estos cambios en el formato de las instrucciones. a.

8 registros

b.

Constantes inmediatas de 10 bits

2.12.1 [5]<2.5> Si se cambia el repertorio de instrucciones del procesador MIPS, también debe cambiarse el formato de las instrucciones. Para los cambios sugeridos en la tabla, indique el tamaño en bits de los campos de una instrucción tipo R. ¿Cuál es el número de bits necesario para cada instrucción? 2.12.2 [5]<2.5> Si se cambia el repertorio de instrucciones del procesador MIPS, también debe cambiarse el formato de las instrucciones. Para los cambios sugeridos en la tabla, indique el tamaño en bits de los campos de una instrucción tipo I. ¿Cuál es el número de bits necesario para cada instrucción? 2.12.3 [5]<2.5, 2.10> ¿Por qué los cambios sugeridos en la tabla podrían reducir el tamaño de un programa ensamblador MIPS? ¿Por qué los cambios sugeridos en la tabla podrían aumentar el tamaño de un programa ensamblador MIPS?

La tabla contiene valores hexadecimales. Deberá averiguar qué instrucción MIPS representan y determinar el formato. a.

0x01090010

b.

0x8D090012

2.12.4 [5]<2.5> ¿Cuál es la representación decimal de los valores de la tabla? 2.12.5 [5]<2.5> ¿Qué instrucción representan los números hexadecimales de la tabla?

2.21

Ejercicios

2.12.6 [5]<2.4, 2.5> ¿Qué tipo de instrucción (tipo I, tipo R) representan los valores de la tabla? ¿Cuál es el valor de los campos op y rt?

Ejercicio 2.13 La tabla muestra el contenido de los registros $t0 y $t1. Deberá hacer varias operaciones lógicas sobre estos registros.

a.

$t0 = 0x55555555,

$t1 = 0x12345678

b.

$t0 = 0xBEADFEED,

$t1 = 0xDEADFADE

2.13.1 [5]<2.6> Determine el valor final de $t2 en la siguiente secuencia de instrucciones: sll $t2, $t0, 4 or $t2, $t2, $t1

2.13.2 [5]<2.6> Determine el valor final de $t2 en la siguiente secuencia de instrucciones: sll $t2, $t0, 4 andi $t2, $t2, -1

2.13.3 [5]<2.6> Determine el valor final de $t2 en la siguiente secuencia de instrucciones: srl $t2, $t0, 3 andi $t2, $t2, 0xFFEF

La tabla muestra varias operaciones lógicas de MIPS. Deberá encontrar el resultado de esas operaciones para ciertos valores de $t0 y $t1. a.

sll $t2, $t0, 1 or $t2, $t2, $t1

b.

srl $t2, $t0, 1 andi $t2, $t2, 0x00F0

2.13.4 [5]<2.6> Suponiendo que $t0 = 0x0000A5A5 y $t1 = 00005A5A, deter-

mine el valor de $t2 después de las dos instrucciones de la tabla.

191

192

Capítulo 2

Instrucciones: el lenguaje del computador

2.13.5 [5]<2.6> Suponiendo que $t0 = 0xA5A50000 y $t1 = A5A50000, deter-

mine el valor de $t2 después de las dos instrucciones de la tabla. 2.13.6 [5]<2.6> Suponiendo que $t0 = 0xA5A5FFFF y $t1 = A5A5FFFF, determine el valor de $t2 después de las dos instrucciones de la tabla.

Ejercicio 2.14 La siguiente figura muestra la posición de un grupo de bits en el registro $t0. 31

i

j

0

Campo 31 - i bits

i - j bits

j bits

En los siguientes ejercicios deberá escribir instrucciones MIPS para extraer el grupo de bits “Campo” de $t0 y situarlos en el registro $t1 en las posiciones indicadas en la siguiente tabla.

a.

i-j

31 0 0 0 ... 0 0 0

b.

31

Campo

14 + i - j bits 0 0 0 ... 0 0 0

14 Campo

0 0 0 0 ... 0 0 0

2.14.1 2.14.1 [20]<2.6> Encuentre la secuencia de instrucciones MIPS más corta para los bits de “Campo” de $t0, para i = 22 y j = 5 y situarlo en $t1 con el formato mostrado en la tabla. 2.14.2 2.14.2 [5]<2.6> Encuentre la secuencia de instrucciones MIPS más corta para los bits de “Campo” de $t0, para i = 4 y j = 0 y situarlo en $t1 con el formato mostrado en la tabla. 2.14.3 2.14.3 [5]<2.6> Encuentre la secuencia de instrucciones MIPS más corta para los bits de “Campo” de $t0, para i = 3 y j = 28 y situarlo en $t1 con el formato mostrado en la tabla.

En los siguientes ejercicios deberá escribir instrucciones MIPS para extraer los bits de “Campo” de $t0 y situarlos en el registro $t1 en las posiciones indicadas en la siguiente tabla. Los bits marcados como “XXX” no pueden cambiarse.

2.21

a.

i-j

31 X X X ... X X X

b.

Ejercicios

Campo

14 + i - j bits

31

X X X ... X X X

14 Campo

0 X X X ... X X X

2.14.4 [20]<2.6> Encuentre la secuencia de instrucciones MIPS más corta para bits de “Campo” de $t0, para i = 17 y j = 11 y situarlo en $t1 con el formato mostrado en la tabla. 2.14.5 [5]<2.6> Encuentre la secuencia de instrucciones MIPS más corta para bits de “Campo” de $t0, para i = 5 y j = 0 y situarlo en $t1 con el formato mostrado en la tabla. 2.14.6 [5]<2.6> Encuentre la secuencia de instrucciones MIPS más corta para bits de “Campo” de $t0, para i = 31 y j = 29 y situarlo en $t1 con el formato mostrado en la tabla.

Ejercicio 2.15 Para estos problemas, en la tabla se muestran algunas operaciones lógicas que no están en el repertorio de instrucciones de MIPS. ¿Como se implementan? a.

andn $t1, $t2, $t3

// operación AND bit a bit

b.

xnor $t1, $t2, $t3

// operación NOR exclusiva

de $t2 y !$t3

2.15.1 [5]<2.6> Las instrucciones lógicas de la tabla no están en el repertorio de instrucciones de MIPS. Determine el resultado si $t2 = 0x00FFA5A5 y $t3 = 0xFFFF003C. 2.15.2 [10]<2.6> Las instrucciones lógicas de la tabla no están en el repertorio de instrucciones de MIPS, pero pueden implementarse utilizando una o varias instrucciones MIPS. Encuentre el conjunto mínimo de instrucciones MIPS que pueden usarse en lugar de las instrucciones de la tabla. 2.15.3 [5]<2.6> Muestre la representación a nivel de bit de las instrucciones en la secuencia del ejercicio 2.15.2.

En la tabla se muestran varias instrucciones lógicas de C. En este ejercicio deberá evaluar e implementar estas instrucciones C utilizando instrucciones ensamblador de MIPS.

193

194

Capítulo 2

Instrucciones: el lenguaje del computador

a.

A = B & C[0]

b.

A = A ? B : C[0]

2.15.4 [5]<2.6> La tabla muestra instrucciones de C que utilizan operadores lógicos. Si la posición de memoria de C[0] contiene el valor entero 0x00001234, y los valores enteros iniciales de A y B son 0x00000000 y 0x00002222, ¿cuál es el valor final de A? 2.15.5 [5]<2.6> Escriba la secuencia mínima de instrucciones ensamblador de MIPS que hacen la misma operación que las instrucciones C de la tabla. 2.15.6 [5]<2.6> Muestre la representación a nivel de bit de las instrucciones en la secuencia del ejercicio 2.15.5.

Ejercicio 2.16 La tabla muestra varios valores binarios del registro $t0. Dado el valor de $t0 deberá determinar la salida de varias instrucciones de salto. a.

1010

1101

0001

0000

0000

0000

0000

0010dos

b.

1111

1111

1111

1111

1111

1111

1111

1111dos

2.16.1 [5]<2.7> Suponga que el registro $t0 tiene el valor mostrado en tabla y $t1 tiene el valor 0011

1111

1111

1000

0000

0000

0000

0000dos

¿Cuál es el valor de $t2 después de las siguientes instrucciones?

ELSE: DONE:

slt beq j addi

$t2, $t0, $t1 $t2, $zero, ELSE DONE $t2, $zero, 2

2.16.2 [5]<2.7> Suponga que el registro $t0 tiene el valor mostrado en tabla y que se compara con el valor X en la siguiente instrucción MIPS. ¿Para qué valores de X (si hay alguno) $t2 será igual a 1? slti $t2, $t0, X

2.21

Ejercicios

2.16.3 [5]<2.7> Suponga que el contador de programa (PC) toma el valor 0x00000020. ¿Es posible utilizar la instrucción MIPS salto incondicional (j) para poner el valor mostrado en la tabla en el PC? ¿Es posible utilizar la instrucción MIPS salto-si-igual (beq) para poner el valor mostrado en la tabla en el PC?

La tabla muestra varios valores binarios para el registro $t0. Dado el valor de $t0, deberá evaluar la salida de diferentes saltos. a.

0x00001000

b.

0x20001400

2.16.4 [5]<2.7> Suponga que el registro $t0 tiene el valor mostrado en tabla,

¿cuál es el valor de $t2 después de las siguientes instrucciones?

ELSE: DONE:

slt bne j addi

$t2, $t0, $t0 $t2, $zero, ELSE DONE $t2, $t2, 2

2.16.5 [5]<2.7> Suponga que el registro $t0 tiene el valor mostrado en tabla,

¿cuál es el valor de $t2 después de las siguientes instrucciones? sll $t0, $t0, 2 slt $t2, $t0, $zero

2.16.6 [5]<2.7> Suponga que el contador de programa (PC) toma el valor 0x20000000. ¿Es posible utilizar la instrucción MIPS salto incondicional (j) para poner el valor mostrado en la tabla en el PC? ¿Es posible utilizar la instrucción MIPS salto-si-igual (beq) para poner el valor mostrado en la tabla en el PC?

Ejercicio 2.17 Para estos problemas se muestran varias instrucciones que no están en el repertorio de instrucciones de MIPS. a.

abs $t2, $t3

# R[rd]=|R[rt]|

b.

sgt $t1, $t2, $t3

# R[rd]=( R[rs] > R[rt]) ? 1:0

2.17.1 [5]<2.7> La tabla muestra varias instrucciones que no están en el repertorio de instrucciones de MIPS y la descripción de cada instrucción. ¿Por qué no están incluidas en el conjunto de instrucciones de MIPS?

195

196

Capítulo 2

Instrucciones: el lenguaje del computador

2.17.2 [5]<2.7> La tabla muestra varias instrucciones que no están en el repertorio de instrucciones de MIPS y la descripción de cada instrucción. Si estas instrucciones se implementasen en el conjunto de instrucciones MIPS, ¿cuál sería el formato de instrucción más apropiado? 2.17.3 [5]<2.7> Encuentre la secuencia de instrucciones MIPS más corta que realiza la misma operación que las instrucciones de la tabla.

La siguiente tabla contiene fragmentos de código ensamblador MIPS. Deberá evaluar cada uno de estos fragmentos y familiarizarse con las instrucciones de salto. a.

LOOP: ELSE:

slt bne j addi subi j

$t2, $t2, DONE $s2, $t1, LOOP

$0, $t1 $zero, ELSE $s2, 2 $t1, 1

DONE: b.

LOOP: addi $t2, LOOP2: addi $s2, subi $t2, bne $t2, subi $t1, bne $t1, DONE:

$0, 0xA $s2, 2 $t2, 1 $0, LOOP2 $t1, 1 $0, LOOP

2.17.4 [5]<2.7> En los lazos anteriores escritos en ensamblador de MIPS, suponga que el valor inicial del registro $t1 es 10, si el valor inicial en $s2 es 0, ¿cuál es el valor final en el registro $s2? 2.17.5 [5]<2.7> Escriba la rutina C equivalente a cada uno de los lazos de la tabla. Suponga que los registros $s1, $s2, $t1 y $t2 son los enteros A, B, i y temp, respectivamente. 2.17.6 [5]<2.7> En los lazos anteriores escritos en ensamblador de MIPS, suponga que el valor inicial del registro $t1 es N. ¿Cuántas instrucciones MIPS se ejecutan?

Ejercicio 2.18 La siguiente tabla muestra fragmentos de código C. Deberá evaluar estos códigos C en ensamblador de MIPS. a.

for (i=0; i<10, i++) a += b;

b.

While (a < 10 ){ D[a] = b + a; a + = 1; }

2.21

Ejercicios

2.18.1 [5]<2.7> Dibuje el gráfico de control de flujo del código C. 2.18.2 [5]<2.7> Utilizando el número mínimo de instrucciones, traduzca el código C a código ensamblador MIPS. Suponga que los valores a, b, i, y j están en los registros $s0, $s1, $t0, $t1, respectivamente, y que la dirección base del vector D está en el registro $s2. 2.18.3 [5]<2.7> ¿Cuántas instrucciones MIPS hay en la traducción del código C? Si el valor inicial de las variables a y b es 10 y 1, y todos los elementos de D valen 0, ¿cuántas instrucciones MIPS se ejecutan hasta completar el lazo?

La siguiente tabla contiene fragmentos de código ensamblador MIPS. Deberá evaluar cada uno de estos fragmentos y familiarizarse con las instrucciones de salto.

a. LOOP:

b. LOOP:

addi lw add addi subi bne

$t1, $s1, $s2, $s0, $t1, $t1,

$0, 100 0($s2) $s2, $s1 $s0, 4 $t1, 1 $0, LOOP

addi lw add lw add addi bne

$t1, $s1, $s2, $s1, $s2, $s0, $t1,

$s0, 400 0($s0) $s2, $s1 4($s0) $s2, $s1 $s0, 8 $s0, LOOP

2.18.4 [5]<2.7> ¿Cuántas instrucciones MIPS se ejecutan? 2.18.5 [5]<2.7> Traduzca el código a C. Suponga que los enteros i y result están

en los registros $t1 y $s2, y que que la dirección base del entero MemArray está en el registro $s0. 2.18.6 [5]<2.7> Reescriba el código MIPS para reducir el número de instrucciones ejecutadas.

Ejercicio 2.19 La tabla muestra funciones en C. Suponga que la primera función de la tabla es la que se llama en primer lugar. Deberá traducir esta funciones a ensamblador del MIPS.

197

198

Capítulo 2

Instrucciones: el lenguaje del computador

a.

int compare(int a, int b) { if (sub(a,b) >= 0) return 1; else return 0; } int sub(int a, int b){ return a-b; }

b.

int fib_iter(int a, int b, int n) { if (n == 0) return b; else return fib_iter(a+b, a, n-1); }

2.19.1 [15]<2.8> Implemente el código de la tabla en ensamblador de MIPS. ¿Cuántas instrucciones MIPS se necesitan para ejecutar la función? 2.19.2 [5]<2.8> Los compiladores a menudo implementan las funciones in-line, es decir, el cuerpo de la función se copia en el espacio del programa, eliminando el sobrecoste de la llamada a la función. Implemente una versión in-line de la función de la tabla. ¿Cuál es la reducción del número de instrucciones MIPS necesarias para completar la función? Suponga que el valor inicial de n es 5. 2.19.3 [5]<2.8> Muestre el contenido de la pila después de cada llamada a la función. El valor inicial del puntero de pila es la dirección 0x7ffffffc. Siga las reglas de registros indicadas en la figura 2.11.

Los siguientes problemas hacen referencia a una función f que invoca a otra función func. La función func está ya compilada en otro módulo usando las reglas de invocación de MIPS de la figura 2.14. La declaración de func es “int func(int a, int b);”. El código de f es:

a.

int f(int a, int b, int c) { return func(func(a,b),c); }

b.

int f(int a, int b, int c) { return func(a,b)+func(b,c); }

2.19.4 [10]<2.8> Traduzca la función f a ensamblador de MIPS utilizando las reglas de invocación de funciones de MIPS de la figura 2.14. Si se necesitan registros de $t0 a $t7, utilice primero los de menor índice.

2.21

Ejercicios

2.19.5 [5]<2.8> ¿Se puede usar la optimización tail-call en esta función? En caso

negativo, explique por qué. En caso afirmativo, ¿cuál es la diferencia en el número de instrucciones de f con y sin optimización? 2.19.6 [5]<2.8> Justo antes de la finalización de la función f del problema 2.19.4,

¿qué se conoce de los contenidos de los registros $t5, $s3, $ra y $sp? Tenga en cuenta que se conoce la función f completa, pero de func sólo se conoce su declaración.

Ejercicio 2.20 Estos problemas abordan las llamadas recursivas a procedimientos. La tabla muestra un fragmento de código ensamblador que calcula el factorial de un número. Sin embargo, hay algunos errores en este fragmento que tendremos que localizar. a.

b.

FACT:

addi sw sw slti beq addi addi jr

$sp, $ra, $a0, $t0, $t0, $v0, $sp, $ra

$sp, -8 4($sp) 0($sp) $a0, 1 $0, L1 $0, 1 $sp, 8

L1:

addi jal lw lw addi mul jr

$a0, FACT $a0, $ra, $sp, $v0, $ra

$a0, -1

FACT:

addi sw sw slti beq addi addi jr

$sp, $ra, $a0, $t0, $t0, $v0, $sp, $ra

$sp, -8 4($sp) 0($sp) $a0, 1 $0, L1 $0, 1 $sp, 8

L1:

addi jal lw lw addi mul jr

$t0, FACT $a0, $ra, $sp, $v0, $ra

$t0, -1

4($sp) 0($sp) $sp, 8 $a0, $v0

4($sp) 0($sp) $sp, 8 $a0, $v0

2.20.1 [5]<2.8> El código ensamblador de la tabla calcula el factorial de un número de entrada dado. La entrada entera se almacena en el registro $a0 y el resultado se almacena en el registro $v0. Corrija los errores del código.

199

200

Capítulo 2

Instrucciones: el lenguaje del computador

2.20.2 [10]<2.8> Reescriba el programa para que opere de forma no recursiva suponiendo que la entrada es 4. Utilice únicamente los registros $s0 - $s7. Compare el número de instrucciones con los de la versión recursiva. 2.20.3 [5]<2.8> Indique el contenido de la pila después de cada llamada a la función, suponiendo que la entrada es 4.

Para los siguientes problemas, la tabla muestra un fragmento de código ensamblador que calcula un número de Fibonacci. Sin embargo, en este fragmento hay algunos errores y tendrá que localizarlos.

a.

b.

FIB:

addi sw sw sw slti beq addi j

$sp, $ra, $s1, $a0, $t0, $t0, $v0, EXIT

$sp, -12 0($sp) 4($sp) 8($sp) $a0, 1 $0, L1 $a0, 0

L1:

addi jal addi addi jal add

$a0, FIB $s1, $a0, FIB $v0,

$a0, -1

EXIT: lw lw lw addi jr

$ra, $a0, $s1, $sp, $ra

0($sp) 8($sp) 4($sp) $sp, 12

FIB:

addi sw sw sw slti beq addi j

$sp, $ra, $s1, $a0, $t0, $t0, $v0, EXIT

$sp, -12 0($sp) 4($sp) 8($sp) $a0, 1 $0, L1 $a0, 0

L1:

addi jal addi addi jal add

$a0, FIB $s1, $a0, FIB $v0,

$a0, -1

EXIT: lw lw lw addi jr

$ra, $a0, $s1, $sp, $ra

0($sp) 8($sp) 4($sp) $sp, 12

$v0, $0 $a0, -1 v0, $s1

$v0, $0 $a0, -1 v0, $s1

2.21

Ejercicios

2.20.4 [5]<2.8> Este fragmento de código ensamblador calcula el Fibonacci de una entrada. La entrada entera se almacena en el registro $a0 y el resultado se almacena en el registro $v0. Corrija los errores del código. 2.20.5 [10]<2.8> Reescriba el programa para que opere de forma no recursiva suponiendo que la entrada es 4. Utilice únicamente los registros $s0 - $s7. Compare el número de instrucciones con las de la versión recursiva. 2.20.6 [5]<2.8> Indique el contenido de la pila después de cada llamada a la función, suponiendo que la entrada es 4.

Ejercicio 2.21 Suponga que la pila y el segmento estático de datos están vacíos y que los punteros de pila y global apuntan a las direcciones 0x7fff fffc y 0x1000 8000, respectivamente. Suponga que se usan las reglas de llamada de la figura 2.11 y que la entrada de las funciones se pasan a través de $a0 y el valor de retorno se almacena en $v0. Suponga finalmente que la función leaf no puede utilizar registros temporales.

a.

main() { leaf_function(1) } int leaf_function(int f) { int result; result=f+1; if (f>5) return result; leaf_function(result); }

b.

int my_global = 100; main() { int x=10; int y =20; int z; z=my_function(x, my_global) } int my_function(int x, int y) { return x-y; }

2.21.1 [5]<2.8> Muestre los contenidos de la pila y del segmento estático de datos después de la llamada a cada función.

201

202

Capítulo 2

Instrucciones: el lenguaje del computador

2.21.2 [5]<2.8> Escriba el código MIPS para el código C de la tabla. 2.21.3 [5]<2.8> Escriba el código MIPS para el código C de la tabla, suponiendo que la función leaf puede utilizar registros temporales ($t0, $t1, etc).

Para los siguientes problemas se utilizará la función escrita en ensamblador de MIPS de la tabla, que sigue las reglas de llamada de la figura 2.14. a.

f: sub sll add sub jr

b.

f: addi sw sw move jal add lw lw addi jr

$s0, $v0, $v0, $v0, $ra $sp, $ra, $s0, $s0, g $v0, $ra, $s0, $sp, $ra

$a0, $s0, $a2, $v0,

$a3 0x1 $v0 $a1

$sp, 8 4($sp) 0($sp) $a2 $v0, $s0 4($sp) 0($sp) $sp, -8

2.21.4 [10]<2.8> En este código hay un error que viola las reglas de llamada de MIPS. Encuentre el error y corríjalo. 2.21.5 [10]<2.8> Traduzca el código a C. Suponga que los argumentos de las funciones del programa C se llaman a, b, c, etc. 2.21.6 [10]<2.8> Cuando se llama la función f, los registros $a0, $a1, $a2 y $a3

tienen los valores 1, 100, 1000 y 30, respectivamente. Determine el valor devuelto por la función. En caso de que haya una llamada a otra función g en f, suponga que el valor devuelto por g es siempre 500.

Ejercicio 2.22 Estos problemas estudian las conversiones a ASCII y Unicode. En la siguiente tabla se muestran algunas secuencias de caracteres. a.

A byte

b.

computer

2.21

Ejercicios

2.22.1 [5]<2.9> Traduzca esta secuencia a valores en bytes ASCII decimales. 2.22.2 [5]<2.9> Traduzca esta secuencia a Unicode de 16 bits (utilizando notación hexadecimal y el conjunto de caracteres Básico Latino).

La siguiente tabla muestra valores ASCII hexadecimales. a.

61 64 64

b.

73 68 69 66 74

2.22.3 [5]<2.5, 2.9> Traduzca los valores ASCII hexadecimales a texto.

Ejercicio 2.23 En estos problemas se le pedirá que escriba un programa ensamblador MIPS que convierta secuencias de caracteres a números. a.

Secuencia de enteros decimales positivos

b.

Enteros hexadecimales en complemento a 2

2.23.1 [10]<2.9> Escriba un programa ensamblador MIPS para convertir una secuencia de números ASCII en las condiciones mostradas en la tabla a un entero. La dirección de la secuencia de dígitos 0 a 9 está en el registro $a0. La secuencia termina con un null. El valor equivalente a esta secuencia de dígitos debe guardarse en el registros $v0. El programa debe poner el valor -1 en el registro $v0 y parar si se encuentra algún carácter que no sea un dígito. Por ejemplo, si el registro $a0 apunta a la secuencia de tres bytes 50diez, 52diez, 0diez (“24” terminado con null), al final del programa el registro $v0 debe tener el valor 24diez.

Ejercicio 2.24 Suponga que el registro $t1 tiene la dirección 0x1000 0000 y $t2 la dirección 0x1000 0010. a.

lb $t0, 0($t1) sw $t0, 0($t2)

b.

lb $t0, 0($t1) sb $t0, 0($t2)

203

204

Capítulo 2

Instrucciones: el lenguaje del computador

2.24.1 [5]<2.9> Suponga que el contenido (hexadecimal) de la dirección 0x1000 0000 es: 1000 0000

12

34

56

78

Suponiendo que el valor inicial en la posición de memoria apuntada por $t2 es 0xFFFF FFFF, ¿qué valor se almacena en la dirección apuntada por el registro $t2? 2.24.2 [5]<2.9> Suponga que el contenido (hexadecimal) de la dirección 0x1000 0000 es: 1000 0000

80

80

80

80

Suponiendo que el valor inicial en la posición de memoria apuntada por $t2 es 0x0000 0000, ¿qué valor se almacena en la dirección apuntada por el registro $t2? 2.24.3 [5]<2.9> Suponga que el contenido (hexadecimal) de la dirección 0x1000 0000 es: 1000 0000

11

00

00

FF

Suponiendo que el valor inicial en la posición de memoria apuntada por $t2 es 0x5555 5555, ¿qué valor se almacena en la dirección apuntada por el registro $t2?

Ejercicio 2.25 En estos problemas se estudia la utilización de constantes de 32 bits en MIPS, utilizando los valores binarios de la tabla. a.

1010 1101 0001 0000 0000 0000 0000 0010dos

b.

1111

1111 1111

1111 1111 1111 1111

1111dos

2.25.1 [10]<2.10> Escriba un código MIPS que cree las constantes de 32 bits de la tabla y las almacene en $t1. 2.25.2 [5]<2.6, 2.10> Si el valor actual del PC es 0x00000000, ¿se puede poner en el PC las direcciones de la tabla con una única instrucción de salto incondicional? 2.25.3 [5]<2.6, 2.10> Si el valor actual del PC es 0x00000600, ¿se puede poner en el PC las direcciones de la tabla con una única instrucción de salto condicional?

2.21

Ejercicios

2.25.4 [5]<2.6, 2.10> Si el valor actual del PC es 0x00400600, ¿se puede poner en el PC las direcciones de la tabla con una única instrucción de salto condicional? 2.25.5 [10]<2.10> Suponiendo que el campo inmediato de una instrucción MIPS tiene 8 bits, escriba un código MIPS que cree las constantes de 32 bits de la tabla y las almacene en $t1. No utilice la instrucción lui.

Para los siguientes problemas, utilice el código ensamblador MIPS de la tabla. a.

lui $t0, 0x1234 ori $t0, $t0, 0x5678

b.

ori $t0, $t0, 0x5678 lui $t0, 0x1234

2.25.6 [5]<2.6, 2.10> ¿Cuál es el valor en el registro $t0 después de la secuencia de código de la tabla? 2.25.7 [5]<2.6, 2.10> Escriba el código C equivalente al código ensamblador de la tabla. Suponga que la mayor constante que se puede cargar en un registro entero de 32 bits es de 16 bits.

Ejercicio 2.26 En estos problemas deberá analizar el rango de las instrucciones de salto incondicional y condicional. Utilice los valores hexadecimales de la tabla. a.

0x00001000

b.

0xFFFC0000

2.26.1 [10]<2.6, 2.10> Si el contenido del PC es 0x00000000, ¿cuántas instrucciones de salto condicional se necesitan para poner llegar a la dirección de la tabla? 2.26.2 [10]<2.6, 2.10> Si el contenido del PC es 0x00000000, ¿cuántas instrucciones de salto incondicional (no instrucciones de salto incondicional con registro) se necesitan para poner llegar a la dirección de la tabla? 2.26.3 [10]<2.6, 2.10> Para reducir el tamaño de los programas en ensamblador de MIPS, los diseñadores de MIPS decidieron reducir el tamaño del campo inmediato de las instrucciones tipo I de 16 a 8 bits. Si el contenido del PC es 0x00000000, ¿cuántas instrucciones de salto condicional se necesitan para poner llegar a la dirección de la tabla?

205

206

Capítulo 2

Instrucciones: el lenguaje del computador

En los siguientes problemas tendrá que utilizar las modificaciones a la arquitectura del repertorio de instrucciones MIPS mostradas en la tabla. a.

8 registros

b.

Campo de dirección/inmediato de 10 bits

2.26.4 [10]<2.6, 2.10> Si se modifica el repertorio de instrucciones del procesador, también hay que modificar el formato de las instrucciones. Para cada uno de los cambios de la tabla, ¿cuál es el impacto en el rango de direcciones de una instrucción beq? Suponga que las instrucciones siguen siendo de 32 bits y que cualquier cambio en la instrucciones tipo I aumentan/disminuyen solo el campo inmediato de la instrucción beq. 2.26.5 [10]<2.6, 2.10> Si se modifica el repertorio de instrucciones del procesador, también hay que modificar el formato de las instrucciones. Para cada uno de los cambios de la tabla, ¿cuál es el impacto en el rango de direcciones de una instrucción de salto incondicional? Suponga que las instrucciones siguen siendo de 32 bits y que cualquier cambio en la instrucciones tipo I aumentan/disminuyen solo el campo inmediato de la instrucción de salto incondicional. 2.26.6 [10]<2.6, 2.10> Si se modifica el repertorio de instrucciones del procesador, también hay que modificar el formato de las instrucciones. Para cada uno de los cambios de la tabla, ¿cuál es el impacto en el rango de direcciones de una instrucción de salto incondicional con registro? Suponga que las instrucciones siguen siendo de 32 bits.

Ejercicio 2.27 En los siguientes problemas deberá explorar los modos de direccionamiento de la arquitectura del conjunto de instrucciones MIPS mostrados en la tabla. a.

Direccionamiento de registro

b.

Direccionamiento relativo al PC

2.27.1 [5]<2.10> En la tabla se muestran algunos modos de direccionamiento de la arquitectura del conjunto de instrucciones MIPS. Ponga un ejemplo de instrucción MIPS con el modo de direccionamiento de la tabla. 2.27.2 [5]<2.10> Para las instrucciones del problema 2.27.1, determine el tipo de formato de instrucción utilizado. 2.27.3 [5]<2.10> Indique las ventajas e inconvenientes de los modos de direcciona-

miento de la tabla. Escriba un código MIPS que ilustre estas ventajas e inconvenientes.

2.21

Ejercicios

En los siguientes problemas deberá utilizar código ensamblador MIPS para explorar los pros y los contras del campo inmediato en las instrucciones tipo I. a.

0x00000000 0x00000004

lui ori

$s0, 100 $s0, $s0, 40

b.

0x00000100 0x00000104

addi lw

$t0, $0, 0x0000 $t1, 0x4000($t0)

2.27.4 [15]<2.10> Muestre la representación a nivel de bit de las instrucciones de la tabla. 2.27.5 [10]<2.10> Si se reduce el tamaño del campo inmediato de las instrucciones tipo I y tipo J podemos reducir el número de bits necesarios para representar instrucciones. Reescriba el código MIPS de la tabla suponiendo que el campo inmediato de las instrucciones tipo I y tipo J fuese de 8 bits. No utilizar la instrucción lui. 2.27.6 [5]<2.10> ¿Cuántos ciclos adicionales se necesitan para ejecutar el código del problema 2.27.5 respecto al código de la tabla?

Ejercicio 2.28 A continuación se muestra el código MIPS para un bloqueo (lock). try:

MOV MOV LL LL SC SC BEQZ MOV MOV

R3, R6, R2, R5, R3, R6, R3, R4, R7,

R4 R7 0(R2) 0(R1) 0(R1) 0(R1) try R2 R5

2.28.1 [5]<2.11> ¿Cuántas instrucciones hay que ejecutar para cada chequeo y fallo del almacenamiento condicional? 2.28.2 [5]<2.11> Indique por qué puede fallar este código. 2.28.3 [15]<2.11> Reescriba el código para que funcione correctamente. Evite las condiciones de carrera.

La siguiente tabla tiene fragmentos de código y contenidos de registros. La notación “($s1)” muestra el contenido de la posición de memoria apuntada por el registro $s1. Las instrucciones se ejecutan en el ciclo indicado en un procesador paralelo con espacio de memoria compartida.

207

208

Capítulo 2

Instrucciones: el lenguaje del computador

a.

Procesador 1 Procesador 1

Procesador 2

ll $t1, 0($s1)

ll $t1, 0($s1)

MEM

Procesador 2

Ciclo

$t1

$t0

$s1

$t1

$t0

0

1

2

99

30

40

1

sc $t0, 0($s1)

2 sc $t0, 0($s1)

3

b.

Procesador 1 Procesador 1

Procesador 2

Ciclo $s4 $t1 $t0 0

try: add $t0,$0,$s4

try: add $t0,$0,$s4

1

ll $t1, 0($s1)

2

ll $t1, 0($s1)

2

3

4

MEM Procesador 2 $t1 99

$s4 $t1 $t0 10

20

30

3

sc $t0, 0($s1)

4

beqz $t0, try

sc $t0, 0($s1)

5

add $s4, $0, $t1

beqz $t0, try

6

2.28.4 [5]<2.11> Rellene la tabla con el valor de los registros en cada ciclo.

Ejercicio 2.29 Los tres primeros problemas hacen referencia a una sección crítica de la forma lock(lk); operation unlock(lk);

donde “operation” actualiza una variable compartida shvar usando una variable local (no compartida) x, como se muestra en la tabla. Operación a.

shvar=shvar+x;

b.

shvar=min(shvar,x);

2.29.1 [10]<2.11> Escriba el código ensamblador MIPS para esta sección crítica suponiendo que la variable lk está en $a0, la dirección de shvar en $a1 y el valor de x en $a2. La sección crítica no debe tener ninguna llamada a funciones, es decir, se deben incluir instrucciones MIPS para lock( ), unlock( ), max( ) y min( ). Utilice instrucciones ll/sc para la implementación de lock( ); unlock( ) es simplemente una instrucción de almacenamiento ordinaria.

2.21

Ejercicios

2.29.2 [10]<2.11> Repita el problema 2.29.1, pero utilizando ll/sc para realizar una actualización atómica de shvar directamente, sin utilizar lock( ) y unlock( ). En este caso no es necesaria la variable lk. 2.29.3 [10]<2.11> Compare las prestaciones del mejor-caso de los códigos de los problemas 2.29.1 y 2.29.2, suponiendo que cada instrucción necesita un ciclo para ejecutarse. Nota: mejor-caso significa que ll/sc siempre se ejecuta con éxito, el bloqueo está siempre disponible cuando queremos ejecutar lock( ), y si hay un salto se toma siempre la vía que necesita ejecutar el menor número de instrucciones. 2.29.4 [10]<2.11> Usando el código de 2.29.2 como ejemplo, explique qué ocurre cuando dos procesadores intentan acceder a la sección crítica al mismo tiempo, suponiendo que cada procesador ejecuta exactamente una instrucción por ciclo. 2.29.5 [10]<2.11> Explique por qué en el código del problema 2.29.2 el registro $a1 tiene la dirección de la variable shvar y no el valor de esta variable, y el registro $a2 contiene la variable x y no su dirección.

2.29.6 [10]<2.11> Podemos hacer la misma operación atómicamente en dos variables compartidas (shvar1 y shvar2) en la misma sección crítica utilizando la aproximación de 2.29.1 (poniendo simplemente las dos actualizaciones entre las operaciones lock y su correspondiente unlock). Explique por qué no se puede hacer con la aproximación de 2.29.2, es decir, por qué no podemos utilizar ll/sc para acceder a ambas variables compartidas de forma que se garantice que las dos actualizaciones se van a ejecutar juntas como una única operación atómica.

Ejercicio 2.30 Las instrucciones pseudoensamblador no son parte del repertorio de instrucciones MIPS, pero aparecen frecuentemente en programas MIPS. La tabla muestra varias pseudoinstrucciones MIPS que serán traducidas a otras instrucciones MIPS. a.

move $t1, $t2

b.

beq $t1, small, LOOP

2.30.1 [5]<2.12> Para cada pseudoinstrucción de la tabla, obtenga un código MIPS que haga la misma función. Se pueden utilizar registros temporales si es necesario. En la tabla, large hace referencia a un número de 32 bits y small a un número de 16 bits.

La tabla muestra varias pseudoinstrucciones MIPS que serán traducidas a otras instrucciones MIPS.

209

210

Capítulo 2

Instrucciones: el lenguaje del computador

a.

la $s0, v

b.

blt $a0, $v0, LOOP

2.30.2 [5]<2.12> ¿Necesitan ser editadas durante la fase de enlazado. ¿Por qué?

Ejercicio 2.31 La tabla muestra algunos detalles del nivel de enlazado para dos procedimientos diferentes. En este ejercicio hará el papel del enlazador. Procedimiento A

a.

Segmento de texto

Segmento de datos Inform. de recolocación

Tabla de símbolos

Procedimiento B

Dirección

Instrucción

Dirección

0

lw $a0, 0($gp)

4

jal 0

...

...

0

(X)

...

...

Dirección

Tipo instr.

Dependencia

0

lw

X

4

jai

B

Dirección _

Símbolo

_

B

Segmento de texto

Segmento de datos Inform. de recolocación

Tabla de símbolos

X

0

sw $al, 0($gp)

4

jal 0

...

...

0

(Y)

...

...

Dirección

Tipo instr.

0

Sw

4

jal

Dirección _

Y

_

A

Procedimiento A

b.

Segmento de texto

Segmento de datos

Inform. de recolocación

Tabla de símbolos

Dirección

Instrucción

0

lui $at, 0

4

ori $a0, $at 0

8

jal 0

...

...

Dependencia

Símbolo

Procedimiento B

Segmento de texto

0

(X)

...

...

Dirección

Tipo instr.

Dependencia

0

lui

X

4

ori

X

8

jal

B

Dirección _

Símbolo

_

B

X

Instrucción

Segmento de datos

Inform. de recolocación

Tabla de símbolos

Dirección

Instrucción

0

sw $a0, 0($gp)

4

jmp 0

...

...

0x180

jr $ra

...

...

0

(Y)

...

...

Dirección

Tipo instr.

Dependencia

0

sw

Y

4

jmp

F00

Dirección _

Y

0x180

F00

Símbolo

2.21

Ejercicios

2.31.1 [5]<2.12> Enlace los ficheros objeto de la tabla para formar la cabecera de un fichero ejecutable. Suponga que el tamaño del texto y de datos del procedimiento A es 0x140 y 0x40, respectivamente, y que el tamaño del texto y de datos del procedimiento B es 0x300 y 0x50, respectivamente. Suponga también que la estrategia para colocación en memoria es la de la figura 2.13. 2.31.2 [5]<2.12> ¿Cuáles son las limitaciones en el tamaño de un ejecutable? 2.31.3 [5]<2.12> ¿Por qué podría tener problemas un ensamblador para implementar directamente las instrucciones de salto incondicional y salto condicional en el fichero objeto?

Ejercicio 2.32 En los tres primeros problemas de este ejercicio se supone que la función swap está definida con el siguiente código C, en lugar del código de la figura 2.24. a.

void swap(int v[], int k, int j){ int temp; temp=v[k]; v[k]=v[j]; v[j]=temp; }

b.

void swap(int *p){ int temp; temp=*p; *p=*(p+1);; *(p+1)=*p; }

2.32.1 [10]<2.13> Traduzca esta función a código ensamblador de MIPS. 2.32.2 [5]<2.13> ¿Qué hay que cambiar en la función sort? 2.32.3 [5]<2.13> Si estuviese ordenando bytes de 8 bits en lugar de palabras de 32 bits, ¿en qué cambiaría el código ensamblador de MIPS del problema 2.32.1?

Para los tres problemas restantes se supone que la función sort de la figura 2.27 se cambia de la siguiente forma: a.

Usar registros s en lugar de registros t

b.

Usar la instrucción bltz (branch on less than zero) en lugar de slt y bne en la etiqueta for2tst

2.32.4 [5]<2.13> ¿Este cambio afecta al código para almacenamiento y recuperación de registros de la figura 2.27?

211

212

Capítulo 2

Instrucciones: el lenguaje del computador

2.32.5 [10]<2.13> Al ordenar un vector de 10 elementos que ya ha sido ordenado, ¿cuántas instrucciones de más (o de menos) se ejecutan como resultado de este cambio? 2.32.6 [10]<2.13> Al ordenar un vector de 10 elementos que ya ha sido ordenado en orden descendente (contrario al orden creado por sort( )), ¿cuántas instrucciones de más (o de menos) se ejecutan como resultado de este cambio?

Ejercicio 2.33 Los problemas de este ejercicio hacen uso de la siguiente función:

a.

int find(int a[], int n, int x){ int i; for (i=0; i!=n; i++) if (a[i]==x) return i; return -1; }

b.

int count(int a[], int n, int x){ int res=0; int i; for (i=0; i!=n; i++) if (a[i]==x) res=res+1; return res; }

2.33.1 [10]<2.14> Traduzca esta función a código ensamblador de MIPS. 2.33.2 [10]<2.14> Traduzca esta función a código C basado en punteros. 2.33.3 [10]<2.14> Traduzca tu código C basado en punteros a código ensamblador de MIPS. 2.33.4 [5]<2.14> Compare el peor-caso en el número de instrucciones ejecutadas en las iteraciones que no son las últimas del lazo del código del problema 2.33.1 con el del código basado en punteros del problema 2.33.2. Nota: se considera el peor-caso como aquel en el que se toma la salida más larga en las instrucciones de salto condicional; por ejemplo, en caso de una sentencia if, el resultado de la comprobación de la condición es tal que se toma el camino con más instrucciones. Sin embargo, si el resultado de la comprobación de la condición llevase a salir del lazo, entonces se considera que se toma la salida que nos mantiene en el lazo. 2.33.5 [5]<2.14> Compare el número de registros temporales (registros t) que se necesitan en el código del problema 2.33.1 con los que se necesitan en el código basado en punteros de 2.33.3.

2.21

Ejercicios

2.33.6 [5]<2.14> Si los registros $t0-$t7 y $a0-$a7 en las reglas de llamada de MIPS fuesen guardados por el llamado como los registros $s0 - $s7, ¿qué cambiaría en la respuesta del problema 2.33.4?

Ejercicio 2.34 La siguiente tabla muestra código ensamblador de ARM. En los siguientes problemas tendremos que traducirlo a código MIPS. a. LOOP:

b.

MOV ADD SUBS BNE

r0, r0, r0, LOOP

#10 r1 1

; ; ; ;

iniciar el contador del lazo a 10 sumar r1 a r0 decrementar el contador se repite el lazo si Z=0

ROR

r1,

r2, #4

; r1 = r23:0 concatenado con r231:4

2.34.1 [5]<2.16> Traduzca este código ensamblador de ARM a código ensamblador de MIPS. Suponga que los registros r0, r1 y r2 de ARM tienen los mismos valores que los registros $s0, $s1 y $s2 de MIPS. Utilice registros temporales de MIPS ($t0, etc.) si es necesario. 2.34.2 [5]<2.16> Muestre los campos de bits que representan las instrucciones ARM para las instrucciones de la tabla.

La siguiente tabla muestra código ensamblador de MIPS. En los siguientes problemas tendremos que traducirlo a código de ARM. a.

slt $t0, $s0, $s1 blt $t0, 0, FARAWAY

b.

add $s0, $s1, $s2

2.34.3 [5]<2.16> Traduzca este código a código ensamblador de ARM. 2.34.4 [5]<2.16> Muestre los campos de bits que representan el código ensamblador de ARM.

Ejercicio 2.35 El procesador ARM tiene unos modos de direccionamiento que no están soportados en el MIPS. Los siguientes problemas exploran estos modos de direccionamiento. a.

LDR

r0, [r1]

; r0=memoria[r1]

b.

LDMIA

r0, [r1, r2, r4]

; r1=memoria[r0], r2=memoria[r0+4], ; r4=memoria[r0+8]

213

214

Capítulo 2

Instrucciones: el lenguaje del computador

2.35.1 [5]<2.16> Identifique el tipo de modo de direccionamiento de las instrucciones ensamblador de ARM de la tabla. 2.35.2 [5]<2.16> Para las instrucciones ensamblador de ARM de la tabla, escriba una secuencia de instrucciones ensamblador de MIPS que realicen la misma transferencia de datos.

En los siguientes problemas se comparan códigos escritos con los conjuntos de instrucciones de ARM y de MIPS. La tabla muestra fragmentos de códigos escritos con instrucciones de ARM. a. ADDLP:

b.

LDR LDR EOR LDR ADD ADD

r0, r1, r2, r4, r2, r0,

=Table1 #100 r2, r2 [r0] r2, r4 r0, #4

SUBS r1, r1, #1 BNE ADDLP

carga dirección base de una tabla inicia el contador del lazo borra r2 accede al primer operando suma a r2 incremento para siguiente elemento de la tabla ; decrementa el contador del lazo ; si contador de lazo !=0, salto a ADDLP

ROR

; r1 = r23:0 concatenado con r231:4

r1, r2, #4

; ; ; ; ; ;

2.35.3 [10]<2.16> Escriba una rutina en código ensamblador de MIPS equivalente al código ensamblador ARM de la tabla. 2.35.4 [5]<2.16> ¿Cuál es el número total de instrucciones ensamblador de ARM del código? ¿Cuál es el número total de instrucciones ensamblador de MIPS? 2.35.5 [5]<2.16> Suponga que el CPI medio de la rutina MIPS es igual al CPI medio de la rutina ARM, y que frecuencia del procesador MIPS es 1.5 veces la frecuencia del procesador ARM. Determine cuántas veces es más rápido el procesador ARM que el MIPS.

Ejercicio 2.36 El procesador ARM tiene una forma interesante de incluir constantes inmediatas. En este ejercicio se investigan estas diferencias. La tabla muestra instrucciones ARM. a.

ADD

r3, r2, r1, LSL #3

; r3=r2 + (r1 << 3)

b.

ADD

r3, r2, r1, ROR #3

; r3=r2 + (r1, rotado 3 bits a la derecha)

2.36.1 [5]<2.16> Escriba un código MIPS equivalente al código ARM de la tabla.

2.21

Ejercicios

2.36.2 [5]<2.16> Suponiendo que el contenido del registro R1 es 8, reescriba el código MIPS para minimizar el número de instrucciones ensamblador MIPS. 2.36.3 [5]<2.16> Suponiendo que el contenido del registro R1 es 0x06000000, reescriba el código MIPS para minimizar el número de instrucciones ensamblador MIPS.

La siguiente tabla tiene instrucciones MIPS. a.

addi $r3, r2, 0x1

b.

addi $r3, r2, 0x8000

2.36.4 [5]<2.16> Escriba un código ARM equivalente al código MIPS de la

tabla.

Ejercicio 2.37 Este ejercicio explora las diferencias entre los conjuntos de instrucciones MIPS y x86. La tabla contiene código ensamblador x86. mov edx, [esi+4*ebx]

a. b.

START:

mov mov mov and or

ax, cx, bx, ax, ax,

00101100b 00000011b 11110000b bx cx

2.37.1 [10]<2.17> Escriba un pseudocódigo para la rutina de la tabla. 2.37.2 [10]<2.17> Escriba un código MIPS equivalente a la rutina de la tabla.

La siguiente tabla contiene código ensamblador x86. a.

mov edx, [esi+4*ebx]

b.

mov eax, 0x12345678

2.37.3 [5]<2.17> Indique el tamaño de los campos de bits que representan cada una de las instrucciones de la tabla. Suponga que la etiqueta MY_FUNCTION es una constante de 32 bits. 2.37.4 [10]<2.17> Escriba instrucciones MIPS equivalentes.

215

216

Capítulo 2

Instrucciones: el lenguaje del computador

Ejercicio 2.38 El repertorio de instrucciones x86 incluye el prefijo REP, que hace que la instrucción se repita un número dado de veces o hasta que se satisfaga una condición. Los tres primeros problemas utilizan las siguientes instrucciones x86: Instrucción

Interpretación

a.

REP MOVSB

Repetir hasta que ECX =0: Mem8[EDI]=Mem8[ESI], EDI=EDI+1, ESI=ESI+1, ECX=ECX-1

b.

REP MOVSD

Repetir hasta que ECX =0: Mem32[EDI]=Mem32[ESI], EDI=EDI+4, ESI=ESI+4, ECX=ECX-1

2.38.1 [5]<2.17> ¿Cuál es la utilización típica de esta instrucción? 2.38.2 [5]<2.17> Escriba un código MIPS equivalente al de la tabla, suponiendo que $a0, $a1, $a2 y $a3 corresponden a ECX, EDI, ESI y EAX, respectivamente. 2.38.3 [5]<2.17> Si la instrucción x86 necesita un ciclo para leer la memoria, un ciclo para escribir en memoria y un ciclo para cada actualización de registro, y si MIPS necesita un ciclo por instrucción, ¿cuál es la aceleración al utilizar la instrucción x86 en lugar del código MIPS equivalente si ECX es muy grande? Suponga que los ciclo de reloj de MIPS y del x86 son iguales.

Los siguientes tres problemas de este ejercicio trabajan con la siguiente función, dada en C y en ensamblador de x86. Para cada instrucción x86 se muestra su longitud en el formato de instrucciones de longitud variable y la interpretación (lo que hace la instrucción). Obsérvese que la arquitectura x86 tiene muy pocos registros en comparación con la arquitectura MIPS y, en consecuencia, las reglas de llamada de x86 ponen todos los argumentos en la pila. El valor de retorno se devuelve al que hizo la llamada en el registro EAX. Código C

Código x86

a.

int f(int a, int b){ f: push %ebp ; 1B, lleva %ebp a la pila return a+b; mov %esp, %ebp ; 2B, mueve %esp a %ebp } mov 0xc(%ebp),%eax ; 3B, carga 2º argumento en %aex add 0x8(%ebp),%eax ; 3B, suma 1º argumento a %eax pop %ebp ; 1B, restaura %ebp ret ; vuelta de la rutina

b.

void f(int*a, int *b){ *a=*a+*b; *b=*a; }

f: push %ebp mov %esp, %ebp mov 8(%ebp), %eax mov mov add mov mov pop ret

; 1B, lleva %ebp a la pila ; 2B, mueve %esp a %ebp ; 3B, carga 1º argumento en %aex 12(%ebp), %ecx ; 3B, carga 2º argumento en %ecx (%eax), %edx ; 2B, carga *a en %edx (%ecx), %edx ; 2B, suma *b a %edx %edx, (%eax) ; 2B, almacena %edx en *a %edx, (%ecx) ; 2B, almacena %edx en *b %ebp ; 1B, restaura %ebp ; vuelta de la rutina

2.21

Ejercicios

2.38.4 [5]<2.17> Traduzca esta función a ensamblador de MIPS. Compare el tamaño (número de bytes de memoria de instrucciones) del código MIPS con el código x86. 2.38.5 [5]<2.17> Si el procesador tiene la capacidad de ejecutar dos instrucciones por ciclo, debe ser capaz de leer dos instrucciones consecutivas en cada ciclo. Explique cómo se puede hacer esto en MIPS y en x86. 2.38.6 [5]<2.17> Si cada instrucción MIPS necesita un ciclo para ejecutarse y cada instrucción x86 un ciclo más un ciclo adicional por cada lectura o escritura de memoria que tenga que hacer en la instrucción, ¿cuál es la aceleración al utilizar x86 en lugar de MIPS? Suponga que el ciclo del reloj es el mismo en ambos casos y que la ejecución se realiza por la vía más corta de la función (es decir, en cada lazo se hace una única iteración y la salida de cada sentencia if es la que lleva hacia el retorno de la función). Observe que la instrucción ret del x86 lee la dirección de retorno de la pila.

Ejercicio 2.39 La siguiente tabla muestra el CPI de varios tipos de instrucciones. Aritméticas

Carga/almacenamiento

Salto

a.

2

10

3

b.

1

10

4

2.39.1 [5]<2.18> Suponga un programa con la siguiente cuenta de instrucciones: Instrucciones (en millones) Aritméticas

500

Carga/almacenamiento

300

Salto

100

Determine el tiempo de ejecución en un procesador con los CPI de la tabla y una frecuencia de 1.5 GHz. 2.39.2 [5]<2.18> Suponga que se añaden nuevas y más potentes instrucciones aritméticas al conjunto de instrucciones. El uso de estas nuevas instrucciones nos permite reducir el número de instrucciones aritméticas del programa en un 25%, a costa de un incremento del 10% en el tiempo de ciclo de la señal de reloj. ¿Es una buena decisión de diseño? ¿Por qué?

217

218

Capítulo 2

Instrucciones: el lenguaje del computador

2.39.3 [5]<2.18> Suponga que se encuentra una forma de duplicar las prestaciones de las instrucciones aritméticas. ¿Cuál es la aceleración que se obtiene para nuestro procesador? ¿Qué sucede si se encuentra una forma de mejorar las prestaciones de las instrucciones aritméticas en un factor 10?

La siguiente tabla muestra el porcentaje de ejecución de instrucciones.

Aritméticas

Carga/almacenamiento

Salto

a.

60%

20%

20%

b.

80%

15%

5%

2.39.4 [5]<2.18> Dados los valores de la tabla y suponiendo que una instrucción aritmética tarda 2 ciclos, una de carga/almacenamiento 6 ciclos y una de salto 3 ciclos, determine el CPI medio. 2.39.5 [5]<2.18> Si queremos mejorar las prestaciones un 25%, ¿cuántos ciclos debería tardar las instrucciones aritméticas, en media, si no se mejoran ni las instrucciones de salto ni las de carga/almacenamiento? 2.39.6 [5]<2.18> Si queremos mejorar las prestaciones un 50%, ¿cuántos ciclos debería tardar las instrucciones aritméticas, en media, si no se mejoran ni las instrucciones de salto ni las de carga/almacenamiento?

Ejercicio 2.40 Los tres primeros problemas de este ejercicio utilizan la siguiente función ensamblador MIPS. Desafortunadamente, el programador ha cometido el error de suponer que MIPS se basa en el direccionamiento de palabras, cuando en realidad se basa en direccionamiento de bytes.

a.

: int f(int a[], int n, int x); f: move $v0, $zero ; ret=0 move $t0, $zero ; i=0 L: add $t1, $t0, $a0 ; &(a[i]) lw $t1, 0($t1) ; read a[i] bne $t1, $a2, S ; if (a[i]==x) addi $v0, $v0, 1 ; ret++ S: addi $t0, $t0, 1 ; i++ bne $t0, $a1, L ; repeat if i!=n jr $ra ; return ret

2.21

b.

Ejercicios

: void f(int *a, int *b, int n); f: move $t0, $a0 ; p=a move $t1, $a1 ; q=b add $t2, $a2, $a0 ; &(a[n]) L: lw $t3, 0($t0) ; read *p lw $t4, 0($t1) ; read *q add $t3, $t3, $t4 ; *p+*q sw $t3, 0($t0) ; *p=*p+*q addi $t0, $t0, 1 ; p=p+1 addi $t1, $t1, 1 ; rq=q+1 bne $t0, $t2, L ; repeat if p!=&(a[n] jr $ra ; return

Observe que “;” significa que el resto de la línea es un comentario. 2.40.1 [5]<2.18> Para que los accesos a palabras de memoria en la arquitectura MIPS (instrucciones sw y lw) se ejecuten correctamente las direcciones deben estar alineadas, es decir, los dos bits menos significativos de la dirección deben ser cero. En caso contrario, se produce una excepción “bus error”. Explique cómo afecta este requerimiento a la ejecución de la función de la tabla. 2.40.2 [5]<2.18> Si “a” es un puntero que apunta al comienzo de un vector de elementos de 1 byte y si reeplazamos lw y sw por lb (carga de un byte) y sb (almacenamiento de un byte), respectivamente, ¿funcionaría correctamente la función? Nota: lb lee un byte de memoria, hace extensión de signo y lo carga en el registro destino; sb almacena el byte menos significativo del registro en memoria. 2.40.3 [5]<2.18> Cambie el código para que funcione correctamente con enteros de 32 bits.

Los siguientes tres problemas hacen uso de un programa que reserva memoria para un vector, da valores a los elementos del vector, llama a la función sort de la figura 2.27 e imprime en pantalla el vector. La función principal del programa es la siguiente (en C y código MIPS). Código principal en C main(){ int *v; int n=5; v=my_alloc(5); my_init(v,n); sort(v,n); . . .

Versión MIPS del código principal main: li move jal move move move jal move move jal

$s0,5 $a0, $s0 my_alloc $s1, $v0 $a0, $s1 $a1, $s0 my_init $a0, $s1 $a1, $s0 sort

219

220

Capítulo 2

Instrucciones: el lenguaje del computador

La función my_alloc se define en la tabla. Observe que el programador ha cometido el error de utilizar un puntero a una variable automática arr fuera de la función en la que se ha definido. my_alloc en C

Código MIPS para my_alloc

int *my_alloc(int n){ int arr[n]; return arr; }

my_alloc: addu $sp, sw $fp, move $fp, sll $t0, sub $sp, move $v0, move $sp, lw $fp, addiu $sp, jr $ra

$sp, -4 ; puntero de pila 0($sp) ; $fp a la pila $sp ; guarda $sp en $fp $a0, 2 ; se necesitan 4*n bytes $sp, $t0 ; espacio para arr $sp ; dirección de retorno de arr $fp ; restaura $sp desde $fp 0($sp) ; Extrae $fp $sp, 4 ; de la pila

A continuación se muestra la función my_init en código MIPS.

a.

b.

my_init: move move L: sw addiu addiu bne jr

$t0, $zero $t1, $a0 $zero, 0($t1) $t1, $t1, 4 $t0, $t0, 1 $t0, $a1, L $ra

; i=0

my_init: move move L: sub sw addiu addiu bne jr

$t0, $t1, $t2, $t2, $t1, $t0, $t0, $ra

; i=0

$zero $a0 $a1, $t0 0($t1) $t1, 4 $t0, 1 $a1, L

; v[i]=0 ; i=i+1 ; until i==n

; a[i]=n-i ; i=i+1 ; until i==n

2.40.4 [5]<2.18> ¿Cuál es el contenido (valores de los cinco elementos) del vector v antes de la ejecución de la instrucción “jal sort” en el código principal? 2.40.5 [5]<2.18, 2.13> ¿Cuál es el contenido del vector v antes de que se entre por primera vez en el lazo más externo de la función sort? Suponga que los valores en los registros $sp, $s0, $s1, $s2 y $s3 son 0x10000, 20, 40, 7 y 1, respectivamente, al comienzo del código principal (antes de la ejecución de “li $s0, 5”). 2.40.6 [5]<2.18, 2.13> ¿Cuál es el contenido del vector de 5 elementos apuntado por v justo después de que “jal sort” devuelva el control al código principal?

2.21

Ejercicios

221

$2.2, página 80: MIPS, C, Java. Respuestas a las $2.3, página 87: 2) muy lento. autoevaluaciones $2.4, página 93: 3) -8diez. $2.5, página 101: 4) sub $s2, $s0, $s1. $2.6, página 104: Ambos. AND con una máscara de unos pondrá ceros en todas parte excepto en el campo deseado e indicado por la máscara. Desplazando a la izquierda el número de posiciones correcto elimina los bits a la izquierda del campo. Desplazando a la derecha el número de posiciones correcto sitúa el campo en los bits más a derecha de la palabra. Observe que la operación AND deja el campo donde ya estaba originalmente, y que los desplazamientos trasladan el campo a los bits más a la derecha de la palabra. $2.7, página 111: I. Todos son correctos. II. 1). $2.8, página 122: Ambos son verdad. $2.9, página 127: I. 2) II. 3). $2.10, página 136: I. 4) +-128K. II. 6) un bloque de 256M. III.4) sll. $2.11, página 139: Ambos son verdad. $2.12, página 148: 4) Independencia de la máquina.

3 Aritmética para computadores

La precisión numérica es el alma verdadera de la ciencia Sir D’arcy Wentworth Thompson On Growth and Form, 1917

3.1

Introducción 224

3.2

Suma y resta 224

3.3

Multiplicación 230

3.4

División 236

3.5

Punto flotante 242

3.6

Paralelismo y aritmética del computador:

3.7

Caso real: punto flotante en el x86 272

3.8

Falacias y errores habituales 275

asociatividad 270

3.9

Conclusiones finales 280

3.10

Perspectiva histórica y lecturas recomendadas 283

3.11

Ejercicios 283

Nota importante: En la presente edición en castellano, los contenidos del CD incluido en la edición original (en inglés) son accesibles a través de la página web www.reverte.com/microsites/pattersonhennessy. Aunque en la presente edición no se proporciona un CD-ROM físico, a lo largo de todo el texto se menciona el CD y se utiliza el icono que lo representa para hacer referencia a su contenido.

Los cinco tipos de componentes de un computador

224

Capítulo 3

3.1

Aritmética para computadores

Introducción

3.1

Las palabras del computador están formadas por bits; de este modo, las palabras se pueden representar como números binarios. Aunque los números naturales 0, 1, 2, … se pueden representar tanto en forma decimal como binaria, ¿qué podemos decir de los otros números que usamos comúnmente? Por ejemplo: ■

¿Qué podemos decir de las fracciones y los números reales?



¿Qué pasa si una operación genera un número más grande de lo que se puede representar?



Y detrás de todas estas cuestiones hay un misterio: ¿cómo multiplica o divide números el hardware?

El objetivo de este capítulo es desvelar este misterio, incluyendo la representación de números, los algoritmos aritméticos, el hardware que implementa estos algoritmos y las implicaciones de todo ello en los repertorios de instrucciones. Estas revelaciones pueden incluso explicar peculiaridades de los computadores con las que ya haya tropezado.

Resta: la compañera tramposa de la suma DAVID LETTERMAN et al., Núm.10, Los diez mejores cursos para atletas en una factoría de fútbol, Book of Top Ten Lists, 1990.

3.2

Suma y resta

3.2

La suma es justo lo que se esperaría de ella en los computadores. Los dígitos se suman bit a bit de derecha a izquierda, pasando los arrastres o acarreos (carries) al próximo dígito de la izquierda, tal y como se hace a mano. La resta usa la suma: el operando apropiado simplemente se niega antes de ser sumado.

Suma y resta binaria

EJEMPLO

Sumemos 6diez a 7diez en binario y luego restemos 6diez de 7diez en binario. +

0000 0000 0000 0000 0000 0000 0000 0111dos = 7diez 0000 0000 0000 0000 0000 0000 0000 0110dos = 6diez

=

0000 0000 0000 0000 0000 0000 0000 1101dos = 13diez

Sobre los cuatro bits de la derecha recae toda la acción; la figura 3.1 muestra las sumas y los acarreos. Los acarreos se muestran entre paréntesis, con flechas que muestran cómo se pasan.

3.2

225

Suma y resta

Restar 6diez de 7diez se puede hacer directamente:

RESPUESTA



0000 0000 0000 0000 0000 0000 0000 0111dos = 7diez 0000 0000 0000 0000 0000 0000 0000 0110dos = 6diez

=

0000 0000 0000 0000 0000 0000 0000 0001dos = 1diez

o vía suma, usando la representación en complemento a dos de –6diez: +

0000 0000 0000 0000 0000 0000 0000 0111dos = 7diez 1111 1111 1111 1111 1111 1111 1111 1010dos = –6diez

=

0000 0000 0000 0000 0000 0000 0000 0001dos = 1diez

(0) 0 0 (0) 0

(0) 0 0 (0) 0

(1) 0 0 (0) 1

(1) 1 1 (1) 1

(0) 1 1 (1) 0

(Acarreos)

(0)

1 0 1

FIGURA 3.1 Suma binaria, mostrando los acarreos de derecha a izquierda. El bit situado más a la derecha suma 1 a 0, cuyo resultado es 1 y el acarreo producido por este bit es 0. Por tanto, la operación para el segundo bit de la derecha es 0 + 1 + 1. Esto genera un 0 para este bit de suma y un acarreo de 1. El tercer bit es la suma de 1 + 1 + 1, resultando un acarreo de 1 y un bit de suma de 1. El cuarto bit es 1 + 0 + 0, lo que da un 1 de suma sin acarreo.

Recordemos que el desbordamiento ocurre cuando el resultado de una operación no puede ser representado con el hardware disponible, en este caso una palabra de 32 bits. ¿Cuándo puede producirse desbordamiento en la suma? Cuando se suman operandos de signo diferente no puede producirse desbordamiento. La razón es que la suma no puede ser mayor que uno de los operandos. Por ejemplo, –10 + 4 = –6. Puesto que los operandos caben en 32 bits y la suma no es mayor que ninguno de los operandos, la suma debe caber en 32 bits también. Por lo tanto, no puede producirse desbordamiento cuando se suman operandos positivos y negativos. Existen restricciones similares para la aparición de desbordamiento en la resta, pero es justo el principio contrario: cuando el signo de los operandos es el mismo, no se puede producir desbordamiento. Para verlo, recuerde que x – y = x + (–y), porque restamos negando el segundo operando y entonces sumamos. Así, cuando restamos operandos del mismo signo, al final acabamos sumando operandos de signo diferente. Del párrafo anterior, sabemos que en este caso tampoco se puede producir desbordamiento. Aunque hemos visto cuándo no se puede producir desbordamiento en la suma y la resta, aún no hemos respondido a la pregunta de cómo detectarlo cuando sí ocurre. Claramente, la suma o resta de dos números de 32 bits puede dar un resul-

226

Capítulo 3

Aritmética para computadores

tado que necesite 33 bits para ser representado completamente. La falta del bit 33º bit significa que cuando ocurre desbordamiento el bit de signo toma el valor del resultado en lugar del signo correcto del resultado. Puesto que necesitamos justo un bit extra, sólo el bit de signo puede estar incorrecto. El desbordamiento se produce cuando se suman dos números positivos y la suma es negativa, o viceversa. Esto significa que se ha producido un acarreo saliente en el bit de signo. El desbordamiento se produce en la resta cuando restamos un número negativo de un número positivo y obtenemos un resultado negativo, o cuando restamos un número positivo de uno negativo y obtenemos un resultado positivo. Esto significa que se ha producido un acarreo en el bit de signo. La figura 3.2 muestra las combinaciones de las operaciones, operandos y resultados que producen desbordamiento. Acabamos de ver cómo se detecta el desbordamiento para números en complemento a dos en un computador. ¿Qué pasa con los números sin signo? Los números sin signo se usan comúnmente para direcciones de memoria donde los desbordamientos se ignoran. El diseñador del computador debe, por tanto, proporcionar una manera de ignorar el desbordamiento en algunos casos y reconocerlo en otros. La solución de MIPS es tener dos tipos de instrucciones aritméticas para reconocer las dos alternativas: ■

Suma (add), suma inmediato (addi) y resta (sub) provocan excepciones cuando hay desbordamiento.



Suma sin signo (addu), suma inmediato sin signo (addiu) y resta sin signo (subu) no causan excepciones cuando hay desbordamiento.

Puesto que C ignora los desbordamientos, los compiladores de C de MIPS siempre generarán la versión sin signo de las instrucciones aritméticas addu, addiu y subu sin importar el tipo de las variables. Los compiladores Fortran de MIPS, sin embargo, escogen las instrucciones aritméticas apropiadas, dependiendo del tipo de los operandos.

Operación

Operando A

Operando B

A+B

v0

v0

A+B

<0

<0

A–B

v0

A–B

<0

Resultado que indica desbordamiento <0

v0

<0

<0

v0

v0

Unidad Aritmética y Lógica (ALU): hardware

FIGURA 3.2

para hacer sumas y restas y habitualmente operaciones lógicas como AND y OR.

El apéndice C describe el hardware para realizar sumas y restas. Recibe el nombre de Unidad Aritmética y Lógica, ALU.

Condiciones de desbordamiento para la suma y la resta.

3.2

227

Suma y resta

El diseñador del computador debe decidir cómo manejar los desbordamientos aritméticos. Aunque algunos lenguajes como C ignoran los desbordamientos entre enteros, lenguajes como Ada y Fortran requieren que se le notifique al programa el desbordamiento. El programador o el entorno de programación deben decidir qué hacer cuando ocurre desbordamiento. MIPS detecta el desbordamiento mediante una excepción, también llamada interrupción en muchos computadores. Una excepción o interrupción es esencialmente una llamada a procedimiento no prevista. La dirección de la instrucción que produjo desbordamiento se guarda en un registro, y el computador salta a una dirección predefinida para invocar a la rutina apropiada para esta excepción. La dirección interrumpida se guarda para que en algunas situaciones el programa pueda continuar después de ejecutar cierto código de correción. (La sección 4.9 cubre las excepciones con más detalle; los capítulos 5 y 6 describen otras situaciones donde pueden ocurrir excepciones e interrupciones.) MIPS incluye un registro llamado contador de programa de excepción (exception program counter, EPC) para almacenar la dirección de la instrucción que ha causado la excepción. La instrucción mover desde el control del sistema (move from system control, mfc0) se usa para copiar el EPC a un registro de propósito general para que el software MIPS tenga la opción de volver a la instrucción que produjo la excepción con una instrucción de salto a través de registro.

Aritmética para multimedia Dado que todos los microprocesadores de computadores de sobremesa tienen su propio monitor gráfico, y que la disponibilidad de transistores ha aumentado, ha sido inevitable la incorporación hardware de apoyo para operaciones gráficas. En muchos sistemas, se utilizaron originalmente 8 bits para representar cada uno de los tres colores básicos más otros 8 bits para la posición del píxel. Posteriormente, con la utilización de micrófonos y altavoces para teleconferencia y videojuegos se pensó en añadir también apoyo para el sonido. Para audio se necesitan más de 8 bits, pero con16 bits es suficiente. Todos los microprocesadores incluyen apoyo para que los datos tipo byte y media palabra ocupen menos espacio en memoria (véase sección 2.9), pero puesto que las operaciones aritméticas con datos de este tamaño en programas con enteros son muy infrecuentes, el apoyo disponible no va más allá de la transferencia de datos. Los arquitectos se dieron cuenta de que muchas aplicaciones de gráficos y audio podrían hacer las operaciones sobre vectores de datos de esos tamaños. Un procesador podría hacer operaciones simultáneas sobre vectores de ocho operandos de 8 bits, cuatro operandos de 16 bits o dos operandos de 32 bits, simplemente rompiendo la cadena de propagación del acarreo de 64 bits. El coste de esta partición es pequeño. Estas extensiones se llamaron extensiones vectoriales o SIMD, de instrucción única, múltiples datos (single instruction, multiple data) (véase la sección 2.17 y el capítulo 7).

Interfaz hardware software Excepción (también llamada interrupción): evento no previsto que interrumpe la ejecución del programa; se usa para detectar desbordamiento.

Interrupción: excepción que viene del exterior del procesador. (Algunas arquitecturas usan el término interrupción para todas las excepciones).

228

Capítulo 3

Aritmética para computadores

Otra característica, que no está disponible en los procesadores de propósito general, son las operaciones con saturación. Saturación quiere decir que cuando se produce desbordamiento en una operación, se fuerza a que el resultado sea el mayor número positivo o el menor número negativo (el más negativo), en lugar del resultado de una operación módulo como en complemento a 2. La saturación es necesaria en operaciones de audio o gráficos. Por ejemplo, la utilización del mando de volumen en una radio sería muy frustrante si cuando estamos subiendo volumen llega un momento en que éste desciende repentinamente a un nivel muy bajo. Un mando de volumen con saturación pararía al nivel máximo aunque siguiésemos intentando subirlo con el mando. En la figura 3.3 se muestran las operaciones aritmética y lógicas habituales en muchas extensiones multimedia de los repertorios de instrucciones modernos. Categoría de la instrucción

Operandos

Suma/resta sin signo

Ocho de 8 bits o cuatro de 16 bits

Suma/resta con saturación

Ocho de 8 bits o cuatro de 16 bits

Max/min/mínimo

Ocho de 8 bits o cuatro de 16 bits

Media

Ocho de 8 bits o cuatro de 16 bits

Desplazamiento a la derecha/izquierda

Ocho de 8 bits o cuatro de 16 bits

FIGURA 3.3 Resumen del apoyo multimedia para computadores de sobremesa.

Extensión: MIPS puede saltar al producirse desbordamiento, pero a diferencia de muchos otros computadores no hay saltos condicionales para detectar el desbordamiento. Una secuencia de instrucciones MIPS puede descubrirlo. Para la suma con signo, la secuencia es la siguiente (véase el apartado Extensión sobre las instrucciones lógicas del capítulo 2 para la definición de las instrucciones xor): addu xor slt bne xor

$t0, $t3, $t3, $t3, $t3,

slt bne

$t3, $t3, $zero $t3, $zero, Desbordamiento

$t1, $t1, $t3, $zero, $t0,

$t2 $t2 $zero No_desbordamiento $t1

# # # # # # # #

$t0 = suma, pero sin detectar comprueba si los signos son distintos $t3 = 1 si los signos difieren los signos de $t1 y $t2 |, no hay desbordamiento los signos son iguales, ¿el de la suma también? $t3 negativo si el signo de la suma es distinto $t3 = 1 si el signo de la suma es diferente los tres signos |; saltar a desbordamiento

Para la suma sin signo ($t0 = $t1 + $t2), la comprobación es addu nor

$t0, $t1, $t3, $t1,

$t2 $zero

sltu

$t3, $t3

$t2

bne

$t3 $zero, Desbordamiento

# # # # # #

$t0 = suma $t3 = NOT $t1 (comp a 2 – 1: 232 – $t1 – 1) (232 – $t1 – 1) < $t2 ¡ 232 – 1 < $t1 + $t2 si(232–1 < $t1 + $t2) ir a desbordamiento

3.2

Suma y resta

229

Resumen El punto principal de esta sección es que, independientemente de la representación, el tamaño finito de palabra de los computadores significa que las operaciones aritméticas pueden producir resultados que son demasiado largos para caber en ese tamaño de palabra fijo. Es fácil detectar desbordamiento en números sin signo, aunque éstos se ignoran casi siempre porque los programas no necesitan detectar desbordamiento en la aritmética de direcciones, el uso más común de los números naturales. El complemento a dos presenta un reto mayor, pero dado que algunos sistemas software requieren la detección de desbordamiento, hoy en día todos los computadores tienen una manera de detectarlo. La creciente popularidad de las aplicaciones multimedia ha llevado a instrucciones aritméticas que utilizan operaciones más estrechas (con menos bits) que pueden operar fácilmente en paralelo. Algunos lenguajes de programación permiten aritmética entera en complemento a dos sobre variables declaradas tipo “byte y medio”. ¿Qué instrucciones MIPS se deberían usar? 1. Carga con lbu, lhu; aritmética con add, sub, mult, div; y almacenamiento usando sb, sh. 2. Carga con lb, lh; aritmética con add, sub, mult, div; y almacenamiento usando sb, sh. 3. Carga con lb, lh; aritmética con add, sub, mult, div, usando and para enmascarar el resultado a 8 o 16 bits después de cada operación; y almacenamiento usando sb, sh. Extensión: En el texto precedente dijimos que se copia el EPC en un registro con mfc0 y entonces retornamos al código interrumpido con un salto por registro. Esto conduce a una pregunta interesante: puesto que primero se debe transferir el EPC a un registro para usarlo con un salto por registro, ¿cómo puede el salto por registro volver al código interrumpido y restaurar los valores originales de todos los registros? O se restauran los registros antiguos en primer lugar, destruyendo consecuentemente el valor de retorno del EPC que se colocó en un registro para usar en el salto por registro, o se restauran todos los registros excepto el que guarda la dirección de retorno de manera que se pueda realizar el salto –¡lo que significa que una excepción cambiaría un registro en cualquier momento durante la ejecución de un programa! –. Ninguna de las dos opciones es aceptable. Para rescatar al hardware de este problema, los programadores MIPS se han puesto de acuerdo en reservar los registros $k0 y $k1 para el sistema operativo; estos registros no se restauran en las excepciones. Así como los compiladores MIPS evitan usar el registro $at de manera que el ensamblador pueda usarlo como registro temporal (véase la sección Interfaz hardware software en la sección 2.10), los compiladores también se abstienen de usar los registros $k0 y $k1 para que estén disponibles para el sistema operativo. Las rutinas de excepciones colocan la dirección de retorno en uno de estos registros y entonces usan un salto por registro para restaurar la dirección de la instrucción.

Extensión: La velocidad de la suma se puede aumentar determinado el acarreo de entrada a los bits de mayor peso cuánto antes. Hay varios esquemas de anticipación de acarreo de forma que, en el peor caso, el retraso es proporcional al log2 del número de

Autoevaluación

230

Capítulo 3

Aritmética para computadores

bits del sumador. La propagación de estas señales anticipadas es más rápida porque atraviesan menos puertas, pero la anticipación del acarreo real necesita muchas más puertas. El esquema más popular es carry lookahead, que se describe en la sección C.6 del apéndice C en el CD.

La multiplicación es un fastidio, la división es un mal; la regla de tres me deja perplejo, y los ejercicios me enloquecen. Anónimo, manuscrito de la época de la reina Elisabeth, 1570

3.3

Multiplicación

3.3

Ahora que hemos completado la explicación de la suma y la resta, estamos preparados para construir la más fastidiosa operación de multiplicación. Pero primero revisemos la multiplicación de números decimales manual para recordar los pasos y los nombres de los operandos. Por razones que aparecerán claras pronto, limitamos este ejemplo decimal al uso de los dígitos 0 y 1. Multiplicar 1000diez por 1001 diez: Multiplicando Multiplicador

x

1000diez 1001diez 1000 0000 0000 1000

Producto

1001000diez

El primer operando se llama multiplicando y el segundo multiplicador. El resultado final se llama producto. Tal y como podrá recordar, el algoritmo que se aprende en la enseñanza primaria es tomar los dígitos del multiplicador de uno en uno de derecha a izquierda, multiplicar el multiplicando por cada dígito del multiplicador y desplazar el producto parcial un dígito a la izquierda de los productos parciales anteriores. La primera observación es que el número de dígitos del producto es considerablemente mayor que el número de dígitos tanto en el multiplicando como el multiplicador. De hecho, si ignoramos los bits de signo, la longitud de una multiplicación de un multiplicando de n bits y un multiplicador de m bits es de n + m bits. Esto es, n + m bits son necesarios para representar todos los posibles productos. Por ello, como la suma, la multiplicación debe tener en cuenta el desbordamiento, porque frecuentemente queremos un producto de 32 bits como resultado de multiplicar dos números de 32 bits. En este ejemplo restringimos los dígitos decimales a 0 y 1. Con sólo dos alternativas, cada paso de la multiplicación es simple: 1. Colocar una copia del multiplicando (1 × multiplicando) en el lugar adecuado si el dígito del multiplicador es 1, o 2. Colocar un 0 (0 × multiplicando) en el lugar correcto si el dígito es 0. Aunque el ejemplo decimal anterior hacía uso sólo de 0 y 1, la multiplicación de números binarios debe usar 0 y 1 siempre, y por eso ofrece sólo estas dos opciones siempre.

3.3

231

Multiplicación

Una vez que hemos visto las bases de la multiplicación, tradicionalmente el próximo paso ha sido proporcionar el hardware de multiplicación altamente optimizado. Rompemos con la tradición, convencidos de que conseguirá un mejor entendimiento, viendo la evolución del hardware de multiplicación y el algoritmo a lo largo de múltiples generaciones. Por ahora, asumamos que estamos multiplicando sólo números positivos.

Versión secuencial del algoritmo y el hardware de multiplicación Este diseño imita el algoritmo que todos nosotros aprendimos en primaria; el hardware se muestra en la figura 3.4. Hemos dibujado el hardware de manera que los datos fluyan de arriba hacia abajo para que se parezca más al método de lápiz y papel. Supongamos que el multiplicador está en el registro de 32 bits “Multiplicador” y que el registro de 64 bits “Producto” tiene un valor inicial igual a 0. Del ejemplo de lápiz y papel anterior, está claro que necesitaremos mover el multiplicando a la izquierda un dígito cada paso para que pueda ser añadido a los productos intermedios. A lo largo de 32 pasos un multiplicando de 32 bits se movería 32 bits a la izquierda. Por esto, necesitamos un registro multiplicando de 64 bits, cuyo valor inicial es el multiplicando de 32 bits a la derecha y 0s en la mitad izquierda. Este registro se desplaza un bit a la izquierda en cada paso para alinear el multiplicando con la suma que está siendo acumulada en el registro “Producto” de 64 bits. La figura 3.5 muestra los tres pasos básicos necesarios para cada bit. El bit menos significativo del multiplicador (Multiplicador0) determina si el multiplicando se suma al registro producto. El desplazamiento a la izquierda del paso 2 tiene el efecto de mover los operandos intermedios a la izquierda, justo como cuando se multi-

Multiplicando Desp. Izq. 64 bits

ALU de 64 bits

Multiplicador Desp. Der. 32 bits

Producto Escribir

Control comprobación

64 bits FIGURA 3.4 Primera versión del hardware de multiplicación. El registro multiplicando, la ALU y el registro producto tienen un ancho de 64 bits, y sólo el multiplicador contiene 32 bits. (El apéndice C describe la ALU.) El multiplicando de 32 bits empieza en la mitad derecha del registro multiplicando y se desplaza a la izquierda 1 bit en cada paso. El multiplicador se desplaza en la dirección opuesta a cada paso. El algoritmo empieza con el producto iniciado a cero. El control decide cuándo desplazar los registros multiplicando y multiplicador y cuándo escribir nuevos valores en el registro producto.

232

Capítulo 3

Aritmética para computadores

plica a mano. El desplazamiento a la derecha en el paso 3 nos da el siguiente bit del multiplicador que se debe examinar en la siguiente iteración. Estos tres pasos se repiten 32 veces para obtener el producto. Si cada paso tarda un ciclo de reloj, este algoritmo requeriría al menos 100 ciclos de reloj para multiplicar 2 números de 32 bits. La importancia relativa de las operaciones aritméticas como la multiplicación varía según el programa, pero la suma y la resta pueden ser en cualquier parte entre

Inicio

1 Multiplicador0 = 1

Comprobar

Multiplicador0 = 0

Multiplicador0

1a. Sumar el multiplicando al producto y guardar el resultado en el resgistro Producto

2. Desplazar el registro multiplicando 1 bit a la izquierda

3. Desplazar el registo multiplicador 1 bit a la derecha

No: < 32 repeticiones ¿32ª repetición?

Sí: 32 repeticiones

Fin FIGURA 3.5 Primer algoritmo de multiplicación, que utiliza el hardware mostrado en la figura 3.4. Si el bit menos significativo del multiplicador es 1, se suma el multiplicando al producto. Si no, se va al siguiente paso. En los dos próximos pasos, se desplaza el multiplicando a la izquierda y el multiplicador a la derecha. Estos tres pasos se repiten 32 veces.

3.3

233

Multiplicación

5 y 100 veces más frecuentes que la multiplicación. Consecuentemente, en muchas aplicaciones, las multiplicaciones pueden tardar varios ciclos de reloj sin afectar significativamente a las prestaciones. Sin embargo, la ley de Amdahl (véase la sección 1.8) nos recuerda que incluso una frecuencia moderada para una operación lenta puede limitar las prestaciones. Este algoritmo y su hardware se pueden refinar fácilmente para que tarde un ciclo de reloj en cada paso. El aumento de velocidad se obtiene al realizar las operaciones en paralelo: el multiplicador y multiplicando se desplazan mientras el multiplicando se suma al producto si el bit del multiplicador es 1. El hardware sólo tiene que asegurarse de que comprueba el bit correcto del multiplicador y de que obtiene la versión del multiplicando antes del desplazamiento. El hardware se suele optimizar aún más partiendo por la mitad el tamaño del sumador y los registros al fijarse dónde hay trozos no usados de los registros y del sumador. La figura 3.6 muestra el hardware modificado.

En el caso de multiplicaciones por constantes, es posible reemplazar el cálculo aritmético por desplazamientos. Algunos compiladores reemplazan multiplicaciones por constantes cortas por una serie de desplazamientos y sumas. Puesto que en base 2 un bit a la izquierda representa un número dos veces mayor, desplazar los bits a la izquierda tiene el mismo efecto que multiplicar por una potencia de 2. Tal y como se mencionó en el capítulo 2, casi todos los compiladores implementan la optimización de reducción de esfuerzo que supone sustituir una multiplicación por una potencia de 2 por un desplazamiento a la izquierda.

Multiplicando 32 bits

ALU de 32 bits

Producto

Desp.der. Escribir

Control comprobación

64 bits

FIGURA 3.6 Versión refinada del hardware de multiplicación. Compárela con la primera versión de la figura 3.4. El registro multiplicando, la ALU y el registro multiplicador tienen un ancho de 32 bits, y sólo el registro producto sigue siendo de 64 bits. Ahora el producto se desplaza a la derecha. El registro multiplicador separado también ha desaparecido. En su lugar, el multiplicador se almacena en la mitad derecha del registro del producto. Estos cambios se destacan en color. El registro Producto debería tener, en realidad, 65 bits para almacenar el acarreo de salida del sumador, pero se muestra con sólo 64 bits para destacar la evolución de la figura 3.4.

Interfaz hardware software

234

Capítulo 3

Aritmética para computadores

.

Algoritmo de multiplicación

EJEMPLO

RESPUESTA

Usando números de 4 bits para ahorrar espacio, multiplicar 2diez × 3diez, o 0010dos × 0011dos. La figura 3.7 muestra el valor de cada registro para cada paso, etiquetados de acuerdo a la figura 3.5, con el valor final de 0000 0110 dos o 6 diez. Se usa el color para indicar los valores del registro que cambian en cada paso, y el bit rodeado de un círculo es el bit examinado para determinar la operación de cada paso.

Multiplicación con signo Hasta el momento multiplicado números positivos. La forma más fácil de entender cómo multiplicar números con signo es convertir primero el multiplicador y multiplicando a números positivos y después recordar los signos originales. Los algoritmos deberían entonces ejecutar 31 iteraciones, dejando los signos fuera del cálculo. Tal y como aprendimos en primaria, debemos negar el producto sólo si los signos originales no son iguales. Es evidente que el último algoritmo funcionará correctamente con números con signo siempre que recordemos que los números con los que estamos tratando tienen infinitos dígitos, y que sólo los estamos representando con 32 bits. Por tanto, los pasos de desplazamiento necesitarían extender el signo del producto para números con signo. Cuando el algoritmo acaba, la palabra inferior tendría el producto de 32 bits.

Iteración

Paso

Multiplicador

Multiplicando

Producto

0

Valores iniciales

0011

0000 0010

0000 0000

1

1a: 1 ¡ Prod = Prod + Mcando

0011

0000 0010

0000 0010

2: Desplazar Mcando a la izq.

0011

0000 0100

0000 0010

3: Desplazar Mdor a la der.

0001

0000 0100

0000 0010

1a: 1 ¡ Prod = Prod + Mcand

0001

0000 0100

0000 0110

2: Desplazar Mcando a la izq.

0001

0000 1000

0000 0110

3: Desplazar Mdor a la der.

0000 0000

0000 1000

0000 0110

0000 1000

0000 0110

2: Desplazar Mcando a la izq.

0000

0001 0000

0000 0110

3: Desplazar Mdor a la der.

0000

0001 0000

0000 0110

1: 0 ¡ no operation

0000

0001 0000

0000 0110

2: Desplazar Mcando a la izq.

0000

0010 0000

0000 0110

3: Desplazar Mdor a la der.

0000

0010 0000

0000 0110

2

3

4

1: 0

¡ ninguna operación

FIGURA 3.7 Ejemplo de multiplicación usando el algoritmo de la figura 3.6. El bit examinado para determinar el próximo paso está rodeado de un círculo de color.

3.3

235

Multiplicación

Multiplicación más rápida La ley de Moore ha suministrado tantos recursos que los diseñadores de hardware pueden ahora construir un hardware de multiplicación mucho más rápido. Que el multiplicando se vaya a sumar o no se sabe al principio de la multiplicación examinando cada uno de los 32 bits del multiplicador. Es posible realizar multiplicaciones más rápidas proporcionando un sumador de 32 bits para cada bit del multiplicador: una entrada es la operación lógica AND del multiplicando con un bit del multiplicador y la otra es la salida del sumador anterior. Una forma directa para conseguir esto sería conectando las salidas de los sumadores a las entradas del sumador situadas a su izquierda, formando una pila de 32 sumadores. Otra alternativa para organizar estos 32 sumadores es un árbol paralelo, tal como se muestra en la figura 3.8. En lugar de un retraso igual al tiempo necesario para hacer 32 sumas, el retraso es log2(32) o el tiempo necesario para realizar cinco sumas de 32 bits. La figura 3.8 muestra cómo se realiza esta conexión más rápida. De hecho, la multiplicación puede ser todavía más rápida que cinco sumas si se utilizan sumadores de acarreo almacenado (carry save adders) (véase la sección C.6 en el apéndice C en el CD) y además, como se puede segmentar fácilmente, este diseño es capaz de soportar varias multiplicaciones al mismo tiempo (véase el capítulo 4).

Multiplicación en MIPS MIPS proporciona un par de registros de 32 bits separados para almacenar el producto de 64 bits, llamados Hi y Lo. Para realizar un producto con o sin signo correcto, MIPS tiene dos instrucciones: multiplicar (mult) y multiplicar sin signo (multu). Para buscar el producto de 32 bits entero, el programador puede usar moves from lo (mflo) (mover desde lo). El ensamblador MIPS genera una pseudoinstrucción para multiplicar que especifica tres registros de propósito general, generando las instrucciones mflo y mfhi para colocar el producto en registros.

Resumen La multiplicación se realiza con un hardware sencillo de suma y desplazamiento, derivado del método de lápiz y papel aprendido en la escuela primaria. Los compiladores incluso usan instrucciones de desplazamiento para multiplicaciones por potencias de dos.

Las dos instrucciones de multiplicar de MIPS ignoran el desbordamiento, por lo que es responsabilidad del software comprobar si el producto es demasiado grande para caber en un registro de 32 bits. No hay desbordamiento si hi es 0 para multu o es igual al signo replicado 32 veces para mult. Se puede usar la instrucción move from hi (mfhi) (mover desde hi) para transferir hi a un registro de propósito general para comprobar el desbordamiento.

Interfaz hardware software

236

Capítulo 3

Aritmética para computadores

Mdor31 • Mcando Mdor30 • Mcando

Mdor29 • Mcando Mdor28 • Mcando

32 bits

32 bits

Mdor3 • Mcando Mdor2 • Mcando

Mdor1 • Mcando Mdor0 • Mcando

32 bits

32 bits

...

32 bits 1 bit

1 bit

32 bits ...

...

...

1 bit

1 bit

32 bits Producto63 Producto62

...

...

Producto47..16

Producto1 Producto0

FIGURA 3.8 Hardware para multiplicación rápida. En lugar de usar 31 veces un único sumador de 32 bits, este hardware “desenrolla el lazo” para usar 31 sumadores y los organiza para minimizar el retraso.

Divide et impera. “Divide y vencerás” en latín, antigua máxima política citada por Maquiavelo, 1532.

3.4

División

3.4

La operación recíproca de la multiplicación es la división, una operación que es menos frecuente y más peculiar. Incluso ofrece la oportunidad de realizar una operación no válida matemáticamente: dividir por 0. Comenzamos con un ejemplo de división larga usando números decimales para recordar los nombres de los operandos y el algoritmo de la división de la escuela primaria. Por razones similares a las de la sección anterior, limitamos los dígitos decimales a 0 y 1. El ejemplo es dividir 1 001 010diez entre 1000diez: 1001diez Divisor 1000diez 1001010diez

Cociente Dividendo

–1000 10 101 1010 –1000 10diez

Resto

3.4

237

División

Los dos operandos (dividendo y divisor) y el resultado (cociente) de la división van acompañados por un segundo resultado denominado resto. Esta es otra forma de expresar la relación entre los componentes: Dividendo = cociente × divisor + resto donde el resto es menor que el divisor. A veces los programas utilizan la instrucción de dividir sólo para obtener el resto, ignorando el cociente. El algoritmo de la división de la escuela primaria intenta ver cuántas veces se puede restar un número, creando un dígito del cociente en cada intento. Nuestro ejemplo decimal, cuidadosamente seleccionado, sólo usa los números 0 y 1, de manera que es fácil determinar cuántas veces cabe el divisor en la parte del dividendo: es o 0 veces o 1 vez. Los números binarios contienen sólo 0 o 1, así que la división binaria está restringida a estas dos posibilidades, razón por la cual se simplifica la división binaria. Supongamos que ambos (dividendo y divisor) son positivos y, por lo tanto, el cociente y el resto son no negativos. Los operandos de la división y ambos resultados son valores de 32 bits, y por ahora ignoraremos el signo.

Algoritmo y hardware de división La figura 3.9 muestra el hardware que imita nuestro algoritmo de la escuela primaria. Empezamos con un registro Cociente de 32 bits con valor inicial 0. Cada iteración del algoritmo necesita mover el divisor a la derecha un dígito, de manera que empezamos con el divisor situado en la mitad izquierda del registro Divisor de 64 bits y, en cada paso, lo desplazamos un bit a la derecha para alinearlo con el dividendo. El valor inicial del registro Resto es el dividendo.

Divisor Desp. der. 64 bits

Cociente Desp. izq.

ALU de 64 bits

32 bits Resto Escribir

Control comprobación

64 bits

FIGURA 3.9 Primera versión del hardware de división. El registro Divisor, la ALU y registro Resto tienen un ancho de 64 bits, y sólo el registro Cociente es de 32 bits. El divisor de 32 bits empieza en la mitad izquierda del registro Divisor y se desplaza 1 bit a la derecha en cada iteración. El valor inicial del resto es el el dividendo. El control decide cuándo desplazar los registros Divisor y Cociente y cuándo escribir el nuevo valor en el registro Resto.

Dividendo: número que ha de dividirse por otro.

Divisor: número por el cual se divide el dividendo. Cociente: resultado primario de la división; número que cuando se multiplica por el divisor y se le suma el resto da como resultado el dividendo. Resto: resultado secundario de la división; número que cuando se suma al producto del cociente y el divisor da como resultado el dividendo.

238

Capítulo 3

Aritmética para computadores

Inicio

1. Hacer la resta registro Resto menos registro Divisor y guardar el resultado en el registro Resto

Resto ≥ 0

Comprobación del resto

2a. Desplazar a la derecha el registro Cociente poniendo un 1 en su bit menos significativo

Resto < 0

2b. Restaurar el valor original del registro Resto sumándole el registro Divisor. Desplazar, al mismo tiempo, el registro Cociente, dejando su bit menos significativo a 0

3. Desplazar el registo Divisor 1 bit a la derecha

No: < 33 repeticiones ¿33ª repetición?

Sí: 33 repeticiones

Fin FIGURA 3.10 Un algoritmo de división que utiliza el hardware de la figura 3.9. Si el resto es positivo, el divisor cabía en el dividendo, de manera que el paso 2a genera un 1 en el cociente. Un resto negativo después del paso 1 significa que el divisor no cabía en el dividendo, de manera que el paso 2b genera un 0 en el cociente y suma el divisor al resto, invirtiendo la resta del paso 1. El desplazamiento final, en el paso 3, alinea el divisor adecuadamente, con relación al dividendo, para la siguiente iteración. Estos pasos se repiten 33 veces.

3.4

239

División

La figura 3.10 muestra los tres pasos del primer algoritmo de división. A diferencia de los humanos, el computador no es suficientemente inteligente para conocer por anticipado si el divisor es menor que el dividendo. Primero debe restar el divisor en el paso 1; recuerde que así es cómo hacíamos la comparación en la instrucción set on less than (activar si menor que). Si el resultado es positivo, el divisor es menor o igual que el dividendo, así que generamos un 1 en el cociente (paso 2a). Si el resultado es negativo, el próximo paso es restaurar el valor original añadiendo el divisor al resto y generar un 0 en el cociente (paso 2b). El divisor se desplaza a la derecha y volvemos a iterar de nuevo. El resto y el cociente se encontrarán en sus registros homónimos tras completar todas las iteraciones.

Un algoritmo de división

Usando una versión de 4 bits del algoritmo para ahorrar páginas, intentamos dividir 7diez entre 2diez, o 0000 0111 dos entre 0010dos. La figura 3.11 muestra el valor de cada registro para cada uno de los pasos, con el cociente igual a 3diez y el resto a 1diez. Observe que la comprobación en el paso 2 de si el resto es positivo o negativo comprueba simplemente que el bit de signo del registro resto es un 0 o un 1. El requisito sorprendente de este algoritmo es que necesita n + 1 pasos para obtener el cociente y el resto correctos. Este algoritmo y su hardware se pueden refinar para ser más rápidos y baratos. Se obtiene una mayor rapidez desplazando los operandos y el cociente al mismo tiempo que se realiza la resta. Este refinamiento divide en dos el tamaño del sumador y los registros observando dónde están las porciones no usadas de los registros y el sumador. La figura 3.12 muestra el hardware modificado.

División con signo Hasta ahora hemos ignorado los números con signo en la división. La solución más sencilla es recordar los signos del divisor y el dividendo y entonces negar el cociente si los signos no son iguales. Extensión: La única complicación de la división con signo es que debemos determinar también el signo del resto. Recordemos que la siguiente ecuación debe cumplirse siempre: Dividendo = cociente x divisor + resto Para comprender cómo se determina el signo del resto, fijémonos en el ejemplo de dividir todas las combinaciones de ±7 diez entre ±2diez. El primer caso es fácil: +7 ÷ +2: cociente = +3, resto = +1

EJEMPLO

RESPUESTA

240

Capítulo 3

Iteración 0 1

2

3

4

5

Aritmética para computadores

Paso

Cociente

Divisor

Valores iniciales

0000

0010 0000

1: Resto = Resto – Div

0000

0010 0000

2b: Resto < 0 ¡ +Div, sll Q, Q0 = 0

0000

0010 0000

3: Desp. Div a la derecha

0000

0001 0000

1: Resto = Resto – Div

0000

0001 0000

2b: Resto < 0 ¡ +Div, sll Q, Q0 = 0

0000

0001 0000

3: Desp. Div a la derecha

0000

0000 1000

1: Resto = Resto – Div

0000

0000 1000

2b: Resto < 0 ¡ +Div, sll Q, Q0 = 0

0000

0000 1000

3: Desp. Div a la derecha

0000

0000 0100

1: Resto = Resto – Div

0000

0000 0100

Resto 0000 0111 1110 0111 0000 0111 0000 0111 1111 0111 0000 0111 0000 0111 1111 1111 0000 0111 0000 0111 0000 0011

2a: Resto v 0 ¡ sll Q, Q0 = 1

0001

0000 0100

3: Desp. Div a la derecha

0001

0000 0010

1: Resto = Resto – Div

0001

0000 0010

2a: Resto v 0 ¡ sll Q, Q0 = 1

0011

0000 0010

0000 0001

3: Resto Div a la derecha

0011

0000 0001

0000 0001

0000 0011 0000 0011 0000 0001

FIGURA 3.11 Ejemplo de división usando el algoritmo de la figura 3.10. El bit examinado para determinar el próximo paso está rodeado por un círculo en color.

Divisor 32 bits

ALU de 32 bits

Resto

Desp. der. Desp. izq. Escribir

Control comprobación

64 bits

FIGURA 3.12 Una versión mejorada del hardware de división. El registro Divisor, la ALU y el registro Cociente tienen todos un ancho de 32 bits, y sólo el registro Resto sigue siendo de 64 bits. Comparado con la figura 3.10, el número de bits de la ALU y el registro Divisor se ha dividido a la mitad y el resto se desplaza a la izquierda. Esta versión también combina el registro Cociente con la mitad derecha del registro Resto. (Como en la figura 3.6, el registro Resto debería tener 65 bits, para asegurarnos de que el acarreo de la suma no se pierde.)

Comprobando los resultados: 7 = 3 × 2 + (+1) = 6 + 1 Si cambiamos el signo del dividendo, el cociente debe cambiar también: –7 ÷ +2: cociente = –3

3.4

División

Reescribiendo nuestra fórmula básica para calcular el resto: Resto = (dividendo – cociente × divisor) = –7 – (–3 × +2) = –7 – (–6) = –1 Así pues, –7 ÷ +2: cociente = –3, resto = –1 Comprobando los resultados de nuevo: –7 = –3 × 2 + (–1) = –6 – 1 La razón por la que la respuesta no es un cociente de –4 y un resto de +1, que también cumplirían la fórmula, es que el valor absoluto del cociente cambiaría dependiendo de los signos del dividendo y del divisor. Claramente, si –(x ÷ y) | (–x) ÷ y la programación sería aún un reto mayor. Este comportamiento anómalo se evita siguiendo la regla de que el dividendo y el resto deben tener los mismos signos, sin importar cuáles son los signos del divisor y el cociente. Calculamos las otras combinaciones siguiendo la misma regla: +7 ÷ –2: cociente = –3, resto = +1 –7 ÷ –2: cociente = +3, resto = –1 De esta forma, el algoritmo de la división con signo correcto niega el cociente si los signos de los operandos son opuestos y hace que el signo del resto no nulo sea igual al del dividendo.

División más rápida En la multiplicación usamos muchos sumadores para acelerarla, pero no podemos utilizar el mismo truco para la división. La razón es que necesitamos saber el signo de la diferencia antes de que podamos ejecutar el siguiente paso del algoritmo, mientras que con la multiplicación podíamos calcular los 32 productos parciales inmediatamente. Existen técnicas para producir más de un bit del cociente por bit. La técnica de la división SRT intenta adivinar varios bits del cociente por paso, usando una tabla de consulta basada en los bits superiores del dividendo y del resto. Utiliza los pasos siguientes para corregir las suposiciones incorrectas. Un valor típico hoy en día es 4 bits. La clave está en adivinar el valor que hay que restar. En la división binaria solamente hay una única elección. Estos algoritmos usan 6 bits del resto y 4 bits del divisor para indexar una tabla que determina el cálculo para cada paso. La precisión de este método rápido depende de que se tenga valores apropiados en la tabla de consulta. La falacia de la página 276 de la sección 3.8 muestra qué puede pasar si la tabla no es correcta.

División en MIPS De las figuras 3.6 y 3.12, ya habrá observado que se puede usar el mismo hardware secuencial tanto para multiplicar como para dividir. El único requisito es un registro de 64 bits que pueda desplazarse a la izquierda o a la derecha y una ALU de 32 bits que sume o reste. Por ello, MIPS usa los registros de 32 bits hi y lo

241

242

Capítulo 3

Aritmética para computadores

tanto para multiplicar como para dividir. Como podemos esperar del algoritmo anterior, al terminar la instrucción de división, Hi contiene el resto y Lo contiene el cociente. Para manejar tanto enteros con signo como sin signo, MIPS tiene dos instrucciones: dividir (div) y dividir sin signo (divu). El ensamblador MIPS permite especificar tres registros de las instrucciones de dividir, generando las instrucciones mflo y mfhi para colocar el resultado deseado en un registro de propósito general.

Resumen El hardware común para soportar la multiplicación y la división permite que MIPS proporcione un par de registros de 32 bits que se usan tanto para multiplicar como para dividir. La figura 3.13 resume los elementos añadidos a la arquitectura MIPS en las dos últimas secciones.

Interfaz hardware software

Las instrucciones de división de MIPS ignoran el desbordamiento, de manera que el software debe determinar si el cociente es demasiado grande. Además del desbordamiento, la división también puede producir un cálculo impropio: la división por 0. Algunos computadores distinguen entre estos dos sucesos anómalos. El software MIPS debe comprobar el divisor para descubrir la división por 0, así como el desbordamiento. Extensión: Un algoritmo de la división aún más rápido no suma el divisor inmediatamente para restaurar si el resto es negativo. Simplemente suma el dividendo al resto desplazado en el próximo paso puesto que (r + d) × 2 – d = r × 2 + d × 2 – d = r × 2 + d. Este algoritmo sin restauración, que tarda un ciclo por paso, se examina con más detalle en los ejercicios; el algoritmo que hemos visto aquí se llama división con restauración. Un tercer algoritmo que no almacena el resultado de la resta cuando es negativo es el llamado algoritmo de división sin representación (nonperforming division), que de media tiene un tercio menos de operaciones aritméticas.

La velocidad no lleva a ninguna parte si estás en la dirección equivocada

3.5

Punto flotante

3.5

Proverbio americano

Además de los enteros con y sin signo, los lenguajes de programación soportan números con decimales, que en matemáticas se llaman reales. Veamos algunos ejemplos de números reales: 3.14159265 . . .diez (U) 2.71828 . . .diez (e) 0.000000001diez o 1.0diez × 10–9 (segundos en un nanosegundo) 3 155 760 000diez o 3.15576diez × 109 (segundos en un siglo normal)

3.5

243

Punto flotante

Lenguaje ensamblador del MIPS Categoria Instruccción

Aritmética

Transferencia de datos

Lógicas

Salto condicional

Salto incondicional

Ejemplo

Significado

add subtract add immediate add unsigned subtract unsigned add immediate unsigned move from coprocessor register multiply multiply unsigned divide

add sub addi addu subu addiu mfc0

$s1,$s2,$s3 $s1,$s2,$s3 $s1,$s2,100 $s1,$s2,$s3 $s1,$s2,$s3 $s1,$s2,100 $s1,$epc

mult multu div

$s2,$s3 $s2,$s3 $s2,$s3

divide unsigned

divu

$s2,$s3

move from Hi move from Lo load word

mfhi mflo lw

$s1 $s1 $s1,100($s2)

store word

sw

$s1,100($s2)

load half unsigned

lhu

$s1,100($s2)

store half

sh

$s1,100($s2)

load byte unsigned

lbu

$s1,100($s2)

store byte

sb

$s1,100($s2)

load upper immediate and or nor and immediate or immediate shift left logical shift right logical branch on equal

lui and or nor andi ori sll srl beq

$s1,100 $s1,$s2,$s3 $s1,$s2,$s3 $s1,$s2,$s3 $s1,$s2,100 $s1,$s2,100 $s1,$s2,10 $s1,$s2,10 $s1,$s2,25

branch on not equal

bne

$s1,$s2,25

set on less than

slt

$s1,$s2,$s3

set less than immediate

slti

$s1,$s2,100

set less than unsigned

sltu

$s1,$s2,$s3

set less than immediate unsigned jump jump register jump and link

sltiu

$s1,$s2,100

j jr jal

2500 $ra 2500

Comentarios

$s1 = $s2 + $s3 $s1 = $s2 – $s3 $s1 = $s2 + 100 $s1 = $s2 + $s3 $s1 = $s2 – $s3 $s1 = $s2 + 100 $s1 = $epc

Tres operandos; desbordamiento detectado Tres operandos; desbordamiento detectado + constante; desbordamiento detectado Tres operandos; desbordamiento no detectado Tres operandos; desbordamiento no detectado + constante; desbordamiento no detectado Usado para copiar Exception PC más otros registros especiales Hi, Lo = $s2 × $s3 Producto con signo de 64 bits en Hi, Lo Hi, Lo = $s2 8.0 pt $s3 Producto sin signo de 64 bits en Hi, Lo Lo = $s2 / $s3, Lo = cociente, Hi = resto Hi = $s2 mod $s3 Lo = $s2 / $s3, Cociente y resto sin signo Hi = $s2 mod $s3 $s1 = Hi Para obtener copia de Hi $s1 = Lo Para obtener copia de Lo $s1 = Memory[$s2 + Palabra de memoria a registro 100] Memory[$s2 + 100] = Palabra de memoria a registro $s1 $s1 = Memory[$s2 + Media palabra de memoria a registro 100] Memory[$s2 + 100] = Media palabra de registro a memoria $s1 $s1 = Memory[$s2 + Byte de memoria a registro 100] Memory[$s2 + 100] = Byte de registro a memoria $s1 Carga una constante en los 16 bits superiores $s1 = 100 * 216 $s1 = $s2 & $s3 Operandos en tres registros; AND bit a bit $s1 = $s2 | $s3 Operandos en tres registros; OR bit a bit $s1 = ~ ($s2 |$s3) Operandos en tres registros; NOR bit a bit $s1 = $s2 & 100 ANDbit a bit con constante t $s1 = $s2 | 100 OR bit a bit con constante $s1 = $s2 << 10 Desplazamiento por una constante a la izquierda $s1 = $s2 >> 10 Desplazamiento por una constante a la derecha if ($s1 == $s2) go to Comprobación de igualdad; salto relativo al PC PC + 4 + 100 if ($s1 != $s2) go to Comprobación de desigualdad; salto relativo al PC PC + 4 + 100 if ($s2 < $s3) $s1 = 1; Comparación menor que; complemento a dos else $s1 = 0 if ($s2 < 100) $s1 = 1; Comparación < constante; complemento a dos else $s1=0 if ($s2 < $s3) $s1 = 1; Comparación menor que; sin signo else $s1=0 if ($s2 < 100) $s1 = 1; Comparación < constante; sin signo else $s1 = 0 go to 10000 Salto a la dirección destino go to $ra Para switch, procedimiento de retorno $ra = PC + 4; go to Para llamada a procedimiento 10000

FIGURA 3.13 Arquitectura MIPS presentada hasta ahora. La memoria y los registros de la arquitectura MIPS no se incluyen por razón de espacio, pero esta sección añadió los registros Hi y Lo para soportar la multiplicación y la división. El lenguaje máquina de MIPS se muestra en la tarjeta de referencia resumen MIPS en la contraportada de este libro.

244

Notación científica: notación que expresa los números con un único dígito a la izquierda del punto decimal. Normalizado: número en notación en punto flotante que no tiene 0s por delante.

Punto flotante: aritmética del computador que representa números en los cuales el punto binario no es fijo.

Capítulo 3

Aritmética para computadores

En el último ejemplo, observe que el número no representa una mantisa pequeña, pero es mayor de lo que podemos representar con un entero con signo de 32 bits. La notación alternativa para los dos últimos números se llama notación científica, que tiene un único dígito a la izquierda del decimal. Un número en notación científica que no tenga 0s al principio se llama un número normalizado, que es la forma habitual de escribirlo. Por ejemplo, 1.0diez × 10–9 está en notación científica normalizada, pero 0.1diez × 10–8 y 10.0diez × 10–10 no lo están. De la misma forma que representamos números decimales en notación científica, también podemos representar números binarios en notación científica: 1.0dos × 2–1 Para mantener un número binario en forma normalizada, necesitamos una base que podamos incrementar y decrementar por el número exacto de bits que deben ser desplazados para tener un dígito no nulo a la izquierda del punto decimal. Solamente la base 2 cumple este requisito. Puesto que la base no es 10, también necesitamos un nuevo nombre para el punto decimal; una buena opción es punto binario. La aritmética del computador que soporta tales números se llama punto flotante porque representa números en los cuales el punto binario no es fijo, como ocurre con los enteros. El lenguaje de programación C usa el nombre float para estos números. Igual que en la notación científica, los números se representan con un único dígito no cero a la izquierda del punto binario. En binario, la forma es 1.xxxxxxxxxdos × 2yyyy (Aunque el computador también representa el exponente en base 2 al igual que el resto del número, para simplificar la notación mostramos el exponente en decimal). Una notación científica estándar para reales en forma normalizada ofrece tres ventajas: simplifica el intercambio de datos que incluyen números en punto flotante; simplifica los algoritmos de la aritmética en punto flotante al saber que los números estarán siempre en este formato; e incrementa la precisión de los números que se pueden almacenar en una palabra, puesto que los 0s precedentes innecesarios se reemplazan por dígitos significativos a la derecha del punto binario.

Representación en punto flotante Mantisa: valor, generalmente entre 0 y 1, situado en el campo mantisa. Exponente: en el sistema de representación numérica de la aritmética en punto flotante, el valor que se coloca en el campo de exponente.

Un diseñador de una representación en punto flotante debe encontrar un compromiso entre el tamaño de la mantisa y el tamaño del exponente porque una longitud fija de palabra significa que se debe tomar un bit de uno para añadirlo al otro. Este compromiso es entre precisión y rango: incrementar el tamaño de la mantisa mejora la precisión de la mantisa, mientras que incrementar el tamaño del exponente incrementa el rango de números que se pueden representar. Tal y como nos recuerda nuestra directriz de diseño del capítulo 2, un buen diseño implica un buen compromiso. Los números en punto flotante frecuentemente son múltiplos del tamaño de una palabra. A continuación se muestra la representación de un número en punto flotante MIPS, donde s es el signo del número en punto flotante (un 1 significa negativo), el exponente es el valor del campo exponente de 8 bits (incluido el signo

3.5

245

Punto flotante

del exponente), y la mantisa es el número de 23 bits. Esta representación se llama signo y magnitud, puesto que el signo tiene un bit separado del resto del número. 31 30 29 28 27 26 25 24 23 22 21 20 19 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0 s 1 bit

exponente

mantisa

8 bits

23 bits

En general, los números en punto flotante son habitualmente de la forma (–1)S × F × 2E F implica el valor del campo mantisa y E el del campo exponente; la relación exacta entre estos campos se explicará pronto. (Veremos en breve que MIPS hace algo un poco más sofisticado). Estos tamaños escogidos de exponente y mantisa dan a la aritmética del computador MIPS un rango extraordinario. Fracciones tan pequeñas como 2.0diez × 10–38 y números casi tan grandes como 2.0diez × 1038 se pueden representar en un computador. Extraordinario sí, pero no infinito, por lo que aún es posible que haya números demasiado grandes para el computador. Por tanto, en la aritmética de punto flotante, al igual que en la de enteros, pueden ocurrir interrupciones de desbordamiento. Obsérvese que aquí desbordamiento significa que el exponente es demasiado grande para ser representado en el campo exponente. El punto flotante presenta también un nuevo tipo de suceso excepcional. Así como los programadores querrán saber cuándo han calculado un número que es demasiado grande para ser representado, también querrán saber si la mantisa que están calculando se ha hecho tan pequeña que no se puede representar; ambos sucesos podrían hacer que un programa diese respuestas incorrectas. Para distinguirlo del desbordamiento, se le llama desbordamiento a cero (underflow). Esta situación ocurre cuando el exponente negativo es demasiado grande para caber en el campo del exponente. Una forma de reducir la posibilidad de desbordamiento a cero o desbordamiento es ofrecer otro formato que tiene un exponente mayor. En C este número se llama double, y las operaciones con doubles se llaman aritmética en punto flotante de precisión doble, mientras que el nombre del formato anterior es punto flotante de precisión simple. La representación de un número en punto flotante de precisión doble ocupa dos palabras MIPS, como se muestra más abajo, donde s sigue siendo el signo del número, exponente es el valor del campo del exponente de 11 bits y mantisa es el número de 52 bits del campo mantisa.

Desbordamiento (punto flotante): situación en la cual un exponente positivo es demasiado grande para caber en el campo exponente.

Desbordamiento a cero (underflow) (punto flotante): situación en la cual un exponente negativo es demasiado grande para caber en el campo exponente. Precisión doble: valor en punto flotante representado en dos palabras de 32 bits. Precisión simple: valor en punto flotante representado en una palabra de 32 bits.

31 30 29 28 27 26 25 24 23 22 21 20 19 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0 s 1 bit

exponente

mantisa

11 bits

20 bits mantisa (continuación) 32 bits

246

Capítulo 3

Aritmética para computadores

La precisión doble de MIPS permite números casi tan pequeños como 2.0diez × 10–308 y casi tan grandes como 2.0diez × 10308. Aunque la precisión doble incrementa el rango del exponente, su principal ventaja es la mayor precisión a causa de la mantisa más grande. Estos formatos van más allá de MIPS. Son parte del estándar en punto flotante IEEE 754, que se utiliza en casi todos los computadores creados desde 1980. Este estándar ha mejorado enormemente tanto la portabilidad de los programas de punto flotante como la calidad de la aritmética de los computadores. Para empaquetar aún más bits en la parte significativa, el IEEE 754 hace implícito el valor 1 para el primer bit de los números binarios normalizados. Por esto, el número tiene en realidad 24 bits de longitud en precisión simple (un 1 implícito u oculto y una mantisa de 23 bits) y de 53 bits en precisión doble (1 + 52). Para ser precisos, usamos el término la parte significativa para representar los números de 24 o 53 bits, que es 1 más la parte fraccionaria, y mantisa cuando nos referimos al número de 23 o 52 bits. Puesto que 0 no tiene un primer 1, al valor 0 se le reserva un exponente con valor 0 de manera que el hardware no le añadirá un primer 1. Así, 00 . . . 00dos representa el 0; la representación del resto de los números usa la forma anterior con el 1 oculto añadido: (–1)S × (1 + mantisa) × 2E donde los bits de la mantisa representan un número entre 0 y 1, y E especifica el valor del campo exponente, que se detallará en breve. Si numeramos los bits de la mantisa de izquierda a derecha s1, s2, s 3, . . . , el valor es (–1)S × (1 + (s1 × 2–1) + (s2 × 2–2) + (s3 × 2–3) + (s4 × 2–4 ) + . . .) × 2E La figura 3.14 muestra las codificaciones de los números en punto flotante IEEE 754. Otras características del IEEE 754 son los símbolos especiales para representar sucesos inusuales. Por ejemplo, en lugar de interrumpir ante una división por 0, el software puede forzar el resultado a un patrón de bits representando +h o h; el mayor exponente se reserva para estos símbolos especiales. Cuando el programador imprime los resultados, el programa imprimirá un símbolo de infinito. (Para los expertos en matemáticas, el propósito del infinito es dar completitud topológica a los reales).

Precisión simple

Precisión doble

Objeto representado

Exponente

Mantisa

Exponente

0

0

0

Mantisa 0

0

0

Distinto de cero

0

Distinto de cero

± número no normalizado

1–254

cualquiera

1–2046

cualquiera

± número en punto flotante

255

0

2047

0

± infinito

255

Distinto de cero

2047

Distinto de cero

NaN (Not a Number)

FIGURA 3.14 Codificación IEEE 754 para números en punto flotante. Un bit de signo independiente determina el signo. Los números desnormalizados se describen en la extensión de la página 270. Esta información se encuentra también en la columna 4 de la Tarjeta de Datos de Referencia de MIPS, que e incluye con el libro.

3.5

247

Punto flotante

El IEEE 754 tiene incluso un símbolo para el resultado de operaciones inválidas, tales como 0/0 o restar infinito de infinito. Este símbolo es NaN, por “No un Número” (Not a Number, en inglés). El propósito de NaN es permitir a los programadores posponer varias comprobaciones y decisiones en el programa para cuando sea conveniente. Los diseñadores del IEEE 754 también querían una representación que se pudiese procesar fácilmente mediante comparaciones de enteros, especialmente para ordenar. Este deseo es el que hace que el signo esté en el bit más significativo, permitiendo una comprobación rápida de menor que, mayor que, o igual a 0. (Es un poco más complicado que una simple ordenación de enteros, puesto que esta notación es esencialmente de signo y magnitud en lugar de complemento a dos). Colocar el exponente antes que la mantisa también simplifica la ordenación de números en punto flotante mediante instrucciones de comparación de enteros, puesto que los números con un exponente mayor aparentan ser mayores que los números con exponentes menores, siempre que ambos exponentes tengan el mismo signo. Los exponentes negativos plantean una dificultad a la ordenación simplificada. Si usamos complemento a dos o cualquier otra notación en la cual los exponentes negativos tengan un 1 en el bit más significativo del campo del exponente, un exponente negativo parecerá un número mayor. Por ejemplo, 1.0dos × 2–1 se representaría como 31 30 29 28 27 26 25 24 23 22 21 20 19 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0 0

1

1

1

1

1

1

1

1

0

0

0

0

0

0

0

0

0

0

0

0

0

0 0 0 0 0 0 0

.

.

.

(Recuerde que el primer 1 está implícito en la mantisa). El valor 1.0dos × 2+1 parecería un número menor. 31 30 29 28 27 26 25 24 23 22 21 20 19 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0 0

0

0

0

0

0

0

0

1

0

0

0

0

0

0

0

0

0

0

0

0

0

La notación deseable debe por tanto representar el exponente más negativo como 00 . . . 00dos, y el más positivo como 11 . . . 111dos. Esta convención se llama notación sesgada (biased notation), siendo el sesgo (o desplazamiento) el número que se resta a la representación normal, sin signo, para determinar el valor real. IEEE 754 usa un sesgo de 127 para la precisión simple, de manera que –1 se representa por el patrón de bits del valor –1 + 127diez, o 126diez = 0111 1110dos, y +1 se representa por 1 + 127, o 128diez = 1000 0000dos. El sesgo del exponente para precisión doble es 1023. El exponente sesgado significa que el valor representado por un número en punto flotante es realmente (–1)S × (1 + mantisa) × 2(exponente-sesgo) El rango de los números en precisión simple va desde un número tan pequeño como el ±1.0000 0000 0000 0000 0000 000dos × 2–126 hasta uno tan grande como el ±1.1111 1111 1111 1111 1111 111dos × 2+127

0 0 0 0 0 0 0

.

.

.

248

Capítulo 3

Aritmética para computadores

Veamos la representación.

Representación en punto flotante

Mostrar la representación binaria IEEE 754 del número –0.75diez en precisión simple y doble

EJEMPLO

El número –0.75diez también es

RESPUESTA

–3/4diez o –3/22diez También se representa por la mantisa binaria –11dos/22diez o –0.11dos En notación científica, este valor es –0.11dos × 20 y en notación científica normalizada es –1.1dos × 2–1 La representación general para un número en simple precisión es (–1)S × (1 + mantisa) × 2(exponente – 127) Cuando restamos el sesgo 127 del exponente de –1.1dos x 2–1, el resultado es (–1)1 × (1 + .1000 0000 0000 0000 0000 000dos) × 2(126 – 127) La representación binaria en precisión simple de –0.75diez entonces es 31 30 29 28 27 26 25 24 23 22 21 20 19 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0 1

0

1 bit

1

1

1

1

8 bits

1

1

0

1

0

0

0

0

0

0

0

0

0

0

0

23 bits

0

0 0 0 0 0 0 0 0 0 0

3.5

249

Punto flotante

La representación en precisión doble es (–1)1 × (1 + .1000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000dos) × 2(1022–1023)

31 30 29 28 27 26 25 24 23 22 21 20 19 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0 1

0

1

1

1

1

1 bit 0

0

1

1

1

1

1

0

1

0

0

0

0

0

0

0

11 bits 0

0

0

0

0

0

0

0 0 0 0 0 0 0 0 0 0

20 bits 0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0 0 0 0 0 0 0 0 0 0

32 bits

Ahora vamos a mostrar cómo ir en la otra dirección.

Conversión de binario a decimal en punto flotante

¿Qué número decimal está representado por este número en punto flotante de precisión simple?

EJEMPLO

31 30 29 28 27 26 25 24 23 22 21 20 19 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0 1

1

0

0

0

0

0

0

1

0

1

0

0

0

0

0

0

0

0

0

0

0

El bit de signo es 1, el campo de exponente contiene 129, y el campo mantisa contiene 1 × 2–2 = 1/4, o 0.25. Usando la ecuación básica, (–1)S × (1 + mantisa) × 2(exponente – sesgo) = (–1)1 × (1 + 0.25) × 2(129–127) = –1 × 1.25 × 22 = –1.25 × 4 = –5.0

En las próximas secciones daremos los algoritmos para la suma y la multiplicación en punto flotante. Esencialmente usan las operaciones enteras correspondientes sobre las mantisas, pero es necesario un mantenimiento extra para manipular los exponentes y normalizar el resultado. En primer lugar damos una deducción intuitiva de los algoritmos en decimal, y a continuación una versión binaria más detallada en las figuras.

0 0 0 0 0 0 0

.

.

RESPUESTA

.

250

Capítulo 3

Aritmética para computadores

Extensión: En un intento de incrementar el rango sin eliminar bits de la mantisa, algunos computadores anteriores al estándar IEEE 754 usaban una base diferente de la binaria. Por ejemplo, los grandes computadores IBM 360 y 370 usaban la base 16. Puesto que cambiar el exponente de IBM en 1 significa desplazar la mantisa 4 bits, los números en la base “normalizada” 16 pueden tener hasta 3 bits de comienzo a 0. De ahí que los dígitos hexadecimales significan que pueden eliminarse hasta 3 bits, lo que conduce a sorprendentes problemas en la precisión en la aritmética en punto flotante, como se muestra en la sección 3.6. Los últimos grandes computadores IBM admiten el estándar IEEE 755, así como el formato hexadecimal.

Suma en punto flotante Sumemos a mano números en notación científica para ilustrar los problema que se encuentran en la suma en punto flotante: 9.999diez × 101 + 1.610diez × 10–1. Suponga que sólo podemos almacenar cuatro dígitos decimales de la mantisa y dos dígitos decimales del exponente. Paso 1. Para ser capaces de sumar estos números correctamente, debemos alinear el punto decimal del número que tiene el menor exponente. Por ello necesitamos una forma del número menor, 1.610diez × 10–1, que coincida con el mayor exponente. Para obtenerla nos fijamos que hay varias representaciones de un número en punto flotante no normalizado en notación científica: 1.610diez × 10–1 = 0.1610diez × 100 = 0.01610diez × 101 El número de la derecha es la versión que deseamos, puesto que su exponente coincide con el exponente del número mayor, 9.999diez × 101. Así, el primer paso desplaza la mantisa del número menor a la derecha hasta que el exponente corregido coincida con el del número mayor. Pero podemos representar sólo cuatro dígitos decimales, así que, después de desplazar, el número es en realidad: 0.016diez × 10–1 Paso 2. A continuación viene la suma de las mantisas: 9.999diez + 0.016diez 10.015diez La suma es 10.015diez × 101.

3.5

251

Punto flotante

Paso 3. Esta suma no está normalizada en notación científica, de manera que necesitamos ajustarla: 10.015diez × 101 = 1.0015diez × 102 De este modo, después de la suma puede que tengamos que desplazar la suma para ponerla en forma normalizada, ajustando el exponente de manera apropiada. Este ejemplo muestra el desplazamiento a la derecha, pero si un número fuese positivo y el otro negativo, podría ser posible que la suma tuviese muchos ceros al comienzo, lo que requeriría desplazamientos a la izquierda. Siempre que el exponente se incrementa o decrementa, debemos comprobar el desbordamiento o el desbordamiento a cero —es decir, debemos asegurarnos de que el exponente aún cabe en su campo—. Paso 4. Puesto que supusimos que la mantisa sólo podía tener 4 dígitos (excluyendo el signo), debemos redondear el número. En nuestro algoritmo de la escuela primaria, las reglas de redondeo truncan el número por el dígito que se redondea si el valor del dígito que está su derecha es de 0 a 4, y suman 1 al dígito que se redondea si el que está a su derecha vale de 5 a 9. El número 1.0015diez × 102 se redondea a 4 dígitos en la mantisa a 1.002diez × 102 puesto que el cuarto dígito a la derecha del punto decimal estaba entre 5 y 9. Observe que si hubiésemos tenido mala suerte al redondear, tal como sumar 1 a una cadena de 9s, la suma ya no estaría normalizada y tendríamos que realizar de nuevo el paso 3. La figura 3.15 muestra el algoritmo para la suma binaria en punto flotante que sigue este ejemplo en decimal. Los pasos 1 y 2 son similares al ejemplo mostrado: se ajusta la mantisa del número con el exponente menor y después se suman las dos mantisas. El paso 3 normaliza el resultado, forzando una comprobación del desbordamiento y del desbordamiento a cero. La comprobación del desbordamiento y del desbordamiento a cero depende de la precisión de los operandos. Recuerde que el patrón de bits todos a cero en el exponente está reservado y se usa para la representación del cero en punto flotante. También, el patrón de bits todos a uno en el exponente está reservado para indicar valores y situaciones que están fuera del ámbito normal del los números en punto flotante (véase la extensión de la página 270). Así, para precisión simple, el máximo exponente es 127, y el mínimo exponente es –126. Los límites para precisión doble son 1023 y –1022.

252

Capítulo 3

Aritmética para computadores

Inicio

1. Comparar los exponentes de los dos números: desplazar el número menor a la derecha hasta que su exponente es igual al exponente del número mayor

2. Sumar las mantisas

3. Normalizar la suma, bien desplazando a la derecha e incrementando el exponente o bien desplazando a la izquierda y decrementando el exponente

Sí ¿Desbordamiento o desbordamiento a cero? No

Excepción

4. Redondear el significando (o mantisa) al número apropiado de bits

No

¿Todavía normalizado?



Fin FIGURA 3.15 Suma en punto flotante. Lo normal es ejecutar los pasos 3 y 4 una sola vez, pero si el redondeo provoca la desnormalización de la suma, hay que repetir el paso 3.

3.5

253

Punto flotante

Suma binaria en punto flotante

Intentar sumar los números 0.5diez y –0.4375diez en binario usando el algoritmo de la figura 3.15.

EJEMPLO

Primero examinemos la versión binaria de los dos números en notación científica normalizada, suponiendo que se tienen 4 bits de precisión:

RESPUESTA

0.5diez = = –0.4375diez = =

1/2diez 0.1dos –7/16diez –0.0111dos

= = = =

1/21diez 0.1dos × 20 = 1.000dos × 2–1 4 –7/2 diez – 0.0111dos × 20 = –1.110dos × 2–2

Ahora, sigamos el algoritmo: Paso 1. El significando (o mantisa) del número con el exponente menor (– 1.11dos × 2–2) se desplaza a la derecha hasta que su exponente coincide con el del número mayor: –1.110dos × 2–2 = –0.111dos × 2–1 Paso 2. Sumamos los significandos: 1.000dos × 2–1 + (–0.111dos × 2–1) = 0.001dos × 2–1 Paso 3. A continuación, normalizamos la suma, comprobando el desbordamiento o el desbordamiento a zero: 0.001dos × 2–1 = 0.010dos × 2–2 = 0.100dos × 2–3 = 1.000dos × 2–4 Puesto que 127 v –4 v –126, no hay desbordamiento ni desbordamiento a cero. (El exponente sesgado sería –4 + 127, o 123, que está entre 1 y 254, que son los exponentes sesgados no reservados menor y mayor, respectivamente). Paso 4. Redondeamos la suma: 1.000dos × 2–4 La suma ya cabe exactamente en 4 bits, de manera que no hay cambios en los bits debido al redondeo. La suma es entonces: 1.000dos × 2–4 = 0.0001000dos = 0.0001dos = 1/24diez = 1/16diez

= 0.0625diez

Esta suma es el resultado esperado de sumar 0.5diez a –0.4375diez

254

Capítulo 3

Aritmética para computadores

Muchos computadores dedican hardware para ejecutar las operaciones en punto flotante lo más rápido posible. La figura 3.16 esboza la organización básica del hardware para la suma en punto flotante.

Signo Exponente

Mantisa

Signo Exponente

Mantisa

Comparación de exponentes

ALU pequeña

Dif. de exponentes 0

1

0

1

0

Desplazamiento a la derecha del número menor

Desplazar a la derecha

Control

ALU grande

0

0

1

Incremento o decremento

Signo Exponente

Suma

1

Desplazar a la derecha o a la izquierda

Circuitería de redondeo

1

Normalización

Redondeo

Mantisa

FIGURA 3.16 Diagrama de bloques de una unidad aritmética dedicada a la suma en punto flotante. Los pasos de la figura 3.15 corresponden a cada bloque, de arriba abajo. Primero, el exponente de un operando se resta del otro usando una pequeña ALU para determinar cuál es mayor y por cuánto. Esta diferencia controla los tres multiplexores; de izquierda a derecha, seleccionan el mayor exponente, el significando del número menor y el significando del número mayor. El significando del menor se desplaza a la derecha, y se suman los significandos en la ALU grande. El paso de normalización desplaza la suma a la izquierda o derecha e incrementa o decrementa el exponente. El redondeo genera el resultado final, el cual puede requerir una nueva normalización para obtener el resultado final.

3.5

255

Punto flotante

Multiplicación en punto flotante Ahora que hemos explicado la suma en punto flotante, veamos la multiplicación. Empezamos multiplicando números decimales en notación científica a mano: 1.110diez × 1010 × 9.200diez × 10–5. Suponga que sólo podemos almacenar cuatro cifras en la mantisa y dos en el exponente. Paso 1. A diferencia de la suma, calculamos el exponente del producto simplemente sumando los exponentes de ambos operandos: Nuevo exponente = 10 + (–5) = 5 Veamos cómo funcionaría esto con los exponentes sesgados, para asegurarnos de que se obtiene el mismo resultado: 10 + 127 = 137, y –5 + 127 = 122, de manera que Nuevo exponente = 137 + 122 = 259 Este resultado es demasiado grande para el campo exponente de 8 bits, por lo que algo falla. El problema está en el sesgo porque estamos sumando los sesgos junto con los exponentes: Nuevo exponente = (10 + 127) + (–5 +127) = (5 + 2 × 127) = 259 De acuerdo con esto, para obtener la suma sesgada correcta cuando sumamos numeros sesgados, debemos restar el sesgo a la suma: Nuevo exponente = 137 + 122 – 127 = 259 – 127 = 132 = (5 + 127) y 5 es, además, el resultado que calculamos inicialmente. Paso 2. Después viene la multiplicación de los significandos: 1.110diez x 9.200diez

0000 0000 2220 9990 10212000diez Hay tres cifras a la derecha del punto decimal para cada operando, de manera que el punto decimal está colocado seis cifras desde la derecha en el significando del producto: 10.212000diez

256

Capítulo 3

Aritmética para computadores

Suponiendo que sólo se pueden almacenar tres cifras a la derecha del punto decimal, el producto es 10.212 × 105. Paso 3. Este producto no está normalizado, de manera que tenemos que normalizarlo: 10.212diez × 105 = 1.0212diez × 106 Así, después de la multiplicación, el producto puede verse desplazado una cifra a la derecha para ponerlo en forma normalizada, sumando 1 al exponente. En este punto, podemos comprobar si hay desbordamiento o desbordamiento a cero. El desbordamiento a cero se puede producir si los dos operandos son pequeños –es decir, si ambos tienen exponentes negativos grandes–. Paso 4. Como supusimos que el significando tenía sólo 4 cifras (excluyendo el signo), debemos redondear el número. El número 1.0212diez × 106 se redondea a cuatro cifras en el significando, dando 1.021diez × 106 Paso 5. El signo del producto depende de los signos de los operandos originales. Si ambos son el mismo, el signo es positivo; en caso contrario, es negativo. De aquí que el producto sea +1.021diez × 106 El signo del resultado de la suma en el algoritmo de sumar estaba determinado por la suma de los significandos, pero en la multiplicación el signo del producto se determina a partir de los signos de los operandos. De nuevo, como muestra la figura 3.17, la multiplicación de números binarios en punto flotante es muy similar a los pasos que acabamos de describir. Empezamos calculando el nuevo exponente del producto sumando los exponentes sesgados, asegurándonos de restar un sesgo para obtener el resultado correcto. Después se multiplican los significandos, seguido por un paso de normalización opcional. Se comprueba que el tamaño del exponente no presente desbordamiento o desbordamiento a cero y, después, se redondea el producto. Si el redondeo conduce a una normalización adicional, tenemos que comprobar de nuevo el tamaño del exponente. Finalmente, se actualiza el bit de signo a 1 si el signo de los operandos era diferente (producto negativo) o a 0 si tenían el mismo (producto positivo).

Multiplicación decimal en punto flotante

EJEMPLO

Multiplicar los números 0.5diez y –0.4375diez, usando los pasos de la figura 3.17.

3.5

257

Punto flotante

En binario, la tarea es multiplicar 1.000dos × 2–1 por –1.110dos × 2–2. Paso 1. Sumar los exponentes sin sesgo: –1 + (–2) = –3 o, usando las representaciones sesgadas: (–1 + 127) + (–2 + 127) – 127 = (–1 – 2)+(127 + 127 – 127) = –3 + 127 = 124 Paso 2. Multiplicar los significandos: 1.000dos 1.110dos 0000 1000 1000 1000 1110000dos

x

El producto es 1.110000dos × 2–3, pero necesitamos ajustarlo a cuatro bits, así que será 1.110dos × 2–3. Paso 3. Ahora comprobamos el producto para asegurarnos de que está normalizado, y después comprobamos si hay desbordamiento o desbordamiento a cero. El producto ya está normalizado y, puesto que 127 v –3 v –126, no hay ningún desbordamiento. (Usando la representación sesgada, 254 v 124 v 1, de manera que el exponente quepa). Paso 4. El redondeo del producto no produce ningún cambio: 1.110dos × 2–3 Paso 5. Puesto que los signos de los operandos originales difieren, el signo del producto es negativo. Por lo tanto, el producto es –1.110dos × 2–3 Si lo convertimos a decimal para comprobar el resultado: –1.110dos × 2–3 = –0.001110dos = –0.00111dos = –7/25diez = –7/32diez = –0.21875diez El producto de 0.5diez y –0.4375diez es efectivamente –0.21875diez.

RESPUESTA

258

Capítulo 3

Aritmética para computadores

Inicio

1. Sumar los exponentes sesgados de los dos números, restando el sesgo para obtener el nuevo exponente

2. Multiplicar os significandos

3. Normalizar el producto, si es preciso, desplazando a la derecha e incrementando el exponente.

¿Desbordamiento o desbordamiento a cero?



No

Excepción

4. Redondear el significando al número de bits apropiado

No

¿Está todavía normalizado? Sí

5. Poner el signo del producto como positivo si los signos de los operandos originales eran iguales o como negativo si eran diferentes

Fin FIGURA 3.17 Multiplicación en punto flotante. Lo normal es ejecutar los pasos 3 y 4 una sola vez, pero si el redondeo provoca que el resultado no esté normalizado, se debe repetir el paso 3.

3.5

259

Punto flotante

Instrucciones de punto flotante en MIPS MIPS soporta los formatos IEEE 754 de precisión simple y doble con las siguientes instrucciones: ■

Suma en punto flotante, precisión simple (add.s) y doble (add.d)



Resta en punto flotante, precisión simple (sub.s) y doble (sub.d)



Multiplicación en punto flotante, precisión simple (mul.s) y doble (mul.d)



División en punto flotante, precisión simple (div.s) y doble (div.d)



Comparación en punto flotante, precisión simple (c.×.s) y doble (c.×.d), donde × puede ser igual (eq), diferente (neq), menor (lt), menor o igual (le), mayor (gt), mayor o igual (ge)



Salto condicional en punto flotante, verdadero (bclt) o falso (bclf)

La comparación en punto flotante actualiza un bit a cierto o verdadero, dependiendo de la condición de comparación, y el salto condicional en punto flotante decide si salta o no, dependiendo de la condición. Los diseñadores de MIPS decidieron añadir registros en punto flotante separados, llamados $f0, $f1, $f2, . . . —usados tanto para precisión simple como para precisión doble–. De ahí que incluyeran instrucciones de carga y almacenamiento separadas para los registros de punto flotante: lwc1 y swc1. Los registros base para las transferencias de datos en punto flotante siguen siendo los registros de enteros. El código MIPS para cargar dos números en precisión simple desde memoria, y almacenar el resultado de la suma podría ser de la siguiente manera: lwc1 lwc1

$f4,x($sp) $f6,y($sp)

# # # add.s $f2,$f4,$f6 # swc1 $f2,z($sp) # #

número de 32–bit punto flotante en F4 Carga un número de 32–bit punto flotante en F6 F2 = F4 + F6 precisión simple Almacena número de 32-bit punto flotante desde F2

Un registro en precisión doble es en realidad una pareja de registros de precisión simple par-impar, que utiliza el registro par como nombre. La figura 3.18 resume la parte de la arquitectura MIPS relativa al punto flotante vista en este capítulo, con los elementos añadidos para soportar punto flotante resaltados en color. De manera similar a la figura 2.19 en el capítulo 2, la figura 3.19 muestra la codificación de estas instrucciones.

260

Capítulo 3

Aritmética para computadores

Operandos en punto flotante del MIPS Nombre

Ejemplo

Comentarios

32 registros de $f0, $f1, $f2, . . . , $f31 punto flotante 230 palabras Memory[0], de memoria Memory[4], . . . , Memory[4294967292]

Registros de punto flotante. Se usan por pares para números de precisión doble. Accesible sólo por instrucciones de transferencia de datos. El MIPS utiliza direcciones de byte, de modo que la dirección de dos palabras consecutivas difieren en 4. La memoria mantiene estructuras de datos, como tablas y registros almacenados, como en el caso de los que se salvan en llamadas a procedimientos.

Lenguaje ensamblador de punto flotante del MIPS Categoría

Instrucción

Ejemplo

FP add single FP subtract single FP multiply single

add.s sub.s mul.s

Significado

div.s $f2,$f4,$f6 add.d $f2,$f4,$f6 sub.d $f2,$f4,$f6 mul.d $f2,$f4,$f6 div.d $f2,$f4,$f6 lwc1 $f1,100($s2) Transferencia de datos store word copr. 1 swc1 $f1,100($s2) branch on FP true bc1t 25 branch on FP false bc1f 25 Salto FP compare single c.lt.s $f2,$f4 condicional (eq,ne,lt,le,gt,ge) FP compare double c.lt.d $f2,$f4 (eq,ne,lt,le,gt,ge) Aritmética

Comentarios

$f2,$f4,$f6 $f2 = $f4 + $f6 $f2,$f4,$f6 $f2 = $f4 – $f6 $f2,$f4,$f6 $f2 = $f4 × $f6

FP divide single FP add double FP subtract double FP multiply double FP divide double load word copr. 1

$f2 $f2 $f2 $f2 $f2 $f1

= = = = = =

suma en punto flotante (precisión simple) resta en punto flotante (precisión simple) multiplicación en punto flotante (precisión simple) $f4 / $f6 división en punto flotante (precisión simple) $f4 + $f6 suma en punto flotante (precisión doble) $f4 – $f6 resta en punto flotante (precisión doble) $f4 × $f6 multiplicación en punto flotante (precisión doble) $f4 / $f6 división en punto flotante (precisión doble) Memory[$s2 + 100] dato de 32 bits a registro de punto flotante

Memory[$s2 + 100] = $f1

dato de 32 bits a memoria

if (cond == 1) go to PC + 4 + 100 if (cond == 0) go to PC + 4 + 100 if ($f2 < $f4) cond = 1; else cond = 0 if ($f2 < $f4) cond = 1; else cond = 0

salto relativo al PC si condición salto relativo al PC si no condición comparación de menor, en punto flotante, precisión simple comparación de menor, en punto flotante, precisión doble

Lenguaje máquina de punto flotante del MIPS Nombre add.s sub.s mul.s div.s add.d sub.d mul.d div.d lwc1 swc1 bc1t bc1f c.lt.s c.lt.d Tamaño del campo

Forma to R R R R R R R R I I I I R R

Ejemplo 17 17 17 17 17 17 17 17 49 57 17 17 17 17 6 bits

16 16 16 16 17 17 17 17 20 20 8 8 16 17 5 bits

6 6 6 6 6 6 6 6 2 2 1 0 4 4 5 bits

Comentarios 4 4 4 4 4 4 4 4

2 2 5 bits

2 2 2 2 2 2 2 2 100 100 25 25 0 0 5 bits

0 1 2 3 0 1 2 3

60 60 6 bits

add.s $f2,$f4,$f6 sub.s $f2,$f4,$f6 mul.s $f2,$f4,$f6 div.s $f2,$f4,$f6 add.d $f2,$f4,$f6 sub.d $f2,$f4,$f6 mul.d $f2,$f4,$f6 div.d $f2,$f4,$f6 lwc1 $f2,100($s4) swc1 $f2,100($s4) bc1t 25 bc1f 25 c.lt.s $f2,$f4 c.lt.d $f2,$f4 Todas las instrucciones del MIPS tienen 32 bits

FIGURA 3.18 Arquitectura en punto flotante de MIPS presentada hasta ahora. Véase el apéndice B, sección B.10, para más detalles. Esta información se encuentra también en la columna 2 de la Tarjeta de Datos de Referencia de MIPS, que se incluye con el libro.

3.5

261

Punto flotante

op(31:26): 28–26

0(000)

1(001)

2(010)

3(011)

4(100)

5(101)

6(110)

7(111)

31–29 0(000)

Rfmt

Bltz/gez

j

jal

beq

bne

blez

bgtz

1(001)

addi

addiu

slti

sltiu

andi

ori

xori

lui

2(010)

TLB

FlPt

4(100)

lb

lh

lwl

lw

lbu

lhu

5(101)

sb

sh

swl

sw

lwr swr

6(110)

lwc0 swc0

lwc1 swc1

3(011)

7(111)

op(31:26) = 010001 (FlPt), (rt(16:16) = 0 => c = f, rt(16:16) = 1 => c = t), rs(25:21): 23–21

0(000)

1(001)

2(010)

3(011)

4(100)

5(101)

6(110)

7(111)

25–24 0(00)

mfc1

1(01)

bc1.c

2(10)

f

cfc1

= single

f

mtc1

ctc1

= double

3(11)

op(31:26) = 010001 (FlPt), (f above: 10000 => f = s, 10001 => f = d), funct(5:0): 2–0

0(000)

1(001)

2(010)

3(011)

4(100)

5(101)

6(110)

7(111)

5–3

add.f

sub.f

cvt.s.f

cvt.d.f

6(110)

c.f.f

c.un.f

c.eq.f

c.ueq.f

7(111)

c.sf.f

c.ngle.f

c.seq.f

c.ngl.f

0(000)

mul.f

div.f

abs.f

mov.f

neg.f

c.olt.f

c.ult.f

c.ole.f

c.ule.f

c.lt.f

c.nge.f

c.le.f

c.ngt.f

1(001) 2(010) 3(011) 4(100)

cvt.w.f

5(101)

FIGURA 3.19 Codificación de las instrucciones en punto flotante de MIPS. Esta notación da el valor de cada campo por fila y columna. Por ejemplo, en la parte superior de la figura lw se encuentra en la fila número 4 (100dos en los bits 31-29 de la instrucción) y en la columna número 3 (011 dos en los bits 28-26 de la instrucción), de manera que el valor correspondiente al campo op (bits 31-26) es 100011dos. El subrayado indica que el campo se usa en otra parte. Por ejemplo, FlPt en la fila 2 columna 1 (010001dos) se define en la parte inferior de la figura. Por tanto, sub.f en la fila 0 columna 1 de la parte inferior de la sección significa que el campo funct (bits 5-0) de la instrucción es 000001dos y el campo op (bits 31-26) es 010001dos. Obsérvese que el campo rs, de 5 bits, especificado en la parte central de la figura, determina si la operación es de precisión simple(f = s, o sea rs = 10 000) o precisión doble (f = d, o sea rs = 10 001). Análogamente, el bit 16 de la instrucción determina si la instrucción bc1.c comprueba el valor cierto (bit 16 = 1 => bc1.t) o falso (bit 16 = 0 => bc1.f). Las instrucciones en color se describen en el capítulo 2, mientras que el apéndice B cubre todas las instrucciones. Esta información se encuentra también en la columna 2 de la Tarjeta de Datos de Referencia de MIPS, que se incluye con el libro.

262

Capítulo 3

Interfaz hardware software

Aritmética para computadores

Una cuestión que deben afrontar los diseñadores al incorporar aritmética en punto flotante es la de si utilizar los mismos registros usados por las instrucciones para enteros o añadir un conjunto separado de registros para punto flotante. Debido a que los programas realizan normalmente operaciones enteras y de punto flotante sobre datos distintos, separar los registros sólo incrementará ligeramente las instrucciones necesarias para ejecutar el programa. El impacto mayor está en crear un conjunto separado de instrucciones de transferencia de datos para mover datos entre los registros de punto flotante y la memoria. El beneficio de tener registros de punto flotante separados es que se tiene el doble de registros sin tener que usar más bits en el formato de instrucción, se tiene más ancho de banda de registros al tener conjuntos separados de registros de enteros y de punto flotante y se tiene la capacidad de configurar registros para punto flotante; por ejemplo, algunos computadores convierten todos los operandos de registros en un único formato interno.

Compilación de un programa de punto flotante en C a código ensamblador MIPS

EJEMPLO

Convirtamos una temperatura de Fahrenheit a Celsius: float f2c (float fahr) { return ((5.0/9.0) * (fahr - 32.0)); }

Supongamos que el argumento en punto flotante fahr se pasa en $12 y el resultado debe quedar en $f0. (A diferencia de los registros de enteros, el registro en punto flotante 0 puede contener un número). ¿Cuál es el código ensamblador MIPS?

RESPUESTA

Supongamos que el compilador coloca las tres constantes en punto flotante en memoria dentro del rango de fácil acceso con el puntero global $gp. Las dos primeras instrucciones cargan las constantes 5.0 y 9.0 en registros de punto flotante: f2c: lwc1 $f16,const5($gp) # $f16 = 5.0 (5.0 en memoria) lwc1 $f18,const9($gp) # $f18 = 9.0 (9.0 en memoria)

A continuación, se dividen para obtener la mantisa 5.0/9.0: div.s $f16, $f16, $f18 # $f16 = 5.0 / 9.0

3.5

263

Punto flotante

(Muchos compiladores dividirían 5.0 entre 9.0 en tiempo de compilación y guardarían la constante 5.0/9.0 en memoria, evitando así tener que dividir en tiempo de ejecución). A continuación, cargamos la constante 32.0 y la restamos de fahr ($f12): lwc1 $f18, const32($gp) # $f18 = 32.0 sub.s $f18, $f12, $f18 # $f18 = fahr – 32.0

Finalmente, multiplicamos los dos resultados intermedios, dejando el producto en $f0 como valor a retornar, y, entonces, se retorna: mul.s$f0, $f16, $f18 # $f0 = (5/9)*(fahr – 32.0) jr $ra # return

Ahora, veamos operaciones en punto flotante entre matrices, código muy habitual en programas científicos.

Compilación en MIPS de un procedimiento en punto flotante en C con dos matrices de dos dimensiones

Muchas operaciones en punto flotante se realizan en precisión doble. Realicemos la multiplicación de matrices X = X + Y * Z. Supongamos que X, Y y Z son matrices cuadradas con 32 elementos en cada dimensión.

EJEMPLO

void mm (double x[][], double y[][], double z[][]) { int i, j, k; for (i = 0; for (j = 0; for (k = 0; x[i][j] =

i!= 32; j!= 32; k!= 32; x[i][j]

i j k +

= i + 1) = j + 1) = k + 1) y[i][k] * z[k][j];

}

Las direcciones de inicio de las matrices son parámetros, de manera que están en $a0, $a1 y $a2. Supongamos que las variables enteras están en $s0, $s1 y $s2, respectivamente. ¿Cuál es el código ensamblador MIPS para el cuerpo del procedimiento? Observe que en el lazo más interno se usa x[i][j]. Puesto que el índice del lazo es k, este índice no afecta a x[i][j], por lo que se puede evitar cargarlo y almacenarlo en cada iteración. En lugar de eso, el compilador carga x[i][j] en un registro antes del lazo, acumula la suma de los productos de y[i][k] y z[k][j] en ese mismo registro, y almacena la suma en x[i][j] al finalizar el lazo más interno. Conseguimos un código más simple usando la pseudoinstrucción del lenguaje ensamblador li (que carga una constante en un registro), y l.d y s.d (que el ensamblador transforma en un par de instrucciones de transferencia de datos, lwc1 o swc1, para un par de registros en punto flotante).

RESPUESTA

264

Capítulo 3

Aritmética para computadores

El cuerpo del procedimiento empieza guardando el valor 32 de finalización del lazo en un registro temporal e inicializando las tres variables de los lazos for: mm:... li

$t1, 32

L1:

li li

$s0, 0 $s1, 0

L2:

li

$s2, 0

# $t1 = 32 (tamaño de la fila/ # fin del lazo) # i = 0; inicia el primer lazo for # j = 0; reinicia el segundo # lazo for # k = 0; reinicia el tercer # lazo for

Para calcular la dirección de x[i][j], necesitamos saber cómo se almacena en memoria una matriz de dos dimensiones 32 × 32. Como es de esperar, su organización es la misma que si hubiera 32 vectores de 32 elementos cada uno. Así que el primer paso es saltar los i vectores, o filas, para obtener el que queremos. Así, multiplicamos el índice en la primera dimensión por el tamaño de la fila, 32. Puesto que 32 es una potencia de 2, en lugar del producto podemos usar un desplazamiento: sll

$t2, $s0, 5 # $t2 = i * 25 (tamaño de la fila de x)

Ahora añadimos el segundo índice para seleccionar el j-ésimo elemento de la fila deseada: addu

$t2, $t2, $s1 # $t2 = i * tamaño(fila) + j

Para convertir esta suma en un índice de byte, la multiplicamos por el tamaño en bytes de un elemento de la matriz. Como cada elemento es de 8 bytes por ser de precisión doble, en lugar de multiplicar se puede desplazar a la izquierda tres posiciones: sll

$t2, $t2, 3 # $t2 = desplazamiento byte de [i][j]

A continuación, añadimos esta suma a la dirección base de x, que da la dirección de x [i][j], y después se carga el número en precisión doble x[i][j] en $f4: addu l.d

$t2, $a0, $t2 # $t2 = dirección byte de x[i][j] $f4, 0($t2) # $f4 = 8 bytes de x[i][j]

Las siguientes cinco instrucciones son casi idénticas a las cinco precedentes: calculan la dirección y después cargan el número en precisión doble z[k][j]. L3: sll $t0, $s2, 5 # $t0 = k * 25 (tamaño de la fila de z) addu $t0, $t0, $s1 # $t0 = k * tamaño(fila) + j sll $t0, $t0, 3 # $t0 = desplazamiento byte de [k][j] addu $t0, $a2, $t0 # $t0 = dirección byte de z[k][j] l.d $f16, 0($t0) # $f16 = 8 bytes de z[k][j]

Análogamente, las siguientes cinco instrucciones son como las últimas cinco: calculan la dirección y después cargan el número en precisión doble y[i][k].

3.5

265

Punto flotante

sll addu sll addu l.d

$t2, $s0, 5 $t0, $t0, $s2 $t0, $t0, 3 $t0, $a1, $t0 $f18, 0($t0)

# # # # #

$t0 = i * 25 (tamaño de la fila de y) $t0 = i * tamaño (fila) + k $t0 = tamaño (fila) [i][k] $t0 = desplazamiento byte de y[i][k] $f18 = 8 bytes de y[i][k]

Ahora que ya tenemos todos los datos cargados, ¡estamos preparados para hacer alguna operación en punto flotante! Multiplicamos los elementos de y y z que se encuentran en los registros $f18 y $f16, y entonces acumulamos la suma en $f4. mul.d $f16, $f18, $f16 # $f16 = y[i][k] * z[k][j] add.d $f4, $f4, $f16 # f4 = x[i][j]+ y[i][k] * z[k][j]

El bloque final incrementa el índice k y vuelve a iterar si el índice no es 32. Si es 32, y por tanto se ha acabado el lazo más interno, necesitamos almacenar en x[i][j] el resultado de la suma acumulada en $f4. addiu bne s.d

$s2, $s2, 1 $s2, $t1, L3 $f4, 0($t2)

# $k k + 1 # si (k != 32) ir a L3 # x[i][j] = $f4

Análogamente, estas últimas cuatro instrucciones incrementan las variables índice de los lazos intermedio y externo, volviendo a iterar si el índice no es 32 y finalizando si el índice es 32. addiu bne addiu bne ...

$s1, $s1, $s0, $s0,

$s1, $t1, $s0, $t1,

1 L2 1 L1

# # # #

$j si $i si

= j + (j != = i + (i !=

1 32) ir a L2 1 32) ir a L1

Extensión: La organización de las matrices presentada en este ejemplo, llamada por filas, se usa en C y muchos otros lenguajes de programación. Fortran, en cambio, usa la organización por columnas, donde la matriz se guarda columna por columna.

Extensión: Sólo 16 de los 32 registros en punto flotante de MIPS se pueden usar para operaciones en precisión doble: $f0, $f2, $f4,…, $f30. La precisión doble se calcula usando pares de estos registros de precisión simple. Los registros impares se usan sólo para cargar y almacenar la mitad derecha de los números en punto flotante de 64 bits. MIPS-32 añadió l.d y s.d al repertorio de instrucciones. MIPS-32 también añadió versiones “para pares sueltos” de todas las instrucciones en punto flotante, donde una instrucción simple se convertía en dos operaciones en punto flotante en paralelo sobre dos operandos de 32 bits dentro de los registros de 64 bits. Por ejemplo, add.ps $f0, $f2, $f4 es equivalente a add.s $f0, $f2, $f4 seguida por add.s $f1, $f3, $f5.

266

Capítulo 3

Aritmética para computadores

Extensión: Otra razón para separar los registros enteros y de punto flotante es que los microprocesadores en los años 80 no tenían suficientes transistores para poner la unidad de punto flotante en el mismo circuito integrado que la unidad de enteros. Por ello, la unidad de punto flotante, incluidos los registros de punto flotante, estaba disponible de manera opcional en un segundo circuito integrado. Estos circuitos aceleradores se llaman coprocesadores, y explican el acrónimo de las instrucciones load de punto flotante de MIPS: lwc1 significa cargar una palabra en el coprocesador 1, la unidad de punto flotante. (El coprocesador 0 se ocupa de la memoria virtual, descrita en el capítulo 5). Desde principios de los años 90, los microprocesadores integraron el punto flotante (y casi todo lo demás) en el mismo circuito integrado, y por ello el término “coprocesador”, junto con “acumulador” y “memoria central”, se añade a la lista de términos anticuados que delatan la edad de quien los usa.

Extensión: Como se ha mencionado en la sección 3.4, acelerar la división es más difícil que acelerar la multiplicación. Además de la SRT, otra técnica para aprovechar las ventajas de un multiplicador rápido es la iteración de Newton, en la que la división se convierte en la búsqueda del cero de una función para hallar el inverso 1/x, el cual se multiplica por el otro operando. Las técnicas de iteración no se pueden redondear adecuadamente sin calcular muchos bits extra. Un chip de TI (Texas Instruments) resuelve este problema calculando un inverso extra-preciso.

Extensión: Java adopta el estándar IEEE 754 en su definición de tipos de datos y operaciones en punto flotante. Así, el código del primer ejemplo podría haber sido generado para un método de una clase que convirtiese Fahrenheit a Celsius. El segundo ejemplo usa matrices multidimensionales, las cuales no están soportadas explícitamente en Java. Java permite vectores de vectores, pero cada vector puede tener su propia longitud, a diferencia de las matrices multidimensionales en C. Como los ejemplos del capítulo 2, una versión Java de este segundo ejemplo requeriría un buen trabajo de código de comprobación de los límites de las matrices, incluyendo un cálculo de una nueva longitud al final de cada fila. También necesitaría comprobar que las referencias a los objetos no son nulas.

Aritmética exacta Guarda: el primero de los dos bits extra almacenados a la derecha durante los cálculos intermedios de números en punto flotante; se usa para mejorar la precisión del redondeo.

Redondeo: método para hacer que el resultado en punto flotante quepa en el formato en punto flotante; normalmente el objetivo es encontrar el número más cercano que se puede representar en el formato.

A diferencia de los enteros, que pueden representar con exactitud todos los números que hay entre el menor y el mayor, los números en punto flotante normalmente son aproximaciones de un número que no pueden representar. La razón es que entre dos reales (0 y 1, por ejemplo) hay siempre una cantidad infinita de números reales, pero en punto flotante de precisión doble no se pueden representar más de 253 números. Lo mejor que se puede hacer es conseguir la representación en punto flotante más cercana al número real. Así, el IEEE 754 ofrece varios métodos de redondeo para permitir al programador elegir la aproximación deseada. El redondeo parece bastante simple, pero para redondear de forma exacta es necesario que el hardware incluya bits extra en el cálculo. En los ejemplos precedentes no concretamos el número de bits que podían ocupar las representaciones intermedias, pero claramente si hubiera que truncar cada resultado intermedio al número exacto de cifras, entonces no habría oportunidad de redondear. El IEEE 754, por tanto, mantiene 2 bits adicionales a la derecha durante las sumas intermedias, llamados guarda y redondeo, respectivamente. Veamos un ejemplo en decimal para mostrar la importancia de estos bits adicionales.

3.5

267

Punto flotante

Redondeo con dígitos de guarda

Sumar 2.56diez × 100 a 2.34diez × 102, suponiendo que tenemos 3 dígitos decimales significativos. Redondear al decimal más cercano con tres dígitos decimales, primero con dígitos de guarda y de redondeo, y luego sin ellos.

Primero debemos desplazar el número menor a la derecha para alinear los exponentes, de manera que 2.56diez × 100 se convierte en 0.0256diez × 102. Puesto que tenemos dígitos de guarda y redondeo, somos capaces de representar las dos cifras menos significativas cuando alineamos los exponentes. El dígito de guarda mantiene el 5, y el dígito de redondeo el 6. La suma es

EJEMPLO

RESPUESTA

2.3400diez + 0.0256diez 2.3656diez

Por lo tanto, la suma es 2.3656diez × 102. Puesto que tenemos dos dígitos para redondear, queremos redondear hacia abajo los valores entre 0 y 49, y hacia arriba los valores entre 51 y 99, siendo 50 el punto medio. Redondear hacia arriba la suma con tres dígitos significativos da 2.37diez × 102. Hacer esto sin dígitos de guarda ni redondeo deja dos cifras fuera del cálculo. La nueva suma es 2.34diez + 0.02diez

2.36diez La respuesta es 2.36diez × 102, errónea en 1 unidad en el último dígito respecto a la suma anterior. Puesto que el peor caso para el redondeo sería cuando el número verdadero estuviese exactamente a mitad de dos representaciones en punto flotante, la exactitud en punto flotante se mide, normalmente, en términos del número de bits de error en los bits menos significativos del significando; la medida se llama número de unidades en el último lugar, o ulp (units in the last place). Si un número estuviera equivocado en 2 unidades en los bits menos significativos, se diría que es erróneo en 2 ulps. Suponiendo que no hay desbordamiento, ni desbordamiento a cero, o excepciones de operación inválida, IEEE 754 garantiza que el computador usa el número que está dentro de media ulp. Extensión: Aunque el ejemplo anterior en realidad necesitaba justo un dígito extra, la multiplicación puede necesitar dos. Un producto binario puede tener un 0 inicial, lo que significa que el paso de normalización debe desplazar el producto un bit a la izquierda. Esto desplaza el bit de guarda a la posición del bit menos significativo del producto, haciendo que sea el bit de redondeo el que ayude a redondear con precisión el producto.

Unidades en el último lugar (ulp): número de bits erróneos en los bits menos significativos del significando entre el número real y el número que puede ser representado.

268

Bit sticky: bit usado para redondear, además de los bits de guarda y redondeo, que se activa a 1 siempre que hay bits no nulos a la derecha del bit de redondeo.

Capítulo 3

Aritmética para computadores

IEEE 754 tiene 4 modos de redondeo: redondear siempre hacia arriba (hacia +h), siempre hacia abajo (hacia –h), truncar, y redondear al par más próximo. El último modo es el que determina qué hacer si el número está exactamente a la mitad de dos representaciones. En Estados Unidos, el organismo recaudador de impuestos siempre redondea 0.50 dólares al alza, posiblemente para beneficio del propio organismo. Una manera más equitativa sería redondear al alza la mitad de las veces y a la baja la otra mitad. El IEEE 754 dice que si el bit menos significativo guardado es impar, se añade uno y, si es par, se trunca. Este método siempre crea un 0 en el bit menos significativo en el caso de desempate, y de aquí el nombre de este modo de redondeo, que es el que se usa con mayor frecuencia y el único que soporta Java. El objetivo de los bits adicionales para redondeo es conseguir que el computador obtenga el mismo resultado que se obtendría si los resultados intermedios fuesen calculados con precisión infinita y luego redondeados. Para dar soporte a este objetivo y redondear al próximo par, el estándar tiene un tercer bit, además de los de guarda y redondeo, que se actualiza a 1 cuando hay bits no nulos a la derecha del bit de redondeo. Este bit pegajoso (sticky bit) permite al computador ver la diferencia entre 0.50 . . . 00diez y 0.50 . . . 01diez cuando se redondea. El bit sticky se activa a 1, por ejemplo, durante la suma, cuando el número menor se desplaza a la derecha. Supongamos que sumamos 5.01diez × 10–1 a 2.34diez × 102 en el ejemplo anterior. Incluso con guarda y redondeo, estaríamos sumando 0.0050 a 2.34, con un resultado de 2.3450. El bit sticky se activaría porque hay bits no nulos a la derecha. Sin el bit sticky para recordar que hay algún 1 que se desplazó fuera, supondríamos que el número es igual a 2.345000...00, y redondeando al próximo par daría 2.34. Con el bit sticky, que recuerda que el número es mayor que 2.345000…000, redondearíamos a 2.35.

Extensión: Las arquitecturas PowerPC, SPARC64 y AMD SSE5 tienen un instrucción

Multiplicación-suma fusionada (fused multiply add): instrucción punto flotante que realiza la multiplicación y la suma, con un solo redondeo después de la suma.

para hacer una multiplicación y una suma con tres registros: a = a + (b x c). Obviamente, esta instrucción permite, potencialmente, unas mayores prestaciones punto flotante para esta operación tan frecuente. Igualmente importante es que en lugar de hacer dos redondeos —después de la multiplicación y después de la suma— como ocurriría si se implementa con dos instrucciones diferentes, la instrucción de multiplicación-suma sólo hace un redondeo después de la suma, y esto aumenta la precisión de la multiplicación-suma. Esta operación con un solo redondeo se llama multiplicación-suma fusionada (fused multiply add) y se ha incorporado a la revisión del estándar IEEE754 (véase sección 31.0 en el CD).

Resumen La siguiente Idea clave refuerza el concepto de programa almacenado del capítulo 2; el significado de la información no se puede determinar simplemente mirando los bits, porque los mismos bits pueden representar diversos objetos. Esta sección muestra que la aritmética del computador es finita, y por ello puede no coincidir con la aritmética natural. Por ejemplo, la representación en punto flotante del estándar IEEE 754 (–1)S × (1 +mantisa) × 2(exponente – sesgo) es casi siempre una aproximación al número real. Los sistemas de computación deben tener cuidado para minimizar este desfase entre la aritmética del computador y la aritmética del mundo real, y los programadores a veces necesitan estar atentos a las implicaciones de esta aproximación.

3.5

269

Punto flotante

Las tiras de bits no tienen ningún significado inherente. Pueden representar números con signo, sin signo, en punto flotante, instrucciones, etc. Lo que se representa depende de la instrucción que opera con los bits de esa palabra. La mayor diferencia entre los números del computador y los del mundo real es que los números del computador están limitados en su tamaño, lo cual limita su precisión; es posible calcular un número demasiado grande o demasiado pequeño para representarlo en una palabra. Los programadores deben recordar estos límites y escribir los programas consecuentemente.

Tipo C

Tipo Java

Transferencia de datos

IDEA clave

Operaciones

int

int

lw, sw, lui addu, addiu, subu, mult, div, and, andi, or, ori, nor, slt, slti

unsigned int



lw, sw, lui addu, addiu, subu, multu, divu, and, andi, or, ori, nor, sltu, sltiu

char



lb, sb, lui addu, addiu, subu, multu, divu, and, andi, or, ori, nor, sltu, sltiu



char

lh, sh, lui addu, addiu, subu, multu, divu, and, andi, or, ori, nor, sltu, sltiu

float

float

lwc1, swc1

add.s, sub.s, mult.s, div.s, c.eq.s, c.lt.s, c.le.s

double

double

l.d, s.d

add.d, sub.d, mult.d, div.d, c.eq.d, c.lt.d, c.le.d

En el último capítulo presentamos las clases de almacenamiento del lenguaje de programación C (véase la sección “Interfaz hardware software” de la sección 2.7). La tabla anterior muestra algunos de los tipos de datos de C y Java junto con las instrucciones de transferencia de datos de MIPS y las instrucciones que operan sobre estos tipos que aparecen en el capítulo 2 y en este capítulo. Observe que Java omite los enteros sin signo.

Interfaz hardware software

Imaginemos un formato en punto flotante IEEE 754 de 16 bits con 5 bits de expo- Autoevaluación nente. ¿Cuál sería el rango aproximado de números que podría representar? 1.

1.0000 0000 00 × 2

2. ±1.0000 0000 0 × 2

0

– 14

31

a 1.1111 1111 11 × 2 , 0 15

a ±1.1111 1111 1 × 2 , ±0, ±h, NaN

3. ±1.0000 0000 00 × 2

– 14

a ±1.1111 1111 11 × 2 , ±0, ±h, NaN

15

4. ±1.0000 0000 00 × 2

– 15

a ±1.1111 1111 11 × 2 , ±0, ±h, NaN

14

270

Capítulo 3

Aritmética para computadores

Extensión: Para ayudar a las comparaciones que pueden incluir NaNs, el estándar incluye opciones de comparación con orden y sin orden. Por ello, todo el repertorio de instrucciones tiene muchas formas de comparación para soportar NaNs. (Java no soporta las comparaciones no ordenadas). En un intento de exprimir hasta el último bit de precisión en una operación en punto flotante, el estándar permite que algunos números se representen en forma no normalizada. En lugar de tener un hueco entre 0 y el número normalizado más pequeño, IEEE permite los números desnormalizados (también llamados, en inglés, denorms o subnormals). Tienen el mismo exponente que el 0 pero una mantisa no cero. Permiten reducir paulatinamente la magnitud de un número hasta llegar a ser 0, cualidad llamada desbordamiento a cero gradual. Por ejemplo, el menor número positivo normalizado en precisión simple es 1.0000 0000 0000 0000 0000 000dos × 2–126 pero el menor número no normalizado en precision simple es 0.0000 0000 0000 0000 0000 001dos × 2–126, o 1.0dos × 2–149 Para precisión doble, el hueco al no normalizar va de 1.0 × 2–1022 a 1.0 × 2–1074. La posibilidad de un operando no normalizado ocasional ha dado muchos quebraderos de cabeza a lo diseñadores de punto flotante que intentan hacer unidades en punto flotante rápidas. Por ello, muchos computadores provocan una excepción si un operando no está normalizado, y dejan que el software complete la operación. Aunque las implementaciones en software son perfectamente válidas, sus menores prestaciones han reducido la popularidad de los números denorm en software en punto flotante portable. Además, si los programadores no esperan denorms, sus programas pueden tener sorpresas.

3.6

Paralelismo y aritmética del computador: asociatividad

3.6

Los programas, típicamente, han sido escritos para ejecutarse secuencialmente antes de reescribirlos para una ejecución concurrente, de modo que la pregunta natural es “¿obtienen las dos versiones los mismos resultados?”. Si la respuesta es no, se supone que hay algún error en la versión paralela que debe ser localizado. Actuando de esta forma se asume que al pasar de la versión secuencial a la paralela, el resultado no se ve afectado por la aritmética del computador. Es decir, si se suman un millón de números, el resultado es el mismo usando 1 procesador o 1000 procesadores. Esta suposición se cumple para números enteros en complemento a 2, incluso si se produce desbordamiento. Dicho de otra forma, la suma de enteros es asociativa. Desgraciadamente, esta afirmación no se mantiene para número en representación de punto flotante, porque son aproximaciones de los número reales y la aritmética del computador tiene precisión limitada. Es decir, la suma en punto flotante no es asociativa.

Representación en punto flotante

EJEMPLO

Veamos si x + (y + z) = (x + y) + z. Supongamos x = 1.5diez × 1038, y = 1.5diez × 1038, y z = 1.0 y que son números en precisión simple.

3.6

Paralelismo y aritmética del computador: asociatividad

Debido a que el intervalo de números que pueden representarse en punto flotante es muy grande, pueden aparecer problemas cuando se suman dos número muy grandes de distinto signo más un número pequeño, como se verá a continuación: x + (y + z) = 1.5diez × 1038 + (1.5diez × 1038 + 1.0) = 1.5diez × 1038 + (1.5diez × 1038) = 0.0 (x + y) + z = (1.5diez × 1038 + 1.5diez × 1038) + 1.0 = (0.0diez) + 1.0 = 1.0 Por lo tanto x + (y + z) | (x + y) + z, es decir, la suma punto flotante no es asociativa. Puesto que los números punto flotante tienen precisión limitada y son aproximaciones de número reales, 1.5diez × 1038 es tan grande respecto a 1.0diez que 1.5diez × 1038 + 1.0 es todavía 1.5diez × 1038. Este es el motivo de que la suma de x, y y z sea 0.0 o 1.0 dependiendo del orden en que se hacen las sumas en punto flotante; en consecuencia la suma punto flotante no es asociativa.

Una versión aún más desconcertante de este error habitual se produce en computadores paralelos en los que el planificador del sistema operativo utiliza un número diferente de procesadores dependiendo de qué programas estén ejecutándose en el computador. Un programador de sistemas paralelos no cuidadoso puede quedar desconcertado al ver que su programa proporciona respuestas ligeramente diferentes cada vez que se ejecuta, aún siendo el mismo código y las mismas entradas, porque al variar el número de procesadores de una ejecución a otra causaría que las sumas en punto flotante se calculen en un orden diferente. Dado este dilema, los programadores que escriben código paralelo con números punto flotante deben verificar si los resultados son creíbles incluso si no son exactamente igual a los del código secuencial. El campo que aborda estos aspectos se llama análisis numérico, y hay libros expresamente dedicados al mismo. Estos dilemas son una de las razones de la popularidad de las bibliotecas numéricas, como LAPACK y SCALAPACK, que han sido validadas tanto en su versión secuencial como en su versión paralela.

Extensión: Una versión sutil del problema de la asociatividad se presenta cuando dos procesadores hacen una computación redundante que se ejecutan en orden diferente, de forma que cada uno obtiene resultados ligeramente diferentes, aunque ambas respuestas sean consideras correctas. El error se produce si un salto condicional compara con un número punto flotante y los dos procesadores toman salidas diferentes, cuando el sentido común dice que deberían tomar el mismo camino.

271

RESPUESTA

272

Capítulo 3

3.7

Aritmética para computadores

Caso real: punto flotante en el x86

3.7

El x86 tiene instrucciones normales para multiplicar y dividir que operan enteramente sobre registros, a diferencia de la dependencia de Hi y Lo de MIPS. (De hecho, versiones posteriores del repertorio de instrucciones MIPS incorporan instrucciones similares). Las principales diferencias se encuentran en las instrucciones de punto flotante. La arquitectura en punto flotante de x86 es diferente de las de todos los demás computadores del mundo.

La arquitectura en punto flotante del x86 El coprocesador en punto flotante Intel 8087 se anunció en 1980. Esta arquitectura extendía el 8086 con unas 60 instrucciones en punto flotante. Intel proporcionó una arquitectura tipo pila para sus instrucciones en punto flotante: las cargas apilaban números en la pila, las operaciones buscaban los operandos en los dos elementos del tope de la pila, y los almacenamientos sacaban elementos de la pila. Intel complementó esta arquitectura con instrucciones y modos de direccionamiento que le permitían obtener algunas de las ventajas del modelo memoria-registro. Además de buscar los operandos en los dos elementos del tope de la pila, un operando puede estar en memoria o en uno de los siete registros debajo del tope de la pila internos al chip. De este modo, un repertorio de instrucciones completo tipo pila se complementa con un repertorio limitado de instrucciones memoria-registro. Sin embargo, este híbrido aún es un modelo de memoria-registro restringido, puesto que las cargas siempre mueven datos al tope de la pila, incrementando el puntero del tope de la pila, y los almacenamientos sólo pueden mover el tope de la pila a memoria. Intel usa la notación ST para indicar el tope de la pila, y ST(i) para representar el i-ésimo registro por debajo del tope de la pila. Otra característica novedosa de esta arquitectura es que los operandos son más largos en la pila de registros que cuando se almacenan en memoria, y todas las operaciones se realizan con esta precisión interna mayor. A diferencia del máximo de 64 bits en MIPS, los operandos en punto flotante de x86 en la pila tienen una longitud de 80 bits. Los números se convierten automáticamente al formato interno de 80 bits cuando se cargan y se reconvierten al tamaño adecuado en los almacenamientos. Esta precisión doble extendida no es soportada por los lenguajes de programación, aunque ha sido útil a los programadores de software matemático. Los datos en memoria pueden ser de 32 bits (precisión simple) o 64 bits (precisión doble) para números en punto flotante. La versión memoria-registro de estas instrucciones convierte automáticamente el operando de memoria a este formato de 80 bits de Intel antes de realizar la operación. Las instrucciones de transferencia de datos también convierten automáticamente los enteros de 16 y 32 bits a punto flotante, y viceversa, en las cargas y almacenamientos de enteros.

3.7

273

Caso real: punto flotante en el x86

Las operaciones en punto flotante x86 se pueden dividir en cuatro grandes grupos: 1. Instrucciones de movimiento de datos, que incluyen carga, carga constante y almacenamiento 2. Instrucciones aritméticas, que incluyen suma, resta, multiplicación, división, raíz cuadrada y valor absoluto 3. Comparación, que incluye instrucciones de envío del resultado al procesador entero para que pueda saltar 4. Instrucciones transcendentes, que incluyen las funciones seno, coseno, logaritmo y exponenciación La figura 3.20 muestra algunas de las 60 operaciones en punto flotante. Obsérvese que se dan incluso más combinaciones cuando se incluyen los modos de operando para estas operaciones. La figura 3.21 muestra las diversas opciones para la suma en punto flotante. Transferencia de datos

Aritméticas

Comparación

F{I}LD mem/ST(i) F{I}ADD{P} mem/ST(i) F{I}COM{P} F{I}ST{P} F{I}SUB{R}{P} mem/ST(i) F{I}UCOM{P}{P mem/ST(i) FLDPI FLD1 FLDZ

} F{I}MUL{P} mem/ST(i) FSTSW AX/mem F{I}DIV{R}{P} mem/ST(i) FSQRT FABS FRNDINT

Transcendentales FPATAN F2XM1 FCOS FPTAN FPREM FSIN FYL2X

FIGURA 3.20 Las instrucciones en punto flotante de x86. Se usan las llaves {} para mostrar las variaciones opcionales de las operaciones básicas: {I} significa que hay una versión entera de la instrucción, {P} significa que esta versión sacará un operando de la pila después de la operación, y {R} significa invertir el orden de los operandos en esta operación. La primera columna muestra las instrucciones de tranferencia de datos, las cuales mueven datos a memoria o a uno de los registros que están por debajo del tope de la pila. Las tres últimas operaciones de la primera columna cargan constantes en la pila: pi, 1.0 y 0.0. La segunda columna contiene las operaciones aritméticas descritas más arriba. Observe que las tres últimas operan sólo con el tope de la pila. La tercera columna son la instrucciones de comparación. Puesto que no hay instrucciones de salto específicas para punto flotante, el resultado de una comparación debe transferirse a la CPU de enteros vía la instrucción FSTSW, tanto al registro AX como a memoria, seguida por la instrucción SAHF para activar los códigos de condición. Entonces, se puede comprobar la comparación en punto flotante usando instrucciones de salto de enteros. La última columna muestra las operaciones en punto flotante de mayor nivel. No se soportan todas las operaciones sugeridas por la notación. Así, las operaciones F{I}SUB{R}{P} representan estas instrucciones del x86: FSUB, FISUB, FSUBR, FISUBR, FSUBP, FSUBRP. Para las instrucciones de resta entera, no hay versión con pop (FISUBP) ni pop inverso (FISUBRP).

Las instrucciones en punto flotante se codifican usando el código de operación ESC del 8086 y el especificador de dirección postbyte (véase figura 2.47). Las ope-

raciones de memoria reservan dos bits para decidir si el operando es un punto flotante de 32 o 64 bits o un entero de 16 o 32 bits. Estos dos mismos bits se utilizan

274

Capítulo 3

Aritmética para computadores

Instrucción

Operandos

FADD

Comentarios Ambos operandos en la pila; el resultado reemplaza el tope de la pila.

FADD

ST(i)

FADD

ST(i), ST Un operando fuente es el tope de la pila; el resultado reemplaza

Un operando fuente es el i-ésimo registro por debajo del tope de la pila; el resultado reemplaza el tope de la pila.

el i-ésimo registro por debajo del tope de la pila.

FADD

mem32

Un operando fuente es una posición en memoria de 32 bits; el resultado reemplaza el tope de la pila.

FADD

mem64

Un operando fuente es una posición en memoria de 64 bits; el resultado reemplaza el tope de la pila.

FIGURA 3.21

Las variaciones de operandos para la suma en punto flotante del x86.

en versiones que no acceden a memoria para decidir si se debe sacar un dato de la pila después de la operación y si es el tope de la pila o un registro inferior el que debe recibir el resultado. Las prestaciones del punto flotante de la familia x86 tradicionalmente ha quedado rezagado respecto a otros computadores. Por este motivo, Intel creó una arquitectura en punto flotante más tradicional como parte de SSE2.

La arquitectura en punto flotante Streaming SIMD Extensión 2 (SSE2) de Intel En el capítulo 2 se indica que en el año 2001 Intel añadió 144 instrucciones a su arquitectura, que incluían operaciones y registros en punto flotante de precisión doble. Incluye ocho registros de 64 bits que se pueden usar para operandos en punto flotante, dando al compilador una alternativa diferente para las operaciones en punto flotante que la arquitectura de pila única. Los compiladores pueden escoger utilizar los ocho registros SSE2 como registros en punto flotante como los que se encuentran en otros computadores. AMD amplió el número a 16 como parte de AMD64, que fue renombrado como EM64T por Intel. La figura 3.22 resume las instrucciones SSE y SSE2. Además de almacenar un número punto flotante de precisión simple o doble en un registro, Intel permite que varios operandos punto flotante se empaqueten en un único registro SSE2 de 128 bits: cuatro de precisión simple o dos de precisión doble. Así, los 16 registros punto flotante del SSE2 son realmente de 128 bits. Si los operandos se pueden disponer en memoria como datos alineados de 128 bits, entonces las transferencias de datos de 128 bits pueden cargar o almacenar varios operandos por instrucción. Las operaciones aritméticas admiten este formato de punto flotante empaquetado, ya que pueden operar simultáneamente sobre cuatro simples o dos dobles. Esta nueva arquitectura puede más que doblar las prestaciones de la arquitectura de pila.

3.8

275

Falacias y errores habituales

Transferencia de datos

Aritmética

Comparación

MOV {A/U} {SS/PS/SD/ PD} xmm, mem/xmm

ADD {SS/PS/SD/PD} xmm, mem/xmm

CMP {SS/PS/SD/ PD}

MOV {H/L} {PS/PD} xmm, mem/xmm

SUB{SS/PS/SD/PD} xmm, mem/xmm MUL{SS/PS/SD/PD} xmm, mem/xmm DIV{SS/PS/SD/PD} xmm, mem/xmm SQRT{SS/PS/SD/PD} mem/xmm MAX {SS/PS/SD/PD} mem/xmm MIN{SS/PS/SD/PD} mem/xmm

FIGURA 3.22 Las instrucciones punto flotante SSE/SSE2 del x86. xmm significa que un operando es un registro SSE2 de 128 bits, y mem/xmm que el otro operando o bien está en memoria o bien está en un registro SSE2. Usamos las llaves {} para mostrar variantes opcionales de las operaciones básicas: {SS} significa punto flotante de precisión simple escalar, o un operando de 32 bits en un registro de 128 bits; {PS} significa punto flotante de precisión simple empaquetado, o cuatro operandos de 32 bits en un registro de 128 bits; {SD} significa punto flotante de precisión doble escalar, o un operando de 64 bits en un registro de 128 bits; {PD} significa punto flotante de precisión doble empaquetado, o dos operandos de 32 bits en un registro de 128 bits; {A} significa que el operando de 128 bits está alineado en memoria; {U} significa que el operando de 128 bits no está alineado en memoria; {H} significa mover la mitad superior del operando de 128 bits; {L} significa mover la mitad inferior del operando de 128 bits.

3.8

Falacias y errores habituales

3.8

Entonces las matemáticas se pueden definir como aquella materia en la que nunca sabemos de lo que hablamos, ni si lo que decimos es cierto.

Las falacias y errores en aritmética generalmente surgen de la diferencia entre la precisión limitada de la aritmética del computador y la precisión ilimitada de la BERTRAND RUSSELL, Recent Words on the aritmética natural. Falacia: del mismo modo que una instrucción de desplazamiento a la izquierda puede sustituir una multiplicación entera por una potencia de 2, un desplazamiento a la derecha es lo mismo que una división entera por una potencia de 2. Recordemos que un número binario x, donde xi significa el i–ésimo bit, representa el número . . . + (x3 × 23) + (x2 × 22) + (x1 × 21) + (x0 × 20) Desplazar los bits de x a la derecha n bits parecería ser lo mismo que dividir por 2n. Y esto es cierto para enteros sin signo. El problema está con los enteros con signo. Por ejemplo, supongamos que queremos dividir –5diez entre 4diez; el cociente debería ser –1diez. La representación en complemento a dos de –5diez es 1111 1111 1111 1111 1111 1111 1111 1011dos

Principles of mathematics, 1901

276

Capítulo 3

Aritmética para computadores

Según esta falacia, desplazar a la derecha dos posiciones debería dividir por 4diez (22): 0011 1111 1111 1111 1111 1111 1111 1110dos

Con un 0 en el bit de signo, este resultado es claramente erróneo. El valor creado por el desplazamiento a la derecha es en realidad 1 073 741 822diez en lugar de –1diez. Una solución sería tener un desplazamiento a la derecha aritmético que extiende el signo en lugar de desplazar con 0s. Un desplazamiento aritmético de dos bits a la derecha de –5diez produce 1111 1111 1111 1111 1111 1111 1111 1110dos

El resultado es –2diez en lugar de –1diez; cercano, pero no correcto. Error: la instrucción de suma con inmediato sin signo de MIPS (add immediate unsigned, addiu) extiende el signo de su campo inmediato de 16 bits. A pesar de su nombre, addiu se usa para sumar constantes a enteros con signo cuando no se tiene en cuenta el desbordamiento. MIPS no tiene una instrucción de resta con inmediato y los números negativos necesitan la extensión de signo, de manera que los arquitectos de MIPS decidieron extender el signo del campo inmediato. Falacia: sólo los matemáticos teóricos se preocupan de la precisión en punto flotante. Los titulares de los periódicos de noviembre de 1994 demuestran la falsedad de esta afirmación (véase figura 3.23). A continuación está la historia oculta que se encuentra tras estos titulares. El Pentium usa un algoritmo estándar de división en punto flotante que genera múltiples bits del cociente por paso, usando los bits más significativos del divisor y del dividendo para estimar los 2 bits siguientes del cociente. La estimación se toma de una tabla de búsqueda que contiene –2, –1, 0, +1 ó +2. La predicción se multiplica por el divisor y se resta del resto para generar un nuevo resto. Igual que la división sin restauración, si una predicción previa da un resto demasiado grande, el resto parcial se ajusta en el siguiente paso. Evidentemente, había cinco elementos de la tabla del 80486 a los que Intel pensó que nunca se accedería, y en el Pentium optimizaron la PLA para que devolviera 0 en lugar de 2 en estas situaciones. Intel se equivocaba: mientras los 11 primeros bits eran siempre correctos, se podían producir errores ocasionales en los bits 12 a 52, o de la 4ª a la 15ª cifra decimal. A continuación reconstruimos cronológicamente los hechos del desaguisado del error del Pentium: ■

Julio de 1994: Intel descubre el error en el Pentium. El coste real para arreglarlo asciende a varios cientos de miles de dólares. Siguiendo los procedimientos normales para corregir errores, se tardaría meses en hacer el cambio, volver a verificar y poner el chip correcto en producción. Intel planificó poner chips correctos en producción en enero de 1995, estimando que se fabricarían entre 3 y 5 millones de Pentiums con el error.

3.8

Falacias y errores habituales

FIGURA 3.23 Recortes de artículos de revista y periódicos de noviembre de 1994, que incluyen el New York Times, el San Jose Mercury News, el San Francisco Chronicle y el Infoworld. El error en la división en punto flotante del Pentium llegó a estar en el número uno en la lista de los “Top 10” del show televisivo de David Letterman. Intel finalmente gastó unos 300 millones de dólares para reemplazar los chips defectuosos. ■

Septiembre de 1994: Thomas Nicely, profesor de matemáticas en el Lynchburg College de Virginia, descubre el error. Después de llamar al soporte técnico de Intel y no obtener reacción oficial alguna, informa el descubrimiento a través de Internet. La noticia corre como la pólvora y algunos señalan que incluso los errores pequeños se convierten en grandes cuando se multiplican por números grandes: la proporción de gente con una enfermedad rara multiplicada por la población de Europa, por ejemplo, podría conducir a una estimación incorrecta del número de personas enfermas.



7 de noviembre de 1994: Electronic Engineering Times pone la historia en su portada, y rápidamente otros periódicos la imitan.



22 de noviembre de 1994: Intel publica una nota de prensa, considerándolo un “pequeño error” (glitch). “El Pentium puede tener errores en la décima cifra… La mayoría de los ingenieros y analistas financieros sólo necesitan precisión hasta la cuarta o quinta cifra decimal. Los usuarios de hojas de cálculo y procesadores de texto no tienen por qué preocuparse… Habrá unas cuantas docenas de personas a quienes pueda afectar. Hasta ahora sólo hemos tenido noticias de una… [Sólo] los matemáticos teóricos (con

277

278

Capítulo 3

Aritmética para computadores

máquinas Pentium compradas antes del verano) podrían verse afectados.” Lo que más molestó a muchos usuarios fue que se les pidió que describieran sus aplicaciones a Intel y luego Intel decidiría si su aplicación merecía un Pentium nuevo sin el error de división. ■

5 de diciembre de 1994: Intel asegura que el error se produce una vez cada 27 000 años para los usuarios habituales de hojas de cálculo. Intel supone que un usuario hace 1000 divisiones por día y multiplica la tasa de error suponiendo que los números en punto flotante son aleatorios, lo que es uno entre 9000 millones, con lo que obtiene 9 millones de días, o 27 000 años. La situación empieza a calmarse, a pesar de que Intel no se preocupó de explicar por qué un usuario típico accedería a números en punto flotante de forma aleatoria.



12 de diciembre de 1994: la división de investigación de IBM cuestiona los cálculos de la tasa de errores (este artículo está disponible en www.mkp.com/ books_catalog/cod/links.htm). IBM asegura que las hojas de cálculo habituales, haciedo cálculos durante 15 minutos al día, podrían producir errores relacionados con el Pentium con una frecuencia de uno cada 24 días. IBM supone 5000 divisiones por segundo, durante 15 minutos, lo que da 4.2 millones de divisiones por día, y no tiene en cuenta la distribución aleatoria, calculando en su lugar las posibilidades como una entre 100 millones. Como resultado, IBM detiene inmediatamente la producción de computadores personales basados en el Pentium. Las cosas se ponen de nuevo mal para Intel.



21 de diciembre de 1994: Intel publica lo siguiente, firmado por el presidente de Intel y varios cargos directivos: “Intel desea disculparse sinceramente por su manera de llevar el tema del error

del procesador Pentium recientemente aparecido. El símbolo Intel Inside significa que su computador tiene un microprocesador sin igual en calidad y prestaciones. Miles de empleados en Intel trabajan muy duro para asegurar la veracidad de esta afirmación. Pero ningún microprocesador es perfecto. Lo que Intel continúa considerando un problema tecnológico extremadamente minúsculo ha cobrado vida propia. Aunque Intel se mantiene firme en su confianza en la calidad de la versión actual del procesador Pentium, admitimos que muchos usuarios se han mostrados preocupados. Queremos resolver estas preocupaciones. Intel cambiará la versión actual del procesador Pentium por una actualizada, en la cual este error en la división en punto flotante esté corregido, para cualquier usuario que lo solicite, sin coste alguno durante la vida de su computador”. Los analistas estimaron que esta operación costó a Intel unos 500 millones de dólares, y los empleados de Intel no obtuvieron aguinaldo de Navidad ese año. Esta historia nos lleva a reflexionar sobre algunos puntos. ¿No habría sido más barato reparar el error en julio de 1994? ¿Cuál fue el coste de restituir la reputación de Intel? ¿Y cuál es la responsabilidad de la compañía cuando se descubren errores en un producto tan ampliamente usado y en el que se confía tanto como un microprocesador?

3.8

279

Falacias y errores habituales

Instrucciones del núcleo del MIPS

Nombre

Formato

Núcleo aritmético del MIPS

Nombre

Formato

add

add

R

multiply

mult

R

add immediate

addi

I

multiply unsigned

multu

R

add unsigned

addu

R

divide

div

R

add immediate unsigned

addiu

I

divide unsigned

divu

R

subtract

sub

R

move from Hi

mfhi

R

subtract unsigned

subu

R

move from Lo

mflo

R

and

and

R

move from system control (EPC)

mfc0

R

and immediate

andi

I

floating-point add single

add.s

R

or

or

R

floating-point add double

add.d

R

or immediate

ori

I

floating-point subtract single

sub.s

R

nor

nor

R

floating-point subtract double

sub.d

R

shift left logical

sll

R

floating-point multiply single

mul.s

R

shift right logical

srl

R

floating-point multiply double

mul.d

R

load upper immediate

lui

I

floating-point divide single

div.s

R

load word

lw

I

floating-point divide double

div.d

R

store word

sw

I

load word to floating-point single

lwc1

I

load halfword unsigned

lhu

I

store word to floating-point single

swc1

I

store halfword

sh

I

load word to floating-point double

ldc1

I

load byte unsigned

lbu

I

store word to floating-point double

sdc1

I

store byte

sb

I

branch on floating-point true

bc1t

I

branch on equal

beq

I

branch on floating-point false

bc1f

I

branch on not equal

bne

I

floating-point compare single

c.x.s

R

jump

j

J

(x = eq, neq, lt, le, gt, ge)

jump and link

jal

J

floating-point compare double

c.x.d

R

jump register

jr

R

(x = eq, neq, lt, le, gt, ge)

set less than

slt

R

set less than immediate

slti

I

set less than unsigned

sltu

R

set less than immediate unsigned

sltiu

I

FIGURA 3.24 Instrucciones de MIPS vistas hasta el momento. Este libro se centra en las instrucciones de la columna izquierda. Esta información también se encuentra en las columnas 1 y 2 de la tarjeta de referencia MIPS que se encuentra al principio de este libro.

En abril de 1997 se encontró otro error en el punto flotante de los microprocesadores Pentium Pro y Pentium II. Cuando las instrucciones de almacenamiento punto flotante a entero (fist, fistp) encontraban un número en punto flotante que era demasiado grande para caber en una palabra de 16 o 32 bits tras convertirlo a entero, activaban un bit equivocado en la palabra de estado de FPO (excepción de precisión en lugar excepción de operación no válida). Para honra de Intel, esta vez reconocieron públicamente el error y ofrecieron un software para evitarlo, una reacción bastante diferente de la que tuvieron en 1994.

280

Capítulo 3

3.9

Aritmética para computadores

Conclusiones finales

3.9

Un efecto lateral de los computadores de programa almacenado es que las tiras de bits no tienen un significado propio. La misma tira de bits puede representar un entero con signo, un entero sin signo, un número en punto flotante, una instrucción, etc. Es la instrucción que opera con una palabra la que determina su significado. La aritmética del computador se distingue de la aritmética de lápiz y papel por las restricciones de la precisión limitada. Este límite puede producir operaciones erróneas al calcular números mayores o menores que los límites predefinidos. Tales anomalías, llamadas “desbordamiento” y “desbordamiento a cero”, pueden provocar excepciones o interrupciones, que son casos de emergencia similares a las llamadas a subrutinas no planificadas. El capítulo 4 trata las excepciones con más detalle. La aritmética en punto flotante tiene la dificultad añadida de ser una aproximación de los números reales, y se necesita tener cuidado para poder asegurar que el número elegido por el computador sea la representación más cercana al número real. Los desafíos de la imprecisión y la representación limitada son algunas de las cuestiones que inspiran el campo del análisis numérico. El cambio reciente al paralelismo hará brillar otra vez el faro del análisis numérico, porque las soluciones que fueron consideradas seguras en secuencial durante largo tiempo deberán ser reconsideradas cuando se busque el algoritmo más rápido para computadores paralelos que obtenga resultados correctos. A lo largo de los años, la aritmética del computador se ha ido estandarizando, aumentado así en gran medida la portabilidad de los programas. La aritmética de enteros en complemento a dos y la aritmética en punto flotante IEEE 754 se encuentran en la gran mayoría de los computadores que se venden hoy en día. Por ejemplo, todos los computadores de sobremesa que se han vendido desde la aparición de este libro siguen estas convenciones. Junto con la explicación de la aritmética del computador, en este capítulo se describe la mayor parte del repertorio de instrucciones de MIPS. Un punto de confusión son las diferencias entre las instrucciones que se ven en este capítulo, las instrucciones que realmente ejecutan los chips de MIPS y las instrucciones que aceptan los ensambladores de MIPS. Las dos figuras siguientes intentan aclarar esta cuestión. La figura 3.24 lista las instrucciones MIPS cubiertas en este capítulo y el capítulo 2. Al conjunto de instrucciones del lado izquierdo las llamamos núcleo de MIPS. Las instrucciones del lado derecho se llaman núcleo aritmético de MIPS. A la izquierda de la figura 3.25 están las instrucciones que el procesador MIPS ejecuta y que no están en la figura 3.24. A éstas las llamamos el repertorio completo de instrucciones hardware MIPS-32. A la derecha de la figura 3.25 están las instrucciones aceptadas por el ensamblador que no son parte de MIPS-32. A éstas las llamamos repertorio de instrucciones pseudo MIPS. La figura 3.26 muestra la frecuencia de aparición de las instrucciones MIPS en los programas de prueba de enteros y punto flotante SPEC2006. Se listan todas las instrucciones que representaban al menos un 0,3% de las instrucciones totales ejecutadas. La siguiente tabla resume esta información.

3.9

281

Conclusiones finales

Resto del MIPS-32

Nombre

Formato

exclusive or (rs ˆ rt) exclusive or immediate

xor xori

R I

absolute value negate (signed or unsigned)

shift right arithmetic shift left logical variable shift right logical variable

sra sllv srlv

R R R

rotate left rotate right multiply and don’t check oflw (signed or uns.)

shift right arithmetic variable

srav

R

multiply and check oflw (signed or uns.)

move to Hi move to Lo load halfword

mthi mtlo lh

R R I

divide and check overflow divide and don’t check overflow remainder (signed or unsigned)

load byte load word left (unaligned) load word right (unaligned) store word left (unaligned) store word right (unaligned) load linked (atomic update) store cond. (atomic update)

lb lwl lwr swl swr ll sc

I I I I I I I

load immediate load address load double store double unaligned load word unaligned store word unaligned load halfword (signed or uns.)

move if zero move if not zero multiply and add (S or uns.)

movz movn

R R R

unaligned store halfword branch branch on equal zero

ulhs ush b beqz bxs

rs,rt,L

seq sne sxs

rd,rs,rt rd,rs,rt rd,rs,rt

load to floating point (s or d)

l.f

rd,addr

store from floating point (s or d)

s.f

rd,addr

multiply and subtract (S or uns.)

madds

Pseudo MIPS

msubs bgezal

I

branch on compare (signed or unsigned)

branch on v zero and link

I

(x = lt, le, gt, ge)

branch on < zero and link jump and link register branch compare to zero

bltzal jalr bxz

I R I

set equal set not equal set on compare (signed or unsigned)

bxzl

I

(x = lt, le, gt, ge)

branch compare reg likely trap if compare reg

bxl tx

I R

trap if compare immediate

txi

I

rfe

R

syscall

I

branch compare to zero likely (x = lt, le, gt, ge)

Nombre abs negs rol ror muls mulos div divu rems li la ld sd ulw usw

Formato rd,rs rd,rs rd,rs,rt rd,rs,rt rd,rs,rt rd,rs,rt rd,rs,rt rd,rs,rt rd,rs,rt rd,imm rd,addr rd,addr rd,addr rd,addr rd,addr rd,addr rd,addr Label rs,L

(x = eq, neq, lt, le, gt, ge) return from exception system call break (cause exception)

break

I

move from FP to integer

mfc1

R

move to FP from integer

mtc1

R

FP move (s or d)

mov.f

R

FP move if zero (s or d)

movz.f

R

FP move if not zero (s or d)

movn.f

R

FP square root (s or d)

sqrt.f

R

FP absolute value (s or d)

abs.f

R

FP negate (s or d)

neg.f

R

FP convert (w, s, or d)

cvt.f.f

R

FP compare un (s or d)

c.xn.f

R

FIGURA 3.25 Resto del repertorio de instrucciones de MIPS-32 y “pseudo MIPS”. La presencia de una f significa la existencia de una versión de la instrucción en punto flotante de precisión simple (s) o precisión doble (d), mientras que una s indica la existencia de versiones con signo y sin signo (u). MIPS-32 también tiene instrucciones en punto flotante para multiplicar y sumar/restar (madd, f/msub.f), techo (ceil.f), truncar (trunc.f), redondeo (round.f) y recíproco (recip.f). El subrayado indica la letra que se debe incluir para representar el tipo de dato.

282

Capítulo 3

Núcleo del MIPS

Aritmética para computadores

Nombre

Entero

Pt. ft.

Núcleo aritmético + MIPS-32 Nombre

add add immediate add unsigned add immediate unsigned subtract unsigned AND AND immediate OR OR immediate NOR shift left logical shift right logical load upper immediate load word store word load byte store byte branch on equal (zero) branch on not equal (zero) jump and link

add addi addu addiu subu AND ANDi OR ORi NOR sll srl lui lw sw lbu sb beq bne jal

0.0% 0.0% 5.2% 9.0% 2.2% 0.2% 0.7% 4.0% 1.0% 0.4% 4.4% 1.1% 3.3% 18.6% 7.6% 3.7% 0.6% 8.6% 8.4% 0.7%

0.0% 0.0% 3.5% 7.2% 0.6% 0.1% 0.2% 1.2% 0.2% 0.2% 1.9% 0.5% 0.5% 5.8% 2.0% 0.1% 0.0% 2.2% 1.4% 0.2%

FP add double FP subtract double FP multiply double FP divide double FP add single FP subtract single FP multiply single FP divide single load word to FP double store word to FP double load word to FP single store word to FP single branch on floating-point true branch on floating-point false floating-point compare double multiply shift right arithmetic load half store half

jump register

jr

1.1%

0.2%

set less than

slt

9.9%

2.3%

set less than immediate

slti

3.1%

0.3%

set less than unsigned

sltu

3.4%

0.8%

set less than imm. uns.

sltiu

1.1%

0.1%

add.d sub.d mul.d div.d add.s sub.s mul.s div.s l.d s.d l.s s.s bc1t bc1f c.x.d mul sra lhu sh

Entero 0.0% 0.0% 0.0% 0.0% 0.0% 0.0% 0.0% 0.0% 0.0% 0.0% 0.0% 0.0% 0.0% 0.0% 0.0% 0.0% 0.5% 1.3% 0.1%

Pt. ft. 10.6% 4.9% 15.0% 0.2% 1.5% 1.8% 2.4% 0.2% 17.5% 4.9% 4.2% 1.1% 0.2% 0.2% 0.6% 0.2% 0.3% 0.0% 0.0%

FIGURA 3.26 Frecuencia de las instrucciones MIPS en los programas SPEC2006 de entero y punto flotante. En esta tabla se incluyen todas las instrucciones que obtuvieron al menos un 1%. Las pseudoinstrucciones se convierten a MIPS-32 antes de la ejecución, y por esto no aparecen aquí.

Observe que aunque los programadores y autores de compiladores podrían usar MIPS-32 para tener un menú de opciones más rico, las instrucciones núcleo de MIPS dominan la ejecución de enteros de SPEC2006, y el núcleo entero más el núcleo aritmético dominan el SPEC2006 de punto flotante, como se muestra en la siguiente tabla. Subconjunto de instrucciones Núcleo del MIPS

Entero

Pt. ft.

98%

31%

Núcleo aritmético del MIPS

2%

66%

Resto del MIPS-32

0%

3%

En el resto del libro, nos centraremos en las instrucciones del núcleo de MIPS –el repertorio de instrucciones enteras, excluyendo la multiplicación y la división– para hacer la explicación del diseño del computador más fácil. Como se puede ver, el núcleo de MIPS incluye las instrucciones más habituales, y asegurará que entender un computador que ejecuta el núcleo de MIPS dará suficiente bagaje para comprender computadores más ambiciosos.

3.11

283

Contribución de Matthew Farrens, UC Davis

3.10

Perspectiva histórica y lecturas recomendadas

3.10

La ley de Gresham (“El dinero malo expulsa al bueno”) para los computadores diría “Lo rápido expulsa a lo lento, incluso aunque lo rápido sea erróneo”.

Esta sección examina la historia del punto flotante desde von Neumann, incluido el esfuerzo de estandarización sorprendentemente controvertido de IEEE, más el análisis razonado de la arquitectura de pila de 80 bits para punto flotante del x86. W. Kahan, 1992 Véase sección 3.10.

3.11

Ejercicios

3.11

Contribución de Matthew Farrens, UC Davis

3.11

Nunca rendirse, nunca rendirse, nunca, nunca, nunca —en nada, pequeño o grande, importante o insignificante— nunca rendirse. Winston Churchil, discurso en la Harrow School, 1941.

Ejercicio 3.1 El libro muestra cómo sumar y restar números binarios y números decimales. Sin embargo, hay otros sistemas de numeración que han sido también muy populares en el mundo de los computadores, por ejemplo el sistema de numeración octal (base 8). La siguiente tabla muestra parejas de números octales. A

B

a.

5323

2275

b.

0147

3457

3.1.1 [5]<3.2> ¿Cuál es el resultado de sumar A y B suponiendo que representan números octales de 12 bits sin signo? Dé el resultado en octal. 3.1.2 [5]<3.2> ¿Cuál es el resultado de sumar A y B suponiendo que representan números octales de 12 bits en formato signo-magnitud? Dé el resultado en octal. 3.1.3 [10]<3.2> Convierta A en un número decimal, suponiendo que es un número sin signo. Repita el problema suponiendo que es un número en formato signo-magnitud.

La siguiente tabla muestra parejas de números octales. A

B

a.

2162

2032

b.

2646

1066

284

Capítulo 3

Aritmética para computadores

3.1.4 [5]<3.2> ¿Cuál es el resultado de A  B suponiendo que representan números octales de 12 bits sin signo? Dé el resultado en octal. 3.1.5 [5]<3.2> ¿Cuál es el resultado de A  B suponiendo que representan números octales de 12 bits en formato signo-magnitud? Dé el resultado en octal. 3.1.6 [10]<3.2> Convierta A en un número decimal. ¿Por qué base 8 (octal) es un sistema de numeración interesante para representar valores en un computador?

Ejercicio 3.2 Otro sistema de numeración también ampliamente utilizado para representar números en un computador es el sistema hexadecimal (base 16). De hecho, es mucho más popular que base 8. La siguiente tabla muestra parejas de números hexadecimales. A

B

a.

OD34

DD17

b.

BA1D

3617

3.2.1 [5]<3.2> ¿Cuál es el resultado de sumar A y B suponiendo que representan números hexadecimales de 16 bits sin signo? Dé el resultado en hexadecimal. 3.2.2 [5]<3.2> ¿Cuál es el resultado de sumar A y B suponiendo que representan números hexadecimales de 16 bits en formato signo-magnitud? Dé el resultado en hexadecimal. 3.2.3 [10]<3.2> Convierta A en un número decimal, suponiendo que es un número sin signo. Repita el problema suponiendo que es un número en formato signo-magnitud.

La siguiente tabla muestra parejas de números hexadecimales. A

B

a.

BA7C

241A

b.

AADF

47BE

3.2.4 [5]<3.2> ¿Cuál es el resultado de A  B suponiendo que representan

números hexadecimales de 16 bits sin signo? Dé el resultado en hexadecimal. 3.2.5 [5]<3.2> ¿Cuál es el resultado de A  B suponiendo que representan núme-

ros hexadecimales de 16 bits en formato signo-magnitud? Dé el resultado en hexadecimal.

3.11

285

Contribución de Matthew Farrens, UC Davis

3.2.6 [10]<3.2> Convertir A en un número decimal. ¿Por qué base 16 (hexadecimal) es un sistema de numeración interesante para representar valores en un computador?

Ejercicio 3.3 Cuando un resultado es demasiado grande para ser representado correctamente con un tamaño dado de palabra finito se produce desbordamiento. Cuando número es demasiado pequeño para ser representado correctamente, por ejemplo un número negativo cuando se está trabajando con aritmética sin signo, se produce desbordamiento a cero. (Cuando la suma de dos números enteros negativos da un resultado positivo se considera en muchos textos como desbordamiento a cero, pero en este texto lo consideraremos un desbordamiento.) La siguiente tabla muestra parejas de números decimales. A

B

a.

69

90

b.

102

44

3.3.1 [5]<3.2> Calcule A  B suponiendo que A y B son números decimales enteros de 8 bits sin signo. ¿Se produce desbordamiento, desbordamiento a cero o ninguno de los dos? 3.3.2 [5]<3.2> Calcule A + B suponiendo que A y B son números decimales enteros de 8 bits en formato signo-magnitud. ¿Se produce desbordamiento, desbordamiento a cero o ninguno de los dos? 3.3.3 [5]<3.2> Calcule A  B suponiendo que A y B son números decimales enteros de 8 bits en formato signo-magnitud. ¿Se produce desbordamiento, desbordamiento a cero o ninguno de los dos? La siguiente tabla muestra parejas de números decimales. A

B

a.

200

103

b.

247

237

3.3.4 [10]<3.2> Calcule A + B usando aritmética saturada suponiendo que A y B son números decimales enteros de 8 bits en complemento a 2. Dé el resultado en decimal. 3.3.5 [10]<3.2> Calcule A  B usando aritmética saturada suponiendo que A y B son números decimales enteros de 8 bits en complemento a 2. Dé el resultado en decimal. 3.3.6 [10]<3.2> Calcule A + B usando aritmética saturada suponiendo que A y B son números decimales enteros de 8 bits sin signo. Dé el resultado en decimal.

286

Capítulo 3

Aritmética para computadores

Ejercicio 3.4 Nos centramos ahora en la multiplicación. Usaremos los números de la siguiente tabla. A

B

a.

50

23

b.

66

04

3.4.1 [20]<3.3> Calcule el producto de A y B, octales enteros de 6 bits sin signo, utilizando una tabla similar a la de la figura 3.7 y el hardware descrito en la figura 3.4. Muestra el contenido de los registros en cada paso. 3.4.2 [20]<3.3> Calcule el producto de A y B, hexadecimales enteros de 8 bits sin signo, utilizando una tabla similar a la de la figura 3.7 y el hardware descrito en la figura 3.6. Muestre el contenido de los registros en cada paso. 3.4.3 [60]<3.3> Escriba un programa en lenguaje ensamblador de MIPS para calcular el producto de dos números enteros sin signo A y B, utilizando el esquema de la figura 3.4.

La siguiente tabla muestra parejas de números octales. A

B

a.

54

67

b.

30

07

3.4.4 [30]<3.3> En la multiplicación de números con signo, una forma para obtener el resultado correcto es convertir el multiplicando y el multiplicador en números positivos y determinar el signo del resultado de acuerdo con los signos originales de los operandos. Calcule el producto de A y B utilizando una tabla similar a la de la figura 3.7 y el hardware descrito en la figura 3.4. Muestre el contenido de los registros en cada paso e incluya el paso necesario para obtener correctamente el signo del resultado. Suponga que A y B están en formato signo-magnitud de 6 bits con signo. 3.4.5 [30]<3.3> Al desplazar el contenido de un registro 1 bit a la derecha, hay varias formas para decidir cuál es el valor del bit que debe introducirse en el registro: siempre 0, siempre 1, o el bit que ha sido eliminado del registro por la derecha (convirtiendo el desplazamiento en una rotación), o el valor del bits más a la izquierda se mantiene (esto sería un desplazamiento aritmético a la derecha, porque se man-

3.11

287

Contribución de Matthew Farrens, UC Davis

tiene el signo del número que se está desplazando). Calcule el producto de A y B, números de 6 bits en complemento a 2, utilizando una tabla similar a la de la figura 3.7 y el hardware descrito en la figura 3.6. El desplazamiento a la derecha se realiza como un desplazamiento aritmético a la derecha. Observe que se tendrá que modificar ligeramente el algoritmo, en concreto, si el multiplicador es negativo. Se pueden encontrar más detalles buscando en la web. Muestre el contenido de los registros en cada paso. 3.4.6 [60]<3.3> Escriba un programa en lenguaje ensamblador de MIPS para calcular el producto de dos números enteros con signo A y B. Indique si se están utilizando las alternativas planteadas en los ejercicios 3.4.4 o 3.4.5.

Ejercicio 3.5 Por muchas y diferentes razones, querríamos diseñar un multiplicador más rápido. Se han propuesto muchas alternativas para conseguirlo. En la siguiente tabla, A representa el número de bits de un entero y B representa las unidades de tiempo (ut) necesarias para llevar a cabo un paso de la operación.

(nº de bits)

(unidades de tiempo)

a.

4

3 ut

b.

32

7 ut

3.5.1 [10]<3.3> Calcule el tiempo necesario para realizar una multiplicación usando las alternativas mostradas en las figuras 3.4 y 3.5 si un entero tiene A bits y cada paso de la operación requiere B unidades de tiempo. Suponga que en el paso 1a siempre se realiza una suma, tanto si se suma el multiplicando o si se suma un 0. Suponga también que los registros ya tienen un valor inicial (se necesita contar cuánto tardará el lazo de la multiplicación). En caso de implementarse en hardware, considere que los desplazamientos del multiplicando y el multiplicador se hacen simultáneamente. En caso de implementarse en software, tendrán que hacerse uno detrás del otro. Obtenga el tiempo de ambas implementaciones. 3.5.2 [10]<3.3> Calcule el tiempo necesario para realizar una multiplicación usando la alternativa descrita en el texto (31 sumadores apilados verticalmente) si un entero tiene A bits y cada sumador requiere B unidades de tiempo. 3.5.3 [20]<3.3> Calcule el tiempo necesario para realizar una multiplicación usando la alternativa descrita en la figura 3.8, si un entero tiene A bits y cada sumador requiere B unidades de tiempo.

288

Capítulo 3

Aritmética para computadores

Ejercicio 3.6 En este ejercicio se abordan otras formas de mejorar las prestaciones de la multiplicación, basadas en hacer más desplazamientos y menos operaciones aritméticas. La siguiente tabla muestra parejas de números hexadecimales. A

B

a.

24

c9

b.

41

18

3.6.1 [20]<3.3> Tal como se ha discutido en este capítulo, puede obtenerse una mejora de las prestaciones haciendo desplazamientos y sumas en lugar de una multiplicación real. Por ejemplo, 9 × 6 puede reescribirse como (2 × 2 × 2 + 1) × 6, y se puede calcular el producto desplazando 6 tres veces y sumando 6. Muestre la mejor forma de calcular A × B con desplazamientos y sumas/restas, suponiendo que A y B son números enteros de 8 bit sin signo. 3.6.2 [20]<3.3> Muestre la mejor forma de calcular A × B con desplazamientos y sumas, suponiendo que A y B son números enteros de 8 bit con signo en formato signo-magnitud. 3.6.3 [60]<3.3> Escriba un programa en lenguaje ensamblador de MIPS para calcular el producto de dos números enteros con signo, utilizando desplazamientos y sumas, tal como se describe en 3.6.1.

La siguiente tabla muestra otras parejas de números hexadecimales. A

B

a.

42

36

b.

9F

8E

3.6.4 [30]<3.3> Otra alternativa para reducir el número de operaciones aritméticas de la multiplicación es el algoritmo de Booth. Este algoritmo se ha utilizado durante años y los detalles sobre su funcionamiento están disponibles en la web. Básicamente, supone que un desplazamiento es más rápido que una suma o una resta y, basándose en esta suposición, reduce el número de operaciones aritméticas necesarias para realizar una multiplicación. Identifique secuencias de unos y ceros y los sustituye por desplazamientos. Encuentre una descripción del algoritmo y explique su funcionamiento. 3.6.5 [30]<3.3> Muestre el cálculo paso a paso de la multiplicación de dos números A y B hexadecimales enteros de 8 bits en complemento a 2. 3.6.6 [60]<3.3> Escriba un programa en lenguaje ensamblador de MIPS para calcular el producto de A y B con el algoritmo de Booth.

3.11

289

Contribución de Matthew Farrens, UC Davis

Ejercicio 3.7 En este ejercicio se profundiza en la división. Utilice los número octales de la siguiente tabla. A

B

a.

50

23

b.

25

44

3.7.1 [20]<3.4> Usando una tabla similar a la mostrada en la figura 3.11, calcule A/B usando el hardware de la figura 3.9. Muestre los contenidos de los registros en cada paso. Suponga que A y B son enteros sin signo de 6 bits. 3.7.2 [30]<3.4> Usando una tabla similar a la mostrada en la figura 3.1, calcule A/B usando el hardware de la figura 3.12. Muestre los contenidos de los registros en cada paso. Suponga que A y B son enteros sin signo de 6 bits. Este algoritmo es ligeramente diferente del mostrado en la figura 3.10. Se tendrá que pensar sobre esto, hacer uno o dos ejemplos o incluso consultar en la web para averiguar como conseguir que la operación se haga correctamente. (Pista: Una posible solución se puede obtener teniendo en cuenta que el registro del resto de la figura 3.12 puede desplazarse en ambas direcciones.) 3.7.3 [60]<3.4> Escriba un programa en lenguaje ensamblador de MIPS para calcular A/B con el algoritmo de la figura 3.9 y suponiendo que A y B son enteros sin signo de 6 bits.

La siguiente tabla muestra otras parejas de número octales. A

B

a.

55

24

b.

36

51

3.7.4 [30]<3.4> Usando una tabla similar a la mostrada en la figura 3.11, calcule A/B usando el hardware de la figura 3.9. Muestre los contenidos de los registros en cada paso. Suponga que A y B son enteros con signo de 6 bits en formato signomagnitud. Se debe incluir como se calculan los signos del cociente y el resto. 3.7.5 [30]<3.4> Usando una tabla similar a la mostrada en la figura 3.11, calcule A/B usando el hardware de la figura 3.12. Muestre los contenidos de los registros en cada paso. Suponga que A y B son enteros con signo de 6 bits en formato signo-magnitud. Se debe incluir como se calculan los signos del cociente y el resto.

290

Capítulo 3

Aritmética para computadores

3.7.6 [60]<3.4> Escriba un programa en lenguaje ensamblador de MIPS para calcular A/B con el algoritmo de la figura 3.12 y suponiendo que A y B son enteros con signo.

Ejercicio 3.8 El algoritmo de división con restauración se describe en la figura 3.10, llamado así porque si el resultado de restar el resto menos el divisor es negativo, el divisor se vuelve a sumar al resto, restaurando su valor. Sin embargo, se han desarrollado otros algoritmos que eliminan esta suma extra. Estos algoritmos pueden consultarse en la web. Se analizarán estos algoritmos utilizando las parejas de números octales de la tabla. A

B

a.

75

12

b.

52

37

3.8.1 [30]<3.4> Usando una tabla similar a la mostrada en la figura 3.11, calcule A/B con un algoritmo de división sin restauración. Muestre los contenidos de los registros en cada paso. Suponga que A y B son enteros sin signo de 6 bits. 3.8.2 [60]<3.4> Escriba un programa en lenguaje ensamblador de MIPS para calcular A/B con un algoritmo de división sin restauración y suponiendo que A y B son enteros de 6 bits con signo en formato de complemento a 2. 3.8.3 [60]<3.4> Compare las prestaciones de la división con y sin restauración; muestre el número de pasos necesarios para calcular A/B con cada método. Suponga que A y B son enteros con signo de 6 bits en formato signo-magnitud. Es suficiente con escribir un programa para división con restauración y sin restauración.

La siguiente tabla muestra otras parejas de número octales. A

B

a.

17

14

b.

70

23

3.8.4 [30]<3.4> Usando una tabla similar a la mostrada en la figura 3.11, calcule A/B usando división sin representación. Muestre los contenidos de los registros en cada paso. Suponga que A y B son enteros sin signo de 6 bits.

3.11

Contribución de Matthew Farrens, UC Davis

3.8.5 [60]<3.4> Escriba un programa en lenguaje ensamblador de MIPS para calcular A/B con división sin representación y suponiendo que A y B son enteros de 6 bits con signo en formato de complemento a 2. 3.8.6 [60]<3.4> Compare las prestaciones de la división sin restauración y sin representación; muestre el número de pasos necesarios para calcular A/B con cada método. Suponga que A y B son enteros con signo de 6 bits en formato signo-magnitud. Es suficiente con escribir un programa para división con restauración y sin restauración.

Ejercicio 3.9 La división es una operación tan lenta y compleja que el manual Fortran Optimizations de CRAY T3E dice: “La mejor estrategia para la división es evitarla cuando sea posible”. En este ejercicio se verán diferentes estrategias para realizar la división. a.

División con restauración

b.

División SRT

3.9.1 [30]<3.4> Describa el algoritmo de la tabla en detalle. 3.9.2 [60]<3.4> Explique cómo funciona el algoritmo de la tabla mediante un diagrama de flujo (o un fragmento de código de alto nivel). 3.9.3 [60]<3.4> Escriba un programa en lenguaje ensamblador de MIPS para calcular A/B con el algoritmo de la tabla.

Ejercicio 3.10 En una arquitectura Von Neumann, los grupos de bits no tienen ningún significado en sí mismos; lo que representa un grupo de bits depende completamente de como se usen. La siguiente tabla muestra grupos de bits en notación hexadecimal. a.

0x24A60004

b.

0xAFBF0000

3.10.1 [5]<3.5> ¿Qué número decimal representa si es un entero en complemento a 2? ¿Y un entero sin signo? 3.10.2 [10]<3.5> Si este grupo de bits estuviese en el registro de instrucciones ¿qué instrucción MIPS se ejecutaría?

291

292

Capítulo 3

Aritmética para computadores

3.10.3 [10]<3.5> ¿Qué número decimal representa si es un número en punto flotante? Utilice el estándar IEEE 754.

La siguiente tabla muestra números decimales. a.

-1609.5

b.

-938.8125

3.10.4 [10]<3.5> Escriba la representación binaria del número de la tabla en formato IEEE 754 de precisión simple. 3.10.5 [10]<3.5> Escriba la representación binaria del número de la tabla en formato IEEE 754 de precisión doble. 3.10.6 [10]<3.5> Escriba la representación binaria del número de la tabla en formato IBM (base 16, en vez de base 2, con 7 bits para el exponente).

Ejercicio 3.11 En el estándar de punto flotante IEEE 754 el exponente se almacena con un sesgo (también llamado “Exceso-N”). El motivo de esta desviación es que se quiere que una secuencia con todos los bits igual a cero esté lo más próxima a cero que sea posible. Dado que estamos usando un bit oculto (hidden bit) igual a 1, si el exponente se representase en complemento a 2 la secuencia con todos los bits iguales a cero ¡estaría representando el número 1! (Cualquier número elevado a 0 es 1, entonces 1.00 = 1.) Hay otros muchos aspectos del estándar IEEE 754 que han sido establecidos para conseguir que el hardware de las unidades de punto flotante sean más rápidas. Sin embargo, en muchos computadores antiguos los cálculos en punto flotante se realizan por software y con otros formatos. La siguiente tabla muestra número decimales. a.

5.00736125 x 105

b.

2.691650390625 x 102

3.11.1 [20]<3.5> Escriba la representación binaria del número de la tabla en el formato empleado en el DEC PDP-8 (los 12 bits más a la izquierda representan el exponente en complemento a 2 y los 24 bits más a la derecha representan la mantisa en complemento a 2). No se usa el bit oculto. Compare el rango y la exactitud de esta representación de 36 bits con los estándares IEEE 754 de precisión simple y doble. 3.11.2 [20]<3.5> NVIDIA utiliza un formato “mitad”, similar al IEEE 754 pero con solo 16 bits. El bit de la izquierda es el signo, se reservan 5 bits para el exponente en exceso 16 y 10 bits para la mantisa. Se asume un bit oculto igual a 1.

3.11

293

Contribución de Matthew Farrens, UC Davis

Represente el número de la tabla en este formato. Compare el rango y la exactitud de esta representación de 16 bits con el estándar IEEE 754 de precisión simple. 3.11.3 [20]<3.5> Los Hewlett-Packard 2114, 2115 y 2116 usaban un formato con la mantisa almacenada en los 16 bits más a la izquierda en representación de complemento a 2, seguido de otro campo de 16 bits que reservaba los 8 de la izquierda para una extensión de la mantisa (haciendo que la mantisa tuviese 24 bits) y los otros 8 bits representaban el exponente. El exponente se almacenaba en formato de signomagnitud ¡con el bit de signo a la derecha! No se utilizaba bit oculto igual a 1. Represente el número de la tabla en este formato. Compare el rango y la exactitud de esta representación de 32 bits con el estándar IEEE 754 de precisión simple.

La siguiente tabla muestra números decimales. A

B

a.

1278 x 10

3.90625 x 101

b.

2.3109375 x 101

6.391601562 x 101

3

3.11.4 [20]<3.5> Calcule la suma de A y B suponiendo que A y B están representados en el formato de 16 bits de NVIDIA descrito en el ejercicio 3.11.2. Redondee al más próximo par, para lo que necesitará un bit de guarda, un bit de redondeo y un bit de sticky. Muestre todos los pasos. 3.11.5 [60]<3.5> Escriba un programa en lenguaje ensamblador de MIPS para calcular A + B suponiendo que A y B están representados en el formato de 16 bits de NVIDIA descrito en el ejercicio 3.11.2. Redondee al más próximo par, para lo que necesitará un bit de guarda, un bit de redondeo y un bit de sticky. 3.11.6 [60]<3.5> Escriba un programa en lenguaje ensamblador de MIPS para calcular A + B suponiendo que A y B están representados en el formato descrito en el ejercicio 3.11.1.. Modifique el programa para calcular la suma de dos números en el formato del ejercicio 3.11.3. ¿Qué formato es más sencillo para programar? Comparar con el formato IEEE 754 (en este ejercicio no hay que preocuparse del bit sticky).

Ejercicio 3.12 La multiplicación en punto flotante es más complicada y exigente que la suma, y ambas son insignificantes en comparación con la división punto flotante. A

B

a.

5.66015625 x 100

8.59375 x 100

b.

6.18 x 102

5.796875 x 101

294

Capítulo 3

Aritmética para computadores

3.12.1 [30]<3.5> Calcule el producto de A y B a mano, suponiendo que A y B están representados en el formato de 16 bits de NVIDIA descrito en el ejercicio 3.11.2. Redondee al más próximo par, para lo que necesitará un bit de guarda, un bit de redondeo y un bit de sticky. Muestre todos los pasos; sin embargo, tal como se ha hecho en el ejemplo del capítulo, se puede hacer la multiplicación en un formato legible para los humanos, en lugar de con las técnicas de los ejercicios 3.4 a 3.6. Indique si se produce desbordamiento o desbordamiento a cero. Muestre el resultado en el formato de NVIDIA y en decimal. ¿Cuál es la exactitud del resultado? Compare con el resultado que se obtiene al hacer la multiplicación en una calculadora. 3.12.2 [60]<3.5> Escriba un programa en lenguaje ensamblador de MIPS para calcular el producto de A y B con A y B representados en el formato IEEE 754. Indicar si se produce desbordamiento o desbordamiento a cero. Recuérdese que IEEE 754 asume un bit de guarda, un bit de redondeo, un bit de sticky y redondeo al más próximo par. 3.12.3 [60]<3.5> Escriba un programa en lenguaje ensamblador de MIPS para calcular el producto de A y B con A y B representados en el formato del ejercicio 3.11.1. Modifique el programa para calcular la suma de dos números en el formato del ejercicio 3.11.3. ¿Qué formato es más sencillo para programar? Compare con el formato IEEE 754 (en este ejercicio no hay que preocuparse del bit sticky).

La siguiente tabla muestra otros números decimales. A

B 3

a.

3.264 x 10

6.52 x 102

b.

2.27734375 x 100

1.154375 x 102

3.12.4 [30]<3.5> Calcule A/B, mostrando todos los pasos necesarios para obtener el resultado. Suponga un bit de guarda, un bit de redondeo y un bit de sticky, y utilícelos si es necesario. Escriba el resultado en formato punto flotante de 16 y en decimal y compare con el resultado que se obtiene al hacer la operación en una calculadora.

Los Livermore Loops son un conjunto de núcleos de cálculo intensivo en punto flotante extraídos de programas científicos del Laboratorio Lawrence Livermore. La siguiente tabla identifica algunos núcleos del conjunto. Se pueden obtener en http://www.netlib.org/benchmark/livermore. a.

Livermore Loop 1

b.

Livermore Loop 2

3.12.5 [60]<3.5> Escriba el núcleo en lenguaje ensamblador del MIPS.

3.11

295

Contribución de Matthew Farrens, UC Davis

3.12.6 [60]<3.5> Describa con detalle una técnica para la realización de la división en un computador. Incluya referencias de las fuentes que haya consultado.

Ejercicio 3.13 Las operaciones con enteros en punto fijo se comportan como se espera: se cumplen las propiedades asociativa, conmutativa y distributiva. Esto no siempre es cierto para operaciones con número punto flotante. Nos centramos la propiedad asociativa. La siguiente tabla muestra un conjunto de números decimales. A

B

C

a.

1.6360 x 104

1.6360 x 104

1.0 x 100

b.

2.865625 x 101

4.140625 x 101

1.2140625 x 101

3.13.1 [20]<3.2, 3.5, 3.6> Calcule (A + B) + C a mano, suponiendo que A, B y C están representados en el formato de 16 bits de NVIDIA descrito en el ejercicio 3.11.2. Realice redondeo al más próximo par, para lo que se necesitará un bit de guarda, un bit de redondeo y un bit de sticky. Muestre todos los pasos y escriba el resultado en el formato punto flotante de 16 bits y en decimal. 3.13.2 [20]<3.2, 3.5, 3.6> Calcule A + (B + C) a mano, suponiendo que A, B y C están representados en el formato de 16 bits de NVIDIA descrito en el ejercicio 3.11.2. Redondee al más próximo par, para lo que necesitará un bit de guarda, un bit de redondeo y un bit de sticky. Muestre todos los pasos y escriba el resultado en el formato punto flotante de 16 bits y en decimal. 3.13.3 [10]<3.2, 3.5, 3.6> Basándose en las respuestas de los ejercicios 3.13.1 y 3.13.2, ¿(A + B) + C = A + (B + C)?

La siguiente tabla muestra otro conjunto de números decimales. A

B

C

a.

4.8828125 x 104

1.768 x 103

2.50125 x 102

b.

4.721875 x 101

2.809375 x 101

3.575 x 101

3.13.4 [30]<3.2, 3.5, 3.6> Calcule (A × B) × C a mano, suponiendo que A, B y C están representados en el formato de 16 bits de NVIDIA descrito en el ejercicio 3.11.2. Redondee al más próximo par, para lo que necesitará un bit de guarda, un bit de redondeo y un bit de sticky. Muestre todos los pasos y escriba el resultado en el formato punto flotante de 16 bits y en decimal.

296

Capítulo 3

Aritmética para computadores

3.13.5 [30]<3.2, 3.5, 3.6> Calcule A × (B × C) a mano, suponiendo que A, B y C

están representados en el formato de 16 bits de NVIDIA descrito en el ejercicio 3.11.2. Redondee al más próximo par, para lo que necesitará un bit de guarda, un bit de redondeo y un bit de sticky. Muestre todos los pasos y escriba el resultado en el formato punto flotante de 16 bits y en decimal. 3.13.6 [10]<3.2, 3.5, 3.6> Basándose en las respuestas de los ejercicios 3.13.5 y 3.13.4, ¿(A × B) × C = A × (B × C)?

Ejercicio 3.14 La propiedad asociativa no es la única que no siempre se mantiene cuando se trabaja con números en punto flotante. Hay otras singularidades que también ocurren. La siguiente tabla muestra un conjunto de números decimales. A

B

C

a.

1.5234375 x 101

2.0703125 x 101

9.96875 x 101

b.

2.7890625 x 101

8.088 x 103

1.0216 x 104

3.14.1 [30]<3.2, 3.3, 3.5, 3.6> Calcule A × (B + C) a mano, suponiendo que A, B y C están representados en el formato de 16 bits de NVIDIA descrito en el ejercicio 3.11.2. Redondee al más próximo par, para lo que necesitará un bit de guarda, un bit de redondeo y un bit de sticky. Muestre todos los pasos y escriba el resultado en el formato punto flotante de 16 bits y en decimal. 3.14.2 [30]<3.2, 3.3, 3.5, 3.6> Calcule (A × B) + (A × C) a mano, suponiendo que A, B y C están representados en el formato de 16 bits de NVIDIA descrito en el ejercicio 3.11.2. Redondee al más próximo par, para lo que necesitará un bit de guarda, un bit de redondeo y un bit de sticky. Muestre todos los pasos y escriba el resultado en el formato punto flotante de 16 bits y en decimal. 3.14.3 [10]<3.2, 3.5, 3.6> Basándose en las respuestas de los ejercicios 3.14.1 y 3.14.2, ¿A × (B + C)=(A × B) + (A × C)?

La siguiente tabla muestra parejas de operandos, formadas por una fracción y un entero. A

B

a.

1/3

3

b.

-1/7

7

3.14.4 [10]<3.5> Escriba la representación binaria de A en formato punto flotante IEE 754. ¿Se puede representar A de forma exacta?

3.11

Contribución de Matthew Farrens, UC Davis

297

3.14.5 [10]<3.2, 3.3, 3.5, 3.6> ¿Qué se obtiene si se suma B veces el operando A? ¿Cuál es el resultado de A × B? ¿Son iguales? ¿Cuáles deberían haber sido estos resultados? 3.14.6 [60]<3.2, 3.3, 3.4, 3.5, 3.6> ¿Qué se obtiene si se calcula la raíz cuadrada de B y este valor se multiplica por sí mismo? ¿Qué se debería haber obtenido? Haga este ejercicio para número punto flotante con precisión simple y doble. (Escriba un programa para hacer estas operaciones.)

Ejercicio 3.15 En la mantisa se usa una representación binaria, pero podría usarse otra representación. Por ejemplo, IBM utiliza números en base 16 en algunos de sus formatos de punto flotante. Hay otras alternativas que también son posibles, cada una con sus ventajas e inconvenientes. En la tabla siguiente se muestran fracciones que serán representadas en varios formato de punto flotante. a.

1/2

b.

1/9

3.15.1 [10]<3.5, 3.6> Escriba la representación binaria de la mantisa para una representación punto flotante que utiliza números binarios para la mantisa (esencialmente, esto es lo que se ha estado haciendo en este capítulo). Suponga que la mantisa es de 24 bits y no se necesita normalizar. ¿Esta representación es exacta? 3.15.2 [10]<3.5, 3.6> Escriba la representación binaria de la mantisa para una representación punto flotante que utiliza números BCD (Binary Coded Decimal, base 10) para la mantisa en lugar de base 2. Suponga que la mantisa es de 24 bits y no se necesita normalizar. ¿Esta representación es exacta? 3.15.3 [10]<3.5, 3.6> Escriba la representación binaria de la mantisa para una representación punto flotante que utiliza números en base 15 para la mantisa en lugar de base 2. (Los números en base 16 utilizan los símbolos 0-9 y A-F; los números en base 15 usarían 0-9, A-E.) Suponga que la mantisa es de 24 bits y no se necesita normalizar. ¿Esta representación es exacta? 3.15.4 [10]<3.5, 3.6> Escriba la representación binaria de la mantisa para una representación punto flotante que utiliza números en base 30 para la mantisa en lugar de base 2. (Los números en base 16 utilizan los símbolos 0-9 y A-F; los números en base 15 usarían 0-9, A-T). Supóngase que la mantisa es de 24 bits y no se necesita normalizar. ¿Esta representación es exacta? ¿Esta representación tiene alguna ventaja?

$3.2, página 229: 3. $3.4, página 269: 3.

Respuestas a las autoevaluaciones

4 El procesador 4.1

Introducción 300

4.2

Convenios de diseño lógico 303

4.3

Construcción de un camino de datos 307

4.4

Esquema de una implementación simple 316

4.5

Descripción general de la segmentación 330

4.6

Camino de datos segmentados y control de la segmentación 344

En una materia fundamental, ningún detalle es insignificante.

4.7

Riesgos de datos: anticipación frente a bloqueos 363

4.8

Riesgos de control 375

Proverbio francés

4.9

Excepciones 384

4.10

Paralelismo y paralelismo a nivel de instrucciones avanzado 391

4.11

Casos reales: El pipeline del AMD Opteron X4 (Barcelona) 404

4.12

Tema avanzado: una introducción al diseño digital utilizando un lenguaje de descripción hardware para describir y modelar un pipeline y más figuras sobre segmentación 406

4.13

Falacias y errores habituales 407

4.14

Conclusiones finales 408

4.15

Perspectiva histórica y lecturas recomendadas 409

4.16

Ejercicios 409

Nota importante: En la presente edición en castellano, los contenidos del CD incluido en la edición original (en inglés) son accesibles a través de la página web www.reverte.com/microsites/pattersonhennessy. Aunque en la presente edición no se proporciona un CD-ROM físico, a lo largo de todo el texto se menciona el CD y se utiliza el icono que lo representa para hacer referencia a su contenido.

Los cinco componentes clásicos del computador

300

Capítulo 4

4.1

El procesador

Introducción

4.1

En el capítulo 1 vimos que las prestaciones de una máquina están determinadas por tres factores clave: el número de instrucciones, el tiempo del ciclo de reloj y los ciclos de reloj por instrucción (cycles per instruction, CPI). El compilador y la arquitectura del repertorio de instrucciones, que hemos examinado en el capítulo 2, determinan el número de instrucciones requerido por un cierto programa. Sin embargo, tanto el tiempo de ciclo del reloj como el número de ciclos por instrucción vienen dados por la implementación del procesador. En este capítulo se construye el camino de datos y la unidad de control para dos realizaciones diferentes del repertorio de instrucciones MIPS. Este capítulo incluye una explicación de los principios y técnicas utilizadas en la implementación de un procesador, comenzando con un resumen altamente abstracto y simplificado en esta sección, seguido por una sección que construye un camino de datos y una versión simple de un procesador suficiente para realizar repertorios de instrucciones como MIPS. El núcleo del capítulo describe una implementación segmentada más realista del MIPS, seguido de una sección que desarrolla los conceptos necesarios para la implementación de un repertorio de instrucciones más complejo, como el x86. Para el lector interesado en comprender la interpretación a alto nivel de las instrucciones y su impacto en las prestaciones del programa, esta sección inicial y la sección 4.5 proporcionan los conceptos básicos de la segmentación. Las tendencias actuales se indican en la sección 4.10, y la sección 4.11 describe el microprocesador AMD Opteron X4 (Barcelona). Estas secciones proporcionan suficiente bagaje para comprender los conceptos de segmentación a alto nivel. Las secciones 4.3, 4.4 y 4.6 son útiles para los lectores interesados en entender el procesador y sus prestaciones con mayor profundidad y las secciones 4.2, 4.7, 4.8 y 4.9 para los interesados en como construir un procesador. Para los lectores interesados en diseño hardware moderno, la sección 4.12 en el CD describe como se utilizan los lenguajes de descripción hardware y las herramientas de CAD para la implementación de hardware y como utilizar un lenguaje de descripción hardware para describir una implementación segmentada. También se incluyen más figuras sobre la ejecución en un hardware segmentado.

Una implementación básica MIPS Examinaremos una implementación que incluirá un subconjunto básico del repertorio de instrucciones del MIPS formado por: ■ Las instrucciones de referencia a memoria cargar palabra (load word lw) y almacenar palabra (store word sw). ■ Las instrucciones aritmético-lógicas add, sub, and, or y slt. ■ Las instrucciones de saltar si igual (branch on equal beq) y salto incondicional (jump j), que se añadirán en último lugar.

4.1

Introducción

Este subconjunto no incluye todas las instrucciones con enteros (por ejemplo, se han omitido las de desplazamiento, multiplicación y división), ni ninguna de las instrucciones de punto flotante. Sin embargo, servirá para ilustrar los principios básicos que se utilizan en la construcción del camino de datos y el diseño de la unidad de control. La implementación de las instrucciones restantes es similar. Al examinar la implementación, tendremos la oportunidad de ver cómo la arquitectura del repertorio de instrucciones determina muchos aspectos de la implementación y cómo la elección de varias estrategias de implementación afecta a la frecuencia del reloj y al CPI de la máquina. Muchos de los principios básicos de diseño introducidos en el capítulo 1, como las directrices «hacer rápido el caso común» y «la simplicidad favorece la regularidad», pueden observarse en la implementación. Además, la mayoría de los conceptos utilizados para realizar el subconjunto MIPS en este capítulo y en el siguiente son las ideas básicas que se utilizan para construir un amplio espectro de computadores, desde servidores de altas prestaciones hasta microprocesadores de propósito general o procesadores empotrados.

Una visión general de la implementación En el capítulo 2 analizamos el núcleo básico de instrucciones MIPS, incluidas las instrucciones aritmético-lógicas sobre enteros, las instrucciones de referencia a memoria y las instrucciones de salto. La mayor parte de lo necesario para implementar estas instrucciones es común para todas ellas, independientemente de su tipo concreto. Para cada instrucción, los dos primeros pasos son idénticos: 1. Enviar el contador de programa (PC) a la memoria que contiene el código y cargar la instrucción desde esa memoria. 2. Leer uno o dos registros, utilizando para ello los campos específicos de la instrucción para seleccionar los registros a leer. Para la instrucción de cargar palabra es necesario un solo registro, pero la mayoría del resto de las instrucciones requiere la lectura de dos registros. Después de estos dos pasos, las acciones necesarias para completar la instrucción dependen del tipo de la misma. Afortunadamente, para los tres tipos de instrucciones (referencia a memoria, aritmético-lógicas y saltos) las acciones son generalmente las mismas, independientemente del código de operación exacto. La sencillez y regularidad del repertorio de instrucciones MIPS simplifica la implementación porque la ejecución de muchas clases de instrucciones es similar. Por ejemplo, todos los tipos de instrucciones, excepto las de salto incondicional, utilizan la unidad aritmético-lógica (ALU) después de la lectura de los registros. Las instrucciones de referencia a memoria utilizan la ALU para el cálculo de la dirección, las instrucciones aritmético-lógicas para ejecutar la operación, y las de salto condicional para comparar. Después de utilizar la ALU, las acciones requeridas para completar la ejecución de los diferentes tipos de instrucciones son distintas. Una instrucción de referencia a memoria necesitará acceder a memoria, ya sea para escribir el dato en una operación de almacenamiento o para leer, en caso de una carga. Una instrucción aritmético-lógica o una de carga debe escribir el resultado calculado por la ALU o el leido de la memoria en un registro. Finalmente, en una instrucción de salto condicional es necesario modificar la dirección de la siguiente

301

302

Capítulo 4

El procesador

instrucción según el resultado de la comparación; en caso contrario el PC debe incrementarse en 4 para obtener la dirección de la siguiente instrucción. La figura 4.1 muestra un esquema de alto nivel de una implementación MIPS, en el que se muestran las diferentes unidades funcionales y su interconexión. A pesar de que esta figura muestra la mayor parte del flujo de datos en el procesador, omite dos aspectos importantes de la ejecución de las instrucciones.

4

Sumador

Sumador

Datos

PC

Dirección Instrucción Memoria de instrucciones

nº de registro Registros nº de registro

ALU

Dirección Memoria de datos

nº de registro Datos

FIGURA 4.1 Una visión abstracta de la implementación del subconjunto MIPS en la que se muestra la mayor parte de las unidades funcionales y las conexiones entre ellas. Todas las instrucciones comienzan utilizando el contador de programa (PC) para proporcionar la dirección de la instrucción en la memoria de instrucciones. Tras la captación de la instrucción, ciertos campos de ésta especifican los registros que se utilizan como operandos fuente. Una vez que éstos han sido leídos, puede operarse con ellos, ya sea para calcular una dirección de memoria (en una carga o un almacenamiento), para calcular un resultado aritmético (para una instrucción aritmético-lógica entera), o bien para realizar una comparación (en un salto condicional). Si la instrucción es una instrucción aritmético-lógica, el resultado de la ALU debe almacenarse en un registro. Si la operación es una carga o un almacenamiento, el resultado de la ALU se utiliza como la dirección donde almacenar un valor en memoria o cargar un valor en los registros. El resultado de la ALU o memoria se escribe en el banco de registros. Los saltos condicionales requieren el uso de la salida de la ALU para determinar la dirección de la siguiente instrucción, que proviene de la ALU (donde se suma el PC y el desplazamiento) o desde un sumador que incrementa el valor actual del PC en 4. Las líneas gruesas que interconectan las unidades funcionales representan los buses, que consisten en múltiples señales. Las flechas se utilizan para indicar al lector cómo fluye la información. Ya que las líneas de señal pueden cruzarse, se muestra explícitamente la conexión con la presencia de un punto donde se cruzan las líneas.

En primer lugar, en varios puntos, la figura muestra datos dirigidos a una unidad particular provenientes de dos orígenes diferentes. Por ejemplo, el valor escrito en el PC puede provenir de cualquiera de los dos sumadores, el dato escrito en el banco de registros puede provenir de la ALU o de la memoria de datos, y la segunda entrada de la ALU proviene de un registro o del campo inmediato de la instrucción. En la práctica, estas líneas de datos no pueden conectarse directamente; debe añadirse un elemento que seleccione entre los múltiples orígenes y dirija una de estas fuentes al destino. Esta selección se realiza comúnmente mediante un dispositivo denominado multiplexor, aunque sería más adecuado

4.2

Convenios de diseño lógico

303

denominarlo selector de datos. El multiplexor, que se describe en detalle en el apéndice C, selecciona una de entre varias entradas según la configuración de sus líneas de control. Las líneas de control se configuran principalmente a partir de información tomada de la instrucción en ejecución. En segundo lugar, varias de las unidades deben controlarse dependiendo del tipo de instrucción. Por ejemplo, la memoria de datos debe leer en una carga y escribir en un almacenamiento. Debe escribirse en el banco de registros en una instrucción de carga y en una instrucción aritmético-lógica. Y, por supuesto, la ALU debe realizar una entre varias operaciones, tal como se mostró en el capítulo 2. (El apéndice C describe el diseño lógico detallado de la ALU.) Al igual que los multiplexores, estas operaciones son dirigidas por las líneas de control que se establecen según los diversos campos de la instrucción. La figura 4.2 muestra el camino de datos de la figura 4.1 con los tres multiplexores necesarios añadidos, así como las líneas de control para las principales unidades funcionales. Para determinar la activación de las líneas de control de las unidades funcionales y de los multiplexores se utiliza una unidad de control que toma la instrucción como entrada. El tercer multiplexor, que determina si PC + 4 o la dirección destino del salto se escribe en el PC, se activa según el valor de la salida Cero de la ALU, que se utiliza para realizar la comparación de la instrucción beq. La regularidad y simplicidad del repertorio de instrucciones MIPS implica que un simple proceso de descodificación puede ser utilizado para determinar cómo activar las líneas de control. En el resto de este capítulo, se refina este esquema para añadir los detalles, lo que va a requerir que se incluyan unidades funcionales adicionales, incrementar el número de conexiones entre las unidades y, por supuesto, añadir una unidad de control que determine las acciones que deben realizarse para cada uno de los distintos tipos de instrucciones. Las secciones 4.3 y 4.4 describen una realización simple que utiliza un único ciclo de reloj largo para cada instrucción y sigue la forma general de las figuras 4.1 y 4.2. En este primer diseño, cada instrucción inicia su ejecución en un flanco de reloj y completa la ejecución en el siguiente flanco de reloj. Aunque es más fácil de comprender, este enfoque no es práctico, puesto que hay que alargar el ciclo de la señal de reloj para permitir la ejecución de la instrucción más lenta. Una vez diseñado el control para este computador sencillo, se analizará la implementación segmentada en toda su complejidad, incluyendo el tratamiento de las excepciones. ¿Cuántos de los cinco componentes clásicos de un computador, mostrados en la página 299, se incluyen en la figuras 4.1 y 4.2?

4.2

Convenios de diseño lógico

4.2

Para tratar el diseño de la máquina, se debe decidir cómo operará su implementación lógica y cómo será su sincronización. Esta sección repasa unas cuantas ideas clave de diseño lógico que se utilizarán ampliamente en este capítulo. Si se tiene

Autoevaluación

304

Capítulo 4

El procesador

Branch M u x 4 Sumador

Sumador

M u x ALU operation

Datos

PC

Dirección Instrucción Memoria de instrucciones

nº de registro Registros nº de registro nº de registro RegWrite

MemWrite Dirección

ALU M u x

Cero

Memoria de datos Datos

MemRead

Control

FIGURA 4.2 Implementación básica del subconjunto del MIPS incluyendo los multiplexores y líneas de control necesarias. El multiplexor de la parte superior controla el valor que se va a cargar en el PC (PC + 4 o la dirección destino del salto condicional); el multiplexor es controlado por una puerta que realiza una función «and» entre la salida Cero de la ALU y una señal de control que indica que la instrucción es de salto condicional. El multiplexor cuya salida está conectada al banco de registros se utiliza para seleccionar la salida de la ALU (en el caso de una instrucción aritmético-lógica) o la salida de la memoria de datos (en el caso de una carga desde memoria) para escribir en el banco de registros. Por último, el multiplexor de la parte inferior determina si la segunda entrada de la ALU procede de los registros (en una instrucción aritmético-lógica no inmediata) o del campo de desplazamiento de la instrucción (en una operación inmediata, una carga o almacenamiento, o un salto condicional). Las líneas de control añadidas son directas y determinan la operación realizada por la ALU, si se debe leer o escribir en la memoria de datos, y si los registros deben realizar una operación de escritura. Las líneas de control se muestran en color para facilitar su identificación.

Elemento combinacional: un elemento operacional tal como una puerta AND o una ALU.

escaso o ningún conocimiento sobre diseño lógico, el apéndice C resultará muy útil antes de abordar esta sección. Las unidades funcionales de la implementación MIPS constan de dos tipos de elementos lógicos diferentes: elementos que operan con datos y elementos que contienen el estado. Los elementos que operan con datos son todos combinacionales, lo que significa que sus salidas dependen únicamente de los valores actuales de las entradas. Para una misma entrada, un elemento combinacional siempre produce la misma salida. La ALU mostrada en la figura 4.1 y analizada en detalle en el apéndice C es un elemento combinacional. Para un conjunto de entradas, siempre produce la misma salida porque no tiene almacenamiento interno.

4.2

Convenios de diseño lógico

Otros elementos en el diseño no son combinacionales, sino que contienen estado. Un elemento contiene estado si tiene almacenamiento interno. Estos elementos se denominan elementos de estado porque, si se apaga la máquina, se puede reiniciar cargando dichos elementos con los valores que contenían antes de apagarla. Además, si se guardan y se restauran, es como si la máquina no se hubiera apagado nunca. Así, los elementos de estado caracterizan completamente la máquina. En la figura 4.1, las memorias de instrucciones y datos, así como los registros, son ejemplos de elementos de estado. Un elemento de estado tiene al menos dos entradas y una salida. Las entradas necesarias son el valor del dato a almacenar en el elemento de estado y el reloj, que determina cuándo se almacena el dato. La salida del elemento de estado proporciona el valor que se almacenó en un ciclo de reloj anterior. Por ejemplo, uno de los elementos de estado más simple es un biestable de tipo D (véase el apéndice C), el cual tiene exactamente estas dos entradas (un dato y un reloj) y una salida. Además de los biestables, la implementación MIPS también utiliza otros dos tipos de elementos de estado: memorias y registros, que aparecen en la figura 4.1. El reloj se utiliza para determinar cuándo debería escribirse el elemento de estado. Un elemento de estado se puede leer en cualquier momento. Los componentes lógicos que contienen estado también se denominan secuenciales porque sus salidas dependen tanto de sus entradas como del contenido de su estado interno. Por ejemplo, la salida de la unidad funcional que representa a los registros depende del identificador del registro y de lo que se haya almacenado en los registros anteriormente. La operación tanto de los elementos combinacionales como secuenciales, así como su construcción, se analiza con más detalle en el apéndice C. Se utiliza la palabra activado para indicar que una señal se encuentra lógicamente a alta y activar para especificar que una señal debe ser puesta a alta, mientras que desactivar o desactivado representan un valor lógico a baja. Metodología de sincronización

Una metodología de sincronización define cuándo pueden leerse y escribirse las diferentes señales. Es importante especificar la temporización de las lecturas y las escrituras porque, si una señal se escribe en el mismo instante en que es leída, el valor leído puede corresponder al valor antiguo, al valor nuevo o ¡incluso a una combinación de ambos! No es necesario decir que los diseños de computadores no pueden tolerar tal imprevisibilidad. La metodología de sincronización se diseña para prevenir esta circunstancia. Por simplicidad, supondremos una metodología de sincronización por flanco. Esta metodología de sincronización implica que cualquier valor almacenado en un elemento lógico secuencial se actualiza únicamente en un flanco de reloj. Debido a que sólo los elementos de estado pueden almacenar datos, las entradas de cualquier lógica combinacional deben proceder de elementos de este tipo y las salidas también deben dirigirse hacia elementos de este tipo. Las entradas serán valores escritos en un ciclo anterior de reloj, mientras que las salidas son valores que pueden utilizarse en el ciclo siguiente.

305

Elemento de estado: elemento de memoria.

Activado: la señal está al nivel lógico alto o verdadero. Desactivado: la señal está al nivel lógico bajo o falso.

Metodología de sincronización: aproximación que determina cuándo los datos son válidos y estables utilizando como referencia el reloj.

Sincronización por flanco: esquema de sincronización en el cual todos los cambios de estado se producen en los flancos de reloj.

306

Capítulo 4

El procesador

La figura 4.3 muestra dos elementos de estado que rodean a un bloque de lógica combinacional, que opera con un único ciclo de reloj. Todas las señales deben propagarse desde el elemento de estado 1, a través de la lógica combinacional hasta el elemento de estado 2 en un único ciclo de reloj. El tiempo necesario para que las señales alcancen el elemento de estado 2 define la duración del ciclo de reloj.

Elemento de estado 1

Lógica combinacional

Elemento de estado 2

Ciclo de reloj

FIGURA 4.3 La lógica combinacional, los elementos de estado y el reloj están estrechamente relacionados. En un sistema digital síncrono, el reloj determina cuándo los elementos con estado van a escribir valores en el almacenamiento interno. Cualquiera de las entradas de un elemento de estado debe alcanzar un valor estable (es decir, alcanzar un valor que no va a cambiar hasta el siguiente flanco del reloj) antes de que el flanco de reloj activo cause la actualización del estado. Se supone que todos estos elementos, incluida la memoria, están sincronizados por flanco.

Señal de control: señal utilizada como selección en un multiplexor o para dirigir la operación de una unidad funcional; contrasta con una señal de datos, que continene información sobre la que opera una unidad funcional.

Por simplicidad, no se muestra una señal de control de escritura cuando se escribe en un elemento de estado en cada flanco activo de reloj. En cambio, si el elemento de estado no se actualiza en cada ciclo de reloj, es necesario incluir una señal de control de escritura (write). La señal de reloj y la señal de control de escritura son entradas, y el elemento de estado se actualiza únicamente cuando la señal de control de escritura se activa y ocurre un flanco de reloj. La metodología por flanco permite leer el contenido de un registro, enviar el valor a través de alguna lógica combinacional, y escribir en ese mismo registro en un mismo ciclo de reloj, tal como se muestra en la figura 4.4. Es indiferente donde se asuma la implementación de las escrituras, ya sea en el flanco ascendente o en el descendente, ya que las entradas del bloque combinacional sólo pueden modifi-

Elemento de estado

Lógica combinacional

FIGURA 4.4 La metodología de sincronización por flanco permite leer y escribir en un elemento de estado en un mismo ciclo de reloj sin crear una condición de carrera que pueda conducir a valores indeterminados de los datos. Por supuesto, el ciclo de reloj debe ser suficientemente largo para que los valores de entrada sean estables cuando ocurra el flanco de reloj activo. El baipás no puede darse dentro de un mismo ciclo debido a la actualización por flanco del elemento de estado. Si fuera posible el baipás, puede que este diseño no funcione correctamente. Los diseños de este capítulo como los del próximo se basan en este tipo de metodología de sincronización y en estructuras como la mostrada en la figura.

4.3

Construcción de un camino de datos

307

carse en el flanco elegido. Con este tipo de metodología de sincronización, no existe baipás dentro de un mismo ciclo, y la lógica de la figura 4.4 funciona correctamente. En el apéndice C se analizan brevemente otras limitaciones de la sincronización —como los tiempos de preestabilización (setup times) y de mantenimiento (hold times)— así como otras metodologías. En la arquitectura MIPS de 32 bits, casi todos estos elementos lógicos y de estado tendrán entradas y salidas de 32 bits, ya que ésta es la anchura de muchos de los datos tratados por el procesador. Se marcará de alguna forma cuando una unidad tenga una entrada o una salida con una anchura diferente. Las figuras mostrarán los buses (señales con anchura superior a un bit) mediante líneas más gruesas. Cuando se quiera combinar varios buses para formar uno de anchura superior, por ejemplo, si se quiere tener un bus de 32 bits combinando dos de 16, las etiquetas de dichos buses especificarán que hay varios buses agrupados. También se añaden como ayuda flechas para destacar la dirección del flujo de datos entre los elementos. Finalmente, el color indicará si se trata de una señal de control o de datos. Esta distinción se especificará mejor más adelante en este capítulo. Verdadero o falso: ya que el banco de registros es leído y escrito en el mismo ciclo de reloj, cualquier camino de datos MIPS que utilice escrituras por flanco debe tener más de una copia del banco de registros.

Autoevaluación

Extensión: Existe una versión de 64 bits de la arquitectura MIPS y, naturalmente, la mayor parte de los componentes tienen un ancho de 64 bits. Por otra parte, se usan los términos activado y desactivado porque a veces el 1 representa el estado lógico alto y otras veces el estado lógico bajo.

4.3

Construcción de un camino de datos

4.3

Una forma razonable de empezar el diseño de un camino de datos es examinar los componentes principales necesarios para ejecutar cada tipo de instrucción del MIPS. Primero se consideran los elementos del camino de datos que necesita cada instrucción. Cuando se muestran los elementos del camino de datos, también se muestran sus señales de control. La figura 4.5a muestra el primer elemento que se necesita: una unidad de memoria donde almacenar y suministrar las instrucciones a partir de una dirección. La figura 4.5b también muestra un registro, denominado contador de programa (PC), que hemos visto en el capítulo 2 y se utiliza para almacenar la dirección de la instrucción actual. Finalmente se necesita un sumador encargado de incrementar el PC para que apunte a la dirección de la siguiente instrucción. Este sumador, que es combinacional, se puede construir a partir de la ALU descrita en detalle en el apéndice C, haciendo simplemente que las líneas de control siempre especifiquen una operación de suma. Este sumador se representa como una ALU con la eti-

Elemento del camino de datos: unidad funcional utilizada para operar o mantener un dato dentro de un procesador. En la implementación MIPS, los elementos del camino de datos incluyen las memorias de instrucciones y datos, el banco de registros, la unidad aritmético-lógica (ALU), y sumadores.

Contador de programa (PC): registro que contiene la dirección de la instrucción del programa que está siendo ejecutada.

308

Capítulo 4

El procesador

Dirección de la instrucción PC Suma Sumador

Instrucción Memoria de instrucciones

a. Memoria de instrucciones

b. Contador de programa

c. Sumador

FIGURA 4.5 Se necesitan dos elementos de estado para almacenar y acceder a las instrucciones, y un sumador para calcular la dirección de la instrucción siguiente. Los elementos de estado son la memoria de instrucciones y el contador de programa. La memoria de instrucciones es solo de lectura, ya que el camino de datos nunca escribe instrucciones, y se trata como un elemento de lógica combinacional. La salida en cualquier instante refleja el contenido de la localización especificada por la dirección de entrada, y no se necesita ninguna señal de control de lectura (sólo se necesitará escribir en la memoria de las instrucciones cuando se cargue el programa; esto no es difícil de añadir, por lo que se ignora para simplificar). El contador de programa es un registro de 32 bits que se modifica al final de cada ciclo de reloj y, de esta manera, no necesita ninguna señal de control de escritura. El sumador es una ALU cableada para que sume siempre dos entradas de 32 bits y dé el resultado en su salida.

Banco de registros: elemento de estado que consiste en un conjunto de registros que pueden ser leídos y escritos proporcionando el identificador del registro al que se desea acceder.

queta sumador como en la figura 4.5 para indicar que es un sumador permanente y que no puede realizar ninguna otra función propia de una ALU. Para ejecutar cualquier instrucción se debe empezar por cargar la instrucción desde memoria. Para poder ejecutar la siguiente instrucción se debe incrementar el contador de programa para que apunte hacia ella 4 bytes más allá. La figura 4.6 muestra cómo se combinan los tres elementos de la figura 4.5 para formar un camino de datos que busca instrucciones e incrementa el PC para obtener la dirección de la siguiente instrucción secuencial. Ahora considérense las instrucciones tipo R (véase la figura 2.20 de la página 136). Todas ellas leen dos registros, operan con la ALU los contenidos de dichos registros como operandos, y escriben el resultado. Estas instrucciones se denominan de tipo R o aritmético-lógicas (ya que realizan operaciones aritméticas o lógicas). En este tipo de instrucciones se incluyen las instrucciones add, sub, and, or y slt, que fueron introducidas en el capítulo 2. Recuérdese también que el ejemplo típico de este tipo de instrucciones es add $t1,$t2,$t3, que lee $t2 y $t3 y escribe en $t1. Los registros de 32 bits del procesador se agrupan en una estructura denominada banco de registros. Un banco de registros es una colección de registros donde cualquier registro puede leerse o escribirse especificando su número. El banco de registros contiene el estado de los registros de la máquina. Además, se necesita una ALU para poder operar con los valores leídos de los registros. Debido a que las instrucciones tipo R tienen tres operandos registro, por cada instrucción se necesita leer dos datos del banco y escribir uno en él. Por cada registro que se lee, se necesita una entrada en el banco donde se especifique el número de registro que se quiere leer, así como una salida del banco donde se

4.3

Construcción de un camino de datos

Sumador 4 PC

Dirección a leer Instrucción Memoria de instrucciones

FIGURA 4.6 Parte del camino de datos utilizado para la búsqueda de las instrucciones y el incremento del contador de programa. La instrucción cargada se utiliza en otras partes del camino de datos.

presentará el valor correspondiente. Para escribir un valor se necesitan dos entradas: una para especificar el número de registro donde escribir y otra para suministrar el dato. El banco de registros siempre devuelve en sus salidas los contenidos de los registros cuyos identificadores están en las entradas de los registros a leer. Las escrituras se controlan mediante una señal de control de escritura, que debe estar activa para que una escritura se lleve a cabo en el flanco de reloj. Así, se necesitan un total de cuatro entradas (tres para los números de los registros y uno para los datos) y dos salidas (ambas de datos), como se puede ver en la figura 4.7a. Las entradas de los identificadores de registro son de cinco bits para especificar uno de los 32 (32 = 25) registros, mientras que los buses de la entrada y las dos salidas de datos son de 32 bits. La figura 4.7b muestra la ALU, que tiene dos entradas de 32 bits y produce un resultado de 32 bits, así como una señal de 1 bit que se activa si el resultado es 0. La señal de control de cuatro bits de la ALU se describe en detalle en el apéndice C; repasaremos el control de la ALU brevemente cuando necesitemos saber cómo establecerlo. Consideremos ahora las instrucciones del MIPS de carga y almacenamiento de palabras, las cuales tienen el formato general lw $t1, despl($t2) o sw $t1, offset ($t2). Estas instrucciones calculan la dirección de memoria añadiendo al registro base ($t2), el campo de desplazamiento con signo de 16 bits contenido en la instrucción. Si la instrucción es un almacenamiento, el valor a almacenar debe leerse del registro especificado por $t1. En el caso de una carga, el valor leído de memoria debe escribirse en el registro especificado por $t1 en el banco de registros. Así, se necesitarán tanto el banco de registros como la ALU que se muestran en la figura 4.7.

309

310

Capítulo 4

El procesador

5

Número de registro

5 5

Dato

Reg. de lectura 1

4

Reg. de lectura 2 Reg. de escritura

ALU operation

Dato leído 1 Datos

Registros

Cero ALU resultado

Dato leído 2

Dato a escribir

RegWrite a. Registros

b. ALU

FIGURA 4.7 El banco de registros y la ALU son los dos elementos necesarios para la implementación de instrucciones de tipo R. El banco de registros contiene todos los registros y tiene dos puertos de lectura y uno de escritura. El diseño de los bancos de registros multipuerto se analiza en la sección C8 del apéndice C. El banco de registros siempre devuelve a la salida el contenido de los registros correspondientes a los identificadores que se encuentran en las entradas de los registros a leer; sin ser necesaria ninguna otra entrada de control. En cambio, la escritura de un registro debe indicarse explícitamente mediante la activación de la señal de control de escritura. Recuerde que las escrituras se sincronizan por flanco, por lo que todas las señales implicadas (el valor a escribir, el número de registro y la señal de control de escritura) deben ser válidas en el flanco de reloj. Por esta razón, este diseño puede leer y escribir sin ningún problema el mismo registro en un mismo ciclo: la lectura obtiene el valor escrito en un ciclo anterior, mientras que el valor que se escribe ahora estará disponible en ciclos siguientes. Las entradas que indican el número de registro al banco son todas de 5 bits, mientras que las líneas de datos son de 32 bits. La operación que realiza la ALU se controla mediante su señal de operación, que es de 4 bits, utilizando la ALU diseñada en el apéndice C. La salida de detección de Cero se utiliza para la implementación de los saltos condicionales. La salida de desbordamiento (overflow) no se necesitará hasta la sección 4.9, cuando se analicen las excepciones; por lo tanto se omitirá hasta entonces.

Extensión de signo: incrementar el tamaño de un dato mediante la replicación del bit de signo del dato original en los bits de orden alto del dato destino más largo.

Dirección destino de salto: dirección especificada en un salto que se convierte en la nueva dirección del contador de programas (PC) si se realiza el salto. En la arquitectura MIPS, el destino del salto viene dado por la suma del campo de desplazamiento de la instrucción y la dirección de la instrucción siguiente al salto.

Además se necesita una unidad para extender el signo del campo de desplazamiento de 16 bits de la instrucción a un valor con signo de 32 bits, y una unidad de memoria de datos para leer y escribir. La memoria de datos se escribe con instrucciones almacenamiento; por lo que tiene señales de control de escritura y de lectura, una entrada de dirección y una entrada de datos a escribir en memoria. La figura 4.8 muestra estos dos elementos. La instrucción beq tiene tres operandos, dos registros que se comparan para comprobar su igualdad y un desplazamiento de 16 bits utilizado para calcular la dirección destino del salto relativa a la dirección de la instrucción. Su formato es beq $t1,$t2, offset. Para realizar esta instrucción se debe calcular la dirección destino del salto sumando el campo de desplazamiento con el signo extendido al PC. Hay dos detalles en la definición de las instrucciones de salto condicional (véase el capítulo 2) a los cuales se debe prestar atención: ■

La arquitectura del repertorio de instrucciones especifica que es la dirección de la siguiente instrucción en orden secuencial la que se utiliza como base para el cálculo de la dirección destino. Puesto que se calcula el PC + 4 (la dirección de la siguiente instrucción) en el camino de datos de la carga de instrucciones, es fácil utilizar este valor como base para el cálculo de la dirección destino del salto.

4.3

Construcción de un camino de datos

311

MemWrite Dirección

Dato leído 16

Dato a escribir

Memoria de datos

Extensión de signo

32

MemRead a. Unidad de memoria de datos

b. Unidad de extensión de signos

FIGURA 4.8 Las dos unidades necesarias para la implementación de cargas y almacenamientos, además del banco de registros y la ALU de la figura 4.7, son la unidad de memoria de datos y la unidad de extensión de signo. La unidad de memoria es un elemento de estado que tiene como entradas la dirección y el dato a escribir, y como única salida el valor leído. Hay controles separados de escritura y de lectura, aunque sólo uno de los dos puede estar activo en un momento determinado. La unidad de memoria requiere también una señal de lectura, ya que a diferencia del caso del banco de registros, la lectura de una dirección no válida puede causar problemas, como se verá en el capítulo 5. La unidad de extensión de signo tiene una entrada de 16 bits que se extenderá a los 32 bits que aparecen en la salida (véase capítulo 2). Se supone que la memoria de datos sólo escribe en el flanco. De hecho, los chips estándar de memoria tienen una señal de habilitación de escritura y, aunque esta señal no es con flanco, este diseño podría fácilmente adaptarse para trabajar con chips reales de memoria. Véase la sección C.8 del apéndice C para analizar más ampliamente cómo trabajan estos chips.



La arquitectura también impone que el campo de desplazamiento se desplace hacia la izquierda 2 bits para que este desplazamiento corresponda a una palabra; de forma que se incrementa el rango efectivo de dicho campo en un factor de 4. Para tratar la última complicación se necesita desplazar dos posiciones el campo de desplazamiento. Además de calcular la dirección destino del salto, también se debe determinar si la siguiente instrucción a ejecutar es la que sigue secuencialmente o la situada en la dirección destino del salto. Cuando la condición se cumple (es decir, los operandos son iguales), la dirección calculada pasa a ser el nuevo PC, y se dice que el salto condicional se ha tomado. Si los operandos son diferentes, el PC incrementado debería reemplazar al PC actual (igual que para cualquier otra instrucción); en este caso se dice que el salto no se ha tomado. Resumiendo, el camino de datos para saltos condicionales debe efectuar dos operaciones: calcular la dirección destino del salto y comparar el contenido de los registros (los saltos condicionales también requieren que se modifique la parte de carga de instrucciones del camino de datos, como se verá más adelante). La figura 4.9 muestra el camino de datos de los saltos condicionales. Para calcular la dirección destino del salto, el camino de datos incluye una unidad de extensión de signo, como en la figura 4.8, y un sumador. Para la comparación se necesita utilizar el banco de registros mostrado en la figura 4.7a a fin de obtener los dos regis-

Salto tomado: salto en el que se cumple la condición de salto y el contador de programa (PC) es cargado con la dirección destino. Todos los saltos incondicionales son saltos tomados.

Salto no tomado: salto donde la condición de salto es falsa y el contador de programa (PC) se carga con la dirección de la instrucción siguiente al salto.

312

Capítulo 4

El procesador

PC + 4 del camino de datos de instrucciones

Desp. 2 bits a la izq. Instrucción

Reg. de lectura 1

Dato leído 1

Reg. de lectura 2 Reg. de escritura

Suma Sumador

4

ALU operation

ALU Cero

Registros

Destino salto

A la lógica de control de salto

Dato leído 2

Dato a escribir RegWrite 16

Extensión del signo

32

FIGURA 4.9 El camino de datos para un salto condicional utiliza la ALU para evaluar la condición de salto y un sumador aparte para calcular la dirección destino del salto como la suma del PC incrementado y los 16 bits de menor peso de la instrucción con el signo extendido (el desplazamiento de salto) y desplazado dos bits a la izquierda . La unidad etiquetada como «desp. 2 bits a la izq.» es simplemente un encaminamiento de las señales entre la entrada y la salida que añade 00dos en la zona baja del campo de desplazamiento. No se necesita circuitería para el desplazamiento, ya que la cantidad a desplazar es constante. Como se sabe que el desplazamiento se extiende desde 16 bits, esta operación sólo desperdicia bits de signo. La lógica de control se utiliza para decidir si el PC incrementado o el destino del salto deberían reemplazar al PC, basándose en la salida Cero de la ALU.

tros de operando (aunque no será necesario escribir en él) y la ALU (diseñada en el apéndice C) para realizar dicha operación. Ya que esta ALU proporciona una señal de salida que indica si el resultado era 0, se le pueden enviar los dos operandos con la señal de control activada de forma que efectúe una resta. Si la señal Cero a la salida de la ALU está activa, entonces los dos valores son iguales. Aunque la salida Cero siempre indica si el resultado es 0, sólo se utiliza para realizar el test de igualdad en saltos condicionales. Más tarde se estudiará de forma más exacta cómo se conectan las señales de control de la ALU para utilizarla en el camino de datos. La instrucción de salto incondicional reemplaza los 28 bits de menor peso del PC con los 26 bits de menor peso de la instrucción desplazados 2 bits hacia la izquierda. Este desplazamiento se realiza simplemente concatenando 00 a estos 26 bits (como se describe en el capítulo 2).

Construcción de un camino de datos

313

Extensión: En el repertorio de instrucciones del MIPS, los saltos condicionales se

Salto retardado: tipo de

retardan, lo cual significa que la siguiente instrucción inmediatamente después del

salto en el que la instrucción immediatamente siguiente al salto se ejecuta siempre, independientemente de si la condición del salto es verdadera o falsa.

4.3

salto se ejecuta siempre, independientemente de si la condición de salto se cumple o no. Cuando la condición es falsa, la ejecución se comporta como un salto normal. Cuando es cierta, un salto retardado primero ejecuta la instrucción inmediatamente posterior al salto y posteriormente salta a la dirección destino. El motivo de los saltos retardados surge por el modo en que les afecta la segmentación (véase sección 4.8). Para simplificar, se ignorarán los saltos retardados en este capítulo y se realizará una instrucción beq no retardada.

Implementación de un camino de datos sencillo Una vez que se han descrito los componentes necesarios para los distintos tipos de instrucciones, éstos pueden combinarse en un camino de datos sencillo y añadir el control para completar la implementación. El más sencillo de los diseños intentará ejecutar todas las instrucciones en un solo ciclo. Esto significa que ningún elemento del camino de datos puede utilizarse más de una vez por instrucción, de forma que cualquier recurso que se necesite más de una vez deberá estar replicado. Por tanto, la memoria de instrucciones ha de estar separada de la memoria de datos. Aunque se necesite duplicar algunas de las unidades funcionales, muchos de estos elementos pueden compartirse en los diferentes flujos de instrucciones. Para compartir un elemento del camino de datos entre dos tipos de instrucciones diferentes, se requiere que dicho elemento disponga de múltiples conexiones a la entrada de un elemento utilizando un multiplexor, así como de una señal de control para seleccionar entre las múltiples entradas.

Construcción de un camino de datos

El camino de datos de las instrucciones aritmético-lógicas (o tipo R), así como el de las instrucciones de referencia a memoria son muy parecidos, siendo las principales diferencias las siguientes: ■

Las instrucciones aritmético-lógicas utilizan como entradas de la ALU los valores procedentes de dos registros. Las instrucciones de referencia a memoria pueden utilizar la ALU para realizar el cálculo de la dirección, aunque la segunda entrada es el campo de desplazamiento de 16 bits procendente de la instrucción con signo extendido.



El valor guardado en el registro destino, o bien proviene de la ALU (para instrucciones tipo R) o de memoria (en caso de una carga).

Determine cómo construir un camino de datos para la parte operacional de las instrucciones de referencia a memoria y aritmético-lógicas que utilice un único banco de registros y una sola ALU para soportar ambos tipos de instrucciones, añadiendo los multiplexores necesarios.

EJEMPLO

314

Capítulo 4

RESPUESTA

Para combinar ambos caminos de datos y usar un único banco de registros y una sola ALU, la segunda entrada de ésta ha de soportar dos tipos de datos diferentes, además de dos posibles caminos para el dato a almacenar en el banco de registros. De esta manera, se coloca un multiplexor en la entrada de la ALU y un segundo en la entrada de datos del banco de registros. La figura 4.10 muestra este nuevo camino de datos.

Reg. de lectura 1

Instrucción

El procesador

4

Dato leído 1

Reg. de lectura 2 Registros Dato Reg. de leído 2 escritura

MemWrite ALUSrc 0 M u x 1

Dato a escribir

MemtoReg

Cero ALU resultado

Dirección

Dato leído

1 M u x 0

Memoria Dato a de datos escribir

RegWrite 16

ALU operation

Extensión de signo

32

MemRead

FIGURA 4.10 El camino de datos para las instrucciones de memoria y tipo R. Este ejemplo muestra cómo puede obtenerse un camino uniendo diferentes piezas (figuras 4.7 y 4.8) y añadiendo multiplexores. Se requieren dos multiplexores tal como se describe en el ejemplo.

Ahora se pueden combinar el resto de las piezas para obtener un camino de datos único para la arquitectura MIPS, añadiendo la parte encargada de la búsqueda de las instrucciones (figura 4.6), al camino de datos de las instrucciones tipo R y de referencia a memoria (figura 4.10), y el camino de datos para los saltos (figura 4.9). La figura 4.11 muestra este camino de datos obtenido al ensamblar las diferentes piezas. Las instrucciones de salto utilizan la ALU para la comparación de los registros fuente, de forma que debe ponerse el sumador de la figura 4.9 para calcular la dirección de destino del salto. También es necesario un nuevo multiplexor para escoger entre seguir en secuencia (PC + 4) y la dirección destino del salto para escribir esta nueva dirección en el PC. Una vez completado este sencillo camino de datos, se puede añadir la unidad de control. Ésta debe ser capaz de generar las señales de escritura para cada elemento de estado y las de control para la ALU y para cada multiplexor a partir de las entradas. Debido a que el control de la ALU es diferente del resto en mayor o menor medida, resultaría útil diseñarlo como paso previo al diseño de la unidad de control.

4.3

315

Construcción de un camino de datos

PCSrc

M u x

Sumador 4

PC

Sumador

Extensión de signo 2

Dirección de lectura Instrucción Memoria de instrucciones

Reg. de lectura 1

ALUSrc Dato leído 1

4

ALU operation MemWrite

Reg. de lectura 2 Registros Dato Reg. de leído 2 escritura

MemtoReg

Cero ALU resultado

Dirección

M u x

Dato a escribir

Dato leído

M u x

Memoria Dato a de datos escribir

RegWrite 16

resultado

Extensión de signo

32

MemRead

FIGURA 4.11 Un camino de datos sencillo para la arquitectura MIPS que combina los elementos necesarios para los diferentes tipos de instrucciones. Este camino de datos puede ejecutar las instrucciones básicas (carga/almacenamiento de palabras, operaciones de la ALU y saltos) en un solo ciclo de reloj. Es necesario un nuevo multiplexor para integrar saltos condicionales. La lógica necesaria para ejecutar instrucciones de salto incondicional (jump) se añadirá más tarde.

I. ¿Cuáles de las siguientes afirmaciones son correctas para una instrucción de carga? Utilizar la figura 4.10. a. MemtoReg debe ser activada para provocar que el dato procedente de memoria sea enviado al banco de registros. b. MemtoReg debe se activada para provocar que el registro destino correcto sea enviado al banco de registros. c. La activación de MemtoReg no es relevante. II. El camino de datos de ciclo único descrito conceptualmente en esta sección debe tener memorias de datos e instrucciones separadas, porque a. En MIPS los formatos de datos e instrucciones son diferentes, y por lo tanto, se necesitan memorias diferentes.

Autoevaluación

316

Capítulo 4

El procesador

b. Tener memorias separadas es más barato. c. El procesador opera en un solo ciclo y no puede usar una memoria con un único puerto para dos acceso diferentes en el mismo ciclo.

4.4

Esquema de una implementación simple

4.4

En esta sección veremos la que podría considerarse como la implementación más sencilla posible de nuestro subconjunto de instrucciones MIPS. Se construirá esta sencilla implementación utilizando el camino de datos de la sección anterior y añadiendo una función de control simple. Esta implementación simple será capaz de ejecutar instrucciones de almacenamiento y carga de palabras de memoria (lw y sw), saltar si igual (beq), instrucciones aritméticas (add, sub, and, or y slt) y activar si es menor que (set on less than). Posteriormente se mejorará dicho diseño incluyendo la instrucción de salto incondicional (j).

El control de la ALU La ALU MIPS del entradas de control:

apéndice C define las siguientes 6 combinaciones de 4

Líneas de conrol de la ALU

Función

0000

Y-lógico (AND)

0001

O-lógico (OR)

0010

sumar

0110

restar

0111

iniciar si menor que

1100

NOR

Dependiendo del tipo de instrucción a ejecutar, la ALU debe realizar una de las cinco primeras operaciones (NOR es necesaria para otras partes del repertorio de instrucciones MIPS). Las instrucciones de referencia a memoria utilizan la ALU para calcular la dirección de memoria por medio de una suma. Para las instrucciones tipo R, la ALU debe ejecutar una de las cinco operaciones (and, or, add, sub y slt) en función del valor de los 6 bits de menor peso de la instrucción, los cuales componen el código de función (véase el capítulo 2), mientras que para las instrucciones de saltar si igual, la ALU debe realizar una resta. Mediante una pequeña unidad de control que tiene como entradas el código de función de la instrucción y 2 bits de control adicionales que reciben el nombre de ALUOp, se pueden generar los 4 bits que conforman las señales de control de la ALU. Estos bits (ALUOp) indican si la operación a realizar debería ser o bien una suma (00) para accesos a memoria, o bien una resta (01) para saltos condicionales, o bien si la operación a realizar está codificada en el código de función (10). La

4.4

Esquema de una implementación simple

317

salida de la unidad de control de la ALU es una señal de 4 bits que controla la ALU codificando una de las combinaciones de 4 bits mostradas anteriormente. En la figura 4.12 se observa el conjunto de combinaciones de las señales de entrada formadas por los 2 bits que conforman la señal de ALUOp y los 6 bits del código de función. Posteriormente, en este capítulo, se verá cómo genera la unidad de control principal los bits de ALUOp. Código de operación

ALUOp

Operación

Campo de la función

Acción deseada Entrada del de la ALU control de la ALU

LW

00

cargar palabra

XXXXXX

sumar

0010

SW

00

almacenar palabra

XXXXXX

sumar

0010

Branch equal

01

saltar si igual

XXXXXX

restar

0110

R-type

10

sumar

100000

sumar

0010

R-type

10

restar

100010

restar

0110

R-type

10

AND

100100

Y lógica

0000

R-type

10

OR

100101

O lógica

0001

10

activar si es menor que

101010

activar si es menor que

0111

R-type

FIGURA 4.12 Cálculo de los bits de control de la ALU en función de los bits de ALUOp y los diferentes códigos de función de las instrucciones tipo R. El código de operación, que aparece en la primera columna, determina los valores de los bits del campo ALUOp. Todas las codificaciones se muestran en binario. Obsérvese que cuando ALUOp tiene como valor 00 ó 01, la salida no depende del código de función, y se dice que su valor es no-determinado y codificado como XXXXXX. En el caso de que ALUOp valga 10, el código de la función se utiliza para calcular la señal de control de la ALU. Véase el apéndice C.

Esta técnica basada en el uso de múltiples niveles de descodificación (o decodificación), es decir, la unidad de control principal genera los bits de ALUOp que se utilizarán como entrada de la unidad de control encargada de generar las señales de la ALU, es muy común. El hecho de usar múltiples niveles puede reducir el tamaño de la unidad principal. De igual manera, el uso de varias unidades de control más pequeñas puede incluso incrementar la velocidad de dicha unidad de control. Todas estas optimizaciones son importantes ya que, a menudo, la unidad de control se encuentra en el camino crítico. Existen diferentes formas de establecer la correspondencia entre los 2 bits del campo de ALUOp y los 6 del código de función con los 3 bits que conforman la operación a realizar por la ALU. Debido a que sólo una pequeña parte de los 64 valores posibles del código de función son importantes y que dichos bits únicamente se utilizan cuando ALUOp vale 10, podría utilizarse una pequeña parte de lógica que reconociera dicho subconjunto de valores posibles y diera como resultado los valores correctos de los bits de control de la ALU. Como paso previo al diseño de la lógica combinacional, es útil construir una tabla de verdad para aquellas combinaciones de interés de los códigos de función y de los bits de ALUOp, tal como se ha realizado en la figura 4.13. Esta tabla muestra cómo dependen de ambos campos las señales de control de la ALU. Debido a que la tabla de verdad es muy grande (28 = 256 entradas), y teniendo en cuenta que para muchas de dichas combinaciones los valores de la salida no tienen importancia, únicamente

Tabla de verdad: desde el punto de vista lógico, es una representación de una operación lógica que muestra todos los valores de las entradas y el valor de las salidas para cada caso.

318

Capítulo 4

El procesador

ALUOp ALUOp1

Campo de la función ALUOp0

F5

F4

F3

F2

F1

F0

Operación

0

0

X

X

X

X

X

X

0010

X

1

X

X

X

X

X

X

0110

1

X

X

X

0

0

0

0

0010

1

X

X

X

0

0

1

0

0110

1

X

X

X

0

1

0

0

0000

1

X

X

X

0

1

0

1

0001

1

X

X

X

1

0

1

0

0111

FIGURA 4.13 Tabla de verdad de los 3 bits de control de la ALU (también llamados operación). Las entradas son la ALUOp y el código de función. Únicamente se muestran aquellas entradas para las cuales la señal de control de la ALU tiene sentido. También se han añadido algunas entradas cuyo valor es indeterminado. Por ejemplo, el campo ALUOp no utiliza la codificación 11, de forma que la tabla de verdad puede contener las entradas 1X y X1 en vez de 10 y 01. También, cuando se utiliza el código de función, los 2 primeros bits (F5 y F4) de dichas instrucciones son siempre 10, de forma que también se consideran no-determinados y se reemplazan por XX en la tabla de verdad.

Términos indeterminados: elementos de una función lógica cuya salida no depende de los valores de todas las entradas. Los términos no-determinados pueden especificarse de distintas maneras.

se dan los valores de las salidas para aquellas entradas de la tabla donde el control de la ALU debe tener un valor específico. Las diferentes tablas de verdad que se irán viendo a lo largo de este capítulo contendrán únicamente aquellos subconjuntos de entradas que deban estar activadas, eliminando aquellos cuyos valores de salida sean indeterminados. (Este método tiene un inconveniente que se analizará en la sección D.2 del apéndice D. Debido a que en muchos casos los valores de algunas entradas no tienen importancia, para mantener la tabla compacta incluimos términos indeterminados. Un término de este tipo (representado en la tabla mediante una X en la columna de entrada correspondiente) indica que la salida es independiente del valor de dicha entrada. Por ejemplo, cuando el campo ALUOp vale 00, caso de la primera fila de la tabla de la figura 4.13, la señal de control de la ALU siempre será 0010, independientemente del código de función. Es decir, en este caso, el código de la función se considera indeterminado en esta fila de la tabla de verdad. Más adelante se verán ejemplos de otro tipo de términos indeterminados. Si no se está familiarizado con este tipo de términos, véase el apéndice C para mayor información. Una vez que se ha construido la tabla de verdad, ésta puede optimizarse y entonces pasar a su implementación mediante puertas lógicas. Este proceso es completamente mecánico. Así, en lugar de mostrar aquí los pasos finales, este proceso, así como sus resultados, se describe en la sección D.2 del apéndice D.

Diseño de la unidad de control principal Una vez que ya se ha descrito como diseñar una ALU que utiliza el código de función y una señal de dos bits como entradas de control, podemos centrarnos en el resto del control. Para iniciar este proceso se deben identificar los campos de las instrucciones y las líneas de control necesarias para la construcción del camino de datos mostrado en la figura 4.11. Para entender mejor cómo se conectan los diferentes campos de las instrucciones en el camino de datos, sería útil revisar los formatos de los tres tipos de instrucciones: el tipo R (aritmético-lógica), los saltos y las operaciones de carga y almacenamiento. Estos formatos se muestran en la figura 4.14.

4.4

Campo Posición de los bits

a.

Posición de los bits

rs

rt

rd

shamt

funct

25:21

20:16

15:11

10:6

5:0

35 o 43

rs

rt

dirección

31:26

25:21

20:16

15:0

Instrucción de carga o almacenamiento

Campo Posición de los bits

c.

0 31:26

Instrucción tipo R

Campo

b.

319

Esquema de una implementación simple

4

rs

rt

dirección

31:26

25:21

20:16

15:0

Instrucción de salto condicional

FIGURA 4.14 Los tres tipos de instrucciones (tipo R, referencia a memoria y salto condicional) utilizan dos formatos de instrucción diferentes. La instrucción de salto incondicional hace uso de otro formato, que se explicará más adelante. (a) Formato para las instrucciones tipo R, que tiene el campo de tipo de operación a 0. Estas instrucciones disponen de tres operandos registros: rs, rt y rd, donde rs y rt son registros fuente y rd es el registro destino. La función a realizar en la ALU se encuentra en el código de función (campo funct) y es descodificada por la unidad de control de la ALU, tal como se ha visto en la sección anterior. Pertenecen a este tipo las instrucciones add, sub, and, or y slt. El campo de desplazamiento (shamt) sólo se utiliza para desplazar y se ignora en este capítulo. (b) Formato de las instrucciones carga (código de operación 35diez) y almacenamiento (código de operación 43diez). El registro rs se utiliza como registro base al cual se le añade el campo de dirección de 16 bits para obtener la dirección de memoria. En el caso de las instrucciones de carga, rt es el registro destino del valor cargado de memoria, mientras que en los almacenamientos, rt es el registro fuente del valor que se debe almacenar en memoria. (c) Formato de las instrucciones de saltar si igual (código de operación 4diez). Los registros rs y rt son los registros fuente que se compararán. Al campo de 16 bits de la dirección se le extiende el signo, se desplaza y se suma al PC para calcular la dirección de destino de salto.

Existen varias observaciones importantes a realizar sobre este formato de las instrucciones y que siempre se supondrán ciertas. ■

El campo de código de operación (opcode) estará siempre contenido en los bits 31-26. Se referenciará dicho campo como Op[5-0].



Los dos registros de lectura están siempre especificados en los campos rs y rt en las posiciones 25-21 y 20-16. Este hecho se cumple para las instrucciones tipo R, beq y almacenamiento.



El registro base para las instrucciones de acceso a memoria se encuentra siempre en rs (bits 25-21).



El desplazamiento relativo de 16 bits para saltar si igual (beq), cargas y almacenamientos está siempre en las posiciones 15-0.



El registro destino puede estar en dos lugares. Para instrucciones de carga, su posición es 20-16 (rt), mientras que en las instrucciones aritmético-lógicas está en los bits 15-11 (rd). Esto implica tener que añadir un multiplexor para seleccionar cuál de estos dos campos de la instrucción va a utilizarse en el momento de indicar el registro en el que se va a escribir.

Opcode: campo que denota la operación y formato de una instrucción.

320

Capítulo 4

El procesador

El primer principio de diseño del capítulo 2, la sencillez favorece la regularidad, vale para la especificación del control. Mediante esta información pueden añadirse las etiquetas de las diferentes partes de las instrucciones y el multiplexor adicional (para el registro destino) a nuestro sencillo camino de datos. La figura 4.15 muestra estos añadidos, que incluyen la unidad de control de la ALU, las señales de escritura de los elementos de estado, la señal de lectura de la memoria de datos y las señales de control de los multiplexores. Debido a que estos últimos únicamente tienen dos entradas, sólo requieren una única línea de control cada uno. La figura 4.15 muestra las siete señales de control de 1 bit a las que hay que añadir la señal de ALUOp (de 2 bits). El funcionamiento de dicha señal ya se ha explicado y ahora sería útil definirlo informalmente para el resto de las señales antes de determinar cómo van a trabajar durante la ejecución de las instrucciones. La figura 4.16 describe la funcionalidad de dichas líneas de control.

PCSrc 0 Sumador Sumador 4

resultado

Instrucción [25:21] Dirección de lectura Instrucción [20:16] Instrucción [31:0] Memoria de instrucciones

0 M u Instrucción [15:11] x 1 RegDst

Instrucción [15:0]

Reg. de lectura 1 Reg. de lectura 2 Reg. de escritura

MemWrite Dato leído 1 Dato leído 2

Dato a escribir Registros

16

1

Desp. 2 a la izq.

RegWrite

PC

M u x

Extensión de signo

ALUSrc

ALU Cero

0 M u x 1

resultado

MemtoReg Dirección

Dato leído

Memoria de datos Dato a escribir

1 M u x 0

32 ALU control

MemRead

Instrucción [5:0] ALUOp

FIGURA 4.15 El camino de datos de la figura 4.12 con todos los multiplexores necesarios y todas las líneas de control identificadas. Las líneas de control se muestran en color. También se ha añadido el bloque encargado de controlar la ALU. El registro PC no necesita un control de escritura ya que sólo se escribe una vez al final de cada ciclo de reloj. La lógica de control del salto es la que determina si el nuevo valor será el valor anterior incrementado o la dirección destino del salto.

4.4

Señal de control RegDst RegWrite ALUSrc

PCSrc

MemRead

MemWrite MemtoReg

Efecto cuando no está activa

Esquema de una implementación simple

Efecto cuando está activa

El identificador del registro destino viene El identificador del registro destino viene determinado por el campo rt (bits 20-16). determinado por el campo rd (bits 15-11). El registro destino se actualiza con el valor Ninguno. a escribir. El segundo operando de la ALU proviene El segundo operando de la ALU son los 16 bits del segundo registro leído del banco de de menor peso de la instrucción con el signo extendido. registros. El PC es reemplazado por la salida del El PC es reemplazado por su valor sumador que calcula la dirección destino anterior más 4 (PC + 4). del salto. El valor de la posición de memoria designada Ninguno. por la dirección se coloca en la salida de lectura. El valor de la posicón de memoria designada Ninguno. por la dirección se reemplaza por el valor de la entrada de datos. El valor de entrada del banco de registros El valor de entrada del banco de registros proviene de la ALU. proviene de la memoria.

FIGURA 4.16 El efecto de cada una de las siete señales de control. Cuando el bit de control encargado de controlar un multiplexor de dos vías está a 1, dicho multiplexor seleccionará la entrada etiquetada como l. De lo contrario, si el control está desactivado, se tomará la entrada etiquetada como 0. Recuerde que todos los elementos de estado tienen un reloj como señal de entrada implícita y cuya función es controlar las escrituras. Nunca se hace atravesar puertas al reloj, ya que puede crear problemas. (Véase en apéndice C una discusión más detallada de este problema.)

Una vez definidas las funciones de cada una de las señales de control, se puede pasar a ver cómo activarlas. La unidad de control puede activar todas las señales en función de los códigos de operación de las instrucciones exceptuando una de ellas (la señal PCSrc). Esta línea de control debe activarse si la instrucción es de tipo salto condicional (decisión que puede tomar la unidad de control) y la salida Cero de la ALU, que se utiliza en las comparaciones, está a 1. Para generar la señal de PCSrc se necesita aplicar una AND a una señal llamada Branch, que viene de la unidad de control, con la señal Cero procedente de la ALU. Estas nueve señales de control (las siete de la figura 4.16, más las dos de la señal ALUOp), pueden activarse en función de las seis señales de entrada de la unidad de control, las cuales pertenecen al código de operación, bits 31 a 26. El camino de datos con la unidad y las señales de control se muestra en la figura 4.17. Antes de intentar escribir el conjunto de ecuaciones de la tabla de verdad de la unidad de control, sería útil definirla informalmente. Debido a que la activación de las líneas de control únicamente depende del código de operación, se define si cada una de las señales de control debería valer 0, 1 o bien indeterminada (X), para cada uno de los códigos de operación. La figura 4.18 define cómo deben activarse las señales de control para cada código de operación; información que proviene directamente de las figuras 4.12, 4.16 y 4.17. Funcionamiento del camino de datos

Con la información contenida en las figuras 4.16 y 4.18, se puede diseñar la lógica de la unidad de control, pero antes de esto, se verá cómo cada instrucción hace uso del

321

322

Capítulo 4

El procesador

0 Sumador Sumador 4

Control

PC

Instrucción [25–21]

Reg. de lectura 1

Instrucción [20–16]

Reg. de lectura 2

Dirección de lectura Instrucción [31–0] Memoria de instrucciones

0 M u Instrucción [15–11] x 1

Desp. 2 a la izq.

RegDst Branch MemRead MemtoReg ALUOp MemWrite ALUSrc RegWrite

Instrucción [31–26]

Reg. de escritura

16

1

Dato leído 1 Dato leído 2

Dato a escribir Registros

Instrucción [15–0]

resultado ALU

M u x

Extensión de signo

0 M u x 1

Cero ALU resultado ALU

Dirección

Dato leído

Memoria de datos Dato a escribir

1 M u x 0

32 ALU Control

Instrucción [5–0]

FIGURA 4.17 Un sencillo camino de datos con la unidad de control. La entrada de la unidad de control está compuesta por los seis bits pertenecientes al campo del código de operación de la instrucción. Las salidas de dicha unidad consisten en tres señales de un bit que se utilizan para controlar los multiplexores (RegDst, ALUSrc, y MemtoReg), tres señales para controlar lecturas y escrituras en el banco de registros y en la memoria (RegWrite, MemRead, y MemWrite), una señal de 1 bit para determinar si es posible saltar (branch), y una señal de control de 2 bits para la ALU (ALUOp). Se utiliza una puerta AND para combinar la señal de control branch de la unidad de control y la salida Cero de la ALU. Dicha puerta será la que controle la selección del próximo PC. Obsérvese que ahora PCSrc es una señal derivada aunque una de sus partes proviene directamente de la unidad de control. En adelante se suprimirá este nombre.

camino de datos. En las siguientes figuras se muestra el flujo a través del camino de datos de las tres clases diferentes de instrucciones. Las señales de control activadas, así como los elementos activos del camino de datos, están resaltados en cada caso. Obsérvese que un multiplexor cuyo control está a 0 es una acción definida, a pesar de que su línea de control no esté destacada. Las señales de control formadas por múltiples bits están destacadas si cualquiera de los que la constituyen está activa.

4.4

323

Esquema de una implementación simple

Instrucción

RegDst

ALUSrc

MemtoReg

Reg Write

Formato R

1

0

0

1

lw sw beq

0

1

1

1

X

1

X

0

X

0

X

0

Mem Read

Mem Write

Branch

ALUOp1

ALUOp0

0

0

0

1

0

1

0

0

0

0

0

1

0

0

0

0

0

1

0

1

FIGURA 4.18 La activación de las líneas de control está completamente determinada por el código de operación de las instrucciones. La primera fila de la tabla corresponde a las instrucciones tipo R (add, sub, and, or y slt). En todas estas instrucciones, los registros fuente se encuentran en rs y rt, y el registro destino en rd, de forma que las señales ALUSrc y RegDst deben estar activas. Además, este tipo de instrucciones siempre escribe en un registro (RegWrite = 1) y en ningún caso accede a la memoria de datos. Cuando la señal Branch está a 0, el PC se reemplaza de forma incondicional por PC + 4; en otro caso, el registro de PC se reemplaza por la dirección destino del salto si la salida Cero de la ALU también está en nivel alto. El campo ALUOp para las instrucciones tipo R siempre tiene el valor de 10 para indicar que las señales de control de la ALU deben generarse con ayuda del campo de función. La segunda y tercera fila de la tabla contienen el valor de las señales de control para las instrucciones lw y sw. En ambos casos, ALUSrc y ALUOp están activas para poder llevar a cabo el cálculo de direcciones. Las señales MemRead y MemWrite estarán a 1 según el tipo de acceso. Finalmente, cabe destacar que RegDst y RegWrite deben tener los valores 0 y 1, respectivamente, en caso de una carga, para que el valor se almacene en el registro rt. El control de las instrucciones de salto es similar a las tipo R ya que también envía los registros rs y rt a la ALU. La señal ALUOp, en caso de instrucción beq, toma el valor 01, para que la ALU realice una operación de resta y compruebe así la igualdad. Observe que el valor de la señal MemtoReg es irrelevante cuando RegWrite vale 0 (como el registro no va a ser escrito, el valor del dato existente en el puerto de escritura del banco de registros no va a ser utilizado). De esta manera, en las dos últimas filas de la tabla, los valores de la señal MemtoReg se han reemplazado por X, que indica términos indeterminados. De igual forma, este tipo de términos pueden añadirse a RegDst cuando RegWrite vale 0. Este tipo de términos los debe añadir el diseñador, pues dependen del conocimiento que se tenga sobre el funcionamiento del camino de datos.

La figura 4.19 muestra el funcionamiento de una instrucción tipo R, tal como add $t1, $t2, $t3. Aunque todo sucede en un único ciclo de reloj, la ejecución

de una instrucción se puede dividir en cuatro pasos; estos pasos se pueden ordenar según el flujo de información: 1. Se carga una instrucción de la memoria de instrucciones y se incrementa el PC. 2. Se leen los registros $t2 y $t3 del banco de registros. Adicionalmente, la unidad de control principal se encarga de calcular la activación de las señales de control durante este paso. 3. La ALU se encarga de realizar la operación adecuada con los datos procedentes del banco de registros, utilizando para ello el campo de función (bits 5-0) para obtener la función de la ALU. 4. El resultado calculado en la ALU se escribe en el banco de registros utilizando los bits 15-11 de la instrucción para seleccionar el registro destino ($t1). De forma similar, se puede ilustrar la ejecución de una instrucción de carga, tal como lw $t1, offset($t2)

de un modo parecido a la figura 4.19. La figura 4.20 muestra las unidades funcionales y las líneas de control activadas para esta instrucción. Puede verse una carga como una instrucción que se ejecuta en 5 pasos (similares a los de las instrucciones tipo R, que se ejecutaban en 4). 1. Se carga una instrucción de la memoria de instrucciones y se incrementa el PC. 2. Se 1ee el valor del registro $t2 del banco de registros.

324

Capítulo 4

El procesador

0 Sumador Sumador 4

Control

PC

Instrucción [25–21]

Reg. de lectura 1

Instrucción [20–16]

Reg. de lectura 2

Dirección de lectura Instrucción [31–0] Memoria de instrucciones

0 M u Instrucción [15–11] x 1

Desp. 2 a la izq.

RegDst Branch MemRead MemtoReg ALUOp MemWrite ALUSrc RegWrite

Instrucción [31–26]

Reg. de escritura

16

1

Dato leído 1 Dato leído 2

Dato a escribir Registros

Instrucción [15–0]

resultado ALU

M u x

Extensión de signo

32

0 M u x 1

Cero ALU resultado ALU

Dirección

Dato leído

Memoria de datos Dato a escribir

1 M u x 0

ALU Control

Instrucción [5–0]

FIGURA 4.19 Funcionamiento del camino de datos para una instrucción R-type tal como add $t1, $t2, $t3. Las líneas de control, las unidades del camino de datos y las conexiones activas se muestran resaltadas.

3. La ALU calcula la suma del valor leído del banco de registros y los 16 bits de menor peso de la instrucción, con el signo extendido (offset). 4. Dicha suma se utiliza como dirección para acceder a la memoria de datos. 5. El dato procedente de la memoria se escribe en el banco de registros. El registro destino viene determinado por los bits 20-16 de la instrucción ($t1).

4.4

325

Esquema de una implementación simple

0 Sumador Sumador 4

Control

PC

Instrucción [25–21]

Reg. de lectura 1

Instrucción [20–16]

Reg. de lectura 2

Dirección de lectura Instrucción [31–0] Memoria de instrucciones

0 M u Instrucción [15–11] x 1

Desp. 2 a la izq.

RegDst Branch MemRead MemtoReg ALUOp MemWrite ALUSrc RegWrite

Instrucción [31–26]

Reg. de escritura

16

1

Dato leído 1 Dato leído 2

Dato a escribir Registros

Instrucción [15–0]

resultado ALU

M u x

Extensión de signo

32

0 M u x 1

Cero ALU resultado ALU

Dirección

Dato leído

Memoria de datos Dato a escribir

1 M u x 0

ALU Control

Instrucción [5–0]

FIGURA 4.20 Funcionamiento del camino de datos para una instrucción de carga. Las líneas de control, las unidades del camino de datos y las conexiones activas se muestran resaltadas. Una instrucción de almacenamiento opera de forma similar. La diferencia principal es que el control de la memoria indicará una escritura en lugar de una lectura, el valor del segundo registro contendría el dato a almacenar, y la operación final de escribir el valor de memoria en el banco de registros no se realizaría.

Finalmente, se puede ver de la misma manera la operación de saltar si igual, por ejemplo, beq $t1, $t2, offset. Su ejecución es similar a las instrucciones tipo R, pero la salida de la ALU se utiliza para determinar si el PC se actualizará con PC + 4 o con la dirección destino del salto. La figura 4.21 muestra los cuatro pasos de la ejecución. 1. Se carga una instrucción de la memoria de instrucciones y se incrementa el PC. 2. Se leen los registros $t1 y $t2 del banco de registros.

326

Capítulo 4

El procesador

0 Sumador Sumador 4

Control

PC

Instrucción [25–21]

Reg. de lectura 1

Instrucción [20–16]

Reg. de lectura 2

Dirección de lectura Instrucción [31–0] Memoria de instrucciones

0 M u Instrucción [15–11] x 1

Desp. 2 a la izq.

RegDst Branch MemRead MemtoReg ALUOp MemWrite ALUSrc RegWrite

Instrucción [31–26]

Reg. de escritura

16

1

Dato leído 1 Dato leído 2

Dato a escribir Registros

Instrucción [15–0]

resultado ALU

M u x

Extensión de signo

32

0 M u x 1

Cero ALU resultado ALU

Dirección

Dato leído

Memoria de datos Dato a escribir

1 M u x 0

ALU Control

Instrucción [5–0]

FIGURA 4.21 Funcionamiento del camino de datos para una instrucción de salto si igual. Las líneas de control, las unidades del camino de datos y las conexiones activas se muestran resaltadas. Después de usar el banco de registros y la ALU para realizar la comparación, la salida Cero se utiliza para seleccionar el siguiente valor del contador de programas entre los dos valores candidatos.

3. La ALU realiza una resta de los operandos leídos del banco de registros. Se suma el valor de PC + 4 a los 16 bits de menor peso (con el signo extendido) de la instrucción (offset) desplazados 2 bits. El resultado es la dirección destino del salto. 4. Se utiliza la señal Cero de la ALU para decidir qué valor se almacena en el PC.

4.4

Esquema de una implementación simple

327

Finalización del control

Una vez que se ha visto los pasos en la ejecución de las instrucciones continuaremos con la implementación del control. La función de control puede definirse de forma precisa utilizando los contenidos de la figura 4.18. Las salidas son las líneas de control y las entradas los 6 bits que conforman el campo del código de operación (Op[5-0]). De esta manera se puede crear una tabla de verdad para cada una de las salidas basada en la codificación binaria de los códigos de operación. La figura 4.22 muestra la lógica de la unidad de control como una gran tabla de verdad que combina todas las salidas y utiliza los bits del código de operación como entradas. Ésta especifica de forma completa la función de control y puede implementarse mediante puertas lógicas de forma automática. Este paso final se muestra en la sección D.2 en el apéndice D. Ahora que ya tenemos una implementación de ciclo de reloj único para la mayor parte del núcleo del repertorio de instrucciones MIPS, se va a añadir la instrucción de salto incondicional (jump) y se verá cómo puede extenderse el camino de datos y su control para poder ejecutar otro tipo de instrucciones del repertorio. Entrada o salida Nombre de la señal Entradas

Salidas

Formato R

lw

sw

beq

Op5

0

1

1

0

Op4

0

0

0

0

Op3

0

0

1

0

Op2

0

0

0

1

Op1

0

1

1

0

Op0

0

1

1

0

RegDst

1

0

X

X

ALUSrc

0

1

1

0

MemtoReg

0

1

X

X

RegWrite

1

1

0

0

MemRead

0

1

0

0

MemWrite

0

0

1

0

Branch

0

0

0

1

ALUOp1

1

0

0

0

ALUOp0

0

0

0

1

FIGURA 4.22 La función de control para una implementación de ciclo único está completamente especificada en esta tabla de verdad. La mitad superior de la tabla muestra las posibles combinaciones de las señales a la entrada, que corresponden a los cuatro posibles códigos de operación y que determinan la activación de las señales de control. Recuerde que Op[5-01] se corresponde con los bits 31-26 de la instrucción (el código de operación). La parte inferior muestra las salidas. Así, la señal de salida RegWrite está activada para dos combinaciones diferentes de entradas. Si únicamente se consideran estos cuatro posibles códigos de operación, esta tabla puede simplificarse utilizando términos indeterminados en las señales de entrada. Por ejemplo, puede detectarse una instrucción de tipo aritmético-lógico con la expresión Op5 • Op2, ya que es suficiente para distinguirlas del resto. De todas formas, no se utilizará esta simplificación, pues el resto de los códigos de operación del MIPS se usarán en la implementación completa.

Implementación de ciclo único (implementación de ciclo de reloj único): implementación en la que una instrucción se ejecuta en un único ciclo de reloj.

328

Capítulo 4

El procesador

Implementación de saltos incondicionales (jump)

EJEMPLO

RESPUESTA

La figura 4.17 muestra la implementación de muchas de las instrucciones vistas en el capítulo 2. Un tipo de instrucción ausente es el salto incondicional. Extienda el camino de datos de la figura 4.17, así como su control, para incluir dicho tipo de instrucciones. Describa cómo se ha de activar cualquier línea de control nueva. La instrucción jump, mostrada en la figura 4.23, tiene un cierto parecido a la instrucción de salto condicional, pero calcula la dirección de destino de forma diferente y, además, es incondicional. Como en los saltos condicionales, los 2 bits de menor peso de la dirección de salto son siempre 00dos. Los siguientes 26 bits de menor peso de la dirección están en el campo inmediato de la instrucción. Los 4 bits de mayor peso de la dirección que debería reemplazar al PC vienen del PC de la instrucción al cual se le ha sumado 4. Así, podría realizarse un salto incondicional almacenando en el registro de PC la concatenación de: ■

Los 4 bits de mayor peso del actual PC + 4 (son los bits 31-28 de la dirección de la siguiente instrucción en orden secuencial).



Los 26 bits correspondientes al campo inmediato de la instrucción jump.



Los bits 00dos.

La figura 4.24 muestra las partes añadidas al control para este tipo de instrucciones respecto a la figura 4.17. Se necesita un nuevo multiplexor para seleccionar el origen del nuevo PC, la dirección de 1a siguiente instrucción en orden secuencial (PC + 4), la dirección de salto condicional o la de una instrucción de salto incondicional. También se necesita una nueva señal de control para este multiplexor. Esta señal, llamada Jump, únicamente se activa cuando la instrucción es un salto incondicional, es decir, cuando el código de operación es 2. Campo Posición de los bits

000010

dirección

31-26

25-0

FIGURA 4.23 Formato de la instrucción jump (código de operación = 2). La dirección destino de este tipo de instrucciones se forma mediante la concatenación de los 4 bits de mayor peso de PC + 4 y los 26 bits de campo de dirección de la instrucción añadiendo 00 como los 2 bits de menor peso.

Por qué no se utiliza una implementación de ciclo único hoy en día Aunque este tipo de implementaciones funciona correctamente, no se utiliza en los diseños actuales porque es ineficiente. Para ver por qué ocurre esto, debe saberse que el ciclo de reloj debe tener la misma longitud para todos los tipos de instrucciones. Por supuesto, el ciclo de reloj viene determinado por el mayor

4.4

Instrucción [25–0] 26

Despl. 2 Dirección de salto [31–0] a la izq.

28

PC + 4 [31–28]

Sumador

Sumador

4

resultado ALU RegDst Jump Branch MemRead MemtoReg ALUOp MemWrite ALUSrc RegWrite

Instrucción [31–26] Control

PC

329

Esquema de una implementación simple

Dirección de lectura Instrucción [31–0] Memoria de instrucciones

Instrucción [25–21]

Reg. de lectura 1

Instrucción [20–16]

Reg. de lectura 2

0 M u Instrucción [15–11] x 1

Reg. de escritura

16

1

M u x

M u x

1

0

Despl. 2 a la izq.

Dato leído 1 Dato leído 2

Dato a escribir Registros

Instrucción [15–0]

0

Extensión de signo

0 M u x 1

Cero ALU resultado ALU

Dirección

Dato leído

Memoria de datos Dato a escribir

1 M u x 0

32 ALU Control

Instrucción [5–0]

FIGURA 4.24 Un camino de datos sencillo y su control extendido para ejecutar instrucciones de salto incondicional. Se utiliza un multiplexor adicional (en la esquina superior derecha) para elegir entre el destino de la instrucción de salto incondicional y la dirección salto condicional o la siguiente en orden secuencial. Este multiplexor se controla mediante la señal de salto incondicional. La dirección de destino de la instrucción de salto incondicional se obtiene desplazando los 26 bits de menor peso 2 bits a la izquierda (se añaden 00 como bits de menor peso), concatenando los 4 bits de mayor peso de PC + 4 como bits de mayor peso, y obteniendo de esta manera una dirección de 32 bits.

tiempo de ejecución posible en el procesador. Esta instrucción en la mayoría de los casos es la instrucción load, que usa 5 unidades funcionales en serie: la memoria de instrucciones, el banco de registros, la ALU, la memoria de datos y el banco de registros nuevamente. Aunque el CPI es 1 (véase el capítulo 1), las prestaciones generales de la implementación de ciclo único probablemente no es muy bueno, porque el ciclo de reloj es demasiado largo. La penalización por utilizar un diseño de ciclo único con un ciclo de reloj fijo es importante, pero podría considerarse aceptable para un repertorio con pocas instrucciones. Históricamente, los primeros computadores con un repertorio de instrucciones muy simple usaban esta técnica de implementación. Sin embargo, si

330

Capítulo 4

El procesador

se intenta implementar una unidad de punto flotante o un repertorio de instrucciones con instrucciones más complejas, el diseño de ciclo único podría no funcionar correctamente. Es inútil intentar implementar técnicas que reduzcan el retraso del caso más habitual pero que no mejoren el tiempo de ciclo del peor caso, porque el ciclo de la señal de reloj debe ser igual al retraso del peor caso de todas las instrucciones. Así, la implementación de ciclo único viola el principio de diseño clave del capítulo 2, de hacer rápido el caso más habitual. En la siguiente sección analizaremos otra técnica de implementación, llamada segmentación, que utiliza un camino de datos muy similar a la de ciclo único pero mucho más eficiente, con una productividad más elevada. La segmentación mejora la eficiencia mediante la ejecución de varias instrucciones simultáneamente.

Autoevaluación

Fíjese en el control de la figura 4.22. ¿Se pueden combinar algunas señales de control? ¿Se puede reemplazar alguna señal de control de salida por la inversa de otra? (ténganse en cuenta las indeterminaciones). Si es así, ¿se puede reemplazar una señal por otra sin añadir inversores?

No malgastes el tiempo. Proverbio Americano

Segmentación (pipelining): técnica de implementación que solapa la ejecución de múltiples instrucciones, de forma muy similar a una línea de ensamblaje.

4.5

Descripción general de la segmentación

4.5

Segmentación (pipelining) es una técnica de implementación que consiste en solapar la ejecución de múltiples instrucciones. Hoy en día, la segmentación es universal. En esta sección utilizaremos una analogía para describir los términos básicos y las características principales de la segmentación. Si el lector sólo está interesado en la idea general, debe centrarse en esta sección y después saltar a las secciones 4.10 y 4.11 para leer una introducción a las técnicas avanzadas de segmentación que usan procesadores recientes como el AMD Opteron X4 (Barcelona) o Intel Core. Pero si está interesado en explorar la anatomía de un computador segmentado, entonces esta sección es una buena introducción a las secciones de la 4.6 a la 4.9. Cualquiera que haya tenido que lavar grandes cantidades de ropa ha usado de forma intuitiva la estrategia de la segmentación. El enfoque no segmentado de hacer la colada sería 1. Introducir ropa sucia en la lavadora. 2. Cuando finaliza el lavado, introducir la ropa mojada en la secadora. 3. Cuando finaliza el secado, poner la ropa seca en una mesa para ordernarla y doblarla. 4. Una vez que toda la ropa está doblada, pedir al compañero de piso que guarde la ropa. Cuando el compañero ha finalizado, entonces se vuelve a comenzar con la siguiente colada. El enfoque segmentado de lavado require mucho menos tiempo, tal y como muestra la figura 4.25. Tan pronto como la lavadora termina con la primera colada y ésta es colocada en la secadora, se vuelve a cargar la lavadora con una

4.5

Descripción general de la segmentación

segunda colada de ropa sucia. Cuando la primera colada esté seca, se pone encima de la mesa para empezar a doblarla, se pasa la colada mojada de la lavadora a la secadora, y se mete en la lavadora la tercera colada de ropa sucia. Después, mientras el compañero de piso se lleva la ropa doblada, se empieza a doblar la segunda colada al tiempo que la tercera colada se pasa de la lavadora a la secadora y se introduce la cuarta colada en la lavadora. Llegados a este punto todos los pasos – denominados etapas de segmentación (o segmentos)– se llevan a cabo de forma concurrente. Se podrán segmentar las tareas siempre y cuando se disponga de recursos separados para cada etapa. La paradoja de la segmentación es que el tiempo desde que se pone un calcetín sucio en la lavadora hasta que se seca, se dobla y se guarda no es más corto al utilizar la segmentación; la razón por la cual la segmentación es más rápida para varias coladas es que todas las etapas se llevan a cabo en paralelo, y por tanto se completan más coladas por hora. La segmentación mejora la productividad de la lavandería sin mejorar el tiempo necesario para completar una única colada. Consecuentemente, la segmentación no reducirá el tiempo para completar una colada, pero si tenemos que hacer muchas coladas, la mejora de la productividad reducirá el tiempo total para completar todo el trabajo. 6 PM

7

8

9

10

11

12

1

2 AM

6 PM

7

8

9

10

11

12

1

2 AM

Tiempo Orden de las tareas A B C D

Tiempo

Orden de las tareas A B C D

FIGURA 4.25 La analogía de la lavandería. Ann, Brian, Cathy y Don tienen ropa sucia que lavar, secar, doblar y guardar. Cada una de las cuatro tareas (lavar, secar, doblar y guardar) dura 30 minutos. Hacer la colada secuencialmente lleva 8 horas para realizar 4 coladas, mientras que la colada organizada de forma segmentada necesita solamente 3.5 horas. Aunque en realidad se dispone de un único recurso de cada tipo, para mostrar la etapa de segmentación usada por las diferentes coladas a lo largo del tiempo se usan copias de los 4 recursos.

331

332

Capítulo 4

El procesador

Si todos los segmentos o etapas requieren la misma cantidad de tiempo y si hay el suficiente trabajo por hacer, entonces la ganancia que proporciona la segmentación es igual al número de segmentos en que se divide la tarea, que en este caso son cuatro: lavar, secar, doblar y colocar. Por tanto, el lavado segmentado es potencialmente cuatro veces más rápido que el no segmentado: 20 coladas necesitarán alrededor de 5 veces más tiempo que una única colada, mientras que la estrategia secuencial de lavado requiere 20 veces el tiempo de una colada. En la figura 4.25 la segmentación sólo es 2.3 veces más rápida porque se consideran únicamente 4 coladas. Observe que tanto al principio como al final de la tarea segmentada, el cauce de segmentación (que denominaremos pipeline) no está completamente lleno. Estos efectos transitorios iniciales y finales afectan a las prestaciones cuando el número de tareas es pequeño en comparación con el número de etapas. Cuando el número de coladas es mucho mayor que 4, las etapas estarán ocupadas la mayor parte del tiempo y el incremento en la productividad será muy cercano a 4. Los mismos principios son aplicables a los procesadores cuando se segmenta la ejecución de instrucciones. Clásicamente se consideran cinco pasos para ejecutar las instrucciones MIPS: 1. Buscar una instrucción en memoria. 2. Leer los registros mientras se descodifica la instrucción. El formato de las instrucciones MIPS permite que la lectura y descodificación ocurran de forma simultánea. 3. Ejecutar una operación o calcular una dirección de memoria. 4. Acceder a un operando en la memoria de datos. 5. Escribir el resultado en un registro. Por tanto, el pipeline MIPS que se explorará en este capítulo tiene cinco etapas. El siguiente ejemplo muestra que la segmentación acelera la ejecución de instrucciones del mismo modo que lo hace para la colada.

Las prestaciones en un diseño de ciclo único (monociclo) frente a un diseño segmentado

EJEMPLO

Para concretar la discusión se creará un pipeline. En este ejemplo y en el resto del capítulo limitaremos nuestra atención a sólo 8 instrucciones: cargar palabra (lw), almacenar palabra (sw), sumar (add), restar (sub), y-lógica (and), o-lógica (or), activar si menor (set-less-than, slt), y saltar si es igual (beq). Compare el tiempo promedio entre la finalización de instrucciones consecutivas en una implementación monociclo, en la que todas las instrucciones se ejecutan en un único ciclo de reloj, con una implementación segmentada. En este ejemplo, la duración de las operaciones para las unidades funcionales principales es de 200 ps para los accesos a memoria, de 200 ps para las operaciones en la ALU, y de 100 ps para las lecturas o las escrituras en el banco de registros. En el modelo monociclo cada instrucción dura exactamente un ciclo de reloj, y por tanto el ciclo de reloj debe estirarse lo suficiente para poder acomodar a la instrucción más lenta.

4.5

Descripción general de la segmentación

La figura 4.26 indica el tiempo requerido para cada una de las ocho instrucciones. El diseño monociclo debe permitir acomodar a la instrucción más lenta —que en la figura 4.26 es lw— y por tanto la duración de todas las instrucciones será de 800 ps. Del mismo modo que la figura 4.25, la figura 4.27 compara las ejecuciones segmentadas y no segmentadas de tres instrucciones lw. En el caso del diseño no segmentado, el tiempo transcurrido entre el inicio de la primera instrucción y el inicio de la cuarta instrucción es de 3 × 800 ps ó 2400 ps. Todas las etapas de segmentación duran un único ciclo de reloj, de modo que el periodo del reloj debe ser suficientemente largo como para dar cabida a la operación más lenta. Así, del mismo modo que el diseño monociclo debe tener un ciclo de reloj de 800 ps, necesario para el caso más desfavorable, aun cuando algunas instrucciones podrían ser ejecutadas en 500 ps, la ejecución segmentada debe tener un ciclo de reloj de 200 ps para dar cabida a la etapa más desfavorable, aunque algunas etapas sólo necesiten 100 ps. Aún así, la segmentación cuadriplica las prestaciones: el tiempo entre el inicio de la primera instrucción y el inicio de la cuarta instrucción es de 3 × 200 ps o 600 ps. Búsqueda de Lectura de Operación Acceso Escritura Tiempo ALU al dato en registro total Clase de instrucción la instrucción registros Almacenar palabra (lw)

200 ps

100 ps

200 ps

200 ps

Cargar palabra (sw)

200 ps

100 ps

200 ps

200 ps

Formato R (add, sub, and, or, slt)

200 ps

100 ps

200 ps

Salto (beq)

200 ps

100 ps

200 ps

100 ps

800 ps

100 ps

600 ps

700 ps

500 ps

FIGURA 4.26 Tiempo total para cada instrucción calculado a partir del tiempo de cada componente. El cálculo supone que no existe ningún retardo debido a los multiplexores, a la unidad de control, a los accesos al registro PC o a la unidad de extensión de signo.

Es posible convertir en una fórmula la discusión anterior sobre la ganancia de la segmentación. Si las etapas están perfectamente equilibradas, entonces el tiempo entre instrucciones en el procesador segmentado —suponiendo condiciones ideales— es igual a Tiempo entre instrucciones no segmentado Tiempo entre instrucciones segmentado = ---------------------------------------------------------------------------------------Número de etapas de segmentación En condiciones ideales y con un gran número de instrucciones, la ganancia debida a la segmentación es aproximadamente igual al número de etapas; un pipeline de cinco etapas es casi cinco veces más rápido. La fórmula sugiere que con cinco etapas se debe mejorar en cinco veces el tiempo de 800 ps que proporciona el esquema no segmentado, es decir, lograr un ciclo de reloj de 160 ps. Sin embargo, el ejemplo muestra que las etapas pueden estar equilibradas de manera imperfecta. Además, la segmentación implica algún gasto o sobrecarga adicional, cuya fuente será presentada con más claridad en breve. Así, el tiempo por instrucción en el procesador segmentado excederá el mínimo valor posible, y la ganancia será menor que el número de etapas de segmentación.

333

RESPUESTA

334

Capítulo 4

El procesador

Orden de ejecución Tiempo del programa (en instrucciones) lw $1, 100($0)

200

Búsqueda Reg

lw $2, 200($0)

400

600

Acceso al dato

ALU

800

1000

1200

1400

ALU

Acceso al dato

1600

1800

Reg

800 ps

Búsqueda Reg

lw $3, 300($0)

800 ps

Reg Búsqueda

800 ps Orden de ejecución Tiempo del programa (en instrucciones) lw $1, 100($0)

200

400

600

Búsqueda

Reg

lw $2, 200($0) 200 ps

Búsqueda

Reg

200 ps

Búsqueda

lw $3, 300($0)

ALU

800

Acceso al dato ALU Reg

1000

1200

1400

Reg Acceso al dato ALU

Reg Acceso al dato

Reg

200 ps 200 ps 200 ps 200 ps 200 ps

FIGURA 4.27 Arriba: ejecución monociclo sin segmentar; abajo: ejecución segmentada. Ambos esquemas usan los mismos componentes hardware, cuyo tiempo se lista en la figura 4.26. En este caso se aprecia una ganancia de 4 en el tiempo promedio entre instrucciones, desde 800 ps hasta 200 ps. Comparar esta figura con la figura 4.25. En el caso de la lavandería, se suponía que todas las etapas eran iguales. Si la secadora hubiera sido el elemento más lento, entonces hubiera determinado el tiempo de cada etapa. Los segmentos del computador están limitados a operar a la velocidad del más lento, bien sea la operación de la ALU o el acceso a memoria. Se supondrá que la escritura en el banco de registros ocurre durante la primera mitad del ciclo de reloj mientras que las lecturas del banco de registros ocurren durante la segunda mitad. Se usará esta suposición a lo largo de todo el capítulo.

Incluso nuestra afirmación de que en el ejemplo se mejoran cuatro veces los resultados no queda reflejada en el tiempo total de ejecución para las tres instrucciones: 1400 ps frente a 2400 ps. Por supuesto, esto es debido a que el número de instrucciones analizado es muy pequeño. ¿Qué ocurriría si se incrementara el número de instrucciones? Podemos extender los números del ejemplo previo a 1.000.003 instrucciones. Añadiendo 1.000.000 instrucciones en el ejemplo segmentado se suman 200 ps por instrucción al tiempo total de ejecución. El tiempo total de ejecución sería de 1.000.000 × 200 ps + 1400 ps, ó 200.001.400 ps. En el ejemplo no segmentado, añadiríamos 1.000.000 instrucciones, cada una con una duración de 800 ps, de modo que el tiempo total de ejecución sería 1.000.000 × 800 ps + 2400 ps, ó 800.002.400 ps. En estas condiciones ideales, la razón entre los tiempos totales de ejecución para programas reales en procesadores no segmentados comparados con los tiempos en procesadores segmentados es cercana a la razón de los tiempos entre instrucciones: 800.002.400 ps 800 ps ------------------------------------- ~ --------------- ~ 4,00 200.001.400 ps 200 ps

4.5

Descripción general de la segmentación

335

La segmentación mejora las prestaciones incrementando la productividad (throughput) de las instrucciones, en lugar de disminuir el tiempo de ejecución de cada instrucción individual, pero la productividad de las instrucciones es la métrica importante, ya que los programas reales ejecutan miles de millones de instrucciones.

Diseño de repertorios de instrucciones para la segmentación Incluso con esta explicación sencilla de la segmentación es posible comprender el diseño del repertorio de instrucciones del MIPS, diseñado expresamente para la ejecución segmentada. En primer lugar, todas las instrucciones MIPS tienen la misma longitud. Esta restricción hace que sea mucho más fácil la búsqueda de instrucciones en la primera etapa de la segmentación y la descodificación en la segunda etapa. En un repertorio de instrucciones como el x86, donde las instrucciones varían de 1 byte a 17 bytes, la segmentación es un reto considerable. Las implementaciones recientes de la arquitectura x86 en realidad convierten las instrucciones x86 en microoperaciones simples muy parecidas a instrucciones MIPS. Y se segmenta la ejecución de las microoperaciones en lugar de las instrucciones x86 nativas (véase sección 4.10). Segundo, MIPS tiene sólo unos pocos formatos de instrucciones, y además en todos ellos los campos de los registros fuentes están situados siempre en la misma posición de la instrucción. Esta simetría implica que la segunda etapa pueda empezar a leer del banco de registros al mismo tiempo que el hardware está determinando el tipo de la instrucción que se ha leído. Si los formatos de instrucciones del MIPS no fueran simétricos, sería necesario partir la segunda etapa, dando como resultado un pipeline de seis etapas. En breve se indicarán la desventaja de los pipelines más largos. En tercer lugar, en el MIPS los operandos a memoria sólo aparecen en instrucciones de carga y almacenamiento. Esta restricción hace que se pueda usar la etapa de ejecución para calcular la dirección de memoria y en la siguiente etapa se pueda acceder a memoria. Si se permitiera usar operandos en memoria en todas las operaciones, como hace la arquitectura x86, las etapas 3 y 4 se extenderían a una etapa de cálculo de dirección, una etapa de acceso a memoria, y luego una etapa de ejecución. Y cuarto, como se discutió en el capítulo 2, los operandos deben estar alineados en memoria. Por lo tanto, no hay que preocuparse de que una instrucción de transferencia de datos necesite dos accesos a memoria para acceder a un solo dato; el dato pedido siempre podrá ser transferido entre procesador y memoria en una única etapa.

Riesgos del pipeline Hay situaciones de segmentación en las que la instrucción siguiente no se puede ejecutar en el ciclo siguiente. Estos sucesos se llaman riesgos (hazards) y los hay de tres tipos diferentes Riesgos estructurales

El primer riesgo se denomina riesgo estructural (structural hazard). Significa que el hardware no admite una cierta combinación de instrucciones durante el mismo ciclo. Un riesgo estructural en la lavandería ocurriría si se usara una combinación lavadora-secadora en lugar de tener la lavadora y la secadora separadas,

Riesgo estructural: caso en el que una instrucción en curso no se puede ejecutar en el ciclo de reloj adecuado porque el hardware no admite la combinación de instrucciones que se pretenden ejecutar en ese ciclo de reloj.

336

Capítulo 4

El procesador

o si nuestro compañero de piso estuviera ocupado y no pudiera guardar la ropa. De esta forma, se frustraría la tan cuidadosamente planeada segmentación. Como se ha mencionado con anterioridad, el repertorio de instrucciones MIPS fue diseñado para ser segmentado, por lo que facilita a los diseñadores evitar riesgos estructurales. Supongamos, sin embargo, que se tuviera un solo banco de memoria en lugar de dos. Si el pipeline de la figura 4.27 tuviera una cuarta instrucción, se vería que en un mismo ciclo la primera instrucción está accediendo a datos de memoria mientras que la cuarta está buscando una instrucción de la misma memoria. Sin disponer de dos bancos de memoria, nuestra segmentación podría tener riesgos estructurales. Riesgos de datos Riesgo de datos: (riesgo de datos en el pipeline): caso en el que la instrucción en curso no se puede ejecutar en el ciclo de reloj adecuado porque el dato necesario para ejecutar la instrucción no está todavía disponible

Los riesgos de datos (data hazards), ocurren cuando el pipeline se debe bloquear debido a que un paso de ejecución debe esperar a que otro paso sea completado. Volviendo a la lavandería, supongamos que se está doblando una colada y no se encuentra la pareja de un cierto calcetín. Una estrategia posible es ir al piso a buscar en todos los armarios hasta encontrar la pareja. Obviamente, mientras se busca deberán esperar todas las coladas secas y preparadas para ser dobladas, y todas las coladas limpias y preparadas para ser secadas. En un pipeline del computador, los riesgos de datos surgen de las dependencias entre una instrucción y otra anterior que se encuentra todavía en el pipeline (una relación que en realidad no se da en la lavandería). Por ejemplo, suponer que se tiene una instrucción add seguida inmediatamente por una instrucción sub que usa el resultado de la suma ($s0): add sub

Anticipación de resultados (realimentación): método de resolver un riesgo de datos que consiste en obtener el dato buscado de los búfer internos en lugar de esperar a que llegue a los registros visibles a nivel de programación o a la memoria.

$s0, $t0, $t1 $t2, $s0, $t3

Si no se interviene, un riesgo de datos puede bloquear severamente al procesador. La instrucción add no escribe su resultado hasta la quinta etapa, por lo que se tendrían que añadir tres burbujas en el procesador. Aunque se podría confiar que los compiladores eliminaran todos esos riesgos, los resultados no serían satisfactorios. Estas dependencias ocurren demasiado a menudo y el retardo es demasiado largo para esperar que el compilador nos salve de este problema. La principal solución se basa en la observación de que no es necesario esperar a que la instrucción se complete antes de intentar resolver el riesgo de datos. Para la secuencia de código anterior, tan pronto como la ALU calcula la suma para la instrucción add, se puede suministrar el resultado como entrada de la resta. Se denomina anticipación de resultados (forwarding) o realimentación (bypassing) al uso de hardware extra para anticipar antes el dato buscado usando los recursos internos del procesador. Anticipar datos entre dos instrucciones

EJEMPLO

Para las dos instrucciones anteriores, mostrar qué etapas del pipeline estarían conectadas por el mecanismo de anticipación de resultados. Usar el dibujo de la figura 4.28 para representar el camino de datos durante las cinco etapas del pipeline. Alinear una copia del camino de datos para cada instrucción, de forma similar a como se hizo en la figura 4.25 para el pipeline de la lavandería.

4.5

200

Tiempo add $s0, $t0, $t1

IF

Descripción general de la segmentación

400

600

EX

ID

800

337

1000

WB

MEM

FIGURA 4.28 Representación gráfica del pipeline de instrucciones, similar en espíritu a la segmentación de la lavandería de la figura 4.25. Para representar los recursos físicos se usan símbolos con las abreviaciones de las etapas del pipeline usadas a lo largo del capítulo. Los símbolos para las cinco etapas son: IF para la etapa de búsqueda de instrucciones (instruction fetch), con la caja que representa la memoria de instrucciones; ID para la etapa de descodificación de instrucciones y de lectura del banco de registros (instruction decode), con el dibujo que muestra la lectura al banco de registros; EX para la etapa de ejecución de instrucciones (instruction execution), con el dibujo que representa la ALU; MEM para la etapa de acceso a memoria (memory access), con la caja que representa la memoria de datos; y WB para la etapa de escritura de resultado (write back), con el dibujo que muestra la escritura en el banco de registros. El sombreado del dibujo indica el elemento que se usa por parte de la instrucción. De este modo, MEM tiene un fondo blanco porque la instrucción add no accede a la memoria de datos. El sombreado en la mitad derecha del banco de registros o de la memoria significa que el elemento se lee en esa etapa, y el sombreado en la mitad izquierda del banco de registros significa que se escribe en esa etapa. Por tanto, la mitad derecha de ID está sombreada en la segunda etapa porque se lee el banco de registros, y la mitad izquierda de WB está sombreada en la quinta etapa porque se escribe en el banco de registros.

La figura 4.29 muestra la conexión para anticipar el valor de $s0 después de la etapa de ejecución de la instrucción add a la entrada de la etapa de ejecución de la instrucción sub. Orden de ejecución del programa Tiempo (en instrucciones) add $s0, $t0, $t1

sub $t2, $s0, $t3

200

IF

400

600

800

ID

EX

MEM

IF

ID

EX

1000

WB

MEM

WB

FIGURA 4.29 Representation gráfica de la anticipación de datos. La conexión muestra el camino de anticipación de datos desde la salida de la etapa EX de add hasta la entrada de la etapa EX de sub, reemplazando el valor del registro $s0 leído en la segunda etapa de la instrucción sub.

En esta representación gráfica de los sucesos, las líneas de anticipación de datos sólo son válidas si la etapa destino está más adelante en el tiempo que la etapa origen. Por ejemplo, no puede establecerse una línea válida de anticipación desde la salida de la etapa de acceso al dato de la primera instrucción hasta la entrada de la etapa de ejecución de la siguiente, puesto que eso significaría ir hacia atrás en el tiempo. La anticipación funciona muy bien y será descrita en detalle en la sección 4.7. De todos modos, no es capaz de evitar todos los bloqueos. Por ejemplo, suponer que la primera instrucción fuera una carga del registro $s0, en lugar de una suma. Como se puede deducir a partir de una mirada a la figura 4.29, el dato deseado sólo estaría dis-

RESPUESTA

338

Capítulo 4

El procesador

Orden de ejecución del programa Tiempo (en instrucciones) lw $s0, 20($t1)

sub $t2, $s0, $t3

200

IF

400

800

600

ID

EX

Burbuja

Burbuja

IF

MEM

Burbuja

ID

1000

1200

1400

WB

Burbuja

EX

Burbuja

MEM

WB

FIGURA 4.30 El bloqueo es necesario incluso con la anticipación de resultados cuando una instrucción de Formato R intenta usar el dato de una instrucción de carga precedente. Sin el bloqueo, la ruta desde la salida de la etapa de acceso a memoria hasta la entrada de la etapa de ejecución sería hacia atrás en el tiempo, lo cual es imposible. Esta figura es en realidad una simplificación, pues hasta después de que la instrucción de resta es buscada y descodificada no se puede conocer si será o no será necesario un bloqueo. La sección 4.7 muestra los detalles de lo que sucede en realidad en el caso de los riesgos.

Riesgo del dato de una carga: forma específica de riesgo en la que el dato de una instrucción de carga no está aún disponible cuando es pedido por otra instrucción.

Bloqueo del pipeline (burbuja): bloqueo iniciado para resolver un riesgo.

ponible después de la cuarta etapa de la primera instrucción, lo cual es demasiado tarde para la entrada de la etapa EX de la instrucción sub. Por lo tanto, incluso con la anticipación de resultados, habría que bloquear durante una etapa en el caso del riesgo del dato de una carga (load-use data hazard), tal como se puede ver en la figura 4.30. Esta figura muestra un importante concepto sobre la segmentación, oficialmente denominado un bloqueo del pipeline (pipeline stall), pero que frecuentemente recibe el apodo de burbuja (bubble). Se verán bloqueos en otros lugares del pipeline. La sección 4.7 muestra cómo se pueden tratar casos difíciles como éstos, bien con un hardware de detección y con bloqueos, bien por software, que reordena el código para evitar los bloqueos debido a riesgos de datos de una carga (load-use), como se ilustra en el siguiente ejemplo.

Reordenación de código para evitar bloqueos del pipeline

EJEMPLO

Considere el siguiente segmento de código en lenguaje C: a = b + e; c = b + f;

Aquí se muestra el código MIPS generado para este segmento, suponiendo que todas las variables están en memoria y son direccionables como desplazamientos a partir de $t0:

4.5

lw lw add sw lw add sw

$t1, $t2, $t3, $t3, $t4, $t5, $t5,

Descripción general de la segmentación

339

0($t0) 4($t0) $t1,$t2 12($t0) 8($01) $t1,$t4 16($t0)

Encuentre los riesgos que existen en el segmento de código y reordene las instrucciones para evitar todos los bloqueos del pipeline. Las dos instrucciones add tienen un riesgo debido a sus respectivas dependencias con la instrucción lw que les precede inmediatamente. Observe que la anticipación de datos elimina muchos otros riesgos potenciales, incluidos la dependencia del primer add con el primer lw y los riesgos con las instrucciones store. Mover hacia arriba la tercera instrucción lw elimina ambos riesgos. lw lw lw add sw add sw

$t1, $t2, $t4, $t3, $t3, $t5, $t5,

RESPUESTA

0($t0) 4($t1) 8($01) $t1,$t2 12($t0) $t1,$t4 16($t0)

En un procesador segmentado con anticipación de resultados, la secuencia reordenada se completará en dos ciclos menos que la versión original. La anticipación de resultados conlleva otra característica que hace comprender la arquitectura MIPS, además de las cuatro mencionadas en la página 335. Cada instrucción MIPS escribe como mucho un resultado y lo hace cerca del final del pipeline. La anticipación de resultados es más complicada si hay múltiples resultados que avanzar por cada instrucción, o si se necesita escribir el resultado antes del final de la ejecución de la instrucción. Extensión: El nombre “anticipación de resultado” (forwarding) viene de la idea de que el resultado se pasa hacia adelante, de una instrucción anterior a una instrucción posterior. El término “realimentación” (bypassing) viene del hecho de pasar el resultado a la unidad funcional deseada sin tener que copiarlo previamente en el banco de registros.

Riesgos de control

El tercer tipo de riesgo se llama riesgo de control (control hazard) y surge de la necesidad de tomar una decisión basada en los resultados de una instrucción mientras las otras se están ejecutando. Supongamos que los trabajadores de nuestra lavandería tienen la feliz tarea de lavar los uniformes de un equipo de fútbol. Dependiendo de lo sucia que esté la colada, se necesitará determinar si el detergente y la temperatura del agua

Riesgo de control (riesgo de saltos): caso en el que la instrucción en curso no se puede ejecutar en el ciclo de reloj adecuado porque la instrucción que ha sido buscada no es la que se requería; esto es, el flujo de direcciones de instrucciones no es el que el pipeline esperaba.

340

Capítulo 4

El procesador

seleccionadas son suficientemente fuertes para lavar los uniformes, pero no tan fuertes como para que éstos se desgasten y se rompan pronto. En nuestra lavandería segmentada, se tiene que esperar a la segunda etapa de la segmentación para examinar el uniforme seco y comprobar si se tiene que cambiar algunos de los parámetros del lavado. ¿Qué se debe hacer? A continuación se presenta la primera de dos soluciones posibles a los riesgos de control en la lavandería y sus equivalentes para los computadores. Bloquear (stall): Operar de manera secuencial hasta que la primera carga esté seca, y entonces repetir hasta que se consiga la fórmula correcta. Esta opción conservadora realmente funciona, pero es lenta. En un computador, la tarea de decisión equivalente es la instrucción de salto (branch). Observe que se debe comenzar a buscar la instrucción que sigue a un salto justo en el siguiente ciclo de reloj. Pero el pipeline puede no conocer cuál es la siguiente instrucción, ya que ¡justo acaba de obtener la instrucción de salto de la memoria! Igual que con la lavandería, una posible solución es bloquear inmediatamente después de haber ido a buscar un salto, y esperar a que el pipeline determine el resultado del salto y conozca cuál es la instrucción que se debe ir a buscar. Supongamos que se emplea suficiente hardware adicional para poder examinar los registros, calcular la dirección del salto y actualizar el registro PC durante la segunda etapa del pipeline (véase la sección 4.8 para más detalles). Incluso con este hardware adicional, el pipeline que trata los saltos condicionales se parecería al de la figura 4.31. La instrucción lw, ejecutada si el salto no se toma, se bloquea un ciclo extra de 200 ps antes de empezar.

Orden de ejecución Tiempo del programa (en instrucciones)

200

add $4, $5, $6 beq $1, $2, 40

400

Reg

200 ps

600

ALU

Búsqueda

Reg

800

Acceso al dato ALU

1000

1200

1400

Reg Acceso al dato

Reg

burbuja burbuja burbuja burbuja burbuja lw $3, 300($0) 400 ps

Búsqueda

Reg

ALU

Acceso al dato

Reg

FIGURA 4.31 Pipeline que muestra el bloqueo en cada salto condicional como solución a los riesgos de control. En este ejemplo se supone que el salto condicional es tomado y que la instrucción destino del salto es la instrucción “OR”. Se produce un bloqueo en el pipeline de una etapa, o burbuja, después del salto. En realidad, el proceso de crear el bloqueo es ligeramente más complicado, tal y como se verá en la sección 4.8. El efecto en las prestaciones, en cambio, sí que es el mismo que se daría si realmente se insertara una burbuja.

4.5

Descripción general de la segmentación

341

Rendimiento de “bloquear los saltos”

Estimar el impacto en ciclos de reloj por instrucción (CPI) de bloqueo en los saltos. Suponer que todas las otras instrucciones tienen un CPI de 1. La figura 3.27 del capítulo 3 muestra que los saltos condicionales representan el 17% de las instrucciones ejecutadas en SPECint2006. Dado que la ejecución del resto de las instrucciones tiene un CPI de 1 y los saltos necesitan un ciclo de reloj adicional debido al bloqueo, entonces se observará un CPI de 1.17 y por lo tanto una desaceleración de 1.17 respecto al caso ideal.

EJEMPLO RESPUESTA

Si el salto no se puede resolver en la segunda etapa, como ocurre frecuentemente en pipelines más largos, bloquearse ante la presencia de un salto supondrá una mayor disminución del rendimiento. El coste de esta opción es demasiado alto para la mayoría de los computadores y motiva una segunda solución para los riesgos de control: Predicción: Si se está bastante seguro de que se tiene la fórmula correcta para lavar los uniformes, entonces basta con predecir que funcionará bien y lavar la segunda colada mientras se espera que la primera se seque. Cuando se acierta, el rendimiento de la segmentación no se reduce. Sin embargo, cuando se falla, se tiene que rehacer la colada que fue lavada mientras se comprobaba si la decisión era correcta. Los computadores realmente también usan la predicción para tratar los saltos. Una estrategia simple es predecir que siempre se dará un salto no tomado. Cuando se acierta, el pipeline funciona a máxima velocidad. El procesador sólo se bloquea cuando los saltos son tomados. La figura 4.32 muestra este ejemplo. Una versión más sofisticada de la predicción de saltos (branch prediction) supondría predecir que algunos saltos saltan (se toman) y que otros no saltan (no se toman). En nuestra analogía, los uniformes oscuros o los de casa podrían necesitar una fórmula y los claros o de calle otra fórmula. En los computadores, los saltos que cierran un lazo saltan hacia atrás hasta el principio del lazo. Ya que probablemente estos saltos serán tomados y saltan hacia atrás, se podría predecir como tomados aquellos saltos cuya dirección destino es una dirección anterior a la de la instrucción de salto. Este tipo de enfoque tan rígido de la predicción de saltos se basa en un comportamiento estereotipado y no tiene en cuenta las características individuales de cada instrucción de salto específica. Los predictores dinámicos realizados en hardware, en claro contraste con los explicados anteriormente, hacen sus predicciones dependiendo del comportamiento de cada salto y pueden cambiar las predicciones para un mismo salto a lo largo de la ejecución de un programa. Siguiendo nuestra analogía, usando predicción dinámica una persona miraría cómo está de sucio el uniforme e intentaría adivinar la fórmula, ajustando la siguiente predicción dependiendo del acierto de las últimas predicciones hechas.

Predicción de saltos: método de resolver los riesgos de saltos que presupone un determinado resultado para el salto y procede a partir de esta suposición, en lugar de esperar a que se establezca el resultado real.

342

Capítulo 4

El procesador

Orden de ejecución Tiempo del programa (en instrucciones) add $4, $5, $6 beq $1, $2, 40

200

lw $3, 300($0)

Orden de ejecución Tiempo del programa (en instrucciones) add $4, $5, $6 beq $1, $2, 40

Reg

Búsqueda

200 ps

200 ps

600

Reg

200 ps

Búsqueda

400

Reg

800

Acceso al dato

ALU

Búsqueda

200

Búsqueda

400

ALU

Reg

Búsqueda

Reg

ALU

1400

Reg Acceso al dato

ALU

800

Acceso al dato

1200

Reg Acceso al dato

ALU

600

1000

1000

Reg

1200

1400

Reg Acceso al dato

Reg

burbuja burbuja burbuja burbuja burbuja or $7, $8, $9 400 ps

Búsqueda

Reg

ALU

Acceso al dato

Reg

FIGURA 4.32 Predecir que los saltos son no tomados como solución a los riesgos de control. El dibujo de la parte superior muestra el pipeline cuando el salto es no tomado. El dibujo de la parte inferior muestra el pipeline cuando el salto es tomado. Tal como se mostraba en la figura 4.31, la insercción de una burbuja de esta manera simplifica lo que sucede en realidad, al menos durante el primer ciclo de reloj que sigue de forma inmediata al salto. La sección 4.8 revelará los detalles.

Un enfoque muy utilizado en la predicción dinámica de saltos en computadores es mantener una historia para cada salto que indique si ha sido tomado o no tomado, y entonces usar el pasado reciente para predecir el futuro. Tal como se verá más tarde, la cantidad y tipo de historia almacenada ha ido creciendo, con el resultado de que los predictores dinámicos de saltos pueden predecir saltos correctamente con alrededor del 90% de precisión (véase sección 4.8). Cuando la predicción es incorrecta, el control del procesador segmentado debe asegurar que las instrucciones que siguen al salto mal predicho no tienen efecto y debe reinicializar el pipeline desde la dirección correcta de salto. En nuestra analogía de la lavandería, se deben detener las coladas para reiniciar la colada que fue incorrectamente predicha. Como en el caso de las otras soluciones a los riesgos de control, los pipelines largos agravan el problema, en este caso elevando el coste del fallo de predicción. En la sección 4.8 se describen con más detalle las soluciones a los riesgos de control. Extensión: Hay un tercer enfoque para los riesgos de control, llamado decisión retardada (delayed decision) mencionado anteriormente. En nuestra analogía, cada vez que se vaya a tomar una decisión acerca del lavado se pone en la lavadora ropa que no sea de

4.5

Descripción general de la segmentación

343

fútbol, mientras se espera a que los uniformes de fútbol se sequen. Mientras se tenga suficiente ropa sucia que no se vea afectada por el test, esta solución funciona bien. En los computadores este enfoque se llama salto retardado (delayed branch), y es la solución que se usa en la arquitectura MIPS. El salto retardado siempre ejecuta la siguiente instrucción secuencial, mientras que el salto se ejecuta después del retardo de una instrucción. Esta funcionalidad es oculta al programador de lenguaje ensamblador MIPS, ya que el ensamblador arregla el código automáticamente para que el comportamiento de los saltos sea el que desea el programador. El ensamblador de MIPS pondrá inmediatamente después del salto retardado una instrucción que no dependa del salto, y si el salto es tomado la dirección que se cambiará es la de la instrucción que sigue a esta instrucción no dependiente (que es seguro que siempre se va a ejecutar). En el ejemplo, la instrucción add que está antes del salto en la figura 4.31 no afecta al salto, y puede moverse después del salto para ocultar completamente su retardo. Como los saltos retardados son útiles cuando los saltos son cortos, ningún procesador usa un salto retardado de más de 1 ciclo. Para retardos mayores de los saltos, generalmente se usa la predicción basada en hardware.

Resumen de la visión general del pipeline La segmentación es una técnica que explota el paralelismo entre las instrucciones de un flujo secuencial. A diferencia de otras técnicas para incrementar la velocidad del procesador, tiene la ventaja sustancial de ser fundamentalmente invisible al programador. En las siguientes secciones de este capítulo se analiza el concepto de segmentación utilizando el subconjunto del repertorio de instrucciones MIPS ya utilizado en la implementación de ciclo único de la sección 4.4 y una versión simplificada de su pipeline. Se estudiarán los problemas introducidos por la segmentación y el rendimiento que se puede conseguir en diversas situaciones típicas. Si el lector desea centrarse más en las implicaciones que suponen la segmentación para el software y para las prestaciones, después de acabar esta sección tendrá suficientes conocimientos básicos como para pasar a la sección 4.10, que introduce conceptos avanzados de la segmentación, como son los procesadores superescalares y la planificación dinámica y la sección 4.11 examina el pipeline de microprocesadores recientes. Alternativamente, si se está interesado en comprender cómo se implementa el pipeline y los retos de manejar los riesgos, se puede comenzar a examinar el diseño de un camino de datos segmentado y del control básico, que se explican en la sección 4.6. Se debería ser capaz de usar este conocimiento para analizar la implementación de la anticipación de datos y los bloqueos en la sección 4.7. Se puede leer la sección 4.8 para aprender más sobre soluciones para los riesgos de saltos, y después leer en la sección 4.9 cómo se gestionan las excepciones. Para cada una de las secuencias de código mostradas abajo, afirmar si se deben producir bloqueos, si se pueden evitar los bloqueos usando solamente la anticipación de resultados, o si se pueden ejecutar sin bloquear y sin avanzar resultados: Secuencia 1

Secuencia 2

Secuencia 3

lw add

add addi addi

addi addi addi addi addi

$t0,0($t0) $t1,$t0,$t0

$t1,$t0,$t0 $t2,$t0,#5 $t4,$t1,#5

$t1,$t0,#1 $t2,$t0,#2 $t3,$t0,#2 $t3,$t0,#4 $t5,$t0,#5

Autoevaluación

344

Capítulo 4

El procesador

Comprender Aparte del sistema de memoria, el funcionamiento efectivo del pipeline es generallas prestaciones mente el factor más importante para determinar el CPI del procesador, y por tanto sus de los programas prestaciones. Tal como veremos en la sección 4.10, comprender las prestaciones de un

procesador segmentado actual con ejecución múltiple de instrucciones es complejo y requiere comprender muchas más cosas de las que surgen del análisis de un procesador segmentado sencillo. De todos modos, los riesgos estructurales, de datos y de control mantienen su importancia tanto en los pipelines sencillos como en los más sofisticados. En los pipelines actuales, los riesgos estructurales generalmente involucran a la unidad de punto flotante, que no puede ser completamente segmentada, mientras que los riesgos de control son generalmente un problema grande en los programas enteros, que tienden a tener una alta frecuencia de instrucciones de salto además de saltos menos predecibles. Los riesgos de datos pueden llegar a ser cuellos de botella para las prestaciones tanto en programas enteros como de punto flotante. Con frecuencia es más fácil tratar con los riesgos de datos en los programas de punto flotante, porque la menor frecuencia de saltos y un patrón de accesos a memoria más regular facilitan que el compilador pueda planificar la ejecución de las instrucciones para evitar los riesgos. Es más difícil realizar esas optimizaciones con programas enteros que tienen patrones de accesos a memoria menos regulares y que involucran el uso de apuntadores en más ocasiones. Tal como se verá en la sección 4.10, existen técnicas más ambiciosas, tanto por parte del compilador como del hardware, para reducir las dependencias de datos mendiante la planificación de la ejecución de las instrucciones.

IDEA clave Latencia (del pipeline): número de etapas en un pipeline, o el número de etapas entre dos instrucciones durante la ejecución.

La segmentación incrementa el número de instrucciones que se están ejecutando a la vez y la rapidez con que las instrucciones empiezan y acaban. La segmentación no reduce el tiempo que se tarda en completar una instrucción individual, también denominada la latencia (latency). Por ejemplo, el pipeline de cinco etapas todavía necesita que las instrucciones tarden 5 ciclos para ser completadas. Según los términos usados en el capítulo 4, la segmentación mejora la productividad (throughput) de instrucciones en vez del tiempo de ejecución o latencia de cada instrucción. Los repertorios de instrucciones pueden tanto simplificar como dificultar la tarea de los diseñadores de procesadores segmentados, los cuales tienen ya que hacer frente a los riesgos estructurales, de control y de datos. La predicción de saltos, la anticipación de resultados y los bloqueos ayudan a hacer un computador más rápido y que aún siga produciendo las respuestas correctas.

Aquí hay menos de lo que el ojo puede ver. Tallulah Bankhead, observación a Alexander Wollcott, 1922

4.6

Camino de datos segmentados y control de la segmentación

4.6

La figura 4.33 muestra el camino de datos monociclo de la sección 4.4 con las etapas de segmentación identificadas. La división de una instrucción en cinco pasos implica

4.6

345

Camino de datos segmentados y control de la segmentación

IF: Búsqueda de instrucciones

ID: Descodificación/ lectura del banco de registros

EX: Ejecución/ cálculo de direcciones

MEM: Acceso a memoria

WB: Escritura del resultado

Sumador 4

Sumador Resultado

Despl. 2 izq.

0 M u x 1

PC

Dirección

Instrucción Memoria de instrucciones

Leer registro 1

Dato leído 1

Leer registro 2 Registros Escribir Dato registro leído 2 Escribir registro

16

0 M u x 1

Cero ALU resultado ALU

Dirección

Dato leído Memoria de datos

1 M u x 0

Escribir dato

Extensión de signo

32

FIGURA 4.33 Camino de datos monociclo extraído de la sección 4.4 (similar al de la figura 4.17). Cada paso de la instrucción se puede situar en el camino de datos de izquierda a derecha. Las únicas excepciones a esta regla son la actualización del registro PC y el paso final de escritura de resultados, mostrados en color. En este último caso, el resultado de la ALU o de la memoria de datos se envía hacia la izquierda para ser escritos en el banco de registros. (Aunque normalmente se utilizan las líneas de color para representar líneas de control, en este caso representan líneas de datos.)

una segmentación de cinco etapas, lo que a su vez significa que durante un ciclo de reloj se estarán ejecutando hasta cinco instrucciones. Por tanto el camino de datos se debe dividir en cinco partes, y cada una de ellas se nombrará haciéndola corresponder con un paso de la ejecución de la instrucción: 1. IF: Búsqueda de instrucciones 2. ID: Descodificación de instrucciones y lectura del banco de registros 3. EX: Ejecución de la instrucción o cálculo de dirección 4. MEM: Acceso a la memoria de datos 5. WB: Escritura del resultado (write back) En la figura 4.33 estas cinco etapas quedan identificadas aproximadamente por la forma como se dibuja el camino de datos. En general, a medida que se va completando la ejecución, las instrucciones y los datos se mueven de izquierda a derecha a través de las cinco etapas. Volviendo a la analogía de la lavandería, la ropa se lava, se seca y se dobla mientras se mueve a través de la línea de segmentación, y nunca vuelve hacia atrás.

346

Capítulo 4

El procesador

Sin embargo, hay dos excepciones a este movimiento de izquierda a derecha de las instrucciones: ■ La etapa de escritura de resultados, que pone el resultado en el banco de registros que está situado más atrás, a mitad del camino de datos. ■ La selección del siguiente valor del PC, que se elige entre el PC incrementado y la dirección de salto obtenida al final de la etapa MEM. El flujo de datos de derecha a izquierda no afecta a la instrucción actual. Estos movimientos de datos hacia atrás sólo influyen a las instrucciones que entran al pipeline después de la instrucción en curso. Obsérvese que la primera flecha de derecha a izquierda puede dar lugar a riesgos de datos y la segunda flecha puede dar lugar a riesgos de control. Una manera de mostrar lo que ocurre en la ejecución segmentada es suponer que cada instrucción tiene su propio camino de datos, y entonces colocar estas rutas de datos en una misma línea de tiempo para mostrar su relación. La figura 4.34 muestra la ejecución de las instrucciones de la figura 4.27 representando en una línea de tiempo común sus rutas de datos particulares. Para mostrar estas relaciones, la figura 4.33 usa una versión estilizada del camino de datos que se había mostrado en la figura 4.34. Tiempo (en ciclos de reloj) Orden de ejecución del programa (en instrucciones) lw $1, 100($0)

lw $2, 200($0)

lw $3, 300($0)

CC 1

CC 2

IM

Reg

IM

CC 3

ALU

Reg

IM

CC 4

CC 5

DM

Reg

ALU

DM

Reg

ALU

DM

Reg

CC 6

CC7

Reg

FIGURA 4.34 Ejecución de instrucciones usando el camino de datos monociclo de la figura 4.33, suponiendo que la ejecución está segmentada. De un modo similar a como se hace en las figuras 4.28 a 4.30, esta figura supone que cada instrucción tiene su propio camino de datos, y cada parte del camino de datos está sombreado según su uso. A diferencia de las otras figuras, ahora cada etapa está etiquetada con el recurso físico usado en esa etapa, y que corresponde con la parte del camino de datos mostrado en la figura 4.33. IM representa tanto a la memoria de instrucciones como al registro PC de la etapa de búsqueda de instrucciones, Reg representa el banco de registros y la unidad de extensión de signo en la etapa de descodificación de instrucciones/lectura del banco de registros (ID), y así todas las demás etapas. Para mantener correctamente el orden temporal, este camino de datos estilizado divide el banco de registros en dos partes lógicas: la lectura de registros durante la etapa ID y la escritura en registro durante la etapa WB. Para representar este doble uso, en la etapa ID se dibuja la mitad izquierda del banco de registros sin sombrear y con líneas discontinuas, ya que no está siendo escrito, mientras que en la etapa WB es la mitad derecha del banco de registros la que se dibuja sin sombrear y con líneas discontinuas, ya que no está siendo leído. Igual que se ha hecho con anterioridad, se supone que la escritura en el banco de registros se hace en la primera mitad del ciclo y la lectura durante la segunda mitad.

4.6

347

Camino de datos segmentados y control de la segmentación

La figura 4.34 puede parecer sugerir que tres instrucciones necesitan tres rutas de datos. Así, se añadieron registros para almacenar datos intermedios y permitir así compartir partes del camino de datos durante la ejecución de las instrucciones. Por ejemplo, tal como muestra la figura 4.34, la memoria de instrucciones sólo se usa durante una de las cinco etapas de una instrucción, permitiendo que sea compartida con otras instrucciones durante las cuatro etapas restantes. Para poder conservar el valor de cada instrucción individual durante las cuatro últimas etapas del pipeline, el valor leído de la memoria de instrucciones se debe guardar en un registro. Aplicando argumentos similares a cada una de las etapas del pipeline, se puede razonar la necesidad de colocar registros en todas las líneas entre etapas de segmentación que se muestran en la figura 4.33. Volviendo a la analogía de la lavandería, se debería tener una cesta entre cada una de las etapas para mantener la ropa producida por una etapa y que debe ser usada por la siguiente etapa. La figura 4.35 muestra el camino de datos segmentado en la que se han resaltado los registros de segmentación. Durante cada ciclo de reloj todas las instrucciones avanzan de un registro de segmentación al siguiente. Los registros tienen el nombre de las dos etapas que separan. Por ejemplo, el registro de segmentación entre las etapas IF e ID se llama IF/ID.

IF/ID

ID/EX

EX/MEM

MEM/WB

Sumador

Sumador

4

Resultado

0 M u x 1

PC

Dirección

Memoria de instrucciones

Instrucción

Despl. 2 izq.

Leer registro 1

Dato leído 1 Leer registro 2 Registro Dato Escribir leído 2 registro Escribir dato

0 M u x 1

Cero ALU resultado ALU

Dirección Memoria de datos

Dato leído

0 M u x 1

Escribir dato

16

Extensión 32 de signo

FIGURA 4.35 Versión segmentada del camino de datos de la figura 4.33. Los registros de segmentación, en color, separan cada una de las etapas. Están etiquetados con las etapas que separan; por ejemplo, el primero está etiquetado como IF/ID ya que separa las etapas de búsqueda de instrucciones y de descodificación. Los registros han de ser suficientemente anchos para almacenar todos los datos que corresponden con las líneas que pasan a través de ellos. Por ejemplo, el registro IF/ID debe ser de 64 bits de ancho, ya que debe guardar tanto los 32 bits de la instrucción leída de memoria como los 32 de la dirección obtenida del PC e incrementada. Aunque a lo largo de este capítulo estos registros se ampliarán, de momento supondremos que los otros tres registros de segmentación contienen 128, 97 y 64 bits, respectivamente.

Obsérvese que no hay registro de segmentación al final de la etapa de escritura de resultado. Todas las instrucciones deben actualizar algún estado de la máquina –el banco de registros, la memoria o el registro PC– por lo que sería redundante usar un

348

Capítulo 4

El procesador

registro de segmentación específico para un estado que ya se está actualizando. Por ejemplo, una instrucción de carga guarda su resultado en uno de los 32 registros generales, y cualquier instrucción posterior que necesite el dato puede leerlo directamente de ese registro concreto. Todas las instrucciones actualizan el registro PC, bien sea incrementando su valor o bien modificando su valor con la dirección destino de un salto. El registro PC se puede considerar un registro de segmentación: el que se utiliza para alimentar la etapa IF del pipeline. Sin embargo, a diferencia de los registros de segmentación mostrados de forma sombreada en la figura 4.35, el registro PC forma parte del estado visible de la arquitectura; es decir, su contenido debe ser guardado cuando ocurre una excepción, mientras que el contenido del resto de registros de segmentación puede ser descartado. En la analogía de la lavandería, se puede considerar que el PC corresponde con la cesta que contiene la carga inicial de ropa sucia antes de la etapa de lavado. Para mostrar el funcionamiento de la segmentación, durante este capítulo se usarán secuencias de figuras que ilustran la operación sobre el pipeline a lo largo del tiempo. Quizás parezca que se requiere mucho tiempo para comprender estas páginas adicionales. No se debe tener ningún temor: sólo se trata de comparar las secuencias entre sí para identificar los cambios que ocurren en cada ciclo de reloj y ello requiere menos esfuerzo de comprensión del que podría parecer. La sección 4.7 describe lo que ocurre cuando se producen riesgos de datos entre las instrucciones segmentadas, así que de momento pueden ser ignorados. Las figuras 4.36 a 4.38, que suponen la primera secuencia, muestran resaltadas las partes activas del pipeline a medida que una instrucción de carga avanza a través de las cinco etapas de la ejecución segmentada. Se muestra en primer lugar una instrucción de carga porque está activa en cada una de las cinco etapas. Igual que en las figuras 4.28 a 4.30, se resalta la mitad derecha del banco de registros o de la memoria cuando están siendo leídos y se resalta la mitad izquierda cuando están siendo escritos. En cada figura se muestra la abreviación de la instrucción, lw, junto con el nombre de la etapa que está activa. Las cinco etapas son las siguientes: 1. Búsqueda de instrucción: La parte superior de la figura 4.36 muestra cómo se lee la instrucción de memoria usando la dirección del PC y después se coloca en el registro de segmentación IF/ID. La dirección del PC se incrementa en 4 y se escribe de nuevo en el PC para prepararse para el siguiente ciclo de reloj. Esta dirección incrementada se guarda también en el registro IF/ID por si alguna instrucción, como por ejemplo beq, la necesita con posterioridad. El computador no puede conocer el tipo de instrucción que se está buscando hasta que ésta es descodificada, por lo que debe estar preparado ante cualquier posible instrucción, pasando la información que sea potencialmente necesaria a lo largo del pipeline. 2. Descodificación de instrucción y lectura del banco de registros: La parte inferior de la figura 4.36 muestra la parte del registro de segmentación IF/ID donde está guardada la instrucción. Este registro proporciona el campo inmediato de 16 bits, que es extendido a 32 bits con signo, y los dos identificadores de los registros que se deben leer. Los tres valores se guardan, junto con la dirección del PC incrementada, en el registro ID/EX. Una vez más se transfiere todo lo que pueda necesitar cualquier instrucción durante los ciclos posteriores.

4.6

349

Camino de datos segmentados y control de la segmentación

lw Búsqueda de la instrucción

IF/ID

ID/EX

EX/MEM

Sumador 4

PC

Sumador Resultado

Despl. 2 izq. Instrucción

0 M u x 1

Dirección

Memoria de instrucciones

MEM/WB

Leer registro 1

Dato leído 1 Leer registro 2 Registro Dato Escribir leído 2 registro

0 M u x 1

Escribir dato

Cero ALU resultado ALU

Dirección

Dato leído

0 M u x 1

Memoria de datos

Escribir dato

16

Extensión 32 de signo

lw Descodificación de la instrucción

IF/ID

ID/EX

EX/MEM

Sumador

Sumador Resultado

4

PC

Dirección

Memoria de instrucciones

Instrucción

Despl. 2 izq. 0 M u x 1

MEM/WB

Leer registro 1

Dato leído 1 Leer registro 2 Registro Dato Escribir leído 2 registro Escribir dato

0 M u x 1

Cero ALU resultado ALU

Dirección Memoria de datos

Dato leído

0 M u x 1

Escribir dato

16

Extensión 32 de signo

FIGURA 4.36 IF e ID: primera y segunda etapa de segmentación de una instrucción de carga, resaltando las partes del camino de datos de la figura 4.35 que se usan en estas dos etapas. La convención para resaltar los elementos del camino de datos es la misma que se usó en la figura 4.28. Igual que en la sección 4.2, no hay confusión al leer y escribir registros porque el contenido de éstos cambia sólo con la transición de la señal de reloj. Aunque la instrucción de carga en la etapa 2 sólo necesita el registro de arriba, el procesador no sabe qué instrucción se está descodificando, así que extiende el signo de la constante de 16 bits obtenida de la instrucción y lee ambos registros de entrada y los tres valores se almacenan sobre el registro de segmentación ID/EX. Seguro que no se necesitan los tres operandos, pero disponer de los tres simplifica el control.

350

Capítulo 4

El procesador

lw Ejecución

IF/ID

ID/EX

EX/MEM

Sumador

Sumador Resultado

4

PC

Dirección

Memoria de instrucciones

Instrucción

Despl. 2 izq. 0 M u x 1

MEM/WB

Leer registro 1

Dato leído 1 Leer registro 2 Registro Dato Escribir leído 2 registro Escribir dato

0 M u x 1

Cero ALU resultado ALU

Dirección Memoria de datos

Dato leído

0 M u x 1

Escribir dato

16

Extensión 32 de signo

FIGURA 4.37 EX: tercera etapa de la segmentación de una instrucción de carga, resaltando las partes del camino de datos de la figura 4.35 que se usan en esta etapa. El registro se suma al valor inmediato con el signo extendido, y el resultado de la suma se coloca en el registro de segmentación EX/MEM.

3. Ejecución o cálculo de dirección: La figura 4.38 muestra que la instrucción de carga lee del registro IF/ID el contenido del registro 1 y el valor inmediato con el signo extendido, y los suma usando la ALU. El resultado de esta suma se coloca en el registro de segmentación EX/MEM. 4. Acceso a memoria: La parte superior de la figura 4.38 muestra la instrucción de carga cuando usa la dirección obtenida del registro EX/MEM para leer un dato de memoria y después guardar el dato leído en el registro de segmentación MEM/WB. 5. Escritura de resultado: La parte inferior de la figura 4.38 muestra el paso final: la lectura del resultado guardado en el registro MEM/WB y la escritura de este resultado en el banco de registros mostrado en el centro de la figura. Este recorrido de la instrucción de carga indica que toda la información que se pueda necesitar en etapas posteriores del pipeline se debe pasar a cada etapa mediante los registros de segmentación. El recorrido de una instrucción de almacenamiento es similar en la manera de ejecutarse y en la manera de pasar la información a las etapas posteriores del pipeline. A continuación se muestran las cinco etapas de un almacenamiento: 1. Búsqueda de la instrucción: Se lee la instrucción de la memoria usando la dirección del PC y se guarda en el registro IF/ID. Esta etapa ocurre antes de

4.6

351

Camino de datos segmentados y control de la segmentación

lw Memoria

IF/ID

ID/EX

EX/MEM

Sumador

Sumador Resultado

4

PC

Instrucción

Despl. 2 izq. 0 M u x 1

Dirección

Memoria de instrucciones

MEM/WB

Leer registro 1

Dato leído 1 Leer registro 2 Registro Dato Escribir leído 2 registro

0 M u x 1

Escribir dato

Cero ALU resultado ALU

Dirección

Dato leído

0 M u x 1

Memoria de datos

Escribir dato

16

Extensión de signo

32

lw Escritura de resultado IF/ID

ID/EX

EX/MEM

Sumador

Sumador Resultado

4

PC

Dirección

Memoria de instrucciones

Instrucción

Despl. 2 izq. 0 M u x 1

MEM/WB

Leer registro 1

Dato leído 1 Leer registro 2 Registro Dato Escribir leído 2 registro

0 M u x 1

Escribir dato

Cero ALU resultado ALU

Dirección Memoria de datos

Dato leído

0 M u x 1

Escribir dato

16

Extensión de signo

32

FIGURA 4.38 MEM y WB: cuarta y quinta etapas de la segmentación de una instrucción de carga, resaltando las partes del camino de datos de la figura 4.35 que se usan en esta etapa. Se lee la memoria de datos usando la dirección contenida en el registro EX/MEM, y el dato leído se guarda en el registro de segmentación MEM/WB. A continuación, este dato, guardado en el registro de segmentación MEM/WB, se escribe en el banco de registros, que se encuentra en el medio del camino de datos. Nota: hay un error en este diseño que se corrige en la figura 4.41.

352

Capítulo 4

El procesador

que se identifique la instrucción, por lo que la parte superior de la figura 4.36 sirve igual tanto para cargas como para almacenamientos. 2. Descodificación de instrucción y lectura del banco de registros: La instrucción guardada en el registro IF/ID proporciona los identificadores de los dos registros que deben ser leídos y proporciona el valor inmediato de 16 bits cuyo signo ha de ser extendido. Estos tres valores de 32 bits se guardan en el registro de segmentación ID/EX. La parte inferior de la figura 4.36 para las instrucciones de carga también muestra las operaciones a realizar en la segunda etapa de las instrucciones de almacenamiento. En realidad, estos dos primeros pasos se ejecutan siempre igual para todas las instrucciones, ya que es demasiado pronto para que se conozca el tipo de la instrucción 3. Ejecución o cálculo de dirección: La figura 4.39 muestra el tercer paso; la dirección efectiva se coloca en el registro de segmentación EX/MEM. 4. Acceso a memoria: La parte superior de la figura 4.40 muestra el dato que se está escribiendo en memoria. Observe que el registro que contiene el dato que se tiene que guardar en memoria fue leído en una etapa anterior y guardado en el registro ID/EX. La única manera de hacer que el dato esté disponible durante la etapa MEM es que se coloque en el registro de segmentación EX/MEM durante la etapa EX, de la misma manera que también se ha guardado la dirección efectiva. 5. Escritura de resultado: La parte inferior de la figura 4.40 muestra el paso final del almacenamiento. En esta etapa no ocurre nada para esta instrucción. Puesto que todas las instrucciones posteriores al almacenamiento ya están en progreso, no hay manera de acelerarlas aprovechando que la instrucción de almacenamiento no tiene nada que hacer. En general, las instrucciones pasan a través de todas las etapas aunque en ellas no tengan nada que hacer, ya que las instrucciones posteriores ya están progresando a la máxima velocidad. La instrucción de almacenamiento ilustra una vez más que, para pasar datos de una etapa del pipeline a otra posterior, la información se debe colocar en un registro de segmentación; si no se hiciera así, la información se perdería cada vez que llegase la siguiente instrucción. Para ejecutar el almacenamiento se ha necesitado pasar uno de los registros leídos en la etapa ID a la etapa MEM, donde se guarda en memoria. El dato se ha tenido que guardar primero en el registro de segmentación ID/EX y después se ha tenido que pasar al registro de segmentación EX/MEM. Las cargas y almacenamientos ilustran un segundo punto importante: cada componente lógico del camino de datos —como la memoria de instrucciones, los puertos de lectura del banco de registros, la ALU, la memoria de datos y el puerto de escritura en el banco de registros— pueden usarse solamente dentro de una única etapa de la segmentación. De otra forma se tendría un riesgo estructural (véase la página 335). Por lo tanto, estos componentes y su control pueden asociarse a una sola etapa de la segmentación. En este momento ya se puede destapar un error en el diseño de la instrucción de carga. ¿Lo ha descubierto? ¿Qué registro se modifica en la última etapa de la carga? Más específicamente, ¿qué instrucción proporciona el identificador del registro de escritura? La instrucción guardada en el registro de segmentación

4.6

353

Camino de datos segmentados y control de la segmentación

sw Ejecución

IF/ID

ID/EX

EX/MEM

Sumador

Sumador Resultado

4

PC

Dirección

Memoria de instrucciones

Instrucción

Despl. 2 izq. 0 M u x 1

MEM/WB

Leer registro 1

Dato leído 1 Leer registro 2 Registro Dato Escribir leído 2 registro Escribir dato

0

MM uu xx 11

Cero ALU resultado ALU

Dirección Memoria de datos

Dato leído

0 M u x 1

Escribir dato

16

Extensión 32 de signo

FIGURA 4.39 EX: tercera etapa de la segmentación de una instrucción de almacenamiento. A diferencia de la tercera etapa de la instrucción de carga en la figura 4.37, el valor del segundo registro es cargado en el registro de segmentación EX/MEM para ser usado en la siguiente etapa. Aunque no importaría mucho si siempre se escribiera este segundo registro en el registro de segmentación EX/MEM, para hacer el pipeline más fácil de entender sólo se escribirá en el caso de las instrucciones store.

IF/ID proporciona el número del registro sobre el que se va a escribir, ¡pero en el orden de ejecución, esta instrucción es bastante posterior a la carga que debe hacer la escritura! Por lo tanto, para la instrucción de carga es necesario conservar el identificador del registro destino. Así como el almacenamiento ha pasado el contenido del registro que se tenía que guardar en memoria desde el registro de segmentación ID/EX al registro EX/MEM para poder ser usado posteriormente en la etapa MEM, también la carga debe pasar el identificador de registro destino desde ID/EX a través de EX/MEM y hasta MEM/WB para que pueda ser usado correctamente en la etapa WB. Otra manera de interpretar el paso del número del registro es que, para poder compartir el camino de datos segmentado, es necesario preservar la información de la instrucción que se ha leído en la etapa IF, de modo que cada registro de segmentación contiene las partes de la instrucción necesarias tanto para esa etapa como para las siguientes. La figura 4.41 muestra la versión correcta del camino de datos, en la que se pasa el número del registro de escritura primero al registro ID/EX, después al EX/MEM y finalmente al MEM/WB. Este identificador del registro se usa durante la etapa WB para especificar el registro sobre el que se debe escribir. La figura 4.42 representa el camino de datos correcto en un solo dibujo, resaltando el hardware que se usa en cada una de las cinco etapas de la carga de las figuras 4.36 a 4.38. La explicación de cómo lograr que la instrucción de salto funcione tal y como se espera se dará en la sección 4.8.

354

Capítulo 4

El procesador

sw Memoria

IF/ID

ID/EX

EX/MEM

Sumador

Sumador Resultado

4

PC

Instrucción

Despl. 2 izq. 0 M u x 1

Dirección

Memoria de instrucciones

MEM/WB

Leer registro 1

Dato leído 1 Leer registro 2 Registro Dato Escribir leído 2 registro

0 M u x 1

Escribir dato

Cero ALU resultado ALU

Dirección

Dato leído

0 M u x 1

Memoria de datos

Escribir dato

16

Extensión de signo

32

sw Escritura de resultado IF/ID

ID/EX

EX/MEM

Sumador 4

PC

Sumador Resultado

Despl. 2 izq.

Dirección

Memoria de instrucciones

Instrucción

0 M u x 1

MEM/WB

Leer registro 1

Dato leído 1 Leer registro 2 Registro Dato Escribir leído 2 registro Escribir dato

0 M u x 1

Cero ALU resultado ALU

Dirección Memoria de datos

Dato leído

0 M u x 1

Escribir dato

16

Extensión 32 de signo

FIGURA 4.40 MEM y WB: cuarta y quinta etapas en la ejecución de un almacenamiento. En la cuarta etapa, el dato se escribe en la memoria de datos. Observe que el dato viene del registro de segmentación EX/MEM y que no se cambia nada en el registro de segmentación MEM/WB. Una vez escrito el dato en memoria, no hay nada más que hacer para el almacenamiento, por lo que en la etapa 5 no ocurre nada.

4.6

IF/ID

ID/EX

EX/MEM

Sumador

Instrucción

Despl. 2 izq.

PC

Dirección

Memoria de instrucciones

MEM/WB

Sumador Resultado

4

0 M u x 1

355

Camino de datos segmentados y control de la segmentación

Leer registro 1

Dato leído 1 Leer registro 2 Registro Dato Escribir leído 2 registro

0 M u x 1

Escribir dato

Cero ALU resultado ALU

Dirección

Dato leído

0 M u x 1

Memoria de datos

Escribir dato

16

Extensión 32 de signo

FIGURA 4.41 Camino de datos correctamente modificado para gestionar debidamente la instrucción de carga. Ahora el identificador del registro de escritura viene, junto con el dato a escribir, del registro segmentado MEM/WB. Este identificador se pasa desde la etapa ID hasta que llega al registro de segmentación MEM/WB, añadiendo 5 bits más a los tres últimos registros de segmentación. Este nuevo camino se muestra coloreado.

IF/ID

ID/EX

EX/MEM

Sumador

Sumador Resultado

4

PC

Dirección

Memoria de instrucciones

Instrucción

Despl. 2 izq. 0 M u x 1

MEM/WB

Leer registro 1

Dato leído 1 Leer registro 2 Registro Dato Escribir leído 2 registro Escribir dato

0 M u x 1

Cero ALU resultado ALU

Dirección Memoria de datos

Dato leído

0 M u x 1

Escribir dato

16

FIGURA 4.42 de carga.

Extensión 32 de signo

Porción del camino de datos de la figura 4.41 que es usada por las cinco etapas de la instrucción

356

Capítulo 4

El procesador

Representación gráfica de la segmentación La segmentación puede ser difícil de entender, ya que en cada ciclo de reloj hay varias instrucciones ejecutándose simultáneamente en la mismo camino de datos. Para ayudar a entenderla, el pipeline se dibuja usando dos estilos básicos: diagramas multiciclo de segmentación, como el de la figura 4.34 de la página 346, y diagramas monociclo de segmentación, como los de las figuras 4.36 a 4.40. Los diagramas multiciclo son más simple, pero no contienen todos los detalles. Por ejemplo, consideremos la siguiente secuencia de cinco instrucciones: lw

$10, 20($1)

sub

$11, $2, $3

add

$12, $3, $4

lw

$13, 24($1)

add

$14, $5, $6

La figura 4.43 muestra el diagrama multiciclo de segmentación para estas instrucciones. El tiempo avanza de izquierda a derecha a lo largo de la página y las instrucciones avanzan desde la parte superior a la parte inferior de la página, de una forma parecida a como se representaba la segmentación de la lavandería de la figura 4.25. En cada fila del eje de las instrucciones, a lo largo de él, se coloca una representación de las etapas del pipeline, ocupando los ciclos que sean necesarios. Estas rutas de datos estilizadas representan las cinco etapas de nuestro pipeline, pero un rectángulo con el nombre de cada etapa funciona igual de bien. La figura 4.44 muestra la versión más tradicional del diagrama multiciclo de la segmentación. Debe notarse que la figura 4.43 muestra los recursos físicos usados en cada etapa, mientras que la figura 4.44 emplea el nombre de cada etapa. Los diagramas monociclo muestran el estado del camino de datos completo durante un ciclo de reloj, y las cinco instrucciones que se encuentran en las cinco etapas diferentes del pipeline normalmente se identifican con etiquetas encima de las respectivas etapas. Se usa este tipo de figura para mostrar con más detalle lo que está ocurriendo durante cada ciclo de reloj dentro del pipeline. Habitualmente los diagramas se agrupan para mostrar las operaciones de la segmentación a lo largo de una secuencia de ciclos. Usaremos los diagramas multiciclo para dar visiones generales de las distintas circunstancias de la segmentación. (Si se quieren ver más detalles de la figura 4.43, en la sección 4.12 se pueden encontrar más diagramas monociclo). Un diagrama monociclo representa un corte vertical de un diagrama multiciclo, mostrando la utilización del camino de datos por cada una de las instrucciones que se encuentran en el pipeline durante un determinado ciclo de reloj. Por ejemplo, la figura 4.45 muestra el diagrama monociclo que corresponde al quinto ciclo de las figuras 4.43 y 4.44. Obviamente, los diagramas monociclo incluyen más detalles y requieren un espacio significativamente mayor para mostrar lo que ocurre durante un cierto número de ciclos. Los ejercicios le pedirán que cree estos diagramas para otras secuencias de código.

4.6

357

Camino de datos segmentados y control de la segmentación

Tiempo (en ciclos de reloj) CC 1 CC 2 CC 3

CC 4

CC 5

DM

Reg

CC 6

CC 7

CC 8

CC 9

Orden de ejecución del programa (en instrucciones) lw $10, 20($1)

sub $11, $2, $3

add $12, $3, $4

IM

Reg

IM

ALU

Reg

IM

lw $13, 24($1)

ALU

Reg

IM

add $14, $5, $6

DM

ALU

Reg

IM

Reg

DM

ALU

Reg

Reg

DM

ALU

Reg

DM

Reg

FIGURA 4.43 Diagrama multiciclo de la segmentación de cinco instrucciones. Este estilo de representación del pipeline muestra la ejecución completa de las instrucciones en una sola figura. La relación de instrucciones se hace en orden de ejecución desde la parte superior a la inferior, y los ciclos de reloj avanzan de izquierda a derecha. Al contrario de la figura 4.28, aquí se muestran los registros de segmentación entre cada etapa. La figura 4.44 muestra la manera tradicional de dibujar este diagrama.

Tiempo (en ciclos de reloj) CC 1 CC 2 CC 3

CC 4

CC 5

Acceso a datos

Escritura de resultado

CC 6

CC 7

CC 8

CC 9

Orden de ejecución del programa (en instrucciones) lw $10, 20($1) sub $11, $2, $3 add $12, $3, $4 lw $13, 24($1) add $14, $5, $6

Búsqueda de Descodificación Ejecución instrucciones instrucciones

Búsqueda de Descodificación instrucciones instrucciones Ejecución

Acceso a datos

Búsqueda de Descodificación Ejecución instrucciones instrucciones

Escritura de resultado Acceso a datos

Búsqueda de Descodificación instrucciones instrucciones Ejecución

Escritura de resultado Acceso a datos

Búsqueda de Descodificación instrucciones instrucciones Ejecución

Escritura de resultado Acceso a datos

Escritura de resultado

FIGURA 4.44 Versión tradicional del diagrama multiciclo de la segmentación de cinco instrucciones que se muestra en la figura 4.43.

358

Capítulo 4

El procesador

add $14, $5, $6

lw $13, 24 ($1)

add $12, $3, $4, $11

sub $11, $2, $3

lw$10, 20($1)

Búsqueda instrucciones

Descodificación instrucciones

Ejecución

Memoria

Escritura de resultado

IF/ID

ID/EX

EX/MEM

Sumador 4

PC

Sumador Resultado

Despl. 2 izq.

Dirección

Memoria de instrucciones

Instrucción

0 M u x 1

MEM/WB

Leer registro 1

Dato leído 1 Leer registro 2 Registro Dato Escribir leído 2 registro

0 M u x 1

Escribir dato

Cero ALU resultado ALU

Dirección Memoria de datos

Dato leído

0 M u x 1

Escribir dato

16

Extensión de signo

32

FIGURA 4.45 Diagrama monociclo correspondiente al ciclo 5 del pipeline de las figuras 4.43 y 4.44. Como puede verse, la figura monociclo es una porción vertical del diagrama multiciclo.

Autoevaluación

Un grupo de estudiantes estaba debatiendo sobre la eficiencia de un pipeline de cinco etapas, cuando uno de ellos se dio cuenta de que no todas las instrucciones están activas en cada una de las etapas del pipeline. Después de decidir ignorar el efecto de los riesgos, los estudiantes hicieron las siguientes afirmaciones. ¿Cuáles son correctas? 1. Permitiendo que las instrucciones de salto condicional e incondicional y que las instrucciones que usan la ALU tarden menos ciclos que los cinco requeridos por la instrucción de carga incrementará las prestaciones en todos los casos. 2. Intentar que algunas instrucciones tarden menos ciclos en el pipeline no ayuda, ya que la productividad viene determinada por el ciclo de reloj; el número de etapas del pipeline que requiere cada instrucción afecta a la latencia y no a la productividad. 3. No se puede hacer que las instrucciones que usan la ALU tarden menos ciclos debido a la escritura del resultado final, pero las instrucciones de salto condicional e incondicional sí que pueden tardar menos ciclos, y por tanto hay alguna oportunidad de mejorar. 4. En lugar de tratar que las instrucciones tarden menos ciclos, deberíamos explorar la posibilidad de hacer que el pipeline fuera más largo, de forma que las instrucciones tardaran más ciclos, pero que los ciclos fueran más cortos. Esto podría mejorar las prestaciones.

4.6

359

Camino de datos segmentados y control de la segmentación

Control de la segmentación

En el Computador 6600, quizás aún más que en cualquier computador anterior, es el sistema de control el que marca la diferencia.

Del mismo modo que en la sección 4.3 añadimos el control a un camino de datos de ciclo único, ahora añadiremos el control a un camino de datos segmentado. Comenzaremos con un diseño simple en el que el problema se verá a través de una gafas con cristales de color rosa. En las secciones 4.7 a 4.9, se prescindirá de estas gafas para desvelar así los riesgos presentes en el mundo real. El primer paso consiste en etiquetar las líneas de control en el nuevo camino de datos. La figura 4.46 muestra estas líneas. El control del camino de datos sencillo de la figura 4.17 se ha reutilizado lo máximo posible. En concreto, se usa la misma lógica de control para la ALU, la misma lógica de control para los saltos, el mismo multiplexor para los identificadores de registro destino, y las mismas líneas de control. Estas funciones se definieron en las figuras 4.12, 4.16 y 4.18. Para que el texto que sigue sea más fácil de seguir, las figuras 4.47 a 4.48 reproducen la misma información clave.

James Thornton, Design of a Computer: The Control Data 6600, 1970

PCSrc

IF/ID

ID/EX

EX/MEM

Sumador

Sumador Resultado

4 Despl. 2 izq.

Branch

RegWrite PC

Dirección

Memoria de instrucciones

Instrucción

0 M u x 1

MEM/WB

Leer registro 1 Leer registro 2 Registro Escribir registro

Dato leído 1

MemWrite ALUSrc

Dato leído 2

0 M u x 1

Escribir dato

Cero ALU resultado ALU

MemtoReg Dirección

Dato leído

Memoria de datos

1 M u x 0

Escribir dato Instrucción (15–0)

Instrucción (20–16)

Instrucción (15–11)

16

Extensión de signo

32

6

0 M u x 1

Control ALU

MemRead

ALUOp

RegDst

FIGURA 4.46 El camino de datos segmentado de la figura 4.41 en la que se identifican las señales de control. Este camino de datos toma prestada la lógica de control de la sección 4.4 para el PC, el identificador de registro fuente y destino, y el control de la ALU. Debe notarse que en la etapa EX, como entrada de control de la ALU, son ahora necesarios los 6 bits del campo funct (código de función) de la instrucción, y por tanto estos bits también deben ser incluidos en el registro de segmentación ID/EX. Recuerde que ya que estos 6 bits también pueden representar los 6 bits menos significativos del campo inmediato de la instrucción, el registro de segmentación ID/EX los proporciona como parte del campo inmediato, pues la extensión del signo mantiene el valor de los bits.

360

Código de operación

Capítulo 4

ALUOp

El procesador

Operación

Código Función

Entrada Control ALU

Acción deseada en ALU

LW

00

cargar palabra

XXXXXX

sumar

0010

SW

00

almacenar palabra

XXXXXX

sumar

0010

Saltar si igual

01

saltar si igual

XXXXXX

restar

0110

tipo R

10

sumar

100000

sumar

0010

tipo R

10

restar

100010

restar

0110

tipo R

10

AND

100100

Y-lógica

0000

tipo R

10

OR

100101

O-lógica

0001

tipo R

10

iniciar si menor que

101010

iniciar si menor que

0111

FIGURA 4.47 Copia de la figura 4.12. Esta figura muestra cómo activar los bits de control de la ALU dependiendo de los bits de control de ALUOp y de los diferentes códigos de función de las instrucciones de tipo R.

Nombre de señal

Efecto cuando desactiva (0)

Efecto cuando activa (1)

RegDst

El identificador del registro destino para la escritura a registro viene del campo rt (bits 20:16).

El identificador del registro destino para la escritura a registro viene del campo rd (bits 15:11).

RegWrite

Ninguno.

El registro se escribe con el valor de escritura.

ALUSrc

El segundo operando de la ALU proviene del El segundo operando de la ALU son los 16 bits de menor peso de segundo registro leído del banco de registros. la instrucción con el signo extendido.

PCSrc

El PC es reemplazado por su valor anterior más 4 (PC + 4).

El PC es reemplazado por la salida del sumador que calcula la dirección destino del salto.

MemRead

Ninguno.

El valor de la posición de memoria designada por la dirección se coloca en la salida de lectura.

MemWrite

Ninguno.

El valor de la posicón de memoria designada por la dirección se reemplaza por el valor de la entrada de datos.

MemtoReg

El valor de entrada del banco de registros proviene de la ALU.

El valor de entrada del banco de registros proviene de la memoria.

FIGURA 4.48 Copia de la figura 4.16. Se define la función de cada una de las siete señales de control. Las líneas de control de la ALU (ALUOp) se definen en la segunda columna de la figura 4.47. Cuando se activa el bit de control de un multiplexor de dos entradas, éste selecciona la entrada correspondiente a 1. En caso contrario, si el control está desactivado, el multiplexor selecciona la entrada 0. Obsérvese que en la figura 4.46, PCSrc se controla mediante una puerta AND. Si la señal Branch y la señal Cero de la ALU están activadas, entonces la señal PCSrc es 1; en caso contrario es 0. El control activa la señal Branch sólo para una instrucción beq; en otro caso PCSrc se pone a 0.

Ejecución / cálculo de dirección líneas de control Instrucción

Reg Dst

Escritura de resultado líneas de control

Acceso a memoria líneas de control

ALU Op1

ALU Op0

ALU Src

Branch

Mem Read

Mem Write

Reg Write

Mem to Reg

Formato R

1

1

0

0

0

0

0

1

0

lw

0

0

0

1

0

1

0

1

1

sw beq

X

0

0

1

0

0

1

0

X

X

0

1

0

1

0

0

0

X

FIGURA 4.49 Los valores de las líneas de control son los mismos que en la figura 4.18, pero han sido distribuidas en tres grupos que corresponden a las tres últimas etapas de la segmentación.

4.6

Camino de datos segmentados y control de la segmentación

Igual que para la implementación monociclo, se supondrá que el PC se escribe en cada ciclo y que no es necesaria una línea separada para controlar la escritura en el PC. Por el mismo motivo, no existen señales de escritura separadas para los registros de segmentación (IF/ID, ID/EX, EX/MEM y MEM/WB), en los cuales también se escribe en cada ciclo de reloj. Para especificar el control en el pipeline sólo se necesita activar los valores de control durante cada etapa de la segmentación. Puesto que cada línea de control se asocia con un componente activo en una única etapa, las líneas de control se pueden dividir en cinco grupos según las etapas de la segmentación: 1. Búsqueda de instrucción: Las señales de control para leer de la memoria de instrucciones y para escribir el PC están siempre activadas, por lo que el control en esta etapa no tiene nada de especial. 2. Descodificación de instrucción y lectura del banco de registros: Aquí pasa lo mismo que en la etapa anterior, por lo que no hay líneas de control opcionales que activar. 3. Ejecución / cálculo de dirección: Las señales a activar son RegDst, ALUOp y ALUSrc (véanse las figuras 4.47 y 4.48). Estas señales seleccionan el registro de resultado, la operación de la ALU, y seleccionan como entrada de la ALU o bien el dato leído del segundo registro o bien el valor inmediato con signo extendido.

WB

Instrucción Control

IF/ID

M

WB

EX

M

WB

ID/EX

EX/MEM

MEM/WB

FIGURA 4.50 Líneas de control para las tres etapas finales. Observe que cuatro de las nueve líneas de control se usan en la etapa EX, mientras que las cinco restantes pasan al registro de segmentación EX/MEM, que ha sido extendido para poder almacenar las líneas de control; tres se usan durante la etapa MEM, y las dos últimas se pasan al registro MEM/WB para ser usadas en la etapa WB.

361

362

Capítulo 4

El procesador

4. Acceso a memoria: Las líneas de control que se activan en esta etapa son Branch, MemRead y MemWrite. Estas señales se activan para las instrucciones beq, load, y store respectivamente. Recuerde que PCSrc en la figura 4.48 selecciona la dirección siguiente en orden secuencial a no ser que la lógica de control active la señal Branch y el resultado de la ALU sea cero. 5. Escritura de resultado: Las dos líneas de control son MemtoReg, la cual decide entre escribir en el banco de registros o bien el resultado de la ALU o bien el valor leído de memoria, y RegWrite, que escribe el valor escogido. Ya que al segmentar el camino de datos no cambia el significado de las líneas de control, se pueden emplear los mismos valores para el control que antes. La figura 4.49 presenta los mismos valores que en la sección 4.4, pero ahora las nueve líneas de control se agrupan por etapas de segmentación.

PCSrc

ID/EX

IF/ID

EX/MEM

M

WB

EX

M

Sumador

Sumador Resultado

4

Dirección

Memoria de instrucciones

Leer registro 1 Leer registro 2 Registro Escribir registro

WB

Branch

ALUSrc MemWrite

RegWrite PC

Instrucción

0 M u x 1

Despl. 2 izq.

MEM/WB

Dato leído 1

Dato leído 2

Cero ALU resultado ALU

0 M u x 1

Escribir dato

MemtoReg

Control

WB

Dato leído

Dirección Memoria de datos

0 M u x 1

Escribir dato Instrucción [15–0]

16

Extensión 32 de signo

6

Instrucción [20–16]

Instrucción [15–11]

0 M u x 1

Control ALU

MemRead

ALUOp

RegDst

FIGURA 4.51 Camino de datos segmentado de la figura 4.40, con las señales de control conectadas a la parte de control de los registros de segmentación. Los valores de control de las tres últimas etapas se crean durante la descodificación de la instrucción y son escritos en el registro de segmentación ID/EX. En cada etapa de segmentación se usan ciertas líneas de control, y las líneas restantes se pasan a la etapa siguiente.

4.7

363

Riesgos de datos: anticipación frente a bloqueos

Realizar el control significa activar las nueve líneas de control a estos valores en cada etapa para cada instrucción. La manera más simple de hacerlo es extendiendo los registros de segmentación para incluir la información de control. Ya que las líneas de control empiezan en la etapa EX, se puede crear la información de control durante la descodificación de la instrucción. La figura 4.50 muestra que estas señales de control se usan en la etapa de segmentación adecuada mientras la instrucción avanza por el pipeline, tal y como avanza el identificador de registro destino de las cargas en la figura 4.41. La figura 4.51 muestra el camino de datos completo con los registros de segmentación extendidos y con las líneas de control conectadas a la etapa correcta. (Si se quieren más detalles, la sección 4.12 tiene más diagramas monociclo con ejemplos de ejecución de códigos MIPS en el pipeline).

4.7

Riesgos de datos: anticipación frente a bloqueos

4.7

Los ejemplos de la sección anterior muestran la potencia de la ejecución segmentada y cómo el hardware realiza esta tarea. Ahora es el momento de quitarse las gafas con cristales de color rosa y mirar qué pasa en programas reales. Las instrucciones de las figuras 4.43 a 4.45 eran independientes; ninguna de ellas usaba los resultados calculados por alguna de las anteriores. En cambio, en la sección 4.5 se vio que los riesgos de datos suponían un obstáculo para la ejecución segmentada. Vamos a ver una secuencia de instrucciones con varias dependencias, mostradas en color. sub

$2, $1,$3

# sub escribe en registro $2

and

$12,$2,$5

# 1er operando($2) depende de sub

or

$13,$6,$2

# 2º operando($2) depende de sub

add

$14,$2,$2

# 1er($2) y 2º($2) depende de sub

sw

$15,100($2)

# Base ($2) depende de sub

Las últimas cuatro instrucciones dependen todas del resultado de la primera instrucción, guardado en el registro $2. Si este registro tenía el valor 10 antes de la instrucción de resta y el valor -20 después, la intención del programador es que el valor 20 sea usado por las instrucciones posteriores que referencian al registro $2. ¿Cómo se ejecutaría esta secuencia de instrucciones en el procesador segmentado? La figura 4.52 ilustra la ejecución de estas instrucciones usando una representación multiciclo. Para mostrar la ejecución de esta secuencia de instrucciones en el pipeline, la parte superior de la figura 4.52 muestra el valor del registro $2, que cambia en la mitad del quinto ciclo, cuando la instrucción sub escribe su resultado. Uno de los riesgos potenciales se puede resolver con el propio diseño del hardware del banco de registros: ¿qué ocurre cuando un registro se lee y se escribe en el mismo ciclo de reloj? Se supone que la escritura se hace en la primera mitad del ciclo y la lectura se hace en la segunda mitad, por lo que la lectura proporciona el valor que acaba de ser escrito. Como esto ya lo hacen muchas implementaciones de bancos de registros, en este caso entenderemos que no hay riesgo de datos.

¿Qué quieres decir, por qué se debería construir? Es un baipás. Debes construir realimentaciones. Douglas Adams, Hitchhikers Guide to the Galaxy, 1979

364

Capítulo 4

El procesador

Tiempo (en ciclos de reloj) CC 1 CC 2 Valor del registro $2:

10

10

IM

Reg

CC 3

CC 4

CC 5

CC 6

CC 7

CC 8

CC 9

10

10

10/–20

–20

–20

–20

–20

DM

Reg

Orden de ejecución del programa (en instrucciones) sub $2, $1, $3

and $12, $2, $5

or $13, $6, $2

add $14, $2, $2

sw $15, 100($2)

IM

Reg

IM

DM

Reg

IM

Reg

DM

Reg

IM

Reg

DM

Reg

Reg

DM

Reg

FIGURA 4.52 Dependencias en la segmentación de la ejecución de la secuencia de cinco instrucciones usando caminos de datos simplificados para mostrar las dependencias. Todas las acciones dependientes se muestran en color y “CC i” en la parte superior de la figura representa el ciclo de reloj i. La primera instrucción escribe en $2, y todas las siguientes instrucciones leen de $2. Este registro se escribe en el ciclo 5, por lo que el valor correcto no está disponible antes del ciclo 5. (La lectura de un registro durante un ciclo de reloj retornará el valor escrito al final de la primera mitad del ciclo, si es que esa escritura se produce). Las líneas coloreadas desde la parte superior del camino de datos a la parte inferior muestran las dependencias. Aquellas que deben ir hacia atrás en el tiempo constituyen los riesgos de datos en el pipeline.

La figura 4.52 muestra que los valores que se leen del registro $2 no representarían el resultado de la instrucción sub a menos que la lectura del registro se hiciera durante el ciclo 5 o después. Las instrucciones que obtendrían el valor correcto de -20 son add y sw; las instrucciones and y or obtendrían el valor 10, que es incorrecto. Al utilizar este estilo de dibujo estos problemas se ven claramente porque una línea de dependencia va hacia atrás en el tiempo. Como se ha mencionado en la sección 4.5, el resultado está disponible al final de la etapa EX, o lo que es lo mismo al final del tercer ciclo. ¿Cuándo se necesita realmente ese dato para las instrucciones and y or? Al principio de la etapa EX, o lo que es lo mismo en los ciclos 4 y 5, respectivamente. Por tanto, podemos ejecutar este segmento sin bloqueos si simplemente anticipamos los datos tan pronto como estén disponibles a cualquiera de las unidades que necesiten el dato antes de que esté disponible en el banco de registros para ser leído. ¿Cómo funciona la anticipación de resultados? Por simplicidad, en el resto de esta sección se considerará la posibilidad de anticipar datos sólo a las operaciones que están en la etapa EX, que pueden ser operaciones de tipo ALU o el cálculo de

4.7

Riesgos de datos: anticipación frente a bloqueos

365

una dirección efectiva. Esto significa que cuando una instrucción trata de usar en su etapa EX el registro que una instrucción anterior intenta escribir en su etapa WB, lo que realmente se necesita es disponer de su valor como entrada a la ALU. Una notación que ponga nombre a los campos de los registros de segmentación permite una descripción más precisa de las dependencias. Por ejemplo, “ID/EX.RegisterRs” se refiere al identificador de un registro cuyo valor se encuentra en el registro de segmentación ID/EX; esto es, el que viene del primer puerto de lectura del banco de registros. La primera parte del nombre, a la izquierda, es el identificador del registro de segmentación; la segunda parte es el nombre del campo de ese registro. Usando esta notación, existen dos parejas de condiciones para detectar riesgos: 1a. EX/MEM.RegisterRd = ID/EX.RegisterRs 1b. EX/MEM.RegisterRd = ID/EX.RegisterRt 2a. MEM/WB.RegisterRd = ID/EX.RegisterRs 2b. MEM/WB.RegisterRd = ID/EX.RegisterRt El primer riesgo en la secuencia de la página 363 se encuentra en el registro $2, entre el resultado de sub $2, $1, $3 y el primer operando de lectura de la instrucción and $12, $2, $5. Este riesgo se puede detectar cuando la instrucción and está en la etapa EX y la otra instrucción está en la etapa MEM, por lo que corresponde con la condición 1a: EX/MEM.RegisterRd = ID/EX.RegisterRs = $2

Detección de dependencias

Clasificar las dependencias en esta secuencia de la página 363: sub and or add sw

$2, $12, $13, $14, $15,

$1, $3 $2, $5 $6, $2 $2, $2 100($2)

# # # # #

Registro $2 escrito por sub 1er operando($2) escrito por sub 2º operando($2) escrito por sub 1er($2) y 2º($2) escrito por sub Índice($2) escrito por sub

Tal como se ha mencionado anteriormente, el riesgo sub-add es de tipo 1a. Los riesgos restantes son: ■

El riesgo sub-or es de tipo 2b: MEM/WB.RegisterRd = ID/EX.RegisterRt = $2



Las dos dependencias en sub-add no son riesgos ya que el banco de registros proporciona el valor correcto durante la etapa ID de add.



No hay riesgo de datos entre sub y sw ya que sw lee $2 un ciclo de reloj después que sub escriba $2.

EJEMPLO

RESPUESTA

366

Capítulo 4

El procesador

Ya que algunas instrucciones no escriben en registros, esta política es poco precisa; algunas veces se anticiparía un valor innecesario. Una solución consiste sencillamente en comprobar si la señal RegWrite estará activa: para ello se examina el campo de control WB del registro de segmentación durante las etapas de EX y MEM. Además, MIPS requiere que cada vez que se use como operando el registro $0, se debe producir un valor para el operando igual a cero. En el caso que una instrucción tenga como destino $0 (por ejemplo, sll $0, $1, $2), se debe evitar la anticipación de un resultado que posiblemente sea diferente de cero. El no anticipar resultados destinados a $0 libera de restricciones al programador de ensamblador y al compilador para usar $0 como registro destino. Por lo tanto, las condiciones mencionadas antes funcionarán correctamente siempre que se añada EX/MEM.RegisterRd | 0 a la primera condición de riesgo y MEM/WB.RegisterRd | 0 a la segunda condición. Una vez detectados los riesgos, la mitad del problema está resuelto, pero todavía falta anticipar el dato correcto. La figura 4.53 muestra las dependencias entre los registros de segmentación y las entradas de la ALU para la misma secuencia de código de la figura 4.52. El cambio radica en que la dependencia empieza en un registro de segmentación en lugar de esperar a que en la etapa WB se escriba en el banco de registros. Por lo tanto, el dato que se tiene que adelantar ya está disponible en los registros de segmentación con tiempo suficiente para las instrucciones posteriores. Si se pudieran obtener las entradas de la ALU de cualquier registro de segmentación en vez de sólo del registro de segmentación ID/EX, entonces se podría adelantar el dato correcto. Bastaría con añadir multiplexores adicionales en la entrada de la ALU, y el control apropiado para poder ejecutar el pipeline a máxima velocidad en presencia de estas dependencias. Por ahora, supondremos que las únicas instrucciones que necesitan avanzar su resultado son las cuatro de tipo R: add, sub, and y or. La figura 4.54 muestra un primer plano de la ALU y de los registros de segmentación antes y después de añadir la anticipación de datos. La figura 4.55 muestra los valores de las líneas de control de los multiplexores de la ALU que seleccionan bien los valores del banco de registros, o bien uno de los valores anticipados. El control de la anticipación estará en la etapa EX, ya que los multiplexores de anticipación previos a la ALU se encuentran en esta etapa. Por lo tanto se deben pasar los identificadores de los registros fuente desde la etapa ID a través del registro de segmentación ID/EX para determinar si se deben adelantar los valores. El campo rt ya se tiene (bits 20-16). Antes de la anticipación, el registro ID/EX no necesitaba incluir espacio para guardar el campo rs. Por lo tanto se debe añadir rs (bits 25-21) al registro ID/EX. Ahora escribiremos tanto las condiciones para detectar riesgos como las señales de control para resolverlos: 1. Riesgo EX: si (EX/MEM.RegWrite y (EX/MEM.RegisterRd y (EX/MEM.RegisterRd si (EX/MEM.RegWrite y (EX/MEM.RegisterRd y (EX/MEM.RegisterRd

 0)

= ID/EX.RegisterRs)) ForwardA = 10  0)

= ID/EX.RegisterRt)) ForwardB = 10

4.7

367

Riesgos de datos: anticipación frente a bloqueos

Tiempo (en ciclos de reloj) Valor del registro $2: Valor de EX/MEM: Valor de MEM/WB:

CC 1

CC 2

CC 3

CC 4

CC 5

CC 6

CC 7

CC 8

CC 9

10 X X

10 X X

10 X X

10 –20 X

10/–20 X –20

–20 X X

–20 X X

–20 X X

–20 X X

IM

Reg

DM

Reg

Orden de ejecución del programa (en instrucciones) sub $2, $1, $3

and $12, $2, $5

or $13, $6, $2

add $14, $2 , $2

sw $15, 100($2)

IM

Reg

IM

DM

Reg

IM

Reg

DM

Reg

IM

Reg

DM

Reg

Reg

DM

Reg

FIGURA 4.53 Las dependencias entre los registros de segmentación se mueven hacia adelante en el tiempo, por lo que es posible proporcionar a la ALU los operandos que necesitan las instrucciones and y or anticipando los resultados que se encuentran en dichos registros. Los valores en los registros de segmentación muestran que el valor deseado está disponible antes de que se escriba en el banco de registros. Se supone que el banco de registros adelanta el valor que se lee y escribe durante el mismo ciclo de reloj, por lo que la instrucción add no se debe bloquear, ya que los valores le vienen desde el banco de registros en vez de desde un registro de segmentación. La anticipación de datos dentro del banco de registros -esto es, la lectura obtiene el valor que se escribe en ese mismo ciclo- explica por qué el ciclo 5 muestra al registro $2 con el valor 1 al principio y con el valor -20 al final del ciclo de reloj. Como en el resto de la sección, consideraremos todos los casos de anticipación de datos excepto para los valores que deben ser guardados en memoria en el caso de las instrucciones de almacenamiento.

Observe que el campo EX/MEM.RegisterRd