Skip to content

latency numbers

  1. 1Measure memory, locks, network and disk on this machine

Run a step yourself: cd builds/latency-numbers/steps/<step> && go test -v ./...

Step 1

Measure memory, locks, network and disk on this machine

Time each rung of the ladder with a small benchmark, so the lesson uses numbers from real hardware, not from 2009.

Build step 1:Measure memory, locks, network and disk on this machine

// Package latency measures a few rungs of the latency ladder on the machine that runs it.
// Each function returns the median of several runs, because one run is easy to disturb.
package latency

import (
	"bufio"
	"io"
	"math/rand/v2"
	"net"
	"os"
	"path/filepath"
	"slices"
	"sync"
	"time"
)

const runs = 7

func median(f func() time.Duration) time.Duration {
	ds := make([]time.Duration, runs)
	for i := range ds {
		ds[i] = f()
	}
	slices.Sort(ds)
	return ds[runs/2]
}

// chase builds a random cycle over n slots and follows it, so every load depends on the
// previous one. The CPU cannot prefetch or overlap the loads, so the time per step is the
// real latency of one memory access at that working-set size.
func chase(bytes, steps int) time.Duration {
	n := bytes / 8
	next := make([]int32, n)
	perm := rand.Perm(n)
	for i := range n {
		next[perm[i]] = int32(perm[(i+1)%n])
	}
	var p int32
	for range min(n, steps) { // warm up
		p = next[p]
	}
	start := time.Now()
	for range steps {
		p = next[p]
	}
	d := time.Since(start)
	sink = int(p)
	return d / time.Duration(steps)
}

var sink int

// L1Hit is one dependent load from a 16 KB working set, which fits in the L1 data cache.
func L1Hit() time.Duration {
	return median(func() time.Duration { return chase(16<<10, 20_000_000) })
}

// RandomMemoryRead is one dependent load from a 512 MB working set, which is far larger than
// every cache, so almost every load goes to main memory. The cycle is built once.
func RandomMemoryRead() time.Duration {
	memOnce.Do(func() { memResult = median(func() time.Duration { return chase(512<<20, 4_000_000) }) })
	return memResult
}

var (
	memOnce   sync.Once
	memResult time.Duration
)

// SequentialMemoryReadMB is the time to read 1 MB that is already in memory, in order.
func SequentialMemoryReadMB() time.Duration {
	buf := make([]byte, 64<<20)
	for i := range buf {
		buf[i] = byte(i)
	}
	return median(func() time.Duration {
		var sum uint64
		start := time.Now()
		for i := 0; i < len(buf); i += 8 {
			sum += uint64(buf[i])
		}
		d := time.Since(start)
		sink = int(sum)
		return d / 64
	})
}

// MutexLockUnlock is one Lock and Unlock of a sync.Mutex that no other goroutine holds.
func MutexLockUnlock() time.Duration {
	var mu sync.Mutex
	return median(func() time.Duration {
		const n = 10_000_000
		start := time.Now()
		for range n {
			mu.Lock()
			mu.Unlock()
		}
		return time.Since(start) / n
	})
}

// LoopbackRoundTrip sends a 64-byte message to a local TCP echo server and waits for the
// answer. It measures the operating system's network path with no physical network.
func LoopbackRoundTrip() (time.Duration, error) {
	ln, err := net.Listen("tcp", "127.0.0.1:0")
	if err != nil {
		return 0, err
	}
	defer ln.Close()
	go func() {
		c, err := ln.Accept()
		if err != nil {
			return
		}
		defer c.Close()
		_, _ = io.Copy(c, c)
	}()
	c, err := net.Dial("tcp", ln.Addr().String())
	if err != nil {
		return 0, err
	}
	defer c.Close()
	c.(*net.TCPConn).SetNoDelay(true)
	msg := make([]byte, 64)
	got := make([]byte, 64)
	r := bufio.NewReader(c)
	trip := func() error {
		if _, err := c.Write(msg); err != nil {
			return err
		}
		_, err := io.ReadFull(r, got)
		return err
	}
	for range 1000 { // warm up the connection
		if err := trip(); err != nil {
			return 0, err
		}
	}
	var failed error
	d := median(func() time.Duration {
		const n = 5000
		start := time.Now()
		for range n {
			if err := trip(); err != nil {
				failed = err
			}
		}
		return time.Since(start) / n
	})
	return d, failed
}

// SyncedWrite4KB writes 4 KB to a file and calls fsync, so the data must reach the storage
// device before the call returns. This is the cost that a database pays to make a commit durable.
func SyncedWrite4KB(dir string) (time.Duration, error) {
	f, err := os.Create(filepath.Join(dir, "sync.bin"))
	if err != nil {
		return 0, err
	}
	defer f.Close()
	block := make([]byte, 4096)
	var failed error
	d := median(func() time.Duration {
		const n = 50
		start := time.Now()
		for range n {
			if _, err := f.Write(block); err != nil {
				failed = err
			}
			if err := f.Sync(); err != nil {
				failed = err
			}
		}
		return time.Since(start) / n
	})
	return d, failed
}

// SequentialFileReadMB reads a 64 MB file in order and returns the time for each MB. The file
// was just written, so the operating system serves it from its page cache in memory.
func SequentialFileReadMB(dir string) (time.Duration, error) {
	path := filepath.Join(dir, "seq.bin")
	if err := os.WriteFile(path, make([]byte, 64<<20), 0o600); err != nil {
		return 0, err
	}
	buf := make([]byte, 1<<20)
	var failed error
	d := median(func() time.Duration {
		f, err := os.Open(path)
		if err != nil {
			failed = err
			return 0
		}
		defer f.Close()
		start := time.Now()
		for {
			_, err := io.ReadFull(f, buf)
			if err == io.EOF || err == io.ErrUnexpectedEOF {
				break
			}
			if err != nil {
				failed = err
				break
			}
		}
		return time.Since(start) / 64
	})
	return d, failed
}
Test outputfrom the site build7 passed
=== RUN   TestCacheHitIsFasterThanRandomMemoryRead
    measure_test.go:15: cache hit (pointer chase, 16 KB): 1ns
    measure_test.go:16: random main-memory read (pointer chase, 512 MB): 137ns
--- PASS: TestCacheHitIsFasterThanRandomMemoryRead
=== RUN   TestSequentialMemoryReadPerMB
    measure_test.go:24: read 1 MB sequentially from memory: 35.995µs
--- PASS: TestSequentialMemoryReadPerMB
=== RUN   TestMutexLockUnlock
    measure_test.go:32: mutex lock + unlock (no contention): 10ns
--- PASS: TestMutexLockUnlock
=== RUN   TestLoopbackRoundTrip
    measure_test.go:43: TCP round trip on localhost (64-byte message): 15.247µs
--- PASS: TestLoopbackRoundTrip
=== RUN   TestDiskSyncedWrite
    measure_test.go:54: write 4 KB and fsync to SSD: 296.364µs
--- PASS: TestDiskSyncedWrite
=== RUN   TestSequentialDiskReadPerMB
    measure_test.go:65: read 1 MB sequentially from a file (OS page cache): 33.825µs
--- PASS: TestSequentialDiskReadPerMB
=== RUN   TestLadderOrder
    measure_test.go:81: ratio: memory read / cache hit = 137×
    measure_test.go:82: ratio: localhost round trip / memory read = 112×
--- PASS: TestLadderOrder
PASS
ok  	shardedmind/builds/latency-numbers/01-measure

Run it on your machine:cd builds/latency-numbers/steps/01-measure && go test -v ./...