diff --git a/README.md b/README.md
index 0da85aa..a7f6e8e 100644
--- a/README.md
+++ b/README.md
@@ -17,6 +17,7 @@ $ ls -lh ant
- [Why Ant?](#why-ant)
- [Installation](#installation)
+- [Benchmarks](#benchmarks)
- [Spec conformance](#spec-conformance)
- [Building Ant](#building-ant)
- [Security](#security)
@@ -25,13 +26,13 @@ $ ls -lh ant
## Why Ant?
-| | Ant | Node | Bun | Deno |
-| ------------------- | ----------- | --------- | -------- | --------- |
-| Binary size | **~9 MB** | ~120 MB | ~60 MB | ~90 MB |
-| Cold start | **~3-5 ms** | ~30-50 ms | ~5-10 ms | ~20-30 ms |
-| Engine | Ant Silver | V8 | JSC | V8 |
-| JIT | ✓ | ✓ | ✓ | ✓ |
-| WinterTC conformant | ✓ | partial | ✓ | ✓ |
+| | Ant | Node | Bun | Deno |
+| ------------------- | ---------- | ------- | ------ | ------ |
+| Binary size | **~9 MB** | ~120 MB | ~60 MB | ~90 MB |
+| Cold start | **~5 ms** | ~31 ms | ~13 ms | ~25 ms |
+| Engine | Ant Silver | V8 | JSC | V8 |
+| JIT | ✓ | ✓ | ✓ | ✓ |
+| WinterTC conformant | ✓ | partial | ✓ | ✓ |
Ant is designed for environments where size and startup time matter: serverless functions, edge computing, embedded systems, CLI tools, and anywhere you'd want JavaScript but can't afford a 50MB+ runtime.
@@ -55,6 +56,43 @@ Ant targets the [WinterTC Minimum Common API](https://min-common-api.proposal.wi
| js-zoo (overall) | **88%** | 1211/1368 passing |
| test262 | ~50% | Improving, focus is on real-world coverage |
+## Benchmarks
+
+### Cold start
+
+Measures the time to import [Hono](https://hono.dev), register routes, and exit. Each runtime loads he same `bench-coldstart.js` script from `examples/npm/hono/` that creates a Hono app with two routes, prints "ready", and calls `process.exit(0)`. No HTTP server is actually started, this isolates module resolution and initialization overhead.
+
+Measured with hyperfine (10 warmup runs, 100 timed runs):
+
+```bash
+hyperfine --warmup 10 --runs 100 \
+ 'ant examples/npm/hono/bench-coldstart.js' \
+ 'node examples/npm/hono/bench-coldstart.js' \
+ 'bun examples/npm/hono/bench-coldstart.js' \
+ 'deno run --allow-read --allow-env examples/npm/hono/bench-coldstart.js'
+```
+
+| Runtime | Mean | Min | Max | Relative |
+| ------- | ---------- | ------- | -------- | ------------ |
+| **Ant** | **5.7 ms** | 5.0 ms | 7.3 ms | **1.00** |
+| Bun | 12.8 ms | 11.6 ms | 16.4 ms | 2.24× slower |
+| Deno | 24.8 ms | 22.2 ms | 29.4 ms | 4.32× slower |
+| Node | 31.1 ms | 27.1 ms | 151.7 ms | 5.41× slower |
+
+
+Environment
+
+| Detail | Value |
+| -------- | --------------------------------- |
+| Hardware | Apple M4 Pro, 24 GB RAM, 14 cores |
+| OS | macOS 15.7.5 (arm64) |
+| Ant | 0.9.1 |
+| Node | 25.9.0 |
+| Bun | 1.3.13 |
+| Deno | 2.7.12 |
+
+
+
## Building Ant
See [BUILDING.md](BUILDING.md) for instructions on how to build Ant from source and a list of supported platforms.
diff --git a/examples/npm/hono/bench-coldstart.js b/examples/npm/hono/bench-coldstart.js
new file mode 100644
index 0000000..c9d890a
--- /dev/null
+++ b/examples/npm/hono/bench-coldstart.js
@@ -0,0 +1,10 @@
+import { Hono } from 'hono';
+
+const app = new Hono();
+
+app.get('/', c => c.text('hello hono!'));
+app.get('/json', c => c.json({ message: 'hello' }));
+
+// signal that startup is complete, then exit
+console.log('ready');
+process.exit(0);
diff --git a/include/arena.h b/include/arena.h
index 716d60e..13fe4d8 100644
--- a/include/arena.h
+++ b/include/arena.h
@@ -16,18 +16,6 @@
#include
#endif
-#define ANT_ARENA_MIN (32 * 1024)
-#define ANT_ARENA_MAX (64ULL * 1024 * 1024 * 1024)
-
-#define ANT_ARENA_THRESHOLD (256ULL * 1024 * 1024)
-#define ARENA_GROW_INCREMENT (8ULL * 1024 * 1024)
-#define ANT_CLOSURE_ARENA_MAX (2ULL * 1024 * 1024 * 1024)
-
-// preferred base for mmap on platforms where the kernel may hand out
-// addresses above the 47-bit NaN-boxing ceiling. We pick 0x100000000
-// inside the 47-bit range and above the typical text/data segments.
-#define ANT_MMAP_HINT ((void *)0x100000000ULL)
-
typedef struct {
uint8_t *base;
size_t committed;
@@ -39,6 +27,44 @@ typedef struct {
void *free_list;
} ant_fixed_arena_t;
+#define ANT_ARENA_MIN (32 * 1024)
+#define ANT_ARENA_MAX (64ULL * 1024 * 1024 * 1024)
+
+#define ANT_ARENA_THRESHOLD (256ULL * 1024 * 1024)
+#define ARENA_GROW_INCREMENT (8ULL * 1024 * 1024)
+#define ANT_CLOSURE_ARENA_MAX (2ULL * 1024 * 1024 * 1024)
+
+// the kernel can hand out mmap addresses above the 47-bit NaN-boxing ceiling.
+// ant_mmap_low() probes the low VA range with MAP_FIXED_NOREPLACE
+// before falling back to an unpinned mmap. only needed on Linux
+#ifdef __linux__s
+
+#ifndef MAP_FIXED_NOREPLACE
+#define MAP_FIXED_NOREPLACE 0x100000
+#endif
+
+#define ANT_MMAP_LOW_START 0x10000000000ULL
+#define ANT_MMAP_LOW_END 0x7F0000000000ULL
+#define ANT_MMAP_LOW_STEP 0x10000000000ULL
+
+static inline void *ant_mmap_low(size_t size, int prot, int extra_flags) {
+ int flags = MAP_PRIVATE | MAP_ANON | extra_flags;
+
+ for (
+ uintptr_t addr = ANT_MMAP_LOW_START;
+ addr + size <= ANT_MMAP_LOW_END;
+ addr += ANT_MMAP_LOW_STEP
+ ) {
+ void *p = mmap((void *)addr, size, prot, flags | MAP_FIXED_NOREPLACE, -1, 0);
+ if (p != MAP_FAILED) return p;
+ }
+
+ void *p = mmap(NULL, size, prot, flags, -1, 0);
+ return (p == MAP_FAILED) ? NULL : p;
+}
+
+#endif
+
#ifdef _WIN32
static inline void *ant_arena_reserve(size_t max_size) {
@@ -70,14 +96,13 @@ static inline int ant_arena_decommit(void *base, size_t old_size, size_t new_siz
#else
static inline void *ant_arena_reserve(size_t max_size) {
- void *p = mmap(ANT_MMAP_HINT, max_size, PROT_NONE, MAP_PRIVATE | MAP_ANON, -1, 0);
- if (p == MAP_FAILED) return NULL;
- if ((uintptr_t)p >> 47) {
- munmap(p, max_size);
- p = mmap(NULL, max_size, PROT_NONE, MAP_PRIVATE | MAP_ANON, -1, 0);
- if (p == MAP_FAILED) return NULL;
- }
- return mantissa_chk(p, "mmap");
+#ifdef __linux__
+ void *p = ant_mmap_low(max_size, PROT_NONE, 0);
+#else
+ void *p = mmap(NULL, max_size, PROT_NONE, MAP_PRIVATE | MAP_ANON, -1, 0);
+ if (p == MAP_FAILED) p = NULL;
+#endif
+ return p ? mantissa_chk(p, "mmap") : NULL;
}
static inline int ant_arena_commit(void *base, size_t old_size, size_t new_size) {
@@ -105,14 +130,13 @@ static inline void ant_arena_free(void *base, size_t reserved_size) {
}
static inline void *ant_os_alloc(size_t size) {
- void *p = mmap(ANT_MMAP_HINT, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0);
- if (p == MAP_FAILED) return NULL;
- if ((uintptr_t)p >> 47) {
- munmap(p, size);
- p = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0);
- if (p == MAP_FAILED) return NULL;
- }
- return mantissa_chk(p, "mmap");
+#ifdef __linux__
+ void *p = ant_mmap_low(size, PROT_READ | PROT_WRITE, 0);
+#else
+ void *p = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0);
+ if (p == MAP_FAILED) p = NULL;
+#endif
+ return p ? mantissa_chk(p, "mmap") : NULL;
}
static inline int ant_arena_decommit(void *base, size_t old_size, size_t new_size) {
diff --git a/meson.build b/meson.build
index 7900f18..7fe3bb2 100644
--- a/meson.build
+++ b/meson.build
@@ -94,7 +94,7 @@ if host_machine.system() == 'windows'
elif host_machine.system() == 'darwin'
ant_link_args += ['-Wl,-stack_size,0x2000000']
else
- ant_link_args += ['-Wl,-z,stacksize=134217728']
+ ant_link_args += ['-Wl,-z,stack-size=134217728']
endif
if get_option('linker_map')
ant_linkmap = meson.project_build_root() + '/ant.linkmap'