Chapter 02 · Week 2

Not All Cores Are Equal

Why does a body have many members, each with different work?

Chapter 2 — Not All Cores Are Equal

“The fastest core is the one you did not have to wake.” — an industry adage

“If the whole body were an eye, where would be the sense of hearing? If the whole body were an ear, where would be the sense of smell?” — 1 Corinthians 12:17 (ESV)


Why This Matters

Last week you took a system-on-chip apart and learned to read it as a block diagram: clusters, GPU, NPU, DSP, ISP, modem, memory controller, secure subsystem, all sharing one interconnect and one power budget. You met The Four Questionshow fast, at what energy, for how long before it throttles, and on which processor — and started your soc-architecture-review.docx. This week you go into the largest, most misunderstood block on that diagram: the CPU.

Here is the fact that should bother you. Open the spec sheet for almost any phone shipped in the last decade and it says “octa-core.” A desktop reader parses that as eight of a thing. It is not. It is a deliberately unequal collection — two or three distinct microarchitectures executing the same instruction set, differing in pipeline depth, issue width, reorder capacity, cache and area by factors that are not small. One of those cores may occupy five times the silicon of another and burn nearly ten times the power at its top operating point. Counting to eight is the most common way a competent engineer misreads a mobile CPU.

This is not a compromise. It is the opposite of one. A desktop architect designs one core and asks the operating system to run everything on it, because a wall socket forgives a bad match between workload and machine. A mobile architect cannot, so instead of one core that is mediocre at two jobs they build two that are each excellent at one — and hand the operating system the much harder problem of deciding, thousands of times a second, which job goes where. Heterogeneity moves the difficulty from the silicon to the scheduler, on purpose.

The Four Questions run right through it. Performance: a scroll frame that misses its deadline is a visible stutter, and no amount of average throughput repairs it. Energy: the same work costs wildly different joules depending on where you put it, and the difference is not a rounding error. Thermals: Chapter 5 shows that placement and frequency are one control problem, not two. Placement: this week the fourth question stops being an abstraction and becomes a table you build, defend, and hand in.

And there is a Christian question underneath the engineering that fits this material almost embarrassingly well. The epigraph is Paul writing to a congregation that had begun ranking its members, and the cheap version of that analogy — big cores and little cores, just like the body — is not worth your time. The version worth your time is the harder argument Paul is actually making: the members are not ranked, and a body that treats one member as the lesser member is not humble, it is broken. Hold that thought. By the end of this chapter you will have seen the engineering consequence of the ranking error, and it is a scheduler that places badly and a benchmark that lies.


2.1 — Why One Core Cannot Serve Both a Scroll and a Sync

Start with two real workloads that a phone runs constantly, and notice that they have almost nothing in common except that both are “CPU work.”

Workload A — a frame while the user scrolls. The display refreshes at a fixed rate, and that rate is an absolute wall: at 60 Hz you have 1000 / 60 = 16.7 ms to produce a frame; at 120 Hz, 1000 / 120 = 8.3 ms. That is arithmetic, not an opinion. Your application does not get the whole budget either — the compositor, the display pipeline and the system’s own work take a share. The work is short, bursty, latency-critical, hard to parallelize (much of it is one dependent chain of layout and drawing), and it repeats at the refresh rate for as long as the user’s thumb is moving. Miss the deadline once and a human sees it. No partial credit.

Workload B — the photo library indexing in the background. No deadline anyone can perceive; it must finish before the user next opens the gallery, perhaps an hour from now. It is long, throughput-bound, embarrassingly parallel, and — this is what matters — it runs for seconds or minutes, so every joule it spends is a joule the battery notices.

Now design one core for both.

Optimize for Workload A and you build a wide, deep, aggressively speculative core with a large private cache and a high top frequency, because single-thread latency on a dependent chain is the only thing that saves the frame. That core is superb at scrolling and catastrophic at Workload B: it runs flat out for ninety seconds to index photos, spending several times the necessary energy on a task with no deadline at all. Optimize for Workload B instead and you build a narrow, shallow, modestly-clocked core with a small cache and excellent performance-per-watt — which indexes photos beautifully and drops frames when the user scrolls.

A single compromise core is worse at both than either specialist. So the mobile answer is: build both, put them on the same die, give them the same instruction set so a thread can move between them, and make placement a first-class problem.

Coach’s Note — Notice what just happened to the word “fast.” Workload A wants the lowest latency on a dependent chain; Workload B wants the lowest joules per unit of work. A core that maximizes one is structurally bad at the other. So when someone asks which core is faster, the professional answer is a question: fast at what, under what deadline, and paid for how? That is not pedantry. It is the whole discipline.


2.2 — Same Instruction Set, Different Machine

The cores in a heterogeneous cluster must implement the same instruction set. That is not a convenience, it is the enabling condition: it lets the operating system take a thread mid-execution — live register file, live stack, live program counter — and resume it on a completely different core without the program noticing. The architectural state is identical; only the implementation differs.

Here is what differs, and why each difference costs or saves energy.

AxisPerformance-class coreEfficiency-class coreWhy it matters
Issue widthWiderNarrowerWidth costs area and power superlinearly for sublinear gain
Pipeline depthDeeper, to reach a higher clockShallowerDepth raises the mispredict penalty and the voltage the clock needs
Execution orderAggressively out-of-order, large reorder windowHistorically in-order; more recent ones modestly out-of-orderReordering hardware is expensive in area and leakage
Branch predictionLarge, sophisticatedSmaller, simplerEach wrong prediction discards real work — energy for nothing
Private cacheLarger L1 and L2SmallerCache is area, and area is leakage; Chapter 3 owns this
Top frequencyHigherLowerA higher sustained clock needs a higher voltage, and power tracks V²
AreaSeveral times larger per coreThe unitYou could have had several efficiency cores instead

