Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions SharpTS.Benchmarks/Baselines/EquivalentCSharp.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,4 +72,84 @@ public static class EquivalentCSharp
}
return count;
}

/// <summary>
/// JSON round-trip using Dictionary&lt;string, object?&gt; nodes - mirrors the
/// boxed object/array representation the SharpTS runtime produces.
/// </summary>
public static object? JsonRoundTrip(object? n)
{
double nVal = Convert.ToDouble(n);
int nInt = (int)nVal;
var items = new List<object?>(nInt);
for (int i = 0; i < nInt; i++)
{
items.Add(new Dictionary<string, object?>
{
{ "id", (double)i },
{ "name", "item-" + i },
{ "value", (double)(i * 3 - 1) },
});
}
var root = new Dictionary<string, object?> { { "items", items } };
string json = System.Text.Json.JsonSerializer.Serialize(root);
using var doc = System.Text.Json.JsonDocument.Parse(json);
double sum = 0;
foreach (var el in doc.RootElement.GetProperty("items").EnumerateArray())
{
sum += el.GetProperty("value").GetDouble();
}
return sum;
}

/// <summary>
/// Typed-array kernel using List&lt;object?&gt; with boxed doubles - the dynamic
/// representation tax SharpTS AVOIDS by compiling Float64Array to a real buffer.
/// </summary>
public static object? TypedArrayKernel(object? n)
{
double nVal = Convert.ToDouble(n);
int nInt = (int)nVal;
var a = new List<object?>(nInt);
for (int i = 0; i < nInt; i++)
{
a.Add(i * 1.5 + (i % 7));
}
double sum = 0;
for (int i = 1; i < nInt - 1; i++)
{
sum += Convert.ToDouble(a[i - 1]) - 2 * Convert.ToDouble(a[i]) + Convert.ToDouble(a[i + 1]);
}
return sum;
}

/// <summary>
/// binary-trees using Dictionary&lt;string, object?&gt; nodes with dynamic
/// property lookups - mirrors a boxed SharpTS object graph.
/// </summary>
public static object? BinaryTrees(object? depth)
{
double d = Convert.ToDouble(depth);
return (double)ItemCheckBoxed(BuildTreeBoxed((int)d));
}

private static Dictionary<string, object?> BuildTreeBoxed(int depth)
{
if (depth <= 0)
{
return new Dictionary<string, object?> { { "left", null }, { "right", null } };
}
return new Dictionary<string, object?>
{
{ "left", BuildTreeBoxed(depth - 1) },
{ "right", BuildTreeBoxed(depth - 1) },
};
}

private static int ItemCheckBoxed(Dictionary<string, object?>? node)
{
if (node is null) return 1;
return 1 + ItemCheckBoxed(node["left"] as Dictionary<string, object?>)
+ ItemCheckBoxed(node["right"] as Dictionary<string, object?>);
}
}
61 changes: 61 additions & 0 deletions SharpTS.Benchmarks/Baselines/IdiomaticCSharp.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,65 @@ public static int CountPrimes(int n)
}
return count;
}

/// <summary>
/// JSON round-trip - typed serialize, parse via JsonDocument, sum a field.
/// </summary>
public static int JsonRoundTrip(int n)
{
var items = new List<object>(n);
for (int i = 0; i < n; i++)
{
items.Add(new { id = i, name = "item-" + i, value = i * 3 - 1 });
}
string json = System.Text.Json.JsonSerializer.Serialize(new { items });
using var doc = System.Text.Json.JsonDocument.Parse(json);
int sum = 0;
foreach (var el in doc.RootElement.GetProperty("items").EnumerateArray())
{
sum += el.GetProperty("value").GetInt32();
}
return sum;
}

/// <summary>
/// Typed-array numeric kernel - native double[] fill + 3-point stencil sweep.
/// </summary>
public static double TypedArrayKernel(int n)
{
var a = new double[n];
for (int i = 0; i < n; i++)
{
a[i] = i * 1.5 + (i % 7);
}
double sum = 0;
for (int i = 1; i < n - 1; i++)
{
sum += a[i - 1] - 2 * a[i] + a[i + 1];
}
return sum;
}

/// <summary>
/// binary-trees (CLBG) - build a node tree to `depth`, then checksum it.
/// </summary>
public static int BinaryTrees(int depth) => ItemCheck(BuildTree(depth));

private sealed class TreeNode
{
public TreeNode? Left;
public TreeNode? Right;
}

private static TreeNode BuildTree(int depth)
{
if (depth <= 0) return new TreeNode();
return new TreeNode { Left = BuildTree(depth - 1), Right = BuildTree(depth - 1) };
}

