
The Intel 8087 chip: floating-point addition and subtraction
One complication with subtraction is that the result can be negative. If that occurs,
以下正文同步自 Hacker News,版权归原站所有,已转换为易读排版。
One complication with subtraction is that the result can be negative. If that occurs, the significand is made positive, and the sign of the result is flipped. Subtraction can also result in zero. In this case, normalization cannot be performed; instead, a special value +0 or -0 is returned as appropriate.
In this section, I'll explain some features of the 8087 that play important roles in the microcode implementation of addition and subtraction.
The 8087 supports a variety of data types, but internally, the 8087 stores numbers in an 80-bit format called "temporary real". A temporary real number has a sign bit, a 15-bit exponent, and a 64-bit significand. The 15-bit exponent allows numbers that are absurdly large, over 104932. The 64-bit significand supports extremely accurate numbers. The exponent is stored with a "bias" of 16383 added to it. Thus, the stored exponent is always positive, even if the real exponent is negative. For instance, an exponent of 1 is stored as 0x4000, and an exponent of -16 is stored as 0x3fef.
The 80-bit temporary real format and the register formats. The dot indicates the binary point, analogous to the decimal point.
The 8087 has eight internal registers, organized as a stack. The programmer uses these registers when programming the 8087. Each register holds an 80-bit floating-point number. To optimize performance, each value in the register stack also has an associated "tag" value: zero, valid, special, or empty. A value of zero is tagged as zero. A "normal" floating-point value is tagged as valid. If the value is infinity, Not a Number (NaN), or a denormalized value, then it is tagged as special. Finally, an empty tag indicates that a register does not hold a value; this allows detection of stack underflow (reading an empty register) or stack overflow (storing to a non-empty register).
The 8087 also has temporary registers that it uses internally. tmpA and tmpB are 80-bit registers, along with two tag bits. However, tmpC is different: it doesn't have a sign, exponent, or tag. Moreover, the significand is 68 bits; tmpC has an additional bit to prevent overflow and three additional low-order bits for rounding, known as guard, round, and sticky.
In the microcode, each 16-bit micro-instruction performs one low-level operation. Many of the operations move data from one register to another. Other operations provide conditional jumps, subroutine calls, and returns.
The heart of the 8087's processing is the 69-bit integer adder, which I discussed here. (The adder has more than 64 bits because it has three rounding bits and a couple of additional high-order bits for overflow and multiplication.) Integer addition takes two micro-instructions. The first micro-instruction specifies the first argument to add and selects various options such as carry and rounding. (For subtraction, an option complements the second argument.) The second argument to the adder always comes from the B register, which confusingly is unrelated to the tmpB register. The result of the addition operation is placed in a special register called the sum register. A second micro-instruction (which may be many instructions after the first) moves the result from the sum register to a destination.
Floating-point also makes extensive use of the bit shifter, which shifts a 68-bit value by an arbitrary number of positions, left or right; I wrote about it here. Like the adder, a bit shift operation takes two micro-instructions: the first moves a value to the shifter and configures the direction and amount of shift. The second micro-instruction moves the result from the shifter to a destination.
The 8087 implements almost all of its operations in microcode. The basic math operations (addition, subtraction, multiplication, and division) start by executing the same microcode (at decimal address #0001. The microcode moves the two arguments into temporary registers tmpA and tmpB. Normally, these arguments are taken from the stack registers. The arguments can also be taken from memory, but I won't discuss that here.
Next, the microcode does a micro-subroutine call to handle the specific operation type. The 8087's microcode engine has a tiny ROM that holds a table of addresses, targets for microcode jumps or subroutine calls. Target 0 is special, however; it has three different addresses for multiplication, division, or addition/subtraction. (This is implemented by special-case circuitry in the ROM. When you're designing the chip, you can do weird, hacky optimizations like this if you want.)
The addition/subtraction micro-subroutine starts at address #0232; this microcode is shown below. The control flow is complicated, so I've put a flowchart in the footnotes.5 The microcode first handles the zero cases 0+0 or 0-0. Next, it subtracts the exponents of the two arguments. If the first argument has a smaller exponent (i.e., the result is negative), the arguments are swapped so the argument with the larger argument is first. (The 8087 keeps track of the sign of the result in the "sign latch"; if exchanging the arguments would change the sign, the latch is toggled at #0244.)
#0232 jmp #0236 if not tmpA:tag ZERO Check for 0+0 #0233 jmp #0236 if not tmpB:tag ZERO #0234 return if not unsigned sub 0+0 same signs #0235 jmp #0263 handle 0+0 different signs #0236 tmpA sign -> sign latch nonzero: tmpA sign to sign latch #0237 tmpB:exp -> Breg #0238 adder: tmpA:exp - Breg cin=1 compare exponents #0239 jmp #0245 if adder pos #0240 sumreg:frac -> Breg swap if tmpB's exponent larger #0241 adder: zero - Breg cin=1 #0242 xchg tmp #0243 jmp #0245 if not unsigned sub #0244 !sign latch -> sign latch toggle sign if swapping matters #0245 xchg tmp continue #0246 shift zero R 0 bytes, 0 bits zero #0247 call CREATE_DENORM shift tmpB to match exponents #0248 xchg tmp #0249 zero -> tmpB:exp store exp adjustment in tmpB:exp #0250 jmp #0269 if not unsigned sub base addition? #0251 adder: tmpA:frac - Breg cin=1, roundmode tmp A - tmpB #0252 jmp #0256 if adder pos #0253 sumreg:frac,rnd -> Breg/rnd negate frac #0254 adder: zero - Breg cin=1, roundmode #0255 !sign latch -> sign latch toggle sign #0256 jmp #0275 if Sum register is zero jump if result zero #0257 shift R -> tmpB:frac orig tmpB, from CREATE_DENORM #0258 jmp #0282 if tmpA top bit tmpA not denorm? #0259 jmp #0282 if tmpB:frac[63] tmpB not denorm? #0260 jmp #0289 skip normalization if both denorms #0261 tmpA:exp -> expConv handle zero #0262 jmp #0264 if not tmpA top bit #0263 zero -> tmpA handle zero cases #0264 0 -> tmpA sign +0 #0265 zero -> tmpA:frac #0266 return if not round chop #0267 1 -> tmpA sign return -0 #0268 return #0269 adder: tmpA:frac + Breg cin=0, roundmode base addition: tmpA + tmpB #0270 jmp #0289 if not sumreg[64] check for overflow #0271 shift 1 sumreg:sign,frac,rnd R shift right 1 bit to normalize #0272 shift R -> sumreg:frac,sign,rnd #0273 expAdj++ increment exp adjustment #0274 jmp #0289 wrap up addition #0275 shift sumreg:frac,rnd L 0 bytes, 3 bits zero? shift to get rounding bits #0276 shift L -> sumreg:frac #0277 jmp #0261 if Sum register is zero jump if no rounding bits #0278 zero -> sumreg:frac clear result #0279 jmp #0289 if not tmpA top bit denorm? #0280 expAdj-- if not denorm, adjust exp down #0281 1 -> sumreg:frac,rnd set lowest bit in result #0282 lzcount -> shiftcountH leading zero adjust: handle bytes #0283 shift sumreg:frac,rnd L count byte sticky #0284 shift L -> sumreg:frac,rnd #0285 lzcount -> shiftcountL handle leading zero bits #0286 shift sumreg:frac,rnd L count bit sticky #0287 shift L -> sumreg:frac,rnd #0288 shiftcount -> tmpB:exp remember the bit shift amount #0289 sign latch -> tmpA sign store sign #0290 call ADJUST_PRECISION if not FBLD/FBSTP adjust precision if necessary #0291 sumreg:frac -> tmpA:frac store adjusted frac part #0292 tmpB:exp -> Breg get exp #0293 adder: tmpA:exp - Breg cin=0 adjust by Breg #0294 expAdj -> Breg #0295 adder: sumreg:frac + Breg cin=0 adjust by expAdj #0296 sumreg:frac -> expConv exp in expConv to check for overflow, etc #0297 sumreg:frac -> tmpA:exp store exp in tmpA #0298 return done
At address #0247, the important alignment step takes place, shifting tmpB so the binary point lines up with tmpA. This is done by the microcode subroutine CREATE_DENORM. The number of positions to shift tmpB is passed in the sum register; the difference between the two exponents was calculated in the previous subtraction. (If the difference was negative, it was complemented, so the value passed in is positive.)
Typically, the exponent of the result will be the same as the exponent of the (larger) value, but not always. Changes to the exponent are tracked in two places. First, a special two-bit register that I call the exponent adjustment (expAdj) holds adjustments due to one-bit shifts left or right. The hardware initializes the expAdj register to 1 so there is room for a decrement. Larger exponent changes are tracked in the tmpB register's exponent field; the microcode initializes it to 0 at #0249.
At address #0250, the microcode makes a key decision: is the low-level operation unsigned addition or unsigned subtraction? Depending on the signs involved, the high-level operation (addition or subtraction) may not match the low-level operation. For example, for 2+3, -2-3, -2+(-3), and so forth, the two unsigned numbers are added; the result is 5, either positive or negative. But for -2+3, 2-3, 3-2, and so forth, the two unsigned numbers are subtracted; the result is 1, either positive or negative. I'll use "unsigned addition" to refer to the former cases, where the unsigned magnitudes are added, and "unsigned subtraction" to refer to the latter cases, where the unsigned magnitudes are subtracted. The key point is that an addition instruction may result in unsigned addition or subtraction, and the same with a subtraction instruction.
The 8087 has specialized circuitry to determine whether the low-level unsigned operation is addition or subtraction, allowing this to be tested with a conditional. The tmpA and tmpB values have sign bits of 1 for negative, 0 for positive. If you assign 1 to subtraction and 0 to addition, then tmpAsign⊕tmpBsign⊕op yields 1 if the unsigned operation is subtraction and 0 if the unsigned operation is addition. The 8087 also has a conditional to test if the signs are the same or different.
Since the unsigned addition path is simpler, I'll start with it, at address #0269. The steps are essentially the same as described earlier: aligning the numbers, adding them, and normalizing. First, the smaller number, in tmpB, is shifted right until the exponents match. Then the (unsigned) values are added. If there is a carry from the top bit, the result is shifted right at #0271. Because the result is shifted right one bit, the special two-bit expAdj counter is incremented to track the required exponent adjustment.
At #0289, the result's sign is updated with the value in the sign register. The 8087 supports three sizes of values: 80-bit temporary reals, 64-bit reals, or 32-bit reals. For the smaller sizes, the microcode subroutine ADJUST_PRECISION trims the value to the appropriate size. (This is not as trivial as it sounds, since rounding up the result could require an adjustment to the result's exponent, recorded in the expAdj register.) Finally, the result's exponent is calculated from the initial exponent (in tmpA), subtracting the adjustment value in tmpB's exponent (normally 0), adding the adjustment value in the expAdj register, and subtracting 1 (because expAdj starts at 1). The exponent is stored in the result but also stored in the "exponent converter", a circuit that evaluates the exponent to detect overflow or underflow.
At this point, the addition/subtraction micro-subroutine returns to the common microcode. The common microcode checks if there was an overflow or underflow, indicating the value doesn't fit in a float. In this case, the subroutine NONNORMAL_RESULT is calle
正文由 探How 从来源站点 RSS 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。