Skip to main content

Endive 1.2.0: SIMD in the Interpreter, GC-Friendly Native Memory, and Components on the Horizon

· 6 min read
Andrea Peruffo
Principal Software Engineer @ IBM

Endive 1.2.0 is out. Modules that use SIMD now run in the interpreter on any Java from 11 up, with nothing to add or configure. redline releases native memory once the Java objects using it become unreachable, so closing an instance is optional. Atomics behave the same across memory implementations and no longer collapse on JDK 25. And next door, Component Model support is taking shape.

SIMD in the Interpreter, from Java 11 Up​

All 236 instructions of the SIMD proposal are now part of the core interpreter: no extra module, no JVM flag, no machine factory to wire. The implementation is plain Java with no dependency on the incubating Vector API, so one jar behaves identically on every JDK, and it passes the WebAssembly SIMD specification tests.

We measured libsodium and OpenSSL, each built with and without SIMD: wherever the C compiler vectorised a kernel, the SIMD build runs faster in the interpreter, because there are fewer instructions to dispatch. The plain Java implementation also keeps pace with one built on the Vector API, which is why it is the only one we ship. The numbers are in the pull request.

The bytecode compilers and redline don't support SIMD yet: compiling a module that uses it fails, so such modules need the interpreter for now. Compiler support is next. SIMD execution relies on validation, so keep it enabled for modules that use v128.

Upgrading from run.endive:simd? Drop the dependency and the withMachineFactory(SimdInterpreterMachine::new) call.

redline: GC-Friendly Native Memory​

redline compiles your Wasm module to native code with Cranelift at build time and runs it through Panama FFM or jffi.

  • Native memory follows its Java objects. Code, linear memories, tables and globals live off-heap, and are released once the Java objects that use them become unreachable. close() is optional. The collector can't see off-heap bytes, so redline requests a collection as its native footprint grows. The documentation covers the threshold and how it interacts with your GC flags. In the measurement that accompanies the change, 200 instances of a Javy module that were never closed were all reclaimed, at the same peak memory as closing each one.
  • No compiler at runtime. The trampolines between Java and native code are generated at build time with the rest of the module. The runners only load and link precompiled code, and Cranelift is not one of their dependencies.
  • One watchdog. Thread.interrupt() reaches running native code through a single watchdog thread shared by every instance.
  • call_indirect across instances. A function placed in a table by one instance and called through call_indirect by another now runs with its own memory, globals and tables. This is how wit-component lays out every component with imports: a shim module calls the host through a table that a separate fixup module fills. Host reads and writes of a native memory also got cheaper, as the memory layouts are built once.
  • Correctness. Memory growth seen across instances and host imports, host-side atomics, wait/notify, and traps raised by the runner got a round of fixes in both runners, with new tests.

CPython compiled to Wasm is one of the modules we exercised it on.

Before you upgrade​

  • The native code format is now version 5. Recompile modules built with an earlier version.
  • close() stops an instance and frees nothing: the garbage collector reclaims the memory. -XX:+DisableExplicitGC turns off the collections redline requests.
  • redline-runner-jffi-experimental no longer brings in jffi, so its version can follow the rest of your application. Add com.github.jnr:jffi and its native classifier next to the runner.
  • Instances nested through host imports on one thread share one stack budget, and withMemoryLimits rejects limits that would change whether a memory is shared.
  • Like any Instance, a native instance is not thread-safe: use it from one thread at a time (#211).
  • Modules using exception handling: an exnref is now a WasmException object. apply() throws on exnref signatures, use applyWithRefs(); Instance.exn() and registerException() are deprecated and throw; and build-time compiled output from 1.1.0 must be regenerated.

Atomics and Heap​

  • JDK 25. ByteArrayMemory asked the JVM on every atomic load and store whether it supports volatile access on array views. On JDK 21 the answer is cheap; on JDK 25 it is false and expensive, enough to make the multi-threaded sieve benchmark run two orders of magnitude slower than on JDK 21. The answer is now resolved once. Wide atomics on ByteArrayMemory still take a lock on JDK 25, since its array views no longer support them lock-free.
  • Uniform atomics. In both memory implementations, atomics work on shared and non-shared memories alike, and overlapping accesses of different widths exclude each other. When hardware atomics aren't available, the fallback takes one of a fixed set of striped locks, with no allocation per operation.
  • Leaner compiled modules. After compiling, the parsed bodies of compiled functions are only useful to the interpreter; in a server holding many modules they were most of the retained heap. MachineFactoryCompiler.Builder.compileModule() returns a CompiledModule that keeps only the bodies the interpreter may still run, so the rest can be garbage collected. compile() is unchanged, and nothing is dropped when the reachable set can't be determined or the result comes from the cache.
var compiled = MachineFactoryCompiler.builder(module).compileModule();
var instance = Instance.builder(compiled).build();

Conformance​

Exception handling (exnref, throw_ref, catch_ref), tail calls, and the interpreter's v128 locals and store address computation all moved closer to the specification. The release notes list every change.

On the Horizon​

Component Model. Support is taking shape in endive-cm, a companion repository built on Endive: the component type model, a binary parser, Canonical ABI lifting and lowering, a bindgen annotation processor that turns WIT into Java, component instantiation and linking, and WASI 0.2 host implementations of wasi:io, wasi:clocks, wasi:cli, wasi:filesystem and wasi:random, with wasi:sockets in progress. It is tested against the official Component Model test suite and a corpus of components built with real toolchains.

Java 11. We intend to drop Java 11. Supporting it costs more with every release: Jackson 3 and other dependencies have moved on, and the build relies on forks and Maven profiles to keep up. We asked the projects that depend on Endive, and Trino, JRuby, Apache Camel, Microcks and Confidence all run Java 17 or later. If you are on Java 11, tell us on #250: concrete use cases will shape the transition.

Thanks​

Much of this release arrived as pull requests from the community, often from people running Endive in production, complete with reproducers, measurements and regression tests. Thank you to everyone who shipped code, filed an issue, tested a snapshot, or fixed a line in the docs. The adopters list is the place to add your project.

Documentation | GitHub | Join the conversation on Zulip