How to reduce compiled file size?

Viewed 60007

Lets compare c and go: Hello_world.c :

#include<stdio.h>
int main(){
    printf("Hello world!");
}

Hello_world.go:

package main
import "fmt"
func main(){
    fmt.Printf("Hello world!")
}

Compile both:

$gcc Hello_world.c -o Hello_c 
$8g Hello_world.go -o Hello_go.8
$8l Hello_go.8 -o Hello_go

and ... what is it?

$ls -ls
... 5,4K 2010-10-05 11:09 Hello_c
... 991K 2010-10-05 11:17 Hello_go

About 1Mb Hello world. Are you kidding me? What I do wrong?

(strip Hello_go -> 893K only)

11 Answers

create a file named main.go, let's try with simple hello world program.

package main

import "fmt"

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

I use go version 1.9.1

$ go version
 go version go1.9.1 linux/amd64

Compile with standard go build command.

$ go build main.go
$ ls -lh
-rwxr-xr-x-x 1 nil nil 1.8M Oct 27 07:47 main

Let's compile once again with go build but with ldflags as suggested above,

$ go build -ldflags "-s -w" main.go
$ ls -lh
-rwxr-xr-x-x 1 nil nil 1.2M Oct 27 08:15 main

File size is reduced by 30%.

Now, lets use gccgo,

$ go version
 go version go1.8.1 gccgo (GCC) 7.2.0 linux/amd64

Building go with gccgo,

$ go build main.go
$ ls -lh
-rwxr-xr-x 1 nil nil 34K Oct 27 12:18 main

Binary size is reduced by almost 100%. Let's once again try building our main.go with gccgo but with build flags,

$ go build -gccgoflags "-s -w" main.go
-rwxr-xr-x 1 nil nil 23K Oct 27 13:02 main

Warning: As gccgo binaries were dynamically linked. If you have a binary which is very big in size, your binary when compiled with gccgo will not be decreased by 100%, but it will be reduced in size by considerable amount.

Compared to gc, gccgo is slower to compile code but supports more powerful optimizations, so a CPU-bound program built by gccgo will usually run faster. All the optimizations implemented in GCC over the years are available, including inlining, loop optimizations, vectorization, instruction scheduling, and more. While it does not always produce better code, in some cases programs compiled with gccgo can run 30% faster.

The GCC 7 releases are expected to include a complete implementation of the Go 1.8 user libraries. As with earlier releases, the Go 1.8 runtime is not fully merged, but that should not be visible to Go programs.

Pros:

  1. Reduced size
  2. Optimized.

Cons

  1. Slow
  2. Cannot use the latest version of go.

You can see over here and here.

By default gcc links dynamically and go - statically.

But if you link you C code statically, you might get a binary with a bigger size.

In my case:

  • go x64 (1.10.3) - generated binary with size 1214208 byte
  • gcc x64 (6.2.0) - generated binary with size 1421312 byte

both binaries are statically linked and without debug_info.

go build -ldflags="-s -w" -o test-go test.go
gcc -static -s -o test-c test.c

2018 answer for the next Go 1.11, as tweeted by Brad Fitzpatrick (mid-June 2018):

As of a few minutes ago, DWARF sections in #golang ELF binaries are now compressed, so at tip binaries are now smaller than Go 1.10, even with all the extra debug stuff at tip.

https://pbs.twimg.com/media/DfwkBaqUEAAb8-Q.jpg:large

Cf. Golang issue 11799:

Compressing our debug info might offer a significant, cheap file size win.

See more in commit 594eae5

cmd/link: compress DWARF sections in ELF binaries

The trickiest part of this is that the binary layout code (blk, elfshbits, and various other things) assumes a constant offset between symbols' and sections' file locations and their virtual addresses.

Compression, of course, breaks this constant offset.
But we need to assign virtual addresses to everything before compression in order to resolve relocations before compression.

As a result, compression needs to re-compute the "address" of the DWARF sections and symbols based on their compressed size.
Luckily, these are at the end of the file, so this doesn't perturb any other sections or symbols. (And there is, of course, a surprising amount of code that assumes the DWARF segment comes last, so what's one more place?)

name        old exe-bytes   new exe-bytes   delta
HelloSize      1.60MB ± 0%     1.05MB ± 0%  -34.39%  (p=0.000 n=30+30)
CmdGoSize      16.5MB ± 0%     11.3MB ± 0%  -31.76%  (p=0.000 n=30+30)
[Geo mean]     5.14MB          3.44MB       -33.08%

Rob Pike mentions:

That only helps on machines that use ELF.
Binaries are still too big, and growing.

Brad replied:

At least it's something. Was about to be much worse.
Stopped the bleeding for one release.

Reasons: Debug info, but also register map for the GC to let any instruction be a savepoint.

Related