speps/LibTessDotNet

pool performance (new T) and bugs (not balanced alloc/free)

Pro100AlexHell opened this issue · 19 comments

Hello! I use some port of libtessdotnet, it's like this repo. My current version is upgraded to totally pool, and very much measured and compared. I must write some points to everyone who use this repo:

  1. don't use allocator from this repo without change "new T" to Func.. new T is Activator.CreateInstance which is very slow. If you use pool with new T - it will be slower than not use pool at all! Take param "Func construct" in constructor() and invoke in Create().

  2. disable debug checks (public void Check(), but also all Assert). I use version in unity3d and not delete of this methods is decrease performance.

  3. struct EdgePair is allocated on heap due to lifetime, and don't have Pooled, it increases GC totally. Change it to class and make Pooled, and add logic for take and return it to pool.

  4. Edge allocations count don't match free count. I have measured it in my pool counter - it's totally disbalanced.
    Vertex and Face allocations don't free the head and minor disbalanced.
    Correct logic should be in Mesh.OnFree() (copy from my implementation)

public void Reset()
        {
            MeshUtils.Face f = _fHead._next;
            while (true)
            {
                MeshUtils.Face fNext = f._next;
                _pooledAllocatorsCollection.FaceAllocator.Free(f);
                if (f == _fHead) break;
                f = fNext;
            }
            MeshUtils.Vertex v = _vHead._next;

            while (true)
            {
                MeshUtils.Vertex vNext = v._next;
                _pooledAllocatorsCollection.VertexAllocator.Free(v);
                if (v == _vHead) break;
                v = vNext;
            }

            for (int i = 0; i < _allEdgePairs.Count; i++)
            {
                MeshUtils.EdgePair pair = _allEdgePairs[i];

                MeshUtils.Edge e = pair._e;
                if (!e.IsReturnedToPool()) // (can be Free in KillEdge)
                {
                    _pooledAllocatorsCollection.EdgeAllocator.Free(e);
                }

                MeshUtils.Edge eSym = pair._eSym;
                if (!eSym.IsReturnedToPool()) // (can be Free in KillEdge)
                {
                    _pooledAllocatorsCollection.EdgeAllocator.Free(eSym);
                }

                _pooledAllocatorsCollection.EdgePairAllocator.Free(pair);
            }
            _allEdgePairs.Clear();

            _vHead = null;
            _fHead = null;
            _eHead = _eHeadSym = null;
        }

// in Edge class
public bool IsReturnedToPool()
            {
                return (_pair == null);
            }

// Reset corrected
public override void Reset()
            {
// NOT THIS _pair.Reset(); BUT NEXT:
_pair = null;
..
}

// MeshUtils.EdgePair.Create(); should be incapsulated in mesh and every create should add to List<> allEdgePairs; also consider MeshUtils.MakeEdge too.
speps commented

Thanks, I'll have a look at those issues. However, if you have made improvements it's nice to contribute back instead of telling people not to use the library.

Yes such things would be useful and any fixes contributed.
This is the most robust triangulator I found though is there not an optimised 2d version?

speps commented

Even without those changes, this is a fast library. It's only 2D, not sure why you would want an optimized version of it for 2D.

yes it is good.
It is 2d at core but it uses 3d vertices and projects them to 2d first right? (interpolates z?)
I was just wanting to use Vec2 essentially and strip the rest, but not had time to look properly at it

speps commented

The Z coordinate isn't touched at all by the algorithm, so just plug your Vec2 XY into it and get it back.

it's very fast, I'm using it for 2d.
I'm still not have time to port changes from my fork, but it's have point only if you have GC limits, like me, I'm using it for real-time triangulation in unity3d.

Also using for Unity3d, really appreciate this project as best I found for the purpose.
Looked at ProjectPolygon() function - it has "Compute ST bounds." section which apparently is not needed as results unused.
Also for 2d/pre-projected it can be much simpler - like:

private void ProjectPolygon()
{
  // Project the vertices onto the sweep plane
  for (var v = _mesh._vHead._next; v != _mesh._vHead; v = v._next)
  {
    v._s = v._coords.X;
    v._t = v._coords.Y;
  }
}

When using pooling you return meshes to the pool stack but you never put them on it when allocating?!
So the Mesh Pooling stack just gets bigger and bigger - full of empty meshes.

