Hacker News new | ask | show | jobs
by pron 671 days ago
That's a matter of perspective.

Calling a C function in a shared library (dll, so) from Java using the new FFM API has the same overhead as calling such a function from C++ (although the overhead is higher if the called function upcalls into Java again, though that is relatively rare, or if the function blocks, only that makes the additional overhead negligible). But the FFM API does not directly expose Java objects to native code at all, although it does allow Java code to access and mutate "off-heap" native memory (C data) from Java code as efficiently as accessing and mutating Java heap memory. So if your goal is to expose Java objects to native code, then yes, that would require marshalling (although ideally you should do the opposite and expose native memory to Java code as trhough a Java interface, which would have no overhead).

However, relying on FFI in Java is far less common than in Python, Rust, or even C# or Go, and in the rare cases it's done it's easy to do it cheaply as I described. So I guess it's true to say that if you wanted FFI to work in the same manner it is employed in those other languages then yes, it would be more expensive as it would require marshalling, but that's just not the case in Java given the combination of Java's performance and size of its ecosystem of libraries.

Languages with worse performance or with smaller ecosystems do need to rely much more heavily on FFI and so they often choose to sacrifice the flexibility of their implementation in favour of a more direct flavour of FFI.

1 comments

I agree with your general point, that it depends on your specific problem how difficult this is, but I disagree about how common or easy to work around.

Regarding

> But the FFM API does not directly expose Java objects to native code at all, although it does allow Java code to access and mutate "off-heap" native memory (C data) from Java code as efficiently as accessing and mutating Java heap memory

I just don’t buy it. First, I think it’s very common to want to expose managed memory to native. In fact, it might be the dominant case. If I want to call out to perform a crypto operation on a block of bytes I got from a Java operation, I don’t want to copy them first.

Second, I think you’re missing the use case for manipulating system APIs. If you want to perform some system call and the call requires setting up some structures as arguments, that’s going to be pretty expensive in Java. For things that are called a lot it can add up. For example, windows has a profiling and eventing system called ETW. To use it you create a set of events and call the system. It’s not uncommon to do this for thousands or millions of events per second. The way C# handles this is stack allocating an event blob and calling directly. I can’t imagine a Java workaround that would be as fast or simple. It seems like you’d have to pool a native event blob allocation and fill it in from Java.

It’s true that most Java programmers aren’t blocked by this but I think that’s because many Java programmers don’t try to use Java for these tasks. They don’t write systems software in Java and they don’t embed into big, performance-sensitive native apps, like games.

> First, I think it’s very common to want to expose managed memory to native. In fact, it might be the dominant case. If I want to call out to perform a crypto operation on a block of bytes I got from a Java operation, I don’t want to copy them first.

Doing it this way is not so common in Java anyway. First, primitive operations for crypto are intrinsics in Java and operate without FFI at all. Second, IO input and output buffers in high-performance applications are typically in off-heap buffers anyway (i.e. you serialize data to an off-heap buffer and then do crypto and then send it over the wire, or you receive data in an off-heap buffer, do crypto, and then deserialize).

> Second, I think you’re missing the use case for manipulating system APIs. If you want to perform some system call and the call requires setting up some structures as arguments, that’s going to be pretty expensive in Java.

It's not, because FFM allows you to manipulate native structs with no overhead. You do this efficient kind of stack allocation of native structures with FFM's Arenas and SegmentAllocator (https://docs.oracle.com/en/java/javase/22/docs/api/java.base...)

> They don’t write systems software in Java and they don’t embed into big, performance-sensitive native apps, like games.

It's true low-level programs are typically not written in Java, but the applications programming market is bigger. I wouldn't be at all surprised if applications written in Java alone comprise a bigger market than all intrinsically low-level applications combined. As for embedding in another application, there is no intrinsic reason not to do it in Java, but 1. traditionally and for "environmental" reasons Java hasn't been huge in the games space (except for Minecraft, of course) and 2. it's been less than six months since FFM became a permanent feature in the JDK; JNI, the FFI mechanism that preceded FFM was really quite cumbersome to use so it's not surprising people opted for more convenient FFI.

> First, primitive operations for crypto are intrinsics in Java and operate without FFI at all.

This is a pretty strange assertion given that I didn’t specify the crypto operation I wanted to perform. Is XAES-256-GCM available in the Java standard library?

> Doing it this way is not so common in Java anyway

Sure, because doing it the other way would be very expensive. But that doesn’t mean applications which can’t front or backload native processing don’t exist, it just means they will have slower throughput in Java.

It’s fine for a language to make that tradeoff, but it is a tradeoff

> Is XAES-256-GCM available in the Java standard library?

No (is it in any language's standard library?) but everything you need to implement it in Java is available.

> But that doesn’t mean applications which can’t front or backload native processing don’t exist, it just means they will have slower throughput in Java.

They won't, because working with native memory is just as efficient as working with heap memory. You store your bytes in a MemorySegment and you don't care if it's backed by an on- or off-heap buffer. I guess you could say, oh, but when working with FFI in Java you may need to keep some buffers off-heap if you don't want to copy bytes, but that's common practice in Java since JDK 1.4 (2002).

> It’s fine for a language to make that tradeoff, but it is a tradeoff

There is a tradeoff, but it's not on performance. Rather than expose Java heap objects directly to native code (which is possible with the old JNI, but not the recommended approach), Java says keep the bytes that you want to efficiently pass to native code off-heap and makes it easy to do (through the same interface for on- and off-heap data).

Rather than constrain the implementation, which could have performance implications always, Java gives you the choice to have no FFI overhead at the cost of a tiny bit of convenience when doing FFI. Given how rare FFI is in Java compared to many other languages, that is obviously the right design decision and it helps performance rather than harms it. So there is a tradeoff, but you're clearly trading away less than you would have if FFI were more common and the core implementation were impacted by it.

Ultimately, the question of "is it better to sacrifice language performance and flexibility in exchange for doing X (without significant performance overhead) in 3 lines instead of 30" depends entirely on the answer to the question how often users of the language need to do X. If the language is Java and X is FFI, the answer is "rarely" and so you're paying a small cost for a large gain. The tradeoff between the convenience of low/no-overhead FFI and language performance and flexibility becomes much more difficult and impactful in languages where FFI is more common.