The Trade-Offs of Optimizing and Compressing Go Binaries
Tags: go, beginners, upx
Published: 2023-12-14 05:19:33

Binary optimization is an art that strikes a balance between performance gains and potential drawbacks. In [Go](https://go.dev/), this is often achieved using a series of compiler and linker flags, followed by binary compression. These processes can dramatically impact the size and speed of Go applications but require a nuanced understanding to apply effectively.

## Optimizing with Compiler and Linker Flags

The `go build` command includes options that allow developers to tailor the compilation process. Using `-gcflags` and `-ldflags`, developers can control compiler optimizations and linker behaviors. These flags can lead to smaller and faster binaries but can also remove useful information and checks.

## The Role of Binary Compression

Following optimization, tools like [UPX](https://upx.github.io/) can compress the resulting binary, significantly reducing file size. This compression is invaluable for resource-constrained environments but adds a decompression step during binary execution.

## A Deeper Dive into Optimization Commands

```shell
go build -a -gcflags=all="-l -B" -ldflags="-w -s" -o myapp main.go
```
### Step by Step Analysis:

1. **Rebuild with** `-a`: Ensuring all dependencies are compiled with the same optimizations.
2. **Compiler Flags** `-gcflags=all="-l -B"`:
    - **Inlining** `(-l)`: In some cases, disabling inlining can reduce the binary size, but it may also lead to performance degradation since inlining can optimize function calls.
    - **Bounds Checking** `(-B)`: Removing bounds checking is a high-risk optimization. It can increase speed for CPU-bound programs by removing runtime checks but can also lead to undetectable bugs if index out-of-bounds errors occur.

3. **Linker Flags** `-ldflags="-w -s"`:
    - **Debug Information** `(-w)`: Stripping debug information reduces size but eliminates the ability to perform detailed debugging.
    - **Symbol Table** `(-s)`: Omitting the symbol table decreases size but can hinder profiling tools and runtime panic reports.

4. **Compression with UPX**:
```shell
upx --best --ultra-brute myapp
```
   - **Compression**: `UPX` uses advanced algorithms to reduce the binary size. The `--best` and `--ultra-brute` options aim for maximum compression, which is most effective but can also be time-consuming and potentially add runtime overhead.

## Detailed Example: A Microservice with a Database Connection

Imagine a microservice responsible for handling web requests and interacting with a database. We'll optimize this Go binary and then measure the impact on size and load time.

- **Initial binary size**: ~11 MB
- **With** `-ldflags="-w -s"`: ~8 MB
- **With UPX**: ~3 MB

**Observations:**

- **Size Reduction**: The optimized binary is around 73% smaller than the original.
- **Load Time**: Decompression adds a slight delay to the service startup time, potentially affecting services where rapid scaling is necessary.

## Conclusion

In conclusion, the decision to optimize and compress Go binaries should be tailored to the specific needs of a project. Developers must weigh the importance of performance and size against the potential risks and ensure they are not compromising the application's integrity for marginal gains. Testing and profiling remain indispensable tools in this decision-making process.

Image source: https://www.pexels.com/photo/photo-of-a-burger-between-a-person-s-hands-14001304/