The Godbolt Compiler Explorer is an amazing tool for assembler programmers. In this post, I show you how to use Compiler Explorer to generate RISC-V assembly code and offer some advice to help you get the right result.
In the last few years, we’ve seen an explosion of RISC-V CPU designs on FPGA and ASIC, including the RP2350 found on the Raspberry Pi Pico 2. Thankfully, RISC-V is ideal for assembly programming with its compact, easy-to-learn instruction set. This series will help you learn and understand 32-bit RISC-V instructions and programming.
RISC-V Assembler: Arithmetic | Logical | Shift | Load and Store | Branch and Set | Jump and Function | Multiply and Divide | Assembler Cheat Sheet
DRAFT POST - not yet public
Getting Started with Compiler Explorer
The Godbolt Compiler Explorer (godbolt.org) lets you see the results of compiling C, C++, Rust and other high-level languages in your browser. Change your code and see the assembled code update immediately. This is invaluable when experimenting and learning, and Compiler Explorer even shows you which instructions correspond to which parts of your high-level code.
Compiler Explorer supports an impressive collections of instruction sets: 6502, aarch64, amd64 (inc. i386), arm32, avr, c6x, ebpf, kvx, loongarch, m68k, mips, mrisc32, msp430, powerpc, riscv32, riscv64, s390x, sh (SuperH), sparc, vax, wasm32, and xtensa!
I’m going focus on 32-bit RISC-V, but much of this advice applies to other architectures too.
Choosing a Compiler
For 32-bit RISC-V, you have a choice of GCC or Clang in many versions. While I use GNU assembler (gas) to assemble my RISC-V designs, I’ve found Clang often produces more readable asm from high-level languages.
If you’re unsure what to choose, I recommend the latest (non-trunk) version of Clang for readability or GCC if you want your code to match your GCC toolchain. I use both.
Add your chosen compilers to favourites, otherwise you’re going to be doing a lot of scrolling in the dropdown. You do this by clicking on the compiler drop down and selecting the stars next to your favourite compilers.
Optimisation
The best way to experiment with simple designs is by writing a function.
By default, generated code is unoptimised. This is probably not what you want because it adds a stack frame to your functions, making it hard to see what’s happening.
Consider this trivial C function that squares a number:
int square(int num) {
+
The Godbolt Compiler Explorer is an amazing tool for assembler programmers. In this post, I show you how to use Compiler Explorer to generate RISC-V assembly code and offer some advice and ideas to make best use of this tool.
In the last few years, we’ve seen an explosion of RISC-V CPU designs on FPGA and ASIC, including the RP2350 found on the Raspberry Pi Pico 2. Thankfully, RISC-V is ideal for assembly programming with its compact, easy-to-learn instruction set. This series will help you learn and understand 32-bit RISC-V instructions and programming.
RISC-V Assembler: Arithmetic | Logical | Shift | Load and Store | Branch and Set | Jump and Function | Multiply and Divide | Compiler Explorer | Assembler Cheat Sheet
This is a draft post. More content to follow.
Getting Started with Compiler Explorer
The Godbolt Compiler Explorer lives at godbolt.org.
Compiler Explorer lets you see the results of compiling C, C++, Rust and other high-level languages in your browser. Change your high-level code and see the assembled code update immediately. This is invaluable when experimenting and learning, and Compiler Explorer even shows you which assembly instructions correspond to which parts of your high-level code.
Add screenshot labelling important parts of the interface.
Compiler Explorer supports an impressive collections of instruction sets: 6502, aarch64, amd64 (inc. i386), arm32, avr, c6x, ebpf, kvx, loongarch, m68k, mips, mrisc32, msp430, powerpc, riscv32, riscv64, s390x, sh (SuperH), sparc, vax, wasm32, and xtensa!
I’m going focus on C and 32-bit RISC-V, but much of this advice applies to other languages and architectures.
Choosing a Compiler
For 32-bit RISC-V, you can choose GCC or Clang in many versions. While I normally use GNU assembler (gas) to assemble my RISC-V designs, I’ve found Clang often generates more readable asm.
If you’re unsure what to choose, I recommend the latest (non-trunk) version of Clang for readability or GCC if you want your code to match your GCC toolchain. I use both.
Add your chosen compilers to favourites, otherwise you’re going to be doing a lot of scrolling! You do this by clicking on the compiler drop down and selecting the stars next to your chosen compilers.
Functions and Optimisation
The best way to experiment with simple designs is to write a function. That way, the inputs and outputs are clear, and you can plainly see what’s happening.
By default, generated code is unoptimised. This is probably not what you want because it adds a stack frame to your functions, making it harder to see what your algorithm is doing.
Consider this trivial C function that squares a number:
int square(int num) {
return num * num;
}
In Clang 18.1, without optimisation, you get 11 instructions!

If we add -O to the compiler options (top right of window), we get more useful code:

It makes sense if you know that sp is the stack pointer, ra is the return address, and s0 is the frame pointer. However, unless you’re learning about functions, these instructions are just getting in the way.
If we add -O to the compiler options (top right of window), we get more readable code:

C Types
Types in C are broadly architecture-dependent, and C sets a low bar for acceptable implementations. For example, int is signed and must be capable of the range −32767 to +32767.
If you’re doing anything vaguely numerical, you want to be precise with your types using stdint.h.
For example, compare 32-bit and 64-bit addition on RV32 (32-bit RISC-V):
#include <stdint.h>
+ret">Squaring a number needs just one multiply instruction.
C Types
Types in C are broadly architecture-dependent, and C sets a low bar for acceptable implementations. For example, int is signed and must be capable of the range −32767 to +32767.
If you’re doing anything vaguely numerical, you want to be precise with your types using stdint.h.
Types you might want to use include:
- signed:
int8_t, int16_t, int32_t, int64_t
- unsigned:
uint8_t, uint16_t, uint32_t, uint64_t
For example, compare 32-bit and 64-bit addition on RV32 (32-bit RISC-V):
#include <stdint.h>
int32_t add32(int32_t a, int32_t b) {
return a + b;
}
@@ -52,7 +52,7 @@
mul a2, a1, a0
mulh a1, a1, a0
mv a0, a2
-ret">Comparing Architectures
Compiler Explorer is a great way to compare and contrast architectures. For example, RISC-V handles condition codes…
You can pass all the usual compiler options to select specific architectures. Refer to your chosen compiler documentation for details. e.g. with GCC we can generate 486 code with -m32 -march=i486
.
Tips
Another handy option is -fno-inline
to avoid your functions being inlined at higher optimisation levels.
Optimisation levels compare performance and space…
Ideas: watch out for library calls (e.g. 68000 32-bit multiply).
Compare my handwritten version of a function with the CE version. Why is the CE version better (compressed instructions).
What’s Next?
Check out the RISC-V Assembler Cheat Sheet and all my FPGA & RISC-V Tutorials.
Share your thoughts with me on Mastodon or X. If you enjoy my work, please sponsor me. Sponsors help me create new projects for everyone, and they get early access to blog posts and source code. 🙏