Why are Odin's binaries so large and compilation times so slow?

Hello,
this is my first post here.

I’m doing Advent of Code in Odin to familiarize myself with the language a little bit. So far I’m very impressed with the design of the language and the expansive core library, but I’ve noticed that for very simple programs, no more than 150 LoC, all the binaries are ~180 kB in size and the compilation times are ~5 s (90% of which is LLVM API Code Gen). Additionally, it doesn’t seem to matter whether I compile with -o:speed or -o:size, the binary size isn’t affected at all.
Last year I was doing AoC in 4 different languages for which the binaries ranged from 919 B to 359 kB.

This is not a critique (mostly), I would just like to know the background of these things. Is it all due to LLVM, or maybe partially Odin’s runtime or other things I hadn’t considered?

p. s. Keep in mind that I’m on Linux. I know that Odin was started on Windows and probably still has the largest user-base on that platform but I don’t have a way to try it on there to see what the situation is like.

1 Like

Binary size-wise, Odin’s run-time code will have a base cost. The standard library is generally pruned in the sense that code that isn’t used isn’t included, but even with an “empty” program there will be a fair amount of code to set up the context (allocators, RNG, etc.). core:fmt is also a relatively large addition, because that entails run-time type information for all types that are used in the program (not just the types that are printed). This is generally a fixed-cost. That’s in comparison to C programs and other languages that use libc, where its run-time is common enough that it’s off-loaded to a shared library on most systems, so the executable itself is small.

Compilation time-wise, LLVM is known to be quite slow for optimized builds. If you use the default optimization (which is equivalent to -o:minimal), compilation should be quite quick in comparison–generally, that should be your go-to for iterating during development. Hold off on optimized builds until you’re ready for it (e.g. to share, or to benchmark).

2 Likes

Anything which leverages RTTI (run-time type information) in Odin will result in a much larger binary. In core:flags, I tried to keep this minimal, but it was largely unavoidable due to how the package works. It’s unfortunate, but thankfully it doesn’t incur any program-wide operation cost beyond that.

You said you’re on Linux, so you should have upx available in some fashion. With a trivial example of an Odin program that just uses one call to fmt.println, I can compress the resulting binary (compiled with -o:size) down to 36% of its original size. (161KiB to 59KiB)

I’ve wondered if there might be demand for a “light” alternative to fmt that’s merely a layer over the system-specific calls for printing without all the RTTI/formatting. Such an API would necessarily be unable to print anything but the standard types and all calls would be single-argument.

import "core:printer"

printer.append("Hellope ")
printer.append(42)
printer.append('\n')

For anything more complex such as structs, arrays, or maps, you’d have to build your own printer on top of the base calls.

Chris Wellons has a good article on this topic of print-is-append: Let's implement buffered, formatted output

Finally, I have also wondered if a non-LLVM Odin backend would be able to sidestep a lot of the binary size increase of RTTI through some custom and specific manner, but I don’t know enough about compilers at this time to say whether or not this is possible.


EDIT: As a follow-up experiment as to how much smaller a fmt-free program can be, I wrote this:

package main

import "core:sys/linux"

main :: proc () {
	str := "Hellope world!\n"
	linux.write(linux.STDOUT_FILENO, transmute([]u8)str[:])

}

Compiling with -o:size, I get a binary that is 21KiB. Using upx reduces this down to 12KiB.

But we can go further. Let’s compile with -o:size -no-crt -default-to-nil-allocator -no-bounds-check. No dependencies on libc, hence no default allocator because it uses malloc. We won’t need dynamic allocation anyway.

The binary I compile is now 9.9KiB. Using upx --best on this results in a file that is 5.5KiB large on my system.

9.9KiB is still a ton of space just to print a single static string, but it’s far smaller than what we started with.

2 Likes

Regarding the executable sizes of other languages, “919 B” is extremely small for any modern system. This would have to be a handwritten assembler, forth, etc or something akin to that. Or that “919 B” thing something else entirely?

On Windows, the smallest binary that printed a string I’ve been able to produce for C was 2 KiB and for Odin was 3.5 KiB.

As for executable size, unless you are targeting an embedded system where ROM is tiny, I honestly do not see why people worry about something that is orders of magnitude smaller than a basic image (JPEG/PNG)—or even when loads of applications nowadays are literal web browsers.

I know people might be worried about it because they don’t see it in other languages, but as others have stated, it’s because Odin statically links against pretty much everything. And the static linking just makes it clear in the first place rather hiding it in external dynamic libraries.

I am not going to explain the argument for static linking here but there are numerous reasons to prefer it, and the cases where dynamic linking are warranted is less than most people realize.

7 Likes

919 B was written in FASM, yes.

I’m not worried about the ~200 kB size being too large, I just noticed all the binaries being very similar in size and was curious. And I agree static linking makes sense in most cases.
Thank you for your answer.

If you’re not passing -use-separate-modules for debug builds, try it and you should see a nice improvement.

That flag has been the default now for quite a while. It was made default when we could prove it was stable enough to use by anyone.

(it’s only the default on windows)

Maybe we should make the default on Linux and Darwin too?

2 Likes