Optimizing Printf for Java Applications

Printf formatting is often used in loops to produce neatly formatted tables. Sharkysoft's Printf for Java was designed with the ability to accelerate printf formatting in loops. To demonstrate the many ways in which Printf for Java can be optimized, we'll examine a simple program that outputs an ASCII table. Each of the code samples shown below produces exactly the same output.

AsciiTable1

This first example shows the easiest way to use printf in Java.

import com.sharkysoft.printf.Printf;
import com.sharkysoft.printf.PrintfData;

public class AsciiTable1
{

  public static void main(String[] args)
  {
    for (int i = 0; i < 256; ++ i)
    Printf.out
    ( "%#10c%+10d%10.4o%#10.2x\n",
      new PrintfData()
      .add((char) i)
      .add(i)
      .add(i)
      .add(i)
    );
  }
  
}

Since Java does not support variable argument lists, a special class, PrintfData, is used to emulate them. PrintfData's add method is overloaded for all of the primitive Java types. The completion of the argument list is (optionally) indicated by calling the done() method.

AsciiTable2

We're just kidding ourselves. We all know that no matter how you dress it up, in Java an untyped argument list is nothing more than a vector of Objects. In fact, I'll confess now that PrintfData simply uses a java.util.ArrayList in its implementation. Thus, in the above example, each add call simply appends its arguments to the ArrayList, and PrintfData automatically converts the list into an Object array.

Let's shed the overhead of building an Object array one call at a time. Let's do it the fast way instead:

import com.sharkysoft.printf.Printf;

public class AsciiTable2
{

  public static void main(String[] args)
  {
    for (int i = 0; i < 256; ++ i)
    Printf.out
    ( "%#10c%+10d%10.4o%#10.2x\n",
      new Object[]
      { new Character((char) i),
        new Integer(i),
        new Integer(i),
        new Integer(i)
      }
    );
  }
  
}

Pay careful attention to the syntax used here to build the argument list. Not only does this syntax cause the Java compiler to automatically choose the correct length for the Object array (in this case, 4), but it also populates the array with the 4 objects. This approach requires significantly less processing overhead than PrintfData, because the final array length is known at compile time (eliminating the need for dynamic array growth) and because there is no need to create an ArrayList, or to convert it to an Object array when you are done.

AsciiTable3

Here's another handly trick. Since the last three parameters to the argument list are the same value, why waste time creating three identical objects? Instead, let's optimize the loop further by creating just one Integer object, and then reusing it to populate the array:

import com.sharkysoft.printf.Printf;

public class AsciiTable3
{

  public static void main(String[] args)
  {
    for (int n = 0; n < 256; ++ n)
    {
      Integer i = new Integer(n);
      Printf.out
      ( "%#10c%+10d%10.4o%#10.2x\n",
        new Object[]
        { new Character((char) n),
          i,
          i,
          i
        }
      );
    }
  }
  
}

This optimization eliminates 2 object creations for each iteration.

AsciiTable4

Before printf can produce output, the format string must be parsed. In the above examples, the format string must be parsed once for each pass through the loop. What a waste of time! Why not just parse the format string outside the loop, and then reuse the same parse results every time inside the loop? This would certainly be an optimization. In Printf for Java, format strings can be "pre-parsed" by constructing instances of PrintfFormatString. When PrintfFormatString objects are used in place of a regular String objects for format strings, the resulting printf operation is always faster. The PrintfFormatStrings are like ready-to-go "formatting engines." Here's what the optimized code looks like:

import com.sharkysoft.printf.Printf;
import com.sharkysoft.printf.PrintfFormatString;

public class AsciiTable4
{

  public static void main(String[] args)
  {
    PrintfFormatString fmt = new PrintfFormatString("%#10c%+10d%10.4o%#10.2x\n");
    for (int n = 0; n < 256; ++ n)
    {
      Integer i = new Integer(n);
      Printf.out
      ( fmt,
        new Object[]
        { new Character((char) n),
          i,
          i,
          i
        }
      );
    }
  }
  
}

This little trick saves us the overhead of parsing the format string with each iteration through the loop. Whereever printf is used in a loop or in a method that is called repeatedly, the programmer should consider storing the pre-initialized format string in a private static variable in the class. This way, it will be initialized just once, when the classes is loaded, and ready for use each time it is needed. It's too bad the C version of printf didn't have this feature!

AsciiTable5

You may think we have completed all of the optimizations possible for this application of Printf for Java. Would you believe there's more? Most C programmers are unfamiliar with the fact that printf has a return value. The ANSI C specification of printf declares that in normal circumstances, the value returned by printf shall be the total number of characters formatted.

What does this have to do with speeding up AsciiTable? Well, the relationship is indirect. You see, Printf.out doesn't actually do any formatting. Instead, it delegates the job to another method, which performs the formatting and returns the results in a String. Then, Printf.out simply writes the String to System.out and returns the String's length.

In our case, however, we really don't care about counting characters, so we can bypass the syntactic sugar of Printf.out and call the formatting routine ourselves.

The main printf formatting workhorse is -- you guessed it -- the format method. The format method does the formatting and returns the formatted string. In many ways, this is similar to sprintf. What you do with the string when you get it is your own business, not Printf's. Let's optimize:

import com.sharkysoft.printf.Printf;
import com.sharkysoft.printf.PrintfFormatString;

public class AsciiTable5
{

  public static void main(String[] args)
  {
    PrintfFormatString fmt = new PrintfFormatString("%#10c%+10d%10.4o%#10.2x\n");
    for (int n = 0; n < 256; ++ n)
    {
      Integer i = new Integer(n);
      System.out.print
      ( Printf.format
        ( fmt,
          new Object[]
          { new Character((char) n),
            i,
            i,
            i
          }
        )
      );
    }
  }
  
}

Not pretty, but a bit faster. You see what's happening here? We're taking the formatted string returned by format and passing it to System.out.print for printing, bypassing the call to Printf.out entirely.

Conclusion

Compare AsciiTable1 to AsciiTable5 and see just how much our code has evolved. The first version was syntactically sweet, but the price for this sugar was unnecessary processing overhead at runtime (burning more calories, to keep the analogy). The final version is not as pleasant to look at (health food), and requires significantly more labor from the programmer (gardening), but the optimizing techniques employed result in significantly faster output (OK, family-friendly analogies fail me here).

A simple benchmark I ran, based on these examples, indicates that AsciiTable5 runs approximately 32% faster than AsciiTable1. For small formatting jobs, this speedup may not be worth the extra effort. However, if your program uses Printf for Java to produce large, formatted tables that are thousands of lines longs, this 32% speedup may be worth it.

Copyright ©1998-2004 Sharkysoft. All rights reserved.