AI EngineeringZero to ProductionHome·About·Contact
System Design · Chapter SD8

Operating systems

The operating system is the layer every service you designed in SD5–SD7 actually runs on. This lesson opens the box: processes vs threads, context switching, how the CPU scheduler decides who runs next, and the user/kernel boundary a system call crosses — with a runnable scheduler simulation you can measure.

⏱️ ~3 hours🧪 5 labs🎯 Beginner→Expert
🌱 Start here — from zeroThe OS, from scratch — no systems background needed. Every program you write runs on top of an operating system: it hands your code a slice of the CPU, its own private memory, and a safe way to touch disk and network. This chapter builds the mental model — process, thread, scheduler, system call — and then simulates the CPU scheduler in plain Python so you can measure why one policy beats another. The sims are models of the algorithms, not a real kernel.

Learning objectives

  • Explain what an OS provides: CPU time, memory isolation, and mediated I/O.
  • Distinguish a process from a thread and say what a context switch costs.
  • Simulate FCFS, SJF, round-robin and priority scheduling and compare their metrics.
  • Trace a system call across the user/kernel boundary and say why the boundary exists.
▶ Runnable companionEvery code block here is plain Python (standard library only) — no OS internals are touched. The scheduler is a discrete-event model: it computes the same waiting and turnaround times a real scheduler would produce for the same workload.

1 · What an operating system actually does

An operating system is the program that sits between your code and the hardware. Three jobs matter most: it shares the CPU among many programs (scheduling), it gives each program its own private memory (isolation, covered in SD9), and it mediates access to devices — disk, network, screen — so one buggy program cannot corrupt another or crash the machine. The OS is a resource manager and a protection boundary at the same time.

Your app processes OS kernel scheduler + drivers Hardware CPU / RAM / disk
Kernel vs the restThe kernel is the trusted core that runs with full hardware privilege — the scheduler, memory manager and device drivers live there. Everything else (your app, the shell, even most of the "OS") runs as ordinary user-space programs that must ask the kernel for anything privileged.

2 · Processes vs threads

A process is a running program with its own private address space — its own view of memory. A thread is a single flow of execution inside a process; a process can have many threads that share the same memory. That sharing is the whole trade-off: threads are cheap to create and communicate through shared memory, but that shared memory is exactly what causes the race conditions you will fight in SD9.

ProcessThread
Memoryprivate address spaceshared with siblings
Creation costhigh (new address space)low
CommunicationIPC (pipes, sockets, shared mem)shared variables
Crash blast radiusisolated — one dies alonea bad thread can corrupt the process
Scheduled by OS?yesyes (kernel threads)
Example code for learning — review, test, and adapt it before running against real or production systems. Commands can create, change, or delete resources. See the Terms & Disclaimer.
Step 1 · A process is a table of state (model)
process.py# MODEL: a process is really just a bundle of state the OS tracks in a
# "process control block" (PCB). We model that bundle as a dataclass.
from dataclasses import dataclass, field

@dataclass
class Process:
    pid: int
    arrival: int          # tick it becomes ready
    burst: int            # CPU ticks it needs
    priority: int = 0     # lower = more important (convention here)
    remaining: int = field(init=False)
    def __post_init__(self):
        self.remaining = self.burst

jobs = [Process(1, 0, 5), Process(2, 1, 3), Process(3, 2, 8)]
for j in jobs:
    print(f"pid={j.pid} arrival={j.arrival} burst={j.burst} remaining={j.remaining}")
pid=1 arrival=0 burst=5 remaining=5
pid=2 arrival=1 burst=3 remaining=3
pid=3 arrival=2 burst=8 remaining=8
What the OS remembers per processA real PCB holds far more — the saved CPU registers, the program counter, memory maps, open file handles, and scheduling state. Our model keeps only the fields the scheduler needs: when the job arrives, how much CPU it wants, and how much is left.

3 · Context switching — the illusion of many at once

A single CPU core runs exactly one thread at a time. The OS creates the illusion of many programs running at once by switching between them extremely fast. A context switch is that hand-off: the kernel saves the current thread's registers and program counter, then loads another thread's saved state. It is not free — the switch itself burns CPU cycles and cold caches, so a scheduler that switches too often wastes real work.

Save regs Pick next Load regs Resume
Step 2 · Counting switch overhead (model)
switch_cost.py# MODEL: each context switch costs a fixed number of "overhead" ticks.
# Fewer switches -> less overhead, but (see round-robin) worse responsiveness.
def total_time(work_ticks, switches, switch_cost=1):
    return work_ticks + switches * switch_cost

