All A Level 12 60 min read 10 question quiz 17 exam marks

Binary, Bitwise Operations and Floating Point

Two's complement, subtraction by addition and overflow, then AND, OR, XOR and the shifts with masks, flags and packed colours, then the mantissa and exponent of floating point numbers, and what Unicode adds to ASCII.

Two's complement

Python's integers can be as big as memory allows. A real processor's cannot: it holds an integer in a fixed number of bits, a word. To see what the hardware does you must keep to a fixed width, and in Python the way is to mask, with & 0xFF for 8 bits.

The problem with signs

How do you store -5 in 8 bits? Using the first bit as a sign, with the other seven for the magnitude, wastes a code on a second zero (+0 and -0) and makes arithmetic awkward, because addition has to check the signs. Two's complement removes both problems.

Two's complementThe column values of an eight bit two's complement number are minus 128, 64, 32, 16, 8, 4, 2 and 1. The number 5 is 00000101, which is 4 plus 1. The number minus 5 is 11111011, which is minus 128 plus 64 plus 32 plus 16 plus 8 plus 2 plus 1, which makes minus 5. The number minus 128 is 10000000, with only the negative column set. To turn 5 into minus 5, invert every bit of 00000101 to get 11111010, then add 1 to get 11111011.Eight bit two's complement: the leftmost column is negative-1286432168421column500000101-511111011-12810000000To make 5 into -5: invert every bit, then add 1500000101invert11111010add 111111011-5 = -128 + 64 + 32 + 16 + 8 + 2 + 1
The leftmost bit is not a sign flag. It is a column worth minus 128.

In two's complement the leftmost bit has a negative place value. In 8 bits the columns are -128, 64, 32, 16, 8, 4, 2, 1. That one change is everything: a number is just the sum of the columns that hold a 1.

To negate a number: invert every bit, then add 1.

Python
def to_twos_complement(value, bits):
    return format(value & (2 ** bits - 1), "0" + str(bits) + "b")

def from_twos_complement(pattern):
    number = int(pattern, 2)
    if pattern[0] == "1":
        number = number - 2 ** len(pattern)
    return number

for value in [5, -5, 127, -128, 0, -1]:
    pattern = to_twos_complement(value, 8)
    print(str(value).rjust(4), pattern, from_twos_complement(pattern))

Negating 5 by hand: 5 is 00000101. Invert it: 11111010. Add 1: 11111011, which is -5. The sum of its columns is -128 + 64 + 32 + 16 + 8 + 2 + 1 = -5.

BitsUnsigned rangeTwo's complement range
80 to 255-128 to 127
160 to 65 535-32 768 to 32 767
320 to 4 294 967 295-2 147 483 648 to 2 147 483 647

The range is asymmetric: there is one more negative number than positive, because zero uses up one of the positive codes.

Subtraction is addition

A processor needs no subtraction circuit. To work out 7 - 5, it adds 7 and -5, and ignores the carry out of the top bit.

Python
def from_twos_complement(pattern):
    number = int(pattern, 2)
    if pattern[0] == "1":
        number = number - 2 ** len(pattern)
    return number

a = 7
b = -5
total = (a + (b & 0xFF)) & 0xFF         # add the 8 bit patterns, and keep only 8 bits
print(format(a, "08b"), "+", format(b & 0xFF, "08b"), "=", format(total, "08b"))
print("which is", from_twos_complement(format(total, "08b")))

The sum of the two patterns is nine bits long, 1 00000010. The carry out of the top is thrown away, because the register has only eight bits, and what is left, 00000010, is 2.

Overflow

If the true answer does not fit in the number of bits, the pattern wraps round and the sign is wrong. This is overflow.

Python
def from_twos_complement(pattern):
    number = int(pattern, 2)
    if pattern[0] == "1":
        number = number - 2 ** len(pattern)
    return number

total = (100 + 100) & 0xFF
pattern = format(total, "08b")
print(pattern, from_twos_complement(pattern))

100 + 100 is 200, which is above 127, the biggest 8 bit value. Its pattern is 11001000, which has the sign bit set, so it reads as -56. Adding two positive numbers and getting a negative one is the sign of overflow.

Hexadecimal

Four bits make one hexadecimal digit, so hex is a short way to write binary. It is used for memory addresses, colours and machine code because every byte is exactly two hex digits.

