Files
spring-boot-demo/spring-batch-partitioning/docs/06-why-cpu-bound-not-io-bound.md
T

62 lines
3.6 KiB
Markdown

# 6. Why this module's work is CPU-bound, not I/O-bound
[← Previous](05-the-diagnostic-endpoint.md) | [README](../README.md) | [Next: The RejectedExecutionException →](07-the-rejectedexecutionexception.md)
## The design choice
[`RiskScoringProcessor`](../src/main/java/com/ankurm/batchpartition/processing/RiskScoringProcessor.java)
does `risk.iterations` (150 by default) real 64-bit XOR/multiply operations per order, then flags
the order high-risk if the resulting score exceeds a threshold or the amount exceeds
`risk.high-risk-amount-cents`. No network call, no sleep, no artificial delay — every
millisecond it spends is spent computing, on whichever core its thread is scheduled on.
That is deliberate. A partitioning demo built around an I/O-bound step (a network call per item, a
slow downstream service) shows a speedup even on a single-core machine, because the threads spend
almost all their time blocked, not competing for CPU — the "parallelism" it demonstrates is
really just concurrency hiding latency, which is a real and useful thing but a different claim than
"this uses more of the machine's compute." A CPU-bound processor makes the speedup measured in
[`docs/output/09-full-scale-throughput.txt`](output/09-full-scale-throughput.txt) mean what it
looks like it means: more cores actually doing more arithmetic per second, capped by how many
cores physically exist.
## Determinism, and what it buys
[`RiskScoringProcessorTest`](../src/test/java/com/ankurm/batchpartition/processing/RiskScoringProcessorTest.java)
pins that the same `Order` always produces the same score:
```console
order: Order[orderId=42, customerId=777, amountCents=1234567, region=NORTH]
first.process() -> riskScore=61 highRisk=false
second.process() -> riskScore=61 highRisk=false
```
Full transcript: [`docs/output/01-processor-determinism.txt`](output/01-processor-determinism.txt).
Determinism matters for a partitioned job specifically because of
[chapter 8](08-restart-reruns-only-the-failed-partition.md): a partition that fails and re-runs
must produce the same output the second time, or a restart silently changes results depending on
which attempt happened to write. `MERGE ... KEY(order_id)` in the writer
([chapter 4](04-the-writer-and-the-beanmapped-trap.md)) handles the "don't duplicate the row"
half of that; a deterministic processor handles the "don't change the row's *content* between
attempts" half.
## What the CPU cost does and does not explain about the scaling numbers
Raising `risk.iterations` to 5000 (thirty-three times the default) at a fixed 300,000-row data
volume moved the grid-size-2 speedup from 1.10x to 1.26x — see
[`docs/output/09-full-scale-throughput.txt`](output/09-full-scale-throughput.txt) for both numbers
side by side. That is evidence that at least part of the shortfall from a clean 2x speedup on 2
cores is time spent somewhere that does *not* scale with thread count — the shared H2
writer is the leading candidate, discussed further in
[chapter 10](10-scaling-sensitivity-to-data-size.md) — but it is not proof by itself; this
module did not isolate the writer completely (by, say, writing to per-partition tables and
comparing) to rule out GC pressure or thread scheduling overhead as contributing causes too. Read
the 1.10x-to-1.26x shift as "consistent with the write-contention hypothesis," not as a closed
case.
## Going deeper
- Amdahl's law, for the general shape of "some fraction of the work cannot be parallelized":
[Wikipedia — Amdahl's law](https://en.wikipedia.org/wiki/Amdahl%27s_law) (`rel="nofollow"`).
[Next: The RejectedExecutionException →](07-the-rejectedexecutionexception.md)