Microcode in de Intel 8087 floating-point chip: de scale-instructie

Dit veranderde toen Intel in 1980 de 8087 floating-point coprocessor introduceerde, die was ontworpen om zo nauwkeurig mogelijk te zijn, zelfs in extreme uitzonderingsgevallen. De 8087 werd populair omdat hij in de IBM PC kon worden geïnstalleerd, waardoor floating-point operaties in applicaties variërend van spreadsheets tot CAD tot wel 100 keer sneller werden. Maar belangrijker nog: de 8087 werd de floating-point standaard die door de meeste computers van vandaag wordt gebruikt.

De 8087 implementeerde zijn instructies in complexe low-level code, genaamd microcode. Als onderdeel van het Opcode Collective ben ik deze microcode aan het reverse-engineeren. In dit artikel onderzoek ik de microcode voor een van de instructies van de 8087—FSCALE—en beschrijf ik hoe deze werkt.

De FSCALE (Floating-point Scale) instructie biedt een snelle manier om een getal te schalen met een macht van twee, wat veel sneller is dan een volledige vermenigvuldiging. Hoewel FSCALE in eerste instantie triviaal lijkt, is dat niet het geval: FSCALE gebruikt meer dan 140 micro-instructies en drie niveaus van subroutine-aanroepen om vele speciale gevallen af te handelen. De FSCALE-microcode illustreert interessante onderdelen van de 8087, zoals de shifter, de adder en de exponent-converter, en onthult zelfs een verborgen functie van de chip.

De hardware van de 8087

Om de microcode te verkennen, is de 8087 chip geopend en met een microscoop in hoge resolutie gefotografeerd. De centrale componenten zijn:

  • Microcode ROM: Bevat de 1648 micro-instructies die de chip aansturen.
  • Microcode engine: Verwerkt de microcode en handelt sprongen en subroutine-aanroepen af.
  • Datapath: Het circuit dat de floating-point berekeningen uitvoert. Dit is verdeeld in een 16-bit datapath voor de exponent en een 64-bit datapath voor de significand (het fractionele deel).

Belangrijke componenten in het datapath

  • Exponent ROM: Bevat diverse constanten.
  • Exponent converter: Een gespecialiseerd circuit dat exponenten analyseert, speciale waarden detecteert en converteert tussen exponent-formaten.
  • Shifter: Een groot component waarmee een 64-bit waarde willekeurige aantallen bits naar links of rechts kan worden verschoven.
  • Adder: Het hart van de berekeningen; wordt in een loop gebruikt voor vermenigvuldiging, deling en worteltrekkingen.
  • Registers: Het B-register houdt één input voor de adder vast, terwijl het sum-register de output bevat. Daarnaast zijn er acht stack-registers en tijdelijke registers voor floating-point getallen.

Technische details van de 8087

Registerstack en Tags

De programmeur slaat waarden op in acht interne registers, georganiseerd als een stack. Elk register bevat een 80-bit floating-point getal. Om de prestaties te optimaliseren, heeft elke waarde een bijbehorende "tag" die onzichtbaar is voor de programmeur. Een tag labelt een waarde als:

  • Valid: Een normaal floating-point getal.
  • Special: Oneindig ($\infty$), Not a Number (NaN), of een gedenormaliseerde waarde.
  • Zero: Een nulwaarde.
  • Empty: Het register is leeg (bijvoorbeeld nadat een waarde is gepopt).

De chip heeft ook tijdelijke registers: tmpA, tmpB (beiden 80-bit met tag-bits) en tmpC (alleen een 64-bit significand).

Het 80-bit "Temporary Real" formaat

Intern wordt alles opgeslagen als een 80-bit floating-point getal. Een getal bestaat uit drie delen:

  1. Voorteken-bit (sign bit)
  2. 15-bit exponent: Opgeslagen met een "bias" van 16383 (waardoor de opgeslagen exponent altijd positief is). Het bereik is van -16382 tot 16383.
  3. 64-bit significand: Een binair getal in de vorm 1.bbb..., waarbij de leidende 1 expliciet aanwezig is in het interne formaat.

