Binnen in de Intel 8087-chip: floating-point optelling en aftrekking

De 8087 zorgde voor een spectaculaire snelheidswinst ten opzichte van de 8086-processor; optellingen werden bijna 100 keer sneller uitgevoerd, en complexe bewerkingen nog meer. In dit artikel wordt de microcode en de schakelingen van de 8087 onderzocht om exact uit te leggen hoe floating-point optelling en aftrekking zijn geïmplementeerd, inclusief diverse speciale gevallen voor oneindigheid en andere bijzondere waarden.

De hardware-architectuur

Om de schakelingen van de 8087 te onderzoeken, is de behuizing van een chip verwijderd en is een microscoop gebruikt om beelden van hoge resolutie te maken.

De belangrijkste componenten op de chip zijn:

  • Microcode ROM: Het grote rechthoekige gebied in het centrum van de chip, dat de 1648 micro-instructies bevat die de chip aansturen.
  • Datapath: Het onderste deel van de chip, de schakelingen die floating-point-berekeningen uitvoeren op 80-bits waarden.

Binnen het datapath bevinden zich diverse functionele blokken die essentieel zijn voor optelling en aftrekking:

  • Integer adder: Gelegen in het centrum. Deze ontvangt één argument van het B-register en een ander argument van componenten zoals de tijdelijke registers of de stack-registers. De output wordt opgeslagen in het sum register.
  • Shifter: Kan bits in een waarde naar links of rechts verschuiven. De shifter neemt veel ruimte in beslag omdat de schakelingen het mogelijk maken om een waarde snel over een willekeurig aantal bits te verschuiven. Floating-point-bewerkingen maken veelvuldig gebruik van de shifter om waarden uit te lijnen.
  • Leading zero circuitry: Telt het aantal nulbits of -bytes aan de linkerkant van een waarde, zodat deze kunnen worden weggeschoven.
  • Exponent converter: Verwerkt exponenten en detecteert overflows en underflows.

Floating-point optelling en aftrekking

De meeste computers gebruiken integers (gehele getallen) als basisrepresentatie. Veel applicaties hebben echter getallen nodig met een fractie (bijv. 1,23) en een veel groter bereik dan een 32-bit integer (die slechts getallen tussen ongeveer -2 miljard en 2 miljard kan weergeven).

Om dit op te lossen implementeren computers floating-point-getallen. In de meeste gevallen wordt een floating-point-getal gerepresenteerd als: teken × significand × 2^exponent

De significand is een binair getal in de vorm 1.bbb…: een leidende 1, gevolgd door het binaire punt en de rest van de bits. In een binair getal representeert elke bit een macht van 2. Voor het fractionele deel (na het binaire punt) zijn de machten van 2 negatief: $2^{-1}$, $2^{-2}$, enzovoort.

  • Voorbeeld: Binair 1.1 is $1 + 1/2$, oftewel 1,5 decimaal.
  • Voorbeeld: Binair 1.0011 is $1 + 1/8 + 1/16$, oftewel 1,1875.

De exponent stelt getallen in staat om buiten het beperkte bereik van de significand te treden. Zo is $1.1 (\text{binair}) \times 2^8 = 384$.

Het proces van optelling en aftrekking

Floating-point optelling en aftrekking zijn complexer dan integer-bewerkingen. Voordat getallen kunnen worden opgeteld of afgetrokken, moeten de binaire punten worden uitgelijnd (vergelijkbaar met het uitlijnen van decimale punten bij handmatige berekeningen). Na de bewerking moet de exponent van het resultaat worden aangepast om de significand te normaliseren, zodat deze weer met een 1-bit begint.

Stappen voor een eenvoudige optelling (bijv. 13,5 + 3,5):

  1. De twee argumenten worden uitgedrukt met een exponent en significand.
  2. Het tweede argument wordt uitgelijnd door het naar rechts te verschuiven totdat de exponenten gelijk zijn.
  3. De significanden worden opgeteld middels integer-optelling.
  4. Indien dit leidt tot een overflow (een "extra" bit aan de linkerkant), wordt het resultaat genormaliseerd door het één positie naar rechts te verschuiven en de exponent met 1 te verhogen.

Stappen voor aftrekking (bijv. 8 - 7,5):

  1. Net als bij optelling wordt het tweede argument naar rechts verschoven zodat de exponenten overeenkomen.
  2. De significanden worden via integer-aftrekking van elkaar afgehaald.
  3. Het resultaat wordt genormaliseerd door naar links te verschuiven totdat de bovenste bit een 1 is. Dit kan een grote verschuiving vereisen.
  4. De exponent wordt dienovereenkomstig verlaagd.

Bij aftrekking kan het resultaat negatief zijn. In dat geval wordt de significand positief gemaakt en wordt het teken van het resultaat omgeklapt. Als de aftrekking nul oplevert, kan er niet genormaliseerd worden; er wordt dan een speciale waarde (+0 of -0) teruggegeven.

Hardware-details van de 8087

Datatypes en Registers

Intern slaat de 8087 getallen op in een 80-bit formaat genaamd "temporary real". Dit bestaat uit:

  • Een tekenbit.
  • Een 15-bit exponent (met een bias van 16383, zodat de opgeslagen exponent altijd positief is).
  • Een 64-bit significand.

De 8087 heeft acht interne registers die zijn georganiseerd als een stack. Elk register bevat een 80-bit floating-point-getal en een bijbehorende "tag":

  • Zero: De waarde is nul.
  • Valid: Een normale floating-point-waarde.
  • Special: De waarde is oneindig, Not a Number (NaN), of een gedenormaliseerde waarde.
  • Empty: Het register bevat geen waarde (voor detectie van stack underflow of overflow).

