How much overhead does a Java Foreign Linker API call have?

Viewed 574

I was curious how much overhead there was in calling functions via JEP 389: Foreign Linker API. I have searched around but I can't seem to find that kind of info. I was taking a look to see if it is realistic to wrap a c++ 3d math library that uses SIMD stuff with it. For this case the overhead must be very low since the operations themselves take little time.

By my estimations on my machine it takes 10 nanoseconds to call. This is certainly very impressive but that does not work out when the target only takes nanoseconds to run!

I don't have a ton of Java experience so I was wondering if I am missing out on some configuration things or just something obvious. I'm just trying to wrap something up for Clojure.

This is (roughly) how I got my numbers:

rtm.cpp

extern "C" {
    float inc_float(float f) {
        return f + 1.0f;
    }
}

Compiled with

clang -std=c++17 -O0 -shared -undefined dynamic_lookup -o librtm.so rtm.cpp

(also tested with -Ofast)

WrapperTest.java

import static jdk.incubator.foreign.CLinker.C_FLOAT;
import jdk.incubator.foreign.*;
import java.lang.invoke.*;
import java.nio.file.Path;

@Test public void testTonsOfSimpleCallsAlt() throws Throwable {
    var path = Path.of("/path/to/librtm.so");
    var libraryLookup = LibraryLookup.ofPath(path);
    var incFloatHandle = CLinker.getInstance().downcallHandle(
        libraryLookup.lookup("inc_float").get(),
        MethodType.methodType(float.class, float.class),
        FunctionDescriptor.of(C_FLOAT, C_FLOAT)
    );

    // warmup
    for (var i = 0; i < 1024; ++i) {
        float dummy = (float)incFloatHandle.invokeExact(1.0f);
    }

    long startTime = System.nanoTime();

    float total = 1.0f;
    int loops = 1024 * 1024 * 100;
    int ops = 10;
    for (var i = 0; i < loops; ++i) {
        // The actual important call!
        total = (float)incFloatHandle.invokeExact(total);
        // nine more of the above line...
        total = total - 10.0f;
    }

    long endTime = System.nanoTime();
    float nanos = (float)(endTime - startTime);
    System.out.println("Time taken is ms: " + (nanos / 1000000.0f));
    System.out.println("Time taken per op in ns:" + (nanos / (float)loops / (float)ops));
}

JVM arguments

--add-modules jdk.incubator.foreign -Dforeign.restricted=permit

Java version

> java --version
openjdk 16 2021-03-16
OpenJDK Runtime Environment AdoptOpenJDK (build 16+36)
OpenJDK 64-Bit Server VM AdoptOpenJDK (build 16+36, mixed mode, sharing)

Looking in a profiler I am getting these sorts of call stacks around the call to the c++ function:

- WrapperTest.java:131 java.lang.invoke.LambdaForm$MH.0x0000000800227000.invokeExact_MT(Object, float, Object) 991 1
  - LambdaForm$MH java.lang.invoke.LambdaForm$MH.0x0000000800229c00.invoke(Object, float) 991 1
    - LambdaForm$MH java.lang.invoke.LambdaForm$MH.0x000000080021d800.invoke(Object, float, long) 991 1

I have just run this via the IntelliJ test runner.

So is the overhead 10ns per call (machine dependent blah blah) or is there a way to get this running faster?

1 Answers

I was really curious about this as well, so I wrote a quick benchmark to see how it compares to JNA as well as shared memory.

The results and code are here :

https://github.com/TwoClocks/benchmark_java_foreign_functions

I got around 25ns on this particular machine, which to me seems pretty good. Calling a C++ virtual function in C++ is 5-10ns-ish? Pretty good calling native code from the JVM (at least I think so). JNA comes in around 700ns. JNI might be a bit faster, but still far slower than the new foreign function stuff.

Related