Skip to content

Benchmarks

Wyn compiles to C and runs at near-native speed, in small binaries. On an Apple M3 Pro: fib(35) in 41ms (23x faster than Python, ~15% faster than Go), a 50KB hello-world binary, 14ms for 1M StringBuilder appends, and ~288ms to compile hello world.

Method. Apple M3 Pro (12 core), macOS 26, Wyn 1.20.0. Wyn binaries built with wyn build --release; C with clang -O2; Go 1.26.5 with go build; Rust 1.96.0 with rustc -O; Python 3.14.6. Every figure is the median of at least 5 warm runs of the whole process, wall-clock. That means a ~7ms process-startup floor is included in every Wyn row - it is not subtracted, so short benchmarks look slower than the work they do. Discard the first run of any new binary on macOS: first-exec malware scanning adds 1-3s to any binary, Wyn's or C's.

Honest numbers, including the ones we lose. Run them yourself.

Compute

BenchmarkWynPython 3.14Go 1.26RustC
fib(35)41ms~958ms48ms46ms-
sort 10K ints9ms~43ms*---
sort 100K ints18ms~86ms*18ms*--
sort 1M ints124ms~591ms*101ms*--

* All four languages sort via optimized native code (Wyn uses C's qsort). Note the ~7ms startup floor dominates the 10K row - the sort itself is ~1-2ms.

Compute-heavy code runs at near-native speed because it is native code. CPU performance is largely the C compiler's (clang/gcc), not Wyn's. Wyn edges out Go and Rust on this particular recursive benchmark; on 1M-element sort, Go is ~20% ahead.

Strings

BenchmarkWynPython 3.14
1M appends (StringBuilder)14ms~85ms (list + join)
1M appends (s = s + "x")11.7s~10.2s
1M .len()9ms (O(1) cache)~136ms
100K method chain (.upper().trim())18ms~56ms

Wyn uses reference-counted strings with a length cache in the RC header, which is why .len() is O(1).

Strings are immutable, and naive concatenation is a real trap. s = s + "x" copies the whole string every time, so accumulating in a loop is O(n²) - the same trap as Java string concat. At 1M iterations that costs 11.7 seconds, and Wyn is slower than Python on this exact shape (~10.2s; CPython is quadratic here too, so being compiled buys nothing when the algorithm is quadratic). Use StringBuilder - the 14ms row above, ~800x faster - or build an array and .join() it.

Binary Size

ProgramSize
Hello world (--release)50KB (51,400 bytes)
Hello world (dev build)51KB (52,248 bytes)
REST API server (92 lines, web package)69KB (70,968 bytes)

For scale, on the same machine: C hello world is 33KB, Rust 421KB, Go 2.4MB.

Binaries include the Wyn runtime (networking, file I/O, concurrency, ARC). No external runtime needed. Dead-code stripping removes unused functions, which is why dev and release builds are within a kilobyte of each other for a trivial program.

Compilation

MetricValue
wyn check (type check only)10ms
wyn build (hello world, dev)288ms
wyn build --release1.17s
wyn run (hello world, already compiled)30ms

For scale: go build on hello world is ~96ms, rustc -O is ~181ms. Wyn's build is dominated by the C compiler invocation, not Wyn's own codegen.

Concurrency

BenchmarkValue
spawn + await, 10K sequential32ms (~2.4μs/task)
spawn + await, 100K sequential208ms (~2.0μs/task)
1M fire-and-forget spawn0.67s (Go goroutines: 0.25s)
8 overlapping Time::sleep(100), awaited112ms (not 800ms)
8 overlapping Time::sleep(100) via await_all113ms
50 overlapping Time::sleep(200) via await_all214ms (not 10s)
4x fib(35) in parallel { }42ms - same as one fib(35) (4x scaling)

Fire-and-forget spawn runs on the coroutine scheduler; awaited work overlaps cooperatively. The sleep rows are the headline result: overlapping I/O waits cost about as much as the single longest wait.

Memory: 10K outstanding spawns peak at ~3.2MB RSS; 1M at ~71MB. Go's 1M goroutines peak at ~7MB, so Go is far denser on massive fan-out.

Web

Reproduce every row below with the committed harness:

./benchmarks/http_load.sh

ab against a release build on loopback, plain-text response, one coroutine per connection. Each configuration gets a warmup run that is thrown away and then a measured run of 20,000 (keep-alive) or 10,000 (connection-per-request) requests.

ScenarioThroughput
HTTP/1.1 keep-alive, 100 concurrent~23,700 req/s, 0 failed
HTTP/1.1 keep-alive, 200 concurrent~22,900 req/s, 0 failed
HTTP/1.1 keep-alive, 10 concurrent~23,200 req/s, 0 failed
No keep-alive (connection per request), 200 concurrent~6,600 req/s, 0 failed
No keep-alive (connection per request), 10 concurrent~6,900 req/s, 0 failed

Keep-alive is worth ~3.5x here - without it you pay a TCP handshake per request.

Two things worth knowing before you compare these to anything:

  • Warmup matters more than concurrency. The scheduler's worker pool spins up lazily, so the same configuration measures ~15,000 req/s cold and ~23,000 warm. That is why the harness discards a warmup run; a benchmark without one is mostly measuring pool startup. Concurrency past ~10 barely moves the number.
  • Keep-alive needs a handler that loops. Persistent connections are a property of your handler, not just the runtime: a handler that reads one request and returns has nothing left to serve a second request with. Write it as while true { req = web.read_request(conn) ... } (the shape used by the web package's examples/hello.wyn and by this harness) or every request pays for a new connection and you get the ~6,600 row instead.

Memory

BenchmarkValue
Hello world1.4MB peak RSS
1M mixed string ops1.5MB peak RSS
1M naive s = s + "x" concat6.4MB peak RSS

For scale, hello world peak RSS on the same machine: C 1.4MB, Rust 1.6MB, Go 4.6MB, Python 15MB.

Wyn uses Automatic Reference Counting (ARC) with scope-based cleanup. Strings, arrays, and HashMaps are freed deterministically at block exit. The runtime is continuously tested under AddressSanitizer and ThreadSanitizer in CI.

Sharing mutable state across concurrent tasks requires Shared types; plain arrays and counters are not synchronized (like Go maps, which also require external locking under concurrency).

See Also

MIT License - v1.20.0