DEV Community

Cover image for Optimizing Go code with GCCGO for improved performance
Possawat Sanorkam
Possawat Sanorkam

Posted on

Optimizing Go code with GCCGO for improved performance

One day, my teacher asked me why Go compiled program has terrible perfomance and he kept mentioning about how a good compiler should behave.

Tldr; Check out my demo!

It is using gccgo -O3 -o cool main.go instead of go build main.go.

Basically, you will find that some code could run faster by changing the compiler instead of rewriting the code.

So, we want to turn our code into its peak performance despite getting bugs.

Super gopher

Consider the following code...

package main

import (
    "errors"
    "fmt"
)

const limA = 2000

type EulerSolution struct {
    first   int
    second  int
    third   int
    fourth  int
    culprit int
}

func intPow(n, m int) int {
    if m == 0 {
        return 1
    }
    result := n
    for i := 2; i <= m; i++ {
        result *= n
    }
    return result
}

func computeEuler() (EulerSolution, error) {
    for e := 1; e < limA; e++ {
        for a := 1; a < e; a++ {
            var a5 = intPow(a, 5)
            for b := 1; b <= a; b++ {
                var b5 = intPow(b, 5)
                for c := 1; c < b; c++ {
                    var c5 = intPow(c, 5)
                    for d := 1; d < c; d++ {
                        var d5 = intPow(d, 5)
                        var e5 = intPow(e, 5)
                        var got int = a5 + b5 + c5 + d5

                        if got == e5 {
                            return EulerSolution{
                                first:   a,
                                second:  b,
                                third:   c,
                                fourth:  d,
                                culprit: e,
                            }, nil
                        }

                    }
                }
            }
        }
    }

    return EulerSolution{}, errors.New("Euler solution")
}

Enter fullscreen mode Exit fullscreen mode

We know that this code is poorly written. Let's write the another function that produce similar result.

func computeBeastEuler() (EulerSolution, error) {
    for e := 1; e < limA; e++ {
        var e5 = intPow(e, 5)
        for a := 1; a < e; a++ {
            var a5 = intPow(a, 5)
            for b := 1; b <= a; b++ {
                var b5 = intPow(b, 5)
                for c := 1; c < b; c++ {
                    var c5 = intPow(c, 5)
                    for d := 1; d < c; d++ {
                        var d5 = intPow(d, 5)
                        var got int = a5 + b5 + c5 + d5
                        if got == e5 {
                            return EulerSolution{
                                first:   a,
                                second:  b,
                                third:   c,
                                fourth:  d,
                                culprit: e,
                            }, nil
                        }

                    }
                }
            }
        }
    }

    return EulerSolution{}, errors.New("Euler solution")
}
Enter fullscreen mode Exit fullscreen mode

Much better, now we want to prove that the new function, computeBeastEuler, is faster.

go test -bench main_test.go -v

=== RUN   Test_computeEuler
=== RUN   Test_computeEuler/empty
--- PASS: Test_computeEuler (4.19s)
    --- PASS: Test_computeEuler/empty (4.19s)
=== RUN   Test_computeBeastEuler
=== RUN   Test_computeBeastEuler/empty
--- PASS: Test_computeBeastEuler (2.51s)
    --- PASS: Test_computeBeastEuler/empty (2.51s)
PASS
ok      go-beast-demo   6.705s
Enter fullscreen mode Exit fullscreen mode

It is better, and we expect our go build to do the optimization for us.

go build main.go
time ./main 
{133 110 84 27 144}

real    0m4.371s
...

Enter fullscreen mode Exit fullscreen mode

Well, it is not what we expected. The problem is that Go compiler is not focusing on optimization by default. It only focuses on compilation speed.

Now, let me talk about another compiler that will help us get the faster program using the same code.

Let's install gccgo to our machine. Make sure that you are not using Darwin OS!

git clone --branch devel/gccgo git://gcc.gnu.org/git/gcc.git gccgo
mkdir objdir
cd objdir
../gccgo/configure --prefix=/opt/gccgo --enable-languages=c,c++,go --with-ld=/opt/gold/bin/ld
make
make install
Enter fullscreen mode Exit fullscreen mode

References:
gccgo

Anyway, you might run into some problems with its prerequisites for the compiler.

cd gccgo
./contrib/download_prerequisites
Enter fullscreen mode Exit fullscreen mode

The script will download all you need for the gccgo.

Instead, we can just use a dockerfile to help us set up the environment for our new compiler.

# Download base image ubuntu 20.04
FROM ubuntu:20.04

# Disable Prompt During Packages Installation
ARG DEBIAN_FRONTEND=noninteractive

# Update Ubuntu Software repository
RUN apt update

RUN apt-get update -y

RUN apt-get install -y gccgo

WORKDIR /go/app

COPY . .

RUN gccgo -O3 -o cool main.go

CMD ["./cool"]
Enter fullscreen mode Exit fullscreen mode

References:
gcc optimize

Inside the container, let's do the benchmarking again!

docker compose build
docker run -it go-beast-mode_app bash
Enter fullscreen mode Exit fullscreen mode

Inside my container, here is what I got.

time ./cool
{133 110 84 27 144}

real    0m0.906s
user    0m0.583s
sys     0m0.028s
Enter fullscreen mode Exit fullscreen mode

It is even 2x faster than our computeBeastEuler in time.

In fact, Go can compile blazingly fast, but the trade-off is the lost of performance. If we want to optimize our Go program, then this is an example of what you could try. It will be compiled slower, but the performance is greatly increased along with the slight increase in memory usage.

Top comments (0)