Back to Parallel Streams
Q2Question 2 of 10

Stream vs for-loop performance: when is each faster, when does parallelStream help vs hurt?

Key Concepts
  • Streams are generally not faster than for-loops for simple operations: they are about the same or slightly slower due to pipeline overhead.
  • For-loops are faster for primitive-heavy operations (no autoboxing), index access, and simple single-step iterations.
  • Streams match for-loops for complex multi-step pipelines because fusion compensates for the overhead.
  • parallelStream() helps when data is large (10K+), work is CPU-bound, and the source splits well (ArrayList, array).
  • parallelStream() hurts for small datasets (overhead exceeds benefit), I/O-bound work, and LinkedList sources.
  • Use IntStream and LongStream for primitive operations to avoid autoboxing overhead in stream pipelines.
  • The real benchmarking tool is JMH (Java Microbenchmark Harness), not System.nanoTime().

Definition

Streams are about the same speed as loops for most operations; for-loops win for primitives and index access, streams win in readability, and parallelStream only helps for large CPU-bound datasets.
  • Sequential stream vs loop: approximately equal. Streams have pipeline setup overhead; JIT optimises both aggressively.
  • For-loop wins: primitive operations (no autoboxing), index-based access, simple single-step iterations.
  • Stream wins: readability for multi-step pipelines, built-in parallelism, composability.
  • parallelStream wins: large datasets (10K+), CPU-bound per-element work, well-splittable source.
  • parallelStream loses: small datasets (overhead > work), I/O-bound, LinkedList, shared state.
  • Use IntStream/LongStream: avoids autoboxing for primitive operations.
  • Benchmark with JMH: System.nanoTime is unreliable due to JIT warmup, GC, and dead-code elimination.

A for-loop is a clerk with a simple desk calculator at National Bank of India: fast for basic math. A stream is a multi-function automated system: has setup time but handles complex multi-step processing efficiently. For adding 10 numbers, the desk calculator wins. For sorting 100,000 transactions by three criteria, filtering, grouping, and producing a report, the automated system matches or wins. parallelStream is hiring 8 clerks instead of 1: only helps if there is enough work to keep all 8 busy.

Understanding the Concepts

Are streams faster than for-loops?

No, streams are not inherently faster. The JIT compiler optimises both aggressively. The choice should be based on readability and correctness first, performance second.

When does each win?

| Scenario | Winner | Reason |
|----------|--------|--------|
| Sum 10 integers | for-loop | Zero overhead vs pipeline setup |
| Sum 1M integers | Comparable | JIT optimises both |
| filter+map+reduce 1M | Comparable | Fusion compensates overhead |
| Parallel filter+reduce 1M | parallelStream | Multi-core distribution |
| LinkedList parallel | for-loop | LinkedList splits in O(n) |
| Primitive array sum | for-loop | No autoboxing |

How do you avoid autoboxing?

JAVA
package com.bank.core;

import java.util.stream.IntStream;

public class PrimitiveStreamDemo {
    public static void main(String[] args) {
        int[] amounts = {1000, 2000, 3000, 4000, 5000};

        // WRONG: autoboxing with Stream<Integer>
        // int sum = Arrays.stream(amounts).boxed().reduce(0, Integer::sum); // boxing!

        // CORRECT: IntStream avoids boxing
        int sum = IntStream.of(amounts).sum(); // pure primitives
        System.out.println("Sum: " + sum);
    }
}
TEXT
Sum: 15000

When does parallelStream help?

JAVA
package com.bank.service;

import java.math.BigDecimal;
import java.util.List;
import java.util.stream.IntStream;

public class ParallelBenchmark {
    // Simulate CPU-bound work
    static BigDecimal monteCarlo(int seed) {
        BigDecimal result = BigDecimal.ONE;
        for (int i = 0; i < 1000; i++) {
            result = result.add(BigDecimal.valueOf(Math.sin(seed + i)));
        }
        return result;
    }