print("2 switches:", total_time(100, 2))    # 102
print("50 switches:", total_time(100, 50))  # 150
2 switches: 102
50 switches: 150
Why time-slicing is a compromiseA tiny time slice makes the machine feel responsive (every program gets the CPU often) but multiplies context-switch overhead. A huge slice minimises overhead but a short interactive job can wait behind a long batch job. Round-robin (below) is the knob that trades these off.

4 · CPU scheduling — a runnable simulator

The scheduler decides which ready thread runs next. Four classic policies: FCFS (first-come-first-served — a plain queue), SJF (shortest-job-first — run the smallest burst next), round-robin (each job gets a fixed time slice, then goes to the back of the queue), and priority (run the most important first). We measure each with two numbers: waiting time (ticks spent ready but not running) and turnaround time (finish − arrival).

PolicyRuleStrengthWeakness
FCFSrun in arrival ordersimple, no starvationone long job blocks all (convoy effect)
SJFrun shortest burst nextprovably minimal average waitneeds burst known; starves long jobs
Round-robinfixed slice, then rotatefair, responsiveoverhead if slice too small
Priorityhighest priority firsthonours importancelow-priority jobs can starve
Step 3 · Non-preemptive schedulers: FCFS & SJF (model)
scheduler_nonpre.py# MODEL of two non-preemptive schedulers. Each returns per-job (wait, turnaround).
# Non-preemptive = once a job starts it runs to completion.
from dataclasses import dataclass

@dataclass
class Job:
    pid: int; arrival: int; burst: int

def run_nonpreemptive(jobs, pick_next):
    """pick_next(ready, now) -> the Job to run next."""
    jobs = sorted(jobs, key=lambda j: j.arrival)
    now, done, metrics = 0, [], {}
    pending = list(jobs)
    while pending:
        ready = [j for j in pending if j.arrival <= now]
        if not ready:                      # CPU idle until next arrival
            now = min(j.arrival for j in pending); continue
        j = pick_next(ready, now)
        start = now
        now += j.burst                     # runs to completion
        turnaround = now - j.arrival
        wait = start - j.arrival
        metrics[j.pid] = (wait, turnaround)
        pending.remove(j)
    return metrics

def report(name, m):
    avg_w = sum(w for w, _ in m.values()) / len(m)
    avg_t = sum(t for _, t in m.values()) / len(m)
    print(f"{name:5s} avg_wait={avg_w:.2f} avg_turnaround={avg_t:.2f}")

jobs = [Job(1, 0, 7), Job(2, 2, 4), Job(3, 4, 1), Job(4, 5, 4)]
fcfs = run_nonpreemptive(jobs, lambda ready, now: min(ready, key=lambda j: j.arrival))
sjf  = run_nonpreemptive(jobs, lambda ready, now: min(ready, key=lambda j: j.burst))
report("FCFS", fcfs)
report("SJF", sjf)
FCFS  avg_wait=4.75 avg_turnaround=8.75
SJF   avg_wait=4.00 avg_turnaround=8.00
SJF wins on average wait — and that is a theoremFor a fixed set of jobs, shortest-job-first gives the minimum possible average waiting time of any non-preemptive schedule. The catch is real: you must know each burst in advance, and a stream of short jobs can starve a long one forever.
Step 4 · Preemptive round-robin (model)
round_robin.py# MODEL: round-robin gives each job a fixed time "quantum", then rotates.
# Preemptive = a running job can be interrupted before it finishes.
from collections import deque
from dataclasses import dataclass

@dataclass
class Job:
    pid: int; arrival: int; burst: int

def round_robin(jobs, quantum):
    jobs = sorted(jobs, key=lambda j: j.arrival)
    remaining = {j.pid: j.burst for j in jobs}
    finish = {}
    now, i, q = 0, 0, deque()
    def admit(t):                    # move arrivals up to time t into the queue
        nonlocal i
        while i < len(jobs) and jobs[i].arrival <= t:
            q.append(jobs[i].pid); i += 1
    admit(now)
    while q:
        pid = q.popleft()
        slice_ = min(quantum, remaining[pid])
        now += slice_
        remaining[pid] -= slice_
        admit(now)                   # new arrivals queue ahead of the re-added job
        if remaining[pid] > 0:
            q.append(pid)            # not done -> back of the line
        else:
            finish[pid] = now
        if not q:                    # CPU would idle: pull in a future arrival
            admit(now)
            if not q and i < len(jobs):
                now = jobs[i].arrival; admit(now)
    by_pid = {j.pid: j for j in jobs}
    metrics = {}
    for pid, f in finish.items():
        j = by_pid[pid]
        turnaround = f - j.arrival
        metrics[pid] = (turnaround - j.burst, turnaround)  # (wait, turnaround)
    return metrics

