Gates → GPUs · Notes
Stanford CS149 · Lecture 2 companion

A modern multi-core processor

A chip may have several cores, several arithmetic lanes inside each core, and several threads ready to use those lanes. Those numbers describe different ways to keep hardware busy. We will follow one instruction through a small processor, then build up a chip one feature at a time. At each step, the question is what kind of work a program must supply before the extra hardware helps.

You'll be able to
Explain multi-core, SIMD and hardware multithreading. Say what kind of parallelism each exploits and what the program must provide for each to pay off. Calculate the peak throughput of a real chip, and work out how many threads are needed to hide a given memory latency.
Background
Helpful: Units 1–4 of these notes (especially caches, superscalar execution, SMT and SIMD). This page recaps everything it needs, so it can be read on its own.
Source
Follows the structure and examples of Stanford CS149 Parallel Computing, Fall 2023, Lecture 2, "A Modern Multi-Core Processor" (Kayvon Fatahalian and Kunle Olukotun). The explanations, diagrams and explorers here are original study material, not a copy of the slides.
Keep three questions in view. Can different pieces run on different cores? Can several data values follow the same instruction on one core? When one piece waits for memory, is another ready to run? The lecture answers them with multi-core execution, SIMD, and hardware multithreading.
Part 0 · Review

What a processor does, and what memory is

A quick rebuild of the basic model, because every later idea modifies one of its three boxes.

Section 0 · Foundations

Three boxes, one rhythm: the life of an instruction

Start with ADD r0, r1: read two registers, add their values, and keep the answer in r0. Three kinds of hardware take part. Orange fetch/decode finds and interprets the instruction; the yellow execution unit computes; the slate execution context supplies and keeps the thread's registers and next-instruction address. The same colors recur in every diagram.

Fetch / decodeThe program counter (PC) names the next instruction. Fetch gets its bits; decode identifies the operation and its inputs and destination.
Execution unit (ALU)The arithmetic and logic unit produces a result from the requested operation and input values. It might add, compare, or calculate a memory address.
Execution contextRegisters and PC retain one thread's current working values and place in the program. Memory holds additional state outside this context.
The clockIn our first toy core, the context captures a finished result at a clock edge. Between edges, signals have time to travel through the other boxes.

Who reads and writes what

For the add, fetch/decode receives the PC and instruction bits; the execution unit receives the decoded operation and register values; the context receives the result. The table follows those handoffs. In this simplified view, the context and memory retain state between instructions. Real cores also have temporary pipeline storage, which we will introduce after the toy example.

BoxTakes inHands onKeeps state between instructions?
Fetch/decodePC and instruction bitsOperation, input and destination register names, any constantNo, in this toy model
Execution unitDecoded operation and input valuesResult or load/store addressNo, in this toy model
Execution contextResults, loaded values, next PCRegister values and PCYes: per-thread register and PC state
Data cache / memoryAddress; also a value for a storeValue for a loadYes: stored program data

Start simple: a machine you can program

Now try a processor small enough to trace by hand. TINY-4 has four registers, four kinds of instruction, and no data memory. Each instruction occupies one byte. Within one clock, it moves through four conceptual phases:

  • Fetch (fetch/decode): the PC selects one byte of instruction memory.
  • Decode (fetch/decode, reading the context): the byte is cut into fields, and the registers it names are read.
  • Execute (execution unit): the ALU adds, subtracts, passes a constant through, or tests a register for zero.
  • Write back (execution context): at the clock's rising edge, the result register and the PC are updated together.
InstructionBitsMeaning
SET rd, n00 dd nnnnrd ← n, a constant from 0 to 15
ADD rd, rs01 dd ss 00rd ← rd + rs
SUB rd, rs10 dd ss 00rd ← rd − rs
JNZ rd, addr11 dd aaaaif rd ≠ 0, jump to addr; otherwise continue with the next instruction

The first two bits are the opcode, which selects SET, ADD, SUB, or JNZ. The next two select one of the four registers; the final four encode a small constant, a source register with two unused bits, or a jump address. The registers start at zero and hold 8-bit values from 0 to 255; arithmetic wraps around, so 255 + 1 becomes 0. There is no HALT instruction: the simulator stops at an empty instruction-memory slot. Start with “add two numbers” and follow the PC, instruction bits, register inputs, and result through one clock.

Example:

Edit the program
Syntax
One instruction per line, for example ADD r0, r1.
Registers r0 to r3; constants 0 to 15.
loop: in front of an instruction makes a label that JNZ can jump to (or give the address as a number).
; starts a comment.
At most 16 instructions.
Read one instruction from bits to result. The orange row is the instruction being processed; the triangle marks the PC's current position. Fetch brings its eight bits to decode, which separates the fields shown in the table. The ALU computes, then the register result and next PC are captured at the clock edge. The four phases are drawn as quarters of a clock to help you step through them; they are not four full clocks.
Try itUse the simulator to check your answers.
  1. In "multiply 3 × 4", how many instructions does the machine execute, and how many clocks does that take?
    Show answer

    15 instructions and 15 clocks. Three SETs establish the values; then the three-instruction loop runs four times: 3 + 4 × 3 = 15. In each iteration, one ADD grows the product while SUB and JNZ count and repeat. TINY-4 completes one instruction per clock.

  2. In the multiply program, change SET r2, 4 to SET r2, 0. Predict the result, then load it and press Run on fast.
    Show answer

    The loop body runs before the test, so the first SUB r2, r3 computes 0 − 1, which wraps to 255. The loop then runs 256 times in all, adding 3 each time: r0 = 768, which wraps to 0 in 8 bits. That's 3 + 256 × 3 = 771 instructions. The hardware did exactly what the bits said; the bug is in the program.

  3. During the write-back phase of a JNZ, which part of the execution context changes?
    Show answer

    Only the PC. JNZ produces no register result: it just chooses between the jump target and PC + 1. It's the instruction that shows most clearly that "which instruction runs next" is itself part of the program's state.

  4. Constants only go up to 15. Write a program that leaves r0 = 100 while executing as few instructions as possible.
    Show answer

    One six-instruction answer: SET r0, 12, then ADD r0, r0 three times (24, 48, 96), then SET r1, 4 and ADD r0, r1. Doubling with ADD is the trick: it's a shift left by one bit. A loop would be shorter to write but would execute more instructions.

The life cycle, phase by phase

