droid
Clock Replacement
Memory and Space · Stage 3 of 8

Clock Replacement

Give every page a second chance, so the pages in use stay and the ones nobody needs leave

Choose the page that leaves a full pool with the clock: a reference bit on every frame, set by each hit, and a hand that sweeps the frames clearing bits until it finds an unpinned page whose bit is already 0. Show the hand, the bits and the pins with pager clock.

Not run yet · 7 tests
Concepts in this stage
  • The replacement policy decides which page leaves, and it decides how many misses a workload costs
  • Evicting in the order pages came in throws out the pages used most, sooner or later
  • A reference bit records that a page was used since the hand last passed
  • A second chance: the hand clears a set bit instead of evicting, and evicts only a page that has not been used since
  • Pins still win: the hand passes over a pinned page without touching its bit
Where this stage sits in the engine
Talking to the user
REPL
Sessions
Printer
Server
Wire protocol
Understanding SQL
LexerParserValidator
Answering a query
PlannerExecutor
Keeping changes safe
Transactions
Write-ahead log
Undo log
Lock manager
Read views
Storing rows
Row formatSlotted pageB+Tree
Reaching the disk
Pager
Database file
this stagebuilt alreadystill to comeA statement enters at the top; rows come back up.

Why this stage exists

Until now, which page leaves a full pool has been up to you. Most first versions pick the page that came in first, or the next frame in turn. That is simple, it is correct, and it is bad at the one thing a cache is for.

Think of what a query does to a B+Tree. Every lookup starts at the root. With 16 frames and a table of a few hundred leaves, the root is the single most used page in the database. First in, first out does not know that. The root came in early, so it is among the first to be thrown out, and the next lookup reads it straight back from the file. The pages a workload uses most are exactly the ones that have been in the pool longest, and an order-of-arrival policy evicts them for that reason.

What you want is the page that has gone longest without being used. Tracking that exactly means reordering a list on every single hit, which is a lot of work for the most common event in the pool. The clock gets most of the benefit for almost none of the cost: one bit per frame, set on a hit, and a hand that only moves when something has to leave.

From this stage the choice is no longer yours. The contract fixes it exactly, so the counters are exact with any pool, and the tests can watch the hand move.

What changes: a bit per frame and a hand

Picture the frames in a circle with a hand pointing at one of them. Each frame has a reference bit. A hit sets the bit of its frame to 1, and that is all a hit costs.

When a page must come in and there is an empty frame, it goes to the lowest-numbered one, its bit set to 1, and the hand stays where it is. When there is no empty frame, the hand sweeps:

  • a pinned page is passed over, its bit left alone;
  • a bit of 1 is set to 0, and the hand moves on: the page gets a second chance;
  • a bit of 0 on an unpinned page: that is the victim.

The new page takes the victim's frame with its bit set to 1, and the hand moves one past it. A page that was used since the hand last went by survives one more revolution. A page nobody touched in a whole revolution leaves.

Drawing the diagram…

If every frame is pinned the sweep can never stop, so it does not start: the request is 01201 and the hand stays where it was.

A full example

The worked sequence of the contract, on a file of five pages with --cache-pages 3. Opening reads page 0 into frame 0 with its bit at 1, before the first command:

CommandWhat happensFrame 0Frame 1Frame 2Hand
openpage 0 read, lowest empty framepage 0, bit 1emptyempty0
pager get 1miss, lowest empty framepage 0, bit 1page 1, bit 1empty0
pager get 2miss, lowest empty framepage 0, bit 1page 1, bit 1page 2, bit 10
pager get 3miss, no empty frame: the hand clears frames 0, 1 and 2, comes back to frame 0, finds bit 0 and evicts page 0page 3, bit 1page 1, bit 0page 2, bit 01
pager get 1hit: set the bitpage 3, bit 1page 1, bit 1page 2, bit 01
pager get 4miss: frame 1 is cleared, frame 2 has bit 0, page 2 leavespage 3, bit 1page 1, bit 0page 4, bit 10

Page 2 leaves, not page 1, because page 1 was used again after the hand last went by. Leave out that pager get 1 and page 1 is the one that goes. Evicting in the order pages came in would have thrown out page 1 either way.

The first sweep goes all the way round. When every bit is 1, the hand clears all of them and comes back to where it started. That is expected: all three pages had been used since the last sweep, which was never.

Least recently used. LRU evicts the page whose last use is oldest, exactly. It needs every hit to move its page to the front of a list, and on a busy pool that list becomes the bottleneck. The clock approximates it with one bit. PostgreSQL uses a clock with a small usage count in place of the bit; MySQL's InnoDB uses an LRU list split in two, so that one big scan cannot flush the hot pages out; LRU-K and 2Q are other answers to the same problem.

The algorithm in words

1

Keep the bits and the hand

The state of the clock is small: one bit per frame, and one frame number.

  1. 1.The frames are numbered from 0. Each has a reference bit. The hand points at one frame, frame 0 when the pool is new and empty.
  2. 2.A hit sets the bit of the page's frame to 1. Nothing else happens on a hit: the hand does not move.
  3. 3.A page coming in while some frame is empty goes to the lowest-numbered empty frame with its bit set to 1. The hand does not move.
  4. 4.Opening a file reads page 0 into frame 0, so a session on a file with pages starts with page 0 in frame 0, its bit at 1, and the hand at 0.