//Begining of AddContour() should be:
if (_mesh == null)
{
	if (UsePooling)
	{
		_mesh = Mesh.Create();
	}
	else _mesh = new Mesh();
}

//Then Mesh class needs modifying to Reset fully (do as per constructor)
speps commented

As I said before, I would gladly accept a patch as I don't have much time to work on it at the moment. The author of the current issue just kept his changes to himself for some reason.

It took me too long to fix but Alex's point 4 is key - The pooling system is just not releasing everything and should not be used.
Your example code helped me fix it thanks Alex.
Perhaps I can push a fix...

Going back to Your origional points @Pro100AlexHell before I take a break from spamming this thread!

"1. don't use allocator from this repo without change "new T" to Func.. new T is Activator.CreateInstance which is very slow..."

Surely this only matters in extreme performance case - if the pools are working properly you will not allocate much except at the beginning before the pools reach a peak size?

"2. disable debug checks (public void Check(), but also all Assert). I use version in unity3d and not delete of this methods is decrease performance."

Yes best if the debug calls were only included via a define

"3. struct EdgePair is allocated on heap due to lifetime, and don't have Pooled, it increases GC totally. Change it to class and make Pooled, and add logic for take and return it to pool."

Indeed I noticed, but some other classes also if you want zero garbage - see Node/ActiveRegion

"4. Edge allocations count don't match free count. I have measured it in my pool counter - it's totally disbalanced..."

This is the most serious issue.
I initially tried to duplicate what you did and it almost worked but some leaks would still occur for certain shapes (like with intersections).
As a hack I tracked the used instances in all Pools and made sure they are released in Mesh OnFree

A perfect solution would be to ensure that all pooled objects are actually released back to the pool when finished with, but I fear it would take me too long to track where things are slipping through the gaps.

I'm making a patch now..

My implementation use PooledAllocatorCollection, for thread-safety, and some other optimizations (no projections, no x,y,z struct, etc), but not all of users need it, and I don't include this.

My patch is only for fix 3-4 issues: make EdgePair poollable + balance pool create/free.

Surely this only matters in extreme performance case - if the pools are working properly you will not allocate much except at the beginning before the pools reach a peak size?
If you don't need extreme performance - it's ok. If you need - you also can pre-warm the pools by first big mesh, if you don't do this - the 1st freeze will not be the last, the 2nd will be then the big mesh appar (in mid of gameplay for instance).

I have made a patch.
#16

I have test pool balance, it's ok.
And disabled by:
-- uncommented this check in TessBed/MainForm.cs
-- commented //#define DEBUG_CHECK_BALANCE // todo in MeshUtils.cs

I have tested benchmark in TessBed - 100 loops - release mode:
new https://prnt.sc/is8jgc
old http://prntscr.com/is8l0f
performance improvements also exist.

In unity this patch is must have for avoid GC. My game previously have 100-200MiB of garbage per conversion process of a pack of svg's.

I have not included another pools, it's very difficult to extract all of this, but in my project it's totally poollable, for example:
ActiveRegion, PriorityHeap.HandleElem, Dict.Node, int[] for triangle indices, Vertex2[] for vertices.

speps commented

I got your changes, fixed them up a bit, the debug stuff should be in the unit tests for example.

It's submitted in bc0468d.

speps commented

Thanks a lot for sending a PR by the way. Close the issue if you're satisfied with it for now.

Thanks guys this looks good ( better than my attempt )

"I have not included another pools, it's very difficult to extract all of this, but in my project it's totally poollable, for example:
ActiveRegion, PriorityHeap.HandleElem, Dict.Node, int[] for triangle indices, Vertex2[] for vertices."

Might be worth forking a Unity optimised version if you had time.

speps commented

I just submitted a new change 76fc448. It fixes the pooling entirely and allows it to be extended to more types (which I'll do myself too in a next change). I found the original reason why the new/free was mismatched and removed redundant code. It now pools the intersection vertices too.

@Pro100AlexHell can you test this new change please?

EDIT: it seems a lot slower, I'll investigate as it's not normal that it's that slow.

speps commented

I found why it was slow, it was only in unit tests as I was storing callstacks for easier debugging. It's now removed in b8a2bc8.