CPU or GPU: Two Bets About What Work Looks Like
A CPU spends its transistor budget making one instruction stream go fast — speculation, out-of-order issue, large caches. A GPU spends a comparable budget on many simple lanes running the same operation over different data. Neither is faster; the shape of your work decides which bet pays.
Software view, hardware view
The gap between what you wrote and what the machine does is where this whole domain lives.
Where the transistors go
Both devices face the same physical problem: arithmetic is cheap and memory is slow, so most of the time a naive machine would be idle waiting for data. They resolve it in opposite directions, and the resolution is visible in what the silicon is spent on.
A CPU core devotes a large fraction of its area to *latency hiding for one thread*: a branch predictor so the pipeline keeps moving through control flow, an out-of-order window so independent instructions run while one waits, and a cache hierarchy big enough that most accesses never reach DRAM. The arithmetic units are a small part of the picture. That is why a CPU is excellent at code full of unpredictable branches, pointer chasing and dependencies — the machinery exists specifically to survive that.
A GPU devotes its area to *many simple lanes plus the registers to keep them all resident*. There is far less speculation and far less per-lane cache. Instead, when one group of lanes stalls on memory, the scheduler switches to another group that is ready — latency is hidden by having something else to do, not by avoiding the stall. This only works if there genuinely is something else to do, which is the whole condition.
| Design question | CPU | GPU |
|---|---|---|
| What is optimised | Time to finish one instruction stream | Work completed per unit time across many streams |
| How memory latency is hidden | Speculation, out-of-order window, large caches | Switching to other resident lanes that are ready |
| Cost of an unpredictable branch | A misprediction: pipeline refill — see Misprediction: What a Wrong Guess Costs | Divergence: both paths execute in sequence — see Lanes, Divergence and Coalescing |
| Cache per unit of work | Large; a working set can live entirely in cache | Small per lane; bandwidth matters more than capacity |
| Good at | Branchy, dependent, irregular, latency-sensitive work | Wide, regular, independent work with high arithmetic intensity |
| Bad at | Thousands of identical independent operations | Long dependency chains, divergent control flow, small inputs |
The condition, stated precisely
The GPU bet pays when the work has three properties at once, and fails if any one is missing. First, width: enough independent elements to keep the lanes occupied, which is thousands rather than dozens. Second, regularity: the lanes should mostly follow the same control path, because divergent branches serialise. Third, arithmetic intensity: enough computation per byte moved that the device is not simply waiting on its own memory.
The third is the one that surprises people. A GPU has far more memory bandwidth than a CPU, but it also has far more arithmetic capacity, so the *ratio* is not obviously better. A kernel doing one add per element loaded is bandwidth-bound on a GPU exactly as it was on a CPU — you moved the bottleneck to a different device rather than removing it. That is the same reasoning as When the Memory Bus Is the Bottleneck, applied to a different piece of hardware.
The honest test before porting is arithmetic intensity: operations performed divided by bytes moved. High intensity — dense matrix multiplication, where each loaded element participates in many multiply-adds — is the case GPUs were built for. Low intensity — summing an array, copying with a transform — will be limited by memory on either device, and the port buys you the difference in bandwidth and nothing more.
1// Arithmetic intensity ~ 1 op per 4 bytes loaded.2// Bandwidth-bound on a CPU. Still bandwidth-bound on a GPU.3// The port buys you the bandwidth ratio and nothing else.4for i in 0..n:5 out[i] = in[i] * 2.06 7// Arithmetic intensity ~ n ops per element loaded (with tiling).8// Each loaded value is reused many times from on-chip memory.9// This is the shape GPUs were designed for.10for i in 0..n:11 for j in 0..n:12 acc = 013 for k in 0..n:14 acc += A[i][k] * B[k][j] // every A and B tile is reused n times15 C[i][j] = accWhy "faster" is the wrong word
A claim like "the GPU is 40× faster" is meaningless without three qualifiers: which workload, against which CPU implementation, and counting the transfer or not. Published speedups have historically been inflated by all three — comparing a tuned kernel against an untuned single-threaded scalar baseline, on a workload chosen because it suits the device, with the host-to-device copy excluded from the timer.
The defensible comparison is against a CPU implementation that has itself been given a fair chance: multithreaded, vectorised, cache-blocked. Against that baseline, real speedups on suitable workloads remain large and worth having — but they are the honest number, and they are the one your capacity plan should use.
The comparison also has to include what happens *around* the kernel. A GPU that finishes in a tenth of the time but requires two transfers, a synchronisation and a driver launch per call may lose on wall-clock for small inputs. The Transfer You Forgot to Count works through that arithmetic; the summary is that the break-even is a real input size and you should know roughly where it sits before committing.
1// CPU: single-threaded, scalar, no blocking2// GPU: tuned kernel, transfer excluded from the timer3t_cpu = time(naive_single_threaded(data))4t_gpu = time(kernel_only(data_already_on_device))5speedup = t_cpu / t_gpu // large, and not a decision-grade number1// CPU: multithreaded and vectorised — a real baseline2// GPU: transfer in, kernel, transfer out, synchronise3t_cpu = time(parallel_vectorised(data))4t_gpu = time(copy_h2d + kernel + copy_d2h + sync)5speedup = t_cpu / t_gpu // smaller, and true end to endBoth numbers are arithmetically correct; only the second answers the question anyone actually has, which is whether the system gets faster. The first compares the best version of one thing against the worst version of another and omits a cost the user pays.
Key points
- A CPU spends area hiding latency for one stream; a GPU spends it on lanes so a stall can be covered by other work.
- The GPU bet needs width, regularity and arithmetic intensity at the same time — missing any one of the three sinks it.
- A low-arithmetic-intensity kernel is bandwidth-bound on a GPU too; the port relocates the bottleneck rather than removing it.
- Speedup claims are only decision-grade against a fairly tuned CPU baseline with the transfer included.
- "Faster" is not a property of a device; it is a property of a device and a workload together.
Progressive depth
Overview
A CPU makes one stream of instructions fast. A GPU makes many identical streams finish quickly in aggregate. Choose by the shape of the work, not by which device sounds more powerful.
Practical
Before porting, count operations per byte moved. High intensity and thousands of independent elements means the port is likely worth it; low intensity means you will be memory-bound on the new device too. Always time the transfer, and always compare against a CPU baseline that is itself multithreaded and vectorised.
Advanced
Latency hiding is the real difference. A CPU hides latency within one thread using speculation and a reorder window; a GPU hides it across resident groups of lanes. That is why occupancy matters on a GPU and instruction-level parallelism matters on a CPU — they are the same problem solved with different resources, and it is the reason a GPU has so little per-lane cache: it does not need to avoid the stall, only to have something else ready.
Internals
The lane group is the scheduling unit, and it executes one instruction across all its lanes. Divergent branches are handled by executing both paths with lanes masked off, so control flow costs throughput rather than causing a misprediction. Memory requests from lanes in a group are coalesced into as few wide transactions as the address pattern permits, which is why a strided or scattered access pattern can cost many times what the equivalent contiguous pattern costs — the arithmetic is identical and the traffic is not.
Where the Data Is
Change an input and watch which number moves — and which one refuses to.
The exact ratios vary by machine and the absolute times vary far more, which is why none are shown. What is stable enough to build intuition on is the shape: each level is several times the one above, and the gap between the last cache level and memory is the one that decides most program performance.
Follow the mechanism
The path through the machine, hop by hop — and the conclusions it invites that are wrong.
- 1Host → driver: the program enqueues a kernel launch, which is itself a cost paid per call rather than per element.
- 2Driver → device scheduler: the launch becomes many groups of lanes, distributed across the compute units.
- 3Compute unit → lanes: each group executes in lockstep; a group that stalls on memory is switched out for a ready group.
- 4Lanes → device memory: requests from adjacent lanes are combined into wide transactions when the addresses line up — see Lanes, Divergence and Coalescing.
- 5Device → host: results are copied back, and a synchronisation makes them visible to the program that asked.
- • "It is parallel hardware, so any parallel workload will benefit" — parallel is necessary but not sufficient; regularity and intensity are the other two conditions.
- • "The kernel is 30× faster, so the program will be" — Amdahl still applies, and the transfer plus the serial remainder often dominate the end-to-end number.
- • "CPU utilisation is 100%, so the CPU is the bottleneck and offloading will help" — a busy CPU that is mostly stalled on memory will be a GPU that is mostly stalled on memory.
- • "More lanes means proportionally more throughput" — only until bandwidth, occupancy or divergence binds, and one of them usually binds first.
Consequences, controls and cost
- • Workloads with thousands of independent, similar operations can run dramatically faster than on any CPU.
- • Workloads with long dependency chains gain nothing, because there is never enough ready work to hide a stall.
- • Small inputs can be slower on the GPU end to end, dominated by launch and transfer rather than by computation.
- • Data-dependent branching costs far more than the same branching would on a CPU with a good predictor.
- • Estimate arithmetic intensity before writing a kernel — operations per byte moved decides whether the device can help at all.
- • Keep data resident on the device across a chain of kernels rather than round-tripping between each.
- • Restructure the algorithm for regular access and uniform control flow, which usually matters more than tuning the kernel.
- • Compare against a genuinely tuned CPU version; sometimes the CPU version you had was simply untuned.
- • Batch small independent problems into one launch so the fixed launch cost is amortised.
- • Compute arithmetic intensity by hand from the algorithm — operations divided by unique bytes touched — before trusting any profile.
- • Time the full path (copy in, launch, kernel, copy out, synchronise) and compare against a multithreaded vectorised CPU baseline.
- • Use the vendor profiler to check achieved occupancy and achieved memory throughput; a kernel near peak bandwidth and far from peak arithmetic is memory-bound.
- • Sweep input size and find the break-even point where the GPU path overtakes the CPU path end to end.
- • A second programming model, a second toolchain and a second set of failure modes to debug and to keep working.
- • Device memory is a separate, smaller pool; anything that does not fit must be tiled or streamed, which complicates the algorithm.
- • Portability narrows: kernels tuned for one vendor or generation frequently need retuning for another.
- • Code that is restructured for regularity is often less readable than the branchy version it replaced.
Scope
§224 — what these claims are specific to.
- GPU-SPECIFICDiscrete GPUs with their own memory; integrated GPUs sharing the CPU memory controller avoid the transfer cost entirely and change the break-even analysis.
- PLATFORM-SPECIFICVendor architectures differ in lane width, scheduling granularity and cache structure, and do not share terminology; the neutral terms used here map to different vendor names.
Misconceptions
Where the rest of this lives
The kernel speedup bounds nothing on its own; what bounds the program is the fraction that stayed serial, which is a concurrency argument rather than a hardware one.