Skip to content

Commit df67017

Browse files
committed
Improve performance on average by 20-30%
1 parent 1199c7a commit df67017

2 files changed

Lines changed: 67 additions & 48 deletions

File tree

src/encoding/StringEncoder.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,16 @@ export default class StringEncoder implements Serializable<string> {
1111
private encoderArray: Uint8Array = new Uint8Array(this.encoderBuffer);
1212
private currentDecoderBufferSize: number = StringEncoder.ENCODER_BUFFER_SIZE;
1313

14+
// Checked once at construction time to avoid a property lookup on every encode call.
15+
// Safari does not support encodeInto; all other modern environments do.
16+
private readonly useEncodeInto: boolean = this.textEncoder.encodeInto !== undefined;
17+
1418
decode(buffer: Uint8Array): string {
1519
return this.textDecoder.decode(buffer);
1620
}
1721

1822
encode(stringValue: string, destination: Uint8Array): number {
19-
// Safari does not support the encodeInto function
20-
if (this.textEncoder.encodeInto !== undefined) {
23+
if (this.useEncodeInto) {
2124
const maxStringLength = stringValue.length * 3;
2225

2326
if (this.currentDecoderBufferSize < maxStringLength) {

src/map/ShareableMap.ts

Lines changed: 62 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,11 @@ export class ShareableMap<K, V> extends TransferableDataStructure {
6060
private indexView!: DataView;
6161
private dataView!: DataView;
6262

63+
// Cached Int32 view of the index buffer — used exclusively for Atomics lock operations.
64+
// The lock word sits at a fixed offset in the metadata region, so this view never needs
65+
// to be refreshed even after SharedArrayBuffer.grow() calls.
66+
private int32LockArray: Int32Array | null = null;
67+
6368
private textDecoder: TextDecoder = new TextDecoder();
6469

6570
private readonly stringEncoder = new StringEncoder();
@@ -160,6 +165,7 @@ export class ShareableMap<K, V> extends TransferableDataStructure {
160165
protected setBuffers(indexBuffer: SharedArrayBuffer | ArrayBuffer, dataBuffer: SharedArrayBuffer | ArrayBuffer) {
161166
this.indexMem = indexBuffer;
162167
this.indexView = new DataView(this.indexMem);
168+
this.int32LockArray = indexBuffer instanceof SharedArrayBuffer ? new Int32Array(indexBuffer) : null;
163169
this.dataMem = dataBuffer;
164170
this.dataView = new DataView(this.dataMem);
165171
}
@@ -384,28 +390,36 @@ export class ShareableMap<K, V> extends TransferableDataStructure {
384390
}
385391

386392
if (needsToBeStored) {
393+
// Read freeStart once into a local to avoid repeated getUint32 calls on the shared buffer.
394+
let freeStart = this.freeStart;
395+
387396
// Determine if the data storage needs to be resized.
388-
if (maxKeyLength + maxValueLength + this.freeStart + ShareableMap.DATA_OBJECT_OFFSET > this.dataView.byteLength) {
397+
if (maxKeyLength + maxValueLength + freeStart + ShareableMap.DATA_OBJECT_OFFSET > this.dataView.byteLength) {
389398
// We don't have enough space left at the end of the data array. We should now consider if we should just
390399
// perform a defragmentation of the data array, or if we need to double the size of the array.
391-
const defragRatio = this.spaceUsedInDataPartition / this.dataView.byteLength;
400+
const usedSpace = this.spaceUsedInDataPartition;
401+
const defragRatio = usedSpace / this.dataView.byteLength;
392402

393403
if (
394404
defragRatio < ShareableMap.MIN_DEFRAG_FACTOR &&
395-
this.spaceUsedInDataPartition + maxKeyLength + maxValueLength + ShareableMap.DATA_OBJECT_OFFSET < this.dataView.byteLength
405+
usedSpace + maxKeyLength + maxValueLength + ShareableMap.DATA_OBJECT_OFFSET < this.dataView.byteLength
396406
) {
397407
this.defragment();
398408
} else {
399409
this.doubleDataStorage();
400410
}
401411

412+
// Re-read freeStart: defragment() compacts and updates it; doubleDataStorage() may
413+
// have changed the DataView but freeStart stays the same (no re-read strictly needed
414+
// after grow, but we keep it uniform).
415+
freeStart = this.freeStart;
402416
}
403417

404418
const exactKeyLength = this.stringEncoder.encode(
405419
keyString,
406420
new Uint8Array(
407421
this.dataMem,
408-
ShareableMap.DATA_OBJECT_OFFSET + this.freeStart,
422+
ShareableMap.DATA_OBJECT_OFFSET + freeStart,
409423
maxKeyLength
410424
)
411425
);
@@ -414,36 +428,38 @@ export class ShareableMap<K, V> extends TransferableDataStructure {
414428
value,
415429
new Uint8Array(
416430
this.dataMem,
417-
ShareableMap.DATA_OBJECT_OFFSET + this.freeStart + exactKeyLength,
431+
ShareableMap.DATA_OBJECT_OFFSET + freeStart + exactKeyLength,
418432
maxValueLength
419433
)
420434
);
421435

422436
// Store key length
423-
this.dataView.setUint32(this.freeStart + 4, exactKeyLength);
437+
this.dataView.setUint32(freeStart + 4, exactKeyLength);
424438
// Store value length
425-
this.dataView.setUint32(this.freeStart + 8, exactValueLength);
439+
this.dataView.setUint32(freeStart + 8, exactValueLength);
426440
// Keep track of key and value datatypes
427-
this.dataView.setUint16(this.freeStart + 12, typeof key === "string" ? 1 : 0);
428-
this.dataView.setUint16(this.freeStart + 14, valueEncoderId);
429-
this.dataView.setUint32(this.freeStart + 16, hash);
441+
this.dataView.setUint16(freeStart + 12, typeof key === "string" ? 1 : 0);
442+
this.dataView.setUint16(freeStart + 14, valueEncoderId);
443+
this.dataView.setUint32(freeStart + 16, hash);
430444

431-
this.spaceUsedInDataPartition += ShareableMap.DATA_OBJECT_OFFSET + exactKeyLength + exactValueLength;
445+
const itemSize = ShareableMap.DATA_OBJECT_OFFSET + exactKeyLength + exactValueLength;
446+
this.spaceUsedInDataPartition += itemSize;
432447

433-
startPos = this.freeStart;
434-
this.freeStart += ShareableMap.DATA_OBJECT_OFFSET + exactKeyLength + exactValueLength;
448+
startPos = freeStart;
449+
this.freeStart = freeStart + itemSize;
435450

436451
// Increase size of the map since we added a new element.
437452
this.increaseSize();
438453

439454
const bucketPointer = this.indexView.getUint32(bucket + ShareableMap.INDEX_TABLE_OFFSET);
440455
if (bucketPointer === 0) {
441456
this.incrementBucketsInUse();
442-
this.indexView.setUint32(bucket + ShareableMap.INDEX_TABLE_OFFSET, startPos);
457+
// next pointer at startPos is already 0 (zero-initialised data region)
443458
} else {
444-
// Update linked list pointers
445-
this.updateLinkedPointer(bucketPointer, startPos, this.dataView);
459+
// Prepend: new item's next points to the current chain head — O(1) vs O(chain)
460+
this.dataView.setUint32(startPos, bucketPointer);
446461
}
462+
this.indexView.setUint32(bucket + ShareableMap.INDEX_TABLE_OFFSET, startPos);
447463

448464
// If the load factor exceeds the recommended value, we need to rehash the map to make sure performance stays
449465
// acceptable.
@@ -550,8 +566,9 @@ export class ShareableMap<K, V> extends TransferableDataStructure {
550566

551567
private computeHashAndBucket(key: string): [number, number] {
552568
const hash: number = fast1a32(key);
553-
// Bucket in which this value should be stored.
554-
const bucket = (hash % this.buckets) * ShareableMap.INT_SIZE;
569+
// Bucket in which this value should be stored. Because the bucket count is always a
570+
// power of two, a bitwise AND mask is equivalent to modulo and avoids integer division.
571+
const bucket = (hash & (this.buckets - 1)) * ShareableMap.INT_SIZE;
555572
return [hash, bucket];
556573
}
557574

@@ -578,20 +595,13 @@ export class ShareableMap<K, V> extends TransferableDataStructure {
578595

579596
const totalLength = keyLength + valueLength + ShareableMap.DATA_OBJECT_OFFSET;
580597

581-
for (let i = 0; i < totalLength; i++) {
582-
newView.setUint8(newOffset + i, this.dataView.getUint8(dataPointer + i));
583-
}
584-
585-
// Pointer to next block is zero
586-
newView.setUint32(newOffset, 0);
598+
new Uint8Array(newData, newOffset, totalLength)
599+
.set(new Uint8Array(this.dataMem, dataPointer, totalLength));
587600

601+
// Prepend this item at the head of the bucket's chain in the new layout.
588602
const currentBucketLink = this.indexView.getUint32(ShareableMap.INDEX_TABLE_OFFSET + bucket * ShareableMap.INT_SIZE);
589-
if (currentBucketLink === 0) {
590-
this.indexView.setUint32(ShareableMap.INDEX_TABLE_OFFSET + bucket * ShareableMap.INT_SIZE, newOffset);
591-
} else {
592-
// We need to follow the links from the first block here and update those.
593-
this.updateLinkedPointer(currentBucketLink, newOffset, newView);
594-
}
603+
newView.setUint32(newOffset, currentBucketLink);
604+
this.indexView.setUint32(ShareableMap.INDEX_TABLE_OFFSET + bucket * ShareableMap.INT_SIZE, newOffset);
595605

596606
newOffset += totalLength;
597607
dataPointer = this.dataView.getUint32(dataPointer);
@@ -651,21 +661,22 @@ export class ShareableMap<K, V> extends TransferableDataStructure {
651661
);
652662
while (startPos !== 0) {
653663
const hash = this.readHashFromDataObject(startPos);
654-
const newBucket = hash % newBuckets;
664+
const newBucket = hash & (newBuckets - 1);
655665
const existing = tempView.getUint32(
656666
ShareableMap.INDEX_TABLE_OFFSET + newBucket * ShareableMap.INT_SIZE
657667
);
668+
const next = this.dataView.getUint32(startPos);
658669
if (existing === 0) {
659670
bucketsInUse++;
660-
tempView.setUint32(
661-
ShareableMap.INDEX_TABLE_OFFSET + newBucket * ShareableMap.INT_SIZE,
662-
startPos
663-
);
671+
this.dataView.setUint32(startPos, 0); // terminate chain
664672
} else {
665-
this.updateLinkedPointer(existing, startPos, this.dataView);
673+
// Prepend: new item's next points to current head — O(1)
674+
this.dataView.setUint32(startPos, existing);
666675
}
667-
const next = this.dataView.getUint32(startPos);
668-
this.dataView.setUint32(startPos, 0);
676+
tempView.setUint32(
677+
ShareableMap.INDEX_TABLE_OFFSET + newBucket * ShareableMap.INT_SIZE,
678+
startPos
679+
);
669680
startPos = next;
670681
}
671682
}
@@ -828,14 +839,20 @@ export class ShareableMap<K, V> extends TransferableDataStructure {
828839
// Fourth set of 4 bytes keep tracks of the DataBuffer's length.
829840
// Fifth set of 4 bytes keeps track of the space that's being used in total (to track the defrag factor).
830841
// Rest of the index maps buckets onto their starting position in the data array.
831-
const buckets = Math.ceil(expectedSize / ShareableMap.LOAD_FACTOR)
842+
//
843+
// Round up to the next power of two so computeHashAndBucket can use a fast bitwise
844+
// AND mask instead of modulo. doubleIndexStorage always doubles, so subsequent counts
845+
// remain powers of two as well.
846+
const rawBuckets = Math.ceil(expectedSize / ShareableMap.LOAD_FACTOR);
847+
const buckets = rawBuckets <= 1 ? 1 : 2 ** Math.ceil(Math.log2(rawBuckets));
832848
const indexSize = 5 * 4 + buckets * ShareableMap.INT_SIZE;
833849

834850
const maxDataBytes = this.originalOptions.maxBytes ?? ShareableMap.DEFAULT_MAX_DATA_BYTES;
835851
const maxIndexBytes = ShareableMap.DEFAULT_MAX_INDEX_BYTES;
836852

837853
this.indexMem = this.allocateMemory(indexSize, maxIndexBytes);
838854
this.indexView = new DataView(this.indexMem);
855+
this.int32LockArray = this.indexMem instanceof SharedArrayBuffer ? new Int32Array(this.indexMem) : null;
839856

840857
// Free space starts from position 1 in the data array (instead of 0, which we use to indicate the end).
841858
this.indexView.setUint32(ShareableMap.INDEX_FREE_START_INDEX_OFFSET, ShareableMap.INITIAL_DATA_OFFSET);
@@ -868,7 +885,7 @@ export class ShareableMap<K, V> extends TransferableDataStructure {
868885
return true;
869886
}
870887

871-
const int32Array = new Int32Array(this.indexMem);
888+
const int32Array = this.int32LockArray!;
872889
const lockIdx = ShareableMap.INDEX_LOCK_OFFSET / 4;
873890
const startTime = Date.now();
874891

@@ -907,7 +924,7 @@ export class ShareableMap<K, V> extends TransferableDataStructure {
907924
return;
908925
}
909926

910-
const int32Array = new Int32Array(this.indexMem);
927+
const int32Array = this.int32LockArray!;
911928
const lockIdx = ShareableMap.INDEX_LOCK_OFFSET / 4;
912929

913930
// Decrement the reader count. Atomics.sub returns the value *before* subtraction,
@@ -935,7 +952,7 @@ export class ShareableMap<K, V> extends TransferableDataStructure {
935952
return true;
936953
}
937954

938-
const int32Array = new Int32Array(this.indexMem);
955+
const int32Array = this.int32LockArray!;
939956
const lockIdx = ShareableMap.INDEX_LOCK_OFFSET / 4;
940957
const startTime = Date.now();
941958

@@ -973,7 +990,7 @@ export class ShareableMap<K, V> extends TransferableDataStructure {
973990
return;
974991
}
975992

976-
const int32Array = new Int32Array(this.indexMem);
993+
const int32Array = this.int32LockArray!;
977994
const lockIdx = ShareableMap.INDEX_LOCK_OFFSET / 4;
978995

979996
Atomics.store(int32Array, lockIdx, 0);
@@ -984,9 +1001,8 @@ export class ShareableMap<K, V> extends TransferableDataStructure {
9841001
* Initialise the lock word to the unlocked state (0).
9851002
*/
9861003
private initializeLockState(): void {
987-
if (this.indexMem instanceof SharedArrayBuffer) {
988-
const int32Array = new Int32Array(this.indexMem);
989-
Atomics.store(int32Array, ShareableMap.INDEX_LOCK_OFFSET / 4, 0);
1004+
if (this.int32LockArray !== null) {
1005+
Atomics.store(this.int32LockArray, ShareableMap.INDEX_LOCK_OFFSET / 4, 0);
9901006
}
9911007
}
9921008
}

0 commit comments

Comments
 (0)