You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Investigate a columnar internal representation for JqArray instances that contain JqObjects with identical key sets (arrays of records). Instead of each JqObject storing its own keys[] + values[] arrays (row-oriented), store the data as shared keys + per-field value arrays (column-oriented). This eliminates pointer chasing when querying a single field across all elements.
Background
The access pattern
Many jq queries iterate an array of objects and extract a single field:
Look up column index for "mem.util.used" in sharedKeys (once, using hash index)
Access columns[colIdx] — a single contiguous JqValue[502] array
Sequential memory access through one array — L1-friendly
Profiling data (2026-07-02)
Benchmark
L1-dcache-load-misses
Miss rate
Pattern
prod_topField
0.003
0.01%
Single field access (no iteration)
prod_deepField
0.053
0.02%
Deep field chain (no iteration)
prod_keys
0.704
0.15%
Keys of single object
prod_extractMetric
4,658
5.69%
Iterate 502 objects, access one field each
prod_iterateExtract
3.930
0.44%
Iterate small array
prod_extractMetric has 100x more L1 misses than other benchmarks because it iterates a large array of objects — exactly the pattern columnar layout optimizes.
Pro: zero cost at query time. Con: adds parse-time overhead for arrays that are never iterated by field.
Option B: Lazy conversion on first iteration
Store row-oriented initially. On first .[] + field access pattern, convert to columnar and cache.
Pro: zero cost if the array is never iterated by field. Con: first query pays conversion cost.
Option C: Explicit API
JqArray.toColumnar() for callers who know their data shape.
Pro: no heuristic. Con: requires caller awareness.
Recommendation: Option A (parse-time) since h5m's dominant pattern is parse-once-query-many, and the detection mechanism from #48 is already available.
2. JqValue sealed interface constraint
JqValue is a sealed interface with 6 permitted subtypes. A ColumnarJqArray cannot be a new subtype. Options:
Option A: Internal flag on JqArray
publicfinalclassJqArrayimplementsJqValue {
privatefinalList<JqValue> elements; // row-oriented (null when columnar)privatefinalString[] columnKeys; // columnar keys (null when row-oriented)privatefinalJqValue[][] columns; // columnar values (null when row-oriented)privatefinalint[] columnHashSlots; // columnar hash indexprivatefinalintcolumnHashMask;
privatefinalintrowCount;
}
The get(int index) method returns a view JqObject backed by the column data:
publicJqValueget(intindex) {
if (columns != null) {
// Return a view object that reads from column arraysreturnnewColumnViewJqObject(columnKeys, columns, columnHashSlots, columnHashMask, index);
}
// Fall back to row-orientedreturnelements.get(resolveIndex(index));
}
Option B: Wrapper JqArray
A ColumnarJqArray that wraps column data and is returned from parsing instead of the standard JqArray. Since it's the same JqArray class (just different internal state), it satisfies the sealed constraint.
3. The ColumnViewJqObject
When columnar, get(int rowIndex) returns a lightweight view:
// A JqObject view backed by columnar storage — no allocation except the view object itselfclassColumnViewJqObjectextendsJqObject {
privatefinalJqValue[][] columns;
privatefinalintrowIndex;
JqValueget(Stringkey) {
intcolIdx = hashLookup(key); // uses shared hash indexreturncolumns[colIdx][rowIndex]; // direct array access
}
}
This view object is small (2 references + 1 int) and could be cached/reused during iteration to avoid per-element allocation.
4. Column-major vs row-major
Column-major (JqValue[numFields][numRows]): one array per field. Best for single-field queries (.[] | .x). Each field's values are contiguous.
Row-major (JqValue[numRows][numFields]): one array per row. Best for full-object access (.[] | {a, b, c}). Each row's values are contiguous.
Recommendation: column-major since the dominant query pattern is single-field extraction.
5. Write operations
Columnar layout is read-optimized. JqArray's copy-on-write methods (with(), append()) would need to either:
Convert back to row-oriented on mutation (simple, safe)
Since jjq values are immutable after construction, mutations are rare (only in jq's |= operator and setpath). Converting back to row-oriented on mutation is the pragmatic choice.
6. Non-uniform arrays
Arrays where objects have different key sets remain row-oriented. The detection threshold could be configurable:
100% uniform → columnar (strict, safe)
80% uniform → columnar with row-oriented outliers stored separately
Recommendation: start with 100% uniform (simplest, covers the common case)
Parallels h5m #155 (storage redesign) — the database-level equivalent of row-to-column transformation
The jq VM's COLLECT_ITERATE opcode could be optimized to detect columnar arrays and extract the column directly without per-element get() calls (future optimization)
Summary
Investigate a columnar internal representation for
JqArrayinstances that contain JqObjects with identical key sets (arrays of records). Instead of each JqObject storing its ownkeys[]+values[]arrays (row-oriented), store the data as shared keys + per-field value arrays (column-oriented). This eliminates pointer chasing when querying a single field across all elements.Background
The access pattern
Many jq queries iterate an array of objects and extract a single field:
These are the most common patterns in h5m's rhivos pipeline (30% of all production jq expressions are this shape).
The current layout (row-oriented)
Querying
.["mem.util.used"]on all 502 objects requires:JqObjectpointer → accesshashSlots[]→ accesskeys[]→ accessvalues[]hashSlots[]arrays, 502 differentkeys[]arrays, 502 differentvalues[]arraysThe columnar layout (column-oriented)
Querying
.["mem.util.used"]becomes:"mem.util.used"insharedKeys(once, using hash index)columns[colIdx]— a single contiguousJqValue[502]arrayProfiling data (2026-07-02)
prod_extractMetrichas 100x more L1 misses than other benchmarks because it iterates a large array of objects — exactly the pattern columnar layout optimizes.Design Considerations
1. Detection: when to create columnar layout
Option A: Parse-time detection (builds on #48)
keys[](detected by Share key arrays across JqObjects with identical schemas to reduce heap pressure and cache misses #48's shared key mechanism), convert to columnar after the array is completeOption B: Lazy conversion on first iteration
.[]+ field access pattern, convert to columnar and cache.Option C: Explicit API
JqArray.toColumnar()for callers who know their data shape.Recommendation: Option A (parse-time) since h5m's dominant pattern is parse-once-query-many, and the detection mechanism from #48 is already available.
2. JqValue sealed interface constraint
JqValueis a sealed interface with 6 permitted subtypes. AColumnarJqArraycannot be a new subtype. Options:Option A: Internal flag on JqArray
The
get(int index)method returns a viewJqObjectbacked by the column data:Option B: Wrapper JqArray
A
ColumnarJqArraythat wraps column data and is returned from parsing instead of the standardJqArray. Since it's the sameJqArrayclass (just different internal state), it satisfies the sealed constraint.3. The ColumnViewJqObject
When columnar,
get(int rowIndex)returns a lightweight view:This view object is small (2 references + 1 int) and could be cached/reused during iteration to avoid per-element allocation.
4. Column-major vs row-major
JqValue[numFields][numRows]): one array per field. Best for single-field queries (.[] | .x). Each field's values are contiguous.JqValue[numRows][numFields]): one array per row. Best for full-object access (.[] | {a, b, c}). Each row's values are contiguous.Recommendation: column-major since the dominant query pattern is single-field extraction.
5. Write operations
Columnar layout is read-optimized. JqArray's copy-on-write methods (
with(),append()) would need to either:Since jjq values are immutable after construction, mutations are rare (only in jq's
|=operator andsetpath). Converting back to row-oriented on mutation is the pragmatic choice.6. Non-uniform arrays
Arrays where objects have different key sets remain row-oriented. The detection threshold could be configurable:
Estimated Impact
prod_extractMetric (
[.pcp_time_series[] | .["mem.util.used"]])Other production queries
Any query matching
[.array[] | .field]benefits:[.data[] | .sample_uuid]— same pattern[.stressng_workload[] | .test_results]— same pattern{user, uuid, run_id}on single objects — no benefit (not an array iteration)Implementation Plan
prod_extractMetricwith-prof perfnormwith()/append()convert to row-orientedAcceptance Criteria
get(int)returns a ColumnViewJqObject that reads from column arraysJqObject.get(String)on column views uses the shared hash indexwith,append) work correctly (convert to row-oriented)Relationship to Other Issues
COLLECT_ITERATEopcode could be optimized to detect columnar arrays and extract the column directly without per-elementget()calls (future optimization)References