Among the many other projects I've launched in quarantine (some of which I've even finished), I have been taking the time to familiarize myself with the Go language. While I am not a software engineer by trade and do not typically encounter problems on the scale normally associated with Go, I wanted to plug the hole in my knowledge where the C-family language information would reside. I'm a journeyman user of Python and Powershell; I can bring either of these languages to bear against the "one-off script" type problems I typically encounter, such as mass data manipulation (i.e. "too big for Excel"), light process automation, interaction with an API, etc.
In short, I know a few good tricks, and I am always interested in adding a few new ones to my bag. In addition to being derived from C, I have often heard that Go is blazing fast, especially in comparison with something like Python. With that in mind, I set out to find out just how fast is "blazing fast", and just how much faster that is than Python.
In terms of prior art, the best resource I came across is without a doubt the Computer Language Benchmarks Game. They benchmark many languages, far more than just Python and Go, and I like the cut of their webpage's jib.
One of the methods they use for benchmarking is to implement a program for drawing a Mandelbrot set. As I write this, Benoit Mandelbrot's birthday occurred just a few days prior on November 20th; he would have been 96. The tenth anniversary of his death happened just last month. The problem seems to be of the right scale to highlight the performance differences between the two languages, and there are many sample implementations to get a sense of what will need to be written. I wanted to write something that would actually generate an image instead of just returning an array of values indicating the set, and I came across a legible implementation from a gentleman named Ted Burke that draws a grayscale image using the PGM file format, which was new to me (proper attribution appears below in the Python code).
I will assume the reader is already familiar with the Mandelbrot set and fractals more generally. If not, there are a number of excellent resources available: the reference I came across the most was a talk by Dr. Holly Krieger of MIT which is available on Youtube Invidious. As such, the only additional background knowledge you need to read the following code concerns the PGM format. The specification is available here, but all you really need to know is that a PGM file is a magic string and some metadata paramters defining the image, followed by an array of values that indicate the "grayscale intensity" of a pixel at that location, where 0 represents black and 255 represents white.
With that out of the way, here is Python code to generate a PGM file representing the Mandelbrot set:
mandelbrot.py
#with appreciation to Ted Burke
#https://batchloaf.wordpress.com/2016/02/03/c-or-python-comparison-of-execution-time-for-mandelbrot-image-generation/
import numpy
def mandelbrot(c):
z = complex(0,0)
#px is the pgm representation of the shade of the pixel at p[x][y]
px = 255
while abs(z) <= 4.0 and px >= 10:
z = z*z + c
px -= 10
return px
#Initialize the array that will become the PGM bytearray
w,h = 4000,4000
p = numpy.zeros((h,w),dtype=numpy.uint8)
#create file for pgm, write the magic number so the file will be valid
#http://netpbm.sourceforge.net/doc/pgm.html
#Later, we will write the array p as a bytearray so the magic number string needs to be byte encoded first
f = open('mandelbrot_py.pgm', 'wb')
f.write(('P5\n{:d} {:d}\n255\n'.format(w, h)).encode())
for y in range(h):
for x in range(w):
c = complex(-2.0 + x*4.0/w,2.0 - y*4.0/h)
#print(c, mandelbrot(c))
p[y][x] = mandelbrot(c)
f.write(bytearray(p))
f.close()
The Python implementation is relatively concise, and includes only minor deviations (style preferences, mostly) from Mr. Burke's. This implementation highlights a number of user-friendly features of Python. In no particular order:
When I shift to Go, most of these training wheels fall off. Quite frankly, I never fully appreciated how much work Python abstracts away. Since the reference website includes a C version but no Go implementation, I had to do some translation. The C language has some features that have no direct equivalent in Go, such as support for while loops and the fprintf function, which would have come in handy here. This is what I came up with in Go:
mandelbrot.go
package main
import (
"io/ioutil"
"math/cmplx"
"os"
"math"
"encoding/binary"
)
var (
px,w,h float64
x,y int
p,q []byte
perms os.FileMode
c,z complex128
)
func main() {
w,h = 100.0,100.0
x,y = 0,0
//Convert the magic string to a byte slice
magic := []byte("P5\n100 100\n255\n")
for i := 0.0; i < h; i++ {
for j := 0.0; j < w; j++ {
c = complex(-2.0 + j*4.0/w, 2.0 - i*4.0/h)
tmp := mandelbrot(c)
//The mandelbrot function returns a 64 bit float but our goal
//is to write a byte sequence
//So we need a utility function that converts a float to a
//fixed size byte buffer
byte := float64ToByte(tmp)
p = append(p, byte...)
}
}
q = append(magic, p...)
perms = 0644
ioutil.WriteFile("mandelbrot_go.pgm",q,perms)
}
func mandelbrot(c complex128) float64 {
z = complex(0,0)
px = 255
for {
z = z*z + c
if cmplx.Abs(z) > 4.0 { return px } else if px < 11 { return px }
px = px - 10
}
}
func float64ToByte(f float64) []byte {
var buf [8]byte
binary.BigEndian.PutUint64(buf[:], math.Float64bits(f))
return buf[:]
}
Here are the results for mandelbrot.py:
time python mandelbrot.py
real 3m28.550s
user 3m3.920s
sys 0m0.336s
And -- drumroll, please -- the result of timing the Go code:
time go run mandelbrot.go
real 0m15.753s
user 0m14.452s
sys 0m0.260s
Wow. The Go code is an order of magnitude faster (15s vs. 208s), and I am sure I did not happen upon the most efficient implementation possible, even given all the neat compiler tricks Go likely performs on my behalf. Reassuringly, the results from the Benchmarks Game site were in line with what I observed; their Mandelbrot implementations took 3.75(!) and 172.58 seconds in Go and Python, respectively. This is despite the fact that their implementations are much better optimized than mine, making use of parallel processing, and that theirs consider the function to diverge if the absolute value exceeds 2 as compared to my (arbitrary) selection of 4. And of course that their test machine had (and used) four cores, whereas mine has only two.
Overall, this was a fun and very effective exercise. This is an excellent way to learn a new programming language. Attacking the problem in Python first allowed me to organize my thoughts in such a way that reimplmenting it in Go, a language with which I am less familiar and demands stricter attention to types, felt like a natural progression. I will leave off with two images of a Mandelbrot set: first, the PGM file output by either of the above programs, and next a colorized version retrieved from Geeksforgeeks.org. Happy Birthday to Benoit Mandelbrot.