Skip to content

Commit deea8ad

Browse files
dfa1claude
andcommitted
perf: fair benchmark — import all 19 taxi cols, project at read time
Previous run imported only 3 columns into Vortex (smaller file = unfair advantage). Now both formats hold all 19 columns; projection happens at read time. Results (scalar-vs-scalar, 3 M rows): parquetRead 1 col: 81.26 ops/s (12.3 ms) vortexRead 1 col: 151.96 ops/s (6.6 ms) → 1.9× parquetReadMultiColumn: 46.80 ops/s (21.4 ms) vortexReadMultiColumn: 57.65 ops/s (17.4 ms) → 1.2× Vortex file is now 76 MB vs Parquet's 47.6 MB — compressor does not yet beat ZSTD on this mixed dataset. Speedup comes from batch columnar API and mmap zero-copy, not file size. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent b21dc62 commit deea8ad

2 files changed

Lines changed: 24 additions & 31 deletions

File tree

docs/explanation.md

Lines changed: 22 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -124,53 +124,51 @@ The Java write is faster but also produces bigger files (more optimization work
124124
|-----------|--------------|------------------|--------------|
125125
| scan | 20.4 ± 0.9 | 5.7 ± 0.6 | **3.6×** |
126126

127-
### Parquet vs Vortex read — NYC Yellow Taxi 2024-01, 3 M rows, 3 columns
127+
### Parquet vs Vortex read — NYC Yellow Taxi 2024-01, 3 M rows, 19 columns
128128

129-
Source: 47.6 MB Parquet → 12.0 MB Vortex (4× compression). Both sides scalar decode
129+
Both formats store all 19 columns; projection happens at read time. Both sides scalar decode
130130
(Hardwood disables SIMD on JDK 25; Vortex Java uses FFM scalar reads throughout).
131+
File sizes: Parquet 47.6 MB, Vortex 76.0 MB (Vortex is larger here — cascading compressor
132+
does not yet beat Parquet's ZSTD on this mixed dataset).
131133

132134
| Benchmark | ops/s | ms/scan | vs Parquet |
133135
|---|---|---|---|
134-
| `parquetRead` — Hardwood, 1 col (`trip_distance`) | 66.96 ± 1.99 | 14.9 ms | baseline |
135-
| `vortexRead` — 1 col (`trip_distance`) | 201.61 ± 1.63 | 4.96 ms | **3.0×** |
136-
| `parquetReadMultiColumn` — 2 cols (`fare_amount`, `PULocationID`) | 53.07 ± 1.25 | 18.8 ms | baseline |
137-
| `vortexReadMultiColumn` — 2 cols (`fare_amount`, `PULocationID`) | 140.31 ± 5.42 | 7.13 ms | **2.6×** |
136+
| `parquetRead` — Hardwood, 1 col (`trip_distance`) | 81.26 ± 0.76 | 12.3 ms | baseline |
137+
| `vortexRead` — 1 col (`trip_distance`) | 151.96 ± 0.84 | 6.58 ms | **1.9×** |
138+
| `parquetReadMultiColumn` — 2 cols (`fare_amount`, `PULocationID`) | 46.80 ± 0.90 | 21.4 ms | baseline |
139+
| `vortexReadMultiColumn` — 2 cols (`fare_amount`, `PULocationID`) | 57.65 ± 0.58 | 17.4 ms | **1.2×** |
138140

139-
#### Why Vortex is faster than Parquet here
141+
#### Why Vortex is faster despite being a larger file
140142

141-
Three compounding factors:
143+
The file size advantage is gone on this dataset — Vortex is 60% larger than the source Parquet.
144+
The speedup comes from two other factors:
142145

143-
**1. Smaller file = less I/O bandwidth.**
144-
ALP encodes taxi floats very compactly (fare amounts cluster around small integers, trip
145-
distances are short floats). Parquet uses PLAIN or BYTE_STREAM_SPLIT for doubles — 8 raw
146-
bytes per value. 47.6 MB → 12.0 MB means 4× less memory to read even when the file is
147-
hot in the OS page cache.
148-
149-
**2. Batch columnar API vs row-by-row cursor.**
146+
**1. Batch columnar API vs row-by-row cursor.**
150147
Hardwood's `RowReader` requires `rows.next()` + `rows.getDouble("trip_distance")` per row
151148
— 2 virtual calls × 3 M rows = 6 M calls, plus a string-keyed column lookup on every
152149
access. `DoubleArray.fold()` is a tight loop over a flat `MemorySegment`; the JIT sees a
153150
scalar reduction over contiguous memory with no dispatch overhead.
154151

155-
**3. mmap zero-copy.**
152+
**2. mmap zero-copy.**
156153
Vortex reads directly from the mmap'd `MemorySegment` — the file bytes _are_ the decode
157-
input, no intermediate copies. Hardwood reads into internal page buffers and then
158-
materialises values into a row cursor (one extra copy per page).
159-
160-
Parquet also pays per-page overhead absent in Vortex: RLE-encoded definition/repetition
161-
levels for every page (even for non-null columns), page header parsing, and optional
162-
dictionary decode. Vortex's layout is a flat array of encoded values with no per-row
163-
framing.
154+
input, no intermediate copies. Hardwood reads into internal page buffers and materialises
155+
values into a row cursor (one extra copy per page). Parquet also pays per-page framing
156+
overhead: RLE-encoded definition/repetition levels, page header parsing, optional dictionary
157+
decode. Vortex's layout is a flat array of encoded values with no per-row framing.
164158

165159
```
166160
Hardwood parquetRead (per 3 M rows) Vortex vortexRead (per 3 M rows)
167161
──────────────────────────────────── ──────────────────────────────────
168-
47.6 MB read 12.0 MB read (4× less bandwidth)
162+
47.6 MB on disk (smaller file) 76.0 MB on disk (larger file)
169163
+ page header parse × N pages + ALP decode (branch-free ×/+)
170164
+ definition-level RLE decode × 3 M rows + fold() tight loop, no dispatch
171165
+ getDouble("col") × 3 M virtual calls
172166
```
173167

168+
The multi-column gap narrows to 1.2× because projecting 2 of 19 columns forces Vortex
169+
to skip more data per scan; Parquet's column-chunk layout already isolates each column
170+
on disk, so its per-column I/O is better amortised when reading multiple columns.
171+
174172
## Design principles
175173

176174
- Zero-copy everywhere via FFM `MemorySegment`

performance/src/main/java/io/github/dfa1/vortex/performance/ParquetVsVortexReadBenchmark.java

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
import io.github.dfa1.vortex.core.array.IntArray;
99
import io.github.dfa1.vortex.encoding.EncodingRegistry;
1010
import io.github.dfa1.vortex.io.VortexReader;
11-
import io.github.dfa1.vortex.parquet.ImportOptions;
1211
import io.github.dfa1.vortex.parquet.ParquetImporter;
1312
import io.github.dfa1.vortex.scan.ScanResult;
1413
import org.openjdk.jmh.annotations.Benchmark;
@@ -32,7 +31,6 @@
3231
import java.net.http.HttpResponse;
3332
import java.nio.file.Files;
3433
import java.nio.file.Path;
35-
import java.util.List;
3634
import java.util.concurrent.TimeUnit;
3735

3836
/// Benchmark: Hardwood Parquet read vs Java Vortex read on the same real-world dataset.
@@ -62,8 +60,6 @@ public class ParquetVsVortexReadBenchmark {
6260
"https://d37ci6vzurychx.cloudfront.net/trip-data/yellow_tripdata_2024-01.parquet";
6361
private static final Path CACHE_PATH =
6462
Path.of(System.getProperty("java.io.tmpdir"), "yellow_tripdata_2024-01.parquet");
65-
private static final List<String> COLUMNS = List.of("trip_distance", "fare_amount", "PULocationID");
66-
6763
private static final Object SETUP_LOCK = new Object();
6864
private static Path sharedParquetFile;
6965
private static Path sharedVortexFile;
@@ -88,9 +84,8 @@ public void setup() throws Exception {
8884
sharedParquetFile, Files.size(sharedParquetFile) / 1_048_576.0);
8985
}
9086
sharedVortexFile = Files.createTempFile("taxi-bench", ".vortex");
91-
System.out.print("[ParquetVsVortexReadBenchmark] converting to vortex...");
92-
ImportOptions opts = ImportOptions.defaults().withColumns(COLUMNS);
93-
ParquetImporter.importParquet(sharedParquetFile, sharedVortexFile, opts);
87+
System.out.print("[ParquetVsVortexReadBenchmark] converting to vortex (all columns)...");
88+
ParquetImporter.importParquet(sharedParquetFile, sharedVortexFile);
9489
System.out.printf(" done (%.1f MB)%n", Files.size(sharedVortexFile) / 1_048_576.0);
9590
}
9691
refCount++;

0 commit comments

Comments
 (0)