Definition
- Small data (10 elements): parallel is slower because splitting, scheduling, and merging cost more than the work itself.
- Large data (10M elements): parallel wins because the work across cores far exceeds the overhead.
- Crossover point depends on per-element cost, core count, and source splittability.
- JMH is the proper tool for reliable micro-benchmarking;
nanoTimeis indicative only. - Default to sequential and parallelize only after measuring with representative data.
Hiring extra clerks at National Bank of India to total a three-line ledger wastes time coordinating them. But for a million-line ledger, the extra clerks finish far sooner because the work outweighs the coordination.
Understanding the Concepts
Why does data size matter for parallel performance?
Parallelism has a fixed cost: splitting the source into chunks, scheduling tasks on the ForkJoinPool, and merging partial results. This cost is roughly constant regardless of data size. For tiny inputs, this fixed cost exceeds the actual work, so sequential wins. For huge inputs, the per-element work across many cores far exceeds the overhead, so parallel wins.
How do you demonstrate the difference?
package com.bank.hr;
import java.util.List;
import java.util.stream.IntStream;
public class ParallelBenchmark {
public static void main(String[] args) {
// Small list: 10 elements
List<Integer> small = IntStream.rangeClosed(1, 10).boxed().toList();
benchmarkSum("Small (10)", small);
// Large list: 10 million elements
List<Integer> large = IntStream.rangeClosed(1, 10_000_000).boxed().toList();
benchmarkSum("Large (10M)", large);
}
static void benchmarkSum(String label, List<Integer> data) {
// Warm up
for (int i = 0; i < 5; i++) {
data.stream().mapToLong(Integer::longValue).sum();
data.parallelStream().mapToLong(Integer::longValue).sum();
}
// Sequential timing
long start = System.nanoTime();
long seqSum = data.stream().mapToLong(Integer::longValue).sum();
long seqTime = System.nanoTime() - start;
// Parallel timing
start = System.nanoTime();
long parSum = data.parallelStream().mapToLong(Integer::longValue).sum();
long parTime = System.nanoTime() - start;
System.out.printf("%s: seq=%,d ns, par=%,d ns, speedup=%.2fx%n",
label, seqTime, parTime, (double) seqTime / parTime);
}
}
Small (10): seq=1,200 ns, par=85,000 ns, speedup=0.01x
Large (10M): seq=28,000,000 ns, par=8,500,000 ns, speedup=3.29x
For 10 elements, parallel is about 70x slower. For 10 million, parallel is about 3x faster (on a 4-core machine).
Why is a nanoTime benchmark unreliable?
Hand-rolled System.nanoTime() benchmarks suffer from:
- JIT warm-up: the first few runs are interpreted, not compiled, distorting the timing.
- Dead-code elimination: the JIT may optimize away computation whose result is unused.
- GC pauses: garbage collection can spike timing randomly.
How do you benchmark properly with JMH?
import org.openjdk.jmh.annotations.*;
import java.util.List;
import java.util.stream.IntStream;
@State(Scope.Benchmark)
public class StreamBenchmark {
private List<Integer> data;
@Param({"10", "10000000"})
int size;
@Setup
public void setup() {
data = IntStream.rangeClosed(1, size).boxed().toList();
}
@Benchmark
public long sequential() {
return data.stream().mapToLong(Integer::longValue).sum();
}
@Benchmark
public long parallel() {
return data.parallelStream().mapToLong(Integer::longValue).sum();
}
}
JMH handles warm-up iterations, prevents dead-code elimination, and provides statistically reliable results.
What factors affect the crossover point?
| Factor | Effect on crossover |
|---|---|
| Per-element cost | Expensive operations lower the crossover (parallel wins sooner) |
| Core count | More cores lower the crossover |
| Source splittability | Array/ArrayList split cheaply; LinkedList splits poorly |
| Operation type | Stateless (filter/map) parallelizes well; stateful (sorted) less so |
What are common mistakes?
Mistake: Assuming parallel is always faster without measuring.
// WRONG: blindly parallelizing a small list
List<String> names = List.of("Rajesh", "Priya", "Amit");
names.parallelStream().map(String::toUpperCase).toList();
// Parallel overhead dominates for 3 elements
// CORRECT: use sequential for small data
names.stream().map(String::toUpperCase).toList();
What should a senior engineer know?
A senior engineer measures before parallelizing and knows that the crossover depends on the workload. For trivial operations like addition, the crossover is around 100K+ elements. For expensive operations (parsing, hashing, network calls), it can be much lower. The rule is: measure with representative data, default to sequential, and parallelize only when measured benefit justifies the complexity.
What are common interview follow-up questions?
- "What tool should you use for benchmarking?" JMH (Java Microbenchmark Harness).
- "Why not use System.nanoTime()?" JIT warm-up, dead-code elimination, and GC pauses distort results.
- "What is the typical crossover size?" Depends on per-element cost, but 100K+ for cheap operations.
- "Does more cores always mean more speedup?" No: Amdahl's Law limits speedup based on the sequential fraction.
Visual Overview
10 elements: ┌────────────────────────────────────────┐ │ Sequential: [1..10] → sum → 55 │ │ Time: ~1,200 ns │ ├────────────────────────────────────────┤ │ Parallel: split + schedule + merge │ │ Overhead alone: ~85,000 ns │ │ ← parallel LOSES (70x slower) │ └────────────────────────────────────────┘ 10,000,000 elements: ┌────────────────────────────────────────┐ │ Sequential: [1..10M] → sum │ │ Time: ~28 ms │ ├────────────────────────────────────────┤ │ Parallel: 4 cores × 2.5M each │ │ Time: ~8.5 ms │ │ ← parallel WINS (3.3x faster) │ └────────────────────────────────────────┘
Key Takeaway
To decide whether parallelism pays off, you benchmark a sequential sum against a parallel sum on both a tiny list and a huge list. For 10 elements, the parallel version is much slower because the fixed cost of splitting, scheduling, and merging dwarfs the actual work. For 10 million elements, the parallel version is several times faster because spreading the work across cores saves far more time than the overhead costs.
At the National Bank of India, Rajesh uses this comparison to decide whether to parallelize the nightly salary-sum job. He uses JMH for reliable numbers because hand-rolled nanoTime benchmarks suffer from JIT warm-up, dead-code elimination, and GC pauses that distort results.
The lesson is that parallelism is never free. The crossover point depends on per-element cost, core count, and source splittability. For cheap operations like addition, the crossover is around 100K+ elements. For expensive operations, it can be much lower. A senior engineer defaults to sequential and parallelizes only after measuring with representative data.
Remember one sentence: measure, do not guess: parallel wins on large data with expensive work, but loses on small data where overhead dominates.
---