Python
print(hex(255), hex(172), format(172, "08b"))
print(int("AC", 16), int("10101100", 2))
print(format(0b10101100, "02X"))

1010 1100 split into two nibbles is A and C, which is AC.

Two's complement: the left bit is worth minus 128, and how to find -5 from +5Two's complement stores negative numbers in binary. In eight bits the place values are minus 128, 64, 32, 16, 8, 4, 2 and 1, so the leftmost bit is worth minus 128 instead of plus 128. The bits 1111 1011 are minus 128 plus 64 plus 32 plus 16 plus 8 plus 2 plus 1, which is minus 5. To find minus 5 from plus 5, write 5 as 0000 0101, flip every bit to get 1111 1010, then add 1 to get 1111 1011. Eight bits give 256 values from minus 128 to plus 127, one more negative number than positive because there is no plus 128.Eight bits, with the left bit worth -128111111011-1286432168421-128 + 64 + 32 + 16 + 8 + 2 + 1 = -5Finding -5 from +52Write +500000101Flip every bit11111010Add 111111011The range3-12801278 bits give256 valuesone more negative thanpositive: there is no +128
  1. The leftmost bit has a negative place value, -128. Add up the columns that hold a 1.
  2. To negate a number: write it in binary, flip every bit, then add 1.
  3. Eight bits hold -128 to 127. There is one more negative number than positive.
The left bit is the sign bit: 1 means negative. Check an answer by adding the column values back up.

Bitwise operations

A bitwise operation works on each bit position of its operands separately, as a logic gate does.

OperatorNameWhat it doesExample
&ANDeach result bit is 1 when both bits are 10b1100 & 0b1010 is 0b1000
|OReach result bit is 1 when at least one bit is 10b1100 | 0b1010 is 0b1110
^XOReach result bit is 1 when the bits are different0b1100 ^ 0b1010 is 0b0110
~NOTflips every bit, 0 to 1 and 1 to 0~0b1100 flips them all
<<Shift leftmoves every bit to the left, filling with 03 << 2 is 12
>>Shift rightmoves every bit to the right, dropping the end12 >> 2 is 3
Python
a = 0b1100
b = 0b1010
print(format(a & b, "04b"), format(a | b, "04b"), format(a ^ b, "04b"))
print(format(~a & 0b1111, "04b"))
print(3 << 2, 12 >> 2)

~a in Python gives a negative number, since Python's integers have no width, so to see an 8 bit or 4 bit NOT you mask it: ~a & 0b1111.

Shifts

A left shift moves every bit one place to the left, filling with 0. That doubles the number, exactly as adding a zero multiplies a denary number by ten. A right shift halves it, dropping the lowest bit.

Python
x = 5
print(x << 1, x << 2, x << 3)           # 10 20 40: times 2, 4, 8
print(100 >> 1, 100 >> 2, 101 >> 1)      # 50 25 50: divide by 2, 4, and drop the remainder
print((200 << 1) & 0xFF)                 # 144: in 8 bits, 400 overflows and wraps

Masks: reading and changing individual bits

A mask is a number with 1s in the positions you care about. The four masks you must know, applied to a byte called flags:

ToUseBecause
Test a bitflags & maskthe result is non zero only if that bit is 1
Set a bit to 1flags | maskOR with 1 gives 1, and OR with 0 changes nothing
Clear a bit to 0flags & ~maskAND with 0 gives 0, and AND with 1 changes nothing
Toggle a bitflags ^ maskXOR with 1 flips the bit, with 0 changes nothing
Python
BOLD = 0b0001
ITALIC = 0b0010
UNDERLINE = 0b0100

style = 0
style = style | BOLD                    # set bold
style = style | UNDERLINE               # set underline
print(format(style, "04b"))             # 0101

print(bool(style & ITALIC), bool(style & BOLD))      # False True

style = style & ~BOLD                   # clear bold
style = style ^ ITALIC                  # toggle italic: it was off, so it comes on
print(format(style, "04b"))             # 0110

Eight yes or no settings fit in one byte this way, instead of eight variables. File permissions work like this, and so do the flags register in a processor and the options passed to many programming functions.

Where else it appears

Python
# Is a number odd? Its lowest bit is 1.
print(7 & 1, 8 & 1)