TINY-4 has no data memory. To understand a load or store, add a path to a data cache. The next simulator breaks an instruction into seven conceptual steps. They are a guide to the data flow in this teaching core, not a rule that every real instruction takes seven clocks or executes those steps strictly one after another. An add skips memory access; a store skips register write back.

  1. Fetch. The PC, held in the execution context, is the address of the next instruction. Fetch/decode sends it to instruction memory (in practice, the instruction cache) and gets back the instruction's bits.
  2. Decode. The bits are split into fields and turned into control signals: which operation the ALU should perform, which registers supply its inputs, which register receives the result, and whether memory is involved.
  3. Read operands. The register names from decode select entries in the context's register file, which supplies their current values to the ALU's inputs. The context is read, not changed.
  4. Execute. The ALU performs the operation. For arithmetic, the output is the answer. For a load or store, the output is an address: base plus offset.
  5. Memory (loads and stores). The address goes to the data cache. A load waits for the value; a store sends its value to memory. A miss can extend the wait over many clocks.
  6. Write back (instructions with a register result). The ALU result, or a loaded value, is saved in the destination register. A store instead changes memory and has no register result.
  7. Advance the PC. The PC takes the next instruction's address. It advances by four bytes in this fixed-width example, by one byte in TINY-4, or to a branch target when a branch is taken.

Use the next explorer to follow a load through all seven steps. The four-instruction program reads x[2] = 3, multiplies twice to make 9 and then 27, and stores y[2] = 27. Registers r1 and r2 begin at 2, the array index. Step first through the load; then use “Next instruction” to see how its result becomes the first multiply's input. Finally compare an L1 hit with a DRAM miss.

The load from x[2]:

Follow both the value and the state. Highlights and teal arrows show which box supplies each step. The load leaves r0 unchanged while the address is computed and memory is read; r0 becomes 3 at write back. The PC update is shown separately for teaching, although this model captures it at the same edge as write back. The final store changes y[2] in memory, not a register. A DRAM miss lengthens the load's memory wait; compare the final utilization readout.

Staying in sync: what the clock does

The clock explains when a computed value becomes stored state. The ALU and much of fetch/decode are combinational logic: as their inputs change, their outputs can change during the clock period. Registers in the execution context are storage elements; they capture settled inputs at a clock edge. The clock period must give the relevant logic enough time to settle before that edge.

TINY-4 is deliberately simple: its fetch, decode, execute, and update fit within one clock. The load simulator relaxes that assumption because even an L1 hit takes several cycles; it holds the thread while memory responds. A pipelined core, which we will see next, adds temporary registers between stages so several instructions can be in progress at once.

Across these models, two correctness rules survive:

  • Dependencies are honoured. The second mul reads r1, which the first mul wrote. It must see the new value, 9, not the old one. In the simple core this is automatic, because the first instruction's write back happens at a clock edge before the second instruction reads.
  • Visible behavior is preserved. The core may overlap or reorder internal work, but the program must observe results allowed by its instruction and memory rules. The simplified examples here match one-at-a-time execution.

Keeping every box busy: overlapping the phases

In the one-instruction-at-a-time picture, much of the hardware waits while another box works. Pipelining overlaps instructions by placing temporary registers between stages: while one instruction executes, the next can decode and another can fetch. The timeline below uses a different timing model from TINY-4: each instruction has five one-clock stages, F, D, X, M, and W. Doing four instructions serially takes 4 × 5 = 20 clocks; overlapping the stages, with one dependency stall, takes 9.

Same four instructions, same boxes, less than half the time. Colours match the boxes: F and D are fetch/decode, X is the execution unit, M is the data cache, W is the write into the execution context. Pipelined, a new instruction starts almost every clock. The one hatched stall is the first mul waiting for r0, which the load produces only at the end of its M stage. The core forwards the value straight to the ALU rather than waiting for W, so it waits just one clock. The second mul needs r1 from the first, which the ALU has just produced and forwards with no wait at all. Unit 3 covers pipelining and forwarding in detail.

The drawn timeline gives the load a one-clock M stage to isolate the pipeline idea. A real miss to DRAM can add hundreds of cycles. The first multiply needs that load's result, and the later instructions depend on the multiply, so this short program has no independent work to cover the wait. That is the opening for the rest of the lecture: use other work while one chain is blocked.

Which timing model are we using? TINY-4 is a single-cycle teaching core: one instruction completes within one clock. Its clock must be long enough for the slowest path through its simple hardware. The memory explorer instead lets a load wait across several clocks. A real processor may split and overlap work in further ways:

  • Multicycle: one instruction takes several short clocks, one per phase (Unit 2, Lesson 6).
  • Pipelined: an instruction crosses several stages and clocks, while a new one can start each clock once the pipeline is full (Unit 3, Lesson 8).
  • Wide out-of-order core: when several instructions are ready, it can start and complete more than one in a clock, even if they execute out of program order. It updates the visible registers when instructions retire, in order (Unit 3, Lessons 13–15).

Latency is how long one instruction takes from start to finish; throughput is how many instructions finish per unit time. Pipelining can raise throughput without shortening latency. Later diagrams usually say “one instruction per clock” to mean one instruction can be issued per clock when no dependency or memory wait blocks it. A load may still take many clocks to finish. Units 3–4 explain forwarding, precise exceptions, and store buffers that make the real overlap safe.

Why this matters for the rest of the lecture

Once you see the three boxes as separate resources with a clear contract between them, every design in this lecture is just a different answer to one question: which box do you duplicate?

Four ways to reuse or replicate hardware. In this simplified diagram, superscalar adds fetch/decode and execution capacity to one context; multi-core makes complete cores; SIMD lets several ALUs follow one decoded instruction; multithreading keeps several register/PC contexts ready for one core. Memory state sits outside those contexts. A core can switch to another ready thread when the current one waits, provided it also tracks which outstanding memory request belongs to which thread.
PredictAnswer before opening.
  1. During the execute phase of ld r0, x[r1], what does the ALU compute? Has r0 changed yet?
    Show answer

    The ALU computes the address of x[2]: the base address of x plus the index 2 scaled by the element size. r0 has not changed. It changes only at write back, after the data cache has returned the value 3.

  2. In the one-at-a-time core, the load goes to DRAM. What fraction of clocks does the ALU do useful work across the four instructions?
    Show answer

    The ALU works for one clock per instruction, so 4 useful clocks. The load takes 1 clock to fetch, decode and compute its address, plus 248 clocks stalled waiting for DRAM: 249. The other three take 1 each, so the run takes 252 clocks. That's 4/252, about 1.6%. With an L1 hit, the load takes 1 + 4 = 5 clocks and the run takes 8, so 4/8 = 50%. Memory latency, not arithmetic, sets the pace.

  3. A second program is waiting to run on this core. Which state must the core keep separately to switch to it during the DRAM wait? What extra bookkeeping would real hardware need?
    Show answer

    Keep a second execution context: another PC and register set. Then fetch/decode and the ALU can serve the other thread while the first waits. A real core also tracks each outstanding memory request so its answer returns to the right context. This is hardware multithreading, developed in Section 10.

