@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.
@@ -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
+ }
@@ -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";