Featured image of post Golang High-Performance Programming EP2:  Reduce The Size Of Executable Binary Files With upx

Golang High-Performance Programming EP2: Reduce The Size Of Executable Binary Files With upx

 

We all know that Golang has a significant feature: its compilation speed is extremely fast. The speed of compilation was a key consideration when Go was being designed. But have you ever looked at the size of the binary executable file produced after Go compiles your code? Let’s take a look at a simple HTTP server example.

This article was first published in the Medium MPP plan. If you are a Medium user, please follow me on Medium. Thank you very much.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import (  
    "fmt"  
    "net/http"
)  
func main() {  
    // create a http server and create a handler hello, return hello world  
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {  
        fmt.Fprintf(w, "Hello, World\n")  
    })  
    // listen to port 8080  
    err := http.ListenAndServe(":8080", nil)  
    if err != nil {  
        return  
    }  
}

The compiled size is 6.5M.

1
2
3
➜  binary-size git:(main) ✗ go build  -o server main.go                 
➜  binary-size git:(main) ✗ ls -lh server 
-rwxr-xr-x  1 hxzhouh  staff   6.5M Jul  2 14:20 server

The Go compiler trims the size of the binary file. If you’re interested in this, check out my other article How Does the Go Compiler Reduce Binary File Size?.

Now let’s try to optimize the size of the server.

Removing Debug Information

By default, the Go compiler includes a symbol table and debug information in the compiled program. Typically, you can remove this debug information for a release version to reduce the binary size.

1
2
3
➜  binary-size git:(main) ✗ go build -ldflags="-s -w" -o server main.go
➜  binary-size git:(main) ✗ ls -lh server                              
-rwxr-xr-x  1 hxzhouh  staff   4.5M Jul  2 14:30 server
  • -s: Omit the symbol table and debug information.
  • -w: Omit the DWARFv3 debug information. With this option, you cannot use gdb to debug.
    The size dropped from 6.5M to 4.5M, about a 30% reduction. This is a good first step.

Using UPX

UPX is an advanced executable file compressor. UPX will typically reduce the file size of programs and DLLs by around 50%-70%, thus reducing disk space, network load times, download times, and other distribution and storage costs.

On Mac, you can install UPX via brew.

1
brew install upx

Compressing with UPX Alone

UPX has many parameters, with the most important being the compression ratio, ranging from 1-13. 1 represents the lowest compression ratio, and 13 is the highest.
Let’s see how much we can reduce the binary size using UPX alone.

1
2
➜  binary-size git:(main) ✗ go build -o server main.go && upx --brute server && ls -lh server 
-rwxr-xr-x  1 hxzhouh  staff   3.9M Jul  2 14:38 server

The compression ratio is about 60%.

UPX + Compiler Options

Enabling both UPX and -ldflags="-s -w":

1
2
➜  binary-size git:(main) ✗ go build -ldflags="-s -w"  -o server main.go && upx --brute server && ls -lh server 
-rwxr-xr-x  1 hxzhouh  staff   1.4M Jul  2 14:40 server

Finally, we get an executable file size of 1.4M compared to the uncompressed 6.5M, saving about 80% of the space. This is quite considerable for large applications.

How UPX Works

The compressed program works like the original and can run normally without decompression. This compression method is called shell compression, which includes two parts:

  • Insert decompression code at the beginning or another suitable place in the program.
  • Compress the rest of the program.

When executed, it also includes two parts:

  • The decompression code inserted at the beginning is executed first, decompressing the original program into memory.
  • Then, the decompressed program is executed.
    UPX introduces an extra decompression step when the program is executed, but this time is almost negligible.
    If you don’t have strict requirements on the compiled size, you might choose not to use UPX compression. Generally, server-side standalone background services do not need compressed size.

Conclusion

This post contains many interesting answers:

  • For example, when implementing the same functionality with C and Go (a small demo), the executable size generated by C is 1/20th of that generated by Go (why?).
  • Using println instead of fmt.Println can avoid importing the fmt package, further reducing the size.
true
Last updated on Jul 02, 2024 15:02 CST
Built with Hugo
Theme Stack designed by Jimmy