remove unnecessary data structs Dict+MapArray

This commit is contained in:
Schluffe
2026-08-27 23:43:28 +02:00
parent 2f7957b28a
commit 7b3c9865fe
10 changed files with 12 additions and 466 deletions
-22
View File
@@ -1,22 +0,0 @@
structure,size,latency_ns_avg,latency_ns_med
MapArray,100,0.03,0.03
Dictionary,100,0.00,0.00
Map,100,0.00,0.00
MapArray,250,0.05,0.05
Dictionary,250,0.00,0.00
Map,250,0.00,0.00
MapArray,500,0.09,0.09
Dictionary,500,0.00,0.00
Map,500,0.00,0.00
MapArray,999,0.16,0.16
Dictionary,999,0.01,0.00
Map,999,0.00,0.00
MapArray,2500,0.42,0.42
Dictionary,2500,0.01,0.01
Map,2500,0.01,0.01
MapArray,5000,0.81,0.80
Dictionary,5000,0.02,0.02
Map,5000,0.01,0.01
MapArray,10000,1.66,1.66
Dictionary,10000,0.05,0.04
Map,10000,0.02,0.02
1 structure size latency_ns_avg latency_ns_med
2 MapArray 100 0.03 0.03
3 Dictionary 100 0.00 0.00
4 Map 100 0.00 0.00
5 MapArray 250 0.05 0.05
6 Dictionary 250 0.00 0.00
7 Map 250 0.00 0.00
8 MapArray 500 0.09 0.09
9 Dictionary 500 0.00 0.00
10 Map 500 0.00 0.00
11 MapArray 999 0.16 0.16
12 Dictionary 999 0.01 0.00
13 Map 999 0.00 0.00
14 MapArray 2500 0.42 0.42
15 Dictionary 2500 0.01 0.01
16 Map 2500 0.01 0.01
17 MapArray 5000 0.81 0.80
18 Dictionary 5000 0.02 0.02
19 Map 5000 0.01 0.01
20 MapArray 10000 1.66 1.66
21 Dictionary 10000 0.05 0.04
22 Map 10000 0.02 0.02
-89
View File
@@ -1,89 +0,0 @@
// bench/churn-sweep.ts
import { Bench } from "tinybench";
import { type Box, box } from "../engine/lib/box.ts";
import {
createDictionary,
deleteEntry,
setEntry,
} from "../engine/lib/Dictionary.ts";
import MapArray from "../engine/lib/MapArray.ts";
const SIZES = [100, 250, 500, 999, 2500, 5000, 10000];
const CHURN_N = 100;
const churnKeys = Array.from({ length: CHURN_N }, (_, i) => `churn-${i}`);
let sink = 0;
function makeBaseData(size: number): Array<[string, Box<number>]> {
const data: Array<[string, Box<number>]> = [];
let i = size;
while (i-- > 0) data.push([String(i), box(i)]);
return data;
}
type Row = {
structure: string;
size: number;
latencyNsAvg: number;
latencyNsMed: number;
};
const rows: Row[] = [];
for (const size of SIZES) {
const baseData = makeBaseData(size);
const ma = new MapArray(structuredClone(baseData));
const dict = createDictionary(structuredClone(baseData));
const map = new Map(structuredClone(baseData));
const bench = new Bench({ name: `churn @ size=${size}`, time: 500 });
bench
.add("MapArray", () => {
for (const key of churnKeys) ma.set(key, box(1));
for (const key of churnKeys) ma.delete(key);
sink += ma.entries().length;
})
.add("Dictionary", () => {
for (const key of churnKeys) setEntry(dict, [key, box(1)]);
for (const key of churnKeys) deleteEntry(dict, key);
sink += dict.entries.length;
})
.add("Map", () => {
for (const key of churnKeys) map.set(key, box(1));
for (const key of churnKeys) map.delete(key);
sink += map.size;
});
await bench.run();
for (const task of bench.tasks) {
const res = task.result;
if (!res) continue;
rows.push({
structure: task.name,
size,
latencyNsAvg: res.latency.mean,
latencyNsMed: res.latency.p50 ?? res.latency.mean,
});
}
console.log(`-- size=${size} done --`);
console.table(bench.table());
}
// CSV output
const header = "structure,size,latency_ns_avg,latency_ns_med";
const csvLines = rows.map(
(r) =>
`${r.structure},${r.size},${r.latencyNsAvg.toFixed(2)},${r.latencyNsMed.toFixed(2)}`,
);
const csv = [header, ...csvLines].join("\n");
(await Deno.writeTextFile?.("churn-sweep.csv", csv)) ??
(await import("node:fs/promises")).then((fs) =>
fs.writeFile("churn-sweep.csv", csv),
);
console.log("\nWrote churn-sweep.csv");
console.log("sink (ignore):", sink);
-77
View File
@@ -1,77 +0,0 @@
// bench/churn.ts
import { Bench } from "tinybench";
import { type Box, box } from "./../engine/lib/box.ts";
import {
createDictionary,
deleteEntry,
getEntry,
setEntry,
} from "./../engine/lib/Dictionary.ts";
import MapArray from "./../engine/lib/MapArray.ts";
const bench = new Bench({ name: "churn", time: 1000 });
const SIZE = 999;
const baseData: Array<[string, Box<number>]> = [];
{
let i = SIZE;
while (i-- > 0) baseData.push([String(i), box(i)]);
}
// Churn keys: a rotating window of "new" entities not in the base set,
// so every set/delete pair actually does real insert+remove work
// rather than repeatedly hitting the same already-resident key.
const CHURN_N = 100;
const churnKeys = Array.from({ length: CHURN_N }, (_, i) => `churn-${i}`);
let sink = 0; // anti-DCE: something observable reads this later
function freshMapArray() {
return new MapArray(structuredClone(baseData));
}
function freshDictionary() {
return createDictionary(structuredClone(baseData));
}
function freshMap() {
return new Map(structuredClone(baseData));
}
// Each task rebuilds nothing per-iteration except doing insert+remove
// on a fixed churn window against a persistent SIZE-element base structure —
// this mirrors "N entities steady-state, M entering/leaving per frame".
const ma = freshMapArray();
const dict = freshDictionary();
const map = freshMap();
bench
.add("MapArray churn (set+delete)", () => {
for (const key of churnKeys) {
ma.set(key, box(1));
}
for (const key of churnKeys) {
ma.delete(key);
}
sink += ma.entries().length;
})
.add("Dictionary churn (set+delete)", () => {
for (const key of churnKeys) {
setEntry(dict, [key, box(1)]);
}
for (const key of churnKeys) {
deleteEntry(dict, key);
}
sink += dict.entries.length;
})
.add("Map churn (set+delete)", () => {
for (const key of churnKeys) {
map.set(key, box(1));
}
for (const key of churnKeys) {
map.delete(key);
}
sink += map.size;
});
await bench.run();
console.table(bench.table());
console.log("sink (ignore):", sink);
+4 -46
View File
@@ -1,53 +1,11 @@
import { Bench } from "tinybench"; import { Bench } from "tinybench";
import { type Box, box } from "./../engine/lib/box.ts";
import {
createDictionary,
forEntry,
getEntry,
} from "./../engine/lib/Dictionary.ts";
import MapArray from "./../engine/lib/MapArray.ts";
const bench = new Bench({ name: "MapArray get", time: 3000 }); const bench = new Bench({ name: "", time: 1000 });
let sink = 0; let sink = 0;
const testData: Array<[string, Box<number>]> = []; bench.add("", () => {
let i = 999; sink += 1;
while (i-- > 0) { });
testData.push([String(i), box(i)]);
}
const ma = new MapArray(structuredClone(testData));
const dict = createDictionary(structuredClone(testData));
const map = new Map(structuredClone(testData));
bench
.add("MapArray iter/get reads", () => {
let sum = 0;
for (const [key] of ma.entries()) {
sum += ma.get(key)!.value;
}
sink += sum;
})
.add("Dictionary iter/get reads", () => {
let sum = 0;
forEntry(dict, (_, key) => {
sum += getEntry(dict, key)!.value;
});
sink += sum;
})
.add("Map iter/get reads", () => {
let sum = 0;
for (const [key] of map.entries()) {
sum += map.get(key)!.value;
}
sink += sum;
});
await bench.run(); await bench.run();
+6 -7
View File
@@ -4,9 +4,8 @@ import {
entityProcessMap, entityProcessMap,
} from "#/engine/Entity.ts"; } from "#/engine/Entity.ts";
import { type Graph, getNeighbours } from "#/engine/Graph.ts"; import { type Graph, getNeighbours } from "#/engine/Graph.ts";
import { box } from "#/engine/lib/box"; import { type Box, box } from "#/engine/lib/box";
import { memoize } from "#/engine/lib/cache.ts"; import { memoize } from "#/engine/lib/cache.ts";
import MapArray from "#/engine/lib/MapArray.ts";
export type Process = (ctx: CanvasRenderingContext2D, delta: number) => void; export type Process = (ctx: CanvasRenderingContext2D, delta: number) => void;
export type PreProcess = () => void; export type PreProcess = () => void;
@@ -24,7 +23,7 @@ export type SceneData = {
entities: Array<EntityData<unknown>>; entities: Array<EntityData<unknown>>;
}; };
export type ComponentsMap = Map<string, MapArray<string, unknown>>; export type ComponentsMap = Map<string, Map<string, Box<unknown>>>;
export type Scene = { export type Scene = {
data: SceneData; data: SceneData;
@@ -113,7 +112,7 @@ const [loadScene, sceneCache] = memoize(async (name: string) => {
const localEntityProcessMap = new Map<string, EntityProcess>(); const localEntityProcessMap = new Map<string, EntityProcess>();
sceneEntityProcessMap.set(name, localEntityProcessMap); sceneEntityProcessMap.set(name, localEntityProcessMap);
const componentsMap = new Map<string, MapArray<string, unknown>>(); const componentsMap: ComponentsMap = new Map();
sceneComponentsMap.set(name, componentsMap); sceneComponentsMap.set(name, componentsMap);
await Promise.all( await Promise.all(
@@ -138,7 +137,7 @@ const [loadScene, sceneCache] = memoize(async (name: string) => {
if (componentsMap.has(key)) { if (componentsMap.has(key)) {
componentsMap.get(key)!.set(id, boxedValue); componentsMap.get(key)!.set(id, boxedValue);
} else { } else {
componentsMap.set(key, new MapArray([[id, boxedValue]])); componentsMap.set(key, new Map([[id, boxedValue]]));
} }
} }
}, },
@@ -179,6 +178,6 @@ export async function setScene(name: string): Promise<void> {
} }
} }
export default function <T>(name: string): MapArray<string, T> { export default function <T>(name: string): Map<string, Box<T>> {
return currentScene.components.get(name) as MapArray<string, T>; return currentScene.components.get(name) as Map<string, Box<T>>;
} }
-64
View File
@@ -1,64 +0,0 @@
import { expect, test } from "vitest";
import {
createDictionary,
type Dictionary,
deleteEntrySave,
forEntry,
getEntry,
setEntrySave,
} from "./Dictionary.ts";
test("Dictionary", () => {
const dict: Dictionary<string, number> = createDictionary([
["a", 1],
["b", 2],
["c", 3],
]);
setEntrySave(dict, "foo", 42);
setEntrySave(dict, "bar", 0);
deleteEntrySave(dict, "bar");
expect(getEntry(dict, "foo")).toBe(42);
expect(getEntry(dict, "bar")).toBe(undefined);
expect(dict.indices.size).toBe(dict.entries.length);
let sum = 0;
forEntry(dict, (value) => {
sum += value;
});
expect(sum).toBe(48);
});
test("Dictionary don't mutate, when used 'save', while iterating", () => {
const dict: Dictionary<string, number> = createDictionary([
["a", 1],
["b", 2],
["c", 3],
]);
let sum = 0;
forEntry(dict, (value, _, index) => {
if (index < 1) {
setEntrySave(dict, "d", 4);
setEntrySave(dict, "e", 5);
}
sum += value;
});
expect(sum).toBe(6);
expect(dict.indices.size === 5).toEqual(dict.entries.length === 5);
sum = 0;
forEntry(dict, (value, _, index) => {
if (index < 1) {
deleteEntrySave(dict, "d");
deleteEntrySave(dict, "e");
}
sum += value;
});
expect(sum).toBe(15);
expect(dict.indices.size === 3).toEqual(dict.entries.length === 3);
});
-95
View File
@@ -1,95 +0,0 @@
export type Dictionary<K, V> = {
entries: Array<[K, V]>;
indices: Map<K, number>;
isIterating: boolean;
deleteQueue: Array<K>;
setQueue: Array<[K, V]>;
};
export function createDictionary<K, V>(
iterable?: Iterable<[K, V]>,
): Dictionary<K, V> {
const entries = iterable ? [...iterable] : [];
const indices = new Map(entries.map(([k], i) => [k, i]));
return {
entries,
indices,
isIterating: false,
deleteQueue: [],
setQueue: [],
};
}
export function deleteEntry<K, V>(dict: Dictionary<K, V>, key: K): void {
const index = dict.indices.get(key);
if (index === undefined) {
return;
}
const last = dict.entries.length - 1;
if (index !== last) {
dict.entries[index] = dict.entries[last];
dict.indices.set(dict.entries[index][0], index);
}
dict.entries.pop();
dict.indices.delete(key);
}
export function deleteEntrySave<K, V>(dict: Dictionary<K, V>, key: K): void {
if (dict.isIterating) {
dict.deleteQueue.push();
} else {
deleteEntry(dict, key);
}
}
export function setEntry<K, V>(
dict: Dictionary<K, V>,
[key, value]: [K, V],
): void {
const index = dict.indices.get(key);
if (index === undefined) {
dict.indices.set(key, dict.entries.length);
dict.entries.push([key, value]);
} else {
dict.entries[index][1] = value;
}
}
export function setEntrySave<K, V>(
dict: Dictionary<K, V>,
key: K,
value: V,
): void {
if (dict.isIterating) {
dict.setQueue.push([key, value]);
} else {
setEntry(dict, [key, value]);
}
}
export function getEntry<K, V>(dict: Dictionary<K, V>, key: K): V | undefined {
const index = dict.indices.get(key);
return index === undefined ? undefined : dict.entries[index][1];
}
export function forEntry<K, V>(
dict: Dictionary<K, V>,
callback: (value: V, key: K, index: number) => void,
): void {
dict.isIterating = true;
for (let i = 0; i < dict.entries.length; i++) {
callback(dict.entries[i][1], dict.entries[i][0], i);
}
dict.isIterating = false;
while (dict.deleteQueue.length > 0) {
deleteEntry(dict, dict.deleteQueue.pop()!);
}
while (dict.setQueue.length > 0) {
setEntry(dict, dict.setQueue.pop()!);
}
}
-27
View File
@@ -1,27 +0,0 @@
import { expect, test } from "vitest";
import { box } from "./box.ts";
import MapArray from "./MapArray.ts";
test("MapArray", () => {
const mapArray = new MapArray([
["a", box(1)],
["b", box(2)],
["c", box(3)],
]);
mapArray.set("foo", box(42));
mapArray.set("bar", box(0));
mapArray.delete("bar");
expect(mapArray.get("foo")!.value).toBe(42);
expect(mapArray.get("bar")).toBe(undefined);
expect(mapArray.map.size).toBe(mapArray.array.length);
expect(
mapArray.entries().reduce(
(a, c) => {
a[1].value = a[1].value + c[1].value;
return a;
},
["_", box(0)],
)[1].value,
).toBe(48);
});
-37
View File
@@ -1,37 +0,0 @@
import type { Box } from "./box.ts";
export default class MapArray<K, V> {
map: Map<K, Box<V>>;
array: Array<[K, Box<V>]>;
constructor(iterable?: Iterable<[K, Box<V>]>) {
this.map = new Map(iterable);
this.array = iterable ? [...iterable] : [];
}
delete(key: K): void {
if (this.map.delete(key)) {
this.array[this.array.findIndex((e) => e[0] === key)] =
this.array[this.array.length - 1];
this.array.pop();
}
}
set(key: K, value: Box<V>): void {
if (this.map.has(key)) {
this.array.find((e) => e[0] === key)![1] = value;
} else {
this.array.push([key, value]);
}
this.map.set(key, value);
}
get(key: K): Box<V> | undefined {
return this.map.get(key);
}
entries(): Array<[K, Box<V>]> {
return this.array;
}
}
+2 -2
View File
@@ -13,13 +13,13 @@ sceneChangeSet.add((sceneData) => {
drawables.length = 0; drawables.length = 0;
const sprites = [ const sprites = [
...sceneComponentsMap.get(sceneData.name)!.get("sprite")!.map.values(), ...sceneComponentsMap.get(sceneData.name)!.get("sprite")!.values(),
] as Array<Box<Sprite>>; ] as Array<Box<Sprite>>;
sceneComponentsMap sceneComponentsMap
.get(sceneData.name)! .get(sceneData.name)!
.get("position")! .get("position")!
.map.values() .values()
.forEach((box, index) => { .forEach((box, index) => {
drawables.push({ drawables.push({
position: box as Box<Vector>, position: box as Box<Vector>,