What else can I do to improve performance in this class?

I am currently developing a 2D game with C # / XNA. A feature of the game core are bullets with completely different behavior (this will be a kind of game with bullet hell). Updating all bullets can take quite a while, as they can be infinitely complex with their behavior. And they all have to do 1 collision check. Initially, I just saved them in the list and updated them and pulled out all of them, removing inactive markers from the list in each frame. However, this quickly accelerated the game when 8k bullets appeared on the screen, so I decided to implement multithreading and use LINQ to improve performance.

The thing is still slowing down by about 16k bullets. I was told that I can reach up to 7 MILLION active bullets if I do it right, so I'm not satisfied with 16k ...

Is there anything else to improve performance here?

Additional information before the code: my bullets have fields for speed, direction, speed <500>, acceleration, speed limit and behavior. The only thing mentioned is the behavior. He can at any time modify any of the marker fields or generate more bullets and even put himself in them, so it is difficult for me to use the Data-Driven Solution and just store all these fields in arrays, and not have a bullet list.

internal class BulletManager : GameComponent { public static float CurrentDrawDepth = .82f; private readonly List<Bullet> _bullets = new List<Bullet>(); private readonly int _processorCount; private int _counter; private readonly Task[] _tasks; public BulletManager(Game game) : base(game) { _processorCount = VariableProvider.ProcessorCount; _tasks = new Task[_processorCount]; } public void ClearAllBullets() { _bullets.Clear(); } public void AddBullet(Bullet bullet) { _bullets.Add(bullet); } public override void Update(GameTime gameTime) { if (StateManager.GameState != GameStates.Ingame && (StateManager.GameState != GameStates.Editor || EngineStates.GameStates != EEngineStates.Running)) return; var bulletCount = _bullets.Count; var bulletsToProcess = bulletCount / _processorCount; //Split up the bullets to update among all available cores using Tasks and a lambda expression for (var i = 0; i < _processorCount; ++i ) { var x = i; _tasks[i] = Task.Factory.StartNew( () => { for(var j = bulletsToProcess * x; j < bulletsToProcess * x + bulletsToProcess; ++j) { if (_bullets[j].Active) _bullets[j].Update(); } }); } //Update the remaining bullets (if any) for (var i = bulletsToProcess * _processorCount; i < bulletCount; ++i) { if (_bullets[i].Active) _bullets[i].Update(); } //Wait for all tasks to finish Task.WaitAll(_tasks); //This is an attempt to reduce the load per frame, originally _bullets.RemoveAll(s => !s.Active) ran every frame. ++_counter; if (_counter != 300) return; _counter = 0; _bullets.RemoveAll(s => !s.Active); } public void Draw(SpriteBatch spriteBatch) { if (StateManager.GameState != GameStates.Ingame && StateManager.GameState != GameStates.Editor) return; spriteBatch.DrawString(FontProvider.GetFont("Mono14"), _bullets.Count.ToString(), new Vector2(100, 20), Color.White); //Using some LINQ to only draw bullets in the viewport foreach (var bullet in _bullets.Where(bullet => Camera.ViewPort.Contains(bullet.CircleCollisionCenter.ToPoint()))) { bullet.Draw(spriteBatch); CurrentDrawDepth -= .82e-5f; } CurrentDrawDepth = .82f; } } 
+6
source share
6 answers

Wow. There are a lot of errors in this code (and possibly a code that you have not published). Here's what you need to do to increase productivity, in approximately descending order of importance / necessity:

Performance measurement. At the most basic level, a frame rate counter (or, even better, a frame counter). You want to check that you are doing better.

Do not allocate memory during the game cycle. The best way to check if you are using CLR Profiler . Although you cannot use new (to allocate class types, structs are fine), this will not surprise me if most of this LINQ allocates memory overs.

Note that ToString will allocate memory. There are non-highlighting methods (using StringBuilder ) to draw numbers if you need them.

This article provides more information.

Do not use LINQ. LINQ is a simple, convenient, and absolutely not the fastest or most memory-efficient way to manage collections.

Use a data-based approach. The main idea of ​​a data-based approach is that you maintain cache consistency ( more ). That is: all of your Bullet data is stored linearly in memory. To do this, make sure Bullet is a struct and you save them in a List<Bullet> . This means that when one Bullet loaded into the CPU cache, it adds others along with it (the memory is loaded into the cache in large blocks), which reduces the time spent by the processor on waiting for the memory to load.

To quickly remove cartridges, overwrite the one you are deleting with the last bullet in the list, and then delete the last item. This allows you to delete items without copying most of the list.

Use SpriteBatch wisely for performance. Make a separate batch of sprites ( Begin()/End() block) for your bullets. Use SpriteSortMode.Deferred - this is by far the fastest mode. Sorting (as shown in your CurrentDrawDepth ) is slow! Make sure all your bullets use the same texture (use a texture atlas if necessary). Remember that batch processing is only a performance improvement if consecutive sprites use texture. ( More )

If you use SpriteBatch well, then it will probably be faster to draw all your sprites, and then let the GPU select them if they are off-screen.

(Optional) Maintain a different list for each behavior . This reduces the number of branches in your code and can potentially make the code itself (i.e. instructions, not data) more cache-coherent. Unlike the points above, this will only give a slight performance improvement, so use it if you need to.

(NOTE: Beyond this point, these changes are difficult to implement, make your code more difficult to read, and even slower. Use them only when absolutely necessary, and you measure performance.)

(Optional) Paste your code. As soon as you start getting thousands of rounds of ammo, you may need to embed your code (remove method calls) in order to squeeze even more performance. The C # compiler is not built in, and JIT only does it a bit, so you need to embed it manually. Calling methods includes things like the + and * operators that you can use on vectors - embedding these parameters will improve performance.

(Optional) Use a custom shader. If you want more performance than just using SpriteBatch , write your own shader that takes your Bullet data and calculates as much on the GPU as possible.

(Optional) Make your data even smaller and (if possible) immutable. Save the initial conditions (position, direction, time stamp) in the Bullet structure. Then use the basic equations of motion to calculate the current position / speed / etc., only if you need them. You can often get these calculations for β€œfree” - since you are probably using unused CPU time while it is waiting for memory.

If your data is immutable, you can not transfer it to the GPU in every frame! (If you add / remove markers, you will have to update it on the GPU on these frames, though).

If you completed all these elements, I think you could probably get up to 7 million bullets in a good car. Although this is likely to leave little CPU time for the rest of your game.

+7
source
  • Profile it and see where the hot spots are.
  • I doubt using Tasks gives you a performance boost. In fact, they can even slow down your game.
  • How many inactive bullets do you have? Perhaps removing them at the beginning of the game cycle will slightly improve performance.
+1
source

Why are you deleting inactive bullets?

I think that this kind of thing is often solved by the concept of "pool" - maybe I missed something from your code, but it looks like you already have the concept of active, so why delete an inactive one, then create a new bullet, which at some point will be deleted again to handle the GC. Just reuse the inactive bullet.

Also, I can't tell you how painful this is, but using ToString () in your draw 30 times per second creates garbage to clean.

+1
source

If bullet ' Update() methods are a bottleneck (make sure you do it like @PiRX and use the profiler first to find the bottlenecks), you can either:

a) Updating only visible bullets of each frame and regeneration of invisible cartridges less often.

b) Simplify the update process: say, a bullet performs its specific (time-consuming) behavior every 10 frames (every 0.5 seconds, whatever) and does a simple thing (for example, fly straight) the rest of the time.

Both offers are a compromise between performance and accuracy, of course.

0
source

At least for a bullet off-screen (or off-screen marked) you can replace all bullet use by checking when she shot because she had to hit and sending a delayed message to the hit target for which it hit the bullet N times. Then the slow message replaces all UPDATE calculations of these cartridges and still does the damage.

0
source

profiling is the best tool for finding bottlenecks, as other frets have pointed out. it is important that the Update() method is optimized as it can be.

I would also try reorganizing nested for loops to reduce the number of iterations, like this one (unchecked, from the top of my chapter code):

 _tasks.ForEach(i=> { i.Factory.StartNew(()=> { _bullets.Where(j=> _bullets.IndexOf(j)%_tasks.IndexOf(i)==0 && j.Active).Update(); } } ); 
0
source

Source: https://habr.com/ru/post/900705/


All Articles