jobs = [Job(1, 0, 7), Job(2, 2, 4), Job(3, 4, 1), Job(4, 5, 4)]
for qn in (2, 4):
    m = round_robin(jobs, qn)
    avg_w = sum(w for w, _ in m.values()) / len(m)
    print(f"RR q={qn} avg_wait={avg_w:.2f}")
RR q=2 avg_wait=5.00
RR q=4 avg_wait=4.50
Reading the quantum knobA smaller quantum (q=2) rotates more often — more responsive, but more switching and, here, a higher average wait for this workload. As the quantum grows past the largest burst, round-robin degenerates into FCFS. There is no universally best quantum; it is a workload-dependent tuning decision.

5 · System calls & the user/kernel boundary

Your program cannot touch the disk or network directly — it would be a security and stability disaster. Instead it makes a system call: a controlled request that traps into the kernel, switches the CPU from user mode (restricted) to kernel mode (full privilege), runs the privileged operation, and returns. open, read, write, fork and socket are all system calls. The mode switch is the hardware-enforced line between "your code" and "trusted code".

User code Trap Kernel runs op Return
Step 5 · A syscall gate (model)
syscall_gate.py# MODEL: a syscall is a guarded entry point. In user mode you may NOT run a
# privileged op directly; you must request it and let the "kernel" validate + run it.
class Kernel:
    def __init__(self):
        self.mode = "user"
        self._files = {}
    def syscall(self, name, *args):
        self.mode = "kernel"            # trap: switch to kernel mode
        try:
            handler = getattr(self, f"_sys_{name}", None)
            if handler is None:
                return (-1, "ENOSYS")   # no such system call
            return handler(*args)
        finally:
            self.mode = "user"          # return: back to user mode
    def _sys_write(self, path, data):
        self._files.setdefault(path, "")
        self._files[path] += data
        return (0, len(data))
    def _sys_read(self, path):
        return (0, self._files.get(path, ""))

k = Kernel()
print(k.syscall("write", "/tmp/log", "hello"))   # (0, 5)
print(k.syscall("read", "/tmp/log"))             # (0, "hello")
print(k.syscall("launch_missiles"))              # (-1, "ENOSYS")
print("mode after return:", k.mode)              # back in user mode
(0, 5)
(0, 'hello')
(-1, 'ENOSYS')
mode after return: user
Why the boundary is worth its costEvery syscall pays for a mode switch, so batching I/O (one big write beats a thousand tiny ones) is a real optimisation. But the boundary is what lets the OS enforce permissions and keep a crashing program from taking down the machine — the same isolation principle behind the stateless app servers in SD5.

✓ Checkpoint — you can move on when you can…

  • Name the three core jobs of an OS and where the kernel/user line falls.
  • State two concrete differences between a process and a thread.
  • Run the scheduler sim and explain why SJF beat FCFS on average wait.
  • Trace a write() system call across the user/kernel boundary.

Knowledge check

check yourself
✓ Knowledge check

SJF gives the minimum possible average waiting time for a fixed set of jobs, yet real operating systems rarely use pure SJF. Give the two reasons the lesson names.

Show answer
First, SJF needs each job's CPU burst known in advance, which is generally not available. Second, a steady stream of short jobs can starve a long job — it may never get the CPU. Real schedulers approximate SJF (e.g. estimating bursts) and add aging to prevent starvation.
✓ Knowledge check

A system call switches the CPU from user mode to kernel mode and back. Why does this boundary exist at all, and what practical performance advice follows from the fact that crossing it is not free?

Show answer
The boundary exists for protection and isolation: user code cannot be trusted to touch hardware directly, so privileged operations run only in the kernel where permissions are enforced and a crash is contained. Because each crossing costs a mode switch, you should batch I/O — one large read/write instead of many tiny ones.

🪜 Practice ladder beginner → industry

Six graded exercises, easy to real-world. Try each before opening its solution.