Speciale waarden en exceptions

De 8087 ondersteunt speciale exponenten voor waarden zoals nul, oneindigheid en NaN. Daarnaast zijn er gedenormaliseerde en ongenormaliseerde waarden voor extreem kleine getallen waarbij de significand geen leidende 1 heeft.

Het systeem hanteert zes typen exceptions:

  • Invalid operation: Bijvoorbeeld $0/0$ of $\infty - \infty$.
  • Overflow: Waarde is te groot.
  • Underflow: Waarde is te klein.
  • Divide-by-zero: Delen door nul (exclusief $0/0$).
  • Denormalized operand: Resultaat is te klein voor een normale waarde, maar kan als denormal worden opgeslagen.
  • Precision: Waarde kan niet exact worden weergegeven en moet worden afgerond.

Exceptions kunnen worden "gemaskerd" (de chip gaat door met een benadering, zoals NaN of $\infty$) of "unmasked" (de chip stuurt een interrupt naar de 8086 processor).

De microcode van de 8087

Een instructie vereist honderden interne stappen, geïmplementeerd in microcode. Elke micro-instructie is 16 bits breed. De eerste drie bits bepalen het type:

  • Transfer operatie: Verplaatst data tussen interne registers.
  • Shift operatie: Gebruikt de barrel shifter.
  • Adder operatie: Stuurt de adder aan (kan ook aftrekken).
  • Miscellaneous: Stack pointer operaties, tag-modificatie, exceptions en subroutine-returns.
  • Far jump/call: Springt naar een doeladres in een vaste lijst.
  • Local jump: Een relatieve sprong naar een nabijgelegen micro-instructie.

De FSCALE-microcode

De FSCALE-instructie schaalt een floating-point getal met $2^N$ (voor een integer $N$) door $N$ op te tellen bij de exponent van het getal. Dit is aanzienlijk sneller dan een volledige vermenigvuldiging.

Algemene werking

De microcode begint op decimaal adres 748. De globale stappen zijn:

  1. Controle op nul-argumenten.
  2. Conversie van het schaal-argument naar een integer.
  3. Optellen van deze waarde bij de exponent.
  4. Afhandeling van overflow of underflow.

De microcode-routine:

FSCALE:
#0748 st(0) -> tmpA        Input argument van top van stack
#0749 jmp #0776 if tmpA:tag ZERO Stop als 0
#0750 stackPtr++
#0751 st(0) -> tmpB        Schaal argument van stack(1)
#0752 stackPtr--
#0753 jmp #0776 if tmpB:tag ZERO Stop als 0
#0754 expconst 0x403e      Const 403e: exp shift voor conversie naar int
#0755 jmp #0763 if not tmp empty/special/div
#0756 call SPECIAL_TMPS    Speciale afhandeling
#0757 jmp #0762 if flag
#0758 jmp #0761 if not tmpB:tag SPECIAL
#0759 except:invalid       Invalid exception, gebruik NaN
#0760 NaN -> tmpA
#0761 jmp #0776 if intr
#0762 jmp #0775 if expConv[0] Return tmpA als expConv set, anders doorgaan
#0763 tmpB:exp -> Breg     Normaal pad
#0764 tmpB:sign,exp -> expConv ExpConv test teken van tmpB
#0765 expConst -> tmpC     Const 403e
#0766 adder: tmpC - Breg cin=1 403e-exp is shift amount voor int conversie
#0767 sumreg:frac -> shiftcount Sla op in shifter control
#0768 shift tmpB:frac R count byte bit Voer shift uit
#0769 shift R -> Breg      Breg bevat schaal argument als int
#0770 jmp #0777 if neg     Negatieve Breg vereist aparte afhandeling
#0771 adder: tmpA:exp + Breg cin=0 Voeg schaal toe aan exponent
#0772 sumreg:frac -> expConv Resultaat in expConv voor controle
#0773 sumreg:frac -> tmpA:exp Update exponent met som
#0774 call NONNORMAL_RESULT if not exp normal Handel overflow/underflow af
#0775 tmpA -> st(0)        Sla resultaat terug op stack
#0776 RNI                  Klaar: Volgende instructie
#0777 adder: tmpA:exp - Breg cin=1 Trek Breg af
#0778 jmp #0772            Ga door met verwerken