Section 1

A program is a list of instructions

We can now compress the long trace into one picture. A compiler produces an instruction stream—loads, arithmetic, branches, and stores. The PC selects the next instruction; fetch/decode interprets it; the execution unit works with values held in registers or returned by memory. In the simplest model, one stream uses one context and one execution path:

One core, one instruction stream. Orange fetch/decode reads the instruction selected by the PC; yellow execution hardware computes; slate register/PC storage retains the thread's context. Memory is separate. The picture is a resource map, not a promise that every instruction finishes in one clock.

The next question is whether this one stream contains independent instructions that the core could overlap. That is the last kind of parallelism we can find without splitting the program into threads. Unit 1 builds a version of the simple machine from gates.

Section 2

Instruction-level parallelism and superscalar cores

In the load-and-cube trace, the first multiply had to wait for the load. Other programs have instructions that can run together. In the explorer, a = 3 and b = 5 are independent; c = a × b must wait for both. An arrow in the dependency graph marks such a “needs this result” relationship. Safe overlap of independent instructions from one stream is instruction-level parallelism (ILP). Try issue width 1, then 2, then unlimited; compare which clocks still contain only one instruction.

Issue width:

Look for the longest required chain. In this one-cycle-per-instruction model, 00 → 02 → 05 → 07 → 08 → 09 has six operations, so more issue slots cannot finish the program in fewer than six clocks. The branch at 08 chooses one print: h = 7 is greater than 5, so 09 runs and 10 does not. Width helps by overlapping work outside the chain.

A superscalar core can start several instructions in a clock when it has enough execution units and ready inputs. More advanced cores use out-of-order control to find an independent later instruction while an earlier one waits. This diagram considers one thread and one execution context; its overlap comes from within that thread's instruction stream.

Overlap must preserve the program's intended result. An instruction may execute early when its inputs are ready, but it cannot use an old value in place of one a preceding instruction should produce. Modern out-of-order cores also retire instructions in order to present precise visible state; Unit 3 explains the reorder buffer that supports this.

The graph also shows the limit: extra issue slots cannot shorten a dependency chain. How much width pays off depends on the program. For the array calculation coming next, the abundant independence is mostly between elements, rather than between nearby instructions for one element.

Section 3

Memory: an array of bytes, implemented with caches

The load in Section 0 named x[2], but the program did not specify where hardware should find it. A program sees an address space: byte locations numbered by addresses. A cache holds recently used data close to the core. A hit finds the requested data there; a miss looks in a slower cache level or DRAM. For a correctly synchronized program, this changes access time, not the memory result promised by the machine.

Caches move data in fixed-size cache lines, rather than fetching just the named byte. A request for address 0x0 in the toy cache below brings in its four-byte line, addresses 0x0–0x3. A later request for 0x1 can then hit without another trip to memory.

Try it: a two-line cache

The explorer has a 16-byte array and a cache that holds either two or four lines of four bytes each. It is fully associative: any line can occupy any slot. When full, it evicts the least recently used (LRU) line. Step through “locality demo” first and watch which requests hit, then read the array twice with both capacities.

Capacity:
Sequence:

Two ways to reuse cached data. Spatial locality means using nearby addresses in a loaded line; temporal locality means revisiting data before its line is evicted. In “read array twice,” a two-line cache cannot retain all four lines of the array, so the first access to each line misses again on the second pass. A four-line cache holds the whole array; the second pass hits.

The hierarchy, and why it matters

Real machines stack cache levels: small, fast L1 near a core; larger L2; often a shared L3; then DRAM. A load checks near levels first and goes farther only on misses. The chart gives approximate total access latency for each destination on an Intel Kaby Lake desktop CPU at 4 GHz; the times are alternatives, not numbers to add together.

About 60× from L1 to DRAM in this example. An instruction that needs the loaded value must wait until it arrives. The core may continue with independent instructions if it has any; otherwise execution slots go unused. Later sections show ways to shorten the wait or keep the core useful during it.
PredictAnswer before opening.
  1. In "read array twice" with a 2-line cache, why is the second read of address 0x0 a miss? What about 0x4?
    Show answer

    After the first pass the cache holds only the last two lines, 0x8 and 0xC. Line 0x0 was evicted (least recently used) when 0x8 arrived, so 0x0 misses. Loading it evicts 0x8, the LRU line, and then line 0x4 is also gone (it was evicted when 0xC came in), so 0x4 misses too. Every line misses on the second pass: a textbook capacity problem.

  2. Would the answer change with 4 lines?
    Show answer

    Yes. 4 lines × 4 bytes = 16 bytes, the whole array. After 4 cold misses in the first pass, every access of the second pass hits.

Part 1 · Parallel execution

Three ways to do more work per clock

Superscalar execution ran out of steam. Here's what chip designers did with their transistors instead.

Section 4

The running example: sin(x) for a whole array

For the rest of the lecture, keep one job in mind: given an array of N numbers, approximate sin(x[i]) for each element and store it in y[i]. The input angle is in radians. One way to approximate sine is to add a fixed number of terms from its Taylor series:

sin(x) = x − x³/3! + x⁵/5! − x⁷/7! + …

for i = 0 … N−1:one iteration per array element
  value = x[i]; numer = x[i]³; denom = 6; sign = −1first term x, then set up x³/3!
  for j = 1 … terms:add one more term each time
    value += sign · numer / denom
    numer ·= x[i]²; denom ·= (2j+2)(2j+3); sign = −signnext power, next factorial, flip sign
  y[i] = valuewrite the result

The code starts value at x, then adds −x³/6, then +x⁵/120, and so on. numer, denom, and sign prepare each next term without recomputing powers and factorials from scratch. A finite term count makes this an approximation; for large inputs, practical sine implementations also reduce the angle's range. Here the term count is fixed so every array element follows the same loop structure.

