Learn Computer Architecture
Start from what you wrote and follow it down: instructions, the front end, execution units, registers, caches, memory. Every lesson names what its claims are specific to, because almost nothing here is true of every machine.
How arithmetic emerges from logic: binary and two's complement, why floating point approximates, gates, adders, and the step from combinational logic to stored state.
One line of source becomes a handful of instructions, and the addition itself is the cheapest thing in it. The expensive question — the one this entire domain exists to answer — is where `a` and `b` were when the CPU went looking for them.
Binary is not a numbering curiosity you convert for exam questions. It is the substrate: instructions, addresses, permission flags, protocol headers and floating-point values are all bit patterns, and hexadecimal exists because humans cannot read them otherwise.
The byte is nearly universal. The word is not: it means whatever a given architecture, compiler or document says it means, and the confusion this causes is responsible for a surprising share of portability bugs.
Negative numbers are not stored with a minus sign. They are stored so that ordinary binary addition produces the right answer without the hardware ever knowing a value was negative — which is why subtraction needs no separate circuit.
Add one to the largest 8-bit signed value and the bits roll around to the most negative one. The hardware behaviour is simple and identical everywhere; what your language claims about it ranges from "wraps" to "this can never happen, and I will optimise on that basis".
A float is scientific notation in binary: a sign, an exponent that slides the point, and a fraction. That design buys an enormous range from a fixed number of bits, and it pays for it with precision that varies depending on how large the value is.
The famous result is not a bug and not a rounding display quirk. 0.1 has no exact binary representation for the same reason 1/3 has no exact decimal one, and every consequence — failed equality tests, drifting sums, order-dependent results — follows from that single fact.
AND, OR, NOT and XOR are not programming conveniences layered over arithmetic. They are the primitive operations from which arithmetic itself is constructed, and they remain the cheapest instructions a CPU offers.
A gate is a few transistors that compute one Boolean function of its inputs. Everything above — arithmetic, memory, control, the entire machine — is gates composed with other gates, and the two properties that matter are that composition is universal and that propagation takes time.
XOR gives you the sum bit, AND gives you the carry, and that is a half adder. Chain them and you can add any width — but the carry has to travel through every stage in turn, and that dependency is the reason adder design is a real engineering problem.
Combinational logic has no memory — the output is a function of the inputs right now. Add feedback and you can store a bit; add a clock and you can control when stored values change. That step is what turns a calculator into a machine that executes programs.
The parts that execute an instruction: registers as the fastest storage you have, the ALU, the datapath they sit on, the control unit that steers it, and why clock speed is not performance.
A CPU is not one thing that runs instructions. It is a front end that fetches and decodes them, a set of execution units that do the work, a register file they read and write, and caches feeding all of it — with most of the silicon spent on keeping those units busy rather than on the arithmetic itself.
Registers are the only storage the ALU can read directly, they are the fastest thing in the machine by a wide margin, and an ISA typically exposes a couple of dozen of them. Everything a compiler does with local variables is an attempt to keep the right values in that tiny space.
The arithmetic logic unit performs the operations that source-level arithmetic compiles into, sets the flags that comparisons and branches depend on, and — crucially — does not treat all arithmetic as equal. Add and XOR are nearly free; divide is not.
Something has to read a decoded instruction and tell the rest of the core what to do with it — which register file ports to open, which ALU operation to select, whether to write memory. That something is the control unit, and it is the least visible and most quietly consequential block in the machine.
Registers feed the ALU, the ALU feeds registers back, and a separate route runs from the register file through address calculation to the data cache and back. That loop, plus the memory path hanging off it, is the datapath — and its shape explains why instruction sets look the way they do.
One register holds the address of the next instruction. Incrementing it is trivial; redirecting it is the single most disruptive thing that can happen to a modern CPU, because everything the front end fetched behind the redirect turns out to have been the wrong guess.
A clock cycle is the machine's unit of time, and clock rate is one of three factors in how long a program takes — the other two being how many instructions it runs and how many cycles each takes. Comparing CPUs by gigahertz alone ignores two thirds of the equation and all of the memory system.
The contract between software and hardware — and the distinction that most performance arguments get wrong: the same ISA is implemented by wildly different microarchitectures.
An instruction set architecture is the specification a compiler targets and a processor promises to honour: which instructions exist, which registers they name, how memory behaves, and how addresses are formed. Everything about how that promise is kept is deliberately outside it.
The instruction set is a specification; the microarchitecture is one machine that implements it. Confusing the two produces most of the bad arguments about processor performance, because it attributes to a published contract things that are properties of a particular chip.
Three instruction sets dominate current computing, and their differences are largely differences of origin and constraint rather than of achievable performance. Knowing what each optimised for explains their shape better than any claim about which is better.
The distinction described a genuine design disagreement about where complexity should live, and it mattered. Then implementations converged: complex instruction sets began decoding into simple internal operations, and reduced ones grew complex instructions. The labels survived the situation that gave them meaning.
Assembly is the readable form of what the machine will actually execute. Most engineers will never write it, and being able to read it is the single most direct way to check what the compiler did rather than guessing.
Instructions do not just name registers — they name ways of computing an address from registers, constants and a scale factor. The reason `arr[i]` costs one instruction rather than three is that the hardware performs exactly the arithmetic that array indexing requires.
Fetch, decode, execute, write back — then the pipeline that overlaps them, the hazards that break the overlap, and the forwarding and stalls that patch it up.
Every introduction to CPUs teaches a tidy four-step cycle. It is genuinely useful and it has not described a shipping processor since the early 1990s. Both halves of that sentence matter: learn the model, then learn precisely which parts of it modern hardware abandoned.
Before a CPU can do anything with an instruction it has to load it from memory, through a cache, at an address it may have had to guess. The front end is a supply chain, and a starved front end leaves the most sophisticated execution engine in the world with nothing to do.
A fetched block is just bytes. Decode is where the CPU works out where one instruction ends and the next begins, what operation is requested, and which registers it touches — and how hard that is depends enormously on the instruction encoding the ISA chose.
The execute phase is where the work happens, in a set of specialised functional units. Two things surprise people: different operations take very different numbers of cycles, and an operation's latency and its throughput are separate numbers that can differ by an order of magnitude.
Almost every ISA makes you bring data into a register before you can compute with it, and write it back explicitly. That looks like bureaucracy until you notice that a load is the one instruction whose cost varies by two orders of magnitude depending on where the data happens to be.
A pipelined CPU does not execute any single instruction more quickly than an unpipelined one. It overlaps them, so instructions complete more often. Understanding that pipelining buys throughput and not latency explains most of what modern CPUs do and why they do it.
Pipelining assumes the next instruction can always start. Three situations break that assumption — a needed value is not ready, the next address is not known, or two instructions want the same hardware — and every hardware performance problem is a variation on one of them.
When one instruction needs another's result, the hardware has two options: route the value directly to where it is needed, or wait. Forwarding covers most cases at no cost. The case it cannot cover — a load feeding the very next instruction — is the shape of every serious memory performance problem.
A CPU cannot afford to wait to find out where a branch goes, so it guesses. What that buys, what a misprediction costs, and why unpredictable data hurts more than extra work.
A pipelined CPU must fetch an instruction every cycle, but at a conditional branch it does not yet know which instruction comes next. Waiting for the answer is unaffordable on a deep pipeline, which is why every high-performance CPU guesses instead.
The CPU has to supply a fetch address before it knows the branch outcome, so it predicts one from history. Modern predictors are accurate enough that well-behaved code pays essentially nothing for its branches — which is exactly why the badly-behaved cases stand out so sharply.
When the predictor is wrong, everything fetched and executed down the wrong path is discarded and the pipeline refills from the correct address. The cost is not the discarded work — it is the emptiness afterwards, and it scales with how deep the pipeline is.
Modern CPUs execute instructions they may have to discard, on the bet that the guess was right. It works because the guess usually is, and because discarded results never become architecturally visible. What they do leave behind — microarchitectural traces — turned out to matter enormously.
Replacing an unpredictable branch with arithmetic converts a variable cost into a fixed one. That is a win when the branch mispredicts often and the work it guards is trivial — and a loss in every other case, which is most of them.
Modern CPUs are throughput machines: many instructions in flight, executed as their inputs become ready, retired in program order. Renaming, superscalar issue, ILP and what IPC actually tells you.
You wrote A, B, C. If B is waiting on a cache miss, the machine will run C first — and then hand you a result indistinguishable from having run them in order. This is the lesson where "the CPU executes my code line by line" stops being a useful model.
Program order is a line. What the machine actually obeys is a graph — and the longest path through that graph, not the number of nodes in it, is what sets the floor on how fast a loop can run.
A single thread, on a single core, with no threading library anywhere in sight, routinely has a dozen operations in flight at once. That is ILP — parallelism the hardware extracts from your sequential code without being asked, and the first thing to understand before reaching for threads.
A pipelined core finishes one instruction per cycle at best. A superscalar core has several execution units and finishes several — provided your instructions need different units and do not depend on each other. Port contention is why the theoretical peak is theoretical.
The ISA gives you a handful of register names. Reusing one creates a dependency that has nothing to do with your data — a naming collision, not a real ordering requirement. Renaming maps those names onto a much larger physical file and the false dependency disappears.
Execution finishes in whatever order the data allows. Something has to put the results back in order before anyone can see them — and that same something is what lets a page fault, an interrupt or a mispredicted branch unwind cleanly instead of corrupting your program.
The ratio that connects "how much work" to "how long it took". It is the most useful single number for diagnosing a CPU-bound loop — and one of the easiest to misuse, because a change that raises IPC can leave the program slower.
One instruction, many elements. What makes a loop vectorizable, what stops the compiler from doing it, and where data-level parallelism sits among the other kinds.
Instruction-level, data-level, thread-level and core-level parallelism are four different mechanisms with four different requirements, four different costs and four different failure modes. Most confused performance arguments come from conflating two of them.
A vector register holds several values and a vector instruction applies one operation to all of them at once. It is the cheapest parallelism on the machine — single-threaded, race-free, and frequently left unused because a single unprovable pointer relationship disabled it.
The transformation from one-element-per-iteration to many, and the four conditions that have to hold for it to be legal. Most loops that fail to vectorise fail on a single unprovable assumption rather than on anything fundamental.
Compilers vectorise loops automatically, sometimes. It is a best-effort optimisation with no guarantee, it fails silently, and it can stop working after an unrelated edit — so the only responsible position is to check rather than believe.
The deepest module, because this is where most real programs spend their time. Lines, locality, associativity, replacement, thrashing and prefetching — the machinery behind "why is this loop slow".
One big fast memory is not buildable at a price anyone would pay, so machines are built as a stack of progressively larger, slower, cheaper memories that pretend to be one. The gaps between the levels are enormous, and nothing in your source code tells you which level you just hit.
A cache is not a faster memory. It is a small tagged store holding copies of recently used lines, managed entirely by hardware, betting that your program will ask for the same or nearby data again. When the bet pays it is invisible; when it fails it is also invisible, which is the problem.
The cache has no concept of your variables. It moves fixed-size blocks — typically 64 bytes today — so reading one byte fetches the 63 around it. Almost every practical memory optimisation, and one notorious concurrency bug, follows directly from that one fact.
If you touch an address, you will probably touch its neighbours soon. Hardware bets on this at every level — line size, prefetchers, DRAM row buffers — so code that walks memory in order gets most of its data effectively for free, and code that scatters pays full price for every element.
If you touched something recently, you will probably touch it again. That assumption is what makes keeping copies worthwhile at all — and it is why the size of the data you revisit, rather than the size of the data you own, determines whether a program is fast.
A miss is not an error; it is a cost, and it is the normal way data arrives. What matters is where the miss is satisfied — one level out, three levels out, or in DRAM — because those outcomes differ by more than an order of magnitude and imply completely different fixes.
Compulsory, capacity and conflict misses look identical in a counter and have almost nothing in common as problems. Prefetching helps one, blocking helps another, and layout changes help the third — so classifying the miss is what turns a measurement into a plan.
The simplest way to build a cache: every memory address has exactly one line it is allowed to occupy. Lookup becomes trivial and the hardware stays cheap — but two hot addresses that happen to share an index evict each other forever, while the rest of the cache sits empty.
Give each address a set of N possible homes instead of one. Conflicts stop being catastrophic, lookup stays affordable, and you inherit a new problem — with N candidates, something has to decide which one to evict.
A cache does not search. It slices the address into three fields — offset, index, tag — and each field's width is forced by the geometry rather than chosen. Once you can do the split, most cache behaviour stops being mysterious.
With N ways in a set, a miss requires choosing a victim. Textbooks say least-recently-used. Real hardware implements approximations that are cheaper, sometimes adaptive, generally undocumented, and different between levels on the same die.
Two ways to make a cache useless: overflow it, or arrange for everything you touch to land in one set. Both produce the same signature — a performance cliff at a specific input size or stride, where the curve falls off rather than bending.
The working set is the data a program actually touches in a window of time. Whichever level of the hierarchy it fits in determines what the program costs — and because the levels are discrete, crossing a boundary produces a step change rather than a gradual decline.
A cache miss costs far more than an instruction, so the hardware tries not to take one: it watches your access stream, predicts the next addresses and fetches them early. Predictable patterns get their data before they ask. Pointer chasing does not — which is most of the answer to why an array beats a linked list at the same complexity.
Past the last-level cache: how DRAM is organized, why latency and bandwidth are different resources, and how to tell a bandwidth-bound workload from a latency-bound one.
When every cache misses, the request leaves the CPU entirely. It goes to a memory controller that queues it, reorders it against other pending requests, and drives a DRAM device that is nothing like the flat byte array your program believes in.
DRAM is not a flat array. It is a grid of rows and columns across banks, and reading it means activating a whole row into a buffer first. Whether your next access hits that open row or forces another activation is a several-fold cost difference nothing in your code mentions.
A workload can saturate memory bandwidth while barely being affected by latency, or be crippled by latency while using a fraction of available bandwidth. Conflating the two sends people to the wrong fix — and "the memory is slow" is almost never a complete diagnosis.
Streaming code that touches each byte once cannot be helped by caches, cannot be helped by more cores, and cannot be helped by faster arithmetic. It is limited by how fast bytes arrive, and the only real lever is moving fewer of them.
Some loops use almost no memory bandwidth and are still dominated by memory. Each access must complete before the next address is even known, so the hardware's ability to overlap misses is worth nothing, and the loop runs at one DRAM round trip per step.
Where the bytes actually sit: alignment and padding, endianness, address arithmetic, why an array beats a linked list at equal complexity, and array-of-structs versus struct-of-arrays.
Hardware prefers a four-byte value at an address divisible by four. Break that and the penalty ranges from literally nothing, through a silent extra memory access, to a fault that kills the process — and which one you get depends entirely on the architecture.
A struct with a char and an int is not five bytes. The compiler inserts padding to keep every field naturally aligned, and in a large array of those structs the padding is memory you pay to move but never read.
The value `0x12345678` is unambiguous. The four bytes it occupies in memory are not — their order depends on the machine. It matters exactly when bytes leave the machine, which is why it is a networking and file-format problem more than a CPU one.
Indexing an array is not a lookup. It is arithmetic: base plus index times element size, computed in a single addressing mode on most architectures. That is why arrays are the cheapest random-access structure hardware supports.
Traversing an array and traversing a linked list are both linear. On real hardware the array can be an order of magnitude faster, because complexity counts operations and hardware charges for data movement and dependencies.
Any traversal where the next address comes out of the current load runs at one memory round trip per step. It is the mechanism behind slow lists, slow trees, slow graphs and hash lookups that underperform their O(1) label.
The same particles can be one array of records or several parallel arrays of fields. Which is faster depends entirely on whether your loop reads most fields of a few records, or one field of many — and the difference is how much of each cache line you actually use.
Organise data around how it is processed rather than around how the domain is modelled. It is a real technique with real wins in hot loops — and a genuinely bad default for code where clarity matters more than cache lines.
The hardware half of a mechanism the OS owns: the MMU that translates, the page tables it walks, the TLB that caches translations, and the protection bits that make isolation possible.
A pointer is not a memory location. It is an index the CPU must translate before anything can be read, and that translation happens on every load and every store, in hardware, before the access can even begin.
The memory management unit is not a lookup table off to the side. It is a gate every load and store passes through, and it answers two questions at once: where does this address really point, and is this process allowed to touch it that way?
A translation the TLB does not have must be looked up in tables that live in memory. The lookup is multi-level, each level depends on the one before it, and any of them can miss in cache — which is why a TLB miss is expensive out of all proportion to the work it represents.
The translation lookaside buffer holds recently used virtual-to-physical mappings so the common case skips the walk entirely. It is small, it is split by purpose, and its capacity is measured in pages — which makes its working set a completely different quantity from your data cache's.
A profile shows memory stalls. The data fits in cache. Cache miss rates look fine. The stalls are real and the usual suspects are all innocent — because the CPU is not waiting for data, it is waiting to find out where the data is.
If the TLB can only hold so many entries, make each entry cover more memory. That is the whole idea, and it can transform a translation-bound workload — but it costs memory, complicates allocation, and the transparent variety can make things worse.
Process isolation is not a promise the operating system makes and enforces in software. It is a consequence of permission bits the MMU checks on every access, and of the simple fact that one process has no way to name another's physical memory at all.
User mode and kernel mode are not a convention the kernel politely observes. They are a hardware state, and the CPU refuses certain instructions and certain memory outright depending on which one it is in.
A function call is a jump and a stack push. A system call changes the privilege level, redirects control to an address you do not choose, and disturbs enough microarchitectural state that the cost outlives the call itself.
What changes when there is more than one core: hardware threads versus OS threads, the coherence protocol keeping caches consistent, false sharing, NUMA and why moving a thread costs cache.
Eight cores is not one core that goes eight times faster. It is eight execution engines, each with private caches, sharing one last-level cache and one memory system through an interconnect — and that shared half is where multicore performance is usually won or lost.
Three things routinely called "a thread", stacked on top of each other. A core is silicon that executes. A hardware thread is an execution context inside it. A software thread is an OS bookkeeping structure that gets mapped onto one. Conflating them is how "we have 16 threads" becomes a wrong capacity estimate.
Simultaneous multithreading gives one physical core a second register set so it can switch instruction streams instantly and fill cycles the first stream would waste. It does not add execution units, and it does not add a core. On the right workload it is a solid gain; on the wrong one it is negative.
A §224 distinction the whole concurrency stack rests on. A hardware thread is a fixed execution context built into silicon. An OS thread is an allocated software object. The OS multiplexes many of the second onto few of the first, and every scheduling cost you can measure lives in that mapping.
Two cores cache the same variable. One writes. Nothing in your code tells the other core to look again — yet it must not read stale data. Coherence is the hardware protocol that guarantees it, running underneath every shared-memory program, and it is emphatically not free.
Coherence needs each cached line tagged with what the core is allowed to do with it. MESI — Modified, Exclusive, Shared, Invalid — is the canonical four-state answer and the one worth learning. It is a family, not a standard: real chips extend it, and which variant yours uses is usually undocumented.
Two threads update two different variables. They never touch each other's data and the code is obviously correct. Throughput is worse than single-threaded, because the two variables happen to sit in one cache line and the hardware shares by line, not by variable.
On a multi-socket machine, memory is attached to sockets. A core reaching its own socket's memory is on a short path; reaching the other socket's memory crosses an inter-socket link. Same instruction, same address space, materially different cost — and the allocator decides which you get.
Affinity constrains which cores a thread may run on. It buys cache locality, NUMA locality and predictable latency, and it costs the scheduler's ability to balance load. It is a genuine tool for latency-critical work and a genuine way to make a machine slower if applied by reflex.
The expensive part of a context switch is not saving registers. It is that the thread resumes on a core whose caches and TLB hold someone else's data, so it must take a burst of cold misses to rebuild a working set that existed perfectly well a moment ago somewhere else.
Why the order you wrote is not the order the machine performs, what a barrier actually constrains, and the hardware primitives — compare-and-swap and friends — every lock is built from.
Everyone reasons about shared memory as if there were one global order of operations that every core agrees on. That model is intuitive, teachable, and not what any mainstream CPU implements — which is exactly why it is worth stating precisely before taking it away.
Two independent agents reorder your memory operations before any other core sees them: the compiler, which rewrites the code, and the CPU, which executes and retires it out of order. Neither is malfunctioning, and on one core neither is detectable.
A store instruction finishes long before the value reaches coherent cache. In between it sits in a per-core queue that only its own core can see — which is the concrete mechanism behind the one reordering even x86-64 permits.
A barrier is one of the most misdescribed instructions in computing. It does not flush caches, it does not push data anywhere, and it does not lock anything. It constrains the order in which one core's memory operations may become visible relative to each other.
Your CPU has a memory model. Your language has a different one. The compiler stands between them, and code that "works on x86 and breaks on ARM" has almost always been written against the hardware model of the machine it was tested on.
An atomic read-modify-write is a single instruction that no other core can observe half-finished. That is a narrow and precise guarantee, and it is routinely mistaken for a much broader one about program correctness.
Change this value, but only if it is still what I last saw. That conditional write is what makes lock-free algorithms possible, and the trap in it — that "still the same value" is not the same as "nothing happened" — is called ABA.
A mutex is not an operating-system object you call into. In the uncontended case it is one atomic instruction and no system call at all — which is why an uncontended lock is nearly free and a contended one costs thousands of times more.
How the world outside the CPU gets in: interrupts against polling, DMA moving bytes without the CPU copying them, the path to storage, and why a CPU cache is not the OS page cache.
A network card cannot call a function. It raises a line, and the CPU abandons what it was doing at the next instruction boundary. The handler's instruction count is the smallest part of what that costs.
Being told costs a fixed amount per event; asking costs a fixed amount per unit of time. Which is cheaper is arithmetic, and above a crossover rate the "wasteful" busy loop wins decisively.
A disk read does not consume a core, because the CPU never touches the bytes. It writes a descriptor, the device masters the bus and writes straight into RAM, and the CPU finds out afterwards.
Storage, network and accelerators do not each get a private path to memory. They share an interconnect with finite bandwidth, and a saturated link explains slowdowns that look like they belong to whichever device you happened to be watching.
Device registers live in the address space, so talking to hardware looks exactly like writing to memory. It is not memory: the write has a side effect, the read may change state, and every optimisation the machine normally applies has to be turned off.
The link between a CPU and an accelerator is not free capacity. It has a width, a generation and a ceiling — and for many workloads the transfer over it, not the computation at either end, is what sets the runtime.
An SSD is a parallel device pretending to be a disk. Give it one request at a time and you measure its latency; give it many and you measure its throughput — and those two numbers are not related the way rotational intuition expects.
Both are called cache, both make things faster, both live in the machine. One is hardware holding lines of physical memory and you cannot address it; the other is ordinary RAM the kernel fills with file data and you can control it precisely.
Throughput hardware and its price: why GPUs win on wide regular work, why transfers and divergence undo it, and what specialized accelerators trade away to be fast at one thing.
A CPU spends its transistor budget making one instruction stream go fast — speculation, out-of-order issue, large caches. A GPU spends a comparable budget on many simple lanes running the same operation over different data. Neither is faster; the shape of your work decides which bet pays.
A host CPU, a device with many compute units, an unusually large register file, a small block of programmer-managed on-chip memory per unit, and a large pool of high-bandwidth global memory. The register file being large — and being the thing that limits how many lanes stay resident — is the part that surprises people.
Lanes execute in lockstep groups: one instruction, many lanes, different data. Two consequences follow and both are unlike anything on a CPU — a data-dependent branch makes the group execute both sides in sequence, and the memory addresses the lanes request must line up or the traffic multiplies.
Host memory and device memory are separate pools connected by a bus that is slow relative to both. A kernel ten times faster than the CPU loses if you pay two crossings to use it — so the real question is not how fast the kernel is, but at what input size the whole path overtakes staying put.
Dense linear algebra, graphics, model training and inference, image processing, scientific simulation. The list looks unrelated until you notice that every entry is wide, regular and reuses each loaded byte many times — and that the workloads which disappoint share the opposite properties.
GPUs, AI accelerators, NPUs and FPGAs are not four unrelated products. They are points on one axis running from fully general to fully fixed, and each step along it trades away the ability to run arbitrary work in exchange for doing one kind of work with less silicon and less power.
Specialized hardware is more efficient because it does less. That is a genuine gain and a genuine risk: the efficiency comes from decisions frozen at design time, and workloads have a habit of changing shape faster than silicon can be replaced.
The path from prompt to token runs through matrix operations on an accelerator, and the surprise is which resource binds. Generating tokens one at a time reads the entire model from memory per token, so inference is usually bandwidth-bound rather than compute-bound — which is why model size and memory bandwidth dominate the conversation.
Parameters times bytes per parameter gives a floor, not an answer. Activations, the KV cache that grows with context and concurrency, and runtime overhead all sit on top — which is why a model that "fits in memory" by the simple calculation frequently does not.
Reading the machine: performance counters, CPI, telling compute-bound from memory-bound, and every way a microbenchmark will lie to you about frequency, caches and dead code.
Every modern CPU carries a small unit whose only job is to tally what the rest of the chip did: cycles, instructions retired, misses at each cache level, mispredicted branches, stalled cycles. It is the only direct evidence you will ever get about the hardware — and it is sampled, approximate, and named differently on every chip.
Cycles per instruction, and its reciprocal instructions per cycle, describe how smoothly work is flowing through the machine. Neither is a measure of performance. A change that halves CPI while tripling instruction count made the program slower, and a vectorized loop that runs twice as fast often shows a worse CPI than the scalar loop it replaced.
A core showing 100% utilisation may be executing a dense stream of arithmetic, or it may be stalled almost the entire time waiting for data that has not arrived. The operating system reports both as "busy". They are different problems with disjoint fixes, and only the counters can tell them apart.
A cache miss costs a great deal if the core has nothing else to do, and almost nothing if it does. Modern cores keep several misses outstanding at once, so ten independent misses can cost barely more than one — while ten dependent misses cost ten times as much. This is why miss counts alone never predict runtime.
Instructions are fetched from memory through their own cache, and that cache is small. A hot loop that fits runs at full speed; a sprawling call graph with aggressive inlining can spend a large fraction of its cycles waiting for instructions to arrive — a stall that data-focused profiling is structurally unable to see.
A microbenchmark measures what it measures, which is frequently not what you meant. The compiler deletes work whose result is unused, the caches and predictors are warm in ways production never is, the clock speed moves underneath you, and the timer itself costs more than the operation. Each of these has produced published results that were simply wrong.
The number printed on the box is a nominal figure, not an operating one. Real clock frequency moves continuously with load, thermal headroom, power budget, how many cores are active, and even which instructions are executing — wide vector code frequently runs at a lower clock than scalar code on the same chip.
Silicon has a temperature limit, and the only lever the chip has to stay under it is to slow down. A workload that starts on a cool chip runs at one speed and settles at a lower one, which is why burst performance and sustained performance are different numbers and why short benchmarks systematically flatter the machine.
Energy, not time, is the constraint that actually binds on phones, on laptops and across datacentres. The configuration that finishes soonest is frequently not the one that uses least energy, and the relationship between the two is non-linear enough that "run slower to save power" is often exactly wrong.
Nearly every technique modern CPUs use — pipelining, superscalar issue, out-of-order execution, speculation — increases the number of operations completed per unit time without reducing, and sometimes while increasing, the time any single operation takes. This is why decades of architectural progress leave a dependent chain almost exactly as slow as it was.
Cache-aware algorithms and tiling, what the compiler did before the CPU saw your code, side channels as a consequence of speculation, and the hardware under virtual machines and containers.
Two algorithms with identical asymptotic complexity can differ by an order of magnitude in wall clock, because complexity analysis counts operations and hardware charges for data movement. Blocking, compact layouts and node sizes matched to the transfer granularity are all the same idea: arrange the work so that data pays its travel cost once.
The tiled matrix multiply performs exactly the same multiply-accumulate operations as the naive triple loop, in a different order. It wins because a block of each matrix is brought into cache once and used many times, instead of a row or column being re-fetched on every pass.
Between the line you wrote and the work the machine performs sit two independent reordering layers: a compiler that transforms code under the language's rules, and a processor that executes the result out of order under the architecture's rules. Each preserves its own notion of observable behaviour, and neither preserves the order you wrote.
Two adjacent lines of source imply neither two instructions nor two steps in time. Source code specifies *what result is required*, and it is an excellent tool for reasoning about correctness — but it deliberately says nothing about instruction count, ordering or cost, which is exactly why measurement exists.
Every mechanism that makes a CPU fast by remembering something — caches, branch predictors, translation buffers — creates state that outlives the operation and can be observed indirectly through timing. Information leaks not through what a program outputs, but through how long other things take afterwards.
In 2018 a class of vulnerabilities showed that speculative execution — a two-decade-old performance technique — could be steered into performing accesses that architecturally never happened, while leaving microarchitectural traces that a timing side channel could read. The durable lesson is not the specific bug but its shape: a performance optimisation created a security boundary violation, and the mitigations cost real performance.
Running a guest operating system that believes it owns the machine used to require interpreting or rewriting its privileged instructions. Hardware virtualization support added a mode below the kernel's, so a guest can run its own privileged code at native speed while the hypervisor stays in control — and a second layer of address translation so guest memory works without the hypervisor intervening on every access.
A vCPU is not a core. It is a schedulable thread of execution that the hypervisor multiplexes onto physical hardware, sharing that hardware with other guests. This is why cloud instance performance varies, why steal time exists, and why the count in the instance description does not translate into a guaranteed amount of compute.
An allocation call returns a pointer, but between that call and a cache line being filled sit an allocator, a virtual address space, a page fault, a physical frame chosen by the kernel and finally the hardware that transfers the line. Each layer shapes where your data lands, which is why allocation pattern becomes cache behaviour.