diff --git a/assembly/Histogram.ts b/assembly/Histogram.ts index a37478d..c649878 100644 --- a/assembly/Histogram.ts +++ b/assembly/Histogram.ts @@ -686,6 +686,11 @@ export default class Histogram extends AbstractHistogramBase { } incrementCountAtIndex(index: i32): void { + if (idof() == idof()) { + // One set-tree walk per populated byte, instead of a get plus a set. + changetype(this.counts).increment(index); + return; + } // @ts-ignore const currentCount = unchecked(this.counts[index]); const newCount = currentCount + 1; @@ -704,6 +709,13 @@ export default class Histogram extends AbstractHistogramBase { } addToCountAtIndex(index: i32, value: u64): void { + if (idof() == idof() && value > 0) { + // Positive adds walk only the bytes they touch. Wrapped negative values + // (from subtract) keep going through get + set, which does not inflate + // every byte set of the packed array. + changetype(this.counts).add(index, value); + return; + } // @ts-ignore const currentCount = unchecked(this.counts[index]); const newCount = currentCount + value; diff --git a/assembly/__tests__/PackedArrayAccess.test.ts b/assembly/__tests__/PackedArrayAccess.test.ts new file mode 100644 index 0000000..31c2333 --- /dev/null +++ b/assembly/__tests__/PackedArrayAccess.test.ts @@ -0,0 +1,151 @@ +/* + * Cross-checks PackedArray get/set/add (with its leaf cache) against a plain + * array model, including interleaved reads and writes within a leaf group, + * carries across bytes, clear and resize. + */ + +import { describe, test, expect } from "assemblyscript-unittest-framework/assembly"; +import { PackedArray } from "../packedarray/PackedArray"; + +let seed: u32 = 777; +function nextRandom(): u32 { + seed = seed * 1664525 + 1013904223; + return seed; +} + +function mismatches(packed: PackedArray, model: StaticArray): i32 { + let count = 0; + // forward scan (cache hits), then random access (cache misses) + for (let i = 0; i < model.length; i++) { + if (packed.get(i) != model[i]) count++; + } + for (let n = 0; n < model.length; n++) { + const i = (nextRandom() % model.length); + if (packed.get(i) != model[i]) count++; + } + return count; +} + +describe("Packed Array add", () => { + test("add should carry into the next byte", () => { + const packed = new PackedArray(1024); + packed.add(42, 200); + packed.add(42, 100); + expect(packed.get(42)).equal(300); + }); + + test("increment should carry across several bytes", () => { + const packed = new PackedArray(1024); + packed.set(7, 0xffff); + packed.increment(7); + expect(packed.get(7)).equal(0x10000); + }); + + test("add should reach the top byte", () => { + const packed = new PackedArray(1024); + packed.add(3, 0x0100000000000000); + packed.add(3, 0xff00000000000000); + packed.add(3, 1); + expect(packed.get(3)).equal(1); + }); +}); + +describe("Packed Array leaf cache", () => { + test("reads within a group see writes made between them", () => { + const packed = new PackedArray(64); + expect(packed.get(16)).equal(0); // caches group 2 as empty + packed.set(17, 5); // inserts set 0 leaf for group 2 + expect(packed.get(16)).equal(0); + expect(packed.get(17)).equal(5); + packed.set(18, 0x1234); // inserts set 1 leaf for group 2 + expect(packed.get(18)).equal(0x1234); + expect(packed.get(17)).equal(5); + packed.add(17, 0x100); // set 1 already present, written through + expect(packed.get(17)).equal(0x105); + packed.add(19, 0x10000); // inserts set 2 leaf while group is cached + expect(packed.get(19)).equal(0x10000); + expect(packed.get(17)).equal(0x105); + }); + + test("set to a smaller value clears the higher bytes", () => { + const packed = new PackedArray(64); + packed.set(9, 0x010203); + expect(packed.get(9)).equal(0x010203); + packed.set(9, 7); + expect(packed.get(9)).equal(7); + packed.set(9, 0); + expect(packed.get(9)).equal(0); + }); + + test("random set / add / get matches a plain array", () => { + const length = 5000; + const packed = new PackedArray(length); + const model = new StaticArray(length); + for (let n = 0; n < 20000; n++) { + const i = (nextRandom() % length); + const op = nextRandom() % 3; + if (op == 0) { + // counts below 2^24: a packed array is meant for sparse, small values and + // its 16 bit entry indexes cap the physical size (same as the Java version) + const v = (nextRandom() % 16777216); + packed.set(i, v); + model[i] = v; + } else if (op == 1) { + const v = (nextRandom() % 100000); + packed.add(i, v); + model[i] += v; + } else { + // read in the middle of writes, keeps the cache exercised + if (packed.get(i) != model[i]) { + expect(packed.get(i)).equal(model[i]); + } + } + } + expect(mismatches(packed, model)).equal(0); + }); + + test("clear invalidates the cache", () => { + const packed = new PackedArray(256); + packed.set(100, 42); + expect(packed.get(100)).equal(42); + packed.clear(); + expect(packed.get(100)).equal(0); + packed.set(101, 1); + expect(packed.get(100)).equal(0); + expect(packed.get(101)).equal(1); + }); + + test("resize keeps values readable", () => { + const packed = new PackedArray(256, 16); + const model = new StaticArray(4096); + for (let i = 0; i < 256; i += 3) { + const v = (nextRandom() % 1000000); + packed.set(i, v); + model[i] = v; + } + expect(packed.get(255)).equal(model[255]); + packed.resize(4096); + for (let i = 256; i < 4096; i += 7) { + const v = (nextRandom() % 1000000); + packed.add(i, v); + model[i] += v; + } + expect(mismatches(packed, model)).equal(0); + }); + + test("physical array growth during a cached group keeps the cache valid", () => { + // Small initial capacity so inserts trigger resizeArray() repeatedly. + const packed = new PackedArray(8192, 16); + const model = new StaticArray(8192); + for (let i = 0; i < 8192; i++) { + packed.get(i); // cache group of i + const v = (nextRandom() % 3000); + packed.add(i, v); + model[i] += v; + if (packed.get(i) != model[i]) { + expect(packed.get(i)).equal(model[i]); + } + } + expect(mismatches(packed, model)).equal(0); + }); +}); diff --git a/assembly/packedarray/PackedArray.ts b/assembly/packedarray/PackedArray.ts index 421ea20..94c05e6 100644 --- a/assembly/packedarray/PackedArray.ts +++ b/assembly/packedarray/PackedArray.ts @@ -11,7 +11,8 @@ import { MINIMUM_INITIAL_PACKED_ARRAY_CAPACITY, } from "./PackedArrayContext"; -const NUMBER_OF_SETS: u8 = 8; +const NUMBER_OF_SETS: i32 = 8; +const NO_LEAF: i32 = -1; /** * A Packed array of signed 64 bit values, and supports {@link #get get()}, {@link #set set()}, @@ -29,6 +30,19 @@ export class PackedArray { private arrayContext: PackedArrayContext; + // Leaf cache for sequential reads. + // + // The 8 virtual indexes of a group (index >> 3) share one leaf per byte set, + // so a scan over consecutive indexes would otherwise repeat the same set-tree + // walks 8 times. leafIndexBySet remembers, for the group last read, the long + // index of each set's leaf (NO_LEAF when the set has no entry for that + // group). set() and add() record the leaves they insert for the cached group, + // and anything that rebuilds the context invalidates the cache. + private cachedGroup: i32 = -1; + private leafIndexBySet: StaticArray = new StaticArray( + NUMBER_OF_SETS + ); + constructor( virtualLength: i32, initialPhysicalLength: i32 = MINIMUM_INITIAL_PACKED_ARRAY_CAPACITY @@ -45,6 +59,7 @@ export class PackedArray { } public setVirtualLength(newVirtualArrayLength: i32): void { + this.cachedGroup = -1; if (newVirtualArrayLength < this.length()) { throw new Error( "Cannot set virtual length, as requested length " + @@ -84,28 +99,59 @@ export class PackedArray { * @return the array value at the virtual index given */ get(index: i32): u64 { + const arrayContext = this.arrayContext; + // Deal with unpacked context: + if (!arrayContext.isPacked) { + return arrayContext.getAtUnpackedIndex(index); + } + // Context is packed: + if (index >> 3 != this.cachedGroup) { + this.fillLeafCache(index); + } + const byteInLeaf = index & 0x7; + const leafIndexBySet = this.leafIndexBySet; let value: u64 = 0; - for (let byteNum: u8 = 0; byteNum < NUMBER_OF_SETS; byteNum++) { - let byteValueAtPackedIndex: u64 = 0; - - // Deal with unpacked context: - if (!this.arrayContext.isPacked) { - return this.arrayContext.getAtUnpackedIndex(index); + for (let byteNum = 0; byteNum < NUMBER_OF_SETS; byteNum++) { + const leafIndex = unchecked(leafIndexBySet[byteNum]); + if (leafIndex == NO_LEAF) { + break; } - // Context is packed: - const packedIndex = this.arrayContext.getPackedIndex( - byteNum, - index, - false - ); + value |= + (arrayContext.getAtByteIndex((leafIndex << 3) + byteInLeaf)) << + (byteNum << 3); + } + return value; + } + + /** + * Walk the set trees once for the group of 8 indexes containing `index`, and + * remember the leaf of every set that has one. + */ + private fillLeafCache(index: i32): void { + const arrayContext = this.arrayContext; + const leafIndexBySet = this.leafIndexBySet; + let byteNum = 0; + for (; byteNum < NUMBER_OF_SETS; byteNum++) { + const packedIndex = arrayContext.getPackedIndex(byteNum, index, false); if (packedIndex < 0) { - return value; + break; } - byteValueAtPackedIndex = - (this.arrayContext.getAtByteIndex(packedIndex)) << (byteNum << 3); - value += byteValueAtPackedIndex; + unchecked((leafIndexBySet[byteNum] = packedIndex >> 3)); + } + // Sets are populated from byte 0 upwards (see set() and add()), so once a + // set has no leaf for this group the higher sets have none either. + for (; byteNum < NUMBER_OF_SETS; byteNum++) { + unchecked((leafIndexBySet[byteNum] = NO_LEAF)); + } + this.cachedGroup = index >> 3; + } + + /** Keep the leaf cache coherent with a leaf just looked up with insertAsNeeded. */ + @inline + private noteLeaf(index: i32, byteNum: i32, packedIndex: i32): void { + if (index >> 3 == this.cachedGroup) { + unchecked((this.leafIndexBySet[byteNum] = packedIndex >> 3)); } - return value; } /** @@ -129,34 +175,27 @@ export class PackedArray { * @param value the value to add */ public add(index: i32, value: u64): void { + const arrayContext = this.arrayContext; + // Deal with unpacked context: + if (!arrayContext.isPacked) { + arrayContext.addAndGetAtUnpackedIndex(index, value); + return; + } + // Context is packed: let remainingValueToAdd = value; - - for ( - let byteNum = 0, byteShift = 0; - byteNum < NUMBER_OF_SETS; - byteNum++, byteShift += 8 - ) { - // Deal with unpacked context: - if (!this.arrayContext.isPacked) { - this.arrayContext.addAndGetAtUnpackedIndex(index, value); - return; - } - // Context is packed: + for (let byteNum = 0; byteNum < NUMBER_OF_SETS; byteNum++) { const packedIndex = this.safeGetPackedIndexgetPackedIndex(byteNum, index); + this.noteLeaf(index, byteNum, packedIndex); - const byteToAdd = remainingValueToAdd & 0xff; - - const afterAddByteValue = this.arrayContext.addAtByteIndex( + const byteToAdd = (remainingValueToAdd & 0xff); + const afterAddByteValue = arrayContext.addAtByteIndex( packedIndex, byteToAdd ); - // Reduce remaining value to add by amount just added: - remainingValueToAdd -= byteToAdd; - - remainingValueToAdd = remainingValueToAdd >> 8; - // Account for carry: - remainingValueToAdd += afterAddByteValue >> 8; + // Move on to the next byte, accounting for the carry: + remainingValueToAdd = + (remainingValueToAdd >> 8) + (afterAddByteValue >> 8); if (remainingValueToAdd == 0) { return; // nothing to add to higher magnitudes @@ -170,47 +209,31 @@ export class PackedArray { * @param value the value to set */ set(index: i32, value: u64): void { - let bytesAlreadySet: u8 = 0; + const arrayContext = this.arrayContext; + // Deal with unpacked context: + if (!arrayContext.isPacked) { + arrayContext.setAtUnpackedIndex(index, value); + return; + } + // Context is packed: let valueForNextLevels = value; - for (let byteNum: u8 = 0; byteNum < NUMBER_OF_SETS; byteNum++) { - // Establish context within: critical section - - // Deal with unpacked context: - if (!this.arrayContext.isPacked) { - this.arrayContext.setAtUnpackedIndex(index, value); - return; - } - // Context is packed: + for (let byteNum = 0; byteNum < NUMBER_OF_SETS; byteNum++) { if (valueForNextLevels == 0) { // Special-case zeros to avoid inflating packed array for no reason - const packedIndex = this.arrayContext.getPackedIndex( - byteNum, - index, - false - ); + const packedIndex = arrayContext.getPackedIndex(byteNum, index, false); if (packedIndex < 0) { return; // no need to create entries for zero values if they don't already exist } } // Make sure byte is populated: - const packedIndex = this.arrayContext.getPackedIndex( - byteNum, - index, - true - ); + const packedIndex = arrayContext.getPackedIndex(byteNum, index, true); + this.noteLeaf(index, byteNum, packedIndex); // Determine value to write, and prepare for next levels const byteToWrite: u8 = (valueForNextLevels & 0xff); valueForNextLevels = valueForNextLevels >> 8; - if (byteNum < bytesAlreadySet) { - // We want to avoid writing to the same byte twice when not doing so for the - // entire 64 bit value atomically, as doing so opens a race with e.g. concurrent - // adders. So dobn't actually write the byte if has been written before. - continue; - } - this.arrayContext.setAtByteIndex(packedIndex, byteToWrite); - bytesAlreadySet++; + arrayContext.setAtByteIndex(packedIndex, byteToWrite); } } @@ -239,6 +262,7 @@ export class PackedArray { * Clear the array contents */ public clear(): void { + this.cachedGroup = -1; this.arrayContext.clear(); } diff --git a/assembly/packedarray/PackedArrayContext.ts b/assembly/packedarray/PackedArrayContext.ts index 71801d1..97f37e4 100644 --- a/assembly/packedarray/PackedArrayContext.ts +++ b/assembly/packedarray/PackedArrayContext.ts @@ -39,6 +39,12 @@ private byteArray!: Uint8Array; private shortArray!: Uint16Array; private longArray!: Uint64Array; + /** + * Address of the first byte of `array`. The accessors on the set-tree walk + * read and write through it directly, skipping the typed array bounds checks + * (indexes come from the tree itself, which is kept within the array). + */ + private dataStart: usize = 0; private populatedShortLength: i32 = 0; private virtualLength: i32; private topLevelShift: i32 = i32.MAX_VALUE; // Make it non-sensical until properly initialized. @@ -61,6 +67,7 @@ this.byteArray = Uint8Array.wrap(array); this.shortArray = Uint16Array.wrap(array); this.longArray = Uint64Array.wrap(array); + this.dataStart = changetype(array); } private init(virtualLength: i32): void { @@ -107,23 +114,27 @@ return (this.getPopulatedShortLength() + 3) >> 2; // round up } + @inline public setAtByteIndex(byteIndex: i32, value: u8): void { - this.byteArray[byteIndex] = value; + store(this.dataStart + byteIndex, value); } + @inline public getAtByteIndex(byteIndex: i32): u8 { - return this.byteArray[byteIndex]; + return load(this.dataStart + byteIndex); } /** * add a byte value to a current byte value in the array * @param byteIndex index of byte value to add to * @param valueToAdd byte value to add - * @return the afterAddValue. ((afterAddValue & 0x100) != 0) indicates a carry. + * @return the afterAddValue, unwrapped: (afterAddValue >> 8) is the carry. */ - public addAtByteIndex(byteIndex: i32, valueToAdd: u8): u8 { - const newValue = this.byteArray[byteIndex] + valueToAdd; - this.byteArray[byteIndex] = newValue; + @inline + public addAtByteIndex(byteIndex: i32, valueToAdd: u8): u32 { + const pointer = this.dataStart + byteIndex; + const newValue = load(pointer) + valueToAdd; + store(pointer, newValue); return newValue; } @@ -138,20 +149,24 @@ return this.physicalLength; } + @inline setAtShortIndex(shortIndex: i32, value: u16): void { - this.shortArray[shortIndex] = value; + store(this.dataStart + (shortIndex << 1), value); } + @inline setAtLongIndex(longIndex: i32, value: u64): void { - this.longArray[longIndex] = value; + store(this.dataStart + (longIndex << 3), value); } + @inline getAtShortIndex(shortIndex: i32): u16 { - return this.shortArray[shortIndex]; + return load(this.dataStart + (shortIndex << 1)); } + @inline getIndexAtShortIndex(shortIndex: i32): u16 { - return this.shortArray[shortIndex]; + return load(this.dataStart + (shortIndex << 1)); } setPackedSlotIndicators(entryIndex: i32, newPackedSlotIndicators: u16): void { @@ -161,10 +176,10 @@ ); } + @inline getPackedSlotIndicators(entryIndex: i32): u16 { - return ( - this.shortArray[entryIndex + NON_LEAF_ENTRY_SLOT_INDICATORS_OFFSET] & - 0xffff + return this.getAtShortIndex( + entryIndex + NON_LEAF_ENTRY_SLOT_INDICATORS_OFFSET ); } @@ -550,10 +565,12 @@ } } + @inline getAtUnpackedIndex(index: i32): u64 { return this.longArray[index]; } + @inline setAtUnpackedIndex(index: i32, newValue: u64): void { this.longArray[index] = newValue; } @@ -563,13 +580,13 @@ } incrementAndGetAtUnpackedIndex(index: i32): u64 { - this.longArray[index]++; - return this.longArray[index]; + return this.addAndGetAtUnpackedIndex(index, 1); } addAndGetAtUnpackedIndex(index: i32, valueToAdd: u64): u64 { - this.longArray[index] += valueToAdd; - return this.longArray[index]; + const newValue = this.longArray[index] + valueToAdd; + this.longArray[index] = newValue; + return newValue; } // diff --git a/src/packedarray/PackedArray.spec.ts b/src/packedarray/PackedArray.spec.ts index 7c9298b..d38bcbd 100644 --- a/src/packedarray/PackedArray.spec.ts +++ b/src/packedarray/PackedArray.spec.ts @@ -173,3 +173,57 @@ describe("Unpacked array", () => { expect(array.get(16)).toBe(42); }); }); + +describe("Packed array leaf cache", () => { + it("Should carry across bytes", () => { + const array = new PackedArray(1024, pow(2, 12)); + array.add(42, 200); + array.add(42, 100); + expect(array.get(42)).toBe(300); + }); + + it("Should keep interleaved reads and writes coherent inside a group", () => { + const array = new PackedArray(1024, pow(2, 12)); + expect(array.get(8)).toBe(0); // caches group 1 as empty + array.set(9, 5); + array.set(8, 70000); + expect(array.get(8)).toBe(70000); + expect(array.get(9)).toBe(5); + array.set(8, 3); // smaller value clears higher bytes + expect(array.get(8)).toBe(3); + }); + + it("Should match a plain array on a random workload", () => { + const size = 2048; + const array = new PackedArray(size, pow(2, 14)); + const model = new Array(size).fill(0); + let seed = 12345; + const rnd = () => (seed = (seed * 1103515245 + 12345) & 0x7fffffff); + for (let i = 0; i < 5000; i++) { + const index = rnd() % size; + const v = rnd() % 100000; + if (rnd() % 2 === 0) { + array.set(index, v); + model[index] = v; + } else { + array.add(index, v); + model[index] += v; + } + const probe = rnd() % size; + expect(array.get(probe)).toBe(model[probe]); + } + for (let i = 0; i < size; i++) { + expect(array.get(i)).toBe(model[i]); + } + }); + + it("Should invalidate on clear and resize", () => { + const array = new PackedArray(1024, pow(2, 12)); + array.set(3, 7); + expect(array.get(3)).toBe(7); + array.setVirtualLength(1 << 20); + expect(array.get(3)).toBe(7); + array.clear(); + expect(array.get(3)).toBe(0); + }); +}); diff --git a/src/packedarray/PackedArray.ts b/src/packedarray/PackedArray.ts index b4f7697..0da89c4 100644 --- a/src/packedarray/PackedArray.ts +++ b/src/packedarray/PackedArray.ts @@ -11,7 +11,10 @@ import { } from "./PackedArrayContext"; const NUMBER_OF_SETS = 8; -const { pow, floor } = Math; +const NO_LEAF = -1; +const { floor } = Math; +// 256^byteNum, the weight of each set's byte in a 64 bit value +const BYTE_WEIGHTS = [0, 1, 2, 3, 4, 5, 6, 7].map((n) => 256 ** n); /** * A Packed array of signed 64 bit values, and supports {@link #get get()}, {@link #set set()}, @@ -27,6 +30,17 @@ const { pow, floor } = Math; export class PackedArray { private arrayContext: PackedArrayContext; + // Leaf cache for sequential reads. + // + // The 8 virtual indexes of a group (index >> 3) share one leaf per byte set, + // so a scan over consecutive indexes would otherwise repeat the same set-tree + // walks 8 times. leafIndexBySet remembers, for the group last read, the long + // index of each set's leaf (NO_LEAF when the set has no entry for that + // group). set() and add() record the leaves they insert for the cached group, + // and anything that rebuilds the context invalidates the cache. + private cachedGroup = -1; + private leafIndexBySet: number[] = new Array(NUMBER_OF_SETS).fill(NO_LEAF); + constructor( virtualLength: number, initialPhysicalLength: number = MINIMUM_INITIAL_PACKED_ARRAY_CAPACITY @@ -38,6 +52,7 @@ export class PackedArray { } public setVirtualLength(newVirtualArrayLength: number) { + this.cachedGroup = -1; if (newVirtualArrayLength < this.length()) { throw new Error( "Cannot set virtual length, as requested length " + @@ -69,28 +84,59 @@ export class PackedArray { * @return the array value at the virtual index given */ get(index: number) { + const arrayContext = this.arrayContext; + // Deal with unpacked context: + if (!arrayContext.isPacked) { + return arrayContext.getAtUnpackedIndex(index); + } + // Context is packed: + const group = floor(index / 8); + if (group !== this.cachedGroup) { + this.fillLeafCache(index, group); + } + const byteInLeaf = index & 0x7; + const leafIndexBySet = this.leafIndexBySet; let value = 0; for (let byteNum = 0; byteNum < NUMBER_OF_SETS; byteNum++) { - let byteValueAtPackedIndex = 0; - - // Deal with unpacked context: - if (!this.arrayContext.isPacked) { - return this.arrayContext.getAtUnpackedIndex(index); + const leafIndex = leafIndexBySet[byteNum]; + if (leafIndex === NO_LEAF) { + break; } - // Context is packed: - const packedIndex = this.arrayContext.getPackedIndex( - byteNum, - index, - false - ); + value += + arrayContext.getAtByteIndex(leafIndex * 8 + byteInLeaf) * + BYTE_WEIGHTS[byteNum]; + } + return value; + } + + /** + * Walk the set trees once for the group of 8 indexes containing `index`, and + * remember the leaf of every set that has one. + */ + private fillLeafCache(index: number, group: number) { + const arrayContext = this.arrayContext; + const leafIndexBySet = this.leafIndexBySet; + let byteNum = 0; + for (; byteNum < NUMBER_OF_SETS; byteNum++) { + const packedIndex = arrayContext.getPackedIndex(byteNum, index, false); if (packedIndex < 0) { - return value; + break; } - byteValueAtPackedIndex = - this.arrayContext.getAtByteIndex(packedIndex) * pow(2, byteNum << 3); - value += byteValueAtPackedIndex; + leafIndexBySet[byteNum] = packedIndex >> 3; + } + // Sets are populated from byte 0 upwards (see set() and add()), so once a + // set has no leaf for this group the higher sets have none either. + for (; byteNum < NUMBER_OF_SETS; byteNum++) { + leafIndexBySet[byteNum] = NO_LEAF; + } + this.cachedGroup = group; + } + + /** Keep the leaf cache coherent with a leaf just looked up with insertAsNeeded. */ + private noteLeaf(index: number, byteNum: number, packedIndex: number) { + if (floor(index / 8) === this.cachedGroup) { + this.leafIndexBySet[byteNum] = packedIndex >> 3; } - return value; } /** @@ -124,36 +170,30 @@ export class PackedArray { * @param value the value to add */ public add(index: number, value: number) { + const arrayContext = this.arrayContext; + // Deal with unpacked context: + if (!arrayContext.isPacked) { + arrayContext.addAndGetAtUnpackedIndex(index, value); + return; + } + // Context is packed: let remainingValueToAdd = value; - - for ( - let byteNum = 0, byteShift = 0; - byteNum < NUMBER_OF_SETS; - byteNum++, byteShift += 8 - ) { - // Deal with unpacked context: - if (!this.arrayContext.isPacked) { - this.arrayContext.addAndGetAtUnpackedIndex(index, value); - return; - } - // Context is packed: + for (let byteNum = 0; byteNum < NUMBER_OF_SETS; byteNum++) { const packedIndex = this.safeGetPackedIndexgetPackedIndex(byteNum, index); + this.noteLeaf(index, byteNum, packedIndex); + // The low byte survives the int32 conversion, even above 2^32 const byteToAdd = remainingValueToAdd & 0xff; - - const afterAddByteValue = this.arrayContext.addAtByteIndex( + const afterAddByteValue = arrayContext.addAtByteIndex( packedIndex, byteToAdd ); - // Reduce remaining value to add by amount just added: - remainingValueToAdd -= byteToAdd; + // Move on to the next byte, accounting for the carry: + remainingValueToAdd = + floor(remainingValueToAdd / 256) + (afterAddByteValue >> 8); - remainingValueToAdd = remainingValueToAdd / pow(2, 8); - // Account for carry: - remainingValueToAdd += floor(afterAddByteValue / pow(2, 8)); - - if (remainingValueToAdd == 0) { + if (remainingValueToAdd === 0) { return; // nothing to add to higher magnitudes } } @@ -165,47 +205,31 @@ export class PackedArray { * @param value the value to set */ set(index: number, value: number) { - let bytesAlreadySet = 0; + const arrayContext = this.arrayContext; + // Deal with unpacked context: + if (!arrayContext.isPacked) { + arrayContext.setAtUnpackedIndex(index, value); + return; + } + // Context is packed: let valueForNextLevels = value; for (let byteNum = 0; byteNum < NUMBER_OF_SETS; byteNum++) { - // Establish context within: critical section - - // Deal with unpacked context: - if (!this.arrayContext.isPacked) { - this.arrayContext.setAtUnpackedIndex(index, value); - return; - } - // Context is packed: - if (valueForNextLevels == 0) { + if (valueForNextLevels === 0) { // Special-case zeros to avoid inflating packed array for no reason - const packedIndex = this.arrayContext.getPackedIndex( - byteNum, - index, - false - ); + const packedIndex = arrayContext.getPackedIndex(byteNum, index, false); if (packedIndex < 0) { return; // no need to create entries for zero values if they don't already exist } } // Make sure byte is populated: - const packedIndex = this.arrayContext.getPackedIndex( - byteNum, - index, - true - ); + const packedIndex = arrayContext.getPackedIndex(byteNum, index, true); + this.noteLeaf(index, byteNum, packedIndex); // Determine value to write, and prepare for next levels const byteToWrite = valueForNextLevels & 0xff; - valueForNextLevels = floor(valueForNextLevels / pow(2, 8)); + valueForNextLevels = floor(valueForNextLevels / 256); - if (byteNum < bytesAlreadySet) { - // We want to avoid writing to the same byte twice when not doing so for the - // entire 64 bit value atomically, as doing so opens a race with e.g. concurrent - // adders. So dobn't actually write the byte if has been written before. - continue; - } - this.arrayContext.setAtByteIndex(packedIndex, byteToWrite); - bytesAlreadySet++; + arrayContext.setAtByteIndex(packedIndex, byteToWrite); } } @@ -229,6 +253,7 @@ export class PackedArray { * Clear the array contents */ public clear() { + this.cachedGroup = -1; this.arrayContext.clear(); } diff --git a/src/packedarray/PackedArrayContext.ts b/src/packedarray/PackedArrayContext.ts index 0f08814..ae3d1e4 100644 --- a/src/packedarray/PackedArrayContext.ts +++ b/src/packedarray/PackedArrayContext.ts @@ -31,7 +31,7 @@ const NON_LEAF_ENTRY_HEADER_SIZE_IN_SHORTS = 1; const PACKED_ARRAY_GROWTH_INCREMENT = 16; const PACKED_ARRAY_GROWTH_FRACTION_POW2 = 4; -const { pow, ceil, log2, max } = Math; +const { ceil, log2, max } = Math; const bitCount = (n: number) => { var bits = 0; @@ -404,7 +404,7 @@ export class PackedArrayContext { const nextLevelIsLeaf = indexShift === LEAF_LEVEL_SHIFT; // Target is a packedSlotIndicators entry const packedSlotIndicators = this.getPackedSlotIndicators(entryIndex); - const slotBitNumber = (virtualIndex / pow(2, indexShift)) & 0xf; //(virtualIndex >>> indexShift) & 0xf; + const slotBitNumber = (virtualIndex >>> indexShift) & 0xf; const slotMask = 1 << slotBitNumber; const slotsBelowBitNumber = packedSlotIndicators & (slotMask - 1); const slotNumber = bitCount(slotsBelowBitNumber);