- 297 - The JDK classes are not all designed with interfaces. Those JDK classes and other third-party
classes that do not have interface definitions should be wrapped by your own classes so that their use can be made more generic. Applications that need to minimize download time, such as applets,
may need to avoid the extra overhead that wrapping causes .
Object creation is one significant place where interfaces fall down, since interfaces do not support constructor declarations, and constructors cannot return an object of a different class. To handle
object creation in a way similar to interfaces, you should use the factory pattern. The factory design pattern recommends that object creation be centralized in a particular factory method . So rather
than calling
new Something
when you want to create an instance of the
Something
class, you call a method such as
SomethingFactory.getNewSomething
, which creates and returns a new instance of the
Something
class. Again, this pattern has performance costs, as there is the overhead of an extra method call for every object creation, but the pattern provides more flexibility when it
comes to tuning.
Design for reusable objects: do not unnecessarily throw away objects. The factory design pattern can help, as it supports the recycling of objects. Canonicalize objects where possible see
Section 4.2.4
. Keep in mind that stateless objects can usually be safely shared, so try to move to stateless objects where appropriate.
Using stateless objects is a good way to support changing algorithms easily, by implementing different algorithms in particular types of objects. For example, see
Section 9.2 , where different
sorting algorithms are implemented in various sorting classes. The resulting objects can be interchanged whenever the sorting algorithm needs to be varied.
Consider whether to optimize objects for update or access. For example, a statistics-calculating object might update its average and standard deviation each time a value is added to it, thus slowing
down updates but making access of those statistics lightning-fast. Or, the object could simply store added values and calculate the average and standard deviation each time those statistics are
accessed, making the update as fast as possible, but increasing the time for statistics access.
13.4.5 Techniques for Predicting Performance
Predicting performance is the mainstay of performance tuning at the analysis and design stages. Often it is the experience of the designers that steers design one way or another. Knowing why a
particular design element has caused bad performance in another project allows the experienced designer to alter the design in just the right way to get good performance.
Some general guidelines can guide the application designer and avoid bad performance. In the following sections we consider some of these guidelines.
13.4.5.1 Factor in comparative performance of operations
Different types of operations have widely varying execution times. Some design abstractions decouple the type of intercomponent-communication mechanism from any specific implementation.
The design allows the intercomponent communication to be based on a local or remote call, which allows components to be placed very flexibly. However, the performance of different types of calls
varies hugely and helps define whether some designs can perform fast enough.
Specifically, if local procedure calls have an average time overhead of one unit, a local interprocess call incurs an overhead of about 100 units. On the same scale, a remote procedure call RPC on a
- 298 - local area network takes closer to 1000 time units, and an RPC routed across the Internet likely
takes over 10,000 time units.
Applying these variations to the design and factoring the number of messages that components need to send to each other may rule out some distributed architecture s. Alternatively, the overhead
predictions may indicate that a redesign is necessary to reduce the number of intercomponent messages.
Note also that process startup overheads may need to be considered. For example, Common Gateway Interface CGI scripts for HTTP servers typically need to be started for every message
sent to the server. For this type of design, the time taken to start up a script is significant, and when many scripts are started together, this can slow down the server considerably. Similarly, if your
design allows many thread startups within short intervals, you need to determine whether the architecture can handle this, or if it may be a better option to redesign the applications to use thread
pools see
Section 10.7 .
13.4.5.2 Consider the relative costs of different types of accesses and updates
Accesses and updates to system memory are always going to be significantly faster than accesses and updates to other memory media. For example, reads from a local disk can be a thousand times
slower than memory access, and disk writes are typically half as fast as disk reads. Random access of disks is significantly slower than sequential access.
Recognizing these variations may steer your design to alternatives you might otherwise not have considered. For example, one application server that supports a shared persistent cache redesigned
the persistent cache update mechanism to take account of these different update times GemStone application server,
http:www.gemstone.com . The original architecture performed transactional
updates to objects by writing the changes to the objects on the disk, which required random disk access and updates. The modified architecture wrote all changes to shared memory as well as to a
sequential journaling log file for crash recovery. Another asynchronous process handled flushing the changes from shared memory to the objects stored on disk. Because disk navigation to the
various objects was significant, this change in architecture improved performance by completely removing that bottleneck from the transaction .
13.4.5.3 Use simulations and benchmarks