views:

278

answers:

6

I'm developing a 2D overhead shooter game using C# and XNA. I have a class that I'll call "bullet" and need to update many of these instances every fraction of a second.

My first way to do this was to have a generic List of bullets and simply remove and add new bullets as needed. But in doing so the GC kicks in often and my game had some periodic jerky lag. (Alot of code cut out, but just wanted to show a simple snippet)

if (triggerButton)
{
    bullets.Add(new bullet());
}
if (bulletDestroyed)
{
    bullets.Remove(bullet);
}

My second and current attempt is to have a separate generic Stack of bullets that I push to when I'm done with a bullet, and pop off a bullet when I need a new one if there's anything in the stack. If there's nothing in the stack then I add a new bullet to the list. It seems to cut the jerky lag but then again, sometimes there's still some jerky lag springing up (though I don't know if it's related).

if (triggerButton)
{
    if (bulletStack.Count > 0)
    {
        bullet temp = bulletStack.Pop();
        temp.resetPosition();
        bullets.Add(temp);
    }
    else
    {
        bullets.Add(new bullet());
    }
}
if (bulletDestroyed)
{
    bulletStack.Push(bullet);
    bullets.Remove(bullet);
}

So, I know premature optimization is the root of all evil, but this was very noticeable inefficiency that I could catch early (and this was before even having to worry about enemy bullets filling the screen). So my questions are: Will pushing unused objects to a stack invoke the garbage collection? Will the references by kept alive or are objects still being destroyed? Is there a better way to handle updating many different objects? For instance, am I getting too fancy? Would it be fine to just iterate through the list and find an unused bullet that way?

+10  A: 

There are a lot of issues here, and it's tricky to tell.

First off, is bullet a struct or a class? If bullet is a class, any time you construct one, then unroot it (let it go out of scope or set it to null), you're going to be adding something the GC needs to collection.

If you're going to be making many of these, and updating them every frame, you may want to consider using a List<bullet> with bullet being a struct, and the List being pre-allocated (generate it with a size large enough to hold all of your bullets, so it's not being recreated as you call List.Add). This will help dramatically with the GC pressure.

Also, just because I need to rant:

So, I know premature optimization is the root of all evil, but this was very noticeable inefficiency

Never, ever, be afraid to optimize a routine that you know is causing problems. If you're seeing a performance issue (ie: your lags), this is no longer premature optimization. Yes, you don't want to be optimizing every line of code, but you do need to optimize code, especially when you see a real performance issue. Optimizing it as soon as you see it's a problem is much easier than trying to optimize it later, as any design changes required will be much more easily implemented before you've added a lot of other code that uses your bullet class.

Reed Copsey
Thanks for the reply. Yes, it's a class. It needs to be a class as it's inheriting from another class.Are you saying the way I'm currently doing it will still invoke the garbage collection? I didn't know List gets recreated when Add is called, I thought it simply added a new reference to another object...
Bob
structs have many nuances to account for, but as long as he keeps the structure small (few members) this could be a great option. Also, `List.RemoveAll` offers improved performance over multiple calls to `List.Remove` (O(N) instead of O(MN), with N being the number of items in the list and M being the number of removed items).
280Z28
I agree that optimization is sometimes warranted inline (i.e. it's not always premature to do it now instead of later). However, I emphasize the caveat that you need to ensure you have *diagnosed an actual problem* before making changes. Premature optimization is less about when to do something and more about *if* you should do it.
Bryan Watts
@Bob: Unfortunately, for games, sometimes you need to make some sacrifices for optimization purposes, as games (unlike business apps) really need good perf. characteristics. There are other options - such as not removing bullets from your list, but rather just deactivating them, and, every few frames, recreating your list, etc. Otherwise, a large number of relatively small classes can cause quite a bit of GC pressure in a game...
Reed Copsey
@Bryan: That's why I included the bolded line "you know is causing problems" - once you know there is actually a problem, it's no longer premature opt. in my opinion, but rather something that is a problem, and will at some point need addressing. If you're working on the code now, you'll be more efficient in your optimizations now.
Reed Copsey
@Bob: I highly recommend reading Rico Mariani's value type programming quiz (+answers). It's very relevant to your situation: http://blogs.msdn.com/ricom/archive/2006/08/31/733887.aspx
Reed Copsey
Thanks for all your help so far. That link is an interesting read. I realize now that List in .NET is, I guess, not a linked list. I had just assumed it was. Would changing to a LinkedList be better? At any rate, I'm going to stew on the situation and try some of this out tonight so I won't be marking any replies as the answer until later today :)
Bob
@Bob: Stack/Queue are based on LinkedList. It depends on how you're using it. If you're adding and removing a lot, and only iterating through the whole list, but never accessing by index, LinkedList<T> will be MUCH faster than List<T>...
Reed Copsey
@Bob: LinkedList<T>.Add/Remove are O(1), and iteration is fast. Accessing by index is O(N), though, so it's very slow... List<T>, on the other hand, is just an array internally, so index access is O(1), but add/remove are O(N) (add/remove at the end is fast, in the beginning is very slow).
Reed Copsey
I don't access by index, just iterate, so it may be a good way to go? I was using List in thinking it was a linked list, but I was wrong about that. LinkedList creates nodes right? So will adding or removing from LinkedList still invoke the GC due to removing nodes?
Bob
Since you're using classes, any removal will potentially add pressure to the GC. (It doesn't "trigger" the GC, just adds extra things to later cleanup). However, LinkedList<T> will probably perform much better in this case. That being said, I'd still highly suggest looking into using structs for this, since you're probably going to be doing many, many updates per frame. This can dramatically reduce your overhead.
Reed Copsey
Syndog
+2  A: 

