Computer Systems

Thirteen modules on what really happens when a C program runs: bits, machine code, pipelines, caches, linking, processes, virtual memory, I/O, sockets and threads. Toggle bits, step through a function call, run a cache, schedule two threads by hand, then check yourself with concept questions and practice problems.

Follows the chapter order of R. E. Bryant and D. R. O'Hallaron, Computer Systems: A Programmer's Perspective, 2nd ed. (Prentice Hall, 2011), Chapters 2–12. Chapter 1, the book's overview tour, has no module of its own. Chapter numbers refer to that book; the questions and examples here are written for this page.

InteractiveBryant–O'Hallaron, Ch. 2: Representing and Manipulating Information

Integers: one pattern, two readings

A ww-bit word is only a pattern. Read it as unsigned and you get one number; read it as two's complement and, if the top bit is set, you get a number exactly 2w2^w smaller. The adder does not care which reading you meant, so the same addition can overflow in one reading and be exact in the other.

The idea

Unsigned. Bits xw−1…x1x0x_{w-1}\dots x_1x_0 stand for B2Uw(x)=∑i=0w−1xi2i\mathrm{B2U}_w(x)=\sum_{i=0}^{w-1}x_i2^i, a number in [0,2w−1][0,\,2^w-1].
Two's complement. The top bit has negative weight: B2Tw(x)=−xw−12w−1+∑i=0w−2xi2i\mathrm{B2T}_w(x)=-x_{w-1}2^{w-1}+\sum_{i=0}^{w-2}x_i2^i, a number in [−2w−1,2w−1−1][-2^{w-1},\,2^{w-1}-1]. The range is lopsided: there is one more negative value than positive.
Same bits, two values. If the top bit is 0 both readings agree. If it is 1, B2Uw(x)=B2Tw(x)+2w\mathrm{B2U}_w(x)=\mathrm{B2T}_w(x)+2^w. Casting between signed and unsigned in C keeps the bits and changes only the reading.
Addition is arithmetic mod 2w2^w. The hardware computes x+yx+y and drops the carry out of bit w−1w-1. Read as unsigned, the result is wrong exactly when the true sum is at least 2w2^w (the carry flag). Read as two's complement, it is wrong exactly when the true sum leaves [−2w−1,2w−1−1][-2^{w-1},2^{w-1}-1] (the overflow flag): two operands of the same sign whose sum has the other sign.

Draw the 2w2^w bit patterns around a clock. Adding moves you around the clock. The unsigned reading breaks at the top, between 2w−12^w-1 and 00; the signed reading breaks at the bottom, between 2w−1−12^{w-1}-1 and −2w−1-2^{w-1}. Overflow means your path crossed the break for the reading you care about.

C's trap. When an expression mixes int and unsigned, the signed operand is converted to unsigned. So -1 < 0u is false: −1-1 becomes 232−12^{32}-1.

Explore

Outer numbers: unsigned. Inner numbers: two's complement. x adding y, unsigned reading adding y, signed reading
Word size
x
y

Worked example

A 16-bit sensor value is stored as short s = -300;. The program then does

unsigned short u = s;     /* same 16 bits, unsigned reading   */
int  i = u;               /* widen an unsigned value          */
int  j = s;               /* widen a signed value             */
signed char k = (signed char) s;   /* keep the low 8 bits     */

Find the bits and the value of every variable.

  1. What are the 16 bits of s?

    300=256+32+8+4300 = 256+32+8+4 is 0x012C. Its negation in 16 bits is 216−300=652362^{16}-300 = 65236, which is 0xFED4. (Check: flip the bits of 0x012C to get 0xFED3, then add 1.)

  2. What value does u hold?

    The same bits read as unsigned: −300+216=65236-300+2^{16}=65236.

  3. Widening to 32 bits: what are i and j?

    An unsigned value is zero-extended: i is 0x0000FED4 =65236=65236. A signed value is sign-extended (the top bit is copied into the new bits): j is 0xFFFFFED4 =−300=-300. Each extension keeps the value of its own reading.

  4. What is k?

    Truncation keeps the low byte 0xD4 =212=212. Its top bit is 1, so as a signed char it is 212−256=−44212-256=-44. Truncation is reduction mod 282^8: −300≡−44(mod256)-300 \equiv -44 \pmod{256}.

Check your understanding

For a 32-bit int x with x≠0x\neq 0, is -x always different from x?

  1. Yes: a nonzero number never equals its negative.That's true for integers, but int arithmetic is mod 2322^{32}. Try the most negative value.
  2. No: for x=−231x=-2^{31}, -x equals x.
  3. No: -x overflows for every negative x.Every negative value except one has a positive partner in range.
−(−231)=231-(-2^{31}) = 2^{31} does not fit, and 231≡−231(mod232)2^{31}\equiv -2^{31} \pmod{2^{32}}. On the clock, −2w−1-2^{w-1} sits at the bottom, directly opposite 0, so it is its own negation (try it with the "y ← −y" button on 1000).

int x = -1; unsigned u = 1; What does x < u evaluate to?

  1. 1 (true), since −1<1-1 < 1.C does not compare the mathematical values here. Which conversion happens first?
  2. 0 (false)
  3. It depends on the compiler.The conversion rule is fixed by the C standard.
With one unsigned operand, x is converted to unsigned: its bits are all ones, which reads as 232−12^{32}-1, and that is not less than 1.

You add two ww-bit two's-complement numbers of opposite sign. Can the result overflow?

  1. Yes, when one of them is −2w−1-2^{w-1}.Try −2w−1+(2w−1−1)=−1-2^{w-1}+(2^{w-1}-1)=-1, the most extreme case. Is it in range?
  2. Never.
  3. Yes, whenever the carry flag is set.The carry flag is about the unsigned reading. Try 4 bits: −1+1-1+1 sets the carry but is exact as signed.
If x<0≤yx<0\le y then x≤x+y≤yx\le x+y\le y, so the true sum lies between two in-range numbers. On the clock, the signed path for a positive and a negative operand can never reach the bottom break.

Practice

Sign extension keeps the valueA one-line proof

Show that copying the top bit into a new bit position does not change the two's-complement value: B2Tw+1(xw−1,xw−1,xw−2,…,x0)=B2Tw(xw−1,…,x0)\mathrm{B2T}_{w+1}(x_{w-1},x_{w-1},x_{w-2},\dots,x_0)=\mathrm{B2T}_w(x_{w-1},\dots,x_0). Conclude that sign-extending by any number of bits is safe.

Hint

Only the two top bits differ in weight. Compare −xw−12w+xw−12w−1-x_{w-1}2^{w}+x_{w-1}2^{w-1} with −xw−12w−1-x_{w-1}2^{w-1}.

Solution

The low w−1w-1 bits contribute the same in both. In the longer word, the top two bits contribute −xw−12w+xw−12w−1=−xw−12w−1-x_{w-1}2^{w}+x_{w-1}2^{w-1}=-x_{w-1}2^{w-1}, which is exactly what the single top bit contributes in the shorter word. Induction on the number of added bits gives the general case.

Shifting is not dividingRounding direction

In 8 bits, let x = -7. Compute x >> 1 with an arithmetic shift and compare it with C's x / 2. Then find a value b such that (x + b) >> 1 equals x / 2 for negative x.

Hint

An arithmetic right shift by kk computes ⌊x/2k⌋\lfloor x/2^k\rfloor. C's division truncates toward zero.

Solution

−7-7 is 0xF9 = 11111001; shifting right and copying the sign bit gives 11111100 = −4=⌊−3.5⌋-4=\lfloor -3.5\rfloor. C gives -7/2 = -3. Adding the bias b=2k−1=1b=2^k-1=1 first fixes it: (−7+1)≫1=−6≫1=−3(-7+1)\gg 1=-6\gg1=-3. Compilers emit exactly this bias for signed division by a power of two, applied only when x<0x<0.

More practice: Bryant–O'Hallaron, Practice Problems 2.17, 2.21 and 2.29; Homework 2.81.

InteractiveBryant–O'Hallaron, Ch. 2: Representing and Manipulating Information

Floating point: sign, exponent, fraction

IEEE floating point is binary scientific notation with a fixed number of digits. That one decision explains everything odd about it: the gaps between numbers double at each power of two, most decimals are stored only approximately, and adding numbers in a different order can give a different answer.

The idea

Fields. A sign bit ss, a kk-bit exponent field ee, and an nn-bit fraction field f=0.fn−1…f0f=0.f_{n-1}\dots f_0 (in binary). The value is V=(−1)sM2EV=(-1)^s\,M\,2^E, with bias B=2k−1−1B=2^{k-1}-1. For float, k=8, n=23k=8,\ n=23; for double, k=11, n=52k=11,\ n=52.
Three cases. Normalized (ee neither all zeros nor all ones): E=e−BE=e-B and M=1+fM=1+f, so there is a hidden leading 1. Denormalized (e=0e=0): E=1−BE=1-B and M=fM=f; these fill the gap around 0 evenly, and include ±0\pm0. Special (ee all ones): f=0f=0 is ±∞\pm\infty, anything else is NaN.
Spacing. Between 2E2^E and 2E+12^{E+1} there are exactly 2n2^n evenly spaced values, 2E−n2^{E-n} apart. So the absolute gap grows with the size of the number while the relative gap stays about 2−n2^{-n}.
Rounding. A result that falls between two representable values is rounded to the nearer one; a tie goes to the one whose last fraction bit is 0 (round to even), which avoids a bias in long sums.
Not associative. In float, 224+12^{24}+1 is a tie between 2242^{24} and 224+22^{24}+2 and rounds to 2242^{24}. So (16777216.0f + 1.0f) - 16777216.0f is 0, while 16777216.0f + (1.0f - 16777216.0f) is 1. Compilers therefore don't reorder floating-point sums for you.