# Is a number a power of two? Exactly one bit is set, so clearing the lowest leaves nothing.
for n in [8, 12, 64]:
    print(n, n & (n - 1) == 0)

# A subnet mask keeps the network part of an IP address.
ip = (192 << 24) | (168 << 16) | (1 << 8) | 77
mask = 0xFFFFFF00
network = ip & mask
print(".".join(str((network >> shift) & 0xFF) for shift in (24, 16, 8, 0)))

# A colour packed into one number: red, green and blue, a byte each.
colour = (255 << 16) | (128 << 8) | 0
print(hex(colour), (colour >> 16) & 0xFF, (colour >> 8) & 0xFF, colour & 0xFF)

# Parity: XOR every bit, to give a bit that makes the count of 1s even.
def parity_bit(byte):
    bit = 0
    while byte:
        bit = bit ^ (byte & 1)
        byte = byte >> 1
    return bit

print(parity_bit(0b1011001), parity_bit(0b1011000))

# Encrypt with XOR, and decrypt with the same key.
secret = [ord(c) ^ 0b0101 for c in "HELLO"]
print("".join(chr(c ^ 0b0101) for c in secret))

XOR is its own inverse: (x ^ key) ^ key is x. That one property is behind parity checks, simple encryption and the one time pad.

Binary shifts: left multiplies by two, right divides, and a bit can be lostThree worked binary shifts on an eight bit number. First, shifting 0000 0110, which is 6, left by one place gives 0000 1100, which is 12: every bit moves one column left, which doubles its value, and a new 0 fills the gap on the right. Second, shifting 0001 1000, which is 24, right by two places gives 0000 0110, which is 6: every bit moves two columns right, which divides the value by four, and two new 0s fill the gaps on the left. Third, a warning: shifting 1100 0000, which is 192, left by one place pushes the leading 1 off the end of the register, so the result is 1000 0000, which is 128, not 384. A bit that is shifted off the end is lost.Shift left by 1 place: × 20000011000001100<< 1× 2= 6= 12Shift right by 2 places: ÷ 40001100000000110>> 2÷ 4= 24= 6Careful: a 1 can fall off the end11000000100000001<< 1bit lost= 192= 128, not 384a 0 is shifted ina bit is lost off the end123
  1. A left shift moves every bit one column to the left, which doubles it. A 0 fills the gap on the right.
  2. A right shift moves bits to the right, which halves them each place. Shifting by 2 divides by 4.
  3. In a fixed number of bits a 1 can be pushed off the end and lost, which makes the answer wrong.
Left shift by n multiplies by 2 to the power n; right shift divides. It only works while no 1 falls off the end.

Floating point and characters

Why 0.1 is not 0.1

A computer stores a real number as a binary fraction, and some denary fractions cannot be written exactly as one. A third cannot be written exactly in denary, 0.3333 and so on, and in the same way one tenth is a recurring binary fraction, 0.0001100110011 and so on. It is cut off, and the stored value is a very slightly different number.

Python
print(0.1 + 0.2)
print(0.1 + 0.2 == 0.3)
print((0.1).hex())

import math
print(math.isclose(0.1 + 0.2, 0.3))

Never test floating point numbers for equality. Test whether they are close enough.

Mantissa and exponent

Floating point stores a number the way scientific notation does. It splits it into a mantissa, which holds the digits, and an exponent, which says where the point goes:

value = mantissa × 2 to the power of the exponent

The mantissa gives the precision and the exponent gives the range. With a fixed number of bits in total, adding a bit to the mantissa gives more accurate numbers, and adding one to the exponent gives bigger and smaller ones, and one is paid for by the other.

The A Level format is a two's complement mantissa with the binary point after the sign bit, and a two's complement exponent:

Python
def twos(pattern):
    value = int(pattern, 2)
    if pattern[0] == "1":
        value = value - 2 ** len(pattern)
    return value

def decode(mantissa, exponent):
    m = twos(mantissa) / 2 ** (len(mantissa) - 1)
    e = twos(exponent)
    return m * 2 ** e

print(decode("01101000", "0011"))      # 0.1101 is 0.8125, times 2 to the 3
print(decode("10011000", "0010"))      # a negative mantissa

Normalisation

