3ebcb3f758
Adds a `throughput` benchmark to every target and closes two small test-coverage documentation/QA gaps. One PR, no merge of binding code beyond the additive benchmarks and one C test. ## 1. Per-binding throughput benchmarks (all 9 targets) Each benchmark feeds a deterministic synthetic OHLCV series through three indicators chosen by **FFI call-signature archetype** (not algorithm — the same Rust core runs underneath all bindings): - `SMA(20)` — 1-in → 1-out (baseline boundary cost) - `ATR(14)` — multi-in → 1-out (input marshalling) - `MACD(12,26,9)` — 1-in → multi-out (output marshalling) Streaming is timed for all three; batch for the single-output SMA and ATR (median of 3 runs, after a warmup pass). New: Python (PyO3), WASM, C (CMake), C# (Stopwatch), Go, Java (FFM), R, and the Rust core baseline (`examples/rust/.../throughput.rs`, **no FFI** — the ceiling the bindings are measured against and the value their batch paths converge towards). Node already had `throughput.js`. **Not a speed claim:** there is no comparable streaming TA library for C, C#, Go, Java, R or WASM to compare against, so these are raw per-binding throughput numbers documenting each language's FFI overhead — see BENCHMARKS.md §3. The "Wickra is fast" claim still lives in §1/§2 (Rust core + the Python/Rust cross-library runs). ## 2. README `## Testing`: C# and C bullets The section listed every layer except C# and C, even though both have suites. Adds the two missing bullets. ## 3. C archetype ctest `examples/c/archetypes.c` drives one indicator per FFI archetype through the real C boundary (scalar + batch==streaming, multi-output, bars, profile, array input) plus reset, invalid-parameter and NULL-safety — the C counterpart of the Go/R/Java archetype suites. Runs on three OSes via the existing CMake/ctest. ## Notes - Benchmarks are not CI-gated (manual-run scripts, like the existing `throughput.js`); no `ci.yml`/`release.yml` changes. - Docs: BENCHMARKS.md §3, a `## Benchmark` section in every binding README, a CHANGELOG entry. - Verified locally by running: Rust, Python, C, C#, Go, Java (real numbers); the C archetype ctest with `-Wall -Wextra -Wpedantic -Werror`. WASM and R are API-correct and syntax-checked but need their own toolchains to run.
102 lines
4.0 KiB
C#
102 lines
4.0 KiB
C#
// Throughput benchmark for the Wickra C# binding.
|
|
//
|
|
// Measures how many indicator updates per second the binding sustains, both
|
|
// per-tick (streaming Update) and bulk (Batch), over a synthetic OHLCV series.
|
|
// It is the C# counterpart of the Node throughput.js and the Rust criterion
|
|
// benches: it benchmarks Wickra's own O(1) streaming engine across the
|
|
// managed<->C-ABI boundary (there is no comparable streaming TA library on
|
|
// NuGet to compare against), so the headline number is raw per-binding
|
|
// throughput / FFI overhead, not a cross-library ratio.
|
|
//
|
|
// Three indicators are timed, chosen by FFI call-signature archetype rather
|
|
// than algorithm: SMA (1-in -> 1-out), ATR (multi-in -> 1-out), and MACD
|
|
// (1-in -> multi-out). Streaming is timed for all three; batch only for the
|
|
// single-output SMA and ATR (multi-output batch is not exposed uniformly).
|
|
//
|
|
// cargo build -p wickra-c --release
|
|
// dotnet run -c Release --project bindings/csharp/benchmarks # 200k bars
|
|
// dotnet run -c Release --project bindings/csharp/benchmarks -- --bars 1000000
|
|
|
|
using System.Diagnostics;
|
|
using System.Globalization;
|
|
using Wickra;
|
|
|
|
// Deterministic, locale-independent number formatting for the report.
|
|
CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
|
|
|
|
int bars = 200_000;
|
|
for (int i = 0; i < args.Length - 1; i++)
|
|
{
|
|
if (args[i] == "--bars" && int.TryParse(args[i + 1], out var n) && n >= 1000)
|
|
{
|
|
bars = n;
|
|
}
|
|
}
|
|
|
|
// Deterministic synthetic OHLCV (no RNG, so runs are comparable).
|
|
var open = new double[bars];
|
|
var high = new double[bars];
|
|
var low = new double[bars];
|
|
var close = new double[bars];
|
|
var volume = new double[bars];
|
|
var timestamp = new long[bars];
|
|
for (int i = 0; i < bars; i++)
|
|
{
|
|
double mid = 100 + Math.Sin(i * 0.001) * 20 + i * 1e-4;
|
|
double c = mid + Math.Sin(i * 0.05) * 2;
|
|
close[i] = c;
|
|
open[i] = mid;
|
|
high[i] = Math.Max(c, mid) + 1.5;
|
|
low[i] = Math.Min(c, mid) - 1.5;
|
|
volume[i] = 1000 + (i % 97) * 13;
|
|
timestamp[i] = i;
|
|
}
|
|
|
|
double Mups(double ns) => bars / (ns / 1e9) / 1e6;
|
|
|
|
// Median elapsed-ns over a few repetitions, after one warmup pass.
|
|
double TimeNs(Action fn, int reps = 3)
|
|
{
|
|
fn(); // warmup (JIT + cache)
|
|
var samples = new double[reps];
|
|
for (int r = 0; r < reps; r++)
|
|
{
|
|
long t0 = Stopwatch.GetTimestamp();
|
|
fn();
|
|
samples[r] = (Stopwatch.GetTimestamp() - t0) * (1e9 / Stopwatch.Frequency);
|
|
}
|
|
Array.Sort(samples);
|
|
return samples[reps / 2];
|
|
}
|
|
|
|
// SMA (scalar 1-in/1-out), ATR (multi-in/1-out), MACD (1-in/multi-out).
|
|
var indicators = new (string Name, Action Stream, Action? Batch)[]
|
|
{
|
|
("SMA(20)",
|
|
() => { using var ind = new Sma(20); for (int i = 0; i < bars; i++) ind.Update(close[i]); },
|
|
() => { using var ind = new Sma(20); ind.Batch(close); }),
|
|
("ATR(14)",
|
|
() => { using var ind = new Atr(14); for (int i = 0; i < bars; i++) ind.Update(open[i], high[i], low[i], close[i], volume[i], timestamp[i]); },
|
|
() => { using var ind = new Atr(14); ind.Batch(open, high, low, close, volume, timestamp); }),
|
|
("MACD(12,26,9)",
|
|
() => { using var ind = new MacdIndicator(12, 26, 9); for (int i = 0; i < bars; i++) ind.Update(close[i]); },
|
|
null), // multi-output: streaming only
|
|
};
|
|
|
|
Console.WriteLine($"Wickra C# throughput - {bars:N0} bars (median of 3 runs)\n");
|
|
Console.WriteLine($"{"Indicator",-22}{"streaming (Mupd/s)",20}{"batch (Mupd/s)",18}");
|
|
Console.WriteLine(new string('-', 60));
|
|
|
|
foreach (var (name, stream, batch) in indicators)
|
|
{
|
|
string streamMups = Mups(TimeNs(stream)).ToString("F1");
|
|
string batchMups = batch is null ? "-" : Mups(TimeNs(batch)).ToString("F1");
|
|
Console.WriteLine($"{name,-22}{streamMups,20}{batchMups,18}");
|
|
}
|
|
|
|
Console.WriteLine(
|
|
"\nMupd/s = million indicator updates per second. Streaming is the per-tick\n" +
|
|
"Update path crossing the managed<->C-ABI boundary once per value; batch is\n" +
|
|
"the bulk array path (one boundary crossing). Higher is better. Numbers are\n" +
|
|
"machine-dependent - use them for relative comparison, not as a speed claim.");
|