Explore

Format
Bits (click to toggle)
Number line range

Worked example

Encode −6.75-6.75 in the 8-bit format above (1 sign bit, k=4k=4, n=3n=3, bias 7).

  1. Write 6.756.75 in binary and normalize it.

    6.75=4+2+12+14=110.112=1.10112×226.75 = 4+2+\tfrac12+\tfrac14 = 110.11_2 = 1.1011_2\times 2^2. So E=2E=2 and the digits after the point are 1011.

  2. What goes in the exponent field?

    e=E+B=2+7=9e = E + B = 2+7 = 9 = 1001. It is neither 0 nor 15, so the number is normalized.

  3. There are four fraction digits but only three bits. Round.

    1.101|1: the dropped part is exactly half of the last kept place, a tie. Round to even: 1.101 ends in 1, so round up to 1.110. The stored value is −1.75×22=−7-1.75\times 2^2=-7.

  4. Give the bit pattern, then check it in the explorer.

    1 1001 110 = 0xCE. Type -6.75 above: the explorer shows the same bits, stored value −7-7, and an error of 0.250.25, half the gap of 0.50.5 between neighbours in [4,8)[4,8).

  5. What are the smallest positive value and the largest finite value of this format?

    Smallest: the denormal 0 0000 001 =18×21−7=2−9≈0.00195=\tfrac18\times2^{1-7}=2^{-9}\approx0.00195. Largest: 0 1110 111 =1.875×214−7=240=1.875\times2^{14-7}=240.

Check your understanding

Why do denormalized numbers use E=1−BE=1-B rather than E=−BE=-B?

  1. So that the largest denormal and the smallest normal are one gap apart, with no jump in spacing.
  2. To make room for NaN.NaNs use the all-ones exponent field, not the all-zeros one.
  3. Because the hidden bit would otherwise be 1.The hidden bit is 0 for denormals by definition; the question is which power of two multiplies it.
With E=1−BE=1-B, denormals are f⋅21−Bf\cdot 2^{1-B} and the smallest normal is 1.0⋅21−B1.0\cdot 2^{1-B}: the same spacing 21−B−n2^{1-B-n} continues across the boundary. Set the range to "0 to 1/8" in the 8-bit format and look at the ticks near 2−62^{-6}.

What is the smallest positive integer that a float cannot represent exactly?

  1. 223+12^{23}+1Don't forget the hidden leading 1: a normalized float has 24 significant bits.
  2. 224+12^{24}+1
  3. 2312^{31}A power of two needs only one significant bit, so it is exact.
Every integer up to 2242^{24} fits in 24 significant bits. Between 2242^{24} and 2252^{25} the gap is 2, so 224+1=167772172^{24}+1=16777217 is the first one skipped. Type it into the explorer in float mode.

float x is finite. Can x + 1.0f == x be true?

  1. No: adding 1 always changes a number.Only if the result can be stored. What is the gap between floats near 10810^8?
  2. Yes, for example at x=224x=2^{24}, where the gap between floats is 2.
  3. Only for denormalized x.Near zero the gaps are tiny, so adding 1 certainly changes the value.
224+12^{24}+1 is a tie between 2242^{24} (fraction ending in 0) and 224+22^{24}+2 (ending in 1); round-to-even picks 2242^{24}. From 2252^{25} on the gap is at least 4, and x+1x+1 always rounds back to xx. A loop for (float f = 0; f != 2e7f; f += 1) never ends.

Practice

Decode a floatFrom hex to value

Which number does the float bit pattern 0x41200000 represent?

Hint

Write out the first 12 bits: the sign is bit 31, the exponent is bits 30–23.

Solution

0100 0001 0010 0000 …: s=0s=0, e=e=10000010=130=130, so E=130−127=3E=130-127=3; the fraction is 010…0 =0.25=0.25, so M=1.25M=1.25. The value is 1.25×8=101.25\times 8 = 10. Check it by toggling the bits in float mode.

Counting a formatHow many values are there?

For the 8-bit format (k=4k=4, n=3n=3), how many bit patterns represent positive finite numbers, and how many of those are denormalized?

Solution

With s=0s=0: exponent field 0 gives zero and 7 denormals; fields 1 to 14 give 14×8=11214\times 8=112 normalized values; field 15 gives +∞+\infty and 7 NaNs. So there are 7+112=1197+112=119 positive finite values, 7 of them denormalized.

More practice: Bryant–O'Hallaron, Practice Problems 2.45, 2.47 and 2.52; Homework 2.84 and 2.88.

Step-throughBryant–O'Hallaron, Ch. 3: Machine-Level Representation of Programs

Procedures and the run-time stack

A function call is three cooperating conventions: call leaves a return address on the stack, the arguments and the result travel in agreed registers, and each function promises to restore certain registers. Step through a recursive call one instruction at a time and watch the stack grow and shrink.

The idea

The book's main examples are IA32; it then extends them to x86-64 (Section 3.13). We use x86-64, where the first six integer arguments travel in registers.

The stack. A region of memory that grows toward lower addresses. %rsp holds the address of its top element. pushq S subtracts 8 from %rsp and stores SS there; popq D loads from (%rsp) into DD and adds 8.
Call and return. call f pushes the address of the instruction after the call (the return address) and jumps to f. ret pops an address and jumps there.
Register conventions (x86-64). Arguments in %rdi, %rsi, %rdx, %rcx, %r8, %r9; result in %rax. Callee-saved: %rbx, %rbp, %r12–%r15 must hold the same values on return, so a function that wants one pushes it first and pops it at the end. All other registers are caller-saved: the caller cannot expect them to survive a call.
Frames. Each active call owns a slice of the stack, its frame: the return address its caller pushed, any callee-saved registers it saved, and locals that don't fit in registers (arrays, or variables whose address is taken). Recursion needs nothing special, because each activation gets its own frame.
long fact(long n) {
    if (n <= 1)
        return 1;
    return n * fact(n - 1);
}

Below, n arrives in %rdi. The function needs n after the recursive call, and %rdi won't survive it, so it parks n in the callee-saved %rbx, which means saving the caller's %rbx first.

Explore

main calls fact(n) with n =

          
The highlighted line is the next instruction to run (%rip). Addresses are on the left.
return address saved %rbx · brackets show each call's frame

Worked example

Here a local variable must live on the stack:

void incr(long *p, long d) { *p += d; }

long twice(long x) {
    long v = x;
    incr(&v, 10);
    return v * 2;
}
  1. Why can't v stay in a register?

    incr needs a memory address for it: &v. Registers have no addresses, so twice must reserve a stack slot for v.

  2. Write plausible x86-64 code for twice.

    twice:
        subq   $24, %rsp        # room for v (plus padding)
        movq   %rdi, 8(%rsp)    # v = x
        leaq   8(%rsp), %rdi    # 1st argument: &v
        movl   $10, %esi        # 2nd argument: 10
        call   incr
        movq   8(%rsp), %rax    # reload v: incr changed it
        addq   %rax, %rax       # v * 2
        addq   $24, %rsp        # free the frame
        ret
  3. Why 24 bytes, when v needs only 8?

    The x86-64 ABI requires %rsp to be a multiple of 16 at every call. On entry to twice, the caller's call has just pushed 8 bytes, so %rsp is 8 more than a multiple of 16. Subtracting 24 restores the alignment and leaves room for v.

  4. What does incr compile to? Does it need a frame?

    addq %rsi, (%rdi) followed by ret. It calls nothing and keeps nothing, so beyond the return address it needs no stack at all.

  5. What goes wrong if twice returned &v instead?

    The slot is freed by addq $24, %rsp. The next call made by the caller reuses those bytes for its own frame, so the returned pointer would point at whatever that call stores there.

Check your understanding

Just after call fact runs, before the first instruction of fact, what is stored at (%rsp)?

  1. The argument n.On x86-64 the first argument travels in %rdi, not on the stack.
  2. The address of the instruction right after the call.
  3. The caller's %rbp.Only if the callee pushes it. call itself pushes exactly one thing.
That return address is what ret will pop. Step once past a call in the explorer: a blue slot appears holding the address of the next line in the caller.

fact pushes %rbx before using it, but never saves %rdi although it overwrites it. Why is that correct?

  1. %rbx is callee-saved, so the caller may rely on it; %rdi is caller-saved, so the caller may not.
  2. %rdi holds the argument, which belongs to fact.That is part of it, but the real rule covers every register: who is responsible for preserving it?
  3. It is a bug that happens not to matter here.It is the convention working as designed.
Each register has one owner of the saving duty. A caller that needs a caller-saved value after a call must save it itself, which is exactly why fact copies n into %rbx.

For fact(4) called from main, how many return addresses are on the stack at the deepest point?

  1. 3Count the call from main too.
  2. 4
  3. 7That adds the saved copies of %rbx, which aren't return addresses.
Four calls are active, fact(4) to fact(1), each with its return address. There are also three saved %rbx values, because the base case returns without touching %rbx. Set n = 4 and run to the deepest point to see them.

Practice

How deep does it go?Stack usage of recursion

How many bytes below main's %rsp does the stack reach during fact(n), for n≥1n\ge1, with the code above?

Solution

