Foundations to FrontierPart IV: Algorithms and systemsBezhig

C09: Bits, instructions, caches, and virtual memory

5 exercises, each with a complete solution. 1 figure. 3 code listings. 1 note from the engine.

On this page
  1. A. Prerequisites and observable goals
  2. B. The central question: how can the same bits mean different things?
  3. C. Floating-point numbers represent a scaled significand
  4. D. Boolean circuits and instruction execution
  5. E. From one source function to actual generated assembly
  6. F. Locality, caches, and address translation
  7. G. Independent practice
  8. H. Complete solutions: C09
  9. I. Retention and readiness

A. Prerequisites and observable goals#

Before

C01–C03 values, types, and lifetimes; C04 Boolean logic.

After

Decode integer and floating-point representations, distinguish language rules from compiler and hardware choices, trace a cache model, and explain how source code becomes an executable.

Level: Degree core; one concrete x86-64 example, not a universal assembly language.

B. The central question: how can the same bits mean different things?#

A bit is a binary digit. Eight bits can encode an unsigned integer from 0 through 255, a signed value under a chosen representation, a character-code fragment, or part of an instruction. The bits do not carry their interpretation independently of the type, protocol, or program. Hexadecimal groups four bits per digit: 0010 10102=2A16=42100010\,1010_2=2A_{16}=42_{10}. Binary and hexadecimal are numeral systems, not different underlying quantities.

For an NN-bit unsigned representation, values range from 0 to 2N−12^N-1. In an NN-bit two’s-complement interpretation, a bit pattern with unsigned value u≥2N−1u\ge2^{N-1} represents u−2Nu-2^N; otherwise it represents uu. For eight bits, 111111102=25411111110_2=254 unsigned but 254−256=−2254-256=-2 signed. Addition at the bit level may produce the same low bits, while the language’s signed and unsigned overflow semantics differ.

In C++, unsigned arithmetic wraps modulo 2N2^N for its type. Signed arithmetic overflow is undefined behavior, not a promised wraparound operation. Mathematical integers have no fixed maximum; Python integers grow with available memory and arithmetic cost. A safe implementation must choose whether overflow is impossible by a proved bound, explicitly checked, saturating by a specified algorithm, or intentionally unsigned modular arithmetic. Merely hoping a large value will not occur is not a contract.

The same eight bits, read two ways. The bit pattern 1010 0110 is A6 in hexadecimal. Read as an unsigned integer, the leading bit is worth +128+128 and the value is 166166. Read in two’s complement, that bit is worth −128-128 and the value is −90-90. The bits are identical; the type decides the meaning.

C. Floating-point numbers represent a scaled significand#

A common IEEE binary32 value has one sign bit, eight exponent bits, and 23 stored fraction bits. A normal finite value has the form (−1)s(1.f)2 2E−127(-1)^s(1.f)_2\,2^{E-127}. Binary64 uses one sign bit, eleven exponent bits, and 52 stored fraction bits with a different exponent bias. Special exponent encodings represent subnormals, infinities, and NaNs. The spacing grows with magnitude, so adding a small number to a very large one can leave the represented value unchanged.

The fraction 1/101/10 has a factor 5 in its reduced denominator, so its binary expansion does not terminate. A finite binary floating-point representation of 0.1 is therefore an approximation. Equality comparisons between independently rounded calculations require thought: use exact comparisons when the representation and contract justify them, and scale-aware tolerances when comparing numerical approximations. NaN is not equal to itself under ordinary IEEE comparisons, so floating-point “equality” is not a mathematical equivalence relation over all bit patterns. A tolerance relation such as ∣a−b∣<ε|a-b|<\varepsilon is generally not transitive either.

Endianness describes the order of bytes in a multibyte representation, not which mathematical value the programmer intended. A network format must specify byte order, widths, signedness, and encoding. Serializing a native C++ object by copying its raw bytes can leak padding and depend on platform layout; C12 instead uses an explicit length prefix.

D. Boolean circuits and instruction execution#

Logic gates realize Boolean operations in a hardware abstraction. A one-bit full adder takes a,b,cina,b,c_{in}, producing

s=a⊕b⊕cin,cout=(a∧b)∨(cin∧(a⊕b)).s=a\oplus b\oplus c_{in},\qquad c_{out}=(a\land b)\lor(c_{in}\land(a\oplus b)).

