Ask HN: Why are there no traditional language compilers that target the JVM?
114 comments
I wrote a book on that almost a quarter-century ago (Programming for the Java Virtual Machine). It includes toy versions of both a Lisp and Prolog compiler. It can definitely be done.
I don't know if it's worth it to port existing code to the JVM that way. There are so many pitfalls in porting that it's often easier to rewrite it. The older code is, the more likely it has something specific to the compiler/platform/library, and you'll spend more effort debugging that than just using it as a reference for a clean-sheet implementation in a modern language.
I picked Lisp and Prolog because those are languages with a very different style. Compared to that C/C++/Fortran are all kinda the same language. Since then functional programming has taken on a lot more prominence, though logic-style programming still hasn't caught much attention. (The book actually came out of a project to use logic programming as a database query language, and I still feel like that's better than where we are now.)
It blows my mind that the book is still in print, and dribbles out a few copies a year. It does need a good rewrite, since so many of the things I talked about are now practical rather than theoretical. The JVM has some new features for supporting non-Java languages, and there are now standard libraries for manipulating bytecode. (I had to roll my own.)
I don't know if it's worth it to port existing code to the JVM that way. There are so many pitfalls in porting that it's often easier to rewrite it. The older code is, the more likely it has something specific to the compiler/platform/library, and you'll spend more effort debugging that than just using it as a reference for a clean-sheet implementation in a modern language.
I picked Lisp and Prolog because those are languages with a very different style. Compared to that C/C++/Fortran are all kinda the same language. Since then functional programming has taken on a lot more prominence, though logic-style programming still hasn't caught much attention. (The book actually came out of a project to use logic programming as a database query language, and I still feel like that's better than where we are now.)
It blows my mind that the book is still in print, and dribbles out a few copies a year. It does need a good rewrite, since so many of the things I talked about are now practical rather than theoretical. The JVM has some new features for supporting non-Java languages, and there are now standard libraries for manipulating bytecode. (I had to roll my own.)
Very curious what bytecode manipulation libraries you would recommend. I am aware of ASM [1], which was pleasant, but written in Java itself, I was wondering about something in (say) C, so that it can be used from other languages more easily.
[1] https://asm.ow2.io/
[1] https://asm.ow2.io/
Because there is no sensible mapping from, like, C source to the JVM. C expresses semantics that are not available on the JVM, like most casts, and the JVM is built around concepts that don't exist in C or Fortran, like objects and method dispatch. They do exist to some extent in C++ but the semantics, memory model, etc, are completely different.
Someone could make up a mapping but it would be a numerological exercise, a flight of fancy forever lacking utility.
And anyway there is a practical integration between C and Fortran world and JVM world. The JVM can link to compiled C and Fortran object code, and for those who like suffering, JVM structures can be reached from C source.
Someone could make up a mapping but it would be a numerological exercise, a flight of fancy forever lacking utility.
And anyway there is a practical integration between C and Fortran world and JVM world. The JVM can link to compiled C and Fortran object code, and for those who like suffering, JVM structures can be reached from C source.
That’s absolutely not true. C only needs a huge array, and you can do anything you want with that in any language. That’s pretty much what WASM does as well.
I’m sure there are some other ways as well, but a supported one is through the Graal project, which can run LLVM bitcode (and thus any of C, C++, Rust, etc) natively. Hell, it can interop with regular Java code, or that of other Truffle languages, and even optimize across language barriers!
I’m sure there are some other ways as well, but a supported one is through the Graal project, which can run LLVM bitcode (and thus any of C, C++, Rust, etc) natively. Hell, it can interop with regular Java code, or that of other Truffle languages, and even optimize across language barriers!
Yes, but at that point you've implemented abstractions on the JVM's abstractions, or used platform/implementation dependent code.
What's the point, just to say you did it?
It's not impossible, it's just not worth the ROI time wise for most people with the desire to make a C/C++ standard compliant compiler to do so-- and even if they do, the amount of traction it'll gain will be minimal because developers can make an educated choice not to use it when native C/C++ compilers are available, or alternative JVM languages like Kotlin.
Which is why you have a bunch of attempts at this that stopped development in 2009/2015.
What's the point, just to say you did it?
It's not impossible, it's just not worth the ROI time wise for most people with the desire to make a C/C++ standard compliant compiler to do so-- and even if they do, the amount of traction it'll gain will be minimal because developers can make an educated choice not to use it when native C/C++ compilers are available, or alternative JVM languages like Kotlin.
Which is why you have a bunch of attempts at this that stopped development in 2009/2015.
while technically true in the sense that any turing machine can simulate another turing machine, it probably wouldn't be efficient enough to be worth it.
What would be the point of doing that?
>Because there is no sensible mapping from, like, C source to the JVM
Really?
C is available on so many hardware sets ... it's practically universal (Arduino, x86, MIPS, 68k, PowerPC, ARM, SPARC, RISCV...)
I find it incredibly hard to believe that you couldn't target the JVM (which also runs on SPARC, ARM, RISCV, x86, POWER...) with C as the source language!
Really?
C is available on so many hardware sets ... it's practically universal (Arduino, x86, MIPS, 68k, PowerPC, ARM, SPARC, RISCV...)
I find it incredibly hard to believe that you couldn't target the JVM (which also runs on SPARC, ARM, RISCV, x86, POWER...) with C as the source language!
There has been multiple C compilers targetting the JVM, however they've all also had bad performance penalties since the JVM has no notion of references and pointers so it becomes "emulated", best case you write one that works on ByteBuffer objects (Like early Asm.JS before WASM used a WebGL TypedArray objects before they became part of JavaScript proper) but once you go that route it becomes frustrating to interface with the rest of the Java ecosystem (and you kinda loose the entire point of a JVM targeting C compiler).
Even Sun/Oracle recognized the mismatch and that's why GraalVM is a thing (and why Graal isn't entirely meshable with classic Java runtimes even if I hope they have a merge-strategy for the long-term).
Even Sun/Oracle recognized the mismatch and that's why GraalVM is a thing (and why Graal isn't entirely meshable with classic Java runtimes even if I hope they have a merge-strategy for the long-term).
Java understands "references" and "pointers" (or, at least did in the 1.6 (and before) era...does it not any more?)
C++ and Java reference semantics are very different. Java, like Python, has a model of "references passed by value".
void caller() {
T a = new T(0);
T b = new T(1);
callee(a, b);
}
void callee(T a, T b) {
a = b;
}
In Java, a and b are references, but from caller's perspective, callee is a no-op. On the other hand, using C++'s reference semantics, (and replacing T with T&) callee would modify the a object, not just locally change the reference.I don’t see its relevance, that’s up to the compiler to “solve”, adhering to reference’s semantics. It is just pointers/transformations either way in machine code, which can be trivially emulated in any language/platform that has an array of bytes available.
Yes, the language runtime version of the "Turing tarpit". Any Turing-complete system can emulate any other. However, in this case, you're bounds-checking absolutely every memory operation, whereas with native Java field access, the class verifier proves the field access is legal just once at class load time.
The JIT compiler can elide most of those just as well at runtime
The JVM can elide most of the Java bounds checks because it has much more static information from Java class files than is available from C code. Once you're emulating a memory-unsafe language, you lose most of those static semantic guarantees you get from valid Java code. In the general case, in a memory unsafe language, statically removing bounds checks is equivalent to solving the halting problem.
One could definitely add some extra load-time static analysis to the JVM, similar to what the NaCL / PNaCL did at binary load time to greatly increase the opportunities for JVM bounds check removal. However, this would need some extensions to the class file format and/or some new class file annotations to expose some more static information than the JVM has for vanilla Java class files.
In addition, there are also plenty of ways one could optimize away some bounds checks based on undefined behavior, which would result in a standards-conforming C implementation, yet break tons of C code in the wild. (There at least used to be a C to Go transpiler that implemented C unions this way.) For instance, it's UB to write to one field of a union and then read another field, so it'd be legal for a compiler to implement a union as a struct. However, there's plenty of code in the wild (such as many nanboxing/nunboxing implementations) that rely on union fields occupying the same physical addresses.
One could definitely add some extra load-time static analysis to the JVM, similar to what the NaCL / PNaCL did at binary load time to greatly increase the opportunities for JVM bounds check removal. However, this would need some extensions to the class file format and/or some new class file annotations to expose some more static information than the JVM has for vanilla Java class files.
In addition, there are also plenty of ways one could optimize away some bounds checks based on undefined behavior, which would result in a standards-conforming C implementation, yet break tons of C code in the wild. (There at least used to be a C to Go transpiler that implemented C unions this way.) For instance, it's UB to write to one field of a union and then read another field, so it'd be legal for a compiler to implement a union as a struct. However, there's plenty of code in the wild (such as many nanboxing/nunboxing implementations) that rely on union fields occupying the same physical addresses.
A JIT compiler doesn’t operate at that compile time, so it simply doesn’t have to solve it statically — it notices that this invocation at this place always uses 46 for n, so it can use it as a constant for its compilation, at which point it is more than possible to elide many checks.
Bounds checks are either runtime checks or removed by static analysis. JIT just pushes back the time at which that static analysis is done and increases the amount of context available (largely through inlining).
It's still fundamentally static analysis, just run at JIT time, not at class file generation time. My point is that the JIT-time static analysis in the JVM is heavily optimized for memory accesses typical of Java programs.
Typical C code is drastically different, especially since pointer arithmetic is common in C code. In order to properly statically check an access via pointer arithmetic from a pointer allocated via malloc(), the JIT would need to be operating over a code fragment that included both the malloc call (to know where the base address is relative to the bound) and the memory access site. Lots of C code has way too much code executing between malloc and your pointer arithmetic to reasonably expect that the JIT is going to have all of that code in the execution trace it's currently optimizing.
In the specific case suggested above, just using a giant byte array to represent all of memory, bounds-checking some memory access to an arithmetic-modified pointer gotten from malloc(), the bounds removal would be dependent on the exact address returned by malloc(). In some programs, maybe there's tons of access to a small number of buffers allocated at program start-up time. In that case, current JVMs might do a pretty good job at eliding bounds checks by noticing that the base address is usually one of a small number of values and having a dedicated code path for those common cases. However, if you're doing frequent malloc(), then you're getting tons of different base addresses. In other programs, the malloc() and the access will be close enough together that the JIT'd fragment will include both the malloc and the access, in which case the JVM will have enough context to elide the bounds access. However, it's way too hand-wavy to just say the JVM is good at eliding array bounds checks.
The JVM is very good at eliding array bounds checks typical of Java programs. This is very different from eliding array bounds checks typical in memory-unsafe languages.
It's still fundamentally static analysis, just run at JIT time, not at class file generation time. My point is that the JIT-time static analysis in the JVM is heavily optimized for memory accesses typical of Java programs.
Typical C code is drastically different, especially since pointer arithmetic is common in C code. In order to properly statically check an access via pointer arithmetic from a pointer allocated via malloc(), the JIT would need to be operating over a code fragment that included both the malloc call (to know where the base address is relative to the bound) and the memory access site. Lots of C code has way too much code executing between malloc and your pointer arithmetic to reasonably expect that the JIT is going to have all of that code in the execution trace it's currently optimizing.
In the specific case suggested above, just using a giant byte array to represent all of memory, bounds-checking some memory access to an arithmetic-modified pointer gotten from malloc(), the bounds removal would be dependent on the exact address returned by malloc(). In some programs, maybe there's tons of access to a small number of buffers allocated at program start-up time. In that case, current JVMs might do a pretty good job at eliding bounds checks by noticing that the base address is usually one of a small number of values and having a dedicated code path for those common cases. However, if you're doing frequent malloc(), then you're getting tons of different base addresses. In other programs, the malloc() and the access will be close enough together that the JIT'd fragment will include both the malloc and the access, in which case the JVM will have enough context to elide the bounds access. However, it's way too hand-wavy to just say the JVM is good at eliding array bounds checks.
The JVM is very good at eliding array bounds checks typical of Java programs. This is very different from eliding array bounds checks typical in memory-unsafe languages.
I'm really interested in this space. I'm the original author of the Wikipedia page on SafeTSA. I really want compiling to native code to be as common as writing inline assembly currently is (and I see no fundamental reason why it won't eventually be). I used to be one of 5 developers for the most popular Java desktop program in the early 2000s. I have some ideas for hardware features to reduce the overhead of run-time code re-optimization based on real-time profiles. I guess when I eventually retire, I'll play around with an FPGA CPU implementation of my ideas.
I really believe JITs can do what you're describing, but I think it's important not to over-sell current JIT implementations. Over-selling the technology results in skepticism.
In my ideal world, we compile to either a high-level control-flow-graph/static-single-assignment representation (maybe similar to SafeTSA) as a distribution format. At installation time, we AoT-compile to native code (similar to the current Android runtime). At run-time, we take advantage of extremely low overhead hardware profiling/tracing features to re-optimize hot spots. Optimal ordering/inclusion of various optimizations is a bit of a black art, so I expect some degree of randomization here. In cases where profiling shows the re-optimized code out-performs the AoT version, we'd then cache the re-optimizations to disk. Optionally, a periodic process would periodically perform more expensive offline static-re-optimization of the AoT'd binary, similar to the current Android runtime.
Crucially, the garbage collector should be decoupled from the binary generation/re-optimization system. The CPU overhead of garbage collection can be brought pretty low, but there's a size/space trade off. Last I checked, the rule of thumb is that unless you're manually pooling objects, you can expect a GC'd program to use roughly twice as much memory as an equivalent C++ program. Now, the cognitive/development overhead of manual memory management usually isn't worth the memory savings. However, I've worked in several domains where the memory savings are worth the slower development time.
I really believe JITs can do what you're describing, but I think it's important not to over-sell current JIT implementations. Over-selling the technology results in skepticism.
In my ideal world, we compile to either a high-level control-flow-graph/static-single-assignment representation (maybe similar to SafeTSA) as a distribution format. At installation time, we AoT-compile to native code (similar to the current Android runtime). At run-time, we take advantage of extremely low overhead hardware profiling/tracing features to re-optimize hot spots. Optimal ordering/inclusion of various optimizations is a bit of a black art, so I expect some degree of randomization here. In cases where profiling shows the re-optimized code out-performs the AoT version, we'd then cache the re-optimizations to disk. Optionally, a periodic process would periodically perform more expensive offline static-re-optimization of the AoT'd binary, similar to the current Android runtime.
Crucially, the garbage collector should be decoupled from the binary generation/re-optimization system. The CPU overhead of garbage collection can be brought pretty low, but there's a size/space trade off. Last I checked, the rule of thumb is that unless you're manually pooling objects, you can expect a GC'd program to use roughly twice as much memory as an equivalent C++ program. Now, the cognitive/development overhead of manual memory management usually isn't worth the memory savings. However, I've worked in several domains where the memory savings are worth the slower development time.
Not like C. They are not comparable. You get a reference to something you allocate on the heap, and you can have primitives on the stack. But you cannot take a reference to a primitive, or a reference to another reference, or a reference to the second element of an array. To run C on the JVM you are really swimming uphill.
You see &variable in a function body, you allocate a stackframe for that in the heap abstraction and return that’s address.
And now for a reference to a field in a struct living on the heap?
Of course you can get C to run on the JVM (any CPU emulator written in Java proves that) but due to JVM limitations you cannot do so efficiently. Which is why it the JVM is not an attractive target for the 'traditional' languages.
Of course you can get C to run on the JVM (any CPU emulator written in Java proves that) but due to JVM limitations you cannot do so efficiently. Which is why it the JVM is not an attractive target for the 'traditional' languages.
That struct on the heap is just consecutive bytes in an array, so you just return an integer.
warrenm was asking whether Java's (and implicitly: the JVMs) concept of references could be used. I'm glad that you agree with me that they cannot.
Representing the memory of a compiled C program with a giant byte array would of course work, but also defeat the purpose of running on the JVM in the first place. You lose all the advantages that running on the JVM might have given you, and are slower than a natively compiled program.
Representing the memory of a compiled C program with a giant byte array would of course work, but also defeat the purpose of running on the JVM in the first place. You lose all the advantages that running on the JVM might have given you, and are slower than a natively compiled program.
It has references, but from what I understand they don't entirely conceptually match in use, implementation, and semantics to C++ references.
It has pointers aliased as references. They are pointers for all practical purposes. NullPointerException is named as such for this very reason - pointers were considered scary back then, hence the marketing term.
Yes, implementation wise underneath in most cases they're likely some structs/pointers/whatever, but the point was that interaction with them from the perspective of an application running on the JVM is not the same as what the JVM sees itself, nor are the same exact operations available-- you'd have to create abstractions or use implementation specific jdk.internal.misc.* calls, and at that point why are you using the JVM.
Java doesn’t have pointers in the C sense. Code such as
while(*dst++=*src++);
or char buf[100];
char *p = buf+13;
p[-3] = p[1];
doesn’t map well to the JVM. As others said, you’d have to (almost) emulate memory in a byte array or byte buffer to fully support C.I think the point is that you are unable to address memory in the same way as C because the underlying technologies are different.
You're making the mistake of assuming transitivity in targets—because C targets all these architectures and the JVM runs on all these architectures, therefore C should target the JVM. The whole point of the JVM is to abstract away the underlying machine, so which architectures it runs on is irrelevant to whether its instruction set is a good semantic fit for C! I can write a Lisp to target any of those architectures as well, but I wouldn't suggest you should compile C to it.
The big problem is that C assumes a low-level target language, which all these architectures provide but the JVM does not. A C runtime on the JVM would have to re-implement things like direct memory access on top of JVM arrays, which, while possible, would introduce performance penalties that would completely negate the purpose of writing C.
The big problem is that C assumes a low-level target language, which all these architectures provide but the JVM does not. A C runtime on the JVM would have to re-implement things like direct memory access on top of JVM arrays, which, while possible, would introduce performance penalties that would completely negate the purpose of writing C.
The "mapping" is from concept to concept. For example if there is no concept of collective objects in a spoken language- no way to talk about books for example only a book. Then mapping "we (plural) like books on all subjects" gets to be very hard. The hardware (people) is the same but the conceptual frame works are just too different. Not sure if that was what you were talking about...
You could… but why would you want to? C can’t use any of the Java features so you’d just be adding pointless inefficiencies
The architectures that the code runs on has no bearing on whether it makes sense. What, for instance, do you turn a malloc() call for a number of bytes into? The JVM manages your memory for you and assumes you don't do it yourself. There are no instructions for telling the JVM how to manually fiddle with raw memory. And why would you want that? You already have code that expresses how to fiddle with the memory! The JVM provides functionality that C code already implements manually itself.
The JVM knows how to handle memory (to some greater or lesser degree ... hence Xmin/Xmax) - https://antipaucity.com/2011/08/08/why-technical-intricacies...
That's the point. The JVM deals with objects. Classes. It has a garage collector. You can't make that work (at least not without a huge amount of effort) with a language whose code doesn't have a concept of those things. The abstraction the JVM implements isn't useful to C code.
That’s nonsense. Ad absurdum every Turing-complete language can emulate every other. And in this particular case it is not even particularly complex.
[deleted]
The point is that emulating the JVM abstraction isn't useful to C programmers. Just because it can be done in theory doesn't mean it's practical.
I don't think they said otherwise. I believe the point was that JVM bytecode doesn't expose anything for manual memory deallocation at runtime.
> There are no instructions for telling the JVM how to manually fiddle with raw memory.
Ahem, sun.misc.Unsafe.{get,put}{Byte,Short,Int,Long,Float,Double,Address}
> What, for instance, do you turn a malloc() call for a number of bytes into?
Perhaps sun.misc.Unsafe.allocateMemory?
Ahem, sun.misc.Unsafe.{get,put}{Byte,Short,Int,Long,Float,Double,Address}
> What, for instance, do you turn a malloc() call for a number of bytes into?
Perhaps sun.misc.Unsafe.allocateMemory?
From what I understand jdk.internal.misc.Unsafe.* methods do not contain direct JVM opcodes for memory management, but virtual methods with implementation defined behavior, that usually require reflection to access, and are not 100% portable behavior wise between JVM implementations-- am I wrong?
In order to execute C/C++ code in the JVM you would essentially have to write a whole emulator. Allocate a single huge byte array and treat that as the "memory" of the emulated computer. Then you can write malloc that operates on it.
In practice this is totally pointless.
In practice this is totally pointless.
Why would it be pointless? A region of the heap which malloc can return doesn’t sound too problematic at all, nor would it have significant performance penalty. Getting pointers to stack variables is a bit more cumbersome, but you can just escape analyze functions, and store in a heap-allocated ByteBuffer stack variables that might be dereferenced.
the JVM IS a "whole emulator" ... that's the whole point of the JVM :)
You make a ByteBuffer at program startup, indexes into it are your pointers. Malloc finds free space in it in exactly analogous mode to how it works in a C-implementation.
Arduino is not a chip. It's either AVR or ARM depending on which Arduino board you buy.
I don't know if it counts, but you could compile it down to llvm bitcode and run it on sulong as a part of graalvm. I imagine it comes with caveats but it's used for real stuff.
Four COBOL for the JVM implementations here along with Prolog, Common Lisp, four versions of Scheme (don't forget Clojure), Smalltalk, two versions of Pascal, Go, Rust, three JavaScript, Simula (the original OO language), and Basic (among others):
https://en.wikipedia.org/wiki/List_of_JVM_languages
GCC-Bridge, a C/Fortran compiler targeting the Java Virtual Machine (JVM) that makes it possible for Renjin to run R packages that include "native" C and Fortran code
https://www.renjin.org/blog/2016-01-31-introducing-gcc-bridg...
Earlier attempts at FORTRAN struggle because of performance issues but there were some successes:
JLAPACK – Compiling LAPACK FORTRAN to Java https://www.hindawi.com/journals/sp/1999/179617/
https://www.semanticscholar.org/paper/Automatic-translation-...
https://en.wikipedia.org/wiki/List_of_JVM_languages
GCC-Bridge, a C/Fortran compiler targeting the Java Virtual Machine (JVM) that makes it possible for Renjin to run R packages that include "native" C and Fortran code
https://www.renjin.org/blog/2016-01-31-introducing-gcc-bridg...
Earlier attempts at FORTRAN struggle because of performance issues but there were some successes:
JLAPACK – Compiling LAPACK FORTRAN to Java https://www.hindawi.com/journals/sp/1999/179617/
https://www.semanticscholar.org/paper/Automatic-translation-...
There's also GraalVM if you want to target the JVM for multiple languages (e.g. you can use https://www.graalvm.org/latest/reference-manual/llvm/ for C/C++/Fortran).
Because of pointers.
C and C++ work with a large pointer-addressable heap, the JVM works with objects.
You could emulate a heap in the JVM though. You wouldn't get the advantage of JVM's automatic garbage collection, but at least you'd get some form of portability (don't expect to run the JVM inside the JVM this way).
C and C++ work with a large pointer-addressable heap, the JVM works with objects.
You could emulate a heap in the JVM though. You wouldn't get the advantage of JVM's automatic garbage collection, but at least you'd get some form of portability (don't expect to run the JVM inside the JVM this way).
There are C compilers targeted at Lisp Machines, one targeted at the JVM could use the same techniques to implement pointers.
Yes, but why? The original question wasn't whether it was possible it was why it hasn't been done, and the obvious answer is that there are barriers that make it impractical (not impossible).
What would be the point? For example, what would you gain by having a C compiler that compiled to bytecode? It would be amazingly low level (C is just an assembler with macros) yet amazingly non-performant. And of course you'd need to port libc et al to do anything non-trivial.
Don't forget that the JVM already has ways to communicate with native programs (JNI originally, but now a new FFI is in the works), so if you depended on any native code you could still use it.
Don't forget that the JVM already has ways to communicate with native programs (JNI originally, but now a new FFI is in the works), so if you depended on any native code you could still use it.
> What would be the point? For example, what would you gain by having a C compiler that compiled to bytecode?
You would use it for the same thing that you can do with GraalVM Enterprise today: use native libraries written in C/Fortran/… from your other JVM code in a sandboxed way, without any possibility that the entire VM can crash due to memory corruption bugs in the C/Fortran/… code.
You would use it for the same thing that you can do with GraalVM Enterprise today: use native libraries written in C/Fortran/… from your other JVM code in a sandboxed way, without any possibility that the entire VM can crash due to memory corruption bugs in the C/Fortran/… code.
There are some advantages to it, at least if the bytecode is designed for it like WebAssembly is. You get portability and the loss of performance isn’t always that bad (WASM is maybe 70%ish as fast as native though that will depend on the workload and VM implementation), and you can reuse existing code. But the JVM isn’t really designed for it
> WASM is maybe 70%ish as fast as native
That counts as bad performance in my book.
That counts as bad performance in my book.
It’s not “amazingly non-performant” though. I assume the comment was thinking of something dynamic being run through an interpreter, which might be 1-10% as performant as native, but if you design the bytecode to be statically compilable and simple you can do a lot better. A 30%ish performance hit really isn’t bad for perfect cross-platform compatibility, determinism, and sandboxing for free, and the gap will close as it matures
True, it's not amazingly poor. But the performance hit is very discouraging nonetheless.
C is not too low-level at all. More low-level than Java, but no more than C++/Rust and other low-level languages. It’s hardly an assembler with macros in this century — it’s just the compiler at O3 doing so many mental acrobatics to make you believe that lie.
I respectfully disagree. The mental model of writing assembly code and C code are very close - you can write the best C code if you think about how you would translate it to CPU instructions in your head.
As a matter of fact, you said it yourself in a neighbouring thread that all C needs is a large array - I cannot imagine how can you get any lower than that: mapping almost 1:1 to the general memory model of a CPU executing instructions with some addressable memory.
C++ has classes and templates, Rust has... a lot of things, they are definitely higher up in terms of abstractions built into the language.
As a matter of fact, you said it yourself in a neighbouring thread that all C needs is a large array - I cannot imagine how can you get any lower than that: mapping almost 1:1 to the general memory model of a CPU executing instructions with some addressable memory.
C++ has classes and templates, Rust has... a lot of things, they are definitely higher up in terms of abstractions built into the language.
Re C++/Rust: That’s expressivity, which is a different axis.
C can’t control cache behavior, has no SIMD control, and no threading support (non-standard extensions doesn’t count) It maps well to PDP-11, but that has barely any similarity to modern CPUs.
For what its worth, one might argue that C++ and Rust are lower level, as they can even control some of the above listed functions.
As for “closeness to generated assembly”, as I said, at O3 the generated code doesn’t resemble the source at all — it might not even execute anything your wrote, it may have calculated the result at compile time and will just return the result/look it up in a map at runtime.
C can’t control cache behavior, has no SIMD control, and no threading support (non-standard extensions doesn’t count) It maps well to PDP-11, but that has barely any similarity to modern CPUs.
For what its worth, one might argue that C++ and Rust are lower level, as they can even control some of the above listed functions.
As for “closeness to generated assembly”, as I said, at O3 the generated code doesn’t resemble the source at all — it might not even execute anything your wrote, it may have calculated the result at compile time and will just return the result/look it up in a map at runtime.
Cache is a microarchitectural thing. There's no "controlling" it without explicitly knowing the properties of the caches of the running processor (size, latency, associativity, replacement policy, and whatever other quirks the hardware designers decide to add) and building your code around it, at which point nothing will be able to do it in a "standard" way (unless your standard hard-codes all the past, present, and future architectures, which, good luck).
SIMD isn't really feasible to fully do in a standard way either. Without specific knowledge of the target, you can easily write SIMD-looking code that ends up slower than a scalar equivalent (e.g. if you do any non-constant shuffle on x86-64 pre-SSSE3 (i.e. the default), you'll get utterly awful results. On ARM NEON, a non-constant i32x4 shuffle is gonna be quite awful too).
Threading - <threads.h> is a standard header, no? (granted, it doesn't appear to be used much, but it does exist)
And, sure, every statement/expression/operator of yours doesn't end up always literally mapping to a single assembly instruction, but there's pretty much always a way it could, and quite often it does (and in the cases where it doesn't, it (usually) maps to something better than what you wrote). The main thing that doesn't is macros, but those are extremely trivially expandable.
SIMD isn't really feasible to fully do in a standard way either. Without specific knowledge of the target, you can easily write SIMD-looking code that ends up slower than a scalar equivalent (e.g. if you do any non-constant shuffle on x86-64 pre-SSSE3 (i.e. the default), you'll get utterly awful results. On ARM NEON, a non-constant i32x4 shuffle is gonna be quite awful too).
Threading - <threads.h> is a standard header, no? (granted, it doesn't appear to be used much, but it does exist)
And, sure, every statement/expression/operator of yours doesn't end up always literally mapping to a single assembly instruction, but there's pretty much always a way it could, and quite often it does (and in the cases where it doesn't, it (usually) maps to something better than what you wrote). The main thing that doesn't is macros, but those are extremely trivially expandable.
You can prefetch memory though (but yeah, it indeed is very finicky to get right. So is any sort of decision compilers do, so there is that). Off topic, but the OCaml GC does use prefetch instructions for I believe a 3-4x speedup in an already mature project, so there is that.
SIMD is still the panacea of compiler optimizations, you won’t get significant performance improvements from trivial peephole optimizations like replacing * 2 with << 1, and the supposedly low-level C has no way of controlling that. Sure, vector sizes are different between processors, but so is many other things we already generalize/assume.
But an optimizing compiler will change up your loops all around, inline your functions, etc, so just because it could correspond to some naive assembly, it won’t unless you compile it with some toy.
SIMD is still the panacea of compiler optimizations, you won’t get significant performance improvements from trivial peephole optimizations like replacing * 2 with << 1, and the supposedly low-level C has no way of controlling that. Sure, vector sizes are different between processors, but so is many other things we already generalize/assume.
But an optimizing compiler will change up your loops all around, inline your functions, etc, so just because it could correspond to some naive assembly, it won’t unless you compile it with some toy.
Ok, prefetching is a thing that could maybe be standardized as a hint, but even then there could be differences in how many bytes apart your prefetches optimally need to be, and what range they'll prefetch (might even be a property of the specific CPU at runtime, at which point things get really hairy to do properly).
My point with SIMD is that pretty much all current things do, at worst, map to one instruction (maybe more for things like large loads/stores) on modern 64-bit architectures. But with SIMD the operations supported by various architectures are so diverse, such that taking the intersection (i.e. preserving things mapping mostly-1-to-1 at worst), you'll have an unusable thing (no non-constant shuffles, 8-bit shifts, float rounding, movemask, fault-only-first loads, compress, limited conversions (for 64-bit floats x86 only has i32↔f64, but NEON only has i64↔f64; and narrowing integers takes a sequence of shuffles on pre-AVX-512 x86)). And if you take the union of architectures, a large amount of operations will just suck on any architecture that's not where it was from. Never mind architectures without SIMD.
Different vector sizes is a comparatively trivial problem to tackle (but then you get to the scalable SIMD sets, i.e. SVE & RISC-V V; and even those differ in an important way - SVE vectors are some multiple of 128 bits long, up to 2048, but rvv also guarantees it being a power of two, and can be up to 2^16 bits, at which point indexes in the vectors don't fit in 8-bit integers).
Sure, the optimizing compiler will optimize things, but that's what it does - optimize things, so its actions should be preferable, and manually writing something equivalent to what the compiler did is still very possible, and often not even that weird (with the discussed exception of vectorization).
My point with SIMD is that pretty much all current things do, at worst, map to one instruction (maybe more for things like large loads/stores) on modern 64-bit architectures. But with SIMD the operations supported by various architectures are so diverse, such that taking the intersection (i.e. preserving things mapping mostly-1-to-1 at worst), you'll have an unusable thing (no non-constant shuffles, 8-bit shifts, float rounding, movemask, fault-only-first loads, compress, limited conversions (for 64-bit floats x86 only has i32↔f64, but NEON only has i64↔f64; and narrowing integers takes a sequence of shuffles on pre-AVX-512 x86)). And if you take the union of architectures, a large amount of operations will just suck on any architecture that's not where it was from. Never mind architectures without SIMD.
Different vector sizes is a comparatively trivial problem to tackle (but then you get to the scalable SIMD sets, i.e. SVE & RISC-V V; and even those differ in an important way - SVE vectors are some multiple of 128 bits long, up to 2048, but rvv also guarantees it being a power of two, and can be up to 2^16 bits, at which point indexes in the vectors don't fit in 8-bit integers).
Sure, the optimizing compiler will optimize things, but that's what it does - optimize things, so its actions should be preferable, and manually writing something equivalent to what the compiler did is still very possible, and often not even that weird (with the discussed exception of vectorization).
This seems overly pessimistic :)
We have actually implemented all of those things except first-fault loads in a cross-platform way at reasonable cost in https://github.com/google/highway.
If any of those is the one missing piece that's preventing you from vectorizing, then it's totally worthwhile to emulate it. It may sound horrible to take 4-5 instructions instead of one, but it can still be very worthwhile.
FYI for SVE, Arm has requested input and recently clarified that there will only be power of two vector lengths.
Happy to discuss.
If any of those is the one missing piece that's preventing you from vectorizing, then it's totally worthwhile to emulate it. It may sound horrible to take 4-5 instructions instead of one, but it can still be very worthwhile.
FYI for SVE, Arm has requested input and recently clarified that there will only be power of two vector lengths.
Happy to discuss.
Yeah, I've seen it! https://google.github.io/highway/en/master/quick_reference.h...
In many cases, yeah, emulating things can be fine, but in others, very much not so. If not doing trivial (i.e. autovectorizable) things, I'd imagine you'd still have to be at least somewhat careful about what things will get emulated, and see whether the gain from everything around it makes it worth it for your desired architectures, which is still far from a state of things I'd consider worthy of being standardized.
Interesting note on SVE vector lengths though. I'll keep that in mind. Haven't gotten to exploring SVE/RVV much; don't have any personal hardware to optimize for to encourage it.
In many cases, yeah, emulating things can be fine, but in others, very much not so. If not doing trivial (i.e. autovectorizable) things, I'd imagine you'd still have to be at least somewhat careful about what things will get emulated, and see whether the gain from everything around it makes it worth it for your desired architectures, which is still far from a state of things I'd consider worthy of being standardized.
Interesting note on SVE vector lengths though. I'll keep that in mind. Haven't gotten to exploring SVE/RVV much; don't have any personal hardware to optimize for to encourage it.
Yes, I appreciated your previous list of 'things it would be better to work around if possible'. But "a large amount of operations will just suck" did sound overly pessimistic :) We are talking about a small percentage of the ~220 ops in Highway.
I agree it's important to benchmark the performance win in any case. Perhaps even automatically picking the best instruction set, because it's nontrivial to predict.
As to SVE, it can help on AWS Graviton3 - but it's still only 2x256 bit vs 4x128 bit, so whether it's a win depends on whether you can/do use some of the newer instructions that were not yet supported in NEON.
I agree it's important to benchmark the performance win in any case. Perhaps even automatically picking the best instruction set, because it's nontrivial to predict.
As to SVE, it can help on AWS Graviton3 - but it's still only 2x256 bit vs 4x128 bit, so whether it's a win depends on whether you can/do use some of the newer instructions that were not yet supported in NEON.
By "large" I mean compared to the fraction of needs-slow-emulation things present currently in C (which is currently approximately zero, other than using integer/float types unsupported on the target hardware (and maybe multiplication/division), but that's comparatively extremely easy to determine).
Maybe my use-case - implementing an array language - is unusually unfit for being done in a generic way, but I just don't see how it (or much of anything outside of very trivial things) would be reasonable to figure out how to do performantly with anything other than either manually using intrinsics, or with custom-written abstractions around such with known worst-case emulation amount (we're doing[0] the latter with a DSL[1] that's basically a basic compile-time code generator; that just leaves unsupported things undefined on any given architecture, and in most occurrences of hitting that I've found ways to write things avoiding expensive emulation).
[0]: https://github.com/dzaima/CBQN/tree/master/src/singeli/src
[1]: https://github.com/mlochbaum/Singeli
Maybe my use-case - implementing an array language - is unusually unfit for being done in a generic way, but I just don't see how it (or much of anything outside of very trivial things) would be reasonable to figure out how to do performantly with anything other than either manually using intrinsics, or with custom-written abstractions around such with known worst-case emulation amount (we're doing[0] the latter with a DSL[1] that's basically a basic compile-time code generator; that just leaves unsupported things undefined on any given architecture, and in most occurrences of hitting that I've found ways to write things avoiding expensive emulation).
[0]: https://github.com/dzaima/CBQN/tree/master/src/singeli/src
[1]: https://github.com/mlochbaum/Singeli
Cool, thanks for sharing, wasn't aware of those.
It is natural that C has about zero emulated operators - today's CPUs are compatible with ones likely built with that use-case in mind. That is a low bar for comparison, especially given a large speedup from SIMD :)
I agree a custom abstraction is currently the most reasonable approach for SIMD. We're basically doing the same thing, just differing a bit in how much emulation is acceptable, and the language for compile-time code generation (it's nice how compactly you can express things in Singeli).
It is natural that C has about zero emulated operators - today's CPUs are compatible with ones likely built with that use-case in mind. That is a low bar for comparison, especially given a large speedup from SIMD :)
I agree a custom abstraction is currently the most reasonable approach for SIMD. We're basically doing the same thing, just differing a bit in how much emulation is acceptable, and the language for compile-time code generation (it's nice how compactly you can express things in Singeli).
> but no more than C++/Rust and other low-level languages.
Hmm, I disagree. C is a mid-level language, one level of abstraction up from assembly. C++ and Rust are high-level languages, one or two levels of abstraction higher than C.
Hmm, I disagree. C is a mid-level language, one level of abstraction up from assembly. C++ and Rust are high-level languages, one or two levels of abstraction higher than C.
That depends on the definition one uses. The traditional, but quite useless one is everything above assemblies is high level, only those are low-level PLs.
I prefer the one of “what features does the language give you idiomatic control of”. In that vein, Rust is more low-level than by having simd support.
I prefer the one of “what features does the language give you idiomatic control of”. In that vein, Rust is more low-level than by having simd support.
Interesting. I wasn't aware there was such a divergence in understanding of what counts as "low" or "high" level.
I thought everyone just went by the amount of abstraction of the machine code. So machine code is no abstraction, assembly is one level up, C is another level up, etc. The less the language "looks" like machine, the higher the "level" of the language.
The inclusion of SIMD support, in my view, falls in the mid-level category. It's abstracting the differing parallel computing implementations of different processors to make them all usable with the same code. That's not low-level in my book.
I'm not saying this to say that your stance is incorrect -- the "level" of a programming language has never been a technically well-defined thing anyway. I'm just saying this to express that my eyes have been opened to a perspective I was unaware of.
I thought everyone just went by the amount of abstraction of the machine code. So machine code is no abstraction, assembly is one level up, C is another level up, etc. The less the language "looks" like machine, the higher the "level" of the language.
The inclusion of SIMD support, in my view, falls in the mid-level category. It's abstracting the differing parallel computing implementations of different processors to make them all usable with the same code. That's not low-level in my book.
I'm not saying this to say that your stance is incorrect -- the "level" of a programming language has never been a technically well-defined thing anyway. I'm just saying this to express that my eyes have been opened to a perspective I was unaware of.
In addition to other answers in this thread, those languages came first. Java's main motivation initially was to fix all the memory bugs that come from manually managing memory in C/C++ and give a pure OOP development experience. Finally for the use cases that C/C++ are still good for, you typically don't want memory management or the JVM performance overhead so there isn't any incentive.
You can literally compile C/C++(using clang)/Rust to LLVM bitcode and run those using GraalVM: https://www.graalvm.org/latest/reference-manual/llvm/Compili...
The Oberon programming language is 37 years old. Since it is a memory safe language a compiler for the JVM can be written (with some workarounds), for example see the self-hosting compiler oberonc [0].
[0] https://github.com/lboasso/oberonc
[0] https://github.com/lboasso/oberonc
Here is the More Than You Ever Wanted To Know answer whilst I wait for a build to go green.
Firstly, for the last few years when you talk about this topic you have to distinguish between the "old world" JVMs which can only be given bytecode as input, and GraalVM, which is a superset of those JVMs and enables language implementations to use the JVM's features whilst bypassing the bytecode layer entirely.
Java byte code has some problems that make it unsuitable for C-like languages. Most obviously you can't do pointer arithmetic or arbitrary casts, which is a fundamental requirement for real C code. This doesn't mean it can't be done, these are all Turing complete machines after all, but it means there's no point because performance would be very poor due to all the workarounds. For C++ the gap gets wider because Java has its own ideas about how objects work that aren't the same as those in C++, e.g. multiple inheritance, so you can't implement C++ classes as Java classes.
These sorts of problems affect any language that is semantically too far away from Java, like scripting languages. JVM bytecode is fairly well designed, is reasonably general, and the invokedynamic bytecode was put in there to make scripting languages easier. But it's a high level bytecode so the semantic mismatch is still there.
For a long time this problem seemed inherent to the design space. Any VM bytecode language you can design will end up encoding some assumptions about language design into it, if it doesn't then you've just got assembly language and we already have those. In effect, trying to create a universal bytecode is like trying to create a universal programming language.
But the JVM guys didn't give up. Sun/Oracle Labs spent many years on research and the result is Truffle. Instead of asking people to encode their language into a universal ISA so the JVM can understand it, Truffle is an API. It comes as part of GraalVM. You write an interpreter for your language using this API and compile that to JVM bytecode instead (it doesn't have to be written in Java but it does have to be a language that can produce bytecode). This interpreter is then fused with the code of your target language at runtime and fed through a very advanced optimizing compiler (Graal) which generates machine code for your language. This code is then "installed" into the running JVM using another API called JVMCI, and then has access to all the normal JVM services like the garbage collectors, profiling, deoptimization, observability, standard libraries, OS abstractions, ability to call to/from bytecode world and so on.
With Truffle you don't think about the low level details of all that. You just write an interpreter. You do have the learn the API - your generated machine code will be relatively slow until you start using the API to annotate your interpreter and optimize it for common cases. But the API is pretty comprehensive and offers a lot of functionality, like language interop, debugging, tracing, hot swapping ...
The first languages implemented with Truffle were scripting languages. But the technique is general. It's not scripting or JVM specific and there's no specific reason the language your interpreter reads has to be textual. So they implemented an interpreter for LLVM bitcode. Now you can compile C/C++/FORTRAN/Rust using LLVM, and then execute that on the JVM. Performance is good, not quite as fast as natively compiled with GCC but in the general area. It's a fairly specialized thing to do and today is mostly used for running Python/Ruby extensions. However, because the whole thing is virtualized you can do some mind-bending tricks with it, for example, you can eliminate all the memory errors in the software run this way and then sandbox it without using kernel sandboxing. So it has some potentially big security benefits.
And that's how you can run C or any other language on the JVM without killing performance.
Firstly, for the last few years when you talk about this topic you have to distinguish between the "old world" JVMs which can only be given bytecode as input, and GraalVM, which is a superset of those JVMs and enables language implementations to use the JVM's features whilst bypassing the bytecode layer entirely.
Java byte code has some problems that make it unsuitable for C-like languages. Most obviously you can't do pointer arithmetic or arbitrary casts, which is a fundamental requirement for real C code. This doesn't mean it can't be done, these are all Turing complete machines after all, but it means there's no point because performance would be very poor due to all the workarounds. For C++ the gap gets wider because Java has its own ideas about how objects work that aren't the same as those in C++, e.g. multiple inheritance, so you can't implement C++ classes as Java classes.
These sorts of problems affect any language that is semantically too far away from Java, like scripting languages. JVM bytecode is fairly well designed, is reasonably general, and the invokedynamic bytecode was put in there to make scripting languages easier. But it's a high level bytecode so the semantic mismatch is still there.
For a long time this problem seemed inherent to the design space. Any VM bytecode language you can design will end up encoding some assumptions about language design into it, if it doesn't then you've just got assembly language and we already have those. In effect, trying to create a universal bytecode is like trying to create a universal programming language.
But the JVM guys didn't give up. Sun/Oracle Labs spent many years on research and the result is Truffle. Instead of asking people to encode their language into a universal ISA so the JVM can understand it, Truffle is an API. It comes as part of GraalVM. You write an interpreter for your language using this API and compile that to JVM bytecode instead (it doesn't have to be written in Java but it does have to be a language that can produce bytecode). This interpreter is then fused with the code of your target language at runtime and fed through a very advanced optimizing compiler (Graal) which generates machine code for your language. This code is then "installed" into the running JVM using another API called JVMCI, and then has access to all the normal JVM services like the garbage collectors, profiling, deoptimization, observability, standard libraries, OS abstractions, ability to call to/from bytecode world and so on.
With Truffle you don't think about the low level details of all that. You just write an interpreter. You do have the learn the API - your generated machine code will be relatively slow until you start using the API to annotate your interpreter and optimize it for common cases. But the API is pretty comprehensive and offers a lot of functionality, like language interop, debugging, tracing, hot swapping ...
The first languages implemented with Truffle were scripting languages. But the technique is general. It's not scripting or JVM specific and there's no specific reason the language your interpreter reads has to be textual. So they implemented an interpreter for LLVM bitcode. Now you can compile C/C++/FORTRAN/Rust using LLVM, and then execute that on the JVM. Performance is good, not quite as fast as natively compiled with GCC but in the general area. It's a fairly specialized thing to do and today is mostly used for running Python/Ruby extensions. However, because the whole thing is virtualized you can do some mind-bending tricks with it, for example, you can eliminate all the memory errors in the software run this way and then sandbox it without using kernel sandboxing. So it has some potentially big security benefits.
And that's how you can run C or any other language on the JVM without killing performance.
> But the technique is general. It's not scripting or JVM specific and there's no specific reason the language your interpreter reads has to be textual. […] And that's how you can run C or any other language on the JVM without killing performance.
That's not entirely true though, because the Graal compiler has certain assumptions about your language (e.g. you need some form of "methods" and "calls", otherwise Graal won't compile anything), which means it is not exactly straight forward or efficient to run let's say x86 machine code on the GraalVM. It's also not easy to properly support an "mmap" call in an efficient way although it is required by virtually every compiled program that uses glibc, because none of the "scripting or JVM languages" need this and therefore there are no facilities in Truffle to support this.
That being said, a fully sandboxed x86_64 emulator for the GraalVM exists and IIRC it is roughly as fast as the JIT based qemu-x86_64 after warmup.
Sulong (the LLVM bitcode interpreter of GraalVM) is also not ideal, because it directly executes the LLVM IR although some C code assumes that it is compiled to machine code. This means some libraries use e.g. inline assembly, or try to do some stack walking, or use entire subroutines implemented in assembly, or rely on certain POSIX behavior, which isn't (fully) implemented in Sulong. A lot of programs work, but if something doesn't work, chances are it's caused by one of the few fundamental flaws of Sulong. Oracle is experimenting with a workaround by not running everything in LLVM IR in the future, but we'll have to wait and see how good this works in the end.
That's not entirely true though, because the Graal compiler has certain assumptions about your language (e.g. you need some form of "methods" and "calls", otherwise Graal won't compile anything), which means it is not exactly straight forward or efficient to run let's say x86 machine code on the GraalVM. It's also not easy to properly support an "mmap" call in an efficient way although it is required by virtually every compiled program that uses glibc, because none of the "scripting or JVM languages" need this and therefore there are no facilities in Truffle to support this.
That being said, a fully sandboxed x86_64 emulator for the GraalVM exists and IIRC it is roughly as fast as the JIT based qemu-x86_64 after warmup.
Sulong (the LLVM bitcode interpreter of GraalVM) is also not ideal, because it directly executes the LLVM IR although some C code assumes that it is compiled to machine code. This means some libraries use e.g. inline assembly, or try to do some stack walking, or use entire subroutines implemented in assembly, or rely on certain POSIX behavior, which isn't (fully) implemented in Sulong. A lot of programs work, but if something doesn't work, chances are it's caused by one of the few fundamental flaws of Sulong. Oracle is experimenting with a workaround by not running everything in LLVM IR in the future, but we'll have to wait and see how good this works in the end.
How do you mean, support mmap efficiently? Do you mean being able to close the mapping without a GC? If so then Panama is fixing that.
I agree it's not designed to JIT machine code. To sandbox that it may be better to use a flyweight CPU-level VM anyway.
Yes, C can do anything and if it does stuff like trying to disassemble itself, then that will clearly fail. But then you could argue it's not really written in C.
I agree it's not designed to JIT machine code. To sandbox that it may be better to use a flyweight CPU-level VM anyway.
Yes, C can do anything and if it does stuff like trying to disassemble itself, then that will clearly fail. But then you could argue it's not really written in C.
> How do you mean, support mmap efficiently? Do you mean being able to close the mapping without a GC? If so then Panama is fixing that.
Everything relevant that can be done with Panama in this context can already be done in a Truffle language with sun.misc.Unsafe and e.g. Sulong used it for exactly this purpose. In fact the Unsafe allowed a lot more with a much simpler API because you really get a function for raw memory access to arbitrary addresses.
But what's the problem anyway? For any normal compiled program, the dynamic linker will mmap the code and data into memory during startup. And a standard memory allocator in the libc like what's used by malloc also uses mmap (and sbrk) internally. Some C programs also directly use mmap to map files into memory or to reserve large amounts of memory, potentially at fixed addresses and with custom protection bits. All of this requires a proper implementation of mmap in the VM if you want to run such programs, in a way that accesses to unmapped or protected memory can be caught without crashing the VM. Side note: Panama does not provide this. The problem here is that the emulated address space only contains a few mapped regions and a lot of unmapped space in between, so you have to come up with a good way how to implement this. You could implement the emulated memory purely in Java, but it is quite slow because you essentially recreate an MMU. For performance reasons you really have to use the hardware MMU in a smart way. It can be done in the GraalVM (and was done in the GraalVM based x86_64 interpreter [1]), but it's not obvious how to do it and it's not particularly efficient either, at least if you want to catch segfaults properly. To efficiently catch segfaults, changes to at least HotSpot would be necessary.
What's even worse here is that it's perfectly valid for a program to register a segfault handler, then cause a segfault and catch it. A few real world programs do exactly this, including the JVM itself. You might ignore such custom signal handling from the guest program, but you definitely have to avoid VM crashes caused by such signals. And again, Panama cannot do it.
> Yes, C can do anything and if it does stuff like trying to disassemble itself, then that will clearly fail. But then you could argue it's not really written in C.
Sure, C programs can do anything, but the problem is that on e.g. a Linux/x86_64 system many things are allowed, certain low level hacks are necessary for performance reasons, and therefore many real world Linux/x86_64 programs do weird things internally, even if it's hidden within some library where you'll never see it. If you want to run an "average" program, you'll have to handle many such cases in your VM. Otherwise you end up with a toy VM which can run a lot of toy programs but fails at larger "real world" programs.
You can do what Sulong does and say "I don't care, I'll just pass malloc/free/mmap/... calls directly to the OS", but then you run into various problems, like e.g. you'll be unable to properly sandbox memory = the guest program can easily crash the VM. You can also do what Sulong in GraalVM Enterprise does and say "we don't support certain features like mmap", but a lot of interesting real world programs won't run. Or you can do what the x86_64 interpreter does and properly (although with reduced performance) emulate all these features, but then you end up building a Java implementation of e.g. the Linux kernel.
In case you wonder, the x86_64 interpreter I mentioned started as a tech demo to show that you can in fact emulate x86_64 with a limited Linux userspace in a fully sandboxed and cross-platform way and with somewhat decent performance on the GraalVM. It even supported Truffle interop with standard Linux .so libraries in the past. Of course it also showed some limitations of the Graal compiler and Truffle, after all that was the entire point of the project. Don't expect Sulong-like peak performance, it's much slower than that. One of the more interesting findings was that machine code emulation with Graal is feasible and works even for larger real world programs like GCC or xpdf or CPython and peak performance can (or at least could at some point in the past) somewhat compete with qemu which uses a hand crafted JIT compiler. This was especially interesting since machine code is the worst imaginable "language" for Graal and Graal is absolutely not built for this.
[1] https://github.com/pekd/tracer/tree/master/vmx86
Everything relevant that can be done with Panama in this context can already be done in a Truffle language with sun.misc.Unsafe and e.g. Sulong used it for exactly this purpose. In fact the Unsafe allowed a lot more with a much simpler API because you really get a function for raw memory access to arbitrary addresses.
But what's the problem anyway? For any normal compiled program, the dynamic linker will mmap the code and data into memory during startup. And a standard memory allocator in the libc like what's used by malloc also uses mmap (and sbrk) internally. Some C programs also directly use mmap to map files into memory or to reserve large amounts of memory, potentially at fixed addresses and with custom protection bits. All of this requires a proper implementation of mmap in the VM if you want to run such programs, in a way that accesses to unmapped or protected memory can be caught without crashing the VM. Side note: Panama does not provide this. The problem here is that the emulated address space only contains a few mapped regions and a lot of unmapped space in between, so you have to come up with a good way how to implement this. You could implement the emulated memory purely in Java, but it is quite slow because you essentially recreate an MMU. For performance reasons you really have to use the hardware MMU in a smart way. It can be done in the GraalVM (and was done in the GraalVM based x86_64 interpreter [1]), but it's not obvious how to do it and it's not particularly efficient either, at least if you want to catch segfaults properly. To efficiently catch segfaults, changes to at least HotSpot would be necessary.
What's even worse here is that it's perfectly valid for a program to register a segfault handler, then cause a segfault and catch it. A few real world programs do exactly this, including the JVM itself. You might ignore such custom signal handling from the guest program, but you definitely have to avoid VM crashes caused by such signals. And again, Panama cannot do it.
> Yes, C can do anything and if it does stuff like trying to disassemble itself, then that will clearly fail. But then you could argue it's not really written in C.
Sure, C programs can do anything, but the problem is that on e.g. a Linux/x86_64 system many things are allowed, certain low level hacks are necessary for performance reasons, and therefore many real world Linux/x86_64 programs do weird things internally, even if it's hidden within some library where you'll never see it. If you want to run an "average" program, you'll have to handle many such cases in your VM. Otherwise you end up with a toy VM which can run a lot of toy programs but fails at larger "real world" programs.
You can do what Sulong does and say "I don't care, I'll just pass malloc/free/mmap/... calls directly to the OS", but then you run into various problems, like e.g. you'll be unable to properly sandbox memory = the guest program can easily crash the VM. You can also do what Sulong in GraalVM Enterprise does and say "we don't support certain features like mmap", but a lot of interesting real world programs won't run. Or you can do what the x86_64 interpreter does and properly (although with reduced performance) emulate all these features, but then you end up building a Java implementation of e.g. the Linux kernel.
In case you wonder, the x86_64 interpreter I mentioned started as a tech demo to show that you can in fact emulate x86_64 with a limited Linux userspace in a fully sandboxed and cross-platform way and with somewhat decent performance on the GraalVM. It even supported Truffle interop with standard Linux .so libraries in the past. Of course it also showed some limitations of the Graal compiler and Truffle, after all that was the entire point of the project. Don't expect Sulong-like peak performance, it's much slower than that. One of the more interesting findings was that machine code emulation with Graal is feasible and works even for larger real world programs like GCC or xpdf or CPython and peak performance can (or at least could at some point in the past) somewhat compete with qemu which uses a hand crafted JIT compiler. This was especially interesting since machine code is the worst imaginable "language" for Graal and Graal is absolutely not built for this.
[1] https://github.com/pekd/tracer/tree/master/vmx86
Thanks, that answer is very interesting. If you're ever on the GraalVM slack please do ping me, I've been interested in Sulong and related topics for a long time. Edit: I found your report, will take a look.
HotSpot has a thing called libjsig which lets HotSpot interop with code that needs to install its own signal handlers. I guess you could use this from a native component of a Truffle language to intercept segfaults, and then if a bit of code is observed to be catching traps you could deopt/reopt. Intel MPKs could be used to protect the VM's own areas of memory. So, still use the native MMU but with a few twists to allow it to be fully virtualized.
Still, at some point I guess the question becomes why not just use something like NOAH. If you have an amd64 host then you can run userspace code inside a custom VM and just trap and emulate the syscalls from outside that space. The main benefit I can think of using Graal to do this is not needing nested virtualization, and maybe with additional work the pure software approach can be faster eventually.
HotSpot has a thing called libjsig which lets HotSpot interop with code that needs to install its own signal handlers. I guess you could use this from a native component of a Truffle language to intercept segfaults, and then if a bit of code is observed to be catching traps you could deopt/reopt. Intel MPKs could be used to protect the VM's own areas of memory. So, still use the native MMU but with a few twists to allow it to be fully virtualized.
Still, at some point I guess the question becomes why not just use something like NOAH. If you have an amd64 host then you can run userspace code inside a custom VM and just trap and emulate the syscalls from outside that space. The main benefit I can think of using Graal to do this is not needing nested virtualization, and maybe with additional work the pure software approach can be faster eventually.
HAXE also targets JVM [0] but it is not traditional.
https://haxe.org/manual/target-jvm-getting-started.html
https://haxe.org/manual/target-jvm-getting-started.html
Interesting, i didnt realize haxe could generate bytecode directly, rather than generating source code (java), which would then be required to be compiled using the jdk.
I don't understand what the use case for doing that would be. One of the advantages of traditionally compiled languages is that you don't need a runtime component for the resulting binary.
There is the GraalVM Python Runtime, Renjin GCC-Bridge (for C, C++, R)...
I feel like all of this kind of exists but it's quite esoteric "non-standard stuff" and not necessarily something sane people want in production.
https://github.com/bedatadriven/renjin/tree/master/tools/gcc...
https://www.graalvm.org/python/
I feel like all of this kind of exists but it's quite esoteric "non-standard stuff" and not necessarily something sane people want in production.
https://github.com/bedatadriven/renjin/tree/master/tools/gcc...
https://www.graalvm.org/python/
Graal's also got an LLVM frontend.
https://www.graalvm.org/latest/reference-manual/llvm/Compati...
https://www.graalvm.org/latest/reference-manual/llvm/Compati...
Things that are central to C, bust the memory model of the JVM (e.g., pointer arithmetic). I did come across a C to JVM project which I used to try and port something slightly complex and it failed to work, thought it may have been sufficient for the author's purposes of accelerating R projects that had dependencies on C libraries.
https://www.renjin.org/blog/2016-01-31-introducing-gcc-bridg...
https://www.renjin.org/blog/2016-01-31-introducing-gcc-bridg...
There is FastR, part of the Graal project which could likely do what the author was looking for - it can run both R and its C dependencies inside the JVM, and the two may be even cross-optimized across language barriers.
Free Pascal (fpc) has had a JVM target since 2011:
https://wiki.freepascal.org/FPC_JVM
There are many but most never gained traction. Compiling C to JVM bytecode obviously is possible but the resulting code would not run efficiently due to the semantic mismatch. For example, implementing call-by-value semantics that C has using bytecode would not be efficient. Neither would pointer arithmetic.
Your assumption is incorrect; it does exist. GraalVM actually supports some of these. And long before GraalVM, there have been multiple commercial offerings to run FORTRAN on the JVM, but they've gone in and out of existence.
Most of the time if you're writing C, Cpp, FORTRAN, your primary goal is performance, not memory safety or portability. So executing on the JVM wouldn't buy you much.
Most of the time if you're writing C, Cpp, FORTRAN, your primary goal is performance, not memory safety or portability. So executing on the JVM wouldn't buy you much.
Just wanted to note that maybe “traditional” isn’t the most accurate description. They are used not because of a tradition but rather due to mountains of legacy code.
And, eventually everything boils down to specific requirements and tasks that need to be accomplished, and the question should be what is the best tool for the given job, given a set of circumstances and constraints…
Unfortunately, the JVM ecosystem did not fix that problem. There are mountains and mountains of Java code from 25+ years ago that are in the same situation. If there was a way to reliability run code on top of the JVM it would be gold. There do exist several products that perform a line-by-line analysis and spit out a comparable Java module, but it is extremely procedural, and is very high touch from an engineering perspective.
Maybe you're right ... I'm using "traditional" in place of "legacy" or "classic" (I believe )
There are a lot of "modern" or "up-and-coming" languages out there (Hare, Python, Ruby, Swift...)
And there are a slew of "old", "traditional", or "classic" languages out there, too :) (APL, FORTRAN, C/C++...)
There are a lot of "modern" or "up-and-coming" languages out there (Hare, Python, Ruby, Swift...)
And there are a slew of "old", "traditional", or "classic" languages out there, too :) (APL, FORTRAN, C/C++...)
I will just leave this here: http://nestedvm.ibex.org/
Because portability is overrated compared to performance/RAM usage hit. Someone stubborn enough could figure it out but why ?
It's not as much of a counterexample now as it would have been years ago, thanks to performance improvements, but JavaScript won on portability despite not being high-performance.
This is economically ineffective.
You could write backend for most compilers, to target JVM (instead of pure machine codes), but result will be extremely slow.
Going in opposite way is much more fruitful - for example, compiling Java to native machine codes you will got about 10 times faster execution.
You could write backend for most compilers, to target JVM (instead of pure machine codes), but result will be extremely slow.
Going in opposite way is much more fruitful - for example, compiling Java to native machine codes you will got about 10 times faster execution.
I have written one (gcc-bridge). Fortran is actually a really good fit for JVM byte code. C/c++ is doable, but only with some unpleasant trade offs that others have mentioned here.
Sure, compile to WASM and then use https://github.com/cretz/asmble to convert to JVM bytecode.
Plus, to slowness, troubles with JVM license. Better to use Apache-licensed LLVM, or something like it (BSD/MIT licenses).
TruffleRuby & JRuby targets the JVM aswell.
did not know about those - I did know about Jython, however :)
But (and correct me if I'm wrong, please), isn't Ruby a "scripting" language? Ie one not 'meant' to be compiled?
But (and correct me if I'm wrong, please), isn't Ruby a "scripting" language? Ie one not 'meant' to be compiled?
Ruby (CRuby) itself is a scripting language, that is correct.
JRuby & TruffleRuby is an implementation of the Ruby language but targets another VM.
JRuby targets the JVM while TruffleRuby is based on the graalVM.
If my memory stands correct, none of these have a compilation step, they both are interpreted. However, TruffleRuby has a feature called Native Image which compiles the language!
JRuby targets the JVM while TruffleRuby is based on the graalVM.
If my memory stands correct, none of these have a compilation step, they both are interpreted. However, TruffleRuby has a feature called Native Image which compiles the language!
I've used Ruby precisely one time in my life ... and only tangentially (building-out a Mastodon instance)
Thank you for the additional information :)
Thank you for the additional information :)
There's a whole bunch of reasons, but like so many other things one of them involves RMS: https://gcc.gnu.org/legacy-ml/gcc/2001-02/msg00895.html
I kinda don't care about RMS any more ... especially on a nearly-quarter-century-old opinion post that doesn't jive with "today" ;)
doublejames(2)
"Traditional" compilable languages, like C, C++, FORTRAN, etc all ought to be targetable to a portable runtime like the JVM (since it defines its own instruction set (Java byte code)).
Scala and other languages target the JVM as their runtime environment.
Why not older/traditionally-compiled languages?