Skip to content

Instantly share code, notes, and snippets.

@magiconair
Last active September 14, 2016 07:24
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save magiconair/68a524fc847ba2860893a799f637f532 to your computer and use it in GitHub Desktop.
Save magiconair/68a524fc847ba2860893a799f637f532 to your computer and use it in GitHub Desktop.
Port of the NaiveGame Java impl
// This is a port of the naive Java version of
// https://jackmott.github.io/programming/2016/09/01/performance-in-the-large.html
//
// Code is not pretty!!! ;)
//
// Results:
//
// $ GODEBUG=gctrace=1 go run naive/main.go
// gc 1 @0.079s 0%: 0.11+0.30+0.077 ms clock, 0.34+0.008/0.40/0.48+0.23 ms cpu, 4->4->0 MB, 5 MB goal, 8 P
// # command-line-arguments
// gc 1 @0.004s 4%: 0.031+2.0+0.057 ms clock, 0.12+0/2.1/0.37+0.22 ms cpu, 4->4->3 MB, 5 MB goal, 8 P
// gc 2 @0.016s 5%: 0.006+1.6+0.60 ms clock, 0.048+0.061/1.4/2.5+4.8 ms cpu, 6->6->4 MB, 7 MB goal, 8 P
// # command-line-arguments
// gc 1 @0.001s 8%: 0.035+2.7+0.042 ms clock, 0.10+2.2/0.46/0.14+0.12 ms cpu, 4->4->4 MB, 5 MB goal, 8 P
// gc 2 @0.009s 6%: 0.023+2.6+0.082 ms clock, 0.16+0.28/2.4/0.35+0.57 ms cpu, 7->8->7 MB, 8 MB goal, 8 P
// gc 3 @0.025s 5%: 0.005+3.6+0.049 ms clock, 0.042+0.20/4.9/1.6+0.39 ms cpu, 13->14->13 MB, 15 MB goal, 8 P
// Loading World...
// gc 1 @0.000s 10%: 0.047+2.9+0.091 ms clock, 0.19+2.9/0.007/3.0+0.36 ms cpu, 4->4->4 MB, 5 MB goal, 8 P
// gc 2 @0.016s 4%: 0.032+2.1+0.063 ms clock, 0.22+0.028/2.0/0.11+0.44 ms cpu, 7->7->6 MB, 9 MB goal, 8 P
// gc 3 @0.026s 4%: 0.005+5.3+0.087 ms clock, 0.040+3.0/2.2/3.1+0.69 ms cpu, 15->15->15 MB, 16 MB goal, 8 P
// gc 4 @0.054s 4%: 0.025+7.6+0.039 ms clock, 0.20+4.5/5.5/4.5+0.31 ms cpu, 26->26->26 MB, 30 MB goal, 8 P
// gc 5 @0.110s 3%: 0.008+9.8+0.067 ms clock, 0.069+3.0/9.3/3.1+0.54 ms cpu, 48->49->47 MB, 52 MB goal, 8 P
// gc 6 @0.211s 2%: 0.008+13+0.045 ms clock, 0.068+0/14/6.9+0.36 ms cpu, 86->93->90 MB, 95 MB goal, 8 P
// gc 7 @0.387s 2%: 0.009+21+0.068 ms clock, 0.072+2.7/35/2.9+0.54 ms cpu, 170->170->166 MB, 181 MB goal, 8 P
// gc 8 @0.714s 2%: 0.021+36+0.075 ms clock, 0.17+2.9/64/3.4+0.60 ms cpu, 313->313->305 MB, 332 MB goal, 8 P
// gc 9 @1.353s 2%: 0.016+37+0.11 ms clock, 0.12+18/69/91+0.90 ms cpu, 593->601->585 MB, 611 MB goal, 8 P
// FINISHED!
// Load Time: 2.468865991s
// 1.096094ms
// 1.466952ms
// 3.008006ms
// 1.601441ms
// 1.025923ms
// ...
// gc 10 @20.295s 0%: 0.034+145+0.060 ms clock, 0.27+3.8/280/2.8+0.48 ms cpu, 1143->1143->1080 MB, 1171 MB
//
package main
import (
"fmt"
"math"
"math/rand"
"time"
)
type Vector struct {
x, y, z float64
}
func Mult(a, b Vector) Vector {
return Vector{a.x * b.x, a.y * b.y, a.z * b.z}
}
func Add(a, b Vector) Vector {
return Vector{a.x + b.x, a.y + b.y, a.z + b.z}
}
func Sub(a, b Vector) Vector {
return Vector{a.x - b.x, a.y - b.y, a.z - b.z}
}
func Dist(a, b Vector) float64 {
v := Sub(a, b)
return math.Sqrt(v.x*v.x + v.y*v.y + v.z*v.z)
}
type Block struct {
location Vector // x,y,z within the chunk
name string
durability int
textureid int
breakable bool
visible bool
typ int
}
type Type int
const (
Zombie Type = iota
Chicken
Exploder
TallCreepyThing
)
type Entity struct {
r *rand.Rand
location Vector
name string
health int
speed Vector
typ Type
}
func NewEntity(location Vector, typ Type) *Entity {
e := &Entity{
r: rand.New(rand.NewSource(time.Now().UnixNano())),
location: location,
typ: typ,
}
switch typ {
case Zombie:
e.name = "Zombie"
e.health = 50
e.speed = Vector{0.5, 0.0, 0.5} //slow, can't fly
case Chicken:
e.name = "Chicken"
e.health = 25
e.speed = Vector{0.75, 0.25, 0.75} //can fly a bit
case Exploder:
e.name = "Exploder"
e.health = 75
e.speed = Vector{0.75, 0.0, 0.75}
case TallCreepyThing:
e.name = "Tall Creepy Thing"
e.health = 500
e.speed = Vector{1.0, 1.0, 1.0} //does what he wants
}
return e
}
func (e *Entity) updatePosition() {
// Complex movement AI
rndUnitIshVector := Vector{1, 1, 1}
movementVector := Mult(rndUnitIshVector, e.speed)
e.location = Add(movementVector, e.location)
}
const NUM_BLOCKS = 65536 //just like minecraft!
const NUM_ENTITIES = 1000
type Chunk struct {
blocks []Block
entities []*Entity
location Vector // x,y,z within world
}
func NewChunk(location Vector) *Chunk {
c := &Chunk{location: location}
// Preallocate the growable List because we are clever!
c.blocks = make([]Block, NUM_BLOCKS)
for i := range c.blocks {
c.blocks[i] = Block{
location: Vector{float64(i), float64(i), float64(i)},
name: fmt.Sprintf("Block:%d", i),
durability: 100,
textureid: 1,
breakable: true,
visible: true,
typ: 1,
}
}
c.entities = make([]*Entity, 0, NUM_ENTITIES)
for i := 0; i < NUM_ENTITIES/4; i++ {
// Fancy proc gen initial position equation
f := float64(i)
c.entities = append(c.entities, NewEntity(Vector{f, f, f}, Chicken))
c.entities = append(c.entities, NewEntity(Vector{f + 2, f, f}, Zombie))
c.entities = append(c.entities, NewEntity(Vector{f + 3, f, f}, Exploder))
c.entities = append(c.entities, NewEntity(Vector{f + 4, f, f}, TallCreepyThing))
}
return c
}
func (c *Chunk) processEntities() {
for _, e := range c.entities {
e.updatePosition()
}
}
const CHUNK_COUNT = 100
type Game struct {
chunks []*Chunk
playerLocation Vector
chunkCounter int
}
func (g *Game) loadWorld() {
g.chunks = make([]*Chunk, CHUNK_COUNT)
for i := range g.chunks {
g.chunks[i] = NewChunk(Vector{float64(g.chunkCounter), 0, 0})
g.chunkCounter++
}
}
func (g *Game) updateChunks() {
chunks := []*Chunk{}
for _, c := range g.chunks {
c.processEntities()
chunkDistance := Dist(c.location, g.playerLocation)
if chunkDistance > CHUNK_COUNT {
chunks = append(chunks, NewChunk(Vector{float64(g.chunkCounter), 0, 0}))
g.chunkCounter++
continue
}
chunks = append(chunks, c)
}
g.chunks = chunks
}
func main() {
game := &Game{}
fmt.Println("Loading World...")
start := time.Now()
game.loadWorld()
loadWorldTime := time.Since(start)
fmt.Println("FINISHED!")
fmt.Printf("Load Time: %s\n", loadWorldTime)
// Game Loop, you can never leave
for {
// check for dead entities
start = time.Now()
// mocking polling of the VR controller
playerMovement := Vector{0.1, 0, 0}
game.playerLocation = Add(game.playerLocation, playerMovement)
game.updateChunks()
t := time.Since(start)
fmt.Println(t)
// Lock it at 60FPS
if t < 16*time.Millisecond {
time.Sleep(16*time.Millisecond - t)
}
}
}
// This is a port of the FAST Java version of
// https://jackmott.github.io/programming/2016/09/01/performance-in-the-large.html
// Original Go translation of naive version from https://gist.github.com/magiconair/68a524fc847ba2860893a799f637f532
//
// Code is not pretty!!! ;)
// Added histogram
//
package main
import (
"fmt"
"math"
"strconv"
"time"
)
type Vector struct {
x, y, z float64
}
func Mult(a, b Vector) Vector {
return Vector{a.x * b.x, a.y * b.y, a.z * b.z}
}
func Add(a, b Vector) Vector {
return Vector{a.x + b.x, a.y + b.y, a.z + b.z}
}
func Sub(a, b Vector) Vector {
return Vector{a.x - b.x, a.y - b.y, a.z - b.z}
}
func Dist(a, b Vector) float64 {
v := Sub(a, b)
return math.Sqrt(v.x*v.x + v.y*v.y + v.z*v.z)
}
type Block struct {
location Vector // x,y,z within the chunk
name string
durability int
textureid int
breakable bool
visible bool
typ int
}
type Type int
const (
Zombie Type = iota
Chicken
Exploder
TallCreepyThing
)
type Entity struct {
location Vector
name string
health int
speed Vector
typ Type
}
func NewEntity(location Vector, typ Type) *Entity {
e := &Entity{
location: location,
typ: typ,
}
switch typ {
case Zombie:
e.name = "Zombie"
e.health = 50
e.speed = Vector{0.5, 0.0, 0.5} //slow, can't fly
case Chicken:
e.name = "Chicken"
e.health = 25
e.speed = Vector{0.75, 0.25, 0.75} //can fly a bit
case Exploder:
e.name = "Exploder"
e.health = 75
e.speed = Vector{0.75, 0.0, 0.75}
case TallCreepyThing:
e.name = "Tall Creepy Thing"
e.health = 500
e.speed = Vector{1.0, 1.0, 1.0} //does what he wants
}
return e
}
func (e *Entity) updatePosition() {
// Complex movement AI
rndUnitIshVector := Vector{1, 1, 1}
movementVector := Mult(rndUnitIshVector, e.speed)
e.location = Add(movementVector, e.location)
}
const NUM_BLOCKS = 65536 //just like minecraft!
const NUM_ENTITIES = 1000
type blockID byte
type Chunk struct {
blocks []blockID
entities []*Entity
location Vector // x,y,z within world
}
func NewChunk(location Vector) *Chunk {
c := &Chunk{location: location}
// Preallocate the growable List because we are clever!
c.blocks = make([]blockID, NUM_BLOCKS)
for i := range c.blocks {
c.blocks[i] = blockID(i)
}
c.entities = make([]*Entity, 0, NUM_ENTITIES)
for i := 0; i < NUM_ENTITIES/4; i++ {
// Fancy proc gen initial position equation
f := float64(i)
c.entities = append(c.entities, NewEntity(Vector{f, f, f}, Chicken))
c.entities = append(c.entities, NewEntity(Vector{f + 2, f, f}, Zombie))
c.entities = append(c.entities, NewEntity(Vector{f + 3, f, f}, Exploder))
c.entities = append(c.entities, NewEntity(Vector{f + 4, f, f}, TallCreepyThing))
}
return c
}
func (c *Chunk) processEntities() {
for _, e := range c.entities {
e.updatePosition()
}
}
const CHUNK_COUNT = 100
type Game struct {
chunks []*Chunk
playerLocation Vector
chunkCounter int
blocks []Block
}
func (g *Game) loadWorld() {
g.chunks = make([]*Chunk, CHUNK_COUNT)
for i := range g.chunks {
g.chunks[i] = NewChunk(Vector{float64(g.chunkCounter), 0, 0})
g.chunkCounter++
}
g.blocks = make([]Block, NUM_BLOCKS)
for i := range g.blocks {
g.blocks[i] = Block{
location: Vector{float64(i), float64(i), float64(i)},
name: "Block:" + strconv.Itoa(i),
durability: 100,
textureid: 1,
breakable: true,
visible: true,
typ: 1,
}
}
}
func (g *Game) updateChunks() {
var toRemove []int
for i, c := range g.chunks {
c.processEntities()
chunkDistance := Dist(c.location, g.playerLocation)
if chunkDistance > CHUNK_COUNT {
toRemove = append(toRemove, i)
}
}
for _, v := range toRemove {
g.chunks[v] = NewChunk(Vector{float64(g.chunkCounter), 0, 0})
g.chunkCounter++
}
}
func main() {
h := NewHistogram(time.Millisecond, 2*time.Millisecond, 5*time.Millisecond, 10*time.Millisecond, 100*time.Millisecond)
game := &Game{}
fmt.Println("Loading World...")
start := time.Now()
game.loadWorld()
loadWorldTime := time.Since(start)
fmt.Println("FINISHED!")
fmt.Printf("Load Time: %s\n", loadWorldTime)
// Game Loop, you can never leave
var frames int
for {
// check for dead entities
start = time.Now()
// mocking polling of the VR controller
playerMovement := Vector{0.1, 0, 0}
game.playerLocation = Add(game.playerLocation, playerMovement)
game.updateChunks()
t := time.Since(start)
h.Inc(t)
if frames%60 == 0 {
fmt.Println(h)
}
//fmt.Println(frames, t)
// Lock it at 60FPS
if t < 16*time.Millisecond {
time.Sleep(16*time.Millisecond - t)
}
frames++
}
}
type Histogram struct {
buckets []time.Duration
count []int
total int
}
func NewHistogram(buckets ...time.Duration) *Histogram {
return &Histogram{
buckets: append(buckets, time.Second),
count: make([]int, len(buckets)+1),
}
}
func (h *Histogram) Inc(v time.Duration) {
for i, b := range h.buckets {
if v > b {
continue
}
h.count[i]++
h.total++
return
}
}
func (h *Histogram) String() string {
total := float64(h.total)
s := ""
for i, c := range h.count {
s += fmt.Sprintf("%6s : %d/%d (%2.1f%%)\n", h.buckets[i], c, h.total, float64(c*100)/total)
}
return s
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment