Rust has garnered a large following among software developers thanks to its guarantees of memory safety without sacrificing performance. Once a niche language for hobby developers, Rust is now widely used in your day-to-day systems like Windows and Android.
Rust compilers are well known for optimizing code performance and memory management via borrow checkers. Rust code is compiled using rustc
, the language’s official compiler.
rustc
uses LLVM in the backend to optimize and translate high-level Rust code to low-level machine code. Recently, however, there has been an emergence of an alternative GCC frontend for the rustc
compiler called gccrs
.
In this article, we will explore the evolving landscape of Rust compilation, focusing on two native compiler projects: LLVM and GCC.
LLVM is a collection of compiler infrastructure made up of reusable compiler and toolchain components. LLVM is technically an acronym for Low-Level Virtual Machine, but over time, the acronym itself has become the project’s brand. LLVM is known for its ability to optimize code and generate high-performance machine code across various programming languages.
A standard compiler infrastructure can be divided into the frontend, middle end, and backend. The frontend will function as a translation layer between the high-level programming language with an intermediate representation, and this will be similar across different compilers, including both LLVM and GCC.
The middle end applies various optimizations to the code, such as loop unrolling and function inlining. LLVM IR is the intermediate representation of LLVM, and it can be directly optimized to compile multiple different backends depending on the targeted architecture.
LLVM has been the default backend for the Rust compiler, rustc
, since its inception; rustc
is essentially an LLVM frontend. This partnership between Rust and LLVM has proven successful, as LLVM’s advanced optimization techniques enhance the performance of Rust programs and allow them to run on multiple platforms.
GCC, which stands for GNU Compiler Collection, is an open source compiler collection that supports various programming languages such as C, C++, Fortran, and others. It is widely known for its stability, reliability, and extensive support for different architectures and operating systems.
Born out of the need for a free software ecosystem, the programming world owes a great deal to GCC’s contributions in enabling code compilation for various platforms. On top of the languages listed above, GCC has grown to support many others, including Ada, Java, Go, and, more recently (and still in development), Rust.
There are multiple frontends to support various languages, and each will translate a programming language to an abstract syntax tree (AST). An AST is the intermediary between the frontend and the middle end, which is the intermediary representation.
While LLVM has IR as its intermediary representation, GCC has GIMPLE and RTL. GIMPLE is the high-level intermediary representation processed by the GCC middle end. GIMPLE provides a simplified representation of the program that retains high-level semantics and simplifies optimization tasks.
Then, after the GIMPLE representation, the code is further transformed into RTL. This low-level representation closely resembles assembly language instructions, with further optimizations before it is used for generating machine code.
There is ongoing work to develop a GCC frontend for Rust named gccrs
. This project is not yet stable and has yet to be officially integrated into GCC.
As a compiler collection, GCC has a different approach to compilation compared to LLVM. GCC takes a more traditional approach, using a frontend to parse the source code and generate an AST.
This AST is then transformed into a high-level intermediary representation called GIMPLE, which retains the high-level semantics of the program. Unlike LLVM, GCC adds an intermediary representation, RTL:
Both optimizations are targeted differently. While GIMPLE focuses on high-level optimizations, RTL focuses on low-level optimizations and transformation into assembly-like instructions.
LLVM takes directly from the frontend to its intermediate representation, LLVM IR. The LLVM IR optimizations are language-agnostic and architecture-independent. This allows LLVM to perform various optimizations that can benefit different programming languages and target architectures:
The most staggering difference between GCC and LLVM, however, is how they structure their source code. LLVM is modular; from the beginning, it was built to be extensible and to be used by multiple languages, targeting a wide array of backend machines.
On the other hand, GCC is designed as a monolithic compiler with tightly coupled components. It is possible to create extensions for GCC, but most of its code is tightly knit, and you would need to download the entire GCC codebase to make changes or additions.
The Rust programming language primarily uses LLVM as its default compiler infrastructure. As mentioned above, rustc
is a frontend that uses LLVM as its backend. This means that Rust code is compiled by default using LLVM’s optimizations and transformations to generate machine code.
If you want to set up LLVM for Rust, you can follow the Rust installation instructions. However, to use GCC for Rust, you will need to use gccrs
. gccrs
is an alternative frontend for the Rust compiler that leverages a native GCC backend.
To install the gccrs
package, open the gccrs
repository and follow the installation instructions provided there. The instructions are based on Debian Linux, so you must adjust the instructions based on which distro you are using:
sudo apt install build-essential libgmp3-dev libmpfr-dev libmpc-dev flex bison autogen gcc-multilib dejagnu
Then, you can clone the gccrs
repository:
git clone https://github.com/Rust-GCC/gccrs
Finally, build the toolchain to make a new directory adjacent to the gccrs
repository, and run make
to compile the build there:
mkdir gccrs-build cd gccrs-build ../gccrs/configure — prefix=$HOME/gccrs-install — disable-bootstrap — enable-multilib — enable-languages=rust make
Once the installation process is complete, you will have the gccrs
binary ready to use as a frontend for compiling Rust code using the GCC backend.
gccrs
is still in its early development stage, so it cannot support most Rust syntax, especially compared to rustc
with LLVM. For example, at the time of writing this article, gccrs
doesn’t support Rust macros. So it’s tough to closely compare Rust between GCC and LLVM.
To start compiling Rust code with GCC, you can find the readily available test code in the gccrs
repository provided by the gccrs
project’s test suite. Let’s take one example: a file named type_infer1.rs
:
struct Foo { one: i32, // { dg-warning "field is never read" "" { target *-*-* } .-1 } two: i32, // { dg-warning "field is never read" "" { target *-*-* } .-1 } } fn test(x: i32) -> i32 { return x + 1; } fn main() { let logical: bool = true; // { dg-warning "unused name" "" { target *-*-* } .-1 } let an_integer = 5; let mut default_integer = 7; default_integer = 1 + an_integer; let call_test = test(1); // { dg-warning "unused name" "" { target *-*-* } .-1 } let struct_test = Foo { one: 1, two: 2 }; // { dg-warning "unused name" "" { target *-*-* } .-1 } }
This file has code that declares a struct, defines functions, and demonstrates variable usage in Rust. It’s a simple type inference example where a variable containing an integer isn’t explicitly typed, as Rust can infer the type based on its usage.
To compile this Rust code using LLVM, you can use the rustc
compiler:
rustc ./gccrs/gcc/testsuite/rust/compile/torture/type_infer1.rs type_infer1.rs
It’s pretty straightforward to run it using rustc
. However, if you want to compile the same Rust code using GCC as a frontend, you must pass many more flags:
For example, you would use the binary to compile this Rust code using GCC. Go to the gccrs-build
folder created in the previous section and run the following command:
./gcc/crab1 ../gccrs/gcc/testsuite/rust/compile/torture/type_infer1.rs -frust-debug -Warray-bounds -dumpbase ../gccrs/gcc/testsuite/rust/compile/torture/type_infer1.rs -mtune=generic -march=x86–64 -O0 -version -fdump-tree-gimple -o test -L/lib/x86_64-linux-gnu -L/lib/../lib64 -L/usr/lib/x86_64-linux-gnu -L/usr/lib/../lib64 -frust-incomplete-and-experimental-compiler-do-not-use
This command will build a binary that you can use to execute the Rust code. It should output something like this:
Analyzing compilation unit Performing interprocedural optimizations <*free_lang_data> {heap 1432k} <visibility> {heap 1432k} <build_ssa_passes> {heap 1432k} <opt_local_passes> {heap 1704k} <remove_symbols> {heap 1704k} <targetclone> {heap 1704k} <free-fnsummary> {heap 1704k}Streaming LTO <whole-program> {heap 1704k} <fnsummary> {heap 1704k} <inline> {heap 1704k} <modref> {heap 1704k} <free-fnsummary> {heap 1704k} <single-use> {heap 1704k} <comdats> {heap 1704k}Assembling functions: <simdclone> {heap 1704k} type_infer1::test type_infer1::main Time variable usr sys wall GGC phase parsing : 0.00 ( 0%) 0.00 ( 0%) 0.01 ( 50%) 82k ( 24%) phase opt and generate : 0.00 ( 0%) 0.01 (100%) 0.01 ( 50%) 121k ( 35%) trivially dead code : 0.00 ( 0%) 0.00 ( 0%) 0.01 ( 50%) 0 ( 0%) parser (global) : 0.00 ( 0%) 0.00 ( 0%) 0.01 ( 50%) 82k ( 24%) initialize rtl : 0.00 ( 0%) 0.01 (100%) 0.00 ( 0%) 12k ( 4%) TOTAL : 0.00 0.01 0.02 343k Extra diagnostic checks enabled; compiler may run slowly. Configure with - enable-checking=release to disable checks.
Be mindful that gccrs
is still in its very early development phases; you cannot import external packages or libraries yet, and it only partially supports all features of the Rust language.
Compiling Rust code with GCC vs. LLVM can lead to different outcomes in terms of performance and optimization. Both approaches have their distinct advantages. In the case of GCC, it can target a variety of architectures and has been around for a longer time, making it more mature and stable in certain areas. It has a mature codebase that has been optimized over several decades.
Two projects are working on making Rust GCC-compatible. The first, of course, is gccrs
, and the second is rustc_codegen_gcc
.
The difference between the two is that rustc_codegen_gcc
uses the rustc
frontend to generate the intermediary representation for a GCC backend. It’s more stable compared to gccrs
and can provide you with a better compilation experience with Rust code using GCC.
gccrs
matters for the Rust communityIt’s unfair to make a broad comparison of rustc
with the current state of gccrs
.
For one thing, it’s in the very early stages of a community-led effort, and rustc
has the Rust Foundation behind it — rustc
is the main Rust compiler, after all. But having a community-led compiler adds more diversity to the Rust ecosystem, helping to make Rust more versatile in multiple ecosystems.
Having gccrs
also helps foster more community innovation. Rust is already a high-performing language in most target platforms, but using GCC’s architecture may lead to optimizations that will be more efficient for niche use cases.
GCC is old and relatively stable, and it targets more legacy systems that are incompatible with LLVM — for example, the Motorola 68000 (m68k), a legacy microprocessor commonly used throughout the 1980s and 1990s. Many such niches are easily accessible through GCC, whereas it’s unreasonable for LLVM to support them given how outdated the technology is.
Today we’ve explored the evolving landscape of Rust compilation, focusing on two projects: LLVM and GCC. Both projects have unique benefits, design philosophies, and goals in their approach to Rust compilation. However, it should be noted that the gccrs
project is still in its early stages of development and does not fully support all features of the Rust language.
Take a look at the following resources if you’d like to learn more about the subject:
rustc
compiler overviewgccrs
homepage
Debugging Rust applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking the performance of your Rust apps, automatically surfacing errors, and tracking slow network requests and load time, try LogRocket.
LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your Rust application. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app’s performance, reporting metrics like client CPU load, client memory usage, and more.
Modernize how you debug your Rust apps — start monitoring for free.
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowMatcha, a famous green tea, is known for its stress-reducing benefits. I wouldn’t claim that this tea necessarily inspired the […]
Backdrop and background have similar meanings, as they both refer to the area behind something. The main difference is that […]
AI tools like IBM API Connect and Postbot can streamline writing and executing API tests and guard against AI hallucinations or other complications.
Explore DOM manipulation patterns in JavaScript, such as choosing the right querySelector, caching elements, improving event handling, and more.