Write the secrets of high performance J2ME games! !

zhaozj2021-02-16  103

Original address: http://www.microjava.com/articles/techtalk/optimization? Pageno = 1

J2ME Game Optimization Secret

By Mike Shivas -01/09/2004

This article describes the role that code optimization plays in writing fast games for mobile devices. Using examples I will show how, when and why to optimize your code to squeeze every last drop of performance out of MIDP-compliant handsets. We will discuss why optimization is necessary and why it is often best NOT to optimize. I explain the difference between high-level and low-level optimization and we will see how to use the Profiler utility that ships with the J2ME Wireless Toolkit to discover where to optimize your code. Finally this Article Reveals Lots of Techniques for Making Your Midlets Move.

Why Optimize?

It is possible to divide video games into two broad categories;.. Real-time and input-driven Input-driven games display the current state of game play and wait indefinitely for user input before moving on Card games fall into this category, as do .

Skill and Action games are often characterized by a great deal of on-screen movement (think of Galaga or Robotron). Refresh rates must be at least 10fps (frames per second) and there has to be enough action to keep gamers challenged. They require fast reflexes and good hand-eye co-ordination from the player, so compelling S & A games must also be extemely responsive to user input. Providing all that graphical activity at high framerates while responding quickly to key-presses is why code for real-time games has to be fast The challenges are even greater when developing for J2ME.Java 2 Micro Edition (J2ME) is a stripped-down version of Java, suitable for small devices with limited capabilities, such as cell phones and PDAs J2ME devices have..:

Limited Input Capabilities (No Keyboard!)

Small Display Sizes

Restricted Storage Memory and Heap Sizes

Slow CPUS

WRITIFORM FURTER CHALLENGES Developers To Write Code That Will Perform on Cpus Far Slower Than Those Found On Desktop Computers.

When not to Optimize

If you're not writing a Skill and Action game, there's probably no need to optimize. If the player has pondered her next move for several seconds or minutes, she will probably not mind if your game's response to her action takes more than a couple hundred milliseconds. An exception to this rule is if the game needs to perform a great deal of processing in order to determine its next move, such as searching through a million possible combinations of chess pieces. in this case, you might want to optimize your Code So That The Computer's Next Move Can Be Calculate In A Few Seconds, Not Minutes.

Even if you are writing this type of game, optimization can be perilous Many of these techniques come with a price tag -. They fly in the face of conventional notions of "Good" programming and can make your code harder to read Some are a. trade-off, and require the developer to significantly increase the size of the app in order to gain just a minor improvement in performance. J2ME developers are all too familiar with the challenges of keeping their JAR as small as possible. Here are a few more Reasons Not to Optimize: Optimization Is a Good Way To Introduce Bugs

Some Techniques Decrease the Portability of Your Code

You Can Expend a Lot of Effort for Little or No Results

Optimization is Hard

That last point needs some illumination. Optimization is a moving target, only more so on the Java platform, and even more so with J2ME because the execution environments vary so greatly. Your optimized code might run faster on an emulator, but slower on the actual DEVICE, OR VICE VERSA. Optimizing for One Handset Might Actually Decrease Performance on Another.

But there is hope. There are two passes you can make at optimization, a high-level and a low-level. The first is likely to increase execution performance on every platform, and even improve the overall quality of your code. The second pass is the one more likely to cause you headaches, but these low-level techniques are quite simple to introduce, and even simpler to omit if you do not want to use them. At the very least, they're very interesting to look at .

We will also use the System timer to profile your code on the actual device, which can help you to gauge exactly how effective (or utterly ineffective) these techniques can be on the hardware you're targeting for deployment.And one last bullet-point :

Optimizing is fun

A Bad EXAMPLE

Let's Take a Look At A Simple Application That Consists of Two Classes. First, The MIDlet ...

Import javax.microedition.midlet. *;

Import javax.microedition.lcdui. *;

Public Class Optimizeme Extends Midlet Immancelistener {

Private static final boolean debug = false DEBUG = FALSE;

Private display display;

Private Ocanvas Ocanvas;

PRIVATE FORM FORM;

Private stringItem TimeItem = New StringItem ("Time:", "unknown");

Private stringItem ResultItem =

New StringItem ("Result:", "No Results");

Private Command Cmdstart = New Command ("Start", Command.screen, 1);

Private command cmdexit = New Command ("exit", command.exit, 2);

Public Boolean Running = True;

Public Optimizeme () {

Display = display.getdisplay (this);

Form = New form ("Optimize");

Form.Append (TimeItem);

Form.Append (ResultItem);

Form.Addcommand (cmdstart);

Form.Addcommand (cmdexit);

Form.setCommandListener (this);

Ocanvas = new ocanvas (this);

}

Public void startapp () throws midletStateChangeException {

Running = true;

Display.SetCurrent (Form);

}

Public void pauseApp () {

Running = false;

}

Public void exitcanvas (int status) {

Debug ("EXITCANVAS - STATUS =" STATUS);

Switch (status) {

Case Ocanvas.user_exit:

TimeItem.Settext ("Aborted");

ResultItem.Settext ("Unknown");

Break;

Case ocanvas.exit_done: TimeItem.Settext (Ocanvas.Elapsed "MS");

ResultItem.Settext (String.Valueof (Ocanvas.Result));

Break;

}

Display.SetCurrent (Form);

}

Public void destroyApp (Boolean Unconditional)

Throws MidletStateChangeException {

Ocanvas = NULL;

Display.setcurrent (null);

Display = NULL;

}

Public void CommandAction (Command C, Displayable D) {

IF (c == cmdexit) {

Ocanvas = NULL;

Display.setcurrent (null);

Display = NULL;

NotifyDestroyed ();

}

Else {

Running = true;

Display.SetCurrent (Ocanvas);

Ocanvas.start ();

}

}

PUBLIC Static Final Void Debug (String S) {

IF (debug) system.out.println (s);

}

}