Two of those rows deserve more than a line.

Speculation is a bet paid in joules. A deep out-of-order core keeps hundreds of instructions in flight, guessing which way branches will go so the front end never starves. Guess right and the work is free. Guess wrong and everything downstream is thrown away and the pipeline refills — typically on the order of ten to twenty cycles on a deep core, though the penalty depends entirely on the microarchitecture. On a plugged-in machine that is a performance story. On a phone it is also an energy story: you fetched, decoded, renamed, scheduled and executed instructions whose results you discarded. Full price, nothing delivered. That is precisely why efficiency cores are shallower and less speculative — not because their designers were less ambitious, but because on that side of the die a wrong guess is not affordable.

Cache is not free just because it is fast. A larger private L2 buys hit rate, and hit rate buys both time and energy (Chapter 3 gives you the energy ratio between an on-chip access and a DRAM access, and it is not a factor of two). But SRAM leaks, used or not. An efficiency core with a small L2 chooses lower leakage over higher hit rate — defensible for a core expected to be awake a great deal of the time doing very little.

For concrete sizes the honest statement is a range, not a number. In 2024–2025-generation Arm application processors, L1 instruction and data caches are typically 32–128 KB per core, private L2 typically 256 KB to a few megabytes per core or cluster, and a shared L3 behind the cluster’s shared unit typically low single-digit to low double-digit megabytes. They are ranges because they genuinely vary by vendor, tier and generation. For a specific part you read that vendor’s own specification — which brings us to a rule you will use for the rest of this course.

Coach’s Note — A language model will hand you a cache size: a specific number, confidently formatted, for a specific part — and possibly invented, because “512 KB L2” is exactly the kind of plausible token sequence these systems produce whether or not it is true. This course’s rule is in Appendix D and it is short: use AI to explain, never to source. Ask it what a reorder buffer does and it is a fine tutor. Ask how large the reorder buffer is on a named core and you have asked the wrong question of the wrong tool. Every figure in a submitted report needs a primary source — a vendor specification page, an architecture reference manual, or your own measurement. A fabricated number is not a rubric deduction; it is an integrity failure.


2.3 — From big.LITTLE to DynamIQ

The idea has a history, and knowing it will keep you from misreading a modern block diagram.

big.LITTLE, introduced in 2011, paired a high-performance core cluster with a high-efficiency one. Getting work onto the right cluster went through three successive operating models: cluster migration (the whole system runs on one cluster or the other — coarse and hopeless for a mixed workload); the in-kernel switcher, or core pairing (each big core paired with a little one, the pair appearing to the OS as a single virtual core — better granularity, but you still cannot use a big and a little core at once); and global task scheduling, also called heterogeneous multiprocessing (HMP), in which every core is simultaneously visible to the scheduler and each thread is placed individually.

HMP won completely, for one reason: it is the only model in which the scheduler answers the placement question per thread. Under HMP your foreground frame work sits on a performance core at the same moment a background sync sits on an efficiency core — §2.1’s situation exactly. Every heterogeneous system you will meet today is HMP; when someone says “big.LITTLE,” they almost always mean “HMP on an asymmetric CPU.”

DynamIQ, announced in 2017, changed the packaging. Instead of separate big and little clusters wired together, unlike core types live inside a single DynamIQ cluster behind a DynamIQ Shared Unit (DSU). The DSU is the piece to remember, because it provides three things at once:

  • The shared L3 for the whole cluster — which is why a thread migrating between cores in the same cluster does not lose everything (see §2.5).
  • The coherency point, where the snoop filter lives and coherence between unlike cores is actually maintained.
  • Per-core power and frequency domains — the quiet revolution. Individual cores can be clocked and gated independently rather than a whole cluster at a time, which is what makes fine-grained configurations practical instead of theoretical.

A typical 2024–2025-generation Arm flagship phone CPU is commonly one prime core, three to five performance cores, and two to four efficiency cores, often totalling eight — but treat that as the common shape and not a rule, because some 2024-generation flagships shipped all-large configurations with no small cores at all.

Sit with that rather than explaining it away. It does not refute the chapter; it sharpens it. A wide core clocked low is not the same machine as a narrow core clocked low — more cache, more reorder capacity, more leakage — and which wins for a given workload mix on a given process is a measurement, not a slogan. Vendors have published different rationales and are not all making the same bet. The professional posture is to hold the tradeoff, name what would settle it, and go measure. That posture is the entire content of Chapter 8.


2.4 — Coherence Between Unlike Cores

Migration is only legal because coherence is guaranteed. If a thread’s data sat in one core’s private cache and the thread resumed on another core that could not see it, the program would observe stale memory and the model would collapse. Before asking what migration costs, be clear about what makes it correct.

The teaching model is MESI — each cache line, in each cache, is Modified, Exclusive, Shared, or Invalid — and its common extension MOESI, which adds an Owned state so a dirty line can be shared without first being written back to memory. The rules are familiar: a core that wants to write must gain exclusive ownership, invalidating every other copy; a core reading a line another core has modified gets the up-to-date data from that core rather than from DRAM.

The implementation on an Arm system is not MESI-the-textbook-diagram. It is Arm’s coherent interconnect protocols — AMBA ACE and, for larger systems, CHI — with a snoop filter at the coherency point (the DSU for a DynamIQ cluster) tracking which caches might hold which lines, so the interconnect need not broadcast every request to everyone. Know both layers: MESI/MOESI is the model you reason with, ACE/CHI is what is on the die.

Two consequences belong to this chapter.