The same number can be written many ways, as 0.5 × 4 and 0.25 × 8 are both 2. To use the bits of the mantissa properly, a floating point number is normalised: shifted so that the mantissa starts 01 if positive, or 10 if negative. The first two bits are different. Each shift left means the exponent drops by one, so the value does not change.

Python
def twos(pattern):
    value = int(pattern, 2)
    if pattern[0] == "1":
        value = value - 2 ** len(pattern)
    return value

def normalise(mantissa, exponent):
    e = twos(exponent)
    while mantissa[0] == mantissa[1] and "1" in mantissa:
        mantissa = mantissa[1:] + "0"
        e = e - 1
    return mantissa, format(e & 0b1111, "04b")

print(normalise("00011010", "0100"))

00011010 with exponent 4 becomes 01101000 with exponent 2, after two shifts. Both mean 3.25. The normalised form uses all of the precision available.

The exponent goes down as the mantissa shifts left

Shifting the mantissa left makes it bigger, so the exponent must get smaller by the same amount to leave the value unchanged. Do it the wrong way round, and the number is quadrupled instead.

Seeing the real format

Most computers use the IEEE 754 standard. A 32 bit float is 1 sign bit, an 8 bit exponent and a 23 bit mantissa.

Python
import struct

def float_bits(x):
    raw = struct.unpack(">I", struct.pack(">f", x))[0]
    pattern = format(raw, "032b")
    return pattern[0], pattern[1:9], pattern[9:]

sign, exponent, mantissa = float_bits(0.15625)
print(sign, exponent, mantissa)

Characters

A character is stored as a number. ASCII uses 7 bits, 128 codes, which holds English letters, digits and punctuation. Unicode is a table of more than a hundred thousand characters in every writing system, plus emoji, each with a code point. UTF-8 stores a code point in one to four bytes, and its first 128 codes are identical to ASCII, so every ASCII file is already valid UTF-8.

Python
for character in ["A", "é", "€", "😀"]:
    print(character, ord(character), len(character.encode("utf-8")), "bytes")
Characters stored as numbers, and the three character sets ASCII, extended ASCII and UnicodeComputers store text as numbers. A character set is the agreed list that says which number means which character. In ASCII the capital letter A is 65, which is 0100 0001 in binary, B is 66, a lower case a is 97 and the character 7 is 55. The codes run in order, so B is always one more than A, and a lower case letter is 32 higher than its capital. The character 7 is text with code 55, which is different from the number 7. Below are three character sets. ASCII uses 7 bits per character and so holds 128 characters. Extended ASCII uses 8 bits and holds 256, adding accents and symbols such as é, ñ and £. Unicode uses up to 32 bits and holds over a million characters, covering every writing system and emoji, with the first 128 codes kept the same as ASCII.Every character is stored as a numberA6501000001B6601000010a970110000175500110111Codes run in order:A 65, B 66, C 67 ...Lower case is 32 higher:a is 97, A is 65The character 7 is code 55,not the number 7Three character setsASCII7 bits each128 charactersAz5?Extended ASCII8 bits each256 characterséñ£©Unicodeup to 32 bits eachover a millionΩЖ123
  1. Each character has a code number, and the computer stores that number in binary.
  2. The codes run in order, so you can work out C from A. The character 7 is not the number 7.
  3. More bits per character means more characters: 128, then 256, then over a million.
A character set is just an agreed table of numbers. Unicode is needed because 256 characters cannot cover the world's languages.
6.5 stored in floating point as the mantissa 01101000 and the exponent 0011A floating point number has two parts. The mantissa, here 01101000, is read with the binary point after the first bit, which gives 0.1101000, or 0.8125. The exponent, here 0011, is 3. The value is the mantissa times 2 to the power of the exponent. The exponent moves the binary point three places to the right, turning 0.1101000 into 110.1000, which is 4 plus 2 plus 0.5, which is 6.5. A mantissa is normalised when its first two bits differ, 01 for a positive number or 10 for a negative one, so no accuracy is wasted on leading zeros. More mantissa bits give more precision, and more exponent bits give a bigger range.A number stored as a mantissa and an exponent101101000binary pointmantissa 0.1101000 = 0.8125x20011exponent 0011 = 3the power of 2The exponent moves the binary point20.11010003 places right110.1000= 6.5110.1000 = 4 + 2 + 0.5 = 6.5, and 0.8125 x 2^3 = 6.5 as well3Normalised: the first two mantissa bits differ (01 or 10), so no accuracy is wasted on leading zeros.More mantissa bits give more precision. More exponent bits give a bigger range.
  1. Mantissa times two to the power of the exponent. The binary point sits after the first mantissa bit.
  2. The exponent moves the point: 0.1101000 moved 3 places right is 110.1000, which is 6.5.
  3. Normalise so the first two bits differ. More mantissa bits give precision, more exponent bits give range.
