Performance

Zena is designed to deliver predictable, near-native execution speed on WebAssembly GC.

This performance profile comes from two complementary factors: and hidden runtime checks simply by omitting those features from the language.

What each construct costs ​

Understanding how language features translate to WebAssembly instructions helps you make informed architectural choices.

Cost Category Language Construct WebAssembly Lowering Runtime Impact
Zero-Cost Primitive operations (+, *, ==) i32.add, f64.mul, i32.eq Raw hardware ALU speed; no boxing or tagging.
Zero-Cost Inline tuples (inline (a, b)) (result T_A T_B) Kept on the execution stack/registers; zero GC allocations.
Zero-Cost Extension class methods Direct (func $name ...) Erased at compile time; receiver passed as first argument.
Zero-Cost final methods and direct calls Direct call $func Direct call target; eligible for host engine inlining.
Zero-Cost Top-level function declarations Plain (func ...) Zero closure environment allocation.
Low-Cost Class field access (obj.field) struct.get $Class $offset Single-instruction direct offset load.
Low-Cost Class construction (new Point(...)) struct.new $Point Single-instruction allocation with pre-evaluated fields.
Low-Cost Virtual method calls struct.get + call_ref Single load from static vtable followed by indirect call.
Moderate Interface method calls Fat Pointer ITable lookup Two-word indirection (load ITable from fat pointer, then call_ref).
Moderate Arrow functions / Closures struct.new $Closure Allocates a GC closure struct to capture local variables.
Moderate Function arity adaptation Adapter trampoline Lightweight wrapper when passing a callback with fewer arguments.
Allocation Explicit boxing (new Box(42)) struct.new $Box Heap allocation to wrap primitive in a reference type.
Allocation GrowableArray<T> resizing array.copy + array.new Reallocates backing buffer when exceeding capacity.

Monomorphized generics ​

Zena uses generic monomorphization (specialization) rather than runtime generic erasure to Object or anyref.

When you write a generic class or function:

zena
class Cell<T>(value: T)

let intCell = new Cell<i32>(42);
let floatCell = new Cell<f64>(3.14);

The compiler synthesizes distinct, specialized WebAssembly types and functions for each unique concrete type argument:

wasm
;; Specialized struct for Cell<i32>
(type $Cell_i32 (struct (field $value i32)))

;; Specialized struct for Cell<f64>
(type $Cell_f64 (struct (field $value f64)))

Benefits of monomorphization ​

  • Unboxed primitive collections: An Array<i32> is backed by a native Wasm (array i32). Reading or writing elements requires no boxing or unboxing conversions.
  • Direct struct layout: Generic fields containing primitives occupy exact scalar bitwidths (e.g., 4 bytes for i32, 8 bytes for f64) without pointer indirection.
  • Specialized operations: Arithmetic inside generic functions compiles to native hardware instructions rather than virtual method dispatch.

Managing specialization overhead ​

Monomorphizing generics creates a separate copy of code and type declarations for each concrete type instantiation. To keep binaries compact:

  • The compiler's reachability analysis only emits specializations that are actually instantiated in the program.
  • Shared logic that does not depend on type parameters can be factored into non-generic base classes or helper functions.

Devirtualization and inlining ​

Virtual method dispatch in object-oriented programming introduces a level of indirection by loading a function reference from a vtable before calling it.

Zena applies compile-time devirtualization to eliminate this indirection wherever possible, replacing virtual dispatches with direct WebAssembly call $func instructions.

Explicit devirtualization with final ​

Declaring a class or method as final informs the compiler that it cannot be overridden:

zena
final class FastCalculator {
  compute(x: i32): i32 {
    return x * 2 + 1;
  }
}

Calls to FastCalculator.compute bypass vtable lookups completely. Direct calls are prime candidates for host JIT compilers (such as V8 or Wasmtime) to inline the method body entirely.

Whole-program devirtualization ​

Even when a class is not explicitly marked final, Zena's compiler performs whole-program reachability analysis during compilation. If the compiler proves that a class has no derived subclasses instantiated anywhere in the application, it automatically devirtualizes calls on that class.