Second, The Ocanvas Class That Does Most of The Work In this Example ...

Import javax.microedition.midlet. *;

Import javax.microedition.lcdui. *;

Import java.util.random;

Public Class Ocanvas Extends Canvas IMPLEments Runnable {

Public static final int user_exit = 1;

Public static final int exit_done = 2;

Public static final int loop_count = 100;

Public static final int draw_count = 16;

Public static final int number_count = 64;

Public static final int Divisor_count = 8;

Public static final int waiting_time = 50;

Public Static Final Int Color_bg = 0x00fffff;

Public Static Final Int Color_fg = 0x00000000;

Public long elapsed = 0L;

Public int exitstatus;

Public int result;

PRIVATE Thread Animationthread;

Private Optimizeme MIDLET;

Private boolean finished;

PRIVATE long Started;

PRIVATE long framestarted;

Private long frametime;

Private int [] Numbers;

Private int loopcounter;

Private random random = new random (system.currenttimemillis ());

Public Ocanvas (OptimizeMe_o) {

MIDlet = _o; NumBers = new int [number_count];

For (int I = 0; i

Numbers [i] = i 1;

}

}

Public synchronized void start () {

Started = framestarted = system.currenttimemillis ();

LoopCounter = Result = 0;

Finished = false;

EXITSTATUS = exit_done;

AnimationthRead = New Thread (this);

AnimationthRead.start ();

}

Public void run () {

Thread Currentthread = thread.currentthread ();

Try {

While (AnimationthRead == Currentthread && Midlet.running

&&! finished) {

Frametime = system.currenttimemillis () - frameestarted;

FrameStarted = system.currenttimemillis ();

Result = Work (Numbers);

Repaint ();

Synchronized (this) {

Wait (wait_time);

}

LoopCounter ;

Finished = (loopcounter> loop_count);

}

}

Catch (InterruptedException IE {

OptimizeMe.debug ("interrupted");

}

ELAPSED = system.currenttimemillis () - start;

MIDlet.exitcanvas (exitstatus);

}

Public void paint (graphics g) {

g.setcolor (color_bg);

G.fillRect (0, 0, getWidth (), getHeight ());

G.SetColor (Color_fg);

G.SetFont (Font.Face_Proportional,

Font.Style_Bold | font.style_italic, font.size_small);

For (int i = 0; i

g.drawstring (Frametime "MS Per Frame",

Getrandom (getWidth ()),

Getrandom (GetHeight ()),

Graphics.top | graphics.hcenter;

}

}

Private int Divisor;

Private int R;

Public Synchronized Int Work (int [] n) {

R = 0;

For (int J = 0; j

For (int i = 0; i

Divisor = getDivisor (j);

R = Workmore (N, I, Divisor);

}

Return R;

}

Private int A;

Public synchronized int getDivisor (int N) {

IF (n == 0) Return 1;

A = 1;

For (int i = 0; i

A * = 2;

}

Return A;

}

Public synchronized int workmore (int []) {

Return n [_i] * n [_i] / _d n [_i];

}

Public void keyreleased (int keycode) {

IF (System.currentTimeMillis () - Started> 1000L) {

EXITSTATUS = User_Exit;

MIDlet.Running = false;

}

}

Private int getrandom (int Bound)

{// Return a Random, Positive Integer Less Than Bound

Return Math.abs (random.nextint ()% bound);

}

}

This app is a midlet what Simulates a Simple Game Loop:

Work

Draw

Poll for user input

Repeat

For fast games, this loop has to be as tight and fast as possible. Our loop continues for a finite number of iterations (LOOP_COUNT = 100) and uses the System timer to calculate how long in milliseconds the whole exercise took, so we can measure and improve its performance. The time and the results of the work are displayed on a simple Form. Use the Start command to begin the test. Pressing any key will exit the loop prematurely. The Exit command exits the application.

In most games, the work phase of the main game loop involves updating the state of the game world - moving all the actors, testing for and reacting to collisions, updating scores, etc. In this example, we're not doing anything particularly useful ..................................

The run () method also calculates the amount of time it takes to execute each iteration of the loop. Every frame, the OCanvas.paint () method displays this value in milliseconds at 16 random locations on screen. Normally you would be drawing the graphical elements of your game in this method, but our code offers a reasonable facsimile of this process.Regardless of how pointless this code may seem, it will allow us ample opportunity to improve its performance.

Where to optimize - the 90/10 rule

In performance-hungry games, 90 percent of a program's execution time is spent running 10 percent of the code. It is in this 10 percent of the code that we should concentrate all of our optimization efforts. We locate that 10 percent using a profiler. To turn on the Profiler Utility in the J2ME Wireless Toolkit, select the Preferences item from the Edit menu. This will bring up the Preferences window. Select the Monitoring tab, check the box marked "Enable Profiling", and click the OK button. Nothing Will happen. That's Okay - We need to run our program in the emulator and then before the profiler window appers.

Figure 1 Illustrates How To Turn on The Profiler Utility.

My emulator (running under Windows XP on a 2.4GHz Intel P4) reports that 100 iterations of the loop took 6,407ms, or just under six and a half seconds. The app reported 62 or 63ms per frame. On the hardware (a Motorola i85s IT Ran Much Slower. Time Per Frame Was Around 500ms and The Whole Thing Ran in 52460ms. We'll Try To Improve these Figures in The Course of this article.

When you exit the application the profiler window will appear and you will see something that resembles a folder browser, with the familiar tree widget displayed in a panel on the left. Method relationships are shown in this hierarchical list. Each folder is a method and opening a methods folder displays the methods called by it. Selecting a method in the tree shows the profiling information for that method and all the methods called by it in the panel on the right. Notice that a percentage is displayed next to each element. This is the percentage of total execution time spent in that particular method We must navigate through this tree to find where all the time is going, and optimize the methods with the highest percentages, where possible.Figure 2 -. the Profiler Utility call graph.A couple Of Notes About The PrOFiler. First, Your Percentages Will Almost Certainly Vary from Mine, But They Will Be Similarly ProPertioned - Always Follow The Biggest NumBers. My NumBers Varied Every Tim e I ran the app. To keep things as uniform as possible, you might want to close any background applications, like email clients, and keep activity to a minimum while you're running tests. Also, do not obfuscate your code before using the profiler or all your methods will be mysteriously named "b" or "a" or "ff". Finally, the profiler does not shed any light on the performance of whatever device you are emulating. The hardware itself is a completely different animal .

Opening the folder with the highest percentages we see that 66.8% of the execution time went to a method named "com.sun.kvem.midp.lcdui.EmulEventHandler $ EventLoop.run", which does not really help us. Digging down further unfolds a level or two of methods with similarly obscure names. Keep going and you'll trace the fat percentages to serviceRepaints () and finally to our OCanvas.paint () method. Another 30% of the time went into our OCanvas.run ( ) method. It should come as no surprise that both of these methods live inside of our main game loop. We will not be spending any time trying to optimize code in our MIDlet class, just as you should not bother optimizing code in your game that's outside the main game loop. Only optimize where it counts.The way that these percentages are divided in our example app is not entirely uncharacteristic of how they would be in a real game. You will most likely find that the vast proportion of execution Time in a real videogame is spot in the point () Method. g raphics routines take a very long time when compared to non-graphical routines. Unfortunately, our graphics routines are already written for us somewhere below the surface of the J2ME API, and there's not much we can do to improve their performance. What we can do IS Make Smart Decisions About Which One of Uses We Use.

High-level vs. low-level Optimization

Later in this article we will look at low-level code optimization techniques. You will see they are quite easy to plug into existing code and tend to degrade readability as much as they improve performance. Before you employ those techniques, it is always best to work on the design of your code and its algorithms. This is high-level optimization.Michael Abrash, one of the developers of id software's "Quake", once wrote, "the best optimizer is between your ears". There's more than one way to do it (TMTOWTDI) and if you take the extra time to think about doing things the right way first, then you will reap the greatest reward. Using the right (ie fastest) algorithm will increase performance much more than using low-level techniques To Improve a Mediocre Algorithm. You might Shave A Few More Percentage Points Off, But Always Start At The Top and Use Your Brain (You'll Find It Between Your EARS).

So let's look at what we're do the ing. We're calling graphics.drawstring () 16 Times evening "n ms per frame" onscreen. We don't know, iScreen. We don't know anything About the inner Workings of Drawstring, But We Can See That's USING UP A LOT OF TIME, SO Let's Try Another Approach. Let's Draw The String Directly ONTO An Image Object Once and The Draw The Image 16 Times.

Public void paint (graphics g) {

g.setcolor (color_bg);

G.fillRect (0, 0, getWidth (), getHeight ());

Font font = font.getfont (font.face_propor),

Font.Style_Bold | font.style_iTALIC,

Font.size_small;

String msimentage = frametime "ms per frame";

Image stringImage =

Image.createImage (Font.StringWidth (MsMessage), Font.getBaselinePosition ());

Graphics imagegraphics = stringimage.getgraphics ();

ImageGraphics.SetColor (Color_BG);

Imagegraphics.fillRect (0, 0, StringImage.GetWidth (),

StringImage.getHeight ());

ImageGraphics.SetColor (Color_fg);

ImageGraphics.SetFont (font);

ImageGraphics.drawstring (MsMessage, 0, 0,

Graphics.top | graphics.Left);

For (int i = 0; i

G.DrawImage (StringImage, getrand (getWidth ()),

Getrandom (GetHeight ()),

Graphics.vcenter | graphics.hcenter;

}

}

When we run this version of our software, we see that the percentage of time spent in our paint method is a little less. Looking deeper we can see that the drawString method is only being called 101 times, and it is now the drawImage method that SEES MOST OF THE ACTION, Being Called 1616 Times. Even Though WE Are Doing More Work, The App Runs A Little Quicker Because The Graphics Calls WE Are Using Are Faster.

You will probably notice that drawing the string to an image affects the display because J2ME does not support image transparency, so a lot of the background is overwritten. This is a good example of how optimization might cause you to re-evaluate application requirements. If You real, you might be forced to de agload, you might be forward.

This Code Might Be Slightly Better, But It Still Has A Lot of Room for Improvement. Let's Look At Our First Low-Level Optimization Technique.

Out of the loop?

Code inside a for () loop will be executed as many times as the loop iterates. To improve performance, therefore, we want to leave as much code as possible outside of our loops. We can see from the profiler that our paint () method is being called 101 times, and the loop inside iterates 16 times. What can we leave out of those two loops? Let's start with all those declarations. We are declaring a Font, a String, an Image and a Graphics object every time paint ( IS Called. We'll Move these Outside of the method and to the top of our class.public static final font font =

Font.getFont (font.face_propor,

Font.Style_Bold | font.style_iTALIC,

Font.size_small;

Public Static Final Int graphicanchor =

Graphics.vcenter | graphics.hcenter;

Public Static Final Int textanchor =

Graphics.top | graphics.Left;

Private static final string message = "ms per frame";

Private string msimentage = "000" mess;

PRIVATE.

PRIVATE graphics imagegraph;

Private long oldframetime;

You'll notice I made the Font object a public constant. This is often a useful thing to do in your apps, as you can gather the font declarations you use most together in one place. I have found the same goes for anchors, so I've Done The Same with The Text and Graphic Anchors. Pre-calculating these Things Keeps Those Calculation, However Insignificant, Out of Our loop.

I've also made the MESSAGE a constant. That's because Java loves to create String objects all over the place. Strings can be a huge memory drain if they're not controlled. Do not take them for granted, or you will probably bleed memory, which can in turn affect performance, especially if the garbage collector is being called too often. Strings create garbage, and garbage is bad. Using a String constant reduces the problem. Later we'll see how to use a StringBuffer to completely stop Memory Loss from string Abuse.now That We've ass, we need to add this code to the constructor:

StringImage = image.createImage (font.stringwidth),

Font.getBaselinePosition ());

ImageGraphics = stringimage.getgraphics ();

ImageGraphics.SetFont (font);

Another cool thing about getting our Graphics object up-front is that we can set the font once and then forget about it instead of setting it every time we iterate through the loop. We still have to wipe the Image object each time using fillRect () . Gung-ho coders might see an opportunity there to create two Graphics objects from the same Image, and pre-set the color of one to cOLOR_BG for the call to fillRect () and COLOR_FG on the other for the calls to drawString (). Unfortunately, the behavior of getGraphics () when called multiple times on the same Image is ill-defined in J2ME and differs across platforms so your optimization tweak might work on Motorola but not NOKIA. If in doubt, assume nothing.

There is another way to improve on our paint () method. Using our brain again we realize that we only need to re-draw the string if the frameTime value has changed since the method was last called. That's where our new variable oldFrameTime comes in Here's The New Method: Public Void Paint (Graphics G) {

g.setcolor (color_bg);

G.fillRect (0, 0, getWidth (), getHeight ());

IF (frametime! = oldframetime) {

MsMessage = frametime message;

ImageGraphics.SetColor (Color_BG);

Imagegraphics.fillRect (0, 0, StringImage.GetWidth (),

StringImage.getHeight ());

ImageGraphics.SetColor (Color_fg);

ImageGraphics.drawstring (Msmessage, 0, 0, Textanchor);

}

For (int i = 0; i

G.DrawImage (StringImage, getrand (getWidth ()),

Getrandom (GetHeight ()), graphicanchor;

}

Oldframetime = frametime;

}

The Profiler now shows that the time spent in OCanvas paint is down to 42.01% of the total. Comparing the frameTime across calls to paint () resulted in drawString () and fillRect () being called 69 times instead of 101. That's a decent savings , and there's not much more to go, but now it's time to get serious. The more you optimize, the harder it gets. now we are down to scraping out the last pieces of superfluous, cycle-eating code. We're dealing with SHAVING OFF VERY SMALL Percentages, OR EVEN FRACTIONS OF Percentages Now, But If We're Lucky, They'll Add Up To Something Significant.

Let's start with something easy. Instead of calling getHeight () and getWidth (), let's call those methods once and cache the results outside our loop. Next, we're going to stop using Strings and do everything manually with a StringBuffer. We ' re then going to shave a little off the calls to drawImage () by restricting the drawing area through calls to Graphics.setClip (). Finally, we're going to avoid making calls to java.util.Random.nextInt () inside our Loop.here area Our New Variables ...

Private static final string message = "MS Per frame:";

Private IW, IH, DW, DH;

Private stringbuffer stringbuffer;

Private int MessageLength;

Private int stringLength;

PRIVATE CHAR [] STRINGCHARS;

Private static final int randomcount = 256;

Private int [] randomnumbersx = new int rt [randomcount];

Private int [] randomnumbersy = new int rt [randomcount];

Private int ri;

... and here is the new code for our constructor:

IW = stringimage.getwidth ();

IH = StringImage.getHeight ();

DW = getWidth ();

DH = GetHeight ();

For (int i = 0; i

RandomnumBersX [i] = getrandom (dw);

RandomnumBersy [i] = getrandom (DH);

}

Ri = 0;

StringBuffer = New StringBuffer (Message "000");

MessageLength = message.length ();

StringLength = StringBuffer.Length ();

Stringchars = new char [stringLength];

StringBuffer.getchars (0, StringLength, StringChars, 0);

You can see we're pre-calculating Display and Image dimensions. We're also cacheing the results of 512 calls to getRandom (), and we've dispensed with the msMessage String in favor of a StringBuffer. The meat is still in the Paint () Method, Of Course:

Public Void Paint (Graphics G) {g.setcolor (color_bg);

g.fillRect (0, 0, DW, DH);

IF (frametime! = oldframetime) {

StringBuffer.delete (MessageLength, StringLength);

StringBuffer.Append ((int) frametime);

StringLength = StringBuffer.Length ();

StringBuffer.getchars (MessageLength,

StringLength,

StringChars,

MessageLength);

IW = font.charswidth (StringChars, 0, StringLength);

ImageGraphics.SetColor (Color_BG);

ImageGraphics.FillRect (0, 0, IW, IH);

ImageGraphics.SetColor (Color_fg);

Imagegraphics.drawchars (StringChars, 0,

StringLength, 0, 0, Textanchor;

}

For (int i = 0; i

G.setClip (RandomnumBersx [ri], randomnumbersy [ri], iw, ih);

g.drawimage (StringImage, RandomnumBersx [ri],

RandomnumBersy [ri], textanchor;

Ri = (Ri 1)% randomcount;

}

Oldframetime = frametime;

}

We're using a StringBuffer now to draw the characters of our message. It's easier to append characters to the end of a StringBuffer than to insert them at the beginning, so I've switched our display text around and the frameTime is now at the end of the message, eg "ms per frame: 120".. We're just writing over the last few frameTime characters each time, and leaving the message part intact Using a StringBuffer explicitly like this saves the system from creating and destroying Strings and StringBuffers each time through our paint () method. It's extra work, but it's worth it. Note that I'm casting frameTime to an int. I found that using append (long) caused a memory leak. I do not know why, But it's a good example of why you shouth uTilities to keep an eye on victings.

We're Also Using Font.charswidth () To Calculate The Width of The Message Image So That We Have Been Using A ProPorrtional Font, So The Image for "MS Per Frame: 1" WILL Be SMALLER THAN THE Image for "MS Per Frame: 888", And We're Using Graphics.SetClip () So We don't have to draw the extra. this also means we online to flank out the area we need. We're hoping that the drawing time we save makes up for the extra time spent calling font.charsWidth (). It may not make much of a difference here, but this is a great technique to use in a game for drawing a player's score on the screen. In that case, there's a big difference between drawing a score of 0 and a score of 150,000,000. This is hampered somewhat by the implementation's incorrect return values ​​for font.getBaselinePosition (), which seems to return The Same Value as font.getHeight (). sigh.

Finally, we're just looking up the pre-calculated "random" co-ordinates in our two arrays, which saves us making those calls. Note the use of the modulo operator to implement a circular array. Note also that we're using ................

We are now firmly in a gray area with respect to the numbers this version of the code produces. The profiler tells me that the code is spending roughly 7% more time in paint () than without these changes. The call to font.charsWidth ( ) is probably to blame, weighing in at 4.6%. (that's not great, but it could be reduced. Notice that we're retrieving the width of the MESSAGE string every time. We could easily calculate that ahead of the loop body and simply add it to the frameTime width.) Also, the new call to setClip () is labelled 0.85%, and seems to significantly increase the percentage of time spent in drawImage (to 33.94% from 27.58%) .At this point, it looks like all this extra code must certainly be slowing things down, but the values ​​generated by the application contradict with this assumption. Figures on the emulator fluctuate so much as to be inconclusive without running longer tests but my i85s reports that things are a little faster with the Extra Clipping Code Than Withnout, coming in at 37130ms without either call to setClip () or charsWidth (), and coming in at 36540 with both. I ran this test as many times as I had patience for, and the results were solid. This highlights the issues of varying execution environments. Once you get to the point where you're not sure if you're making headway, you might be forced to continue all your testing on the hardware, which would require a lot of installing and uninstalling of JAR files.

SO IT Looks Lot More Performance from Our Graphical Routines. Now it's time to take the same high-level and low-level approaches with our work () Method. Let's review That Method:

Public Synchronized Int Work (int [] n) {

R = 0;

For (int J = 0; j

Divisor = getDivisor (j);

R = Workmore (N, I, Divisor);

}

}

Return R;

}

Every time through the loop in run () we're passing in our array of numbers. The outer loop in the work () method calculates our divisor, then calls workMore () to actually perform the division. All kinds of things are wrong here , as you can probably tell. For a start, the programmer has put the call to getDivisor () inside the inner loop. Given that the value of j does not change through the inner loop, the divisor is an invariant, and really belongs outside The inner loop.

But Let's Think About this Some More. The Call Itself Is Completely Unnecessary. This code does the Same Thing ...

Public Synchronized Int Work (int [] n) {

R = 0;

Divisor = 1;

For (int J = 0; j

For (int i = 0; i

R = Workmore (N, I, Divisor);

}

Divisor * = 2;

}

Return R;

}

... without that call to getDivisor (). Now our profiler is telling me that we are spending 23.72% in our run () method, versus 38.78% before we made these improvements. Always optimize with your head first before messing with low- Level Optimization Tricks. With That Said, Let's Take a Look at some of those tricks.

Low-Level Optimization

All programmers are familiar with the concepts of the sub-routine and the function - separate out common code to avoid duplicating it in several places around your app Unfortunately, this commonly "Good" programming practice can impact performance because method calls involve a certain amount. of overhead. The easiest way to reduce the amount of time it takes to call a method is by carefully selecting its declaration modifiers. Our programmer has been very cautious, and has made his work () and workMore () methods synchronized, in case some other thread might call them simultaneously. This is all well and good, but if we're serious about performance, we often have to make sacrifices, and today we will be sacrificing safety.Well, not really. We know for a fact that no -one else is going to be calling these methods, SO We can de-synchronize the without to much thing. What else can we do? Take a Look at this list of method type:

Synchronized Methods Are The Slowst, Since An Object Lock Has To Be Obtained

Interface Methods Are The Next Slowest

Instance Methods Are In the Middle

Final Methods Are Faster

Static Methods Are Fastest

.

Another factor that affects the performance of method calls is the number of parameters passed into the method. We are calling workMore () 51712 times, passing an int array and two ints the method and returning an int each time. In this, somewhat trivial into , example it's going to be easy to collapse the workMore () method into the the body of work () to avoid the call entirely. In the real world, that decision might be harder to make, especially if it means that code will be duplicated around your application. Test with the profiler and on the device to see how much difference it actually makes before taking that step. If you can not remove the method altogether, try to reduce the number of parameters you're passing into it. The More parameters, the greater the overhead.public final static int work (int [] n) {

Divisor = 1;

R = 0;

For (int J = 0; j

For (int i = 0; i

R = n [i] * n [i] / divisor n [i];

}

Divisor * = 2;

}

Return R;

}

! Wow Removing the call to workMore () slashed the amount of time spent in run to 9.96% From here on it will be uphill all the way Let's look at two general optimization techniques -.. Strength reduction and loop unrolling.

Strength reduction is where you replace a relatively slow operation with a fast one that does exactly the same thing. The most common example is using the bitshift operators, which are equivalent to multiplication and division by a power of two. For example, x >> 2 is equivalent to x / 4 (2 to the power of 2) and x << 10 is equivalent to x * 1024 (2 to the power of 10). By An Amazing Coincide, Our Divisor is Always A Power of 2 (ISN 't that lucky!) so we can use bitshifting in place of division.Unrolling loops reduces the overhead of flow control code by performing more than one operation each time through the loop, executing fewer iterations, or even removing the loop entirely. As our Divisor_count is Only 8, IT's Going to Be Easy To Unroll Our Outer Loop:

Public final static int work (int [] n) {

R = 0;

For (int i = 0; i

For (int i = 0; i > 1) n [i];

For (int i = 0; i > 2) n [i];}

For (int i = 0; i > 3) n [i];

For (int i = 0; i > 4) n [i];}

For (int i = 0; i > 5) n [i];

For (int i = 0; i > 6) n [i];}

For (int i = 0; i > 7) n [i];

Return R;

}

Two important points. First, you'll notice that unrolling our loop requires us to duplicate some code. This is the last thing you want in J2ME, where developers are often fighting JAR bloat, but remember that the JARing process involves compression, and compression works best on repetitive data, so the above code might not make as big an impact on your jar size as you might think. Again, it's all about the trade-off. your code can be small, fast, easy to read. Pick any two. The second point is that the bitshift operators have a different precedence from / and *, so you will often have to put parentheses around statements that otherwise would not need them.Unrolling the loop and using the bitshift operators shaved off slightly more than 1 % Not bad Now let's turn our attention to array access Arrays are fast data structures in C, but for that reason they can also be dangerous -... if your code starts accessing array elements beyond the end of the array, you're over -Writing Sections of ME .

In contrast, Java is a very safe language - running off the end of an array like that will simply throw an ArrayIndexOutOfBoundsException The system checks for an invalid index value every time you access an array, which makes array access slower than in C. Again. , the internating we can do it by Making Smart Decisions in The Code Above, for Example, We're Accessing N [i] 24 Times. We can omit a lot Of Those Array Accesses by Storing The Value of N [I] in a Variable. A Little High-Level Thought Also Reveals That We Can RearRange Things in a Much Smarter Way Like this ... private static int Divisor

Private static int R;

Private static int ni;

Public final static int work (int [] n) {

R = 0;

For (int i = 0; i

Ni = n [i];

R = Ni * Ni Ni;

R = (NI * Ni >> 1) Ni;

R = (Ni * Ni >> 2) Ni;

R = (Ni * Ni >> 3) Ni;

R = (Ni * Ni >> 4) Ni;

R = (Ni * Ni >> 5) Ni;

R = (Ni * Ni >> 6) Ni;

R = (Ni * Ni >> 7) Ni;

}

Return R;

}

We've eliminated a whole lot of looping and array accessing. Let's now eliminate a whole lot of squaring using a technique known as common sub-expression elimination. We're calculating the square of n [i] eight times each time through our loop We can Eliminate Those Repetitive Calculation by Working Out The Square ONCE AT THE BEGINNING:

Private static int R;

Private static int ni;

Private static int NI;

Public final static int work (int [] n) {

R = 0;

For (int i = 0; i

NIS = NI * NI;

R = NIS NI;

R = (NIS >> 1) Ni;

R = (NIS >> 2) Ni;

R = (NIS >> 3) Ni;

R = (NIS >> 4) Ni;

R = (NIS >> 5) Ni;

R = (NIS >> 6) Ni;

R = (NIS >> 7) Ni;

}

Return R;

}

... Which Cuts Down The Time Spent In Run () To 6.18%. NOT BAD AT All. Before We Move ON, Let Me Say A Couple More Things About Arrays. A Little High-Level Optimization (IE "THOUGHT") Might reveal that an array is not necessarily the right data structure to use. Think about using a linked list or some other structure if that would increase performance. Secondly, if you are going to use an array and you ever need to copy its contents into another array, always use System.arraycopy (). It's way faster than trying to write your own routine to do the same thing. Finally, arrays are generally higher performance than the java.util.Vector object. If you require the kind of functionality .

Okay, we're really running out of things to optimize. We just introduced another couple of variables to cache the array element and the value of that element squared. You may have been wondering why those variables have been declared outside the body of the method Declaration. They're outside Because Even Declaring INTS Takes a little Time and We Should Keep That Out Our Loop, Right? Wrong!

Assume nothing. It's true that we're saving time by avoiding the int declarations, but this code may actually be slower than if those variables were declared locally, inside the method. That's because local variables perform better, as the JVM takes longer to resolve variables declared outside a method. So let's turn them into local variables.Finally, we can tweak our for () loop slightly. Computers are generally better at comparing a number to zero than to any other non-zero number. That means we can turn Our loop Upside Down and Rewrite The Method Like this So We're Comparing Against Zero:

Public final static int work (int [] n) {

INT R = 0;

INT NI;

Int nis;

INT I;

For (i = n.length; --i> = 0;) {

Ni = n [i];

NIS = NI * NI;

R = NIS NI;

R = (NIS >> 1) Ni;

R = (NIS >> 2) Ni;

R = (NIS >> 3) Ni;

R = (NIS >> 4) Ni;

R = (NIS >> 5) Ni;

R = (NIS >> 6) Ni;

R = (NIS >> 7) Ni;

}

Return R;

}

And that's it! This code may be a little faster, but the profiler results are less conclusive. What is clear is that this method is tougher to read. There may be further room for improvement, but let's instead take another look at our paint ( ) Method, to see if any of what we learned can be introducesd there.

Remember what we learned about local variables? If you are forced to use an instance variable and you are referencing that variable multiple times inside a method, it might be worth introducing a local variable so that the JVM only has to resolve the reference once. You 'll introduce a declaration and an assignment which will slow things down, but as a rule of thumb, we'll use that technique if an instance variable is referenced more than twice.We can also use strength reduction in our paint () method to replace the modulo operator with a circular counter that uses a bit operator. This is only possible because the length of our random numbers cache array is (amazingly!) a power of two. Finally, we can juggle our comparisons to always compare with zero. Here's Our new and improved paint () Method:

Public void paint (graphics g) {

StringBuffer SB = StringBuffer;

Graphics ig = imagegraphics;

CHAR [] sc = stringchars;

Int Sl;

INT ml = messageLength;

INT ril = ri;

INT IW = 0;

g.setcolor (color_bg);

g.fillRect (0, 0, DW, DH);

IF (Frametime - Oldframetime! = 0) {

Sb.delete (ML, StringLength);

Sb.append ((int) frametime);

SL = StringLength = sb.length ();

Sb.getChars (ML, SL, SC, ML);

IW = font.charswidth (SC, 0, SL);

Ig.SetColor (color_bg);

Ig.FillRect (0, 0, IW, IH);

Ig.SetColor (Color_fg);

Ig.drawchars (SC, 0, SL, 0, 0, TEXTANCHOR);

}

For (int i = Draw_count; --i> = 0;) {

G.setClip (RandomnumBersx [RIL], RandomnumBersy [RIL], IW, IH);

g.drawimage (StringImage, RandomnumBersx [RIL],

RandomnumBersy [RIL], TextAnchor;

RIL ;

RIL & = 255;

}

Ri = RIL;

Oldframetime = frametime;

}

Again, we are at the end of the road with respect to profiler results. These changes do not affect the number of times the graphics routines are called, so the difference will be minor at best. But when we combine all the changes we made to our work () method and load the new JAR onto the device, the difference is huge My motorola i85s now completes the test in 14030ms -.!. over twice as fast There is one last change to make to the code I have left it to the end because it uses a method that is not particularly well documented and my experience has been that its behavior varies across implementations. Looking at the start () and run () methods on OCanvas, you can see that I've been using a separate animation thread. This is the traditional way of doing animation in Java. An issue with using this technique for games is that we are forced to pause every time we iterate through the loop to allow system events, such as key presses and command actions to Be Delivered. We call the Wait () Method i na synchronized block to make this happen. This is hardly optimized code. After all our hard work optimizing everything else, we're actually stopping to do nothing right in the thick of things. What's worse, it's not easy to arrive at a good figure For Wait_Time. if we wait () TOO Long, The Game Slows Down. IF we don't wait () for long enough, key presses area missed and the game...

J2ME provides a solution to this problem by using the Display.callSerially () method. The API states that callSerially (Runnable r) "causes the Runnable object r to have its run () method called later, serialized with the event stream, soon after completion of the repaint cycle ". By using callSerially () we can miss out the call to wait () entirely. The system will ensure that our work () and paint () methods are called in synch with the user input routines, so our Game Will Remain Responsive. Here Are The New Methods ... public void start () {

Started = framestarted = system.currenttimemillis ();

LoopCounter = Result = 0;

Finished = false;

EXITSTATUS = exit_done;

Run ();

}

Public void run () {

Frametime = system.currenttimemillis () - frameestarted;

FrameStarted = system.currenttimemillis ();

IF (MIDlet.Running &&! finished) {

Result = Work (Numbers);

Repaint ();

Display.CallSerially (this);

LoopCounter ;

Finished = (loopcounter> loop_count);

}

Else {

ELAPSED = system.currenttimemillis () - start;

MIDlet.exitcanvas (exitstatus);

}

}

... Plus We Have to Declare and Get A Handle On The Display:

Display Display = Display.getDisplay (MIDLET);

Without the call to wait () my i85s now runs this code in 10180ms -. Around a 40% savings You might expect a greater increase in performance, given that we just eliminated 100 calls that wait () 50ms, but remember this technique is also About Responsiveness to User INPUT.

Again, let me stress that this method of animation should be used with caution. I had trouble getting it to work on NOKIA, and all of their example code (even game code) uses the wait () technique instead. Even on Motorola I had Problems Using CallSerially () if I added command Objects to the animated canvas. Test Carefully Before You Test Carefully Before You Try this at home.other technology

One technique I was unable to include in my example code was the optimal use of a switch () statement. Switches are very commonly used to implement Finite State Machines, which are used in game Artificial Intelligence code to control the behavior of non-player actors IT IS Good Programming Practice to Write Code Like this:

Public static final int 2_running = 1000;

Public static final int 2_jumping = 2000;

Public static final int 2_shooting = 3000;

Switch (n) {

Case State_Running:

DORUN ();

Case State_jumping:

DOJUMP ();

Case State_shooting:

Doshoot ();

}

There's nothing wrong with this, and the int constants are nice and far apart, in case we might want to stick another constant in between RUNNING and JUMPING, like STATE_DUCKING = 2500. But apparently switch statements can be compiled into one of two byte codes, And the Faster of the Two is buy ing the INTS Used Are Close TOGETHER, SO this Would Be Better:

Public static final int state_running = 1;

Public static final int state_jumping = 2;

Public static final int 2_shooting = 3;

There are also some optimizations you can perform when using a Fixed Point math library. First, if you're doing a lot of division by a single number, you should instead work out the inverse of that number and perform a multiplication. Multiplication is slightly Quicker Than Division. so INSTEAD OF ... INT fpp = fp.div (fpx, fpd);

INT fpq = fp.div (fpy, fpd);

INT fpr = fp.div (fPZ, FPD);

... You Should Rewrite It Like this:

INT fpid = fp.div (1, fpd);

INT fpp = fp.mul (fpx, fpid);

INT fpq = fp.mul (fpy, fpid);

INT fpr = fp.mul (fpz, fpid);

If you're performing hundreds of divisions every frame, this will help. Secondly, do not take your FP math library for granted. If you have source for it, open it up and take a look at what's going on in there. Make Sure All the Methods Are Decland Final Static and Look for Other Opportunities To Improve The Cast. for Example, You May Find That The Multiplication Method Has To Cast Both INTS To Longs and The Back to An Int:

Public Static Final Int Mul (int x, int y) {

LONG Z = (long) x * (long) y;

Return ((INT) (Z >> 16);

}

Those casts take time. Collision detection using bounding circles or spheres involves adding the squares of ints together. That can generate some big numbers that might overflow the upper bound of your int Fixed Point data type. To avoid this, you could write your own square Function That Returns a long:

Public Static Final Long SQR (INT X) {

Long z = (long) x;

z * = z;

Return (z >> 16);

}

This optimized method avoids a couple of casts. If you're doing a great deal of Fixed Point math, you might consider replacing all of the library calls in the main game loop with the long-hand math. That will save a lot of method calls and parameter passing. you may also find that when the math is written out manually you can reduce the number of casts that are required. This is especially true if you are nesting several calls to your library, egint fpA = FP.Mul ( Fp.toint (5),

Fp.mul (fp.div (1 / fpb),

Fp.mul (fp.div (FPC, FPD),

FP.TOINT (13))))))))))))))))))