Floating point trades precision against range, and most decimals cannot be stored exactly.

Key terms you must be able to define

Two's complement
A way of storing signed integers in which the leftmost bit has a negative place value, so negative numbers are found by inverting the bits and adding one.
Overflow
An error that occurs when the result of a calculation is too large to be stored in the number of bits available.
Bitwise operation
An operation that acts on each bit position of its operands independently, such as AND, OR, XOR or NOT.
Mask
A binary pattern used with a bitwise operation to test, set, clear or toggle selected bits.
Logical shift
Moving every bit of a binary number a number of places left or right, filling the gap with zeros.
Mantissa
The part of a floating point number that holds its significant digits, and so determines its precision.
Exponent
The part of a floating point number that gives the power of two it is multiplied by, and so determines its range.
Normalisation
Adjusting a floating point number so that the mantissa starts with 01 or 10, which maximises the precision.
Rounding error
The small difference between a real number and its stored value, because the number of bits is limited.
Unicode
A character set that gives a unique code point to the characters of nearly every writing system.

Flashcards

Say the definition out loud before you flip the card. Retrieving it from memory is what builds recall, reading it again is not.

DefineTwo's complement
AnswerA way of storing signed integers in which the leftmost bit has a negative place value, so negative numbers are found by inverting the bits and adding one.
DefineOverflow
AnswerAn error that occurs when the result of a calculation is too large to be stored in the number of bits available.
DefineBitwise operation
AnswerAn operation that acts on each bit position of its operands independently, such as AND, OR, XOR or NOT.
DefineMask
AnswerA binary pattern used with a bitwise operation to test, set, clear or toggle selected bits.
DefineLogical shift
AnswerMoving every bit of a binary number a number of places left or right, filling the gap with zeros.
DefineMantissa
AnswerThe part of a floating point number that holds its significant digits, and so determines its precision.
DefineExponent
AnswerThe part of a floating point number that gives the power of two it is multiplied by, and so determines its range.
DefineNormalisation
AnswerAdjusting a floating point number so that the mantissa starts with 01 or 10, which maximises the precision.

Reach the top grade on this topic

Binary questions are marks for method as well as for the answer, so write every step down.

  • Show the working for a negative number: write the positive, invert, add one, then state the answer
  • For subtraction, say that the second number is converted to two's complement and added, and that the carry is ignored
  • Name the mask, and the operator that goes with it: AND to test or clear, OR to set, XOR to toggle
  • In floating point, say that the mantissa decides the precision and the exponent the range
  • Check a normalised answer starts 01 or 10, and that the exponent was reduced for each shift left

Mistakes that cost marks here

Avoid these
  • Forgetting to add 1 after inverting the bits when making a number negative.
  • Reading a two's complement pattern with a leading 1 as a large positive number.
  • Writing the answer to a floating point question without adjusting the exponent after shifting the mantissa.
  • Using AND to set a bit and OR to clear it, which are the other way round.
  • Comparing floating point numbers for equality instead of checking that they are close.
Fun fact

The Boeing 787 had a bug in which its generators shut down if left powered for 248 days without a restart. A counter of hundredths of a second, held in a signed 32 bit integer, overflows after 248.55 days. Two's complement overflow is a real hazard, and not only an exam topic.

Knowledge check

0 of 10 answered
1What is the range of numbers that can be stored in 8 bit two's complement?
2What is the 8 bit two's complement representation of -5?
3What denary number does the 8 bit two's complement pattern 10000000 represent?
4Which bitwise operation would you use to clear a particular bit, leaving the rest unchanged?
5What is 0b1100 ^ 0b0110, written in binary?
6What is the effect of shifting a binary number left by 2 places?
7What is the hexadecimal value of the binary number 10101100?
8In a floating point number, what does adding a bit to the mantissa, and taking one from the exponent, do?
9Why does 0.1 + 0.2 == 0.3 give False in Python?
10How does UTF-8 relate to ASCII?
Score 0 / 10

