Coffee Space


Listen:

Compiler Comparison

Preview Image

I was recently experimenting with tcc for a project, and wanted to benchmark just how good it really is when considering other options. I of course though about gcc, but later found clang to be something also worth testing. I think discovered fil-c as a supposed memory safe version of C, which was also worth a consideration.

For tcc I am using “tinycc”, an unofficial mirror that appears to be actively maintained.

For fil-c I am using the pre-compiled v0.673, specifically a pre-compiled version:

filc-0.673-linux-x86_64.tar.xz is the traditional, self-contained “pizfix” distribution. It only includes the compiler, doesn’t require root, and is based on the musl libc. This is recommended for quickly trying out Fil-C.

As this version of fil-c is complied with musl libc, this isn’t quite apples-to-apples, but it is good enough to give us an idea.

Each test is run 10 times against a set of 9 compiler flags:

0001 FLAGS = [
0002   "",       # No optimisation
0003   "-O1",    # Optimize
0004   "-O2",    # Optimize even more
0005   "-O3",    # Optimize yet more
0006   "-O0",    # Reduce compilation time and make debugging produce the expected results
0007   "-Os",    # Optimize for size
0008   "-Ofast", # Disregard strict standards compliance
0009   "-Og",    # Optimize while keeping in mind debugging experience
0010   "-Oz",    # Optimize aggressively for size rather than speed
0011 ]

For 10 runs, 9 flags and 4 compilers, we see 10×9×4=36010 \times 9 \times 4 = 360 tests per graph. With 18 graphs that’s 6,480 tests. It takes a while.

Compiling TCC

For the first test, we compile TCC and compare the final binary size and how long it took to compile. The TCC binary was chosen because it’s a relatively large project and it is written in compatible C.

Compilation size comparison for tcc

clang, gcc and tcc produce similar binary sizes, but fil-c produces are greatly larger binary.

Compilation time comparison for tcc

Similarly, fil-c takes significantly longer to compile, whilst tcc enjoys a reduction in compilation time.

fil-c is likely to be bloated due to the memory safety features, so it really depends on how much you value those. For roaring performance, it does seem to not do as well.

Compiling u-database

For the next test we compile and execute the performance test for u-database, a flat-binary database key-value store written in C. It will happily read and write hundreds of thousands of records a second (sub microsecond per transaction). In this high-performance application, small changes in compiler and flags can be felt.

Compilation size comparison for u-database

When compiling my u-database library, there appears to be no real binary size change for each compiler.

Compilation time comparison for u-database

The optimisation flags do appear to hit gcc and fil-c strangely enough, otherwise there is an unexpected uniformity across the software.

Execution time comparison for u-database

With the execution time, it for some reason appears that the longer it took to compile, the longer it also takes to run.

Compiling smollibs

For the next test we compile a series of single-file headers from a project called “smollibs”. Each of them is a tiny C99 library with a basic example test program.

For the next graphs, we will evaluate them together…

Compilation size comparison for smollibs git
Compilation size comparison for smollibs json
Compilation size comparison for smollibs ppm
Compilation size comparison for smollibs prop
Compilation size comparison for smollibs serv

There appears to be a “striping” affect on the graphs, particularly around the -Ofast flag. Looking at the GNU explanation for the flags:

-Ofast

Disregard strict standards compliance. -Ofast enables all -O3 optimizations. It also enables optimizations that are not valid for all standard-compliant programs. It turns on -ffast-math, -fallow-store-data-races and the Fortran-specific -fstack-arrays, unless -fmax-stack-var-size is specified, and -fno-protect-parens. It turns off -fsemantic-interposition.

It doesn’t appear to specifically be the -O3 flag as we also test this, so it’s safe to assume that one of the other flags causes the binary size to increase.

Compilation time comparison for smollibs git
Compilation time comparison for smollibs json
Compilation time comparison for smollibs ppm
Compilation time comparison for smollibs prop
Compilation time comparison for smollibs serv

Rather than making a case for a specific compiler, these tests seem to be making a case for testing which compiler specifically works for your use-case!

Compiling rclc

For the last comparison, I wanted to see how well each compiler performed when compiling rclc, the C-based wrapper for ROS2. The first issue is that there is not an easy-to-point-to file that represents the output of the compilation process, colcon hides files away and I kind of ran out of time.

Compilation time comparison for rclc

clang and gcc gave relatively predictable compile times over all of the test flags, but tcc failed to compile the project unfortunately. I believe it is because it is a C99 based compiler, whereas rclc uses more modern C features. fil-c also fails to compile, but I didn’t have time to try and set it up correctly.

Conclusion

Should you switch to tcc as your daily driver C compiler? Probably not, but it’s not as terrible an option as you may have original considered! But it will produce valid C code for many projects, it is mostly faster to compile, and the execution time is comparable. For speed or memory restricted applications, you may very well consider tcc as a valid option!

In the future it would be good to get rclc working correctly, and to compare libc implementations. Additionally it is important to consider shared vs static library building - I suspect that we see a move towards static libraries as disk size is cheap, RAM is plentiful, and the cost of sharing memory across increasing numbers of cores is more heavily felt.