Design for performance, Part 3: Remote interfaces

Learn to avoid performance hazards when designing Java classes

This series explores some of the ways in which early design decisions can significantly affect application performance. Part 1, examined how a class's object-creational behavior can be embedded in its interface. Certain interface constructs virtually require that a class create temporary objects, or that its callers create temporary objects in order to use the class. Because temporary object creation is a significant performance issue for Java programs, it pays to review your class interfaces for performance hazards while you can still do something about it -- at design time.

I focused on object creation in Parts 1 and 2 because it is a significant performance problem for many Java programs. However, in distributed applications, such as those built on RMI, CORBA, or COM, an entirely different set of performance issues comes into play. This article explores some of the performance issues specific to remote applications and shows how you can predict many distributed application performance problems simply by examining a class's interface.

Read the whole "Design for Performance" series:

An overview of remote invocation

In distributed applications, an object running on one system can invoke methods on objects running in another system. That is accomplished with the aid of a great deal of machinery that makes the remote objects appear local. To access a remote object, you first need to find it, which can usually be accomplished through the use of a directory or naming service, such as the RMI registry, JNDI, or the CORBA naming service.

When you obtain a reference to a remote object through a directory service, you don't receive an actual reference to that object, but rather a reference to a stub object that implements the same interface as the remote object. When you invoke a method on the stub object, the stub has to marshal the method parameters -- convert them into a byte-stream representation, a process similar to serialization. The stub sends the marshaled parameters over the network to a skeleton object, which unmarshals them and invokes the actual remote method you wanted to invoke. Then the method returns a value to the skeleton, the skeleton marshals the return value and ships it to the stub, and the stub unmarshals it and returns the value to the caller. Phew! That's a lot of work for a single method call. Clearly, despite an outward similarity, a remote method invocation is a more expensive operation than a local method invocation.

The above description glosses over some important details that are significant for program performance. What happens when a remote method returns not a primitive type, but an object? That depends. If the returned object is a type that supports remote method invocation, then it creates a stub and skeleton object, as is the case when looking up a remote object in the registry. That is clearly an expensive operation. (Remote objects support a form of distributed garbage collection, which involves each of the participating JVMs maintaining a thread for talking to the remote garbage collection thread of other JVMs, and sending reference status information back and forth.) If the returned object doesn't support remote invocation, then all of the object's fields and any objects referenced by the returned object have to be marshaled, which could also be an expensive operation.

Performance comparison between remote and local method invocation

The performance characteristics of remote object access differ from those of local access:

  • Remote object creation is more expensive than local object creation. Not only does the object need to be created if it does not already exist, but the stub and skeleton objects need to be created, and they have to be made aware of each other.
  • Remote method invocations involve a network round-trip -- the marshaled parameters must be sent to the remote system and the response must be marshaled and sent back before the calling program regains control. The delays from marshaling, unmarshaling, network latency, and the actual remote invocation are additive; the client generally waits while all of those steps are being accomplished. The costs of a remote invocation also vary depending on the latency of the underlying network.
  • Different data types have different marshaling overhead. Marshaling primitive types is relatively inexpensive; marshaling simple objects like Point or String is slightly more expensive; marshaling remote objects is much more expensive, and marshaling objects that reference many other objects (like collections) can be even more expensive. That is in stark contrast to local invocations, for which passing a reference to a simple object is no less expensive than passing a reference to a complex object.

Interface design is critical

A poorly designed remote interface can kill a program's performance. Unfortunately, the characteristics that make for a good interface design for local objects can prove inappropriate for remote objects. Excessive temporary object creation, as discussed in parts 1 and 2 of this series, can hamper distributed applications too, but excessive round-trips are an even bigger performance problem. Therefore, calling a remote method that returns multiple values contained in a temporary object (such as a Point) rather than making multiple consecutive method calls to retrieve them individually is likely to be more efficient. (Note that this is exactly the opposite of the advice I offered in Part 2 for local objects.)

Some important performance guidelines for designing remote applications:

  • Watch out for unnecessary round-trips. If a caller wants to retrieve several related items simultaneously, make it easy to do so in one remote invocation, if possible.
  • Watch out for returning remote objects when the caller may not need to hold a reference to the remote object.
  • Watch out for passing complex objects to remote methods when the remote object doesn't necessarily need to have a copy of the object.

Fortunately, you can spot all of these problems simply by looking at the remote object's interface. The sequence of method calls required to perform any high-level action is usually obvious from the class interface. If you see that a common high-level operation requires many consecutive remote method calls, this should be a warning sign that perhaps you need to revisit the class's interface.

Techniques for reducing remote invocation overhead

As an example, consider the following hypothetical application for managing a corporate directory: A remote Directory object dispenses references to DirectoryEntry objects, which represent entries in a phone book:

public interface Directory extends Remote {
  DirectoryEntry[] getEntries();
  void addEntry(DirectoryEntry entry);
  void removeEntry(DirectoryEntry entry);
}
public interface DirectoryEntry extends Remote {
  String getName();
  String getPhoneNumber();
  String getEmailAddress();
}

Now, suppose you want to use the Directory facility in a GUI email application. The application first calls getEntries() to retrieve a list of entries, and then calls getName() on each entry, populating a list box with the results. When the user selects a recipient, the application then calls getEmailAddress() on the corresponding entry to retrieve the email address.

How many remote method invocations must occur before you can compose an email? You must call getEntries() once, getName() once for each entry in your address book, and then getEmailAddress() once. So if there are N entries in your address book, you have to make N+2 remote invocations. Note that you also must create N+1 remote object references, which is also an expensive operation. Not only would that slow the process of opening an email window if you had numerous names in your address book, but it would also create heavy network traffic and place a high load on your directory server application, leading to scalability problems.

Now consider this improved interface for Directory:

public interface Directory extends Remote {
  String[] getNames();
  DirectoryEntry[] getEntries();
  DirectoryEntry getEntryByName(String name);
  void addEntry(DirectoryEntry entry);
  void removeEntry(DirectoryEntry entry);
}

How much will this reduce the overhead imposed on your email application? Now you can call Directory.getNames() once and retrieve all the names simultaneously, and you only have to call getEntryByName() for the recipient to which you actually wish to send email. This process requires three remote invocations, instead of N+2, and two remote objects, instead of N+1. If the address book has more than a few names, this invocation reduction will make a huge difference in application responsiveness and in total network and system load.

This technique used to reduce remote invocation and reference-passing overhead is called using a secondary object identifier. Instead of passing back a remote object, use an identifying attribute of the object -- in the case of the email application, the name -- to act as a lightweight identifier for the object. The secondary identifier conveys enough information about the object it describes so that you only need to fetch the remote objects you actually need. In the case of a directory system, a person's name is usually a good secondary identifier. As another example, in a securities portfolio management system, a stock's ticker symbol could be a good secondary identifier.

Another useful technique for reducing the number of remote invocations is bulk retrieval. You could further improve the Directory interface by adding another method to retrieve multiple desired DirectoryEntry objects at once:

public interface Directory extends Remote {
  String[] getNames();
  DirectoryEntry[] getEntries();
  DirectoryEntry getEntryByName(String name);
  DirectoryEntry[] getEntriesByName(String names[]);
  void addEntry(DirectoryEntry entry);
  void removeEntry(DirectoryEntry entry);
}

Now, not only can you retrieve only the remote DirectoryEntry objects you need, but you can retrieve all the desired entries with a single remote method call. While that doesn't reduce the marshaling overhead, it can significantly reduce the number of round-trips and, if the network latency is at all significant, it will result in a more responsive system (and reduce total network usage).

A third technique for further lightening the load on the RMI layer would be to not make the DirectoryEntry a remote object, but instead define it as an ordinary object with fields or accessors for name, address, email address, and so forth. (In a CORBA system, we would use the analogous object-by-value mechanism.) Then, when the email application calls getEntryByName(), it will retrieve an entry object by value -- which doesn't require the creation of a stub or skeleton, and the invocation of getEmailAddress() will be a local invocation instead of a remote one.

Of course, all of these techniques rely on an understanding of how the remote objects will actually be used, but with that understanding, you don't even need to look at the remote class's implementation to spot several serious potential performance problems.

Conclusion

Distributed applications have performance characteristics that are substantially different from those of local applications. Many operations that are quite cheap in a local application can be very expensive in a remote application, and a naively designed remote interface can lead to an application that has serious scalability and performance problems.

Fortunately, it is easy to identify and fix many common distributed performance problems at design time by examining the common use cases and analyzing them for expensive operations like remote invocations and remote object creations. Judicious use of the techniques presented here -- secondary object identifiers, bulk retrieval, and return-by-value -- can substantially improve both user response time and total system throughput.

Brian Goetz is a professional software developer with more than 15 years of experience. He is a principal consultant at Quiotix, a software development and consulting firm located in Los Altos, Calif.

Learn more about this topic

  • Books on Java performance:
  • Distributed application design
  • Read more from Brian Goetz:

Join the discussion
Be the first to comment on this article. Our Commenting Policies
See more