For a=b=1,cin=0a=b=1,c_{in}=0, the sum bit is zero and the carry is one, representing binary 10. Connecting carry outputs to later bit positions builds a multi-bit addition circuit. Actual processors use more sophisticated arrangements for performance, but the logical input-output relation remains the object to verify.

At a conceptual level, a processor fetches an instruction, decodes it, reads operands, executes an operation, and updates architectural state. Registers hold small amounts of directly accessible state; a program counter identifies instruction flow. Pipelines overlap stages for different instructions. Out-of-order and speculative execution can schedule work before earlier instructions retire while preserving required architectural behavior. Branch prediction attempts to keep work flowing; misprediction can discard speculative work. These mechanisms affect performance and sometimes security, but a C++ source statement is not guaranteed to correspond to one instruction.

E. From one source function to actual generated assembly#

The companion includes this complete source:

code/examples/add.cppC++
// C09: defined modulo-2^32 addition when uint32_t exists.
#include <cstdint>
std::uint32_t add(std::uint32_t a, std::uint32_t b) {
    return a + b;
}

On the recorded Linux x86-64 environment, GCC 14.2.0 with -std=c++20 -O2 -S emitted the central instructions

leal (%rdi,%rsi), %eax
ret

Under this example’s System V calling convention, the arguments arrive through registers whose low 32 bits are used, and the result is returned in eax. Here lea performs arithmetic address calculation without loading memory from that address. This is an observed compiler choice for the tiny function, not a C++ requirement or a promise about Windows calling conventions. The complete emitted assembly is saved in the verification directory.

A typical build preprocesses includes and macros, compiles each translation unit, assembles object code, and links objects with required libraries and symbol definitions. A missing declaration, a type error, an unresolved external symbol, and a runtime fault belong to different stages. Calling conventions determine argument passing, return values, and register responsibilities. Stack frames conceptually preserve call-local state, but optimized code may omit a frame pointer, inline a call, or keep values in registers. C02’s call-stack diagram describes semantic dependencies, not an invariant physical layout under optimization.

F. Locality, caches, and address translation#

A cache stores recently used memory blocks. Temporal locality means reusing data; spatial locality means accessing nearby addresses. A cache line is a transfer unit whose size is hardware-specific; the lab deliberately models 64-byte lines and four direct-mapped slots. An address maps to a block number by integer division by 64, to a slot by block number modulo four, and to a tag distinguishing blocks that share a slot.

For addresses 0,4,64,0,256,00,4,64,0,256,0, accesses to 0 and 4 initially share one line. Address 256 maps to the same slot as zero but a different tag, evicting it. Returning to zero misses again. This model illustrates compulsory and conflict behavior; it is not a model of every real cache’s associativity, prefetching, coherence, or replacement policy.

Virtual memory maps process-visible virtual addresses to physical memory through page tables and translation hardware. A TLB caches translations. A TLB miss is not necessarily a page fault, and a data-cache miss is not a page fault. Pages may be absent, protected, or shared according to the operating system’s mappings. A pointer’s numeric value is not a disk location and does not reveal ownership or lifetime. C10 connects this representation to processes and resource management. [OSTEP]

Multicore execution can run work in parallel, but serial work and coordination limit speedup. If fraction ss remains serial and the remainder divides perfectly across pp workers, ideal speedup is 1/[s+(1−s)/p]1/[s+(1-s)/p]. Real communication, load imbalance, and contention can make it worse. GPUs emphasize large parallel workloads and throughput, with their own execution and memory organization; T02 adds measured data-movement reasoning rather than assuming a GPU always wins.

G. Independent practice#

F foundation C standard course work T transfer S optional stretch L runnable lab or project. Attempt the complete set before opening its solutions.

C09.01FSolution

Decode 11111110 as an unsigned eight-bit integer and as a two’s-complement signed integer. Encode decimal 42 in binary and hexadecimal. Explain why interpretation is part of the contract.

C09.02CSolution

Explain why 0.1 has no finite binary expansion. Why can a == a fail for a floating-point value, and why is “within epsilon” not generally an equivalence relation?

C09.03CSolution

Trace the six-address cache example, recording block, slot, hit/miss, and evictions. State what this simplified model does not establish about the machine the reference run used.

C09.04TSolution

A 100 ms computation has 20 ms of inherently serial work and 80 ms that parallelizes perfectly. Find the ideal runtime with four workers and the infinite-worker speedup limit. Then name three reasons a real measurement could be worse.

C09.05LabSolution