private static int ItemCheck(TreeNode? node)
{
if (node is null) return 1;
return 1 + ItemCheck(node.Left) + ItemCheck(node.Right);
}
}
84 changes: 84 additions & 0 deletions SharpTS.Benchmarks/Benchmarks/StarterWorkloadBenchmarks.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Order;
using SharpTS.Benchmarks.Baselines;

namespace SharpTS.Benchmarks.Benchmarks;

// Starter-set workloads that broaden coverage beyond the original arithmetic
// kernels: a builtin-heavy JSON round-trip, a data-parallel typed-array kernel,
// and allocation/GC-heavy binary-trees. The TypeScript bodies live in
// benchmarks/scripts/lib/algorithms.ts (shared byte-identical with the
// cross-runtime shell harness) and are reached through a cached
// Func<double,double> delegate via ComputationalBenchmarkBase.LoadCompiled.
//
// As elsewhere: SharpTS-compiled vs idiomatic C# (native types — the ceiling)
// vs "equivalent" C# (object?/boxing — the dynamic-typing tax).

[MemoryDiagnoser]
[RankColumn]
[Orderer(SummaryOrderPolicy.FastestToSlowest)]
public class JsonRoundTripBenchmarks : ComputationalBenchmarkBase
{
private Func<double, double> _jsonRoundTrip = null!;

[Params(100, 1000, 10000)]
public int N { get; set; }

[GlobalSetup]
public void Setup() => _jsonRoundTrip = LoadCompiled("jsonRoundTrip");

[Benchmark]
public double SharpTS() => _jsonRoundTrip(N);

[Benchmark]
public int Idiomatic() => IdiomaticCSharp.JsonRoundTrip(N);

[Benchmark]
public object? Equivalent() => EquivalentCSharp.JsonRoundTrip((double)N);
}

[MemoryDiagnoser]
[RankColumn]
[Orderer(SummaryOrderPolicy.FastestToSlowest)]
public class TypedArrayBenchmarks : ComputationalBenchmarkBase
{
private Func<double, double> _typedArrayKernel = null!;

[Params(1000, 100000, 1000000)]
public int N { get; set; }

[GlobalSetup]
public void Setup() => _typedArrayKernel = LoadCompiled("typedArrayKernel");

[Benchmark]
public double SharpTS() => _typedArrayKernel(N);

[Benchmark]
public double Idiomatic() => IdiomaticCSharp.TypedArrayKernel(N);

[Benchmark]
public object? Equivalent() => EquivalentCSharp.TypedArrayKernel((double)N);
}

[MemoryDiagnoser]
[RankColumn]
[Orderer(SummaryOrderPolicy.FastestToSlowest)]
public class BinaryTreesBenchmarks : ComputationalBenchmarkBase
{
private Func<double, double> _binaryTrees = null!;

[Params(8, 12, 16)]
public int N { get; set; }

[GlobalSetup]
public void Setup() => _binaryTrees = LoadCompiled("binaryTrees");

[Benchmark]
public double SharpTS() => _binaryTrees(N);

[Benchmark]
public int Idiomatic() => IdiomaticCSharp.BinaryTrees(N);

[Benchmark]
public object? Equivalent() => EquivalentCSharp.BinaryTrees((double)N);
}
9 changes: 9 additions & 0 deletions benchmarks/scripts/binary-trees.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { binaryTrees } from "./lib/algorithms.ts";
import { bench } from "./lib/bench.ts";

// Param is tree depth: node count is 2^(depth+1) - 1, so cost grows fast.
const params: number[] = [8, 12, 16];
for (let p: number = 0; p < params.length; p++) {
const n: number = params[p];
bench("binary-trees", n, () => binaryTrees(n));
}
78 changes: 78 additions & 0 deletions benchmarks/scripts/brainfuck.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { bench } from "./lib/bench.ts";

// A small Brainfuck interpreter — a "macro" workload dominated by interpreter
// dispatch: a tight loop over the program string (charCodeAt), precomputed
// bracket jumps, and tape reads/writes on a Uint8Array. The program is a
// pointer-neutral snippet concatenated `n` times, so larger n = more dispatch
// (linear) with a deterministic final tape across SharpTS, Node, and Bun.

// "+++++[>+<-]>[<+>-]<": adds 5 to cell0 (mod 256), pointer returns to cell0.
const SNIPPET: string = "+++++[>+<-]>[<+>-]<";

function buildProgram(reps: number): string {
let s: string = "";
for (let i: number = 0; i < reps; i++) {
s = s + SNIPPET;
}
return s;
}

