This repository contains reproducible benchmarks and performance experiments for j-util libraries. It is a benchmark and demo workspace, not a reusable library.
Benchmark results depend on the hardware, operating system, JDK, JVM options, and runtime conditions under which they are collected. Results from this repository must not be treated as universal performance claims.
- JDK 25 or newer
The Maven Wrapper downloads the project's Maven version automatically.
benchmark-corecontains the shared benchmark models, deterministic data generation, workload implementations, and correctness tests.benchmark-jmhcontains the OpenJDK JMH benchmark classes and produces the executable benchmark JAR.
./mvnw clean verifyOrdinary verification tests belong under benchmark-core/src/test/java and use
JUnit 5.
Dataset generation is a standalone step and is never performed as part of a measured benchmark operation. Build the self-contained JAR, then pass the desired row count to the generator:
./mvnw clean package
java -cp benchmark-jmh/target/benchmarks.jar io.github.jutil.performancelab.CsvDatasetGenerator 1000000This example deterministically writes 1,000,000 data rows plus a header to
target/benchmark-data/benchmark-rows-1000000.csv. Repeating the command with
the same row count produces identical CSV data. To choose another destination,
pass it as the second argument:
java -cp benchmark-jmh/target/benchmarks.jar io.github.jutil.performancelab.CsvDatasetGenerator 1000 /tmp/benchmark.csvGenerated files under target/ are build artifacts and must not be committed.
For a quick development run, generate the benchmark's default 10,000-row dataset:
java -cp benchmark-jmh/target/benchmarks.jar io.github.jutil.performancelab.CsvDatasetGenerator 10000The benchmark methods are organized into classes so that each class contains only directly comparable operations. Separate ready-data classes add the narrow and wide average comparisons and the maximum-by-double comparison described below, and a focused iteration suite compares traversal APIs within Columnar Projection Store.
- streaming directly to the full-row consumer;
ArrayListmaterialization with the expected row count as its initial capacity;ArrayListmaterialization starting with an initial capacity of 10;LinkedListmaterialization;ProjectionStorematerialization with the expected row count as its initial capacity; andProjectionStorematerialization starting with an initial capacity of 10.
Each measured operation includes opening and reading the file, parsing CSV, materializing the selected representation where applicable, and running the same full-row checksum consumer. Dataset generation remains a separate, unmeasured step.
Three end-to-end benchmarks produce the same aggregate from the same CSV input:
arrayListFilteredPriceSumEndToEndparses everyBenchmarkRowinto an expected-sizeArrayList, then scans the list;columnarFilteredPriceSumEndToEndparses everyBenchmarkRowinto an expected-size columnarProjectionStore, then scans its quantity and price projections; andreductionStoreFilteredPriceSumEndToEndfeeds everyBenchmarkRowto the generated reduction store, which incrementally appliesFilteredPriceSumwithout retaining a row collection.
All three compute:
quantity >= 5
sum(priceCents) for matching rows
Each measured operation includes opening and reading the file, parsing the same
BenchmarkRow objects through the same parser and InputStreamProcessor, and
producing the final long sum. The architectural work intentionally differs:
the ArrayList retains objects and performs a later traversal, the columnar
store retains projections and performs a later columnar traversal, and the
reduction store computes during ingestion without materializing retained rows.
The ready-data benchmarks compare reductions after data has already been
materialized. ReadyPriceSumBenchmark contains the unfiltered selected-column
sum for ArrayList, LinkedList, and columnar ProjectionStore:
sum(priceCents)
ReadyFilteredPriceSumBenchmark contains the filtered business operation for
the same three representations:
quantity >= 5
sum(priceCents) for matching rows
The existing retained LinkedList scan benchmarks remain available as an
additional representation. These benchmarks prepare their structures in JMH
trial setup, so measured execution excludes CSV ingestion and materialization.
Separate JMH states retain only the representation required by a benchmark.
The retained ArrayList and ProjectionStore use the expected row count as
their initial capacity.
The initial-capacity comparison is intentionally limited to the end-to-end benchmarks because it measures construction and growth cost. Once a structure is already loaded, its starting capacity is not part of the measured scan.
This benchmark asks: after deterministic input has already been materialized, how long do native convenient operations and identical naive arithmetic take to calculate the average price across the compared storage representations?
All actual collection and store comparators retain the complete logical
PriceTick(long timestamp, double price) record even though the calculation
reads only price. ArrayList<PriceTick>, FastUtil
ObjectArrayList<PriceTick>, and Eclipse Collections FastList<PriceTick>
store records as objects. Tablesaw stores the same logical records column-wise
in a Table with timestamp and price columns, while Columnar Projection Store
stores both timestamp and price projections.
The ordinary-addition comparator methods are:
ArrayList: indexed traversal with ordinary addition, divided by list size;- FastUtil
ObjectArrayList: traversal of the publicelements()backing array through its logical size with ordinary addition, divided by list size; - Eclipse Collections
FastListnaive: explicit indexed traversal of the existing list with ordinary addition, divided by list size; - Tablesaw naive: explicit indexed traversal of the existing price
DoubleColumnwith ordinary addition, divided by column size; and - Columnar Projection Store: price summation through its public cursor API, divided by store size.
The native-operation methods coexist with those identical-naive-arithmetic comparisons:
- Eclipse Collections
FastList: nativesumOfDouble(PriceTick::price), divided by list size; and - Tablesaw: the price column's native
mean()operation on the complete table.
The separate double[] calculation baseline contains only prices. It traverses
that array directly with ordinary addition and divides by the array length,
providing the lowest-abstraction reference for the measured calculation. It is
intentionally not a complete PriceTick representation. This documented
exception must not be used for full-record retained-memory-footprint claims.
Construction, deterministic data generation, row-count checks, and correctness validation run in JMH trial setup and are excluded from measurement. Each JMH state retains only its own representation, and the native and naive methods for each framework reuse the same state. Ordinary-addition implementations traverse the same finite prices in the same encounter order and are checked for exact agreement. Eclipse Collections and Tablesaw retain their existing native operation semantics and are validated with a small floating-point tolerance because their arithmetic order can differ.
This ready-data suite repeatedly traverses each already-materialized representation and reports its average execution time in microseconds per operation. Each warmup and measurement iteration lasts one second; construction and data generation remain outside the measured operation.
Without a rowCount override, ReadyPriceAverageBenchmark runs all eight
methods at each of its four default sizes: 1,000, 100,000, 1,000,000, and
10,000,000 rows. Passing -p rowCount=... overrides the source parameter list
for that invocation, so only the requested size is run.
Run only the eight price-average methods with a chosen positive row count:
java -jar benchmark-jmh/target/benchmarks.jar \
ReadyPriceAverageBenchmark \
-p rowCount=10000This in-memory benchmark generates its deterministic records directly by row index and does not read or generate a CSV dataset.
This serial ready-data suite is an
Eclipse Collections MaxByDoubleTest
JMH-derived workload. It uses project-owned deterministic domain and fixture
code; it is not an official Eclipse Collections benchmark result.
Every method performs the same logical operation: scan all Position rows by
the primitive double marketValue, calculated as quantity * product.price(),
and return the original Position having the maximum value. A deterministic
shared product population and row order are used. The fixture places one unique
maximum at a non-terminal position for datasets of at least three rows,
avoiding tie-semantics differences between implementations.
The five benchmark methods are:
arrayListImperativeMaxByDouble: indexed imperative traversal of a JDKArrayList<Position>;arrayListStreamMaxByDouble: a serial JDK stream usingComparator.comparingDouble;eclipseFastListMaxByDouble: Eclipse CollectionsFastList.maxBy;columnarProjectionStoreMaxByDouble: one Columnar Projection Store cursor over projected market values and retained original references; andmanualHybridMaxByDouble: a manual lower-bound baseline pairing a completePosition[]reference array with a precomputeddouble[]market-value array.
This is intentionally a repeated-query comparison after construction.
Generation, allocation, representation population, projection evaluation,
sealing, and correctness validation occur in JMH trial setup and are excluded
from the measured scan. Columnar Projection Store and the manual hybrid compute
and retain marketValue during population. The object collections instead call
Position.marketValue() during every measured scan. This asymmetry is part of
the ready-data comparison and means the suite does not measure total end-to-end
cost.
The only default rowCount is 3,000,000, matching the scale of the source
workload. A command-line -p rowCount=... overrides that value. Run a quick
smoke test with:
java -jar benchmark-jmh/target/benchmarks.jar \
MaxByDoubleBenchmark \
-p rowCount=1000 \
-wi 1 \
-i 1 \
-f 1For a publication-quality run at the default scale:
java -jar benchmark-jmh/target/benchmarks.jar \
MaxByDoubleBenchmark \
-p rowCount=3000000 \
-wi 5 \
-i 10 \
-f 2Results are not directly comparable with official Eclipse Collections runs: this suite has a different implementation set, deterministic project-owned fixtures, and different harness modes and configuration. No parallel variants are included because they would add a separate execution-model comparison.
This separate wide-record suite performs the same ready-data average operation
over a realistic MarketDataSnapshot containing capture time, symbol, last
trade price and size, best bid and ask prices, and best bid and ask sizes. Each
fixture row is the state immediately after a distinct completed trade, and the
measured operation reads only lastTradePrice.
Unlike the narrow PriceTick(timestamp, price) suite, every complete comparator
in this suite retains all eight snapshot fields. The storage representations fall
into four categories:
- heap row objects:
ArrayList<MarketDataSnapshot>, FastUtilObjectArrayList<MarketDataSnapshot>, and Eclipse CollectionsFastList<MarketDataSnapshot>; - off-heap row records: a raw JDK
MemorySegmentbaseline with a fixed-width 64-byte row layout, and typed Chronicle Values flyweights over consecutive direct Chronicle Bytes records; - on-heap columnar storage: Tablesaw
Table, a DFLibDataFrame, Columnar Projection Store, and complete manually assembled HPPC primitive columns; and - off-heap columnar storage: an Apache Arrow
VectorSchemaRootcontaining one vector for each snapshot field.
The MemorySegment representation is the raw off-heap row baseline. Its symbol
field stores a one-byte UTF-8 length followed by up to seven UTF-8 bytes, and it
uses explicit Arena ownership. Chronicle Values + Bytes provides the typed
off-heap row representation with the same seven-byte UTF-8 symbol capacity and
one reusable flyweight. Apache Arrow is the established off-heap columnar
representation, using BigIntVector for capture time, VarCharVector for the
symbol, and Float8Vector for all six double fields. All three retain the
complete eight-field snapshot and release native resources at JMH trial teardown.
The representations therefore compare the same complete logical records even
though the calculation selects a single field.
The DFLib comparator is one complete in-memory columnar DataFrame containing
one long series, one String series, and six double series. Its native method
delegates average calculation to DFLib's public DoubleSeries.avg() operation,
while its naive method indexes that same retained lastTradePrice series and
performs ordinary encounter-order addition. The HPPC comparator is not a
DataFrame: it is a complete manual column layout made from one LongArrayList,
one ObjectArrayList<String>, and six DoubleArrayList instances. Its measured
method indexes only the retained lastTradePrice list with ordinary addition.
Both representations retain all eight snapshot fields, while the measured
operation reads only lastTradePrice.
As in the narrow suite, the separate double[] baseline contains only the
generated lastTradePrice values. It is a calculation-only reference and must
not be included in complete-record retained-memory comparisons. Eclipse
Collections' native sumOfDouble(), Tablesaw's native mean(), and DFLib's
native avg() methods retain their framework-defined numerical semantics; the
naive companion methods define ordinary encounter-order addition explicitly.
Native results are therefore validated with the existing small floating-point
tolerance instead of making their arithmetic implementation part of this
benchmark's contract.
The suite measures hot sequential traversal of lastTradePrice; every ordinary
implementation adds doubles in encounter order and divides by its logical row
count. Setup, allocation, fixture generation, population, and validation are
excluded from the measured operation. Off-heap storage primarily provides an
explicit lifecycle and reduced heap and garbage-collection pressure; it is not
assumed to be faster, and no performance claim is made before results are
collected.
The suite uses the same JMH configuration and default row counts as
ReadyPriceAverageBenchmark. Run all fourteen wide-record methods at one positive
row count with:
java -jar benchmark-jmh/target/benchmarks.jar \
ReadyMarketDataSnapshotAverageBenchmark \
-p rowCount=10000Construction, fixture generation, and validation remain outside measured time. No comparative performance or retained-memory conclusion is claimed before controlled results are collected on the intended hardware and row counts.
Chronicle Values performs runtime value-class generation and Apache Arrow's
Netty allocator accesses direct-buffer internals. Maven tests configure the
required module access. The packaged JMH methods also append the narrowly scoped
fork arguments automatically: Chronicle opens java.lang, exports
jdk.compiler/com.sun.tools.javac.file, and enables native access; Arrow opens
java.nio. Both native-library forks allow the legacy sun.misc.Unsafe memory
operations required when running on JDK 26. Users do not need to discover or add
these flags when running the packaged benchmark normally.
This focused ready-data suite compares the ergonomics and efficiency of the
three public row-oriented traversal APIs on the same sealed Columnar Projection
Store. The cursor exposes one reusable projection view whose contents advance
with the cursor and therefore must not be retained. Indexed viewAt(index)
provides explicit random access through stable, retainable views. forEach is
the conventional OO traversal API and also supplies stable, retainable views.
The benchmark measures the cost of the stable-view convenience; it does not assume in advance that one traversal will be faster or allocate more at runtime. JIT escape analysis may eliminate some temporary stable-view allocations. Run with JMH's GC profiler to observe the allocation behavior that remains in the measured runtime:
java -jar benchmark-jmh/target/benchmarks.jar \
ColumnarProjectionStoreIterationBenchmark \
-p rowCount=1000000 \
-prof gcEach API runs both a narrow lastTradePrice sum and a checksum that reads all
eight fields. Within a workload, all traversal mechanisms pass every row to the
same resettable accumulator, visit rows in encounter order, and reuse the
Consumer between invocations. Store construction, fixture generation, sealing,
and validation happen in trial setup rather than measured code. The cursor
itself is created inside each cursor operation, matching normal public usage.
For publication-quality measurements, increase warmup, measurement, and fork counts and use the largest configured data set:
java -jar benchmark-jmh/target/benchmarks.jar \
ColumnarProjectionStoreIterationBenchmark \
-p rowCount=10000000 \
-wi 5 \
-i 10 \
-f 2 \
-prof gcThese APIs deliberately have different contracts: the cursor prioritizes
maximum traversal efficiency through a reusable view, viewAt provides stable
views for explicit random access, and forEach provides conventional OO
traversal through stable views. Interpret timing and allocation results in that
ergonomics-versus-efficiency context rather than treating convenience as an
inferior contract.
Run all methods with:
java -jar benchmark-jmh/target/benchmarks.jar \
'CsvFullRowProcessingBenchmark|CsvFilteredPriceSumEndToEndBenchmark|ReadyPriceSumBenchmark|ReadyFilteredPriceSumBenchmark|ReadyPriceAverageBenchmark|MaxByDoubleBenchmark|ReadyMarketDataSnapshotAverageBenchmark|ColumnarProjectionStoreIterationBenchmark'Override the rowCount JMH parameter with -p; the corresponding dataset must
already exist for the CSV-backed benchmarks:
java -jar benchmark-jmh/target/benchmarks.jar \
'CsvFullRowProcessingBenchmark|CsvFilteredPriceSumEndToEndBenchmark|ReadyPriceSumBenchmark|ReadyFilteredPriceSumBenchmark|ReadyPriceAverageBenchmark|MaxByDoubleBenchmark|ReadyMarketDataSnapshotAverageBenchmark|ColumnarProjectionStoreIterationBenchmark' \
-p rowCount=100000Add JMH's GC profiler to collect allocation and garbage-collection metrics:
java -jar benchmark-jmh/target/benchmarks.jar \
'CsvFullRowProcessingBenchmark|CsvFilteredPriceSumEndToEndBenchmark|ReadyPriceSumBenchmark|ReadyFilteredPriceSumBenchmark|ReadyPriceAverageBenchmark|MaxByDoubleBenchmark|ReadyMarketDataSnapshotAverageBenchmark|ColumnarProjectionStoreIterationBenchmark' \
-p rowCount=10000 \
-prof gcThe GC profiler does not directly measure retained heap or peak heap usage.
Dataset generation is separate and unmeasured for all categories. JMH warmup means filesystem and operating-system page-cache effects may be present in the end-to-end comparisons. No performance conclusions should be drawn without running controlled experiments on the intended hardware and dataset sizes.
The Manual Benchmarks workflow is a manually dispatched, artifact-producing
alternative to the Bencher workflow. Choose one suite, one supported row count,
and one execution preset. Each workflow run maps the suite to exactly one
existing benchmark class, generates a CSV dataset only when that class requires
one, and uploads the JMH JSON results together with commit, input, command, Java,
Maven, operating-system, and CPU metadata. The in-memory
ready-price-average and max-by-double suites never use a CSV dataset.
The presets control JMH execution as follows:
smoke: 1 warmup iteration, 1 measurement iteration, and 1 fork;default: 2 warmup iterations, 3 measurement iterations, and 1 fork;extended: 5 warmup iterations, 10 measurement iterations, and 2 forks.
One workflow run executes one comparable benchmark class. Results from a GitHub-hosted runner are suitable for comparing methods within that same controlled run. Separate workflow runs may be scheduled on different hardware, so their results should not be treated as directly comparable without accounting for the recorded environment metadata.