Skip to content

Commit eccb59a

Browse files
committed
add zig bench
1 parent 576926b commit eccb59a

File tree

7 files changed

+159
-24
lines changed

7 files changed

+159
-24
lines changed

samples/bench/bench.mjs

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { init as initDotNet } from "./dotnet/init.mjs";
33
import { init as initDotNetLLVM } from "./dotnet-llvm/init.mjs";
44
import { init as initGo } from "./go/init.mjs";
55
import { init as initRust } from "./rust/init.mjs";
6+
import { init as initZig } from "./zig/init.mjs";
67
import * as fixtures from "./fixtures.mjs";
78

89
/**
@@ -17,23 +18,28 @@ const baseline = new Map;
1718

1819
if (!lang || lang.toLowerCase() === "rust")
1920
await run("Rust", await initRust());
21+
if (!lang || lang.toLowerCase() === "zig")
22+
await run("Zig", await initZig());
2023
if (!lang || lang.toLowerCase() === "llvm")
2124
await run(".NET LLVM", await initDotNetLLVM());
22-
if (!lang || lang.toLowerCase() === "net")
23-
await run(".NET AOT", await initDotNet());
2425
if (!lang || lang.toLowerCase() === "boot")
2526
await run("Bootsharp", await initBootsharp());
27+
if (!lang || lang.toLowerCase() === "net")
28+
await run(".NET AOT", await initDotNet());
2629
if (!lang || lang.toLowerCase() === "go")
2730
await run("Go", await initGo());
2831

2932
/** @param {string} lang
3033
* @param {Exports} exports */
3134
async function run(lang, exports) {
3235
console.log(`\n\nBenching ${lang}...\n`);
36+
37+
global.gc();
3338
await new Promise(r => setTimeout(r, 100));
34-
bench("Echo number", exports.echoNumber, 100, 3, 1000, fixtures.getNumber());
35-
bench("Echo struct", exports.echoStruct, 100, 3, 100, fixtures.getStruct());
39+
3640
bench("Fibonacci", () => exports.fi(33), 100, 3, 1);
41+
bench("Echo number", exports.echoNumber, 100, 3, 100000, fixtures.getNumber());
42+
bench("Echo struct", exports.echoStruct, 100, 3, 1000, fixtures.getStruct());
3743
}
3844

3945
function bench(name, fn, iters, warms, loops, expected = undefined) {
@@ -53,19 +59,27 @@ function bench(name, fn, iters, warms, loops, expected = undefined) {
5359
for (let l = 0; l < loops; l++) fn();
5460
if (i >= 0) results.push(performance.now() - start);
5561
}
56-
let media = median(results);
57-
62+
const med = getMedian(results, 0.3);
63+
const dev = getDeviation(results);
5864
if (baseline.has(name)) {
59-
console.log(`${name}: ${(media / baseline.get(name)).toFixed(1)}`);
65+
console.log(`${name}: ${(med / baseline.get(name)).toFixed(1)} ${dev}`);
6066
} else {
61-
baseline.set(name, media);
62-
console.log(`${name}: ${media.toFixed(3)} ms`);
67+
baseline.set(name, med);
68+
console.log(`${name}: ${med.toFixed(3)} ms ${dev}`);
6369
}
6470
}
6571

