Back to Parallel Streams
Q4Question 4 of 10

Benchmark parallel vs sequential for 10 elements vs 10 million and explain the overhead

Key Concepts
  • For 10 elements, parallel is slower because the fixed split/merge overhead dominates the trivial work
  • For 10 million elements, parallel is faster because the work dwarfs the overhead across multiple cores
  • The crossover point depends on per-element cost, core count, and source splittability
  • A proper micro-benchmark uses JMH to account for JIT warm-up and dead-code elimination
  • Hand-rolled nanoTime benchmarks are indicative but not reliable for production decisions
  • The lesson is to measure rather than assume: default to sequential and parallelize only after measuring

Definition

Parallel loses on tiny inputs where overhead dominates and wins on huge inputs where work dominates the fixed cost.
  • 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; nanoTime is 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?

JAVA
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);
    }
}
TEXT
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?

JAVA
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.

JAVA
// 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

DIAGRAM
  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.

---

Share
Use ← → arrow keys to navigate