Conversie van float naar integer

Om een floating-point getal om te zetten naar een integer, moet de significand "right justified" worden gemaakt. Als de exponent $n$ is, wordt de significand verschoven met $63-n$ bits. Omdat de exponent een bias van 16383 heeft, is de verschuiving $0x403e - \text{exp}$ bits. Dit verklaart de constante 0x403e in de code.

Omgang met lege of speciale argumenten

Wanneer een argument een lege stack-locatie raakt of een speciale waarde is ($\infty$, denorm, NaN), wordt de subroutine SPECIAL_TMPS aangeroepen.

Deze routine is complex omdat hij prioriteiten moet stellen (bijv. wanneer één argument leeg is en het andere een denorm). Een opmerkelijk detail is dat wanneer beide argumenten NaN zijn, de microcode de twee NaN-waarden vergelijkt en de grootste teruggeeft. Hoewel dit vreemd lijkt, is het een gedocumenteerde feature; NaN is namelijk geen enkele waarde, maar een familie van waarden die door programmeurs gebruikt kan worden om aan te geven waar een probleem is ontstaan.

De SPECIAL_TMPS subroutine (fragment):

SPECIAL_TMPS (J5):
#1484 call SPECIAL_VAL if tmpA:tag SPECIAL Handel speciaal in tmpA/tmpB
#1485 xchg tmp
#1486 call SPECIAL_VAL if tmpA:tag SPECIAL Handel tmpB speciaal
#1487 xchg tmp
#1488 1 -> flag            Standaard Flag=1
#1489 jmp #1500 if not tmp empty/special/div 0 -> expConv als tmps oké
...
#1518 tmpB:frac -> Breg    Beide args zijn NaN, zoek grootste
#1519 adder: tmpA:frac - Breg cin=1
#1520 jmp #1522 if adder sign Kijk of tmpA < tmpB
#1521 tmpB -> tmpA         Neem grootste
#1522 except:invalid       Invalid exception
...
#1526 return

Omgang met niet-normale resultaten

Bij extreme schaling kan overflow of underflow optreden. De 8087 ondersteunt vier afrondingsmodi (round to nearest, round down, round up, chop toward zero). Afhankelijk van de modus kan een overflow resulteren in $\infty$ of het grootste mogelijke floating-point getal.

De subroutine NONNORMAL_RESULT handelt dit af. Interessant is dat voor overflow en underflow de microcode de taak delegeert aan hardware: de "exponent converter" bepaalt of er een exception moet worden getriggerd.

Interrupt-bias

Wanneer een overflow of underflow unmasked is en een interrupt optreedt, voegt de 8087 de constante 0x6000 toe aan of af van de exponent. Hierdoor past de waarde in het exponent-veld, waardoor de interrupt-handler de exacte waarde kan reconstrueren door de constante weer weg te rekenen.

Hulp-subroutines

Het maken van een denormaal getal (CREATE_DENORM)

Denormale getallen zijn getallen die kleiner zijn dan reguliere floats; ze breken de regel dat de significand met een 1 moet beginnen. Dit breidt het bereik enorm uit, maar is trager.

Om afronding correct uit te voeren, gebruikt de 8087 drie extra bits: de Guard bit, de Round bit, en de Sticky bit. De Sticky bit is de logische OR van alle resterende bits in de "tail". Om deze bits te genereren, verschuift de chip de waarde eerst naar rechts en vervolgens weer naar links om de bits die verloren zouden gaan te isoleren.

