Прежняя git-история утрачена при переносе проекта на машину владельца (снапшот без .git). Хэши коммитов в docs/reports/* относятся к утраченной истории. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
141 lines
5.1 KiB
TypeScript
141 lines
5.1 KiB
TypeScript
/**
|
||
* Микро-бенчмарк: flood fill vs union-find для поиска групп и дамэ
|
||
* на случайных позициях 19×19. Запуск: npx tsx packages/core/bench/group-bench.mts
|
||
* (tsx — эфемерный инструмент прогона, не зависимость пакета).
|
||
* Случайность — только createSeededRng с фиксированным seed (детерминизм).
|
||
*/
|
||
import { performance } from 'node:perf_hooks';
|
||
import { createSeededRng } from '../src/index.js';
|
||
|
||
const SIZE = 19;
|
||
const CELLS = SIZE * SIZE;
|
||
const POSITIONS = 40;
|
||
const ITERATIONS = 300;
|
||
|
||
/** Случайная позиция: клетки заняты с вероятностью density, цвет 50/50. */
|
||
function randomPosition(seed: number, density: number): Uint8Array {
|
||
const rng = createSeededRng(seed);
|
||
const grid = new Uint8Array(CELLS);
|
||
for (let index = 0; index < CELLS; index += 1) {
|
||
if (rng.next() < density) grid[index] = rng.next() < 0.5 ? 1 : 2;
|
||
}
|
||
return grid;
|
||
}
|
||
|
||
/** Соседи клетки по индексу. */
|
||
function neighborIndices(index: number): number[] {
|
||
const x = index % SIZE;
|
||
const y = Math.floor(index / SIZE);
|
||
const result: number[] = [];
|
||
if (x > 0) result.push(index - 1);
|
||
if (x < SIZE - 1) result.push(index + 1);
|
||
if (y > 0) result.push(index - SIZE);
|
||
if (y < SIZE - 1) result.push(index + SIZE);
|
||
return result;
|
||
}
|
||
|
||
/** Flood fill: группы + число дамэ (как в движке, groupAt). */
|
||
function groupsFloodFill(grid: Uint8Array): number {
|
||
const seen = new Uint8Array(CELLS);
|
||
let groups = 0;
|
||
let liberties = 0;
|
||
for (let start = 0; start < CELLS; start += 1) {
|
||
if (grid[start] === 0 || seen[start] === 1) continue;
|
||
groups += 1;
|
||
const color = grid[start];
|
||
const stack = [start];
|
||
seen[start] = 1;
|
||
const libs = new Set<number>();
|
||
while (stack.length > 0) {
|
||
const current = stack.pop() as number;
|
||
for (const next of neighborIndices(current)) {
|
||
if (grid[next] === 0) libs.add(next);
|
||
if (grid[next] === color && seen[next] === 0) {
|
||
seen[next] = 1;
|
||
stack.push(next);
|
||
}
|
||
}
|
||
}
|
||
liberties += libs.size;
|
||
}
|
||
return groups * 1000 + liberties;
|
||
}
|
||
|
||
/** Union-find: группы + число дамэ. */
|
||
function groupsUnionFind(grid: Uint8Array): number {
|
||
const parent = new Int32Array(CELLS);
|
||
for (let index = 0; index < CELLS; index += 1) parent[index] = index;
|
||
const find = (value: number): number => {
|
||
let root = value;
|
||
while (parent[root] !== root) root = parent[root] ?? root;
|
||
let node = value;
|
||
while (parent[node] !== node) {
|
||
const next = parent[node] ?? node;
|
||
parent[node] = root;
|
||
node = next;
|
||
}
|
||
return root;
|
||
};
|
||
for (let index = 0; index < CELLS; index += 1) {
|
||
if (grid[index] === 0) continue;
|
||
for (const next of neighborIndices(index)) {
|
||
if (grid[next] === grid[index]) parent[find(index)] = find(next);
|
||
}
|
||
}
|
||
const libsByRoot = new Map<number, Set<number>>();
|
||
for (let index = 0; index < CELLS; index += 1) {
|
||
if (grid[index] === 0) continue;
|
||
const root = find(index);
|
||
let libs = libsByRoot.get(root);
|
||
if (libs === undefined) {
|
||
libs = new Set<number>();
|
||
libsByRoot.set(root, libs);
|
||
}
|
||
for (const next of neighborIndices(index)) {
|
||
if (grid[next] === 0) libs.add(next);
|
||
}
|
||
}
|
||
let liberties = 0;
|
||
for (const libs of libsByRoot.values()) liberties += libs.size;
|
||
return libsByRoot.size * 1000 + liberties;
|
||
}
|
||
|
||
/** Прогон одного алгоритма на наборе позиций; возвращает мс и контрольную сумму. */
|
||
function measure(
|
||
positions: Uint8Array[],
|
||
algorithm: (grid: Uint8Array) => number,
|
||
): { ms: number; checksum: number } {
|
||
let checksum = 0;
|
||
const started = performance.now();
|
||
for (let iteration = 0; iteration < ITERATIONS; iteration += 1) {
|
||
for (const grid of positions) checksum = (checksum + algorithm(grid)) % 1000003;
|
||
}
|
||
return { ms: performance.now() - started, checksum };
|
||
}
|
||
|
||
const rng = createSeededRng(7);
|
||
const densities = [0.35, 0.55, 0.75];
|
||
let totalFlood = 0;
|
||
let totalUnion = 0;
|
||
for (const density of densities) {
|
||
const positions = Array.from({ length: POSITIONS }, (_, index) =>
|
||
randomPosition(Math.floor(rng.next() * 100000), density),
|
||
);
|
||
const flood = measure(positions, groupsFloodFill);
|
||
const union = measure(positions, groupsUnionFind);
|
||
if (flood.checksum !== union.checksum) {
|
||
console.warn(`ОШИБКА: контрольные суммы не совпали (${flood.checksum} != ${union.checksum})`);
|
||
process.exitCode = 1;
|
||
}
|
||
totalFlood += flood.ms;
|
||
totalUnion += union.ms;
|
||
console.warn(
|
||
`density=${density}: flood fill ${flood.ms.toFixed(1)} мс, union-find ${union.ms.toFixed(1)} мс, ` +
|
||
`отношение u/f ${(union.ms / flood.ms).toFixed(2)}`,
|
||
);
|
||
}
|
||
console.warn(
|
||
`ИТОГО: flood fill ${totalFlood.toFixed(1)} мс, union-find ${totalUnion.toFixed(1)} мс, ` +
|
||
`отношение u/f ${(totalUnion / totalFlood).toFixed(2)} ` +
|
||
`(${POSITIONS * ITERATIONS * densities.length} прогонов позиций 19x19)`,
|
||
);
|