The short version of the story - By setting gc=false and adding the
following parameter to the VM command line" -XX:+AggressiveHeap, ECJ used
28% less computational time. Same seed.
The performance increase is a result of instructing the JVM to use a
more efficient garbage collection algorithm.
Note, using this switch uses memory more aggressively, so you may want
to read on.
The long version of the story.
I have been using ECJ for the last year and in the past few months
became interested in increasing performance. To that end, I added timers
to each of the major operations of ECJ. Here's what a typical report my
look like:
> Java Default GC Parameters
>
> Run Time:93.94 -> 100.00%
> Setup:3.98 -> 4.23%
> Evaluate:17.94 -> 19.09%
> PostEval Stats:4.32 -> 4.60%
> Breed:67.02 -> 71.34%
> Final Stats:0.90 -> 0.96%
First number is seconds and second number is percent of CPU time.
I was surprised to see that breeding consumed such a large proportion of
the CPU consumption - 71%. Timers were added to specific functions inside
of breeding:
> Run Time:93.94 -> 100.00%
> Setup:3.98 -> 4.23%
> Evaluate:17.94 -> 19.09%
> PostEval Stats:4.32 -> 4.60%
> Breed:67.02 -> 71.34%
> --PrepareToProduce:7.47 -> 7.95%
> --Produce:59.38 -> 63.21%
> ----Produce_BreedingSources:0.78 -> 0.83%
> ----Produce_Produce:57.95 -> 61.68%
> ------CrossOverPipeline_Breed6:33.62 -> 35.79%
> -------CrossOverPipeline_Breed7_childAllocate:8.02 -> 8.54%
> Final Stats:0.90 -> 0.96%
One line of code “obj.children = new GPNode[children.length]” was found to
consume 8.54% of the CPU time. This line of code allocates memory from the
heap for tree children - a very common operation in breeding.
A first tack against this consumer was to think that dynamic object
allocation was eating processor time. This could be cured by statically
defining a collection of nodes that the GP could recycle through without
JVM memory allocation/de-allocation. This was briefly explored until
realizing that creating/maintaining such a collection would require
extensive code changes.
A second and ultimately successful tack was to consider the de-allocation
side. Was the JVM’s garbage collection consuming the processor time?
Garbage collection is performed by the JVM without needing to be
programmed directly – one of the beauties of Java – no memory leaks. In
most programs, this does not introduce performance degradation. However,
most programs don’t allocate millions of objects per second as ECJ.
While Sun Java does offer some access to performing explicit garbage
collection in code, doing this in ECJ only introduced additional
performance degradation. The other way to control GC is through JVM
command line parameters. After a bit of internet research, the following
command GC/Heap parameters were discovered and tested.
> 75000 individuls x 5gen
> Command Line Switch Total Run Time
> --none-- 11.5s
> -XX:+UseParallelGC 13.7s
> -XX:+UseConcMarkSweepGC 16.5s
> -Xincgc 16.7s
> -XX:+UseAdaptiveSizePolicy 11.6s
> -XX:+AggressiveHeap 7.2s
After determining the best command line switch, I tested combinations of
ECJ's garabge collection and the switch.
> 75000 inds x 50gen – testing switch with ECJ code GC
> Command Line Switch ECJ GC Total Run Time
> --none-- none 93.9s
> -XX:+AggressiveHeap none 62.0s
> --none-- normal 100.1s
> -XX:+AggressiveHeap normal 78.8s
> -none aggreesive 111.2s
> -XX:+AggressiveHeap aggressive 91.0s
As can be observed from the above results, using -XX:+AggressiveHeap and
turning off ECJ GC resulted in the much fastred run times by far.
A note on memory usage. -XX:+AggressiveHeap is just that - aggressive. If
you get an "Out of Heap Memory" error when using this switch, then you
will need to setthe VM parameter Xmx???m. This parameter sets the maximum
heap size and units are megabytes. The smallest value I ever use is:
Xmx75m. I'm running symbolic regression with average trees sizes of 18. My
rule of thumb is to allocate 2M of memory for every 1,000 individuals. For
example, a run of 200,000 individuals, I will set the parameter to
Xmx400m. Your needs may be more or less depending on your tree/population
sizes. There's a balance here. It needs to be set high enough so there's
enough heap space. It needs to be set low enough as to not aggressively
consume so much memory that you start doing disk page swaps, especially if
you're running multiple simultaneous runs. Again, this might not be an
issue for smaller populations, non-paralell runs or if you have vast
amounts of memory.
For more information on the -XX:+AggressiveHeap switch and other heap
related switches:
http://java.sun.com/j2se/1.4.2/reference/whitepapers/
I'd love to hear back from any of you who experiment with this.
Also, if there's popular demand, I can include the Performance
Monitoring Class that I've created.
best initentions, Chris Ellingwood
(University of Vermont, Botany)
-----------
Configuration I:
Sun Java: 1.5.0_05
Dual Processor Xeon 3.6 GHZ
Windows XP - 64 bit
ECJ 11
Configuration II:
Sun Java: 1.5.0_05
Pentium 4 - 2.4 GHZ
Windows XP
ECJ 11