Skip to content
Try Nulang

Performance

import JitDemo from ‘../../../components/animations/JitDemo.astro’;

Nulang deliberately separates semantic correctness from execution optimizations:

  • Bytecode VM — the semantic reference implementation. Full-language behavior is judged against this path.
  • Cranelift JIT — an optimization layer for hot bytecode regions. A JIT speedup is valid only when it preserves bytecode semantics.
  • WASM — the canonical portable/cloud execution target. The backend is still experimental and does not yet implement every runtime surface.
  • Native AOT — a secondary backend for supported programs and differential/conformance testing. It must not be treated as full-language parity until the relevant semantics are proven.

This ordering matters when reading performance results: faster execution on a restricted backend is not evidence that the same speedup applies to arbitrary Nulang programs.

Nulang’s bytecode VM uses hot-counter tiering to compile frequently-executed code paths:

  • Threshold: After 1,000 invocations at a given PC, the VM triggers Cranelift compilation.
  • Region compilation: The compiler scans up to 500 instructions, stopping at unsupported opcodes or Ret.
  • Typed compilation: When register types are statically provable (via TypeMetadata), the typed compiler strips NaN-tag guards, emitting unboxed integer and float operations directly.
  • SIMD auto-vectorization: Element-wise loops on arrays are detected by the SIMD analyzer and compiled to I64x2, F64x2, I32x4, or F32x4 vector instructions with scalar prefix/epilogue.

Warm-up behavior: a region becomes eligible for compilation after its hot counter reaches 1,000 interpreted visits. Compilation and subsequent JIT execution depend on the visited program counters and whether the region is supported; 1,000 source-level loop iterations are not a universal compilation guarantee.

The --backend native path lowers supported MIR through Cranelift to native code:

  • Pipeline: Source → AST → HIR → MIR → Cranelift CLIF → native object code.
  • Typed lowering: Compile-time type metadata can remove tagged-value checks for supported integer and float operations.
  • No interpreter dispatch for compiled functions: supported code executes as native machine code rather than through the bytecode dispatch loop.

The WASM backend (--backend wasm, requires --features wasm-backend) compiles MIR to WebAssembly:

  • Compiler: wasm-encoder emits .wasm modules with i64-tagged values to avoid WASM NaN canonicalization.
  • Runtime: Wasmtime host runtime with 4 GiB guard pages, Cranelift speed optimizations (inlining enabled), and SIMD support.
  • AOT compilation: wasmtime compile produces .cwasm files for instant startup — no JIT warm-up on the client side.
  • SIMD lowering: MIR array operations lower to WASM SIMD instructions via raw byte emission.

The bytecode VM is designed for compact code and fast dispatch:

  • 32-bit instructions: {opcode: u8, op1: u8, op2: u8, op3: u8} — fixed-width, cache-friendly.
  • 256 registers per frame: Flat Vec<u64> with 48-bit payload + 16-bit type tag.
  • Opcode inventory: see the authoritative OpCode enum in src/bytecode.rs. Counts evolve as instructions are added; avoid treating a snapshot count as a format contract.
  • i64-tagged values: integers, floats, booleans, nil, and unit are all represented inline — no boxing, no heap allocation for primitives.
  • Per-actor heaps: A new actor configures a 16 KiB initial bump block, allocated lazily on first small-object allocation rather than on spawn. Additional blocks are chained as needed; live objects do not move.

  • ORCA GC: Reference counting with cycle detection, per-actor — one actor’s GC never pauses another.

  • Size-class free lists: Small allocations reuse exact-size slots; allocations over 256 bytes use a large-object space.

  • Global allocator: mimalloc for all non-actor allocations (compiler, runtime, JIT buffers).

  • String interning: Strings are interned once per module. Message passing shares interned pool handles within a node — no deep copies, though Arc-shared message payloads incur atomic reference-counting overhead.

  • Reference capabilities: iso and val references are sendable without deep copies; the type system guarantees no aliasing at compile time.

  • Cross-node: String content travels by value on the wire and is re-interned at the destination. Heap pointers, closures, and actor refs are rejected at send time.

The repository includes Criterion benchmarks that separate interpreter, warm-JIT, actor/runtime, cache, and supported AOT paths. Run the benchmark harness with:

Terminal window
cargo bench --bench bench_main

For any published number, record the commit SHA, CPU/OS, Rust toolchain, enabled Cargo features, workload, input size, and whether the measurement is cold interpreter, warm JIT, WASM, or native AOT. Do not compare a warmed or restricted backend against a cold/full-language path without labeling that difference.

Performance claims should also preserve semantic confidence: differential fuzzing and conformance tests are the guardrail for accepting backend optimizations.

The tables below compare execution architecture and language/runtime features, not universal benchmark winners. Actual throughput and latency depend on workload, warm-up, backend support, allocation behavior, and hardware.

Nulang Erlang/BEAM
Compilation Bytecode + tiered Cranelift JIT; secondary native AOT BEAM bytecode + JIT (OTP 24+)
GC Per-actor ORCA, no global pause Per-process, generational
Type system Static, HM-inferred Dynamic
Native code Restricted Cranelift AOT path JIT-generated native code; HiPE is deprecated
Nulang Rust
Distribution Built-in clustering, CRDTs Manual (gRPC, custom protocols)
Supervision OTP-style supervision trees Manual error handling
Performance toolchain Bytecode + Cranelift JIT/AOT rustc normally uses LLVM; alternative codegen backends also exist
Memory safety Capabilities (compile-time) Ownership + borrowing (compile-time)
Nulang Go
Execution Register VM + tiered JIT; secondary AOT Native code + goroutine scheduler + GC
Concurrency Actors with supervision Goroutines + channels
Messaging Actor messages; some immutable/interned data can be shared within a node, while cross-node payloads serialize Channels share/copy values according to Go value/reference semantics
Effect tracking Compile-time effect rows No effect system
Nulang Python
Execution Bytecode VM + tiered JIT; optional WASM/AOT paths CPython bytecode interpreter with version-dependent specialization/JIT work
Types Static, full HM inference Dynamic, optional type hints
Concurrency Actor model, no GIL GIL (CPython), asyncio