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
| Benchmark | Wyn | Python 3.14 | Go 1.26 | Rust | C |
|---|---|---|---|---|---|
| fib(35) | 41ms | ~958ms | 48ms | 46ms | - |
| sort 10K ints | 9ms | ~43ms* | - | - | - |
| sort 100K ints | 18ms | ~86ms* | 18ms* | - | - |
| sort 1M ints | 124ms | ~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
| Benchmark | Wyn | Python 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
| Program | Size |
|---|---|
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
| Metric | Value |
|---|---|
wyn check (type check only) | 10ms |
wyn build (hello world, dev) | 288ms |
wyn build --release | 1.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
| Benchmark | Value |
|---|---|
spawn + await, 10K sequential | 32ms (~2.4μs/task) |
spawn + await, 100K sequential | 208ms (~2.0μs/task) |
1M fire-and-forget spawn | 0.67s (Go goroutines: 0.25s) |
8 overlapping Time::sleep(100), awaited | 112ms (not 800ms) |
8 overlapping Time::sleep(100) via await_all | 113ms |
50 overlapping Time::sleep(200) via await_all | 214ms (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.shab 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.
| Scenario | Throughput |
|---|---|
| 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'sexamples/hello.wynand by this harness) or every request pays for a new connection and you get the ~6,600 row instead.
Memory
| Benchmark | Value |
|---|---|
| Hello world | 1.4MB peak RSS |
| 1M mixed string ops | 1.5MB peak RSS |
1M naive s = s + "x" concat | 6.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
- Wyn vs Go - language comparison with benchmarks
- Wyn vs Python - performance comparison
- Wyn vs Rust - compile time and runtime comparison
- Spawn & Await - concurrency performance details