// Precompute matching-bracket targets so '[' / ']' are O(1) in the hot loop.
function buildJumps(program: string): number[] {
const len: number = program.length;
const jumps: number[] = [];
for (let i: number = 0; i < len; i++) {
jumps.push(0);
}
const stack: number[] = [];
for (let i: number = 0; i < len; i++) {
const c: number = program.charCodeAt(i);
if (c === 91) { // '['
stack.push(i);
} else if (c === 93) { // ']'
const open: number = stack[stack.length - 1];
stack.pop();
jumps[open] = i;
jumps[i] = open;
}
}
return jumps;
}

function runBF(program: string, jumps: number[]): number {
const TAPE: number = 4096;
const tape: Uint8Array = new Uint8Array(TAPE);
let ptr: number = 0;
let ip: number = 0;
const len: number = program.length;
while (ip < len) {
const c: number = program.charCodeAt(ip);
if (c === 43) { // '+'
tape[ptr] = (tape[ptr] + 1) & 255;
} else if (c === 45) { // '-'
tape[ptr] = (tape[ptr] - 1) & 255;
} else if (c === 62) { // '>'
ptr = ptr + 1;
} else if (c === 60) { // '<'
ptr = ptr - 1;
} else if (c === 91) { // '['
if (tape[ptr] === 0) ip = jumps[ip];
} else if (c === 93) { // ']'
if (tape[ptr] !== 0) ip = jumps[ip];
}
ip = ip + 1;
}
let sum: number = 0;
for (let i: number = 0; i < TAPE; i++) {
sum = sum + tape[i];
}
return sum;
}

const params: number[] = [50, 500, 5000];
for (let p: number = 0; p < params.length; p++) {
const n: number = params[p];
const program: string = buildProgram(n);
const jumps: number[] = buildJumps(program);
bench("brainfuck", n, () => runBF(program, jumps));
}
8 changes: 8 additions & 0 deletions benchmarks/scripts/json.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { jsonRoundTrip } from "./lib/algorithms.ts";
import { bench } from "./lib/bench.ts";

const params: number[] = [100, 1000, 10000];
for (let p: number = 0; p < params.length; p++) {
const n: number = params[p];
bench("json", n, () => jsonRoundTrip(n));
}
60 changes: 60 additions & 0 deletions benchmarks/scripts/lib/algorithms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,63 @@ export function arrayMethodWork(n: number): number {
const evens = doubled.filter((x: number): boolean => x % 4 === 0);
return evens.reduce((acc: number, x: number): number => acc + x, 0);
}

// ── Builtin-heavy / allocation / data-parallel workloads ──────────────────
// These diverge most across SharpTS-compiled, V8 (Node), and JSC (Bun): JSON
// and typed-array kernels lean on hand-tuned engine builtins, binary-trees on
// the GC. Each still funnels to a `number` so the BDN harness can reflect it.

// JSON round-trip: build a record array, stringify, parse it back, sum a field.
// The single most common server hot path; V8/JSC JSON are hand-tuned C++.
export function jsonRoundTrip(n: number): number {
const items: { id: number; name: string; value: number }[] = [];
for (let i: number = 0; i < n; i++) {
items.push({ id: i, name: "item-" + i, value: i * 3 - 1 });
}
const json: string = JSON.stringify({ items: items });
const parsed = JSON.parse(json);
const back: { id: number; name: string; value: number }[] = parsed.items;
let sum: number = 0;
for (let i: number = 0; i < back.length; i++) {
sum = sum + back[i].value;
}
return sum;
}

// Typed-array numeric kernel: fill a Float64Array, then a 3-point stencil sweep.
// Data-parallel arithmetic over a real typed buffer — where compiled IL should
// approach native and the dynamic/boxed representation pays the most.
export function typedArrayKernel(n: number): number {
const a: Float64Array = new Float64Array(n);
for (let i: number = 0; i < n; i++) {
a[i] = i * 1.5 + (i % 7);
}
let sum: number = 0;
for (let i: number = 1; i < n - 1; i++) {
sum = sum + (a[i - 1] - 2 * a[i] + a[i + 1]);
}
return sum;
}

// binary-trees (Computer Language Benchmarks Game): build a `{ left, right }`
// object tree to `depth`, then checksum it. Allocates and discards heavily —
// exercises the GC and recursion rather than arithmetic.
type TreeNode = { left: TreeNode | null; right: TreeNode | null };

function buildTree(depth: number): TreeNode {
if (depth <= 0) {
return { left: null, right: null };
}
return { left: buildTree(depth - 1), right: buildTree(depth - 1) };
}

function itemCheck(node: TreeNode | null): number {
if (node === null) {
return 1;
}
return 1 + itemCheck(node.left) + itemCheck(node.right);
}

export function binaryTrees(depth: number): number {
return itemCheck(buildTree(depth));
}
Loading
Loading