Go pathtracer
23/Sep 2013
Recently I’ve been experimenting with the Go programming language a little bit. It’s my second approach actually - I gave it a half-hearted try last year, but gave up pretty quickly (I think it was some petty reason, too, probably K&R braces). This time around I actually managed to stick to it a little bit longer and learn a thing or two. I decided my test application would be a simple pathtracer. Probably not exactly area that language creators had in mind, but I wasn’t really interested in coding web server stuff (Go did surprisingly well in the end). As far as features go, pathtracer itself is not really interesting, my main goal was to get to know the language a little bit. It’s pretty much a poor man’s version of smallpt (I borrowed scene description and “use huge sphere to fake box trick” from there). It doesn’t support dielectric objects (I did implement glossy materials, though, I like that look) and I didn’t really care about squeezing it in as little lines as possible (so it’s ~700 lines, not 99). Some of my observations (please take them for what they are - random ramblings of someone who’s been coding in Go for ~10h total and used it for quite an unorthodox purpose):
if you know C/C++ - you know Go. Syntax is very similar and you can learn about the most important differences in 5 minutes. There’s a good summary here.
interestingly, forced K&R braces were not a problem this time, it somehow felt natural to me (even though I don’t use this convention in C/C++). If you’re struggling - there is gofmt (tool that converts to the ‘official’ format)
my biggest problem was “weird” declaration syntax that’s different from most languages I tried - in Go it’s name followed by type, ie.:
var normal Vector // That's Vector normal in C/C++/Java/C# etc
type-name order is so ingrained in my mind/fingers it took me a few days to switch and I still hesitate for a split second every time (I actually had to think twice when typing the example above [EDIT: and I still got it wrong!]).
no operator overloading. Yes, you don’t need it often and in the wrong hands it can be downright dangerous, but math code is one area where it’s actually quite useful. Take ‘reflect’ function for example, with operator overloading it looks very similar to the math formula, without it might take a while to understand what’s going on exactly. It also results in longer code. Not a big deal, but worth mentioning.
no built-in assertion/compile time assertions. Authors try to justify their decision in the FAQ, but to be I don’t really buy it (yes, you can code one yourself or use some public library, but it didn’t make too much sense for a program of this scale)
profiling. Go has a built-in profiler (well, almost, you need to add some code to your program), described here. Sadly, out of the box, it doesn’t seem to work properly on Windows. I used a custom version of Perl script found at Ex Nihilo blog. It worked OK for big picture views, still couldn’t get list command to work (admittedly didn’t try too hard, it seemed like it was missing some GCC tools like c++filt & nm)
debugging. Apparently you can use gdb, didn’t try it, so can’t comment. Used fmt.Printf and visual debugging - mostly for tracking bugs as crashes are usually very informative and it’s obvious what went wrong. Go is fairly “safe” language, too, can’t easily stomp over memory or address arrays with invalid indices.
goroutines. The most important thing to remember – these are not hardware threads, don’t be too stingy. Switching between goroutines is cheap, so it’s OK to create hundreds of them (in my case it was a single goroutine per 16x16 pixel chunk). In gamedev terms they’re closer to jobs in worker pool scheme (rather than worker threads). In case anyone is interested in gory details, here’s their scheduler implementation (check it out, it’s actually a very nice code). This is one of areas where Go shine. Modifying my tracer to take advantage of multiple threads took me 2 minutes or so. Truth be told, the first version had a data race bug, but here’s where Go surprised me again, it actually has a built-in race detector. All you need to do is to add the -race flag to the command line. One of the authors of the detector is Dmitry Vjukov, guy behind Relacy and someone who knows more about low-level multi-threading than most of us combined, so I expect it to work OK (to play it safe). I know it found my problem in no time.
memory management. I must admit I still don’t really “feel” it. Go uses an automatic memory management (mark-and-sweep GC), so in theory you don’t have to worry much, but I like to understand what’s going on under the hood (especially if you care about performance). The tricky part is, Go tries to be smart and avoid dangling pointers, so heap allocations are not always immediately obvious. Consider:
func f2() *S { var s S return &s }
In this example S will be allocated on the heap, so that caller is not left with dangling pointer (even though there’s no explicit new).
- one that actually caused me some head scratching. Consider the following code where I tried to precalculate radius^2 for each sphere:
func (scene *Scene) Initialize() { for _, s := range scene.objects { s.radiusSqr = s.radius * s.radius } }
If you’re familiar with Go, you probably see the mistake already… After I introduced this optimization all I was getting was a black screen. As it turned out index,value iteration actually operates on the copy of the array element! Seems like a weird decision to me, but I might be missing something. It also has performance implications. My original “intersect” loop was this:
for i, s := range scene.objects {
t := SphereIntersect(&s, ray)
[...]
Same problem here - we copy sphere object every iteration. Rendering 256x256 image with 144 samples per pixel was taking around 2m20s. One little change:
for i := range scene.objects {
s := &scene.objects[i]
t := SphereIntersect(s, ray)
[...]
…and same scene renders in 1m40s!
- most importantly - is it fun to program in Go? It sure is. I actually did most of that stuff during few late nights and I had to drag myself to bed (mostly because I knew my daughter would wake me up at 8am next day). Even though it’s fairly young language, it feels very mature. It takes few seconds to compile, so it might not feel as snappy as some of the scripting languages, but the actual generated code is really fast. Has few quirks that I don’t love (most of them mentioned above), but at the end of the day I really enjoyedit.
Some pretty and not-so-pretty pictures and the source code:
Old comments
Lukasz 2013-09-23 04:04:18
And you actually got the first example wrong, should be “var normal Vector”. I looked it up as it didn’t make sense with the rest of the paragraph:).
admin 2013-09-23 04:34:12
Damn, now you see what I mean.. Thanks for pointing that out.