Files
zzg 661faba845
ci / test (push) Canceled after 0s
initial commit
2026-09-25 22:31:34 -07:00

407 lines
10 KiB
Go

// Package memory models virtual memory: address translation through a page
// table, a TLB, and the page replacement policies that decide what to evict
// when physical memory is full.
package memory
import (
"container/list"
"fmt"
"sort"
)
// PageSize is the bytes per page. A power of two means the split between
// page number and offset is a shift and a mask rather than a division.
const PageSize = 4096
// Address is a virtual or physical address.
type Address int
// PageNumber returns the page containing the address.
func (a Address) PageNumber() int { return int(a) / PageSize }
// Offset returns the address's offset within its page.
func (a Address) Offset() int { return int(a) % PageSize }
// Frame is a physical page frame.
type Frame struct {
Number int
// Page and PID identify what currently occupies the frame.
Page int
PID int
// Referenced and Dirty are the bits hardware would set; the clock
// policy uses Referenced, and Dirty decides whether eviction costs a
// write-back.
Referenced bool
Dirty bool
LoadTime int
LastUsed int
}
// Stats counts what happened during a run.
type Stats struct {
Accesses int
PageFaults int
TLBHits int
TLBMisses int
Evictions int
// DiskWrites counts evictions of dirty frames, which are the expensive
// ones: a clean frame can simply be dropped.
DiskWrites int
}
// FaultRate returns page faults per access.
func (s Stats) FaultRate() float64 {
if s.Accesses == 0 {
return 0
}
return float64(s.PageFaults) / float64(s.Accesses)
}
// TLBHitRate returns TLB hits per lookup.
func (s Stats) TLBHitRate() float64 {
total := s.TLBHits + s.TLBMisses
if total == 0 {
return 0
}
return float64(s.TLBHits) / float64(total)
}
func (s Stats) String() string {
return fmt.Sprintf("accesses=%d faults=%d (%.1f%%) tlb=%d/%d (%.1f%%) evictions=%d writes=%d",
s.Accesses, s.PageFaults, s.FaultRate()*100,
s.TLBHits, s.TLBHits+s.TLBMisses, s.TLBHitRate()*100,
s.Evictions, s.DiskWrites)
}
// Policy chooses which frame to evict when memory is full.
type Policy interface {
Name() string
// Evict returns the frame index to reclaim.
Evict(frames []*Frame, clock int) int
// Touch notes an access to a resident frame, for policies that need
// access history.
Touch(frame *Frame, clock int)
}
// FIFO evicts the frame loaded longest ago.
//
// Simple, but ignores usage entirely — and suffers Bélády's anomaly, where
// adding frames can *increase* the fault count.
type FIFO struct{}
func (FIFO) Name() string { return "FIFO" }
func (FIFO) Evict(frames []*Frame, _ int) int {
oldest := 0
for i, f := range frames {
if f.LoadTime < frames[oldest].LoadTime {
oldest = i
}
}
return oldest
}
func (FIFO) Touch(*Frame, int) {}
// LRU evicts the least recently used frame.
//
// A good approximation of the optimal policy for real workloads, because
// recently used pages tend to be used again. Exact LRU needs a timestamp
// update on every access, which is why hardware usually implements an
// approximation like clock instead.
type LRU struct{}
func (LRU) Name() string { return "LRU" }
func (LRU) Evict(frames []*Frame, _ int) int {
oldest := 0
for i, f := range frames {
if f.LastUsed < frames[oldest].LastUsed {
oldest = i
}
}
return oldest
}
func (LRU) Touch(f *Frame, clock int) { f.LastUsed = clock }
// Clock (second chance) approximates LRU using one reference bit per frame.
//
// The hand sweeps the frames; a frame with its reference bit set gets a
// second chance and the bit is cleared. This is what real hardware supports,
// since it needs only a single bit per page rather than a timestamp.
type Clock struct{ hand int }
func (c *Clock) Name() string { return "clock (second chance)" }
func (c *Clock) Evict(frames []*Frame, _ int) int {
for {
f := frames[c.hand]
if !f.Referenced {
victim := c.hand
c.hand = (c.hand + 1) % len(frames)
return victim
}
// Second chance: clear the bit and move on. The loop terminates
// because each pass clears at least one bit.
f.Referenced = false
c.hand = (c.hand + 1) % len(frames)
}
}
func (c *Clock) Touch(f *Frame, _ int) { f.Referenced = true }
// Optimal evicts the page used furthest in the future.
//
// Unimplementable in reality — it needs the future — but it is the lower
// bound every other policy is measured against.
type Optimal struct {
// Future is the remaining reference string.
Future []int
pos int
}
func (o *Optimal) Name() string { return "optimal (Bélády)" }
func (o *Optimal) Evict(frames []*Frame, _ int) int {
victim, furthest := 0, -1
for i, f := range frames {
next := o.nextUse(f.Page)
if next == -1 {
return i // never used again: evict immediately
}
if next > furthest {
furthest, victim = next, i
}
}
return victim
}
// nextUse returns how far ahead page is referenced, or -1 if never.
func (o *Optimal) nextUse(page int) int {
for i := o.pos; i < len(o.Future); i++ {
if o.Future[i] == page {
return i
}
}
return -1
}
func (o *Optimal) Touch(*Frame, int) {}
// Advance moves the policy's position through the reference string.
func (o *Optimal) Advance() { o.pos++ }
// TLB is a translation lookaside buffer: a small cache of recent
// translations, so a hit avoids walking the page table.
type TLB struct {
capacity int
entries map[int]*list.Element
order *list.List // front is most recently used
}
type tlbEntry struct {
page int
frame int
}
// NewTLB returns a TLB of the given capacity.
func NewTLB(capacity int) *TLB {
if capacity < 0 {
capacity = 0
}
return &TLB{
capacity: capacity,
entries: make(map[int]*list.Element, capacity),
order: list.New(),
}
}
// Lookup returns the frame for a page, if cached.
func (t *TLB) Lookup(page int) (int, bool) {
el, ok := t.entries[page]
if !ok {
return 0, false
}
t.order.MoveToFront(el)
return el.Value.(*tlbEntry).frame, true
}
// Add caches a translation, evicting the least recently used entry when
// full.
func (t *TLB) Add(page, frame int) {
if t.capacity == 0 {
return
}
if el, ok := t.entries[page]; ok {
el.Value.(*tlbEntry).frame = frame
t.order.MoveToFront(el)
return
}
if t.order.Len() >= t.capacity {
back := t.order.Back()
if back != nil {
delete(t.entries, back.Value.(*tlbEntry).page)
t.order.Remove(back)
}
}
t.entries[page] = t.order.PushFront(&tlbEntry{page: page, frame: frame})
}
// Invalidate removes a page's translation. This must happen whenever a
// frame is reclaimed, or the TLB would hand out a translation to memory
// that now belongs to a different page — a stale mapping is a correctness
// bug, not a performance one.
func (t *TLB) Invalidate(page int) {
if el, ok := t.entries[page]; ok {
t.order.Remove(el)
delete(t.entries, page)
}
}
// Flush empties the TLB, as a context switch would.
func (t *TLB) Flush() {
t.entries = make(map[int]*list.Element, t.capacity)
t.order.Init()
}
// Len returns the number of cached translations.
func (t *TLB) Len() int { return t.order.Len() }
// MMU is a memory management unit: it translates virtual addresses, handles
// page faults, and evicts under pressure.
type MMU struct {
frames []*Frame
// pageTable maps a virtual page to a frame index for resident pages.
pageTable map[int]int
tlb *TLB
policy Policy
clock int
Stats Stats
}
// NewMMU returns an MMU with numFrames of physical memory.
func NewMMU(numFrames, tlbSize int, policy Policy) *MMU {
if numFrames < 1 {
numFrames = 1
}
if policy == nil {
policy = LRU{}
}
return &MMU{
frames: make([]*Frame, 0, numFrames),
pageTable: map[int]int{},
tlb: NewTLB(tlbSize),
policy: policy,
}
}
// Capacity returns the number of physical frames.
func (m *MMU) Capacity() int { return cap(m.frames) }
// Resident reports whether a page is in memory.
func (m *MMU) Resident(page int) bool {
_, ok := m.pageTable[page]
return ok
}
// ResidentPages returns the pages currently in memory, sorted.
func (m *MMU) ResidentPages() []int {
out := make([]int, 0, len(m.pageTable))
for p := range m.pageTable {
out = append(out, p)
}
sort.Ints(out)
return out
}
// Access translates a virtual address, loading the page on a fault.
// It returns the physical address.
func (m *MMU) Access(addr Address, write bool) Address {
m.clock++
m.Stats.Accesses++
page := addr.PageNumber()
offset := addr.Offset()
// The TLB is consulted first; a hit avoids the page table walk.
if frameIdx, ok := m.tlb.Lookup(page); ok {
m.Stats.TLBHits++
f := m.frames[frameIdx]
m.policy.Touch(f, m.clock)
if write {
f.Dirty = true
}
return Address(f.Number*PageSize + offset)
}
m.Stats.TLBMisses++
frameIdx, resident := m.pageTable[page]
if !resident {
frameIdx = m.handleFault(page)
}
f := m.frames[frameIdx]
m.policy.Touch(f, m.clock)
if write {
f.Dirty = true
}
m.tlb.Add(page, frameIdx)
return Address(f.Number*PageSize + offset)
}
// handleFault brings a page into memory, evicting if necessary.
func (m *MMU) handleFault(page int) int {
m.Stats.PageFaults++
// Free frame available.
if len(m.frames) < cap(m.frames) {
f := &Frame{
Number: len(m.frames),
Page: page,
LoadTime: m.clock,
LastUsed: m.clock,
}
m.frames = append(m.frames, f)
idx := len(m.frames) - 1
m.pageTable[page] = idx
m.policy.Touch(f, m.clock)
return idx
}
// Memory is full: evict.
victim := m.policy.Evict(m.frames, m.clock)
f := m.frames[victim]
m.Stats.Evictions++
if f.Dirty {
// A dirty frame must be written back before reuse; a clean one can
// simply be dropped.
m.Stats.DiskWrites++
}
// The evicted page's translation must leave the TLB, or a later lookup
// would resolve to a frame that no longer holds it.
m.tlb.Invalidate(f.Page)
delete(m.pageTable, f.Page)
f.Page = page
f.Dirty = false
f.Referenced = false
f.LoadTime = m.clock
f.LastUsed = m.clock
m.pageTable[page] = victim
m.policy.Touch(f, m.clock)
return victim
}
// Run replays a reference string, returning the resulting statistics.
func (m *MMU) Run(pages []int, writes map[int]bool) Stats {
for _, p := range pages {
m.Access(Address(p*PageSize), writes[p])
if opt, ok := m.policy.(*Optimal); ok {
opt.Advance()
}
}
return m.Stats
}