66-
function median(numbers) {
72+
function getMedian(numbers, trim) {
6773
const sorted = [...numbers].sort((a, b) => a - b);
68-
const middle = Math.floor(sorted.length / 2);
69-
if (sorted.length % 2 === 1) return sorted[middle];
70-
return (sorted[middle - 1] + sorted[middle]) / 2;
74+
const trimAmount = Math.floor(sorted.length * trim);
75+
const trimmedArray = sorted.slice(trimAmount, sorted.length - trimAmount);
76+
return trimmedArray.reduce((sum, val) => sum + val, 0) / trimmedArray.length;
77+
}
78+
79+
function getDeviation(numbers) {
80+
const mean = numbers.reduce((sum, val) => sum + val, 0) / numbers.length;
81+
const sqr = numbers.map(value => Math.pow(value - mean, 2));
82+
const variance = sqr.reduce((sum, val) => sum + val, 0) / numbers.length;
83+
const dev = Math.sqrt(variance);
84+
return ${((dev / mean) * 100).toFixed(0)}%`;
7185
}

samples/bench/readme.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
## Setup
22

33
1. Build each sub-dir (readme inside)
4-
2. Run `npm bench.mjs` to bench all
5-
3. Or `npm bench.mjs rust|llvm|net|boot|go`
4+
2. Run `node --expose-gc bench.mjs` to bench all
5+
3. Add `rust|zig|llvm|net|boot|go` to bench specific
66

77
## Benches
88

@@ -14,8 +14,8 @@ All results are relative to the Rust baseline (lower is better).
1414

1515
## 2024 (.NET 9)
1616

17-
| | Rust | .NET LLVM | Bootsharp | .NET AOT | Go |
18-
|-------------|-------|-----------|-----------|-----------|---------|
19-
| Echo Number | `1.0` | `1.8` | `11.9` | `21.1` | `718.7` |
20-
| Echo Struct | `1.0` | `1.6` | `1.6` | `4.3` | `20.8` |
21-
| Fibonacci | `1.0` | `1.1` | `1.5` | `1.5` | `3.8` |
17+
| | Rust | Zig | .NET LLVM | Bootsharp | .NET AOT | Go |
18+
|-------------|-------|-------|-----------|-----------|----------|---------|
19+
| Fibonacci | `1.0` | `1.0` | `1.1` | `1.5` | `1.7` | `3.8` |
20+
| Echo Number | `1.0` | `0.9` | `1.6` | `14.0` | `23.5` | `718.7` |
21+
| Echo Struct | `1.0` | `1.1` | `2.0` | `2.5` | `5.9` | `15.2` |

samples/bench/rust/init.mjs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ import { getNumber, getStruct } from "../fixtures.mjs";
55
export async function init() {
66
global.getNumber = getNumber;
77
global.getStruct = () => JSON.stringify(getStruct());
8-
return {
9-
echoNumber,
10-
echoStruct: () => JSON.parse(echoStruct()),
11-
fi
8+
return {
9+
echoNumber,
10+
echoStruct: () => JSON.parse(echoStruct()),
11+
fi
1212
};
1313
}

samples/bench/zig/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
.zig-cache
2+
zit-out

samples/bench/zig/build.zig

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
const std = @import("std");
2+
3+
pub fn build(b: *std.Build) void {
4+
const lib = b.addExecutable(.{
5+
.name = "zig",
6+
.root_source_file = b.path("main.zig"),
7+
.target = b.resolveTargetQuery(.{
8+
.cpu_arch = .wasm32,
9+
.os_tag = .freestanding,
10+
.cpu_features_add = std.Target.wasm.featureSet(&.{
11+
.simd128,
12+
.relaxed_simd,
13+
.tail_call,
14+
}),
15+
}),
16+
.use_llvm = true,
17+
.use_lld = true,
18+
.optimize = b.standardOptimizeOption(.{}),
19+
});
20+
lib.entry = .disabled;
21+
lib.rdynamic = true;
22+
lib.want_lto = true;
23+
b.installArtifact(lib);
24+
}

samples/bench/zig/init.mjs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { getNumber, getStruct } from "../fixtures.mjs";
2+
import fs from "fs/promises";
3+
4+
/** @returns {Promise<import("../bench.mjs").Exports>} */
5+
export async function init() {
6+
const source = await fs.readFile("./zig/zig-out/bin/zig.wasm");
7+
const { instance: { exports } } = await WebAssembly.instantiate(source, {
8+
x: {
9+
getNumber,
10+
getStruct: () => encodeString(JSON.stringify(getStruct())),
11+
}
12+
});
13+
memory = exports.memory, cached = new Uint8Array(memory.buffer);
14+
15+
return {
16+
echoNumber: exports.echoNumber,
17+
echoStruct: () => JSON.parse(decodeString(exports.echoStruct())),
18+
fi: exports.fi
19+
};
20+
}
21+
22+
let memory, cached;
23+
const encoder = new TextEncoder("utf-8");
24+
const decoder = new TextDecoder("utf-8");
25+
const mask = BigInt("0xFFFFFFFF");
26+
27+
function encodeString(str) {
28+
const memory = getMemoryCached();
29+
const { written } = encoder.encodeInto(str, memory);
30+
return BigInt(written) << BigInt(32) | BigInt(0);
31+
}
32+
33+
function decodeString(ptrAndLen) {
34+
const memory = getMemoryCached();
35+
const ptr = Number(ptrAndLen & mask);
36+
const len = Number(ptrAndLen >> BigInt(32));
37+
const bytes = memory.subarray(ptr, ptr + len);
38+
return decoder.decode(bytes);
39+
}
40+
41+
function getMemoryCached() {
42+
if (cached.buffer === memory.buffer) return cached;
43+
return cached = new Uint8Array(memory.buffer);
44+
}

samples/bench/zig/main.zig

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
const std = @import("std");
2+
3+
var arena = std.heap.ArenaAllocator.init(std.heap.wasm_allocator);
4+
const ally = arena.allocator();
5+
6+
const opt = .{
7+
.parse = std.json.ParseOptions{
8+
.ignore_unknown_fields = true,
9+
},
10+
.stringify = std.json.StringifyOptions{
11+
.whitespace = .minified,
12+
},
13+
};
14+
15+
pub const Data = struct {
16+
info: []const u8,
17+
ok: bool,
18+
revision: i32,
19+
messages: []const []const u8,
20+
};
21+
22+
extern "x" fn getNumber() i32;
23+
extern "x" fn getStruct() u64;
24+
25+
export fn echoNumber() i32 {
26+
return getNumber();
27+
}
28+
29+
export fn echoStruct() u64 {
30+
_ = arena.reset(.retain_capacity);
31+
const input = decodeString(getStruct());
32+
const json = std.json.parseFromSlice(Data, ally, input, opt.parse) catch unreachable;
33+
var output = std.ArrayList(u8).init(ally);
34+
std.json.stringify(json.value, opt.stringify, output.writer()) catch unreachable;
35+
return encodeString(output.items);
36+
}
37+
38+
export fn fi(n: i32) i32 {
39+
if (n <= 1) return n;
40+
return fi(n - 1) + fi(n - 2);
41+
}
42+
43+
fn decodeString(ptr_and_len: u64) []const u8 {
44+
const ptr = @as(u32, @truncate(ptr_and_len));
45+
const len = @as(u32, @truncate(ptr_and_len >> 32));
46+
return @as([*]const u8, @ptrFromInt(ptr))[0..len];
47+
}
48+
49+
fn encodeString(str: []const u8) u64 {
50+
return (@as(u64, str.len) << 32) | @intFromPtr(str.ptr);
51+
}

0 commit comments

Comments
 (0)