Exercise 1 · Implement FCFS by handBeginner

Context: First-come-first-served is the plainest scheduler and the baseline every other policy is compared against.

Your task: Given a list of (pid, arrival, burst) jobs, compute each job's completion, turnaround and waiting time under FCFS and print the averages.

Requirements:

  • Sort jobs by arrival time
  • Track a running clock; if the CPU would idle, jump it to the next arrival
  • turnaround = completion − arrival; wait = turnaround − burst
  • Print per-job metrics and the two averages

💡 Hint: The clock only moves forward: now = max(now, arrival) + burst per job.

Show solution

FCFS is just "run them in arrival order," jumping the clock forward when the CPU would otherwise idle:

def fcfs(jobs):  # jobs: list of (pid, arrival, burst)
    jobs = sorted(jobs, key=lambda j: j[1])
    now, metrics = 0, {}
    for pid, arrival, burst in jobs:
        start = max(now, arrival)      # idle-jump if CPU is waiting
        completion = start + burst
        turnaround = completion - arrival
        wait = turnaround - burst
        metrics[pid] = (wait, turnaround)
        now = completion
    return metrics

m = fcfs([(1, 0, 7), (2, 2, 4), (3, 4, 1), (4, 5, 4)])
for pid, (w, t) in m.items():
    print(f"pid={pid} wait={w} turnaround={t}")
print("avg wait:", sum(w for w, _ in m.values()) / len(m))

The clock only ever moves forward, so start = max(now, arrival) is the whole scheduling decision. This is the baseline SJF and RR are measured against.

Exercise 2 · Add SJF and compareIntermediate

Context: Shortest-job-first is provably optimal for average wait; comparing it to FCFS on the same workload makes the convoy effect visible.

Your task: Extend your scheduler so it can pick the shortest ready burst instead of the earliest arrival, and print FCFS vs SJF average waiting time side by side.

Requirements:

  • Reuse one driver; pass in a pick_next(ready, now) selector
  • FCFS selector picks min arrival; SJF selector picks min burst
  • Only jobs that have arrived by now are eligible
  • Report average waiting time for both policies on identical jobs

💡 Hint: The only difference between the two policies is the key function you pass in.

Show solution

One driver, two selectors — the policy is the key function:

def run(jobs, pick_next):
    pending = sorted(jobs, key=lambda j: j[1])  # (pid, arrival, burst)
    now, metrics = 0, {}
    pending = list(pending)
    while pending:
        ready = [j for j in pending if j[1] <= now]
        if not ready:
            now = min(j[1] for j in pending); continue
        pid, arrival, burst = pick_next(ready, now)
        wait = now - arrival
        now += burst
        metrics[pid] = (wait, now - arrival)
        pending.remove((pid, arrival, burst))
    return metrics

def avg_wait(m): return sum(w for w, _ in m.values()) / len(m)

jobs = [(1, 0, 7), (2, 2, 4), (3, 4, 1), (4, 5, 4)]
fcfs = run(jobs, lambda r, now: min(r, key=lambda j: j[1]))
sjf  = run(jobs, lambda r, now: min(r, key=lambda j: j[2]))
print(f"FCFS avg wait {avg_wait(fcfs):.2f}")
print(f"SJF  avg wait {avg_wait(sjf):.2f}")

SJF beats FCFS here (4.00 vs 4.75) because it refuses to let the 7-tick job block the three shorter ones — that blocking is the convoy effect.

Exercise 3 · Preemptive round-robin with a quantumAdvanced

Context: Round-robin is what makes an interactive machine feel responsive; the quantum is the knob that trades responsiveness against switch overhead.

Your task: Implement round-robin with a configurable quantum and show how average waiting time changes as the quantum grows from small toward the largest burst.

Requirements:

  • Use a ready queue (deque); a running job that isn't done goes to the back
  • Admit newly-arrived jobs before re-queueing the preempted one
  • Sweep the quantum over several values and print average wait for each
  • Note the limit: quantum ≥ max burst degenerates to FCFS

💡 Hint: Track remaining[pid]; subtract min(quantum, remaining) each turn.

Show solution

Round-robin with a swept quantum:

from collections import deque

