The performance is expected to be worse than preallocated buffers with correct size.
The core idea is to manage data in fixed-size blocks (Float32Array
s) and only combine them into a single, contiguous Float32Array
when explicitly requested. This avoids the overhead of dynamic resizing that standard JavaScript arrays incur.
This can be used to replace Array.push
when final size is not known at creation.
Memory blocks not pooled yet (and unsure if really needed).
Code is created with the help of AI.
/**
* A block-based builder for Float32Array, optimized for performance.
* It allocates memory in fixed-size blocks and consolidates them into a single
* Float32Array upon request.
*/
class BlockBasedFloat32ArrayBuilder {
/**
* @param {number} blockSize - The size of each block in number of elements.
* A power of 2 is often a good choice for performance.
* @param {number} [initialCapacity=blockSize] - The initial total capacity in elements.
*/
constructor(blockSize, initialCapacity = blockSize) {
if (typeof blockSize !== 'number' || blockSize <= 0 || !Number.isInteger(blockSize)) {
throw new Error("blockSize must be a positive integer.");
}
this.blockSize = blockSize;
this.blocks = [];
this.totalElements = 0;
this.currentBlockIndex = 0;
this.currentOffset = 0;
// Pre-allocate initial blocks up to initialCapacity
const initialBlocksNeeded = Math.ceil(initialCapacity / blockSize);
for (let i = 0; i < initialBlocksNeeded; i++) {
this.allocateNewBlock();
}
}
/**
* Allocates a new block of memory and adds it to the internal blocks list.
* @private
*/
allocateNewBlock() {
const newBlock = new Float32Array(this.blockSize);
this.blocks.push(newBlock);
// If this is the first block, it's the current one.
// Otherwise, the currentOffset should be reset when switching to a new block.
if (this.blocks.length === 1) {
this.currentBlockIndex = 0;
this.currentOffset = 0;
}
}
/**
* Ensures there is enough capacity for at least one more element.
* Allocates a new block if the current one is full.
* @private
*/
ensureCapacity() {
if (this.currentOffset >= this.blockSize) {
this.currentBlockIndex++;
this.currentOffset = 0;
// If we've run out of pre-allocated blocks, create a new one.
if (this.currentBlockIndex >= this.blocks.length) {
this.allocateNewBlock();
}
}
}
/**
* Adds a single number to the builder.
* @param {number} value - The number to add.
*/
push(value) {
this.ensureCapacity();
this.blocks[this.currentBlockIndex][this.currentOffset] = value;
this.currentOffset++;
this.totalElements++;
}
/**
* Adds all elements from a Float32Array to the builder.
* Optimized to copy chunks efficiently using `set()`.
* @param {Float32Array} float32Array - The array to add.
*/
addArray(float32Array) {
if (!(float32Array instanceof Float32Array)) {
throw new Error("Input must be a Float32Array.");
}
let elementsToAdd = float32Array.length;
let sourceOffset = 0;
while (elementsToAdd > 0) {
this.ensureCapacity(); // Ensure current block has space
const currentBlock = this.blocks[this.currentBlockIndex];
const remainingInBlock = this.blockSize - this.currentOffset;
const elementsToCopy = Math.min(elementsToAdd, remainingInBlock);
if (elementsToCopy > 0) {
// Use set() for efficient bulk copying
currentBlock.set(float32Array.subarray(sourceOffset, sourceOffset + elementsToCopy), this.currentOffset);
this.currentOffset += elementsToCopy;
sourceOffset += elementsToCopy;
elementsToAdd -= elementsToCopy;
this.totalElements += elementsToCopy;
}
// If elementsToCopy was 0, it means remainingInBlock was 0,
// so ensureCapacity() would have already moved to the next block.
}
}
/**
* Returns the total number of elements currently stored.
* @returns {number}
*/
get length() {
return this.totalElements;
}
/**
* Consolidates all internal blocks into a single Float32Array.
* This is an O(N) operation where N is the total number of elements.
* @returns {Float32Array} A new Float32Array containing all added elements.
*/
toArray() {
if (this.totalElements === 0) {
return new Float32Array(0);
}
// Calculate the exact size needed for the final array
const finalArray = new Float32Array(this.totalElements);
let currentFinalOffset = 0;
// Copy elements from each block to the final array
for (const block of this.blocks) {
// Determine how many elements from this block are actually used
// This is important if the last block is not completely full.
const elementsInBlock = Math.min(block.length, this.totalElements - currentFinalOffset);
if (elementsInBlock > 0) {
finalArray.set(block.subarray(0, elementsInBlock), currentFinalOffset);
currentFinalOffset += elementsInBlock;
}
if (currentFinalOffset >= this.totalElements) {
break; // Stop if we've copied all elements
}
}
return finalArray;
}
/**
* Clears the builder, freeing up memory and resetting its state.
*/
clear() {
this.blocks = [];
this.totalElements = 0;
this.currentBlockIndex = 0;
this.currentOffset = 0;
// Optionally, re-allocate the first block to reset capacity
this.allocateNewBlock();
}
}
// --- Example Usage ---
// Create a builder with a block size of 1024 elements
const builder = new BlockBasedFloat32ArrayBuilder(1024);
// Add individual numbers
for (let i = 0; i < 500; i++) {
builder.push(i * 0.1);
}
// Add another array
const largeArray = new Float32Array(2000);
for (let i = 0; i < largeArray.length; i++) {
largeArray[i] = Math.sin(i * 0.01);
}
builder.addArray(largeArray);
// Add more individual numbers
for (let i = 0; i < 700; i++) {
builder.push(Math.cos(i * 0.02));
}
// Get the final consolidated Float32Array
const finalArray = builder.toArray();
console.log(`Builder length: ${builder.length}`); // Expected: 500 + 2000 + 700 = 3200
console.log(`Final array length: ${finalArray.length}`); // Expected: 3200
console.log(`First 10 elements: ${finalArray.slice(0, 10)}`);
console.log(`Elements around the largeArray boundary (index 500-510): ${finalArray.slice(500, 511)}`);
console.log(`Last 10 elements: ${finalArray.slice(-10)}`);
// Example of clearing and reusing
builder.clear();
console.log(`Builder length after clear: ${builder.length}`); // Expected: 0
builder.push(1.1);
builder.push(2.2);
console.log(`Builder length after adding 2 elements: ${builder.length}`); // Expected: 2
console.log(`Final array after clear and re-add: ${builder.toArray()}`); // Expected: Float32Array(2) [ 1.1, 2.2 ]