127

I complied a hello world Go program which generated native executable on my linux machine. But I was surprised to see the size of the simple Hello world Go program, it was 1.9MB !

Why is it that the executable of such a simple program in Go is so huge?

Karthic Rao
  • 3,624
  • 8
  • 30
  • 44
  • 30
    Well , im from C/C++ background ! – Karthic Rao Feb 19 '15 at 09:20
  • I just tried this scala-native hello world: http://www.scala-native.org/en/latest/user/sbt.html#minimal-sbt-project It took quite some time to compile, downloading a lot of stuff, and the binary is 3.9MB. – bli Nov 17 '17 at 13:45
  • I have updated [my answer below](https://stackoverflow.com/a/28577424/6309) with 2019 findings. – VonC Apr 02 '19 at 07:13
  • 2
    Simple Hello World app in C# .NET Core 3.1 with `dotnet publish -r win-x64 -p:publishsinglefile=true -p:publishreadytorun=true -p:publishtrimmed=true` generates a binary file about ~26MB! – Jalal May 15 '20 at 19:29
  • @Jalal Why ? This is a huge difference. Do you know why .NET Core executable are that huge. – Zack ISSOIR May 21 '20 at 18:34
  • @ZackISSOIR This is because "dotnet publish -r win-x64" is a self contained deployment, meaning is going to deploy the entire runtime with your code. If you add "self-contained false" then it wil be framework dependent and will deploy only your code. A console hello world in this case will be only 124k. – Jr_ Sep 11 '20 at 14:45
  • This is comparing apples to doughnuts, but I seem to remember `/bin/yes` being 186 bytes on a BSDi (BSD/OS) machine, and it was indeed a compiled file. I had to check it out because I thought it was a mistake. Again, apples to doughnuts. – Erik Bennett Oct 15 '22 at 20:56
  • @Jalal The improvements are done, .NET 8 "Hello world" is only 1.2 MB with NativeAOT compilation, which is even less than the Golang 1.9 MB version. The battle can be tracked at https://github.com/MichalStrehovsky/sizegame – Artur A Jun 14 '23 at 10:29

3 Answers3

114

This exact question appears in the official FAQ: Why is my trivial program such a large binary?

Quoting the answer:

The linkers in the gc tool chain (5l, 6l, and 8l) do static linking. All Go binaries therefore include the Go run-time, along with the run-time type information necessary to support dynamic type checks, reflection, and even panic-time stack traces.

A simple C "hello, world" program compiled and linked statically using gcc on Linux is around 750 kB, including an implementation of printf. An equivalent Go program using fmt.Printf is around 1.9 MB, but that includes more powerful run-time support and type information.

So the native executable of your Hello World is 1.9 MB because it contains a runtime which provides garbage collection, reflection and many other features (which your program might not really use, but it's there). And the implementation of the fmt package which you used to print the "Hello World" text (plus its dependencies).

Now try the following: add another fmt.Println("Hello World! Again") line to your program and compile it again. The result will not be 2x 1.9MB, but still just 1.9 MB! Yes, because all the used libraries (fmt and its dependencies) and the runtime are already added to the executable (and so just a few more bytes will be added to print the 2nd text which you just added).

Community
  • 1
  • 1
icza
  • 389,944
  • 63
  • 907
  • 827
  • 22
    A C "hello world" program, statically linked with glibc is 750K because glibc is expressly not designed for static linking and is even impossible to properly static link in some cases. A "hello world" program statically linked with musl libc is 14K. – Craig Barnes Mar 21 '19 at 22:34
  • I'm still looking, however, it would be nice to know what is linked in so that just maybe an attacker is not linking in evil code. – Richard Aug 02 '20 at 13:29
  • So why isn't the Go runtime library in a DLL file, so that it can be shared among all Go exe files? Then a "hello world" program could be a few KB, as expected, instead of 2 MB. Having the entire runtime library in every program is a fatal flaw for the otherwise wonderful alternative to MSVC on Windows. – David Spector Aug 23 '20 at 22:34
  • I'd better anticipate an objection to my comment: that Go is "statically linked". Okay, no DLLs then. But static linking doesn't mean you need to link in (bind) an entire library, only the functions that are actually used in the library! – David Spector Aug 23 '20 at 22:35
  • 2
    @DavidSpector The Go runtime is a complex piece of machinery: memory allocator, GC, scheduler... You cannot just "shake that tree". – jub0bs Jun 17 '21 at 13:32
  • jub0bs. A "hello world" does not use a memory allocator, garbage collector, or a process scheduler. It should just compile to the machine code to display "hello world" using a simple library routine that puts the output on the screen a modal dialog box, or wherever is most useful in the Go environment. Static linking does not mean you have to link in every possible library function! – David Spector Jun 17 '21 at 18:08
  • 2
    @DavidSpector Linking it statically keeps you from having issues with the runtime version changing and breaking your program. Also it makes it much easier to cross-compile, since you don't have to have all the system libraries installed to get your go program compiled on a different architecture. These are of course trade-offs, but it's entirely worth it if you consider how much 1.9MB cost as hard-disk space or even to transfer over the internet, which is not much. – CodeMonkey Oct 07 '21 at 08:16
  • 2
    @DavidSpector Yes, a "hello world" may not use the "fancy" features added to the binary, but a "hello world" often isn't the app you're willing to write. So to optimize the unused runtime features from a "hello world" is simply not worth the effort. Any decent app will use (directly or indirectly) reflection, gc, scheduler etc. – icza Oct 07 '21 at 14:01
  • That means that a Go program does not depends on any libc because all dependencies is static linked? – Ivan Montilla Sep 05 '22 at 19:34
  • 1
    @IvanMontilla Yes, that's right. Although it's possible to load libraries dynamically. – icza Sep 05 '22 at 19:38
63

Consider the following program:

package main

import "fmt"

func main() {
    fmt.Println("Hello World!")
}

If I build this on my Linux AMD64 machine (Go 1.9), like this:

$ go build
$ ls -la helloworld
-rwxr-xr-x 1 janf group 2029206 Sep 11 16:58 helloworld

I get a a binary that is about 2 Mb in size.

The reason for this (which has been explained in other answers) is that we are using the "fmt" package which is quite large, but the binary has also not been stripped and this means that the symbol table is still there. If we instead instruct the compiler to strip the binary, it will become much smaller:

$ go build -ldflags "-s -w"
$ ls -la helloworld
-rwxr-xr-x 1 janf group 1323616 Sep 11 17:01 helloworld

However, if we rewrite the program to use the builtin function print, instead of fmt.Println, like this:

package main

func main() {
    print("Hello World!\n")
}

And then compile it:

$ go build -ldflags "-s -w"
$ ls -la helloworld
-rwxr-xr-x 1 janf group 714176 Sep 11 17:06 helloworld

We end up with an even smaller binary. This is as small as we can get it without resorting to tricks like UPX-packing, so the overhead of the Go-runtime is roughly 700 Kb.

Joppe
  • 1,465
  • 1
  • 12
  • 17
  • 5
    UPX compresses binaries and decompresses them on-the-fly when they are executed. I wouldn't dismiss it a trick without explaining what it does, since it can be useful in some scenarios. Binary size is reduced somewhat at the expense of startup time and RAM usage; moreover, performance can be slightly affected as well. Just as an example, an executable could be shrinked to 30% of its (stripped) size and take 35ms longer to run. – simlev Nov 26 '19 at 10:20
  • I compiled my project with go build -ldflags "-s -w" and it just reduced the binary from 6.7MB to 5.2MB – Samir Kape Jul 07 '21 at 04:18
  • so IMO along with the symbols there might be something else contributing to a problem. – Samir Kape Jul 07 '21 at 04:28
  • The binaries produced by the Go compiler does not only contain code. They also contain type information (for reflection) and a block that maps addresses to function names (in order to produce readable stack traces, e.g. when panicking). This information is needed by the Go runtime and cannot be stripped from the binary, and it has nothing to do with debug symbols. That is the main reason why Go binaries are much larger than C binaries. – Joppe Jul 08 '21 at 10:16
17

Note that the binary size issue is tracked by issue 6853 in the golang/go project.

For instance, commit a26c01a (for Go 1.4) cut hello world by 70kB:

because we don't write those names into the symbol table.

Considering the compiler, assembler, linker, and runtime for 1.5 will be entirely in Go, you can expect further optimization.


Update 2016 Go 1.7: this has been optimized: see "Smaller Go 1.7 binaries".

But these day (April 2019), what takes the most place is runtime.pclntab.
See "Why are my Go executable files so large? Size visualization of Go executables using D3" from Raphael ‘kena’ Poss.

It is not too well documented however this comment from the Go source code suggests its purpose:

// A LineTable is a data structure mapping program counters to line numbers.

The purpose of this data structure is to enable the Go runtime system to produce descriptive stack traces upon a crash or upon internal requests via the runtime.GetStack API.

So it seems useful. But why is it so large?

The URL https://golang.org/s/go12symtab hidden in the aforelinked source file redirects to a document that explains what happened between Go 1.0 and 1.2. To paraphrase:

prior to 1.2, the Go linker was emitting a compressed line table, and the program would decompress it upon initialization at run-time.

in Go 1.2, a decision was made to pre-expand the line table in the executable file into its final format suitable for direct use at run-time, without an additional decompression step.

In other words, the Go team decided to make executable files larger to save up on initialization time.

Also, looking at the data structure, it appears that its overall size in compiled binaries is super-linear in the number of functions in the program, in addition to how large each function is.

https://science.raphael.poss.name/go-executable-size-visualization-with-d3/size-demo-ss.png

Community
  • 1
  • 1
VonC
  • 1,262,500
  • 529
  • 4,410
  • 5,250
  • 2
    I don't see what he implementation language has to do with it. They need to use shared libraries. Somewhat incredible that they don't already in this day and age. – user207421 Apr 23 '17 at 14:33
  • 3
    @EJP: Why do they need to use shared libraries? – Jonathan Hall Apr 23 '17 at 15:05
  • 13
    @EJP, part of Go's simplicity is in not using shared libraries. In fact, Go doesn't have any dependencies at all, it uses plain syscalls. Just deploy a single binary and it just works. It would significantly hurt the language and it's ecosystem if it would be otherwise. – creker Apr 23 '17 at 15:13
  • 16
    An often forgotten aspect of having statically linked binaries is that it makes it possible to run them in a completely empty Docker-container. From a security standpoint, this is ideal. When the container is empty, you might be able to break in (if the statically linked binary has flaws), but since there is nothing to be found in the container, the attack stops there. – Joppe Sep 11 '17 at 16:12