@fraczak/k 6.10.3 → 6.10.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +43 -0
- package/DOCS/FILE_FORMATS.md +51 -16
- package/Examples/karatsuba-mult.k +215 -0
- package/backends/arm64/scripts/run-tests.mjs +16 -0
- package/backends/llvm/bin/k-llvm-build.mjs +40 -14
- package/backends/llvm/bin/k-llvm-compile.mjs +29 -12
- package/backends/llvm/runtime/krt.c +10 -10
- package/backends/llvm/runtime/krt.h +6 -1
- package/backends/llvm/scripts/run-tests.mjs +16 -0
- package/backends/llvm/src/cli.mjs +1 -1
- package/backends/llvm/src/executable.mjs +36 -0
- package/backends/llvm/src/kvm2llvm.mjs +10 -4
- package/backends/llvm/tests/integration.sh +11 -0
- package/backends/llvm/tests/test-llvm.mjs +15 -1
- package/backends/wasm/scripts/run-tests.mjs +16 -0
- package/browser/vfs-data.mjs +1 -1
- package/core.k +5 -5
- package/package.json +2 -1
- package/scripts/benchmark-idempotency.mjs +27 -0
- package/scripts/benchmark.mjs +51 -0
- package/scripts/build-repl-html.mjs +17 -0
- package/scripts/generate-dependency-graph.mjs +25 -1
- package/scripts/perf-ieee.mjs +75 -0
- package/scripts/perf-int.mjs +67 -0
- package/scripts/perf-multiplication.mjs +644 -0
- package/scripts/perf-poly.mjs +67 -0
- package/scripts/render-benchmark.mjs +25 -0
- package/scripts/run-tests.mjs +19 -0
- package/tests/test-karatsuba.mjs +133 -0
|
@@ -0,0 +1,644 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Multiplication Algorithm & Multi-Backend Performance Harness
|
|
5
|
+
*
|
|
6
|
+
* Compares multiplication algorithms ('times' vs 'karatsuba')
|
|
7
|
+
* across available execution backends:
|
|
8
|
+
* - LLVM (native binary via Clang/LLVM)
|
|
9
|
+
* - WebAssembly (persistent runner via Wasm)
|
|
10
|
+
* - kVM Interpreter (envelope-free bytecode engine)
|
|
11
|
+
* - Native JS (envelope-free convergence engine)
|
|
12
|
+
* - ARM64 (native binary, on supported Linux aarch64 hosts)
|
|
13
|
+
*
|
|
14
|
+
* Modes:
|
|
15
|
+
* --mode squaring : Successive squarings starting from --input
|
|
16
|
+
* (bit length doubles each step)
|
|
17
|
+
* --mode sizes : Multiplies pairs of numbers of specific bit sizes
|
|
18
|
+
* (--sizes 64,128,256,512,...)
|
|
19
|
+
*
|
|
20
|
+
* Safety features:
|
|
21
|
+
* - Per-step timeout (--timeout <ms>, default: 30000ms)
|
|
22
|
+
* - Max RSS memory cap (--max-rss <mb>, default: 1024MB)
|
|
23
|
+
* - Automated conformance checking against BigInt oracle
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import fs from "node:fs";
|
|
27
|
+
import os from "node:os";
|
|
28
|
+
import path from "node:path";
|
|
29
|
+
import { performance } from "node:perf_hooks";
|
|
30
|
+
import { fileURLToPath } from "node:url";
|
|
31
|
+
|
|
32
|
+
import {
|
|
33
|
+
codes,
|
|
34
|
+
createState,
|
|
35
|
+
decodeWire,
|
|
36
|
+
evaluateInput,
|
|
37
|
+
executeKVM,
|
|
38
|
+
run_converged,
|
|
39
|
+
valueForCode
|
|
40
|
+
} from "@fraczak/k/backend-api.mjs";
|
|
41
|
+
import { parse as parseIntValue } from "@fraczak/k/codecs/int.mjs";
|
|
42
|
+
import { compileObjectBuffer } from "@fraczak/k/object.mjs";
|
|
43
|
+
import {
|
|
44
|
+
compileObjectToExecutable
|
|
45
|
+
} from "../backends/llvm/src/executable.mjs";
|
|
46
|
+
import {
|
|
47
|
+
compileWasmArtifactFromObject
|
|
48
|
+
} from "../backends/wasm/src/wasm.mjs";
|
|
49
|
+
import {
|
|
50
|
+
PersistentExecutable,
|
|
51
|
+
makeCacheDir,
|
|
52
|
+
prepareRelation,
|
|
53
|
+
wireInput
|
|
54
|
+
} from "../backends/llvm/tests/perf-support.mjs";
|
|
55
|
+
|
|
56
|
+
const args = process.argv.slice(2);
|
|
57
|
+
function getArg(flag, fallback) {
|
|
58
|
+
const idx = args.indexOf(flag);
|
|
59
|
+
return idx !== -1 && args[idx + 1] ? args[idx + 1] : fallback;
|
|
60
|
+
}
|
|
61
|
+
function hasFlag(flag) {
|
|
62
|
+
return args.includes(flag);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function printHelp() {
|
|
66
|
+
console.log(`Usage: node scripts/perf-multiplication.mjs [options]
|
|
67
|
+
|
|
68
|
+
Multiplication Algorithm & Multi-Backend Performance Harness.
|
|
69
|
+
Compares multiplication algorithms ('times' vs 'karatsuba') across
|
|
70
|
+
available execution backends (LLVM, Wasm, kVM, JS, ARM64).
|
|
71
|
+
|
|
72
|
+
Options:
|
|
73
|
+
--mode <squaring|sizes>
|
|
74
|
+
Benchmark mode: 'squaring' (successive squarings starting from
|
|
75
|
+
--input) or 'sizes' (multiplication of fixed bit-width pairs).
|
|
76
|
+
(default: squaring)
|
|
77
|
+
--backends <csv>
|
|
78
|
+
Comma-separated list of execution backends to test:
|
|
79
|
+
llvm, wasm, kvm, js, arm64. (default: llvm,wasm,kvm,js)
|
|
80
|
+
--algos <csv>
|
|
81
|
+
Comma-separated list of multiplication algorithms to test:
|
|
82
|
+
times, karatsuba. (default: times,karatsuba)
|
|
83
|
+
--steps <n>
|
|
84
|
+
Number of squaring steps in squaring mode. (default: 8)
|
|
85
|
+
--input <int>
|
|
86
|
+
Initial integer operand for squaring mode. (default: 987654321)
|
|
87
|
+
--sizes <csv>
|
|
88
|
+
Comma-separated bit sizes in sizes mode.
|
|
89
|
+
(default: 64,128,256,512,1024,2048)
|
|
90
|
+
--timeout <ms>
|
|
91
|
+
Execution timeout per step in milliseconds. (default: 30000)
|
|
92
|
+
--max-rss <mb>
|
|
93
|
+
Maximum RSS memory limit in MB before stopping. (default: 1024)
|
|
94
|
+
--json
|
|
95
|
+
Emit benchmark results in JSON format.
|
|
96
|
+
-h, --help
|
|
97
|
+
Show this help message and exit.
|
|
98
|
+
|
|
99
|
+
Environment Variables:
|
|
100
|
+
K_LLVM_CLANG_OPT
|
|
101
|
+
Clang optimization flag for LLVM compilation (e.g. -O3).
|
|
102
|
+
K_LLVM_RUNTIME_MODE
|
|
103
|
+
LLVM runtime compilation mode ('fast' or 'compact').
|
|
104
|
+
K_LLVM_CACHE_DIR
|
|
105
|
+
Directory used to cache compiled LLVM executable binaries.
|
|
106
|
+
LLVM_PIPELINE
|
|
107
|
+
Set to '1' to use persistent pipelined LLVM runner mode.
|
|
108
|
+
LLVM_SPAWN_PER_CALL
|
|
109
|
+
Set to '1' to spawn a new LLVM process on each invocation.
|
|
110
|
+
ARM64_PIPELINE
|
|
111
|
+
Set to '1' to use persistent pipelined ARM64 runner mode.
|
|
112
|
+
ARM64_SPAWN_PER_CALL
|
|
113
|
+
Set to '1' to spawn a new ARM64 process on each invocation.`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (hasFlag("-h") || hasFlag("--help")) {
|
|
117
|
+
printHelp();
|
|
118
|
+
process.exit(0);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const mode = getArg("--mode", "squaring"); // 'squaring' or 'sizes'
|
|
122
|
+
const backendsArg = getArg("--backends", "llvm,wasm,kvm,js");
|
|
123
|
+
const selectedBackends = backendsArg
|
|
124
|
+
.split(",").map(b => b.trim().toLowerCase()).filter(Boolean);
|
|
125
|
+
const algosArg = getArg("--algos", "times,karatsuba");
|
|
126
|
+
const selectedAlgos = algosArg
|
|
127
|
+
.split(",").map(a => a.trim().toLowerCase()).filter(Boolean);
|
|
128
|
+
|
|
129
|
+
const squaringSteps = parseInt(getArg("--steps", "8"), 10);
|
|
130
|
+
const squaringInputText = getArg("--input", "987654321");
|
|
131
|
+
const sizesList = getArg("--sizes", "64,128,256,512,1024,2048")
|
|
132
|
+
.split(",")
|
|
133
|
+
.map(s => parseInt(s.trim(), 10))
|
|
134
|
+
.filter(n => !isNaN(n) && n > 0);
|
|
135
|
+
const stepTimeoutMs = parseInt(getArg("--timeout", "30000"), 10);
|
|
136
|
+
const maxRssMb = parseInt(getArg("--max-rss", "1024"), 10);
|
|
137
|
+
const outputJson = hasFlag("--json");
|
|
138
|
+
|
|
139
|
+
// Helper: Inspect process RSS / peak RSS
|
|
140
|
+
function getProcessMemory(pid) {
|
|
141
|
+
try {
|
|
142
|
+
const status = fs.readFileSync(`/proc/${pid}/status`, "utf8");
|
|
143
|
+
const vmrss = status.match(/VmRSS:\s+(\d+)\s+kB/);
|
|
144
|
+
const vmhwm = status.match(/VmHWM:\s+(\d+)\s+kB/);
|
|
145
|
+
const parseMb = (match) =>
|
|
146
|
+
match ? Number((parseInt(match[1], 10) / 1024).toFixed(1)) : null;
|
|
147
|
+
return {
|
|
148
|
+
rssMb: parseMb(vmrss),
|
|
149
|
+
peakRssMb: parseMb(vmhwm)
|
|
150
|
+
};
|
|
151
|
+
} catch {
|
|
152
|
+
return { rssMb: null, peakRssMb: null };
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function getNodeMemory() {
|
|
157
|
+
const mem = process.memoryUsage();
|
|
158
|
+
return {
|
|
159
|
+
heapMb: Number((mem.heapUsed / (1024 * 1024)).toFixed(1)),
|
|
160
|
+
rssMb: Number((mem.rss / (1024 * 1024)).toFixed(1))
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function countBitsValue(val) {
|
|
165
|
+
let count = 0;
|
|
166
|
+
let cur = val?.tag === "+" || val?.tag === "-" ? val.value : val;
|
|
167
|
+
while (cur && (cur.tag === "0" || cur.tag === "1")) {
|
|
168
|
+
count++;
|
|
169
|
+
cur = cur.value;
|
|
170
|
+
}
|
|
171
|
+
return count;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function countBitsWire(wireBuf) {
|
|
175
|
+
try {
|
|
176
|
+
const decoded = decodeWire(wireBuf);
|
|
177
|
+
return countBitsValue(decoded.value);
|
|
178
|
+
} catch {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async function requestWithTimeout(server, wire, timeoutMs) {
|
|
184
|
+
let timer;
|
|
185
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
186
|
+
timer = setTimeout(
|
|
187
|
+
() => reject(new Error(`Timeout after ${timeoutMs}ms`)),
|
|
188
|
+
timeoutMs
|
|
189
|
+
);
|
|
190
|
+
});
|
|
191
|
+
try {
|
|
192
|
+
return await Promise.race([server.request(wire), timeoutPromise]);
|
|
193
|
+
} finally {
|
|
194
|
+
clearTimeout(timer);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Format numbers
|
|
199
|
+
function fmtMs(ms) {
|
|
200
|
+
if (ms == null) return "-";
|
|
201
|
+
if (ms < 1) return `${(ms * 1000).toFixed(0)}µs`;
|
|
202
|
+
if (ms < 10) return `${ms.toFixed(2)}ms`;
|
|
203
|
+
if (ms < 1000) return `${ms.toFixed(1)}ms`;
|
|
204
|
+
return `${(ms / 1000).toFixed(2)}s`;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function fmtSpeedup(baseMs, altMs) {
|
|
208
|
+
if (baseMs == null || altMs == null || altMs === 0) return "-";
|
|
209
|
+
const ratio = baseMs / altMs;
|
|
210
|
+
return `${ratio.toFixed(2)}x`;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const sep72 = "=".repeat(72);
|
|
214
|
+
console.log(sep72);
|
|
215
|
+
console.log(" MULTIPLICATION ALGORITHM & MULTI-BACKEND HARNESS");
|
|
216
|
+
console.log(sep72);
|
|
217
|
+
console.log(`Mode: ${mode}`);
|
|
218
|
+
console.log(`Algorithms: ${selectedAlgos.join(", ")}`);
|
|
219
|
+
console.log(`Backends: ${selectedBackends.join(", ")}`);
|
|
220
|
+
if (mode === "squaring") {
|
|
221
|
+
console.log(
|
|
222
|
+
`Squaring Steps: ${squaringSteps} (input: ${squaringInputText})`
|
|
223
|
+
);
|
|
224
|
+
} else {
|
|
225
|
+
console.log(`Bit Sizes: ${sizesList.join(", ")} bits`);
|
|
226
|
+
}
|
|
227
|
+
console.log(
|
|
228
|
+
`Timeout / step: ${stepTimeoutMs} ms | Max RSS: ${maxRssMb} MB`
|
|
229
|
+
);
|
|
230
|
+
console.log("-".repeat(72) + "\n");
|
|
231
|
+
|
|
232
|
+
// 1. Map algorithms to their source files
|
|
233
|
+
const algoFiles = {
|
|
234
|
+
times: fileURLToPath(
|
|
235
|
+
import.meta.resolve("../Examples/arithmetics.k")
|
|
236
|
+
),
|
|
237
|
+
karatsuba: fileURLToPath(
|
|
238
|
+
import.meta.resolve("../Examples/karatsuba-mult.k")
|
|
239
|
+
)
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
const cacheDir = makeCacheDir("k-mult-perf-");
|
|
243
|
+
|
|
244
|
+
// 2. Prepare Squaring Relations & Binaries
|
|
245
|
+
// For squaring: relation name `s_${algo} = {()x, ()y} ${algo}; s_${algo}`
|
|
246
|
+
const algoRelations = {};
|
|
247
|
+
const algoServers = { llvm: {}, wasm: {} };
|
|
248
|
+
|
|
249
|
+
let commonState = null;
|
|
250
|
+
|
|
251
|
+
for (const algo of selectedAlgos) {
|
|
252
|
+
const filePath = algoFiles[algo] || algoFiles.times;
|
|
253
|
+
const rawSource = fs.readFileSync(filePath, "utf8");
|
|
254
|
+
const libSource = rawSource.replace(/\s*\(\)\s*$/, "\n");
|
|
255
|
+
|
|
256
|
+
const algoState = createState();
|
|
257
|
+
await evaluateInput(`:load ${filePath}`, algoState);
|
|
258
|
+
codes.load(algoState.codes);
|
|
259
|
+
|
|
260
|
+
if (!commonState) commonState = algoState;
|
|
261
|
+
|
|
262
|
+
const relName = `s_${algo}`;
|
|
263
|
+
await evaluateInput(`${relName} = {()x, ()y} ${algo};`, algoState);
|
|
264
|
+
codes.load(algoState.codes);
|
|
265
|
+
|
|
266
|
+
const relSource =
|
|
267
|
+
`${libSource}\n${relName} = {()x, ()y} ${algo};\n${relName}`;
|
|
268
|
+
const prepared = prepareRelation(algoState, relName, {
|
|
269
|
+
source: relSource,
|
|
270
|
+
sourceLabel: `${filePath}#${relName}`
|
|
271
|
+
});
|
|
272
|
+
prepared.state = algoState;
|
|
273
|
+
algoRelations[algo] = prepared;
|
|
274
|
+
|
|
275
|
+
// LLVM
|
|
276
|
+
if (selectedBackends.includes("llvm")) {
|
|
277
|
+
const exePath = path.join(cacheDir, `perf_mult_llvm_${algo}`);
|
|
278
|
+
try {
|
|
279
|
+
compileObjectToExecutable(
|
|
280
|
+
algoRelations[algo].object, exePath, { clangOpt: "-O2" }
|
|
281
|
+
);
|
|
282
|
+
algoServers.llvm[algo] = new PersistentExecutable(exePath);
|
|
283
|
+
} catch (e) {
|
|
284
|
+
console.warn(
|
|
285
|
+
`[WARN] Failed to compile LLVM for ${algo}: ${e.message}`
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Wasm
|
|
291
|
+
if (selectedBackends.includes("wasm")) {
|
|
292
|
+
const wasmPath = path.join(cacheDir, `perf_mult_wasm_${algo}.wasm`);
|
|
293
|
+
const exePath = path.join(cacheDir, `perf_mult_wasm_${algo}.exe`);
|
|
294
|
+
try {
|
|
295
|
+
const wasmBuf = await compileWasmArtifactFromObject(
|
|
296
|
+
algoRelations[algo].object, { entry: "__main__" }
|
|
297
|
+
);
|
|
298
|
+
fs.writeFileSync(wasmPath, wasmBuf);
|
|
299
|
+
const runBin = fileURLToPath(
|
|
300
|
+
import.meta.resolve("../backends/wasm/bin/k-wasm-run.mjs")
|
|
301
|
+
);
|
|
302
|
+
fs.writeFileSync(
|
|
303
|
+
exePath,
|
|
304
|
+
`#!/bin/sh\nexec node "${runBin}" "${wasmPath}" "$@"\n`,
|
|
305
|
+
{ mode: 0o755 }
|
|
306
|
+
);
|
|
307
|
+
algoServers.wasm[algo] = new PersistentExecutable(exePath);
|
|
308
|
+
} catch (e) {
|
|
309
|
+
console.warn(
|
|
310
|
+
`[WARN] Failed to compile Wasm for ${algo}: ${e.message}`
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// 3. Squaring Mode Benchmark
|
|
317
|
+
if (mode === "squaring") {
|
|
318
|
+
const sInitVal = valueForCode(
|
|
319
|
+
parseIntValue(squaringInputText),
|
|
320
|
+
commonState.typeAliases.int,
|
|
321
|
+
codes.find
|
|
322
|
+
);
|
|
323
|
+
const { inputWire: sInitWire } = wireInput(sInitVal);
|
|
324
|
+
|
|
325
|
+
const results = {};
|
|
326
|
+
for (const backend of selectedBackends) {
|
|
327
|
+
results[backend] = {};
|
|
328
|
+
for (const algo of selectedAlgos) {
|
|
329
|
+
results[backend][algo] = [];
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const stepBits = [];
|
|
334
|
+
|
|
335
|
+
try {
|
|
336
|
+
// A. Run persistent backends (LLVM, Wasm)
|
|
337
|
+
const activeBackends = ["llvm", "wasm"]
|
|
338
|
+
.filter(b => selectedBackends.includes(b));
|
|
339
|
+
for (const backend of activeBackends) {
|
|
340
|
+
for (const algo of selectedAlgos) {
|
|
341
|
+
const server = algoServers[backend]?.[algo];
|
|
342
|
+
if (!server) continue;
|
|
343
|
+
|
|
344
|
+
console.log(
|
|
345
|
+
`==> Running [${backend.toUpperCase()}] with [${algo}]...`
|
|
346
|
+
);
|
|
347
|
+
let curWire = sInitWire;
|
|
348
|
+
for (let step = 1; step <= squaringSteps; step++) {
|
|
349
|
+
const memBefore = getProcessMemory(server.child.pid);
|
|
350
|
+
if (memBefore.peakRssMb && memBefore.peakRssMb > maxRssMb) {
|
|
351
|
+
console.warn(
|
|
352
|
+
`[ABORT] Step ${step} aborted: peak RSS ` +
|
|
353
|
+
`(${memBefore.peakRssMb}MB) exceeded cap (${maxRssMb}MB)`
|
|
354
|
+
);
|
|
355
|
+
results[backend][algo].push({
|
|
356
|
+
status: "oom-abort",
|
|
357
|
+
timeMs: null
|
|
358
|
+
});
|
|
359
|
+
break;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
try {
|
|
363
|
+
const t0 = performance.now();
|
|
364
|
+
curWire = await requestWithTimeout(
|
|
365
|
+
server, curWire, stepTimeoutMs
|
|
366
|
+
);
|
|
367
|
+
const t1 = performance.now();
|
|
368
|
+
const timeMs = t1 - t0;
|
|
369
|
+
const memAfter = getProcessMemory(server.child.pid);
|
|
370
|
+
|
|
371
|
+
if (stepBits[step - 1] == null) {
|
|
372
|
+
stepBits[step - 1] = countBitsWire(curWire);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
results[backend][algo].push({
|
|
376
|
+
status: "ok",
|
|
377
|
+
timeMs,
|
|
378
|
+
peakRssMb: memAfter.peakRssMb,
|
|
379
|
+
wireLen: curWire.length
|
|
380
|
+
});
|
|
381
|
+
} catch (e) {
|
|
382
|
+
console.warn(
|
|
383
|
+
` ${backend} ${algo} step ${step} stopped: ${e.message}`
|
|
384
|
+
);
|
|
385
|
+
results[backend][algo].push({
|
|
386
|
+
status: "failed",
|
|
387
|
+
error: e.message
|
|
388
|
+
});
|
|
389
|
+
break;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// B. Run in-process backends (kVM, JS)
|
|
396
|
+
if (selectedBackends.includes("kvm")) {
|
|
397
|
+
for (const algo of selectedAlgos) {
|
|
398
|
+
console.log(`==> Running [kVM] with [${algo}]...`);
|
|
399
|
+
let curVal = sInitVal;
|
|
400
|
+
const kvmContext = {
|
|
401
|
+
rels: algoRelations[algo].state.rels,
|
|
402
|
+
findCode: codes.find,
|
|
403
|
+
options: { envelopeFree: true }
|
|
404
|
+
};
|
|
405
|
+
const kvmFunc = algoRelations[algo].kvmFunc;
|
|
406
|
+
|
|
407
|
+
for (let step = 1; step <= squaringSteps; step++) {
|
|
408
|
+
try {
|
|
409
|
+
const t0 = performance.now();
|
|
410
|
+
curVal = executeKVM(kvmFunc, curVal, kvmContext);
|
|
411
|
+
const t1 = performance.now();
|
|
412
|
+
const timeMs = t1 - t0;
|
|
413
|
+
const mem = getNodeMemory();
|
|
414
|
+
|
|
415
|
+
if (stepBits[step - 1] == null) {
|
|
416
|
+
stepBits[step - 1] = countBitsValue(curVal);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
results.kvm[algo].push({
|
|
420
|
+
status: "ok",
|
|
421
|
+
timeMs,
|
|
422
|
+
heapMb: mem.heapMb
|
|
423
|
+
});
|
|
424
|
+
if (timeMs > stepTimeoutMs) {
|
|
425
|
+
console.warn(
|
|
426
|
+
` kVM ${algo} step ${step} exceeded timeout ` +
|
|
427
|
+
`(${fmtMs(timeMs)})`
|
|
428
|
+
);
|
|
429
|
+
break;
|
|
430
|
+
}
|
|
431
|
+
} catch (e) {
|
|
432
|
+
console.warn(
|
|
433
|
+
` kVM ${algo} step ${step} stopped: ${e.message}`
|
|
434
|
+
);
|
|
435
|
+
results.kvm[algo].push({
|
|
436
|
+
status: "failed",
|
|
437
|
+
error: e.message
|
|
438
|
+
});
|
|
439
|
+
break;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
if (selectedBackends.includes("js")) {
|
|
446
|
+
for (const algo of selectedAlgos) {
|
|
447
|
+
console.log(`==> Running [JS Free] with [${algo}]...`);
|
|
448
|
+
let curVal = sInitVal;
|
|
449
|
+
run_converged.defs = algoRelations[algo].state;
|
|
450
|
+
const relDef = algoRelations[algo].relDef;
|
|
451
|
+
|
|
452
|
+
for (let step = 1; step <= squaringSteps; step++) {
|
|
453
|
+
try {
|
|
454
|
+
const t0 = performance.now();
|
|
455
|
+
curVal = run_converged(
|
|
456
|
+
codes.find, relDef.def, curVal, relDef.typePatternGraph
|
|
457
|
+
);
|
|
458
|
+
const t1 = performance.now();
|
|
459
|
+
const timeMs = t1 - t0;
|
|
460
|
+
const mem = getNodeMemory();
|
|
461
|
+
|
|
462
|
+
if (stepBits[step - 1] == null) {
|
|
463
|
+
stepBits[step - 1] = countBitsValue(curVal);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
results.js[algo].push({
|
|
467
|
+
status: "ok",
|
|
468
|
+
timeMs,
|
|
469
|
+
heapMb: mem.heapMb
|
|
470
|
+
});
|
|
471
|
+
if (timeMs > stepTimeoutMs) {
|
|
472
|
+
console.warn(
|
|
473
|
+
` JS ${algo} step ${step} exceeded timeout ` +
|
|
474
|
+
`(${fmtMs(timeMs)})`
|
|
475
|
+
);
|
|
476
|
+
break;
|
|
477
|
+
}
|
|
478
|
+
} catch (e) {
|
|
479
|
+
console.warn(
|
|
480
|
+
` JS ${algo} step ${step} stopped: ${e.message}`
|
|
481
|
+
);
|
|
482
|
+
results.js[algo].push({
|
|
483
|
+
status: "failed",
|
|
484
|
+
error: e.message
|
|
485
|
+
});
|
|
486
|
+
break;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// 4. Print Squaring Results Table
|
|
493
|
+
console.log("\n" + sep72);
|
|
494
|
+
console.log(" SQUARING BENCHMARK RESULTS");
|
|
495
|
+
console.log(sep72);
|
|
496
|
+
|
|
497
|
+
for (const backend of selectedBackends) {
|
|
498
|
+
console.log(`\n### Backend: ${backend.toUpperCase()}`);
|
|
499
|
+
const hasTimes = selectedAlgos.includes("times");
|
|
500
|
+
const hasKara = selectedAlgos.includes("karatsuba");
|
|
501
|
+
|
|
502
|
+
let header = "Step | Bits ";
|
|
503
|
+
if (hasTimes) header += "| Times Time ";
|
|
504
|
+
if (hasKara) header += "| Karatsuba ";
|
|
505
|
+
if (hasTimes && hasKara) header += "| Speedup (T/K)";
|
|
506
|
+
if (hasTimes) header += "| Times Mem ";
|
|
507
|
+
if (hasKara) header += "| Kara Mem ";
|
|
508
|
+
console.log(header);
|
|
509
|
+
console.log("-".repeat(header.length));
|
|
510
|
+
|
|
511
|
+
for (let s = 1; s <= squaringSteps; s++) {
|
|
512
|
+
const bits = stepBits[s - 1] != null
|
|
513
|
+
? `${stepBits[s - 1]}b`.padEnd(5)
|
|
514
|
+
: " ? ";
|
|
515
|
+
const tEntry = results[backend].times?.[s - 1];
|
|
516
|
+
const kEntry = results[backend].karatsuba?.[s - 1];
|
|
517
|
+
|
|
518
|
+
if (!tEntry && !kEntry) break;
|
|
519
|
+
|
|
520
|
+
const formatEntry = (e) =>
|
|
521
|
+
e?.status === "ok" ? fmtMs(e.timeMs)
|
|
522
|
+
: (e?.error ? "error" : "-");
|
|
523
|
+
const tTime = formatEntry(tEntry);
|
|
524
|
+
const kTime = formatEntry(kEntry);
|
|
525
|
+
const speedup = (tEntry?.status === "ok" &&
|
|
526
|
+
kEntry?.status === "ok")
|
|
527
|
+
? fmtSpeedup(tEntry.timeMs, kEntry.timeMs)
|
|
528
|
+
: "-";
|
|
529
|
+
|
|
530
|
+
function fmtMem(entry) {
|
|
531
|
+
if (entry?.peakRssMb != null) return `${entry.peakRssMb}MB RSS`;
|
|
532
|
+
if (entry?.heapMb != null) return `${entry.heapMb}MB heap`;
|
|
533
|
+
return "-";
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
let row = `${s.toString().padStart(4)} | ${bits} `;
|
|
537
|
+
if (hasTimes) row += `| ${tTime.padStart(11)} `;
|
|
538
|
+
if (hasKara) row += `| ${kTime.padStart(11)} `;
|
|
539
|
+
if (hasTimes && hasKara) row += `| ${speedup.padStart(13)} `;
|
|
540
|
+
if (hasTimes) row += `| ${fmtMem(tEntry).padStart(11)} `;
|
|
541
|
+
if (hasKara) row += `| ${fmtMem(kEntry).padStart(11)} `;
|
|
542
|
+
console.log(row);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
console.log();
|
|
547
|
+
|
|
548
|
+
} finally {
|
|
549
|
+
// Teardown persistent servers
|
|
550
|
+
for (const backend of ["llvm", "wasm"]) {
|
|
551
|
+
for (const algo of selectedAlgos) {
|
|
552
|
+
algoServers[backend]?.[algo]?.child?.kill();
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// 4. Bit Sizes Mode (Multiply random N-bit numbers)
|
|
559
|
+
if (mode === "sizes") {
|
|
560
|
+
console.log("==> Running Bit Sizes Multiplication Mode...");
|
|
561
|
+
function randomBigIntBits(bits) {
|
|
562
|
+
let hex = "";
|
|
563
|
+
const nibbles = Math.ceil(bits / 4);
|
|
564
|
+
for (let i = 0; i < nibbles; i++) {
|
|
565
|
+
hex += Math.floor(Math.random() * 16).toString(16);
|
|
566
|
+
}
|
|
567
|
+
const val = BigInt(`0x${hex}`) | (1n << BigInt(bits - 1));
|
|
568
|
+
return val.toString();
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
const th = "Size (bits) | Backend | Times " +
|
|
572
|
+
"| Karatsuba | Speedup (T/K) | Conformance";
|
|
573
|
+
console.log("\n" + th);
|
|
574
|
+
console.log("-".repeat(th.length));
|
|
575
|
+
|
|
576
|
+
const relDefTimes = selectedAlgos.includes("times")
|
|
577
|
+
? prepareRelation(algoRelations.times.state, "times").relDef
|
|
578
|
+
: null;
|
|
579
|
+
const relDefKara = selectedAlgos.includes("karatsuba")
|
|
580
|
+
? prepareRelation(algoRelations.karatsuba.state, "karatsuba").relDef
|
|
581
|
+
: null;
|
|
582
|
+
|
|
583
|
+
for (const bits of sizesList) {
|
|
584
|
+
const strA = randomBigIntBits(bits);
|
|
585
|
+
const strB = randomBigIntBits(bits);
|
|
586
|
+
|
|
587
|
+
const valA = valueForCode(
|
|
588
|
+
parseIntValue(strA), commonState.typeAliases.int, codes.find
|
|
589
|
+
);
|
|
590
|
+
const valB = valueForCode(
|
|
591
|
+
parseIntValue(strB), commonState.typeAliases.int, codes.find
|
|
592
|
+
);
|
|
593
|
+
const pairInput = { tag: "product", value: { x: valA, y: valB } };
|
|
594
|
+
|
|
595
|
+
for (const backend of selectedBackends) {
|
|
596
|
+
if (backend === "js") {
|
|
597
|
+
let resTimes = null;
|
|
598
|
+
let tTime = null;
|
|
599
|
+
if (relDefTimes) {
|
|
600
|
+
run_converged.defs = algoRelations.times.state;
|
|
601
|
+
const t0_t = performance.now();
|
|
602
|
+
resTimes = run_converged(
|
|
603
|
+
codes.find,
|
|
604
|
+
relDefTimes.def,
|
|
605
|
+
pairInput,
|
|
606
|
+
relDefTimes.typePatternGraph
|
|
607
|
+
);
|
|
608
|
+
tTime = performance.now() - t0_t;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
let resKara = null;
|
|
612
|
+
let kTime = null;
|
|
613
|
+
if (relDefKara) {
|
|
614
|
+
run_converged.defs = algoRelations.karatsuba.state;
|
|
615
|
+
const t0_k = performance.now();
|
|
616
|
+
resKara = run_converged(
|
|
617
|
+
codes.find,
|
|
618
|
+
relDefKara.def,
|
|
619
|
+
pairInput,
|
|
620
|
+
relDefKara.typePatternGraph
|
|
621
|
+
);
|
|
622
|
+
kTime = performance.now() - t0_k;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
const match = (!resTimes || !resKara)
|
|
626
|
+
? true
|
|
627
|
+
: JSON.stringify(resTimes) === JSON.stringify(resKara);
|
|
628
|
+
const speedup = (tTime != null && kTime != null)
|
|
629
|
+
? fmtSpeedup(tTime, kTime)
|
|
630
|
+
: "-";
|
|
631
|
+
const colBits = bits.toString().padStart(11);
|
|
632
|
+
const colBe = backend.padEnd(7);
|
|
633
|
+
const colT = fmtMs(tTime).padStart(11);
|
|
634
|
+
const colK = fmtMs(kTime).padStart(11);
|
|
635
|
+
const colSp = speedup.padStart(13);
|
|
636
|
+
const colMatch = match ? "MATCH OK" : "MISMATCH";
|
|
637
|
+
console.log(
|
|
638
|
+
`${colBits} | ${colBe} | ${colT} | ${colK} | ` +
|
|
639
|
+
`${colSp} | ${colMatch}`
|
|
640
|
+
);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
}
|
package/scripts/perf-poly.mjs
CHANGED
|
@@ -60,6 +60,73 @@ import { compileARM64ArtifactFromObject } from "../backends/arm64/src/arm64.mjs"
|
|
|
60
60
|
import { compileWasmArtifactFromObject } from "../backends/wasm/src/wasm.mjs";
|
|
61
61
|
import { inputPatternForObjectRelation } from "../backends/llvm/src/executable.mjs";
|
|
62
62
|
|
|
63
|
+
function printHelp() {
|
|
64
|
+
console.log(`Usage: node scripts/perf-poly.mjs [options]
|
|
65
|
+
|
|
66
|
+
Polymorphic List Operations Benchmark Harness.
|
|
67
|
+
Benchmarks reverse, concat, split_by, get_nth, and length on
|
|
68
|
+
Examples/poly.k across Native JS, kVM interpreter, LLVM, Wasm, and ARM64.
|
|
69
|
+
|
|
70
|
+
Options:
|
|
71
|
+
--trace
|
|
72
|
+
Enable 7-phase nanosecond execution timing.
|
|
73
|
+
--profile
|
|
74
|
+
Enable per-function call count profiling.
|
|
75
|
+
--wasm-in-process
|
|
76
|
+
Run WebAssembly in-process instead of persistent subprocess runner.
|
|
77
|
+
-h, --help
|
|
78
|
+
Show this help message and exit.
|
|
79
|
+
|
|
80
|
+
Environment Variables:
|
|
81
|
+
BACKENDS
|
|
82
|
+
Target backend to run: 'llvm', 'wasm', or 'arm64'.
|
|
83
|
+
LLVM_ONLY
|
|
84
|
+
Run only LLVM backend ('1' to enable).
|
|
85
|
+
WASM_ONLY
|
|
86
|
+
Run only WebAssembly backend ('1' to enable).
|
|
87
|
+
ARM64_ONLY
|
|
88
|
+
Run only ARM64 backend ('1' to enable).
|
|
89
|
+
BACKENDS_ONLY
|
|
90
|
+
Skip JS and kVM interpreter baselines ('1' to enable).
|
|
91
|
+
LIST_LENGTH
|
|
92
|
+
Workload size for list operations (default: 40).
|
|
93
|
+
ITERATIONS
|
|
94
|
+
Measured iterations per benchmark (default: 3).
|
|
95
|
+
TRACE, K_TRACE
|
|
96
|
+
Enable nanosecond execution tracing ('1' to enable).
|
|
97
|
+
PROFILE, K_PROFILE
|
|
98
|
+
Enable per-function call count profiling ('1' to enable).
|
|
99
|
+
ARM64_WARMUP_ITERATIONS
|
|
100
|
+
Warmup iterations for ARM64 backend (default: 1).
|
|
101
|
+
ARM64_OPT
|
|
102
|
+
ARM64 backend optimization flags (default: -O2).
|
|
103
|
+
ARM64_SPAWN_PER_CALL
|
|
104
|
+
Spawn process per ARM64 call instead of persistent ('1' or '0').
|
|
105
|
+
ARM64_PIPELINE
|
|
106
|
+
Use persistent pipelined runner for ARM64 ('1' or '0').
|
|
107
|
+
LLVM_WARMUP_ITERATIONS
|
|
108
|
+
Warmup iterations for LLVM backend (default: 1).
|
|
109
|
+
LLVM_SPAWN_PER_CALL
|
|
110
|
+
Spawn process per LLVM call instead of persistent ('1' or '0').
|
|
111
|
+
LLVM_PIPELINE
|
|
112
|
+
Use persistent pipelined runner for LLVM ('1' or '0').
|
|
113
|
+
K_LLVM_CACHE_DIR
|
|
114
|
+
Directory to cache compiled LLVM executable binaries.
|
|
115
|
+
WASM_WARMUP_ITERATIONS
|
|
116
|
+
Warmup iterations for WebAssembly backend (default: 3).
|
|
117
|
+
WASM_RESET
|
|
118
|
+
Reset WebAssembly memory between iterations (default: 1).
|
|
119
|
+
WASM_PIPE
|
|
120
|
+
Use persistent subprocess for Wasm (default: 1, '0': in-process).
|
|
121
|
+
WASM_IN_PROCESS
|
|
122
|
+
Run WebAssembly in-process ('1' to enable).`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (process.argv.includes("-h") || process.argv.includes("--help")) {
|
|
126
|
+
printHelp();
|
|
127
|
+
process.exit(0);
|
|
128
|
+
}
|
|
129
|
+
|
|
63
130
|
// Backend selection
|
|
64
131
|
const backendsEnv = process.env.BACKENDS?.toLowerCase();
|
|
65
132
|
const llvmOnly = process.env.LLVM_ONLY === "1" || backendsEnv === "llvm";
|