|
| 1 | +// Throwaway: how fast can the transpiled Newton kernel go with real JS multithreading? |
| 2 | +// Pure Node (worker_threads + SharedArrayBuffer), no Pyodide, no blosc2 -- isolates the |
| 3 | +// compute ceiling and per-dispatch overhead a Web Worker pool would hit in a browser. |
| 4 | +// The kernel is the same scalar loop dsl_js emits; hand-written here to avoid Pyodide. |
| 5 | +// |
| 6 | +// node bench/js-transpiler/worker-pool-bench.mjs |
| 7 | +import { Worker, isMainThread, workerData } from "node:worker_threads"; |
| 8 | +import os from "node:os"; |
| 9 | +import { performance } from "node:perf_hooks"; |
| 10 | + |
| 11 | +const WIDTH = 320, HEIGHT = 213, MAXITER = 48, NFRAMES = 24, SPANX = 3.4; |
| 12 | +const ASPECT = HEIGHT / WIDTH; |
| 13 | +const N = WIDTH * HEIGHT; |
| 14 | + |
| 15 | +// Striped rows (rowStart, rowStep): worker i does rows i, i+nw, ... so the per-pixel |
| 16 | +// early-exit work spreads evenly across workers instead of clumping in contiguous bands. |
| 17 | +function newtonBand(A, B, OUT, rowStart, rowStep, H, W, maxIter, relax) { |
| 18 | + for (let row = rowStart; row < H; row += rowStep) { |
| 19 | + for (let col = 0; col < W; col++) { |
| 20 | + const i = row * W + col; |
| 21 | + let za = A[i], zb = B[i], it = maxIter; |
| 22 | + for (let k = 0; k < maxIter; k++) { |
| 23 | + const a2 = za * za, b2 = zb * zb; |
| 24 | + const fr = za * a2 - 3 * za * b2 - 1, fi = 3 * a2 * zb - zb * b2; |
| 25 | + const dr = 3 * (a2 - b2), di = 6 * za * zb, den = dr * dr + di * di + 1e-12; |
| 26 | + const qr = relax * (fr * dr + fi * di) / den, qi = relax * (fi * dr - fr * di) / den; |
| 27 | + za -= qr; zb -= qi; |
| 28 | + if (qr * qr + qi * qi < 1e-6) { it = k; break; } |
| 29 | + } |
| 30 | + const d0 = (za - 1) * (za - 1) + zb * zb; |
| 31 | + const d1 = (za + 0.5) * (za + 0.5) + (zb - 0.8660254) * (zb - 0.8660254); |
| 32 | + const d2 = (za + 0.5) * (za + 0.5) + (zb + 0.8660254) * (zb + 0.8660254); |
| 33 | + let root = 0, md = d0; |
| 34 | + if (d1 < md) { md = d1; root = 1; } |
| 35 | + if (d2 < md) { root = 2; } |
| 36 | + OUT[i] = root + 0.9 * (it / maxIter); |
| 37 | + } |
| 38 | + } |
| 39 | +} |
| 40 | + |
| 41 | +// ctrl: Int32[ gen, done ]. params: Float64[ relax, maxIter ]. |
| 42 | +if (!isMainThread) { |
| 43 | + const { ctrlSab, paramsSab, aSab, bSab, outSab, rowStart, rowStep, W } = workerData; |
| 44 | + const ctrl = new Int32Array(ctrlSab), params = new Float64Array(paramsSab); |
| 45 | + const A = new Float64Array(aSab), B = new Float64Array(bSab), OUT = new Float64Array(outSab); |
| 46 | + let gen = 0; |
| 47 | + for (;;) { |
| 48 | + Atomics.wait(ctrl, 0, gen); // block until main bumps the generation |
| 49 | + gen = Atomics.load(ctrl, 0); |
| 50 | + if (gen < 0) break; // shutdown |
| 51 | + newtonBand(A, B, OUT, rowStart, rowStep, HEIGHT, W, params[1] | 0, params[0]); |
| 52 | + Atomics.add(ctrl, 1, 1); // signal this band done |
| 53 | + Atomics.notify(ctrl, 1); |
| 54 | + } |
| 55 | +} else { |
| 56 | + main(); |
| 57 | +} |
| 58 | + |
| 59 | +function buildGrid() { |
| 60 | + const aSab = new SharedArrayBuffer(N * 8), bSab = new SharedArrayBuffer(N * 8), |
| 61 | + outSab = new SharedArrayBuffer(N * 8); |
| 62 | + const A = new Float64Array(aSab), B = new Float64Array(bSab); |
| 63 | + const x0 = -SPANX / 2, dx = SPANX / (WIDTH - 1); |
| 64 | + const y0 = -SPANX * ASPECT / 2, dy = SPANX * ASPECT / (HEIGHT - 1); |
| 65 | + for (let r = 0; r < HEIGHT; r++) |
| 66 | + for (let c = 0; c < WIDTH; c++) { A[r * WIDTH + c] = x0 + dx * c; B[r * WIDTH + c] = y0 + dy * r; } |
| 67 | + return { aSab, bSab, outSab }; |
| 68 | +} |
| 69 | + |
| 70 | +function timeBest(fn, runs) { |
| 71 | + for (let w = 0; w < 2; w++) fn(); // warm V8 / workers |
| 72 | + let best = Infinity; |
| 73 | + for (let r = 0; r < runs; r++) { const t = performance.now(); fn(); best = Math.min(best, performance.now() - t); } |
| 74 | + return best; |
| 75 | +} |
| 76 | + |
| 77 | +async function benchPool(nw, sabs, relaxes) { |
| 78 | + const ctrlSab = new SharedArrayBuffer(8), paramsSab = new SharedArrayBuffer(16); |
| 79 | + const ctrl = new Int32Array(ctrlSab), params = new Float64Array(paramsSab); |
| 80 | + params[1] = MAXITER; |
| 81 | + const workers = []; |
| 82 | + for (let i = 0; i < nw; i++) { |
| 83 | + workers.push(new Worker(new URL(import.meta.url), { |
| 84 | + workerData: { ...sabs, ctrlSab, paramsSab, rowStart: i, rowStep: nw, W: WIDTH }, |
| 85 | + })); |
| 86 | + } |
| 87 | + const frame = (relax) => { |
| 88 | + Atomics.store(ctrl, 1, 0); |
| 89 | + params[0] = relax; |
| 90 | + Atomics.add(ctrl, 0, 1); |
| 91 | + Atomics.notify(ctrl, 0, nw); |
| 92 | + let d; // barrier: wait until all bands reported done |
| 93 | + while ((d = Atomics.load(ctrl, 1)) < nw) Atomics.wait(ctrl, 1, d); |
| 94 | + }; |
| 95 | + const sweep = () => { for (const rx of relaxes) frame(rx); }; |
| 96 | + const best = timeBest(sweep, 5); |
| 97 | + Atomics.store(ctrl, 0, -1); Atomics.notify(ctrl, 0, nw); // shutdown |
| 98 | + await Promise.all(workers.map((w) => w.terminate())); |
| 99 | + return best; |
| 100 | +} |
| 101 | + |
| 102 | +async function main() { |
| 103 | + const cores = os.cpus().length; |
| 104 | + const sabs = buildGrid(); |
| 105 | + const OUT = new Float64Array(sabs.outSab); |
| 106 | + const relaxes = Array.from({ length: NFRAMES }, (_, i) => 1.0 + (1.85 - 1.0) * i / (NFRAMES - 1)); |
| 107 | + |
| 108 | + // Single-thread baseline on the main thread (no worker overhead at all). |
| 109 | + const A = new Float64Array(sabs.aSab), B = new Float64Array(sabs.bSab); |
| 110 | + const tSingle = timeBest(() => { for (const rx of relaxes) newtonBand(A, B, OUT, 0, 1, HEIGHT, WIDTH, MAXITER, rx); }, 5); |
| 111 | + const ref = Float64Array.from(OUT); // last frame (relax=1.85), for correctness check |
| 112 | + |
| 113 | + console.log(`Newton ${WIDTH}x${HEIGHT}, max_iter=${MAXITER}, ${NFRAMES}-frame sweep | cores=${cores}`); |
| 114 | + const per = (ms) => `${ms.toFixed(0)} ms total (${(ms / NFRAMES).toFixed(2)} ms/frame)`; |
| 115 | + console.log(`\nsingle-thread (main): ${per(tSingle)}`); |
| 116 | + |
| 117 | + const counts = [...new Set([1, 2, 4, cores])].filter((n) => n >= 1 && n <= cores * 2).sort((a, b) => a - b); |
| 118 | + for (const nw of counts) { |
| 119 | + const t = await benchPool(nw, sabs, relaxes); |
| 120 | + let maxdiff = 0; |
| 121 | + for (let i = 0; i < N; i++) maxdiff = Math.max(maxdiff, Math.abs(OUT[i] - ref[i])); |
| 122 | + const sp = tSingle / t; |
| 123 | + console.log(`pool x${String(nw).padStart(2)} : ${per(t)} | speedup ${sp.toFixed(2)}x` + |
| 124 | + ` eff ${(100 * sp / nw).toFixed(0)}% | maxdiff ${maxdiff.toExponential(1)}`); |
| 125 | + } |
| 126 | +} |
0 commit comments