Coherence costs bandwidth, and bandwidth is shared. Every invalidate, snoop, and cache-to-cache transfer is interconnect traffic the GPU, NPU, display controller and memory controller are all competing for. It is a real cost of running threads on multiple cores, and one reason parallel speedup falls short of Amdahl’s optimistic curve. Chapter 3 takes the memory hierarchy apart and Chapter 6 the interconnect; for now hold the shape: coherence is not free, and it is paid for in the one resource everything else also needs.

False sharing is the version that will actually bite you. Coherence operates on cache lines, not variables. The line is 64 bytes on essentially all Arm application processors and on x86 — though some Apple silicon designs use 128-byte lines, checkable with sysctl hw.cachelinesize. If two threads on two cores repeatedly write two different variables that happen to share a line, every write by one invalidates the other’s copy and the line ping-pongs across the interconnect at full coherence cost. The program is correct, the variables are genuinely independent, and your parallel version is slower than your serial one. Padding them onto separate lines fixes it entirely.

Coach’s Note — False sharing is the most humiliating bug in parallel programming, because nothing in your source code is wrong. You have to reason at the level of the line, not the variable — and the moment you can, you have started thinking like an architect instead of a programmer. Keep it in your pocket; it explains a startling share of “why didn’t my threads help?“


2.5 — Migration Is Not Free

Now the cost. When the scheduler moves a thread from one core to another, the thread arrives at a machine that knows nothing about it:

  • Private L1 and L2 are cold, and so are the branch predictors and prefetchers. Everything the thread painstakingly pulled in stays behind; the new core must refetch the data and relearn the behaviour — a burst of misses and mispredictions right after the move.
  • The shared L3 survives, if the destination sits in the same DynamIQ cluster behind the same DSU. This is why migration inside a cluster is much cheaper than migration between clusters that do not share a last-level cache.
  • The scheduler itself pays, in run-queue work and an inter-processor interrupt — and the destination core may need to ramp its frequency, which is not instant. Chapter 5 owns that part.

Which is exactly why real schedulers damp migration with hysteresis instead of chasing the theoretically optimal placement every millisecond. Migrate too eagerly and you spend more moving threads than you save by placing them well.

You do not have to take this on faith. Build and run code/migrate.cpp:

g++ -O2 -std=c++17 -pthread migrate.cpp -o migrate
./migrate                      # defaults: 1024 KiB working set, 40 rounds
./migrate --kib 256 --rounds 60

Its kernel is a dependent chase through a random cycle, one node per cache line, so it is latency-bound: no prefetcher can hide a miss and a cold cache shows up directly in the number. It reports four things:

ModeWhat it measures
warm (same core)Rounds back to back on one thread — caches stay hot
cold (evicted)Same thread, each round preceded by a sweep over a buffer larger than the last-level cache. The control experiment: pure cache-warmth cost, no threading
migrating (inner)Each round on a freshly spawned thread — timed inside the worker, kernel only
migrating (outer)The same round timed on the main thread, spawn and join included

One run on my own laptop, at a 256 KiB working set:

mode                     median us      IQR us   ns/access
----------------------------------------------------------
warm (same core)             12.29        0.21        3.00
cold (evicted)               56.17        5.46       13.71
migrating (inner)            12.50        0.21        3.05
migrating (outer)            45.58       10.21       11.13

cache-warmth cost (cold - warm)  :    +43.88 us   (4.57x warm)
migration cost    (inner - warm) :     +0.21 us   (1.02x warm)
spawn + join      (outer - inner):    +33.08 us

That is a laptop with no affinity control — which is exactly why the migration cost came out at zero: the freshly spawned thread was simply put back on the same core. Read the program’s second line before its table.

Your numbers will not match mine and do not need to — Appendix C tells you how to report yours honestly. But three structural facts should hold on any machine, and they are the real lesson:

  1. cold is materially slower than warm. That gap is the price of a cold private cache, and the floor under what a migration costs.
  2. outer - inner is thread-creation overhead, not migration cost. Report their sum as “the cost of migration” and you have published a mixture and called it a measurement. Chapter 8 grades exactly this distinction; start practising now.
  3. migrating (inner) should land between warm and cold where the thread genuinely moves, because the shared last-level cache survives the move even though the private caches do not — §2.4’s DSU showing up in a number you produced yourself. Where affinity is unavailable it will instead look just like warm, as above. That is a result, not a failed experiment; the cold row is your platform-independent control. Report what your machine did and say why.

Coach’s Note — Run ./migrate --kib 64 and then ./migrate --kib 4096 and watch the ratio collapse. When the working set is small enough to live in the private caches, losing them is devastating. When it was already too big to fit, migration costs almost nothing — you were missing anyway. That sweep tells you which threads a scheduler may shuffle freely and which it should leave alone, and it is worth far more than any single number the tool prints.


2.6 — Telling the Scheduler What You Want

So who decides placement, and how much say do you get? The answer differs by platform in one of the best teaching contrasts in this course: the two dominant mobile platforms made opposite decisions and both are defensible.

Linux and Android: name the core

Linux lets a process state, in hardware terms, exactly which CPUs it may run on. The system call is sched_setaffinity (and pthread_setaffinity_np per thread); the command-line front end is taskset:

nproc                                   # how many logical CPUs the OS sees
taskset -c 0 ./migrate                  # run pinned to CPU 0
taskset -c 4-7 ./migrate --kib 256      # restrict to CPUs 4 through 7
taskset -pc 1234                        # show a running PID's affinity mask

On an arm64 Linux system the kernel exports a normalized per-CPU capacity value — the cleanest way to see asymmetry from userspace:

# arm64 only — this file will not exist on an x86 laptop.
cat /sys/devices/system/cpu/cpu*/cpu_capacity
# Per-CPU frequency limits exist much more widely:
cat /sys/devices/system/cpu/cpu*/cpufreq/cpuinfo_max_freq

