First published on IBM developerWorks.
Optimization is one of the foundations of good application design. In this article, wireless expert Mikko Kontio explains how you can optimize your wireless applications for speed, size, and maintenance. He also offers tips for choosing the right transport protocol (particularly important for mobile environments) and performance tuning your Java code.
So, you've just finished your latest development project, ran a demo, and made an unfortunate discovery: the application doesn't run fast enough. You don't know if the problem originates on the server side, the client side, or both, and you don't have much time to figure it out. If you're lucky, the software is only in the internal testing phase. If you're less fortunate, the client is already breathing down your neck for a finished product. In either case, you've just hit a major road bump. And the worst thing about it is that you might not be able to recover.
It probably isn't much comfort, at this point, for me to let you in on a little secret: Design is everything and the rest is just plain work. The key elements of any software project are created in the design. If the software is poorly designed, there's very little you can do to make it better. At best, you can hold your breath and hope you haven't committed any drastic errors.
On the upside, good design techniques can be learned, and applying them will absolutely improve your results. Along with requirements gathering, optimization is one of the foundations of good design. By carefully defining the most important elements of your application and then tailoring both the architecture and the underlying code to support those elements, you can build applications that deliver the results you want. For instance, you can optimize your applications for speed; thus avoiding a scenario like the one described above.
In this article, I'll introduce you to the essentials of optimization: what to optimize and how to avoid the usual pitfalls. I'll also provide a series of tips for performance optimizing the Java language, show you how to use a simple Java utility class to test the results, and close with some guidelines for optimizing your software. Throughout the article we'll focus on the optimization requirements of pervasive applications, or applications designed for mobile environments. Although the coding techniques you'll learn are specific to the Java platform, the basic ideas are adoptable to other environments and technologies.
What to optimize
Optimization should be addressed early in the design process. Start by asking yourself what aspects of the application are essential, and what aspects might require special attention. The first question is more general, addressing the basic functional needs of any application; the second has to do with the specific requirements of the application you're working on. Speed, size, and maintenance are the three most important factors for almost any application, so we'll start there.
Optimizing for speed means that you design your application so that its operations complete quickly. While no operation should tie up system resources for too long, it's up to you to prioritize each function and allocate resources accordingly. Functions that will be called over and over again should execute as fast as possible. Lesser used and more complex functions might take more time.
The second part of optimizing for speed is to make sure the user knows what's going on, and ideally to give him or her some choice as to how to proceed. For longer operations you should include a pop-up message or animation that reports the estimated time to complete. If possible, you should give the user the option to cancel the operation or return to it later.
It is particularly important to optimize for speed in mobile environments, where devices often have very limited processor capabilities and memory resources. The section below on the Java language includes useful hints for this type of optimization, including tips for eliminating typical errors.
Like speed, size is a particularly important factor in mobile application design. Devices such as PDAs and mobile phones often place strict size limitations on the applications they will run. Some devices require that applications be 50 kilobytes or less: any application larger than that simply won't install.
You can take several steps to make your application smaller, starting with minimizing the number of classes. Class files take up a lot of memory, and the more classes your application contains the more objects it will create. So don't create a lot of classes in the design phase. Instead, try to find (or create) larger classes or components that won't get too complex or have totally different kinds of functionality.
The second step is to obfuscate your classes. Obfuscation makes classes smaller, which is important when every kilobyte counts. You can allocate any extra memory that results from obfuscation to making the application look especially good, or adding extra features.
The third step is to minimize the size and number of resource files. For example, a large background image requires memory to run, so keep your images small. If your application uses many small images (say, to represent different objects on the screen), you should place them all in a single image file. Of course, this means that all the images should be of the same width and height. Figure 1 shows a variety of screen images placed in a single file.
Figure 1. Images tiled in a single image file
|Image 1||Image 2||Image 3||Image 4|
Optimizing for maintenance doesn't do much to improve how an application runs, but it's still an important step. You should always think of your applications in the long term and design them so that they'll be easy to update and maintain. It's especially important to think about maintenance during optimization, when you can be so focused on making the code efficient that you forget to also make it readable.
While most developers produce unreadable (or "spaghetti") code by accident, some create it deliberately. By making important parts of the software unnecessarily complicated they hope to render themselves irreplaceable (because no one else knows how to fix the code). In fact, most companies recognize the long-term expense of employing such developers and will get rid of them as soon as possible.
It's important to be aware of the throughput of the various transport protocols your application will employ and how this will affect its performance in different mobile environments. For example, HTTP is a well-used Internet protocol, but it should be carefully used in a pervasive application scenario.
Consider a mobile device using HTTP to access the services of a Web server. What should the Web server's interface be like? Should the operations be divided or large? Depending on the connection, the round-trip (meaning the time between when the client makes the request and when it receives the response) could be quite long. The rule of thumb with HTTP is to make only a few calls to the server and pass larger amounts of data per call.
You can pass the data as strings and integers or, if the device has enough memory to process it, you can use XML as the data format. If the device is good enough, you can minimize network traffic by sending XML only when needed.
A working example
I'll use a simple example to help you understand the importance of optimization, as well as how it works. My example is a game application for mobile phones. The game is set in space, pitting the hero (the user)'s ship against multiple enemy ships. All the ships fire rockets.
I'm going to optimize this application for size, but first I have to build it. Say I'm reading some object-oriented development book and not thinking at all. I start with five classes:
Rocket. Next, I use two vectors, placing all the enemy ships in one and all the rockets in the other. And, finally, I have each
Ship object manage its own outlook, loading its own images and drawing them on the screen as needed.
Now, let's see what happens during game play. When the game starts it creates an instance of
HeroShip and maybe five to 10 instances of
EnemyShip. Then it creates a vector for the enemy ships and another one for the rockets. And finally, each of the ships loads an image to paint on the screen. At this point the application is holding 15 or more objects in its memory, as well as all those loaded images. Then the action begins: Two enemy ships appear on the screen, our hero fires half a dozen rockets at them, and (infuriatingly) the application crashes.
You probably know as well as I do what went wrong: For every screen update the application had to go through two vectors and redraw all the rockets and enemy ships. Looping through the vectors is a very expensive operation. The system memory was already taxed by the ships, and then the firing action started. It resulted in the creation of six rocket objects, each object loaded its own image, and the stage was set for disaster. With a setup like this one, the user really only had to make a move and the game would crash.
Optimizing for size
Fortunately, I can optimize the application for size and salvage the game. The first thing I need to do is minimize the classes. I start by dropping the vectors and placing the ships and rockets in an array. I should also create a super class for all the visual objects and place the visual objects in an array. Adding a class will increase the complexity of the application, but this one class will pay me back in time and speed. Looping through an array is a lot faster than looping through a vector. In addition, some mobile development platforms (such as MIDP) offer very fast routines for copying arrays. This means that arranging the array (like inserting new objects) will also be a lot faster.
The next step is to rationalize image handling. I load each image file only once, using that single file to draw all the visual objects. Of course, I also should try to make the image files and any other resource files smaller. If I have to use a large background picture (for example, 200x100) I should try to design it from reusable pieces that are small and packaged in a single image file (as shown in Figure 1).
Optimizing Java code
Most of the optimization techniques I've discussed so far have to do with the system's architecture -- its backbone. Once you've completed this outer layer of optimization, you should begin thinking about the application's inner layer: the coding details that can make it run either terribly slow or nice and fast. In this section, I'll give you some tips for performance-optimizing your Java code.
If you're defining constants, declare them as
static final. This reduces the time it takes the virtual machine to initialize the variables, which will make your code run faster.
Always carefully calculate the size of a code loop. A loop based on data from a database might be fast and straightforward at the start of the application's life, when it's still working with small amounts of data, but it could begin to slow down incrementally as the data builds up.
To cut down on size, avoid placing unnecessary operations (such as methods calls) inside the loop. And remember that evaluations are executed every time the loop runs, so don't call a method in the evaluation like this:
Instead use a local variable to store the value and then use the local variable, like this:
You'll encounter the pitfalls of string handling when you start concatenating strings. To get around this, avoid
String wherever you can. Use
StringBuffer instead of concatenating
String with plus (+) operators. In some cases,
StringBuffer can run several hundred times faster than
Printing to the console comes in handy during development and testing, but be sure to remove all the print statements from your final build. Otherwise, they'll slow down your application. If you want to keep print statements in your code for debugging purposes, be sure to include a mechanism for turning them on and off, or (even better) use an off-the-shelf product like log4J for logging.
Performance testing in Java code
Once you've optimized your Java code for performance, you'll want to test it out. For performance testing during development, it's usually sufficient to use a simple utility class. The
Timing class in Listing 1 lets you measure the time it takes to complete an operation.
Listing 1. The Timing class for performance testing
Here's a typical usage of the
Performance testing is an essential part of any optimization procedure. Always test your changes to ensure that they have positively impacted the code rather than just assuming that they have.
You can use the following checklist as a basis for optimizing pervasive applications, as well as applications in general:
- Design is everything, never forget this. It's extremely difficult to optimize a bad design.
- Optimize first for speed, size, and maintenance.
- Concentrate on the essentials. Optimize the most frequently used parts of the software for the best results.
- Almost every programming language or technique presents specific ways to make your application faster. Know the optimization features of your chosen tools and make use of them.
- Test things out as you go along; make sure that your changes to the code are having the expected impact.
About the author
Mikko Kontio works as a technology manager for the leading-edge Finnish software company, Enfo Solutions. He holds a Masters degree in computer science and is the author and co-author of several books, the latest being "Professional Mobile Java with J2ME," published by IT Press. Mikko can be reached at firstname.lastname@example.org.