Skip to content

Commit 05fe15e

Browse files
committed
perf: eliminate per-call TypedArray allocations in columnar merge hot path
readColumnValue() created new Float64Array/Int32Array/etc on every call just to read a single element. In the k-way merge this is O(N log K) temporary allocations. Now uses cached DataView for single-element reads. copyColumnValue() created new Uint8Array views per row per column for byte copies. Now caches _u8 view on the column and uses direct byte loop for fixed-width elements (8 bytes = 8 iterations, cheaper than allocating + GC'ing a typed array view). Both caches (_dv, _u8) are lazily created on first access and reused across all subsequent calls on the same column.
1 parent 232d462 commit 05fe15e

File tree

1 file changed

+19
-10
lines changed

1 file changed

+19
-10
lines changed

src/columnar.ts

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@ export interface ColumnarColumn {
5151
vectorDim?: number;
5252
/** Null bitmap: bit set = null. */
5353
nullBitmap?: Uint8Array;
54+
/** @internal Cached DataView for single-element reads (avoids per-call allocation). */
55+
_dv?: DataView;
56+
/** @internal Cached Uint8Array view for bool/byte access. */
57+
_u8?: Uint8Array;
5458
}
5559

5660
/** A batch of columnar data — the unit of zero-copy transfer. */
@@ -517,18 +521,20 @@ export function concatColumnarBatches(batches: ColumnarBatch[]): ColumnarBatch |
517521
/** Read a comparable value from a column at a given row index. */
518522
export function readColumnValue(col: ColumnarColumn, row: number): number | bigint | string | boolean | null {
519523
if (col.nullBitmap && (col.nullBitmap[row >> 3] & (1 << (row & 7)))) return null;
524+
// Use DataView for single-element reads — avoids per-call TypedArray allocation
525+
const dv = col._dv ?? (col._dv = new DataView(col.data));
520526
switch (col.dtype) {
521-
case DTYPE_F64: return new Float64Array(col.data)[row];
522-
case DTYPE_I64: return new BigInt64Array(col.data)[row];
523-
case DTYPE_I32: return new Int32Array(col.data)[row];
524-
case DTYPE_F32: return new Float32Array(col.data)[row];
527+
case DTYPE_F64: return dv.getFloat64(row * 8, true);
528+
case DTYPE_I64: return dv.getBigInt64(row * 8, true);
529+
case DTYPE_I32: return dv.getInt32(row * 4, true);
530+
case DTYPE_F32: return dv.getFloat32(row * 4, true);
525531
case DTYPE_UTF8: {
526532
const offsets = col.offsets!;
527533
const start = offsets[row], end = offsets[row + 1];
528534
return textDecoder.decode(new Uint8Array(col.data, start, end - start));
529535
}
530536
case DTYPE_BOOL: {
531-
const bits = new Uint8Array(col.data);
537+
const bits = col._u8 ?? (col._u8 = new Uint8Array(col.data));
532538
return (bits[row >> 3] & (1 << (row & 7))) !== 0;
533539
}
534540
default: return null;
@@ -545,11 +551,13 @@ function copyColumnValue(
545551
bpe: number,
546552
): void {
547553
if (bpe > 0) {
548-
// Fixed-width: copy bytes
549-
const srcBytes = new Uint8Array(src.data, srcRow * bpe, bpe);
550-
dst.data.set(srcBytes, dstRow * bpe);
554+
// Fixed-width: direct byte copy avoids per-call Uint8Array view allocation
555+
const srcBuf = src._u8 ?? (src._u8 = new Uint8Array(src.data));
556+
const srcStart = srcRow * bpe;
557+
const dstStart = dstRow * bpe;
558+
for (let b = 0; b < bpe; b++) dst.data[dstStart + b] = srcBuf[srcStart + b];
551559
} else if (dtype === DTYPE_BOOL) {
552-
const srcBits = new Uint8Array(src.data);
560+
const srcBits = src._u8 ?? (src._u8 = new Uint8Array(src.data));
553561
if (srcBits[srcRow >> 3] & (1 << (srcRow & 7))) {
554562
dst.data[dstRow >> 3] |= 1 << (dstRow & 7);
555563
}
@@ -560,7 +568,8 @@ function copyColumnValue(
560568
const len = end - start;
561569
dst.offsets![dstRow] = dst.strOffset;
562570
if (len > 0) {
563-
dst.data.set(new Uint8Array(src.data, start, len), dst.strOffset);
571+
const srcBuf = src._u8 ?? (src._u8 = new Uint8Array(src.data));
572+
dst.data.set(srcBuf.subarray(start, end), dst.strOffset);
564573
}
565574
dst.strOffset += len;
566575
}

0 commit comments

Comments
 (0)