Above raw affinity, the Linux and Android stack has a layered scheduling vocabulary you should be able to name:

  • The fair schedulerCFS historically, mainline having moved to EEVDF in recent kernels — decides when a runnable task runs. Energy Aware Scheduling (EAS) decides where, using a per-platform energy model describing what each core class costs at each operating point, so a task lands where it costs the fewest joules for the performance required. That is the kernel doing, in software, the arithmetic code/placement_model.py is about to make you do by hand.
  • PELT (Per-Entity Load Tracking) estimates a task’s utilization over time; some vendor kernels have historically used an alternative called WALT with different responsiveness characteristics.
  • uclamp (utilization clamping) puts a floor or ceiling on a task’s perceived utilization — “treat this as busier than it looks,” or “never let this look busy enough to justify a big core.” cpusets partition tasks into groups; Android uses top-app, foreground, and background to keep a sync out of the way of what the user is looking at.

Apple platforms: name the intent

Apple platforms do not expose CPU affinity. There is no supported way to say “run this thread on that core.” Instead you declare a Quality of Service class and the system decides placement:

QoS classWhat you are asserting
QOS_CLASS_USER_INTERACTIVEThe user is waiting and watching; this is on the critical path of a frame
QOS_CLASS_USER_INITIATEDThe user asked for it and is waiting for a result
QOS_CLASS_UTILITYLong-running work with a progress indicator; the user is not blocked
QOS_CLASS_BACKGROUNDThe user does not know this is happening

You can still observe the asymmetry; you just cannot command it:

sysctl hw.nperflevels                   # how many performance levels this SoC has
sysctl hw.perflevel0.name  hw.perflevel0.physicalcpu
sysctl hw.perflevel1.name  hw.perflevel1.physicalcpu
sysctl hw.cachelinesize

Read the name fields rather than assuming what the levels are called — the naming is vendor-specific and has changed across generations.

Which one is right?

Both, and the argument is the one you will meet in every hardware/software contract in this book. Affinity is indispensable for measurement: to compare two core classes or reproduce a result tomorrow you must control placement, or the scheduler is a hidden variable in every number you publish. Chapter 8 makes that a graded requirement. But an affinity mask is a claim about a specific hardware topology, baked into software that will outlive it. Ship an app pinned to “CPU 6 and 7 because those are the big ones,” run it on next year’s part where the numbering changed, and you have shipped a bug that only manifests on hardware that did not exist when you wrote it. A QoS class survives the hardware because it describes the workload, not the machine — and it lets the platform apply information the application does not have: thermal state, what else is running, what the user is doing.

Coach’s Note — The rule I would give a team: express intent in shipping code, take control only in measurement code. A hard-coded CPU number in a production path is almost always a bug wearing an optimization costume. A benchmark harness that does not control placement is a measurement with an uncontrolled variable. Different jobs, opposite defaults. Knowing which job you are doing is the skill.


2.7 — DVFS: The Scheduler’s Other Lever

Placement is one of two levers, and it would be dishonest to leave you thinking it is the only one. The other is dynamic voltage and frequency scaling (DVFS): the platform moves each core along a discrete set of voltage-and-frequency operating points based on how busy it is. Linux exposes this as cpufreq with governors, the modern one being schedutil — so named because it is driven by the scheduler’s own utilization signal rather than a separate sampling loop.

Why this belongs in a chapter about cores: a core class is not a point, it is a curve. The performance core in code/core-profiles.csv is described at exactly one operating point — its maximum — but it can run at many, and at a low one it is a genuinely different proposition. Dynamic power goes roughly as α · C · V² · f, and because sustaining a higher frequency generally requires a higher voltage, power rises roughly as the cube of frequency across the range where voltage must track frequency. State that as the approximation it is, and name the assumption when you use it: the last slice of clock speed can cost far more than its share of the power.

So the honest statement of the placement problem is two-dimensional — which core, at which operating point — solved jointly by the scheduler and the governor, which is exactly why schedutil and EAS are designed to talk to each other.

This chapter deliberately holds the frequency axis fixed so you can learn placement cleanly first. Say so in your report: “this model evaluates one operating point per class” is a limitation you state, not one you hope nobody notices. Chapter 5 opens the second axis, adds thermals, and re-reads this entire chapter through the energy lens. Some of what looks obvious this week will look different after it.


2.8 — Three Different Numbers: Single-Thread, Multicore, Sustained

“How fast is this CPU” is three questions with three different answers, and conflating them is how marketing happens.

Single-thread performance is what the frame in §2.1 needs. It is set by the best core’s IPC and frequency and nothing else — seven idle cores contribute exactly zero to a dependent chain. It governs whether the UI feels good.

Multicore performance is what the photo indexer needs, and it obeys Amdahl’s law: if a fraction p of the work parallelizes and the rest is serial,

speedup(p, n) = 1 / ((1 - p) + p / n)

Run code/amdahl.py and read the table before you go on:

python3 amdahl.py
Amdahl speedup — rows are the parallel fraction p, columns are core count n
     p        1        2        4        8       16   ceiling
-------------------------------------------------------------
  0.50     1.00     1.33     1.60     1.78     1.88       2.0
  0.75     1.00     1.60     2.29     2.91     3.37       4.0
  0.90     1.00     1.82     3.08     4.71     6.40      10.0
  0.95     1.00     1.90     3.48     5.93     9.14      20.0
  0.99     1.00     1.98     3.88     7.48    13.91     100.0

At p = 0.90 — a very parallel program — eight cores buy 4.71× and infinite cores buy 10×. The serial 10% is a wall you cannot spend your way through. This is the arithmetic that kills “more cores is more fast.”

But on a heterogeneous CPU there is a second correction, and it is the one this chapter exists to teach. Amdahl’s n assumes n equal cores. Yours are not equal. Run:

python3 amdahl.py --hetero 1,3,4 --p 0.95
Heterogeneous correction — a cluster of unlike cores is not n equal cores
core_class      count   rel. throughput  contribution
-----------------------------------------------------
prime               1             1.000         1.000
performance         3             0.732         2.197
efficiency          4             0.289         1.154
-----------------------------------------------------
TOTAL               8                           4.351   (prime-equivalent cores)

You bought 8 physical cores and 4.35 prime-equivalent ones.
At p = 0.95:
  Amdahl on the physical count   (8 cores) : 5.93x
  Amdahl on the equivalent count (4.35 cores) : 3.73x

Eight physical cores are worth about 4.35 prime-equivalent ones under this model’s ratios, and the speedup is 3.73× rather than 5.93×. That gap is the difference between the core count on the box and the machine you actually have. Note what the tool says about itself: neither p nor the throughput ratios are measurements. They are model inputs, and yours have to be measured — which is Project 2.

Sustained performance is the third number, and this chapter cannot give it to you. Both numbers above assume the cores can hold their operating points indefinitely. They cannot. A phone has no fan. Chapter 5 is where “how fast is it” acquires the phrase “for how long,” and where you learn that a peak score is a true number about a false situation.


2.9 — Performance-per-Watt: The Metric That Decides the Design

Here is the metric that settles arguments in a mobile CPU design review, and the rule that follows from it.

First, the units. For a fixed task the correct efficiency metric is joules per task — energy, not power. E = P × t, so a low-power core that takes four times as long is not automatically cheaper. For a continuous stream the metric is performance-per-watt. Two views of the same thing; move between them without confusion.

Open code/core-profiles.csv. It is synthetic teaching data — its first line says so, because Week 8 grades you on telling modeled data from measured data. Every value is a ratio chosen to reproduce the shape of a published heterogeneous design, not copied from a datasheet. Do the arithmetic yourself before running anything; this is the most useful arithmetic in the chapter:

Core classRelative throughput (IPC × GHz)Relative powerRelative perf-per-wattRelative area
prime6.939.600.725.20
performance5.085.400.943.10
efficiency2.001.002.001.00

Read the two ratios that matter. The prime core delivers about 3.5× the throughput of the efficiency core — at about 0.36× the performance-per-watt, meaning roughly 2.8× the energy per instruction retired, on 5.2× the silicon area. The design review writes itself: you could have had five efficiency cores for the area of one prime core, delivering about 10 units of throughput to the prime core’s 6.93, at roughly half the power.

So why build the prime core at all? Because §2.1’s frame does not care about aggregate throughput, and five efficiency cores cannot make one dependent chain finish in 8.3 ms. You buy the prime core to satisfy a latency deadline no amount of parallelism can satisfy — you pay in area, power and leakage, and you keep it asleep as much as you possibly can. That is the whole bargain, and it is why the adage at the top of this chapter is not a joke.

Which gives the rule. Not “use the fastest core,” not “use the most efficient core”:

Place work on the cheapest core that meets the deadline.

That sentence puts both objectives in the right order — the deadline is a constraint, energy is what you minimize subject to it — and it is what code/placement_model.py computes:

python3 placement_model.py --instructions 24e6 --deadline-ms 8.3
core_class           ms    core mJ   total mJ   perf/W  deadline
----------------------------------------------------------------
prime             3.463      8.312      8.312    2.888  MET
performance       4.729      6.384      6.384    3.759  MET
efficiency       12.000      3.000      3.000    8.000  MISS

CHEAPEST CORE THAT MEETS THE DEADLINE: performance (4.729 ms, 6.384 mJ total)
The fastest core (prime) would finish in 3.463 ms and spend 1.30x the energy
for a deadline that was already met.

That is a frame’s worth of work at 120 Hz. The efficiency core is the most efficient thing on the die and it is the wrong answer, because it misses. The prime core meets the deadline and wastes 30% of the energy doing it. The performance core is right. Neither “fastest” nor “most efficient” would have found that — only the rule did.

Now run the background indexer and watch the answer move:

python3 placement_model.py --instructions 1.2e9 --deadline-ms 60000

Everything meets a 60-second deadline, so the cheapest wins outright: the efficiency core at 150 mJ against the prime core’s 416 mJ — 2.8× the energy for a deadline that was never in danger.

And now the wrinkle that will haunt you until Chapter 5:

python3 placement_model.py --instructions 1.2e9 --deadline-ms 60000 --platform-mw 1000

--platform-mw charges a fixed platform draw — display, radios, the rest of the SoC — for the whole run, because if the screen is on you pay for it whether the CPU crawls or sprints. Set it high enough and the ranking inverts: the efficiency core’s 600 ms now costs more total energy than the performance core’s 236 ms, even though it is far better per instruction. Finishing sooner stopped paying the platform bill sooner.

I am not going to hand you a number for a real platform floor; it depends on the device, the display, the radios, and the moment. That is a measurement, and refusing to invent it is the discipline. Take the shape: the efficiency ordering of core classes is not absolute — it depends on what else is awake. Chapter 5 turns this into race-to-idle and tells you honestly when it wins and when it loses.

Coach’s Note — Get used to answering some placement questions with “none of the above.” Where should an on-device language model’s token generation run? Not on a CPU core at all — on the NPU, for reasons that are mostly about memory traffic rather than arithmetic. Where should always-on wake-word detection run? Not on an application core — on the sensor hub. The Four Questions include “which processor,” and sometimes the answer is a block this chapter does not cover. Chapter 4 takes the accelerator fleet and Chapter 6 the always-on processor. Your placement table this week is CPU-only on purpose; Chapter 4 makes you widen it.


2.10 — Interactive Lab: The Core Placement Bench

