13.2 Tuning Class Libraries and Beans

Most code can be categorized into one of two general types:

  • Application-specific code, normally used for one particular application. If this code is reused, it usually provides only a skeleton that needs reimplementing. Occasionally, application-specific code is generic enough to reuse in another application, but even then it usually needs some rewriting to make it more generic.

  • Classes written specifically with reusability in mind. This type of code is usually referred to as class libraries, frameworks, components, and beans. I refer to all of these together as reusable code.

The first type of code, application-specific code, is considerably easier to tune. You can run the application as it is intended to be used, determine any bottlenecks, and successively tune away those bottlenecks. Typically, 80% of the application time is spent in less than 20% of the code, and only 5% of the application code actually needs to be changed during the tuning process.

The second type of code, reusable code, is much more difficult to tune. This code may be used in many situations that could never be foreseen by the developers. Without knowing how the code will be used, it is very difficult to create tests that appropriately determine the performance of reusable code. There is no truly valid technique that can be applied. Even exhaustively testing every method is of little use (not to mention generally impractical), since you almost certainly cannot identify useful performance targets for every method. Well-tuned reusable code can have 95% of the code altered in some way by the tuning process.[4]

[4] I have not seen any studies that show this cost. Instead, I base it on my own impression from examining early versions of various class libraries and comparing these classes with later versions. I find that most methods in a random selection of classes are altered in some way that I can identify as giving performance improvements.

The standard way to tune reusable code is to tune in response to identified problems. Usually the development team releases alpha and beta versions to successively larger groups of testers: other team developers, demo application developers, the quality-assurance team, identified beta testers, general beta testers, and customers of the first released version (some of these groups may overlap). Each of these groups provides feedback in identifying both bugs and performance problems. In fact, as we all know, this feedback process continues throughout the lifetime of any reusable code. But the majority of bugs and performance problems are identified by this initial list of users. This reactive process is hardly ideal, but any alternative makes tuning reusable code very expensive. This is unlike bug testing, in which the quality of the test suite and quality-assessment process makes a big difference to the reliability of the released version, and is fully cost-effective.

There are several consequences to this reactive process. First, from the viewpoint of the developer using reusable components, you need to be aware that first versions frequently have suboptimal performance. Note that this does not imply anything about the quality of the software: it may have wonderfully comprehensive features and be delightfully bug-free. But even in a large beta testing program with plenty of feedback, there is unlikely to be sufficient time to tune the software and repeat the test and release procedures. Getting rid of identified bugs rightfully takes precedence, and developers normally focus on the (next) released version being as bug-free as possible.

Second, for developers creating reusable code, the variety of applications testing the reusable code is more important than the absolute number of those applications. Ten people telling you that method X is slow is not as useful as two telling you that method X is slow and two telling you that method Y is slow.

A further consequence when developing reusable code is that to provide greater performance flexibility for the users of those classes, you need to design more flexible method entry points to your classes. Providing performance flexibility unfortunately clashes with the "defensive" programming that is (reasonably) used when creating reusable classes. For example, a defensive developer creating a collection class based on an array (e.g., java.util.Vector) might provide a constructor that accepts an array and copies its elements:

public class ArrayBasedCollection
{
  int arraySize;
  Object[  ] array;
  public ArrayBasedCollection(Object[  ] passedArray)
  {
    arraySize = passedArray.length;
    array = new Object[arraySize];
    System.arraycopy(passedArray, 0, array, 0, arraySize);
  }
  ...

The defensive developer always ensures that elements are copied into a new array so that no external object retains a reference to the internal array and interrupts the logic of the class. This ensures that the new class cannot be inadvertently corrupted. However, this provides inefficient performance. There will be cases where the application coder has created the array specifically to hold the objects and will discard that array immediately. Developing flexibly with performance in mind directs you to add an extra method that allows the array to be used directly:

public class ArrayBasedCollection
{
  int arraySize;
  Object[  ] array;
  public ArrayBasedCollection(Object[  ] passedArray)
  {
    this(passedArray, true);
  }
  /**
   * If <copy> is true, the elements of <passedArray> are
   * copied into the a new underlying array in the collection.
   * If <copy> is false, the <passedArray> is assigned directly
   * as the underlying array. This is potentially dangerous:
   * the collection object can be corrupted if the <passedArray>
   * is altered directly by another object afterwards.
   */
  public ArrayBasedCollection(Object[  ] passedArray, boolean copy)
  {
    arraySize = passedArray.length;
    if (copy)
    {
      array = new Object[arraySize];
      System.arraycopy(passedArray, 0, array, 0, arraySize);
    }
    else
      array = passedArray;
  }
  ...

This opens the collection object to potential corruption, but by retaining the original one-arg constructor, you have reduced the chance that the two-arg constructor will be used accidentally. A developer looking quickly for a constructor is likely to use the one-arg constructor, whereas a developer desperately searching through the documentation for ways to reduce the number of copies made from several large arrays will be delighted to discover the two-arg constructor.

Finally, perhaps the most significant way to create reusable code that performs well is for developers to be well-versed in performance tuning. After any significant amount of performance tuning, many of the techniques in this book can become second nature. Developers experienced in performance tuning can produce reusable code that is further along the performance curve right from the first cut. Writing reusable code is one of the few situations in which it is sometimes preferable to consider performance when first writing the code.