def round_robin(jobs, quantum):        # (pid, arrival, burst)
    jobs = sorted(jobs, key=lambda j: j[1])
    remaining = {p: b for p, a, b in jobs}
    arrival = {p: a for p, a, b in jobs}
    burst = {p: b for p, a, b in jobs}
    now, i, q, finish = 0, 0, deque(), {}
    def admit(t):
        nonlocal i
        while i < len(jobs) and jobs[i][1] <= t:
            q.append(jobs[i][0]); i += 1
    admit(now)
    while q:
        pid = q.popleft()
        s = min(quantum, remaining[pid]); now += s; remaining[pid] -= s
        admit(now)
        if remaining[pid] > 0: q.append(pid)
        else: finish[pid] = now
        if not q and i < len(jobs):
            now = jobs[i][1]; admit(now)
    return {p: ((finish[p] - arrival[p]) - burst[p], finish[p] - arrival[p]) for p in finish}

jobs = [(1, 0, 7), (2, 2, 4), (3, 4, 1), (4, 5, 4)]
for q in (1, 2, 4, 8):
    m = round_robin(jobs, q)
    print(f"q={q}: avg wait {sum(w for w,_ in m.values())/len(m):.2f}")

As the quantum climbs past the largest burst (8) the schedule stops preempting at all and becomes identical to FCFS — the quantum has no single best value, it trades responsiveness against switch overhead.

Exercise 4 · Priority scheduling with agingExpert

Context: Pure priority scheduling starves low-priority jobs; production schedulers fix this with aging — slowly boosting a waiting job's priority.

Your task: Implement non-preemptive priority scheduling, then add aging so a job that has waited a long time cannot be starved indefinitely, and demonstrate the difference.

Requirements:

  • Lower number = higher priority; pick the best ready priority each step
  • Without aging, show a low-priority job's wait growing unbounded under load
  • With aging, raise a job's effective priority by 1 for every k ticks it waits
  • Show the aged job eventually runs where the un-aged one starved