Take the time to unravel nested calls like this and see if you can reduce the amount of casting. Another way to avoid casting to longs is if you know that the numbers involved are small enough that they definitely will not cause an overflow.

To help with high-level optimization, you should look for articles on game programming. A lot of the problems presented by game programming such as fast 3D geometry and collision detection have already been solved very elegantly and efficiently. If you can not find Java source, you will almost certainly find C source or pseudo-code to convert. Bounds checking, for example, is a common technique that we could have used inside our paint () method. Instead of clearing the entire screen every time, we really only need to clear the section of the screen that changes from frame to frame. Because graphics routines are relatively slow you will find that the extra housekeeping required to keep track of which parts of the screen need to be cleared is well worth the effort.

Some phone manufacturers offer proprietary APIs that help programmers get around some of the limitations J2ME presents, such as lack of sound, lack of Image transparency, etc. Motorola, for example, offers a floating point math library that uses floating point math instructions on the chip. This library is much faster than the fastest Fixed Point math library, and a lot more accurate. Using these libraries completely destroys the portability of your code, of course, but they may be an option to consider if deployment on many different handsets is Not a concern.conclusions

Only Optimize Code if you need to

Only Optimize Where it

Use the profiler to see where to optimize

The Profiler Won't help you on the device, so use the system timer on the hardware