Each non-base call uses 16 bytes (return address and saved %rbx); the base case uses 8 (return address only). Total 16(n−1)+8=16n−816(n-1)+8=16n-8 bytes. For n=3n=3 that is 40 bytes: compare the lowest %rsp in the explorer with the starting one.

Overwriting a frameWhy buffer overflows are dangerous

A function starts with subq $24, %rsp and keeps char name[16] at (%rsp). It calls strcpy(name, s) where s is a 39-character string. Which bytes of the stack get overwritten, and what happens at ret?

Hint

Draw the frame: the array, 8 bytes of padding, then the return address pushed by the caller. strcpy also writes the terminating zero byte.

Solution

strcpy writes 40 bytes, offsets 0–39 from %rsp: name (0–15), the padding (16–23), the return address (24–31), and 8 bytes of the caller's frame (32–39). After addq $24, %rsp, ret pops characters 24–31 of the string as an address and jumps there. Whoever chooses the string chooses where the program goes.

More practice: Bryant–O'Hallaron, Practice Problems 3.32, 3.33, 3.51 and 3.52.

InteractiveBryant–O'Hallaron, Ch. 4: Processor Architecture

Pipelining and its hazards

A pipelined processor works on five instructions at once, each in a different stage, like an assembly line. It runs at one instruction per cycle until an instruction needs a result that is still on the line. Then the hardware must either wait or pass the value along a shortcut.

The idea

Five stages. The book's Y86 processor splits each instruction into Fetch (read the instruction), Decode (read source registers), Execute (ALU), Memory (read or write data memory) and Write-back (write result registers). Pipeline registers between stages let a new instruction enter every cycle.
Throughput and latency. With logic split into stages of delays d1,…,dkd_1,\dots,d_k and a register delay rr, the clock period is maxi⁡di+r\max_i d_i + r. Throughput is one instruction per period; the latency of one instruction becomes kk periods. Uneven stages and register overhead limit the gain from deeper pipelines.
Data hazard. An instruction reads a register (in Decode) that an earlier instruction has not yet written (in Write-back). Two remedies: stalling holds the reader in Decode and sends bubbles (do-nothing instructions) down the pipe until the value has been written; forwarding routes a result straight from a later stage back to Decode as soon as it exists.
Load/use. A value loaded from memory exists only at the end of the Memory stage, one stage too late for an instruction directly behind the load. Even with forwarding, that pair costs one bubble.

With NN instructions and bb bubbles the pipeline takes N+b+4N+b+4 cycles (four to fill it). So the cycles per instruction approach CPI=1+b/N\mathrm{CPI}=1+b/N. Branches and returns add control hazards on top: a mispredicted branch costs 2 bubbles in the book's design and a ret costs 3.

Explore

Program
Hazard handling
Rows are instructions, columns are clock cycles. bubble value forwarded value read from the register file after write-back

Worked example

A block of combinational logic has a total delay of 240 ps. Each pipeline register adds 15 ps. Measure throughput in GIPS (billions of instructions per second; a period of pp ps gives 1000/p1000/p GIPS).

  1. What is the throughput without pipelining?

    One register at the end: period 240+15=255240+15=255 ps, so 1000/255≈3.921000/255\approx 3.92 GIPS, and latency 255 ps.

  2. Split into three equal stages of 80 ps. Throughput and latency?

    Period 80+15=9580+15=95 ps: 10.5310.53 GIPS, about 2.7 times faster. Latency grows to 3×95=2853\times95=285 ps.

  3. Suppose the logic only splits as 60, 120, 60 ps. Now?

    The slowest stage sets the clock: period 120+15=135120+15=135 ps, 7.417.41 GIPS. The two short stages sit idle for half of every cycle.

  4. Go deeper: six equal stages of 40 ps.

    Period 5555 ps, 18.1818.18 GIPS, latency 330330 ps. Doubling the stages from three to six gave only a 1.73 times speedup, because the 15 ps of register delay is now 15/55≈27%15/55\approx27\% of every cycle.

Check your understanding

With forwarding, mrmovl 0(%esi),%eax followed directly by addl %eax,%ebx still needs one bubble. Why?

  1. Forwarding only works for ALU results.The pipeline can forward the loaded value too, from the Memory stage. The problem is timing.
  2. When addl is in Decode, the load is only in Execute: memory hasn't been read yet.
  3. Memory is slower than registers, so every load stalls.A load followed by an unrelated instruction causes no bubble at all.
The value appears at the end of the load's Memory stage. Holding addl in Decode for one cycle lines it up with that moment, and then the value is forwarded. Pick "Load then use" in the explorer.

Which reordering of mrmovl 0(%esi),%eax, addl %eax,%ebx, irmovl $4,%ecx gives the same result with no bubble (forwarding on)?

  1. Move the irmovl between the load and the add.
  2. Move the addl before the load.Then addl would use the old %eax: a different program.
  3. Nothing helps: the load/use bubble is unavoidable.It is unavoidable only when the use comes immediately after the load.
irmovl touches neither %eax nor %ebx, so it can fill the slot the bubble would have taken. Compilers schedule code this way. Compare "Load then use" with "Loads hoisted" in the explorer.

Without forwarding, how many bubbles does an instruction need when it reads a register written by the instruction directly before it?

  1. 1That's the load/use cost with forwarding.
  2. 2Count from the writer's Decode stage to its Write-back stage.
  3. 3
The writer goes through Execute, Memory and Write-back after its Decode. The reader may decode only in the cycle after the write-back, three cycles later than it would have. Choose "Stall only" and "Chain of adds" to see 3 bubbles per instruction.

Practice

Counting bubblesCheck your answer in the explorer

For the program below, count the bubbles and the total number of cycles, first with stalling only, then with forwarding.

irmovl $1, %eax
addl   %eax, %ecx
mrmovl 8(%ecx), %edx
addl   %edx, %eax
Hint

Each of the last three instructions reads a register written by the one just before it. Only one of those writers is a load.

Solution

Stalling only: each of the three adjacent dependences costs 3 bubbles, 9 in total, so 4+9+4=174+9+4=17 cycles. Forwarding: only the load followed by its use stalls, 1 bubble, 4+1+4=94+1+4=9 cycles. (The last addl also reads %eax from the first instruction, which is long finished.) The explorer's "Mixed" program is this one.

The cost of mispredictionsCPI from frequencies

In some loop, one instruction in six is a conditional jump, and the predictor is wrong a quarter of the time. Each misprediction costs 2 bubbles. Ignoring other hazards, what is the CPI?

Solution

Bubbles per instruction: 16×14×2=112\tfrac16\times\tfrac14\times2=\tfrac1{12}. So CPI=1+112≈1.083\mathrm{CPI}=1+\tfrac1{12}\approx1.083: about 8% slower than the ideal.

More practice: Bryant–O'Hallaron, Practice Problems 4.26, 4.27 and 4.41.

SimulationBryant–O'Hallaron, Ch. 5: Optimizing Program Performance

Latency, throughput and parallel accumulators

A modern core can start a multiply every cycle, yet a simple product loop runs several times slower than that. The reason is a chain of dependences: each multiply waits for the one before. Break the chain into several independent ones and the hardware's parallelism comes back.

The idea

CPE. Cycles per element: the slope of running time against the number of elements nn. It ignores fixed overhead and measures the loop itself.
Latency and issue. A functional unit has a latency LL (cycles from inputs to result) and, if fully pipelined, can accept a new operation every cycle. With uu such units, at most uu operations start per cycle.
long prod(long *a, long n) {
    long acc = 1;
    for (long i = 0; i < n; i++)
        acc = acc * a[i];        /* needs the previous acc */
    return acc;
}
Two bounds. If the work forms kk independent chains of dependent operations, then CPE≥L/k\mathrm{CPE}\ge L/k (latency bound: each chain does one operation per LL cycles) and CPE≥1/u\mathrm{CPE}\ge 1/u (throughput bound). The loop above has one chain, so its CPE is LL.
long prod2(long *a, long n) {     /* k = 2 accumulators */
    long acc0 = 1, acc1 = 1, i;
    for (i = 0; i + 1 < n; i += 2) {
        acc0 = acc0 * a[i];
        acc1 = acc1 * a[i+1];       /* independent of acc0 */
    }
    for (; i < n; i++)
        acc0 = acc0 * a[i];
    return acc0 * acc1;
}
Only if reordering is allowed. prod2 multiplies in a different order. That is harmless for integers (arithmetic mod 2642^{64} is associative) but changes rounding for floating point, so a compiler won't make this change to a floating-point loop unless told it may.
Amdahl's law. If a fraction α\alpha of the running time is sped up by a factor kk, the overall speedup is S=1(1−α)+α/kS=\dfrac{1}{(1-\alpha)+\alpha/k}, never more than 1/(1−α)1/(1-\alpha).

Explore

Operation latency L
Each bar is one operation of a 24-element loop, from the cycle it starts to the cycle its result is ready. Element ii goes to accumulator imodki \bmod k. An operation starts as soon as its accumulator's previous result is ready and a unit is free.

Worked example

