Rekenen met computergetallen Getallenstelsel en notaties Getallen in computers zijn opgebouwd met het kleinste element dat een computer kent: een bit. Een bit kan twee logische waardes bevatten, een nul of een één. Elektrisch gezien komt dat overeen met 0 Volt en +5 Volt (voor oudere computers, tegenwoordig al lager dan 1,8 Volt). Door middel van veel elektronische componenten kunnen bewerkingen uitgevoerd worden die overeenkomen met rekenen. Omdat een bit maar twee waardes kent, is dit gelijk aan tweetallig getallenstelsel, het binaire stelsel. Het tellen begint daarmee met 0, gevolg door 1. Maar daarna houdt het op, de spanning kan niet hoger worden. Er is daarom een extra karakter nodig, een extra digit. Het volgende getal wordt als 10 genoteerd. Daarna volgt 11, 100, 101, 110, 111, 1000 etc. Het aanvullen met binaire tientallen, honderdtallen etc. gaat dus hetzelfde als bij het decimale stelsel alleen veel sneller komen er extra noteringen (digits) bij. Om binaire getallen om te rekenen naar decimale getallen, kan eenvoudig gebruik gemaakt worden van de formule 2 ^ (n 1), waarbij n de positie van het getal is, van rechts gezien geteld. In het binaire getal 1010 is de meest rechter 1 daarom 2ˆ(2-1)=2ˆ1=2. De linker 1 is 2ˆ(4-1)=2ˆ3=8. De nullen blijven gewoon de waarde 0 vertegenwoordigen. Het binaire getal 1010 is daarmee decimaal 8+2=10. Enkele voorbeelden: 0110 à 2ˆ(3-1) + 2ˆ(2-1) = 2ˆ2 + 2ˆ1 = 4+2 = 6 1111 à 2ˆ(4-1) + 2ˆ(3-1) + 2ˆ(2-1) + 2ˆ(1-1) = 2ˆ3 + 2ˆ2 + 2ˆ1 + 2ˆ0 = 8+4+2+1 = 15 Voor een duidelijke notatie wordt meestal per groepje van 4 bits geschreven. Dit blokje wordt ook wel nibble genoemd. Een nibble kan de waarde 0 tot en met 15 bevatten. Twee nibbles vormen weer een byte. De byte bestaat daarmee dus uit 8 bits. Een byte kan de waarde 0 tot en met 255 bevatten. Soms wordt nibble ook wel als nyble of nybble geschreven vanwege de y in byte. Wat betreft de berekening van binair naar decimaal geldt dezelfde regel alleen zal de waarde van de macht per digit groter worden. Enkele voorbeelden: 0001 1000 à 2ˆ(5-1) + 2ˆ(4-1) = 2ˆ4 + 2ˆ3 = 16+8 = 24 1001 0001 à 2ˆ(8-1) + 2ˆ(5-1) + 2ˆ(1-1) = 2ˆ7 + 2ˆ4 + 2ˆ0 = 128+16+1 = 145 Omdat binair een onhandige schrijfwijze is, zijn er in het verleden ook andere manieren bedacht om de notatie te vereenvoudigen. Zo zijn er met de komst van het computertijdperk ook acht- en zestientallig getallenstelsel ontstaan. Het achttallig getallenstelsel wordt ook wel octaal getallenstelsel genoemd. Eén karakter kan de waarde 0 tot en met 7 bevatten. Dat is geen toeval want dat komt overeen met een binaire waarde van resp. 000 tot en met 111. In het verleden was een achttallig getallenstelsel gunstig voor computers in verband met de samenstelling van woorden, zoals de computers van IBM en Digital (DEC). Tegenwoordig wordt octaal niet gebruikt in de computerwereld.
Een getallenstelsel dat veelvuldig gebruikt wordt, zelfs in de modernste systemen, is het hexadecimale getallenstelsel. Oftewel een zestientallig getallenstelsel. Eén zestientallige digit komt overeen met 4 bits of nibble en is gelijk aan de decimale waarde 0 tot en met 15 vertegenwoordigen. In tegenstelling tot octaal is hexadecimaal niet met decimalen uit te schrijven. Daarom is na het getal 9 nog een toevoeging gedaan van de letters A tot en met F, voor de decimale waardes 10 tot en met 15. Wie iets met een computer heeft gedaan of hier iets over heeft gelezen, zal deze codering herkennen. Vaak komen deze hexadecimale getallen in groepjes van twee, vier of zelfs acht digits. Een bekende is het blauwe scherm bij Windows waar een aantal van deze getallen op staan. Om aan te geven dat het een hexadecimaal getal betreft, zetten technici het voorvoegsel 0x voor het getal. Dit om verwarring met decimale getallen te voorkomen in het geval een hexadecimaal getal toevallig uit alleen cijfers bestaat. Omrekenen van hexadecimaal naar decimaal gaat in principe hetzelfde als van binair naar decimaal. Alleen in plaats van machten van 2 wordt gebruik gemaakt van het vermenigvuldigen van 16 tot de macht van de digit minus 1. Enige aandachtspunt is het omrekenen van de letters A tot en met F naar de decimale waarde 10 tot en met 15. Enkele voorbeelden (op basis van 1 byte): 0x05 = 0 * 16ˆ(2-1) + 5 * 16ˆ(1-1) = 0 * 16 + 5 * 1 = 0+5 = 5 0x05 = 0000 0101 0xF4 = 15 * 16ˆ(2-1) + 4 * 16ˆ(1-1) = 15 * 16 + 4 * 1 = 240+4 = 244 0xF4 = 1111 0100 Bij de eerste elektronische computers werden alleen waardes ingevoerd en het resultaat kwam als uitvoer beschikbaar. Met de behoefte aan computers die complexere berekeningen uit konden voeren, kwam ook de behoefte om informatie op te slaan voor verdere bewerkingen. Er was geheugen nodig. Kort gezegd, voor de eerste echte complexere computers bleek dat 8 bits te onvoldoende was en er werd een extra byte toegevoegd. Deze twee bytes werden daarmee een word (Engels voor woord) genoemd. Een word is daarmee 16 bits of vier hexadecimale digits en kan de waardes van 0 tot en met 65535 bevatten. Met de komst van nog sneller computers en de honger naar meer geheugen en de wens om grotere getallen binnen de berekeningen te gebruiken, heeft ook geresulteerd in nog grotere getalnotaties. Een double word, bestaande uit de dubbele lengte van een word en daarmee 32 bits groot. Of zelfs een quad word met 64 bits of octaword met 128 bits. In de programmeerwereld hebben veel van deze getalnotaties hun eigen benaming. Deze benaming is nodig om de compiler te vertellen hoeveel geheugen gereserveerd moet worden bij het omzetten naar processorcode. Onderstaand de meest voorkomende benamingen: 1 bit boolean 8 bits byte, char 16 bits integer 32 bits long
Rekenen met getallen De kracht van computers is dat berekeningen heel snel uitgevoerd kunnen worden en ook altijd kloppen. Die laatste bewering zal voor veel discussie zorgen maar een computer doet niets meer dan alleen maar logische bewerkingen. Elke fout die in een berekening ontstaat, ontstaat door technische tekortkomingen van het systeem of onnauwkeurigheid van de gebruikte methode. Technisch gezien is de berekening nog steeds foutloos. Rekenkundig kan het compleet mis gaan. Een uitleg hoe computers rekenen. Computers rekenen niets anders dan mensen gewend zijn met een decimaal getallenstelsel. Wanneer een getal groter wordt dan 9, wordt een extra getal toegevoegd dat de tientallen weergeeft. Een berekening als 8 + 7 wordt aangeleerd als 5 opschrijven voor de eenheden, 1 onthouden voor de tientallen en uiteindelijk het totaal van de tientallen noteren, in dit geval blijft dat 1. Het eindresultaat is daarmee 15. Zoals in het begin bij het binaire tellen al te zien was, geldt dit net zo voor het binaire getallenstelsel. Het enige verschil is dat er heel snel extra getallen/digits toegevoegd moeten worden. 8 + 7 = 1000 + 0111 1000 0111 + 1111 = 2ˆ(4-1) + 2ˆ(3-1) + 2ˆ(2-1) + 2ˆ(1-1) = 8+4+2+1 = 15 Okay, toeval dat er niets te onthouden viel. Nog even een term benoemen voordat het moeilijker wordt. Elke keer als een bit meengenomen wordt naar een volgende digit, wordt dat een carry- bit of carry- around genoemd, die van één onthouden en later opschrijven. Nog een berekening, alleen nu met het gebruik van carry- bits. 6 + 3 = 0110 + 0011 11- - ß De carry- bits 0110 0011 + 1001 = 2ˆ(4-1) + 2ˆ(1-1) = 2ˆ3 + 2ˆ0 = 8+1 = 9 De berekening klopt, maar wat is er nu precies gebeurt? Voor het gemak is er een extra regel boven de twee binaire getallen gezet. Dit is het rijtje onthouden. De meest rechter digits zijn 0+1, wat resulteert in een 1 voor de laagste digit. Niets spannends tot nu toe. De volgende twee digits zijn 1+1. Omdat het slechts een tweetallig getallenstelsel is, zal het resultaat hiervan 10 binair worden. Eén plus één is dus echt 10!!! Zie hier ook de herkomst van de bekende uitspraak onder nerds: There are only 10 kind of people, those who understand binary and those who don t. Uiteraard is 10 binair gelijk aan het decimale getal 2. Echter is het resultaat twee digits. De laagste digit is een 0, deze wordt dan ook op de tweede digit van het resultaat gezet. En die andere dan? Dat is de onthouden en nemen we als carry- bit mee naar de volgende kolom digits, niets anders dan dat mensen met decimale getallen doen. De berekening wordt echter nu 1+1+0. Het resultaat hiervan is
wederom 10 binair. In het resultaat wordt wederom de laagste digit genoteerd, een 0. Het onthouden getal als carry- bit wordt weer toegevoegd aan de kolom en de volgende berekening wordt 1+0+0. Daarvan is de uitkomst 1. Dit getal kan als hoogste digit bij het resultaat genoteerd worden. Zie hier, het binaire antwoord is 1001. Maar wat als er na de eerste vier digits nog een carry- bit overblijft? Dan is er een probleem. Althans, niet voor de computer, wel voor het rekenkundige resultaat. In het geval een carry- bit na berekening overblijft, wordt dat een overflow genoemd. Het resultaat is groter dan in de eenheid van het getal geplaatst kan worden. 9 + 10 = 1001 + 1010 1 - - - - ß De carry- bits 1001 1010 + 0011 = 2ˆ(2-1) + 2ˆ(1-1) = 2ˆ1 + 2ˆ0 = 2+1 = 3!!!! Dat ging niet goed. De hoogste twee digits van de berekening is 1+1, met als resultaat 10. De 0 wordt genoteerd bij het resultaat. Maar de carry- bit past niet in de nibble. In diet geval is een overflow situatie ontstaan. Wanneer een extra nibble toegevoegd zou worden, zou deze de carry- bit opgeteld krijgen en het resultaat wordt dan 0001 0011. Dat resulteert in 16+2+1=19, het juiste antwoord. Helaas zijn computers domme dingen en zullen nooit en te nimmer een extra nibble, byte of wat voor geheugenruimte benodigde voor een getal dat groter wordt dan de oorspronkelijke grootte zelf kiezen. Dat is aan de programmeur om goed te regelen. Waarom dan niet alle getallen als double word of groter definiëren? Prima keuze, dan past een getal altijd en de berekeningen ook. Keerzijde hiervan is dat elk bitje berekend wordt, ook al zijn deze niet gebruikt. Voor een nibble zijn dat vier berekeningen, plaatsten we een nibble in een byte, dan zijn de laatste drie digits overbodig om te berekenen maar worden wel berekend. Drie extra acties waarvan het resultaat nul blijft. Dat kost dus rekenkracht van de microprocessor. En bij kleine systemen zoals microcontrollers of PLC s, is het interne geheugen beperkt en zal al snel vol zitten als er niet spaarzaam met de geheugenruimte omgegaan wordt. Rekencapaciteit en opslagruimte zijn dus bepalend voor de definitie van de grootte van elke variabele!!! Wanneer er twee getallen van elkaar af getrokken moeten worden, geldt eveneens hetzelfde als mensen voor decimale getallen gebruiken. Ook bij binair rekenen moet in sommige gevallen geleend worden. 9-4 = 1001-0100 1001 0100 - - - - - - - - 0101 = 2ˆ(3-1) + 2ˆ(1-1) = 2ˆ2 + 2ˆ0 = 4+1 = 5
De hele berekening doorlopen, van rechts naar links zoals mensen ook geleerd is. Als eerste 1-0=1, een logisch resultaat. Evenals voor de tweede digit waarbij 0-0=0 is. Nu wordt het spannend, de derde digit is 0-1. Om dit te berekenen moeten we een extra 1 lenen van de digit ervoor zodat de berekening wijzigt in 10-1, waarvan het resultaat 1 is. Of logisch gezegd 01. De 1 wordt genoteerd als resultaat voor de derde digit. Echter is de hoogste digit van het eerste getal nu niet meer een 1 maar een 0 geworden, deze is immers geleend. De berekening voor de laatste digit wordt daarmee weer 0-0=0, en daarmee is de berekening voltooid. Nu ging dat prima omdat het tweede getal kleiner is dan het eerste getal. Maar wat gebeurt er dan als dit niet het geval is? 4-9 = 0100-1001 0100 1001-10010 ß De carry- bits 1011 = 2ˆ(4-1)+2ˆ(2-1)+2ˆ(1-1) = 2ˆ3+2ˆ1+2ˆ0 = 8+2+1= 11. Nogmaals de berekening doorlopen. De berekening voor de laagste digit is 0-1. Dat kan niet, er moet dus geleend worden bij een digit hoger. Daar ontstaan een nieuw probleem, dat digit is namelijk 0 en kan niet geleend worden. In dat geval schijft de carry- bit nog een positie door. De berekening wordt daarmee 100-001=011. Immers omgerekend is 011+001 ook weer 100. Als laagste digit wordt een 1 genoteerd als resultaat. Wat meegenomen is, is dat op de plek van de tweede digit nu een 1 staat in plaats van een 0. Het resultaat van 1-0 wordt daarmee 1. De derde digit van het eerste getal was al geleend en is nu een 0. De berekening voor de derde digit is dan 0-0=0, de nul wordt genoteerd als resultaat. Bij de hoogste digit ontstaan een nieuw probleem. De berekening is nu 0-1. Helaas valt er niets te lenen want er zijn maar 4 bits beschikbaar. Technisch is de berekening met lenen 10-01=01 geworden en bij het resultaat van de hoogste digit zal een 1 genoteerd worden. In dit geval is er ook een overflow situatie ontstaan, of eigenlijk een underflow situatie. In het laatste geval had de berekening sowieso niet mogelijk geweest. Alle uitkomsten zijn altijd een positieve waarde. Er moet een manier bedacht worden om ook negatieve waardes uit te kunnen rekenen. Alle waarden in dit soort variabelen worden ook wel unsigned variabelen genoemd. Lukt vermenigvuldigen ook? Jazeker, net als bij het decimaal uitrekenen van een vermenigvuldiging kan dat ook ook binair. Immers is de methode hetzelfde alleen het getallenstelsel is anders. Waar op school geleerd is bij een vermenigvuldiging met getallen groter dan 9 de rijtjes te maken per getal, geldt dat evenzo voor binair.
3 * 5 = 0011 * 0101 0011 0101 x - - 0011 00000 001100 0000000 + - - - 0001111 = 2ˆ(4-1)+2ˆ(3-1)+2ˆ(2-1)+2ˆ(1-1) = 2ˆ3+2ˆ2+2ˆ1+2ˆ0 = 8+4+2+1 = 15 Zie hier het bewijs dat ook dit werkt. Evenals bij de voorgaande berekeningen zal elke uitkomst groter dan 15 een foutief rekenkundig resultaat geven en een overflow situatie veroorzaken. Voor het leuke dan ook nog delen? Prima, gewoon net als de ouderwetse staartdelingen maar dan met een tweetallig getallenstelsel. 13 / 3 = 1101 / 0011 = 1101 / 11 11 / 1101 \ 11 à 1 - - - - - 000 11 à 0 - - - - - 001 11 à 0 - - - - - 1 à Rest 1 Het resultaat is hiermee 100 binair = 2ˆ(3-1)+0+0 = 2ˆ2+0+0 = 4+0+0 = 4, rest 1. Hoe simpel kan het zijn? Bij het delen wordt echter wel gebruik gemaakt van de carry- bit, wanneer er geen underflow is, kan de berekening niet uitgevoerd worden en is het resultaat een 0. Is er geen underflow, dan was de berekening mogelijk en is het resultaat een 1. Aan het eind van de berekening kan er een restwaarde overblijven. Hiermee is het delen voor een microprocessor bijzonder omdat er twee waarden als resultaat volgen: Het quotiënt en de restwaarde Negatieve getallen Het optellen en aftrekken is eenvoudig, niet veel anders dan mensen gewoon doen, eigenlijk nog simpeler omdat er alleen maar een 0 of een 1 is. Maar hoe wordt dat gedaan met negatieve getallen? Negatieve getallen zijn op zich niet zo n probleem. Uit voorgaande probleem blijkt de hoogste digit te veranderen in een overflow of underflow situatie. Daar kan een soort van
misbruik van gemaakt worden door de hoogste digit wel de desbetreffende decimale waarde te laten vertegenwoordigen, maar dan negatief. In het voorbeeld met de nibble is de 4 e bit de hoogste waarde, gelijk aan 2ˆ(4-1) = 2ˆ3 = 8. Deze waarden wordt nu als - 8 gedefinieerd. Als deze bit 0 is, vormen de overige bits een positief getal. Is deze bit 1, dan moeten alle overige bits die een positieve waarde vertegenwoordigen erbij opgeteld worden. Hierdoor is het mogelijk om een getal te maken dat decimaal een waarde kan hebben van - 8, als 1000 binair, tot en met +7, als 0111 binair. 0010 = 0 * 2ˆ(4-1) + 2ˆ(2-1) = 0 * 2ˆ3 + 2ˆ1 = 0 + 2 = 2 1110 = - 1 * 2ˆ(4-1) + 2ˆ(3-1) + 2ˆ(2-1) = - 1 * 2ˆ3 +2ˆ2 + 2ˆ1 = - 8 + 4 + 2 = - 2 Rekenen wordt nu wel complexer. Eerst de makkelijke gevallen. 2-3 = 0010-0011 0010 0011-1110 ß De carry- bits 1111 = - 1 * 2ˆ(4-1) + 2ˆ(3-1) + 2ˆ(2-1) + 2ˆ(1-1)) = - 1 * 2ˆ3 + 2ˆ2 + 2ˆ1 + 2ˆ0 = - 8 + 4 + 2 + 1 = - 1 Hoe werkt de berekening nu? De Voor de berekening van de laagste digit geldt 0-1. Hiervoor moet al geleend worden. Dan kan want de volgende digit is een 1. De berekening wordt 10-01=01. De 1 wordt genoteerd bij de laagste digit. Voor de tweede digit wordt de berekening 0-1, de 1 van het eerste getal was immers al geleend voor de vorige berekening. Hierdoor wordt de berekening 10-01 en kan een 1 als resultaat genoteerd worden voor de tweede digit. Maar op de derde digit was geen 1 beschikbaar, die was geleend van de digit daarvoor, de berekening is daarmee 1-0=1. Deze 1 wordt genoteerd bij de derde digit als resultaat. Hetzelfde geldt voor de vierde speciale digit. Ook deze is geleend bij de 5 e bit. Die bit bestaat niet en er is dus een overflow/underflow situatie. Maar Het resultaat van de berekening 1-0 wordt 1 zodat deze bij de vierde digit genoteerd kan worden. Het wonder is geschied. De hoogste digit is een 1 en daarmee is een negatief getal ontstaan. En samen met de andere drie positieve digits klopt wonderbaarlijk genoeg de uitkomst. Eureka!!! Maar werkt het ook andersom? Dus bijvoorbeeld een negatief getal en daarbij een positief getal optellen. - 2 + 3 = 1101 + 0011 1101 0011 + 11110 ß De carry- bits 0001 = 0 * 2ˆ(4-1) + 2ˆ(1-1) = 0 + 2ˆ0 = 0 + 1 = 1
Hoe bizar, het werkt echt. Ook in dit geval is er een extra carry- bit als overflow ontstaan. Hier wordt wederom niets mee gedaan in deze berekeningen. In bepaalde omstandigheden kan het wenselijk zijn gebruik te maken van overflow en underflow signaleringen. Over het algemeen is niet voor hobbymatige doeleinden. Deze manier van variabelen definiëren wordt logischerwijs signed genoemd. Regel in de programmeerwereld is dat alle definities van variabelen signed zijn, tenzij aangegeven als unsigned. Voor de definitie van een variabele van het type integer, dat gelijk is aan een word, staat alleen int mijnvariabele, waarbij int een afkorting is voor integer of word. Indien expliciet gebruik gemaakt wordt van een unsigned variabele, wordt dit aangegeven als unsigned int mijnvariabele. Hoe snel rekent een microprocessor eigenlijk Eerder is gezegd dat elke bit berekend moet worden. Dat is niet helemaal waar. De grootste kracht van een microprocessor is vergelijken, optellen en aftrekken. Hiervoor zit een speciaal gedeelte in de microprocessor, de ALU oftewel de Arithmetic Logic Unit. Deze unit vormt de basis van elke microprocessor. Binnen de unit zijn twee zogenaamde registers die een waarde vertegenwoordigen. Daarnaast zijn er een aantal besturingslijnen die op basis van een specifieke opdracht, de microcode operand of opcode, bepalen wat er met de twee registers moet gebeuren. Dit kan het optellen of aftrekken zijn, maar ook bitgewijs schuiven of bitgewijs vergelijken van de twee waardes. Bij de besturingslijnen is ook een mogelijkheid om een carry- bit uit voorgaande berekening mee te nemen. Bij de uitgaande besturingslijnen zijn direct de resultaten van een aantal bitgewijze vergelijkingen beschikbaar en na het uitvoeren van schuiven, optellen of aftrekken ook een nieuwe carry- bit als resultaat van de bewerking. Door de complexiteit van een microprocessor, kunnen deze bewerkingen veelal in één of twee klokpulsen uitgevoerd worden. Eenvoudig rekenen kan dus zeer snel door de microprocessor worden uitgevoerd. Bewerking als delen en vermenigvuldigen vereisen al meerdere klokpulsen. Hoe groter de getallen, des te meer klokpulsen nodig zijn. Dit komt doordat de berekening een cyclische berekening is, het resultaat van de berekening wordt teruggevoerd in één van de registers en opnieuw gebruikt. De exacte details zijn technisch simpel maar elektronisch enorm complex. Is dat alles? Dit was de basis van rekenen door een microprocessor. Maar er is veel meer. De hier gebruikte getallen zijn allemaal gehele getallen. Gebroken getallen zijn heel veel complexer. Merkwaardig genoeg in basis wel gelijk als de signed variabelen waarbij de hoogste bit een negatieve waarde vertegenwoordigd en de overige bits positief, rekenen op deze wijze werkt immers probleemloos. Alleen zijn deze getallen opgedeeld in een teken, exponent en mantisse. Deze zogenaamde drijven komma getallen, of floating point getallen, zijn conform de IEEE 754 standaard 32 bits groot voor normale precisie en 64 bits groot voor dubbele precisie. Het berekenen van de resultaten gaat volgens dezelfde wiskundige regels voor het rekenen met exponentiele getallen in het decimale getallenstelsel, maar dan in een tweetallig systeem.