Find the independence at the right level. Assume different indices do not overlap in memory. Computing y[0] needs only x[0], and computing y[1] needs only x[1]; those outer iterations can run together. Within one iteration, the next term uses values left by the previous term, especially the running sum. The numerator, denominator, and sign updates have some independent work, but the loop still contains long dependencies.

As written, one instruction stream works through the array in index order. A superscalar core may overlap some instructions within an iteration, but it cannot make the running sum of one element independent of itself. The abundant parallel work is across different elements. We will use that fact three ways.

Section 5 · Idea #1

Use the transistors for more cores

ProblemBeyond a point, more control hardware on one core brings diminishing gains when its instruction stream has dependency chains.
IdeaBuild several simpler cores instead, each with its own fetch/decode, ALU and execution context.
Program must provideMultiple instruction streams (threads), with enough independent work to give every core some.
Trade-offIn the illustrated design, each simpler core is slower alone. Several cores help only when there are independent streams to run.

One way to use a chip's transistor budget is to make a core more elaborate: add out-of-order control, branch prediction, and caches to accelerate one stream. Another is to build more cores. The diagram makes the trade-off deliberately simple: replace one “fancy” core with two smaller ones.

Trade sophistication for count. Suppose each simpler core runs a single stream 25% slower than the fancy one. Two of them still give a potential 2 × 0.75 = 1.5× the throughput, if there are two streams to run.

If we leave the sine loop as one sequential stream, it uses one of the two simple cores and, under the diagram's assumptions, runs 25% slower than on the fancy core. We already know that different elements are independent; software must expose or establish that fact so a compiler or runtime can divide them safely. Compilers sometimes prove independence automatically, but an ordinary loop alone is no promise that its iterations can be split.

Expressing the parallelism

Option 1: threads. Create a second software thread and give it half the array. The main thread computes the other half, then waits for the helper to finish ("join"). With a C++ threading library that's a few lines. The programmer decides the split explicitly.

Option 2: declare independence. The lecture's imaginary forall means “run iterations i = 0 through N − 1 in any order.” A compiler or runtime can divide those iterations among cores. Real tools express related ideas through constructs such as OpenMP's parallel for; later lectures use ISPC and CUDA for other forms of data-parallel work.

Different cores, different elements. Sixteen cores can run sixteen instruction streams at once, each working on a different array region. This is thread-level parallelism. Real chips range from multi-core CPUs to GPUs with many streaming multiprocessors (SMs). An SM is not equivalent to a CPU core; the RTX 4090 enables 128 SMs from a 144-SM die. Later sections show why those designs count parallelism differently.
Section 6 · Idea #2

Share one instruction stream across many ALUs

ProblemBuilding a complete new core for each data element repeats instruction-fetch and control hardware even when every element needs the same operations.
IdeaSIMD (single instruction, multiple data): one fetch/decode drives many ALUs, all running the same instruction on different data.
Program must provideGroups of data elements that can follow the same instruction sequence at the same time.
CostWide registers and datapaths. When elements need different instructions, lanes sit idle (divergence).

With a fixed number of Taylor terms, eight sine iterations can advance through the same step together: each needs its own x[i] and running sum, but all can multiply their current numerators at once. SIMD—single instruction, multiple data—fetches and decodes one vector instruction and applies it across eight lanes. Each lane handles a different element, so the instruction-control cost is shared.

One instruction, eight lane results. A 256-bit vector register holds eight 32-bit floating-point values. Each lane multiplies one pair when vmulps runs, producing eight results from one instruction. The lanes share control but keep their data values separate.

A C implementation can process eight array elements per loop trip, advancing i by eight. value and numer become vectors because their values differ by lane; the shared term count, denominator, and sign may remain scalar and be used by every lane. Intel AVX intrinsics, C functions that request particular vector instructions, include _mm256_mul_ps(a, b) for eight pairwise float multiplies. If N is not divisible by eight, the remaining elements need a shorter masked vector or a scalar cleanup loop.

Instruction setVector width32-bit floats per instruction
Intel/AMD AVX2256 bits8
Intel AVX-512512 bits16
Arm NEON128 bits4

Vector instructions can come from hand-written intrinsics, a language construct that expresses independent element work, or an auto-vectorizing compiler that proves a loop safe to group. On a CPU, those choices produce vector instructions in the compiled binary: explicit SIMD. The compiler still has to account for dependencies, alignment, and leftover elements; “same arithmetic” by itself is not enough.

Combine independent streams with shared instructions. Sixteen cores can run sixteen streams; if each stream uses all eight lanes, the chip has capacity for 128 simultaneous element operations. The sine array provides enough independent elements, provided the lanes receive data and follow the same path.
Section 7

Conditional execution: what if lanes disagree?

The fixed-term sine loop gives eight lanes the same path. Other array loops contain a branch, so lane 0 might need one operation while lane 1 needs another. Imagine each lane holds one t = x[i] and runs this condition:

if (t > 0):"then" side: 3 instructions
  t = t · t; t = t + 1; t = √t
else:"else" side: 2 instructions
  t = −t; t = t · 2
y[i] = teveryone meets again here

One vector instruction cannot perform two different operations in its lanes at the same instant. When both sides are needed, the execution follows them in turn with a mask marking which lanes are active for each side. CPU compilers can emit masked operations or compute and blend results; GPU hardware tracks active lanes within a group of threads. The explorer models the shared cost: click lanes to change their conditions, then watch how many lanes do useful work in each clock.

Presets:
Masked lanes do no useful work. The copper cells are lanes doing useful work. The hatched cells are lanes switched off while the other side of the branch runs. A side that no lane needs can be skipped entirely ("all positive" costs 3 clocks, not 5).

If only one lane takes a long branch, the other seven are idle during that branch. For example, with one lane per consecutive index, if (i % 8 == 0) { long computation } gives just one active lane in an eight-lane group for the long part: 1/8 of that vector unit's lane capacity during those instructions. Other parts of the program may still use all lanes.

Useful vocabulary

  • Coherent execution: the grouped elements follow the same instruction path, so most SIMD lanes can stay active. Different CPU cores have separate instruction streams and need not follow the same path.
  • Divergent execution: grouped elements need different paths, leaving some lanes masked while another path runs.

Explicit versus implicit SIMD