Precisie aanpassen (ADJUST_PRECISION)

De 8087 voert alle berekeningen uit met 80-bit "temporary reals". Aan het einde van een instructie converteert ADJUST_PRECISION het resultaat naar de gewenste precisie (short, long of temporary real) volgens de geselecteerde afrondingsmodus.

Een ongedocumenteerde feature is dat een sprong met de "round up" conditie ook het condition code register (CC1) bijwerkt, om aan te geven of het resultaat is afgerond. Dit werd door Intel pas in 1987 officieel gedocumenteerd voor de 387SX chip.

Conclusies

De 8087 was revolutionair omdat hij was ontworpen voor maximale wiskundige nauwkeurigheid, mede dankzij expert William Kahan. Dit leidde uiteindelijk tot de IEEE 754 standaard.

De complexiteit van de chip is enorm: drie verschillende float-groottes, vier integer-groottes, vier afrondingsmodi, en een uitgebreid systeem van exceptions en speciale waarden. Deze complexiteit is opgelost via een combinatie van gespecialiseerde hardware-circuits en complexe microcode. Ter illustratie: een software-emulator van de 8087 voor de 8086 nam 16 KB in beslag, terwijl de hardware-implementatie in de 8087 slechts 3,3 KB microcode vereiste.

***

Notities en referenties

  1. Exponent Converter: Deze is complex omdat de 8087 drie verschillende formaten heeft met exponent-velden van 8, 11 en 15 bits, elk met een eigen bias.
  2. Significand breedte: De significand is nominaal 64 bits, maar in het datapath zijn er extra bits voor afronding (Guard, Round, Sticky), waardoor de shifter 68 bits en de adder 69 bits breed is.
  3. Tags: Hoewel onzichtbaar voor de programmeur, kunnen tags worden uitgelezen via een "tag word" wanneer de status van de 8087 naar het geheugen wordt gedumpt.
  4. Impliciete 1: In externe representaties is de leidende 1 impliciet, maar intern in de 80-bit representatie is deze expliciet om berekeningen te vereenvoudigen.
  5. Bias: Het gebruik van een bias zorgt ervoor dat floating-point getallen lexicografisch als signed integers vergeleken kunnen worden om de grootste waarde te bepalen.
  6. FSCALE en Nul: De microcode laat zien dat het schalen van 0, of schalen met 0, de waarde onveranderd laat. Strikt genomen zou $0 \times 2^\infty$ een NaN moeten opleveren, maar de ontwerpers kozen hier waarschijnlijk voor een korter pad.
  7. Systeem-bussen: De 8087 heeft aparte bussen voor de exponent en significand. Een 16-bit gateway maakt het mogelijk om de exponent naar de significand-bus te kopiëren voor bewerkingen in de adder.
  8. Aftrekken: Aftrekken gebeurt door de waarde in het B-register te inverteren en een carry-in van 1 te gebruiken (two's-complement).
  9. Signed Addition: Omdat waarden als een teken-bit en een unsigned waarde worden opgeslagen (niet in two's-complement), kan de adder geen signed addition direct uitvoeren; dit moet expliciet via de microcode worden aangestuurd.
  10. NaN-vergelijking: De SPECIAL_TMPS routine bevat specifieke logica om de grootste van twee NaN-waarden te bepalen.
  11. SPECIAL_VAL: Deze hulp-subroutine handelt denormals, $\infty$ en NaN af. Het detecteert $\infty$ versus NaN door de significand één bit naar links te verschuiven; een nulresultaat duidt op $\infty$, een non-nul resultaat op NaN.
  12. Afrondingslogica: De logica voor afronding is zeer complex en wordt ondersteund door hardware-circuits die de rounding bits, modus en het teken analyseren.