Skip to content

Latest commit

 

History

History
112 lines (78 loc) · 3.74 KB

File metadata and controls

112 lines (78 loc) · 3.74 KB

Vector API + FFM: Bulk Data Processing (Why it complements native interop)

FFM is excellent at calling native code and moving data efficiently between Java and off-heap memory. In many real systems, the next bottleneck is what you do with the data once you have it:

  • converting raw buffers into domain-friendly Java types
  • scanning/aggregating large arrays (register blocks, sensor samples, signal vectors)
  • doing small math kernels repeatedly

This is where the Vector API (jdk.incubator.vector) can complement FFM: it lets you express SIMD-friendly bulk operations in Java so the JVM can use vector instructions (when available).

Status: the Vector API is an incubator module in JDK 25. It requires --add-modules jdk.incubator.vector at compile/run time.


1) Where this fits in this repo

mymodbus reads Modbus registers/coils via a native backend (libmodbus) and returns:

  • int[] for registers (unsigned 16-bit values promoted to int)
  • boolean[] for coils

The native call itself is only part of the cost. The post-processing step—copying and converting values—is often a hot path for large reads (e.g., 125 registers repeatedly).

Today, this repo uses simple scalar loops for conversions (clear + correct). The Vector API is a tool you can reach for after profiling if conversion becomes dominant.


2) Example: vectorized unsigned short → int conversion

A common pattern in native interop is “read uint16_t[] (or short[]) and expose int[] in Java”.

Scalar baseline:

static int[] toUnsignedIntScalar(short[] in) {
  int[] out = new int[in.length];
  for (int i = 0; i < in.length; i++) {
    out[i] = Short.toUnsignedInt(in[i]);
  }
  return out;
}

Vector API version (JDK 25), using ZERO_EXTEND_S2I and widening in two “parts”:

import jdk.incubator.vector.*;

static final VectorSpecies<Short> SS = ShortVector.SPECIES_PREFERRED;
static final VectorSpecies<Integer> IS = IntVector.SPECIES_PREFERRED;

static int[] toUnsignedIntVector(short[] in) {
  int[] out = new int[in.length];
  int shortLanes = SS.length();
  int intLanes = IS.length();

  int i = 0;
  int upper = SS.loopBound(in.length);
  for (; i < upper; i += shortLanes) {
    ShortVector sv = ShortVector.fromArray(SS, in, i);
    IntVector v0 = (IntVector) sv.convertShape(VectorOperators.ZERO_EXTEND_S2I, IS, 0);
    IntVector v1 = (IntVector) sv.convertShape(VectorOperators.ZERO_EXTEND_S2I, IS, 1);
    v0.intoArray(out, i);
    v1.intoArray(out, i + intLanes);
  }
  for (; i < in.length; i++) out[i] = Short.toUnsignedInt(in[i]);
  return out;
}

To compile/run code like this:

javac --add-modules jdk.incubator.vector YourFile.java
java  --add-modules jdk.incubator.vector YourMain

3) How this complements FFM (the “why”)

FFM helps you:

  • avoid JNI glue code and keep adapters in Java
  • allocate/copy deterministically with arenas
  • keep native lifetimes and errors isolated behind clean APIs

The Vector API helps you:

  • make the “copy/convert/process” step faster for large blocks
  • keep computation in Java (no need to bounce back to C for every small kernel)
  • express bulk transforms while staying within the JVM tooling ecosystem (JFR, profilers, tests)

In practice, you often combine them like:

  1. FFM/jextract to pull data from native into a flat buffer
  2. Copy into Java arrays (safety boundary)
  3. Vector API to transform/aggregate large blocks efficiently

4) Guardrails

  • Profile first. SIMD adds complexity; only worth it if it’s a proven hot path.
  • Keep fallbacks. A scalar loop is the reference implementation and is easier to reason about.
  • Be explicit about flags. Vector API requires --add-modules jdk.incubator.vector for compilation and runtime.