Boxing and how to avoid it ​

In many languages, boxing occurs implicitly whenever primitive values are passed to generic APIs, collections, or interfaces.

In Zena, primitives are never implicitly boxed.

Soundness without a universal any ​

Zena has no universal any or Object type that can transparently hold both primitives and references. anyref is the top type for references only.

If code requires putting a primitive value behind a reference type, it must be explicitly wrapped using Box<T>:

zena
class Box<T> {
  value: T;
  new(this.value);
}

let boxed: anyref = new Box<i32>(42);

Because boxing requires an explicit new Box(...), allocations are never hidden inside language syntax.

Best practices for avoiding allocations ​

  1. Use monomorphic collections: Use Array<i32> or FixedArray<f64> instead of generic wrapper types.
  2. Use inline tuples for multi-value returns: Return multiple values using inline (A, B) to leverage unboxed WebAssembly multi-value returns:
    zena
    function divide(a: i32, b: i32): inline (i32, i32) {
      return (a / b, a % b); // Stored in registers; zero heap allocations
    }
    
  3. Prefer value parameters in extension classes: Extension classes on primitives (extension class IntOps on i32) are completely erased at compile time with zero object wrapper overhead.
  4. Use concrete array types in hot loops: Because Array<T> is an interface, accessing elements via Array<T> incurs Fat Pointer interface dispatch. Typing variables or parameters as concrete classes (FixedArray<T> or GrowableArray<T>) allows direct method dispatch and inlining in tight loops.

What ends up in the binary ​

Zena produces compact, standalone WebAssembly modules containing only the code required to execute the application.

A compiled .wasm file consists of:

  • Type section: Nominal declarations for every reached class struct, interface vtable, closure environment, and array type.
  • Function section: Compiled WebAssembly bytecode for all reached methods, functions, and synthesized initializers.
  • Global section: Static singletons, immutable vtables, and constant string references.
  • Element section: Function pointer tables used for indirect vtable calls.
  • Data section: Static string bytes and binary data literals.

Impact of debug metadata (-g) ​

By default, production builds strip all symbol names and debug metadata to minimize binary size.

When compiling with the -g flag, the compiler attaches a WebAssembly name section containing original function, class, and method names. While invaluable for debugging and stack traces, the name section adds metadata bytes and should be omitted in size-critical production deployments.

Dead code elimination ​

Zena utilizes a whole-program Rapid Type Analysis (RTA) pass during compilation to eliminate unused code and types.

How reachability analysis works ​

  1. Rooting entry points: Compilation begins by marking the main entry point function (export function main(...)) or public library exports as live.
  2. Propagating references: As functions are analyzed, any classes constructed, methods called, or fields read are added to the reachable set.
  3. Pruning unused members: Methods in imported libraries or unused classes that are never instantiated or invoked are completely stripped from the final WebAssembly binary.
  4. Pruning type declarations: Any Wasm GC struct or array types that are never constructed or referenced are omitted from the module's type section.

This whole-program analysis allows developers to import comprehensive standard library modules without incurring code size penalties for features they do not use.

Measuring and benchmarking ​

To measure and optimize Zena applications, use the built-in benchmarking tools and inspection utilities.

Running benchmarks ​

The Zena repository benchmarks workload pairs under benchmarks/workloads/: each pair sets an abstract form of a computation (an interface-typed parameter, map over a closure, the iterator protocol) against the same computation written by hand, built at -O2 and timed with statistical sampling, alongside AssemblyScript and Rust builds of the same workloads.

bash
npm run bench:speed

To build and time one workload:

bash
npm run bench -- --build --speed iface-param

Inspecting WebAssembly bytecode ​

To inspect the generated WebAssembly bytecode and verify compiler optimizations:

bash
# Compile to WebAssembly Text format (.wat)
npm run zena -w @zena-lang/zena-cli -- build main.zena -o main.wat

# Inspect section sizes using wasm-tools
wasm-tools objdump main.wasm

Next ​