GraalVM Native Image meets RISC-V

Sacha Coppey
graalvm
Published in
5 min readJan 16, 2023

--

It is now possible to use GraalVM Native Image on RISC-V! I will explain here how to compile applications for RISC-V and the implementation. By default, Native Image uses the Graal compiler to produce machine code, but it can use the LLVM compiler instead by enabling the LLVM backend (documentation). One of the key reasons for using LLVM is that it supports a wide range of architectures, meaning that we do not need to implement the Graal compiler for them, which is the hardest step when adding support for a new architecture. To demonstrate this, I adapted the Native Image LLVM backend for RISC-V machines under Linux with a GraalVM dev build.

RISC-V

RISC-V is an open-source Instruction Set Architecture (ISA) created in 2010. The main difference between this architecture and the current leaders of the market is the fact that RISC-V is open source. While its first intended use was for research and education, the industry’s interest is now growing.

It uses a simple base integer ISA, which avoids over-fitting specific micro-architectural patterns and over-optimized features designed for particular domains, reducing cost and improving performance for more straightforward tasks. For more complex uses, extensions can be added on top of the base ISA, such as support for multiplication, floating-points, vectors, and more. Some of those extensions are predefined, but they can also be custom.

Micronaut Demonstration

To show the performance difference between running on the JVM and Native Image, we performed a demonstration based on a Micronaut application, a small web server, on a RISC-V emulator. The demo sources can be found here.

Micronaut server initialization using an application compiled with Native Image:

__  __ _                                  _   
| \/ (_) ___ _ __ ___ _ __ __ _ _ _| |_
| |\/| | |/ __| '__/ _ \| '_ \ / _` | | | | __|
| | | | | (__| | | (_) | | | | (_| | |_| | |_
|_| |_|_|\___|_| \___/|_| |_|\__,_|\__,_|\__|
Micronaut (v3.6.0)

16:52:29.965 [main] INFO io.micronaut.runtime.Micronaut - Startup completed in 305ms. Server Running: http://fedora-riscv:8080

We observed a startup time of ~300ms for the Micronaut application compiled with Native Image. For comparison, the startup time when using Java is ~9300ms. Using Native Image thus results in a speedup of around 31x!

Cross-build Binaries for RISC-V with the LLVM Backend

Considering most of the current ways to run Linux on a RISC-V machine are slow, it is advised to cross-build the binaries for RISC-V. Native Image supports it with some prerequisites.

RISC-V Toolchain with zlib

The RISC-V toolchain has to be provided to Native Image. It can be found here. The versions of gcc and glibc need to be the same or older than the ones used by the RISC-V machine.

zlib is included in the RISC-V toolchain but has to be compiled manually using the produced riscv-gcc. The outputted library libz.a has to be moved to path/to/built/toolchain/sysroot/usr/lib/.

CAPCaches and Static Libraries

The CAPCaches are information about C intrinsics on the target architecture, as well as the values of the CPU features and flags. They can be downloaded here.

Native Image needs Java and Graal static libraries. They can be downloaded here.

Procedure to Cross-Build a Binary

The following procedure describes how to cross-build a binary from a simple HelloWorld.java file.

The first step is to meet the prerequisites mentioned above.

Then, download a dev build of GraalVM along with Native Image.

Finally, run the following commands:

$JAVA_HOME/bin/javac Helloworld.java
$JAVA_HOME/bin/native-image HelloWorld -H:CompilerBackend=llvm \
-Dsvm.targetPlatformArch=riscv64 -H:CAPCacheDir=/path/to/capcache \
-H:CCompilerPath=/path/to/riscv-gcc -H:CustomLD=/path/to/riscv-ld \
-H:CLibraryPath=/path/to/static-libraries \
--add-exports=jdk.internal.vm.ci/jdk.vm.ci.riscv64=org.graalvm.nativeimage.builder

Options Explanation

  • -Dsvm.targetPlatformArch=riscv64 enables you to specify the target architecture.
  • -H:CompilerBackend=llvm specifies that we use the LLVM backend.
  • -H:CAPCacheDir=/path/to/capcache provides the precomputed CAPCaches to Native Image.
  • -H:CCompilerPath=/path/to/riscv-gcc provides gcc from the RISC-V toolchain to Native Image.
  • -H:CustomLD=/path/to/riscv-ld provides ld from the RISC-V toolchain to Native Image.
  • -H:CLibraryPath=/path/to/static-libraries provides the static libraries to Native Image.

More information about the LLVM backend and other options can be found in the documentation.

Compute CAPCaches and Compile Static Libraries Manually

To obtain the CAPCaches or the static libraries manually, one solution is to build Native Image on RISC-V from source. This can be done using the following commands.

git clone https://github.com/oracle/graal.git
git clone https://github.com/graalvm/mx.git
export PATH=$PWD/mx:$PATH
cd graal/substratevm
mx fetch-jdk
# Choose one of the jdk version and run the provided export command
export SKIP_LIBRARIES=true
mx build

The static libraries will then be in graal/sdk/latest_graalvm_home/lib and graal/sdk/latest_graalvm_home/lib/svm/clibraries/linux-riscv64/.

To compute the CAPCaches, run the following command.

$JAVA_HOME/bin/native-image HelloWorld -H:CompilerBackend=llvm \
-H:+ExitAfterCAPCache -H:CAPCacheDir=/path/to/capcache -H:+NewCAPCache \
-add-exports=jdk.internal.vm.ci/jdk.vm.ci.riscv64=org.graalvm.nativeimage.builder

The CAPCaches will be stored in /path/to/capcache.

Build a RISC-V Binary Natively

Even though it is recommended to cross-build binaries, it is still possible to build them on a RISC-V machine. The procedure is very similar to cross-building. The toolchain is not needed anymore, as it will be the native one, and the CAPCaches and the static libraries will be provided automatically. The last difference is in the final command:

$JAVA_HOME/bin/native-image HelloWorld -H:CompilerBackend=llvm \
--add-exports=jdk.internal.vm.ci/jdk.vm.ci.riscv64=org.graalvm.nativeimage.builder

Implementation

To add the RISC-V target to Native Image, we had to implement all the changes described in the documentation. There are however some other unmentioned changes related to RISC-V.

JVMCI Implementation

The Graal compiler depends on the Java Virtual Machine Compiler Interface (JVMCI), meaning this Java subcomponent has to be implemented to use Native Image since it depends on the Graal compiler. Although the LLVM backend does not use the whole Graal compiler, it still uses its frontend and thus, needs a subset of JVMCI. The implementation of this subset for RISC-V can be found here.

Conclusion

The RISC-V LLVM backend supports almost all the features the other LLVM backends support!

There is still work to do, such as adding support for macOS and other operating systems. Some target-specific optimizations may be interesting to investigate since RISC-V provides some particular registers not all other architectures have. For example, there is the thread register (x4).

Considering that it took only around six months to add RISC-V support to Native Image using the LLVM backend, the idea to add support for other potential targets such as WebAssembly is appealing. In fact, adding other architectures will become even more accessible, as the LLVM backend will soon emit the Native Image data section itself, allowing it to skip adding information about object files.

We welcome your feedback! You can share it via Slack, GitHub, Twitter, or Mastodon.

--

--