CPU vector instructions name a vector operation directly: explicit SIMD. On NVIDIA GPUs, programmers describe many logical scalar threads. Hardware groups neighboring threads into fixed 32-thread warps and schedules the warp's instructions across execution lanes. This is often called SIMT (single instruction, multiple threads) or implicit SIMD. A divergent warp must serve its different paths with only the appropriate lanes active; hardware does not gather arbitrary threads from across the chip just because they reach the same instruction.

Two ways to express grouped work. A CPU binary contains explicit vector instructions selected by the compiler. A GPU program presents scalar-looking threads; hardware schedules fixed groups of those threads together and masks lanes when their control paths differ. Both approaches depend on enough lanes doing useful work at once.
Section 8

Three forms of parallel execution, on real chips

FormWhat runs in parallelWho finds the parallelismNeeds from the program
SuperscalarDifferent instructions from one stream, within one coreHardware, automatically, at run timeIndependent instructions nearby (ILP)
SIMDThe same instruction on many data elements, within one coreCompiler (explicit) or hardware (implicit)Data parallelism + coherent control flow
Multi-coreCompletely different instruction streams on different coresSoftware: threads, or a forall-style constructIndependent tasks (threads)
Count what is shared. In the simplified superscalar drawing, two execution paths use one thread's context. In the dual-core drawing, each core has its own instruction stream. In the SIMD quad-core drawing, each of four streams drives eight data lanes. The three designs ask for different kinds of independence from the same sine array.

Peak rates count what the hardware could do if instructions, independent data, and memory supply all line up. They are useful as ceilings; the program's achieved rate can be much lower. The next two calculations use the particular chips and operation assumptions shown in the lecture.

Worked example: peak throughput of a real CPU

The lecture's Intel Core i7-7700K example has 4 cores and uses an idealized rate of three 8-wide vector arithmetic instructions per core per clock at its 4.2 GHz base clock:

4 cores × 3 units × 8 lanes × 4.2 × 10⁹ clocks/s ≈ 400 billion operations per second (≈ 400 GFLOP/s)

That is about 400 billion single floating-point operations per second in this model, for an instruction mix that can keep the pictured units busy. Three vector operations per clock is not a universal throughput for every instruction: the lecture's core allows fewer vector multiplies than adds, and fused multiply-add (FMA) counts as two floating-point operations. A one-operation-per-clock scalar baseline is 4.2 GFLOP/s in the same simplified comparison, about 1% of 400. Reaching the larger rate also needs threads, vectorized work, enough independent instructions, and data supply.

The NVIDIA V100 SXM2 GPU has 80 streaming multiprocessors (SMs), with 64 FP32 lanes per SM. At about 1.53 GHz, an FMA on each lane contributes two floating-point operations:

80 × 64 × 2 × 1.53 × 10⁹ ≈ 15.7 TFLOP/s (SXM2, up to about 300 W)

This is a different peak-operation mix from the CPU's illustrative vector-arithmetic count, so their ratio does not predict speedup for one program. It does show why GPUs devote much more hardware to parallel arithmetic. Section 12 looks inside an SM and asks what software must provide to use it.

Part 2 · Accessing memory

Keeping all those ALUs fed

More execution capacity increases the demand for data. A long load can leave dependent work waiting unless the core has something else ready.

Section 9

Stalls, and two ways to shorten or hide them

The sine array has independent elements, but each element still has to load its input. If a later operation needs a value that has not arrived, that dependency chain stalls. A DRAM trip takes roughly 250 cycles on the example Kaby Lake machine. An out-of-order core may run other ready instructions or overlap several loads during that time; if it runs out of such work, its execution units go unused.

Follow a load outward on misses. In this illustrative multi-core CPU, each core has private L1 (32 KB) and L2 (256 KB) caches, the cores share an 8 MB L3, and DRAM provides about 38 GB/s of peak transfer capacity. The same data may have copies at several levels. Nearby caches reduce latency for hits and can supply data at higher rates than DRAM.

Technique 1: caches reduce the length of stalls

If a line is already in L1, a load may take about four cycles rather than a DRAM trip of hundreds. Reusing a recent line or a neighboring byte in that line makes hits likely. A first demand for a cold line cannot benefit from temporal reuse, though a prefetch or an earlier request for a nearby address might have brought it in. If the working set exceeds the cache, useful lines can be evicted before their next use.

Technique 2: prefetching hides stalls

The sine loop walks predictably through x[0], x[1], and onward. A prefetcher can notice such address patterns and request future cache lines before the program asks for them. If the line arrives in time, a later load hits. Prefetching overlaps data travel with earlier useful work; it does not make the memory device itself faster.

Prefetching moves the waiting earlier, overlapping it with useful work. Wrong guesses have a cost: they use memory bandwidth and can evict data the program actually needs ("cache pollution").

Now imagine x = some_function(); y = A[x];, where x becomes known only after a dependent computation and A is too large for cache. A prefetcher may have no reliable next address, and the load may miss. We need a way to use the core while that request is outstanding.

Section 10 · Idea #3

Hide stalls by switching to another thread

ProblemLong, unpredictable memory stalls leave the ALUs idle, and caches and prefetchers can't always prevent them.
IdeaKeep several threads' execution contexts on the core. When one stalls, run another.
Program must provideOther independent threads that are ready while one waits, with enough work to use the execution units.
Trade-offMore contexts use chip storage. Throughput may rise; an individual thread can take longer if it must wait its turn for a busy core.

Think of starting a wash, then cooking dinner while the machine runs. A multithreaded core keeps several threads' register and PC contexts ready. Thread 1 can request data for one group of sine elements; while it waits, the core issues instructions from thread 2's independent group. By the time other threads have taken turns, thread 1's data may be ready. The memory request still takes just as long; the core has found useful work during the wait.

One core, four hardware threads. Each thread processes 8 elements with the 8-wide SIMD unit. Blue is running, red is stalled on memory, and grey is runnable but waiting its turn. The ALUs are almost always busy.
Watch ready threads as well as stalled ones. A thread whose data has arrived is runnable. If the core is already serving another thread, it waits its turn; that contention can increase its completion time. When the core would otherwise have been idle, switching to another thread can raise total throughput without delaying the first thread. GPUs provide many resident threads because long memory waits often leave useful gaps to fill.

No free lunch: context storage

Each resident hardware thread needs registers for its live values. The register storage is finite: a workload that uses few registers per thread can keep more threads resident, while one with many live values may fit fewer. More resident threads offer more chances to hide a stall, but only when those threads have independent work and memory can serve their requests.

