Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Chapter 40: PhantomData, Atomics, and Profiling

Prerequisites

You will understand

  • PhantomData for unused type/lifetime parameters
  • Atomic types and memory ordering
  • Profiling with perf, flamegraph, criterion

Reading time

40 min
+ 25 min exercises
Type Marker

PhantomData Encodes a Relationship Without Runtime Bytes

struct Id<T> raw: u64 _marker: PhantomData<T> same runtime bytes, different type identity and compiler semantics
Ordering Ladder

Atomic Orderings From Weakest to Strongest

Relaxed Acquire Release AcqRel SeqCst counter load fence publish RMW sync default if unsure
Measurement Loop

Profile, Benchmark, Change, Measure Again

Profile hot path Benchmark target Verify correctness Measure again

Step 1 - The Problem

Some systems concerns do not fit neatly into ordinary fields and methods.

You may need a type to behave as if it owns or borrows something it does not physically store. You may need lock-free coordination between threads. You may need measurement discipline so performance claims are based on evidence rather than intuition.

These are different topics, but they share a theme: they are about engineering with invisible structure.

Step 2 - Rust’s Design Decision

Rust provides:

  • PhantomData to express type- or lifetime-level ownership relationships without runtime data
  • atomic types with explicit memory ordering
  • strong tooling for profiling and benchmarking rather than folklore tuning

Rust accepted:

  • memory model complexity for atomics
  • more explicit performance workflow

Rust refused:

  • hiding ordering semantics behind vague “thread-safe” marketing
  • letting type relationships disappear just because the bytes are zero-sized

Step 3 - The Mental Model

Plain English rule:

  • PhantomData tells the compiler about a relationship your fields do not represent directly
  • atomics are for tiny shared state transitions whose ordering rules you must understand
  • performance work starts with measurement, not instinct

Step 4 - Minimal Code Example

#![allow(unused)]
fn main() {
use std::marker::PhantomData;

struct Id<T> {
    raw: u64,
    _marker: PhantomData<T>,
}
}

Step 5 - Line-by-Line Compiler Walkthrough

Id<T> stores only raw, but the compiler still treats T as part of the type identity. PhantomData<T> ensures:

  • variance is computed as if T matters
  • auto traits consider the intended relationship
  • drop checking can reflect ownership or borrowing semantics, depending on the phantom form you use

This is why PhantomData is not “just to silence the compiler.” It carries semantic information for the type system.

Step 6 - Three-Level Explanation

PhantomData is how a type says, “I logically care about this type or lifetime even if I do not store a value of it.”

Use PhantomData for:

  • typed IDs and markers
  • FFI wrappers
  • lifetime-carrying pointer wrappers
  • variance control

Use atomics only when the shared state transition is small enough that you can explain the required ordering in a sentence. Otherwise, prefer a mutex.

There are different phantom patterns with different implications:

  • PhantomData<T> often signals ownership-like relation
  • PhantomData<&'a T> signals borrowed relation
  • PhantomData<fn(T)> and related tricks can influence variance in advanced designs

Atomics expose the memory model explicitly. Relaxed gives atomicity without synchronization. Acquire/Release establish happens-before edges. SeqCst gives the strongest globally ordered model and is often the right starting point when correctness matters more than micro-optimizing ordering.

Atomics and Ordering Decision Rules

OrderingMeaningUse when
Relaxedatomicity onlycounters and statistics not used for synchronization
Acquiresubsequent reads/writes cannot move beforeloading a flag guarding access to published data
Releaseprior reads/writes cannot move afterpublishing data before flipping a flag
AcqRelacquire + release on one operationread-modify-write synchronization
SeqCststrongest total-order modelstart here unless you can prove weaker ordering is enough

The practical rule:

  • if the atomic value is a synchronization edge, not just a statistic, ordering matters
  • if you cannot explain the happens-before relationship clearly, use SeqCst or a lock

Profiling and Benchmarking

Performance engineering workflow:

  1. profile to find hot paths
  2. benchmark targeted changes
  3. verify correctness stayed intact
  4. measure again

Useful tools:

  • cargo flamegraph
  • perf
  • criterion
  • cargo bloat

Criterion matters because naive benchmarking is noisy. It helps with warmup, repeated sampling, and statistical comparison. black_box helps prevent the optimizer from deleting the work you thought you were measuring.

Step 7 - Common Misconceptions

Wrong model 1: “PhantomData is just a compiler pacifier.”

