go-learn/packages/core/bench/group-bench.mts
sab.code.lab 1c85091186 chore: восстановление репозитория из снапшота v0.3.1
Прежняя git-история утрачена при переносе проекта на машину владельца
(снапшот без .git). Хэши коммитов в docs/reports/* относятся к утраченной
истории.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-07 09:34:02 +02:00

141 lines
5.1 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Микро-бенчмарк: 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)`,
);