Below this chapter on the website you will find The Core Placement Bench, an interactive panel that lets you run the placement decision far more times than you could by hand. It is part of the chapter, not an extra. Use it now, before the reps.

The bench gives you a workload — UI frame, background sync, sustained compute, or burst compute — with controls for its deadline and duration. You assign it to a prime, performance, or efficiency core, and it returns the completion time, the joules, whether the deadline was met, and why — plus what a real OS scheduler would likely have done, which is frequently not what you chose.

Work it in this order, and do not skip the first step:

  1. Predict before you click. For each workload write down which core you think is right and, more importantly, the rule you used. If your rule is “the fast one for the important thing,” you are about to be corrected. That is the exercise.
  2. Run all four workloads on all three classes. Twelve assignments. Watch the joules column, not just the deadline column, until a met deadline at 3× the energy reads as a bad answer rather than a safe one.
  3. Find the trap. One scenario is built so the obviously-fast choice is wrong on energy, and wrong the way real systems are wrong — the deadline was already met, comfortably, by a cheaper core. Then vary the duration and watch the ranking move.

In one line: placement is a constrained optimization, not a preference. The deadline is the constraint; the joules are the objective. Get that ordering into your hands here, where being wrong costs nothing.

A short “Check Your Reps” quiz sits below the lab. It is an ungraded self-check, covering what HMP means, what a migration costs, why affinity is not exposed everywhere, performance-per-watt against peak, and what the DSU provides. Take it before you move on. Then sit this week’s graded knowledge check in Canvas — worth 1.5%, same material, larger pool, randomized draw, so retaking it is real practice rather than memorization.


2.11 — Many Members, One Body

The temptation with 1 Corinthians 12 here is to reach for the easy correspondence — big cores and little cores, eyes and ears, isn’t that neat — and move on feeling Scripture has been honored. It has not. That reading takes the passage’s image and discards its argument, and the argument is what will make you a better engineer.

Paul is writing to a congregation that has begun ranking its members. Some gifts are visible and impressive, others quiet and unglamorous, and Corinth has drawn the obvious conclusion that the impressive ones are the important ones. Paul’s response is not “everyone matters in their own way.” It is sharper: a body made entirely of the impressive member would not be a superior body — it would not be a body at all. “If the whole body were an eye, where would be the sense of hearing? If the whole body were an ear, where would be the sense of smell?” (1 Corinthians 12:17, ESV). And then he goes further than comfort allows, saying the members which seem weaker are indispensable, and the parts given less honor are given greater honor.

Notice what that does to the ranking. It does not soften it. It inverts it, and then dissolves it, because ranking was the wrong operation to perform on a body in the first place. The parts are not competing on one scale of value. They have different offices, and the body’s health is each part doing its own work — not every part being as much like the eye as it can manage.

Hold that against a heterogeneous CPU, because the correspondence is structural rather than decorative, and it has a bug attached. The efficiency core is not a worse prime core, and not a prime core the vendor could not afford. It holds a different office: work that must happen constantly, cheaply and invisibly, for hours, on a battery. Measured on the eye’s scale — peak single-thread throughput — it looks like a failure. Measured on its own office’s scale — joules per unit of work done while nobody is watching — it is roughly 2.8× better than the prime core, and it is why the device survives the night. Those are not two opinions about one ranking. They are two different questions, and the second is the one the device’s usefulness depends on.

Here is why that gets a section rather than a sentence: systems built by people who believe the efficiency core is the lesser core schedule badly. The engineer who has internalized “big core = good” writes a uclamp floor forcing background work onto performance cores, and ships worse battery life with no user-visible gain. The benchmark suite that reports only peak single-thread throughput measures the eye and calls it the body — and the device that scores best on it may be the one that throttles first and lasts least.

Each is the Corinthian error committed against a die instead of a congregation: the honor goes to the impressive member, and the body degrades. The scheduler that gets this right is not being egalitarian. It is being accurate — reading each core’s actual office and placing work accordingly. And accuracy about what a thing is for turns out to be nearly indistinguishable, in practice, from a certain kind of respect for it.

There is a vocational point underneath that the Lutheran tradition presses harder than most, worth naming because you are being trained for a profession. A calling is not made worthy by being visible. The unglamorous office — the one nobody photographs, the one that runs while everyone sleeps — is not a lesser participation in the work; it is the work, done in its own place. Most of what you build in a career will be efficiency-core work: correct, cheap, constant, never complimented. Learn now to measure it on its own scale.


2.12 — Common Pitfalls

Pitfall: Reading “eight cores” as eight of the same thing. Example: A capacity estimate assumes eight equal cores and projects a 5.9× speedup at p = 0.95; the device delivers under 4×. Fix: Convert to equivalent cores before any Amdahl arithmetic — python3 amdahl.py --hetero 1,3,4 --p 0.95 — and say plainly that the ratios are model inputs, measured or assumed.


Pitfall: Benchmarking on whichever core the scheduler happened to pick. Example: Two runs of the same kernel differ by 2.4×; the “optimization” between them changed nothing. One landed on a performance core, the other on an efficiency core. Fix: Control placement in measurement code (taskset -c 0 ./bench) — or, where affinity is unavailable, run enough repetitions to see the bimodal distribution and report it as bimodal.


Pitfall: Treating the efficiency core as the lesser core and forcing work onto big cores. Example: A uclamp floor or affinity mask is applied to a background sync “so it finishes faster.” Battery life drops; nothing the user can see improves. Fix: Ask what deadline the work actually has. If nobody is waiting, the deadline is soft and the objective is joules. Let the sync take four times as long at a third of the energy.


Pitfall: Reporting thread-creation overhead as migration cost. Example: A student measures spawn-to-join around a short kernel, sees tens of microseconds, and concludes migration is catastrophically expensive. Fix: Separate them. code/migrate.cpp reports inner and outer for exactly this reason: outer - inner is spawn and join, inner - warm is the cold-cache cost. Report them separately, and say so.