The register-file trade-off. The picture divides the same storage into many small or a few large per-thread contexts. GPUs can hold many warps per SM; CPUs commonly expose one or two hardware threads per core. If each GPU thread uses more registers, fewer warps may fit. The fraction of available warp slots actually occupied is part of what GPU programmers call occupancy.

Interleaved versus simultaneous

Interleaved (temporal) multithreading: each clock, the core picks one thread and runs its instruction. That's the model in these diagrams. Simultaneous multithreading (SMT): each clock, the core can pick instructions from several threads and run them on different ALUs at once. Intel's Hyper-Threading, with 2 threads per core, is SMT. Either way, the core has the same number of ALUs. Multithreading doesn't add compute. It only helps keep the existing ALUs busy when some threads are waiting.

Section 11

How many threads does it take?

Count how many ready threads are needed to cover a memory wait. The explorer uses a deliberately simple one-issue core: one thread occupies it for k arithmetic clocks, then launches a memory request and waits L clocks before its next arithmetic burst. The request itself takes no separate issue slot in this model. Real loads do use hardware resources, so the formula below is a teaching estimate, not an exact sizing rule for a real chip.

Watch running, waiting, and ready time. Blue uses the core, red waits for memory, and grey is ready but waiting its turn. With k = 3 and L = 12, one thread supplies work for 3 of every 15 clocks: 20% utilization. Five staggered threads can fill the 12-clock gap in this model. Raising k to 6 reduces the number needed to three. Additional threads do not raise the one-issue core's peak rate.

One thread's cycle is k clocks of arithmetic plus L of waiting. If T threads stagger their bursts perfectly, together they can supply T × k busy clocks in that k + L interval. Utilization cannot exceed 100%, and the smallest whole-number T that covers the interval is:

threads needed = ⌈(k + L) / k⌉ = ⌈1 + L / k⌉     utilization = min(1, T·k / (k + L))

What the formula teaches. Other threads can keep an execution unit busy during a memory wait without shortening the wait itself. More useful arithmetic per request means each thread fills a larger part of the gap, so fewer threads are needed. This assumes independent ready threads, perfect switching, and enough memory capacity for their requests; real performance also depends on those limits.
PredictUse the formula, then check with the sliders.
  1. With 4 arithmetic instructions per load and a 20-cycle memory latency, how many threads reach 100%? What is the utilization with 3 threads?
    Show answer

    (4 + 20) / 4 = 6 threads. With 3: 3 × 4 / 24 = 50%.

  2. Adding an 8th thread to the 3-instruction / 12-cycle program: does anything get faster?
    Show answer

    Total throughput cannot rise above the already full one-issue rate. With extra ready threads sharing the same core, a thread may wait longer for its turn after data arrives; its individual completion time can rise. That delay comes from contention for the core, not from a slower memory access.

Section 12

All the ideas in one chip

Put the pieces together in the lecture's fictitious chip. Sixteen cores give independent instruction streams somewhere to run. Eight SIMD lanes per core process groups of elements. Four contexts per core keep replacement threads ready when one waits. These counts play different roles: cores and lanes add arithmetic capacity; contexts help use that capacity during stalls.

Separate active lanes from resident work. Sixteen 8-wide cores provide 128 arithmetic lanes. In this interleaved model, at most 16 instruction streams issue in one clock, one per core; up to 64 streams have contexts resident. Keeping all 64 contexts supplied with eight elements each takes 16 × 4 × 8 = 512 independent elements. That is a full-residency count, not a universal minimum to keep 128 lanes busy when loads are already ready.

A real CPU core: Intel Skylake / Kaby Lake

Several kinds of overlap in one CPU core. The lecture's simplified Skylake/Kaby Lake diagram shows two hardware threads, scalar issue capacity, and eight-wide vector units. Its “up to four scalar” and “up to three vector adds” are separate ceilings, not seven instructions that can all issue together; vector multiplies have a lower limit of two. Load/store units are omitted. Four such cores make the illustrated CPU chip.

A GPU core: one NVIDIA V100 SM

A GPU pushes the same choices toward throughput. NVIDIA calls a major execution block a streaming multiprocessor (SM). An SM holds many warps, fixed groups of 32 logical threads that execute together. When one warp waits for memory, another ready warp can use the execution units.

Inside a V100 SM. Four sub-cores each have warp selection, execution units, and a 64 KB register file. FP32 and INT32 units are 16-wide; FP64 units are 8-wide, with tensor and load/store hardware alongside them. A 32-thread warp's FP32 instruction uses the 16-wide unit over two clocks in this diagram. The hardware supports up to 64 resident warps per SM: at that maximum, 64 × 32 = 2,048 threads per SM and 80 × 2,048 = 163,840 across the GPU. Register and shared-memory use can lower actual residency. Keeping register state on chip avoids a save/restore on a warp switch, though scheduling is not literally free.

Why room for so many threads? V100 memory can transfer roughly 900 GB/s through its HBM interface, yet an individual request can still take hundreds of cycles. Many ready warps let the SM issue other work during that latency. The 163,840 figure is a maximum resident-thread capacity across the GPU, not a number every program must reach. The useful count depends on arithmetic per load, resource use per thread, and available independent work.

Wrap-up

What your program must provide

Three requirements, one thought experiment, and a chip you can build yourself.

Section 13

The story so far

The sine example gave each mechanism a job: independent array elements fed cores, fixed-term loops filled SIMD lanes, and extra ready threads could cover a load miss. The diagram collects those three requirements. They are useful checks, but satisfying them does not guarantee that memory can deliver data quickly enough.

Match the work to the hardware. Without independent work, cores or execution units go unused. Without similar control paths, vector lanes may be masked. Without another ready thread, an unpredictable load can leave a core with no work to issue. Even when all three conditions hold, memory transfer rate can set a separate ceiling.

A thought experiment: multiplying two big vectors

Now multiply two very large arrays: C[i] = A[i] × B[i]. Each element is independent, follows the same instructions, and offers plenty of tasks to keep threads resident. Yet every multiply needs two input values delivered and one output stored. Even an ideal schedule cannot multiply faster than memory can supply those bytes. Use the explorer to compare that supply with the chip's nominal compute rate.

Memory bandwidth sets the ceiling here. At minimum, each element transfers 12 bytes: two 4-byte inputs and one 4-byte output; real traffic can be higher. At 900 GB/s, memory can feed at most 75 billion plain multiplies per second, or 0.075 TFLOP/s (one FLOP per multiply). The chart's compute bar uses the V100's 15.7 TFLOP/s FMA peak as a generous nominal ceiling for multiplies; FMA counts two FLOPs, so that label is not a measured plain-multiply peak. Even under this favorable comparison, memory feeds under 1% of the nominal rate. More threads can hide latency, but cannot exceed available bandwidth in bytes per second. Lecture 3 begins there.
Section 14

How it all fits together: build your own chip

Build a chip one choice at a time. Adding cores or SIMD lanes raises its peak arithmetic capacity; adding contexts gives each core more threads to turn to during stalls. Use the presets first, then change one control and watch which counter moves. The “work needed” counter describes enough elements to populate every context and lane, not a minimum for arithmetic saturation when one well-fed thread can keep a core busy.

Cores
SIMD width per ALU
Issue per clock (superscalar)
Hardware threads per core
Presets:

Read the boxes and the counters separately. Grey is cache, orange is instruction selection and fetch/decode, yellow is arithmetic, and blue is resident register/PC contexts. With issue width 2 and SIMD enabled, this model pairs one vector instruction with one scalar instruction. “Peak ALU operations” counts lane capacity; “work needed” is cores × contexts × SIMD lanes for full residency. The “streams issuing per clock” counter counts distinct threads, not instructions: one stream with enough ILP can still fill both issue slots.

Try the superscalar preset: it has one resident thread and two issue slots. The distinct-stream counter remains one, while two independent instructions from that one stream could issue together. Then add a second hardware context. The arithmetic peak stays the same, but a second thread can use a slot or cover a stall. That difference is central to reading the next simulator.

Section 15 · Going further

Every combination: what does it take to saturate the chip?

The closing diagrams combine issue slots, scalar and vector units, contexts, and cores. More execution units or cores can raise arithmetic peak, the operations the hardware could do each clock. More contexts do not add arithmetic units; they can raise achieved throughput by giving a stalled core another thread to run. The matrix and simulator let you test both questions: what capacity did the design add, and what work would use it?

The four dials, and what each one asks of the program

DialHardware addedWhat the program must supply to use itWhat happens if it doesn't
Fetch/decode units (issue width)More instructions started per clockEither ILP: independent instructions next to each other in one stream. Or, with simultaneous multithreading, another ready thread.Extra issue slots stay empty
Execution units (kind and width)Scalar ALUs, 8-wide SIMD ALUs, or bothInstructions of the right kind (a scalar/vector mix matching the units) and, for SIMD, coherent data: 8 elements running the same instructionsUnits of one kind idle, or SIMD lanes masked off
Execution contexts (hardware threads)Storage for more resident threads; no new ALUsOther ready threads if memory waits leave issue slots emptyExtra contexts add no benefit when existing threads already keep units busy
CoresMore complete execution resourcesAt least one independent stream per core to activate every core; more may help cover stallsUnassigned cores sit idle

The permutation matrix

Rows are four simplified core designs; columns choose contexts per core. Set core count and, for two-issue designs, whether both instructions in a clock come from one thread or may come from different threads (SMT). Each card shows arithmetic peak and a full-residency count: enough streams to fill every context, with eight elements per stream for SIMD. Those counts are capacity markers, not universal minimums for peak arithmetic. Click a card to load a sample workload intended to exercise the design under the simulator's assumptions. For a 16-core card, the simulator demonstrates the same per-core design with four cores, while the matrix retains the 16-core peak. If the sample cannot cover a 12-clock memory wait with the available contexts, it sets modeled memory wait to zero; that is a simulation choice, not a claim that real cache hits take no time.

Cores:
2-issue cores pick instructions from:
Read a card in layers. Grey marks cache, orange instruction selection and fetch/decode, yellow arithmetic units, and blue resident PC/register contexts. “Peak” counts lane operations per clock. “Streams” counts threads to occupy every context, and “work items” counts elements to fill every resident SIMD group. Neither is necessarily the smallest count that keeps ALUs busy. The latency line is a rough estimate for the toy pattern of three arithmetic clocks followed by a 12-clock wait; use the simulator's achieved rate to inspect a particular case.

Try it: the saturation simulator

Pick a design, then change one software control at a time: thread count, independent instructions per stream, scalar/vector mix, active SIMD lanes, and modeled memory waiting. The simulator runs 120 clocks per core and reports achieved lane operations beside the chip's peak. Start with no modeled memory wait to see arithmetic limits; then raise latency and add contexts to see when extra threads recover otherwise empty slots.

Design
Contexts per core
Cores
2-issue selection
ILP per stream
Instruction mix
SIMD coherence

What this simulator counts. A core has one or two issue slots per clock. In “one thread” mode, filling a second slot needs two independent instructions in that stream; SMT may fill it from another ready thread. Instructions need suitable free units, and a scalar instruction can use one lane of an idle vector unit. After k arithmetic instructions, the model triggers a memory wait of L clocks without a separate load-issue slot, then schedules another ready thread if one exists. L = 0 means no modeled stall, not a physically zero-latency cache. Only threads with resident contexts can hide waits. Each timeline cell is an issue slot on core 0.

Use “fraction of peak” to judge the actual modeled run. The checklist also marks whether every context and every possible SIMD work item is filled; those are full-occupancy checks, so they can show a shortfall even when a smaller number of compute-bound threads already keeps the arithmetic units busy.

The general recipe

For a chip with C cores, T contexts per core, issue width W, and SIMD width S, use the table as a set of separate checks. Some describe arithmetic peak; others describe filling all resident contexts or hiding a particular memory wait. They are not six universal minimum requirements for every program.

#RequirementQuantityHardware it feeds
1Streams to activate cores / fill all contexts≥ C / ≥ C × Tat least one stream per core / every resident slot
2Ready instructions per core per clockup to W, from ILP and/or ready SMT threadsissue slots, subject to matching execution units
3Instruction mixmatches the unit mix every clockscalar vs vector units
4Coherent data per vector instructionS elements, same control pathSIMD lanes
5Resident threads to cover a toy memory waitsingle-issue estimate: ⌈1 + L / k⌉ per coreready work while another thread waits
6Elements to fill every resident SIMD groupC × T × Sfull context-and-lane occupancy, not arithmetic minimum