Always Study Your Code and try to improve the algorithms before using low-level techniques

Drawing is Slow, So Use the Graphics Calls as Sparingly As Possible

Use setclip () Where Possible to Minimize the Drawing Area

Keep As Much Stuff As Possible Out of Loops

Pre-Calculate and Cache Like Crazy

Strings Create Garbage and Garbage IS Bad So Use StringBuffers Instead

Assume Nothing

Use static final methods where Possible and Avoid The Synchronized Modifier

Pass as few parameters as possible into frequently-caled methods

WHERE POSSIBLE, REMOVE METHOD CALLS ALTOGETHER

Unroll loops

Use bit shift operators instead of division or multiplication by a Power of TWO

You can use bit operators to import circular loops instead of modulo

Try to Compare To Zero Instead of Any Other Number

Array Access Is Slower Than C, So Cache Array Elements

Eliminate Common Sub-Expressions

Local Variables Are Faster Than Instance Variables

Don't wait () if you can callserially () Use small, close constants in switch () Statements

Look Inside your fixed point Math Library and Optimize IT

Unravel Nested FP Calls to Reduce Casting

Division is Slower Than Multiplication, So Multiply by the inverse instead of dividing

Use tried and tested algorithms

Use proprietary high-perform to preserve portability

WHERE to NEXT?

