Why removeFromParent()/removeChild() Could Be Dangerous?

OK i will rephrase the Question:

Why removeFromParent()/removeChild() could be dangerous in big heavy scenes?

we all know that nodes children are maintained inside a vector like this:

// brief version of CCNode.h
class Node: public Ref {
	...
	protected:
	cocos2d::Vector<cocos2d::Node*> _children;        ///< array of children nodes
	....
};

i will first start explaining the problem by digging into addChild() code. and we all know that when calling addChild() method to add a child node to a parent node this code gets triggered:

i will highlight only the important parts of the methods in here:

void Node::addChild(Node *child, int localZOrder, int tag)
{    
	...
    addChildHelper(child, localZOrder, tag, "", true);
	...
}

which as you see internally calls addChildHelper()

void Node::addChildHelper(Node* child, int localZOrder, int tag, const std::string &name, bool setTag)
{
	...
    this->insertChild(child, localZOrder);
	...
}

which internally calls insertChild()

void Node::insertChild(Node* child, int z)
{
	...
    _children.pushBack(child);
	...
}

which finally calls pushBack()

now that we know the implantation of addChild().let us assume that we are adding say 5000 node to a layer which is basically a node.

no problems wright! 5000 node will be added to the layer.

know lets assume that some nodes are dying and we have to use removeFromParent() to remove them, that should be simple write! … Big No?

and in order to see whats happening loud and clear we’ll have to dive in to the code one more time to illustrate the problem but this time its removeFromParent() code:

void Node::removeFromParent()
{
    this->removeFromParentAndCleanup(true);
}

internally calls removeFromParentAndCleanup()

void Node::removeFromParentAndCleanup(bool cleanup)
{
	_parent->removeChild(this,cleanup);
}

internally calls removeChild()

void Node::removeChild(Node* child, bool cleanup /* = true */)
{
	...
    this->detachChild( child, index, cleanup );
	...
}

internally calls detachChild()

void Node::detachChild(Node *child, ssize_t childIndex, bool doCleanup)
{
	...
    _children.erase(childIndex); // bad very bad idea
	...
}

which finally calls vectore::erase()

well it’s not that bad until you realize that vectors are very bad at removing or inserting things other than the end.

still not convinced why its bad? OK, what if the dying node that we want to remove is in the middle of the vector or worse in the front of the vector. and combining that with the ugly truth of vectors which is every time an item is removed, all items after that one are moved/copied one position to the front and in the end you will simply get a disaster?!

knowing the fact that cocos2d relies heavily on the use of nodes imagine how many copies will be made in big scenes? or what will happen in a fast pace game?

you can read all about the consequences more in this article along with their proposed solutions just to widen your vision a bit:

so know as you see every time you call removeFromParent() it doesn’t just remove it also shifts children elements. this is especially important on mobile devices where resources are limited (like cpu cycles) and too valuable to waste on unnecessary easy to avoid copying.

of course this won’t hurt in small light weight scenes but in big projects with big scenes this might come back and bite you.

and you might justify its safety by the fact that pointers are cheap to copy i mean they are just 4 bytes in size in case of 32 bit machines and 8 bytes for 64 bit machines and your right they are cheap to copy and here is even a confirmation in this book which confirms it but still that’s not an excuse for bad behavior at least not for me:
https://books.google.be/books?hl=ar&id=XclHqTM37UIC&q=copy+overhead#v=snippet&q=copy%20overhead&f=false

or you might justify its safety with the other fact that you can always override the default behavior but what about newcomers that are not fully aware of the risks of the default behavior and they will eventually pay for what they didn’t buy.

one last thing even if this is not bad as i might think just why opening the door for a potential future performance bottlenecks and just why not be on the safe side of things and use a good future proof remove mechanism (as there are lots out there to choose from) as a default behavior that will work for small or big scenes without having to override … why not do it even just for the sake of good design point of view.

P.S: This is a warning more than a question but i would like to hear your opinions on this matter and if it’s worth a pull request?

3 Likes

Let us tag @ricardo and @zhangxm, they would have great input here. If @stevetranby isn’t busy too :slight_smile:

Ah, I forgot to mention the main reason to argue for using std::vector.

All aspects of the container must be considered.
Iterating - on visit every frame (or multiple times per frame depending on camera or material passes)
Inserting - not common
Adding (at end) - infrequent for most games during game loop (done mostly at init)
Remove - infrequent or low count per frame for most games (don’t remove often)

Therefore the _children container type was chosen mostly based on iteration.


My previous replies with useless information removed:

I do know the core developer team (ricardo, et al) do care about performance and are always interested in PRs or discussions around how things could be improved.

Cocos2d is not setup for you to make a game that requires 5000 nodes in this manner. For high turnover collections (high element count with frequent modification) of nodes a Pool is actually a better pattern since it reuses instead of destroying/creating memory. See particle system implementations.
http://gameprogrammingpatterns.com/optimization-patterns.html .

Fast pace games usually require some custom “node” management depending on the game and the minimum device requirements chosen.

That said if you look at AAA games they all use their own container types, pools, freelist allocators, and other means. EASTL is an example of these types of containers and may be beneficial to cocos2d in the future.

I’m sure there’s plenty that could be improved in the engine for performance, but the current model is not dangerous, per se, but rather a reasonable solution for the design target of small to medium-core games.

@Joseph39 yep, it has an issue for your situation. As @stevetranby said, it is better to use our own container that will reuse memory as possible. And we created Allocator ago though it is not used by default for some reason.

If you have a solution that will improve the performance without bring too much complicated and break compatibility, feel free to send a pull request.

You are forgetting one very important point: Data locality
http://gameprogrammingpatterns.com/data-locality.html

It is a trade-off. We chose to be faster at iterating the vector of nodes than adding/removing them.
If are going to add/remove hundreds of nodes per frame, probably you should “enable/disable” them instead of adding/removing it

+1 for this.

Thank you all for your clarification, But sadly that doesn’t change the fact that there is unnecessary easy to avoid copying.

and what really bothers me is that fixing/improving it is so easy that it blurs out any excuse for not doing it?!

We welcome pull requests! We succeed because our community gets involved.

@Joseph39 - you’re under the false assumption that changing vector to list would fix/improve the engine, but it would not except possibly in specific rare cases.

i never suggested a different container i only suggested a better remove mechanism while keeping the same container.
a better more efficient remove mechanism would be the swap and pop mechanism (breaks order) which is very easy to apply and very efficient (no copying).
and another would be the remove - erase idiom (keeps order intact and no copying).

@Joseph39 - ah, sorry I guess I didn’t fully understand your initial reply then as it seemed like mainly you were discussing a switch to using the list container over vector.

Order is important for children because by default children draw themselves back to front based on when they’re added.

If you have or create a modification or new Vector<> implementation that draws correctly, again, feel free to submit the PR for it or post pseudo-code or code here.

There are potential enhancements for removal, but I still think that managing large children count separately is the better implementation using either a custom node type that renders all children efficiently (ParticleSystem, BatchNode, TileMap, etc) or by using a custom node type that pools its children and overrides visit to ignore inactive objects.