diff --git a/posts/riscv-compiler-explorer/index.html b/posts/riscv-compiler-explorer/index.html index d4c6ba3a..de029444 100644 --- a/posts/riscv-compiler-explorer/index.html +++ b/posts/riscv-compiler-explorer/index.html @@ -1,10 +1,10 @@ RISC-V Assembler: Compiler Explorer - Project F -
Project F

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!

Compiler Explorer without optimisation

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

Compiler Explorer with optimisation

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:

Compiler Explorer with optimisation

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. 🙏