Skip to content

Commit dbb51e1

Browse files
committed
faster object-heap
1 parent cf7a4f3 commit dbb51e1

File tree

9 files changed

+297
-55
lines changed

9 files changed

+297
-55
lines changed

Plugins/PackageToJS/Templates/runtime.d.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@ type ref = number;
22
type pointer = number;
33

44
declare class JSObjectSpace {
5-
private _heapValueById;
6-
private _heapEntryByValue;
7-
private _heapNextKey;
5+
private _valueMap;
6+
private _values;
7+
private _rcById;
8+
private _freeStack;
89
constructor();
910
retain(value: any): number;
1011
retainByRef(ref: ref): number;

Plugins/PackageToJS/Templates/runtime.mjs

Lines changed: 40 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -240,38 +240,55 @@ const globalVariable = globalThis;
240240

241241
class JSObjectSpace {
242242
constructor() {
243-
this._heapValueById = new Map();
244-
this._heapValueById.set(1, globalVariable);
245-
this._heapEntryByValue = new Map();
246-
this._heapEntryByValue.set(globalVariable, { id: 1, rc: 1 });
247-
// Note: 0 is preserved for invalid references, 1 is preserved for globalThis
248-
this._heapNextKey = 2;
243+
this._values = [];
244+
this._values[0] = undefined;
245+
this._values[1] = globalVariable;
246+
this._valueMap = new Map();
247+
this._valueMap.set(globalVariable, 1);
248+
this._rcById = [];
249+
this._rcById[0] = 0;
250+
this._rcById[1] = 1;
251+
this._freeStack = [];
249252
}
250253
retain(value) {
251-
const entry = this._heapEntryByValue.get(value);
252-
if (entry) {
253-
entry.rc++;
254-
return entry.id;
255-
}
256-
const id = this._heapNextKey++;
257-
this._heapValueById.set(id, value);
258-
this._heapEntryByValue.set(value, { id: id, rc: 1 });
259-
return id;
254+
const id = this._valueMap.get(value);
255+
if (id !== undefined) {
256+
this._rcById[id]++;
257+
return id;
258+
}
259+
if (this._freeStack.length > 0) {
260+
const newId = this._freeStack.pop();
261+
this._values[newId] = value;
262+
this._rcById[newId] = 1;
263+
this._valueMap.set(value, newId);
264+
return newId;
265+
}
266+
const newId = this._values.length;
267+
this._values[newId] = value;
268+
this._rcById[newId] = 1;
269+
this._valueMap.set(value, newId);
270+
return newId;
260271
}
261272
retainByRef(ref) {
262-
return this.retain(this.getObject(ref));
273+
this._rcById[ref]++;
274+
return ref;
263275
}
264276
release(ref) {
265-
const value = this._heapValueById.get(ref);
266-
const entry = this._heapEntryByValue.get(value);
267-
entry.rc--;
268-
if (entry.rc != 0)
277+
if (--this._rcById[ref] !== 0)
269278
return;
270-
this._heapEntryByValue.delete(value);
271-
this._heapValueById.delete(ref);
279+
const value = this._values[ref];
280+
this._valueMap.delete(value);
281+
if (ref === this._values.length - 1) {
282+
this._values.length = ref;
283+
this._rcById.length = ref;
284+
}
285+
else {
286+
this._values[ref] = undefined;
287+
this._freeStack.push(ref);
288+
}
272289
}
273290
getObject(ref) {
274-
const value = this._heapValueById.get(ref);
291+
const value = this._values[ref];
275292
if (value === undefined) {
276293
throw new ReferenceError("Attempted to read invalid reference " + ref);
277294
}

Runtime/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
/lib
2+
/bench/dist
23
/node_modules

Runtime/bench/_original.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { globalVariable } from "../src/find-global.js";
2+
import { ref } from "../src/types.js";
3+
4+
type SwiftRuntimeHeapEntry = {
5+
id: number;
6+
rc: number;
7+
};
8+
9+
/** Original implementation kept for benchmark comparison. Same API as JSObjectSpace. */
10+
export class JSObjectSpaceOriginal {
11+
private _heapValueById: Map<number, any>;
12+
private _heapEntryByValue: Map<any, SwiftRuntimeHeapEntry>;
13+
private _heapNextKey: number;
14+
15+
constructor() {
16+
this._heapValueById = new Map();
17+
this._heapValueById.set(1, globalVariable);
18+
19+
this._heapEntryByValue = new Map();
20+
this._heapEntryByValue.set(globalVariable, { id: 1, rc: 1 });
21+
22+
// Note: 0 is preserved for invalid references, 1 is preserved for globalThis
23+
this._heapNextKey = 2;
24+
}
25+
26+
retain(value: any) {
27+
const entry = this._heapEntryByValue.get(value);
28+
if (entry) {
29+
entry.rc++;
30+
return entry.id;
31+
}
32+
const id = this._heapNextKey++;
33+
this._heapValueById.set(id, value);
34+
this._heapEntryByValue.set(value, { id: id, rc: 1 });
35+
return id;
36+
}
37+
38+
retainByRef(ref: ref) {
39+
return this.retain(this.getObject(ref));
40+
}
41+
42+
release(ref: ref) {
43+
const value = this._heapValueById.get(ref);
44+
const entry = this._heapEntryByValue.get(value)!;
45+
entry.rc--;
46+
if (entry.rc != 0) return;
47+
48+
this._heapEntryByValue.delete(value);
49+
this._heapValueById.delete(ref);
50+
}
51+
52+
getObject(ref: ref) {
53+
const value = this._heapValueById.get(ref);
54+
if (value === undefined) {
55+
throw new ReferenceError(
56+
"Attempted to read invalid reference " + ref,
57+
);
58+
}
59+
return value;
60+
}
61+
}

Runtime/bench/bench-runner.ts

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
/**
2+
* Benchmark runner for JSObjectSpace implementations.
3+
* Run with: npm run bench (builds via rollup.bench.mjs, then node bench/dist/bench.mjs)
4+
*/
5+
6+
import { JSObjectSpace } from "../src/object-heap.js";
7+
import { JSObjectSpaceOriginal } from "./_original.js";
8+
9+
export interface HeapLike {
10+
retain(value: unknown): number;
11+
release(ref: number): void;
12+
getObject(ref: number): unknown;
13+
}
14+
15+
const ITERATIONS = 5;
16+
const HEAVY_OPS = 200_000;
17+
const FILL_LEVELS = [1_000, 10_000, 50_000] as const;
18+
const MIXED_OPS_PER_LEVEL = 100_000;
19+
20+
function median(numbers: number[]): number {
21+
const sorted = [...numbers].sort((a, b) => a - b);
22+
const mid = Math.floor(sorted.length / 2);
23+
return sorted.length % 2 !== 0
24+
? sorted[mid]!
25+
: (sorted[mid - 1]! + sorted[mid]!) / 2;
26+
}
27+
28+
function runHeavyRetain(Heap: new () => HeapLike): number {
29+
const times: number[] = [];
30+
for (let iter = 0; iter < ITERATIONS; iter++) {
31+
const heap = new Heap();
32+
const start = performance.now();
33+
for (let i = 0; i < HEAVY_OPS; i++) {
34+
heap.retain({ __i: i });
35+
}
36+
times.push(performance.now() - start);
37+
}
38+
return median(times);
39+
}
40+
41+
function runHeavyRelease(Heap: new () => HeapLike): number {
42+
const times: number[] = [];
43+
for (let iter = 0; iter < ITERATIONS; iter++) {
44+
const heap = new Heap();
45+
const refs: number[] = [];
46+
for (let i = 0; i < HEAVY_OPS; i++) {
47+
refs.push(heap.retain({ __i: i }));
48+
}
49+
const start = performance.now();
50+
for (let i = 0; i < HEAVY_OPS; i++) {
51+
heap.release(refs[i]!);
52+
}
53+
times.push(performance.now() - start);
54+
}
55+
return median(times);
56+
}
57+
58+
function runMixedFillLevel(Heap: new () => HeapLike, fillLevel: number): number {
59+
const times: number[] = [];
60+
for (let iter = 0; iter < ITERATIONS; iter++) {
61+
const heap = new Heap();
62+
const refs: number[] = [];
63+
for (let i = 0; i < fillLevel; i++) {
64+
refs.push(heap.retain({ __i: i }));
65+
}
66+
let nextId = fillLevel;
67+
const start = performance.now();
68+
for (let i = 0; i < MIXED_OPS_PER_LEVEL; i++) {
69+
const idx = i % fillLevel;
70+
heap.release(refs[idx]!);
71+
refs[idx] = heap.retain({ __i: nextId++ });
72+
}
73+
times.push(performance.now() - start);
74+
}
75+
return median(times);
76+
}
77+
78+
function runBenchmark(
79+
name: string,
80+
Heap: new () => HeapLike,
81+
): { name: string; heavyRetain: number; heavyRelease: number; mixed: Record<string, number> } {
82+
return {
83+
name,
84+
heavyRetain: runHeavyRetain(Heap),
85+
heavyRelease: runHeavyRelease(Heap),
86+
mixed: {
87+
"1k": runMixedFillLevel(Heap, 1_000),
88+
"10k": runMixedFillLevel(Heap, 10_000),
89+
"50k": runMixedFillLevel(Heap, 50_000),
90+
},
91+
};
92+
}
93+
94+
function main() {
95+
const implementations: Array<{ name: string; Heap: new () => HeapLike }> = [
96+
{ name: "JSObjectSpaceOriginal", Heap: JSObjectSpaceOriginal },
97+
{ name: "JSObjectSpace (current)", Heap: JSObjectSpace },
98+
];
99+
100+
console.log("JSObjectSpace benchmark");
101+
console.log("======================\n");
102+
console.log(
103+
`Heavy retain: ${HEAVY_OPS} ops, Heavy release: ${HEAVY_OPS} ops`,
104+
);
105+
console.log(
106+
`Mixed: ${MIXED_OPS_PER_LEVEL} ops per fill level (${FILL_LEVELS.join(", ")})`,
107+
);
108+
console.log(`Median of ${ITERATIONS} runs per scenario.\n`);
109+
110+
const results: Array<ReturnType<typeof runBenchmark>> = [];
111+
for (const { name, Heap } of implementations) {
112+
console.log(`Running ${name}...`);
113+
runBenchmark(name, Heap);
114+
results.push(runBenchmark(name, Heap));
115+
}
116+
117+
console.log("\nResults (median ms):\n");
118+
const pad = Math.max(...results.map((r) => r.name.length));
119+
for (const r of results) {
120+
console.log(
121+
`${r.name.padEnd(pad)} retain: ${r.heavyRetain.toFixed(2)}ms release: ${r.heavyRelease.toFixed(2)}ms mixed(1k): ${r.mixed["1k"].toFixed(2)}ms mixed(10k): ${r.mixed["10k"].toFixed(2)}ms mixed(50k): ${r.mixed["50k"].toFixed(2)}ms`,
122+
);
123+
}
124+
125+
const total = (r: (typeof results)[0]) =>
126+
r.heavyRetain + r.heavyRelease + r.mixed["1k"] + r.mixed["10k"] + r.mixed["50k"];
127+
const best = results.reduce((a, b) => (total(a) <= total(b) ? a : b));
128+
console.log(`\nFastest overall (sum of medians): ${best.name}`);
129+
}
130+
131+
main();

Runtime/rollup.bench.mjs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import typescript from "@rollup/plugin-typescript";
2+
3+
/** @type {import('rollup').RollupOptions} */
4+
export default {
5+
input: "bench/bench-runner.ts",
6+
output: {
7+
file: "bench/dist/bench.mjs",
8+
format: "esm",
9+
},
10+
plugins: [typescript({ tsconfig: "tsconfig.bench.json" })],
11+
};

0 commit comments

Comments
 (0)