1

I am writing a simulation which has an update loop that's called every frame. In the update function, I have millions of objects to update, so it looks like this.

public void Update(float deltaTime)
{
    Time += deltaTime;
    foreach (SimulationObject obj in objects.ToArray())
        obj.Update(deltaTime);
}

where objects is
List<SimulationObject> objects = new List<SimulationObject>();
and is populated at program initialization time.

You can probably see objects.ToArray() makes a copy of this huge list every frame then the copy gets garbage-collected. This causes huge performance issue for me. For a run of about 2 minutes, the garbage collected reaches about 2G. Since the list of this objects is being asynchronously modified in the background by some third-party library, it seems I can't remove ToArray().

I am wondering if there's a good way to reduce garbage collection, either avoid copying or reuse the allocated space?

I am new to C# and tried searching for answers, but was not able to. If this is a duplicated post, I apologize.

Dave
  • 239
  • 1
  • 2
  • 6
  • 2
    FYI .ToArray() makes a copy of the collection though the objects in the collection aren't copied http://referencesource.microsoft.com/#System.Core/System/Linq/Enumerable.cs,783a052330e7d48d – Eminem Apr 03 '16 at 06:08
  • Please see the answer at: [Prevent garbage collection for a short period of time](http://stackoverflow.com/questions/6005865/prevent-net-garbage-collection-for-short-period-of-time) – Eminem Apr 03 '16 at 06:10
  • 1
    @Eminem - assuming `SimulationObject` is a `class` (reference type), not a `struct` (value type). – Corak Apr 03 '16 at 06:13
  • It's hard to imagine, how exactly you reach about 2 GB of garbage that fast. If `SimulationObject` is a `class`, then you just copy references. Not sure, how that could accumulate so fast. - about reusing, you *could* have a local (on class level, outside the `Update` method) `List` that cannot be modified from elsewhere and then `localList.AddRange(objects); /* do stuff */ localList.Clear();`. In theory, you will only ever have a maximum of two lists of references to all the objects. – Corak Apr 03 '16 at 06:34
  • 3
    _"the list of this objects is being asynchronously modified in the background"_ - generally you want to avoid using threading in games/simulations for _fine-grained_ operations such as this. Consider using two lists - a _pending_ (one that is being worked on in the background) and an _active_ list (for `Update`). Then flip them around when the time is right. http://www.amazon.com/Game-Engine-Architecture-Jason-Gregory/dp/1568814135 –  Apr 03 '16 at 06:42
  • @MickyD - that actually sounds like the best way to go. – Corak Apr 03 '16 at 06:45
  • 1
    You can also take a leaf out of XNA where some many fine c# performance tips were suggested to minimise GC impact (which was particularly important for c# heritage Xbox 360 games). For example there is a measurable expense in a simple `foreach`! Check out [Shawn Hargreave's blog (Microsoft)](https://blogs.msdn.microsoft.com/shawnhar/) as well as [Riemer's XNA Tutorials](http://www.riemers.net) –  Apr 03 '16 at 06:56

3 Answers3

2

ToArray() is not really necessary here, it is just a waste of time and memory. Use the for loop. I would start with something like

for (int i = 0; i < objects.Count; i++) 
{
    SimulationObject obj = null;
    try 
    {
        obj = objects[i];
    }
    catch (ArgumentOutOfRangeException) 
    {
        // something was removed from the collection
        // we reached the end of the list
        break;
    }

    obj.Update(deltaTime)
}

This code splits obj = objects[i] and obj.Update(deltaTime) intentionally in order to avoid interrupting the loop if Update() throws ArgumentOutOfRangeException.

Be aware of two facts:

  • Some objects will skip the first iteration after they have been added to the objects collection, which should not actually be a problem, as their addition is asynchronous by design.
  • Some objects will probably be updated just once after they get deleted from objects list (naturally, simultaneously with deletion, but the outcome tends to appear like it happens "after"). Depending upon the simulation this case can be undesirable and will probably require some specific handling.
Diligent Key Presser
  • 4,183
  • 4
  • 26
  • 34
  • 1
    This actually circumvents that exception (while maybe catching a different exception). This probably does the job. – Corak Apr 03 '16 at 06:40
  • Whoops my bad. I think it would be better if the OP used thread-safe collections in the first place or maintained two lists. –  Apr 03 '16 at 06:58
  • If you loop in reverse, the likelihood of throwing an exception is reduced significantly. – egrunin Apr 03 '16 at 07:19
  • @egrunin `objects.Count` is checked on every iteration, so looping order does not make a difference, moreover in the reverse case exception will break the loop on the furst iteration, not after the last one. – Diligent Key Presser Apr 03 '16 at 07:26
0

There is no reason to call ToArray(), so you can just use

public void Update(float deltaTime)
{
    Time += deltaTime;
    foreach (SimulationObject obj in objects)
        obj.Update(deltaTime);
}
user1691896
  • 107
  • 7
  • 2
    This will throw an [InvalidOperationException](https://msdn.microsoft.com/library/system.invalidoperationexception.aspx) - "collection was modified" - if another thread modifies the collection while looping over it, like OP said was a possibility. – Corak Apr 03 '16 at 06:23
0

As someone pointed out you dont need to use toArray() method since it will just create the copy of your current list.

If I understand correctly you are doing this since your list is being modified by third party lib in the background.

I think you could safely use Parallel.ForEach loop in this case, since you are not modifying list itself, only items in the list.

      Float Time; // some value
      Float deltaTime; // some value
      Parallel.ForEach(objects, currObject =>
            {
                // do stuff
             Interlocked.increment(ref Time,deltaTime);
             currObject.Update(Time)
            }
Bola
  • 718
  • 1
  • 6
  • 20