@@ -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.**
150147Hardwood'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
152149access. ` DoubleArray.fold() ` is a tight loop over a flat ` MemorySegment ` ; the JIT sees a
153150scalar reduction over contiguous memory with no dispatch overhead.
154151
155- ** 3 . mmap zero-copy.**
152+ ** 2 . mmap zero-copy.**
156153Vortex 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```
166160Hardwood 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 `
0 commit comments