Patterson,_hennessy_progetto_e_struttura.pdf

  • Uploaded by: lorenzo
  • 0
  • 0
  • November 2019
  • 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 Patterson,_hennessy_progetto_e_struttura.pdf as PDF for free.

More details

  • Words: 82,493
  • Pages: 656
Operation

Ainvert Binvert a

CarryIn

0

0

1 1 Result b

0



2

1 Less

3

CarryOut

Operation

Ainvert Binvert a

CarryIn

0

0

1 1 Result b

0



2

1 Less

3 Set Overflow detection

Overflow

FIGURE B.5.10 (Top) A 1-bit ALU that performs AND, OR, and addition on a and b or b , and (bottom) a 1-bit ALU for the most significant bit. The top drawing includes a direct input that is connected to perform the set on less than operation (see Figure B.5.11); the bottom has a direct output from the adder for the less than comparison called Set. (See Exercise B.24 at the end of this appendix to see how to calculate overflow with fewer inputs.)

B-34

Appendix B

The Basics of Logic Design

Operation

Binvert Ainvert CarryIn

a0 b0

CarryIn ALU0 Less CarryOut

Result0

a1 b1 0

CarryIn ALU1 Less CarryOut

Result1

a2 b2 0

CarryIn ALU2 Less CarryOut

Result2

.. .

. .. . . .. .. a31 b31 0

.. . CarryIn CarryIn ALU31 Less

.. .

Result31 Set Overflow

FIGURE B.5.11 A 32-bit ALU constructed from the 31 copies of the 1-bit ALU in the top of Figure B.5.10 and one 1-bit ALU in the bottom of that figure. The Less inputs are connected to 0 except for the least significant bit, which is connected to the Set output of the most significant bit. If the ALU performs a ⫺ b and we select the input 3 in the multiplexor in Figure B.5.10, then Result ⫽ 0 … 001 if a ⬍ b, and Result ⫽ 0 … 000 otherwise.

Thus, we need a new 1-bit ALU for the most significant bit that has an extra output bit: the adder output. The bottom drawing of Figure B.5.10 shows the design, with this new adder output line called Set, and used only for slt. As long as we need a special ALU for the most significant bit, we added the overflow detection logic since it is also associated with that bit.

B.1

Introduction

Alas, the test of less than is a little more complicated than just described because of overflow, as we explore in the exercises. Figure B.5.11 shows the 32-bit ALU. Notice that every time we want the ALU to subtract, we set both CarryIn and Binvert to 1. For adds or logical operations, we want both control lines to be 0. We can therefore simplify control of the ALU by combining the CarryIn and Binvert to a single control line called Bnegate. To further tailor the ALU to the MIPS instruction set, we must support conditional branch instructions. These instructions branch either if two registers are equal or if they are unequal. The easiest way to test equality with the ALU is to subtract b from a and then test to see if the result is 0, since (a

b

0) ⇒ a

b

Thus, if we add hardware to test if the result is 0, we can test for equality. The simplest way is to OR all the outputs together and then send that signal through an inverter: Zero

(Result31

Result30



Result2

Result1

Result 0)

Figure B.5.12 shows the revised 32-bit ALU. We can think of the combination of the 1-bit Ainvert line, the 1-bit Binvert line, and the 2-bit Operation lines as 4-bit control lines for the ALU, telling it to perform add, subtract, AND, OR, or set on less than. Figure B.5.13 shows the ALU control lines and the corresponding ALU operation. Finally, now that we have seen what is inside a 32-bit ALU, we will use the universal symbol for a complete ALU, as shown in Figure B.5.14.

Defining the MIPS ALU in Verilog Figure B.5.15 shows how a combinational MIPS ALU might be specified in Verilog; such a specification would probably be compiled using a standard parts library that provided an adder, which could be instantiated. For completeness, we show the ALU control for MIPS in Figure B.5.16, which is used in Chapter 4, where we build a Verilog version of the MIPS datapath. The next question is, “How quickly can this ALU add two 32-bit operands?” We can determine the a and b inputs, but the CarryIn input depends on the operation in the adjacent 1-bit adder. If we trace all the way through the chain of dependencies, we connect the most significant bit to the least significant bit, so the most significant bit of the sum must wait for the sequential evaluation of all 32 1-bit adders. This sequential chain reaction is too slow to be used in time-critical hardware. The next section explores how to speed-up addition. This topic is not crucial to understanding the rest of the appendix and may be skipped.

B-35

B-36

Appendix B

The Basics of Logic Design

Operation

Bnegate Ainvert

a0 b0

CarryIn ALU0 Less CarryOut

a1 b1 0

CarryIn ALU1 Less CarryOut

CarryIn ALU2 Less CarryOut

a2 b2 0

.. .

. .. . .. .. . a31 b31 0

Result0

Result1 .. .

Result2

.. . CarryIn CarryIn ALU31 Less

Zero

.. .

.. .

Result31 Set Overflow

FIGURE B.5.12 The final 32-bit ALU. This adds a Zero detector to Figure B.5.11.

ALU control lines

Function

0000

AND

0001

OR

0010

add

0110

subtract

0111

set on less than

1100

NOR

FIGURE B.5.13 The values of the three ALU control lines, Bnegate, and Operation, and the corresponding ALU operations.

B.5

Constructing a Basic Arithmetic Logic Unit

ALU operation

a Zero ALU

Result Overflow

b

CarryOut FIGURE B.5.14 The symbol commonly used to represent an ALU, as shown in Figure B.5.12. This symbol is also used to represent an adder, so it is normally labeled either with ALU or Adder.

FIGURE B.5.15 A Verilog behavioral definition of a MIPS ALU.

B-37

B-38

Appendix B

The Basics of Logic Design

FIGURE B.5.16 The MIPS ALU control: a simple piece of combinational control logic.

Check Yourself

Suppose you wanted to add the operation NOT (a AND b), called NAND. How could the ALU change to support it? 1. No change. You can calculate NAND quickly using the current ALU since (a b) a b and we already have NOT a, NOT b, and OR. 2. You must expand the big multiplexor to add another input, and then add new logic to calculate NAND.

B.6

Faster Addition: Carry Lookahead

The key to speeding up addition is determining the carry in to the high-order bits sooner. There are a variety of schemes to anticipate the carry so that the worstcase scenario is a function of the log2 of the number of bits in the adder. These anticipatory signals are faster because they go through fewer gates in sequence, but it takes many more gates to anticipate the proper carry. A key to understanding fast-carry schemes is to remember that, unlike soft ware, hardware executes in parallel whenever inputs change.

Fast Carry Using “Infinite” Hardware As we mentioned earlier, any equation can be represented in two levels of logic. Since the only external inputs are the two operands and the CarryIn to the least

B.6

Faster Addition: Carry Lookahead

significant bit of the adder, in theory we could calculate the CarryIn values to all the remaining bits of the adder in just two levels of logic. For example, the CarryIn for bit 2 of the adder is exactly the CarryOut of bit 1, so the formula is CarryIn2

(b1 CarryIn1)

(a1 CarryIn1)

(a1 b1)

(a 0 CarryIn0)

(a 0 b0)

Similarly, CarryIn1 is defined as CarryIn1

(b0 CarryIn0)

Using the shorter and more traditional abbreviation of ci for CarryIni, we can rewrite the formulas as c2 c1

(b1 c1) (a1 c1) (a1 b1) (b0 c0) (a 0 c0) (a 0 b0)

Substituting the definition of c1 for the first equation results in this formula: c2

(a1 a 0 b0) (a1 a 0 c0) (a1 b0 c0) (b1 a 0 b0) (b1 a 0 c0) (b1 b0 c0)

(a1 b1)

You can imagine how the equation expands as we get to higher bits in the adder; it grows rapidly with the number of bits. This complexity is reflected in the cost of the hardware for fast carry, making this simple scheme prohibitively expensive for wide adders.

Fast Carry Using the First Level of Abstraction: Propagate and Generate Most fast-carry schemes limit the complexity of the equations to simplify the hardware, while still making substantial speed improvements over ripple carry. One such scheme is a carry-lookahead adder. In Chapter 1, we said computer systems cope with complexity by using levels of abstraction. A carry-lookahead adder relies on levels of abstraction in its implementation. Let’s factor our original equation as a first step:

ci

1

(bi ci) = (ai bi)

(ai ci) (ai bi) (ai bi) ci

If we were to rewrite the equation for c2 using this formula, we would see some repeated patterns: c2

(a1 b1)

(a1 b1) ((a 0 b0)

(a 0

b0) c0)

Note the repeated appearance of (ai ⭈ bi) and (ai ⫹ bi) in the formula above. These two important factors are traditionally called generate (gi) and propagate (pi):

B-39

B-40

Appendix B

The Basics of Logic Design

gi pi

ai bi ai bi

Using them to define ci ⫹ 1, we get ci

1

gi

pi ci

To see where the signals get their names, suppose gi is 1. Then ci

1

gi

pi ci

1

pi ci

1

That is, the adder generates a CarryOut (ci ⫹ 1) independent of the value of CarryIn (ci). Now suppose that gi is 0 and pi is 1. Then ci

1

gi

pi ci

0

1 ci

ci

That is, the adder propagates CarryIn to a CarryOut. Putting the two together, CarryIni ⫹ 1 is a 1 if either gi is 1 or both pi is 1 and CarryIni is 1. As an analogy, imagine a row of dominoes set on edge. The end domino can be tipped over by pushing one far away, provided there are no gaps between the two. Similarly, a carry out can be made true by a generate far away, provided all the propagates between them are true. Relying on the definitions of propagate and generate as our first level of abstraction, we can express the CarryIn signals more economically. Let’s show it for 4 bits: c1

g0

(p0 c0)

c2

g1

(p1 g 0)

(p1 p0 c0)

c3

g2

(p2 g1)

(p2 p1 g 0)

(p2 p1 p0 c0)

c4

g3

(p3 g 2)

(p3 p2 g1)

(p3 p2 p1 g 0)

p3 p2 p1 p0 c0) (p These equations just represent common sense: CarryIni is a 1 if some earlier adder generates a carry and all intermediary adders propagate a carry. Figure B.6.1 uses plumbing to try to explain carry lookahead. Even this simplified form leads to large equations and, hence, considerable logic even for a 16-bit adder. Let’s try moving to two levels of abstraction.

Fast Carry Using the Second Level of Abstraction First, we consider this 4-bit adder with its carry-lookahead logic as a single building block. If we connect them in ripple carry fashion to form a 16-bit adder, the add will be faster than the original with a little more hardware.

B.6

Faster Addition: Carry Lookahead

To go faster, we’ll need carry lookahead at a higher level. To perform carry look ahead for 4-bit adders, we need to propagate and generate signals at this higher level. Here they are for the four 4-bit adder blocks: P0 P1 P2 P3

p3 p2 p1 p0 p7 p6 p5 p4 p11 p10 p9 p8 p15 p14 p13 p12

That is, the “super” propagate signal for the 4-bit abstraction (Pi) is true only if each of the bits in the group will propagate a carry. For the “super” generate signal (Gi), we care only if there is a carry out of the most significant bit of the 4-bit group. This obviously occurs if generate is true for that most significant bit; it also occurs if an earlier generate is true and all the intermediate propagates, including that of the most significant bit, are also true: G0

g3

(p3 g 2)

(p3 p2 g1)

(p3 p2 p1 g 0)

G1

g7

(p7 g 6)

(p7 p6 g 5)

(p7 p6 p5 g 4)

G2

g11

(p11 g10)

(p11 p10 g 9)

G3

g15

(p15 g14)

(p15 p14 g13)

(p11 p10 p9 g 8) (p15 p14 p13 g12)

Figure B.6.2 updates our plumbing analogy to show P0 and G0. Then the equations at this higher level of abstraction for the carry in for each 4-bit group of the 16-bit adder (C1, C2, C3, C4 in Figure B.6.3) are very similar to the carry out equations for each bit of the 4-bit adder (c1, c2, c3, c4) on page B-40: C1

G0

(P0 c0)

C2

G1

(P1 G0)

(P1 P0 c0)

C3

G2

(P2 G1)

(P2 P1 G0)

C4

G3

(P3 G2) (P3 P2 G1) (P3 P2 P1 G0) (P3 P2 P1 P0 c0)

(P2 P1 P0 c0)

Figure B.6.3 shows 4-bit adders connected with such a carry-lookahead unit. The exercises explore the speed differences between these carry schemes, different notations for multibit propagate and generate signals, and the design of a 64-bit adder.

B-41

B-42

Appendix B

The Basics of Logic Design

c0

g0

p0 c1 c0

g0 c0 p0 g0 g1

p0 p1 g1 c2 p1

g2

p2

g3

p3 c4 FIGURE B.6.1 A plumbing analogy for carry lookahead for 1 bit, 2 bits, and 4 bits using water pipes and valves. The wrenches are turned to open and close valves. Water is shown in color. The output of the pipe (ci ⫹ 1) will be full if either the nearest generate value (gi) is turned on or if the i propagate value (pi) is on and there is water further upstream, either from an earlier generate or a propagate with water behind it. CarryIn (c0) can result in a carry out without the help of any generates, but with the help of all propagates.

B.6

Faster Addition: Carry Lookahead

p0

p1 g0 p2

p3

g1

P0 p1

g2

p2

g3

p3 G0 FIGURE B.6.2 A plumbing analogy for the next-level carry-lookahead signals P0 and G0. P0 is open only if all four propagates (pi) are open, while water flows in G0 only if at least one generate (gi) is open and all the propagates downstream from that generate are open.

B-43

B-44

Appendix B

The Basics of Logic Design

Both Levels of the Propagate and Generate

EXAMPLE

Determine the gi, pi, Pi, and Gi values of these two 16-bit numbers: a: b:

0001 1110

1010 0101

0011 1110

0011two 1011two

Also, what is CarryOut15 (C4)?

ANSWER

Aligning the bits makes it easy to see the values of generate gi (ai ⭈ bi) and propagate pi (ai ⫹ bi): a: b: gi: pi:

0001 1010 0011 0011 1110 0101 1110 1011 0000 0000 0010 0011 1111 1111 1111 1011

where the bits are numbered 15 to 0 from left to right. Next, the “super” propagates (P3, P2, P1, P0) are simply the AND of the lower-level propagates: P3 P2 P1 P0

1 1 1 1

1 1 1 0

1 1 1 1 1 1 1 1

1 1 1 0

The “super” generates are more complex, so use the following equations: G0

g 3 (p3 g 2) (p3 p2 g1) (p3 p2 p1 g 0) = 0 (1 0) (1 0 1) (1 0 1 1) 0 0 0 0 0 G1 g 7 (p7 g 6) (p7 p6 g 5) (p7 p6 p5 g 4) 0 (1 0) (1 1 1) (1 1 1 0) 0 0 1 0 1 G2 g11 (p11 g10) (p11 p10 g 9) (p11 p10 p9 g 8) 0 (1 0) (1 1 0) (1 1 1 0) 0 0 0 0 0 G3 g15 (p15 g14) (p15 p14 g13) (p15 p14 p13 g12) 0 (1 0) (1 1 0) (1 1 1 0) 0 0 0 0 0 Finally, CarryOut15 is C4

G3 (P3 G2) (P3 P2 G1) (P3 P2 P1 G0) (P3 P2 P1 P0 c0) 0 (1 0) (1 1 1) (1 1 1 0) (1 1 1 0 0) 0 0 1 0 0 1

Hence, there is a carry out when adding these two 16-bit numbers.

B.6

Faster Addition: Carry Lookahead

CarryIn

a0 b0 a1 b1 a2 b2 a3 b3

a4 b4 a5 b5 a6 b6 a7 b7

a8 b8 a9 b9 a10 b10 a11 b11

a12 b12 a13 b13 a14 b14 a15 b15

CarryIn

Result0–3

ALU0 P0 G0

pi gi C1

ci + 1

CarryIn

Carry-lookahead unit

Result4–7

ALU1 P1 G1

pi + 1 gi + 1 C2

ci + 2

CarryIn

Result8–11

ALU2 P2 G2

pi + 2 gi + 2 C3

ci + 3

CarryIn

Result12–15

ALU3 P3 G3

pi + 3 gi + 3 C4

ci + 4

CarryOut FIGURE B.6.3 Four 4-bit ALUs using carry lookahead to form a 16-bit adder. Note that the carries come from the carry-lookahead unit, not from the 4-bit ALUs.

B-45

B-46

Appendix B

The Basics of Logic Design

The reason carry lookahead can make carries faster is that all logic begins evaluating the moment the clock cycle begins, and the result will not change once the output of each gate stops changing. By taking the shortcut of going through fewer gates to send the carry in signal, the output of the gates will stop changing sooner, and hence the time for the adder can be less. To appreciate the importance of carry lookahead, we need to calculate the relative performance between it and ripple carry adders.

Speed of Ripple Carry versus Carry Lookahead

EXAMPLE

ANSWER

One simple way to model time for logic is to assume each AND or OR gate takes the same time for a signal to pass through it. Time is estimated by simply counting the number of gates along the path through a piece of logic. Compare the number of gate delays for paths of two 16-bit adders, one using ripple carry and one using two-level carry lookahead. Figure B.5.5 on page B-28 shows that the carry out signal takes two gate delays per bit. Then the number of gate delays between a carry in to the least significant bit and the carry out of the most significant is 16 ⫻ 2 ⫽ 32. For carry lookahead, the carry out of the most significant bit is just C4, defined in the example. It takes two levels of logic to specify C4 in terms of Pi and Gi (the OR of several AND terms). Pi is specified in one level of logic (AND) using pi, and Gi is specified in two levels using pi and gi, so the worst case for this next level of abstraction is two levels of logic. pi and gi are each one level of logic, defined in terms of ai and bi. If we assume one gate delay for each level of logic in these equations, the worst case is 2 ⫹ 2 ⫹ 1 ⫽ 5 gate delays. Hence, for the path from carry in to carry out, the 16-bit addition by a carry-lookahead adder is six times faster, using this very simple estimate of hardware speed.

Summary Carry lookahead offers a faster path than waiting for the carries to ripple through all 32 1-bit adders. This faster path is paved by two signals, generate and propagate.

B.6

B-47

Faster Addition: Carry Lookahead

The former creates a carry regardless of the carry input, and the latter passes a carry along. Carry lookahead also gives another example of how abstraction is important in computer design to cope with complexity. Using the simple estimate of hardware speed above with gate delays, what is the relative performance of a ripple carry 8-bit add versus a 64-bit add using carrylookahead logic? 1. A 64-bit carry-lookahead adder is three times faster: 8-bit adds are 16 gate delays and 64-bit adds are 7 gate delays. 2. They are about the same speed, since 64-bit adds need more levels of logic in the 16-bit adder. 3. 8-bit adds are faster than 64 bits, even with carry lookahead. Elaboration: We have now accounted for all but one of the arithmetic and logical operations for the core MIPS instruction set: the ALU in Figure B.5.14 omits support of shift instructions. It would be possible to widen the ALU multiplexor to include a left shift by 1 bit or a right shift by 1 bit. But hardware designers have created a circuit called a barrel shifter, which can shift from 1 to 31 bits in no more time than it takes to add two 32-bit numbers, so shifting is normally done outside the ALU. Elaboration: The logic equation for the Sum output of the full adder on page B-28 can be expressed more simply by using a more powerful gate than AND and OR. An exclusive OR gate is true if the two operands disagree; that is, x ≠ y ⇒ 1 and x ⫽⫽ y ⇒ 0 In some technologies, exclusive OR is more efficient than two levels of AND and OR gates. Using the symbol ⊕ to represent exclusive OR, here is the new equation: Sum ⫽ a ⊕ b ⊕ CarryIn Also, we have drawn the ALU the traditional way, using gates. Computers are designed today in CMOS transistors, which are basically switches. CMOS ALU and barrel shifters take advantage of these switches and have many fewer multiplexors than shown in our designs, but the design principles are similar.

Elaboration: Using lowercase and uppercase to distinguish the hierarchy of generate and propagate symbols breaks down when you have more than two levels. An alternate notation that scales is gi..j and pi..j for the generate and propagate signals for bits i to j. Thus, g1..1 is generated for bit 1, g4..1 is for bits 4 to 1, and g16..1 is for bits 16 to 1.

Check Yourself

B-48

Appendix B

B.7

edge-triggered clocking A clocking scheme in which all state changes occur on a clock edge.

The Basics of Logic Design

Clocks

Before we discuss memory elements and sequential logic, it is useful to discuss briefly the topic of clocks. This short section introduces the topic and is similar to the discussion found in Section 4.2. More details on clocking and timing methodologies are presented in Section B.11. Clocks are needed in sequential logic to decide when an element that contains state should be updated. A clock is simply a free-running signal with a fixed cycle time; the clock frequency is simply the inverse of the cycle time. As shown in Figure B.7.1, the clock cycle time or clock period is divided into two portions: when the clock is high and when the clock is low. In this text, we use only edge-triggered clocking. This means that all state changes occur on a clock edge. We use an edgetriggered methodology because it is simpler to explain. Depending on the technology, it may or may not be the best choice for a clocking methodology.

clocking methodology Falling edge

The approach used to determine when data is valid and stable relative to the clock.

Clock period

Rising edge

FIGURE B.7.1 A clock signal oscillates between high and low values. The clock period is the time for one full cycle. In an edge-triggered design, either the rising or falling edge of the clock is active and causes state to be changed.

state element A memory element.

synchronous system A memory system that employs clocks and where data signals are read only when the clock indicates that the signal values are stable.

In an edge-triggered methodology, either the rising edge or the falling edge of the clock is active and causes state changes to occur. As we will see in the next section, the state elements in an edge-triggered design are implemented so that the contents of the state elements only change on the active clock edge. The choice of which edge is active is influenced by the implementation technology and does not affect the concepts involved in designing the logic. The clock edge acts as a sampling signal, causing the value of the data input to a state element to be sampled and stored in the state element. Using an edge trigger means that the sampling process is essentially instantaneous, eliminating problems that could occur if signals were sampled at slightly different times. The major constraint in a clocked system, also called a synchronous system, is that the signals that are written into state elements must be valid when the active

B.7

Clocks

clock edge occurs. A signal is valid if it is stable (i.e., not changing), and the value will not change again until the inputs change. Since combinational circuits cannot have feedback, if the inputs to a combinational logic unit are not changed, the outputs will eventually become valid. Figure B.7.2 shows the relationship among the state elements and the combinational logic blocks in a synchronous, sequential logic design. The state elements, whose outputs change only after the clock edge, provide valid inputs to the combinational logic block. To ensure that the values written into the state elements on the active clock edge are valid, the clock must have a long enough period so that all the signals in the combinational logic block stabilize, and then the clock edge samples those values for storage in the state elements. This constraint sets a lower bound on the length of the clock period, which must be long enough for all state element inputs to be valid. In the rest of this appendix, as well as in Chapter 4, we usually omit the clock signal, since we are assuming that all state elements are updated on the same clock edge. Some state elements will be written on every clock edge, while others will be written only under certain conditions (such as a register being updated). In such cases, we will have an explicit write signal for that state element. The write signal must still be gated with the clock so that the update occurs only on the clock edge if the write signal is active. We will see how this is done and used in the next section. One other advantage of an edge-triggered methodology is that it is possible to have a state element that is used as both an input and output to the same combinational logic block, as shown in Figure B.7.3. In practice, care must be taken to prevent races in such situations and to ensure that the clock period is long enough; this topic is discussed further in Section B.11. Now that we have discussed how clocking is used to update state elements, we can discuss how to construct the state elements.

State element 1

Combinational logic

State element 2

Clock cycle FIGURE B.7.2 The inputs to a combinational logic block come from a state element, and the outputs are written into a state element. The clock edge determines when the contents of the state elements are updated.

B-49

B-50

Appendix B

The Basics of Logic Design

State element

Combinational logic

FIGURE B.7.3 An edge-triggered methodology allows a state element to be read and written in the same clock cycle without creating a race that could lead to undetermined data values. Of course, the clock cycle must still be long enough so that the input values are stable when the active clock edge occurs.

register file A state element that consists of a set of registers that can be read and written by supplying a register number to be accessed.

Elaboration: Occasionally, designers find it useful to have a small number of state elements that change on the opposite clock edge from the majority of the state elements. Doing so requires extreme care, because such an approach has effects on both the inputs and the outputs of the state element. Why then would designers ever do this? Consider the case where the amount of combinational logic before and after a state element is small enough so that each could operate in one-half clock cycle, rather than the more usual full clock cycle. Then the state element can be written on the clock edge corresponding to a half clock cycle, since the inputs and outputs will both be usable after one-half clock cycle. One common place where this technique is used is in register files, where simply reading or writing the register file can often be done in half the normal clock cycle. Chapter 4 makes use of this idea to reduce the pipelining overhead.

B.8

Memory Elements: Flip-Flops, Latches, and Registers

In this section and the next, we discuss the basic principles behind memory elements, starting with flip-flops and latches, moving on to register files, and finishing with memories. All memory elements store state: the output from any memory element depends both on the inputs and on the value that has been stored inside the memory element. Thus all logic blocks containing a memory element contain state and are sequential. R Q

Q S FIGURE B.8.1 A pair of cross-coupled NOR gates can store an internal value. The value stored on the output Q is recycled by inverting it to obtain Q and then inverting Q to obtain Q. If either R or Q is asserted, Q will be deasserted and vice versa.

B.8

B-51

Memory Elements: Flip-Flops, Latches, and Registers

The simplest type of memory elements are unclocked; that is, they do not have any clock input. Although we only use clocked memory elements in this text, an unclocked latch is the simplest memory element, so let’s look at this circuit first. Figure B.8.1 shows an S-R latch (set-reset latch), built from a pair of NOR gates (OR gates with inverted outputs). The outputs Q and Q represent the value of the stored state and its complement. When neither S nor R are asserted, the cross-coupled NOR gates act as inverters and store the previous values of Q and Q. For example, if the output, Q, is true, then the bottom inverter produces a false output (which is Q), which becomes the input to the top inverter, which produces a true output, which is Q, and so on. If S is asserted, then the output Q will be asserted and Q will be deasserted, while if R is asserted, then the output Q will be asserted and Q will be deasserted. When S and R are both deasserted, the last values of Q and Q will continue to be stored in the cross-coupled structure. Asserting S and R simultaneously can lead to incorrect operation: depending on how S and R are deasserted, the latch may oscillate or become metastable (this is described in more detail in Section B.11). This cross-coupled structure is the basis for more complex memory elements that allow us to store data signals. These elements contain additional gates used to store signal values and to cause the state to be updated only in conjunction with a clock. The next section shows how these elements are built.

Flip-Flops and Latches

flip-flop A memory

Flip-flops and latches are the simplest memory elements. In both flip-flops and latches, the output is equal to the value of the stored state inside the element. Furthermore, unlike the S-R latch described above, all the latches and flip-flops we will use from this point on are clocked, which means that they have a clock input and the change of state is triggered by that clock. The difference between a flipflop and a latch is the point at which the clock causes the state to actually change. In a clocked latch, the state is changed whenever the appropriate inputs change and the clock is asserted, whereas in a flip-flop, the state is changed only on a clock edge. Since throughout this text we use an edge-triggered timing methodology where state is only updated on clock edges, we need only use flip-flops. Flip-flops are often built from latches, so we start by describing the operation of a simple clocked latch and then discuss the operation of a flip-flop constructed from that latch. For computer applications, the function of both flip-flops and latches is to store a signal. A D latch or D flip-flop stores the value of its data input signal in the internal memory. Although there are many other types of latch and flip-flop, the D type is the only basic building block that we will need. A D latch has two inputs and two outputs. The inputs are the data value to be stored (called D) and a clock signal (called C) that indicates when the latch should read the value on the D input and store it. The outputs are simply the value of the internal state (Q)

element for which the output is equal to the value of the stored state inside the element and for which the internal state is changed only on a clock edge.

latch A memory element in which the output is equal to the value of the stored state inside the element and the state is changed whenever the appropriate inputs change and the clock is asserted. D flip-flop A flip-flop with one data input that stores the value of that input signal in the internal memory when the clock edge occurs.

B-52

Appendix B

The Basics of Logic Design

and its complement (Q). When the clock input C is asserted, the latch is said to be open, and the value of the output (Q) becomes the value of the input D. When the clock input C is deasserted, the latch is said to be closed, and the value of the output (Q) is whatever value was stored the last time the latch was open. Figure B.8.2 shows how a D latch can be implemented with two additional gates added to the cross-coupled NOR gates. Since when the latch is open the value of Q changes as D changes, this structure is sometimes called a transparent latch. Figure B.8.3 shows how this D latch works, assuming that the output Q is initially false and that D changes first. As mentioned earlier, we use flip-flops as the basic building block, rather than latches. Flip-flops are not transparent: their outputs change only on the clock edge. A flip-flop can be built so that it triggers on either the rising (positive) or falling (negative) clock edge; for our designs we can use either type. Figure B.8.4 shows how a falling-edge D flip-flop is constructed from a pair of D latches. In a D flipflop, the output is stored when the clock edge occurs. Figure B.8.5 shows how this flip-flop operates.

C Q

Q D FIGURE B.8.2 A D latch implemented with NOR gates. A NOR gate acts as an inverter if the other input is 0. Thus, the cross-coupled pair of NOR gates acts to store the state value unless the clock input, C, is asserted, in which case the value of input D replaces the value of Q and is stored. The value of input D must be stable when the clock signal C changes from asserted to deasserted.

D

C

Q FIGURE B.8.3 Operation of a D latch, assuming the output is initially deasserted. When the clock, C, is asserted, the latch is open and the Q output immediately assumes the value of the D input.

B.8

D

D C

Q D latch

B-53

Memory Elements: Flip-Flops, Latches, and Registers

D C

Q D latch Q

Q Q

C

FIGURE B.8.4 A D flip-flop with a falling-edge trigger. The first latch, called the master, is open and follows the input D when the clock input, C, is asserted. When the clock input, C, falls, the first latch is closed, but the second latch, called the slave, is open and gets its input from the output of the master latch.

D

C

Q FIGURE B.8.5 Operation of a D flip-flop with a falling-edge trigger, assuming the output is initially deasserted. When the clock input (C) changes from asserted to deasserted, the Q output stores the value of the D input. Compare this behavior to that of the clocked D latch shown in Figure B.8.3. In a clocked latch, the stored value and the output, Q, both change whenever C is high, as opposed to only when C transitions.

Here is a Verilog description of a module for a rising-edge D flip-flop, assuming that C is the clock input and D is the data input: module DFF(clock,D,Q,Qbar); input clock, D; output reg Q; // Q is a reg since it is assigned in an always block output Qbar; assign Qbar = ~ Q; // Qbar is always just the inverse of Q always @(posedge clock) // perform actions whenever the clock rises Q = D; endmodule

Because the D input is sampled on the clock edge, it must be valid for a period of time immediately before and immediately after the clock edge. The minimum time that the input must be valid before the clock edge is called the setup time; the

setup time The minimum time that the input to a memory device must be valid before the clock edge.

B-54

Appendix B

The Basics of Logic Design

D

Setup time

Hold time

C FIGURE B.8.6 Setup and hold time requirements for a D flip-flop with a falling-edge trigger. The input must be stable for a period of time before the clock edge, as well as after the clock edge. The minimum time the signal must be stable before the clock edge is called the setup time, while the minimum time the signal must be stable after the clock edge is called the hold time. Failure to meet these minimum requirements can result in a situation where the output of the flip-flop may not be predictable, as described in Section B.11. Hold times are usually either 0 or very small and thus not a cause of worry.

hold time The minimum time during which the input must be valid after the clock edge.

minimum time during which it must be valid after the clock edge is called the hold time. Thus the inputs to any flip-flop (or anything built using flip-flops) must be valid during a window that begins at time tsetup before the clock edge and ends at thold after the clock edge, as shown in Figure B.8.6. Section B.11 talks about clocking and timing constraints, including the propagation delay through a flip-flop, in more detail. We can use an array of D flip-flops to build a register that can hold a multibit datum, such as a byte or word. We used registers throughout our datapaths in Chapter 4.

Register Files One structure that is central to our datapath is a register file. A register file consists of a set of registers that can be read and written by supplying a register number to be accessed. A register file can be implemented with a decoder for each read or write port and an array of registers built from D flip-flops. Because reading a register does not change any state, we need only supply a register number as an input, and the only output will be the data contained in that register. For writing a register we will need three inputs: a register number, the data to write, and a clock that controls the writing into the register. In Chapter 4, we used a register file that has two read ports and one write port. This register file is drawn as shown in Figure B.8.7. The read ports can be implemented with a pair of multiplexors, each of which is as wide as the number of bits in each register of the register file. Figure B.8.8 shows the implementation of two register read ports for a 32-bit-wide register file. Implementing the write port is slightly more complex, since we can only change the contents of the designated register. We can do this by using a decoder to generate a signal that can be used to determine which register to write. Figure B.8.9 shows how to implement the write port for a register file. It is important to remember that the flip-flop changes state only on the clock edge. In Chapter 4, we hooked up write signals for the register file explicitly and assumed the clock shown in Figure B.8.9 is attached implicitly. What happens if the same register is read and written during a clock cycle? Because the write of the register file occurs on the clock edge, the register will be

B.8

Memory Elements: Flip-Flops, Latches, and Registers

Read register number 1 Read register number 2 Write register

Register file

Write data

Read data 1

Read data 2

Write

FIGURE B.8.7 A register file with two read ports and one write port has five inputs and two outputs. The control input Write is shown in color.

Read register number 1 Register 0 Register 1 ... Register n – 2

M u

Read data 1

x

Register n – 1

Read register number 2

M u

Read data 2

x

FIGURE B.8.8 The implementation of two read ports for a register file with n registers can be done with a pair of n-to-1 multiplexors, each 32 bits wide. The register read number signal is used as the multiplexor selector signal. Figure B.8.9 shows how the write port is implemented.

B-55

B-56

Appendix B

The Basics of Logic Design

Write C 0 1 Register number

n-to-2n decoder

Register 0 .. .

D C Register 1

n–2 n–1

D .. .

C Register n – 2 D C Register n – 1 Register data

D

FIGURE B.8.9 The write port for a register file is implemented with a decoder that is used with the write signal to generate the C input to the registers. All three inputs (the register number, the data, and the write signal) will have setup and hold-time constraints that ensure that the correct data is written into the register file.

valid during the time it is read, as we saw earlier in Figure B.7.2. The value returned will be the value written in an earlier clock cycle. If we want a read to return the value currently being written, additional logic in the register file or outside of it is needed. Chapter 4 makes extensive use of such logic.

Specifying Sequential Logic in Verilog To specify sequential logic in Verilog, we must understand how to generate a clock, how to describe when a value is written into a register, and how to specify sequential control. Let us start by specifying a clock. A clock is not a predefined object in Verilog; instead, we generate a clock by using the Verilog notation #n before a statement; this causes a delay of n simulation time steps before the execution of the statement. In most Verilog simulators, it is also possible to generate a clock as an external input, allowing the user to specify at simulation time the number of clock cycles during which to run a simulation. The code in Figure B.8.10 implements a simple clock that is high or low for one simulation unit and then switches state. We use the delay capability and blocking assignment to implement the clock.

B.8

Memory Elements: Flip-Flops, Latches, and Registers

FIGURE B.8.10 A specification of a clock.

Next, we must be able to specify the operation of an edge-triggered register. In Verilog, this is done by using the sensitivity list on an always block and specifying as a trigger either the positive or negative edge of a binary variable with the notation posedge or negedge, respectively. Hence, the following Verilog code causes register A to be written with the value b at the positive edge clock:

FIGURE B.8.11 A MIPS register file written in behavioral Verilog. This register file writes on the rising clock edge.

Throughout this chapter and the Verilog sections of Chapter 4, we will assume a positive edge-triggered design. Figure B.8.11 shows a Verilog specification of a MIPS register file that assumes two reads and one write, with only the write being clocked.

B-57

B-58

Appendix B

Check Yourself

The Basics of Logic Design

In the Verilog for the register file in Figure B.8.11, the output ports corresponding to the registers being read are assigned using a continuous assignment, but the register being written is assigned in an always block. Which of the following is the reason? a. There is no special reason. It was simply convenient. b. Because Data1 and Data2 are output ports and WriteData is an input port. c. Because reading is a combinational event, while writing is a sequential event.

B.9 static random access memory (SRAM) A memory where data is stored statically (as in flip-flops) rather than dynamically (as in DRAM). SRAMs are faster than DRAMs, but less dense and more expensive per bit.

Memory Elements: SRAMs and DRAMs

Registers and register files provide the basic building blocks for small memories, but larger amounts of memory are built using either SRAMs (static random access memories) or DRAMs (dynamic random access memories). We first discuss SRAMs, which are somewhat simpler, and then turn to DRAMs.

SRAMs SRAMs are simply integrated circuits that are memory arrays with (usually) a single access port that can provide either a read or a write. SRAMs have a fixed access time to any datum, though the read and write access characteristics often differ. An SRAM chip has a specific configuration in terms of the number of addressable locations, as well as the width of each addressable location. For example, a 4M ⫻ 8 SRAM provides 4M entries, each of which is 8 bits wide. Thus it will have 22 address lines (since 4M ⫽ 222), an 8-bit data output line, and an 8-bit single data input line. As with ROMs, the number of addressable locations is often called the height, with the number of bits per unit called the width. For a variety of technical reasons, the newest and fastest SRAMs are typically available in narrow configurations: ⫻ 1 and ⫻ 4. Figure B.9.1 shows the input and output signals for a 2M ⫻ 16 SRAM.

Address

21

Chip select SRAM 2M ⫻ 16

Output enable

16

Dout[15–0]

Write enable

Din[15–0]

16

FIGURE B.9.1 A 32K ⴛ 8 SRAM showing the 21 address lines (32K ⴝ 215) and 16 data inputs, the 3 control lines, and the 16 data outputs.

B.9

Memory Elements: SRAMs and DRAMs

To initiate a read or write access, the Chip select signal must be made active. For reads, we must also activate the Output enable signal that controls whether or not the datum selected by the address is actually driven on the pins. The Output enable is useful for connecting multiple memories to a single-output bus and using Output enable to determine which memory drives the bus. The SRAM read access time is usually specified as the delay from the time that Output enable is true and the address lines are valid until the time that the data is on the output lines. Typical read access times for SRAMs in 2004 varied from about 2–4 ns for the fastest CMOS parts, which tend to be somewhat smaller and narrower, to 8–20 ns for the typical largest parts, which in 2004 had more than 32 million bits of data. The demand for low-power SRAMs for consumer products and digital appliances has grown greatly in the past five years; these SRAMs have much lower stand-by and access power, but usually are 5–10 times slower. Most recently, synchronous SRAMs—similar to the synchronous DRAMs, which we discuss in the next section—have also been developed. For writes, we must supply the data to be written and the address, as well as signals to cause the write to occur. When both the Write enable and Chip select are true, the data on the data input lines is written into the cell specified by the address. There are setup-time and hold-time requirements for the address and data lines, just as there were for D flip-flops and latches. In addition, the Write enable signal is not a clock edge but a pulse with a minimum width requirement. The time to complete a write is specified by the combination of the setup times, the hold times, and the Write enable pulse width. Large SRAMs cannot be built in the same way we build a register file because, unlike a register file where a 32-to-1 multiplexor might be practical, the 64K-to1 multiplexor that would be needed for a 64K ⫻ 1 SRAM is totally impractical. Rather than use a giant multiplexor, large memories are implemented with a shared output line, called a bit line, which multiple memory cells in the memory array can assert. To allow multiple sources to drive a single line, a three-state buffer (or tristate buffer) is used. A three-state buffer has two inputs—a data signal and an Output enable—and a single output, which is in one of three states: asserted, deasserted, or high impedance. The output of a tristate buffer is equal to the data input signal, either asserted or deasserted, if the Output enable is asserted, and is otherwise in a high-impedance state that allows another three-state buffer whose Output enable is asserted to determine the value of a shared output. Figure B.9.2 shows a set of three-state buffers wired to form a multiplexor with a decoded input. It is critical that the Output enable of at most one of the three-state buffers be asserted; otherwise, the three-state buffers may try to set the output line differently. By using three-state buffers in the individual cells of the SRAM, each cell that corresponds to a particular output can share the same output line. The use of a set of distributed three-state buffers is a more efficient implementation than a large centralized multiplexor. The three-state buffers are incorporated into the flipflops that form the basic cells of the SRAM. Figure B.9.3 shows how a small 4 ⫻ 2 SRAM might be built, using D latches with an input called Enable that controls the three-state output.

B-59

B-60

Appendix B

The Basics of Logic Design

Select 0 Data 0

Enable In

Select 1 Data 1

Enable In

Select 2 Data 2

Out

Enable In

Select 3 Data 3

Out

Output

Out

Enable In

Out

FIGURE B.9.2 Four three-state buffers are used to form a multiplexor. Only one of the four Select inputs can be asserted. A three-state buffer with a deasserted Output enable has a high-impedance output that allows a three-state buffer whose Output enable is asserted to drive the shared output line.

The design in Figure B.9.3 eliminates the need for an enormous multiplexor; however, it still requires a very large decoder and a correspondingly large number of word lines. For example, in a 4M ⫻ 8 SRAM, we would need a 22-to-4M decoder and 4M word lines (which are the lines used to enable the individual flip-flops)! To circumvent this problem, large memories are organized as rectangular arrays and use a two-step decoding process. Figure B.9.4 shows how a 4M ⫻ 8 SRAM might be organized internally using a two-step decode. As we will see, the two-level decoding process is quite important in understanding how DRAMs operate. Recently we have seen the development of both synchronous SRAMs (SSRAMs) and synchronous DRAMs (SDRAMs). The key capability provided by synchronous RAMs is the ability to transfer a burst of data from a series of sequential addresses within an array or row. The burst is defined by a starting address, supplied in the usual fashion, and a burst length. The speed advantage of synchronous RAMs comes from the ability to transfer the bits in the burst without having to specify additional address bits. Instead, a clock is used to transfer the successive bits in the burst. The elimination of the need to specify the address for the transfers within the burst significantly improves the rate for transferring the block of data. Because of this capability, synchronous SRAMs and DRAMs are rapidly becoming the RAMs of choice for building memory systems in computers. We discuss the use of synchronous DRAMs in a memory system in more detail in the next section and in Chapter 5.

B.9

Din[1]

Din[1]

Write enable

B-61

Memory Elements: SRAMs and DRAMs

D

D

C

latch

Q

D

D

C

latch

Enable

Enable

D

D

D

C

latch

Q

0

2-to-4 decoder

C

D latch

Q

Enable

Enable

D

D

D

D

C

latch

C

latch

Q

1

Address

Q

Enable

Enable

D

D

D

D

C

latch

C

latch

Q

2

Q

Enable

Q

Enable

3

Dout[1]

Dout[0]

FIGURE B.9.3 The basic structure of a 4 ⴛ 2 SRAM consists of a decoder that selects which pair of cells to activate. The activated cells use a three-state output connected to the vertical bit lines that supply the requested data. The address that selects the cell is sent on one of a set of horizontal address lines, called word lines. For simplicity, the Output enable and Chip select signals have been omitted, but they could easily be added with a few AND gates.

12 to 4096 decoder 4096

Mux

Dout6

Mux

Dout7

1024

Dout5

Mux

4K ⫻ 1024 SRAM

Dout4

Mux

4K ⫻ 1024 SRAM

Dout3

Mux

4K ⫻ 1024 SRAM

Dout2

Mux

4K ⫻ 1024 SRAM

Dout1

Mux

4K ⫻ 1024 SRAM

Dout0

Mux

4K ⫻ 1024 SRAM

FIGURE B.9.4 Typical organization of a 4M ⴛ 8 SRAM as an array of 4K ⴛ 1024 arrays. The first decoder generates the addresses for eight 4K ⫻ 1024 arrays; then a set of multiplexors is used to select 1 bit from each 1024-bit-wide array. This is a much easier design than a single-level decode that would need either an enormous decoder or a gigantic multiplexor. In practice, a modern SRAM of this size would probably use an even larger number of blocks, each somewhat smaller.

Address [9–0]

Address [21–10]

4K ⫻ 1024 SRAM

Appendix B

4K ⫻ 1024 SRAM

B-62 The Basics of Logic Design

B.9

Memory Elements: SRAMs and DRAMs

DRAMs In a static RAM (SRAM), the value stored in a cell is kept on a pair of inverting gates, and as long as power is applied, the value can be kept indefinitely. In a dynamic RAM (DRAM), the value kept in a cell is stored as a charge in a capacitor. A single transistor is then used to access this stored charge, either to read the value or to overwrite the charge stored there. Because DRAMs use only a single transistor per bit of storage, they are much denser and cheaper per bit. By comparison, SRAMs require four to six transistors per bit. Because DRAMs store the charge on a capacitor, it cannot be kept indefinitely and must periodically be refreshed. That is why this memory structure is called dynamic, as opposed to the static storage in a SRAM cell. To refresh the cell, we merely read its contents and write it back. The charge can be kept for several milliseconds, which might correspond to close to a million clock cycles. Today, single-chip memory controllers often handle the refresh function independently of the processor. If every bit had to be read out of the DRAM and then written back individually, with large DRAMs containing multiple megabytes, we would constantly be refreshing the DRAM, leaving no time for accessing it. Fortunately, DRAMs also use a two-level decoding structure, and this allows us to refresh an entire row (which shares a word line) with a read cycle followed immediately by a write cycle. Typically, refresh operations consume 1% to 2% of the active cycles of the DRAM, leaving the remaining 98% to 99% of the cycles available for reading and writing data. Elaboration: How does a DRAM read and write the signal stored in a cell? The transistor inside the cell is a switch, called a pass transistor, that allows the value stored on the capacitor to be accessed for either reading or writing. Figure B.9.5 shows how the single-transistor cell looks. The pass transistor acts like a switch: when the signal on the word line is asserted, the switch is closed, connecting the capacitor to the bit line. If the operation is a write, then the value to be written is placed on the bit line. If the value is a 1, the capacitor will be charged. If the value is a 0, then the capacitor will be discharged. Reading is slightly more complex, since the DRAM must detect a very small charge stored in the capacitor. Before activating the word line for a read, the bit line is charged to the voltage that is halfway between the low and high voltage. Then, by activating the word line, the charge on the capacitor is read out onto the bit line. This causes the bit line to move slightly toward the high or low direction, and this change is detected with a sense amplifier, which can detect small changes in voltage.

B-63

B-64

Appendix B

The Basics of Logic Design

Word line

Pass transistor

Capacitor

Bit line FIGURE B.9.5 A single-transistor DRAM cell contains a capacitor that stores the cell contents and a transistor used to access the cell.

Row decoder 11-to-2048

2048 ⫻ 2048 array

Address[10–0] Column latches

Mux

Dout FIGURE B.9.6 A 4M ⴛ 1 DRAM is built with a 2048 ⫻ 2048 array. The row access uses 11 bits to select a row, which is then latched in 2048 1-bit latches. A multiplexor chooses the output bit from these 2048 latches. The RAS and CAS signals control whether the address lines are sent to the row decoder or column multiplexor.

B.9

Memory Elements: SRAMs and DRAMs

DRAMs use a two-level decoder consisting of a row access followed by a column access, as shown in Figure B.9.6. The row access chooses one of a number of rows and activates the corresponding word line. The contents of all the columns in the active row are then stored in a set of latches. The column access then selects the data from the column latches. To save pins and reduce the package cost, the same address lines are used for both the row and column address; a pair of signals called RAS (Row Access Strobe) and CAS (Column Access Strobe) are used to signal the DRAM that either a row or column address is being supplied. Refresh is performed by simply reading the columns into the column latches and then writing the same values back. Thus, an entire row is refreshed in one cycle. The two-level addressing scheme, combined with the internal circuitry, makes DRAM access times much longer (by a factor of 5–10) than SRAM access times. In 2004, typical DRAM access times ranged from 45 to 65 ns; 256 Mbit DRAMs are in full production, and the first customer samples of 1 GB DRAMs became available in the first quarter of 2004. The much lower cost per bit makes DRAM the choice for main memory, while the faster access time makes SRAM the choice for caches. You might observe that a 64M ⫻ 4 DRAM actually accesses 8K bits on every row access and then throws away all but 4 of those during a column access. DRAM designers have used the internal structure of the DRAM as a way to provide higher bandwidth out of a DRAM. This is done by allowing the column address to change without changing the row address, resulting in an access to other bits in the column latches. To make this process faster and more precise, the address inputs were clocked, leading to the dominant form of DRAM in use today: synchronous DRAM or SDRAM. Since about 1999, SDRAMs have been the memory chip of choice for most cache-based main memory systems. SDRAMs provide fast access to a series of bits within a row by sequentially transferring all the bits in a burst under the control of a clock signal. In 2004, DDRRAMs (Double Data Rate RAMs), which are called double data rate because they transfer data on both the rising and falling edge of an externally supplied clock, were the most heavily used form of SDRAMs. As we discuss in Chapter 5, these high-speed transfers can be used to boost the bandwidth available out of main memory to match the needs of the processor and caches.

Error Correction Because of the potential for data corruption in large memories, most computer systems use some sort of error-checking code to detect possible corruption of data. One simple code that is heavily used is a parity code. In a parity code the number of 1s in a word is counted; the word has odd parity if the number of 1s is odd and

B-65

B-66

error detection code A code that enables the detection of an error in data, but not the precise location and, hence, correction of the error.

Appendix B

The Basics of Logic Design

even otherwise. When a word is written into memory, the parity bit is also written (1 for odd, 0 for even). Then, when the word is read out, the parity bit is read and checked. If the parity of the memory word and the stored parity bit do not match, an error has occurred. A 1-bit parity scheme can detect at most 1 bit of error in a data item; if there are 2 bits of error, then a 1-bit parity scheme will not detect any errors, since the parity will match the data with two errors. (Actually, a 1-bit parity scheme can detect any odd number of errors; however, the probability of having three errors is much lower than the probability of having two, so, in practice, a 1-bit parity code is limited to detecting a single bit of error.) Of course, a parity code cannot tell which bit in a data item is in error. A 1-bit parity scheme is an error detection code; there are also error correction codes (ECC) that will detect and allow correction of an error. For large main memories, many systems use a code that allows the detection of up to 2 bits of error and the correction of a single bit of error. These codes work by using more bits to encode the data; for example, the typical codes used for main memories require 7 or 8 bits for every 128 bits of data. Elaboration: A 1-bit parity code is a distance-2 code, which means that if we look at the data plus the parity bit, no 1-bit change is sufficient to generate another legal combination of the data plus parity. For example, if we change a bit in the data, the parity will be wrong, and vice versa. Of course, if we change 2 bits (any 2 data bits or 1 data bit and the parity bit), the parity will match the data and the error cannot be detected. Hence, there is a distance of two between legal combinations of parity and data. To detect more than one error or correct an error, we need a distance-3 code, which has the property that any legal combination of the bits in the error correction code and the data has at least 3 bits differing from any other combination. Suppose we have such a code and we have one error in the data. In that case, the code plus data will be one bit away from a legal combination, and we can correct the data to that legal combination. If we have two errors, we can recognize that there is an error, but we cannot correct the errors. Let’s look at an example. Here are the data words and a distance-3 error correction code for a 4-bit data item. Data Word

Code bits

Data

Code bits

0000 0001 0010 0011 0100 0101 0110 0111

000 011 101 110 110 101 011 000

1000 1001 1010 1011 1100 1101 1110 1111

111 100 010 001 001 010 100 111

B.10

B-67

Finite-State Machines

To see how this works, let’s choose a data word, say 0110, whose error correction code is 011. Here are the four 1-bit error possibilities for this data: 1110, 0010, 0100, and 0111. Now look at the data item with the same code (011), which is the entry with the value 0001. If the error correction decoder received one of the four possible data words with an error, it would have to choose between correcting to 0110 or 0001. While these four words with error have only one bit changed from the correct pattern of 0110, they each have two bits that are different from the alternate correction of 0001. Hence, the error correction mechanism can easily choose to correct to 0110, since a single error is a much higher probability. To see that two errors can be detected, simply notice that all the combinations with two bits changed have a different code. The one reuse of the same code is with three bits different, but if we correct a 2-bit error, we will correct to the wrong value, since the decoder will assume that only a single error has occurred. If we want to correct 1-bit errors and detect, but not erroneously correct, 2-bit errors, we need a distance-4 code. Although we distinguished between the code and data in our explanation, in truth, an error correction code treats the combination of code and data as a single word in a larger code (7 bits in this example). Thus, it deals with errors in the code bits in the same fashion as errors in the data bits. While the above example requires n ⫺ 1 bits for n bits of data, the number of bits required grows slowly, so that for a distance-3 code, a 64-bit word needs 7 bits and a 128-bit word needs 8. This type of code is called a Hamming code, after R. Hamming, who described a method for creating such codes.

B.10

Finite-State Machines

As we saw earlier, digital logic systems can be classified as combinational or sequential. Sequential systems contain state stored in memory elements internal to the system. Their behavior depends both on the set of inputs supplied and on the contents of the internal memory, or state of the system. Thus, a sequential system cannot be described with a truth table. Instead, a sequential system is described as a finite-state machine (or often just state machine). A finite-state machine has a set of states and two functions, called the next-state function and the output function. The set of states corresponds to all the possible values of the internal storage. Thus, if there are n bits of storage, there are 2n states. The next-state function is a combinational function that, given the inputs and the current state, determines the next state of the system. The output function produces a set of outputs from the current state and the inputs. Figure B.10.1 shows this diagrammatically. The state machines we discuss here and in Chapter 4 are synchronous. This means that the state changes together with the clock cycle, and a new state is computed once every clock. Thus, the state elements are updated only on the clock edge. We use this methodology in this section and throughout Chapter 4, and we do not

finite-state machine A sequential logic function consisting of a set of inputs and out puts, a next-state function that maps the current state and the inputs to a new state, and an output function that maps the current state and possibly the inputs to a set of asserted outputs.

next-state function A combinational function that, given the inputs and the current state, determines the next state of a finite-state machine.

B-68

Appendix B

The Basics of Logic Design

Next state Current state

Next-state function

Clock Inputs

Output function

Outputs

FIGURE B.10.1 A state machine consists of internal storage that contains the state and two combinational functions: the next-state function and the output function. Often, the output function is restricted to take only the current state as its input; this does not change the capability of a sequential machine, but does affect its internals.

usually show the clock explicitly. We use state machines throughout Chapter 4 to control the execution of the processor and the actions of the datapath. To illustrate how a finite-state machine operates and is designed, let’s look at a simple and classic example: controlling a traffic light. (Chapters 4 and 5 contain more detailed examples of using finite-state machines to control processor execution.) When a finite-state machine is used as a controller, the output function is often restricted to depend on just the current state. Such a finite-state machine is called a Moore machine. This is the type of finite-state machine we use throughout this book. If the output function can depend on both the current state and the current input, the machine is called a Mealy machine. These two machines are equivalent in their capabilities, and one can be turned into the other mechanically. The basic advantage of a Moore machine is that it can be faster, while a Mealy machine may be smaller, since it may need fewer states than a Moore machine. In Chapter 5, we discuss the differences in more detail and show a Verilog version of finite-state control using a Mealy machine. Our example concerns the control of a traffic light at an intersection of a northsouth route and an east-west route. For simplicity, we will consider only the green and red lights; adding the yellow light is left for an exercise. We want the lights to cycle no faster than 30 seconds in each direction, so we will use a 0.033 Hz clock so that the machine cycles between states at no faster than once every 30 seconds. There are two output signals:

B.10

Finite-State Machines



NSlite: When this signal is asserted, the light on the north-south road is green; when this signal is deasserted, the light on the north-south road is red.



EWlite: When this signal is asserted, the light on the east-west road is green; when this signal is deasserted, the light on the east-west road is red.

In addition, there are two inputs: ■

NScar: Indicates that a car is over the detector placed in the roadbed in front of the light on the north-south road (going north or south).



EWcar: Indicates that a car is over the detector placed in the roadbed in front of the light on the east-west road (going east or west).

The traffic light should change from one direction to the other only if a car is waiting to go in the other direction; otherwise, the light should continue to show green in the same direction as the last car that crossed the intersection. To implement this simple traffic light we need two states: ■

NSgreen: The traffic light is green in the north-south direction.



EWgreen: The traffic light is green in the east-west direction.

We also need to create the next-state function, which can be specified with a table:

NSgreen NSgreen NSgreen NSgreen EWgreen EWgreen EWgreen EWgreen

NScar

Inputs EWcar

Next state

0 0 1 1 0 0 1 1

0 1 0 1 0 1 0 1

NSgreen EWgreen NSgreen EWgreen EWgreen EWgreen NSgreen NSgreen

Notice that we didn’t specify in the algorithm what happens when a car approaches from both directions. In this case, the next-state function given above changes the state to ensure that a steady stream of cars from one direction cannot lock out a car in the other direction. The finite-state machine is completed by specifying the output function. Before we examine how to implement this finite-state machine, let’s look at a graphical representation, which is often used for finite-state machines. In this representation, nodes are used to indicate states. Inside the node we place a list of the outputs that are active for that state. Directed arcs are used to show the next-state

B-69

B-70

Appendix B

The Basics of Logic Design

w Outputs

NSgreen EWgreen

NSlite

EWlite

1 0

0 1

function, with labels on the arcs specifying the input condition as logic functions. Figure B.10.2 shows the graphical representation for this finite-state machine.

EWcar

NSgreen

EWgreen NScar NSlite

EWlite

EWcar

NScar

FIGURE B.10.2 The graphical representation of the two-state traffic light controller. We simplified the logic functions on the state transitions. For example, the transition from NSgreen to EWgreen in the next-state table is (NScar EWcar ) (NScar EWcar ) , which is equivalent to EWcar.

A finite-state machine can be implemented with a register to hold the current state and a block of combinational logic that computes the next-state function and the output function. Figure B.10.3 shows how a finite-state machine with 4 bits of state, and thus up to 16 states, might look. To implement the finite-state machine in this way, we must first assign state numbers to the states. This process is called state assignment. For example, we could assign NSgreen to state 0 and EWgreen to state 1. The state register would contain a single bit. The next-state function would be given as NextState

(CurrentState EWcar )

(CurrentState NScar )

B.11

Timing Methodologies

where CurrentState is the contents of the state register (0 or 1) and NextState is the output of the next-state function that will be written into the state register at the end of the clock cycle. The output function is also simple: NSlite ⫽ CurrentState EWlite ⫽ CurrentState The combinational logic block is often implemented using structured logic, such as a PLA. A PLA can be constructed automatically from the next-state and output function tables. In fact, there are computer-aided design (CAD) programs

Outputs Combinational logic

Next state

State register

Inputs FIGURE B.10.3 A finite-state machine is implemented with a state register that holds the current state and a combinational logic block to compute the next state and output functions. The latter two functions are often split apart and implemented with two separate blocks of logic, which may require fewer gates.

that take either a graphical or textual representation of a finite-state machine and produce an optimized implementation automatically. In Chapters 4 and 5, finitestate machines were used to control processor execution. Appendix D discusses the detailed implementation of these controllers with both PLAs and ROMs. To show how we might write the control in Verilog, Figure B.10.4 shows a Verilog version designed for synthesis. Note that for this simple control function, a Mealy machine is not useful, but this style of specification is used in Chapter 5 to implement a control function that is a Mealy machine and has fewer states than the Moore machine controller.

B-71

B-72

Appendix B

The Basics of Logic Design

FIGURE B.10.4 A Verilog version of the traffic light controller.

Check Yourself

What is the smallest number of states in a Moore machine for which a Mealy machine could have fewer states? a. Two, since there could be a one-state Mealy machine that might do the same thing. b. Three, since there could be a simple Moore machine that went to one of two different states and always returned to the original state after that. For such a simple machine, a two-state Mealy machine is possible. c. You need at least four states to exploit the advantages of a Mealy machine over a Moore machine.

B.11

Timing Methodologies

Throughout this appendix and in the rest of the text, we use an edge-triggered timing methodology. This timing methodology has an advantage in that it is simpler to explain and understand than a level-triggered methodology. In this section, we explain this timing methodology in a little more detail and also introduce level-sensitive clocking. We conclude this section by briefly discussing

B.11

Timing Methodologies

the issue of asynchronous signals and synchronizers, an important problem for digital designers. The purpose of this section is to introduce the major concepts in clocking methodology. The section makes some important simplifying assumptions; if you are interested in understanding timing methodology in more detail, consult one of the references listed at the end of this appendix. We use an edge-triggered timing methodology because it is simpler to explain and has fewer rules required for correctness. In particular, if we assume that all clocks arrive at the same time, we are guaranteed that a system with edge-triggered registers between blocks of combinational logic can operate correctly without races if we simply make the clock long enough. A race occurs when the contents of a state element depend on the relative speed of different logic elements. In an edgetriggered design, the clock cycle must be long enough to accommodate the path from one flip-flop through the combinational logic to another flip-flop where it must satisfy the setup-time requirement. Figure B.11.1 shows this requirement for a system using rising edge-triggered flip-flops. In such a system the clock period (or cycle time) must be at least as large as t prop ⫹ t combinational ⫹ t setup for the worst-case values of these three delays, which are defined as follows: ■

tprop is the time for a signal to propagate through a flip-flop; it is also sometimes called clock-to-Q.



tcombinational is the longest delay for any combinational logic (which by definition is surrounded by two flip-flops).



tsetup is the time before the rising clock edge that the input to a flip-flop must be valid.

D

Q Flip-flop

D Combinational logic block

C tprop

Q Flip-flop

C tcombinational

tsetup

FIGURE B.11.1 In an edge-triggered design, the clock must be long enough to allow signals to be valid for the required setup time before the next clock edge. The time for a flip-flop input to propagate to the flip-flip outputs is tprop; the signal then takes tcombinational to travel through the combinational logic and must be valid tsetup before the next clock edge.

B-73

B-74

Appendix B

clock skew The difference in absolute time between the times when two state elements see a clock edge.

The Basics of Logic Design

We make one simplifying assumption: the hold-time requirements are satisfied, which is almost never an issue with modern logic. One additional complication that must be considered in edge-triggered designs is clock skew. Clock skew is the difference in absolute time between when two state elements see a clock edge. Clock skew arises because the clock signal will often use two different paths, with slightly different delays, to reach two different state elements. If the clock skew is large enough, it may be possible for a state element to change and cause the input to another flip-flop to change before the clock edge is seen by the second flip-flop. Figure B.11.2 illustrates this problem, ignoring setup time and flip-flop propagation delay. To avoid incorrect operation, the clock period is increased to allow for the maximum clock skew. Thus, the clock period must be longer than t prop ⫹ t combinational ⫹ t setup ⫹ t skew With this constraint on the clock period, the two clocks can also arrive in the opposite order, with the second clock arriving tskew earlier, and the circuit will work

D

Q Flip-flop

Clock arrives at time t

C

D Combinational logic block with delay time of Δ

Q Flip-flop

Clock arrives after t + Δ

C

FIGURE B.11.2 Illustration of how clock skew can cause a race, leading to incorrect operation. Because of the difference in when the two flip-flops see the clock, the signal that is stored into the first flip-flop can race forward and change the input to the second flipflop before the clock arrives at the second flip-flop.

level-sensitive clocking A timing methodology in which state changes occur at either high or low clock levels but are not instantaneous as such changes are in edgetriggered designs.

correctly. Designers reduce clock-skew problems by carefully routing the clock signal to minimize the difference in arrival times. In addition, smart designers also provide some margin by making the clock a little longer than the minimum; this allows for variation in components as well as in the power supply. Since clock skew can also affect the hold-time requirements, minimizing the size of the clock skew is important. Edge-triggered designs have two drawbacks: they require extra logic and they may sometimes be slower. Just looking at the D flip-flop versus the level-sensitive latch that we used to construct the flip-flop shows that edge-triggered design requires more logic. An alternative is to use level-sensitive clocking. Because state changes in a level-sensitive methodology are not instantaneous, a level-sensitive scheme is slightly more complex and requires additional care to make it operate correctly.

B.11

B-75

Timing Methodologies

Level-Sensitive Timing In level-sensitive timing, the state changes occur at either high or low levels, but they are not instantaneous as they are in an edge-triggered methodology. Because of the noninstantaneous change in state, races can easily occur. To ensure that a levelsensitive design will also work correctly if the clock is slow enough, designers use twophase clocking. Two-phase clocking is a scheme that makes use of two nonoverlapping clock signals. Since the two clocks, typically called φ1 and φ2, are nonoverlapping, at most one of the clock signals is high at any given time, as Figure B.11.3 shows. We can use these two clocks to build a system that contains level-sensitive latches but is free from any race conditions, just as the edge-triggered designs were.

Φ1 Φ2

Nonoverlapping periods FIGURE B.11.3 A two-phase clocking scheme showing the cycle of each clock and the nonoverlapping periods.

D Φ1

Q Latch

C

D Combinational logic block

Φ2

Q Latch

D Combinational logic block

C

Φ1

Latch C

FIGURE B.11.4 A two-phase timing scheme with alternating latches showing how the system operates on both clock phases. The output of a latch is stable on the opposite phase from its C input. Thus, the first block of combinational inputs has a stable input during φ2, and its output is latched by φ2. The second (rightmost) combinational block operates in just the opposite fashion, with stable inputs during φ1. Thus, the delays through the combinational blocks determine the minimum time that the respective clocks must be asserted. The size of the nonoverlapping period is determined by the maximum clock skew and the minimum delay of any logic block.

One simple way to design such a system is to alternate the use of latches that are open on φ1 with latches that are open on φ2. Because both clocks are not asserted at the same time, a race cannot occur. If the input to a combinational block is a φ1 clock, then its output is latched by a φ2 clock, which is open only during φ2 when the input latch is closed and hence has a valid output. Figure B.11.4 shows how a system with two-phase timing and alternating latches operates. As in an edgetriggered design, we must pay attention to clock skew, particularly between the two

B-76

Appendix B

The Basics of Logic Design

clock phases. By increasing the amount of nonoverlap between the two phases, we can reduce the potential margin of error. Thus, the system is guaranteed to operate correctly if each phase is long enough and if there is large enough nonoverlap between the phases.

Asynchronous Inputs and Synchronizers By using a single clock or a two-phase clock, we can eliminate race conditions if clock-skew problems are avoided. Unfortunately, it is impractical to make an entire system function with a single clock and still keep the clock skew small. While the CPU may use a single clock, I/O devices will probably have their own clock. An asynchronous device may communicate with the CPU through a series of handshaking steps. To translate the asynchronous input to a synchronous signal that can be used to change the state of a system, we need to use a synchronizer, whose inputs are the asynchronous signal and a clock and whose output is a signal synchronous with the input clock. Our first attempt to build a synchronizer uses an edge-triggered D flip-flop, whose D input is the asynchronous signal, as Figure B.11.5 shows. Because we communicate with a handshaking protocol, it does not matter whether we detect the asserted state of the asynchronous signal on one clock or the next, since the signal will be held asserted until it is acknowledged. Thus, you might think that this simple structure is enough to sample the signal accurately, which would be the case except for one small problem.

Asynchronous input

D

Clock

C

Q Flip-flop

Synchronous output

FIGURE B.11.5 A synchronizer built from a D flip-flop is used to sample an asynchronous signal to produce an output that is synchronous with the clock. This “synchronizer” will not work properly!

metastability A situation that occurs if a signal is sampled when it is not stable for the required setup and hold times, possibly causing the sampled value to fall in the indeterminate region between a high and low value.

The problem is a situation called metastability. Suppose the asynchronous signal is transitioning between high and low when the clock edge arrives. Clearly, it is not possible to know whether the signal will be latched as high or low. That problem we could live with. Unfortunately, the situation is worse: when the signal that is sampled is not stable for the required setup and hold times, the flip-flop may go into a metastable state. In such a state, the output will not have a legitimate high or low value, but will be in the indeterminate region between them. Furthermore,

B.13

the flip-flop is not guaranteed to exit this state in any bounded amount of time. Some logic blocks that look at the output of the flip-flop may see its output as 0, while others may see it as 1. This situation is called a synchronizer failure. In a purely synchronous system, synchronizer failure can be avoided by ensuring that the setup and hold times for a flip-flop or latch are always met, but this is impossible when the input is asynchronous. Instead, the only solution possible is to wait long enough before looking at the output of the flip-flop to ensure that its output is stable, and that it has exited the metastable state, if it ever entered it. How long is long enough? Well, the probability that the flip-flop will stay in the metastable state decreases exponentially, so after a very short time the probability that the flip-flop is in the metastable state is very low; however, the probability never reaches 0! So designers wait long enough such that the probability of a synchronizer failure is very low, and the time between such failures will be years or even thousands of years. For most flip-flop designs, waiting for a period that is several times longer than the setup time makes the probability of synchronization failure very low. If the clock rate is longer than the potential metastability period (which is likely), then a safe synchronizer can be built with two D flip-flops, as Figure B.11.6 shows. If you are interested in reading more about these problems, look into the references.

Asynchronous input

D

Q

D

C

synchronizer failure A situation in which a flip-flop enters a metastable state and where some logic blocks reading the output of the flip-flop see a 0 while others see a 1.

Synchronous output

Flip-flop

Flip-flop Clock

Q

B-77

Concluding Remarks

C

FIGURE B.11.6 This synchronizer will work correctly if the period of metastability that we wish to guard against is less than the clock period. Although the output of the first flip-flop may be metastable, it will not be seen by any other logic element until the second clock, when the second D flip-flop samples the signal, which by that time should no longer be in a metastable state.

Suppose we have a design with very large clock skew—longer than the register propagation time. Is it always possible for such a design to slow the clock down enough to guarantee that the logic operates properly? a. Yes, if the clock is slow enough the signals can always propagate and the design will work, even if the skew is very large. b. No, since it is possible that two registers see the same clock edge far enough apart that a register is triggered, and its outputs propagated and seen by a second register with the same clock edge.

Check Yourself propagation time The time required for an input to a flip-flop to propagate to the outputs of the flipflop.

B-78

Appendix B

B.12 field programmable devices (FPD) An integrated circuit containing combinational logic, and possibly memory devices, that are configurable by the end user.

programmable logic device (PLD) An integrated circuit containing combinational logic whose function is configured by the end user.

field programmable gate array (FPGA) A configurable integrated circuit containing both combinational logic blocks and flip-flops.

simple programmable logic device (SPLD) Programmable logic device, usually containing either a single PAL or PLA.

programmable array logic (PAL) Contains a programmable and-plane followed by a fixed orplane.

antifuse A structure in an integrated circuit that when programmed makes a permanent connection between two wires.

The Basics of Logic Design

Field Programmable Devices

Within a custom or semicustom chip, designers can make use of the flexibility of the underlying structure to easily implement combinational or sequential logic. How can a designer who does not want to use a custom or semicustom IC implement a complex piece of logic taking advantage of the very high levels of integration available? The most popular component used for sequential and combinational logic design outside of a custom or semicustom IC is a field programmable device (FPD). An FPD is an integrated circuit containing combinational logic, and possibly memory devices, that are configurable by the end user. FPDs generally fall into two camps: programmable logic devices (PLDs), which are purely combinational, and field programmable gate arrays (FPGAs), which provide both combinational logic and flip-flops. PLDs consist of two forms: simple PLDs (SPLDs), which are usually either a PLA or a programmable array logic (PAL), and complex PLDs, which allow more than one logic block as well as configurable interconnections among blocks. When we speak of a PLA in a PLD, we mean a PLA with user programmable and-plane and or-plane. A PAL is like a PLA, except that the or-plane is fixed. Before we discuss FPGAs, it is useful to talk about how FPDs are configured. Configuration is essentially a question of where to make or break connections. Gate and register structures are static, but the connections can be configured. Notice that by configuring the connections, a user determines what logic functions are implemented. Consider a configurable PLA: by determining where the connections are in the and-plane and the or-plane, the user dictates what logical functions are computed in the PLA. Connections in FPDs are either permanent or reconfigurable. Permanent connections involve the creation or destruction of a connection between two wires. Current FPLDs all use an antifuse technology, which allows a connection to be built at programming time that is then permanent. The other way to configure CMOS FPLDs is through a SRAM. The SRAM is downloaded at power-on, and the contents control the setting of switches, which in turn determines which metal lines are connected. The use of SRAM control has the advantage in that the FPD can be reconfigured by changing the contents of the SRAM. The disadvantages of the SRAM-based control are two fold: the configuration is volatile and must be reloaded on power-on, and the use of active transistors for switches slightly increases the resistance of such connections. FPGAs include both logic and memory devices, usually structured in a twodimensional array with the corridors dividing the rows and columns used for

B.14

Exercises

global interconnect between the cells of the array. Each cell is a combination of gates and flip-flops that can be programmed to perform some specific function. Because they are basically small, programmable RAMs, they are also called lookup tables (LUTs). Newer FPGAs contain more sophisticated building blocks such as pieces of adders and RAM blocks that can be used to build register files. A few large FPGAs even contain 32-bit RISC cores! In addition to programming each cell to perform a specific function, the interconnections between cells are also programmable, allowing modern FPGAs with hundreds of blocks and hundreds of thousands of gates to be used for complex logic functions. Interconnect is a major challenge in custom chips, and this is even more true for FPGAs, because cells do not represent natural units of decomposition for structured design. In many FPGAs, 90% of the area is reserved for interconnect and only 10% is for logic and memory blocks. Just as you cannot design a custom or semicustom chip without CAD tools, you also need them for FPDs. Logic synthesis tools have been developed that target FPGAs, allowing the generation of a system using FPGAs from structural and behavioral Verilog.

B.13

Concluding Remarks

This appendix introduces the basics of logic design. If you have digested the material in this appendix, you are ready to tackle the material in Chapters 4 and 5, both of which use the concepts discussed in this appendix extensively.

Further Reading There are a number of good texts on logic design. Here are some you might like to look into. Ciletti, M. D. [2002]. Advanced Digital Design with the Verilog HDL, Englewood Cliffs, NJ: Prentice Hall. A thorough book on logic design using Verilog. Katz, R. H. [2004]. Modern Logic Design, 2nd ed., Reading, MA: Addison-Wesley. A general text on logic design. Wakerly, J. F. [2000]. Digital Design: Principles and Practices, 3rd ed., Englewood Cliffs, NJ: Prentice Hall. A general text on logic design.

B-79

lookup tables (LUTs) In a field programmable device, the name given to the cells because they consist of a small amount of logic and RAM.

B-80

Appendix B

B.14

The Basics of Logic Design

Exercises

B.1 [10] ⬍§B.2⬎ In addition to the basic laws we discussed in this section, there are two important theorems, called DeMorgan’s theorems: A

B

A B and A B

A

B

Prove DeMorgan’s theorems with a truth table of the form A

B

A

B

A+B

A˙B

A˙B

A+B

0

0

1

1

1

1

1

1

0 1 1

1 0 1

1 0 0

0 1 0

0 0 0

0 0 0

1 1 0

1 1 0

B.2 [15] ⬍§B.2⬎ Prove that the two equations for E in the example starting on page B-7 are equivalent by using DeMorgan’s theorems and the axioms shown on page B-7. B.3 [10] ⬍§B.2⬎ Show that there are 2n entries in a truth table for a function with n inputs. B.4 [10] ⬍§B.2⬎ One logic function that is used for a variety of purposes (including within adders and to compute parity) is exclusive OR. The output of a two-input exclusive OR function is true only if exactly one of the inputs is true. Show the truth table for a two-input exclusive OR function and implement this function using AND gates, OR gates, and inverters. B.5 [15] ⬍§B.2⬎ Prove that the NOR gate is universal by showing how to build the AND, OR, and NOT functions using a two-input NOR gate. B.6 [15] ⬍§B.2⬎ Prove that the NAND gate is universal by showing how to build the AND, OR, and NOT functions using a two-input NAND gate. B.7 [10] ⬍§§B.2, B.3⬎ Construct the truth table for a four-input odd-parity function (see page B-65 for a description of parity). B.8 [10] ⬍§§B.2, B.3⬎ Implement the four-input odd-parity function with AND and OR gates using bubbled inputs and outputs. B.9 [10] ⬍§§B.2, B.3⬎ Implement the four-input odd-parity function with a PLA.

B.14

Exercises

B.10 [15] ⬍§§B.2, B.3⬎ Prove that a two-input multiplexor is also universal by showing how to build the NAND (or NOR) gate using a multiplexor. B.11 [5] ⬍§§4.2, B.2, B.3⬎ Assume that X consists of 3 bits, x2 x1 x0. Write four logic functions that are true if and only if ■

X contains only one 0



X contains an even number of 0s



X when interpreted as an unsigned binary number is less than 4



X when interpreted as a signed (two’s complement) number is negative

B.12 [5] ⬍§§4.2, B.2, B.3⬎ Implement the four functions described in Exercise B.11 using a PLA. B.13 [5] ⬍§§4.2, B.2, B.3⬎ Assume that X consists of 3 bits, x2 x1 x0, and Y consists of 3 bits, y2 y1 y0. Write logic functions that are true if and only if ■

X ⬍ Y, where X and Y are thought of as unsigned binary numbers



X ⬍ Y, where X and Y are thought of as signed (two’s complement) numbers



X⫽Y

Use a hierarchical approach that can be extended to larger numbers of bits. Show how can you extend it to 6-bit comparison. B.14 [5] ⬍§§B.2, B.3⬎ Implement a switching network that has two data inputs (A and B), two data outputs (C and D), and a control input (S). If S equals 1, the network is in pass-through mode, and C should equal A, and D should equal B. If S equals 0, the network is in crossing mode, and C should equal B, and D should equal A. B.15 [15] ⬍§§B.2, B.3⬎ Derive the product-of-sums representation for E shown on page B-11 starting with the sum-of-products representation. You will need to use DeMorgan’s theorems. B.16 [30] ⬍§§B.2, B.3⬎ Give an algorithm for constructing the sum-of- products representation for an arbitrary logic equation consisting of AND, OR, and NOT. The algorithm should be recursive and should not construct the truth table in the process. B.17 [5] ⬍§§B.2, B.3⬎ Show a truth table for a multiplexor (inputs A, B, and S; output C ), using don’t cares to simplify the table where possible.

B-81

B-82

Appendix B

The Basics of Logic Design

B.18 [5] ⬍§B.3⬎ What is the function implemented by the following Verilog modules: module FUNC1 (I0, I1, S, out); input I0, I1; input S; output out; out = S? I1: I0; endmodule module FUNC2 (out,ctl,clk,reset); output [7:0] out; input ctl, clk, reset; reg [7:0] out; always @(posedge clk) if (reset) begin out <= 8’b0 ; end else if (ctl) begin out <= out + 1; end else begin out <= out - 1; end endmodule

B.19 [5] ⬍§B.4⬎ The Verilog code on page B-53 is for a D flip-flop. Show the Verilog code for a D latch. B.20 [10] ⬍§§B.3, B.4⬎ Write down a Verilog module implementation of a 2-to-4 decoder (and/or encoder). B.21 [10] ⬍§§B.3, B.4⬎ Given the following logic diagram for an accumulator, write down the Verilog module implementation of it. Assume a positive edgetriggered register and asynchronous Rst.

B.14

In

Exercises



Adder 16

16 Out

Load Clk Rst Register Load

B.22 [20] ⬍§§B3, B.4, B.5⬎ Section 3.3 presents basic operation and possible implementations of multipliers. A basic unit of such implementations is a shiftand-add unit. Show a Verilog implementation for this unit. Show how can you use this unit to build a 32-bit multiplier. B.23 [20] ⬍§§B3, B.4, B.5⬎ Repeat Exercise B.22, but for an unsigned divider rather than a multiplier. B.24 [15] ⬍§B.5⬎ The ALU supported set on less than (slt) using just the sign bit of the adder. Let’s try a set on less than operation using the values ⫺7ten and 6ten. To make it simpler to follow the example, let’s limit the binary representations to 4 bits: 1001two and 0110two. 1001two – 0110two = 1001two + 1010two = 0011two

This result would suggest that ⫺7 ⬎ 6, which is clearly wrong. Hence, we must factor in overflow in the decision. Modify the 1-bit ALU in Figure B.5.10 on page B-33 to handle slt correctly. Make your changes on a photocopy of this figure to save time. B.25 [20] ⬍§B.6⬎ A simple check for overflow during addition is to see if the CarryIn to the most significant bit is not the same as the CarryOut of the most significant bit. Prove that this check is the same as in Figure 3.2. B.26 [5] ⬍§B.6⬎ Rewrite the equations on page B-44 for a carry-lookahead logic for a 16-bit adder using a new notation. First, use the names for the CarryIn signals of the individual bits of the adder. That is, use c4, c8, c12, … instead of C1, C2, C3, …. In addition, let Pi,j; mean a propagate signal for bits i to j, and Gi,j; mean a generate signal for bits i to j. For example, the equation C2

G1

(P1 G0)

(P1 P0 c0)

B-83

B-84

Appendix B

The Basics of Logic Design

can be rewritten as c8

G7 , 4

(P7, 4 G3,0 )

(P7, 4 P3,0 c0)

This more general notation is useful in creating wider adders. B.27 [15] ⬍§B.6⬎ Write the equations for the carry-lookahead logic for a 64bit adder using the new notation from Exercise B.26 and using 16-bit adders as building blocks. Include a drawing similar to Figure B.6.3 in your solution. B.28 [10] ⬍§B.6⬎ Now calculate the relative performance of adders. Assume that hardware corresponding to any equation containing only OR or AND terms, such as the equations for pi and gi on page B-40, takes one time unit T. Equations that consist of the OR of several AND terms, such as the equations for c1, c2, c3, and c4 on page B-40, would thus take two time units, 2T. The reason is it would take T to produce the AND terms and then an additional T to produce the result of the OR. Calculate the numbers and performance ratio for 4-bit adders for both ripple carry and carry lookahead. If the terms in equations are further defined by other equations, then add the appropriate delays for those intermediate equations, and continue recursively until the actual input bits of the adder are used in an equation. Include a drawing of each adder labeled with the calculated delays and the path of the worst-case delay highlighted. B.29 [15] ⬍§B.6⬎ This exercise is similar to Exercise B.28, but this time calculate the relative speeds of a 16-bit adder using ripple carry only, ripple carry of 4-bit groups that use carry lookahead, and the carry-lookahead scheme on page B-39. B.30 [15] ⬍§B.6⬎ This exercise is similar to Exercises B.28 and B.29, but this time calculate the relative speeds of a 64-bit adder using ripple carry only, ripple carry of 4-bit groups that use carry lookahead, ripple carry of 16-bit groups that use carry lookahead, and the carry-lookahead scheme from Exercise B.27. B.31 [10] ⬍§B.6⬎ Instead of thinking of an adder as a device that adds two numbers and then links the carries together, we can think of the adder as a hardware device that can add three inputs together (ai, bi, ci) and produce two outputs (s, ci ⫹ 1). When adding two numbers together, there is little we can do with this observation. When we are adding more than two operands, it is possible to reduce the cost of the carry. The idea is to form two independent sums, called S⬘ (sum bits) and C⬘ (carry bits). At the end of the process, we need to add C⬘ and S⬘ together using a normal adder. This technique of delaying carry propagation until the end of a sum of numbers is called carry save addition. The block drawing on the lower right of Figure B.14.1 (see below) shows the organization, with two levels of carry save adders connected by a single normal adder. Calculate the delays to add four 16-bit numbers using full carry-lookahead adders versus carry save with a carry-lookahead adder forming the final sum. (The time unit T in Exercise B.28 is the same.)

B.14

a3 b3

a2 b2

a1 b1

a0 b0 A



⫹ e3

f3



f1



E

Traditional adder

e0



f2





e1





B





e2

Traditional adder

f0 ⫹



Traditional adder

S s4

s5

s3

s2

s1

s0

b3 e3 f3 b2 e2 f2 b1 e1 f1 b0 e0 f0

⫹ a3





⫹ a1

a2

A



B

s'3 c'2

F

Carry save adder



s'2 c'1

s'1 c'0



Carry save adder C'

s'4 c'3

E

a0



S'

s'0 Traditional adder









s4

s3

s2

s1

S s5

B-85

Exercises

s0

FIGURE B.14.1 Traditional ripple carry and carry save addition of four 4-bit numbers. The details are shown on the left, with the individual signals in lowercase, and the corresponding higher-level blocks are on the right, with collective signals in upper case. Note that the sum of four n-bit numbers can take n + 2 bits.

B.32 [20] ⬍§B.6⬎ Perhaps the most likely case of adding many numbers at once in a computer would be when trying to multiply more quickly by using many adders to add many numbers in a single clock cycle. Compared to the multiply algorithm in Chapter 3, a carry save scheme with many adders could multiply more than 10 times faster. This exercise estimates the cost and speed of a combinational multiplier to multiply two positive 16-bit numbers. Assume that you have 16 intermediate terms M15, M14, …, M0, called partial products, that contain the multiplicand ANDed with multiplier bits m15, m14, …, m0. The idea is to use carry save adders to reduce the n operands into 2n/3 in parallel groups of three, and do this repeatedly until you get two large numbers to add together with a traditional adder.

F

B-86

Appendix B

The Basics of Logic Design

First, show the block organization of the 16-bit carry save adders to add these 16 terms, as shown on the right in Figure B.14.1. Then calculate the delays to add these 16 numbers. Compare this time to the iterative multiplication scheme in Chapter 3 but only assume 16 iterations using a 16-bit adder that has full carry lookahead whose speed was calculated in Exercise B.29. B.33 [10] ⬍§B.6⬎ There are times when we want to add a collection of numbers together. Suppose you wanted to add four 4-bit numbers (A, B, E, F) using 1-bit full adders. Let’s ignore carry lookahead for now. You would likely connect the 1-bit adders in the organization at the top of Figure B.14.1. Below the traditional organization is a novel organization of full adders. Try adding four numbers using both organizations to convince yourself that you get the same answer. B.34 [5] ⬍§B.6⬎ First, show the block organization of the 16-bit carry save adders to add these 16 terms, as shown in Figure B.14.1. Assume that the time delay through each 1-bit adder is 2T. Calculate the time of adding four 4-bit numbers to the organization at the top versus the organization at the bottom of Figure B.14.1. B.35 [5] ⬍§B.8⬎ Quite often, you would expect that given a timing diagram containing a description of changes that take place on a data input D and a clock input C (as in Figures B.8.3 and B.8.6 on pages B-52 and B-54, respectively), there would be differences between the output waveforms (Q) for a D latch and a D flipflop. In a sentence or two, describe the circumstances (e.g., the nature of the inputs) for which there would not be any difference between the two output waveforms. B.36 [5] ⬍§B.8⬎ Figure B.8.8 on page B-55 illustrates the implementation of the register file for the MIPS datapath. Pretend that a new register file is to be built, but that there are only two registers and only one read port, and that each register has only 2 bits of data. Redraw Figure B.8.8 so that every wire in your diagram corresponds to only 1 bit of data (unlike the diagram in Figure B.8.8, in which some wires are 5 bits and some wires are 32 bits). Redraw the registers using D flipflops. You do not need to show how to implement a D flip-flop or a multiplexor. B.37 [10] ⬍§B.10⬎ A friend would like you to build an “electronic eye” for use as a fake security device. The device consists of three lights lined up in a row, controlled by the outputs Left, Middle, and Right, which, if asserted, indicate that a light should be on. Only one light is on at a time, and the light “moves” from left to right and then from right to left, thus scaring away thieves who believe that the device is monitoring their activity. Draw the graphical representation for the finite-state machine used to specify the electronic eye. Note that the rate of the eye’s movement will be controlled by the clock speed (which should not be too great) and that there are essentially no inputs. B.38 [10] ⬍§B.10⬎ Assign state numbers to the states of the finite-state machine you constructed for Exercise B.37 and write a set of logic equations for each of the outputs, including the next-state bits.

B.14

Exercises

B-87

B.39 [15] ⬍§§B.2, B.8, B.10⬎ Construct a 3-bit counter using three D flipflops and a selection of gates. The inputs should consist of a signal that resets the counter to 0, called reset, and a signal to increment the counter, called inc. The outputs should be the value of the counter. When the counter has value 7 and is incremented, it should wrap around and become 0. B.40 [20] ⬍§B.10⬎ A Gray code is a sequence of binary numbers with the property that no more than 1 bit changes in going from one element of the sequence to another. For example, here is a 3-bit binary Gray code: 000, 001, 011, 010, 110, 111, 101, and 100. Using three D flip-flops and a PLA, construct a 3-bit Gray code counter that has two inputs: reset, which sets the counter to 000, and inc, which makes the counter go to the next value in the sequence. Note that the code is cyclic, so that the value after 100 in the sequence is 000. B.41 [25] ⬍§B.10⬎ We wish to add a yellow light to our traffic light example on page B-68. We will do this by changing the clock to run at 0.25 Hz (a 4-second clock cycle time), which is the duration of a yellow light. To prevent the green and red lights from cycling too fast, we add a 30-second timer. The timer has a single input, called TimerReset, which restarts the timer, and a single output, called TimerSignal, which indicates that the 30-second period has expired. Also, we must redefine the traffic signals to include yellow. We do this by defining two out put signals for each light: green and yellow. If the output NSgreen is asserted, the green light is on; if the output NSyellow is asserted, the yellow light is on. If both signals are off, the red light is on. Do not assert both the green and yellow signals at the same time, since American drivers will certainly be confused, even if European drivers understand what this means! Draw the graphical representation for the finite-state machine for this improved controller. Choose names for the states that are different from the names of the outputs. B.42 [15] ⬍§B.10⬎ Write down the next-state and output-function tables for the traffic light controller described in Exercise B.41. B.43 [15] ⬍§§B.2, B.10⬎ Assign state numbers to the states in the traf-fic light example of Exercise B.41 and use the tables of Exercise B.42 to write a set of logic equations for each of the outputs, including the next-state outputs. B.44 [15] ⬍§§B.3, B.10⬎ Implement the logic equations of Exercise B.43 as a PLA. §B.2, page B-8: No. If A ⫽ 1, C ⫽ 1, B ⫽ 0, the first is true, but the second is false. §B.3, page B-20: C. §B.4, page B-22: They are all exactly the same. §B.4, page B-26: A ⫽ 0, B ⫽ 1. §B.5, page B-38: 2. §B.6, page B-47: 1. §B.8, page B-58: c. §B.10, page B-72: b. §B.11, page B-77: b.

Answers to Check Yourself

C

a  p  p  e  n  d  i  c  e

La grafica e il calcolo con la GPU A cura di John Nickolls, direttore di ricerca di NVIDIA, e David Kirk, direttore di progetto di NVIDIA

L’immaginazione è più importante della conoscenza. Albert Einstein, On Science, anni Trenta.

C.1 * Introduzione Unità di elaborazione grafica (GPU): processore ottimizzato per la grafica 2D e 3D, la riproduzione video e l’elaborazione visuale. Elaborazione visuale (visual computing): combinazione di elaborazione grafica e calcolo che permette all’utente di interagire visivamente con i risultati dell’elaborazione computazionale attraverso grafici, immagini e video. Sistema eterogeneo: un sistema che combina differenti tipi di processore. Un PC è un sistema eterogeneo CPU–GPU.

Questa appendice tratta la GPU, ossia l’unità di elaborazione grafica (graphics processing unit), presente in ogni PC, calcolatore portatile o desktop, e in ogni workstation. Nella sua forma più semplice, la GPU permette di visualizzare grafici 2D e 3D, immagini e video e rende possibili i sistemi operativi a finestre, le interfacce utente di tipo grafico, i videogiochi, le applicazioni visuali e la rappresentazione di sequenze video. La GPU moderna qui descritta è un multiprocessore altamente parallelo e multithread, ottimizzato per l’elaborazione visuale. Per fornire un’interazione visuale in tempo reale con i dati elaborati attraverso grafica, immagini e video, la GPU è basata su un’architettura unificata di grafica e calcolo che funge sia da processore grafico programmabile sia da piattaforma di calcolo parallela e scalabile. Nei PC e nelle console per videogiochi la GPU viene aggiunta alla CPU, quindi forma un sistema eterogeneo.

Breve storia delle GPU Quindici anni fa le GPU non esistevano. I grafici sui PC venivano generati da un controllore vettoriale di video e grafica (VGA, Video and Graphics Array controller). Si trattava semplicemente di un controllore della memoria e di un generatore dei segnali per i terminali grafici connessi a una memoria DRAM. Negli anni Novanta la tecnologia dei semiconduttori si era evoluta a tal punto da permettere di aggiungere altre funzioni al controllore VGA. Già nel 1997 i controllori VGA cominciavano a incorporare alcune funzioni di accelerazione

© 978-88-08-06279-6

C.1  Introduzione

C3

della grafica tridimensionale (3D), come la preparazione dei triangoli, la rasterizzazione (la suddivisione dei triangoli nei singoli pixel in essi contenuti), l’applicazione della tessitura (cioè l’applicazione di motivi grafici ai triangoli) e lo shading (l’applicazione delle tonalità di colore). Nel 2000 il chip di un processore grafico incorporava quasi tutti gli elementi di una pipeline grafica tipica delle tradizionali workstation di fascia alta e per questo si meritò un nome nuovo, che indicasse un dispositivo che si spingeva oltre le funzionalità di base di un controllore VGA. Il termine «GPU» fu coniato per sottolineare che il dispositivo grafico era diventato un processore vero e proprio. Col passare del tempo le GPU divennero più programmabili, perché i processori programmabili sostituirono i circuiti logici (in grado di generare solamente le funzioni prefissate), pur mantenendo l’organizzazione di base della pipeline grafica 3D. In aggiunta, i calcoli diventarono man mano più precisi, passando dall’aritmetica indicizzata all’aritmetica intera e decimale in virgola fissa, in seguito all’aritmetica in virgola mobile in singola precisione e, recentemente, all’aritmetica in virgola mobile in doppia precisione. Le GPU sono diventate processori programmabili ad alto livello di parallelismo, con centinaia di unità di elaborazione (core) e migliaia di thread. Recentemente ai processori sono state aggiunte istruzioni e strutture hardware di memoria per poter supportare linguaggi di programmazione di utilizzo generale; è stato inoltre implementato un ambiente di programmazione per consentire di programmare le GPU utilizzando linguaggi familiari, come C e C++. Questa innovazione ha reso le GPU dei processori multicore completamente programmabili e di utilizzo generale, seppure con certe limitazioni. Tendenze delle GPU grafiche Le GPU e i driver a loro associati implementano i modelli di elaborazione grafica OpenGL e DirectX. OpenGL è uno standard aperto di programmazione grafica 3D disponibile per la maggior parte dei calcolatori. DirectX è un insieme di interfacce di programmazione multimediale di Microsoft che include Direct3D per la grafica tridimensionale. Poiché queste interfacce di programmazione delle applicazioni (API, Application Programming Interfaces) hanno un comportamento ben definito, è possibile ottenere un’efficace accelerazione hardware delle funzioni di elaborazione grafica definite. Questa è una delle ragioni (oltre alla crescente densità dei dispositivi) per cui ogni 12–18 mesi vengono sviluppate nuove GPU con prestazioni doppie rispetto alla generazione precedente. Il continuo miglioramento delle prestazioni delle GPU permette di creare applicazioni che in passato non erano possibili. L’intersezione tra elaborazione grafica e calcolo parallelo ha portato allo sviluppo di un nuovo paradigma per la grafica, conosciuto come elaborazione visuale. In questo paradigma, parti consistenti del modello tradizionale della pipeline grafica hardware sequenziale vengono sostituite con elementi programmabili per la geometria, i vertici e i pixel. L’elaborazione visuale in una GPU moderna combina elaborazione grafica e calcolo parallelo secondo nuove modalità, che permettono di implementare algoritmi diversi e aprono la strada a una nuova generazione di applicazioni che sfruttano l’elaborazione parallela su GPU ad alte prestazioni.

Sistemi eterogenei Benché la GPU all’interno di un comune PC sia il processore più potente e con il più alto grado di parallelismo, essa non può essere l’unico processore. La

Interfaccia di programmazione delle applicazioni (API): un insieme di definizioni di funzioni e strutture dati che fornisce un’interfaccia a una libreria di funzioni.

C4

Appendice C  La grafica e il calcolo con la GPU

© 978-88-08-06279-6

CPU, attualmente multicore (e presto con un numero di core molto più elevato di quello odierno), è un processore principalmente seriale che si affianca a una GPU con moltissimi core e massicciamente parallela. Insieme, questi due tipi di processore costituiscono un sistema eterogeneo multiprocessore. Per molte applicazioni, le prestazioni migliori si ottengono utilizzando sia la CPU sia la GPU. Questa appendice ha lo scopo di aiutarvi a comprendere quando e come ripartire al meglio il lavoro tra questi due processori caratterizzati da un livello di parallelismo sempre più elevato.

Le GPU evolvono verso i processori paralleli scalabili Dal punto di vista funzionale, le GPU si sono evolute dai controllori VGA cablati, di limitate capacità, ai processori paralleli programmabili. Questa evoluzione è avvenuta modificando la pipeline grafica logica (basata su API) sia incorporando elementi programmabili sia rendendo gli stadi della pipeline hardware sottostante meno specializzati e più programmabili. Alla fine di questo processo, è risultato conveniente unire i diversi elementi della pipeline programmabile in un’unica schiera di svariati processori programmabili. Nella generazione di GPU GeForce, serie 8, l’elaborazione della geometria, dei vertici e dei pixel è effettuata sullo stesso tipo di processore. Questa unificazione permette una scalabilità estrema, e la presenza di più core programmabili aumenta le prestazioni globali del sistema. Inoltre, l’unificazione dei processori fornisce anche un bilanciamento molto efficace del carico, poiché ogni funzione di calcolo può usare l’intero insieme di processori. Per contro, oggi un insieme di processori può essere costruito con pochissimi processori, poiché tutte le funzioni possono essere eseguite sulle stesse unità.

Perché CUDA e perché utilizzare le GPU per il calcolo?

Calcolo su GPU (GPU computing): utilizzo della GPU per effettuare calcoli mediante linguaggi di programmazione parallela e API. GPGPU: utilizzo di una GPU per calcoli generici attraverso le API grafiche e la pipeline grafica tradizionale. CUDA: modello e linguaggio di programmazione parallela scalabile basato su C/C++. È una piattaforma di programmazione parallela per le GPU e le CPU multicore.

Questa schiera uniforme e scalabile di processori suggerisce un nuovo modello di programmazione delle GPU. La consistente potenza di calcolo in virgola mobile offerta ha reso le GPU appetibili per risolvere problemi non grafici. Dato il notevole grado di parallelismo e di scalabilità della schiera dei processori per le applicazioni grafiche, il modello di programmazione per il calcolo più generale deve esprimere direttamente il massiccio parallelismo, consentendo comunque un’esecuzione scalabile. Calcolo su GPU (GPU computing) è il termine coniato per definire l’utilizzo della GPU per l’elaborazione attraverso un linguaggio di programmazione parallelo e particolari API, senza ricorrere alle API grafiche tradizionali e al modello della pipeline grafica. Ciò è in contrasto con il precedente approccio, ossia l’elaborazione per utilizzo generale su GPU (GPGPU, General Purpose computation on GPU), che consiste nel programmare le GPU utilizzando le API e la pipeline grafiche per svolgere compiti non grafici. CUDA (Compute Unified Device Architecture) è un modello di programmazione parallela scalabile e una piattaforma software per la GPU e per altri processori paralleli che permette al programmatore di non dover utilizzare le API grafiche e le interfacce grafiche della GPU, ma di programmare direttamente in C o C++. Il modello di programmazione CUDA ha un approccio SPMD («singolo programma, dati multipli»), in cui il programmatore scrive un programma per un thread e questo programma viene istanziato ed eseguito da molti thread in parallelo sui molteplici processori della GPU. Di fatto, CUDA fornisce anche un mezzo per programmare core multipli di CPU, per cui è un ambiente che permette di scrivere programmi paralleli per l’intero sistema eterogeneo che costituisce il calcolatore.

© 978-88-08-06279-6

C.1  Introduzione

La GPU unifica grafica e calcolo Con l’aggiunta di CUDA e delle capacità di calcolo delle GPU, è ora possibile utilizzare contemporaneamente la GPU sia come processore grafico sia come processore di calcolo e combinare queste due modalità di utilizzo in applicazioni di elaborazione visuale. L’architettura di elaborazione sottostante la GPU può essere descritta in due modi: attraverso l’implementazione di API grafiche programmabili e come un insieme di processori con un alto grado di parallelismo programmabili in C/C++ con CUDA. Nonostante i processori elementari della GPU siano tutti uguali, non è necessario che i programmi dei thread SPMD siano gli stessi. La GPU può eseguire programmi di elaborazione grafica tipici di una GPU, chiamati comunemente shader, sui diversi elementi della grafica (geometria, vertici e pixel), ma anche eseguire thread di programmi diversi in CUDA. La GPU è un’architettura multiprocessore realmente versatile, in grado di effettuare una grande varietà di elaborazioni. Le GPU eccellono nell’elaborazione grafica e visuale, essendo state progettate specificamente per questo tipo di applicazione, ma risultano eccellenti anche per molte altre applicazioni ad alte prestazioni di tipo generale che sono «cugine prime» delle applicazioni grafiche, in quanto devono svolgere una grande quantità di calcoli in parallelo e si basano su molte strutture regolari. In generale, le GPU sono particolarmente adatte a problemi caratterizzati da un elevato livello di parallelismo nei dati (si veda il Capitolo 7) e a problemi di dimensioni assai rilevanti, mentre sono meno indicate per problemi più piccoli e meno regolari.

Applicazioni di elaborazione visuale su GPU L’elaborazione visuale comprende le diverse tipologie di applicazioni grafiche tradizionali e molte applicazioni nuove. L’ambito originario di una GPU era «qualsiasi cosa fosse collegato ai pixel», mentre ora include molte applicazioni non basate sui pixel ma su calcoli e/o strutture dati regolari. Le GPU sono efficaci per la grafica 2D e 3D, essendo questo lo scopo per cui sono state progettate. La grafica 2D e 3D, come abbiamo già detto, utilizza la GPU nella sua «modalità grafica», accedendo alla potenza di calcolo della GPU attraverso le API grafiche OpenGL e DirectX. I videogiochi sono basati sulla capacità di elaborazione grafica 3D. Oltre alla grafica 2D e 3D, altre applicazioni importanti per la GPU sono l’elaborazione di immagini e video. Queste possono essere implementate utilizzando le API grafiche, oppure attraverso programmi di calcolo (impiegando CUDA per programmare la GPU nella modalità di calcolo). Con CUDA l’elaborazione delle immagini diventa semplicemente l’esecuzione di programmi vettoriali su dati paralleli. Nella misura in cui l’accesso ai dati è regolare e presenta buone caratteristiche di località, il programma sarà efficiente. L’elaborazione delle immagini di fatto è un’applicazione molto adatta alle GPU. L’elaborazione video, specialmente la codifica e decodifica (compressione e decompressione mediante algoritmi standard), è anch’essa particolarmente efficiente. La caratteristica più importante per le applicazioni di elaborazione visuale su GPU è di poter «rompere la pipeline grafica». Le prime GPU implementavano soltanto API specifiche per la grafica, sebbene ad altissime prestazioni. Ciò andava bene se le API dovevano supportare solamente le operazioni desiderate; in caso contrario, la GPU non era in grado di accelerare l’esecuzione, perché le sue funzionalità erano prefissate e immutabili. Ora, con l’avvento di CUDA e del calcolo su GPU, le GPU possono essere programmate per implementare una pipeline virtuale differente semplicemente scrivendo un

C5

C6

Appendice C  La grafica e il calcolo con la GPU

© 978-88-08-06279-6

programma CUDA che descriva il calcolo e il flusso dati desiderato. Qualsiasi applicazione è quindi ora possibile, e ciò sarà di stimolo per tentare nuovi approcci all’elaborazione visuale.

C.2 * Le architetture dei sistemi GPU In questo paragrafo prenderemo in considerazione le architetture dei sistemi GPU attualmente più diffusi: esamineremo le configurazioni di questi sistemi, le funzioni e i servizi offerti, le interfacce standard di programmazione e l’architettura interna di base delle GPU.

Architettura del sistema eterogeneo CPU-GPU L’architettura di un sistema di calcolo eterogeneo che utilizza una GPU e una CPU può essere descritta ad alto livello prendendo in considerazione due caratteristiche principali: anzitutto il numero di sottosistemi funzionali e/o chip che sono utilizzati e il tipo di tecnologia e topologia di interconnessione; la seconda caratteristica è rappresentata dai sottosistemi di memoria disponibili per questi sottosistemi funzionali. Si veda il Capitolo 6 per una descrizione generale dei sistemi di I/O e dei chipset dei PC. Il PC storico (1990 circa) La Figura C.2.1 mostra un diagramma a blocchi ad alto livello di un PC degli anni Novanta. Il north bridge (si veda il capitolo 6) contiene le interfacce a banda larga che connettono la CPU, la memoria e il bus PCI. Il south bridge contiene interfacce e dispositivi ormai obsoleti: il bus ISA (audio, LAN), il controllore degli interrupt, il controllore DMA, il temporizzatore/contatore. In questo sistema, il terminale grafico veniva pilotato da un semplice sottosistema con memoria video (framebuffer) noto come VGA (video graphics array) che era collegato al bus PCI. Sottosistemi grafici con moduli di elaborazione integrati (GPU) non esistevano nei PC del 1990. La figura C.2.2 illustra due configurazioni attualmente assai diffuse. Queste sono caratterizzate da una GPU (GPU discreta) e una CPU separate, Figura C.2.1. PC storico. Il controllore VGA gestisce la visualizzazione grafica mediante la memoria video (frame buffer).

CPU Front Side Bus

North Bridge

Memoria

bus PCI

South Bridge

LAN

UART

controllore VGA Terminale grafico VGA

memoria video (framebuffer)

© 978-88-08-06279-6 a.

b.

CPU Intel Connessione PCI-Express 16x

Terminale grafico

GPU

CPU AMD

Front Side Bus

North Bridge

Connessione PCI-Express 4x secondaria memoria GPU

C7

C.2  Le architetture dei sistemi GPU

core CPU 128-bit 667 MT/s

bus interno Memoria DDR2

North Bridge

128-bit 667 MT/s

Connessione PCI-Express 16x

South Bridge Terminale grafico

GPU

memoria DDR2

HyperTransport 1.03

Chipset

memoria GPU

Figura C.2.2. PC contemporanei con CPU Intel e AMD. Si veda il Capitolo 6 per una descrizione dei componenti e delle interconnessioni riportate in figura.

con i rispettivi sottosistemi di memoria. Nella figura C.2.2a è riportata lo schema di una CPU Intel, alla quale è connessa la GPU mediante un collegamento PCI-Express 2.0 a 16 vie che fornisce una velocità di trasferimento di picco di 16 GB/s (8 GB/s per ogni direzione). Analogamente, nella figura C.2.2b è mostrato lo schema di una CPU AMD nella quale la GPU è connessa al chipset, anche in questo caso mediante PCI-Express, con la stessa larghezza di banda. In entrambi i casi, GPU e CPU possono accedere alla memoria l’una dell’altra, seppure con una larghezza di banda di trasferimento minore che nel caso di accesso diretto. Nel sistema AMD, il north bridge, o controllore di memoria, è integrato sullo stesso chip della CPU. Una variante a basso costo di questi sistemi è rappresentata dai sistemi ad architettura di memoria unificata (UMA, Unified Memory Architecture), in cui non è presente la memoria della GPU e si utilizza soltanto la memoria di sistema della CPU. Questi sistemi adottano GPU con prestazioni relativamente basse, poiché le prestazioni sono comunque limitate dalla banda della memoria di sistema e dalla maggiore latenza di accesso alla memoria (la memoria associata alla GPU garantisce invece banda larga e bassa latenza). Una variante ad alte prestazioni utilizza GPU multiple interconnesse, tipicamente da due a quattro operanti in parallelo, con i loro terminali grafici collegati in sequenza (in daisy-chain); ne è un esempio il sistema multi-GPU SLI (Scalable Link Interconnect) di NVIDIA, progettato per applicazioni di videogioco ad alte prestazioni e per le workstation. La successiva categoria di sistemi ha una struttura con GPU integrata nel north bridge (Intel) o nel chipset (AMD), con o senza memoria grafica dedicata. Nel Capitolo 5 è stato spiegato come le memorie cache mantengano la coerenza quando lo spazio degli indirizzi è condiviso; essendoci ora una CPU e una GPU, gli spazi di indirizzamento diventano multipli. Le GPU possono accedere alla propria memoria locale e alla memoria fisica di sistema della CPU utilizzando indirizzi virtuali che vengono tradotti da un’unità dedicata di gestione della memoria (MMU, Memory Management Unit) a bordo della GPU. Sarà il kernel del sistema operativo a gestire le tabelle delle pagine della GPU: si può accedere a una pagina fisica della memoria di sistema mediante transazioni sulla connessione PCI-Express coerenti o non coerenti, a seconda

PCI-Express (PCIe): un sistema di interconnessione standard di I/O che utilizza collegamenti da punto a punto. Le connessioni hanno un numero di linee e una larghezza di banda configurabili. Architettura con memoria unificata (UMA): un’architettura di sistema in cui CPU e GPU condividono la stessa memoria di sistema. [N.d.R.: qui il termine UMA viene utilizzato in modo diverso rispetto al Capitolo 7, dove indicava la memoria ad accesso uniforme nei multiprocessori].

C8

Appendice C  La grafica e il calcolo con la GPU

© 978-88-08-06279-6

del valore di un attributo specificato nella tabella delle pagine della GPU. La CPU può accedere alla memoria locale della GPU attraverso un intervallo di indirizzi (chiamato anche «apertura») nello spazio di indirizzamento del bus PCI-Express. Console di videogiochi I sistemi basati su console come la Playstation 3 di Sony e l’Xbox 360 di Microsoft hanno un’architettura di sistema simile a quella di un PC sopra descritta. Le console sono progettate per essere prodotte con le stesse prestazioni e funzionalità per un periodo di tempo che può arrivare a cinque anni. Durante questo periodo un sistema può essere riprogettato più volte per sfruttare processi produttivi del silicio più avanzati, e di conseguenza fornire prestazioni costanti a costi sempre inferiori. Le console non hanno la necessità di dover essere espanse e aggiornate come i PC, per cui i principali bus interni del sistema tendono a essere progettati ad hoc piuttosto che standardizzati.

Interfacce e driver delle GPU AGP: una versione estesa del bus PCI di I/O originale, in grado di fornire a una singola scheda fino a otto volte la banda del bus PCI. La sua funzione principale era di connettere i sottosistemi grafici alle architetture dei PC.

In un PC attuale le GPU sono connesse alla CPU attraverso il bus PCI-Express, mentre le generazioni precedenti utilizzavano il bus AGP. Le applicazioni grafiche chiamano funzioni delle API OpenGL (Segal e Akekey, 2006) o DirectX (Microsoft DirectX Specification), le quali utilizzano la GPU come un coprocessore. Le API inviano comandi, programmi e dati alla GPU mediante un driver del dispositivo grafico, ottimizzato per la particolare GPU utilizzata.

La pipeline grafica logica La pipeline grafica logica è descritta nel Paragrafo C.3. La figura C.2.3 illustra i principali stadi di elaborazione e mette in evidenza gli stadi programmabili importanti: stadio di shading dei vertici, della geometria e dei pixel.

Mappatura della pipeline grafica su GPU a processori unificati La Figura C.2.4 mostra come la pipeline logica comprendente degli stadi programmabili separati e indipendenti venga mappata su una schiera di processori distribuiti reali.

Architettura GPU unificata di base Le architetture di GPU unificate sono basate su una schiera parallela di molti processori programmabili; questi unificano l’elaborazione effettuata dagli shader dei vertici, della geometria e dei pixel e il calcolo parallelo generico sugli stessi processori, a differenza delle GPU precedenti che avevano processori separati dedicati a ciascun tipo di elaborazione. L’insieme dei processori programmabili è strettamente integrato con alcuni processori che hanno una funzione prefissata, come il filtraggio della tessitura, la rasterizzazione, le operazioni sulla memoria video, l’anti-aliasing, la compressione, la decomAssemblatore degli input

Shader dei vertici

Shader della geometria

Impostazione e Rasterizzazione

Shader dei pixel

Operazioni di rasterizzazione/ Assemblaggio dell’output

Figura C.2.3. La pipeline logica grafica. Gli stadi degli shader programmabili sono rappresentati in blu, i blocchi che implementano funzioni prefissate in bianco.

© 978-88-08-06279-6

Assemblatore degli input

C.2  Le architetture dei sistemi GPU

Figura C.2.4. Pipeline logica mappata su processori fisici. Gli stadi degli shader programmabili vengono eseguiti dalla schiera di processori unificati e il flusso dei dati previsto dalla pipeline logica della grafica passa attraverso i processori.

Shader dei vertici Shader della geometria

Impostazione & rasterizzazione

Shader dei pixel

C9

Operazioni di rasterizzazione/ assemblaggio dell’output

Schiera di processori unificati

pressione, la visualizzazione, la decodifica video e l’elaborazione video ad alta definizione. Benché i processori che hanno una specifica funzione siano molto superiori a quelli programmabili di utilizzo più generale in termini di prestazioni assolute a parità di area, di costo e di potenza dissipata, concentreremo qui l’attenzione sui processori programmabili. Rispetto alle CPU multicore, le GPU a più core presentano una differente filosofia di progetto dell’architettura, che è focalizzata sull’esecuzione di molti thread paralleli su più processori in modo efficiente. Utilizzando molti core più semplici e ottimizzando il parallelismo sui dati tra gruppi di thread, una frazione maggiore di transistor per ogni chip è dedicata all’elaborazione e non alla memoria cache interna o a funzioni accessorie. Schiera di processori Una schiera di processori unificati di una GPU contiene molti processori core, tipicamente organizzati in multiprocessori multithread. In Figura C.2.5 è mostrata una GPU contenente un’insieme di 112 processori core a flusso continuo (SP, Streaming Processor), organizzati in 14 multiprocessori multithread a flusso continuo (SM, Streaming Multiprocessor). Ogni core SP è altamente multithread, essendo in grado di gestire in hardware 96 thread concorrenti e il loro stato. I processori sono connessi a quattro partizioni di memoria DRAM a 64 bit attraverso una rete di interconnessione. Ogni SM contiene otto core SP, due unità per funzioni speciali (SFU), memorie cache per le istruzioni e le costanti, un’unità per le istruzioni multithread e una memoria condivisa. Questa è l’architettura Tesla di base implementata nella scheda GeForce 8800 di NVIDIA. Essa presenta un’architettura unificata, nella quale i programmi grafici tradizionali per lo shading dei vertici, della geometria e dei pixel vengono eseguiti sugli SM unificati e sui loro core SP, e i programmi di calcolo generico possono essere eseguiti anch’essi sugli stessi processori. L’architettura a schiera di processori consente di creare configurazioni di GPU più o meno grandi, modificando il numero dei multiprocessori e il numero di partizioni di memoria. La Figura C.2.5 mostra sette gruppi di due SM che condividono la stessa unità di tessitura e la stessa memoria cache di primo livello per la tessitura stessa. L’unità di tessitura fornisce i risultati del filtraggio allo SM, dato l’insieme di coordinate della mappa. Poiché le regioni di supporto dei filtri sono spesso sovrapposte tra loro per ottenere un’unica tessitura continua, una piccola cache di primo livello del flusso di dati riduce efficacemente il numero di richieste al sistema di memoria. La schiera dei processori è connessa ai processori che effettuano operazioni di rasterizzazione (ROP), alle cache di secondo livello della tessitura, alle memorie DRAM esterne e alla memoria di sistema attraverso una rete di interconnessione estesa a tutta la GPU. Il numero dei processori e il numero delle memorie può variare,

C10

Appendice C  La grafica e il calcolo con la GPU Host CPU

System Memory

Bridge

GPU

Host Interface

TPC

Input Assembler

Viewport/Clip/ Setup/Raster/ZCull

Vertex Work Distribution

Pixel Work Distribution

TPC

© 978-88-08-06279-6

TPC

High-Definition Video Processors Compute Work Distribution

TPC

TPC

TPC

TPC

SM

SM

SM

SM

SM

SM

SM

SM

SM

SM

SM

SM

SM

SP SP

SP SP

SP SP

SP SP

SP SP

SP SP

SP SP

SP SP

SP SP

SP SP

SP SP

SP SP

SP SP

SP SP

SP SP

SP SP

SP SP

SP SP

SP SP

SP SP

SP SP

SP SP

SP SP

SP SP

SP SP

SP SP

SP SP

SP SP

SP SP

SP SP

SP SP

SP SP

SP SP

SP SP

SP SP

SP SP

SP SP

SP SP

SP SP

SP SP

SP SP

SP SP

SP SP

SP SP

SP SP

SP SP

SP SP

SP SP

SP SP

SP SP

SP SP

SP SP

SP SP

SP SP

SP SP

SP SP

Shared Memory

Shared Memory

Shared Memory

Shared Memory

Shared Memory

Shared Memory

Shared Memory

Shared Memory

Shared Memory

Shared Memory

Shared Memory

Shared Memory

Shared Memory

Shared Memory

Texture Unit Tex L1

Texture Unit Tex L1

Texture Unit Tex L1

Texture Unit Tex L1

Texture Unit Tex L1

Texture Unit Tex L1

Interconnection Network ROP

L2

DRAM

ROP

L2

DRAM

ROP

L2

DRAM

ROP

L2

DRAM

I-Cache MT Issue

SM

Texture Unit Tex L1

SM

Display Interface

C-Cache SP

SP

SP

SP

SP

SP

SP

SP

SFU

SFU

Shared Memory

Display

Figura C.2.5. Architettura di base di una GPU unificata. Esempio di GPU contenente 112 processori core a flusso continuo (SP) organizzati in 14 multiprocessori a flusso continuo (SM); i core sono fortemente multithread. Lo schema riproduce l’architettura Tesla di base di una GeForce 8800 NVIDIA. I processori sono connessi a quattro partizioni di memoria DRAM a 64 bit attraverso una rete di interconnessione. Ogni SM contiene otto core SP, due unità per funzioni speciali (SFU), memorie cache per le istruzioni e per le costanti, un’unità per istruzioni multithread e una memoria condivisa.

al fine di progettare sistemi basati su GPU che siano bilanciati per differenti prestazioni e destinati a differenti segmenti di mercato.

C.3 * Programmazione delle GPU La programmazione di una GPU multiprocessore differisce in maniera sostanziale dalla programmazione di altri multiprocessori (come le CPU multicore). Le GPU forniscono un numero di thread e un livello di parallelismo dei dati da due a tre ordini di grandezza superiori alle CPU, e sono arrivate nel 2008 a contenere centinaia di processori core e decine di migliaia di thread concorrenti. Le GPU continuano ad aumentare il loro grado di parallelismo raddoppiandolo ogni 12–18 mesi circa, in perfetto accordo con la legge di Moore. Per coprire la vasta gamma di prezzi e di prestazioni dei diversi segmenti di mercato, si producono GPU contenenti un numero di processori e thread assai variabile. Ciononostante, l’utente si aspetta che i videogiochi e le applicazioni grafiche, di visualizzazione e di calcolo funzionino con qualsiasi GPU, indipendentemente da quanti processori abbia o da quanti thread possa eseguire in parallelo; inoltre, si aspetta che le GPU più costose (con più core e più thread) eseguano le applicazioni più velocemente. Per questo motivo, i modelli di programmazione delle GPU e i programmi applicativi vengono progettati per coprire una vasta gamma di gradi di parallelismo. Il motivo per cui è necessario disporre di molti core ed eseguire svariati thread in parallelo è rappresentato dalla grafica in tempo reale; per esempio, solo con un sistema di questo tipo è possibile effettuare il rendering di scene 3D complesse ad alta risoluzione a una frequenza compatibile con l’interattivi-

© 978-88-08-06279-6

C11

C.3  Programmazione delle GPU

tà, ossia di almeno 60 immagini al secondo. In conseguenza di ciò, i modelli di programmazione dei linguaggi grafici di shading, come Cg (C per la grafica) e HLSL (linguaggio di shading ad alto livello), sono stati progettati per sfruttare un livello elevato di parallelismo mediante molti thread paralleli indipendenti e per funzionare su un numero qualsiasi di processori core. Analogamente, il modello di programmazione parallela e scalabile CUDA permette a generiche applicazioni di calcolo parallelo di sfruttare grandi quantità di thread paralleli e funzionare su un numero qualsiasi di processori core in modo trasparente all’applicazione. In questi modelli di programmazione scalabili, il programmatore scrive il codice per un singolo thread, ma la GPU lancia un numero molto elevato di istanze del thread in parallelo. I programmi possono perciò funzionare in modo trasparente su un’ampia gamma di hardware paralleli. Questo semplice paradigma è derivato dalle API grafiche e dai linguaggi di shading, in cui si descrive come colorare un vertice o un pixel. È rimasto un paradigma efficace anche quando le GPU hanno iniziato a crescere rapidamente in termini di parallelismo e prestazioni, a partire dalla fine degli anni Novanta. Questo paragrafo descrive brevemente la programmazione di una GPU per applicazioni di grafica in tempo reale mediante API grafiche e linguaggi di programmazione. Descrive quindi la programmazione di una GPU per l’elaborazione visuale e per applicazioni generali di calcolo parallelo utilizzando il linguaggio C e il modello di programmazione CUDA.

Programmazione della grafica in tempo reale Le API hanno giocato un ruolo importante nello sviluppo e nel successo delle GPU e dei processori. Le API grafiche standard principali sono due: le OpenGL e le Direct3D (una delle interfacce di programmazione multimediali DirectX di Microsoft). OpenGL è uno standard aperto che fu originariamente proposto e definito dalla società Silicon Graphics. Lo sviluppo, tuttora in corso, e l’estensione dello standard OpenGL (Segal e Akekey, 2006; Kessenich, 2006) viene gestito da Khronos, un consorzio industriale. Direct3D (Blythe, 2006) è uno standard di fatto ed è stato definito e continuamente aggiornato da Microsoft e dai suoi partner. OpenGL e Direct3D sono API strutturate in maniera simile e continuano a evolvere rapidamente seguendo i progressi dell’hardware delle GPU. Esse definiscono una pipeline logica di elaborazione grafica che viene mappata sui processori e sull’hardware delle GPU, nonché i modelli di programmazione e i linguaggi per gli stadi programmabili della pipeline grafica.

Pipeline grafica logica La figura C.3.1 illustra la pipeline grafica logica delle Direct3D 10 (la struttura di pipeline delle OpenGL è simile). L’API e la pipeline logica forniscono un’infrastruttura per il flusso continuo dei dati e l’impianto per gli stadi degli shader programmabili, evidenziati in blu. L’applicazione 3D invia alla GPU una sequenza di vertici raggruppati in primitive geometriche: punti, linee, triangoli e poligoni. L’assemblatore degli input raccoglie i vertici e le primitive e il programma di shading dei vertici esegue l’elaborazione vertice per vertice, compresa la trasformazione della posizione 3D del vertice nella sua posizione sullo schermo e la determinazione del suo colore. Il programma di shading della geometria esegue elaborazioni sulle primitive geometriche e può aggiungere o eliminare primitive. L’unità di impostazione e di rasterizzazione genera dei frammenti di pixel (i frammenti sono contributi potenziali ai pixel) che vengono ricoperti da una primitiva geometrica. Il programma di

OpenGL: una API grafica a standard aperto. Direct3D: una API grafica definita da Microsoft e dai suoi partner.

C12

Appendice C  La grafica e il calcolo con la GPU Shader dei vertici

Assemblatore degli input

Buffer dei vertici

Shader della geometria

Impostazione & rasterizzazione

Campionatore

Campionatore

Flusso in uscita

Tessitura

Tessitura

Buffer di flusso

Buffer degli indici

Costante

Costante

GPU

© 978-88-08-06279-6

Operazioni di rasterizzazione & assemblaggio dell’output

Shader dei pixel Campionatore

Tessitura Memoria

z-buffer della profondità

Costante

Oggetto del rendering

Stencil

Figura C.3.1. Pipeline grafica delle Direct3D 10. Ogni stadio della pipeline logica è mappato sull’hardware della GPU o su un processore GPU. Gli stadi di shading sono colorati in blu, i blocchi contenenti una funzione prefissata in bianco e i dispositivi di memoria in grigio. Ogni stadio elabora un vertice, una primitiva geometrica o un pixel, in modalità a flusso continuo.

shading dei pixel effettua elaborazioni sui singoli frammenti, tra cui l’interpolazione dei parametri di ogni frammento e l’applicazione della tessitura e del colore. Gli shader dei pixel fanno largo uso delle funzioni di mappatura che campionano e filtrano matrici 1D, 2D o 3D, chiamate tessiture, utilizzando coordinate interpolate espresse in virgola mobile. Gli shader ricorrono ad accessi alla tessitura per mappe, funzioni, decalcomanie, immagini e dati. Lo stadio di elaborazione delle operazioni di rasterizzazione (o assemblaggio dell’output) effettua il controllo della profondità mediante Z-buffer e il controllo degli stencil; ossia, può scartare un frammento di pixel non visibile perché nascosto da altri oggetti o sostituire la profondità del pixel con quella del frammento. Inoltre, può effettuare un’operazione di mescolamento dei colori mescolando il colore del pixel con quello del frammento e assegnare il colore risultante al pixel. Le API e la pipeline grafica forniscono gli ingressi, le uscite, le aree di memoria e l’infrastruttura per i programmi di shading che elaborano ogni vertice, primitiva o frammento di pixel.

I programmi degli shader grafici Tessitura (texture): una matrice 1D (vettore), 2D o 3D che può essere messa in corrispondenza con coordinate interpolate mediante campionamento e filtraggio. Shader: un programma che opera su dati grafici, per esempio vertici o frammenti di pixel. Linguaggio di shading: un linguaggio di rendering grafico, solitamente caratterizzato da un modello di programmazione a flusso di dati o a flusso continuo (streaming).

Le applicazioni di grafica in tempo reale utilizzano molti shader per calcolare come la luce interagisce con i diversi materiali e per effettuare il rendering di illuminazioni e ombreggiature complesse. I linguaggi di shading sono basati su un modello di programmazione a flusso di dati o a flusso continuo (streaming) che trova una corrispondenza nella pipeline grafica logica. Gli shader dei vertici mappano la posizione dei vertici dei triangoli sullo schermo, modificando la loro posizione, il loro colore e il loro orientamento. Tipicamente, un thread di uno shader dei vertici riceve in ingresso la posizione di un vertice espressa in virgola mobile (x, y, z, w) e calcola la sua posizione sullo schermo (x, y, z). Gli shader della geometria operano sulle primitive geometriche (come rette e triangoli) definite da un insieme di vertici, modificando queste primitive o generando primitive aggiuntive. Gli shader dei frammenti di pixel «dipingono» ciascun pixel, calcolando il contributo per ogni pixel (x, y) dell’immagine da sintetizzare nei canali di colore rosso, verde, blu e alpha (RGBA). Gli shader (e le GPU) utilizzano un’aritmetica in virgola mobile per tutte le operazioni sul colore dei pixel, in modo da eliminare possibili artefatti visibili. I pixel possono assumere una gamma molto estesa di valori, specialmente nella sintesi di scene caratterizzate da illuminazioni e ombre complesse o da una dinamica elevata. Per tutti e tre i tipi di shader grafico molteplici istanze di uno stesso programma possono essere lanciate in parallelo, come

© 978-88-08-06279-6

C.3  Programmazione delle GPU

thread paralleli e indipendenti, in quanto ognuno di essi opera su dati indipendenti e produce risultati indipendenti, senza «effetti collaterali». Vertici, primitive geometriche e pixel indipendenti, inoltre, permettono allo stesso programma grafico di essere eseguito su GPU di dimensioni differenti, che sono in grado di elaborare in parallelo un diverso numero di vertici, primitive e pixel. I programmi grafici, quindi, scalano in modo trasparente su GPU con prestazioni e gradi di parallelismo differenti. Per programmare questi tre tipi di thread grafici si usa un linguaggio ad alto livello appositamente pensato per la grafica. Generalmente si impiegano HLSL (High Level Shading Language) o Cg (C per la grafica). Questi linguaggi sono caratterizzati da una sintassi simile a quella del C e dispongono di molte funzioni di libreria per effettuare le operazioni su matrici e funzioni trigonometriche, l’interpolazione, l’accesso e il filtraggio di tessiture. Si tratta di linguaggi di programmazione abbastanza diversi dai linguaggi generici: non sono dotati di accesso globale alla memoria, puntatori, input/output su file e ricorsione. In HLSL e Cg si assume che i programmi «vivano» all’interno di una pipeline grafica logica, per cui le operazioni di input/output sono implicite. Per esempio, uno shader dei frammenti di pixel può aspettarsi che le normali geometriche di una figura e le coordinate multiple di una tessitura vengano interpolate a partire dai valori assunti nei vertici da parte degli stadi precedenti della pipeline con funzione prefissata, e può assegnare semplicemente un valore al parametro di uscita COLOR, il quale verrà passato agli stadi successivi per essere miscelato con il valore assunto da un pixel che si trova in quella posizione (x, y). L’hardware di una GPU crea un nuovo thread indipendente per eseguire un’operazione di shading sui vertici, sulla geometria o sui pixel, per ogni vertice, primitiva e frammento di pixel. Nei videogiochi, la maggior parte dei thread esegue shader di pixel, poiché tipicamente i pixel sono da 10 a 20 volte più numerosi dei vertici, e le sfumature di luce e le ombreggiature complesse richiedono un numero ancora maggiore di thread dei pixel rispetto a quelli dei vertici. Il modello di programmazione grafica a shader ha portato le architetture di GPU a eseguire in modo efficiente migliaia di thread indipendenti, a grana fine, su molti processori core in parallelo.

Un esempio di shader dei pixel Consideriamo il seguente programma Cg di shading dei pixel che implementa la tecnica di rendering della «mappatura d’ambiente». Per ogni thread di pixel, questo shader riceve cinque parametri in ingresso, tra cui le coordinate 2D in virgola mobile dell’immagine della tessitura, necessarie a campionare il colore sulla superficie, e un vettore 3D in virgola mobile che fornisce la direzione della riflessione sulla superficie della direzione di osservazione. Gli altri tre parametri «uniformi» non variano dall’istanza di un pixel (thread) a quella successiva. Lo shader ricava il colore da due immagini di tessitura: un accesso a una tessitura 2D per ricavare il colore della superficie e un accesso a una tessitura 3D contenuta in un cubo (sei immagini corrispondenti alle facce di un cubo) per ottenere il colore del mondo esterno corrispondente alla direzione di riflessione. Quindi, le quattro componenti in virgola mobile (rosso, verde, blu, alfa) del colore finale vengono calcolate applicando una media pesata, chiamata lerp o funzione di interpolazione lineare dei valori ricavati. void riflessione( float2 float3 out float4

CoordTessitura dir_riflessione colore

: TEXCOORD0, : TEXCOORD1, : COLOR,

C13

C14

Appendice C  La grafica e il calcolo con la GPU

© 978-88-08-06279-6

uniform float lucentezza, uniform sampler2D MappaSuperficie, uniform samplerCUBE MappaAmbiente) { // Ricava il colore della superficie da una tessitura float4 ColoreSuperficie = tex2D(MappaSuperficie,  CoordTessitura); // Ricava il colore riflesso campionando una mappa cubica float4 ColoreRiflesso = texCUBE(MappaAmbiente,  dir_riflessione); // L’output è la media pesata dei due colori colore = lerp(ColoreSuperficie, ColoreRiflesso, lucentezza); }

Figura C.3.2. Immagine sintetizzata con una GPU. Per conferire alla pelle profondità e un aspetto traslucido, lo shader dei pixel modellizza tre diversi strati di pelle, ognuno con il proprio comportamento di diffusione della luce e i propri parametri di scattering. Lo shader esegue 1400 istruzioni per generare le componenti cromatiche rossa, verde, blu e alfa di ogni frammento di pixel della pelle.

Nonostante questo programma di shading sia lungo solo tre linee di codice, attiva una gran quantità di hardware della GPU. Per ogni prelievo di tessitura, il sottosistema di accesso alla tessitura deve effettuare accessi multipli alla memoria per campionare i colori nell’intorno delle coordinate di campionamento, e poi calcolare per interpolazione il risultato finale utilizzando l’aritmetica in virgola mobile. Le GPU multithread eseguono migliaia di questi leggeri shader di pixel Cg in parallelo, intrecciandoli pesantemente per minimizzare la latenza dovuta alla lettura della tessitura e alla memoria. Cg focalizza l’attenzione del programmatore su un singolo vertice, primitiva o pixel, che viene implementato nella GPU come un singolo thread; il programma di shading, in modo trasparente, scala l’elaborazione per sfruttare il parallelismo dei thread sui processori disponibili. Essendo un linguaggio dedicato alle applicazioni, Cg mette a disposizione un ricco insieme di tipi di dati, funzioni di libreria e costrutti di linguaggio particolarmente utili per implementare le diverse tecniche di rendering. La figura C.3.2 mostra un esempio di pelle sintetizzata da uno shader. La pelle reale appare molto differente perché la luce viene rifratta all’interno della pelle diverse volte prima di essere riflessa. In questo shader complesso sono stati simulati tre diversi strati di pelle, ciascuno con il suo effetto di diffusione della luce e i suoi parametri di scattering, per dare alla pelle una profondità e un aspetto traslucido. Lo scattering può essere simulato mediante la convoluzione di un filtro di sfocatura (blurring) in uno «spazio di tessitura» appiattito, in cui il rosso viene sfuocato più del verde e il blu viene sfuocato meno degli altri colori. Questo shader Cg compilato esegue 1400 istruzioni per calcolare il colore di un solo pixel. Quando le GPU hanno raggiunto prestazioni elevate nel calcolo in virgola mobile e una larghezza di banda assai ampia nel trasferimento a flusso continuo con la memoria, sono diventate interessanti per l’esecuzione di applicazioni fortemente parallele diverse da quelle grafiche. All’inizio, la possibilità di sfruttare una tale potenza di calcolo era vincolata dal dover esprimere le applicazioni come algoritmi di rendering grafico, e questo approccio risultava spesso scomodo e limitante. Più recentemente, il modello di programmazione CUDA ha reso disponibile una modalità molto più semplice per sfruttare la scalabilità del calcolo in virgola mobile ad alte prestazioni e la larghezza di banda della memoria delle GPU, basata sul linguaggio C.

Programmazione di applicazioni di calcolo parallelo CUDA, Brook e CAL sono interfacce di programmazione per GPU focalizzate sull’elaborazione parallela dei dati più che sulla grafica. CAL (Compute Abstraction Layer) è un’interfaccia in linguaggio assembler a basso livello per

© 978-88-08-06279-6

C.3  Programmazione delle GPU

C15

le GPU AMD. Brook è un linguaggio per flusso continuo di elaborazione adattato alle GPU da Buck et al. (2004). CUDA è stato sviluppato da NVIDIA (2007) ed è un’estensione dei linguaggi C e C++ per la programmazione parallela e scalabile di GPU e CPU multicore. In seguito descriveremo il modello di programmazione CUDA come è stato presentato in un articolo da Nickolls, Buck, Garland e Skadron (2008). Con questo nuovo modello, la GPU eccelle nelle elaborazioni parallele a livello di dati e nel throughput, riuscendo a eseguire applicazioni di calcolo ad alte prestazioni con le stesse prestazioni disponibili per le applicazioni di grafica. Decomposizione di un problema con parallelismo a livello di dati Per mappare in modo efficace problemi di calcolo di grandi dimensioni su un’architettura altamente parallela, il programmatore o il compilatore devono scomporre il problema in tanti problemi più piccoli che possono essere risolti in parallelo. Per esempio, il programmatore può partizionare un grande vettore di dati contenente il risultato in blocchi e poi partizionare ulteriormente ogni blocco in elementi più piccoli, così che i blocchi del risultato possano essere determinati indipendentemente e in parallelo e gli elementi di ogni blocco possano essere elaborati in parallelo. La figura C.3.3 mostra la scomposizione di un vettore contenente il risultato in una griglia 3 × 2 di blocchi, dove ogni blocco è ulteriormente scomposto in una griglia 5 × 3 di elementi. La scomposizione in griglie su due livelli si mappa in modo naturale sull’architettura delle GPU: i multiprocessori calcolano i blocchi-risultato in parallelo, mentre thread paralleli elaborano i singoli elementi del risultato. Il programmatore scrive un programma che calcola una sequenza di matrici contenenti il risultato, partizionando ogni griglia in blocchi-risultato a grana grossa che possono essere calcolati indipendentemente in parallelo. Il programma calcola ogni blocco di risultati con una schiera di thread paralleli a grana fine, suddividendo il lavoro fra i thread in modo che ciascuno calcoli uno o più elementi di risultato.

Sequenza Passo 1:

Passo 2:

Dati del risultato, griglia 1 Blocco (0, 0)

Blocco (1, 0)

Blocco (2, 0)

Blocco (0, 1)

Blocco (1, 1)

Blocco (2, 1)

Dati del risultato, griglia 2

Blocco (1, 1) Elem Elem Elem Elem Elem (0, 0) (1, 0) (2, 0) (3, 0) (4, 0) Elem Elem Elem Elem Elem (0, 1) (1, 1) (2, 1) (3, 1) (4, 1) Elem Elem Elem Elem Elem (0, 2) (1, 2) (2, 2) (3, 2) (4, 2)

Figura C.3.3. Scomposizione dei dati in una griglia di blocchi di elementi che vengono calcolati in parallelo.

C16

Appendice C  La grafica e il calcolo con la GPU

© 978-88-08-06279-6

Programmazione scalabile parallela con CUDA Il modello di programmazione scalabile parallela CUDA estende i linguaggi C e C++ per sfruttare un elevato grado di parallelismo per applicazioni generali su multiprocessori paralleli a elevato grado di parallelismo, in particolare sulle GPU. I primi esperimenti con CUDA hanno mostrato che molti programmi complessi possono essere espressi semplicemente con poche astrazioni di facile comprensione. Da quando NVIDIA ha rilasciato CUDA nel 2007, gli sviluppatori hanno prodotto rapidamente programmi paralleli scalabili per un’ampia gamma di applicazioni, tra cui l’elaborazione di dati sismici, la chimica computazionale, l’algebra lineare, la soluzione di sistemi con matrici sparse, l’ordinamento, i modelli di fisica e l’elaborazione visuale. Queste applicazioni funzionano bene su centinaia di processori core e migliaia di thread concorrenti. Le GPU NVIDIA con architettura unificata per grafica ed elaborazione Tesla (descritte nei Paragrafi C.4 e C.7) eseguono programmi CUDA e sono ampiamente disponibili sui portatili, sui PC, sulle workstation e sui server. Il modello CUDA è applicabile anche ad altre architetture di elaborazione parallela a memoria condivisa, come le CPU multicore (Stratton, 2008). CUDA fornisce tre astrazioni fondamentali: una gerarchia di gruppi di thread, memorie condivise e sincronizzazione a barriera, che permettono una strutturazione chiara del parallelismo all’interno di codice C convenzionale per ciascun thread della gerarchia. Livelli multipli di thread, di memoria e di sincronizzazione permettono un parallelismo sui dati e un parallelismo di thread a grana fine, innestati su un parallelismo sui dati e un parallelismo di procedura a grana grossa. Le astrazioni guidano il programmatore a suddividere il problema dapprima in spezzoni di problema, che possono essere risolti indipendentemente in parallelo, e quindi in spezzoni ancora più piccoli, che possono anch’essi essere risolti in parallelo. Il modello di programmazione scala in modo trasparente su un numero molto elevato di processori: un programma CUDA compilato può essere eseguito su un qualsiasi numero di processori; soltanto il sistema runtime ha bisogno di conoscere il numero reale dei processori. Il paradigma CUDA Kernel: un programma o funzione per un thread, progettato per essere eseguito da molti thread. Blocco di thread: un insieme di thread concorrenti che eseguono lo stesso programma di thread e possono collaborare per il calcolo del risultato. Griglia: un insieme di blocchi di thread che eseguono lo stesso programma kernel.

CUDA è un’estensione minimale dei linguaggi di programmazione C e C++. Il programmatore scrive un programma sequenziale che richiama kernel paralleli, i quali possono contenere semplici funzioni o programmi completi. Un kernel viene eseguito in parallelo su un insieme di thread paralleli. Il programmatore organizza questi thread in una gerarchia di blocchi di thread e di griglie di blocchi di thread. Un blocco di thread è un insieme di thread concorrenti che possono collaborare tra loro mediante sincronizzazione a barriera e accesso condiviso allo spazio di memoria privato del blocco. Una griglia (grid) è un insieme di blocchi di thread che possono essere eseguiti ciascuno in modo indipendente, quindi in parallelo. Quando deve essere lanciato in esecuzione un kernel, il programmatore specifica il numero di thread per blocco e il numero di blocchi costituenti la griglia. A ciascun thread viene assegnato un numero identificativo (univoco) di thread, threadIdx, all’interno del blocco di thread che lo contiene, e a ciascun blocco di thread un numero di blocco univoco, blockIdx, all’interno della sua griglia. CUDA supporta blocchi di thread contenenti fino a 512 thread. Per comodità, i blocchi di thread e le griglie possono essere organizzati in matrici a 1, 2 o 3 dimensioni a cui si accede mediante i campi indice .x, .y e .z.

© 978-88-08-06279-6

C.3  Programmazione delle GPU

C17

Come esempio molto semplice di programmazione parallela consideriamo due vettori, x e y, ciascuno contenente n elementi in virgola mobile e supponiamo di voler calcolare il risultato della funzione y = ax + y per un dato valore dello scalare a. Questa funzione corrisponde al kernel chiamato SAXPY contenuto nella libreria di algebra lineare BLAS. La figura C.3.4 riporta il codice C che effettua tale calcolo sia su un processore seriale sia in parallelo utilizzando CUDA. Lo specificatore __global__ indica che la procedura è il punto di ingresso di una procedura kernel. I programmi CUDA lanciano kernel paralleli con una chiamata a funzione estesa seguendo la seguente sintassi: kernel<<>>(... elenco parametri ...); dove dimGriglia e dimBlocco sono vettori tridimensionali di tipo dim3 che specificano, rispettivamente, le dimensioni della griglia in numero di blocchi e la dimensione del blocco in numero di thread. Se le dimensioni non vengono specificate, il valore di default è 1. Nel codice di figura C.3.4 viene lanciata una griglia di n thread che assegna un thread a ogni elemento dei vettori e inserisce 256 thread in ogni blocco. Ogni singolo thread calcola l’indice di un elemento a partire dal proprio identificativo, ID, del blocco e del thread e quindi effettua il calcolo desiderato sull’elemento del vettore corrispondente. Confrontando la versione sequenziale e parallela di questo codice si nota che queste sono sorprendentemente simili. Il codice sequenziale consiste di un ciclo in cui ogni iterazione è indipendente da tutte le altre. Cicli come questi possono essere trasformati in modo meccanico in kernel paralleli: ogni iterazione del ciclo diventa un thread indipendente. Assegnando un singolo thread a ogni elemento di output, non abbiamo bisogno di sincronizzazione i thread per scrivere i risultati in memoria. Il testo di un kernel CUDA è semplicemente una funzione C scritta per un thread sequenziale. È quindi in genere molto semplice da scrivere, soprattutto rispetto al codice parallelo per operazioni vettoriali. Il parallelismo è deterCalcolo di y = ax + y mediante un ciclo sequenziale: void saxpy_sequenziale(int n, float alpha, float *x, float *y) { for(int i = 0; i>>(n, 2.0, x, y);

Figura C.3.4. Confronto fra codice sequenziale (sopra) in C e codice parallelo (sotto) in CUDA per la funzione SAXPY (si veda il Capitolo 7). I thread paralleli CUDA sostituiscono il ciclo sequenziale del C: ogni thread calcola lo stesso risultato di un’iterazione di ciclo. Il codice parallelo calcola n risultati con n thread, organizzati in blocchi di 256 thread.

C18

Appendice C  La grafica e il calcolo con la GPU

Barriera di sincronizzazione: i thread attendono in corrispondenza di una barriera di sincronizzazione fino a quando tutti i thread appartenenti al blocco non hanno raggiunto la barriera.

minato in modo chiaro ed esplicito specificando le dimensioni della griglia di elaborazione e il numero dei suoi blocchi di thread quando il programma kernel viene lanciato in esecuzione. L’esecuzione parallela e la gestione dei thread sono automatiche. Creazione, pianificazione dell’esecuzione e terminazione dei thread è gestita dal sistema sottostante. Le GPU con architettura Tesla di fatto effettuano tutta la gestione dei thread direttamente in hardware. I thread di un blocco vengono eseguiti in modo concorrente e possono venire sincronizzati in corrispondenza di una barriera di sincronizzazione, chiamando la primitiva __syncthreads(). Questa garantisce che nessun thread del blocco possa proseguire l’esecuzione fino a quando tutti i thread dello stesso blocco non avranno raggiunto la barriera. Dopo averla superata, è garantito che questi thread vedranno i dati che i thread hanno scritto in memoria prima di aver raggiunto la barriera. I thread all’interno di uno stesso blocco, quindi, possono comunicare tra loro scrivendo e leggendo nella memoria condivisa del blocco in corrispondenza della barriera di sincronizzazione. Poiché i thread di un blocco possono condividere la memoria e sincronizzarsi mediante le barriere, essi risiederanno fisicamente sullo stesso processore o multiprocessore. Tuttavia, il numero di blocchi di thread può essere significativamente maggiore del numero dei processori. Il modello CUDA di programmazione dei thread virtualizza i processori e fornisce al programmatore totale flessibilità sul grado di granularità con cui rendere parallelo il codice, consentendo quindi di scegliere il livello di granularità che il programmatore ritiene più conveniente. La virtualizzazione in thread e blocchi di thread permette di scomporre il problema in maniera intuitiva, poiché il numero dei blocchi può essere determinato più dalla dimensione dei dati da elaborare che dal numero di processori del sistema. La virtualizzazione permette inoltre allo stesso programma CUDA di essere eseguito su un numero ampiamente variabile di processori core. Per gestire questa virtualizzazione degli elementi di elaborazione e garantire la scalabilità su più processori, CUDA richiede che i blocchi di thread possano essere eseguiti in modo indipendente: si deve poter eseguire i blocchi in un qualsiasi ordine, in parallelo o in sequenza. Blocchi differenti non hanno possibilità di comunicazione diretta, benché essi possano coordinare le loro attività utilizzando operazioni atomiche di memoria sulla memoria globale visibile da tutti i thread. Questo requisito di indipendenza consente di avviare a esecuzione blocchi di thread in un qualsiasi ordine su un qualsiasi numero di processori, rendendo il modello CUDA scalabile su un numero arbitrario di core, come pure su una grande varietà di architetture parallele. Ciò aiuta anche a evitare la possibilità che si verifichi un blocco critico (deadlock). Un’applicazione può eseguire griglie multiple di funzioni sia in modo indipendente sia dipendente. Griglie indipendenti possono essere eseguite in maniera concorrente in caso di risorse hardware sufficienti. Le griglie dipendenti vengono eseguite in sequenza, con un’implicita barriera inter-kernel tra loro, garantendo così che l’esecuzione di tutti i blocchi della prima griglia termini prima che inizi l’esecuzione di uno dei blocchi della seconda griglia, dipendente dalla prima. Durante la loro esecuzione, i thread possono accedere a dati appartenenti a diversi spazi di memoria. Ogni thread dispone di una memoria locale (local memory) privata. CUDA utilizza la memoria locale per le variabili private dei thread che non riescono a essere allocate nei registri di thread, ma anche le aree di stack delle procedure (stack frame) e per il riversamento dai registri (register spilling). Ogni blocco di thread dispone di una memoria condivisa (shared memory), visibile da tutti i thread del blocco, che ha lo stesso tempo di vita del blocco stesso. Infine, tutti i thread hanno accesso alla memoria globale

Operazione atomica di memoria: una sequenza di operazioni di lettura, modifica o scrittura della memoria che viene completata senza essere interrotta da altri accessi alla memoria.

Memoria locale: memoria locale e privata di ciascun thread. Memoria condivisa: memoria condivisa da tutti i thread di un blocco. Memoria globale: memoria dedicata alle applicazioni che viene condivisa da tutti i thread.

© 978-88-08-06279-6

C.3  Programmazione delle GPU

C19

(global memory). I programmi dichiarano le varabili in memoria condivisa o globale con gli specificatori __shared__ e __device__, rispettivamente. Su una GPU con architettura Tesla, questi spazi di memoria corrispondono a memorie fisicamente separate: la memoria condivisa, dedicata a ogni blocco, è una RAM on chip a bassa latenza, mentre la memoria globale risiede nella DRAM veloce della scheda grafica. La memoria condivisa è tipicamente una memoria a bassa latenza vicina a ogni processore (come una cache L1). È quindi in grado di offrire alte prestazioni nella comunicazione e nella condivisione dei dati tra i thread del blocco. Poiché la memoria condivisa ha lo stesso tempo di vita del corrispondente blocco di thread, il codice del kernel tipicamente inizializzerà i dati nelle variabili condivise, effettuerà l’elaborazione richiesta utilizzando queste variabili e infine copierà i risultati dalla memoria condivisa alla memoria globale. Blocchi di thread appartenenti a griglie sequenzialmente dipendenti comunicano mediante la memoria globale, utilizzandola per leggere i dati in ingresso e scrivere i risultati. La Figura C.3.5 mostra il diagramma dei livelli innestati di thread, blocchi di thread e griglie di blocchi di thread; essa mostra anche i livelli corrispondenti di memoria: locale, condivisa e globale per la condivisione dei dati a livello di thread, di blocco di thread e di applicazione. Un programma gestisce lo spazio di memoria globale visibile dai kernel mediante chiamate runtime a CUDA, quali cudaMalloc() e cudaFree(). I kernel possono essere eseguiti su un dispositivo fisicamente diverso, come nel caso dei kernel eseguiti su GPU. Di conseguenza, l’applicazione deve utilizzare cudaMemcpy() per copiare i dati tra lo spazio allocato e la memoria di sistema della macchina host. Il modello di programmazione CUDA ha uno stile simile al diffuso modello programma singolo per dati multipli (SPMD, Single-Program Multiple Data): il parallelismo è espresso in modo esplicito e ogni kernel viene eseguito con un numero prefissato di thread. Tuttavia, CUDA è più flessibile della

Programma singolo per dati multipli (SPMD): uno stile di programmazione parallela nel quale tutti i thread eseguono lo stesso programma. I thread SPMD sono tipicamente sincronizzati mediante sincronizzazione a barriera.

© 978-88-08-06279-6

Thread

Figura C.3.5. I livelli di granularità innestati: thread, blocco di thread e griglia dispongono di corrispondenti livelli di condivisione della memoria (locale, condivisa e globale). La memoria locale, una per ogni thread, appartiene al thread. La memoria condivisa, una per ogni blocco, è condivisa da tutti i thread di un blocco. La memoria globale, una per ogni applicazione, è condivisa da tutti i thread.

Memoria locale (una per ogni thread)

Blocco di thread Memoria condivisa (una per ogni blocco di thread)

Griglia 0

Sequenza

... Griglia 1

Sincronizzazione inter-griglia

...

Memoria globale

C20

Appendice C  La grafica e il calcolo con la GPU

© 978-88-08-06279-6

maggior parte delle implementazioni SPMD perché ogni chiamata a kernel crea dinamicamente una nuova griglia con il numero di blocchi e di thread richiesto da quella parte dell’applicazione. Il programmatore può scegliere il grado di parallelismo più adatto per ogni kernel, anziché dover progettare tutte le fasi di elaborazione in modo da utilizzare lo stesso numero di thread. La figura C.3.6 mostra un esempio di frammento di codice CUDA di tipo SPMD. Il codice per prima cosa istanzia il kernel kernelF su una griglia 2D di 2 × 3 blocchi di thread, dove ogni blocco 2D consiste di 5 × 3 thread. Successivamente viene istanziato kernelG su una griglia monodimensionale di quattro blocchi di thread, contenenti sei thread ciascuno. Dato che kernelG dipende dai risultati di kernelF, i due kernel vengono separati da una barriera di sincronizzazione inter-kernel. I thread concorrenti di un blocco di thread esprimono un parallelismo dei dati e dei thread a grana fine. I blocchi di thread indipendenti delle griglie esprimono un parallelismo di dati a grana grossa. Griglie indipendenti esprimono un parallelismo di procedura a grana grossa. Il kernel è costituito semplicemente dal codice C per un thread della gerarchia. Restrizioni Per questioni di efficienza e di semplicità di implementazione, il modello di programmazione CUDA presenta alcune restrizioni. I thread e i blocchi di thread possono essere creati soltanto invocando un kernel parallelo, e non

la griglia 2D di kernelF è composta da 3  2 blocchi; ogni blocco contiene 5  3 thread Figura C.3.6. Sequenza di kernelF Sequenza istanziato su una griglia 2D di blocBlocco 0, 0 Blocco 1, 0 Blocco 2, 0 chi 2D di thread, una barriera di sincronizzazione interkernel, seguita da kernelG istanziato su una griglia kernelF<<<(3, 2), (5, 3)>>>(parametri); 1D di blocchi 1D di thread. Blocco 0, 1

Blocco 1, 1

Blocco 2, 1

Blocco 1, 1 Thread 0, 0

Thread 1, 0

Thread 2, 0

Thread 3, 0

Thread 4, 0

Thread 0, 1

Thread 1, 1

Thread 2, 1

Thread 3, 1

Thread 4, 1

Thread 0, 2

Thread 1, 2

Thread 2, 2

Thread 3, 2

Thread 4, 2

Barriera di sincronizzazione tra i kernel la griglia 1D di kernelG è composta da 4 blocchi; ogni blocco contiene 6 thread Blocco 0

Blocco 1

Blocco 2

Blocco 3

kernelG<<<4, 6>>>(parametri);

Blocco 2 Thread 0

Thread 1

Thread 2

Thread 3

Thread 4

Thread 5

© 978-88-08-06279-6

C.3  Programmazione delle GPU

dall’interno dei kernel paralleli; assieme al requisito di indipendenza dei blocchi di thread, ciò rende possibile l’esecuzione di programmi CUDA con uno scheduler («pianificatore») semplice, che introduce un sovraccarico minimo in fase di esecuzione. Di fatto, nell’architettura GPU Tesla la gestione e la pianificazione dell’esecuzione dei thread e dei blocchi di thread è implementata in hardware. Il parallelismo di procedura può essere espresso a livello dei blocchi di thread, ma è difficile esprimerlo all’interno di un blocco, perché le barriere di sincronizzazione agiscono su tutti i thread del blocco. Per consentire ai programmi CUDA di essere eseguiti su un numero qualsiasi di processori, non sono permesse dipendenze tra diversi blocchi di thread della stessa griglia di kernel, cioè i blocchi devono necessariamente essere eseguiti in modo indipendente tra loro. Dato che CUDA richiede che i blocchi di thread siano indipendenti e possano quindi essere eseguiti in un qualsiasi ordine, la combinazione dei risultati prodotti dai diversi blocchi viene in generale ottenuta lanciando un secondo kernel su una nuova griglia di blocchi di thread (anche se i blocchi di thread possono coordinare la loro attività, per esempio, incrementando mediante operazioni atomiche puntatori a code). Le chiamate a funzioni ricorsive non sono al momento permesse nei kernel CUDA. La ricorsione, infatti, non è adatta ai kernel pesantemente paralleli, poiché lo spazio di stack da mettere a disposizione per le decine di migliaia di thread che potrebbero essere attivi richiederebbe quantità notevoli di memoria. Gli algoritmi sequenziali normalmente espressi in forma ricorsiva, come Quick Sort, solitamente sono implementati meglio attraverso un parallelismo sui dati innestati piuttosto che con la ricorsione esplicita. Per gestire l’architettura di sistema eterogenea costituita da una CPU e una GPU, ognuna con il proprio sistema di memoria, i programmi CUDA devono copiare i dati e i risultati tra le due memorie. Il sovraccarico dovuto all’interazione CPU–GPU e al trasferimento dati viene minimizzato utilizzando meccanismi di trasferimento DMA a blocchi e interconnessioni veloci. Problemi di calcolo intensivo sufficientemente grandi da avere bisogno di un incremento di prestazioni fornito da una GPU ammortizzano tale sovraccarico meglio dei problemi di piccole dimensioni.

Implicazioni per un’architettura I modelli di programmazione parallela per grafica e calcolo hanno prodotto una sostanziale differenza tra l’architettura delle GPU e quella delle CPU. Gli aspetti principali dei programmi per GPU che influenzano l’architettura della GPU stessa sono: – Uso estensivo del parallelismo dei dati a grana fine: i programmi di shading descrivono come elaborare un singolo pixel o vertice; i programmi CUDA descrivono come calcolare un singolo risultato. – Modello di programmazione fortemente organizzato in thread: un programma di shading a thread elabora un singolo pixel o vertice; il thread di un programma CUDA è in grado di generare un singolo risultato. Una GPU è in grado di creare ed eseguire milioni di questi thread per ogni immagine, a 60 immagini al secondo. – Scalabilità: un programma deve aumentare automaticamente le sue prestazioni quando vengono messi a sua disposizione altri processori, senza che si debba ricompilare il codice. – Elaborazione intensiva in virgola mobile (o intera). – Supporto all’elaborazione di grande quantità di dati.

C21

C22

Appendice C  La grafica e il calcolo con la GPU

© 978-88-08-06279-6

C.4 * Architettura multiprocessore multithread Per coprire diversi segmenti di mercato, le GPU presentano un numero scalabile di multiprocessori; di fatto, le GPU sono multiprocessori composti da multiprocessori. Inoltre, ogni multiprocessore è fortemente multithread, così da poter eseguire in maniera efficiente molti thread di shader di pixel e di vertici a grana fine. Una GPU di base possiede da due a quattro multiprocessori, mentre le GPU utilizzate nelle console di videogiochi o nelle piattaforme di calcolo ne contengono dozzine. Qui analizziamo in dettaglio l’architettura di un multiprocessore multithread di questo tipo, una versione semplificata del multiprocessore a flusso continuo (SM) Tesla di NVIDIA descritto nel Paragrafo C.7. Perché è meglio utilizzare un multiprocessore invece di molti processori indipendenti? Il parallelismo all’interno dei multiprocessori fornisce elevate prestazioni per il codice locale e supporta estensivamente l’esecuzione di thread multipli (per i modelli di programmazione parallela a grana fine descritti nel Paragrafo C.3). I singoli thread di un blocco vengono eseguiti insieme all’interno di un multiprocessore e possono condividere i dati. L’architettura multiprocessore multithread qui descritta possiede otto processori core scalari organizzati in un’architettura fortemente interconnessa e sono in grado di eseguire fino a 512 thread (il multiprocessore SM descritto nel Paragrafo C.7 esegue fino a 768 thread). Per garantire la maggiore efficienza possibile in termini di occupazione dello spazio e di potenza assorbita, il multiprocessore condivide tra gli otto processori core le unità funzionali grandi e complesse, tra cui la cache delle istruzioni, l’unità delle istruzioni multithread e la memoria RAM condivisa.

Il multithreading massivo I processori GPU hanno un alto livello di multithreading per raggiungere i seguenti obiettivi: – Nascondere la latenza del caricamento dei dati dalla memoria e della tessitura dalla DRAM. – Supportare i modelli di programmazione degli shader grafici paralleli a grana fine. – Supportare i modelli di programmazione per il calcolo parallelo a grana fine. – Rendere virtuali i processori fisici come thread e blocchi di thread per fornire una scalabilità trasparente. – Semplificare il modello di programmazione parallela riducendolo alla scrittura di un programma sequenziale per un singolo thread. La latenza del caricamento dei dati e della tessitura dalla memoria può richiedere centinaia di cicli di clock del processore, poiché le GPU sono tipicamente dotate di cache a flusso continuo di piccole dimensioni (a differenza delle CPU, che invece impiegano cache di grandi dimensioni). Una richiesta di lettura in memoria, in genere, richiede l’intero tempo della latenza di accesso alla DRAM più la latenza di interconnessione e il tempo di caricamento dei dati nei buffer. L’organizzazione multithreading aiuta a riempire i tempi di latenza con altre elaborazioni: mentre un thread attende il completamento del caricamento dei dati dalla memoria o del prelievo di una tessitura, il processore può eseguire un altro thread. I modelli di programmazione paralleli a grana fine generano migliaia di thread indipendenti, i quali possono mantenere impegnati molti processori nonostante le lunghe latenze di memoria viste dal singolo thread.

© 978-88-08-06279-6

C.4  Architettura multiprocessore multithread

C23

Un programma grafico di shading dei vertici o dei pixel è un programma per un singolo thread che elabora un vertice o un pixel. Similmente, un programma CUDA è un programma in C per un singolo thread che calcola un risultato. I programmi di grafica e di calcolo istanziano molti thread paralleli rispettivamente per generare immagini complesse e per calcolare matrici di grandi dimensioni che contengono il risultato. Per bilanciare dinamicamente i carichi di lavoro variabili nel tempo dei thread degli shader dei vertici e dei pixel, ogni multiprocessore esegue in modo concorrente molteplici programmi di thread e differenti tipi di programmi di shading. Per supportare il modello di programmazione indipendente dei vertici, delle primitive geometriche e dei pixel dei linguaggi di shading grafico e il modello di programmazione a singolo thread del C/C++ di CUDA, ogni thread della GPU dispone dei propri registri privati, di memoria privata dedicata al thread, di un registro program counter e di un registro dello stato di esecuzione del thread, ed è quindi in grado di eseguire un frammento di codice in maniera indipendente. Per eseguire in modo efficiente centinaia di thread leggeri e concorrenti, il multiprocessore della GPU implementa il multithreading in hardware, ossia gestisce ed esegue centinaia di thread concorrenti in hardware, senza sovraccarichi per la pianificazione dell’esecuzione. I thread concorrenti dello stesso blocco possono sincronizzarsi a una barriera mediante una singola istruzione. La semplicità della creazione dei thread, la pianificazione dell’esecuzione dei thread senza sovraccarichi e la sincronizzazione veloce mediante barriera supportano in modo efficiente il parallelismo a grana molto fine.

L’architettura multiprocessore Un multiprocessore unificato per grafica e calcolo deve essere in grado di eseguire programmi di shading di vertici, di geometria e di frammenti di pixel, oltre a programmi di calcolo parallelo. Come mostra la Figura C.4.1, il multiprocessore riportato come esempio contiene otto processori scalari (SP), ognuno dei quali equipaggiato di un gran numero di registri multithread (RF,

Multiprocessore multithread Cache istruzioni

Unità di controllo del multiprocessore

Unità per le istruzioni multithread Cache per le costanti

SFU

Interfaccia di processo

SP

SP

SP

SP

SP

SP

SP

SP

RF

RF

RF

RF

RF

RF

RF

RF

Rete di interconnessione

Memoria condivisa

SFU Interfaccia di ingresso Interfaccia di uscita Interfaccia della tessitura Interfaccia della memoria

Figura C.4.1. Multiprocessore multithread contenente otto processori core scalari (SP). Gli otto core SP dispongono ciascuno di un grosso insieme di registri multithread (RF) e condividono una cache istruzioni, un’unità di lancio di istruzioni multithread, una cache per le costanti, due unità per funzioni speciali (SFU), una rete di interconnessione e una memoria condivisa a banchi multipli.

C24

Photo: Judy Schoonmaker

Appendice C  La grafica e il calcolo con la GPU

Pianificatore dell’esecuzione delle istruzioni multithread SIMT tempo warp 8, istruzione 11 warp 1, istruzione 42 warp 3, istruzione 95

warp 8, istruzione 12 warp 3, istruzione 96 warp 1, istruzione 43

Figura C.4.2. Pianificazione dell’esecuzione di warp multithread SIMT. Il pianificatore (scheduler) seleziona un warp pronto per l’esecuzione e lancia in esecuzione un’istruzione in modo sincrono sui thread paralleli che costituiscono il warp. Poiché i warp sono indipendenti, il pianificatore può scegliere ogni volta un warp differente.

Singola istruzione e thread multipli (SIMT): un’architettura di processore che applica un’istruzione a molteplici thread indipendenti eseguiti in parallelo. Warp: l’insieme dei thread paralleli che eseguono contemporaneamente la medesima istruzione in un’architettura SIMT.

© 978-88-08-06279-6

Register File), due unità per funzioni speciali (SFU), un’unità per le istruzioni multithread, una cache istruzioni, una cache a sola lettura per le costanti e una memoria condivisa. La memoria condivisa di 16 kB contiene i buffer per i dati della grafica e i dati condivisi per il calcolo. Le variabili CUDA dichiarate come __shared__ risiedono nella memoria condivisa. Per mappare ripetutamente il flusso di elaborazione della pipeline logica grafica sul multiprocessore, come mostrato nel Paragrafo C.2, i thread dei vertici, della geometria e dei pixel dispongono di buffer di ingresso e uscita indipendenti, e i carichi di lavoro arrivano e se ne vanno indipendentemente dall’esecuzione dei thread. Ogni core SP contiene unità aritmetiche scalari intere e in virgola mobile che eseguono la maggior parte delle istruzioni. Il processore SP implementa il multithreading in hardware ed è in grado di gestire fino a 64 thread. La pipeline di ogni SP esegue un’istruzione scalare per ogni thread per ogni ciclo di clock, la cui frequenza può variare tra 1,2 GHz e 1,6 GHz, a seconda del modello. Ogni processore SP possiede un insieme di registri (RF) di grosse dimensioni, costituito da 1024 registri a 32 bit di utilizzo generale, partizionati tra i thread assegnati al processore. Nei programmi vengono dichiarate le richieste di registri, tipicamente da 16 a 64 registri scalari a 32 bit per ogni thread. Il processore SP può eseguire in modo concorrente molti thread che impiegano pochi registri, oppure un numero minore di thread che necessitano di più registri. Il compilatore ottimizza l’allocazione dei registri, con lo scopo di bilanciare il costo del riversamento dei registri in memoria con il costo derivante da un numero minore di thread. I programmi di shading dei pixel utilizzano spesso 16 registri o meno, permettendo a ogni SP di eseguire fino a 64 thread di shading dei pixel, coprendo così le lunghe latenze dei prelievi della tessitura. I programmi CUDA compilati necessitano spesso di 32 registri per thread, limitando così ogni SP a 32 thread e imponendo che un programma kernel abbia al massimo 256 thread per ogni blocco, anziché il massimo consentito di 512 thread (in questo multiprocessore di esempio). La pipeline della SFU esegue istruzioni di thread che calcolano funzioni speciali e interpolano gli attributi dei pixel a partire dagli attributi primitivi dei vertici. Queste istruzioni possono essere eseguite in modo concorrente con altre istruzioni in esecuzione sugli SP. L’unità SFU verrà descritta più avanti. Il multiprocessore esegue le istruzioni di prelievo della tessitura nell’unità di tessitura attraverso l’interfaccia della tessitura, mentre utilizza l’interfaccia della memoria per istruzioni di lettura, scrittura e accesso atomico alla memoria esterna. Queste istruzioni possono essere eseguite in maniera concorrente con le istruzioni in esecuzione sugli SP. L’accesso alla memoria condivisa utilizza una rete di interconnessione a bassa latenza tra i processori SP e i banchi di memoria condivisa.

Singola istruzione e thread multipli (SIMT) Per gestire ed eseguire in modo efficiente centinaia di thread che svolgono una grande quantità di programmi diversi, il multiprocessore adotta un’architettura a singola istruzione e thread multipli (SIMT, Single Instruction Multiple-Thread). Essa crea, gestisce, pianifica l’esecuzione ed esegue thread concorrenti in gruppi di thread paralleli denominati warp. Il termine warp («ordito di un tessuto») deriva dalla tessitura a telaio, la prima tecnologia con fili (thread) in parallelo. La fotografia in Figura C.4.2 mostra un ordito (warp) di fili (thread) paralleli uscenti da un telaio. Questo esempio di multiprocessore utilizza una dimensione di warp di 32 thread ed esegue quattro thread in ciascuno degli otto core SP, in quattro cicli di clock. Anche il multiprocessore Tesla SM, che verrà descritto nel Paragrafo C.7, utilizza una dimensione di

© 978-88-08-06279-6

C.4  Architettura multiprocessore multithread

warp di 32 thread paralleli ed esegue quattro thread per ogni core SP per ottenere un’elevata efficienza in condizioni di abbondanza di thread di pixel e di calcolo. I blocchi di thread sono costituiti da uno o più warp. Questo esempio di multiprocessore SIMT gestisce un gruppo di 16 warp, per un totale di 512 thread. I singoli thread paralleli che compongono un warp sono dello stesso tipo e partono contemporaneamente dallo stesso indirizzo di codice, ma sono poi liberi di procedere e seguire le biforcazioni del codice in modo indipendente. Ogni volta che viene lanciata in esecuzione un’istruzione, l’unità istruzioni multithread SIMT seleziona un warp che è pronto per eseguire l’istruzione successiva e invia quell’istruzione ai thread attivi di quel warp. Un’istruzione SIMT viene diffusa in modo sincrono sui thread paralleli attivi del warp. Alcuni thread possono risultare inattivi, a causa delle biforcazioni indipendenti del codice o dell’attesa di variabili. In questo multiprocessore ogni processore core scalare SP esegue un’istruzione sui quattro singoli thread di un warp impiegando quattro cicli di clock, rispecchiando così lo stesso rapporto di 4:1 che esiste tra il numero di thread di un warp e il numero di core. L’architettura di un processore SIMT è simile a quella dei processori a singola istruzione e dati multipli (SIMD), in cui si applica una singola istruzione a corsie di elaborazione multiple di dati. La differenza sta nel fatto che l’architettura SIMT applica un’istruzione a thread multipli indipendenti in parallelo e non semplicemente a corsie multiple di dati. Un’istruzione di un processore SIMD controlla un vettore di corsie multiple di dati, tutte assieme, mentre un’istruzione di un processore SIMT controlla un singolo thread ed è l’unità istruzioni SIMT, guadagnando in efficienza, a lanciare quell’istruzione su un warp di thread paralleli e indipendenti. Il processore SIMT trova parallelismi tra i dati fra thread in fase di esecuzione, in maniera analoga a come un processore superscalare trova parallelismi tra le istruzioni in fase di esecuzione. Un processore SIMT ottiene piena efficienza e massime prestazioni quando tutti i thread di un warp seguono lo stesso flusso di esecuzione. Se alcuni thread divergono a causa di un salto condizionato dipendente dai dati, l’esecuzione diventa sequenziale per ognuna delle due diramazioni possibili del codice e, quando tutti i flussi di esecuzione giungono a conclusione, i thread si ricongiungono in un unico flusso. Un codice contente una biforcazione del tipo if else in due flussi di esecuzione di uguale lunghezza ha un’efficienza del 50%. Per gestire thread indipendenti che divergono e convergono, il multiprocessore utilizza uno stack di sincronizzazione delle diramazioni. Warp differenti vengono eseguiti in maniera indipendente fra loro alla massima velocità possibile, anche se stanno eseguendo flussi di codice comuni o disgiunti. Ne consegue che le GPU SIMT sono decisamente più efficienti e flessibili in relazione ai frammenti di codice divergenti rispetto alle GPU delle precedenti generazioni, poiché i loro warp sono molto più sottili se paragonati all’ampiezza delle istruzioni SIMD. Diversamente dalle architetture vettoriali SIMD, le SIMT permettono ai programmatori di scrivere sia codice parallelo a livello di thread (per thread singoli e indipendenti) sia codice parallelo a livello di dati (per thread multipli coordinati tra loro). Perché il codice sia corretto, il programmatore può sostanzialmente ignorare gli attributi dell’esecuzione SIMT dei warp; tuttavia, si può ottenere un incremento sostanziale delle prestazioni avendo cura che il codice raramente abbia bisogno di divergere all’interno di un warp. In pratica, si tratta di un caso analogo al ruolo delle linee di una cache nel codice tradizionale: la dimensione della linea di cache può essere tranquillamente ignorata se l’obiettivo è la correttezza del codice, mentre deve essere considerata nella struttura del codice se l’obiettivo è raggiungere le massime prestazioni di picco.

C25

C26

Appendice C  La grafica e il calcolo con la GPU

© 978-88-08-06279-6

L’esecuzione dei warp SIMT e le biforcazioni del codice L’approccio SIMT alla pianificazione dell’esecuzione dei warp indipendenti è più flessibile della pianificazione effettuata dalle architetture GPU precedenti. Un warp comprende thread paralleli dello stesso tipo: di vertice, di geometria, di pixel o di calcolo. L’unità di base per l’elaborazione degli shader dei frammenti di pixel è il quadrilatero di 2 pixel per 2, implementato mediante quattro thread di shading di pixel. L’unità di controllo del multiprocessore racchiude i quadrilateri di pixel in un warp. Analogamente, essa raggruppa i vertici e le primitive geometriche in warp e racchiude anche i thread di calcolo in un warp. Un blocco di thread contiene uno o più warp. L’architettura SIMT condivide l’unità di prelievo e distribuzione delle istruzioni in modo efficiente tra i thread paralleli di uno stesso warp, ma richiede che tutti i thread del warp siano attivi per ottenere la massima prestazione. Tale multiprocessore unificato pianifica l’esecuzione ed esegue molteplici tipi di warp in modo concorrente, permettendo quindi di eseguire warp di vertice e di pixel. Lo scheduler dei warp lavora a una frequenza minore della frequenza di clock del processore, perché deve riempire le corsie di quattro thread per ogni processore core; durante ogni ciclo di pianificazione, esso seleziona un warp per l’esecuzione di un’istruzione SIMT, come mostrato in Figura C.4.2. Le istruzioni di un warp vengono lanciate in esecuzione come quattro gruppi di otto thread, generando i risultati su quattro cicli di clock del processore. La pipeline del processore impiega diversi cicli di clock per completare ogni istruzione. Se il numero di warp attivi moltiplicato per il numero di cicli di clock per warp supera la latenza della pipeline, il programmatore può ignorare tale latenza. Per questo multiprocessore, uno scheduling di tipo round robin di otto warp ha un periodo di 32 cicli di clock tra due istruzioni successive dello stesso warp. Se il programma è in grado di mantenere attivi 256 thread per ciascun multiprocessore, latenze di istruzione fino a 32 cicli di clock possono essere tenute nascoste al singolo thread sequenziale. Tuttavia, se il numero di warp attivi è piccolo, la profondità della pipeline del processore diviene visibile e può causare lo stallo dei processori. Un problema progettuale interessante è l’implementazione di una pianificazione dell’esecuzione dei warp con sovraccarico nullo per una combinazione dinamica di programmi di warp differenti e di tipi diversi di programma. Il pianificatore (scheduler) deve selezionare un warp ogni quattro cicli di clock in modo da lanciare in esecuzione un’istruzione per ciascun ciclo di clock per ogni thread, il che equivale a un IPC = 1,0 per ogni processore core. Dato che i warp sono indipendenti, le uniche dipendenze possono nascere tra le istruzioni sequenziali di uno stesso warp. Lo scheduler utilizza una tabella sulla quale annotare le dipendenze tra i registri, per identificare i warp nei quali i thread attivi sono pronti a eseguire un’istruzione. Questo rende prioritari tutti i warp pronti e seleziona quindi per il lancio in esecuzione il warp con la priorità più alta. Il calcolo della priorità deve tener conto del tipo di warp, del tipo di istruzione, nonché del desiderio di equità nei confronti di tutti i warp attivi.

La gestione dei thread e dei blocchi di thread L’unità di controllo del multiprocessore e quella delle istruzioni gestiscono i thread e i blocchi di thread. L’unità di controllo accetta richieste di elaborazione e dati in ingresso e arbitra l’accesso alle risorse condivise, comprese l’unità della tessitura, il cammino di accesso alla memoria e i collegamenti di I/O. Per i carichi di lavoro grafici, essa crea e gestisce in modo concorrente tre tipi di thread grafici: vertici, geometria e pixel. Ognuno di questi tipi di elaborazione

C.4  Architettura multiprocessore multithread

C27

grafica possiede collegamenti indipendenti di I/O e viene accumulato e raggruppato in warp SIMT di thread paralleli che eseguono lo stesso programma di thread. L’unità di controllo alloca un warp libero e i registri per i thread del warp e inizia l’esecuzione di warp sul multiprocessore. Ogni programma dichiara il numero di registri per thread di cui ha bisogno; l’unità di controllo fa partire un warp solo quando è in grado di allocare il numero di registri richiesto. Quando tutti i thread di warp hanno terminato l’esecuzione, l’unità di controllo distribuisce i risultati e libera i registri e le risorse del warp. L’unità di controllo crea insiemi di thread cooperativi (CTA, Cooperative Thread Arrays), i quali implementano blocchi di thread CUDA sottoforma di uno o più warp di thread paralleli. Essa crea un CTA quando può produrre tutti i warp del CTA e allocare tutte le risorse richieste dal CTA. Oltre ai thread e ai registri, un CTA richiede l’allocazione di memoria condivisa e le barriere. Prima di lanciare il CTA, il programma dichiara la quantità di memoria richiesta e l’unità di controllo attende fino a quando può allocare una quantità di memoria sufficiente. Si producono quindi i warp del CTA con la frequenza con cui vengono lanciati in esecuzione i warp, in modo tale che un programma CTA parta eseguendo da subito il codice con le prestazioni massime del multiprocessore. L’unità di controllo si accorge quando tutti i thread di un CTA hanno terminato, e libera quindi le risorse condivise utilizzate dal CTA e dai suoi warp.

Insieme di thread cooperativi (CTA): un insieme di thread concorrenti che esegue lo stesso programma di thread e può cooperare per calcolare un risultato. Un CTA di GPU implementa un blocco di thread CUDA.

© 978-88-08-06279-6

Le istruzioni dei thread I processori di thread SP eseguono istruzioni scalari sul singolo thread, a differenza delle precedenti architetture GPU che eseguivano istruzioni su vettori con quattro componenti per ogni programma di shading di vertici o di pixel. I programmi di shading dei vertici in genere calcolano vettori di posizione (x, y, z, w), mentre i programmi di shading dei pixel calcolano vettori di colore (rosso, verde, blu, alfa). Tuttavia, i programmi di shading stanno diventando sempre più lunghi e più scalari, ed è quindi sempre più difficile impegnare completamente anche solo due delle componenti di un’architettura vettoriale a quattro componenti delle precedenti GPU. In effetti, l’architettura SIMT parallelizza l’esecuzione su 32 thread di pixel indipendenti, anziché parallelizzare le quattro componenti vettoriali di un pixel. I programmi C/C++ di CUDA contengono in prevalenza codice scalare per il singolo thread. Le GPU precedenti impiegavano la tecnica del vector packing (cioè il raggruppamento dell’elaborazione in sottovettori, con lo scopo di guadagnare efficienza), ma ciò rendeva più complesso sia l’hardware dello scheduler sia il compilatore, dato che questo riesce a gestire più facilmente le istruzioni scalari. Le istruzioni per la tessitura rimangono vettoriali, accettando in ingresso un vettore sorgente di coordinate e restituendo un vettore di colore filtrato. Per supportare GPU diverse, con differenti formati binari delle microistruzioni, i compilatori dei linguaggi ad alto livello di grafica e di calcolo generano istruzioni intermedie a livello assembler (per esempio, le istruzioni vettoriali Direct3D o le istruzioni scalari PTX), che vengono poi ottimizzate e tradotte nelle microistruzioni binarie della GPU. La definizione dell’insieme di istruzioni PTX di NVIDIA (Parallel Thread eXecution) (2007) ha fornito ai compilatori un’architettura dell’insieme di istruzioni (ISA) stabile, garantendo la compatibilità per parecchie generazioni di GPU con l’insieme delle microistruzioni che sono in continua evoluzione. Nella fase di ottimizzazione, vengono semplicemente espanse le istruzioni vettoriali Direct3D in microistruzioni binarie scalabili multiple. Le istruzioni scalari PTX sono tradotte quasi con corrispondenza uno a uno in microistruzioni binarie scalari, sebbene alcune PTX vengano espanse in più microistruzioni e istruzioni

C28

Appendice C  La grafica e il calcolo con la GPU

© 978-88-08-06279-6

multiple PTX possano essere tradotte in un’unica microistruzione. Poiché le istruzioni intermedie a livello assembler utilizzano registri virtuali, in fase di ottimizzazione si analizzano le dipendenze tra i dati e allocati i registri reali. L’ottimizzazione, inoltre, elimina le parti di codice sorgente che non verranno mai eseguite (il cosiddetto «dead code»), concentra le istruzioni in un’unica istruzione (quando possibile) e ottimizza i punti di divergenza e convergenza delle diramazioni del codice SIMT.

L’architettura dell’insieme delle istruzioni (ISA) L’architettura dell’insieme delle istruzioni (ISA) per i thread qui descritta è una versione semplificata dell’ISA PTX dell’architettura Tesla, un insieme di istruzioni scalari a registri che comprende operazioni su interi, in virgola mobile, logiche e di conversione, funzioni speciali, istruzioni di controllo del flusso e di accesso alla memoria, e operazioni sulla tessitura. In Figura C.4.3 sono elencate le istruzioni PTX di base per i thread di una GPU; per i dettagli si rimanda alle specifiche PTX di NVIDIA (2007). Il formato di un’istruzione è: opcode.type d, a, b, c; dove d è l’operando destinazione, a, b e c sono gli operandi sorgente e .type è uno dei seguenti tipi di dati: Tipo Bit non tipizzati: 8, 16, 32 e 64 bit Intero senza segno: 8, 16, 32 e 64 bit Intero con segno: 8, 16, 32 e 64 bit Virgola mobile: 16, 32 e 64 bit

Specificatore: .type .b8, .b16, .b32, .b64 .u8, .u16, .u32, .u64 .s8, .s16, .s32, .s64 .f16, .f32, .f64

Gli operandi sorgente sono numeri scalari su 32 o 64 bit contenuti nei registri, numeri immediati o costanti; gli operandi predicato sono numeri booleani su 1 bit. Gli operandi destinazione sono registri, a eccezione dell’istruzione di scrittura in memoria. Le istruzioni vengono trasformate in istruzioni eseguite in modo condizionato applicando loro il prefisso @p oppure @!p, dove p è il registro predicato. Le istruzioni di memoria e tessitura trasferiscono scalari o vettori contenenti da due a quattro componenti, fino a 128 bit in totale. Le istruzioni PTX specificano il comportamento di un thread. Le istruzioni aritmetiche PTX operano su numeri in virgola mobile su 32 o 64 bit, e su interi con e senza segno. Le GPU più recenti supportano i dati e le operazioni in virgola mobile su 64 bit in doppia precisione (si veda il Paragrafo C.6). Sulle attuali GPU, le istruzioni PTX su 64 bit, intere e logiche, vengono tradotte in due o più microistruzioni binarie che eseguono operazioni su 32 bit. Le istruzioni per le funzioni speciali delle GPU sono limitate ai dati in virgola mobile su 32 bit. Le istruzioni di controllo di flusso nei thread sono le istruzioni di salto condizionato (branch), di chiamata (call) e di ritorno (return) da procedura e di uscita dal thread (exit), e la sincronizzazione a barriera (bar.sync). L’istruzione di salto condizionato @p bra target utilizza un registro predicato p (o !p), precedentemente scritto da un’istruzione di confronto e assegnamento di predicato (setp), per determinare se il thread debba eseguire il salto. Anche altre istruzioni possono essere predicate, eseguite cioè sulla base del valore vero o falso di un registro predicato. Le istruzioni di accesso alla memoria L’istruzione tex preleva e filtra i campioni di tessitura dalle matrici di tessitura 1D, 2D e 3D contenute in memoria mediante il sottosistema di tessitura. Il

© 978-88-08-06279-6

C.4  Architettura multiprocessore multithread

C29

Istruzioni PTX di base per i thread di una GPU Gruppo

Istruzione Esempio Significato Commenti aritmetiche .type = .s32, .u32, .f32, .s64, .u64, .f64 add.type add.f32 d, a, b d = a + b; sub.type sub.f32 d, a, b d = a - b; mul.type mul.f32 d, a, b d = a * b; moltiplica e somma mad.type add.f32 d, a, b d = a*b + c; microistruzioni multiple div.type div.f32 d, a, b d = a / b; resto intero rem.type rem.f32 d, a, b d = a % b; abs.type abs.f32 d, a d = |a|; neg.type neg.f32 d, a d = 0 - a; Aritmetiche v.m.: seleziona non NaN min.type min.f32 d, a, b d = (ab)? a:b; confronta e assegna il setp.cmp. setp.lt.f32 p, a, b p = (a < b); predicato type comparazione numerica: .cmp = eq, ne, lt, gt, ge; comparazione elementi non ordinati: .cmp = equ, neu, ltu, gtu, geu, num, nan copia mov.type mov.b32 d, a; d = a; seleziona in base al predicato selp.type selp.f32 d, a, p, b d = p? a:b; cvt.type. cvt.f32.s32 d, a d=converti(a); conversione da atype in dtype atype funzione speciale: .type = .f32 (per alcune .f64) reciproco rcp.type rcp.f32 d, a d = 1/a; radice quadrata sqrt.type sqrt.f32 d, a d =sqrt(a); reciproco della radice quadrata rsqrt.type rsqrt.f32, d, a d = 1/sqrt(a); Funzioni speciali seno sin.type sin.f32, d, a d = sin(a) coseno cos.type cos f32, d, a d = cos(a) lg2.type lg2.f32, d, a d = log(a)/log(2) logaritmo in base 2 esponenziale in base 2 ex2.type ex2.f32, d, a d = 2 ** a; logiche .type = .pred, .b32, .b64 reciproco and.type and.b32 d, a, b d = a & b; radice quadrata or.type or.b32 d, a, b d = a | b; reciproco della radice quadrata xor.type xor.b32 d, a, b d = a ^ b; Logiche complemento a 1 not.type not.b32 d, a, b d = ~a; cnot.type cnot.b32 d, a, b d = (a==0)? 1:0; operazione not logica del C scorrimento a sinistra shl.type shl.b32 d, a, b d = a << b; scorrimento a destra shr.type shr.b32 d, a, b d = a >> b; memoria .space = .global, shared, local, const; .type = .b8, .u8, .s8, .b16, .b32, .b64 ld.space. ld.global .b32, d, [a+off] d=*(a+off); lettura di space da memoria type st.space. st.shared.b32 [d+off], a *(d+off)=a; Scrittura di space in type memoria Accesso a tex. lettura di un campione tex.2d.v4.f32.f32 d, a, b d=tex2d(a,b); memoria della tessitura nd.dtyp. btype operazione atomica di letturaatom.spc. atom.global.add.u32 d,[a],b atomic {d=*a; modifica-scrittura *a=op(*a,b); op.type atom.global.cas.b32 d,[a],b,c atom .op = and, or, xor, add, min, max, exch, cas; .spc = .global; .type = .b32



C30

Appendice C  La grafica e il calcolo con la GPU

Gruppo

Controllo di flusso

Istruzione

Esempio

© 978-88-08-06279-6

Significato

Commenti

branch

@p bra target

if (p) goto target;

salto condizionato

call

call (ret), func, (param)

ret=func (param);

chiamata a funzione

ret

ret

return;

ritorno da funzione

bar.sync

bar.sync d

wait for threads; sincronizzazione a barriera /*attesa dei thread*/

exit

Exit

exit;

termine dell’esecuzione di un thread

Figura C.4.3. Istruzioni PTX di base per i thread di una GPU.

prelievo della tessitura in genere viene effettuato utilizzando coordinate interpolate, in virgola mobile. Quando un thread grafico di shading di pixel calcola il colore di un pixel di un frammento, il processore che esegue le operazioni di rasterizzazione miscela il colore del pixel con il colore alla posizione (x, y) e scrive in memoria il colore risultante. Per supportare le richieste del calcolo e del linguaggio C/C++, l’ISA PTX Tesla è dotato di istruzioni di lettura e scrittura della memoria. Queste si basano sull’indirizzamento intero al byte, utilizzando indirizzi formati dal contenuto di un registro a cui viene sommato uno spiazzamento, per facilitare la convenzionale ottimizzazione di codice da parte del compilatore. Le istruzioni di lettura/scrittura della memoria sono comuni nei processori, ma rappresentano una novità significativa nelle GPU con architettura Tesla, poiché le precedenti GPU fornivano soltanto gli accessi ai pixel e alla tessitura richiesti dalle API della grafica. Per le applicazioni di calcolo, le istruzioni di lettura e scrittura accedono ai tre spazi di memoria di lettura/scrittura che implementano i corrispondenti tre spazi di memoria CUDA descritti nel Paragrafo C.3: – Memoria locale, per i dati temporanei privati di ogni singolo thread, implementata nella DRAM esterna. – Memoria condivisa, per l’accesso a bassa latenza ai dati condivisi dai thread che cooperano nello stesso CTA/blocco di thread, implementata in SRAM sul chip. – Memoria globale, per grandi quantità di dati condivise da tutti i thread di un’applicazione di calcolo, implementata nella DRAM esterna. Le istruzioni di lettura e scrittura della memoria ld.global, st.global, ld.shared, st.shared, ld.local e st.local accedono rispettivamente agli spazi di memoria globale, condivisa e locale. I programmi di calcolo utilizzano l’istruzione di sincronizzazione veloce a barriera, bar.synch, per sincronizzare i thread all’interno di un CTA o blocco di thread, i quali comunicano tra loro per mezzo della memoria condivisa e di quella globale. Per incrementare la larghezza di banda della memoria e ridurre il sovraccarico di lavoro, le istruzioni load/store della memoria globale e locale fondono tutte le richieste dei singoli thread paralleli di uno stesso warp SIMT nella richiesta di un unico blocco di memoria, quando gli indirizzi cadono all’interno dello stesso blocco e soddisfano i criteri di allineamento. La fusione delle richieste dei dati dalla memoria fornisce un notevole incremento delle prestazioni rispetto a richieste separate da parte dei singoli thread. L’elevato numero di thread del multiprocessore, unito alle richieste di lettura di grandi

© 978-88-08-06279-6

C.4  Architettura multiprocessore multithread

quantità di dati, aiuta a colmare le latenze dovute alla lettura della memoria locale e globale implementate nella DRAM esterna. Le più recenti GPU con architettura Tesla, inoltre, forniscono efficienti operazioni atomiche sulla memoria mediante le istruzioni atom.op.u32, tra cui le operazioni su interi add, min, max, and, or, xor, exchange e cas (confronta e scambia di posto), facilitando le operazioni di traduzione del codice sequenziale in codice parallelo e la gestione delle strutture dati parallele. La sincronizzazione a barriera per la comunicazione tra i thread La sincronizzazione veloce a barriera permette ai programmi CUDA di comunicare frequentemente attraverso la memoria condivisa e la memoria globale, semplicemente chiamando la funzione __syncthreads() come parte di ogni passo di comunicazione tra i thread. La funzione intrinseca di sincronizzazione genera una singola istruzione bar.sync. Tuttavia, implementare una sincronizzazione veloce a barriera tra thread per i 512 thread di ogni blocco CUDA è molto complesso. Raggruppando i thread in warp SIMT di 32 thread si riduce la complessità della sincronizzazione di un fattore 32. I thread attendono a una barriera all’interno dello scheduler dei thread SIMT, in modo da non utilizzare cicli di processore durante l’attesa. Quando un thread esegue un’istruzione bar.sync, incrementa il contatore del numero di thread arrivati alla barriera e lo scheduler registra che il thread è in attesa. Quando tutti i thread del CTA sono arrivati alla barriera, il conteggio corrisponderà al numero di thread e lo scheduler libererà tutti i thread in attesa, facendo riprendere la loro esecuzione.

I processori a flusso continuo (SP) Un processore core multithread a flusso continuo (streaming processor) è l’elaboratore principale delle istruzioni nel multiprocessore. Il suo insieme di registri (RF) fornisce 1024 registri scalari a 32 bit per un massimo di 64 thread ed è in grado di eseguire tutte le operazioni fondamentali in virgola mobile, tra cui add.f32, mul.f32, mad.f32 (moltiplica e somma in virgola mobile), min.f32, max.f32, setp.f32 (confronta in virgola mobile e imposta il predicato). Le operazioni di somma e prodotto in virgola mobile sono compatibili con lo standard IEEE 754 per i numeri in virgola mobile in singola precisione, compresi i valori not a number (NaN) e infinito. Il core SP implementa anche tutte le istruzioni PTX di aritmetica intera, confronto, conversione e istruzioni logiche riportate nella tabella di Figura C.4.3. Le operazioni add e mul in virgola mobile utilizzano la modalità di arrotondamento round to nearest even (al numero pari più vicino) IEEE come modalità di arrotondamento predefinita. L’istruzione multiply add (moltiplica e somma) in virgola mobile, mad.f32, effettua una moltiplicazione con troncamento seguita da una somma con arrotondamento round to nearest even. Il processore SP trasforma gli operandi in ingresso di tipo denormalizzato nel numero zero, preservandone il segno. I risultati che vanno al di sotto dell’intervallo degli esponenti rappresentabili dopo l’arrotondamento vengono anch’essi trasformati in zero (preservandone il segno).

Le unità per le funzioni speciali (SFU) Alcune istruzioni dei thread possono essere eseguite sulla SFU in modo concorrente con altre istruzioni di thread che vengono eseguite sui processori SP. La SFU esegue le istruzioni per le funzioni speciali riportate in Figura C.4.3, le

C31

C32

Appendice C  La grafica e il calcolo con la GPU

© 978-88-08-06279-6

quali calcolano l’approssimazione in virgola mobile su 32 bit del reciproco di un numero, del reciproco della radice quadrata e delle principali funzioni trascendenti. Essa implementa anche l’interpolazione piana in virgola mobile su 32 bit per gli shader dei pixel, fornendo interpolazioni accurate degli attributi, come il colore, la profondità e le coordinate di tessitura. La pipeline dalla SFU produce il risultato di una funzione speciale in virgola mobile a 32 bit per ciclo di clock; le due SFU contenute in ciascun multiprocessore eseguono quindi istruzioni di funzioni speciali a un quarto della frequenza di esecuzione delle istruzioni semplici eseguite dagli otto SP. Le SFU eseguono anche le istruzioni di moltiplicazione mul.f32 in parallelo agli otto SP, aumentando così la velocità di calcolo di picco fino al 50% per i thread che presentano la combinazione adatta di istruzioni. Per il calcolo delle funzioni, la SFU dell’architettura Tesla utilizza un’approssimazione quadratica basata sull’approssimazione minimax migliorata per approssimare il reciproco di un numero, il reciproco della radice quadrata e le funzioni log2x, 2x, seno e coseno. L’accuratezza della stima delle funzioni varia da 22 a 24 bit di mantissa. Per maggiori dettagli sull’aritmetica delle SFU, si veda il Paragrafo C.6.

Confronto con altri multiprocessori In confronto ad altre architetture vettoriali SIMD, come l’SSE dell’x86, il multiprocessore SIMT può eseguire singoli thread in maniera indipendente invece di eseguirli sempre insieme a gruppi sincroni. L’hardware SIMT ricerca il parallelismo tra i dati nei thread indipendenti, mentre l’hardware SIMD richiede che sia il software a esprimere in modo esplicito il parallelismo tra i dati in ogni istruzione vettoriale. Un’architettura SIMT esegue un warp di 32 thread in modo sincrono quando i thread seguono lo stesso flusso di esecuzione, ma può eseguire ogni thread in modo indipendente quando il flusso di esecuzione dei thread diverge. Il vantaggio è notevole, perché i programmi e le istruzioni SIMT descrivono semplicemente il comportamento di un singolo thread indipendente, anziché un vettore di dati SIMD con quattro o più corsie di elaborazione dei dati. Nonostante ciò, il multiprocessore SIMT presenta un’efficienza pari a un’architettura SIMD, distribuendo l’area e il costo di un’unità istruzioni sui 32 thread di un warp e sugli otto processori core a flusso continuo. L’approccio SIMT unisce l’efficienza dei processori SIMD e la produttività del multithreading, eliminando la necessità di codificare esplicitamente i vettori SIMD per le condizioni al contorno e per le divergenze parziali del flusso di elaborazione. Il multiprocessore SIMT impone solamente un leggero sovraccarico di lavoro, essendo il multithread implementato in hardware, con sincronizzazione hardware a barriera. Ciò permette agli shader grafici e ai thread CUDA di esprimere un parallelismo a grana molto fine. Grafica e programmi CUDA utilizzano i thread per esprimere un parallelismo tra i dati a grana fine per ogni thread del programma, piuttosto che forzare il programmatore a esprimere il parallelismo con istruzioni vettoriali SIMD. È più semplice e più produttivo sviluppare codice scalare per un singolo thread che codice vettoriale. Inoltre, il multiprocessore SIMT esegue il codice con un’efficienza pari ai processori SIMD. Accoppiando otto processori core a flusso continuo in un multiprocessore e utilizzando un numero scalabile di tali multiprocessori, si crea un multiprocessore a due livelli costituito a sua volta da multiprocessori. Il modello di programmazione CUDA sfrutta tale gerarchia a due livelli fornendo thread individuali per il calcolo parallelo a grana fine e griglie di blocchi di thread per le operazioni parallele a grana grossa. Lo stesso programma strutturato in

© 978-88-08-06279-6

C.5  Il sistema parallelo di memoria

thread può effettuare operazioni sia a grana fine sia a grana grossa. Per contro, le CPU con istruzioni vettoriali SIMD devono utilizzare due differenti modelli di programmazione per effettuare operazioni a grana fine e a grana grossa: thread paralleli a grana grossa su core differenti e istruzioni vettoriali SIMD per il parallelismo dei dati a grana fine.

I multiprocessori multithread in sintesi L’esempio di multiprocessore GPU basato su architettura Tesla che abbiamo visto è fortemente multithread, poiché è in grado di eseguire fino a 512 thread leggeri in modo concorrente che supportano shader di pixel e thread CUDA. Esso impiega una variante dell’architettura SIMD e un approccio multithreading chiamato SIMT (singola istruzione, thread multipli) per distribuire in modo efficiente un’istruzione a un warp di 32 thread paralleli, pur permettendo a ogni thread di scegliere diramazioni del codice ed essere eseguito in modo indipendente. Ogni thread esegue il proprio flusso di istruzioni su uno degli otto processori core a flusso continuo (SP) ad architettura multithreading, per un massimo di 64 thread. L’architettura dell’insieme delle istruzioni (ISA) PTX è un’ISA scalare di tipo lettura/scrittura a registri che descrive l’esecuzione di un singolo thread. Dato che le istruzioni PTX sono ottimizzate e tradotte in microistruzioni binarie per la specifica GPU, le istruzioni hardware possono evolvere rapidamente senza dover riprogettare da capo i compilatori e gli strumenti software che generano le istruzioni PTX.

C.5 * Il sistema parallelo di memoria Al di fuori della GPU stessa, il sottosistema di memoria è il componente più importante per le prestazioni di un sistema grafico. I carichi di lavoro della grafica richiedono una velocità di trasferimento molto alta da e verso la memoria. Le operazioni di scrittura e miscelazione (lettura-modifica-scrittura) dei pixel, la lettura e la scrittura del buffer di profondità e la lettura delle mappe di tessitura, oltre alla lettura dei comandi e dei dati dei vertici o degli attributi degli oggetti, costituiscono la maggior parte del traffico di memoria. Le GPU moderne sono fortemente parallele, come mostrato in Figura C.2.5; per esempio, la GeForce 8800 è in grado di elaborare 32 pixel per ciclo di clock, a 600 MHz. Ogni pixel tipicamente richiede la lettura e la scrittura del colore e della profondità, rappresentate complessivamente su 4 byte. Solitamente per generare il colore di un pixel vengono letti in media due o tre texel (elementi di tessitura), ciascuno di 4 byte. Vengono quindi richiesti, in media, 28 byte/ pixel per ciascuno dei 32 pixel, pari a 896 byte per ciclo di clock. Chiaramente la banda richiesta al sistema di memoria è enorme. Per soddisfare questi requisiti, i sistemi di memoria delle GPU possiedono le seguenti caratteristiche: – Ampiezza elevata, nel senso che è presente un elevato numero di piedini per trasferire i dati tra la GPU e i suoi dispositivi di memoria, e che la struttura a matrice della memoria stessa è composta da molti chip di DRAM, in modo da poter sfruttare tutta la larghezza del bus dati. – Elevata velocità, nel senso che sono implementate tecniche aggressive di segnalazione per massimizzare la velocità di trasferimento dei dati (bit/s) per ogni piedino del chip. – Le GPU cercano di utilizzare ogni ciclo di clock disponibile per trasferire dati da o verso la matrice della memoria. Per raggiungere questo obiettivo,

C33

C34

Appendice C  La grafica e il calcolo con la GPU

© 978-88-08-06279-6

nelle GPU non si cerca di minimizzare la latenza del sistema di memoria: un throughput elevato (efficienza di utilizzo) e una latenza bassa sono intrinsecamente in conflitto. – Vengono impiegate tecniche di compressione, sia con perdita di informazioni (eventualità di cui il programmatore deve essere ben consapevole) sia senza perdita, le quali vengono applicate in maniera opportunistica e risultano trasparenti alle applicazioni. – Cache e strutture che fondono le richieste di trasferimento vengono utilizzate per ridurre la quantità di traffico richiesto fuori dal chip e per garantire che i cicli di clock spesi per il trasferimento dei dati vengano sfruttati nella maniera più completa possibile.

Considerazioni sulle DRAM Le GPU devono tener conto delle caratteristiche particolari delle DRAM. I chip di DRAM sono organizzati internamente in banchi multipli (tipicamente da quattro a otto), dove ciascun banco contiene un numero di righe pari a una potenza di 2 (tipicamente 16 384) e ciascuna riga contiene di solito un numero di bit che è potenza di 2 (tipicamente 8192). Le DRAM impongono al processore che le controlla una serie di requisiti di temporizzazione. Per esempio, vengono richieste dozzine di cicli di clock per attivare una riga ma, una volta attivata, i bit in essa contenuti sono accessibili, nello stesso tempo, attraverso il loro indirizzo di colonna, e si può leggere uno di questi bit ogni quattro cicli. Le DRAM sincrone DDR (Double Data Read) trasferiscono dati sul fronte sia di salita sia di discesa del clock dell’interfaccia (si veda il Capitolo 5). Una DRAM DDR con clock a 1 GHz trasferisce quindi dati al ritmo di 2 Gb/s per ogni piedino del chip. Le DRAM DDR per la grafica hanno di solito 32 piedini per il trasferimento bidirezionale dei dati e si possono quindi leggere o scrivere in una DRAM fino a otto byte per ciclo di clock. Le GPU internamente possiedono un gran numero di generatori di traffico di memoria. Ciascuno dei diversi stadi della pipeline grafica logica genera il proprio flusso di richieste: prelievo di comandi e di attributi dei vertici, prelievo e lettura/scrittura della tessitura per gli shader, lettura/scrittura della profondità e del colore dei pixel. Per ogni stadio logico, ci sono spesso unità multiple indipendenti che producono risultati in parallelo. Ognuno di questi è un generatore indipendente di richieste di trasferimento con la memoria. Dal punto di vista del sistema di memoria, si osserva un numero enorme di richieste indipendenti in arrivo. Ciò risulta essere in naturale contrasto con l’organizzazione di riferimento preferita dalle DRAM. Una possibile soluzione è che il controllore di memoria mantenga heap separati per il traffico collegato ai differenti banchi di DRAM; il controllore attenderà che ci sia una quantità sufficiente di traffico pendente per una specifica riga di DRAM, per poi attivare la riga e trasferire tutti i dati in una volta. Si noti che accumulare richieste pendenti è vantaggioso per la località di riga della DRAM, e quindi per l’uso efficiente del bus dati, ma causa latenze medie più lunghe verso i componenti che hanno effettuato le richieste di trasferimento, poiché le richieste devono aspettare che arrivino altre richieste. Nella progettazione del sistema occorre tener conto della necessità che nessuna richiesta debba aspettare troppo a lungo, altrimenti è possibile che alcune unità di elaborazione finiscano per provocare l’inattività anche dei processori vicini. I sottosistemi di memoria delle GPU sono organizzati in partizioni multiple di memoria, ciascuna delle quali comprende un controllore della memoria completamente indipendente e uno o due dispositivi DRAM che appartengono esclusivamente a quella partizione. Per ottenere il miglior bilanciamento del carico e avvicinarsi quindi alle massime prestazioni teoriche di n parti-

© 978-88-08-06279-6

C.5  Il sistema parallelo di memoria

zioni, gli indirizzi vengono interlacciati in modo fine e uniforme su tutte le partizioni di memoria. Il passo di interlacciamento di ciascuna partizione è tipicamente un blocco di qualche centinaio di byte. Il numero di partizioni di memoria viene progettato in modo tale da bilanciare il numero dei processori e degli altri dispositivi che generano richieste di trasferimento dati con la memoria.

Le memorie cache I carichi di lavoro delle GPU presentano insiemi di lavoro di grandi dimensioni, dell’ordine delle centinaia di megabyte, per generare una singola immagine grafica. A differenza delle CPU, non è pratico costruire una cache sul chip grande abbastanza da contenere qualcosa che si avvicini all’intero insieme di lavoro di un’applicazione grafica. Mentre le CPU possono contare su altissimi tassi di hit della cache (99,9% e oltre), le GPU sperimentano tassi di hit vicini al 90%, e devono perciò fare i conti con molti casi di miss durante l’elaborazione. Mentre una CPU può ragionevolmente essere progettata per mettersi in attesa quando si presenta una delle rare miss, una GPU ha la necessità di proseguire anche in presenza di miss e hit mescolate fra loro. Un’architettura di questo tipo è detta architettura cache a flusso continuo. Le cache delle GPU devono fornire una banda notevolmente larga ai loro utilizzatori. Consideriamo il caso di una cache di tessitura. Una tipica unità di tessitura è in grado di calcolare due interpolazioni bilineari per ciascun gruppo di quattro pixel, per ogni ciclo di clock; una GPU può contenere molte di queste unità di tessitura, tutte operanti in modo indipendente. Ciascuna interpolazione bilineare richiede quattro texel separati, e ogni texel potrebbe essere un numero su 64 bit; quattro componenti da 16 bit sono tipici. Ne risulta una larghezza di banda totale di 2 × 4 × 4 × 64 = 2048 bit per ciclo di clock. Ogni singolo texel di 64 bit ha un indirizzo indipendente, quindi la cache deve gestire 32 indirizzi indipendenti per ciclo di clock. Ciò favorisce naturalmente un’organizzazione multibanco e/o multiporta delle matrici di SRAM.

L’unità di gestione della memoria (MMU) Le moderne GPU sono in grado di mappare indirizzi virtuali su indirizzi fisici. Nella GeForce 8800, tutte le unità di elaborazione generano indirizzi di memoria in uno spazio di indirizzamento virtuale a 40 bit. Per il calcolo, le istruzioni dei thread per l’accesso a memoria (load e store) utilizzano indirizzi su 32 bit, i quali vengono estesi a indirizzi virtuali di 40 bit aggiungendo un offset su 40 bit. Un’unità di gestione della memoria effettua la traduzione degli indirizzi virtuali nei corrispondenti indirizzi fisici; l’hardware legge le tabelle delle pagine dalla memoria locale per rispondere alle miss per conto di una gerarchia di buffer di traduzione degli indirizzi (chiamati TLB, Transition Lookaside Buffer) sparsa tra i processori e i motori di rendering. Oltre ai bit delle pagine fisiche, gli elementi di una tabella delle pagine di una GPU specificano l’algoritmo di compressione utilizzato in ogni pagina. Le dimensioni delle pagine vanno da 4 a 128 KB.

Le aree di memoria Come accennato nel Paragrafo C.3, CUDA utilizza differenti spazi di memoria per permettere al programmatore di memorizzare i dati nella maniera ottimale dal punto di vista delle prestazioni. La descrizione seguente si riferisce alle GPU con architettura Tesla di NVIDIA.

C35

C36

Appendice C  La grafica e il calcolo con la GPU

© 978-88-08-06279-6

La memoria globale La memoria globale (global memory) risiede nella DRAM esterna e non è locale a nessuno dei multiprocessori a flusso continuo (SM), poiché è pensata per la comunicazione tra i diversi CTA (blocchi di thread) di griglie diverse. Di fatto, i numerosi CTA che accedono a una stessa locazione della memoria globale potrebbero essere eseguiti nella GPU in tempi differenti; in CUDA, il programmatore non conosce la sequenza con cui vengono eseguiti i CTA (si tratta di un vincolo progettuale). Poiché lo spazio di indirizzamento è distribuito uniformemente fra tutte le partizioni di memoria, deve essere presente un cammino di lettura/scrittura che collega ogni processore a flusso continuo a ogni partizione della DRAM. Non è garantito che la sequenza degli accessi alla memoria globale da parte di thread differenti (e da differenti processori) sia consistente. I programmi dei thread vedono un modello di ordinamento della memoria rilassato. All’interno di un thread viene preservato l’ordine delle letture e delle scritture a uno stesso indirizzo, mentre l’ordine degli accessi a indirizzi differenti può non essere mantenuto. Le letture e scritture della memoria richieste da thread differenti non mantengono l’ordinamento temporale. All’interno di un CTA, l’istruzione di sincronizzazione a barriera bar.sync può essere utilizzata per ottenere una sequenza temporale rigorosa dei thread del CTA, e l’istruzione di thread membar è un’operazione di «barriera/recinto» della memoria che registra gli accessi precedenti alla memoria e li rende visibili agli altri thread prima di procedere. I thread possono anche avvalersi delle operazioni atomiche sulla memoria, descritte nel Paragrafo C.4, per coordinare il lavoro sulla memoria da essi condivisa.

La memoria condivisa La memoria condivisa (shared memory), dedicata a ciascun CTA, è visibile soltanto ai thread che appartengono a quel CTA e alloca spazio di memoria solo dall’istante in cui il CTA viene creato fino all’istante in cui termina; per questo motivo la memoria condivisa può risiedere sul chip. Questo approccio presenta molti vantaggi: anzitutto il traffico della memoria condivisa non ha bisogno di competere con la limitata banda di trasferimento verso l’esterno del chip, utilizzata per l’accesso alla memoria globale; in secondo luogo, risulta efficiente costituire strutture di memoria con banda elevatissima sul chip per supportare le richieste di lettura/scrittura di ciascun multiprocessore a flusso continuo. Di fatto, la memoria condivisa è strettamente accoppiata al multiprocessore a flusso continuo. Ogni multiprocessore a flusso continuo contiene otto processori di thread. Durante un ciclo di clock della memoria condivisa, ciascun processore può elaborare le istruzioni di due thread, per cui in ogni ciclo di clock devono essere gestite le richieste alla memoria condivisa di 16 thread. Poiché ogni thread può generare i propri indirizzi (e normalmente tali indirizzi sono unici), la memoria condivisa è costruita utilizzando 16 banchi di SRAM indirizzabili in maniera indipendente. Per gruppi di indirizzi comuni, 16 banchi sono sufficienti per sostenere il flusso degli accessi, ma si possono verificare dei casi patologici «anomali»: per esempio, potrebbe accadere che tutti i 16 thread accedano a indirizzi differenti dello stesso banco di SRAM; deve essere possibile trasferire una richiesta proveniente da ciascuna corsia di thread a un qualsiasi banco di SRAM, per cui è necessaria una rete di interconnessione 16 × 16.

La memoria locale La memoria locale (local memory), dedicata a ciascun thread, è una memoria privata visibile soltanto al singolo thread. Dal punto di vista architetturale,

© 978-88-08-06279-6

C.5  Il sistema parallelo di memoria

è più grande dell’insieme dei registri del thread. Per permettere di allocare vaste aree di questa memoria (ricordiamo che lo spazio totale da allocare è lo spazio di allocazione di ciascun thread moltiplicato per il numero di thread attivi), si utilizza la DRAM esterna. Sebbene la memoria globale e quella locale di ciascun thread risiedano all’esterno del chip, ben si prestano ad avere una cache sul chip.

La memoria delle costanti La memoria delle costanti (constant memory) è una memoria di sola lettura per i programmi che vengono eseguiti sul multiprocessore SM, e può essere scritta mediante opportuni comandi inviati alla GPU. Essa risiede nella DRAM esterna e utilizza la cache del SM. Dato che comunemente tutti o la maggior parte dei thread di un warp SIMT leggono le costanti dallo stesso indirizzo, è sufficiente l’accesso a un unico indirizzo per ciclo di clock. La cache delle costanti è progettata per distribuire valori scalari a tutti i thread di ogni warp.

La memoria di tessitura La memoria di tessitura (texture memory) contiene matrici di dati di grandi dimensioni ed è a sola lettura. Le tessiture utilizzate nel calcolo generico hanno gli stessi attributi e funzionalità delle tessiture utilizzate per la grafica 3D. Sebbene le tessiture siano costituite di solito da immagini bidimensionali (matrici 2D di valori di pixel), sono disponibili anche tessiture 1D (lineari) e 3D (di volume). Un programma di calcolo referenzia una tessitura utilizzando l’istruzione tex. Gli operandi comprendono un identificatore del nome della tessitura e 1, 2 o 3 coordinate a seconda del numero di dimensioni della tessitura. Nelle coordinate (espresse in virgola mobile), la parte frazionaria specifica la posizione, spesso situata tra due texel. Le coordinate non intere richiamano un’interpolazione bilineare pesata (per una tessitura 2D) dei quattro campioni più vicini per poi restituire il risultato al programma. La tessitura prelevata viene messa in una cache gerarchica a flusso continuo, progettata per ottimizzare un flusso continuo di prelievi di tessitura da parte di migliaia di thread concorrenti. Alcuni programmi utilizzano il prelievo della tessitura come metodo per portare in cache la memoria globale.

Le superfici Superficie è un termine generico per indicare una matrice mono-, bi- o tridimensionale di valori di pixel e un formato a essi associato. Si possono definire diversi formati: per esempio, un pixel può essere definito come quaterna di componenti intere di 8 bit RGBA, oppure come quaterna di componenti in virgola mobile di 16 bit. Un kernel di programma non ha bisogno di conoscere il tipo di superficie. L’istruzione tex converte il risultato in numeri in virgola mobile, in funzione del formato della superficie.

Lettura e scrittura in memoria Le istruzioni di lettura e scrittura con indirizzamento intero al byte permettono lo sviluppo e la compilazione di programmi in linguaggi convenzionali, quali il C e il C++. I programmi CUDA utilizzano istruzioni di load/store per accedere alla memoria.

C37

C38

Appendice C  La grafica e il calcolo con la GPU

© 978-88-08-06279-6

Per aumentare la larghezza di banda del trasferimento con la memoria e ridurre il carico di lavoro aggiuntivo, le istruzioni di load/store della memoria locale e globale fondono le richieste individuali dei thread paralleli dello stesso warp in un’unica richiesta di blocco di memoria, a condizione che gli indirizzi siano riferiti allo stesso blocco e soddisfino opportuni criteri di allineamento. La fusione di tante piccole richieste individuali di memoria nella richiesta di un blocco di grandi dimensioni (coalescenza) porta a un significativo incremento delle prestazioni rispetto alle richieste separate. Il grande numero di thread dei multiprocessori unito al supporto a un numero elevato di richieste di lettura pendenti, aiuta a riempire le latenze di lettura della memoria locale e globale, costituita da DRAM esterna.

I ROP Come mostrato in figura C.2.5, le GPU con architettura Tesla di NVIDIA contengono una schiera scalabile di processori a flusso continuo (SPA, Streaming Processor Array) che svolge tutti i calcoli programmabili della GPU, e un sistema di memoria scalabile che comprende il controllo della memoria DRAM esterna e i processori a funzione prefissata per le operazioni di rasterizzazione (ROP, Raster Operation Processors), i quali effettuano le operazioni sul colore e sulla profondità nel frame buffer, operando direttamente sulla memoria. Ciascuna unità ROP è accoppiata a una specifica partizione di memoria e ciascuna partizione ROP viene alimentata dai multiprocessori SM attraverso una rete di interconnessione. Ogni ROP è responsabile dei controlli sulla profondità e sugli stencil, nonché della miscelatura del colore. I ROP e i controllori della memoria collaborano all’implementazione della compressione senza perdita (fino a 8:1) del colore e della profondità, per ridurre la richiesta di banda di trasferimento verso l’esterno. Le unità ROP svolgono anche operazioni atomiche sulla memoria.

C.6 * Aritmetica in virgola mobile Le GPU attuali effettuano la maggior parte delle operazioni aritmetiche nei processori core programmabili utilizzando operazioni in virgola mobile su 32 bit a singola precisione, compatibili con lo standard IEEE 754 (si veda il Capitolo 3). L’aritmetica in virgola fissa delle GPU precedenti è stata soppiantata da quella in virgola mobile su 16 bit, poi su 24 bit, su 32 bit, e infine dall’aritmetica in virgola mobile su 32 bit compatibile con l’IEEE 754. Alcune funzioni logiche della GPU, come l’hardware per il filtraggio della tessitura, continuano ad avvalersi di formati numerici proprietari. Le GPU recenti forniscono anche istruzioni in virgola mobile su 64 bit a doppia precisione compatibili con l’IEEE 754.

I formati supportati

Mezza precisione: un formato binario su 16 bit per i numeri in virgola mobile con 1 bit di segno, 5 bit di esponente e 10 bit di parte frazionaria; prevede un uno implicito prima della virgola.

Lo standard IEEE 754 per l’aritmetica in virgola mobile (2008) definisce i formati di base e di memorizzazione. Le GPU utilizzano due dei formati di base per il calcolo: il formato binario a virgola mobile su 32 e 64 bit, comunemente chiamati singola precisione e doppia precisione. Lo standard specifica anche un formato a virgola mobile per la memorizzazione di numeri binari su 16 bit, denominato mezza precisione. Le GPU e il linguaggio di shading Cg utilizzano questo formato compatto su 16 bit per ottenere una memorizzazione e un trasferimento efficiente, pur mantenendo un’elevata gamma dinamica. Le GPU effettuano un gran numero di operazioni di filtraggio della tessitura e di miscelazione dei pixel su valori in mezza precisione all’interno delle unità di filtraggio della tessitura e delle operazioni di rasterizzazione. Il formato di file per le

© 978-88-08-06279-6

C.6  Aritmetica in virgola mobile

C39

immagini ad alta gamma dinamica OpenEXR, sviluppato da Industrial Light and Magic (2003), utilizza questo formato a mezza precisione per i valori delle componenti di colore in applicazioni di sintesi digitale di immagini e film.

L’aritmetica di base Le operazioni più comuni in virgola mobile in singola precisione nei core programmabili delle GPU comprendono l’addizione, la moltiplicazione, la moltiplicazione e somma (multiply-add), il minimo, il massimo, il confronto, l’impostazione di un predicato e la conversione tra numeri interi e in virgola mobile. Spesso le istruzioni in virgola mobile forniscono modificatori degli operandi sorgente per ottenere la loro negazione o il loro valore assoluto. Nella maggior parte delle GPU attuali, le operazioni di addizione e moltiplicazione in virgola mobile sono compatibili con lo standard IEEE 754 per i numeri in virgola mobile a singola precisione, compresi i valori NaN e infinito. Le operazioni di addizione e moltiplicazione in virgola mobile utilizzano come modalità predefinita di arrotondamento l’«arrotondamento al numero pari più vicino» (round to nearest even) dello standard IEEE. Per aumentare il throughput dell’esecuzione delle istruzioni in virgola mobile, le GPU utilizzano spesso un’istruzione composta da moltiplicazione e somma (mad). L’operazione moltiplica e somma (MAD) esegue una moltiplicazione in virgola mobile con troncamento seguita da un’addizione in virgola mobile con arrotondamento al numero pari più vicino. Essa esegue due istruzioni in virgola mobile in un solo ciclo di clock, senza bisogno che lo scheduler lanci due istruzioni separate, ma le due operazioni non vengono fuse assieme e il troncamento del prodotto viene effettuato prima dell’addizione. Questo rende l’istruzione diversa dall’istruzione di moltiplicazione e somma integrate, illustrata nel terzo capitolo e utilizzata più avanti in questo Paragrafo. Le GPU tipicamente convertono in zero gli operandi sorgente denormalizzati preservando il segno, e convertono a zero preservando il segno dopo l’arrotondamento anche i risultati che si trovano al di sotto della gamma degli esponenti rappresentabili.

Moltiplica e somma (MAD): una singola istruzione in virgola mobile che esegue un’operazione composta da una moltiplicazione seguita da una somma.

L’aritmetica specializzata Le GPU sono provviste di hardware per accelerare il calcolo di alcune funzioni speciali, l’interpolazione degli attributi e il filtraggio della tessitura. Le funzioni speciali comprendono coseno, seno, esponenziale e logaritmo binario (in base 2), reciproco e reciproco della radice quadrata. Le istruzioni di interpolazione degli attributi consentono di calcolare in modo efficiente gli attributi dei pixel mediante la valutazione dell’equazione associata sul piano. L’unità per le funzioni speciali (SFU, Special Function Unit), presentata nel Paragrafo C.4, calcola le funzioni speciali e interpola gli attributi sul piano (Oberman e Siu, 2005). Esistono diversi metodi per calcolare le funzioni speciali in hardware. È dimostrato che l’interpolazione quadratica basata su approssimazioni migliorate di tipo minimax è un metodo molto efficiente per approssimare alcune funzioni in hardware, tra cui il reciproco, il reciproco della radice quadrata, il log2 x, la funzione 2x, il seno e il coseno. Descriviamo ora sinteticamente il metodo di interpolazione quadratica utilizzato dalle SFU. Per un operando binario in ingresso X caratterizzato da un significando di n bit, il significando viene suddiviso in due parti: chiamiamo Xs la parte superiore contenente m bit e Xi la parte inferiore contenente n – m bit. Gli m bit superiori, Xs, vengono utilizzati per consultare un insieme di tre tabelle di corrispondenza le quali forniscono tre coefficienti su un

Unità per le funzioni speciali (SFU): un’unità hardware che calcola funzioni speciali e interpola attributi bidimensionali.

C40

Appendice C  La grafica e il calcolo con la GPU

© 978-88-08-06279-6

numero finito di bit: C0, C1 e C2. Ogni funzione da approssimare richiede un proprio insieme di tabelle. Questi coefficienti vengono utilizzati per approssimare nell’intervallo Xs ≤ X < Xi + 2–m la funzione data, f(X), con la seguente funzione polinomiale: f(X) = C0 + C1 X1 + C2 Xl2 L’accuratezza delle stime della funzione varia da 22 a 24 bit del significando. Alcune valutazioni dell’accuratezza dell’approssimazione per alcune funzioni sono riportate in Figura C.6.1. Lo standard IEEE 754 specifica i requisiti richiesti dall’arrotondamento esatto per la divisione e la radice quadrata; tuttavia per molte applicazioni della GPU i requisiti possono non essere rispettati. Per tali applicazioni, un maggior throughput dei calcoli è più importante dell’accuratezza dell’ultimo bit. Per le funzioni speciali della SFU, la libreria matematica di CUDA fornisce sia una versione con la massima accuratezza sia una funzione veloce con l’accuratezza dell’istruzione della SFU. Un’altra operazione aritmetica specializzata di una GPU è l’interpolazione degli attributi. Gli attributi principali sono solitamente specificati per i vertici delle primitive che costituiscono la scena da generare. Esempi di attributo sono il colore, la profondità e le coordinate di tessitura. Tali attributi devono essere interpolati nello spazio bidimensionale dello schermo, dovendo determinare il valore degli attributi per ogni pixel. Il valore di un certo attributo, U, sul piano (x, y) può essere espresso utilizzando una funzione della forma U(x, y) = Au x + Bu y + Cu dove A, B e C sono i parametri di interpolazione associati a ogni attributo U. Tali parametri sono tutti rappresentati come numeri in virgola mobile a singola precisione. Poiché ciascun processore di shading dei pixel deve sia valutare delle funzioni sia interpolatore degli attributi, si può progettare una singola SFU che faccia entrambe le cose in modo efficiente: le due funzioni, infatti, utilizzano un’operazione di somma di prodotti per interpolare i risultati; inoltre, il numero di termini da sommare è quasi lo stesso.

Le operazioni sulla tessitura La mappatura e il filtraggio della tessitura costituiscono un altro insieme fondamentale di operazioni aritmetiche in virgola mobile specializzate di una GPU. Le operazioni utilizzate per la mappatura della tessitura comprendono: 1. Ricevere l’indirizzo della tessitura (s, t) per il pixel corrente (x, y), dove s e t sono valori in virgola mobile a singola precisione. Funzione 1/x 1/sqrt(x) 2x log2x sin/cos

Intervallo di supporto [1, 2) [1, 4) [0,1) [1,2) [0,π/2)

Accuratezza (bit validi) 24,02 23,40 22,51 22,57 22,47

Errore misurato in % arrotondamento ULP* esatto 0,98 87 1,52 78 1,41 74 N/A** N/A N/A N/A

Monotonicità Sì Sì Sì Sì No

* ULP: unità in ultima posizione (si veda il Capitolo 3). ** N/A: non applicabile

Figura C.6.1. Statistiche sull’approssimazione delle funzioni speciali. Sono riferite all’unità per le funzioni speciali (SFU) della scheda GeForce 8800 di NVIDIA.

© 978-88-08-06279-6

C.6  Aritmetica in virgola mobile

2. Calcolare il livello di dettaglio per identificare il corretto livello MIP-map della tessitura. 3. Calcolare i coefficienti frazionari dell’interpolazione trilineare. 4. Scalare l’indirizzo della tessitura (s, t) a seconda del livello MIP-map selezionato. 5. Accedere alla memoria e recuperare i texel richiesti. 6. Eseguire le operazioni di filtraggio dei texel. La mappatura della tessitura richiede un numero elevato di operazioni in virgola mobile quando viene effettuata alla massima velocità, e la gran parte di queste operazioni viene effettuata su valori in mezza precisione su 16 bit. Per esempio, la scheda grafica GeForce 8800 Ultra è in grado di eseguire circa 500 GFLOPS in virgola mobile su variabili in formato proprietario per ciascuna istruzione di mappatura della tessitura, oltre alle convenzionali istruzioni IEEE in virgola mobile a singola precisione. Per maggiori dettagli sulla mappatura e filtraggio della tessitura, si consulti Foley e van Dam (1995).

Le prestazioni L’hardware aritmetico per l’addizione e la moltiplicazione in virgola mobile è organizzato in una struttura completamente a pipeline e la latenza è ottimizzata per bilanciare i ritardi e l’occupazione di area. Nonostante siano organizzate in pipeline, il throughput delle funzioni speciali è inferiore a quello delle addizioni e delle moltiplicazioni in virgola mobile: un throughput pari a un quarto della velocità è oggi una misura tipica per le funzioni speciali di una moderna GPU, contenente una SFU condivisa da quattro core SP. D’altra parte, le CPU presentano tipicamente un throughput inferiore per le funzioni analoghe, come la divisione e la radice quadrata, sebbene siano in grado di calcolarle con un’accuratezza maggiore. L’hardware di interpolazione degli attributi, tipicamente, presenta una struttura completamente a pipeline per ottenere lo shading dei pixel alla massima velocità possibile.

La doppia precisione Le GPU più recenti, come la Tesla T10P, supportano in hardware anche le operazioni in virgola mobile in doppia precisione su 64 bit secondo lo standard IEEE 754. Le operazioni aritmetiche standard in virgola mobile in doppia precisione comprendono addizione, moltiplicazione e conversione tra i differenti formati interi e in virgola mobile. Lo standard per la virgola mobile IEEE 754 del 2008 contiene anche le specifiche per l’istruzione di moltiplicazione e somma integrate (FMA) descritta nel Capitolo 3. L’istruzione FMA esegue una moltiplicazione in virgola mobile seguita da un’addizione, con un solo arrotondamento al termine delle due operazioni; le operazioni di moltiplicazione e addizione integrate mantengono la piena accuratezza nei calcoli intermedi. Questa caratteristica consente di ottenere una maggiore accuratezza nei calcoli in virgola mobile che prevedono l’accumulo di prodotti parziali, come per esempio i prodotti scalari, il prodotto di matrici e la valutazione dei polinomi. L’istruzione FMA consente anche di implementare in software in modo efficiente la divisione e la radice quadrata, arrotondate in modo esatto, eliminando la necessità di disporre di un’unità hardware per le divisioni e per il calcolo delle radici quadrate. Un’unità FMA hardware a doppia precisione implementa su 64 bit l’addizione, la moltiplicazione, le conversioni e l’operazione FMA stessa. L’architettura di una FMA a doppia precisione fornisce il supporto alla massima velocità consentita dei numeri denormalizzati sia sugli input sia sugli output. La Figura C.6.2 mostra uno schema a blocchi di un’unità FMA.

C41 MIP-map: dalla frase latina multum in parvo, molto in poco spazio. Una MIP-map contiene immagini precalcolate a differenti risoluzioni che vengono utilizzate per aumentare la velocità di rendering e per ridurre gli artefatti.

C42 Figura C.6.2. Unità di moltiplicazione e somma fuse (FMA). Hardware utilizzato per implementare l’operazione A × B + C in doppia precisione.

Appendice C  La grafica e il calcolo con la GPU 64

© 978-88-08-06279-6

64

A

64

B

53

C

53

53

Complemento

Matrice dei prodotti 53 x 53 Somma

Riporto

Scorrimento per l’allineamento

Numero allineato C

Differenza esponenti

161

3-2 CSA 161 bits

Somma

Riporto

Sommatore a propagazione di riporto

Generazione del complemento Normalizzazione Arrotondamento

Come mostrato in Figura C.6.2, i significandi di A e B vengono moltiplicati formando un prodotto su 106 bit, e il risultato viene scritto con l’ultimo bit di riporto salvato (carry-save). In parallelo, viene calcolato il complemento dell’addendo C, se necessario, su 53 bit e allineato al prodotto su 106 bit. Il risultato della somma e il bit di riporto del prodotto su 106 bit vengono poi sommati all’addendo allineato per mezzo di un sommatore di tipo carry-save (CSA, si veda l’Appendice C) a 161 bit. L’uscita in formato carry-save viene quindi sommata da un sommatore a propagazione di riporto, producendo il risultato non arrotondato in complemento a 2. Se necessario, viene calcolato il complemento del risultato in modo da rappresentare il risultato in formato modulo e segno e viene quindi normalizzato e successivamente arrotondato per adattarlo al formato previsto.

C.7 * Un caso reale: la GeForce 8800 di NVIDIA La GPU NVIDIA GeForce 8800, presentata nel novembre 2006, è un’architettura unificata per l’elaborazione di vertici e di pixel che supporta anche applicazioni di calcolo parallelo scritte in C utilizzando il modello di programmazione parallela CUDA. Si tratta della prima implementazione dell’architettura unificata di grafica e calcolo Tesla, descritta nel Paragrafo C.4 e in Lindholm, Nickolls, Oberman e Montrym (2008). La famiglia di GPU basate su Tesla consente di soddisfare le diverse esigenze dei computer portatili, dei desktop, delle workstation e dei server.

La schiera di processori a flusso continuo (SPA, Streaming Processor Array) La GPU GeForce 8800 mostrata in Figura C.7.1 contiene 128 processori core a flusso continuo (SP, Streaming Processor) organizzati come 16 multiprocessori a flusso continuo (SM). Coppie di SM condividono un’unità di tessitura all’in-

© 978-88-08-06279-6 CPU della macchina host

Bridge

Memoria di sistema

GPU

Interfaccia con l’host

Viewport/Clip/ Setup/Raster/ ZCull

Assemblatore degli input Distribuzione del lavoro sui vertici

SPA TPC SM

C43

C.7  Un caso reale: la GeForce 8800 di NVIDIA

TPC

SM

SM

Distribuzione del lavoro sui pixel

TPC

SM

SM

Processori video ad alta definizione

TPC

SM

SM

SM

Distribuzione del lavoro di calcolo

TPC SM

TPC

SM

SM

TPC

SM

SM

TPC

SM

SM

SM

SP SP SP SP

SP SP SP SP

SP SP SP SP

SP SP SP SP

SP SP SP SP

SP SP SP SP

SP SP SP SP

SP SP SP SP

SP SP SP SP

SP SP SP SP

SP SP SP SP

SP SP SP SP

SP SP SP SP

SP SP SP SP

SP SP SP SP

SP SP SP SP

SP SP SP SP

SP SP SP SP

SP SP SP SP

SP SP SP SP

SP SP SP SP

SP SP SP SP

SP SP SP SP

SP SP SP SP

SP SP SP SP

SP SP SP SP

SP SP SP SP

SP SP SP SP

SP SP SP SP

SP SP SP SP

SP SP SP SP

SP SP SP SP

Memoria condivisa

Memoria condivisa

Unità di tessitura Tex L1

Memoria condivisa

Memoria condivisa

Unità di tessitura Tex L1

Memoria condivisa

Memoria condivisa

Memoria condivisa

Unità di tessitura Tex L1

Memoria condivisa

Unità di tessitura Tex L1

Memoria condivisa

Memoria condivisa

Unità di tessitura Tex L1

Memoria condivisa

Memoria condivisa

Memoria condivisa

Unità di tessitura Tex L1

Memoria condivisa

Unità di tessitura Tex L1

Memoria condivisa

Memoria condivisa

Unità di tessitura Tex L1

Rete di interconnessione ROP

L2

DRAM

ROP

L2

ROP

DRAM

L2

DRAM

ROP

L2

DRAM

ROP

L2

ROP

DRAM

L2

DRAM

Interfaccia

Terminale grafico

Figura C.7.1. Architettura di GPU unificata per calcolo e grafica Tesla di NVIDIA. Questa scheda grafica GeForce 8800 contiene 128 processori core a flusso continuo (SP) distribuiti su 16 multiprocessori a flusso continuo (SM), organizzati in otto nuclei tessitura/processore (TPC). I processori sono collegati a sei partizioni di DRAM con ampiezza di 64 bit attraverso una rete di interconnessione. In altre GPU che implementano l’architettura Tesla varia il numero dei core SP, dei SM, delle partizioni DRAM e delle altre unità.

terno di un nucleo processore/tessitura (TPC, Texture/Processor Cluster). Un insieme di otto TPC costituisce la schiera di processori a flusso continuo (SPA) che esegue tutti i programmi, sia di shading sia di calcolo. L’unità di interfacciamento con l’host comunica con la CPU della macchina attraverso il bus PCI-Express, controlla la consistenza dei comandi ed effettua gli scambi di contesto. L’assemblatore degli input aggrega le primitive geometriche (punti, linee, triangoli); i blocchi di distribuzione del lavoro distribuiscono vertici, pixel e gruppi di thread di calcolo ai TPC appartenenti alla schiera degli SPA. I TPC possono eseguire sia programmi di shading di geometria e di pixel sia programmi di calcolo. I dati geometrici in uscita vengono inviati al blocco viewport/clip/setup/raster/zcull (inquadratura/taglio/ impostazione/rasterizzazione/selezione mediante Z-buffer) per essere rasterizzati trasformandoli in frammenti di pixel che vengono poi ridistribuiti agli SPA per eseguire i programmi di shading dei pixel. I pixel generati vengono quindi inviati attraverso la rete di interconnessione per essere elaborati dalle unità ROP. La rete instrada anche verso la DRAM le richieste di lettura della memoria di tessitura da parte della SPA e legge i dati dalla DRAM trasferendoli alla SPA mediante una cache di livello 2.

Il nucleo tessitura/processore (TPC) Ogni TPC contiene un controllore di geometria, un controllore di multiprocessore (SMC), due multiprocessori a flusso continuo (SM) e un’unità di tessitura, come mostrato in Figura C.7.2. Il controllore della geometria mappa la pipeline logica grafica dei vertici sui processori fisici SM, indirizzando tutto il flusso degli attributi e della topologia dei vertici e delle primitive al TPC.

C44

Appendice C  La grafica e il calcolo con la GPU

© 978-88-08-06279-6

TPC

SM

Controllore di geometria SMC SM

SM

Cache istruzioni

Cache istruzioni

Lancio istruzioni MT

Lancio istruzioni MT

Cache delle costanti

Cache delle costanti

SP

SP

SP

SP

SP

SP

SP

SP

SP

SP

SP

SP

SP

SP

SP

SP

SFU SFU

SFU SFU

Memoria condivisa

Memoria condivisa

Unità di tessitura Cache di tessitura L1

Cache istruzioni Lancio istruzioni MT

Cache delle costanti SP

SP

SP

SP

SP

SP

SP

SP

SFU

SFU

Memoria condivisa

Figura C.7.2. Nucleo tessitura/processore (TPC) e multiprocessore a flusso continuo (SM). Ciascun SM contiene otto processori core a flusso continuo (SP), due SFU e una memoria condivisa.

Un controllore SMC controlla diversi multiprocessori SM, gestendo l’arbitraggio dell’unità di tessitura condivisa e dei percorsi di load/store e di I/O. Il controllore SMC gestisce contemporaneamente tre carichi di lavoro grafico: vertici, geometria e pixel. L’unità di tessitura elabora, a ogni ciclo di clock, un’istruzione di tessitura per ogni vertice, di primitiva geometrica o di quadrilatero di pixel, oppure quattro thread di calcolo. I dati sorgente delle istruzioni di tessitura sono le coordinate di tessitura, mentre le uscite sono campioni pesati tipicamente codificati mediante le quattro componenti RGBA, espresse in virgola mobile. L’unità di tessitura è strutturata in una pipeline profonda. Nonostante contenga una cache a flusso continuo per catturare la località del filtraggio, essa prevede la presenza di hit e miss mescolate nel flusso degli accessi alla cache senza causare stalli.

Il multiprocessore a flusso continuo (SM) L’SM è un multiprocessore unificato per grafica e calcolo che esegue sia programmi di shading di vertici, geometria e pixel sia programmi di calcolo parallelo. Il multiprocessore SM è costituito da otto processori core SP di thread, due SFU, un’unità di prelievo e lancio in esecuzione delle istruzioni multithread, una cache per le istruzioni, una cache a sola lettura per le costanti e una memoria condivisa di lettura/scrittura da 16 kB. Esso esegue istruzioni scalari sui singoli thread. La scheda GeForce 8800 Ultra sincronizza i core SP e le unità SFU con un clock a 1,5 GHz, per una prestazione di picco di 36 GFLOP per singolo SM.

© 978-88-08-06279-6

C.7  Un caso reale: la GeForce 8800 di NVIDIA

Per ottimizzare l’efficienza energetica e di occupazione di area, alcune unità dell’SM ausiliarie, non incluse nel flusso di elaborazione dei dati, lavorano a una frequenza di clock dimezzata. Per eseguire in modo efficiente centinaia di thread paralleli di molti programmi diversi, il multiprocessore SM gestisce il multithreading in hardware; in questo modo è in grado di gestire ed eseguire fino a 768 thread concorrenti in hardware, senza alcun sovraccarico nella gestione della pianificazione. Ogni thread possiede un proprio stato di esecuzione e può seguire un cammino di elaborazione indipendente. Un warp consiste di un massimo di 32 thread dello stesso tipo: vertice, geometria, pixel o calcolo. L’architettura SIMT, descritta nel Paragrafo C.4, condivide in modo efficiente l’unità di prelievo e lancio delle istruzioni, ma richiede un warp completo di thread attivi per raggiungere le prestazioni massime. L’SM pianifica ed esegue molteplici tipi di warp in modo concorrente: in ogni istante in cui vengono lanciate istruzioni in esecuzione, lo scheduler seleziona uno dei 24 warp, lancia in esecuzione l’istruzione di tipo SIMT. L’istruzione del warp viene eseguita su quattro insiemi di 8 thread in quattro cicli di clock del processore. Le unità SP e SFU eseguono istruzioni in modo indipendente e lo scheduler riesce a mantenere entrambe le unità pienamente occupate lanciando istruzioni alternativamente sulle due unità. Una tabella assegna un punteggio a ogni warp pronto per il lancio a ogni ciclo di clock. Lo scheduler delle istruzioni rende prioritari tutti i warp pronti e seleziona per il lancio in esecuzione quello a priorità più elevata. Il calcolo della priorità è basato sul tipo di warp, sul tipo di istruzione e su un criterio di equità tra tutti i warp in esecuzione sull’SM. L’SM esegue vettori di thread cooperativi (CTA) come warp multipli concorrenti che accedono a un’area di memoria condivisa allocata dinamicamente per il CTA stesso.

L’insieme delle istruzioni I thread eseguono istruzioni scalari, a differenza delle precedenti architetture GPU a istruzioni vettoriali. Le istruzioni scalari sono più intuitive e semplici da compilare. Le istruzioni per le tessiture rimangono basate su uno schema vettoriale: ricevono un vettore di coordinate sorgente e restituiscono un vettore di colore filtrato. L’insieme delle istruzioni basate su registri comprende tutte le istruzioni aritmetiche, intere e in virgola mobile, le funzioni trascendenti, logiche, di controllo di flusso, di lettura/scrittura e di tessitura riportate nella tabella delle istruzioni PTX di Figura C.4.3. Le istruzioni di lettura/scrittura utilizzano l’indirizzamento intero al byte che viene formato sommando al contenuto di un registro uno spiazzamento. Per i programmi di calcolo, le istruzioni di lettura/scrittura possono accedere in lettura e in scrittura ai tre spazi di memoria: la memoria locale per i dati privati e temporanei di ogni thread, la memoria condivisa, a bassa latenza, contenente i dati condivisi tra i diversi thread dello stesso CTA, e la memoria globale per i dati condivisi da tutti i thread. I programmi di calcolo utilizzano l’istruzione di sincronizzazione veloce a barriera, bar.sync, per sincronizzare in modo veloce i thread dello stesso CTA che comunicano tra loro per mezzo della memoria condivisa o della memoria globale. Le GPU più recenti con architettura Tesla implementano operazioni PTX atomiche sulla memoria che facilitano la parallelizzazione del codice e la gestione parallela delle strutture dati.

Il processore a flusso continuo (SP) Il core SP multithread del processore è principalmente un processore di thread, come descritto nel Paragrafo C.4. Il suo insieme di registri è costituito

C45

C46

Appendice C  La grafica e il calcolo con la GPU

© 978-88-08-06279-6

da 1024 registri scalari a 32 bit, a disposizione di un massimo di 96 thread, un numero maggiore di thread rispetto all’SP considerato nell’esempio del Paragrafo C.4. Le operazioni di addizione e moltiplicazione in virgola mobile che esegue sono compatibili con lo standard IEEE 754 per i numeri in virgola mobile a singola precisione, compresi i valori NaN e infinito. Le operazioni di addizione e moltiplicazione utilizzano il criterio di arrotondamento al numero pari più vicino specificato da IEEE come criterio di arrotondamento predefinito. Il core SP implementa anche tutte le istruzioni PTX di aritmetica intera, di confronto di conversione, e le istruzioni logiche su 32 e 64 bit riportate in Figura C.4.3. Il processore è completamente strutturato in pipeline e la latenza è ottimizzata per bilanciare ritardi e area occupata.

L’unità per funzioni speciali (SFU) La SFU supporta sia il calcolo di funzioni trascendenti sia l’interpolazione planare di attributi. Come descritto nel Paragrafo C.6, essa sfrutta l’interpolazione quadratica basata su uno schema evoluto di approssimazione minimax per approssimare le funzioni reciproco, reciproco della radice quadrata, log2x, 2x, seno e coseno, ed è in grado di produrre un valore per ciclo di clock. La SFU supporta inoltre l’interpolazione di attributi di pixel, tra cui il colore, la profondità e le coordinate di tessitura al ritmo di quattro campioni per ciclo di clock.

La rasterizzazione Le primitive geometriche passano, nello stesso ordine sequenziale con cui sono state ricevute in ingresso, dagli SM al blocco viewport/clip/setup/raster/ zcull. Le unità di viewport e di clipping (taglio) eliminano le primitive al di fuori del tronco di piramide visualizzato e di eventuali piani di taglio definiti dall’utente, e trasformano poi la posizione dei vertici dallo spazio tridimensionale allo spazio dell’immagine, cioè in coordinate di pixel. Le primitive sopravvissute al taglio entrano nell’unità di setup, la quale genera le equazioni di bordo per la rasterizzazione. Un primo stadio di rasterizzazione grossolana genera tutte le finestrelle di pixel che si trovano anche solo parzialmente all’interno della primitiva. L’unità di taglio lungo la profondità (zcull) conserva una gerarchia delle superfici lungo l’asse z della profondità ed elimina, in modo conservativo, le finestrelle di pixel che risultano già occupate da pixel disegnati in precedenza: un numero massimo di 256 pixel possono essere eliminati a ogni ciclo di clock. I pixel che sopravvivono al zculling entrano in uno stadio di rasterizzazione fine che genera informazioni dettagliate sull’aspetto della superficie e sulla profondità. Il controllo sulla profondità e l’aggiornamento possono essere effettuati prima o dopo lo shading del frammento, a seconda dello stato corrente. Il controllore SMC aggrega i pixel sopravvissuti in warp da far elaborare a un SM che sta eseguendo lo shader sul pixel corrente. L’SMC invia quindi tali pixel e i dati loro associati al ROP.

Il processore delle operazioni di rasterizzazione (ROP) e il sistema di memoria Ogni ROP è associato a una specifica partizione di memoria. Per ogni frammento di pixel emesso da un programma di shading, i ROP effettuano il controllo su profondità e stencil, l’aggiornamento e, in parallelo, la miscelazione del colore e gli aggiornamenti relativi. Per ridurre la banda di trasferimento con la DRAM, viene adottata una compressione senza perdita di dati del colore (fino a 8:1) e della profondità (fino a 8:1). Ogni ROP ha una velocità di picco

© 978-88-08-06279-6

C.7  Un caso reale: la GeForce 8800 di NVIDIA

di quattro pixel per ciclo di clock e supporta i formati HDR in virgola mobile a 16 e 32 bit. I ROP supportano il calcolo della profondità a velocità doppia quando la scrittura del colore è disabilitata. Il supporto dell’antialiasing comprende fino a 16 operazioni di campionamento multiplo e di sovracampionamento (supersampling). L’algoritmo di antialiasing basato sul campionamento dell’aspetto della superficie (CSAA) calcola e memorizza valori booleani su un massimo di 16 campioni e comprime l’informazione ridondante sul colore, sulla profondità e sugli stencil nello spazio di memoria per una banda di trasferimento compresa tra quattro e otto campioni, con lo scopo di ottimizzare le prestazioni. L’ampiezza del bus dati della memoria DRAM è di 384 piedini, organizzati in sei partizioni indipendenti di 64 piedini ciascuna. Ogni partizione supporta il protocollo a doppio trasferimento dei dati DDR2 e il protocollo per la grafica GDDR3 a una frequenza massima di 1.0 GHz, ottenendo una larghezza di banda di 16 GB/s per partizione per un totale di 96 GB/s. I controllori della memoria supportano una vasta gamma di frequenze di clock, protocolli, densità dei dispositivi e ampiezze del bus dati delle DRAM. Le richieste di tessitura e di load/store possono arrivare da qualsiasi TPC a qualsiasi partizione di memoria, per cui una rete di interconnessione instrada sia le richieste sia le risposte.

Scalabilità L’architettura unificata Tesla è stata progettata per essere scalabile. Variando il numero di SM, TPC, ROP, memorie cache e partizioni di memoria, si ottiene un bilanciamento corretto per differenti obiettivi in termini di costo e prestazioni per i diversi segmenti di mercato delle GPU. Il dispositivo di interconnessione scalabile Scalable Link Interconnect (SLI) permette di connettere GPU multiple fornendo ulteriore scalabilità.

Le prestazioni La GeForce 8800 Ultra temporizza i processori SP dei thread e le SFU con un clock a 1,5 GHz per una prestazione di picco teorica di 576 GFLOPS. La GeForce 8800 GTX utilizza un clock del processore di 1,35 GHz e ha una prestazione di picco di 518 GFLOPS. I tre paragrafi seguenti confrontano le prestazioni di una GPU GeForce 8800 con una CPU a core multipli per tre differenti applicazioni: algebra lineare densa, trasformate di Fourier veloci (FFT) e ordinamento. I programmi e le librerie per la GPU sono costituiti da codice C compilato con CUDA. Il codice per CPU utilizza la libreria multithread MKL 10,0 a singola precisione di Intel per trarre vantaggio dalle istruzioni SSE e dai core multipli.

Le prestazioni sull’algebra lineare densa I calcoli di algebra lineare densa sono fondamentali in molte applicazioni. Volkov e Demmel (2008) hanno confrontato le prestazioni ottenute con una GPU e una CPU nella moltiplicazione di matrici dense in singola precisione (procedura SGEMM) e di fattorizzazione matriciale, LU, QR e di Cholesky. La Figura C.7.3 confronta la misura dei GFLOPS per la moltiplicazione matrice-matrice di matrici dense utilizzando la procedura SGEMM di una GPU GeForce 8800 GTX con quella di una CPU quad-core. La Figura C.7.4 confronta i valori di GFLOPS di una GPU con quelli di una CPU quad-core nella fattorizzazione di matrici.

C47

C48

Appendice C  La grafica e il calcolo con la GPU

Figura C.7.3. Prestazioni del prodotto SGEMM tra due matrici dense. Il grafico mostra il numero di GFLOPS ottenuti nel prodotto in singola precisione di matrici quadrate N × N (linee continue) e di matrici sottili di N × 64 e 64 ×nella memoria della CPU.

© 978-88-08-06279-6

A:NN, B:NN

A:N64, B:64N

210

GeForce 8800 GTX

180

GFLOPS

150 120 90 60

Core2 con quattro core

30 0

128

256

512

1024

N

2048

Cholesky

LU

4096

8192

QR

210

co

re

180

co

n

du

e

150

TX

+

Co

re

2

120

G

90

88

00

GFLOPS

eF

or

ce

60 30 0

n quattro

core

4096

8192

Core2 co

G

Figura C.7.4. Prestazioni della fattorizzazione di matrici dense. Il grafico mostra il numero di GFLOP ottenuti nelle fattorizzazioni di una matrice integrando la GPU e utilizzando la CPU da sola. I dati sono stati adattati dalla figura 7 di Volkov e Demmel (2008). Le linee nere corrispondono a una GeForce 8800 GTX che esegue il codice compilato mediante CUDA 1.1 su Windows XP, inserita in un host Core2 Intel con due processori core, Duo E6700, a 2,67 GHz. I GFLOPS tengono conto anche di tutti i tempi di trasferimento dati tra CPU e GPU. Le linee blu corrispondono a un Intel Core2 con quattro processori core, Core2 Quad Q6600, a 2,4 GHz, con sistema operativo Linux a 64 bit e la libreria MKL 10,0 di Intel.

64

64

128

256

512

1024

2048

Ordine della matrice

16384

Poiché il prodotto matrice-matrice SGEMM e le funzioni simili della libreria BLAS3 costituiscono il nucleo delle operazioni richieste per la fattorizzazione delle matrici, le loro prestazioni marcano un limite superiore della velocità di fattorizzazione. Quando l’ordine della matrice aumenta oltre un fattore compreso tra 200 e 400, il problema della fattorizzazione diventa sufficientemente grande da far sì che la funzione SGEMM sfrutti appieno il parallelismo della GPU e superi i sovraccarichi di gestione e di trasferimento GPU-CPU. La moltiplicazione matrice-matrice SGEMM di Volkov raggiunge i 206 GFLOPS, circa il 60% della velocità di picco della GeForce 8800 GTX per le operazioni di moltiplicazione e somma, mentre la fattorizzazione QR raggiunge i 192 GFLOPS, circa 4,3 volte la prestazione della CPU a core quadruplo.

Le prestazioni sulle FFT Le trasformate veloci di Fourier (FTT) sono utilizzate in molte applicazioni. Le trasformate di grandi dimensioni e quelle multidimensionali vengono partizionate in lotti di trasformate monodimensionali più piccole.

© 978-88-08-06279-6

C.7  Un caso reale: la GeForce 8800 di NVIDIA

C49

In Figura C.7.5 sono confrontate le prestazioni su una FFT 1-D complessa a singola precisione di una GeForce 8800 GTX a 1,35 GHz (fine 2006) con quelle di uno Xeon Intel con quattro core della serie E5462 (nome in codice «Harpertown», fine 2007). Le prestazioni della CPU sono state misurate utilizzando la FFT della libreria Math Kernel Library (MKL) 10,0 di Intel con quattro thread. Le prestazioni della GPU sono state misurate utilizzando la libreria CUFFT 2.1 di NVIDIA effettuando gruppi di FFT monodimensionali radix16, a decimazione di frequenza. Il throughput della GPU e della CPU è stato valutato effettuando le FFT su gruppi di dati, ciascuno di dimensioni pari a 224/n, dove n è la dimensione della trasformata. Ne consegue che il carico di lavoro era pari a 128 MB per ciascuna trasformata. Per determinare il numero di GFLOPS, si è assunto un numero di operazioni pari a 5n log2 n.

Le prestazioni sull’ordinamento A differenza delle applicazioni appena descritte, l’ordinamento richiede un coordinamento molto maggiore tra i thread paralleli, di conseguenza è più difficile ottenere che le prestazioni scalino con il grado di parallelismo. Ciononostante, una varietà di algoritmi di ordinamento ben noti può essere resa parallela in modo efficiente per essere eseguita sulla GPU. Satish et al. (2008) descrivono in dettaglio la progettazione di algoritmi di ordinamento in CUDA, e i risultati da loro ottenuti per l’algoritmo Radix Sort sono riassunti in questo paragrafo. In Figura C.7.6 sono messe a confronto le prestazioni di ordinamento parallelo su una GeForce 8800 Ultra con quelle ottenute su un sistema a otto core Clovertown di Intel, entrambi del 2007. I core della CPU sono distribuiti fisicamente su due connettori, dove a ciascun connettore è collegato un modulo a chip multiplo contenente due core gemelli Core2; ogni chip è dotato di 4 MB di cache L2. Tutte le procedure di ordinamento sono state progettate per ordinare coppie chiave-valore in cui sia le chiavi di ordinamenti sia i valori sono numeri interi a 32 bit. L’algoritmo principale qui considerato è Radix Sort, anche se i risultati vengono confrontati anche con la procedura basata su Quick Sort, parallel_sort(), fornita da Intel nei Threading Building Blocks. Delle due procedure Radix Sort scritte per la CPU, una è stata implementata utilizzando soltanto l’insieme delle istruzioni scalari e l’altra utilizzando procedure in linguaggio assembler messe a punto a mano, le quali sfruttano le istruzioni vettoriali SIMD dell’SSE2. GeForce 8800GTX

Xeon 5462

80 70 60

GFLOPS

50 40 30 20 10 0

8

12

6

25

2

51

24

10

48

20

96

40

92

81

4

38

16

8

76

32

6 52 72 76 88 04 44 10 621 242 485 971 943 0 13 0 5 1 2 2 1 4

53

65

Numero di elementi di una trasformata

Figura C.7.5. Prestazioni del throughput della trasformata veloce di Fourier. Il grafico mette a confronto le prestazioni di FFT monodimensionali complesse «sul posto», cioè calcolate nella stessa memoria contenente i dati, su una GeForce 8800 GTX a 1,35 GHz e su uno Xeon Intel con quattro core, della serie E5462 (nome in codice «Harpertown») con 6 MB di cache L2, 4 GB di memoria, FSB 1600, sistema operativo Red Hat Linux e libreria MKL 10,0 di Intel.

C50

Appendice C  La grafica e il calcolo con la GPU

Velocità di ordinamento (coppie/s)

Milioni

Quick Sort su CPU Radix Sort su GPU

© 978-88-08-06279-6

Radix Sort su CPU (scalare) Radix Rort su CPU (SIMD)

80 70 60 50 40 30 20 10 0 1000

10 000

100 000

1 000 000

10 000 000

100 000 000

Dimensione della sequenza

Figura C.7.6. Prestazioni dell’ordinamento parallelo. Questo grafico mette a confronto la velocità di ordinamento dell’implementazione parallela di Radix Sort su una GeForce 8800 Ultra con quella dell’implementazione su un sistema a 8 core Core2 Xeon E5345 a 2,33 GHz di Intel.

Il grafico in Figura C.7.6 mostra la velocità di ordinamento ottenuta, definita come numero di elementi ordinati diviso per il tempo necessario a ordinarli, in funzione delle dimensioni delle sequenze. Si evince chiaramente che il Radix Sort su GPU ottiene la più alta velocità di ordinamento per tutte le sequenze con più di 8000 elementi. In questo intervallo l’algoritmo è in media 2,6 volte più veloce della procedura basata su Quick Sort e circa 2 volte più veloce delle procedure Radix Sort, le quali impiegano tutti gli otto core disponibili. Le prestazioni del Radix Sort su CPU varia ampiamente, probabilmente a causa della scarsa località di cache nelle sue permutazioni globali.

C.8 * Un caso reale: come adattare le applicazioni alla GPU L’avvento delle CPU multicore e delle GPU a moltissimi core ha fatto sì che i chip dei processori principali diventassero sistemi paralleli. Inoltre, il loro grado di parallelismo continua a crescere seguendo la legge di Moore. La sfida attuale consiste nello sviluppo di applicazioni di elaborazione visuale e di calcolo ad alte prestazioni che siano in grado di scalare il loro parallelismo in modo trasparente per trarre vantaggio dal crescente numero di core di elaborazione, come le applicazioni di grafica 3D riescono a scalare il loro parallelismo su GPU con un numero di core altamente variabile. Questo paragrafo presenta alcuni esempi di come sia possibile adattare un’applicazione scalabile di calcolo parallelo su una GPU utilizzando CUDA.

Matrici sparse In CUDA è possibile scrivere svariati algoritmi paralleli in modo molto semplice, anche nel caso in cui le strutture dati coinvolte non siano semplici griglie regolari. Il prodotto matrice-vettore con matrici sparse (SpVM) costituisce un buon esempio di calcolo numerico che può essere parallelizzato abbastanza

© 978-88-08-06279-6

C51

C.8  Un caso reale: come adattare le applicazioni alla GPU

facilmente utilizzando le astrazioni fornite da CUDA. I kernel qui descritti, se combinati con le procedure sui vettori rese disponibili in CUBLAS, permettono di scrivere semplici algoritmi di ottimizazzione iterativi, come il metodo del gradiente coniugato. Una matrice n × n sparsa è una matrice nella quale il numero m di elementi non nulli è solo una piccola parte del numero totale di elementi. Le rappresentazioni delle matrici sparse tendono a memorizzare soltanto gli elementi non nulli della matrice. Poiché una matrice sparsa n × n tipicamente contiene solamente m = O(n) elementi non nulli, questa scelta porta a un consistente risparmio di spazio in memoria e di tempo di elaborazione. Una delle più comuni rappresentazioni delle metrici sparse generiche non strutturate è la rappresentazione a righe sparse compresse (CSR, Compressed Sparse Row). Gli m elementi non nulli della matrice A vengono memorizzati per righe in un vettore Av. Un secondo vettore Aj contiene l’indice di colonna corrispondente a ogni elemento di Av. Infine, un vettore Ap di n + 1 elementi memorizza la lunghezza di ogni riga all’interno dei vettori precedenti: gli elementi della riga i-esima della matrice A saranno così individuati dagli indici memorizzati in Aj e Av tra l’indice contenuto in Ap[i] e l’indice immediatamente precedente al valore contenuto in Ap[i+1]; ciò significa che Ap[0] conterrà sempre 0 e Ap[n] sarà sempre pari al numero di elementi non nulli. La Figura C.8.1 mostra un esempio di rappresentazione CSR di una semplice matrice. Data una matrice A in forma CSR e un vettore x, possiamo calcolare una singola riga del prodotto y = Ax utilizzando la procedura moltiplica_ riga() riportata in Figura C.8.2. Per calcolare l’intero prodotto è quindi sufficiente eseguire un ciclo su tutte le righe e calcolare il risultato per ciascuna riga utilizzando moltiplica_riga(), come avviene nel codice C sequenziale riportato in Figura C.8.3. Questo algoritmo può essere tradotto molto facilmente in un kernel CUDA parallelo: è sufficiente distribuire il ciclo contenuto in moltcrs_sequenz() su più thread paralleli, dove ciascun thread calcolerà esattamente una riga del vettore risultato y. Il codice di tale kernel è riportato in Figura C.8.4. Si noti

3 0 A = 0 1

0 0 2 0

1 0 4 0

0 0 1 1

Riga 0 Riga 2 Av[7] = { 3 1 2 4 1

Row 3 1 1 }

Aj[7] = { 0

2

1

2

3

0

Ap[5] = { 0

2

2

5

7

Figura C.8.1. Matrice a righe sparse compressa (CSR).

3 } }

a. Matrice di esempio A b. Rappresentazione CSR della matrice

float moltiplica_riga(unsigned int dimriga, unsigned int *Aj, float *Av, float *x) { float somma = 0;

// indici di colonna della riga // elementi non nulli della riga // il vettore da moltiplicare

for(unsigned int colonna=0; colonna
C52

Appendice C  La grafica e il calcolo con la GPU

Figura C.8.3. Il codice sequenziale per il prodotto matrice-vettore sparso.

void moltcsr_sequenz(unsigned int *Ap, unsigned int *Aj, float *Av, unsigned int num_righe, float *x, float *y) { for(unsigned int riga=0; row
© 978-88-08-06279-6

   y[riga] = moltiplica_riga(riga_fine, riga_inizio, Aj+riga_inizio, Av+riga_inizio, x); } }

Figura C.8.4. La versione CUDA del prodotto matrice-vettore sparso.

__global__ void moltcsr_kernel(unsigned int *Ap, unsigned int *Aj, float *Av, unsigned int num_righe, float *x, float *y) { unsigned int righe = blockIdx.x*blockDim.x + threadIdx.x; if(riga
che è molto simile al ciclo sequenziale utilizzato nella procedura moltcrs_ sequenz(), e in effetti ci sono soltanto due differenze. Anzitutto, l’indice di riga, riga, per ogni thread viene calcolato a partire dall’indice di thread e di blocco assegnati a ciascun thread, eliminando così il ciclo for. Secondo, è presente un’istruzione di test che fa sì che il prodotto associato a una riga venga calcolato soltanto se l’indice di riga si trova all’interno dei limiti della matrice; ciò si rende necessario perché il numero di righe, n, può non essere un multiplo della dimensione del blocco definita quando il kernel viene lanciato in esecuzione. Assumendo che le strutture dati delle matrici siano già state copiate nella memoria della GPU, i kernel possono essere lanciati in esecuzione in questo modo: unsigned int dimblocco = 128;   // o qualunque altra dimensione fino a 512 unsigned int numblocchi = (num_righe + dimblocco – 1)/ dimblocco; moltcsr_kernel<<>>  (Ap, Aj, Av, num_righe, x, y); La struttura di codice qui descritta è molto diffusa. L’algoritmo sequenziale originario è organizzato in un ciclo le cui iterazioni sono indipendenti fra

© 978-88-08-06279-6

C.8  Un caso reale: come adattare le applicazioni alla GPU

loro; i cicli di questo tipo possono essere parallelizzati molto facilmente, assegnando una o più iterazioni del ciclo a ogni thread parallelo. Il modello di programmazione fornito da CUDA rende particolarmente semplice esprimere questo tipo di parallelismo. La strategia generale di scomporre i calcoli in blocchi di elaborazione indipendenti oppure, in alcuni casi, di spezzettare iterazioni di ciclo indipendenti non è caratteristica solamente di CUDA, ma si tratta di un approccio comune utilizzato nelle varie forme da diversi sistemi di programmazione parallela, quali OpenMP e Threading Building Blocks di Intel.

Utilizzo della memoria condivisa come cache Gli algoritmi di prodotto matrice-vettore sparsi descritti nel paragrafo precedente sono abbastanza semplificati: ci sono molte ottimizzazioni che si possono adottare sia nel codice per la CPU sia in quello per la GPU per produrre un incremento delle prestazioni, come lo srotolamento dei cicli, il riordinamento delle matrici e l’impiego dei blocchi di registri. I kernel paralleli possono anche essere implementati da capo in termini di operazioni di scansione parallele sui dati, come mostrato da Sengupta et al. (2007). Una delle caratteristiche importanti dell’architettura CUDA è la presenza della memoria condivisa per ciascun blocco, una piccola memoria sul chip caratterizzata da una bassissima latenza. Lo sfruttamento di questa memoria può produrre notevoli miglioramenti delle prestazioni; per esempio, si può utilizzare questa memoria condivisa come una cache gestita dal software per contenere i dati riutilizzati frequentemente. Le modifiche del codice precedente per utilizzare la memoria condivisa sono riportate in Figura C.8.5. Nell’ambito del prodotto di matrici sparse, si può osservare che parecchie righe di A potrebbero utilizzare lo stesso elemento x[i]. In molti casi comuni, e in particolare quando la matrice è stata riordinata, le righe che utilizzano x[i] risulteranno vicine alla riga i. Possiamo quindi implementare un semplice meccanismo di utilizzo di una cache e aspettarci di ottenere un incremento delle prestazioni. Il blocco di thread che elabora le righe da i a j caricherà gli elementi compresi tra x[i] e x[j] nella memoria condivisa. Srotoleremo quindi il ciclo moltiplica_riga() e preleveremo gli elementi di x dalla cache, quando possibile. Il codice risultante è riportato in Figura C.8.5. La memoria condivisa può essere anche utilizzata per fare altre ottimizzazioni, come prelevare Ap[riga+1] da un thread adiacente, anziché prelevarlo di nuovo dalla memoria. Poiché l’architettura Tesla fornisce una memoria condivisa su chip gestita in modo esplicito (anziché una cache hardware gestita in modo implicito), questa ottimizzazione viene di solito applicata. Sebbene questo possa imporre al programmatore del lavoro aggiuntivo durante lo sviluppo del codice, si tratta di un lavoro modesto se comparato al potenziale beneficio in termini di prestazioni. Nell’esempio descritto in precedenza questo semplice utilizzo della memoria condivisa fornisce già un incremento di prestazioni attorno al 20% su matrici di dimensioni significative, derivanti da mesh di superfici 3D. La disponibilità di una memoria gestita esplicitamente invece di una cache implicita presenta anche il vantaggio che le politiche di utilizzo della cache e di precaricamento dei dati possono essere adattate alle esigenze dell’applicazione. Quelli appena mostrati sono kernel abbastanza semplici, il cui scopo principale è di illustrare le tecniche di base della programmazione CUDA e non di mostrare come ottenere le prestazioni massime. Esistono svariati modi per ottimizzare il codice, molti dei quali sono stati analizzati da Williams et al. (2007) utilizzando diverse architetture multicore. Ciononostante, è comun-

C53

C54

Appendice C  La grafica e il calcolo con la GPU

© 978-88-08-06279-6

__global__ void moltcsr_cache(unsigned int *Ap, unsigned int *Aj, float *Av, unsigned int num_righe, float *x, float *y) { // Poni in cache le righe di x[] corrispondenti a questo blocco. __shared__ float cache[dimblocco]; unsigned int blocco_inizio = blockIdx.x * blockDim.x; unsigned int blocco_fine = blocco_inizio + blockDim.x; unsigned int riga = blocco_inizio + threadIdx.x; // Preleva e poni in cache la nostra finestra di x[]. if( riga=blocco_inizio && j
que istruttivo esaminare in modo comparato le prestazioni anche per questi semplici kernel. Su un processore Core2 Xeon E5335 a 2 GHz di Intel, il kernel moltcsr_sequenz() elabora circa 202 milioni di elementi non nulli al secondo, per un insieme di matrici laplaciane derivate da superfici 3D a mesh triangolari. La parallelizzazione di questo kernel mediante il costrutto parallel_for fornito nei Threading Building Blocks di Intel produce un’accelerazione di un fattore 2,0, 2,1 e 2,3 quando il codice viene eseguito rispettivamente su una macchina con due, quattro o otto core. Su una GeForce 8800 Ultra, i kernel moltcsr_kernel() e moltcsr_cache() raggiungono rispettivamente una velocità di elaborazione di circa 772 e 920 milioni di elementi non nulli al secondo, che corrispondono a un aumento di velocità pari a 3,8 e 4,6 volte rispetto al codice sequenziale eseguito sulla CPU senza accelerazioni.

© 978-88-08-06279-6

C.8  Un caso reale: come adattare le applicazioni alla GPU

C55

Scansione e riduzione La scansione (scan) parallela, detta anche somma a prefisso (prefix sum) parallela, è uno dei componenti più importanti degli algoritmi che lavorano sui dati in parallelo (Blelloch, 1990). Data una sequenza a di n elementi [a0, a1, …, an–1] e un operatore associativo binario ⊕, la funzione scan calcola la sequenza: scan(a, ⊕) = [a0, (a0 ⊕ a1), …, (a0 ⊕ a1 ⊕ … ⊕ an–1)] Per esempio, se assumiamo che ⊕ sia il normale operatore somma, l’applicazione della scansione al vettore di ingresso a = [3 1 7 0 4 1 6 3] produrrà la sequenza di somme parziali: scan(a, ⊕) = [3 4 11 11 15 16 22 25] Questo operatore di scansione corrisponde a una scansione inclusiva, nel senso che l’i-esimo elemento della sequenza prodotta comprende l’elemento ai in ingresso. L’inclusione solo degli elementi precedenti corrisponde all’operatore di scansione esclusiva, conosciuto anche come operatore di somma a prefisso. L’implementazione sequenziale di tale operazione è molto semplice. Si tratta di un ciclo che itera una volta sull’intera sequenza, come mostrato in Figura C.8.6. template __host__ T plus_scan(T *x, unsigned int n) { for(unsigned int i=1; i
Figura C.8.6. Modello della funzione plus_scan seriale.

A prima vista potrebbe sembrare che questa operazione sia per natura sequenziale. È possibile, invece, implementarla in parallelo in modo efficiente. Il punto cruciale è che, essendo l’addizione associativa, si può cambiare l’ordine con cui gli elementi vengono sommati tra loro. Per esempio, possiamo immaginare di sommare coppie di elementi consecutivi in parallelo per poi sommare le somme parziali risultanti, e così via. Uno schema semplice per ottenere ciò è stato proposto da Hillis e Steele (1989) e un’implementazione del loro algoritmo in CUDA è riportato in Figura C.8.7. template __host__ T plus_scan(T *x) { unsigned int i = threadIdx.x; unsigned int n = blockDim.x; for(unsigned int offset=1; offset
}

   if( i>=offset )    __syncthreads();

t = x[i-offset];

   if( i>=offset )    __syncthreads(); } return x[i];

x[i] = t + x[i];

Figura C.8.7. Modello CUDA della funzione plus_scan parallela.

C56

Appendice C  La grafica e il calcolo con la GPU

© 978-88-08-06279-6

Si assume che il vettore d’ingresso, x[], contenga esattamente un elemento per ciascun thread di un blocco di thread e che vengano effettuate log2 n iterazioni del ciclo raccogliendo le somme parziali. Per comprendere il funzionamento di questo ciclo si consideri la Figura C.8.8, che illustra il semplice caso con n = 8 thread ed elementi. Ogni livello del diagramma rappresenta un passo del ciclo. Le linee indicano la locazione dalla quale il dato viene prelevato. Per ogni elemento dell’uscita, corrispondente all’ultima riga del diagramma, si costruisce un albero delle somme degli elementi d’ingresso. Le linee evidenziate in blu mostrano la struttura dell’albero delle somme per l’elemento finale. Le foglie sono tutti gli elementi iniziali. Procedendo a ritroso, a partire da qualsiasi elemento dell’uscita, i, si vede che l’albero incorpora tutti i valori di ingresso fino all’i-esimo elemento incluso. Benché semplice, questo algoritmo non è così efficiente come vorremmo.

Figura C.8.8. Indirizzamento dei dati nella scansione parallela ad albero.

x[0]

x[1]

x[2]

x[3]

x[4]

x[5]

x[6]

x[7]

x[0]

x[1]

x[2]

x[3]

x[4]

x[5]

x[6]

x[7]

x[i] + = x[i –1];

x[0]

x[1]

x[2]

x[3]

x[4]

x[5]

x[6]

x[7]

x[i] + = x[i –2];

x[0]

x[1]

x[2]

x[3]

x[4]

x[5]

x[6]

x[7]

x[i] + = x[i –4];

Esaminando l’implementazione sequenziale, notiamo che essa richiede O(n) addizioni. L’implementazione parallela, invece, effettua O(n log n) addizioni. Non è quindi efficiente, poiché svolge più lavoro rispetto alla versione sequenziale per calcolare lo stesso risultato. Fortunatamente esistono altre tecniche, più efficienti, per implementare la scansione. Ulteriori dettagli su tali tecniche, nonché l’estensione di questa procedura basata su un blocco a vettori multiblocco, si possono trovare in Sengupta et al. (2007). In alcuni casi, potremmo essere interessati soltanto a calcolare la somma di tutti gli elementi di un array, invece che la sequenza di tutte le somme a prefisso fornite da scan. In questo caso si parla di riduzione parallela. Potremmo utilizzare un algoritmo di scansione per effettuare questo calcolo, ma la riduzione può essere in genere implementata più efficientemente della scansione. La Figura C.8.9 riporta il codice per il calcolo di una riduzione utilizzando la somma. In questo esempio, ogni thread carica semplicemente un elemento della sequenza di ingresso (cioè somma inizialmente una sottosequenza di lunghezza 1). Alla fine della riduzione, vogliamo che il thread 0 contenga la somma di tutti gli elementi caricati inizialmente dai thread del suo blocco. Il ciclo in questo kernel costruisce implicitamente un albero di somme sugli elementi di ingresso, in modo molto simile al precedente algoritmo di scansione.

© 978-88-08-06279-6

C57

C.8  Un caso reale: come adattare le applicazioni alla GPU

__global__ void plus_reduce(int *input, unsigned int N, int *totale) { unsigned int tid = threadIdx.x; unsigned int i = blockIdx.x*blockDim.x + threadIdx.x;

Figura C.8.9. Implementazione CUDA della riduzione «plus» (plus_reduce).

// Ogni blocco carica i suoi elementi in memoria condivisa, // riempiendo di 0 se N non è un multiplo di dimblocco __shared__ int x[dimblocco]; x[tid] = (i0; s=s/2) {    if(tid < s)   x[tid] += x[tid + s];    __syncthreads(); } // Il thread 0 contiene ora la somma di tutti i valori in ingresso // di questo blocco. Aggiungi ora tale somma al totale corrente. if( tid==0 ) atomicAdd(totale, x[tid]); } Al termine di questo ciclo, il thread 0 conterrà la somma di tutti i valori caricati da questo blocco. Se si desidera che il conenuto finale della locazione puntata da total sia il totale di tutti gli elementi del vettore, è necessario combinare le somme parziali di tutti i blocchi della griglia. Una strategia per ottenere questo risultato sarebbe quella di fare in modo che ogni blocco scriva la propria somma parziale in un secondo vettore e quindi lanci di nuovo il kernel di riduzione, ripetendo il processo fino a ridurre la sequenza a un unico valore. Un’alternativa più interessante supportata dall’architettura di GPU Tesla è quella di utilizzare la primitiva atomicAdd(), un’efficiente primitiva di lettura-modifica-scrittura atomica supportata dal sottosistema di memoria. L’impiego di questa funzione elimina la necessità di costruire vettori temporanei addizionali e di lanciare ripetutamente il kernel. La riduzione parallela è una primitiva essenziale per la programmazione parallela ed evidenzia l’importanza della memoria condivisa di blocco e delle barriere di basso costo, con lo scopo di rendere efficiente la cooperazione tra i thread. Questo grado di rimescolamento dei dati fra i thread, infatti, avrebbe un costo proibitivo in termini di velocità se effettuato nella memoria globale fuori dal chip.

Radix Sort Un’applicazione importante delle primitive di scansione è l’implementazione di procedure di ordinamento. Il codice in figura C.8.10 implementa un ordinamento Radix Sort di interi su un singolo blocco di thread. Esso riceve in ingresso un vettore di valori valori contenente un intero a 32 bit per ogni thread del blocco. Per questioni di efficienza, questo vettore dovrebbe essere memorizzato nella memoria condivisa di blocco, pur non trattandosi di un requisito per avere un ordinamento corretto.

C58

Appendice C  La grafica e il calcolo con la GPU

Figura C.8.10. Il codice CUDA per l’ordinamento mediante algoritmo Radix Sort.

__device__ void radix_sort(unsigned int *valori) { for(int bit=0; bit<32; ++bit) {    partizione_per_bit(valori, bit);    __syncthreads(); } }

© 978-88-08-06279-6

Quella presentata è un’implementazione abbastanza semplice di Radix Sort. Si assume di avere a disposizione una procedura partizione_per_bit() che partiziona il vettore dato, in modo che tutti i valori con uno 0 in corrispondenza del bit designato vengano posti prima di tutti i valori che presentano un 1 in tale posizione. Per produrre un risultato corretto, tale partizionamento deve essere robusto. L’implementazione della procedura di partizionamento è una semplice applicazione della scansione. Il thread i possiede il valore xi e deve calcolare l’indice di uscita corretto, in corrispondenza del quale scrivere quel valore. Per fare ciò, è necessario calcolare: 1) il numero di thread j < i per i quali il bit considerato è 1 e 2) il numero totale di bit per i quali il bit considerato è 0. Il codice CUDA per la funzione partizione_per_bit() è riportato in figura C.8.11. Una strategia simile può essere applicata per implementare un kernel di Radix Sort che ordini vettori di grandi dimensioni, anziché soltanto il vettore di un blocco. L’aspetto cruciale rimane la procedura di scansione, sebbene, quando il calcolo è ripartito su kernel multipli, si debba impiegare un doppio buffer per i vettori dei valori piuttosto che effettuare partizioni di spazio. Ulteriori dettagli sull’ordinamento efficiente di grandi vettori con Radix Sort sono forniti da Satish, Harris e Garland (2008).

Figura C.8.11. Il codice CUDA per partizionare i dati in base al confronto bit a bit utilizzato nell’impementazione di Radix Sort.

__device__ void partizione_per_bit(unsigned int *valori,                     unsigned int bit) { unsigned int i = threadIdx.x; unsigned int dimens = blockDim.x; unsigned int x_i = valori[i]; unsigned int p_i = (x_i >> bit) & 1; valori[i] = p_i; __syncthreads(); // Calcola il numero T di bit fino a p_i incluso. // Salva anche il numero totale di bit F. unsigned int T_prima = plus_scan(valori); unsigned int T_totale = valori[dimens-1]; unsigned int F_totale = dimens – T_totale; __syncthreads(); // Scrivi ogni x_i nella corretta posizione if( p_i )    valori[T_prima – 1 + F_totale] = x_i; else    valori[i – T_prima] = x_i; }

© 978-88-08-06279-6

C.8  Un caso reale: come adattare le applicazioni alla GPU

Applicazioni del problema a N corpi su GPU1 Nyland, Harris e Prins (2007) descrivono un kernel di calcolo semplice ma molto utile, con prestazioni su GPU eccellenti: l’algoritmo di tutte le coppie di N corpi, che compare in molte applicazioni scientifiche ed è caratterizzato da un notevole costo computazionale. Le simulazioni a N corpi calcolano l’evoluzione di un sistema di corpi nei quali ogni elemento interagisce permanentemente con tutti gli altri. Un esempio è rappresentato da una simulazione astrofisica, in cui ogni corpo rappresenta una stella e i corpi si attraggono a vicenda grazie alla forza gravitazionale. Altri esempi sono il folding delle proteine, dove la simulazione a N corpi viene utilizzata per calcolare le forze elettrostatiche e di van der Waals, la simulazione dei flussi turbolenti nei fluidi e il calcolo dell’illuminazione globale nella computer grafica. L’algoritmo di tutte le coppie di N corpi calcola la forza totale su ciascun corpo del sistema valutando ogni forza tra due coppie e sommando le forze che agiscono su ogni elemento. Molti scienziati considerano questo metodo il più accurato, dove l’unica perdita di precisione è dovuta alle operazioni in virgola mobile calcolate in hardware. Lo svantaggio è la sua complessità computazionale, dell’ordine di O(n2), di gran lunga troppo elevata per sistemi costituiti da più di 106 corpi. Per ovviare a questo costo eccessivo, sono state proposte svariate semplificazioni per ridurre la complessità degli algoritmi a O(n log n) o O(n): l’algoritmo di Barnes-Hut, il metodo veloce dei Multipoli (FMM, Fast Multipole Method) e la sommatoria particella-reticolo di Ewald (PME, Particle-Mesh-Ewald) sono degli esempi. Tutti questi algoritmi veloci, però, si basano comunque su un metodo che considera tutte le coppie e che costituisce il fondamento per il calcolo accurato delle forze di interazione a breve distanza; tale algoritmo continua quindi ad essere importante. La matematica dei problemi a N corpi Per le simulazioni gravitazionali, la forza d’interazione tra due corpi si calcola utilizzando la fisica elementare. Tra due corpi i e j, il vettore tridimensionale che rappresenta la forza è dato da:

L’intensità della forza è data dal termine a sinistra del secondo membro, mentre la direzione è rappresentata dal termine a destra (il versore è diretto lungo la congiungente i due corpi). Dato un insieme di corpi interagenti tra loro (un sistema intero o un sottoinsieme), il calcolo è semplice: per tutte le coppie di elementi, occorre calcolare la forza e sommarla per ogni corpo. Dopo aver calcolato le forze risultanti, queste vengono utilizzate per aggiornare la posizione e la velocità di ciascun corpo a partire dai precedenti valori di posizione e velocità. Il calcolo delle forze ha complessità O(n2), mentre l’aggiornamento ha complessità O(n). Il codice sequenziale per il calcolo delle forze richiede due cicli for annidati che iterano sulle coppie di corpi. Il ciclo esterno seleziona il corpo sul quale viene calcolata la forza risultante e il ciclo interno itera su tutti i corpi. Quest’ultimo chiama una funzione che calcola la forza tra due corpi e somma poi la forza ottenuta alla somma corrente. Per calcolare le forze in parallelo si può assegnare un thread a ogni corpo, poiché il calcolo della forza su un corpo è indipendente dal calcolo della forza 1   Tratto da Nyland, Harris e Prins [2007], «Fast N-Body Simulation with CUDA», Capitolo 31 di GPU Gems 3.

C59

C60

Appendice C  La grafica e il calcolo con la GPU

© 978-88-08-06279-6

sugli altri corpi. Una volta che tutte le forze sono state calcolate, la posizione e la velocità dei corpi possono essere aggiornate. Il codice della versione sequenziale e parallela è riportato nelle Figure C.8.12 e C.8.13. La versione sequenziale presenta due cicli for annidati. La conversione a CUDA, come in molti altri casi, converte il ciclo seriale esterno in un kernel di thread, dove ogni thread calcola la forza totale su un singolo corpo. Il kernel CUDA assegna un identificativo globale a ciascun thread, il quale sostituisce la variabile di iterazione del ciclo esterno sequenziale. Entrambe le procedure terminano memorizzando l’accelerazione totale di ciascun corpo in un vettore globale, che viene utilizzato, in un passo successivo, per calcolare il nuovo valore di posizione e velocità. Il ciclo esterno viene sostituito da una griglia di kernel CUDA che lancia in esecuzione N thread, uno per ogni corpo. Ottimizzazione per l’esecuzione su GPU Il codice CUDA mostrato in Figura 8.13 è corretto dal punto di vista del funzionamento ma non è efficiente, poiché non considera alcune caratteristiche fondamentali dell’architettura. Si possono ottenere prestazioni migliori implementando tre ottimizzazioni principali. Anzitutto, la memoria condivisa può essere utilizzata per evitare la lettura delle stesse celle di memoria da parte di thread diversi. Secondo, per piccoli valori di N l’utilizzo di più thread per ciascun corpo migliora le prestazioni. Infine, lo srotolamento dei cicli riduce il carico aggiuntivo per la loro gestione. Utilizzo della memoria condivisa La memoria condivisa può contenere un sottoinsieme di posizioni dei corpi in modo molto simile a una cache, eliminando le richieste ridondanti alla mevoid accel_su_tutti_corpi() { int i, j; float3 acc(0.0f, 0.0f, 0.0f);

Figura C.8.12. Il codice sequenziale per calcolare tutte le forze fra le coppie di N corpi.

for(i = 0; i < N; i++) {    for(j = 0; j < N; i++) {     acc = interazione_corpo_corpo( acc, corpo[i], corpo[j]);    }    accel[i] = acc; } }

Figura C.8.13. Il codice di un thread CUDA per calcolare la forza totale su un singolo corpo.

__global__ accel_su_un_corpo() { int i = threadIdx.x + blockDim.x * blockIdx.x; int j; float3 acc(0.0f, 0.0f, 0.0f); for(j = 0; j < N; i++) {    acc = interazione_corpo_corpo(acc, corpo[i], corpo[j]); } accel[i] = acc; }

© 978-88-08-06279-6

C.8  Un caso reale: come adattare le applicazioni alla GPU

C61

moria globale da parte di thread diversi. Si può quindi ottimizzare il codice precedentemente riportato in modo tale che ciascuno dei p thread di un blocco carichi una posizione in memoria condivisa, per un totale di p posizioni. Una volta che tutti i thread hanno caricato una posizione nella memoria condivisa, il che è assicurato da una chiamata a __syncthreads(), ogni thread può effettuare p interazioni utilizzando i dati presenti nella memoria condivisa. Questo viene ripetuto N/p volte per completare il calcolo della forza su ogni corpo, per cui il numero delle richieste alla memoria viene ridotto di un fattore p, tipicamente compreso tra 32 e 128. La funzione accel_su_un_corpo() richiede alcune modifiche per ottenere questa ottimizzazione, e il codice modificato è riportato in Figura C.8.14. Il ciclo che prima iterava su tutti i corpi ora salta con passo pari alla dimensione del blocco, p. Ogni iterazione del ciclo esterno carica p posizioni successive nella memoria condivisa, una per ogni thread. I thread si sincronizzano, quindi ciascuno di essi calcola p forze. Una seconda sincronizzazione si rende necessaria per garantire che i nuovi valori non vengano caricati nella memoria condivisa prima che tutti i thread abbiano completato il calcolo delle forze con i dati attuali. L’impiego della memoria condivisa riduce la banda di trasferimento con la memoria a meno del 10% della banda totale che la GPU è in grado di sostenere: sono necessari, infatti, meno di 5 GB/s. Con questa ottimizzazione, l’applicazione è impegnata a svolgere i calcoli, anziché a restare in attesa del caricamento dei dati dalla memoria (come succede se non si utilizza la memoria condivisa). Le prestazioni, per diversi valori di N, sono mostrate in Figura C.8.15. Impiego di più thread per ciascun corpo La Figura C.8.15 mostra come siano basse le prestazioni per problemi con N piccolo (N < 4096) su una GeForce 8800 GTX. Molte applicazioni scientifiche sono basate sul calcolo di problemi con piccoli valori di N (per tempi lunghi di simulazione), perciò è necessario dedicare i nostri sforzi di ottimizzazione a questi casi. Finora abbiamo supposto che le basse prestazioni fossero semplicemente dovute al fatto che, per N piccolo, non c’è abbastanza lavoro per __shared__ float4 PosizioneCondivisa [256]; ... __global__ void accel_su_un_corpo() { int i = threadIdx.x + blockDim.x * blockIdx.x; int j, k; int p = blockDim.x; float3 acc(0.0f, 0.0f, 0.0f); float4 mioCorpo = corpo[i]; for(j = 0; j < N; i += p) { // Il ciclo esterno salta ogni volta di p    PosizioneCondivisa[threadIdx.x] = corpo[j+threadIdx.x];    __syncthreads();    for(k = 0; k < p; k++) { // Il ciclo interno accede a p posizioni     acc=interazione_corpo_corpo(acc, mioCorpo, PosizioneCondivisa[k]);    }    syncthreads(); } accel[i] = acc; } Figura C.8.14. Il codice CUDA per calcolare la forza totale su ciascun corpo, utilizzando la memoria condivisa per migliorare le prestazioni.

Appendice C  La grafica e il calcolo con la GPU

© 978-88-08-06279-6

Prestazioni dell’algoritmo degli N corpi su GPU

250 200 GFLOPS

150

1 thread, 8800 2 thread, 8800

100

4 thread, 8800 1 thread, 9600

50

32 768

24 576

16 384

12 288

8192

6144

4096

3072

2048

1536

1024

4 thread, 9600

768

0

2 thread, 9600

512

C62

Numero di corpi

Figura C.8.15. Misura delle prestazione dell’applicazione degli N corpi di una GeForce 8800 GTX e di una GeForce 9600. La GeForce 8800 contiene 128 processori a flusso continuo che lavorano a 1,35 GHz, mentre la 9600 ne ha 64 che lavorano a 0,80 GHz (circa il 30% della 8800). Le prestazioni di picco sono di 242 GFLOPS. Per una GPU con più processori, le dimensioni del problema devono essere maggiori per poter ottenere le massime prestazioni: il picco per la 9600 si ha con circa 2048 corpi, mentre la 8800 non raggiunge il proprio picco fino a 16 384 corpi. Per N piccolo, l’impiego di più di un thread per corpo può migliorare nettamente le prestazioni, ma si può incorrere in un calo di prestazioni al crescere di N.

mantenere la GPU impegnata; la soluzione è quindi allocare più thread per ogni corpo. Cambiamo allora le dimensioni del blocco dei thread, da (p, 1, 1) a (p, q, 1), dove q thread si dividono in parti uguali il carico di lavoro relativo a un singolo corpo. Inserendo i thread addizionali all’interno di uno stesso blocco di thread, i risultati parziali possono essere salvati in memoria condivisa. Quando il calcolo di tutte le forze è stato completato, i q risultati parziali possono essere raccolti e sommati per ottenere il risultato finale. Come si può vedere in Figura C.8.15, l’utilizzo di due o quattro thread per ciascun corpo porta a miglioramenti notevoli, per piccoli valori di N. Per esempio, le prestazioni della 8800 GTX aumentano del 110% per N = 1024: con un thread si ottengono 90 GFLOPS, mentre con quattro thread se ne ottengono 190. Le prestazioni si abbassano leggermente al crescere di N, per cui conviene utilizzare questa ottimizzazione per N minore di 4096. L’incremento delle prestazioni è mostrato in Figura C.8.15 per una GPU con 128 processori e per una GPU più piccola, con 64 processori che lavorano a due terzi della frequenza di clock della prima GPU. Confronto delle prestazioni Le prestazioni del codice per il problema a N corpi sono mostrate in Figura C.8.15 e C.8.16. La Figura C.8.15 mostra le prestazioni di due GPU (una di fascia media e una di fascia alta), oltre ai miglioramenti delle prestazioni ottenuti impiegando più thread per ciascun corpo. Le prestazioni della GPU più veloce vanno da 90 a poco meno di 250 GFLOPS. La Figura C.8.16 mostra le prestazioni di una procedura quasi identica (in C++ rispetto al codice precedente in CUDA) che viene eseguita su una CPU Core2 di Intel. Le prestazioni della CPU sono circa l’1% di quelle della GPU in termini di GFLOPS (sono comprese tra lo 0,2% e il 2%), e rimangono pressoché costanti rispetto alle dimensioni del problema che invece variano moltissimo. Il grafico mostra anche i risultati ottenuti compilando per la CPU la versione CUDA del codice, con cui si ottiene un aumento delle prestazioni del 24%.

© 978-88-08-06279-6

Prestazioni dell’algoritmo degli N corpi su CPU Intel

1,8 1,6 1,4 1,2 1 0,8 0,6

E8200 X9775

32 768

24 576

16 384

12 288

8192

6144

4096

3072

2048

1536

1024

X9775-Cuda

768

0,4 0,2 0

T2400

512

GFLOPS

2

C.8  Un caso reale: come adattare le applicazioni alla GPU

Numero di corpi

Figura C.8.16. Misura delle prestazione del codice del problema a N corpi su una CPU. Il grafico mostra le prestazioni del codice del problema a N corpi in singola precisione utilizzando alcune CPU Core2 di Intel; i diversi modelli sono indicati con un simbolo. Si noti la drastica riduzione di prestazioni in termini di GFLOP che mette in evidenza quanto la GPU sia più veloce della CPU. Le prestazioni della CPU sono in genere indipendenti dalla dimensione del problema, eccetto che in un caso anomalo in cui la CPU X9775 produce basse prestazioni per N = 16 384. Il grafico, inoltre, mostra i risultati ottenuti eseguendo la versione CUDA del codice (utilizzando il compilatore CUDA-for-CPU) su una CPU a singolo core; questa versione supera le prestazioni del codice C++ del 24%. Come linguaggio di programmazione, CUDA rende esplicito il parallelismo e la località che possono essere sfruttati dal compilatore. Le CPU analizzate sono Intel: un Core2 Extreme X9775 (nome in codice «Penryn») a 3,2 GHz, un E8200 (nome in codice «Wolfdale») a 2,66 GHz, una CPU per desktop antecedente la Penryn, e un T2400 (nome in codice «Yonah») a 1,83 GHz, una CPU per portatili del 2007. La versione Penryn dell’architettura Core2 è particolarmente interessante per i problemi a N corpi grazie al suo divisore a 4 bit che permette di eseguire divisioni e radici quadrate quattro volte più velocemente delle precedenti CPU Intel.

CUDA, come linguaggio di programmazione, rende esplicito il parallelismo, permettendo così al compilatore di fare un uso migliore dell’unità vettoriale SSE sul singolo core. La versione CUDA del codice degli N corpi è mappata in modo naturale anche su CPU multicore, mediante griglie di blocchi, sulle quali le prestazioni scalano in modo quasi perfetto con il numero di core, ottenendo per N = 4096 un aumento delle prestazioni pari a 2,0, 3,97 e 7,94 con due, quattro e otto core rispettivamente. Risultati Con uno sforzo modesto, abbiamo sviluppato un kernel di calcolo che consente di ottenere prestazioni di GPU superiori fino a un fattore 157 rispetto alle prestazioni delle CPU multicore. Il tempo di esecuzione del codice del problema a N corpi eseguito su una recente CPU Intel (Penryn X9775 a 3,2 GHz, core singolo) è più di 3 secondi per immagine, ma lo stesso codice viene eseguito su una GeForce 8800 a una velocità di 44 immagini al secondo. Su una CPU pre-Penryn il codice richiedeva da 6 a 16 secondi, mentre sui meno recenti processori Core2 e Pentium IV il tempo richiesto era di 25 secondi. In realtà il guadagno nelle prestazioni va dimezzato, poiché la CPU esegue soltanto metà dei calcoli per ottenere lo stesso risultato, sfruttando il fatto che le forze su una coppia di corpi sono uguali in ampiezza e hanno verso opposto. Come fa la GPU a rendere l’esecuzione del codice così veloce? Per rispondere bisogna esaminare l’architettura nel dettaglio. Il calcolo della forza tra una coppia di corpi richiede 20 operazioni in virgola mobile, di cui la maggior parte è costituita da somme e prodotti (alcune delle quali possono essere combinate in un’unica istruzione di moltiplicazione e somma), ma ci sono anche divisioni e radici quadrate richieste per la normalizzazione dei vettori.

C63

C64

Appendice C  La grafica e il calcolo con la GPU

© 978-88-08-06279-6

Le CPU Intel impiegano molti cicli di clock per svolgere le divisioni e le radici quadrate in singola precisione2, sebbene ci sia stato un miglioramento nella più recente famiglia di CPU (Penryn) grazie all’introduzione di un divisore veloce a 4 bit.3 Inoltre, la capacità limitata dei registri costringe a inserire molte istruzioni MOV all’interno del codice x86, presumibilmente per trasferire dati da e verso la cache L1. Al contrario, la GeForce 8800 esegue un’istruzione di thread che calcola il reciproco della radice quadrata in quattro cicli di clock (si veda il Paragrafo C.6), possiede un insieme di registri per ogni thread più grande e una memoria condivisa dalla quale può leggere gli operandi richiesti dalle istruzioni. Infine, il compilatore CUDA produce 15 istruzioni per ciascuna iterazione del ciclo, rispetto alle oltre 40 istruzioni ottenute dai diversi compilatori per le CPU x86. Un maggiore parallelismo, l’esecuzione più veloce di istruzioni complesse, più spazio nei registri e un compilatore più efficiente sono le ragioni che, combinate insieme, spiegano il drastico aumento di prestazioni del codice per il problema a N corpi passando dalla CPU alla GPU. Su una GeForce 8800, l’algoritmo di calcolo su tutte le coppie di N corpi fornisce prestazioni di oltre 240 GFLOPS, a fronte di meno di 2 GFLOPS sui processori sequenziali più recenti. La compilazione ed esecuzione della versione CUDA del codice su una CPU dimostra che il problema scala bene sulle CPU multicore, ma rimane decisamente più lento che su una singola GPU. Abbiamo accoppiato la simulazione degli N corpi su GPU con la visualizzazione grafica del loro moto, riuscendo a visualizzare in modo interattivo 16 384 corpi interagenti tra loro alla velocità di 44 immagini al secondo. Questo permette di visualizzare ed esplorare eventi astrofisici e biofisici in modo interattivo. È inoltre possibile parametrizzare molte impostazioni, come la riduzione del rumore, il fattore di smorzamento e le tecniche di integrazione, visualizzando immediatamente il loro effetto sulla dinamica del sistema. Tutto ciò fornisce agli scienziati una formidabile capacità di visualizzazione che accresce enormemente il livello di comprensione di sistemi altrimenti invisibili, troppo grandi o troppo piccoli, troppo veloci o troppo lenti, e permette di creare modelli migliori dei fenomeni fisici. La Figura C.8.17 presenta la visualizzazione di una sequenza temporale di una simulazione astrofisica di 16 384 corpi, dove ogni corpo rappresenta una galassia. La configurazione iniziale è un guscio sferico di corpi rotanti intorno all’asse z; fenomeni di interesse per gli astrofisici sono la formazione di ammassi e la fusione di galassie. Per il lettore interessato, il codice CUDA per questa applicazione è disponibile nel kit di sviluppo software (SDK) di CUDA, scaricabile da www.nvidia.com/CUDA.

C.9 * Errori e trabocchetti La rapidità con cui si sono evolute le GPU ha fatto nascere molti errori e trabocchetti. In questo paragrafo ne esaminiamo alcuni. Errore: le GPU sono soltanto multiprocessori vettoriali SIMD È facile trarre l’errata conclusione che le GPU siano semplicemente multiprocessori vettoriali SIMD. Le GPU hanno effettivamente un modello di 2   Le istruzioni x86 SSE per il reciproco (RCP*) e il reciproco della radice quadrata (RSQRT*) non vengono considerate nel confronto, perché la loro accuratezza è troppo bassa. 3   Intel 64 and IA-32 Architectures Optimization Reference Manual. Novembre 2007, Intel Corporation. Disponibile anche all’indirizzo: www3.intel.com/design/processor/manuals/248966.pdf.

© 978-88-08-06279-6

C.9  Errori e trabocchetti

C65 Figura C.8.17. 12 immagini catturate durante l’evoluzione di un sistema a N corpi costituito da 16 384 elementi.

programmazione in stile SPMD, con cui il programmatore può scrivere un singolo programma che viene eseguito da istanze di thread multiple, su dati multipli. Tuttavia, l’esecuzione di tali thread non è puramente SIMD o vettoriale, ma avviene secondo la modalità singola istruzione, thread multiplo (SIMT) descritta nel Paragrafo C.4. Ogni thread della GPU possiede propri registri scalari, una memoria privata, uno stato di esecuzione, un numero identificativo del thread, un percorso indipendente di esecuzione e una diramazione condizionata con un proprio program counter, e può indirizzare la memoria in modo indipendente. Anche se un gruppo di thread, come un warp di 32 thread, viene eseguito in modo più efficiente se il program counter dei diversi thread contiene lo stesso valore, questa condizione non è necessaria, per cui i multiprocessori non sono puramente SIMD. Il modello di esecuzione dei thread è MIMD con sincronizzazione a barriera e ottimizzazioni di tipo SIMT. L’esecuzione è più efficiente se gli accessi alla memoria dei singoli thread per la lettura/scrittura possono essere integrati nell’accesso a un unico blocco di dati (coalescenza), ma non è obbligatorio che questo si verifichi. In un’architettura vettoriale SIMD pura, gli accessi ai registri o alla memoria da parte dei diversi thread devono essere allineati secondo una struttura vettoriale regolare. Una GPU non impone tali restrizioni sugli accessi ai registri o alla memoria, anche se l’esecuzione risulta più efficiente se i thread di uno stesso warp accedono a blocchi locali di dati. Un’ulteriore deviazione rispetto al modello SIMD puro consiste nel fatto che una GPU SIMT può eseguire più di un warp di thread in modo concorrente. Nelle applicazioni di grafica, ci possono essere molti gruppi di programmi di vertici, pixel e geometria che vengono eseguiti sulla schiera di multiprocessori in modo concorrente. Anche le applicazioni di calcolo possono eseguire programmi differenti in diversi warp in modo concorrente. Errore: le prestazioni delle GPU non possono crescere più velocemente della legge di Moore La legge di Moore esprime semplicemente una velocità attesa, non definisce un limite massimo di velocità, come la velocità della luce. La legge di Moore

C66

Appendice C  La grafica e il calcolo con la GPU

© 978-88-08-06279-6

prevede che, in base al progresso della tecnologia dei semiconduttori e alla riduzione di dimensioni dei transistor, il costo di produzione del singolo transistor diminuisca esponenzialmente nel tempo. In altre parole, supponendo costante il costo di produzione, il numero di transistor cresce esponenzialmente. Gordon Moore (1965) previde che questo fenomeno avrebbe portato approssimativamente al raddoppio del numero di transistor ogni anno a parità di costo di produzione, e successivamente rivide la previsione riducendo la velocità di crescita a un raddoppio del numero di transistor ogni due anni. Nonostante la prima previsione fosse stata proposta da Moore nel 1965, quando il numero di componenti per circuito integrato era soltanto 50, essa si è dimostrata sorprendentemente vicina alla realtà. La riduzione delle dimensioni dei transistor ha storicamente portato anche altri benefici, come la riduzione del consumo del singolo transistor e l’aumento della velocità del clock, a parità di energia assorbita. La crescente abbondanza di transistor serve ai progettisti dei chip per costruire processori, memoria e altri componenti. Per qualche tempo, i progettisti delle CPU hanno utilizzato i transistor in eccesso per aumentare le prestazioni dei processori a un ritmo simile a quello individuato dalla legge di Moore. In conseguenza di ciò, molti pensano che la crescita delle prestazioni dei processori (il doppio ogni 18-24 mesi) sia la legge di Moore, anche se in realtà non lo è. I progettisti dei microprocessori utilizzano una parte dei transistor in più nei core dei processori per migliorarne l’architettura e si servono della pipeline per aumentare la frequenza di clock. Gli altri transistor in più sono impiegati per fornire una cache di dimensioni maggiori, in modo da rendere più veloce l’accesso alla memoria. I progettisti delle GPU, invece, non utilizzano quasi nessuno dei transistor in più per aumentare le dimensioni della cache: i nuovi transistor vengono utilizzati per migliorare i core dei processori e aggiungere nuovi core. Le GPU diventano sempre più veloci grazie a quattro meccanismi. Primo, i progettisti delle GPU sfruttano abbondantemente la legge di Moore in modo diretto, utilizzando un numero sempre maggiore di transistor per costruire processori più paralleli e, quindi, più veloci. Secondo, i progettisti delle GPU sono riusciti a perfezionare l’architettura, aumentando l’efficienza di elaborazione. Terzo, la legge di Moore assume costi costanti, per cui il ritmo di crescita della legge di Moore può chiaramente essere superato se si spende di più per avere chip di dimensioni maggiori, contenenti ancora più transistor. Quarto, la banda di trasferimento effettiva dei sistemi di memoria delle GPU è cresciuta a un ritmo quasi comparabile a quello dell’aumento della velocità di elaborazione, grazie all’impiego di memorie più veloci e più ampie, alla compressione dei dati e al miglioramento delle cache. La combinazione di questi quattro aspetti ha permesso che le prestazioni delle GPU raddoppiassero regolarmente ogni 12-18 mesi. Questo ritmo di crescita, che supera la legge di Moore, è stato mantenuto per le applicazioni di grafica negli ultimi dieci anni e non dà segnali di rallentamento. Il limite alla crescita, che rende la sfida più impegnativa, sembra essere costituito dal sistema di memoria; tuttavia, l’innovazione sta facendo progressi altrettanto rapidamente in questo settore. Errore: le GPU servono soltanto a gestire la grafica 3D e non a eseguire calcoli generali Le GPU sono costruite per generare grafica 3D, grafica 2D e video. Per soddisfare le esigenze degli sviluppatori di software grafico, espresse dalle specifiche delle interfacce e delle prestazioni/caratteristiche delle API per la grafica,

© 978-88-08-06279-6

C.9  Errori e trabocchetti

le GPU sono diventate processori in virgola mobile programmabili e massicciamente paralleli. Nel campo della grafica, questi processori vengono programmati attraverso le API grafiche, utilizzando arcani linguaggi di programmazione, quali GLSL, Cg e HLSL, costruiti con OpenGL e Direct3D. Tuttavia, nulla impedisce ai progettisti delle GPU di rendere direttamente accessibili ai programmatori i core di elaborazione paralleli senza dover passare attraverso le API grafiche o quei linguaggi di programmazione. Di fatto, la famiglia delle GPU con architettura Tesla rende accessibili i processori attraverso un ambiente software chiamato CUDA, che permette ai programmatori di sviluppare programmi per applicazioni generiche utilizzando il linguaggio C e presto il C++. Le GPU sono elaboratori Turing completi, per cui possono eseguire qualsiasi programma che viene eseguito su una CPU, anche se forse non così bene, ma più velocemente. Errore: le GPU non possono eseguire velocemente i calcoli in virgola mobile a doppia precisione In passato, le GPU non erano in grado di eseguire programmi di calcolo in virgola mobile a doppia precisione se non passando attraverso l’emulazione software, un metodo che risultava tutt’altro che veloce. Nel corso degli anni, le GPU sono passate dalla rappresentazione aritmetica indicizzata (tabelle di corrispondenza dei colori) agli interi su 8 bit per componente di colore, all’aritmetica in virgola fissa, arrivando all’aritmetica in virgola mobile in singola precisione e, recentemente, alla doppia precisione. Le GPU moderne effettuano praticamente tutte le operazioni aritmetiche in virgola mobile in singola precisione secondo lo standard IEEE e stanno iniziando a supportare anche la doppia precisione. A fronte di un piccolo costo addizionale, una GPU può supportare aritmetica in virgola mobile sia in doppia sia in singola precisione. Attualmente, la velocità di calcolo in doppia precisione è minore di quella in singola precisione (da cinque a dieci volte inferiore). Con un costo addizionale, le prestazioni in doppia precisione vengono incrementate, rispetto alla singola precisione, man mano che aumentano le applicazioni che la richiedono. Errore: le GPU non effettuano correttamente i calcoli in virgola mobile Le GPU, o almeno la famiglia di processori con architettura Tesla, effettuano le elaborazioni in virgola mobile in singola precisione al livello di accuratezza indicato dallo standard IEEE 754. In termini di accuratezza, quindi, le GPU sono equivalenti a ogni altro processore conforme allo standard IEEE 754. Attualmente, le GPU non implementano alcune caratteristiche specifiche descritte nello standard, come la gestione dei numeri denormalizzati e la generazione di eccezioni precise in virgola mobile. D’altro canto, la GPU Tesla T10P, recentemente introdotta sul mercato, produce un arrotondamento completamente conforme allo standard IEEE, le operazioni moltiplicazione e somma integrate e il supporto dei numeri denormalizzati per la doppia precisione. Trabocchetto: basta utilizzare più thread per coprire latenze di memoria più lunghe I core di una CPU sono progettati per eseguire un singolo thread alla massima velocità. Perciò, ogni istruzione e i suoi dati devono essere disponibili quando arriva il momento di eseguire l’istruzione. Se l’istruzione successiva non è

C67

C68

Appendice C  La grafica e il calcolo con la GPU

© 978-88-08-06279-6

pronta o se i dati richiesti per quell’istruzione non sono disponibili, l’istruzione non può essere eseguita e il processore viene messo in stallo. La memoria esterna è distante dal processore, per cui si sprecano molti cicli di esecuzione per prelevare i dati da questa memoria; di conseguenza, le CPU necessitano di cache locali capienti, con lo scopo di mantenere il flusso di esecuzione continuo e non andare in stallo. La latenza della memoria è lunga, quindi si cerca di evitare gli stalli facendo tutti gli sforzi possibili per eseguire le istruzioni con la cache. In certi casi, lo spazio di lavoro richiesto dal programma può essere più grande di qualsiasi cache. Alcune CPU hanno adottato un approccio multithreading per rendere tollerabile questa latenza, ma il numero di thread per core è in generale sempre limitato a poche unità. La strategia adottata dalle GPU è differente. I core delle GPU sono progettati per eseguire molti thread in modo concorrente, ma soltanto un’istruzione alla volta per ciascun thread. In altre parole, una GPU esegue ogni thread lentamente ma, nel complesso, se consideriamo tutti i thread, li esegue in modo efficiente. Ogni thread può tollerare un certo livello di latenza della memoria, perché nel frattempo possono essere eseguiti gli altri thread. L’aspetto negativo è che sono necessari moltissimi thread multipli per coprire la latenza della memoria. Inoltre, se gli accessi a memoria sono sparsi o non sono correlati tra i diversi thread, il sistema di memoria è destinato progressivamente a rallentare, perché dovrà rispondere separatamente a ogni singola richiesta; può quindi succedere che anche i thread multipli non siano in grado di coprire la latenza. Quindi, non bisogna puntare ad avere «più» thread, ma bisogna cercare di averne in numero sufficiente; i thread, inoltre, devono «comportarsi bene» in termini di località degli accessi alla memoria. Errore: gli algoritmi O(n) sono difficili da velocizzare Indipendentemente da quanto è veloce la GPU nell’elaborazione dei dati, le operazioni di trasferimento da e verso il dispositivo possono limitare le prestazioni degli algoritmi di complessità O(n) che richiedono una piccola quantità di lavoro per ogni dato. La velocità massima di trasferimento attraverso il bus PCI-Express è di circa di 48 GB/s quando viene utilizzato il trasferimento mediante DMA, ed è leggermente inferiore per trasferimenti non DMA. La CPU, al contrario, presenta velocità tipiche di accesso alla memoria di 8-12 GB/s: alcuni algoritmi, come la somma di vettori, saranno quindi limitati dal trasferimento dei dati dalla memoria di sistema alla GPU e dei risultati dei calcoli dalla GPU alla memoria. Ci sono tre modi per ovviare al costo del trasferimento dei dati. Il primo è cercare di lasciare i dati sulla GPU il più a lungo possibile, invece di spostare i dati avanti e indietro in corrispondenza delle diverse fasi di un algoritmo complesso: CUDA lascia deliberatamente i dati nella GPU tra il lancio di due programmi kernel. Il secondo sfrutta il fatto che la GPU supporta la concorrenza nelle operazioni di copia in ingresso, copia in uscita e calcolo, per cui i dati possono essere fatti fluire dentro e fuori dal dispositivo mentre esso sta elaborando. Questo modello è utile per qualsiasi flusso di dati che debba essere elaborato non appena viene ricevuto, come nel caso dell’elaborazione video, dell’istradamento di pacchetti in rete, della compressione/decompressione di dati e di alcune elaborazioni molto più semplici, come le operazioni matematiche su vettori di grandi dimensioni. Il terzo modo per far fronte al costo del trasferimento è quello di utilizzare la CPU e la GPU insieme, migliorando le prestazioni attraverso l’assegnazione di una parte del lavoro a ciascuna di esse, ossia considerando il sistema

© 978-88-08-06279-6

C.10  Note conclusive

come una piattaforma eterogenea di calcolo. Il modello di programmazione CUDA supporta l’assegnamento dell’attività a una o più GPU e il contemporaneo utilizzo continuato della CPU mediante funzioni asincrone di GPU, senza dover definire dei thread; è quindi relativamente semplice far lavorare in modo concorrente tutte le GPU e la CPU per incrementare ulteriormente le prestazioni.

C.10 * Note conclusive Le GPU sono processori massicciamente paralleli che si sono ormai diffusi non solo nell’ambito della grafica 3D, ma anche nel contesto di molte altre applicazioni. Una così vasta gamma di applicazioni è stata resa possibile dall’evoluzione dei dispositivi grafici in processori programmabili. Il modello di programmazione su GPU per le applicazioni grafiche è solitamente una API, come DirectX o OpenGL. Per le applicazioni di calcolo più generali, il modello di programmazione CUDA è basato sullo stile SPMD (singolo programma, dati multipli) con l’impiego di molti thread paralleli. Il parallelismo delle GPU continuerà a crescere in accordo con la legge di Moore, principalmente grazie all’aumento del numero di processori. Soltanto i modelli di programmazione parallela che possono scalare in modo semplice su centinaia di processori e migliaia di thread avranno successo sulle GPU e CPU con moltissimi core. Inoltre, solo le applicazioni caratterizzate da molti compiti paralleli, in gran parte indipendenti, potranno essere accelerate dalle architetture a moltissimi core massicciamente parallele. I modelli di programmazione delle GPU stanno diventando sempre più flessibili, sia per la grafica sia per il calcolo. Per esempio, CUDA sta evolvendo rapidamente per fornire le funzionalità complete del C/C++. Le API della grafica e i modelli di programmazione si adatteranno probabilmente alle possibilità offerte dal calcolo parallelo e da CUDA. CUDA, inoltre, è organizzato in thread di tipo SPMD, è in grado di scalare e rappresenta un modello pratico, sintetico e facile da capire per implementare un parallelismo elevato. Trascinata da questi cambiamenti nei modelli di programmazione, l’architettura delle GPU, dal canto suo, sta diventando più flessibile e più programmabile. Le unità a funzione fissa delle GPU stanno diventando accessibili ai programmi generici, seguendo le stesse modalità con cui i programmi CUDA già utilizzano le funzioni intrinseche di tessitura per il campionamento della tessitura (attraverso le istruzioni GPU di gestione della tessitura e l’unità di tessitura stessa). L’architettura delle GPU continuerà ad adattarsi alle richieste dei programmatori sia di applicazioni grafiche sia di altre applicazioni. Le GPU, inoltre, continueranno a svilupparsi fornendo potenze di calcolo sempre maggiori attraverso l’aumento dei core di elaborazione, della banda di trasferimento, dei thread e della memoria a disposizione dei programmi. Infine, anche i modelli di programmazione si evolveranno, per adattarsi ai sistemi eterogenei a moltissimi core che contengono sia GPU sia CPU.

Ringraziamenti Quest’appendice è frutto del lavoro di diverse persone di NVIDIA. Esprimiamo la nostra gratitudine per il loro contributo a Michael Garland, John Montrym, Doug Voorhies, Lars Nyland, Erik Lindholm, Paulius Micikevicius, Massimiliano Fatica, Stuart Oberman e Vasily Volkov.

C69

C70

Appendice C  La grafica e il calcolo con la GPU

C.11

© 978-88-08-06279-6

* Inquadramento storico e approfondimenti

Questo paragrafo, che si trova nel CD, fornisce una panoramica storica sulle unità programmabili di elaborazione grafica in tempo reale (GPU) dagli inizi degli anni Ottanta a oggi, periodo in cui il prezzo delle GPU si è ridotto di due ordini di grandezza e le prestazioni sono cresciute altrettanto. Viene descritta l’evoluzione delle GPU dalle pipeline a funzione prefissata ai processori grafici programmabili, con alcuni accenni al calcolo su GPU, ai processori unificati per grafica e calcolo, all’elaborazione visuale e alle GPU scalabili.

D A

P

P

E

N

D

I

A custom format such as this is slave to the architecture of the hardware and the instruction set it serves. The format must strike a proper compromise between ROM size, ROM-output decoding, circuitry size, and machine execution rate. Jim McKevit, et al. 8086 design report, 1997

X

Mapping Control to Hardware D.1

Introduction D-3

D.2

Implementing Combinational Control Units

D.3

D-4

Implementing Finite-State Machine Control D-8

D.4

Implementing the Next-State Function with a Sequencer D-22

D.5

Translating a Microprogram to Hardware D-28

D.6

Concluding Remarks D-32

D.7

Exercises D-33

D.1

Introduction

Control typically has two parts: a combinational part that lacks state and a sequential control unit that handles sequencing and the main control in a multicycle design. Combinational control units are often used to handle part of the decode and control process. The ALU control in Chapter 4 is such an example. A single-cycle implementation like that in Chapter 4 can also use a combinational controller, since it does not require multiple states. Section D.2 examines the implementation of these two combinational units from the truth tables of Chapter 4. Since sequential control units are larger and often more complex, there are a wider variety of techniques for implementing a sequential control unit. The usefulness of these techniques depends on the complexity of the control, characteristics such as the average number of next states for any given state, and the implementation technology. The most straightforward way to implement a sequential control function is with a block of logic that takes as inputs the current state and the opcode field of the Instruction register and produces as outputs the datapath control signals and the value of the next state. The initial representation may be either a finite-state diagram or a microprogram. In the latter case, each microinstruction represents a state.

D-4

Appendix D

Mapping Control to Hardware

In an implementation using a finite-state controller, the next-state function will be computed with logic. Section D.3 constructs such an implementation both for a ROM and a PLA. An alternative method of implementation computes the next-state function by using a counter that increments the current state to determine the next state. When the next state doesn’t follow sequentially, other logic is used to determine the state. Section D.4 explores this type of implementation and shows how it can be used to implement finite-state control. In Section D.5, we show how a microprogram representation of sequential control is translated to control logic.

D.2

Implementing Combinational Control Units

In this section, we show how the ALU control unit and main control unit for the single clock design are mapped down to the gate level. With modern computeraided design (CAD) systems, this process is completely mechanical. The examples illustrate how a CAD system takes advantage of the structure of the control function, including the presence of don’t-care terms.

Mapping the ALU Control Function to Gates Figure D.2.1 shows the truth table for the ALU control function that was developed in Section 4.4. A logic block that implements this ALU control function will have four distinct outputs (called Operation3, Operation2, Operation1, and Operation0), each corresponding to one of the four bits of the ALU control in the last column of Figure D.2.1. The logic function for each output is constructed by combining all the truth table entries that set that particular output. For example, the low-order bit of the ALU control (Operation0) is set by the last two entries of the truth table in Figure D.2.1. Thus, the truth table for Operation0 will have these two entries. Figure D.2.2 shows the truth tables for each of the four ALU control bits. We have taken advantage of the common structure in each truth table to incorporate additional don’t cares. For example, the five lines in the truth table of Figure D.2.1 that set Operation1 are reduced to just two entries in Figure D.2.2. A logic minimization program will use the don’t-care terms to reduce the number of gates and the number of inputs to each gate in a logic gate realization of these truth tables. A confusing aspect of Figure D.2.2 is that there is no logic function for Operation3. That is because this control line is only used for the NOR operation, which is not needed for the MIPS subset in Figure 4.12. From the simplified truth table in Figure D.2.2, we can generate the logic shown in Figure D.2.3, which we call the ALU control block. This process is straightforward

D.2

ALUOp

Implementing Combinational Control Units

Funct field

Operation

ALUOp1

ALUOp0

F5

F4

F3

F2

F1

F0

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

FIGURE D.2.1 The truth table for the 4 ALU control bits (called Operation) as a function of the ALUOp and function code field. This table is the same as that shown in Figure 4.13.

ALUOp

Function code fields

ALUOp1

ALUOp0

F5

F4

F3

F2

F1

F0

0

1

X

X

X

1

X

X

X

X

X

X

X

X

1

X

a. The truth table for Operation2 = 1 (this table corresponds to the second to left bit of the Operation field in Figure D.2.1)

ALUOp ALUOp1

Function code fields

ALUOp0

F5

F4

F3

F2

F1

F0

0

X

X

X

X

X

X

X

X

X

X

X

X

0

X

X

F2

F1

F0

b. The truth table for Operation1 = 1

ALUOp ALUOp1

Function code fields

ALUOp0

F5

F4

F3

1

X

X

X

X

X

X

1

1

X

X

X

1

X

X

X

c. The truth table for Operation0 = 1

FIGURE D.2.2 The truth tables for three ALU control lines. Only the entries for which the output is 1 are shown. The bits in each field are numbered from right to left starting with 0; thus F5 is the most significant bit of the function field, and F0 is the least significant bit. Similarly, the names of the signals corresponding to the 4-bit operation code supplied to the ALU are Operation3, Operation2, Operation1, and Operation0 (with the last being the least significant bit). Thus the truth table above shows the input combinations for which the ALU control should be 0010, 0001, 0110, or 0111 (the other combinations are not used). The ALUOp bits are named ALUOp1 and ALUOp0. The three output values depend on the 2-bit ALUOp field and, when that field is equal to 10, the 6-bit function code in the instruction. Accordingly, when the ALUOp field is not equal to 10, we don’t care about the function code value (it is represented by an X). There is no truth table for when Operation3⫽1 because it is always set to 0 in Figure D.2.1. See Appendix B for more background on don’t cares.

D-5

D-6

Appendix D

Mapping Control to Hardware

ALUOp ALU control block ALUOp0 ALUOp1

F3 F2 F (5–0)

Operation3

Operation2 Operation1

Operation

F1 Operation0 F0

FIGURE D.2.3 The ALU control block generates the four ALU control bits, based on the function code and ALUOp bits. This logic is generated directly from the truth table in Figure D.2.2. Only four of the six bits in the function code are actually needed as inputs, since the upper two bits are always don’t cares. Let’s examine how this logic relates to the truth table of Figure D.2.2. Consider the Operation2 output, which is generated by two lines in the truth table for Operation2. The second line is the AND of two terms (F1 ⫽ 1 and ALUOp1 ⫽ 1); the top two-input AND gate corresponds to this term. The other term that causes Operation2 to be asserted is simply ALUOp0. These two terms are combined with an OR gate whose output is Operation2. The outputs Operation0 and Operation1 are derived in similar fashion from the truth table. Since Operation3 is always 0, we connect a signal and its complement as inputs to an AND gate to generate 0.

and can be done with a CAD program. An example of how the logic gates can be derived from the truth tables is given in the legend to Figure D.2.3. This ALU control logic is simple because there are only three outputs, and only a few of the possible input combinations need to be recognized. If a large number of possible ALU function codes had to be transformed into ALU control signals, this simple method would not be efficient. Instead, you could use a decoder, a memory, or a structured array of logic gates. These techniques are described in Appendix B, and we will see examples when we examine the implementation of the multicycle controller in Section D.3. Elaboration: In general, a logic equation and truth table representation of a logic function are equivalent. (We discuss this in further detail in Appendix B. However, when a truth table only specifies the entries that result in nonzero outputs, it may not completely describe the logic function. A full truth table completely indicates all don’t-care entries. For example, the encoding 11 for ALUOp always generates a don’t care in the output. Thus a complete truth table would have XXX in the output portion for all entries with 11 in the ALUOp field. These don’t-care entries allow us to replace the ALUOp field 10 and

D.2

Implementing Combinational Control Units

01 with 1X and X1, respectively. Incorporating the don’t-care terms and minimizing the logic is both complex and error-prone and, thus, is better left to a program.

Mapping the Main Control Function to Gates Implementing the main control function with an unstructured collection of gates, as we did for the ALU control, is reasonable because the control function is neither complex nor large, as we can see from the truth table shown in Figure D.2.4. However, if most of the 64 possible opcodes were used and there were many more control lines, the number of gates would be much larger and each gate could have many more inputs. Since any function can be computed in two levels of logic, another way to implement a logic function is with a structured two-level logic array. Figure D.2.5 shows such an implementation. It uses an array of AND gates followed by an array of OR gates. This structure is called a programmable logic array (PLA). A PLA is one of the most common ways to implement a control function. We will return to the topic of using structured logic elements to implement control when we implement the finite-state controller in the next section.

Control

Inputs

Outputs

Signal name

R-format

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

FIGURE D.2.4 The control function for the simple one-clock implementation is completely specified by this truth table. This table is the same as that shown in Figure 4.22.

D-7

D-8

Appendix D

Mapping Control to Hardware

Inputs Op5 Op4 Op3 Op2 Op1 Op0

Outputs R-format

Iw

sw

beq

RegDst ALUSrc MemtoReg RegWrite MemRead MemWrite Branch ALUOp1 ALUOp0

FIGURE D.2.5 The structured implementation of the control function as described by the truth table in Figure D.2.4. The structure, called a programmable logic array (PLA), uses an array of AND gates followed by an array of OR gates. The inputs to the AND gates are the function inputs and their inverses (bubbles indicate inversion of a signal). The inputs to the OR gates are the outputs of the AND gates (or, as a degenerate case, the function inputs and inverses). The output of the OR gates is the function outputs.

D.3

Implementing Finite-State Machine Control

To implement the control as a finite-state machine, we must first assign a number to each of the 10 states; any state could use any number, but we will use the sequential numbering for simplicity. Figure D.3.1 shows the finite-state diagram. With 10 states, we will need 4 bits to encode the state number, and we call these state bits S3, S2, S1, and S0. The current-state number will be stored in a state register, as shown in Figure D.3.2. If the states are assigned sequentially, state i is encoded using the

D-9

Implementing Finite-State Machine Control

Instruction decode/ register fetch

Instruction fetch 0

MemRead ALUSrcA = 0 IorD = 0 IRWrite ALUSrcB = 01 ALUOp = 00 PCWrite PCSource = 00

ALUSrcA = 0 ALUSrcB = 11 ALUOp = 00

e)

EQ ')

Start

1

-typ

(Op

2

or W')

= (Op

= 'L

Execution 6

ALUSrcA = 1 ALUSrcB = 10 ALUOp = 00

Branch completion

ALUSrcA = 1 ALUSrcB = 00 ALUOp = 10

= ')

W

'S

3

Memory access 5

MemRead IorD = 1

R-type completion 7

MemWrite IorD = 1

RegDst = 1 RegWrite MemtoReg = 0

Write-back step 4 RegDst = 0 RegWrite MemtoReg = 1

FIGURE D.3.1 The finite-state diagram for multicycle control.

Jump completion

'B 9

ALUSrcA = 1 ALUSrcB = 00 ALUOp = 01 PCWriteCond PCSource = 01

p

Memory access

=

8

(O

(Op = 'LW')

(O

')

'SW

(O p

Memory address computation

R p=

(Op = 'J')

D.3

PCWrite PCSource = 10

Appendix D

Mapping Control to Hardware

PCWrite PCWriteCond IorD MemRead MemWrite IRWrite Control logic

MemtoReg PCSource Outputs

ALUOp ALUSrcB ALUSrcA RegWrite RegDst NS3 NS2 NS1 NS0

Instruction register opcode field

S0

S1

S2

S3

Op0

Op1

Op2

Op3

Op4

Inputs

Op5

D-10

State register

FIGURE D.3.2 The control unit for MIPS will consist of some control logic and a register to hold the state. The state register is written at the active clock edge and is stable during the clock cycle

state bits as the binary number i. For example, state 6 is encoded as 0110two or S3 ⫽ 0, S2 ⫽ 1, S1 ⫽ 1, S0 ⫽ 0, which can also be written as S3 · S2 · S1 · S0 The control unit has outputs that specify the next state. These are written into the state register on the clock edge and become the new state at the beginning of the next clock cycle following the active clock edge. We name these outputs NS3, NS2, NS1, and NS0. Once we have determined the number of inputs, states, and outputs, we know what the basic outline of the control unit will look like, as we show in Figure D.3.2.

D.3

Implementing Finite-State Machine Control

The block labeled “control logic” in Figure D.3.2 is combinational logic. We can think of it as a big table giving the value of the outputs in terms of the inputs. The logic in this block implements the two different parts of the finite-state machine. One part is the logic that determines the setting of the datapath control outputs, which depend only on the state bits. The other part of the control logic implements the next-state function; these equations determine the values of the next-state bits based on the current-state bits and the other inputs (the 6-bit opcode). Figure D.3.3 shows the logic equations: the top portion shows the outputs, and the bottom portion shows the next-state function. The values in this table were

Output

Current states

PCWrite

state0 + state9

PCWriteCond

state8

IorD

state3 + state5

MemRead

state0 + state3

MemWrite

state5

IRWrite

state0

MemtoReg

state4

PCSource1

state9

PCSource0

state8

ALUOp1

state6

ALUOp0

state8

ALUSrcB1

state1 +state2

ALUSrcB0

state0 + state1

ALUSrcA

state2 + state6 + state8

RegWrite

state4 + state7

RegDst

state7

NextState0

state4 + state5 + state7 + state8 + state9

NextState1

state0

Op

NextState2

state1

(Op = 'lw') + (Op = 'sw')

NextState3

state2

(Op = 'lw')

NextState4

state3

NextState5

state2

(Op = 'sw')

NextState6

state1

(Op = 'R-type')

NextState7

state6

NextState8

state1

(Op = 'beq')

NextState9

state1

(Op = 'jmp')

FIGURE D.3.3 The logic equations for the control unit shown in a shorthand form. Remember that “⫹” stands for OR in logic equations. The state inputs and NextState outputs must be expanded by using the state encoding. Any blank entry is a don’t care.

D-11

D-12

Appendix D

Mapping Control to Hardware

determined from the state diagram in Figure D.3.1. Whenever a control line is active in a state, that state is entered in the second column of the table. Likewise, the next-state entries are made whenever one state is a successor to another. In Figure D.3.3, we use the abbreviation stateN to stand for current state N. Thus, stateN is replaced by the term that encodes the state number N. We use NextStateN to stand for the setting of the next-state outputs to N. This output is implemented using the next-state outputs (NS). When NextStateN is active, the bits NS[3–0] are set corresponding to the binary version of the value N. Of course, since a given next-state bit is activated in multiple next states, the equation for each state bit will be the OR of the terms that activate that signal. Likewise, when we use a term such as (Op ⫽ ‘lw’), this corresponds to an AND of the opcode inputs that specifies the encoding of the opcode lw in 6 bits, just as we did for the simple control unit in the previous section of this chapter. Translating the entries in Figure D.3.3 into logic equations for the outputs is straightforward.

Logic Equations for Next-State Outputs

EXAMPLE ANSWER

Give the logic equation for the low-order next-state bit, NS0. The next-state bit NS0 should be active whenever the next state has NS0 ⫽ 1 in the state encoding. This is true for NextState1, NextState3, NextState5, NextState7, and NextState9. The entries for these states in Figure D.3.3 supply the conditions when these next-state values should be active. The equation for each of these next states is given below. The first equation states that the next state is 1 if the current state is 0; the current state is 0 if each of the state input bits is 0, which is what the rightmost product term indicates. NextState1 ⫽ State0 ⫽ S3 · S2 · S1 · S0 NextState3 ⫽ State2 · (Op[5-0]⫽1w) ⫽ S3 · S2 · S1 · S0 · Op5 · Op4 · Op3 · Op2 · Op1 · Op0

D.3

Implementing Finite-State Machine Control

NextState5 ⫽ State2 · (Op[5-0]⫽sw) ⫽ S3 · S2 · S1 · S0 · Op5 · Op4 · Op3 · Op2 · Op1 · Op0 NextState7 ⫽ State6 ⫽ S3 · S2 · S1 · S0 NextState9 ⫽ State1 · (Op[5-0]⫽jmp) ⫽ S3 · S2 · S1 · S0 · Op5 · Op4 · Op3 · Op2 · Op1 · Op0 NS0 is the logical sum of all these terms. As we have seen, the control function can be expressed as a logic equation for each output. This set of logic equations can be implemented in two ways: corresponding to a complete truth table, or corresponding to a two-level logic structure that allows a sparse encoding of the truth table. Before we look at these implementations, let’s look at the truth table for the complete control function. It is simplest if we break the control function defined in Figure D.3.3 into two parts: the next-state outputs, which may depend on all the inputs, and the control signal outputs, which depend only on the current-state bits. Figure D.3.4 shows the truth tables for all the datapath control signals. Because these signals actually depend only on the state bits (and not the opcode), each of the entries in a table in Figure D.3.4 actually represents 64 (⫽ 26) entries, with the 6 bits named Op having all possible values; that is, the Op bits are don’t-care bits in determining the data path control outputs. Figure D.3.5 shows the truth table for the next-state bits NS[3–0], which depend on the state input bits and the instruction bits, which supply the opcode. Elaboration: There are many opportunities to simplify the control function by observing similarities among two or more control signals and by using the semantics of the implementation. For example, the signals PCWriteCond, PCSource0, and ALUOp0 are all asserted in exactly one state, state 8. These three control signals can be replaced by a single signal.

D-13

D-14

Appendix D

Mapping Control to Hardware

s3

s2

s1

s0

s3

s2

s1

s0

s3

s2

s1

s0

0

0

0

0

1

0

0

0

0

0

1

1

1

0

0

1

0

1

0

1

a. Truth table for PCWrite

b. Truth table for PCWriteCond

c. Truth table for IorD

s3

s2

s1

s0

s3

s2

s1

s0

s3

s2

s1

s0

0

0

0

0

0

1

0

1

0

0

0

0

0

0

1

1

d. Truth table for MemRead

e. Truth table for MemWrite

s3

s2

s1

s0

0

1

0

0

g. Truth table for MemtoReg

f. Truth table for IRWrite

s3

s2

s1

s0

1

0

0

1

h. Truth table for PCSource1

s3

s2

s1

s0

1

0

0

0

i. Truth table for PCSource0

s3

s2

s1

s0

s3

s2

s1

s0

s3

s2

s1

s0

0

1

1

0

1

0

0

0

0

0

0

1

0

0

1

0

j. Truth table for ALUOp1

k. Truth table for ALUOp0

l. Truth table for ALUSrcB1

s3

s2

s1

s0

s3

s2

s1

s0

s3

s2

s1

s0

0

0

0

0

0

0

1

0

0

1

0

0

0

0

0

1

0

1

1

0

0

1

1

1

1

0

0

0

m. Truth table for ALUSrcB0

n. Truth table for ALUSrcA

s3

s2

s1

s0

0

1

1

1

o. Truth table for RegWrite

p. Truth table for RegDst

FIGURE D.3.4 The truth tables are shown for the 16 datapath control signals that depend only on the current-state input bits, which are shown for each table. Each truth table row corresponds to 64 entries: one for each possible value of the six Op bits. Notice that some of the outputs are active under nearly the same circumstances. For example, in the case of PCWriteCond, PCSource0, and ALUOp0, these signals are active only in state 8 (see b, i, and k). These three signals could be replaced by one signal. There are other opportunities for reducing the logic needed to implement the control function by taking advantage of further similarities in the truth tables.

D.3

Implementing Finite-State Machine Control

Op5

Op4

Op3

Op2

Op1

Op0

S3

S2

S1

S0

0

0

0

0

1

0

0

0

0

1

0

0

0

1

0

0

0

0

0

1

a. The truth table for the NS3 output, active when the next state is 8 or 9. This signal is activated when the current state is 1.

Op5

Op4

Op3

Op2

Op1

Op0

S3

S2

S1

S0

0

0

0

0

0

0

0

0

0

1

1

0

1

0

1

1

0

0

1

0

X

X

X

X

X

X

0

0

1

1

X

X

X

X

X

X

0

1

1

0

b. The truth table for the NS2 output, which is active when the next state is 4, 5, 6, or 7. This situation occurs when the current state is one of 1, 2, 3, or 6.

Op5

Op4

Op3

Op2

Op1

Op0

S3

S2

S1

S0

0

0

0

0

0

0

0

0

0

1

1

0

0

0

1

1

0

0

0

1

1

0

1

0

1

1

0

0

0

1

1

0

0

0

1

1

0

0

1

0

X

X

X

X

X

X

0

1

1

0

c. The truth table for the NS1 output, which is active when the next state is 2, 3, 6, or 7. The next state is one of 2, 3, 6, or 7 only if the current state is one of 1, 2, or 6.

Op5

Op4

Op3

Op2

Op1

Op0

S3

S2

S1

S0

X

X

X

X

X

X

0

0

0

0

1

0

0

0

1

1

0

0

1

0

1

0

1

0

1

1

0

0

1

0

X

X

X

X

X

X

0

1

1

0

0

0

0

0

1

0

0

0

0

1

d. The truth table for the NS0 output, which is active when the next state is 1, 3, 5, 7, or 9. This happens only if the current state is one of 0, 1, 2, or 6.

FIGURE D.3.5 The four truth tables for the four next-state output bits (NS[3–0]). The nextstate outputs depend on the value of Op[5-0], which is the opcode field, and the current state, given by S[3– 0]. The entries with X are don’t-care terms. Each entry with a don’t-care term corresponds to two entries, one with that input at 0 and one with that input at 1. Thus an entry with n don’t-care terms actually corresponds to 2n truth table entries.

A ROM Implementation Probably the simplest way to implement the control function is to encode the truth tables in a read-only memory (ROM). The number of entries in the memory for the truth tables of Figures D.3.4 and D.3.5 is equal to all possible values of the inputs (the 6 opcode bits plus the 4 state bits), which is 2# inputs ⫽ 210 ⫽ 1024. The inputs

D-15

D-16

Appendix D

Mapping Control to Hardware

to the control unit become the address lines for the ROM, which implements the control logic block that was shown in Figure D.3.2. The width of each entry (or word in the memory) is 20 bits, since there are 16 datapath control outputs and 4 next-state bits. This means the total size of the ROM is 210 ⫻ 20 ⫽ 20 Kbits. The setting of the bits in a word in the ROM depends on which outputs are active in that word. Before we look at the control words, we need to order the bits within the control input (the address) and output words (the contents), respectively. We will number the bits using the order in Figure D.3.2, with the next-state bits being the low-order bits of the control word and the current-state input bits being the low-order bits of the address. This means that the PCWrite output will be the highorder bit (bit 19) of each memory word, and NS0 will be the low-order bit. The high-order address bit will be given by Op5, which is the high-order bit of the instruction, and the low-order address bit will be given by S0. We can construct the ROM contents by building the entire truth table in a form where each row corresponds to one of the 2n unique input combinations, and a set of columns indicates which outputs are active for that input combination. We don’t have the space here to show all 1024 entries in the truth table. However, by separating the datapath control and next-state outputs, we do, since the datapath control outputs depend only on the current state. The truth table for the datapath control outputs is shown in Figure D.3.6. We include only the encodings of the state inputs that are in use (that is, values 0 through 9 corresponding to the 10 states of the state machine). The truth table in Figure D.3.6 directly gives the contents of the upper 16 bits of each word in the ROM. The 4-bit input field gives the low-order 4 address bits of each word, and the column gives the contents of the word at that address. If we did show a full truth table for the datapath control bits with both the state number and the opcode bits as inputs, the opcode inputs would all be don’t cares. When we construct the ROM, we cannot have any don’t cares, since the addresses into the ROM must be complete. Thus, the same datapath control outputs will occur many times in the ROM, since this part of the ROM is the same whenever the state bits are identical, independent of the value of the opcode inputs.

Control ROM Entries

EXAMPLE

For what ROM addresses will the bit corresponding to PCWrite, the high bit of the control word, be 1?

D.3

Outputs

Implementing Finite-State Machine Control

D-17

Input values (S[3–0]) 0000

0001

0010

0011

0100

0101

0110

0111

1000

1001

PCWrite

1

0

0

0

0

0

0

0

0

1

PCWriteCond

0

0

0

0

0

0

0

0

1

0

IorD

0

0

0

1

0

1

0

0

0

0

MemRead

1

0

0

1

0

0

0

0

0

0

MemWrite

0

0

0

0

0

1

0

0

0

0

IRWrite

1

0

0

0

0

0

0

0

0

0

MemtoReg

0

0

0

0

1

0

0

0

0

0

PCSource1

0

0

0

0

0

0

0

0

0

1

PCSource0

0

0

0

0

0

0

0

0

1

0

ALUOp1

0

0

0

0

0

0

1

0

0

0

ALUOp0

0

0

0

0

0

0

0

0

1

0

ALUSrcB1

0

1

1

0

0

0

0

0

0

0

ALUSrcB0

1

1

0

0

0

0

0

0

0

0

ALUSrcA

0

0

1

0

0

0

1

0

1

0

RegWrite

0

0

0

0

1

0

0

1

0

0

RegDst

0

0

0

0

0

0

0

1

0

0

FIGURE D.3.6 The truth table for the 16 datapath control outputs, which depend only on the state inputs. The values are determined from Figure D.3.4. Although there are 16 possible values for the 4-bit state field, only ten of these are used and are shown here. The ten possible values are shown at the top; each column shows the setting of the datapath control outputs for the state input value that appears at the top of the column. For example, when the state inputs are 0011 (state 3), the active datapath control outputs are IorD or MemRead.

PCWrite is high in states 0 and 9; this corresponds to addresses with the 4 low-order bits being either 0000 or 1001. The bit will be high in the memory word independent of the inputs Op[5–0], so the addresses with the bit high are 000000000, 0000001001, 0000010000, 0000011001, .  .  . , 1111110000, 1111111001. The general form of this is XXXXXX0000 or XXXXXX1001, where XXXXXX is any combination of bits, and corresponds to the 6-bit opcode on which this output does not depend.

ANSWER

D-18

Appendix D

Mapping Control to Hardware

We will show the entire contents of the ROM in two parts to make it easier to show. Figure D.3.7 shows the upper 16 bits of the control word; this comes directly from Figure D.3.6. These datapath control outputs depend only on the state inputs, and this set of words would be duplicated 64 times in the full ROM, as we discussed above. The entries corresponding to input values 1010 through 1111 are not used, so we do not care what they contain. Figure D.3.8 shows the lower four bits of the control word corresponding to the next-state outputs. The last column of the table in Figure D.3.8 corresponds to all the possible values of the opcode that do not match the specified opcodes. In state 0, the next state is always state 1, since the instruction was still being fetched. After state 1, the opcode field must be valid. The table indicates this by the entries marked illegal; we discuss how to deal with these exceptions and interrupts opcodes in Section 4.9. Not only is this representation as two separate tables a more compact way to show the ROM contents; it is also a more efficient way to implement the ROM. The majority of the outputs (16 of 20 bits) depends only on 4 of the 10 inputs. The number of bits in total when the control is implemented as two separate ROMs is 24 ⫻ 16 ⫹ 210 ⫻ 4 ⫽ 256 ⫹ 4096 ⫽ 4.3 Kbits, which is about one-fifth of the size of a single ROM, which requires 210 ⫻ 20 ⫽ 20 Kbits. There is some overhead associated with any structured-logic block, but in this case the additional overhead of an extra ROM would be much smaller than the savings from splitting the single ROM.

Lower 4 bits of the address

Bits 19–4 of the word

0000

1001010000001000

0001

0000000000011000

0010

0000000000010100

0011

0011000000000000

0100

0000001000000010

0101

0010100000000000

0110

0000000001000100

0111

0000000000000011

1000

0100000010100100

1001

1000000100000000

FIGURE D.3.7 The contents of the upper 16 bits of the ROM depend only on the state inputs. These values are the same as those in Figure D.3.6, simply rotated 90°. This set of control words would be duplicated 64 times for every possible value of the upper six bits of the address.

D.3

Implementing Finite-State Machine Control

Although this ROM encoding of the control function is simple, it is wasteful, even when divided into two pieces. For example, the values of the Instruction register inputs are often not needed to determine the next state. Thus, the nextstate ROM has many entries that are either duplicated or are don’t care. Consider the case when the machine is in state 0: there are 26 entries in the ROM (since the opcode field can have any value), and these entries will all have the same contents (namely, the control word 0001). The reason that so much of the ROM is wasted is that the ROM implements the complete truth table, providing the opportunity to have a different output for every combination of the inputs. But most combinations of the inputs either never happen or are redundant!

Op [5–0] Current state 000000 S[3–0] (R-format)

000010 (jmp)

000100 (beq)

100011 (lw)

101011 (sw)

Any other value

0000

0001

0001

0001

0001

0001

0001

0001

0110

1001

1000

0010

0010

Illegal

0010

XXXX

XXXX

XXXX

0011

0101

Illegal

0011

0100

0100

0100

0100

0100

Illegal

0100

0000

0000

0000

0000

0000

Illegal

0101

0000

0000

0000

0000

0000

Illegal

0110

0111

0111

0111

0111

0111

Illegal

0111

0000

0000

0000

0000

0000

Illegal

1000

0000

0000

0000

0000

0000

Illegal

1001

0000

0000

0000

0000

0000

Illegal

FIGURE D.3.8 This table contains the lower 4 bits of the control word (the NS outputs), which depend on both the state inputs, S[3–0], and the opcode, Op[5–0], which correspond to the instruction opcode. These values can be determined from Figure D.3.5. The opcode name is shown under the encoding in the heading. The four bits of the control word whose address is given by the current-state bits and Op bits are shown in each entry. For example, when the state input bits are 0000, the output is always 0001, independent of the other inputs; when the state is 2, the next state is don’t care for three of the inputs, 3 for lw, and 5 for sw. Together with the entries in Figure D.3.7, this table specifies the contents of the control unit ROM. For example, the word at address 1000110001 is obtained by finding the upper 16 bits in the table in Figure D.3.7 using only the state input bits (0001) and concatenating the lower four bits found by using the entire address (0001 to find the row and 100011 to find the column). The entry from Figure D.3.7 yields 0000000000011000, while the appropriate entry in the table immediately above is 0010. Thus the control word at address 1000110001 is 00000000000110000010. The column labeled “Any other value” applies only when the Op bits do not match one of the specified opcodes.

D-19

D-20

Appendix D

Mapping Control to Hardware

A PLA Implementation We can reduce the amount of control storage required at the cost of using more complex address decoding for the control inputs, which will encode only the input combinations that are needed. The logic structure most often used to do this is a programmed logic array (PLA), which we mentioned earlier and illustrated in Figure D.2.5. In a PLA, each output is the logical OR of one or more minterms. A minterm, also called a product term, is simply a logical AND of one or more inputs. The inputs can be thought of as the address for indexing the PLA, while the minterms select which of all possible address combinations are interesting. A minterm corresponds to a single entry in a truth table, such as those in Figure D.3.4, including possible don’t-care terms. Each output consists of an OR of these minterms, which exactly corresponds to a complete truth table. However, unlike a ROM, only those truth table entries that produce an active output are needed, and only one copy of each minterm is required, even if the minterm contains don’t cares. Figure D.3.9 shows the PLA that implements this control function. As we can see from the PLA in Figure D.3.9, there are 17 unique minterms—10 that depend only on the current state and 7 others that depend on a combination of the Op field and the current-state bits. The total size of the PLA is proportional to (#inputs ⫻ #product terms) ⫹ (#outputs ⫻ #product terms), as we can see symbolically from the figure. This means the total size of the PLA in Figure D.3.9 is proportional to (10 ⫻ 17) ⫹ (20 ⫻ 17) ⫽ 510. By comparison, the size of a single ROM is proportional to 20 Kb, and even the two-part ROM has a total of 4.3 Kb. Because the size of a PLA cell will be only slightly larger than the size of a bit in a ROM, a PLA will be a much more efficient implementation for this control unit. Of course, just as we split the ROM in two, we could split the PLA into two PLAs: one with 4 inputs and 10 minterms that generates the 16 control outputs, and one with 10 inputs and 7 minterms that generates the 4 next-state outputs. The first PLA would have a size proportional to (4 ⫻ 10) ⫹ (10 ⫻ 16) ⫽ 200, and the second PLA would have a size proportional to (10 ⫻ 7) ⫹ (4 ⫻ 7) ⫽ 98. This would yield a total size proportional to 298 PLA cells, about 55% of the size of a single PLA. These two PLAs will be considerably smaller than an implementation using two ROMs. For more details on PLAs and their implementation, as well as the references for books on logic design, see Appendix B.

D.3

Implementing Finite-State Machine Control

Op5 Op4 Op3 Op2 Op1 Op0 S3 S2 S1 S0 PCWrite PCWriteCond IorD MemRead MemWrite IRWrite MemtoReg PCSource1 PCSource0 ALUOp1 ALUOp0 ALUSrcB1 ALUSrcB0 ALUSrcA RegWrite RegDst NS3 NS2 NS1 NS0

FIGURE D.3.9 This PLA implements the control function logic for the multicycle implementation. The inputs to the control appear on the left and the outputs on the right. The top half of the figure is the AND plane that computes all the minterms. The minterms are carried to the OR plane on the vertical lines. Each colored dot corresponds to a signal that makes up the minterm carried on that line. The sum terms are computed from these minterms, with each gray dot representing the presence of the intersecting minterm in that sum term. Each output consists of a single sum term.

D-21

D-22

Appendix D

D.4

Mapping Control to Hardware

Implementing the Next-State Function with a Sequencer

Let’s look carefully at the control unit we built in the last section. If you examine the ROMs that implement the control in Figures D.3.7 and D.3.8, you can see that much of the logic is used to specify the next-state function. In fact, for the implementation using two separate ROMs, 4096 out of the 4368 bits (94%) correspond to the next-state function! Furthermore, imagine what the control logic would look like if the instruction set had many more different instruction types, some of which required many clocks to implement. There would be many more states in the finite-state machine. In some states, we might be branching to a large number of different states depending on the instruction type (as we did in state 1 of the finite-state machine in Figure D.3.1). However, many of the states would proceed in a sequential fashion, just as states 3 and 4 do in Figure D.3.1. For example, if we included floating point, we would see a sequence of many states in a row that implement a multicycle floating-point instruction. Alternatively, consider how the control might look for a machine that can have multiple memory operands per instruction. It would require many more states to fetch multiple memory operands. The result of this would be that the control logic will be dominated by the encoding of the next-state function. Furthermore, much of the logic will be devoted to sequences of states with only one path through them that look like states 2 through 4 in Figure D.3.1. With more instructions, these sequences will consist of many more sequentially numbered states than for our simple subset. To encode these more complex control functions efficiently, we can use a control unit that has a counter to supply the sequential next state. This counter often eliminates the need to encode the next-state function explicitly in the control unit. As shown in Figure D.4.1, an adder is used to increment the state, essentially turning it into a counter. The incremented state is always the state that follows in numerical order. However, the finite-state machine sometimes “branches.” For example, in state 1 of the finite-state machine (see Figure D.3.1), there are four possible next states, only one of which is the sequential next state. Thus, we need to be able to choose between the incremented state and a new state based on the inputs from the Instruction register and the current state. Each control word will include control lines that will determine how the next state is chosen. It is easy to implement the control output signal portion of the control word, since, if we use the same state numbers, this portion of the control word will look exactly like the ROM contents shown in Figure D.3.7. However, the method

D.4

Implementing the Next-State Function with a Sequencer

PCWrite PCWriteCond IorD MemRead MemWrite IRWrite

Control unit

PLA or ROM

Outputs

Input

MemtoReg PCSource ALUOp ALUSrcB ALUSrcA RegWrite RegDst

AddrCtl

1 State Adder

Op[5–0]

Address select logic

Instruction register opcode field FIGURE D.4.1 The control unit using an explicit counter to compute the next state. In this control unit, the next state is computed using a counter (at least in some states). By comparison, Figure D.3.2 encodes the next state in the control logic for every state. In this control unit, the signals labeled AddrCtl control how the next state is determined.

for selecting the next state differs from the next-state function in the finite-state machine. With an explicit counter providing the sequential next state, the control unit logic need only specify how to choose the state when it is not the sequentially following state. There are two methods for doing this. The first is a method we have already seen: namely, the control unit explicitly encodes the next-state function. The difference is that the control unit need only set the next-state lines when the designated next state is not the state that the counter indicates. If the number of

D-23

D-24

Appendix D

Mapping Control to Hardware

states is large and the next-state function that we need to encode is mostly empty, this may not be a good choice, since the resulting control unit will have lots of empty or redundant space. An alternative approach is to use separate external logic to specify the next state when the counter does not specify the state. Many control units, especially those that implement large instruction sets, use this approach, and we will focus on specifying the control externally. Although the nonsequential next state will come from an external table, the control unit needs to specify when this should occur and how to find that next state. There are two kinds of “branching” that we must implement in the address select logic. First, we must be able to jump to one of a number of states based on the opcode portion of the Instruction register. This operation, called a dispatch, is usually implemented by using a set of special ROMs or PLAs included as part of the address selection logic. An additional set of control outputs, which we call AddrCtl, indicates when a dispatch should be done. Looking at the finite-state diagram (Figure D.3.1), we see that there are two states in which we do a branch based on a portion of the opcode. Thus we will need two small dispatch tables. (Alternatively, we could also use a single dispatch table and use the control bits that select the table as address bits that choose from which portion of the dispatch table to select the address.) The second type of branching that we must implement consists of branching back to state 0, which initiates the execution of the next MIPS instruction. Thus there are four possible ways to choose the next state (three types of branches, plus incrementing the current-state number), which can be encoded in 2 bits. Let’s assume that the encoding is as follows: AddrCtl value 0 1 2 3

Action Set state to 0 Dispatch with ROM 1 Dispatch with ROM 2 Use the incremented state

If we use this encoding, the address select logic for this control unit can be implemented as shown in Figure D.4.2. To complete the control unit, we need only specify the contents of the dispatch ROMs and the values of the address-control lines for each state. We have already specified the datapath control portion of the control word using the ROM contents of Figure D.3.7 (or the corresponding portions of the PLA in Figure D.3.9). The next-state counter and dispatch ROMs take the place of the portion of the control unit that was computing the next state, which was shown in Figure D.3.8. We are

D.4

Implementing the Next-State Function with a Sequencer

PLA or ROM

1 State Adder

3

Mux 2 1

AddrCtl 0 0

Dispatch ROM 2

Dispatch ROM 1

Op

Address select logic

Instruction register opcode field FIGURE D.4.2 This is the address select logic for the control unit of Figure D.4.1.

only implementing a portion of the instruction set, so the dispatch ROMs will be largely empty. Figure D.4.3 shows the entries that must be assigned for this subset.

Dispatch ROM 1

Dispatch ROM 2

Op

Opcode name

Value

Op

Opcode name

Value

000000

R-format

0110

100011

lw

0011

000010

jmp

1001

101011

sw

0101

000100

beq

1000

100011

lw

0010

101011

sw

0010

FIGURE D.4.3 The dispatch ROMs each have 26 ⫽ 64 entries that are 4 bits wide, since that is the number of bits in the state encoding. This figure only shows the entries in the ROM that are of interest for this subset. The first column in each table indicates the value of Op, which is the address used to access the dispatch ROM. The second column shows the symbolic name of the opcode. The third column indicates the value at that address in the ROM.

Now we can determine the setting of the address selection lines (AddrCtl) in each control word. The table in Figure D.4.4 shows how the address control must

D-25

D-26

Appendix D

Mapping Control to Hardware

State number

Address-control action

Value of AddrCtl

0

Use incremented state

3

1

Use dispatch ROM 1

1

2

Use dispatch ROM 2

2

3

Use incremented state

3

4

Replace state number by 0

0

5

Replace state number by 0

0

6

Use incremented state

3

7

Replace state number by 0

0

8

Replace state number by 0

0

9

Replace state number by 0

0

FIGURE D.4.4 The values of the address-control lines are set in the control word that corresponds to each state.

be set for every state. This information will be used to specify the setting of the AddrCtl field in the control word associated with that state. The contents of the entire control ROM are shown in Figure D.4.5. The total storage required for the control is quite small. There are 10 control words, each 18 bits wide, for a total of 180 bits. In addition, the two dispatch tables are 4 bits wide and each has 64 entries, for a total of 512 additional bits. This total of 692 bits beats the implementation that uses two ROMs with the next-state function encoded in the ROMs (which requires 4.3 Kbits). Of course, the dispatch tables are sparse and could be more efficiently implemented with two small PLAs. The control ROM could also be replaced with a PLA. State number

Control word bits 17–2

0

1001010000001000

Control word bits 1–0 11

1

0000000000011000

01

2

0000000000010100

10

3

0011000000000000

11

4

0000001000000010

00

5

0010100000000000

00

6

0000000001000100

11

7

0000000000000011

00

8

0100000010100100

00

9

1000000100000000

00

FIGURE D.4.5 The contents of the control memory for an implementation using an explicit counter. The first column shows the state, while the second shows the datapath control bits, and the last column shows the address-control bits in each control word. Bits 17–2 are identical to those in Figure D.3.7.

D.4

Implementing the Next-State Function with a Sequencer

Optimizing the Control Implementation We can further reduce the amount of logic in the control unit by two different techniques. The first is logic minimization, which uses the structure of the logic equations, including the don’t-care terms, to reduce the amount of hardware required. The success of this process depends on how many entries exist in the truth table, and how those entries are related. For example, in this subset, only the lw and sw opcodes have an active value for the signal Op5, so we can replace the two truth table entries that test whether the input is lw or sw by a single test on this bit; similarly, we can eliminate several bits used to index the dispatch ROM because this single bit can be used to find lw and sw in the first dispatch ROM. Of course, if the opcode space were less sparse, opportunities for this optimization would be more difficult to locate. However, in choosing the opcodes, the architect can provide additional opportunities by choosing related opcodes for instructions that are likely to share states in the control. A different sort of optimization can be done by assigning the state numbers in a finite-state or microcode implementation to minimize the logic. This optimization, called state assignment, tries to choose the state numbers such that the resulting logic equations contain more redundancy and can thus be simplified. Let’s consider the case of a finite-state machine with an encoded next-state control first, since it allows states to be assigned arbitrarily. For example, notice that in the finite-state machine, the signal RegWrite is active only in states 4 and 7. If we encoded those states as 8 and 9, rather than 4 and 7, we could rewrite the equation for RegWrite as simply a test on bit S3 (which is only on for states 8 and 9). This renumbering allows us to combine the two truth table entries in part (o) of Figure D.3.4 and replace them with a single entry, eliminating one term in the control unit. Of course, we would have to renumber the existing states 8 and 9, perhaps as 4 and 7. The same optimization can be applied in an implementation that uses an explicit program counter, though we are more restricted. Because the next-state number is often computed by incrementing the current-state number, we cannot arbitrarily assign the states. However, if we keep the states where the incremented state is used as the next state in the same order, we can reassign the consecutive states as a block. In an implementation with an explicit next-state counter, state assignment may allow us to simplify the contents of the dispatch ROMs. If we look again at the control unit in Figure D.4.1, it looks remarkably like a computer in its own right. The ROM or PLA can be thought of as memory supplying instructions for the datapath. The state can be thought of as an instruction address. Hence the origin of the name microcode or microprogrammed control. The control words are thought of as microinstructions that control the datapath, and the State register is called the microprogram counter. Figure D.4.6 shows a view of the control unit as microcode. The next section describes how we map from a microprogram to microcode.

D-27

D-28

Appendix D

Mapping Control to Hardware

Control unit

Microcode memory

Outputs

Input

PCWrite PCWriteCond IorD MemRead MemWrite IRWrite BWrite MemtoReg PCSource ALUOp ALUSrcB ALUSrcA RegWrite RegDst AddrCtl

Datapath

1 Microprogram counter Adder

Op[5–0]

Address select logic

Instruction register opcode field FIGURE D.4.6 The control unit as a microcode. The use of the word “micro” serves to distinguish between the program counter in the datapath and the microprogram counter, and between the microcode memory and the instruction memory.

D.5

Translating a Microprogram to Hardware

To translate a microprogram into actual hardware, we need to specify how each field translates into control signals. We can implement a microprogram with either finite-state control or a microcode implementation with an explicit sequencer. If we choose a finite-state machine, we need to construct the next-state function from

D.5

Translating a Microprogram to Hardware

D-29

the microprogram. Once this function is known, we can map a set of truth table entries for the next-state outputs. In this section, we will show how to translate the microprogram, assuming that the next state is specified by a sequencer. From the truth tables we will construct, it would be straightforward to build the next-state function for a finite-state machine. Field name

ALU control

Value

Signals active

Comment

Add

ALUOp = 00

Cause the ALU to add.

Subt

ALUOp = 01

Cause the ALU to subtract; this implements the compare for branches.

Func code

ALUOp = 10

Use the instruction’s function code to determine ALU control.

PC

ALUSrcA = 0

Use the PC as the first ALU input.

A

ALUSrcA = 1

Register A is the first ALU input.

B

ALUSrcB = 00

Register B is the second ALU input.

4

ALUSrcB = 01

Use 4 as the second ALU input.

Extend

ALUSrcB = 10

Use output of the sign extension unit as the second ALU input.

Extshft

ALUSrcB = 11

Use the output of the shift-by-two unit as the second ALU input.

SRC1

SRC2

Read Write ALU

RegWrite, RegDst = 1, MemtoReg = 0

Write a register using the rd field of the IR as the register number and the contents of ALUOut as the data.

Write MDR

RegWrite, RegDst = 0, MemtoReg = 1

Write a register using the rt field of the IR as the register number and the contents of the MDR as the data.

Read PC

MemRead, IorD = 0, IRWrite

Read memory using the PC as address; write result into IR (and the MDR).

Read ALU

MemRead, IorD = 1

Read memory using ALUOut as address; write result into MDR.

Write ALU

MemWrite, IorD = 1

Write memory using the ALUOut as address, contents of B as the data.

ALU

PCSource = 00, PCWrite

Write the output of the ALU into the PC.

ALUOut-cond

PCSource = 01, PCWriteCond

If the Zero output of the ALU is active, write the PC with the contents of the register ALUOut.

Jump address

PCSource = 10, PCWrite

Write the PC with the jump address from the instruction.

Register control

Memory

PC write control

Read two registers using the rs and r t fields of the IR as the register numbers and putting the data into registers A and B.

Seq

AddrCtl = 11

Choose the next microinstruction sequentially.

Fetch

AddrCtl = 00

Go to the first microinstruction to begin a new instruction.

Dispatch 1

AddrCtl = 01

Dispatch using the ROM 1.

Dispatch 2

AddrCtl = 10

Dispatch using the ROM 2.

Sequencing

FIGURE D.5.1 Each microcode field translates to a set of control signals to be set. These 22 different values of the fields specify all the required combinations of the 18 control lines. Control lines that are not set, which correspond to actions, are 0 by default. Multiplexor control lines are set to 0 if the output matters. If a multiplexor control line is not explicitly set, its output is a don’t care and is not used.

D-30

Appendix D

Mapping Control to Hardware

Assuming an explicit sequencer, we need to do two additional tasks to translate the microprogram: assign addresses to the microinstructions and fill in the contents of the dispatch ROMs. This process is essentially the same as the process of translating an assembly language program into machine instructions: the fields of the assembly language or microprogram instruction are translated, and labels on the instructions must be resolved to addresses. Figure D.5.1 shows the various values for each microinstruction field that controls the datapath and how these fields are encoded as control signals. If the field corresponding to a signal that affects a unit with state (i.e., Memory, Memory register, ALU destination, or PCWriteControl) is blank, then no control signal should be active. If a field corresponding to a multiplexor control signal or the ALU operation control (i.e., ALUOp, SRC1, or SRC2) is blank, the output is unused, so the associated signals may be set as don’t care. The sequencing field can have four values: Fetch (meaning go to the Fetch state), Dispatch 1, Dispatch 2, and Seq. These four values are encoded to set the 2-bit address control just as they were in Figure D.4.4: Fetch ⫽ 0, Dispatch 1 ⫽ 1, Dispatch 2 ⫽ 2, Seq ⫽ 3. Finally, we need to specify the contents of the dispatch tables to relate the dispatch entries of the sequence field to the symbolic labels in the microprogram. We use the same dispatch tables as we did earlier in Figure D.4.3. A microcode assembler would use the encoding of the sequencing field, the contents of the symbolic dispatch tables in Figure D.5.2, the specification in Figure D.5.1, and the actual microprogram to generate the microinstructions. Since the microprogram is an abstract representation of the control, there is a great deal of flexibility in how the microprogram is translated. For example, the address assigned to many of the microinstructions can be chosen arbitrarily; the only restrictions are those imposed by the fact that certain microinstructions must dispatch table 1

Microcode dispatch table 2

Opcode field

Opcode name

Value

Opcode field

Opcode name

000000

R-format

Rformat1

100011

lw

Value LW2

000010

jmp

JUMP1

101011

sw

SW2

000100

beq

BEQ1

100011

lw

Mem1

101011

sw

Mem1

FIGURE D.5.2 The two microcode dispatch ROMs showing the contents in symbolic form and using the labels in the microprogram.

D.5

Translating a Microprogram to Hardware

occur in sequential order (so that incrementing the State register generates the address of the next instruction). Thus the microcode assembler may reduce the complexity of the control by assigning the microinstructions cleverly.

Organizing the Control to Reduce the Logic For a machine with complex control, there may be a great deal of logic in the control unit. The control ROM or PLA may be very costly. Although our simple implementation had only an 18-bit microinstruction (assuming an explicit sequencer), there have been machines with microinstructions that are hundreds of bits wide. Clearly, a designer would like to reduce the number of microinstructions and the width. The ideal approach to reducing control store is to first write the complete microprogram in a symbolic notation and then measure how control lines are set in each microinstruction. By taking measurements we are able to recognize control bits that can be encoded into a smaller field. For example, if no more than one of eight lines is set simultaneously in the same microinstruction, then this subset of control lines can be encoded into a 3-bit field (log2 8 ⫽ 3). This change saves five bits in every microinstruction and does not hurt CPI, though it does mean the extra hardware cost of a 3-to-8 decoder needed to generate the eight control lines when they are required at the datapath. It may also have some small clock cycle impact, since the decoder is in the signal path. However, shaving five bits off control store width will usually overcome the cost of the decoder, and the cycle time impact will probably be small or nonexistent. For example, this technique can be applied to bits 13–6 of the microinstructions in this machine, since only one of the seven bits of the control word is ever active (see Figure D.4.5). This technique of reducing field width is called encoding. To further save space, control lines may be encoded together if they are only occasionally set in the same microinstruction; two microinstructions instead of one are then required when both must be set. As long as this doesn’t happen in critical routines, the narrower microinstruction may justify a few extra words of control store. Microinstructions can be made narrower still if they are broken into different formats and given an opcode or format field to distinguish them. The format field gives all the unspecified control lines their default values, so as not to change anything else in the machine, and is similar to the opcode of an instruction in a more powerful instruction set. For example, we could use a different format for microinstructions that did memory accesses from those that did register-register ALU operations, taking advantage of the fact that the memory access control lines are not needed in microinstructions controlling ALU operations. Reducing hardware costs by using format fields usually has an additional performance cost beyond the requirement for more decoders. A microprogram using a single microinstruction format can specify any combination of operations in a datapath and can take fewer clock cycles than a microprogram made up of restricted microinstructions that cannot perform any combination of operations in

D-31

D-32

Appendix D

Mapping Control to Hardware

a single microinstruction. However, if the full capability of the wider microprogram word is not heavily used, then much of the control store will be wasted, and the machine could be made smaller and faster by restricting the microinstruction capability. The narrow, but usually longer, approach is often called vertical microcode, while the wide but short approach is called horizontal microcode. It should be noted that the terms “vertical microcode” and “horizontal microcode” have no universal definition—the designers of the 8086 considered its 21-bit microinstruction to be more horizontal than in other single-chip computers of the time. The related terms maximally encoded and minimally encoded are probably better than vertical and horizontal.

D.6

Concluding Remarks

We began this appendix by looking at how to translate a finite-state diagram to an implementation using a finite-state machine. We then looked at explicit sequencers that use a different technique for realizing the next-state function. Although large microprograms are often targeted at implementations using this explicit next-state approach, we can also implement a microprogram with a finite-state machine. As we saw, both ROM and PLA implementations of the logic functions are possible. The advantages of explicit versus encoded next state and ROM versus PLA implementation are summarized below.

BIG

The Picture

Independent of whether the control is represented as a finite-state diagram or as a microprogram, translation to a hardware control implementation is similar. Each state or microinstruction asserts a set of control outputs and specifies how to choose the next state. The next-state function may be implemented by either encoding it in a finite-state machine or using an explicit sequencer. The explicit sequencer is more efficient if the number of states is large and there are many sequences of consecutive states without branching. The control logic may be implemented with either ROMs or PLAs (or even a mix). PLAs are more efficient unless the control function is very dense. ROMs may be appropriate if the control is stored in a separate memory, as opposed to within the same chip as the datapath.

D.5

D.7

Exercises

Exercises

D.1 [10] ⬍§D.2⬎ Instead of using four state bits to implement the finite-state machine in Figure D.3.1, use nine state bits, each of which is a 1 only if the finitestate machine is in that particular state (e.g., S1 is 1 in state 1, S2 is 1 in state 2, etc.). Redraw the PLA (Figure D.3.9). D.2 [5] ⬍§D.3⬎ We wish to add the instruction jal (jump and link). Make any necessary changes to the datapath or to the control signals if needed. You can photocopy figures to make it faster to show the additions. How many product terms are required in a PLA that implements the control for the single-cycle datapath for jal? D.3 [5] ⬍§D.3⬎ Now we wish to add the instruction addi (add immediate). Add any necessary changes to the datapath and to the control signals. How many product terms are required in a PLA that implements the control for the singlecycle datapath for addiu? D.4 [10] ⬍§D.3⬎ Determine the number of product terms in a PLA that implements the finite-state machine for addi. The easiest way to do this is to construct the additions to the truth tables for addi. D.5 [20] ⬍§D.4⬎ Implement the finite-state machine of using an explicit counter to determine the next state. Fill in the new entries for the additions to Figure D.4.5. Also, add any entries needed to the dispatch ROMs of Figure D.5.2. D.6 [15] ⬍§§D.3–D.6⬎ Determine the size of the PLAs needed to implement the multicycle machine, assuming that the next-state function is implemented with a counter. Implement the dispatch tables of Figure D.5.2 using two PLAs and the contents of the main control unit in Figure D.4.5 using another PLA. How does the total size of this solution compare to the single PLA solution with the next state encoded? What if the main PLAs for both approaches are split into two separate PLAs by factoring out the next-state or address select signals?

D-33

E A

P

P

E

N

D

RISC: any computer announced after 1985.

I

X

A Survey of RISC Architectures for Desktop, Server and Embedded Computers Steven Przybylskic A Designer of the Stanford MIPS

Computer Organization and Design. DOI: http://dx.doi.org/10.1016/B978-0-12-407726-3.00001-1 © 2013 Elsevier Inc. All rights reserved.

E.1

Introduction E-3

E.2

Addressing Modes and Instruction Formats E-5

E.3

Instructions: The MIPS Core Subset E-9

E.4

Instructions: Multimedia Extensions of the Desktop/Server RISCs

E.5

E-16

Instructions: Digital Signal-Processing Extensions of the Embedded RISCs E-19

E.6

Instructions: Common Extensions to MIPS Core E-20

E.7

Instructions Unique to MIPS-64 E-25

E.8

Instructions Unique to Alpha E-27

E.9

Instructions Unique to SPARC v9 E-29

E.10

Instructions Unique to PowerPC E-32

E.11

Instructions Unique to PA-RISC 2.0 E-34

E.12

Instructions Unique to ARM E-36

E.13

Instructions Unique to Thumb E-38

E.14

Instructions Unique to SuperH E-39

E.15

Instructions Unique to M32R E-40

E.16

Instructions Unique to MIPS-16 E-40

E.17

Concluding Remarks E-43

E.1

Introduction

We cover two groups of reduced instruction set computer (RISC) architectures in this appendix. The first group is the desktop and server RISCs: ■

Digital Alpha



Hewlett-Packard PA-RISC



IBM and Motorola PowerPC



MIPS INC MIPS-64



Sun Microsystems SPARC

E-4

Appendix E A Survey of RISC Architectures

The second group is the embedded RISCs: ■

Advanced RISC Machines ARM



Advanced RISC Machines Thumb



Hitachi SuperH



Mitsubishi M32R



MIPS INC MIPS-16

Alpha

MIPS I

PA-RISC 1.1

PowerPC

SPARCv8

Date announced

1992

1986

1986

1993

1987

Instruction size (bits)

32

32

32

32

32

Address space (size, model)

64 bits, flat

32 bits, flat

48 bits, segmented

32 bits, flat

32 bits, flat

Data alignment

Aligned

Aligned

Aligned

Unaligned

Aligned

Data addressing modes

1

1

5

4

2

Protection

Page

Page

Page

Page

Page

Minimum page size

8 KB

4 KB

4 KB

4 KB

8 KB

I/O

Memor y mapped

Memor y mapped

Memor y mapped

Memor y mapped

Memor y mapped

Integer registers (number, model, size)

31 GPR × 64 bits 31 GPR × 32 bits

31 GPR × 32 bits 32 GPR × 32 bits

31 GPR × 32 bits

Separate floating-point registers

31 × 32 or 31 × 64 bits

16 × 32 or 16 × 64 bits

56 × 32 or 28 × 64 bits

32 × 32 or 32 × 64 bits

32 × 32 or 32 × 64 bits

Floating-point format

IEEE 754 single, double

IEEE 754 single, double

IEEE 754 single, double

IEEE 754 single, double

IEEE 754 single, double

FIGURE E.1.1 Summary of the first version of five architectures for desktops and servers. Except for the number of data address modes and some instruction set details, the integer instruction sets of these architectures are very similar. Contrast this with Figure E.17.1. Later versions of these architectures all support a flat, 64-bit address space.

ARM

Thumb

SuperH

M32R

MIPS-16

Date announced

1985

1995

1992

1997

1996

Instruction size (bits)

32

16

16

16/32

16/32

Address space (size, model)

32 bits, flat

32 bits, flat

32 bits, flat

32 bits, flat

32/64 bits, flat

Data alignment

Aligned

Aligned

Aligned

Aligned

Aligned

Data addressing modes

6

6

4

3

2

Integer registers (number, model, size)

15 GPR x 32 bits 8 GPR + SP, LR x 32 bits

16 GPR x 32 bits

16 GPR x 32 bits

8 GPR + SP, RA x 32/64 bits

I/O

Memor y mapped

Memor y mapped Memor y mapped

Memor y mapped

Memor y mapped

FIGURE E.1.2 Summary of five architectures for embedded applications. Except for number of data address modes and some instruction set details, the integer instruction sets of these architectures are similar. Con trast this with Figure E.17.1.

E.2

Addressing Modes and Instruction Formats

There has never been another class of computers so similar. This similarity allows the presentation of 10 architectures in about 50 pages. Characteristics of the desktop and server RISCs are found in Figure E.1.1 and the embedded RISCs in Figure E.1.2. Notice that the embedded RISCs tend to have 8 to 16 general-purpose registers while the desktop/server RISCs have 32, and that the length of instructions is 16 to 32 bits in embedded RISCs but always 32 bits in desktop/server RISCs. Although shown as separate embedded instruction set architectures, Thumb and MIPS-16 are really optional modes of ARM and MIPS invoked by call instructions. When in this mode, they execute a subset of the native architecture using 16-bit-long instructions. These 16-bit instruction sets are not intended to be full architectures, but they are enough to encode most procedures. Both machines expect procedures to be homogeneous, with all instructions in either 16-bit mode or 32-bit mode. Programs will consist of procedures in 16-bit mode for density or in 32-bit mode for performance. One complication of this description is that some of the older RISCs have been extended over the years. We have decided to describe the latest versions of the architectures: MIPS-64, Alpha version 3, PA-RISC 2.0, and SPARC version 9 for the desktop/server; ARM version 4, Thumb version 1, Hitachi SuperH SH-3, M32R version 1, and MIPS-16 version 1 for the embedded ones. The remaining sections proceed as follows: after discussing the addressing modes and instruction formats of our RISC architectures, we present the survey of the instructions in five steps: ■

Instructions found in the MIPS core, which is defined in Chapters 2 and 3 of the main text



Multimedia extensions of the desktop/server RISCs



Digital signal-processing extensions of the embedded RISCs



Instructions not found in the MIPS core but found in two or more architectures



The unique instructions and characteristics of each of the ten architectures

We give the evolution of the instruction sets in the final section and conclude with speculation about future directions for RISCs.

E.2

Addressing Modes and Instruction Formats

Figure E.2.1 shows the data addressing modes supported by the desktop architectures. Since all have one register that always has the value 0 when used in address modes, the absolute address mode with limited range can be synthesized using zero as the base in displacement addressing. (This register can be changed

E-5

E-6

Appendix E A Survey of RISC Architectures

by ALU operations in PowerPC; it is always 0 in the other machines.) Similarly, register indirect addressing is synthesized by using displacement addressing with an offset of 0. Simplified addressing modes is one distinguishing feature of RISC architectures. Figure E.2.2 shows the data addressing modes supported by the embedded architectures. Unlike the desktop RISCs, these embedded machines do not reserve a register to contain 0. Although most have two to three simple addressing modes, ARM and SuperH have several, including fairly complex calculations. ARM has an addressing mode that can shift one register by any amount, add it to the other registers to form the address, and then update one register with this new address. References to code are normally PC-relative, although jump register indirect is supported for returning from procedures, for case statements, and for pointer function calls. One variation is that PC-relative branch addresses are shifted left two bits before being added to the PC for the desktop RISCs, thereby increasing the branch distance. This works because the length of all instructions for the desktop RISCs is 32 bits, and instructions must be aligned on 32-bit words in memory. Embedded architectures with 16-bit-long instructions usually shift the PC-relative address by 1 for similar reasons. Addressing mode

Alpha

MIPS-64

PA-RISC 2.0

PowerPC

SPARCv9

X

X

X

X

X

X (FP)

X (Loads)

X

X

Register + offset (displacement or based) Register + register (indexed) Register + scaled register (scaled)

X

Register + offset and update register

X

X

Register + register and update register

X

X

FIGURE E.2.1 Summary of data addressing modes supported by the desktop architectures. PA RISC also has short address versions of the offset addressing modes. MIPS-64 has indexed addressing for floating-point loads and stores. (These addressing modes are described in Figure 2.18.)

Addressing mode

ARMv4

Register + offset (displacement or based)

X

X

X

X

X

Register + register (indexed)

X

Register + scaled register (scaled)

X

Register + offset and update register

X

Register + register and update register

X

Thumb

Register indirect

SuperH

M32R X

X

X

Autoincrement, autodecrement

X

X

X

X

PC-relative data

X

X (loads)

X

MIPS-16 X

X (loads)

FIGURE E.2.2 Summary of data addressing modes supported by the embedded architectures. SuperH and M32R have separate register indirect and register ⫹ offset addressing modes rather than just putting 0 in the offset of the latter mode. This increases the use of 16-bit instructions in the M32R, and it gives a wider set of address modes to different data transfer instructions in SuperH. To get greater addressing range, ARM and Thumb shift the offset left one or two bits if the data size is halfword or word. (These addressing modes are described in Figure 2.18.)

E.2

Addressing Modes and Instruction Formats

Figure E.2.3 shows the format of the desktop RISC instructions, which include the size of the address. Each instruction set architecture uses these four primary instruction formats. Figure E.2.4 shows the six formats for the embedded RISC machines. The desire to have smaller code size via 16-bit instructions leads to more instruction formats. 31

Register-register

25

20

15

10

Alpha

Op6

Rs15

MIPS

Op6

Rs15

Rs25

Rd5

PowerPC

Op6

Rd5

Rs15

Rs25

Op6

Rs15

Rs25

PA-RISC Op2

SPARC

Rd5

Rs25

Opx6

31 29 31

18

25

Const5

15 Rs15

Const16

MIPS

Op6

Rs15

Rd5

Const16

Register-immediate PowerPC

Op6

Rd5

Rs15

Const16

PA-RISC

Op6

Rs25

Rd5

Const16

SPARC

31 29 31

Branch

Rs15

24 25

18 20

MIPS

Op6

Rs15

Opx5/Rs25

PowerPC

Op6

Opx6

Rs15

PA-RISC

Op6

Rs25

Rs15

Op2

0

Rs15

Const21 Const16 Const14 Opx3

Const11

Opx11

31

18 25

Opx2

OC

Const19

31 29

Jump/call

0

15

Op6

SPARC

Const13

1 13 12

Alpha

0 0

Rd5

Opx6

Rs25 4

Op6

Rd5

Rd5

Opx8

0

Alpha

Op2

Opx6

Opx11

13 12

20

0 Rd5

Opx11

Rs15

24

4

Opx11

12

20

1 0 0

Alpha

Op6

MIPS

Op6

PowerPC

Op6

Const24

Opx2

PA-RISC

Op6

Const21

O1 C1

SPARC

Rs15

Const21 Const26

Op2

Const30

31 29

20

Opcode

Register

15

12

1 0

Constant

FIGURE E.2.3 Instruction formats for desktop/server RISC architectures. These four formats are found in all five architectures. (The superscrift notation in this figure means the width of a field in bits.) Although the register fields are located in similar pieces of the instruction, be aware that the destination and two source fields are scrambled. Op ⫽ the main opcode, Opx ⫽ an opcode extension, Rd ⫽ the destination register, Rs1 ⫽ source register 1, Rs2 ⫽ source register 2, and Const ⫽ a constant (used as an immediate or as an address). Unlike the other RISCs, Alpha has a format for immediates in arithmetic and logical operations that is different from the data transfer format shown here. It provides an 8-bit immediate in bits 20 to 13 of the RR format, with bits 12 to 5 remaining as an opcode extension.

E-7

E-8

Appendix E A Survey of RISC Architectures

31

Rs14

Opx4

M32R

Op4

Rd4

Opx4

Rs4

Op5 10

31

M32R

Rd4

Op4

Rd4

15

Rs4

7

4

15 Rs14

Const5

Rs3

Rd4

Rs4

M32R

Op4

Rd4

Opx4

Rs4

Rs3

Const5

Rd3 10

31

7

27

ARM

Opx4

Thumb

Op4

Const8 Const8

Rd3

Op4

M32R

Op4

Const11 Opx4

Const8 Const11

10

31

0

27 Opx4

23 Const24 Const11

Op4

SuperH

0

Op4

Op5

Thumb

0 Const24

Const12

15

ARM

0

23

Op5

MIPS-16

15

Const11

Const24

Op6

MIPS-16

Opx5

Const12 Op8

M32R

Const16

Op4

Op5

SuperH

Rs4

Const8 7

27 Opx4

Thumb

Opx4

Rd4 10

31

0 Const24

Opx4

15

ARM

0

23

Op5

MIPS-16

Const16

Op4

Op4

M32R

0 Const12

Const4

4

Op8

SuperH

11 Rd4

Rd3

Op4

15

Const16 0

19

Op5

0 Const12

Const5

SuperH MIPS-16

Call

Rs3

Op3

Op5

11 Rd4

Const8 Opx4

27 Opx4

Thumb

15

Const8

Rd3 10

31 ARM

Opx2

Rs14

Rd3

Op4

0

Rs24

1 0

19

Op5

MIPS-16

4

Op3

Op5

Thumb

7

27 Opx4

Register-immediate SuperH

Rs13 Rs23

Rd3

3 Opx8

Rs3 Rd3

Rd4

ARM

Jump

Opx4

11 Rd4

Op4

15

Branch

15 Rs14

SuperH MIPS-16

Data transfer

19 Opx4

Op6

Thumb Register-register

27 Opx4

ARM

Const26 25 Opcode

0 Register

Constant

FIGURE E.2.4 Instruction formats for embedded RISC architectures. These six formats are found in all five architectures. The notation is the same as in Figure E.2.3. Note the similarities in branch, jump, and call formats, and the diversity in register-register, register-immediate, and data transfer formats. The differences result from whether the architecture has 8 or 16 registers, whether it is a 2- or 3-operand format, and whether the instruction length is 16 or 32 bits.

E.3

Format: instruction category

E-9

Instructions: the MIPS Core Subset

Alpha

MIPS-64

PA-RISC 2.0

PowerPC

SPARCv9 Sign

Branch: all

Sign

Sign

Sign

Sign

Jump/call: all

Sign



Sign

Sign

Sign

Register-immediate: data transfer

Sign

Sign

Sign

Sign

Sign

Register-immediate: arithmetic

Zero

Sign

Sign

Sign

Sign

Register-immediate: logical

Zero

Zero



Zero

Sign

FIGURE E.2.5 Summary of constant extension for desktop RISCs. The constants in the jump and call instructions of MIPS are not sign-extended, since they only replace the lower 28 bits of PC, leaving the upper 4 bits unchanged. PA-RISC has no logical immediate instructions.

Format: instruction category

Armv4

Thumb

SuperH

M32R

MIPS-16

Branch: all

Sign

Sign

Sign

Sign

Jump/call: all

Sign

Sign/Zero

Sign

Sign

Sign —

Register-immediate: data transfer

Zero

Zero

Zero

Sign

Zero

Register-immediate: arithmetic

Zero

Zero

Sign

Sign

Zero/Sign

Register-immediate: logical

Zero



Zero

Zero



FIGURE E.2.6 Summary of constant extension for embedded RISCs. The 16-bit-length instructions have much shorter immediates than those of the desktop RISCs, typically only five to eight bits. Most embedded RISCs, however, have a way to get a long address for procedure calls from two sequencial halfwords. The constants in the jump and call instructions of MIPS are not sign-extended, since they only replace the lower 28 bits of the PC, leaving the upper 4 bits unchanged. The 8-bit immediates in ARM can be rotated right an even number of bits between 2 and 30, yielding a large range of immediate values. For example, all powers of two are immediates in ARM.

Figures E.2.5 and E.2.6 show the variations in extending constant fields to the full width of the registers. In this subtle point, the RISCs are similar but not identical.

E.3

Instructions: the MIPS Core Subset

The similarities of each architecture allow simultaneous descriptions, starting with the operations equivalent to the MIPS core.

MIPS Core Instructions Almost every instruction found in the MIPS core is found in the other architectures, as Figures E.3.1 through E.3.5 show. (For reference, definitions of the MIPS instructions are found in the MIPS Reference Data Card at the beginning of the book.) Instructions are listed under four categories: data transfer (Figure E.3.1); arithmetic/logical (Figure E.3.2); control (Figure E.3.3); and floating point (Figure E.3.4). A fifth category (Figure E.3.5) shows conventions for register

E-10

Appendix E A Survey of RISC Architectures

Data transfer (instruction formats)

R-I

R-I

R-I, R-R

R-I, R-R

R-I, R-R

Instruction name

Alpha

MIPS-64

PA-RISC 2.0

PowerPC

SPARCv9

Load byte signed

LDBU; SEXTB LB

LDB; EXTRW,S 31,8

LBZ; EXTSB

LDSB

Load byte unsigned

LDBU

LDB, LDBX, LDBS

LBZ

LDUB

Load halfword signed

LDWU; SEXTW LH

LDH; EXTRW,S 31,16 LHA

LDSH

Load halfword unsigned

LDWU

LHU

LDH, LDHX, LDHS

LHZ

LDUH

Load word

LDLS

LW

LDW, LDWX, LDWS

LW

LD

Load SP float

LDS*

LWC1

FLDWX, FLDWS

LFS

LDF

Load DP float

LDT

LDC1

FLDDX, FLDDS

LFD

LDDF

Store byte

STB

SB

STB, STBX, STBS

STB

STB

Store halfword

STW

SH

STH, STHX, STHS

STH

STH

Store word

STL

SW

STW, STWX, STWS

STW

ST

Store SP float

STS

SWC1

FSTWX, FSTWS

STFS

STF

Store DP float

STT

SDC1

FSTDX, FSTDS

STFD

STDF

Read, write special registers

MF_, MT_

MF, MT_

MFCTL, MTCTL

MFSPR, MF_, RD, WR, RDPR, WRPR, MTSPR, MT_ LDXFSR, STXFSR

Move integer to FP register

ITOFS

MFC1/DMFC1 STW; FLDWX

STW; LDFS

ST; LDF

Move FP to integer register

FTTOIS

MTC1/DMTC1 FSTWX; LDW

STFS; LW

STF; LD

LBU

FIGURE E.3.1 Desktop RISC data transfer instructions equivalent to MIPS core. A sequence of instructions to synthesize a MIPS instruction is shown separated by semicolons. If there are several choices of instructions equivalent to MIPS core, they are separated by commas. For this figure, halfword is 16 bits and word is 32 bits. Note that in Alpha, LDS converts single precision floating point to double precision and loads the entire 64-bit register.

usage and pseudoinstructions on each architecture. If a MIPS core instruction requires a short sequence of instructions in other architectures, these instructions are separated by semicolons in Figures E.3.1 through E.3.5. (To avoid confusion, the destination register will always be the leftmost operand in this appendix, independent of the notation normally used with each architecture.) Figures E.3.6 through E.3.9 show the equivalent listing for embedded RISCs. Note that floating point is generally not defined for the embedded RISCs. Every architecture must have a scheme for compare and conditional branch, but despite all the similarities, each of these architectures has found a different way to perform the operation.

Compare and Conditional Branch SPARC uses the traditional four condition code bits stored in the program status word: negative, zero, carry, and overflow. They can be set on any arithmetic or logical instruction; unlike earlier architectures, this setting is optional on each instruction. An explicit option leads to fewer problems in pipelined implementation. Although condition codes can be set as a side effect of an operation, explicit compares are synthesized with a subtract using r0 as the destination. SPARC conditional branches

E.3

E-11

Instructions: the MIPS Core Subset

Arithmetic/logical (instruction formats)

R-R, R-I

R-R, R-I

R-R, R-I

R-R, R-I

R-R, R-I

Instruction name

Alpha

MIPS-64

PA-RISC 2.0

PowerPC

SPARCv9

Add

ADDL

ADDU, ADDU

Add (trap if overflow)

ADDLV

ADD, ADDI

ADDL, LD0, ADDI, UADDCM ADDO, ADDIO

Sub

SUBL

SUBU

SUB, SUBI

SUBF

SUB

Sub (trap if overflow)

SUBLV

SUB

SUBTO, SUBIO

SUBF/oe

SUBcc; TVS

Multiply

MULL

MULT, MULTU

SHiADD;...; (i=1,2,3)

MULLW, MULLI

MULX

Multiply (trap if overflow)

MULLV



SHiADDO;...;





Divide



DIV, DIVU

DS;...; DS

DIVW

DIVX

Divide (trap if overflow)











And

AND

AND, ANDI

AND

AND, ANDI

AND

Or

BIS

OR, ORI

OR

OR, ORI

OR

Xor

XOR

XOR, XORI

XOR

XOR, XORI

XOR

Load high part register

LDAH

LUI

LDIL

ADDIS

Shift left logical

SLL SRL SRA CMPEQ, CMPLT, CMPLE

SLLV, SLL SRLV, SRL SRAV, SRA SLT/U, SLTI/U

DEPW, Z 31-i,32-i EXTRW, U 31, 32-i EXTRW, S 31, 32-i COMB

RLWINM RLWINM 32-i SRAW CMP(I)CLR

SETHI (B fmt.) SLL SRL SRA SUBcc r0,...

Shift right logical Shift right arithmetic Compare

ADD, ADDI

ADD

ADDO; MCRXR; BC

ADDcc; TVS

FIGURE E.3.2 Desktop RISC arithmetic/logical instructions equivalent to MIPS core. Dashes mean the operation is not available in that architecture, or not synthesized in a few instructions. Such a sequence of instructions is shown separated by semicolons. If there are several choices of instructions equivalent to MIPS core, they are separated by commas. Note that in the “Arithmetic/logical” category, all machines but SPARC use separate instruction mnemonics to indicate an immediate operand; SPARC offers immediate versions of these instructions but uses a single mnemonic. (Of course these are separate opcodes!)

Control (instruction formats)

B, J/C

B, J/C

B, J/C

B, J/C

B, J/C

Instruction name

Alpha

MIPS-64

PA-RISC 2.0

PowerPC

SPARCv9

Branch on integer compare

B_ (<, >, <=, >=, =, not=)

BEQ, BNE, B_Z COMB, COMIB (<, >, <=, >=)

BC

Branch on floating-point compare

FB_(<, >, <=, >=, =, not=)

BC1T, BC1F

FSTWX f0; LDW t; BB t

BC

Jump, jump register

BR, JMP BSR

J, JR JAL, JALR

BL r0, BLR r0 BL, BLE

CALL_PAL GENTRAP CALL_PAL REI

BREAK

BREAK

B, BCLR, BCCTR BA, JMPL r0,... BL, BLA, CALL, JMPL BCLRL, BCCTRL TW, TWI Ticc, SIR

JR; ERET

RFI, RFIR

RFI

Call, call register Trap Return from interrupt

BR_Z, BPcc (<, >, <=, >=, =, not=) FBPfcc (<, >, <=, >=, =,...)

DONE, RETRY, RETURN

FIGURE E.3.3 Desktop RISC control instructions equivalent to MIPS core. If there are several choices of instructions equivalent to MIPS core, they are separated by commas.

E-12

Appendix E A Survey of RISC Architectures

Floating point (instruction formats)

R-R

R-R

R-R

R-R

R-R

Instruction name

Alpha

MIPS-64

PA-RISC 2.0

PowerPC

SPARCv9

ADDS, ADDT SUBS, SUBT MULS, MULT DIVS, DIVT CMPT_ (=, <, <=, UN)

ADD.S, ADD.D SUB.S, SUB.D MUL.S, MUL.D DIV.S, DIV.D C_.S, C_.D (<, >, <=, >=, =,...)

FADD FADD/dbl FSUB FSUB/dbl FMPY FMPY/dbl FDIV, FDIV/dbl FCMP, FCMP/dbl (<, =, >)

FADDS, FSUBS, FMULS, FDIVS, FCMP

ADDT Fd, F31, Fs CVTST, CVTTS, CVTTQ, CVTQS, CVTQT

MOV.S, MOV.D CVT.S.D, CVT. D.S, CVT.S.W, CVT.D.W, CVT. W.S, CVT.W.D

FCPY FCNVFF,s,d FCNVFF,d,s FCNVXF,s,s FCNVXF,d,d FCNVFX,s,s FCNVFX,d,s

FMV FMOVS/D/Q —, FRSP, —, FSTOD, FDTOS, FCTIW,—, — FSTOI, FDTOI, FITOS, FITOD

Add single, double Subtract single, double Multiply single, double Divide single, double Compare

Move R-R Convert (single, double, integer) to (single, double, integer)

FADD FSUB FMUL FDIV

FADDS, FADDD FSUBS, FSUBD FMULS, FMULD FDIVS, FDIVD FCMPS, FCMPD

FIGURE E.3.4 Desktop RISC floating-point instructions equivalent to MIPS core. Dashes mean the operation is not available in that architecture, or not synthesized in a few instructions. If there are several choices of instructions equivalent to MIPS core, they are separated by commas.

Conventions Register with value 0 Return address register No-op Move R-R integer Operand order

Alpha r31 (source) (any)

MIPS-64 r0 r31

LDQ_U r31,... SLL r0, r0, r0 BIS..., r31,... ADD..., r0,... OP Rs1, Rs2, Rd OP Rd, Rs1, Rs2

PA-RISC 2.0 r0 r2, r31

PowerPC

SPARCv9

r0 (addressing) r0 link (special) r31

OR r0, r0, r0 ORI r0, r0, #0 OR..., r0,... OR rx, ry, ry OP Rs1, Rs2, Rd OP Rd, Rs1, Rs2

SETHI r0, 0 OR..., r0,... OP Rs1, Rs2, Rd

FIGURE E.3.5 Conventions of desktop RISC architectures equivalent to MIPS core.

test condition codes to determine all possible unsigned and signed relations. Floating point uses separate condition codes to encode the IEEE 754 conditions, requiring a floating-point compare instruction. Version 9 expanded SPARC branches in four ways: a separate set of condition codes for 64-bit operations; a branch that tests the contents of a register and branches if the value is ⫽, not⫽, ⬍, ⬍⫽, ⬎⫽, or ⬍⫽ 0 (see MIPS below); three more sets of floating-point condition codes; and branch instructions that encode static branch prediction. PowerPC also uses four condition codes—less than, greater than, equal, and summary overflow—but it has eight copies of them. This redundancy allows the PowerPC instructions to use different condition codes without conflict, essentially giving PowerPC eight extra 4-bit registers. Any of these eight condition codes can be the target of a compare instruction, and any can be the source of a conditional branch. The integer instructions have an option bit that behaves as if the integer op

E.3

E-13

Instructions: the MIPS Core Subset

Instruction name

ARMv4

Thumb

SuperH

M32R

MIPS-16

Data transfer (instruction formats)

DT

DT

DT

DT

DT

LDRSB LDRB LDRSH LDRH LDR STRB STRH STR MRS, MSR

LDRSB LDRB LDRSH LDRH LDR STRB STRH STR —1

MOV.B MOV.B; EXTU.B MOV.W MOV.W; EXTU.W MOV.L MOV.B MOV.W MOV.L LDC, STC

LDB LDUB LDH LDUH LD STB STH ST MVFC, MVTC

LB LBU LH LHU LW SB SH SW MOVE

Load byte signed Load byte unsigned Load halfword signed Load halfword unsigned Load word Store byte Store halfword Store word Read, write special registers

FIGURE E.3.6 Embedded RISC data transfer instructions equivalent to MIPS core. A sequence of instructions to synthesize a MIPS instruction is shown separated by semicolons. Note that floating point is generally not defined for the embedded RISCs. Thumb and MIPS-16 are just 16-bit instruction subsets of the ARM and MIPS architectures, so machines can switch modes and execute the full instruction set. We use —1 to show sequences that are available in 32-bit mode but not 16-bit mode in Thumb or MIPS-16.

is followed by a compare to zero that sets the first condition “register.” PowerPC also lets the second “register” be optionally set by floating-point instructions. PowerPC provides logical operations among these eight 4-bit condition code registers (CRAND, CROR, CRXOR, CRNAND, CRNOR, CREQV), allowing more complex conditions to be tested by a single branch. MIPS uses the contents of registers to evaluate conditional branches. Any two registers can be compared for equality (BEQ) or inequality (BNE), and then the branch is taken if the condition holds. The set on less than instructions (SLT, SLTI, SLTU, SLTIU) compare two operands and then set the destination register to 1 if less and to 0 otherwise. These instructions are enough to synthesize the full set of relations. Because of the popularity of comparisons to 0, MIPS includes special compare and branch instructions for all such comparisons: greater than or equal to zero (BGEZ), greater than zero (BGTZ), less than or equal to zero (BLEZ), and less than zero (BLTZ). Of course, equal and not equal to zero can be synthesized using r0 with BEQ and BNE. Like SPARC, MIPS I uses a condition code for floating point with separate floating-point compare and branch instructions; MIPS IV expanded this to eight floating-point condition codes, with the floating point comparisons and branch instructions specifying the condition to set or test. Alpha compares (CMPEQ, CMPLT, CMPLE, CMPULT, CMPULE) test two registers and set a third to 1 if the condition is true and to 0 otherwise. Floating-point compares (CMTEQ, CMTLT, CMTLE, CMTUN) set the result to 2.0 if the condition holds and to 0 otherwise. The branch instructions compare one register to 0 (BEQ, BGE, BGT, BLE, BLT, BNE) or its least significant bit to 0 (BLBC, BLBS) and then branch if the condition holds.

E-14

Appendix E A Survey of RISC Architectures

Arithmetic/logical (instruction formats)

R-R, R-I

R-R, R-I

R-R, R-I

R-R, R-I

R-R, R-I

Instruction name

ARMv4

Thumb

SuperH

M32R

MIPS-16

Add

ADD

ADD

ADD

ADD, ADDI, ADD3

ADDU, ADDIU

Add (trap if overflow)

ADDS; SWIVS

ADD; BVC .+4; SWI

ADDV

ADDV, ADDV3

—1

Subtract

SUB

SUB

SUB

SUB

SUBU

Subtract (trap if overflow)

SUBS; SWIVS

SUB; BVC .+1; SWI

SUBV

SUBV

—1

Multiply

MUL

MUL

MUL

MUL

Multiply (trap if overflow) Divide

MULT, MULTU —

DIV1, DIVoS, DIV, DIVU DIVoU

DIV, DIVU

AND

AND

AND, AND3

AND

ORR

OR

OR, OR3

OR

XOR





Divide (trap if overflow)





And

AND

Or

ORR



Xor

EOR

EOR

Load high part register





Shift left logical

LSL3

LSL2

3

2

XOR, XOR3

XOR

SETH

—1

SHLL, SHLLn

SLL, SLLI, SLL3

SLLV, SLL SRLV, SRL

Shift right logical

LSR

LSR

SHRL, SHRLn

SRL, SRLI, SRL3

Shift right arithmetic

ASR3

ASR2

SHRA, SHAD

SRA, SRAI, SRA3

SRAV, SRA

Compare

CMP,CMN, TST,TEQ

CMP, CMN, TST

CMP/cond, TST

CMP/I, CMPU/I

CMP/I2, SLT/I, SLT/IU

FIGURE E.3.7 Embedded RISC arithmetic/logical instructions equivalent to MIPS core. Dashes mean the operation is not available in that architecture, or not synthesized in a few instructions. Such a sequence of instructions is shown separated by semicolons. If there are several choices of instructions equivalent to MIPS core, they are separated by commas. Thumb and MIPS-16 are just 16-bit instruction subsets of the ARM and MIPS architectures, so machines can switch modes and execute the full instruction set. We use —1 to show sequences that are available in 32-bit mode but not 16-bit mode in Thumb or MIPS-16. The superscript 2 shows new instructions found only in 16-bit mode of Thumb or MIPS-16, such as CMP/I2. ARM includes shifts as part of every data operation instruction, so the shifts with superscript 3 are just a variation of a move instruction, such as LSR3 .

PA-RISC has many branch options, which we’ll see in Section E.11. The most straightforward is a compare and branch instruction (COMB), which compares two registers, branches depending on the standard relations, and then tests the least significant bit of the result of the comparison. ARM is similar to SPARC, in that it provides four traditional condition codes that are optionally set. CMP subtracts one operand from the other and the difference sets the condition codes. Compare negative (CMN) adds one operand to the other, and the sum sets the condition codes. TST performs logical AND on the two operands to set all condition codes but overflow, while TEQ uses exclusive OR to set the first three condition codes. Like SPARC, the conditional version of the ARM branch instruction tests condition codes to determine all possible unsigned and signed relations.

E.3

Control (instruction formats)

B, J, C

Instruction name

B, J, C

ARMv4

E-15

Instructions: the MIPS Core Subset

Thumb

B, J, C SuperH

B, J, C

B, J, C

M32R

MIPS-16

Branch on integer compare

B/cond

B/cond

BF, BT

BEQ, BNE, BC, BNC, B__Z BEQZ2, BNEZ2, BTEQZ2, BTNEZ2

Jump, jump register

MOV pc, ri

MOV pc, ri

BRA, JMP

BRA, JMP

B2, JR

Call, call register

BL

BL

BSR, JSR

BL, JL

JAL, JALR, JALX2

Trap

SWI

SWI

TRAPA

TRAP

BREAK

Return from interrupt

MOVS pc, r14

RTS

RTE

—1

1



FIGURE E.3.8 Embedded RISC control instructions equivalent to MIPS core. Thumb and MIPS-16 are just 16-bit instruction subsets of the ARM and MIPS architectures, so machines can switch modes and execute the full instruction set. We use —1 to show sequences that are available in 32-bit mode but not 16-bit mode in Thumb or MIPS-16. The superscript 2 shows new instructions found only in 16-bit mode of Thumb or MIPS-16, such as BTEQZ2. Conventions

ARMv4

Thumb

SuperH

Return address reg.

R14

R14

PR (special)

No-op

MOV r0, r0

MOV r0, r0

Operands, order

OP Rd, Rs1, Rs2

OP Rd, Rs1

M32R

MIPS-16

R14

RA (special)

NOP

NOP

SLL r0, r0

OP Rs1, Rd

OP Rd, Rs1

OP Rd, Rs1, Rs2

FIGURE E.3.9 Conventions of embedded RISC instructions equivalent to MIPS core.

As we shall see in Section E.12, one unusual feature of ARM is that every instruction has the option of executing conditionally depending on the condition codes. (This bears similarities to the annulling option of PA-RISC, seen in Section E.11.) Not surprisingly, Thumb follows ARM. The differences are that setting condition codes are not optional, the TEQ instruction is dropped, and there is no conditional execution of instructions. The Hitachi SuperH uses a single T-bit condition that is set by compare instructions. Two branch instructions decide to branch if either the T bit is 1 (BT) or the T bit is 0 (BF). The two flavors of branches allow fewer comparison instructions. Mitsubishi M32R also offers a single condition code bit (C) used for signed and unsigned comparisons (CMP, CMPI, CMPU, CMPUI) to see if one register is less than the other or not, similar to the MIPS set on less than instructions. Two branch instructions test to see if the C bit is 1 or 0: BC and BNC. The M32R also includes instructions to branch on equality or inequality of registers (BEQ and BNE) and all relations of a register to 0 (BGEZ, BGTZ, BLEZ, BLTZ, BEQZ, BNEZ). Unlike BC and BNC, these last instructions are all 32 bits wide. MIPS-16 keeps set on less than instructions (SLT, SLTI, SLTU, SLTIU), but instead of putting the result in one of the eight registers, it is placed in a special register named T. MIPS-16 is always implemented in machines that also have the full 32-bit MIPS instructions and registers; hence, register T is really register 24 in the full MIPS architecture. The MIPS-16 branch instructions test to see if a register is or is not equal to zero (BEQZ and BNEZ). There are also instructions that branch

E-16

Appendix E A Survey of RISC Architectures

Alpha

MIPS-64

PA-RISC 2.0

PowerPC

SPARCv9

Number of condition code bits 0 (integer and FP)

8 FP

8 FP

8 × 4 both

2 × 4 integer, 4 × 2 FP

Basic compare instructions (integer and FP)

1 integer, 1 FP

1 integer, 1 FP

4 integer, 2 FP

4 integer, 2 FP

1 FP

Basic branch instructions (integer and FP)

1

2 integer, 1 FP

7 integer

1 both

3 integer, 1 FP

Compare register with register/const and branch



=, not=

=, not=, <, <=, >, >=, even, odd





Compare register to zero and branch

=, not=, <, <=, >, >=, even, odd

=, not=, <, <=, >, >=

=, not=, <, <=, >, >=, even, odd



=, not=, <, <=, >, >=

FIGURE E.3.10 Summary of five desktop RISC approaches to conditional branches. Floating-point branch on PA-RISC is accomplished by copying the FP status register into an integer register and then using the branch on bit instruction to test the FP comparison bit. Integer compare on SPARC is synthesized with an arithmetic instruction that sets the condition codes using r0 as the destination.

ARMv4

Thumb

SuperH

M32R

MIPS-16

Number of condition code bits

4

4

1

1

1

Basic compare instructions

4

3

2

2

2

Basic branch instructions

1

1

2

3

2

Compare register with register/const and branch





=, >, >=

=, not=



Compare register to zero and branch





=, >, >=

=, not=, <, <=, >, >=

=, not=

FIGURE E.3.11 Summary of five embedded RISC approaches to conditional branches

if register T is or is not equal to zero (BTEQZ and BTNEZ). To test if two registers are equal, MIPS added compare instructions (CMP, CMPI) that compute the exclusive OR of two registers and place the result in register T. Compare was added since MIPS-16 left out instructions to compare and branch if registers are equal or not (BEQ and BNE). Figures E.3.10 and E.3.11 summarize the schemes used for conditional branches.

E.4

Instructions: Multimedia Extensions of the Desktop/Server RISCs

Since every desktop microprocessor by definition has its own graphical displays, as transistor budgets increased it was inevitable that support would be added for graphics operations. Many graphics systems use eight bits to represent each of the three primary colors plus eight bits for the location of a pixel.

E.4

Instructions: Multimedia Extensions of the Desktop/Server RISCs

The addition of speakers and microphones for teleconferencing and video games suggested support of sound as well. Audio samples need more than eight bits of precision, but 16 bits are sufficient. Every microprocessor has special support so that bytes and halfwords take up less space when stored in memory, but due to the infrequency of arithmetic operations on these data sizes in typical integer programs, there is little support beyond data transfers. The architects of the Intel i860, which was justified as a graphical accelerator within the company, recognized that many graphics and audio applications would perform the same operation on vectors of this data. Although a vector unit was beyond the transistor budget of the i860 in 1989, by partitioning the carry chains within a 64-bit ALU, it could perform simultaneous operations on short vectors of eight 8-bit operands, four 16-bit operands, or two 32-bit operands. The cost of such partitioned ALUs was small. Applications that lend themselves to such support include MPEG (video), games like DOOM (3-D graphics), Adobe Photoshop (digital photography), and teleconferencing (audio and image processing). Like a virus, over time such multimedia support has spread to nearly every desktop microprocessor. HP was the first successful desktop RISC to include such support. As we shall see, this virus spread unevenly. The PowerPC is the only holdout, and rumors are that it is “running a fever.” These extensions have been called subword parallelism, vector, or SIMD (singleinstruction, multiple data) (see Chapter 6). Since Intel marketing uses SIMD to describe the MMX extension of the 8086, that has become the popular name. Figure E.4.1 summarizes the support by architecture. From Figure E.4.1, you can see that in general, MIPS MDMX works on eight bytes or four halfwords per instruction, HP PA-RISC MAX2 works on four halfwords, SPARC VIS works on four halfwords or two words, and Alpha doesn’t do much. The Alpha MAX operations are just byte versions of compare, min, max, and absolute difference, leaving it up to software to isolate fields and perform parallel adds, subtracts, and multiplies on bytes and halfwords. MIPS also added operations to work on two 32-bit floating-point operands per cycle, but they are considered part of MIPS V and not simply multimedia extensions (see Section E.7). One feature not generally found in general-purpose microprocessors is saturating operations. Saturation means that when a calculation overflows, the result is set to the largest positive number or most negative number, rather than a modulo calculation as in two’s complement arithmetic. Commonly found in digital signal processors (see the next section), these saturating operations are helpful in routines for filtering. These machines largely used existing register sets to hold operands: integer registers for Alpha and HP PA-RISC and floating-point registers for MIPS and Sun. Hence data transfers are accomplished with standard load and store instructions. MIPS also added a 192-bit (3*64) wide register to act as an accumulator for some operations. By having three times the native data width, it can be partitioned to accumulate either eight bytes with 24 bits per field or four halfwords with 48 bits

E-17

E-18

Instruction category

Appendix E A Survey of RISC Architectures

Alpha MAX

Add/subtract

MIPS MDMX

PA-RISC MAX2

8B, 4H

4H

Saturating add/sub

8B, 4H

4H

Multiply

8B, 4H

Compare

8B (>=)

Shift right/left

SPARC VIS 4H, 2W

4B/H 4H, 2W (=, not=, >, <=)

8B, 4H (=,<,<=) 8B, 4H

4H

Shift right arithmetic

4H

4H

Multiply and add

8B, 4H

Shift and add (saturating)

PowerPC

4H

And/or/xor

8B, 4H, 2W

Absolute difference

8B

Max/min

8B, 4W

Pack (2n bits --> n bits)

2W->2B, 4H->4B 2*2W->4H, 2*4H->8B 2*4H->8B

2W->2H, 2W->2B, 4H->4B

8B, 4H, 2W

8B, 4H, 2W

Unpack/merge

2B->2W, 4B->4H

4B->4H, 2*4B->8B

8B

Permute/shuffle Register sets

8B, 4H, 2W

Integer

8B, 4H

2*4B->8B, 2*2H->4H 8B, 4H

4H

Fl. Pt. + 192b Acc.

Integer

Fl. Pt.

FIGURE E.4.1 Summary of multimedia support for desktop RISCs. B stands for byte (8 bits), H for half word (16 bits), and W for word (32 bits). Thus 8B means an operation on eight bytes in a single instruction. Pack and unpack use the notation 2*2W to mean two operands each with two words. Note that MDMX has vector/scalar operations, where the scalar is specified as an element of one of the vector registers. This table is a simplification of the full multimedia architectures, leaving out many details. For example, MIPS MDMX includes instructions to multiplex between two operands, HP MAX2 includes an instruction to calculate averages, and SPARC VIS includes instructions to set registers to constants. Also, this table does not include the memory alignment operation of MDMX, MAX, and VIS.

per field. This wide accumulator can be used for add, subtract, and multiply/ add instructions. MIPS claims performance advantages of two to four times for the accumulator. Perhaps the surprising conclusion of this table is the lack of consistency. The only operations found on all four are the logical operations (AND, OR, XOR), which do not need a partitioned ALU. If we leave out the frugal Alpha, then the only other common operations are parallel adds and subtracts on four halfwords. Each manufacturer states that these are instructions intended to be used in hand-optimized subroutine libraries, an intention likely to be followed, as a compiler that works well with multimedia extensions of all desktop RISCs would be challenging.

E.5

E.5

E-19

Instructions: Digital Signal-Processing Extensions of the Embedded RISCs

Instructions: Digital Signal-Processing Extensions of the Embedded RISCs

One feature found in every digital signal processor (DSP) architecture is support for integer multiply-accumulate. The multiplies tend to be on shorter words than regular integers, such as 16 bits, and the accumulator tends to be on longer words, such as 64 bits. The reason for multiply-accumulate is to efficiently implement digital filters, common in DSP applications. Since Thumb and MIPS-16 are subset architectures, they do not provide such support. Instead, programmers should use the DSP or multimedia extensions found in the 32-bit mode instructions of ARM and MIPS-64. Figure E.5.1 shows the size of the multiply, the size of the accumulator, and the operations and instruction names for the embedded RISCs. Machines with accumulator sizes greater than 32 and less than 64 bits will force the upper bits to remain as the sign bits, thereby “saturating” the add to set to maximum and minimum fixed-point values if the operations overflow.

M32R

MIPS-16

Size of multiply

32B × 32B

ARMv4

Thumb —

32B × 32B, 16B × 16B

SuperH

32B × 16B, 16B × 16B



Size of accumulator

32B/64B



32B/42B, 48B/64B

56B



Accumulator name

Any GPR or pairs of GPRs



MACH, MACL

ACC



Operations

32B/64B product + 64B accumulate signed/ unsigned



32B product + 42B/32B accumulate (operands in memory); 64B product + 64B/48B accumulate (operands in memory); clear MAC

32B/48B product + 64B accumulate, round, move



Corresponding instruction names

MLA, SMLAL, UMLAL



MAC, MACS, MAC.L, MAC.LS, MACHI/MACLO, CLRMAC MACWHI/MACWLO, RAC, RACH, MVFACHI/ MVFACLO, MVTACHI/ MVTACLO

FIGURE E.5.1 Summary of five embedded RISC approaches to multiply-accumulate.



E-20

Appendix E A Survey of RISC Architectures

E.6

Instructions: Common Extensions to MIPS Core

Figures E.6.1 through E.6.7 list instructions not found in Figures E.3.5 through E.3.11 in the same four categories. Instructions are put in these lists if they appear in more than one of the standard architectures. The instructions are defined using the hardware description language defined in Figure E.6.8. Although most of the categories are self-explanatory, a few bear comment:

Name



The “atomic swap” row means a primitive that can exchange a register with memory without interruption. This is useful for operating system semaphores in a uniprocessor as well as for multiprocessor synchronization (see Section 2.11 in Chapter 2).



The 64-bit data transfer and operation rows show how MIPS, PowerPC, and SPARC define 64-bit addressing and integer operations. SPARC simply defines all register and addressing operations to be 64 bits, adding only

Definition

Alpha

Atomic swap R/M (for locks and semaphores)

Temp<---Rd; Rd<–Mem[x]; LDL/Q_L; Mem[x]<---Temp STL/Q_C

Load 64-bit integer

Rd<–64 Mem[x]

MIPS-64

PA-RISC 2.0

PowerPC

SPARCv9

LL; SC

— (see D.8)

LWARX; STWCX

CASA, CASX

LDQ

LD

LDD

LD

LDX

Store 64-bit integer

Mem[x]<---64 Rd

STQ

SD

STD

STD

STX

Load 32-bit integer unsigned

Rd32..63<–32 Mem[x]; Rd0..31<–32 0

LDL; EXTLL

LWU

LDW

LWZ

LDUW

Load 32-bit integer signed

Rd32..63<–32 Mem[x]; 32 Rd0..31<–32 Mem[x]0

LDL

LW

LDW; EXTRD,S 63, 8

LWA

LDSW

Prefetch

Cache[x]<–hint

FETCH, FETCH_M*

PREF, PREFX LDD, r0 LDW, r0

DCBT, DCBTST

PRE-FETCH

Load coprocessor

Coprocessor<– Mem[x]



LWCi

CLDWX, CLDWS





Store coprocessor

Mem[x]<– Coprocessor



SWCi

CSTWX, CSTWS





Endian

(Big/little endian?)

Either

Either

Either

Either

Either

Cache flush

(Flush cache block at this address)

ECB

CP0op

FDC, FIC

DCBF

FLUSH

Shared memory synchronization

(All prior data transfers WMB complete before next data transfer may start)

SYNC

SYNC

SYNC

MEMBAR

FIGURE E.6.1 Data transfer instructions not found in MIPS core but found in two or more of the five desktop architectures. The load linked/store conditional pair of instructions gives Alpha and MIPS atomic operations for semaphores, allowing data to be read from memory, modified, and stored without fear of interrupts or other machines accessing the data in a multiprocessor (see Chapter 2). Prefetching in the Alpha to external caches is accomplished with FETCH and FETCH_M; on-chip cache prefetches use LD_Q A, R31, and LD_Y A. F31 is used in the Alpha 21164 (see Bhandarkar [1995], p. 190).

E.6

Name

E-21

Instructions: Common Extensions to MIPS Core

Definition

Alpha

MIPS-64

PA-RISC 2.0

PowerPC

SPARCv9

64-bit integer arithmetic ops

Rd<–64Rs1 op64 Rs2

ADD, SUB, MUL

DADD, DSUB ADD, SUB, DMULT, DDIV SHLADD, DS

ADD, SUBF, MULLD, DIVD

64-bit integer logical ops

Rd<–64Rs1 op64 Rs2

AND, OR, XOR

AND, OR, XOR

AND, OR, XOR

AND, OR, XOR AND, OR, XOR

64-bit shifts

Rd<–64Rs1 op64 Rs2

SLL, SRA, SRL

DSLL/V, DSRA/V, DSRL/V

DEPD,Z EXTRD,S EXTRD,U

SLD, SRAD, SRLD

SLLX, SRAX, SRLX

Conditional move

if (cond) Rd<–Rs

CMOV_

MOVN/Z

SUBc, n; ADD



MOVcc, MOVr

Support for multiword integer add

CarryOut, Rd <– Rs1 + Rs2 + OldCarryOut



ADU; SLTU; ADDC ADDU, DADU; SLTU; DADDU

ADDC, ADDE

ADDcc

Support for multiword integer sub

CarryOut, Rd <– Rs1 Rs2 + OldCarryOut



SUBU; SLTU; SUBB SUBU, DSUBU; SLTU; DSUBU

SUBFC, SUBFE SUBcc

And not

Rd <– Rs1 & ~(Rs2)

BIC



ANDCM

ANDC

ANDN

Or not

Rd <– Rs1 | ~(Rs2)

ORNOT





ORC

ORN





ADDIL (R-I)

ADDIS (R-I)



COPi

COPR,i



IMPDEPi

Add high immediate Rd0..15<–Rs10..15 + (Const<<16); Coprocessor operations

(Defined by coprocessor) —

ADD, SUB, MULX, S/UDIVX

FIGURE E.6.2 Arithmetic/logical instructions not found in MIPS core but found in two or more of the five desktop architectures.

Alpha

MIPS-64

PA-RISC 2.0

PowerPC

SPARCv9

Optimized delayed branches

Name

(Branch not always delayed)

Definition



BEQL, BNEL, B_ZL (<, >, <=, >=)

COMBT, n, COMBF, n



BPcc, A, FPBcc, A

Conditional trap

if (COND) {R31<---PC; PC <–0..0#i}



T_,,T_I (=, not=, <, >, <=, >=)

SUBc, n; BREAK

TW, TD, TWI, TDI

Tcc

No. control registers

Misc. regs (virtual memory, interrupts, . . .)

6

equiv. 12

32

33

29

FIGURE E.6.3 Control instructions not found in MIPS core but found in two or more of the five desktop architectures.

special instructions for 64-bit shifts, data transfers, and branches. MIPS includes the same extensions, plus it adds separate 64-bit signed arithmetic instructions. PowerPC adds 64-bit right shift, load, store, divide, and compare and has a separate mode determining whether instructions are interpreted as 32- or 64-bit operations; 64-bit operations will not work in a machine that

E-22

Appendix E A Survey of RISC Architectures

Name

Definition

Alpha

MIPS-64

PA-RISC 2.0

PowerPC

Multiply and add

Fd <– ( Fs1 × Fs2) + Fs3



MADD.S/D

FMPYFADD sgl/dbl FMADD/S

Multiply and sub

Fd <– ( Fs1 × Fs2) – Fs3



MSUB.S/D

FMSUB/S



NMADD.S/D

FMPYFNEG sgl/dbl FNMADD/S FNMSUB/S

Neg mult and add Fd <– -(( Fs1 × Fs2) + Fs3) Neg mult and sub Fd <– -(( Fs1 × Fs2)

SPARCv9



NMSUB.S/D

Square root

– Fs3) Fd <– SQRT(Fs)

SQRT_

SQRT.S/D

FSQRT sgl/dbl

FSQRT/S

FSQRTS/D

Conditional move

if (cond) Fd<–Fs

FCMOV_

MOVF/T, MOVF/T.S/D

FTESTFCPY



FMOVcc

Negate

Fd <– Fs ^ x80000000

CPYSN

NEG.S/D

FNEG sgl/dbl

FNEG

FNEGS/D/Q

Absolute value

Fd <– Fs & x7FFFFFFF



ABS.S/D

FABS/dbl

FABS

FABSS/D/Q

FIGURE E.6.4 architectures.

Floating-point instructions not found in MIPS core but found in two or more of the five desktop

Name

Definition

ARMv4

Thumb

SuperH

SWP, SWPB

—1

(see TAS)

Memory management unit Paged address translation

Via coprocessor instructions

—1

LDTLB

Endian

Either

Either

Either

Atomic swap R/M (for semaphores)

Temp<–Rd; Rd<–Mem[x]; Mem[x]<–Temp

(Big/little endian?)

M32R LOCK; UNLOCK

MIPS-16

—1 —1

Big

Either

FIGURE E.6.5 Data transfer instructions not found in MIPS core but found in two or more of the five embedded architectures. We use —1 to show sequences that are available in 32-bit mode but not 16-bit mode in Thumb or MIPS-16.

only supports 32-bit mode. PA-RISC is expanded to 64-bit addressing and operations in version 2.0. ■

The “prefetch” instruction supplies an address and hint to the implementation about the data. Hints include whether the data is likely to be read or written soon, likely to be read or written only once, or likely to be read or written many times. Prefetch does not cause exceptions. MIPS has a version that adds two registers to get the address for floating-point programs, unlike nonfloating-point MIPS programs.



In the “Endian” row, “Big/little” means there is a bit in the program status register that allows the processor to act either as big endian or little endian (see Appendix B). This can be accomplished by simply complementing some of the least significant bits of the address in data transfer instructions.

E.6



The “shared memory synchronization” helps with cache-coherent multiprocessors: all loads and stores executed before the instruction must complete before loads and stores after it can start. (See Chapter 2.)



The “coprocessor operations” row lists several categories that allow for the processor to be extended with special-purpose hardware. Name

E-23

Instructions: Common Extensions to MIPS Core

Definition

ARMv4

Thumb

SuperH

M32R

MIPS-16

Load immediate

Rd<---Imm

MOV

MOV

MOV, MOVA

LDI, LD24

LI

Support for multiword integer add

CarryOut, Rd <--- Rd + Rs1 + OldCarryOut

ADCS

ADC

ADDC

ADDX

—1

Support for multiword integer sub

CarryOut, Rd <--- Rd – Rs1 + OldCarryOut

SBCS

SBC

SUBC

SUBX

—1

Negate

Rd <--- 0 – Rs1

NEG2

NEG

NEG

NEG

Not

Rd <--- ~(Rs1)

MVN

MVN

NOT

NOT

NOT

Move

Rd <--- Rs1

MOV

MOV

MOV

MV

MOVE

Rotate right

Rd <--- Rs i, >> Rd0. . . i–1 <--Rs31–i. . . 31

ROR

ROR

ROTC

And not

Rd <--- Rs1 & ~(Rs2)

BIC

BIC

FIGURE E.6.6 Arithmetic/logical instructions not found in MIPS core but found in two or more of the five embedded architectures. We use —1 to show sequences that are available in 32-bit mode but not in 16-bit mode in Thumb or MIPS-16. The superscript 2 shows new instructions found only in 16-bit mode of Thumb or MIPS-16, such as NEG2.

Name

Definition

No. control registers

Misc. registers

ARMv4 21

Thumb

SuperH

M32R

MIPS-16

29

9

5

36

FIGURE E.6.7 Control information in the five embedded architectures.

One difference that needs a longer explanation is the optimized branches. Figure E.6.9 shows the options. The Alpha and PowerPC offer branches that take effect immediately, like branches on earlier architectures. To accelerate branches, these machines use branch prediction (see Chapter 4). All the rest of the desktop RISCs offer delayed branches (see Appendix A). The embedded RISCs generally do not support delayed branch, with the exception of SuperH, which has it as an option. The other three desktop RISCs provide a version of delayed branch that makes it easier to fill the delay slot. The SPARC “annulling” branch executes the instruction in the delay slot only if the branch is taken; otherwise the instruction is annulled. This means the instruction at the target of the branch can safely be copied into the delay slot, since it will only be executed if the branch is taken. The restrictions are that the target is not another branch and that the target is known at compile time. (SPARC also offers a nondelayed jump because an unconditional branch with the annul bit set does not execute the following instruction.) Later versions of the MIPS

E-24

Appendix E A Survey of RISC Architectures

Notation

Meaning

Example

Meaning

<- -

Data transfer. Length of transfer is given by Regs[R1]<--Regs[R2]; the destination’s length; the length is specified when not clear.

M

Array of memory accessed in bytes. The starting address for a transfer is indicated as the index to the memory array.

Regs[R1]<--M[x];

Place contents of memory location x into R1. If a transfer starts at M[i] and requires 4 bytes, the transferred bytes are M[i], M[i+1], M[i+2], and M[i+3].

<- -n

Transfer an n-bit field, used whenever length of transfer is not clear.

M[y]<--16M[x];

Transfer 16 bits starting at memory location x to memory location y. The length of the two sides should match.

Xn

Subscript selects a bit.

Regs[R1]0<--0;

Change sign bit of R1 to 0. (Bits are numbered from MSB starting at 0.)

Xm..n

Subscript selects a field.

Regs[R3]24..31<--M[x];

Moves contents of memory location x into low-order byte of R3. Sets high-order three bytes of R3 to 0. Moves contents of location x into low byte of R3; clears upper three bytes. Moves 64 bits from memory starting at location x; 1st 32 bits go into F2, 2nd 32 into F3.

Transfer contents of R2 to R1. Registers have a fixed length, so transfers shorter than the register size must indicate which bits are used.

Xn

Superscript replicates a bit field.

Regs[R3]0..23<--024;

##

Concatenates two fields.

Regs[R3]<--240## M[x]; F2##F3<--64M[x];

*, &

Dereference a pointer; get the address of a variable.

p*<--&x;

Assign to object pointed to by p the address of the variable x.

<<, >>

C logical shifts (left, right).

Regs[R1] << 5

Shift R1 left 5 bits.

==, !=, >, <, >=, <=

C relational operators; equal, not equal, greater, less, greater or equal, less or equal.

(Regs[R1]== Regs[R2]) & (Regs[R3]!=Regs[R4])

True if contents of R1 equal the contents of R2 and contents of R3 do not equal the contents of R4.

&, |, ^, !

C bitwise logical operations: AND, OR, exclusive OR, and complement.

(Regs[R1] & (Regs[R2]| Regs[R3]))

Bitwise AND of R1 and bitwise OR of R2 and R3.

FIGURE E.6.8 Hardware description notation (and some standard C operators).

(Plain) branch

Delayed branch

Annulling delayed branch

Found in architectures

Alpha, PowerPC, ARM, Thumb, MIPS-64, PA-RISC, SuperH, M32R, MIPS-16 SPARC, SuperH

MIPS-64, SPARC

PA-RISC

Execute following instruction

Only if branch not taken

Only if branch taken

If forward branch not taken or backward branch taken

Always

FIGURE E.6.9 When the instruction following the branch is executed for three types of branches.

E.7

Instructions Unique to MIPS-64

architecture have added a branch likely instruction that also annuls the following instruction if the branch is not taken. PA-RISC allows almost any instruction to annul the next instruction, including branches. Its “nullifying” branch option will execute the next instruction depending on the direction of the branch and whether it is taken (i.e., if a forward branch is not taken or a backward branch is taken). Presumably this choice was made to optimize loops, allowing the instructions following the exit branch and the looping branch to exe cute in the common case. Now that we have covered the similarities, we will focus on the unique features of each architecture. We first cover the desktop/server RISCs, ordering them by length of description of the unique features from shortest to longest, and then the embedded RISCs.

E.7

Instructions Unique to MIPS-64

MIPS has gone through five generations of instruction sets, and this evolution has generally added features found in other architectures. Here are the salient unique features of MIPS, the first several of which were found in the original instruction set.

Nonaligned Data Transfers MIPS has special instructions to handle misaligned words in memory. A rare event in most programs, it is included for supporting 16-bit minicomputer applications and for doing memcpy and strcpy faster. Although most RISCs trap if you try to load a word or store a word to a misaligned address, on all architectures misaligned words can be accessed without traps by using four load byte instructions and then assembling the result using shifts and logical ORs. The MIPS load and store word left and right instructions (LWL, LWR, SWL, SWR) allow this to be done in just two instructions: LWL loads the left portion of the register and LWR loads the right portion of the register. SWL and SWR do the corresponding stores. Figure E.7.1 shows how they work. There are also 64-bit versions of these instructions.

Remaining Instructions Below is a list of the remaining unique details of the MIPS-64 architecture: ■

NOR—This logical instruction calculates ⬃(Rs1 | Rs2).



Constant shift amount—Nonvariable shifts use the 5-bit constant field shown in the register-register format in Figure E.2.3.



SYSCALL—This special trap instruction is used to invoke the operating system.

E-25

E-26

Appendix E A Survey of RISC Architectures

Case 1 Before

Case 2 Before

M[100]

D

A

V

M[200]

100 101 102 103

M[104]

E

200 201 202 203

M[204]

104 105 106 107

R2

J

O

H

R2

D

A

V

R2

D

A

V

R4

R4

E

J

O

H

N

D

O

H

N

LWR R4, 206:

After E

V

LWL R4, 203:

After N

LWR R2, 104:

After

A

204 205 206 207

N

LWL R2, 101:

After

D

R4

D

A

V

E

FIGURE E.7.1 MIPS instructions for unaligned word reads. This figure assumes operation in big-endian mode. Case 1 first loads the three bytes 101, 102, and 103 into the left of R2, leaving the least significant byte undisturbed. The following LWR simply loads byte 104 into the least significant byte of R2, leaving the other bytes of the register unchanged using LWL. Case 2 first loads byte 203 into the most significant byte of R4, and the following LWR loads the other three bytes of R4 from memory bytes 204, 205, and 206. LWL reads the word with the first byte from memory, shifts to the left to discard the unneeded byte(s), and changes only those bytes in Rd. The byte(s) transferred are from the first byte to the lowest-order byte of the word. The following LWR addresses the last byte, right-shifts to discard the unneeded byte(s), and finally changes only those bytes of Rd. The byte(s) transferred are from the last byte up to the highest-order byte of the word. Store word left (SWL) is simply the inverse of LWL, and store word right (SWR) is the inverse of LWR. Changing to little-endian mode flips which bytes are selected and discarded. (If big-little, left-right, load-store seem confusing, don’t worry; they work!)



Move to/from control registers—CTCi and CFCi move between the integer registers and control registers.



Jump/call not PC-relative—The 26-bit address of jumps and calls is not added to the PC. It is shifted left two bits and replaces the lower 28 bits of the PC. This would only make a difference if the program were located near a 256 MB boundary.



TLB instructions—Translation-lookaside buffer (TLB) misses were handled in software in MIPS I, so the instruction set also had instructions for manipulating the registers of the TLB (see Chapter 5 for more on TLBs). These registers are considered part of the “system coprocessor.” Since MIPS I

E.8

Instructions Unique to Alpha

the instructions differ among versions of the architecture; they are more part of the implementations than part of the instruction set architecture. ■

Reciprocal and reciprocal square root—These instructions, which do not follow IEEE 754 guidelines of proper rounding, are included apparently for applications that value speed of divide and square root more than they value accuracy.



Conditional procedure call instructions—BGEZAL saves the return address and branches if the content of Rs1 is greater than or equal to zero, and BLTZAL does the same for less than zero. The purpose of these instructions is to get a PC-relative call. (There are “likely” versions of these instructions as well.)



Parallel single precision floating-point operations—As well as extending the architecture with parallel integer operations in MDMX, MIPS-64 also supports two parallel 32-bit floating-point operations on 64-bit registers in a single instruction. “Paired single” operations include add (ADD.PS), subtract (SUB.PS), compare (C.__.PS), convert (CVT.PS.S, CVT.S.PL, CVT.S.PU), negate (NEG.PS), absolute value (ABS.PS), move (MOV.PS, MOVF.PS, MOVT.PS), multiply (MUL.PS), multiply-add (MADD.PS), and multiply-subtract (MSUB.PS).

There is no specific provision in the MIPS architecture for floating-point execution to proceed in parallel with integer execution, but the MIPS implementations of floating point allow this to happen by checking to see if arithmetic interrupts are possible early in the cycle. Normally, exception detection would force serialization of execution of integer and floating-point operations.

E.8

Instructions Unique to Alpha

The Alpha was intended to be an architecture that made it easy to build highperformance implementations. Toward that goal, the architects originally made two controversial decisions: imprecise floating-point exceptions and no byte or halfword data transfers. To simplify pipelined execution, Alpha does not require that an exception should act as if no instructions past a certain point are executed and that all before that point have been executed. It supplies the TRAPB instruction, which stalls until all prior arithmetic instructions are guaranteed to complete without incurring arithmetic exceptions. In the most conservative mode, placing one TRAPB per exception-causing instruction slows execution by roughly five times but provides precise exceptions (see Darcy and Gay [1996]).

E-27

E-28

Appendix E A Survey of RISC Architectures

Code that does not include TRAPB does not obey the IEEE 754 floating-point standard. The reason is that parts of the standard (NaNs, infinities, and denormals) are implemented in software on Alpha, as they are on many other microprocessors. To implement these operations in software, however, programs must find the offending instruction and operand values, which cannot be done with imprecise interrupts! When the architecture was developed, it was believed by the architects that byte loads and stores would slow down data transfers. Byte loads require an extra shifter in the data transfer path, and byte stores require that the memory system perform a read-modify-write for memory systems with error correction codes, since the new ECC value must be recalculated. This omission meant that byte stores required the sequence load word, replaced the desired byte, and then stored the word. (Inconsistently, floating-point loads go through considerable byte swapping to convert the obtuse VAX floating-point formats into a canonical form.) To reduce the number of instructions to get the desired data, Alpha includes an elaborate set of byte manipulation instructions: extract field and zero rest of a register (EXTxx), insert field (INSxx), mask rest of a register (MSKxx), zero fields of a register (ZAP), and compare multiple bytes (CMPGE). Apparently, the implementors were not as bothered by load and store byte as were the original architects. Beginning with the shrink of the second version of the Alpha chip (21164A), the architecture does include loads and stores for bytes and halfwords.

Remaining Instructions Below is a list of the remaining unique instructions of the Alpha architecture: ■

PAL code—To provide the operations that the VAX performed in microcode, Alpha provides a mode that runs with all privileges enabled, interrupts disabled, and virtual memory mapping turned off for instructions. PAL (privileged architecture library) code is used for TLB management, atomic memory operations, and some operating system primitives. PAL code is called via the CALL_PAL instruction.



No divide—Integer divide is not supported in hardware.



“Unaligned” load-store—LDQ_U and STQ_U load and store 64-bit data using addresses that ignore the least significant three bits. Extract instructions then select the desired unaligned word using the lower address bits. These instructions are similar to LWL/R, SWL/R in MIPS.



Floating-point single precision represented as double precision—Single precision data is kept as conventional 32-bit formats in memory but is converted to 64bit double precision format in registers.



Floating-point register F31 is fixed at zero—To simplify comparisons to zero.

E.9

Instructions Unique to SPARC v9



VAX floating-point formats—To maintain compatibility with the VAX architecture, in addition to the IEEE 754 single and double precision formats called S and T, Alpha supports the VAX single and double precision formats called F and G, but not VAX format D. (D had too narrow an exponent field to be useful for double precision and was replaced by G in VAX code.)



Bit count instructions—Version 3 of the architecture added instructions to count the number of leading zeros (CTLZ), count the number of trailing zeros (CTTZ), and count the number of ones in a word (CTPOP). Originally found on Cray computers, these instructions help with decryption.

E.9

Instructions Unique to SPARC v9

Several features are unique to SPARC.

Register Windows The primary unique feature of SPARC is register windows, an optimization for reducing register traffic on procedure calls. Several banks of registers are used, with a new one allocated on each procedure call. Although this could limit the depth of procedure calls, the limitation is avoided by operating the banks as a circular buffer, providing unlimited depth. The knee of the cost/performance curve seems to be six to eight banks. SPARC can have between 2 and 32 windows, typically using 8 registers each for the globals, locals, incoming parameters, and outgoing parameters. (Given that each window has 16 unique registers, an implementation of SPARC can have as few as 40 physical registers and as many as 520, although most have 128 to 136, so far.) Rather than tie window changes with call and return instructions, SPARC has the separate instructions SAVE and RESTORE. SAVE is used to “save” the caller’s window by pointing to the next window of registers in addition to performing an add instruction. The trick is that the source registers are from the caller’s window of the addition operation, while the destination register is in the callee’s window. SPARC compilers typically use this instruction for changing the stack pointer to allocate local variables in a new stack frame. RESTORE is the inverse of SAVE, bringing back the caller’s window while acting as an add instruction, with the source registers from the callee’s window and the destination register in the caller’s window. This automatically deallocates the stack frame. Compilers can also make use of it for generating the callee’s final return value. The danger of register windows is that the larger number of registers could slow down the clock rate. This was not the case for early implementations. The SPARC architecture (with register windows) and the MIPS R2000 architecture (without)

E-29

E-30

Appendix E A Survey of RISC Architectures

have been built in several technologies since 1987. For several generations, the SPARC clock rate has not been slower than the MIPS clock rate for implementations in similar technologies, probably because cache access times dominate register access times in these implementations. The current-generation machines took different implementation strategies—in order versus out of order—and it’s unlikely that the number of registers by themselves determined the clock rate in either machine. Recently, other architectures have included register windows: Tensilica and IA-64. Another data transfer feature is alternate space option for loads and stores. This simply allows the memory system to identify memory accesses to input/ output devices, or to control registers for devices such as the cache and memory management unit.

Fast Traps Version 9 SPARC includes support to make traps fast. It expands the single level of traps to at least four levels, allowing the window overflow and underflow trap handlers to be interrupted. The extra levels mean the handler does not need to check for page faults or misaligned stack pointers explicitly in the code, thereby making the handler faster. Two new instructions were added to return from this multilevel handler: RETRY (which retries the interrupted instruction) and DONE (which does not). To support user-level traps, the instruction RETURN will return from the trap in nonprivileged mode.

Support for LISP and Smalltalk The primary remaining arithmetic feature is tagged addition and subtraction. The designers of SPARC spent some time thinking about languages like LISP and Smalltalk, and this influenced some of the features of SPARC already discussed: register windows, conditional trap instructions, calls with 32-bit instruction addresses, and multiword arithmetic (see Taylor, et al. [1986] and Ungar, et al. [1984]). A small amount of support is offered for tagged data types with operations for addition, subtraction, and, hence, comparison. The two least significant bits indicate whether the operand is an integer (coded as 00), so TADDcc and TSUBcc set the overflow bit if either operand is not tagged as an integer or if the result is too large. A subsequent conditional branch or trap instruction can decide what to do. (If the operands are not integers, software recovers the operands, checks the types of the operands, and invokes the correct operation based on those types.) It turns out that the misaligned memory access trap can also be put to use for tagged data, since loading from a pointer with the wrong tag can be an invalid access. Figure E.9.1 shows both types of tag support.

E.9

Instructions Unique to SPARC v9

a. Add, sub, or compare integers (coded as 00)

00

(R5)

00

(R6)

00

(R7)

11

(R4)

TADDcc r7, r5, r6

b. Loading via valid pointer (coded as 11) –

3

LD rD, r4, –3 00

(Word address)

FIGURE E.9.1 SPARC uses the two least significant bits to encode different data types for the tagged arithmetic instructions. a. Integer arithmetic takes a single cycle as long as the operands and the result are integers. b. The misaligned trap can be used to catch invalid memory accesses, such as trying to use an integer as a pointer. For languages with paired data like LISP, an offset of –3 can be used to access the even word of a pair (CAR) and ⫹1 can be used for the odd word of a pair (CDR).

Overlapped Integer and Floating-Point Operations SPARC allows floating-point instructions to overlap execution with integer instructions. To recover from an interrupt during such a situation, SPARC has a queue of pending floating-point instructions and their addresses. RDPR allows the processor to empty the queue. The second floating-point feature is the inclusion of floating-point square root instructions FSQRTS, FSQRTD, and FSQRTQ.

Remaining Instructions The remaining unique features of SPARC are as follows: ■ JMPL

uses Rd to specify the return address register, so specifying r31 makes it similar to JALR in MIPS and specifying r0 makes it like JR.

■ LDSTUB

loads the value of the byte into Rd and then stores FF16 into the addressed byte. This version 8 instruction can be used to implement synchronization (see Chapter 2).



CASA (CASXA) atomically compares a value in a processor register to a 32-bit (64-bit) value in memory; if and only if they are equal, it swaps the value in memory with the value in a second processor register. This version 9

E-31

E-32

Appendix E A Survey of RISC Architectures

instruction can be used to construct wait-free synchronization algorithms that do not require the use of locks. ■ XNOR calculates the exclusive OR with the complement of the second operand. ■ BPcc, BPr,

and FBPcc include a branch prediction bit so that the compiler can give hints to the machine about whether a branch is likely to be taken or not.

■ ILLTRAP

causes an illegal instruction trap. Muchnick [1988] explains how this is used for proper execution of aggregate returning procedures in C.

■ POPC

counts the number of bits set to one in an operand, also found in the third version of the Alpha architecture.



Nonfaulting loads allow compilers to move load instructions ahead of conditional control structures that control their use. Hence, nonfaulting loads will be executed speculatively.



Quadruple precision floating-point arithmetic and data transfer allow the floating-point registers to act as eight 128-bit registers for floating-point operations and data transfers.



Multiple precision floating-point results for multiply mean that two single precision operands can result in a double precision product and two double precision operands can result in a quadruple precision product. These instructions can be useful in complex arithmetic and some models of floatingpoint calculations.

E.10

Instructions Unique to PowerPC

PowerPC is the result of several generations of IBM commercial RISC machines— IBM RT/PC, IBM Power1, and IBM Power2—plus the Motorola 8800.

Branch Registers: Link and Counter Rather than dedicate one of the 32 general-purpose registers to save the return address on procedure call, PowerPC puts the address into a special register called the link register. Since many procedures will return without calling another procedure, the link doesn’t always have to be saved. Making the return address a special register makes the return jump faster, since the hardware need not go through the register read pipeline stage for return jumps. In a similar vein, PowerPC has a count register to be used in for loops where the program iterates a fixed number of times. By using a special register, the branch

E.10

Instructions Unique to PowerPC

hardware can determine quickly whether a branch based on the count register is likely to branch, since the value of the register is known early in the execution cycle. Tests of the value of the count register in a branch instruction will automatically decrement the count register. Given that the count register and link register are already located with the hardware that controls branches, and that one of the problems in branch prediction is getting the target address early in the pipeline (see Appendix A), the PowerPC architects decided to make a second use of these registers. Either register can hold a target address of a conditional branch. Thus, PowerPC supplements its basic conditional branch with two instructions that get the target address from these registers (BCLR, BCCTR).

Remaining Instructions Unlike most other RISC machines, register 0 is not hardwired to the value 0. It cannot be used as a base register—that is, it generates a 0 in this case—but in base ⫹ index addressing it can be used as the index. The other unique features of the PowerPC are as follows: ■

Load multiple and store multiple save or restore up to 32 registers in a single instruction.

■ LSW

and STSW permit fetching and storing of fixed- and variable-length strings that have arbitrary alignment.



Rotate with mask instructions support bit field extraction and insertion. One version rotates the data and then per forms logical AND with a mask of ones, thereby extracting a field. The other version rotates the data but only places the bits into the destination register where there is a corresponding 1 bit in the mask, thereby inserting a field.



Algebraic right shift sets the carry bit (CA) if the operand is negative and any 1 bits are shifted out. Thus, a signed divide by any constant power of two that rounds toward 0 can be accomplished with an SRAWI followed by ADDZE, which adds CA to the register.

■ CBTLZ will count leading zeros. ■ SUBFIC computes (immediate - RA), which can be used to develop a one’s or

two’s complement. ■

Logical shifted immediate instructions shift the 16-bit immediate to the left 16 bits before performing AND, OR, or XOR.

E-33

E-34

Appendix E A Survey of RISC Architectures

E.11

Instructions Unique to PA-RISC 2.0

PA-RISC was expanded slightly in 1990 with version 1.1 and changed significantly in 2.0 with 64-bit extensions in 1996. PA-RISC perhaps has the most unusual features of any desktop RISC machine. For example, it has the most addressing modes and instruction formats, and, as we shall see, several instructions that are really the combination of two simpler instructions.

Nullification As shown in Figure E.6.9, several RISC machines can choose not to execute the instruction following a delayed branch to improve utilization of the branch slot. This is called nullification in PA-RISC, and it has been generalized to apply to any arithmetic/logical instruction as well as to all branches. Thus, an add instruction can add two operands, store the sum, and cause the following instruction to be skipped if the sum is zero. Like conditional move instructions, nullification allows PA-RISC to avoid branches in cases where there is just one instruction in the then part of an if statement.

A Cornucopia of Conditional Branches Given nullification, PA-RISC did not need to have separate conditional branch instructions. The inventors could have recommended that nullifying instructions precede unconditional branches, thereby simplifying the instruction set. Instead, PA-RISC has the largest number of conditional branches of any RISC machine. Figure E.11.1 shows the conditional branches of PA-RISC. As you can see, several are really combinations of two instructions.

Synthesized Multiply and Divide PA-RISC provides several primitives so that multiply and divide can be synthesized in software. Instructions that shift one operand 1, 2, or 3 bits and then add, trapping or not on overflow, are useful in multiplies. (Alpha also includes instructions that multiply the second operand of adds and subtracts by 4 or by 8: S4ADD, S8ADD, S4SUB, and S8SUB.) The divide step performs the critical step of nonrestoring divide, adding or subtracting depending on the sign of the prior result. Magenheimer, et al. [1988] measured the size of operands in multiplies and divides to show how well the multiply step would work. Using this data for C programs, Muchnick [1988] found that by making special cases, the average multiply by a constant takes 6 clock cycles and the multiply of variables takes 24 clock cycles. PA- RISC has ten instructions for these operations.

E.11

Name

Instruction

COMB COMIB

Compare and branch

MOVB MOVIB

Move and branch

ADDB ADDIB

Add and branch

BB BVB

Branch on bit

Compare immediate and branch Move immediate and branch Add immediate and branch Branch on variable bit

Instructions Unique to PA-RISC 2.0

E-35

Notation if (cond(Rs1,Rs2)) if (cond(imm5,Rs2))

{PC <-- PC + offset12} {PC <-- PC + offset12}

Rs2 <-- Rs1, if (cond(Rs1,0)) Rs2 <-- imm5, if (cond(imm5,0))

{PC <-- PC + offset12} {PC <-- PC + offset12}

Rs2 <-- Rs1 + Rs2, if (cond(Rs1 + Rs2,0)) Rs2 <-- imm5 + Rs2, if (cond(imm5 + Rs2,0))

{PC <-- PC + offset12} {PC <-- PC + offset12}

if (cond(Rsp,0)) if (cond(Rssar,0))

{PC <-- PC + offset12} {PC <-- PC + offset12}

FIGURE E.11.1 The PA-RISC conditional branch instructions. The 12-bit offset is called offset12 in this table, and the 5-bit immediate is called imm5. The 16 conditions are ⫽, ⬍, ⬍ ⫽, odd, signed overflow, unsigned no overflow, zero or no overflow unsigned, never, and their respective complements. The BB instruction selects one of the 32 bits of the register and branches depending on whether its value is 0 or 1. The BVB selects the bit to branch using the shift amount register, a special-purpose register. The subscript notation specifies a bit field.

The original SPARC architecture used similar optimizations, but with increasing numbers of transistors the instruction set was expanded to include full multiply and divide operations. PA-RISC gives some support along these lines by putting a full 32-bit integer multiply in the floating-point unit; however, the integer data must first be moved to floating-point registers.

Decimal Operations COBOL programs will compute on decimal values, stored as four bits per digit, rather than converting back and forth between binary and decimal. PA-RISC has instructions that will convert the sum from a normal 32-bit add into proper decimal digits. It also provides logical and arithmetic operations that set the condition codes to test for carries of digits, bytes, or halfwords. These operations also test whether bytes or halfwords are zero. These operations would be useful in arithmetic on 8-bit ASCII characters. Five PA-RISC instructions provide decimal support.

Remaining Instructions Here are some remaining PA-RISC instructions: ■

Branch vectored shifts an index register left three bits, adds it to a base register, and then branches to the calculated address. It is used for case statements.



Extract and deposit instructions allow arbitrary bit fields to be selected from or inserted into registers. Variations include whether the extracted field is sign-extended, whether the bit field is specified directly in the instruction or indirectly in another register, and whether the rest of the register is set to zero or left unchanged. PA-RISC has 12 such instructions.

E-36

Appendix E A Survey of RISC Architectures



To simplify use of 32-bit address constants, PA-RISC includes ADDIL, which adds a left-adjusted 21-bit constant to a register and places the result in register 1. The following data transfer instruction uses offset addressing to add the lower 11 bits of the address to register 1. This pair of instructions allows PA-RISC to add a 32-bit constant to a base register, at the cost of changing register 1.



PA-RISC has nine debug instructions that can set breakpoints on instruction or data addresses and return the trapped addresses.



Load and clear instructions provide a semaphore or lock that reads a value from memory and then writes zero.



Store bytes short optimizes unaligned data moves, moving either the leftmost or the rightmost bytes in a word to the effective address, depending on the instruction options and condition code bits.



Loads and stores work well with caches by having options that give hints about whether to load data into the cache if it’s not already in the cache. For example, a load with a destination of register 0 is defined to be a softwarecontrolled cache prefetch.



PA-RISC 2.0 extended cache hints to stores to indicate block copies, recommending that the processor not load data into the cache if it’s not already in the cache. It also can suggest that on loads and stores, there is spatial locality to prepare the cache for subsequent sequential accesses.



PA-RISC 2.0 also provides an optional branch target stack to predict indirect jumps used on subroutine returns. Software can suggest which addresses get placed on and removed from the branch target stack, but hardware controls whether or not these are valid.



Multiply/add and multiply/subtract are floating-point operations that can launch two independent floating-point operations in a single instruction in addition to the fused multiply/add and fused multiply/negate/add introduced in version 2.0 of PA-RISC.

E.12

Instructions Unique to ARM

It’s hard to pick the most unusual feature of ARM, but perhaps it is the conditional execution of instructions. Every instruction starts with a 4-bit field that determines whether it will act as a nop or as a real instruction, depending on the condition codes. Hence, conditional branches are properly considered as conditionally executing the unconditional branch instruction. Conditional execution allows

E.12

Instructions Unique to ARM

avoiding a branch to jump over a single instruction. It takes less code space and time to simply conditionally execute one instruction. The 12-bit immediate field has a novel interpretation. The eight least significant bits are zero-extended to a 32-bit value, then rotated right the number of bits specified in the first four bits of the field multiplied by two. Whether this split actually catches more immediates than a simple 12-bit field would be an interesting study. One advantage is that this scheme can represent all powers of two in a 32-bit word. Operand shifting is not limited to immediates. The second register of all arithmetic and logical processing operations has the option of being shifted before being operated on. The shift options are shift left logical, shift right logical, shift right arithmetic, and rotate right. Once again, it would be interesting to see how often operations like rotate-and-add, shift-right-and-test, and so on occur in ARM programs.

Remaining Instructions Below is a list of the remaining unique instructions of the ARM architecture: ■

Block loads and stores—Under control of a 16-bit mask within the instructions, any of the 16 registers can be loaded or stored into memory in a single instruction. These instructions can save and restore registers on procedure entry and return. These instructions can also be used for block memory copy—offering up to four times the bandwidth of a single register load-store—and today, block copies are the most important use.



Reverse subtract—RSB allows the first register to be subtracted from the immediate or shifted register. RSC does the same thing, but includes the carry when calculating the difference.



Long multiplies—Similarly to MIPS, Hi and Lo registers get the 64-bit signed product (SMULL) or the 64-bit unsigned prod uct (UMULL).



No divide—Like the Alpha, integer divide is not supported in hardware.



Conditional trap—A common extension to the MIPS core found in desktop RISCs (Figures E.6.1 through E.6.4), it comes for free in the conditional execution of all ARM instructions, including SWI.



Coprocessor interface—Like many of the desktop RISCs, ARM defines a full set of coprocessor instructions: data transfer, moves between generalpurpose and coprocessor registers, and coprocessor operations.



Floating-point architecture—Using the coprocessor interface, a floating-point architecture has been defined for ARM. It was implemented as the FPA10 coprocessor.



Branch and exchange instruction sets—The BX instruction is the transition between ARM and Thumb, using the lower 31 bits of the register to set the PC and the most significant bit to determine if the mode is ARM (1) or Thumb (0).

E-37

E-38

Appendix E A Survey of RISC Architectures

E.13

Instructions Unique to Thumb

In the ARM version 4 model, frequently executed procedures will use ARM instructions to get maximum performance, with the less frequently executed ones using Thumb to reduce the overall code size of the program. Since typically only a few procedures dominate execution time, the hope is that this hybrid gets the best of both worlds. Although Thumb instructions are translated by the hardware into conventional ARM instructions for execution, there are several restrictions. First, conditional execution is dropped from almost all instructions. Second, only the first eight registers are easily available in all instructions, with the stack pointer, link register, and program counter used implicitly in some instructions. Third, Thumb uses a twooperand format to save space. Fourth, the unique shifted immediates and shifted second operands have disappeared and are replaced by separate shift instructions. Fifth, the addressing modes are simplified. Finally, putting all instructions into 16 bits forces many more instruction formats. In many ways, the simplified Thumb architecture is more conventional than ARM. Here are additional changes made from ARM in going to Thumb: ■

Drop of immediate logical instructions—Logical immediates are gone.



Condition codes implicit—Rather than have condition codes set optionally, they are defined by the opcode. All ALU instructions and none of the data transfers set the condition codes.



Hi/Lo register access—The 16 ARM registers are halved into Lo registers and Hi registers, with the eight Hi registers including the stack pointer (SP), link register, and PC. The Lo registers are available in all ALU operations. Variations of ADD, BX, CMP, and MOV also work with all combinations of Lo and Hi registers. SP and PC registers are also available in variations of data transfers and add immediates. Any other operations on the Hi registers require one MOV to put the value into a Lo register, perform the operation there, and then transfer the data back to the Hi register.



Branch/call distance—Since instructions are 16 bits wide, the 8-bit conditional branch address is shifted by 1 instead of by 2. Branch with link is specified in two instructions, concatenating 11 bits from each instruction and shifting them left to form a 23-bit address to load into PC.



Distance for data transfer offsets—The offset is now five bits for the generalpurpose registers and eight bits for SP and PC.

E.14

E.14

Instructions Unique to SuperH

Instructions Unique to SuperH

Register 0 plays a special role in SuperH address modes. It can be added to another register to form an address in indirect indexed addressing and PC-relative addressing. R0 is used to load constants to give a larger addressing range than can easily be fit into the 16-bit instructions of the SuperH. R0 is also the only register that can be an operand for immediate versions of AND, CMP, OR, and XOR. Below is a list of the remaining unique details of the SuperH architecture: ■

Decrement and test—DT decrements a register and sets the T bit to 1 if the result is 0.



Optional delayed branch—Although the other embedded RISC machines generally do not use delayed branches (see Appendix B), SuperH offers optional delayed branch execution for BT and BF.



Many multiplies—Depending on whether the operation is signed or unsigned, if the operands are 16 bits or 32 bits, or if the product is 32 bits or 64 bits, the proper multiply instruction is MULS, MULU, DMULS, DMULU, or MUL. The product is found in the MACL and MACH registers.



Zero and sign extension—Byte or halfwords are either zero-extended (EXTU) or sign-extended (EXTS) within a 32-bit register.



One-bit shift amounts—Perhaps in an attempt to make them fit within the 16-bit instructions, shift instructions only shift a single bit at a time.



Dynamic shift amount—These variable shifts test the sign of the amount in a register to determine whether they shift left (positive) or shift right (negative). Both logical (SHLD) and arithmetic (SHAD) instructions are supported. These instructions help offset the 1-bit constant shift amounts of standard shifts.



Rotate—SuperH offers rotations by 1 bit left (ROTL) and right (ROTR), which set the T bit with the value rotated, and also have variations that include the T bit in the rotations (ROTCL and ROTCR).



SWAP—This instruction swaps either the high and low bytes of a 32-bit word or the two bytes of the rightmost 16 bits.



Extract word (XTRCT)—The middle 32 bits from a pair of 32-bit registers are placed in another register.



Negate with carry—Like SUBC (Figure E.6.6), except the first operand is 0.



Cache prefetch—Like many of the desktop RISCs (Figures E.6.1 through E.6.4), SuperH has an instruction (PREF) to prefetch data into the cache.

E-39

E-40

Appendix E A Survey of RISC Architectures



Test-and-set—SuperH uses the older test-and-set (TAS) instruction to perform atomic locks or semaphores (see Chapter 2). TAS first loads a byte from memory. It then sets the T bit to 1 if the byte is 0 or to 0 if the byte is not 0. Finally, it sets the most significant bit of the byte to 1 and writes the result back to memory.

E.15

Instructions Unique to M32R

The most unusual feature of the M32R is a slight VLIW approach to the pairs of 16-bit instructions. A bit is reserved in the first instruction of the pair to say whether this instruction can be executed in parallel with the next instruction— that is, the two instructions are independent—or if these two must be executed sequentially. (An earlier machine that offered a similar option was the Intel i860.) This feature is included for future implementations of the architecture. One surprise is that all branch displacements are shifted left 2 bits before being added to the PC, and the lower 2 bits of the PC are set to 0. Since some instructions are only 16 bits long, this shift means that a branch cannot go to any instruction in the program: it can only branch to instructions on word boundaries. A similar restriction is placed on the return address for the branch-and-link and jump-andlink instructions: they can only return to a word boundary. Thus, for a slightly larger branch distance, software must ensure that all branch addresses and all return addresses are aligned to a word boundary. The M32R code space is probably slightly larger, and it probably executes more nop instructions than it would if the branch address was only shifted left 1 bit. However, the VLIW feature above means that a nop can execute in parallel with another 16-bit instruction so that the padding doesn’t take more clock cycles. The code size expansion depends on the ability of the compiler to schedule code and to pair successive 16-bit instructions; Mitsubishi claims that code size overall is only 7% larger than that for the Motorola 6800 architecture. The last remaining novel feature is that the result of the divide operation is the remainder instead of the quotient.

E.16

Instructions Unique to MIPS-16

MIPS-16 is not really a separate instruction set but a 16-bit extension of the full 32-bit MIPS architecture. It is compatible with any of the 32-bit address MIPS architectures (MIPS I, MIPS II) or 64-bit architectures (MIPS III, IV, V). The ISA mode bit determines the width of instructions: 0 means 32-bit-wide instructions

E.16

Instructions Unique to MIPS-16

and 1 means 16-bit-wide instructions. The new JALX instruction toggles the ISA mode bit to switch to the other ISA. JR and JALR have been redefined to set the ISA mode bit from the most significant bit of the register containing the branch address, and this bit is not considered part of the address. All jump-and-link instructions save the current mode bit as the most significant bit of the return address. Hence, MIPS supports whole procedures containing either 16-bit or 32-bit instructions, but it does not support mixing the two lengths together in a single procedure. The one exception is the JAL and JALX: these two instructions need 32 bits even in the 16-bit mode, presumably to get a large enough address to branch to far procedures. In picking this subset, MIPS decided to include opcodes for some three-operand instructions and to keep 16 opcodes for 64-bit operations. The combination of this many opcodes and operands in 16 bits led the architects to provide only eight easyto-use registers—just like Thumb—whereas the other embedded RISCs offer about 16 registers. Since the hardware must include the full 32 registers of the 32-bit ISA mode, MIPS-16 includes move instructions to copy values between the eight MIPS-16 registers and the remaining 24 registers of the full MIPS architecture. To reduce pressure on the eight visible registers, the stack pointer is considered a separate register. MIPS-16 includes a variety of separate opcodes to do data transfers using SP as a base register and to increment SP: LWSP, LDSP, SWSP, SDSP, ADJSP, DADJSP, ADDIUSPD, and DADDIUSP. To fit within the 16-bit limit, immediate fields have generally been shortened to five to eight bits. MIPS-16 provides a way to extend its shorter immediates into the full width of immediates in the 32-bit mode. Borrowing a trick from the Intel 8086, the EXTEND instruction is really a 16-bit prefix that can be prepended to any MIPS16 instruction with an address or immediate field. The prefix supplies enough bits to turn the 5-bit field of data transfers and 5- to 8-bit fields of arithmetic immediates into 16-bit constants. Alas, there are two exceptions. ADDIU and DADDIU start with 4-bit immediate fields, but since EXTEND can only supply 11 more bits, the wider immediate is limited to 15 bits. EXTEND also extends the 3-bit shift fields into 5-bit fields for shifts. (In case you were wondering, the EXTEND prefix does not need to start on a 32-bit boundary.) To further address the supply of constants, MIPS-16 added a new addressing mode! PC-relative addressing for load word (LWPC) and load double (LDPC) shifts an 8-bit immediate field by two or three bits, respectively, adding it to the PC with the lower two or three bits cleared. The constant word or doubleword is then loaded into a register. Thus 32-bit or 64-bit constants can be included with MIPS-16 code, despite the loss of LIU to set the upper register bits. Given the new addressing mode, there is also an instruction (ADDIUPC) to calculate a PC-relative address and place it in a register. MIPS-16 differs from the other embedded RISCs in that it can subset a 64-bit address architecture. As a result it has 16-bit instruction-length versions of 64-bit

E-41

E-42

Appendix E A Survey of RISC Architectures

data operations: data transfer (LD, SD, LWU), arithmetic operations (DADDU/IU, DSUBU, DMULT/U, DDIV/U), and shifts (DSLL/V, DSRA/V, DSRL/V). Since MIPS plays such a prominent role in this book, we show all the additional changes made from the MIPS core instructions in going to MIPS-16: ■

Drop of signed arithmetic instructions—Arithmetic instructions that can trap were dropped to save opcode space: ADD, ADDI, SUB, DADD, DADDI, DSUB.



Drop of immediate logical instructions—Logical immediates are gone too: ANDI, ORI, XORI.



Branch instructions pared down—Comparing two registers and then branching did not fit, nor did all the other comparisons of a register to zero. Hence these instructions didn’t make it either: BEQ, BNE, BGEZ, BGTZ, BLEZ, and BLTZ. As mentioned in Section E.3, to help compensate MIPS-16 includes compare instructions to test if two registers are equal. Since compare and set on less than set the new T register, branches were added to test the T register.



Branch distance—Since instructions are 16 bits wide, the branch address is shifted by one instead of by two.



Delayed branches disappear—The branches take effect before the next instruction. Jumps still have a one-slot delay.



Extension and distance for data transfer offsets—The 5-bit and 8-bit fields are zero-extended instead of sign-extended in 32-bit mode. To get greater range, the immediate fields are shifted left one, two, or three bits depending on whether the data is halfword, word, or doubleword. If the EXTEND prefix is prepended to these instructions, they use the conventional signed 16-bit immediate of the 32-bit mode.



Extension of arithmetic immediates—The 5-bit and 8-bit fields are zeroextended for set on less than and compare instructions, for forming a PCrelative address, and for adding to SP and placing the result in a register (ADDIUSP, DADDIUSP). Once again, if the EXTEND prefix is prepended to these instructions, they use the conventional signed 16-bit immediate of the 32-bit mode. They are still sign-extended for general adds and for adding to SP and placing the result back in SP (ADJSP, DADJSP). Alas, code density and orthogonality are strange bedfellows in MIPS-16!



Redefining shift amount of 0—MIPS-16 defines the value 0 in the 3-bit shift field to mean a shift of 8 bits.



New instructions added due to loss of register 0 as zero—Load immediate, negate, and not were added, since these operations could no longer be synthesized from other instructions using r0 as a source.

E.17

E.17

E-43

Concluding Remarks

Concluding Remarks

This appendix covers the addressing modes, instruction formats, and all instructions found in ten RISC architectures. Although the later sections of the appendix concentrate on the differences, it would not be possible to cover ten architectures in these few pages if there were not so many similarities. In fact, we would guess that more than 90% of the instructions executed for any of these architectures would be found in Figures E.3.5 through E.3.11. To contrast this homogeneity, Figure E.17.1 gives a summary for four architectures from the 1970s in a format similar to that shown in Figure E.1.1. (Imagine trying to write a single chapter in this style for those architectures!) In the history of computing, there has never been such widespread agreement on computer architecture.

IBM 360/370

Intel 8086

Motorola 68000

DEC VAX

Date announced

1964/1970

1978

1980

Instruction size(s) (bits)

16, 32, 48

8, 16, 24, 32, 40, 48

16, 32, 48, 64, 80

1977 8, 16, 24, 32, . . . , 432

Addressing (size, model)

4 + 16 bits, segmented No

24 bits, flat

32 bits, flat

Data aligned?

24 bits, flat/31 bits, flat Yes 360/No 370

16-bit aligned

No

Data addressing modes

2/3

5

9

= 14

Protection

Page

None

Optional

Page

Page size

2 KB & 4 KB



0.25 to 32 KB

0.5 KB

I/O

Opcode

Opcode

Memor y mapped

Memor y mapped

Integer registers (size, model, number)

16 GPR × 32 bits

8 dedicated data × 16 bits

8 data and 8 address × 32 bits

15 GPR × 32 bits

Separate floating-point registers

4 × 64 bits

Optional: 8 × 80 bits

Optional: 8 × 80 bits

0

Floating-point format

IBM (floating hexadecimal)

IEEE 754 single, double, extended

IEEE 754 single, double, extended

DEC

FIGURE E.17.1 Summary of four 1970s architectures. Unlike the architectures in Figure E.1.1, there is little agreement between these architectures in any category.

This style of architecture cannot remain static, however. Like people, instruction sets tend to get bigger as they get older. Figure E.17.2 shows the genealogy of these instruction sets, and Figure E.17.3 shows which features were added to or deleted from generations of desktop RISCs over time. As you can see, all the desktop RISC machines have evolved to 64-bit address architectures, and they have done so fairly painlessly.

E-44

Appendix E A Survey of RISC Architectures

1960 CDC 6600 1963 1965

IBM ASC 1968

1970

1975

1980

Berkeley RISC-1 Stanford MIPS 1981 1982

ARM2 1987

1995

America 1985

ARM1 1985

1985

1990

IBM 801 1975

Cray-1 1976

SuperH 1992

ARM3 1990

Thumb ARMv4 1995 M32R 1995 1997

SPARCv8 1987

SPARCv9 1994 MIPS-16 1996

MIPS I 1986

PA-RISC 1986

MIPS II Digital PRISM 1988 1989 PA-RISC 1.1 MIPS III 1990 Alpha 1992 1992 MIPS IV 1994 MIPS V 1996

RT/PC 1986

Power1 1990

Power2 PowerPC 1993 1993 Alphav3 1996

PA-RISC 2.0 1996

2000 2002

MIPS-32 2002

MIPS-64 2002

FIGURE E.17.2 The lineage of RISC instruction sets. Commercial machines are shown in plain text and research machines in bold. The CDC 6600 and Cray-1 were load-store machines with register 0 fixed at 0, and with separate integer and floating-point registers. Instructions could not cross word boundaries. An early IBM research machine led to the 801 and America research projects, with the 801 leading to the unsuccessful RT/PC and America leading to the successful Power architecture. Some people who worked on the 801 later joined Hewlett-Packard to work on the PA-RISC. The two university projects were the basis of MIPS and SPARC machines. According to Furber [1996], the Berkeley RISC project was the inspiration of the ARM architecture. While ARM1, ARM2, and ARM3 were names of both architectures and chips, ARM version 4 is the name of the architecture used in ARM7, ARM8, and StrongARM chips. (There are no ARMv4 and ARM5 chips, but ARM6 and early ARM7 chips use the ARM3 architecture.) DEC built a RISC microprocessor in 1988 but did not introduce it. Instead, DEC shipped workstations using MIPS microprocessors for three years before they brought out their own RISC instruction set, Alpha 21064, which is very similar to MIPS III and PRISM. The Alpha architecture has had small extensions, but they have not been formalized with version numbers; we used version 3 because that is the version of the reference manual. The Alpha 21164A chip added byte and halfword loads and stores, and the Alpha 21264 includes the MAX multimedia and bit count instructions. Internally, Digital names chips after the fabrication technology: EV4 (21064), EV45 (21064A), EV5 (21164), EV56 (21164A), and EV6 (21264). “EV” stands for “extended VAX.”

E-45

Further Reading

PA-RISC Feature

SPARC

MIPS

1.0

1.1

2.0

v8

v9

III

IV

Interlocked loads

X





X



+





X



Load-store FP double

X





X



+





X





Semaphore

X





X



+





X





Square root

X





X



+





Single precision FP ops

X





X









Memory synchronize

X





X



Coprocessor

X





X



Base + index addressing

X





X





Equiv. 32 64-bit FP registers Annulling delayed branch

X





Branch register contents

X





+



Big/little endian

X

+











X

+

X

Prefetch data into cache

+

+

64-bit addressing/int. ops

+

+



+

Load-store FP quad

+



+

X

V

1





X



PC ”

” +

X





+

X







X





+

















+





X





+

X





+

X





+







+ X

2

+

+

X

+ X

” +

+

Fused FP mul/add Multimedia suppor t

X

+

Conditional move

String instructions

X



+

+

II

+

Branch prediction bit

32-bit multiply, divide

I

Power





+



X





X





X

FIGURE E.17.3 Features added to desktop RISC machines. X means in the original machine, ⫹ means added later, ” means continued from prior machine, and — means removed from architecture. Alpha is not included, but it added byte and word loads and stores, and bit count and multimedia extensions, in version 3. MIPS V added the MDMX instructions and paired single floating-point operations.

We would like to thank the following people for comments on drafts of this appendix: Professor Steven B. Furber, University of Manchester; Dr. Dileep Bhandarkar, Intel Corporation; Dr. Earl Killian, Silicon Graphics/MIPS; and Dr. Hiokazu Takata, Mitsubishi Electric Corporation.

Further Reading Bhandarkar, D. P. [1995]. Alpha Architecture and Implementations, Newton, MA: Digital Press. Darcy, J. D., and D. Gay [1996]. “FLECKmarks: Measuring floating point performance using a full IEEE compliant arithmetic benchmark,” CS 252 class project, U.C. Berkeley (see HTTP.CS.Berkeley.EDU/⬃darcy/ Projects/cs252/). Digital Semiconductor [1996]. Alpha Architecture Handbook, Version 3, Maynard, MA: Digital Press, Order number EC-QD2KB-TE (October).

E-46

Appendix E A Survey of RISC Architectures

Furber, S. B. [1996]. ARM System Architecture, Harlow, England: Addison-Wesley. (See www.cs.man.ac.uk/ amulet/publications/books/ARMsysArch.) Hewlett-Packard [1994]. PA-RISC 2.0 Architecture Reference Manual, 3rd ed. Hitachi [1997]. SuperH RISC Engine SH7700 Series Programming Manual. (See www.halsp.hitachi.com/tech_ prod/ and search for title.) IBM [1994]. The PowerPC Architecture, San Francisco: Morgan Kaufmann. Kane, G. [1996]. PA-RISC 2.0 Architecture, Upper Saddle River, NJ: Prentice Hall PTR. Kane, G., and J. Heinrich [1992]. MIPS RISC Architecture, Englewood Cliffs, NJ: Prentice Hall. Kissell, K. D. [1997]. MIPS16: High-Density for the Embedded Market. (See www.sgi.com/MIPS/arch/MIPS16/ MIPS16.whitepaper.pdf.) Magenheimer, D. J., L. Peters, K. W. Pettis, and D. Zuras [1988]. “Integer multiplication and division on the HP precision architecture,” IEEE Trans. on Computers 37:8, 980–90. MIPS [1997]. MIPS16 Application Specific Extension Product Description. (See www.sgi.com/MIPS/arch/ MIPS16/mips16.pdf.) Mitsubishi [1996]. Mitsubishi 32-Bit Single Chip Microcomputer M32R Family Soft ware Manual (September). Muchnick, S. S. [1988]. “Optimizing compilers for SPARC,” Sun Technology 1:3 (Summer), 64–77. Seal, D. Arm Architecture Reference Manual, 2nd ed, Morgan Kaufmann, 2000. Silicon Graphics [1996]. MIPS V Instruction Set. (See www.sgi.com/MIPS/arch /ISA5/#MIPSV_indx.) Sites, R. L., and R. Witek (eds.) [1995]. Alpha Architecture Reference Manual, 2nd ed. Newton, MA: Digital Press. Sloss, A. N., D. Symes, and C. Wright, ARM System Developer’s Guide, San Francisco: Elsevier Morgan Kaufmann, 2004. Sun Microsystems [1989]. The SPARC Architectural Manual, Version 8, Part No. 800-1399-09, August 25. Sweetman, D. See MIPS Run, 2nd ed, Morgan Kaufmann, 2006. Taylor, G., P. Hilfinger, J. Larus, D. Patterson, and B. Zorn [1986]. “Evaluation of the SPUR LISP architecture,” Proc. 13th Symposium on Computer Architecture (June), Tokyo. Ungar, D., R. Blau, P. Foley, D. Samples, and D. Patterson [1984]. “Architecture of SOAR: Smalltalk on a RISC,” Proc. 11th Symposium on Computer Architecture (June), Ann Arbor, MI, 188–97. Weaver, D. L., and T. Germond [1994]. The SPARC Architectural Manual, Version 9, Englewood Cliffs, NJ: Prentice Hall. Weiss, S., and J. E. Smith [1994]. Power and PowerPC, San Francisco: Morgan Kaufmann.

More Documents from "lorenzo"