Implement the signed-eight-bit decoder, binary32 bit inspector, and direct-mapped cache simulator in representation_lab.py. Reject values outside the decoder’s domain and invalid addresses. Verify the bits of 1.0, the signed interpretation of 254, and the six-address cache trace. Keep the simulator’s assumptions explicit.

Optional small hints

C09.02: a reduced denominator must divide some power of two to have a finite binary expansion. C09.03: block 4 and block 0 share slot zero.

H. Complete solutions: C09#

Compare the first step where your work differs. A corrected answer is not yet an independent success.

C09.01Show solution

Same pattern, different signed interpretation

The unsigned value is 128+64+32+16+8+4+2=254128+64+32+16+8+4+2=254. The signed two’s-complement value is 254−256=−2254-256=-2. Decimal 42 is 32+8+232+8+2, so its binary representation is 0010101000101010 in eight bits and its hexadecimal representation is 2A. None of these digit strings alone says whether the application expects a signed number, a byte count, or another encoding; the type and protocol provide that meaning.

Back to C09.01
C09.02Show solution

Representability and relation properties

A finite binary fraction is an integer divided by a power of two. Since 1/101/10 in lowest terms has a factor 5 in its denominator, no such finite representation equals it exactly. For NaN, ordinary IEEE equality is false even when comparing the value with itself, violating reflexivity of mathematical equality. A tolerance relation is not transitive: with tolerance 1, values 0 and 0.75 are close, and 0.75 and 1.5 are close, but 0 and 1.5 are not. Approximate comparison is a tool for a specified numerical task, not an automatic replacement for exact equivalence classes or ordered-container comparators.

Back to C09.02
C09.03Show solution

A concrete cache-state trace

The blocks are 0,0,1,0,4,00,0,1,0,4,0 and slots are 0,0,1,0,0,00,0,1,0,0,0. Starting empty, outcomes are miss, hit, miss, hit, miss, miss. The fifth access replaces block 0 with block 4 in slot zero; the sixth replaces block 4 with block 0. Total misses are four. This establishes only the stated direct-mapped model’s behavior. It does not measure the host processor’s real cache hierarchy, prefetcher, associativity, virtual-to-physical mapping, or timing.

Back to C09.03
C09.04Show solution

The serial fraction remains

Ideal runtime is 20+80/4=4020+80/4=40 ms, for speedup 100/40=2.5100/40=2.5. With infinitely many workers, runtime approaches 20 ms and speedup approaches 5. Synchronization overhead, memory-bandwidth saturation, unbalanced work, thread scheduling, cache coherence, or extra data movement can reduce the achieved speedup. The formula assumes the parallel portion itself does not grow with worker count and the original work remains equivalent.

Back to C09.04
C09.05Show solution

Reference representation experiments

code/solutions/representation_lab.pyPython
"""C09.05: explicit representations and a direct-mapped cache simulator."""
import struct

def signed8(bits: int) -> int:
    if type(bits) is not int or not 0 <= bits <= 255:
        raise ValueError("one byte required")
    return bits if bits < 128 else bits - 256

def float32_bits(value: float) -> str:
    raw = struct.unpack(">I", struct.pack(">f", value))[0]
    return f"{raw:032b}"

def cache_misses(addresses: list[int], line_bytes: int = 64,
                 slots: int = 4) -> int:
    if line_bytes <= 0 or slots <= 0:
        raise ValueError("positive geometry required")
    tags: list[int | None] = [None] * slots
    misses = 0
    for address in addresses:
        if type(address) is not int or address < 0:
            raise ValueError("nonnegative integer byte address required")
        block = address // line_bytes
        slot = block % slots
        if tags[slot] != block:
            misses += 1
            tags[slot] = block
    return misses

if __name__ == "__main__":
    print(signed8(254), float32_bits(1.0))
    print(cache_misses([0, 4, 64, 0, 256, 0]))

The signed decoder explicitly applies the u−256u-256 interpretation to the upper half of the byte range. The bit inspector uses an explicit byte order for packing/unpacking, so its result is an encoding observation rather than an unexamined native-memory dump. The cache simulator retains tags for each direct-mapped slot and counts a miss whenever the requested block differs. The tests verify known representations and the predicted trace. The assembly example was separately compiled on the recorded platform; cross-platform compiler behavior is not claimed.

Back to C09.05

I. Retention and readiness#

Retrieve mathematical versus machine integers, function-call semantics, and operation-count versus timing analysis. Advance when you can identify whether a claim belongs to mathematics, language semantics, compiler output, an ABI, or a hardware measurement.