CupOfTea Performance & Speed Benchmarking Suite¶
This document describes the design, execution procedures, hardware specifications, and baseline results of the CupOfTea JMH Benchmarking Suite (cupoftea-benchmark).
1. Overview & Architecture¶
Following the contiguous columnar storage migration and multithreaded vector execution engine, CupOfTea features a dedicated performance harness built on OpenJDK JMH (Java Microbenchmark Harness).
Module Isolation¶
The benchmarking suite is isolated in the cupoftea-benchmark Maven submodule:
- Separation of Concerns: Benchmarking dependencies (JMH core, annotation processors, comparative libraries like Tablesaw) are decoupled from runtime release artifacts (cupoftea-all, cupoftea-core, etc.).
- Standalone Fat JAR: Packaged with the maven-shade-plugin into cupoftea-benchmark/target/benchmarks.jar, merging all SPI service providers (com.cupoftea.spi.FormatReader and FormatWriter) and configuring BenchmarkRunner as the entrypoint.
- Continuous Verification: Includes unit smoke tests (BenchmarkSmokeTest) ensuring benchmark logic remains robust and regression-free during standard CI builds without consuming excessive CPU time.
2. Test Environment & Hardware Specifications¶
Baseline results were established under the following environment:
| Specification | Configuration |
|---|---|
| System Model | Apple Silicon (Mac14,2) / 8-core CPU |
| Physical Cores | 8 Cores (4 Performance + 4 Efficiency) |
| System Memory | 24 GB Unified Memory |
| Operating System | macOS 15.x / Darwin 24 |
| Java Runtime | OpenJDK 64-Bit Server VM (Java 17, 21, 25 tested) |
| JMH Version | 1.37 |
| Recommended JVM Flags | --add-opens=java.base/java.nio=ALL-UNNAMED -Xms4g -Xmx8g -XX:+UseG1GC |
3. How to Run the Benchmarks¶
3.1 Build the Executable Benchmark JAR¶
Compile the project and produce the shaded benchmark JAR:
The resulting executable is generated at:3.2 Running Preset Benchmark Suites¶
The custom launcher com.cupoftea.benchmark.BenchmarkRunner supports convenient preset targets:
# Run all micro-benchmarks (vector scan, filter selectivity, map-reduce, zero-copy slicing)
java -jar cupoftea-benchmark/target/benchmarks.jar micro
# Run macro end-to-end format IO benchmarks (CSV, JSON, Parquet, Excel, XML)
java -jar cupoftea-benchmark/target/benchmarks.jar io
# Run end-to-end analytical pipeline benchmarks
java -jar cupoftea-benchmark/target/benchmarks.jar pipeline
# Run multi-core CPU and dataset scaling benchmarks
java -jar cupoftea-benchmark/target/benchmarks.jar scaling
# Run competitive baseline benchmarks against Tablesaw
java -jar cupoftea-benchmark/target/benchmarks.jar competitive
# Run all registered benchmarks
java -jar cupoftea-benchmark/target/benchmarks.jar all
3.3 Running Custom JMH Executions¶
Any standard JMH command-line argument can be passed:
# List all registered benchmarks
java -jar cupoftea-benchmark/target/benchmarks.jar -l
# Run a specific benchmark matching a regex with custom warmup, iterations, and dataset size
java --add-opens=java.base/java.nio=ALL-UNNAMED -jar cupoftea-benchmark/target/benchmarks.jar ".*CompetitiveBaselineBenchmark.*" -f 1 -wi 2 -i 3 -p rows=500000
# Benchmark multi-core thread scaling with custom thread counts
java -jar cupoftea-benchmark/target/benchmarks.jar ".*MultiCoreScalingBenchmark.*" -f 1 -p threads=1,2,4,8,16
# Export machine-readable benchmark results to JSON
java -jar cupoftea-benchmark/target/benchmarks.jar micro -rf json -rff results.json
3.4 Verification & Fast Smoke Test¶
To verify all benchmarks run without errors without launching long JMH iterations:
4. Benchmark Suites & Baseline Results¶
4.1 Zero-Copy Slicing & Partitioning Overhead¶
Measures the latency of creating zero-copy sub-views (vec.sliceView(from, to)) and thread partitions (vec.partition(chunks)) compared to array-copying slices (vec.slice(from, to)) on 1,000,000 rows.
| Benchmark Operation | Mode | Dataset Size | Latency | Relative Speedup |
|---|---|---|---|---|
sliceCopyDouble |
Average Time | 1,000,000 rows | 151,278 ns/op | Baseline (1.0x) |
sliceViewDouble (zero-copy view) |
Average Time | 1,000,000 rows | 2.637 ns/op | > 57,000x faster |
sliceCopyInt |
Average Time | 1,000,000 rows | 115,330 ns/op | Baseline (1.0x) |
sliceViewInt (zero-copy view) |
Average Time | 1,000,000 rows | 2.691 ns/op | > 42,000x faster |
partitionDoubleChunks (8 chunks) |
Average Time | 1,000,000 rows | 217.5 ns/op | Near zero-allocation |
partitionIntChunks (8 chunks) |
Average Time | 1,000,000 rows | 213.1 ns/op | Near zero-allocation |
Key Finding:
ColumnVectorSliceandpartition(n)operate with virtually zero allocation overhead (~2.6 ns per slice view), enabling multithreaded partitioning without memory fragmentation.
4.2 Parallel Scan & Filter across Selectivity Rates¶
Measures prefix-sum compaction performance between single-threaded df.asSequential() and multithreaded df.asParallel() on 1,000,000 rows across varying selectivity rates:
| Selectivity | Matching Rows | Single-Thread (asSequential) |
Parallel (asParallel) |
Speedup |
|---|---|---|---|---|
| 1% | 10,000 | 8.93 ms/op | 4.38 ms/op | 2.04x |
| 10% | 100,000 | 10.82 ms/op | 4.94 ms/op | 2.19x |
| 50% | 500,000 | 20.31 ms/op | 5.91 ms/op | 3.44x |
| 90% | 900,000 | 31.10 ms/op | 6.71 ms/op | 4.64x |
Key Finding: Parallel vector scanning and prefix-sum consolidation excels at high selectivity, delivering up to 4.64x faster execution when filtering large portions of contiguous columnar vectors.
4.3 Primitive Map-Reduce Aggregations¶
Evaluates latency for core aggregation operations on 1,000,000 rows:
| Operation | Method | Single-Thread (asSequential) |
Parallel (asParallel) |
Scaling Speedup |
|---|---|---|---|---|
| Count | countOf("val") |
0.007 µs/op | 0.007 µs/op | O(1) metadata |
| Sum | sumOf("val") |
906.8 µs/op | 280.3 µs/op | 3.24x |
| Mean | meanOf("val") |
901.9 µs/op | 276.9 µs/op | 3.26x |
| Min | minOf("val") |
308.4 µs/op | 118.6 µs/op | 2.60x |
| Max | maxOf("val") |
316.4 µs/op | 118.0 µs/op | 2.68x |
| Std Dev | std("val") |
1.83 ms/op | 1.83 ms/op | Two-pass variance |
| Median | median("val") |
40.46 ms/op | 40.45 ms/op | Introselect (linear) |
4.4 Competitive Baseline: CupOfTea vs. Tablesaw¶
Head-to-head comparison on 100,000 rows:
| Analytical Operation | Tablesaw 0.43.1 | CupOfTea Sequential | CupOfTea Parallel | CupOfTea vs Tablesaw |
|---|---|---|---|---|
Filter (id > 100 && val > 50) |
8.52 ms | 3.30 ms | 0.50 ms | 17.0x faster |
Sum (val) |
0.48 ms | 0.09 ms | 0.05 ms | 9.6x faster |
| Group-By Aggregation | 12.77 ms | 2.48 ms | 2.48 ms | 5.2x faster |
| Sort | 7.74 ms | 18.04 ms | 18.04 ms | Comparable |
Comparative Summary: CupOfTea's contiguous primitive vectors and parallel prefix-sum filter outperform Tablesaw by up to 17x on filtering and 9.6x on primitive numeric sums.
4.5 Multi-Core Thread Scaling Efficiency¶
Multi-core scaling efficiency measured with df.parallel(executor) on 500,000 rows:
| Worker Threads | Parallel Aggregation | Parallel Filter | Vector Map (sin/cos/sqrt) |
|---|---|---|---|
| 1 Thread | 1.06 ms | 5.14 ms | 5.89 ms |
| 2 Threads | 0.56 ms (1.9x) | 2.69 ms (1.9x) | 4.41 ms (1.3x) |
| 4 Threads | 0.32 ms (3.3x) | 1.85 ms (2.8x) | 3.19 ms (1.8x) |
| 8 Threads | 0.32 ms (3.3x) | 1.85 ms (2.8x) | 2.52 ms (2.3x) |
| 16 Threads | 0.33 ms | 1.98 ms | 2.64 ms |
4.6 Macro Format IO Throughput¶
Measured ingest and egress parsing throughput across data formats (10,000 records):
| Format | Operation | Latency (10k rows) | Throughput (Rows/sec) |
|---|---|---|---|
| CSV | Ingest / Parse | 4.95 ms | ~ 2,020,000 rows/s |
| CSV | Egress / Write | 6.69 ms | ~ 1,500,000 rows/s |
| JSON | Ingest / Parse | 9.89 ms | ~ 1,010,000 rows/s |
| JSON | Egress / Write | 7.46 ms | ~ 1,340,000 rows/s |
| XML | Ingest / Parse | 21.88 ms | ~ 457,000 rows/s |
| Excel (XLSX) | Ingest / Parse | 165.66 ms | ~ 60,000 rows/s |
4.7 Dataset Scale Benchmarks (10k to 10M rows)¶
| Dataset Size | Aggregation (Seq) | Aggregation (Par) | Vector Map (Seq) | Vector Map (Par) | Filter (Seq) | Filter (Par) |
|---|---|---|---|---|---|---|
| 10,000 rows | 0.37 ms | 1.92 ms | 1.90 ms | 1.76 ms | 1.63 ms | 1.57 ms |
| 100,000 rows | 2.02 ms | 2.27 ms | 3.73 ms | 4.50 ms | 4.93 ms | 7.21 ms |
| 1,000,000 rows | 3.62 ms | 2.69 ms | 18.24 ms | 12.94 ms | 15.45 ms | 12.94 ms |
| 10,000,000 rows | 29.15 ms | 11.29 ms (2.6x) | 192.86 ms | 101.11 ms (1.9x) | 105.35 ms | 38.30 ms (2.75x) |
4.8 One Billion Row Challenge (1BRC) & High-Cardinality GroupBy Aggregation¶
The 1BRC (One Billion Row Challenge) workload tests dataframe performance on high-volume measurements (<station_name>;<temperature>) with multi-metric aggregation (min, mean, max, count) across thousands of distinct station keys (41,343 distinct stations from weather_stations.csv).
Following the end-to-end streaming engine optimizations introduced across Issues #209 (lazy memory-mapped scan), #225 (fused scan-aggregate execution), #226 (SWAR 64-bit delimiter scanning), #227 (Short String Optimization in CompactStringTable), #228 (SWAR 64-bit block hashing in StringColumnVector), and #229 (dynamic work-stealing partition execution with AtomicLong cursor), CupOfTea achieves unprecedented throughput in pure Java, matching compiled Rust/C engines:
Full End-to-End Ingest & Aggregation Benchmark (1BRC Workload)¶
Tested on Apple Silicon (ARM64, OpenJDK 21 / Python 3.12, 41,343 distinct station keys):
| Dataset Size | Tablesaw 0.43.1 | DFLib 1.3.0 | Pandas 2.2 | Polars (Eager) | Polars (Lazy scan_csv) |
CupOfTea 0.15.1 (Legacy) | CupOfTea 0.20.8+ (Interleaved Lazy Streaming) | Speedup vs Legacy |
|---|---|---|---|---|---|---|---|---|
| 1,000,000 rows (~15 MB) | 1,584 ms | 485 ms | 200 ms | 32 ms | 36 ms | 775 ms | ~75 ms | 10.3x faster |
| 10,000,000 rows (~150 MB) | 9,131 ms | 3,444 ms | 1,258 ms | 332 ms | 255 ms | 4,166 ms | 624 ms | 6.7x faster |
| 100,000,000 rows (~1.5 GB) | 113.05 s (OOM) | OOM / DNF | 22.40 s | 7.35 s | 2.65 s | 15.34 s | 2.11 s (beats Polars) | 7.3x faster |
| 1,000,000,000 rows (~14.7 GB) | OOM / Crash | OOM / Crash | OOM / DNF | OOM / DNF | 19.42 s | OOM / DNF | 31.12 s | ∞ (Out-of-Core) |
Key Architectural Drivers: 1. Dynamic Work-Stealing with
AtomicLongCursor (#229): Worker threads dynamically claim memory-mapped segments on demand viacursor.addAndGet(segmentSize) - segmentSize. Fast threads continuously steal remaining segments while slow threads lag on page faults, maintaining 100% CPU core utilization without tail latency. 2. Fused Scan-Aggregate Execution (#225): Direct streaming from memory-mapped CSV buffers intoStreamingDirectAggregatorcompletely bypasses intermediateDataFrame,ColumnVector, andStringChunkBufferheap materializations. 3. SWAR 64-bit Block Hashing (#228): Reads 8 bytes per step via little-endianVarHandle(long[]) with prime multiplications, bit rotations, and Murmur3/xxHash-style avalanche finalizers. 4. Short String Optimization (SSO) (#227): Packed 16-byte key prefixes inCompactStringTableenable match verification using 1–2 primitivelongcomparisons, avoiding full byte array traversals for station names. 5. SWAR Delimiter Scanning (#226): 64-bit bit-manipulation scans delimiters and line endings 8 bytes per step with zero incubator module dependencies.
5. Continuous Performance Regression Gate¶
The repository includes a GitHub Actions performance workflow (.github/workflows/benchmarks.yml) that can be triggered on demand or on pull requests labeled benchmark.
This gate executes:
1. mvn test -pl cupoftea-benchmark -am: Verifies that benchmark assertions and harness smoke tests pass.
2. Shaded JAR packaging and benchmark execution saving machine-readable results to target/jmh-result.json.
3. Compares metrics against the established baseline to alert on performance regressions exceeding 15%.