Performance
AOT compilation to LLVM, zero-allocation hot paths, arena memory, and a single-threaded event loop put SuperJ in the same class as the fastest published frameworks — with a latency curve that stays flat because there's no collector to pause on.
HTTP
A web server on the built-in HTTP stack, compiled to a single native binary and load-tested over loopback. The server is pinned to one core, the load generator to another. It serves faster than wrk can issue requests.
| Load generator | Throughput | p50 | p99 |
|---|---|---|---|
| wrk (1 process) | 406,762 req/s | 156µs | 167µs |
| SuperJ WebClient (pipelined ×16) | 515,000 req/s | — | — |
| wrk ceiling (same box) | ~423,000 req/s | wrk is the limit, not the server | |
Ryzen 9 9950X3D · server on CPU 4, client on CPU 3 · loopback TCP · 100 keep-alive connections
class HelloHandler implements HttpRequestHandler {
private final ByteArray response;
HelloHandler() {
String body = "Hello from SuperJ!\n";
this.response = new ByteArray(
"HTTP/1.1 200 OK\r\nContent-Length: "
+ body.length() + "\r\n\r\n" + body);
}
public void handle(HttpRequest r, SessionWriter w) {
this.response.rewind(); // reuse — zero alloc
w.write(this.response);
}
}
Compiled through LLVM to native code. Full speed from request one — no profiling, no deoptimization.
Arena memory is reclaimed deterministically. That's why the latency curve stays flat under load.
The response buffer is built once and rewound per request. Steady-state serving allocates nothing.
One event loop, one thread — no locks, no context switches, no cross-core cache traffic on the hot path.
superj_web
The same stack, wrapped in a full edge web server with TLS termination, routing, a static file cache, and reverse proxy. Despite the added overhead it beats nginx and Caddy on the same box, on every file size, for both static serving and reverse proxy.
| Workload | superj_web | nginx | Caddy | SDK reference |
|---|---|---|---|---|
| Small file (59 bytes) | 405,229 req/s | 218,948 req/s | 70,248 req/s (128B) | 407,094 req/s |
| 1 KB | 286,710 req/s | — | 59,974 req/s | — |
| 16 KB | 254,602 req/s | — | 59,479 req/s | — |
| 64 KB | 146,959 req/s | — | 56,634 req/s | — |
| Large file (10MB / 1MB) | 1,510 req/s (14.75 GB/s) / 15,229 req/s | 1,066 req/s (10.41 GB/s) | 10,971 req/s (1MB) | — |
Ryzen 9 9950X3D · server on one core, wrk on another · loopback TCP · 3-run average. Caddy v2.8.4, superj_web built with --release --enterprise. nginx: 1.24.0, single worker, sendfile on, access_log off.
| File | Caddy req/s | superj_web req/s | Speedup |
|---|---|---|---|
| 128B | 34,677 | 106,817 | 3.1× faster |
| 1 KB | 33,008 | 106,622 | 3.2× faster |
| 16 KB | 24,456 | 88,605 | 3.6× faster |
| 64 KB | 20,022 | 43,242 | 2.2× faster |
100 keep-alive conns · front on CPU 14, back on CPU 15 · 128 pooled keep-alive conns per upstream · raw response forwarding (no rebuild).
Why it wins. The static hot path is allocation-free: pre-built responses, rewind() reuse, an in-memory file cache with lazy mtime invalidation. The proxy hot path adds connection pooling (128 keep-alive conns per upstream) and raw response forwarding with prepareForReuse() on the reader — no per-request allocations on either path. The single-threaded event loop handles 288K req/s (static 128B) and 107K req/s (proxy 128B) on one core; the gaps to nginx/Caddy are algorithmic, not threading.
SEDA
The SEDA sequencer is the total-order event stream that replaces shared mutable state. In-process on an M4 Max it measured 88 ns/round — faster than raw cross-process mmap spin (152 ns/round), because it never crosses cores and pays no cache-coherence tax.
Application — xpe_db
xpe_db is an embedded document database with an HNSW vector index, an inverted index, a hash index, and a JWT/auth subsystem — ported from Java to SuperJ. The binary runs with no JVM, no GC, no JIT warm-up. These are application-level numbers, the kind a user actually feels.
Byte-quantized (uint8) L2 distance, M=16, maxM0=32, ef=200, k=10, dims=128, n=1000. Same seeded RNG, same workload. Precise 20K-query nanoTime bench.
| Metric | SuperJ | C++ hnswlib | Result |
|---|---|---|---|
| Search throughput | 24.8K q/s | 23.7K q/s | SuperJ ~5% faster |
| Insert throughput | ~25K v/s | 25.6K v/s | parity |
| Recall@1 | 100% | 100% | equal |
| Benchmark | Java | SuperJ | Speedup |
|---|---|---|---|
| InvertedIndexer.search(1024) TotalHits | 37 ns | 6 ns | 6.2× faster |
| InvertedIndexer.search(1) TotalHits | 35 ns | 3 ns | 11.7× faster |
| InvertedIndexer.build (100K records) | 755 ns | 118 ns | 6.4× faster |
| HashIndexer.search (100K lookups) | 22 ns | 17 ns | 1.3× faster |
SuperJ: enterprise SDK, -O3 (bounds check on, no SIMD). Java: JDK 17, cold JIT. macOS arm64.
| Operation | Java | SuperJ | Speedup |
|---|---|---|---|
| JWT.sign | 858 ns | 177 ns | 4.8× faster |
| JWT.verify | 801 ns | 181 ns | 4.4× faster |
| JWT.encryptAndSign | 1,238 ns | 364 ns | 3.4× faster |
| JWT.verifyAndDecrypt | 1,237 ns | 342 ns | 3.6× faster |
Case study — superK
superK is a time-series database built from the ground up in SuperJ — fully compatible with the q language and the q-IPC wire protocol: a q client that connects to kdb+ connects to superK without modification, and a q script that runs on kdb+ runs on superK. Same query language, same data model, same client protocol. No JVM, no JIT, no garbage collector; single-threaded by design, scaled with processes, not threads.
The benchmark. The paper "Benchmarking Specialized Databases for High-frequency Data" (arXiv:2301.12561) defines 14 query benchmarks over cryptocurrency exchange data — trades and order book — covering volume aggregation, VWAP, market depth, bid-ask spread, NBBO, log returns, and volatility. It was designed to compare kdb+ against ClickHouse, InfluxDB, and TimescaleDB. kdb+ won. We use the same 14 benchmarks, the same data shape, the same queries — both engines on the same machine, same core, same data, timed the same way. paper_verify.q proves the two databases agree value by value before any timing is quoted; both engines run the same q text through their own REPL, so the clock covers the full path a user hits (parse, evaluate, format, print), not just an isolated kernel.
| ID | Query | superK (µs) | kdb+ (µs) | Winner | Ratio |
|---|---|---|---|---|---|
| T-V1 | Trade volume, by symbol | 7,495 | 14,303 | superK | 1.9× |
| T-V2 | Trade volume, by symbol × exchange | 227,712 | 222,269 | kdb+ | 1.02× |
| T-VWAP | Volume-weighted average price | 2,089 | 1,794 | superK | 1.2× |
| O-T | Order-book top-of-book | 4.8 | 402 | superK | 84× |
| O-B1 | Best bid | 1,111 | 2,556 | superK | 2.3× |
| O-B2 | Best bid & ask | 5,256 | 14,208 | superK | 2.7× |
| O-S | Spread | 709 | 827 | superK | 1.2× |
| O-V1 | Order-book volume, by symbol | 40,294 | 61,012 | superK | 1.5× |
| O-V2 | Order-book volume, by symbol × exchange | 164,769 | 513,127 | superK | 3.1× |
| O-NBBO | National best bid & offer | 420 | 859 | superK | 2.0× |
| C-R | Log returns | 5,571 | 5,774 | superK | 1.04× |
| C-VT | Volatility, trades | 2,868 | 3,048 | superK | 1.06× |
| C-VO1 | Volatility, order book (bid) | 5,522 | 5,735 | superK | 1.04× |
| C-VO2 | Volatility, order book (bid & ask) | 32,097 | 37,053 | superK | 1.2× |
AMD Ryzen 9 9950X3D · 30 GB RAM · NVMe · both engines taskset -c 2 (isolated core), warm cache · 10 iterations, mean · data: 30 partitions + 1 NBBO day, ~20M trades rows, ~33M order_book rows, 39 GB, 3 symbols / 2 sides / 5 exchanges · superK commit 2c84085, superj build --release (AVX2, no bounds check, -O3) · kdb+ 5.0
superK wins 13 of 14. The one remaining loss — T-V2 at 1.02× — is within measurement noise; the two engines are effectively tied. The wins range from 1.04× (C-R) to 84× (O-T). The win comes from the storage and query engine itself, not a different data shape: superK implements the same splayed, parted, p#-indexed columnar model as kdb+, AOT-compiled through LLVM with zero allocation on the hot path.
The ledger
We ran 54 head-to-head benchmarks against C (and where relevant, C++ simdjson/hnswlib, Rust, Java) on the same machine, the same workload, the same iteration count. Each bar below splits at the speed ratio: amber is SuperJ, blue is C. A 50/50 split is a tie; if SuperJ is 2× faster, its segment is twice as long. We show every benchmark, including the ones we lose.
How the bars are calculated. Each benchmark pits SuperJ against C (or C++ simdjson / C++ hnswlib / Rust, where noted) on the same machine, the same workload, the same iteration count. SuperJ is compiled with clang -O3 linking; C with clang -O3 (g++ -O3 -mcpu=native for the C++ benchmarks). Best-of-3 warm runs. The split point of each bar is the speed ratio: if SuperJ is r× the time of C, SuperJ's segment is 1/(1+r) and C's is r/(1+r) — so a 2× win gives SuperJ a 2:1 segment, a tie is 50/50, and a loss shrinks SuperJ's segment. A win is >5% faster, a tie is within ±5%, a loss is >5% slower. No benchmark is cherry-picked — the 54 suites cover arithmetic, array scans, string ops, object alloc/access, method dispatch, networking, integer parsing, hash maps (Object keys, 3 key distributions), SHA-256, BitSet, JSON (4 document sizes), wide-integer arithmetic (i128/i256/i512), secp256k1 ECDSA, 8 Benchmarksgame ports, Base64, Brainfuck, SIMD matrix multiply, and a full HNSW vector index. The complete result table and methodology live in benchmarks/RESULTS.md and doc/hnsw_performance.md in the source tree.
Plain-object puts/gets at parity with C; up to 7× faster than tidwall C on adversarial keys; 4× faster on scattered int keys.
i256/i512 general division 3-10× faster than C; cache-blocked SIMD-tile GEMM beats auto-vectorized C by 1.15-2.57× at every matrix size.
3.4-11.7× faster on indexers and JWT — zero allocation on the hot path, native crypto intrinsics, no GC.
1.25-1.63× slower on allocation-bound paths — SuperJ allocates a real heap String; C writes a throwaway stack buffer. charAt is at parity.
1.10× slower — per-element pointer reload + bounds checks on a stride-3/4 pattern. Encode is now at parity after #2376. --no-bounds-check closes most of the remaining gap.
1.13× slower — the per-byte scalar tail on docs under 64B. SuperJ is faster than simdjson on 7B and 22B docs, and within 9% on 5.7KB.
BitSet scan, object field access, reverse-complement — the per-access checks C doesn't carry. --no-bounds-check closes most of it.
Reproduce
The HTTP server and load generator ship with SuperJ at demo/sj/demo/WebServer.sj and demo/sj/demo/WebClient.sj. The edge server benchmark script is in the superj_web repo.
# HTTP server + wrk
superj compile "$SJ_HOME/demo/sj/demo/WebServer.sj" --sdk-path "$SJ_HOME/sdk" --link --output webserver
./webserver 8080 &
wrk -t2 -c100 -d30s --latency http://127.0.0.1:8080/
# pipelined client (when wrk is the bottleneck)
superj compile "$SJ_HOME/demo/sj/demo/WebClient.sj" --sdk-path "$SJ_HOME/sdk" --link --output webclient
./webclient 8080 64 16 15
# scale across cores — same binary, more copies
for i in 1 2 3 4; do ./webserver 8080 & done