A loop computes a product of doubles with a single accumulator. The multiplier has latency L=4L=4 and is fully pipelined. Profiling shows the loop takes 30% of the program's time.

  1. What CPE do you expect, and why?

    One chain of dependent multiplies: each starts only when the previous result is ready, so CPE=L=4\mathrm{CPE}=L=4.

  2. With kk accumulators and one unit, what is the CPE?

    max⁡(4/k, 1)\max(4/k,\ 1). It improves until k=4k=4, where a new multiply starts every cycle; more accumulators cannot help a single unit.

  3. With uu units, how many accumulators are needed to reach the throughput bound?

    We need L/k≤1/uL/k\le 1/u, so k≥L⋅uk\ge L\cdot u. Enough chains have to be in flight to keep uu units each busy for LL cycles: with u=2u=2, k=8k=8. Try L = 3, u = 2 in the explorer: the CPE stops improving at k=6k=6.

  4. Is the rewrite allowed?

    Only if a slightly different rounding is acceptable. The products are mathematically equal, but floating-point multiplication is not associative, so the last bits can differ.

  5. The loop speeds up from CPE 4 to CPE 1. How much faster is the whole program?

    Amdahl with α=0.3\alpha=0.3, k=4k=4: S=1/(0.7+0.075)=1/0.775≈1.29S=1/(0.7+0.075)=1/0.775\approx1.29. Even an infinitely fast loop would give only 1/0.7≈1.431/0.7\approx1.43.

Check your understanding

L=4L=4, two fully pipelined units, k=4k=4 accumulators. What CPE does the model give for large nn?

  1. 0.5That's the throughput bound 1/u1/u. Is the latency bound L/kL/k also met?
  2. 1
  3. 2Four independent chains, each doing one operation every 4 cycles, give 4 operations per 4 cycles.
max⁡(L/k,1/u)=max⁡(1,0.5)=1\max(L/k,\,1/u)=\max(1,\,0.5)=1. The units are only half busy: there aren't enough independent chains to feed both. Set L = 5, u = 2 and watch the CPE as kk grows.

Can the compiler load *f once, before the loop, in this function?

void scale(long *a, long n, long *f) {
    for (long i = 0; i < n; i++)
        a[i] = a[i] * *f;
}
  1. Yes, *f is never assigned in the loop.Not by name. But what if f points into a?
  2. No: f might point to an element of a, so a store to a[i] could change *f.
  3. No, because loads can never be moved out of loops.They can be, when the compiler can prove nothing in the loop changes them.
This is memory aliasing. If f == &a[0], the first iteration changes *f and later iterations must see the new value. If you know they never overlap, copy *f into a local variable yourself.

A program spends 80% of its time in one function. You make that function 10 times faster. The overall speedup is about…

  1. 8That would need the remaining 20% to vanish as well.
  2. 3.6
  3. 10Only the 80% got faster.
1/(0.2+0.8/10)=1/0.28≈3.571/(0.2+0.8/10)=1/0.28\approx3.57. The untouched 20% now takes most of the time, and no amount of work on the function can push the speedup past 1/0.2=51/0.2=5.

Practice

Parallelism with a serial partAmdahl's law