Correction: it affects variance, drop checking, and auto trait behavior.

Wrong model 2: “Atomics are faster mutexes.”

Correction: atomics trade API simplicity for low-level ordering responsibility.

Wrong model 3: “Relaxed is fine for most things.”

Correction: only if the value is not part of synchronization logic.

Wrong model 4: “If a benchmark got faster once, the optimization is real.”

Correction: measurement needs repeatability, noise control, and representative workloads.

Step 8 - Real-World Pattern

You will see:

  • PhantomData in typed wrappers, pointer abstractions, and unsafe internals
  • atomics in schedulers, refcounts, and coordination flags
  • benchmarking and profiling integrated into crate maintenance, especially for parsers, runtimes, and data structures

Strong Rust projects treat performance like testing: as an engineering loop, not an anecdote.

Step 9 - Practice Block

Code Exercise

Create a typed UserId and OrderId wrapper over u64 using PhantomData, then explain why mixing them is impossible.

Code Reading Drill

What is this counter safe for, and what is it not safe for?

#![allow(unused)]
fn main() {
use std::sync::atomic::{AtomicUsize, Ordering};

static REQUESTS: AtomicUsize = AtomicUsize::new(0);

REQUESTS.fetch_add(1, Ordering::Relaxed);
}

Spot the Bug

Why is this likely wrong?

#![allow(unused)]
fn main() {
READY.store(true, Ordering::Relaxed);
if READY.load(Ordering::Relaxed) {
    use_published_data();
}
}

Assume READY is meant to publish other shared data.

Refactoring Drill

Replace an atomic-heavy state machine with a mutex-based one and explain what complexity disappeared and what throughput tradeoff you accepted.

Compiler Error Interpretation

If a wrapper type unexpectedly ends up Send or Sync when it should not, translate that as: “my phantom relationship may not be modeling ownership or borrowing the way I thought.”

Step 10 - Contribution Connection

After this chapter, you can read:

  • typed wrapper internals using PhantomData
  • lock-free counters and flags
  • profiling and benchmark harnesses
  • binary-size investigations

Good first PRs include:

  • replacing overly weak atomic orderings with justified ones
  • adding criterion benchmarks for hot paths
  • documenting why a phantom marker exists and what invariant it encodes

In Plain English

Some of the most important facts about a system do not show up as ordinary fields. A type may logically own something it does not store directly, a flag may synchronize threads, and performance may depend on details you cannot guess from reading code casually. Rust gives you tools for these invisible relationships, but it expects you to use them precisely.

What Invariant Is Rust Protecting Here?

Type-level relationships, cross-thread visibility, and performance claims must all reflect reality rather than assumption: phantom markers must describe real semantics, atomics must establish real ordering, and optimizations must be measured rather than imagined.

If You Remember Only 3 Things

  • PhantomData communicates semantic relationships to the type system even when no runtime field exists.
  • Atomics are for carefully reasoned state transitions, not as a default replacement for locks.
  • Profile first, benchmark second, optimize third.

Memory Hook

PhantomData is the invisible wiring diagram behind the wall. Atomics are the circuit breakers. Profiling is the voltage meter. None of them matter until something goes wrong, and then they matter a lot.

Flashcard Deck

QuestionAnswer
What is PhantomData for?Encoding type or lifetime relationships that are semantically real but not stored as runtime data.
Why can PhantomData<&'a T> matter differently from PhantomData<T>?It communicates a borrowed relationship rather than an owned one, affecting variance and drop checking.
When is Ordering::Relaxed appropriate?For atomicity-only use cases like statistics that do not synchronize other memory.
What do Acquire and Release establish together?A happens-before relationship across threads.
What ordering should you start with if unsure?SeqCst, or a mutex if the design is complicated.
Why use criterion instead of a naive loop and timer?It provides better statistical benchmarking discipline.
What does cargo flamegraph help reveal?CPU hot paths in real execution.
What is a sign you should use a mutex instead of atomics?You cannot explain the required synchronization edge simply and precisely.

Chapter Cheat Sheet

ProblemToolWhy
Semantic type marker with no dataPhantomDataencode invariant in type system
Publish data with flagAcquire/Release or strongerestablish visibility ordering
Pure counter metricRelaxed atomicatomicity without synchronization
Complex shared statemutex or lockeasier invariants
Measure CPU hot pathflamegraph/perfevidence before tuning