💡 Hint: Effective priority = base − (ticks_waited // k); recompute it when you pick.

Show solution

Priority with optional aging:

def priority_sched(jobs, k=None):      # (pid, arrival, burst, base_priority)
    pending = sorted(jobs, key=lambda j: j[1])
    pending = list(pending)
    now, metrics, entered = 0, {}, {}
    while pending:
        ready = [j for j in pending if j[1] <= now]
        if not ready:
            now = min(j[1] for j in pending); continue
        for j in ready: entered.setdefault(j[0], j[1])
        def eff(j):
            base = j[3]
            if k:                       # aging: waiting lowers the number
                base -= (now - entered[j[0]]) // k
            return base
        j = min(ready, key=eff)         # lowest effective priority number wins
        pid, arrival, burst, _ = j
        metrics[pid] = (now - arrival, now - arrival + burst)
        now += burst
        pending.remove(j)
    return metrics

# job 1 is low priority (base 4); a steady stream of priority-1 jobs keeps arriving,
# so without aging job 1 is bumped again and again.
jobs = [(1, 0, 2, 4), (2, 0, 2, 1), (3, 2, 2, 1),
        (4, 4, 2, 1), (5, 6, 2, 1), (6, 8, 2, 1)]
print("no aging, job 1:", priority_sched(jobs)[1])       # (wait, turnaround)
print("aging k=1, job 1:", priority_sched(jobs, k=1)[1])

Prints no aging, job 1: (10, 12) then aging k=1, job 1: (4, 6). Without aging the low-priority job waits behind every priority-1 arrival — a wait of 10. Aging subtracts waited // k from its number, so its effective priority climbs until it beats the newcomers and it runs far sooner — that is exactly how real schedulers prevent starvation.

Exercise 5 · A metrics harness across all four policiesProfessional

Context: Choosing a scheduler is an empirical decision: you run the candidate policies on a representative workload and compare aggregate metrics.

Your task: Build a harness that runs FCFS, SJF, round-robin and priority on the same generated workload and prints a comparison table of average wait, average turnaround and (for RR) context-switch count.

Requirements:

  • Generate a reproducible workload (seed the RNG)
  • Run all four policies on the identical job list
  • Report avg wait, avg turnaround, and switches where meaningful
  • Draw a one-line conclusion about which policy suits interactive vs batch load

💡 Hint: Keep each scheduler pure (jobs in → metrics out) so the harness just loops over them.

Show solution

A harness that runs every policy on one seeded workload and tabulates the result:

import random
from collections import deque

def gen(n, seed=7):
    r = random.Random(seed)
    return [(i + 1, r.randint(0, 10), r.randint(1, 9)) for i in range(n)]

def run_np(jobs, pick):
    pend, now, m = list(sorted(jobs, key=lambda j: j[1])), 0, {}
    while pend:
        ready = [j for j in pend if j[1] <= now]
        if not ready: now = min(j[1] for j in pend); continue
        pid, a, b = pick(ready); m[pid] = (now - a, now - a + b); now += b; pend.remove((pid, a, b))
    return m

def run_rr(jobs, q):
    jobs = sorted(jobs, key=lambda j: j[1]); rem = {p: b for p, a, b in jobs}
    arr = {p: a for p, a, b in jobs}; bur = {p: b for p, a, b in jobs}
    now, i, dq, fin, sw = 0, 0, deque(), {}, 0
    def admit(t):
        nonlocal i
        while i < len(jobs) and jobs[i][1] <= t: dq.append(jobs[i][0]); i += 1
    admit(0)
    while dq:
        pid = dq.popleft(); sw += 1
        s = min(q, rem[pid]); now += s; rem[pid] -= s; admit(now)
        if rem[pid] > 0: dq.append(pid)
        else: fin[pid] = now
        if not dq and i < len(jobs): now = jobs[i][1]; admit(now)
    m = {p: ((fin[p] - arr[p]) - bur[p], fin[p] - arr[p]) for p in fin}
    return m, sw

def avg(m, idx): return sum(v[idx] for v in m.values()) / len(m)

W = gen(8)
fcfs = run_np(W, lambda r: min(r, key=lambda j: j[1]))
sjf  = run_np(W, lambda r: min(r, key=lambda j: j[2]))
rr, sw = run_rr(W, 3)
print(f"{'policy':6s}{'avg_wait':>10s}{'avg_turn':>10s}{'switches':>10s}")
print(f"{'FCFS':6s}{avg(fcfs,0):10.2f}{avg(fcfs,1):10.2f}{'-':>10s}")
print(f"{'SJF':6s}{avg(sjf,0):10.2f}{avg(sjf,1):10.2f}{'-':>10s}")
print(f"{'RR(3)':6s}{avg(rr,0):10.2f}{avg(rr,1):10.2f}{sw:10d}")

The numbers depend on the seed, but the shape is stable: SJF minimises average wait (batch-friendly), round-robin trades a little average wait for responsiveness and incurs the switch count (interactive-friendly). That empirical comparison is how schedulers are actually chosen.

Exercise 6 · Model a syscall-batching winIndustry scenario

Context: A service doing one syscall per record spends most of its time crossing the user/kernel boundary; batching is a classic real-world latency fix.

Your task: Model the cost of writing N records as (syscall overhead + per-byte cost) and show that batching M records per write beats one-record-per-write, then find the batch size that minimises modelled total time.

Requirements:

  • Model total = num_syscalls × switch_cost + total_bytes × byte_cost
  • Compare unbatched (N syscalls) vs batched (N/M syscalls)
  • Sweep batch size M and report the modelled total for each
  • Label the result as a MODEL — no real I/O is performed
  • State the intuition: batching amortises the fixed mode-switch cost

💡 Hint: The win comes entirely from cutting the number of boundary crossings; the byte cost is unchanged.

Show solution

A cost model (no real I/O) of writing 100k records at various batch sizes:

def model_time(n_records, batch, bytes_per_record=200,
               switch_cost_ns=1500, byte_cost_ns=2):
    """MODEL: total nanoseconds to write n_records, batch records per syscall."""
    import math
    syscalls = math.ceil(n_records / batch)
    total_bytes = n_records * bytes_per_record
    return syscalls * switch_cost_ns + total_bytes * byte_cost_ns

N = 100_000
for m in (1, 10, 100, 1000, 10_000):
    t = model_time(N, m)
    print(f"batch={m:>6}: modelled {t/1e6:.2f} ms")

The per-byte cost is identical across runs; the only thing batching changes is the number of boundary crossings, so total time falls sharply as the batch grows and then flattens once the fixed syscall cost is fully amortised. This is why buffered writers and bulk APIs exist. These are modelled constants, not measured numbers on your machine.

© 2026 studybydoing.in · AI Engineering: Zero to Production · All rights reserved. · About · Privacy Policy · Terms · Contact
Educational content, provided as-is and without warranty. Code samples are examples — review, test, and adapt them before using in production. See the Terms of Use & Disclaimer. Use at your own risk.
© studybydoing.in