    public static void main(String[] args) {
        // Sequential: processes one at a time
        long seqStart = System.nanoTime();
        IntStream.range(0, 10_000).mapToObj(ParallelBenchmark::monteCarlo).toList();
        long seqTime = (System.nanoTime() - seqStart) / 1_000_000;

        // Parallel: distributes across cores
        long parStart = System.nanoTime();
        IntStream.range(0, 10_000).parallel().mapToObj(ParallelBenchmark::monteCarlo).toList();
        long parTime = (System.nanoTime() - parStart) / 1_000_000;

        System.out.println("Sequential: " + seqTime + "ms");
        System.out.println("Parallel: " + parTime + "ms");
    }
}

When does parallelStream hurt?

parallelStream hurts for small datasets, I/O-bound work, and poorly splittable sources.

| Source | Splittability | Parallel benefit |
|--------|--------------|------------------|
| ArrayList, array | Excellent (O(1) split) | High |
| HashSet | Good | Medium |
| TreeSet | Moderate | Medium |
| LinkedList | Poor (O(n) split) | Low or negative |
| Stream.iterate | Poor (sequential by nature) | Negative |

JAVA
// WRONG: parallelStream on a small dataset
List.of(1, 2, 3).parallelStream().sum(); // overhead > benefit

// WRONG: parallelStream on I/O-bound work
accounts.parallelStream()
    .forEach(a -> httpClient.post(a)); // contention on connection pool

// WRONG: parallelStream on LinkedList
LinkedList<Integer> ll = new LinkedList<>();
ll.parallelStream().filter(n -> n > 0).toList(); // O(n) per split

How do you benchmark correctly with JMH?

JAVA
import org.openjdk.jmh.annotations.*;

@BenchmarkMode(Mode.AverageTime)
@State(Scope.Thread)
public class StreamBenchmark {
    private List<Integer> data;

    @Setup
    public void setup() { data = IntStream.range(0, 100_000).boxed().toList(); }

    @Benchmark
    public int forLoop() {
        int sum = 0;
        for (int n : data) sum += n;
        return sum;
    }

    @Benchmark
    public int streamSum() { return data.stream().mapToInt(i -> i).sum(); }

    @Benchmark
    public int parallelSum() { return data.parallelStream().mapToInt(i -> i).sum(); }
}

System.nanoTime is unreliable due to JIT warmup, GC, and dead-code elimination. JMH handles all of these.

What are common mistakes?

JAVA
// WRONG: using parallelStream by default "because it's faster"
accounts.parallelStream().filter(Account::active).toList(); // unnecessary overhead for small data

// CORRECT: default to sequential, parallel only after profiling
accounts.stream().filter(Account::active).toList();

// WRONG: benchmarking with System.nanoTime without JIT warmup
long start = System.nanoTime();
result = accounts.stream().filter(f).toList();
System.out.println(System.nanoTime() - start); // unreliable!

What should a senior engineer know?

  • Default to streams for multi-step pipelines because readability is more valuable than nanoseconds in 99% of code.
  • Use for-loops when you need primitive performance, index access, or mutation.
  • Use parallelStream only after profiling confirms it helps.
  • The real optimisation tool is JMH, not System.nanoTime.
  • The common ForkJoinPool is shared across the JVM; a slow parallel stream can starve other tasks.

Visual Overview

DIAGRAM
  Performance comparison:
  ─────────────────────────
  Small data (< 1K):    loop ≈ stream >> parallelStream (overhead!)
  Medium data (1K-10K):  loop ≈ stream ≈ parallelStream
  Large data (> 10K):    loop ≈ stream < parallelStream (CPU-bound)

  Decision:
  ─────────
  Simple primitive loop?        → for-loop (no autoboxing)
  Multi-step pipeline?          → stream (readability)
  Large CPU-bound dataset?      → parallelStream (speedup)
  Need to benchmark?            → JMH (not System.nanoTime)

Key Takeaway

Streams are not inherently faster or slower than for-loops. The JIT compiler optimises both aggressively. Default to streams for readability in multi-step pipelines. Use for-loops for primitive performance and index access. Use parallelStream only after profiling confirms benefit.

The crossover point where parallel starts winning is typically around 10,000 elements for CPU-bound work. Below that, ForkJoinPool setup cost dwarfs computation. Use JMH for reliable benchmarking.

Remember one sentence: streams equal loops in speed, win in readability, and parallelStream only helps for large CPU-bound datasets.

---

Share
Use ← → arrow keys to navigate