Daarnaast zijn er tijdelijke registers: tmpA en tmpB (80-bit met tags) en tmpC. tmpC is anders: het heeft geen teken, exponent of tag, maar een significand van 68 bits (inclusief extra bits voor overflow en afronding: guard, round en sticky).

De Integer Adder en Shifter

Het hart van de verwerking is de 69-bit integer adder. Een optelling vereist twee micro-instructies:

  1. De eerste specificeert het eerste argument en opties zoals carry en afronding. (Bij aftrekking wordt het tweede argument gecomplementeerd).
  2. Het tweede argument komt altijd uit het B-register.

Het resultaat wordt geplaatst in het sum register, waarna een tweede micro-instructie het resultaat naar een bestemming verplaatst.

De bit shifter verschuift 68-bit waarden over een willekeurig aantal posities. Ook dit proces kost twee micro-instructies: één voor de configuratie (richting en hoeveelheid) en één om het resultaat naar de bestemming te verplaatsen.

De optelling/aftrekking microcode

Bijna alle bewerkingen van de 8087 zijn geïmplementeerd in microcode. Basisoperaties beginnen bij adres #0001, waarbij argumenten naar tmpA en tmpB worden verplaatst. Vervolgens wordt een micro-subroutine aangeroepen voor de specifieke operatie.

De subroutine voor optelling/aftrekking begint bij adres #0232. De controleflow is als volgt:

  1. Afhandeling van nul-gevallen (0+0 of 0-0).
  2. Aftrekken van de exponenten van de twee argumenten.
  3. Indien het eerste argument een kleinere exponent heeft, worden de argumenten omgewisseld.
  4. Uitlijning van tmpB via de subroutine CREATE_DENORM.
  5. Beslissing: moet er een ongetekende optelling of ongetekende aftrekking plaatsvinden?

Ongetekende Optelling

Het pad voor ongetekende optelling (vanaf adres #0269) volgt de standaardstappen:

  • Uitlijnen van getallen → Optellen → Normaliseren.
  • Bij een carry van de bovenste bit wordt het resultaat naar rechts verschoven en wordt de expAdj teller (exponent adjustment) verhoogd.
  • De precisie wordt aangepast via ADJUST_PRECISION (voor 32-bit of 64-bit output).
  • De uiteindelijke exponent wordt berekend en gecontroleerd op overflow of underflow via de exponent converter.

Ongetekende Aftrekking

Het pad voor ongetekende aftrekking (vanaf adres #0251) gebruikt de 64-bit adder om tmpA - tmpB uit te voeren.

  • Als het resultaat nul is, wordt gecontroleerd op afrondingsbits.
  • Indien het resultaat echt nul is, bepaalt de microcode of het resultaat $+0$ of $-0$ moet zijn.
  • De leading zero circuitry telt het aantal leidende nullen om te bepalen hoeveel er naar links verschoven moet worden voor normalisatie. Dit gebeurt in twee stappen: eerst per byte en daarna per bit.

Speciale gevallen in de common microcode

De 8087 bevat uitgebreide logica voor oneindigheid, NaN, denormals en andere speciale waarden. Dit resulteert in "spaghetti code" met veel conditionele vertakkingen.

Oneindigheid en NaN

Er zijn twee modi voor oneindigheid:

  • Affine closure: Oneindigheid heeft een teken ($+\infty$ en $-\infty$). Hier is $\infty + \infty = \infty$, maar $\infty - \infty = \text{NaN}$.
  • Projective closure: Oneindigheid is ongetekend.

Er zijn acht verschillende paden die leiden tot een Not-a-Number (NaN) resultaat, waaronder $\infty \times 0$ en $\infty \div \infty$.

De Leading Zero Circuitry

Bij aftrekking kunnen cijfers elkaar opheffen, waardoor leidende nullen ontstaan. De 8087 gebruikt een hiërarchische aanpak om deze te tellen:

  1. Een eerste pass elimineert leidende nullen per byte.
  2. Een tweede pass elimineert de resterende 0 tot 7 leidende nulbits.

Dit wordt gedaan met twee 8-bit leading zero detectoren in plaats van één complexe 64-bit detector, wat de hardware simpeler maakt, hoewel het iets langzamer is.

Conclusie

De 8087 legde de basis voor de IEEE-754 floating-point standaard die vandaag de dag nog steeds wordt gebruikt. De complexe regels voor het omgaan met positieve en negatieve nullen, oneindigheden en NaN zijn direct terug te voeren op dit ontwerp. Sommige specifieke functies van de 8087, zoals projectieve oneindigheid en pseudo-nullen, zijn uiteindelijk uit de standaard verwijderd omdat ze niet effectief genoeg waren.

***

Notities en referenties

  1. Prestaties: De 8087 versnelde floating-point operaties vaak met een factor 100 vergeleken met emulatie op de 8086, wat cruciaal was voor CAD en spreadsheets.
  2. Intern formaat: De chip gebruikt intern 80 bits (1 teken, 15 exponent, 64 significand) plus drie extra bits voor afronding (Guard, Round, Sticky). De shifter is 68 bits breed en de adder 69 bits.
  3. Floating-point beperkingen: Floating-point kan niet alle getallen exact representeren (bijv. 0,01), wat problematisch kan zijn bij financiële berekeningen.
  4. Temporary Real: In tegenstelling tot externe formaten wordt de leidende 1 in het temporary real formaat expliciet opgeslagen.
  5. William Kahan: De expert achter de 8087. Hij ontwierp veel van de logica, waaronder de reden waarom NaN != NaN.
  6. Denormals en Pseudo-zeros: Denormals vullen het gat tussen de kleinste normale getallen en nul. Pseudo-zeros kunnen ontstaan bij het vermenigvuldigen van zeer kleine unnormale waarden.