Pitfall: False sharing across a cache line. Example: Two threads increment two adjacent counters in one struct. The parallel version is slower than the serial one and the source looks perfect. Fix: Reason in lines, not variables. The line is 64 bytes on essentially all Arm application processors (128 on some Apple designs — check sysctl hw.cachelinesize). Pad per-thread state so no two threads write the same line, and re-measure.


Pitfall: Maximizing performance-per-watt and missing the deadline. Example: Frame work goes to the efficiency core because it has the best perf-per-watt on the sheet. The frame takes 12 ms against an 8.3 ms budget and the UI visibly stutters. Fix: The deadline is a constraint, not a preference. Filter to the cores that meet it first, then minimize energy among the survivors. Reversing that ordering produces a result that is efficient and useless.


2.13 — Reps

Open the reps and do all of them. This week’s conditioning builds the reflexes Project 2 grades: seeing asymmetry on a real machine, computing performance-per-watt from ratios by hand, measuring a cache-warmth cost honestly, separating overheads that arrive mixed together, and refusing to believe a core count.

This week’s AI policy. Use AI to explain anything here — what a reorder buffer does, why MOESI adds an Owned state, how uclamp differs from affinity. Do not use it as a source for any figure. Every number in a submitted artifact traces to a vendor specification page, an architecture reference manual, or your own measurement. Rep 11 makes you prove the failure mode to yourself on purpose. Each AI-touching rep ends with an honest one-line AI-usage note, and the deliverables carry ai-usage.txt per Appendix D.

A preview:

  • Rep 2 — Hand-compute the throughput and performance-per-watt of all three core classes from code/core-profiles.csv before you run any tool.
  • Rep 4 — Measure the migration penalty three ways with code/migrate.cpp, predicting the sign of each gap before you build.
  • Rep 6 — Subtract the thread-creation overhead honestly, so the number you report is a cache cost and not a spawn cost.
  • Rep 8 — Kill “more cores is more fast” with code/amdahl.py and the Amdahl ceiling.
  • Rep 10 — Build a three-row placement table for three named mobile workloads and defend every row.

Everything you need runs on any of the three workbenches in Appendix A. A physical Android device makes two reps richer and is required by none of them.


2.14 — This Week’s Project

You are ready for Project 2 — The Placement Study, in Project 2. It is worth 7% of your course grade, like each of the six weekly labs.

Normal tier has you measure your own machine honestly and reason from it: inventory your CPU’s topology and asymmetry with every claimed figure traced to a primary source; measure a fixed workload at increasing thread counts with warm-up, repetitions, a median and a dispersion measure per Appendix C; infer your workload’s parallel fraction and sanity-check it against code/amdahl.py; then build the placement table — four named mobile workloads, each assigned to a core class with placement_model.py, each row defended under all four of The Four Questions rather than just latency, and at least one row where the fastest core is the wrong answer, argued in joules. You submit report.docx, measurements.xlsx, placement-decision.docx, and ai-usage.txt.

Medium tier adds a measured migration-cost experiment: sweep the working set in code/migrate.cpp, separate the cache cost from the spawn cost, and say which threads a scheduler should be free to move. Hard tier is the judgment piece — a four-core budget for a wearable, which classes you would buy and why, with the energy arithmetic. No tool produces that answer. That is why it is the Hard tier.

One requirement runs through every tier: the model is synthetic and you must say so. Naming what your analysis cannot support is not a weakness in the report. It is what makes the rest of it trustworthy.


2.15 — Coach’s Final Word

You came into this week able to count cores. You leave it unable to take that count seriously, which is a substantial upgrade.

You can now say what differs between two cores running the same instruction set, and price each difference in joules rather than adjectives. You know why HMP won and what a DSU provides. You can explain why migration is legal — coherence — and what it costs: cold private caches, warm shared ones, and a scheduler that damps its own enthusiasm. You measured that cost and separated it from the thread-creation overhead it arrives mixed with, which is a skill Chapter 8 grades. You can name a core on Linux and an intent on Apple platforms, and argue for both. And you have the rule that settles placement arguments: the cheapest core that meets the deadline — a constraint first, then an objective, in that order and never the reverse.

Two threads carry forward. First, everything here is a story about one operating point, and Chapter 5 opens the frequency axis, adds heat, and re-reads all of it. Do not get comfortable with this week’s answers. Second, the discipline about numbers: you refused a cache size from a model that would have handed you one instantly and confidently. That refusal is not friction in your workflow. In this field it is the workflow.

And underneath the arithmetic, the harder thing. The members are not ranked. The efficiency core is not a small prime core; it holds a different office, and it is 2.8× better than the prime core at what that office exists for. Learn to measure each part on its own scale. It makes you better at scheduling, and better at rather more than scheduling.

See you next week.


Up next: Work through the reps — Reps 2, 4, and 10 are Project 2 in miniature. Then open Project 2 and build the placement table. Your toolchain and the three workbenches are in Appendix A; this chapter’s code/ datasets in Appendix B; how to report a median with dispersion in Appendix C; grading and the AI policy in Appendix D; vocabulary in Appendix E. After that, Chapter 3 — the memory hierarchy, where you discover that the algorithm with fewer instructions is often the slower one, and where §2.4’s coherence traffic gets taken apart properly.

Previously: Chapter 1 — the machine in your pocket: constraints, the system-on-chip, and the instruction set underneath it all.

Interactive Lab — Week 2
The Core Placement Bench

Pick a workload, give it a deadline and a size, then assign it to a core class. The bench returns completion time, energy in millijoules, whether the deadline held — and what an energy-aware scheduler would likely have chosen instead. Predict before you click. Write down the core you think is right and, more importantly, the rule you used. If your rule is “the fast one for the important thing,” you are about to be corrected. That is the exercise.

