Out of curiosity, I looked at a basic hello world in C, and compiled it naively using gcc on Ubuntu, by default the executable produced was 15960 bytes, but when adding the -Os option (from the man page: "-Os Optimize for size") to enable many optimisation flags including code size, I was able to reduce it to 15968.
#include <stdio.h>
int main() { printf("Hello, World!"); return 0;}
Of course, If I had read more than 3 words in the man page, the answer was easy to understand: "Os Optimize for size. -Os enables all -O2 optimizations except those that often increase code size", so you can't really get lower size than the default using only the optimization system, there's also "-finline-functions" included in -Os, but it won't help you in a Hello World.
This program is not same to the Rust version. A more faithful version, assuming glibc, would look like this:
#include <stdio.h>
#include <stdlib.h>
#include <execinfo.h>
#include <errno.h>
void print_backtrace(void) {
void *traces[50];
char **symbols;
int num_traces, i;
num_traces = backtrace(traces, sizeof(traces) / sizeof(*traces));
strings = backtrace_symbols(traces, num_traces);
if (!strings) return;
for (i = 0; i < num_traces; ++i) {
fprintf("%d: %s\n", i + 1, strings[i]);
}
free(strings);
}
int main(void) {
static const char FMT[] = "Hello, World!\n";
static int EXPECTED = (int) (sizeof(FMT) - 1);
int ret = printf(FMT);
if (ret < 0) {
fprintf(stderr, "printf failed: %s\n", strerror(errno));
print_backtrace();
return 1;
}
if (ret != EXPECTED) {
fprintf(stderr, "printf failed: only %d characters were written\n", ret);
print_backtrace();
return 1;
}
return 0;
}
While this is still substantially different (for example, Rust's I/O buffering is different from C), this should be enough to demonstrate that this comparison is very unfair.
Building your code (I fixed a few typos, strings -> symbols, fprintf(" -> fprintf(stderr, ") with
gcc -s -Os -fuse-ld=lld a.c && ls -al a.out
leads to a 5496 bytes ELF though. Which is not much larger than just printf("Hello World!\n"), see sibling comment.
I think the point is C "cheated" by including a lot of goodness (format, backtrace etc) in the shared library so they does not have to be copied to each binary.
Thank you for the actual testing (and sorry for typos...). Yes, a C version is small because it dynamically links to libc.so in this case, and I meant to compare against a statically linked version. I primarily wrote this example as a response to the claim that an equivalent---it isn't---C code with musl is very small.
$ gcc -Os a.c && ls -al a.out && size
-rwxr-xr-x 1 user user 15952 Jan 24 17:49 a.out
text data bss dec hex filename
1316 584 8 1908 774 a.out
$ gcc -s -Os a.c && ls -al a.out && size
-rwxr-xr-x 1 user user 14472 Jan 24 17:50 a.out
text data bss dec hex filename
1316 584 8 1908 774 a.out
$ gcc -s -Os -fuse-ld=lld a.c && ls -al a.out && size
-rwxr-xr-x 1 user user 4552 Jan 24 17:50 a.out
text data bss dec hex filename
1199 528 1 1728 6c0 a.out
zig cc -Os -target x86_64-linux-musl hello.c -o hello
...which basically calls Clang under the hood, but comes with out-of-the-box cross-compilation support for Linux and MUSL creates a 5136 bytes executable.