Exam-style questions

Five written questions in the style of the real paper. Write a full answer, then let the marker check it against the mark scheme.

17 marks
1Write the 8 bit two's complement representation of the denary number -45. Show your working.3 marks
Write

Mark scheme0 / 3
  • L45 in binary is 00101101
  • LThe bits are inverted to give 11010010, and 1 is added
  • LThe answer is 11010011
Model answer

45 in 8 bit binary is 00101101. To make it negative, invert every bit, giving 11010010, and add 1, which gives 11010011. This is -128 + 64 + 16 + 2 + 1, which is -45, so the answer is 11010011.

The marker looks for key phrases. If you said it another way and it is still right, give yourself the mark.
2Calculate 25 - 18 in 8 bit binary by using two's complement. Show your working.4 marks
Calculate

Mark scheme0 / 4
  • L25 is 00011001 and 18 is 00010010
  • L-18 is found by inverting 00010010 to 11101101 and adding 1, giving 11101110
  • LThe two are added: 00011001 plus 11101110
  • LThe carry out of the 8th bit is ignored, giving 00000111, which is 7
Model answer

25 is 00011001 and 18 is 00010010. To get -18, invert 00010010 to give 11101101 and add 1 to give 11101110. Adding 00011001 and 11101110 gives 1 00000111, and the carry out of the eighth bit is ignored, so the result is 00000111, which is 7.

The marker looks for key phrases. If you said it another way and it is still right, give yourself the mark.
3A byte called settings holds eight on or off options. Bit 0 is the least significant bit. Bit 3 means that sound is on. Describe how bitwise operations can be used to check whether sound is on, and to turn sound off without changing the other seven bits.4 marks
Describe

Mark scheme0 / 4
  • LA mask with only bit 3 set, 00001000, is used
  • LTo check, the byte is ANDed with the mask and the result is not zero if sound is on
  • LTo turn it off, the byte is ANDed with the inverse of the mask, 11110111
  • LThis sets bit 3 to 0 and leaves the other bits as they were
Model answer

A mask is made with only bit 3 set, 00001000. To check whether sound is on, the byte is ANDed with the mask, and if the result is not zero then bit 3 is 1 and sound is on. To turn sound off, the byte is ANDed with the inverse of the mask, 11110111. That sets bit 3 to 0, and since every other bit is ANDed with 1, the other seven bits remain as they were.

The marker looks for key phrases. If you said it another way and it is still right, give yourself the mark.
4A floating point number is stored using an 8 bit mantissa and a 4 bit exponent. Both are in two's complement, and the binary point of the mantissa is after the first bit. The mantissa is 01011000 and the exponent is 0011. Calculate the denary value of the number.3 marks
Calculate

Mark scheme0 / 3
  • LThe mantissa is 0.1011000 in binary, which is 0.6875
  • LThe exponent is 3
  • LThe value is 0.6875 times 8, which is 5.5
Model answer

The mantissa 01011000 with the point after the first bit is 0.1011000, which is a half plus an eighth plus a sixteenth, 0.6875. The exponent 0011 is 3, so the mantissa is multiplied by 2 to the power of 3, which is 8. The value is 0.6875 times 8, which is 5.5.

The marker looks for key phrases. If you said it another way and it is still right, give yourself the mark.
5Explain why the result of adding 0.1 and 0.2 in many programming languages is not exactly 0.3, and how a programmer should compare floating point numbers.3 marks
Explain

Mark scheme0 / 3
  • LSome decimal fractions, such as 0.1, cannot be stored exactly in binary
  • LThe stored values are rounded, so a small error occurs in the result
  • LSo two values should be compared by checking that the difference is smaller than a small tolerance, not for equality
Model answer

Decimal fractions such as 0.1 and 0.2 are recurring fractions in binary, so they cannot be stored exactly in a mantissa with a limited number of bits, and each is stored as a rounded approximation. The small errors mean that the sum is a tiny amount away from 0.3. A programmer should therefore compare floating point values by checking that the difference between them is smaller than a small tolerance, rather than testing for equality.

The marker looks for key phrases. If you said it another way and it is still right, give yourself the mark.
Machine mark 0 / 17