Zig binary Size

Greetings,

I’m learning zig by following along with codecrafters challenge of writing a shell. I’m using zig 0.15.2, so far it’s pretty simple and can run programs in the path, change dirs and update the PWD and OLD_PWD envs. Nothing to sophisticated.

I notice that with debug mode the shell is 9.0Mb and stripped it’s 1.9Mb. This seems really big. I thought I would build it with –release=small and the binary size is the same. What does release small actually do?

In comparison I built a static bash with musl with the following options:
./configure CC=musl-gcc CFLAGS=“-std=gnu89” LDFLAGS=“-static”
ldd bash
not a dynamic executable
du -h bash
1.8M bash

A striped bash to compare apples to apples is 1.6M

As someone just learning I’m not expecting to just arrive at supper small programs. It just seems weird to me that all of bash statically built is still smaller than a simple shell that doesn’t yet do a whole lot.

Debug builds are quite big compared to other languages, since Zig compiles all of the std in debug mode as well, and even in the smallest executables this includes some debug utilities, like the dwarf parser to parse debug info.

As for release small, you may have fallen for a pitfall of Zig regarding to optimization flags:
If you are compiling without the build system (zig build-exe and zig run),
then the flag for is -OReleaseSmall, --release=small is only valid if you compile with zig build and have a build.zig file.

10 Likes

Just in general, Zig programs are going to be somewhat larger than C programs. It doesn’t tend to be massive, and it isn’t inherent to the language: if you write Zig as much like C as possible, the difference vanishes.

Zig specializes more code. For instance, when you use a format string, Zig will produce a custom function at compile-time, which is safer than printf and usually faster: but it’s more code. If you have two kinds of hashmap, those will also be specialized; ReleaseSmall will try to consolidate that kind of thing sometimes, but that’s difficult and not always possible. C would handle that manually with somewhat terrifying void pointer arithmetic, or, just use a linked list.

Zig is still a ‘no runtime’ language, it’s suitable for microprocessors, embedded, skinny libraries and so on. To get a binary size which is competitive with C is possible, but you’ll need to program with that in mind.

Just as a point of comparison, I recently ported Lemon to Zig, in order to turn it into a parser generator for Zig code. One of the artifacts of that is just Lemon in Zig, which exhibits identical behavior insofar as I was able to make it do so. So it’s a like-for-like comparison, same program in two languages.

On my computer, lemon.c at -O3 is 130KiB, lemon.zig at -D=ReleaseFast is 541KiB. The latter embeds the template, contributing 37KiB to the weight, and is about 5% faster (the embedded template only accounts for about 1% of that, interestingly). A parser generator has a lot of print statements in it, so this comparison is guaranteed to favor C over Zig when it comes to binary size:

$ rg \\.print src/lemon.zig | wc -l
     178

Most programs aren’t like this. But they will usually be somewhat larger, for similar reasons.

@IntegratedQuantum already covered why that is, I just wanted to provide a high-level overview of what you can expect. If you really want to match C byte-for-byte, you can, but you’ll need to work at it.

Forgot to add: 252KiB in ReleaseSmall. But of course the C code is smaller using -Os as well. I did try that, and remember the ratio being roughly the same, but didn’t write the number down.

5 Likes

14 posts were split to a new topic: The Limits of Devirtualization

The project was made with zig init. So I have a build.zig. It’s the default one that is made with it.

As for the size, It seems wired that the release small produced the same as a normal debug build

What is the difference between zig build and zig?

When building the program with zig build
du -h zig-out/bin/main
9.0M zig-out/bin/main

However when using zig build-exe src/main.zig
du -h main
7.4M main

Then building it with your options I got
zig build-exe src/main.zig -O ReleaseSmall -dead_strip -fstrip -fno-unwind-tables

du -h main
44K main

I wonder why there is such a drastic difference in size between the two? I noticed zig build-exe has way more options than zig build does. Which leaves me to wonder why I would even want a build.zig file in the first place.

Quite of few of the options are available to the build system, e.g

root_module = b.createModule(.{
            .unwind_tables = .none,
            .strip = true,
...
1 Like

Thank you. Now that I know these are there. I’ll just have to spend a day playing around with them and seeing what all I want to use.

As for the options I meant from a being able to pass build options to the zig build command as opposed to the zig build-exe. Though from your above answer, I’m sure if the options exist for zig build-exe I can add them into my build.zig and fine tune it to my liking.

I was wondering when reading this thread, is there an issue tracking the augmented binary sizes ? I searched on my phone on codeberg/github and didn’t find it.

I mean that the binary sizes grew bigger with the Io interface.