Ebiten: Performance optimization: avoid allocating vertices when possible

Created on 26 Jan 2019  路  9Comments  路  Source: hajimehoshi/ebiten

@silbinarywolf pointed out that the below code allocates vertices:

func (v *verticesBackend) slice(n int) []float32 {
    const num = 1024
    if n > num {
        panic("not reached")
    }

    v.m.Lock()

    need := n * VertexFloatNum
    if v.head+need > len(v.backend) {
        v.backend = nil
        v.head = 0
        log.Printf("Reset backend. Need more vertices. have: %d, need: %d\n", len(v.backend), v.head+need)
    }

    if v.backend == nil {
        v.backend = make([]float32, VertexFloatNum*num)
        log.Printf("Create more vertices: %d\n", VertexFloatNum*num)
    }

    s := v.backend[v.head : v.head+need]
    v.head += need

    v.m.Unlock()
    return s
}

and was wondering why we cannot reuse vertices slices. My answer is that vertices slices might be recorded for restoring from the context lost.

However, there might be better way to reduce allocation. I'll investigate later.

performance

Most helpful comment

It seems to have resolved the issue. Thanks!

All 9 comments

I've experimented with doing the following.
It reduces the amount of allocations by using the previous v.backend size when allocating a new one.

func (v *verticesBackend) slice(n int) []float32 {
    const num = 1024 // 1024
    if n > num {
        panic("not reached")
    }

    v.m.Lock()

    need := n * VertexFloatNum
    nextAllocSize := VertexFloatNum * num
    if v.head+need > len(v.backend) {
        nextAllocSize = len(v.backend) + v.head + need
        log.Printf("Reset backend. Need more vertices. have: %d, need: %d\n", len(v.backend), v.head+need)
        v.backend = nil
        v.head = 0
    }

    if v.backend == nil {
        v.backend = make([]float32, nextAllocSize)
        log.Printf("Create more vertices: %d\n", VertexFloatNum*num)
    }

    s := v.backend[v.head : v.head+need]
    v.head += need

    v.m.Unlock()
    return s
}

Rather than my game printing a resetting of the backend vertices constantly and all within the same second:

2019/01/26 18:12:55 Reset backend. Need more vertices.
2019/01/26 18:12:55 Reset backend. Need more vertices.
2019/01/26 18:12:56 Reset backend. Need more vertices.
2019/01/26 18:12:56 Reset backend. Need more vertices.
2019/01/26 18:12:56 Reset backend. Need more vertices.
2019/01/26 18:12:56 Reset backend. Need more vertices.
2019/01/26 18:12:56 Reset backend. Need more vertices.
2019/01/26 18:12:56 Reset backend. Need more vertices.

It becomes at least a few seconds apart:

2019/01/26 18:35:26 Create more vertices: 12288
2019/01/26 18:35:28 Reset backend. Need more vertices. have: 196560, need: 196608
2019/01/26 18:35:28 Create more vertices: 12288
2019/01/26 18:35:30 Warning: Rendering is slow, skipping render this frame
2019/01/26 18:35:31 Reset backend. Need more vertices. have: 393168, need: 393216
2019/01/26 18:35:31 Create more vertices: 12288
2019/01/26 18:35:37 Reset backend. Need more vertices. have: 786384, need: 786432
2019/01/26 18:35:37 Create more vertices: 12288

Thanks!

2019/01/26 18:35:30 Warning: Rendering is slow, skipping render this frame

Who shows this log?

Oh. Thats from my engine code. I temporarily have this line in my local copy only.

if ebiten.IsDrawingSkipped() {
   log.Printf("Warning: Rendering is slow, skipping render this frame\n")
   return
}

nextAllocSize = len(v.backend) + v.head + need

This means that the allocation size will be increased. This actually reduces the chance of allocating, but I don't think this is a good solution...

Ebiten seems to allocate a lot of vertices when drawing my game. This is from the output of running with the ebitendebug tag:

draw-triangles: dst: 8 <- src: 4, num of indices: 13392, colorm: <nil>, mode source-over, filter: nearest, address: unsafe

Meanwhile the memory usage continues to increase. go tool pprof -sample_index=inuse_space -list slice shows that (*verticesBackend).slice is using up lots of memory:

Total: 1.35GB
ROUTINE ======================== github.com/hajimehoshi/ebiten/internal/mipmap.(*verticesBackend).slice in ~/go/pkg/mod/github.com/hajimehoshi/[email protected]/internal/mipmap/vertex.go
    1.12GB     1.12GB (flat, cum) 83.43% of Total
         .          .     38:   need := n * graphics.VertexFloatNum
         .          .     39:   if l := len(v.backend); v.head+need > l {
         .          .     40:           for v.head+need > l {
         .          .     41:                   l *= 2
         .          .     42:           }
    1.12GB     1.12GB     43:           v.backend = make([]float32, l)
         .          .     44:           v.head = 0
         .          .     45:   }
         .          .     46:
         .          .     47:   s := v.backend[v.head : v.head+need]
         .          .     48:   if last {

This is using the latest version, v1.12.0-alpha.7

I was able to solve the issue of steadily increasing memory usage by allocating a new slice of float32s in quadVertices instead of calling vertexSlice.

- vs := vertexSlice(4, last)
+ vs := make([]float32, 4*graphics.VertexFloatNum)

Is there a reason that vertexSlice should be called instead? And if there is, is there a way to prevent vertexSlice from allocating so much memory? Perhaps there should be a limit on how many vertices can be allocated.

This is using the latest commit on the master branch.

Is there a reason that vertexSlice should be called instead? And if there is, is there a way to prevent vertexSlice from allocating so much memory? Perhaps there should be a limit on how many vertices can be allocated.

This is especially for GopherJS where allocating arrays by make was expensive.

Now the usage of GopherJS has been replaced with WebAssembly, we might be able to replace this with a simple make. Let me think.

@nanoslayer I've updated the master branch. Does the fix work on your environment?

It seems to have resolved the issue. Thanks!

Was this page helpful?
0 / 5 - 0 ratings