Optimization is a black art. At the heart of any computer lies the CPU and at the heart of Java lies a virtual CPU, the JVM. To squeeze the last ounce of performance from the JVM, you need to know a lot about how it functions beneath the hood. Specifically, you need to know what things the JVM can do fast, and what it does slowly. Look for sites with solid information on the inner workings of Java. you do not necessarily have to learn how to program in byte Code, But The More You Know, The Easier It Will Be To Come Up With Ways To Optimize Your Applications for Performance.

There's no substitute for experience. In time you will discover your own secrets about the performance characteristics of J2ME and of the handsets you are developing for. Even if you can not code around certain idiosynchrasies, you could design your next game around them. While developing my game I found that calling drawImage () five times to draw five images of 25 pixels each is much slower than calling it once to draw an image five times the size. that knowledge will definitely help shape my next game.

Resources:

J2ME's official web site contains the latest on what's happening on this front. Like wireless games? Read the Wireless Gaming Review. Discuss J2ME Game Development at j2me.org A great site on many aspects of Java Optimization Another great site on Optimization Many articles on J2ME Performance Tuning The Amazing Graphics Programming Black Book by Michael Abrash The Art of Computer Game Design by Chris Crawfordabout The Author:

Mike Shivas has been playing video games since before the advent of the 8-bit home microcomputer. He has been programming in Java since 1996, has consulted for MasterCard on wireless solutions and is the published author of several J2ME video games. Readers may contact Mike At mshivas@hotmail.com.

转载请注明原文地址:https://www.9cbs.com/read-11212.html

New Post(0)