2

Sweep for a victim

Only when no frame is empty does the hand move.

  1. 1.Look at the frame under the hand. If its page is pinned, pass over it and leave its bit as it is.
  2. 2.If its bit is 1, set it to 0 and move the hand to the next frame. After the last frame comes frame 0.
  3. 3.If its bit is 0 and its page is unpinned, that page is the victim. Write it first if it is dirty, as before.
  4. 4.Once the victim is chosen, the hand moves one frame past it. The new page then takes the victim's frame with its bit set to 1.
  5. 5.If every page in the pool is pinned there is no victim: 01201, and the hand stays where it was.
3

A read that fails

A page that fails its checksum makes room like any other.

  1. 1.The sweep runs and the victim leaves before the read, so the victim stays evicted and the hand stays moved.
  2. 2.The page that failed does not come in. Its frame is left empty, and the next page to come in takes the lowest empty frame.

Output contract for this stage

The full definition is on the output contract page, section 25.

InputOutput
pager clock[PAGER] Clock: hand=<i> frames=<n>, then for each frame in order [PAGER] Frame <i>: page=<p> ref=<0 or 1> pin=<k> or [PAGER] Frame <i>: empty

pager clock requests nothing. pager frames does not change: it lists pages, in page order; pager clock lists frames, in frame order. From this stage the counters after the pager commands of section 23 are exact with any pool.

What you should see when done

The worked sequence, on the five-page file of the earlier stages:

shell
$droid --db notes.db --cache-pages 3
Welcome to droid!
>pager get 1
[PAGER] Get page 1: CACHE_MISS (loaded from disk)
>pager get 2
[PAGER] Get page 2: CACHE_MISS (loaded from disk)
>pager get 3
[PAGER] Get page 3: CACHE_MISS (loaded from disk)
>pager clock
[PAGER] Clock: hand=1 frames=3
[PAGER] Frame 0: page=3 ref=1 pin=0
[PAGER] Frame 1: page=1 ref=0 pin=0
[PAGER] Frame 2: page=2 ref=0 pin=0
>pager get 1
[PAGER] Get page 1: CACHE_HIT
>pager get 4
[PAGER] Get page 4: CACHE_MISS (loaded from disk)
>pager clock
[PAGER] Clock: hand=0 frames=3
[PAGER] Frame 0: page=3 ref=1 pin=0
[PAGER] Frame 1: page=1 ref=0 pin=0
[PAGER] Frame 2: page=4 ref=1 pin=0
>.exit
exiting.. good bye!

Things to notice. After page 3 came in, the hand was at 1, not 0: it moves one past the frame it filled. The hit on page 1 set its bit back to 1, which is what saved it: the sweep for page 4 cleared it instead of evicting it, and moved on to page 2.

Common bugs in this stage

  • Moving the hand when a frame is empty. Filling an empty frame does not touch the hand. Only a sweep moves it.
  • Leaving the hand on the victim's frame. After the new page goes in, the hand moves one past it. Otherwise the next sweep starts by clearing the page that just arrived.
  • Clearing a pinned page's bit. The hand passes over a pinned page and leaves its bit alone.
  • Sweeping forever. With every page pinned there is no victim. Check for that before sweeping, or stop after passing every frame once with nothing found, and restore nothing you changed: the hand must not move.
  • Setting the bit to 0 for a new page. A page that just came in was just used: its bit is 1.
  • Forgetting the empty frame after a bad page. When the page read for a frame fails its checksum, the victim is already gone and the frame stays empty. The next page goes there, without a sweep.

Language notes

The page above is the same for every language. These are the only language-specific hints.

  • C: the bit can be a field of the frame struct next to its pin count and dirty flag. The hand is an unsigned index into the frames; moving past the last frame brings it back to the first.
  • C++: the same. std::vector<bool> saves space, but a plain bool in the frame struct is simpler to read and debug.
  • Rust: keep the hand as usize and the frames in a Vec; the sweep is a loop with an index, not an iterator, because it wraps round and stops in the middle.
  • Zig: the same shape, with the hand as a usize that wraps at frames.len. A labelled while loop that breaks out with the victim keeps the sweep readable.

Checklist

One line per thing the tests check. The tags name the test cases.

  • The worked sequence of the contract, frame by frame
    clock-worked-sequence
  • Without the second request, page 1 is the one that leaves
    clock-second-chance
  • A page asked for between every two misses is never evicted
    clock-hot-page-stays
  • The hand passes over pinned pages, and with every page pinned it does not move
    clock-skips-pinned
  • A dirty victim is written before its frame is reused
    clock-dirty-victim
  • A page that fails its checksum leaves its frame empty, and the next page takes it
    clock-bad-page-leaves-frame-empty
  • pager clock on a new file shows every frame empty and the hand at 0
    clock-empty-pool