In row 5, k is useful arithmetic clocks between waits and L is the modeled wait in clocks, as in Section 11. The ceiling rounds up to a whole thread. A two-issue core or a mixed scalar/vector design needs additional checks: a thread may issue two operations together only when they are independent and suitable units are free. The matrix's latency percentages are planning heuristics; the discrete simulator gives the outcome for its particular scheduling rules.

Worked example: the Kaby Lake i7-7700K. The lecture depicts four cores, each with two SMT contexts and several scalar/vector execution paths. Its “up to four scalar” and “up to three eight-wide vector” figures are separate simplified ceilings, not seven instructions that can necessarily issue in one clock. Vector multiply throughput is lower than the three-add illustration.

First, give each physical core independent work: four software threads can do that. If each stream supplies enough independent vector instructions and its data is ready, those four may already keep the vector units busy. A second hardware context per core raises the maximum resident count to eight threads; it may help when stalls or issue gaps leave units idle. Eight threads times eight elements per vector gives 64 elements to fill every context's vector group, whereas four active vector streams use 32 elements at a time. Neither count guarantees sustained peak: instruction dependencies, unit mix, and data supply still matter.

Optimized numeric libraries often unroll and vectorize loops and organize data to reuse caches. Their best software-thread count is workload dependent; some run best with one thread per physical core, while SMT helps other cases. Use the achieved-rate measurement, not context occupancy alone, to judge the choice.

PredictThen set it up in the simulator.
  1. A superscalar (2-issue, scalar) core with 1 context runs a single dependency chain with no memory stalls. What's its utilization? What single change fixes it?
    Show answer

    50%. The chain supplies one instruction per clock, so one ALU idles. Fix it with ILP 2 (independent instructions in the stream). Adding a second context alone doesn't help in "one thread per clock" mode. It does help in SMT mode, where a second thread fills the other slot.

  2. A SIMD core with 4 contexts runs 4 vectorized threads with 3 arithmetic instructions per 12-cycle load. Is it saturated? What if the code is only 4/8 coherent?
    Show answer

    It needs ⌈15/3⌉ = 5 threads to hide the latency, so 4 threads give 80% issue utilization. At 4/8 coherence, each vector instruction does only half its lane-work, so the total is 40% of peak. The two losses multiply.

  3. A scalar+SIMD core (2-issue) runs vector-only code. What fraction of peak can it reach, and why?
    Show answer

    Peak is 1 + 8 = 9 lane-ops per clock, and vector-only code can use at most 8 (the scalar unit idles): 89% at best. To reach 100%, every clock needs one scalar and one vector instruction ready, such as address arithmetic and loop counters running alongside the math.

  4. You have 16 cores with 4 contexts each, but your program creates 8 threads. What fraction of the chip can possibly be busy?
    Show answer

    With only eight independent streams, at most eight of the sixteen cores have work, so at most half the chip's arithmetic capacity can be used in this one-stream-per-core allocation. Sixteen threads would give every core a stream. Sixty-four would occupy every context, which might help hide long stalls; it is not automatically needed to saturate arithmetic if the active streams already keep units busy.

Review

Terms to know, and questions to test yourself

Instruction stream
The sequence of instructions one thread executes. Its execution context holds that thread's registers and PC.
Superscalar
One core issuing several independent instructions from the same stream per clock, found automatically by hardware.
Multi-core
Several cores on one chip, each able to run an independent instruction stream.
SIMD
One instruction applied across multiple data lanes. CPU binaries may name vector operations explicitly; GPUs schedule fixed groups of scalar-looking threads together and mask lanes when their paths differ.
Coherent control flow
Many elements following the same instruction sequence, which SIMD needs. Its absence is divergence, handled by masking lanes.
Hardware multithreading
Several execution contexts resident on one core so it can switch threads to hide stalls. Interleaved: one thread per clock. Simultaneous (SMT): several threads in the same clock.
Latency vs throughput
Time for one task or request versus the number completed per unit time. More resident threads can improve throughput during waits; contention may increase an individual thread's time.
Stall
A period when dependent work cannot advance because an input has not arrived. Other independent work may still run.
Bandwidth
The rate at which data can move, measured in bytes per second; distinct from the latency of one request.
Test yourselfThe questions the lecture poses, plus a few more.
  1. A dual-core chip whose cores are each 25% slower than one fancy core runs the original sin(x) program. Is it faster or slower? What would make it faster?
    Show answer

    Slower (by 25%). The program is one instruction stream, so it uses one core. Split the array between two threads (or declare the loop independent with a forall-style construct) and the potential becomes 2 × 0.75 = 1.5×.

  2. Give one branch pattern that uses only 1/8 of an eight-wide SIMD unit's lanes during a long branch body.
    Show answer

    If the grouped lanes hold consecutive indices, if (i % 8 == 0) { long computation } sends only one lane through that body. The other seven lanes are masked for those instructions; this says nothing about the utilization of unrelated code outside the branch.

  3. Why does coherent control flow matter for SIMD but not for multi-core?
    Show answer

    SIMD lanes share one fetch/decode, so they can only execute one instruction at a time. Each core has its own fetch/decode, so different cores can follow different paths without masking one another's lanes. Each path still pays its own branch and memory costs.

  4. Compute the peak rate of a chip with 8 cores, two 16-wide FMA units per core, at 3 GHz (count an FMA as 2 FLOPs).
    Show answer

    8 × 2 × 16 × 2 × 3 × 10⁹ = 1.536 TFLOP/s.

  5. Does hardware multithreading make an individual memory access faster?
    Show answer

    No. The memory request's latency is unchanged. Other threads use execution units during that wait. An individual thread may finish later if it must compete with ready threads for the same core, but multithreading does not necessarily delay it.

  6. What is the V100's maximum resident-thread capacity in the lecture's model, and why is it not a required thread count for every program?
    Show answer

    80 SMs × up to 64 warps per SM × 32 threads per warp = 163,840 resident threads at the hardware maximum. Register or shared-memory use may lower residency, and a program with sufficient ready work may use its arithmetic units without reaching the maximum.

Based on Stanford CS149 (Fall 2023), Lecture 2, "A Modern Multi-Core Processor", by Kayvon Fatahalian and Kunle Olukotun. Explanations, diagrams and interactive models on this page are original study material. Processor figures (Kaby Lake latencies, i7-7700K, V100) are as presented in the lecture and in vendor documentation. For the course's own slides and videos, see the CS149 course site.