A job spends 25% of its time reading input (which can't be sped up) and 75% computing. You spread the computation perfectly over 8 cores. What is the speedup? What is the limit with unlimited cores?

Solution

1/(0.25+0.75/8)=1/0.34375≈2.911/(0.25+0.75/8)=1/0.34375\approx2.91. The limit is 1/0.25=41/0.25=4.

Unrolling is not enoughWhere the parentheses go

Two ways to unroll prod by 2 with a single accumulator: (a) acc = (acc * a[i]) * a[i+1]; (b) acc = acc * (a[i] * a[i+1]);. Give the CPE of each in terms of LL, assuming a unit with enough throughput.

Hint

Find the chain of operations that each depend on acc from the previous iteration.

Solution

(a) Both multiplies sit on the chain through acc: 2L2L cycles per 2 elements, CPE LL. Unrolling alone removed loop overhead but not the dependence. (b) a[i] * a[i+1] doesn't involve acc and can run ahead; only one multiply per 2 elements is on the chain, so CPE L/2L/2. This "reassociation" is the single-accumulator version of the k=2k=2 case in the explorer.

More practice: Bryant–O'Hallaron, Practice Problems 5.4, 5.8 and 5.13; Homework 5.17 and 5.18.

SimulationBryant–O'Hallaron, Ch. 6: The Memory Hierarchy

Caches and locality

A cache is a small, fast copy of the memory you used recently. Whether it helps depends on two things you control: the order in which your loops visit memory, and how your data is laid out. Run the same sum two ways through a simulated cache and compare.

The idea

Locality. Temporal: a location used now is likely to be used again soon. Spatial: locations near it are likely to be used soon. Each level of the hierarchy (registers, L1, L2, L3, DRAM, disk) holds blocks from the level below, and locality is why that works.
Organization (S,E,B)(S,E,B). A cache has S=2sS=2^s sets of EE lines; each line holds one block of B=2bB=2^b bytes, a valid bit and a tag. Capacity C=S⋅E⋅BC=S\cdot E\cdot B. An address splits into
tag⏟t bits | set index⏟s bits | block offset⏟b bits\underbrace{\text{tag}}_{t\ \text{bits}}\ \big|\ \underbrace{\text{set index}}_{s\ \text{bits}}\ \big|\ \underbrace{\text{block offset}}_{b\ \text{bits}}
Lookup. Use the set index to pick a set, compare the tag with every valid line in it, then use the offset to pick the byte. E=1E=1 is direct-mapped; S=1S=1 is fully associative. On a miss in a full set, a line is evicted; the simulator below evicts the least recently used one.
Kinds of miss. Cold: the block was never loaded. Capacity: the data in use is bigger than the cache, so even a fully associative cache of the same size would miss. Conflict: there was room overall, but too many blocks mapped to the same set.
int a[8][8];        /* row i starts 32 bytes after row i-1 */
for (i = 0; i < 8; i++)            /* row by row: stride 1 */
    for (j = 0; j < 8; j++)  s += a[i][j];
for (j = 0; j < 8; j++)            /* column by column: stride 8 */
    for (i = 0; i < 8; i++)  s += a[i][j];

Explore

Sets S
Lines per set E
Block size B
Access pattern
hit miss · ring: the current access · dark bars: where each block starts
The cache: one row per set, one box per line. The highlighted set is the one the current address maps to.

Worked example

A direct-mapped cache holds 128 bytes in 16-byte blocks; addresses have 12 bits. A program sums int v[64], which starts at address 0, twice in a row.

  1. Find SS, ss, bb, tt, and split the address 0x1A4.

    S=128/16=8S=128/16=8, so s=3s=3, b=4b=4, t=12−3−4=5t=12-3-4=5. 0x1A4 = 00011 010 0100: tag 3, set 2, offset 4. Check: 3⋅128+2⋅16+4=420=3\cdot128+2\cdot16+4=420=0x1A4.

  2. What is the miss rate of the first pass?

    A block holds 4 ints. The first access to each block misses and the next three hit: 25% misses, all of them cold.

  3. And of the second pass?

    The array is 256 bytes, twice the cache. By the end of the first pass, v[32..63] has replaced v[0..31] in every set (v[i]v[i] and v[i+32]v[i+32] are 128 bytes apart, so they share a set). The second pass starts on evicted blocks and again misses 25%, now capacity misses.

  4. Reorganize the work: do both passes over v[0..31], then both over v[32..63]. Miss rate?

    Each half is exactly 128 bytes, so it stays in the cache during its second pass, which now hits every time. Overall: 16 misses in 128 accesses, 12.5%. Working on a cache-sized piece at a time is the idea behind blocking.

Check your understanding

You double the block size BB and halve SS, so the capacity stays the same. What happens to the miss rate of the row-by-row sum?

  1. It roughly halves: each miss now brings in twice as many neighbours.
  2. No change: the same elements are read.The number of elements is the same, but how many share each block changed.
  3. It rises because there are fewer sets.In a stride-1 pass, each block is used up before it could be evicted, so the number of sets doesn't matter here.
A stride-1 pass over ints misses once per block: miss rate 4/B4/B. In the explorer, compare S = 4, B = 16 with S = 2, B = 32 for "row by row".

Why do caches take the set index from the middle bits of the address rather than the top bits?

  1. So that consecutive blocks go to different sets and a contiguous array can fill the whole cache.
  2. Because the top bits are needed for the offset.The offset is the lowest bb bits.
  3. It makes the tag comparison faster.The comparison costs the same either way. Think about which sets a long array would use.
With top-bit indexing, a whole chunk of 2t+b2^{t+b} contiguous bytes would map to one set, and a loop over an array would use only one set at a time.

int a[8][8] summed column by column, with S=8S=8, E=1E=1, B=16B=16 (a 128-byte cache, half the array). What is the hit rate?

  1. 75%That would need each block to survive until its other three ints are read, a whole column later.
  2. 50%Look at which rows share a set.
  3. 0%
Rows are 32 bytes apart, so rows ii and i+4i+4 map to the same set. Going down a column, row i+4i+4 evicts row ii's block before the next column comes back to it. Every access misses. Try it, then switch to S = 8 with E = 2, which holds the whole array: the hit rate jumps to 75%.

Practice

Cache geometryFrom size to address bits

A 32 KB, 8-way set-associative cache has 64-byte blocks, and addresses are 48 bits. Find SS, ss, bb and tt.

Solution

S=32768/(8⋅64)=64S=32768/(8\cdot64)=64, so s=6s=6; b=6b=6; t=48−6−6=36t=48-6-6=36.

Layout mattersArray of structs vs. struct of arrays

struct particle { double x, y, z, mass; } p[4096]; A loop sums p[i].mass for all i. With 64-byte blocks and a cache much smaller than the array, what is the miss rate? What if the masses were kept in their own array double mass[4096]?

Solution

A struct is 32 bytes, so a block holds two of them, but the loop uses only 8 bytes of each struct. One miss per two accesses: 50%. With a separate array, a block holds 8 masses: one miss per 8 accesses, 12.5%. The loop reads 4 times fewer bytes from memory.

More practice: Bryant–O'Hallaron, Practice Problems 6.10, 6.13–6.17 and 6.18; Homework 6.37.

InteractiveBryant–O'Hallaron, Ch. 7: Linking

Symbols and how the linker resolves them

Each .c file is compiled on its own, so a name like x used in two files means nothing until the linker decides which definition every reference gets. The rules are short. Some of their outcomes are bugs that compile and link without complaint.

The idea

The pipeline. The compiler driver runs the preprocessor, the compiler, the assembler (each .c becomes a relocatable object file .o) and finally the linker, which combines object files into an executable. The linker does two jobs: symbol resolution (match each reference with one definition) and relocation (give every section and symbol an address and patch the references).
Symbols. An object file's symbol table lists global symbols it defines (non-static functions and global variables), external ones it only references (marked undefined, UND), and local ones (static functions and variables), which are visible only inside that file. Ordinary local variables live on the stack and are not symbols at all.
Strong and weak. Functions and initialized globals are strong. Uninitialized globals (tentative definitions such as int x;) are weak.
The rules. (1) Two strong definitions of a name: link error. (2) One strong and some weak: the strong one wins. (3) Only weak ones: the linker picks one of them.
A newer default. The book (2011) describes gcc's behaviour of that time, where tentative definitions go in a COMMON block. Since gcc 10 the default is -fno-common: int x; is then an ordinary definition in .bss, so cases (2) and (3) with two definitions become link errors. The explorer lets you switch between the two.

The linker matches names only. It does not know types, so if one file defines double x and another uses int x, both silently refer to the same bytes.

Explore

In main.c, x is
In other.c, x is
Compiler

          
Two source files, compiled separately and then linked: gcc main.c other.c.
Each object file's entry for x, and the variable each file's uses of x end up at: main.c other.c

Worked example

main.o calls fmt_time, which is defined in libfmt.a. fmt_time calls pad, defined in libutil.a. The linker scans files left to right, keeping a set UU of names referenced but not yet defined. It takes a member out of a static library only if that member defines something in UU at the moment the library is scanned.

  1. Trace gcc main.o libutil.a libfmt.a.

    After main.o: U={𝚏𝚖𝚝_𝚝𝚒𝚖𝚎}U=\{\texttt{fmt\_time}\}. Scanning libutil.a: no member defines fmt_time, so nothing is taken. Scanning libfmt.a: its member defining fmt_time is added, and U={𝚙𝚊𝚍}U=\{\texttt{pad}\}. End of the command line with U≠⌀U\neq\varnothing: undefined reference to `pad'.

  2. Fix the order.

    gcc main.o libfmt.a libutil.a. When libutil.a is scanned, pad is already in UU. The rule: a library must come after every file that uses it.

  3. Now pad also calls fmt_num, which lives in libfmt.a. What happens with the fixed command?

    The member with pad adds fmt_num to UU, but libfmt.a was already scanned: undefined reference to fmt_num. (Unless fmt_num happens to sit in the same member as fmt_time, which was already taken in whole.)

  4. How do you link two libraries that depend on each other?

    Name one of them twice: gcc main.o libfmt.a libutil.a libfmt.a. (Alternatively, merge them into one library.)

Check your understanding

Which of these does not get an entry in the symbol table of foo.o?

  1. A static int counter = 0; at file scope.That's a local symbol: listed, but invisible to other files.
  2. A function bar that foo.c calls but doesn't define.It appears as an undefined (UND) symbol for the linker to resolve.
  3. A variable int i; declared inside a function.
Non-static locals are created on the stack (or in registers) when the function runs; the linker never sees them. A static variable inside a function, by contrast, does get a local symbol.

a.c has int n = 3; and b.c has int n = 5; (both at file scope). What happens when you link them?

  1. The linker picks one of them.That is rule 3, for weak symbols. Are these weak?
  2. A link error: n is defined twice.
  3. Each file keeps its own n.That happens only with static.
Both are initialized, so both are strong, and rule 1 applies. In the explorer, pick the initialized definition in both files.

a.c defines long buf[2] = {1, 2}; and b.c declares extern long *buf; and reads buf[0]. What happens?

  1. A link error: the types don't match.Object files carry no type information for the linker to compare.
  2. It links. b.c takes the array's first 8 bytes (the value 1) as a pointer and dereferences address 1, which crashes.
  3. It works: an array and a pointer are the same thing.An array name converts to a pointer in expressions, but a pointer variable is stored differently from an array.
The name buf resolves to the address of the array. Code compiled for extern long *buf loads 8 bytes from that address and treats them as a pointer. The linker resolved the name correctly; the declarations disagree.

Practice

Who wins?Weak, strong and static

a.c: int count; void bump(void); int main(void) { bump(); printf("%d\n", count); }. b.c: int count = 10; void bump(void) { count++; }. What does it print under the book's rules? With gcc 10? What if b.c declares static int count = 10; instead?

Solution

Book's rules: a.c's weak count resolves to b.c's strong one; bump makes it 11, and it prints 11. With gcc 10's -fno-common, both are definitions: a link error. With static in b.c there are two different variables; a.c's is the only global one, zero-initialized, and the program prints 0 under either compiler.

Patching a callPC-relative relocation

A call instruction is 5 bytes: opcode e8 followed by a 4-byte displacement relative to the address of the next instruction. It is placed at address 0x401020, and the linker puts the called function at 0x401100. Which 4 bytes does the linker write after e8?

Solution

The next instruction starts at 0x401025, so the displacement is 0x401100 - 0x401025 = 0xDB. As a little-endian 32-bit value: db 00 00 00. If the target were below the call, the displacement would be negative, in two's complement.

More practice: Bryant–O'Hallaron, Practice Problems 7.1, 7.2, 7.3 and 7.4; Homework 7.8 and 7.10.

InteractiveBryant–O'Hallaron, Ch. 8: Exceptional Control Flow

Processes, fork and wait

fork is called once and returns twice. After it, two processes run the same code with separate memory, and the kernel interleaves them as it pleases. The only guarantees about the order of their output are the ones you build in. Draw them as a graph and count the outputs that can really happen.

The idea

Exceptions. Control can leave a program without a jump: an interrupt (a device signals the processor), a trap (a deliberate system call), a fault (a possibly recoverable error such as a page fault) or an abort. Each transfers control to a kernel handler.
Process. A running instance of a program. Each process appears to have the processor to itself (its own logical control flow) and memory to itself (a private address space). The kernel switches between processes with context switches.
Process control. fork() creates a child with a copy of the parent's address space and its open files; it returns 0 in the child and the child's PID in the parent. exit(s) ends a process. waitpid(pid, &status, 0) (or wait(&status) for any child) blocks until a child has terminated, then reaps it. A terminated child that hasn't been reaped is a zombie. execve replaces the running program with a new one and, if it succeeds, never returns.
Process graph. Draw each process as a line of events. Edges run in program order, from each fork to the child's first event, and from a child's exit to the parent's wait that reaps it. Any ordering of the events that respects every edge is a possible execution, and no other ordering is.
Buffers are copied too. The examples assume each printf ends in \n and output goes to a terminal, so each line is written immediately. If text is still in the stdio buffer when fork runs, both processes inherit it and it can be printed twice.

Explore

Program
fork print child exit → parent's wait. After "Run it once", the numbers show the order a random schedule chose.

Worked example

int main(void) {
    int x = 5;
    pid_t pid = fork();
    if (pid == 0) {            /* child */
        x = x * 2;
        printf("child: %d\n", x);
        exit(0);
    }
    waitpid(pid, NULL, 0);     /* parent */
    x = x + 1;
    printf("parent: %d\n", x);
    return 0;
}
  1. What does this print?

    child: 10 then parent: 6.

  2. Why doesn't the child's x = 10 show up in the parent?

    After fork, each process has its own copy of x at the same virtual address. Changes in one are invisible to the other. (The kernel shares the physical pages until one side writes, but that's an implementation detail.)

  3. Is the order of the two lines guaranteed?

    Yes. The parent's printf comes after waitpid, which returns only after the child has exited, and the child prints before it exits.

  4. Delete the waitpid line. Now?

    Both orders are possible. Nothing connects the parent's print to the child's.

  5. Restore waitpid but delete the child's exit(0). Now?

    The child falls through. It calls waitpid(0, NULL, 0), which waits for any child in its process group; it has no children, so the call fails at once. It prints parent: 11 and exits. The real parent then reaps it and prints parent: 6. Output: child: 10, parent: 11, parent: 6, in that order.

Check your understanding

for (int i = 0; i < 3; i++) fork(); printf("x\n"); How many lines are printed?

  1. 3The children also run the rest of the loop.
  2. 4Each process alive at a fork becomes two.
  3. 8
Each pass through the loop doubles the number of processes: 23=82^3=8, and each prints once. Pick "fork in a loop" in the explorer.

After fork, the child sets a global variable g = 1. Does the parent see g == 1?

  1. Yes, globals are shared.Globals are shared between threads. Processes have separate address spaces.
  2. No. The parent has its own copy of g.
  3. Only after the child exits.Exiting doesn't copy anything back.
To pass data between processes you need an explicit channel: a pipe, a file, a socket, shared memory, or the exit status that waitpid collects.

A parent forks a child and never calls wait. The child exits. Until the parent ends, the child is…

  1. Gone: its memory and its entry in the process table are freed at once.The kernel keeps a small record, the exit status, until someone collects it.
  2. An orphan.An orphan is a child whose parent died first. It is then adopted by init.
  3. A zombie: terminated but not reaped.
A zombie holds no memory but still uses a process-table entry. Long-running servers must reap their children, typically in a SIGCHLD handler, or the zombies pile up.

Practice

All the outputsCheck with "if / else" in the explorer
printf("A\n");
if (fork() == 0)
    printf("B\n");
else
    printf("C\n");
printf("D\n");

List every possible output, written as a string of letters.

Hint

A comes first. The child prints B then D, the parent C then D. Interleave the two pairs.

Solution

There are (42)=6\binom42=6 interleavings, but the two Ds look alike, so only 4 distinct outputs: ABDCD, ABCDD, ACBDD, ACDBD.

Printed twicestdio buffers and fork

Change the first line above to printf("A"); (no newline) and run the program as ./prog > out.txt. How many As end up in the file? Why?

Solution

Two. Output to a file is fully buffered, so "A" is still in the stdio buffer in user memory when fork copies the address space. Both processes flush their copy when they exit. Calling fflush(stdout) before fork prevents it.

More practice: Bryant–O'Hallaron, Practice Problems 8.2, 8.3 and 8.4; Homework 8.11–8.15.

SimulationBryant–O'Hallaron, Ch. 9: Virtual Memory

Virtual memory and address translation

Every address your program uses is virtual. On each access the hardware splits it into a page number and an offset, looks the page number up (first in the TLB, then in the page table) and swaps it for a physical page number. Translate a few addresses yourself and watch hits, misses and page faults.

The idea

Pages. Virtual and physical memory are divided into pages of P=2pP=2^p bytes. A virtual address is a virtual page number and an offset, and a physical address is a physical page number and the same offset:
VA=VPN‖VPO,PA=PPN‖PPO,PPO=VPO (p bits).\text{VA} = \text{VPN} \,\|\, \text{VPO}, \qquad \text{PA} = \text{PPN} \,\|\, \text{PPO}, \qquad \text{PPO}=\text{VPO}\ (p\ \text{bits}).
Page table. A per-process array of page table entries (PTEs), indexed by VPN. A valid PTE holds the PPN where the page currently lives in DRAM; an invalid one says the page is on disk, or not allocated at all.
Page fault. Accessing a page whose PTE is invalid but allocated triggers a fault. The kernel's handler picks a victim page (writing it back to disk if it was modified), loads the needed page, updates the PTE, and the faulting instruction runs again. A truly unallocated address is a segmentation fault.
TLB. A small cache of PTEs inside the MMU. The low bits of the VPN select a TLB set (TLBI), the rest form the tag (TLBT). A TLB hit skips the page-table read entirely.
Why it pays. One mechanism gives caching (DRAM holds only the active pages), memory management (every process sees the same tidy layout; pages can be shared), and protection (PTE permission bits are checked on every access).

The explorer uses a toy machine: 16-bit virtual addresses, 12-bit physical addresses, 256-byte pages, a 2-way TLB with 4 sets, and 6 physical frames for this process.

Explore

TLB tag TLB index offset (copied unchanged) · physical page number
Page table (only this program's pages; all others invalid)

Worked example

A 32-bit machine has 4 KB pages and 4-byte PTEs.

  1. How big is a single flat page table per process?

    p=12p=12, so the VPN has 20 bits: 2202^{20} PTEs ×\times 4 bytes = 4 MB, for every process, even one that uses a few pages.

  2. Use two levels instead: the VPN splits into 10 + 10 bits. What does each level hold?

    The level-1 table has 2102^{10} entries (4 KB); each points to a level-2 table, also 4 KB, that maps 2102^{10} pages =4= 4 MB of virtual memory. Level-2 tables exist only for regions in use.

  3. A process uses 8 MB at the bottom of its address space and 1 MB of stack at the top. How much page-table memory does it need now?

    Two level-2 tables for the 8 MB, one for the stack, plus the level-1 table: 4×44\times4 KB = 16 KB instead of 4 MB.

  4. Split the virtual address 0x00403A1C into VPN1, VPN2 and VPO.

    VPO is the low 12 bits: 0xA1C. The VPN is 0x00403A1C >> 12 = 0x403; VPN1 ==0x403 >> 10=1= 1 and VPN2 ==0x403 & 0x3FF=3= 3. So: entry 1 of the level-1 table, entry 3 of that level-2 table, byte 0xA1C of the page.

Check your understanding

With 4 KB pages, the virtual address 0x12345678 is on a page mapped to PPN 0x9A. What is the physical address?

  1. 0x9A678
  2. 0x9A5678The offset has p=12p=12 bits: three hex digits, not four.
  3. 0x1234569AThe page number is replaced; the offset stays in the low bits.
PPO = VPO = 0x678, and the PPN goes on top: 0x9A followed by 678.

A load touches a page that is allocated but currently on disk. What happens?

  1. The process is killed with a segmentation fault.That happens for unallocated addresses or forbidden accesses, not for pages that are merely on disk.
  2. The MMU fetches the page from disk by itself.Page faults are handled in software, by the kernel.
  3. A page fault: the kernel loads the page, updates the PTE, and the load runs again.
The program never notices, apart from the delay. Try the "Heap, on disk" button in the explorer.

Processes A and B both store to virtual address 0x601000, in a private data page. What happens?

  1. They overwrite each other's data.Virtual addresses are meaningful only together with a page table, and each process has its own.
  2. They write different physical bytes: each page table maps that page to its own frame.
  3. The second store faults.Nothing is wrong with either access.
Both processes can use the same layout because translation is per process. Sharing (of library code, say) is done deliberately, by mapping pages of both processes to the same frame.

Practice

Field widthsSizes from parameters

Virtual addresses are 39 bits, physical addresses 32 bits, pages 8 KB. How many bits are in the VPO, VPN and PPN? How many PTEs would a flat page table have?

Solution

8 KB=2138\text{ KB}=2^{13}: VPO 13 bits, VPN 39−13=2639-13=26 bits, PPN 32−13=1932-13=19 bits. A flat table would have 226=67,108,8642^{26}=67{,}108{,}864 entries.

TLB reachWhy huge pages exist

A TLB has 64 entries. How much memory can it map with 4 KB pages, and with 2 MB pages? A program reads random elements of a 64 MB array. Which page size gives far fewer TLB misses?

Solution

64×4 KB=25664\times4\text{ KB}=256 KB versus 64×2 MB=12864\times2\text{ MB}=128 MB. With 4 KB pages the array spans 16384 pages, so almost every random access misses the TLB. With 2 MB pages it spans 32 pages, which all fit, so after the first touches there are no TLB misses.

More practice: Bryant–O'Hallaron, Practice Problems 9.2, 9.3 and 9.4; Homework 9.11–9.13.

InteractiveBryant–O'Hallaron, Ch. 9: Virtual Memory (dynamic memory allocation)

Dynamic memory allocation

malloc and free manage one long run of bytes, the heap, using nothing but a few bits stored inside it. Allocate and free blocks in a small heap, switch the placement policy, and find out how a heap with plenty of free space can still fail a request.

The idea

The contract. malloc(n) returns a pointer to at least nn bytes, aligned (here to 8 bytes), or NULL. free(p) must be given a pointer that malloc returned. The allocator can't move allocated blocks, and must answer requests in the order they arrive.
Implicit free list. Every block starts with a 4-byte header holding the block size, with the low bit set if allocated. Sizes are multiples of 8, so the low three bits are free for flags: an allocated 24-byte block has header 24+1=24+1=0x19. Walking from header to header visits every block, free or not.
Boundary tags. A copy of the header at the end of the block (the footer) lets free find the previous block's size in constant time, so it can merge (coalesce) a freed block with free neighbours on both sides. With header and footer, a block needs at least 16 bytes: size=max⁡(16, 8⌈(n+8)/8⌉)\text{size}=\max\!\big(16,\ 8\lceil (n+8)/8\rceil\big).
Placement. First fit takes the first free block that is big enough; next fit starts searching where the last search stopped; best fit takes the smallest block that is big enough. A block that is larger than needed is split if the leftover can form a block of its own (16 bytes here).
Fragmentation. Internal: bytes inside allocated blocks that the program didn't ask for (header, footer, padding). External: enough free memory in total, but no single free block large enough.

Real allocators keep explicit lists of the free blocks only, often split by size class, so they don't walk past allocated ones. The explorer uses the simple implicit list on a 256-byte heap: 4 bytes of padding, then blocks, then a 4-byte end marker.

Explore

Placement
Coalescing
Try a script:
Each cell is a 4-byte word; each row is 64 bytes. requested bytes padding header and footer (size/allocated) · unfilled: free. Click an allocated block to free it.

Worked example

The heap holds one free 64-byte block. With 4-byte headers and footers, 8-byte alignment and a 16-byte minimum block, run p = malloc(10); q = malloc(20); free(p); free(q);.

  1. What size is p's block, and what is its header?

    10+8=1810+8=18, rounded up to 24. Header 24|1=24\,|\,1=0x19. Splitting leaves a free block of 40 bytes, header 0x28.

  2. What happens for q = malloc(20)?

    It needs 20+8=28→3220+8=28\to32 bytes. The leftover would be 40−32=840-32=8 bytes, too small for a block, so q gets the whole 40-byte block: header 0x29.

  3. How much internal fragmentation does q's block have?

    The program asked for 20 bytes in a 40-byte block: 20 bytes are overhead (8 for header and footer, 12 unused).

  4. free(p): does it coalesce?

    No. Its only neighbour, q, is allocated. The header becomes 0x18.

  5. free(q): what is the heap now?

    Its previous neighbour is free: the footer just before q's header says "24, free". The two merge into one free 64-byte block, header 0x40. Check it in the explorer starting from an empty heap: the steps look the same, only in a larger heap.

Check your understanding

Why does an implicit-list allocator need a footer to coalesce with the previous block in constant time?

  1. A header tells you where the next block starts, but not where the previous one starts. The footer sits just before your header.
  2. Because free must check that the pointer is valid.The allocator generally doesn't check. The question is about finding neighbours.
  3. To store the requested size nn.The footer is a copy of the header: block size and allocated bit.
Without footers, finding the previous block means walking the list from the start, which takes time linear in the number of blocks. A common refinement stores footers only in free blocks and keeps a "previous is allocated" bit in each header.

The heap has 120 free bytes in total, yet malloc(48) fails. What is this called?

  1. Internal fragmentation.That's wasted space inside allocated blocks.
  2. External fragmentation.
  3. A memory leak.A leak is memory the program no longer uses but never freed. These bytes are free.
The free space is split into pieces smaller than the 56-byte block the request needs. The "Fragment the heap" script builds exactly this situation.

Which is true of best fit compared with first fit?

  1. It usually wastes less memory, but each request must search the whole free list.
  2. It is always faster.It can stop early only on an exact fit.
  3. It never needs to split blocks.The best-fitting block can still be larger than the request.
This is the usual trade-off between utilization and throughput. Run "Policies differ" under each policy and compare the largest free block at the end.

Practice

Headers by handBlock sizes

With the rules above, give the block size and header value for malloc(1), malloc(13) and malloc(16).

Solution

1+8=9→161+8=9\to16: header 0x11. 13+8=21→2413+8=21\to24: 0x19. 16+8=2416+8=24: 0x19. Note that malloc(1) costs 16 bytes.

Make malloc failDesign the fragmentation

In the explorer's 256-byte heap (248 bytes for blocks), find a sequence of malloc and free calls after which at least 100 bytes are free but malloc(48) returns NULL.

Hint

Allocate equal blocks until the heap is full, then free every other one.

Solution

Six malloc(32) calls: five 40-byte blocks, and a sixth that takes the last 48 bytes whole (a 40-byte block would leave 8, too small to split off). Free the 1st, 3rd and 5th: 120 bytes free in three 40-byte pieces separated by allocated blocks, so nothing can coalesce. malloc(48) needs a 56-byte block and fails.

More practice: Bryant–O'Hallaron, Practice Problems 9.6, 9.7 and 9.10; Homework 9.15–9.17.

Step-throughBryant–O'Hallaron, Ch. 10: System-Level I/O

Unix I/O: descriptors and sharing

A file descriptor is a small integer, but the kernel connects it to the file through two more tables, and the file position lives in the middle one. That one detail decides what fork, dup2 and opening a file twice do. Step through four short programs and watch the tables.

The idea

Unix I/O. Files, terminals, pipes and sockets are all read and written the same way. open returns the smallest descriptor not in use; a process starts with 0, 1 and 2 (standard input, output and error). read(fd, buf, n) copies up to nn bytes from the current file position and advances it; it returns the number of bytes read, 0 at end of file, or −1-1 on error.
Short counts. read and write may move fewer bytes than asked, near end of file, on terminals, and constantly on network sockets. Robust code loops until it has what it needs.
Three tables. Each process has a descriptor table whose entries point into the kernel's shared open file table. An open file entry holds the file position and a reference count, and points to a v-node entry with the file's metadata (size, type, and so on).
Who shares what. Each call to open creates a new open file entry, so two opens of the same file have independent positions. fork copies the descriptor table, so parent and child point to the same entries and share positions. dup2(old, new) closes new if needed and makes it point to old's entry.

Redirection in a shell (prog > out.txt) is just dup2 of a freshly opened file onto descriptor 1, done in the child before execve.

Explore

Program

        
The highlighted line runs on the next step.
Left: descriptor tables (one per process). Middle: open file table, with each entry's file position (gold bar) and reference count. Right: v-node table. The file notes.txt holds the six bytes kernel.

Worked example

How does a shell run sort < in.txt? Sketch the system calls and the descriptor tables.

  1. Which process should open in.txt: the shell or a child?

    A child. The shell calls fork, and the child changes its own standard input. The shell's descriptor 0 must stay connected to the terminal.

  2. The child calls fd = open("in.txt", O_RDONLY). Which descriptor does it get?

    The smallest unused one: 3, since 0, 1 and 2 are open.

  3. How does it make descriptor 0 refer to the file?

    dup2(fd, 0) closes the child's old descriptor 0 (the terminal's open file entry loses one reference; the shell still holds it) and points 0 at in.txt's entry. Then close(fd), since descriptor 3 is no longer needed.

  4. The child calls execve("/usr/bin/sort", ...). Why does sort read from the file?

    execve replaces the code and data but keeps the descriptor table. sort reads descriptor 0 like always and has no idea it is a file.

Check your understanding

A process has descriptors 0, 1 and 2 open. It calls close(0) and then open("a.txt", O_RDONLY). Which descriptor is returned?

  1. 3Is 3 the smallest descriptor not in use?
  2. 0
  3. It depends on the file system.The rule is fixed: the lowest unused descriptor.
After close(0), 0 is the lowest free slot. Older shells redirected input exactly this way, before dup2 existed.

A file of 8 bytes is opened, then the process forks. Parent and child each call read(fd, buf, 4) once. Do they read the same bytes?

  1. Yes, each has its own copy of everything.The descriptor table is copied, but the entries it points to are not.
  2. No: they share one file position, so one gets bytes 0–3 and the other bytes 4–7.
  3. Only the parent can read; the child's descriptor is closed.Children inherit open descriptors.
Which process gets which half depends on which reads first. Step through "fork shares" in the explorer: the child's read moves the position for the parent too.

read(sockfd, buf, 4096) on a network socket returns 100. What does that mean?

  1. An error: 4096 bytes were requested.Errors are reported as −1-1.
  2. The connection is closed.A closed connection reads as 0, end of file.
  3. A short count: 100 bytes had arrived so far. Read again for more.
Data on a network arrives in pieces, so a single read returns what is there. The book's Rio functions wrap the necessary loop.

Practice

Order of redirectionsTwo dup2 calls

In a shell, cmd > out.txt 2>&1 sends both output and errors to the file, but cmd 2>&1 > out.txt sends errors to the terminal. With f = open("out.txt", ...), write each one as dup2 calls and explain the difference.

Hint

2>&1 means dup2(1, 2): copy what descriptor 1 points to now.

Solution

First form: dup2(f, 1); dup2(1, 2);. Descriptor 1 already points to the file when 2 copies it, so both go to the file. Second form: dup2(1, 2); dup2(f, 1);. Descriptor 2 copies 1 while 1 still points to the terminal; changing 1 afterwards doesn't affect 2.

Redirection without dup2The lowest-descriptor rule

Using only close and open, make printf write to log.txt. Why is dup2 still the better choice in a multithreaded program?

Solution

close(1); open("log.txt", O_WRONLY|O_CREAT|O_TRUNC, 0644); The new file gets descriptor 1, the lowest free one. But between the close and the open, another thread could open something and take descriptor 1. dup2 does the replacement in one atomic step.

More practice: Bryant–O'Hallaron, Practice Problems 10.1–10.5; Homework 10.6.

Worked exampleBryant–O'Hallaron, Ch. 11: Network Programming

Sockets and the client–server model

To a program, a network connection is a pair of file descriptors that happen to be on different machines. Setting one up takes a few calls in a fixed order, and almost every bug in between comes from byte order or from assuming one read returns a whole message.

The idea

Client–server. A server process manages a resource and waits; a client sends a request, the server handles it and sends a response. Both are ordinary processes, often on different hosts.
Addresses. An IPv4 address is a 32-bit number, written in dotted decimal one byte at a time (192.0.2.10). Internet protocols send multi-byte numbers big-endian (network byte order), so a little-endian host converts with htonl/htons and back with ntohl/ntohs. Domain names are mapped to addresses by DNS.
Connections. A connection is identified by its socket pair: (client address : client port, server address : server port). Servers listen on well-known ports (80 for HTTP); the client's kernel picks an unused ephemeral port for each connection.
The calls. Client: socket, then connect to the server's address. Server: socket, bind to a port, listen, then loop on accept, which blocks until a client connects and returns a new connected descriptor. The listening descriptor stays open for further clients. Once connected, both sides use plain read, write and close.
int listenfd = open_listenfd(port);   /* socket + bind + listen */
while (1) {
    int connfd = accept(listenfd, NULL, NULL);   /* waits for a client */
    handle(connfd);                   /* read request, write reply */
    close(connfd);
}
HTTP. A request is text: a request line such as GET /index.html HTTP/1.1, header lines such as Host: www.example.com, then an empty line. Each line ends in \r\n. The response starts with a status line (HTTP/1.1 200 OK), headers, an empty line, then the content.

The book builds addresses with gethostbyname and friends; current code uses getaddrinfo, which also handles IPv6. The ideas below are the same either way.

Worked example

A client on host 192.0.2.10 fetches a page from a web server at 198.51.100.7, port 80. The client machine is little-endian (x86).

  1. Write the server's address as a 32-bit hex number.

    One byte per field: 198=198=C6, 51=51=33, 100=100=64, 7=7=07. The address is 0xC6336407.

  2. In network byte order, in what order do those bytes sit in memory? What value would the x86 client see if it read them as a uint32_t without ntohl?

    Big-endian, so most significant first: C6 33 64 07. Read little-endian, those bytes are 0x076433C6. ntohl swaps them back.

  3. The port: what bytes does htons(80) put in memory, and what port would a server listen on if you stored plain 80 on x86?

    80=80=0x0050. htons(80) gives the bytes 00 50. Stored without conversion on x86, the bytes are 50 00, which the network reads as 0x5000 = port 20480.

  4. The client's kernel picks port 50123. What is the socket pair? Could a second tab in the same browser use the same pair?

    (192.0.2.10:50123, 198.51.100.7:80). No: a second connection gets a different ephemeral port, and the differing pair is how both kernels tell the two connections apart.

  5. On the server, which calls block, and which descriptors exist after accept returns?

    accept blocks until a connection arrives (and read blocks until data does). Afterwards there are two: listenfd, still open for the next client, and connfd, connected to this client only.

  6. The client sends the request below in one write. How does the server know the headers have ended?

    GET /index.html HTTP/1.1\r\n
    Host: www.example.com\r\n
    \r\n

    By the empty line: \r\n right after a \r\n. Since a read may return only part of the request, the server keeps reading, line by line, until it sees that empty line. That is the short-count lesson from the previous module.

Check your understanding

A server's listening descriptor is 3, and accept returns 5. Which descriptor does it read the client's request from?

  1. 3The listening descriptor only produces new connections; no data flows through it.
  2. 5
  3. Either one.They play different roles.
Descriptor 5 is this client's connection. Keeping them separate is what lets a concurrent server accept the next client while still talking to this one.

Two programs on one laptop connect to the same server, port 80. How does the server's kernel deliver each packet to the right connection?

  1. By the client's IP address.Both connections come from the same address.
  2. By the full socket pair: the client ports differ.
  3. It can't; the second connection is refused.A server port accepts many connections at once.
Each connection is a distinct 4-tuple of addresses and ports, and the kernel looks every incoming packet up by that tuple.

Why does the server need the empty line at the end of an HTTP request, when TCP already delivers the bytes reliably?

  1. TCP is a byte stream with no message boundaries, so the protocol has to mark where the request ends.
  2. To give the network time to deliver the last packet.The empty line is data, not a delay.
  3. For compatibility with old browsers only.Every HTTP/1.x server relies on it to find the end of the headers.
The receiver sees a stream of bytes that may arrive in arbitrary chunks. Delimiters (the empty line) and lengths (the Content-Length header for a body) tell it where one message stops.

Practice

Address conversionsDotted decimal and hex

Convert 172.16.254.1 to hex, and 0xC0A80064 to dotted decimal.

Solution

172=172=AC, 16=16=10, 254=254=FE, 1=1=01: 0xAC10FE01. And C0=192=192, A8=168=168, 00=0=0, 64=100=100: 192.168.0.100.

The missing bytesReading from a socket

A client sends a 2000-byte message with one write. The server calls read(connfd, buf, 8192) once and gets 1448 bytes. Is something broken? How should the server read a message whose length it knows in advance?

Solution

Nothing is broken: the rest is in a later packet. Loop: keep calling read on the remaining space until 2000 bytes have arrived, stopping on 0 (the client closed early) or −1-1 (an error, retrying if it was only interrupted by a signal).

More practice: Bryant–O'Hallaron, Practice Problems 11.1, 11.2 and 11.3.

InteractiveBryant–O'Hallaron, Ch. 12: Concurrent Programming

Threads, races and semaphores

Two threads that each add 1 to a shared counter can leave it with only one of the additions. Be the scheduler: choose which thread runs each instruction, and watch the counter, the registers and the path you trace through a progress graph. Then add a lock and see the paths that remain.

The idea

Three ways to be concurrent. Processes (separate address spaces; share data only through explicit channels), I/O multiplexing (one process using select to react to whichever descriptor is ready), and threads (several flows of control in one process).
Threads share memory. Each thread has its own registers, program counter and stack, but globals, the heap, static variables and open files are shared by all of them.
volatile long cnt = 0;              /* shared */
void *worker(void *arg) {
    for (long i = 0; i < NITERS; i++)
        cnt++;    /* load cnt into %rdx; add 1; store %rdx to cnt */
    return NULL;
}
Progress graph. Plot how many instructions thread 1 has completed against how many thread 2 has. An execution is a path that moves one step right or up at a time. The unsafe region is where both threads are inside the load–add–store of the same variable; a path that enters it can lose an update.
Semaphores. A semaphore ss is a counter that is never negative. P(s)P(s): wait until s>0s>0, then decrement. V(s)V(s): increment (waking a waiting thread). With s=1s=1 around a critical section (a mutex), the states where both threads hold it become a forbidden region that surrounds the unsafe region, so every possible path is safe.
Deadlock. If two threads take two locks in opposite orders, each can end up holding one and waiting forever for the other. Rule: give the locks a global order and always acquire them in that order.

Explore

Thread 1 across, thread 2 up. Each iteration is H (loop test), L, U, S (the three instructions of cnt++) and T (i++). unsafe region forbidden region (with the mutex) your path
Each thread runs cnt++
Protection

Worked example

A bank moves money between accounts, each with its own mutex:

void transfer(struct account *from, struct account *to, long amt) {
    P(&from->lock);
    P(&to->lock);
    from->balance -= amt;
    to->balance   += amt;
    V(&to->lock);
    V(&from->lock);
}

Thread 1 calls transfer(&A, &B, 10) while thread 2 calls transfer(&B, &A, 5).

  1. Find a schedule that never finishes.

    Thread 1 does P(&A.lock), then thread 2 does P(&B.lock). Now thread 1 waits for B's lock, which thread 2 holds, and thread 2 waits for A's lock, which thread 1 holds. Neither can proceed: deadlock.

  2. In a progress graph, where is the trouble?

    There are two forbidden regions, one per lock, and they overlap in a way that leaves a pocket below and to the left of their overlap. A path that enters the pocket can only move right into one forbidden region or up into the other: it is stuck. That pocket is the deadlock region.

  3. Fix transfer without a global lock.

    Lock in a fixed order, for example by account number: lock min(from, to) first, then max(from, to); unlock in either order. Both threads now try A first, so whichever gets A will also get B.

  4. Does the fix still work with many threads and many accounts?

    Yes. If everyone acquires locks in increasing order, a cycle of waiting threads is impossible: the thread holding the highest-numbered lock in such a cycle could only be waiting for an even higher lock, which no one in the cycle holds.

Check your understanding

Starting from cnt = 0, two threads each run cnt++ once, with no lock. Which final values are possible?

  1. Only 2.Try running thread 1's load, then all of thread 2.
  2. 1 or 2.
  3. 0, 1 or 2.Whatever happens, some thread stores a value it computed as (a value it loaded) + 1.
If both load before either stores, both store 1. Every path through the unsafe region gives 1, every path around it gives 2.

Which of these is shared by all threads running worker?

  1. long i, declared inside worker.Each thread has its own stack, so each call has its own i.
  2. static long calls, declared inside worker.
  3. The parameter arg.Each thread gets its own copy of the pointer. What it points to may be shared.
A static local has a single instance for the whole program, stored with the globals, even though only worker can name it.

for (long i = 0; i < 4; i++) pthread_create(&tid[i], NULL, work, &i); Each thread starts by reading *(long *)arg. What can a thread see?

  1. Its own index, 0 to 3.That needs the thread to read i before the loop changes it. Nothing guarantees that.
  2. Whatever i holds when it reads: two threads may see the same value, and a thread may even see 4.
  3. Always 4.A thread may well run before the loop finishes.
All four threads get the address of the same variable, a race between the main thread's i++ and each thread's read. Pass each thread its own copy instead: a separate malloc'd long, or the value cast to a pointer.

Practice

Counting interleavingsCheck with "Run 1000 random schedules"

Each thread runs cnt++ once as load, add, store. How many interleavings of the six instructions are there, and how many of them leave cnt = 2?

Solution

(63)=20\binom63=20 interleavings: choose which 3 of the 6 slots belong to thread 1. Only 2 are safe: all of thread 1 before thread 2, or the reverse. Any other interleaving has a load by one thread between the other thread's load and store, and loses an update. The explorer's random schedules are not uniform over interleavings: they also run each thread's loop test H and increment T, and flip a coin at every step. Under that scheduler 3 runs in 8 give 2, so expect about 375 of 1000, not 100.

A one-slot bufferSemaphores for ordering

One producer and one consumer share int buf. Use semaphores slots (initially 1) and items (initially 0) so the consumer gets every value exactly once. Why is no separate mutex needed?

Solution

Producer: P(&slots); buf = x; V(&items); Consumer: P(&items); y = buf; V(&slots);. Since slots + items is at most 1, the producer can write only after the consumer has taken the previous value, and the consumer can read only after a new value is written. The two never touch buf at the same time. Even several producers and consumers would be safe here, since P(&slots) lets only one producer at a time near the single slot; a buffer of several slots, with shared indices, is where a mutex becomes necessary.

More practice: Bryant–O'Hallaron, Practice Problems 12.7, 12.8 and 12.15; Homework 12.18 and 12.29.