Garbage Collection and Skipped Turns

Jump to navigation Jump to search
Revision as of 4 April 2013 at 14:42.
The highlighted comment was created in this revision.

Garbage Collection and Skipped Turns

Starting a new thread to discuss my efforts to deal with the skipped turn issue that is apparently related to garbage collection eating up allowed run time. This was previously discussed in thread "Shielding Success Rates Mystery", for anyone who wants to see where it all started.

I purposely did not contribute to the rumble over the last few days after the pairings for XanderCat 12.6 were lost. I originally ran many of the original pairings on my PC that does not have the skipped turns issue. Most of the re-run pairings were likely run by Voidious, whose system does exhibit the skipped turns issue.

The difference between the two is quite significant. With clients that don't exhibit the skipped turns issue, XanderCat achieved an APS of 87.7. With clients that do exhibit the shipped turns issue, XanderCat achieved an APS of 86.5. The difference -- 1.2 APS -- is quite significant. With the current rumble participants, it makes the difference between 5th and 8th place.

Most of the difference is due to the skipped turns causing the bullet shielding system to fail much of the time. But likely the skipped turns in general -- ignoring the bullet shielding -- also contribute a small amount.

I had previously fixed a few performance bottlenecks to make XanderCat run quite a bit faster (v 12.3), with much lower turn time peaks, but this only achieved a marginal improvement. I now need to shift to figuring out how to reduce the amount of garbage my framework apparently creates. This is not a real easy problem to address, because it is a very unique Java problem that rarely ever needs to be addressed in the real world, so there is not a lot of information or research available online to help on this.

I think one thing I can do is to eliminate as many intermediate local variables as I can. For example, variables with only method scope that are used to break something into multiple easier to maintain steps. These extra method scope variables may be contributing to the garbage collection, especially in the first round. Eliminating them may help to fix the problem, but at the expensive of either combining multiple lines together into more complex lines or making the variables have a wider than necessary scope (declaring them as part of the class), thus eating more memory overall but eliminating the possibility of it triggering garbage collection.

I don't know if these steps will help, but I will probably give it a try. I am also not sure if there are other ways to reduce garbage collection, but maybe I will come across some other ideas. I may actually create a second branch in my source tree for this work, something I never thought I would do for Robocode. I want to keep the current version, as I think it will constitute better code and perhaps someday the garbage collection issue will be addressed by changes to Robocode itself; but if my garbage reduction efforts work, for now I will operate off of a garbage reduced branch.

    Skotty22:43, 3 April 2013

    This seems like a really crappy thing to push onto you as a bot author. I do think our time is probably better spent coming up with a proposal to change Robocode itself and submitting that (as idea, design, or code) to Fnl. There could be a very simple and elegant solution that would work, like "allow 10x the CPU constant for the first 100 ticks". (Disabling CPU limits in first 100 ticks seems problematic, since you want to at least interrupt bots that hit infinite loops.) Another idea is having Robocode run its own GC cycle right before the match starts, in case bots are being penalized for GC of the game engine.

    I'll try to get to another round of tests and find out how much I need to raise the CPU constant to get normal performance out of XanderCat.

      Voidious23:39, 3 April 2013
       

      I've had to deal with this quite a bit when writing games in c#. Similar to Java the GC can case obvious stalls. The easiest way is to stop calling the "new" function at run time by using pooling. For instance at the start of a match, or a round create a container object which contains N pooled objects which you know you create often, eg wave objects. At the point you wish to use one, take it from the pool, initialise it, use it, then return it to the pool when finished at any point later on.

      Because you have not called new, and then nulled the object, the memory used does not go up, it stays constant, thus no GC is run. It's obviously impractical to pool everything so you just do the worst offenders which are things that you create often and throw away.

        Wolfman00:30, 4 April 2013

        That seems like a great approach. And you can even create the pools in a static block, which I'm pretty sure runs before the match starts and won't count against any of your CPU time.

          Voidious03:44, 4 April 2013
           

          Speaking of static blocks, I've noticed that they get run on Robocode/rumble startup for every single bot, which is partly why it takes so long to start when there are lots of bots in the /robocode/robots directory. I also suspect that code in static blocks isn't subject to the security manager, since it can print to the main console. Does somebody feel like writing a test bot to see if this theory is correct?

            Skilgannon09:56, 4 April 2013
             

            Local variables are stored in the stack and not the heap, so they don't affect garbage collection.

            You should look after "new" abuse, like Wolfman said. Although sometimes the instantiation is implicit and simply searching for the "new" keyword doesn't always work.

            There are heap profiling tools which locate automatically where too many objects are being instantiated.

              MN03:33, 4 April 2013
               

              Local variables are stored on the stack but any time you use new it will go on the heap afaik:

              public void MyFunc(Object a) {

                Object b = a; // Variable b is on the stack, pointing at a.
                b = new Object(); // Memory allocated on heap, referenced by variable b on the stack
              

              }

              This is my understanding of it. Please correct me if I am wrong!

                Wolfman06:57, 4 April 2013
                 

                This is correct.

                My understanding of the snippet above is that you have 3 variables. 2 local in the stack (references "a" and "b") and 1 in the heap (Object instance).

                Some variables stay in the stack only, like primitives (double, float, int...).

                  MN14:46, 4 April 2013

                  Where would a primitive array like an int[] end up?

                    Tkiesel15:17, 4 April 2013

                    Java treats an array as an object, so on the heap.

                    However, these days the JVM is more intelligent than you guys are giving it credit for, eg. it has Escape Analysis to determine if objects should be put on the stack if they stay local.

                      Skilgannon15:25, 4 April 2013
                       

                      Yes. However anything that you are creating during a function and keeping hold of for a few frames and then releasing is going to be allocating on the stack. Stuff like "Wave" objects, "Bullet" objects or whatever else you use in your bot will cause GC stalls if you create lots, use for a while and then null. This is where the pooling comes into force. I would definitely recommend pooling objects such as waves etc if you are having trouble with stalls and then go from there.

                      -wolfman

                        Wolfman15:32, 4 April 2013
                         

                        See, my bot always had a skipped turns problem, and now you're giving me a possible solution. You're drawing me right back in to wanting to start Robocoding again, dangit! *laughing*

                          Tkiesel15:39, 4 April 2013

                          Yes, yes, come to the dark side, make your code ugly but fast, like mine :-p

                            Skilgannon15:42, 4 April 2013
                             
                             

                            Arrays are objects in java:

                            public void func() {

                              int[] myArray; // myArray variable on the stack
                              myArray = new int[5]; // Array object allocated on heap, referenced by myArray variable on the stack
                            

                            }

                            Note that member variables of objects are obviously going to take up memory on the heap not the stack - eg if you have 30 primitive member variables (ints, doubles etc) of a class and call new on that class, it will take up more memory allocation than a class that has 1 primitive member variable.

                            However allocating 30 local primitive variables during a function call allocates those primitive types on the stack alongside you local member reference variables.

                              Wolfman15:28, 4 April 2013