The core profiles this bench reasons from

prime
rel. throughput
6.93
rel. power
9.60
rel. perf-per-watt
0.72
rel. area
5.20
private L2
2048 KiB
performance
rel. throughput
5.08
rel. power
5.40
rel. perf-per-watt
0.94
rel. area
3.10
private L2
1024 KiB
efficiency
rel. throughput
2.00
rel. power
1.00
rel. perf-per-watt
2.00
rel. area
1.00
private L2
256 KiB

Straight out of code/core-profiles.csv, normalised so the efficiency core is 1.00 on power and area. Read the two ratios that matter: the prime core delivers about 3.5× the throughput of the efficiency core at about 0.36× its performance-per-watt, on 5.2× the silicon. That is the bargain, and this bench prices it.

1 · Describe the work

12 ms on one efficiency core

2 · Place it

3 · Read the whole row, not just the deadline column

One workload on one core, at one operating point per class
core class finish (ms) core (mJ) total (mJ) perf/W deadline tag
Twelve assignments — four workloads on three classes — is the rep.

This model is relative and illustrative. The ratios come from core-profiles.csv, which is synthetic teaching data chosen to reproduce the shape of a published heterogeneous design — no value here is copied from a datasheet or measured on a product. The absolute anchors are the model’s own normalisation, the same one placement_model.py uses by default: the efficiency core is defined as 1.00 IPC at 2.00 GHz drawing 250 mW at its top operating point. Two limitations to state in your report rather than hope nobody notices: this bench evaluates one operating point per class (Chapter 5 opens the frequency axis), and it models one core — it says nothing about splitting the work across several, which is amdahl.py’s question.

Check Your Reps

Week 2 Knowledge Check

Question 1 of 5
A phone is scrolling — a frame due every 8.3 ms — while the photo library indexes in the background. Under global task scheduling (heterogeneous multiprocessing), what can the operating system do that neither cluster migration nor the in-kernel switcher could?
Why: Cluster migration ran the whole system on one cluster or the other. The in-kernel switcher paired each big core with a little one and presented the pair to the OS as a single virtual core, so you still could not use a big and a little core at once. HMP makes every core simultaneously visible to the scheduler and answers the placement question per thread — exactly §2.1's situation. The tempting wrong answer is the cache one: coherence makes migration correct, it does not make the destination core's private caches warm. And per-core frequency domains arrived with DynamIQ, not with HMP.
Question 2 of 5
A student runs the migration program at a 256 KiB working set, reads the summary below, and reports that migration costs about 33 microseconds on my machine. The program's header line said affinity control: NOT available here. What is wrong with that claim?
mode                     median us      IQR us   ns/access
----------------------------------------------------------
warm (same core)             12.29        0.21        3.00
cold (evicted)               56.17        5.46       13.71
migrating (inner)            12.50        0.21        3.05
migrating (outer)            45.58       10.21       11.13

cache-warmth cost (cold - warm)  :    +43.88 us   (4.57x warm)
migration cost    (inner - warm) :     +0.21 us   (1.02x warm)
spawn + join      (outer - inner):    +33.08 us
Why: outer - inner is spawn and join and has nothing to do with caches; inner - warm is what arriving on a different core cost. This machine reported no affinity control, so the freshly spawned thread landed back where it started and inner came out equal to warm — a result, not a failed experiment. The cold row is the control experiment: it evicts everything, so it prices losing the caches with no threading involved, and a genuine in-cluster migration should land between warm and cold because the shared L3 behind the DSU survives the move. Report two overheads added together as one number and you have published a mixture and called it a measurement.
Question 3 of 5
A team ships an Android app that pins its render thread to CPUs 6 and 7 because those are the big cores on our test device. The chapter calls a hard-coded CPU number in a production path a bug wearing an optimization costume. Why?
Why: An affinity mask is a claim about a specific hardware topology. A Quality of Service class survives the hardware because it describes the workload rather than the machine, and it lets the platform apply what the application cannot see: thermal state, what else is running, what the user is doing. The rule is express intent in shipping code, take control only in measurement code — a benchmark that does not control placement has an uncontrolled variable. Note the inversion in the migration distractor: pinning prevents migration; the bug here is that the pin names the wrong machine.
Question 4 of 5
Do the arithmetic. From §2.9's profiles, the prime core delivers 6.93 units of relative throughput at 9.60 units of relative power; the efficiency core delivers 2.00 at 1.00. A background photo index retires a fixed instruction count and has an hour to finish. Ignoring any platform floor, how much more energy does the prime core spend on that fixed task, and where should the work go?
Why: For a fixed task the metric is joules, and E = P × t with time going as the reciprocal of throughput — so the ratio is (9.60 / 6.93) ÷ (1.00 / 2.00) ≈ 2.8. Pricing only the power gives 9.6 and forgets that the prime core finishes about 3.5 times sooner; pricing only the throughput gives 3.5 and forgets the power. Race-to-idle is not a universal law: with no platform floor charged, the efficiency core's advantage stands, and Chapter 5 shows you honestly when it does not.
Question 5 of 5
A thread migrates to another core inside the same DynamIQ cluster. Which statement about what survives the move is right?
Why: The DSU provides three things at once: the cluster's shared L3, the coherency point where the snoop filter lives, and per-core power and frequency domains. Coherence is what makes migration correct — the thread will never observe stale memory — but correctness is not warmth, and the destination's private caches and its predictors still have to be refilled and relearned. That surviving L3 is exactly why migration inside a cluster is much cheaper than migration between clusters that share no last-level cache, and why migrating (inner) should land between warm and cold when the thread genuinely moves.
YOU FINISHED. NICE WORK.