I will admit that I don't have any experience in this per se, but I would consider using a traditional array. Initialize the array to a size that is more then you need, and would be the theoretical maximum number of bullets, say 100. Then starting at 0 assign the bullets at the beginning of the array, leaving the last element as a null. So if you had four active bullets your array would look like:

0 B 1 B 2 B 3 B 4 null ... 99 null

The benefit is that the array would always be allocated and therefore you are not dealing with the overhead of a more complex data structure. This is actually fairly similar to how strings work, since they are actually char[] with a null terminator.

Might be worth a shot. One downside, you'll have to do some manual manipulation when removing a bullet, probably move everything after that bullet up a slot. But you are just moving pointers at that point, so I don't think it would have a high penalty like allocating memory or a GC.

Andrew Dunaway
You can do this with List<T>, too, and have cleaner usage semantics. Just allocate with new List<bullet>(initialSize), and it will pre-allocate an array internally, but also allow it to grow as needed.
Reed Copsey
That's a good point. The memory overhead of the data structure would be greater, but you would get stronger readability with a List. Since memory is not what is being optimized the List would probably be the better choice.
Andrew Dunaway
+4  A: 

You may find the flyweight design pattern useful. There need be only one bullet object, but multiple flyweights may specify different positions and velocities for it. The flyweights can be stored in a preallocated array (say, 100) and flagged as active or not.

That should eliminate garbage-collection completely, and may reduce the space necessary for tracking each bullet's malleable properties.

Steven A. Lowe
Although, I would suspect that a bullet is probably nothing more than a position, velocity, and mass (or type enum) - in which case, a flyweight may be just as "heavyweight" as the original object...
Reed Copsey
@[Reed Copsey]: entirely possible. But then again, they may have a texture-map, colors, sound effects, etc. also.
Steven A. Lowe
+2  A: 

You are correct in assuming that by keeping the unused Bullets in a Stack prevents them from being Garbage Collected.

As for the cause of the Lag, have you tried any profiling tools? Just to find where the problem is.

TJMonk15
Thanks, I haven't tried profiling tools yet. Do you have any recommendations? I'm using Visual C# Express
Bob
I don't usually use any tools so I cant recommend any. (I'm sure someone here on SO could :) ) I usually just use Console.Writelines with time stamps/time spans to determine where the code is bogging down.
TJMonk15
A few popular profiling tools are Ants and dotTrace. They both cost money, but you could probably do the 30 day trial for this one case:http://www.red-gate.com/products/ants_performance_profiler/index.htmhttp://www.jetbrains.com/profiler/index.html
Andrew Dunaway
+1  A: 

List actually has a built in capacity to prevent allocation for every add/remove. Once you exceed the capacity, it adds more ( I think I doubles every time ). The problem may be more on remove than add. Add will just drop on at the first open spot which is tracked by size. To remove, the list has to be condensed to fill in the now empty slot. If you are always removing for the front of the list, then every element needs to slide down.

A Stack still uses an array as its internal storage mechanism. So you are still bound by the add/remove properties of an array.

To make the array work, you need to create all the bullets up from with an Active property for each. When you need a new one, filled the Active flag to true and set all of the new bullets properties. Once complete, flip the Active flag false.

If you wanted to try to eliminate the need to iterate the list ( which could be very large depending on what you are going to allow ) for each repaint, you could try to implement a double linked list within the array. When a new bullet is needed, asked the array for the first available free entry. Go to the last active bullet ( a variable ) and add the new bullet array position into its next active bullet property. When it is time to remove it, go to the previous bullet and change its active bullet property to the removed next active.

//I am using public fields for demonstration.  You will want to make them properties
public class Bullet {
  public bool Active;
  public int thisPosition;
  public int PrevBullet = -1;
  public int NextBullet = -1;
  public List<Bullet> list;

  public void Activate(Bullet lastBullet) {
    this.Active = true;
    this.PrevBullet = lastBullet.thisPosition;
    list[this.PrevBullet].NextBullet = this.thisPosition;
  }

  public void Deactivate() {
    this.Active = false;
    list[PrevBullet].NextBullet = this.NextBullet;
    list[NextBullet].PrevBullet= this.PrevBullet;
  }
}

That way, you have a prebuilt array with all the needed bullets but the paint only hits the bullets that are active regardless of their position within the array. You just need to maintain a link to the first active bullet to start the paint and the last active bullet to know where list starts anew.

Now you are just worried about the memory to hold the entire list instead of when the GC is going to clean up.

JDMX
+1  A: 

Your stack based solution is pretty close to a class I wrote to generically do this sort of resource pooling:
http://codecube.net/2010/01/xna-resource-pool/

You mentioned that this makes the problem mostly go away, but it still crops up here and there. What's happening is that with this stack/queue based method of pooling, the system will reach a point of stability once you are no longer requesting more new objects than the pool can supply. But if the requests go higher than your previous max # of requested items, it will cause you to have to create a new instance to service the request (thus invoking GC from time to time).

One way you can side-step this is to go through and pre-allocate as many instances as you think you might need at the peak. That way, you won't have any new allocations (at least from the pooled objects), and the GC won't be triggered :-)

Joel Martinez