Skip to content

Instantly share code, notes, and snippets.

@bemasher
Created August 15, 2011 04:11
Show Gist options
  • Save bemasher/1145700 to your computer and use it in GitHub Desktop.
Save bemasher/1145700 to your computer and use it in GitHub Desktop.
Linear and concurrent julia set renderers in Google's Go.
On an Intel Core 2 Duo E6600 @ 2.4Ghz the rendering portion took place in:
Linear = 24.107s
Concurrent = 9.063s
Both produce images whose SHA256 sum's are both:
3c68084ae742cfc7070e871ca721cb90694c28e886b77b4f289c8e84d18f124a
package main
import (
"os"
"fmt"
"math"
"time"
"image"
"runtime"
"image/png"
)
const (
// Complex parameter C for the julia set
C = complex(0.285, 0.01)
// Edge length for the image
DIM = 4096
// Slice division: 2^4 = 16
DIV = 4
// Slice length: 4096 >> 4 = 256
S_LEN = DIM >> DIV
// Will render a viewport of -1.25 to 1.25 on both axes
VIEW = 1.25
// See comment in func init()
NCPUS = 16
// Bailout limit so we don't get stuck in infinite loops
CUTOFF = 32
// Where the image should be written
FILENAME = "julia_con.png"
)
// Contains a viewport to render and
// a subsection of the main image to render it onto
type Slice struct {
r image.Rectangle
img *image.Gray
}
// Rewrote this as cmath.Abs() is very slow
func Abs(c complex128) float64 {
return math.Sqrt(real(c)*real(c) + imag(c)*imag(c))
}
// Initializes a slice of the image
func NewSlice(r image.Rectangle, img *image.Gray) Slice {
return Slice{r, img.SubImage(r).(*image.Gray)}
}
// Translate pixel coordinates into viewport coordinates
func translate(n int) float64 {
return float64(n) / DIM * (VIEW*2) - VIEW
}
// Determines the coloring a pixel should have based
// on whether or not the pixel exists in the julia set
func isJulia(x, y int) image.GrayColor {
z := complex(translate(x), translate(y))
// Iterate until |z| < 4.0 or we reach
// reach the iteration cutoff
for i := 0; i < CUTOFF && Abs(z) < 2.0; i++ {
z = z * z + C
}
// We could return a binary value here 0 or 255
// but i find that scaling the magnitude of z
// from 0 to 255 produces more interesting results
return image.GrayColor{uint8((Abs(z) / 2.0) * 255)}
}
// Renders the portion of the image described by the slice
func drawJulia(s Slice, done chan int) {
// For each pixel in the slice
for y := s.r.Min.Y; y < s.r.Max.Y; y++ {
for x := s.r.Min.X; x < s.r.Max.X; x++ {
// Set the color based on the result of
// the isJulia function
s.img.SetGray(x, y, isJulia(x, y))
}
}
fmt.Printf("Rendered slice %+v\n", s.r)
done <- 1
}
func init() {
// Until the go scheduler gets better you have to tell it how many threads
// it should execute goroutines on. I've found that specifying a reasonable
// maximum number of threads has no effect on performance as the OS's
// scheduler then takes care of what those threads do so I've set NCPUS to
// 16 as it will be fairly uncommon for consumer CPU's to have more than 8
// cores or be capable of executing more than 16 threads at a time.
runtime.GOMAXPROCS(NCPUS)
}
func main() {
// Get current time for benchmark
start := time.Nanoseconds()
// Initialize image and channel for signalling
// that a slice is finished
julia := image.NewGray(DIM, DIM)
done := make(chan int)
// Create the number of slices specified with
// the DIV constant and start their rendering goroutines
for y := 0; y < DIM; y += S_LEN {
for x := 0; x < DIM; x += DIM >> DIV {
go drawJulia(NewSlice(image.Rect(x, y, x + S_LEN, y + S_LEN), julia), done)
}
}
// Block until all of the slices have
// completed rendering
for i := 0; i < 1 << DIV << DIV; i++ {
<-done
}
// Stop the benchmark as we're not interested in
// file I/O speed
stop := time.Nanoseconds()
// If the Rendered slice complete messages are enabled
// use this to make sure they all print before moving onto
// file writing stati
os.Stdout.Sync()
// Create or truncate the image file and
// queue file close with defer
file, err := os.Create(FILENAME)
if err != nil {
fmt.Println(err)
return
}
defer file.Close()
// Let the user know we're starting to write the image
fmt.Printf("Writing %s\n", FILENAME)
png.Encode(file, julia)
// Let the user know we're done writing and the
// time it took to render the image
fmt.Printf("Done writing %s\n", FILENAME)
fmt.Printf("Rendered in %0.3fs\n", float64(stop - start) / 1000000000.0)
}
package main
import (
"os"
"fmt"
"math"
"time"
"image"
"runtime"
"image/png"
)
const (
// See comment in func init()
NCPUS = 16
// Bailout limit so we don't get stuck in infinite loops
CUTOFF = 32
// Complex parameter C for the julia set
C = complex(0.285, 0.01)
// Edge length for the image
DIM = 4096
// Will render a viewport of -1.25 to 1.25 on both axes
VIEW = 1.25
// Where the image should be written
FILENAME = "julia_lin.png"
)
// Rewrote this as cmath.Abs() is very slow
func Abs(c complex128) float64 {
return math.Sqrt(real(c)*real(c) + imag(c)*imag(c))
}
// Translate pixel coordinates into viewport coordinates
func translate(n int) float64 {
return float64(n) / DIM * (VIEW*2) - VIEW
}
// Determines the coloring a pixel should have based
// on whether or not the pixel exists in the julia set
func isJulia(x, y int) image.GrayColor {
z := complex(translate(x), translate(y))
// Iterate until |z| < 4.0 or we reach
// reach the iteration cutoff
for i := 0; i < CUTOFF && Abs(z) < 2.0; i++ {
z = z * z + C
}
// We could return a binary value here 0 or 255
// but i find that scaling the magnitude of z
// from 0 to 255 produces more interesting results
return image.GrayColor{uint8((Abs(z) / 2.0) * 255)}
}
// Renders entire image
func drawJulia(img *image.Gray) {
// For each pixel in the image
for y := 0; y < DIM; y++ {
for x := 0; x < DIM; x++ {
// Set the color based on the result of
// the isJulia function
img.SetGray(x, y, isJulia(x, y))
}
}
}
func init() {
// Until the go scheduler gets better you have to tell it how many threads
// it should execute goroutines on. I've found that specifying a reasonable
// maximum number of threads has no effect on performance as the OS's
// scheduler then takes care of what those threads do so I've set NCPUS to
// 16 as it will be fairly uncommon for consumer CPU's to have more than 8
// cores or be capable of executing more than 16 threads at a time.
runtime.GOMAXPROCS(NCPUS)
}
func main() {
// Get current time for benchmark
start := time.Nanoseconds()
// Initialize the image
julia := image.NewGray(DIM, DIM)
// Render the entire image
drawJulia(julia)
// Stop the benchmark as we're not interested in
// file I/O speed
stop := time.Nanoseconds()
// Create or truncate the image file and
// queue file close with defer
file, err := os.Create(FILENAME)
if err != nil {
fmt.Println(err)
return
}
defer file.Close()
// Let the user know we're starting to write the image
fmt.Printf("Writing %s\n", FILENAME)
png.Encode(file, julia)
// Let the user know we're done writing and the
// time it took to render the image
fmt.Printf("Done writing %s\n", FILENAME)
fmt.Printf("Rendered in %0.3fs\n", float64(stop - start) / 1000000000.0)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment