- 104 -
.appendcharForDigit[c10]; else if i 1000000 six digits
... Im sure you get the idea else if i 10000000 seven digits
... so just keep doing the same, but more else if i 100000000 eight digits
... because my editor doesnt like wasting all this space else if i 1000000000 nine digits
... on unnecessary repetitions else
{ ten
digits s.appendcharForDigit[i1000000000];
s.appendcharForDigit[c=i1000000000100000000]; s.appendcharForDigit[c=10000000010000000];
s.appendcharForDigit[c=100000001000000]; s.appendcharForDigit[c=1000000100000];
s.appendcharForDigit[c=10000010000]; s.appendcharForDigit[c=100001000];
s.appendcharForDigit[c=1000100]; s.appendcharForDigit[c=10010];
s.appendcharForDigit[c10]; }
}
If you compare this implementation to executing
StringBuffer.appendint
, the algorithm listed here runs in less time for all except the latest VM, and creates two fewer objects
[3]
see Table 5-2
. This is faster than the JDK optimized version, has a smaller impact on garbage creation, and has all
the other advantages previously listed for the
long
conversion i.e., it is easily generalized for other radix values, digits are iterated in order so you can write to a stream, and it is easier to alter for
formatting without using temporary objects. Note that the
long
conversion method can also be improved using two of the three techniques we used for the
int
conversion method: inlining the magnitude method and unrolling the loop.
[3]
If the
StringBuffer.appendint
used the algorithm shown here, it would be faster for all JDK versions measured in this chapter, since the characters could be added directly to the
char
buffer without going through the
StringBuffer.appendchar
method.
Table 5-2, Time Taken to Append an int to a StringBuffer
VM 1.2 1.3
HotSpot 1.0
1.1.6
JDK int conversion 100 61
89 148
Optimized int conversion 84 60
81 111
5.3.3 Converting bytes, shorts, chars, and booleans to Strings
You can use the
int
conversion method for
byte
s and
short
s using overloading. You can make
byte
conversion even faster using a
String
array as a lookup table for the 256
byte
values. The conversion of
byte
s and
short
s to
String
s in the JDK appears not to have been tuned to as high a standard as radix 10
int
s up to JDK 1.3. This means that the
int
conversion algorithm shown previously, when applied to
byte
s and
short
s, is significantly faster than the JDK conversions and does not produce any temporary objects.
When it comes to using the other data types, there is no need to handle
boolean
s in any special way: the
Boolean.toString
already uses canonical strings. And there is obviously nothing in particular that needs to be done for
char
s apart from making sure you add them to strings as characters, not numbers.
- 105 -
5.3.4 Converting floats to Strings
Converting floating-point numbers to strings turns out to be hideously underoptimized in every version of the JDK up to 1.3 and maybe beyond. Looking at the JDK code and comments, it
seems that no one has yet got around to tuning these conversions. Floating-point numbers can be converted using similar optimizations to the number conversions previously addressed. You need to
check for and handle the special cases separately. You then scale the floats into an integer value and use the previously defined
int
conversion algorithm to convert to characters in order, ensuring that you format the decimal point at the correct position. The case of values between .001 and
10,000,000 are handled differently, because these are printed without exponent values; all other floats are printed with exponents. Finally, it would be possible to overload the
float
and
double
case, but it turns out that if you do this, the
float
does not convert as well in correctness or speed, so it is necessary to duplicate the algorithms for the
float
and
double
cases. Note that the printed values of
float
s and
double
s are, in general, only representative of the underlying value. This is true both for the JDK algorithms and the conversions here. There are times
when the string representation comes out differently for the two implementations, and neither is actually more accurate. The algorithm used by the JDK prints the minimum number of digits
possible, while maintaining uniqueness of the printed value with respect to the other floating-point values adjacent to the value being printed. The algorithm presented here prints the maximum
number of digits not including trailing zeros regardless of whether some digits are not needed to distinguish the number from other numbers. For example, the
Float.MIN_VALUE
is printed by the JDK as 1.4E-45, whereas the algorithm here prints it as 1.4285714E-45. Because of the
limitations in the accuracy of numbers, neither printed representation is more or less accurate compared to the underlying floating-point number actually held in
Float.MIN_VALUE
e.g., assigning both 1.46e-45F and 1.45e-45F to a
float
results in
Float.MIN_VALUE
being assigned. Note that the code that follows shortly uses the previously defined
append
method for appending
long
s to
StringBuffers
. Also note that the dot character has been hardcoded as the decimal separator character here for clarity, but it is straightforward to change for
internationalization.
This method of converting floats to strings has the same advantages as those mentioned previously for integral types, i.e., it is printed in digit order, no temporary objects are generated, etc. The
double
conversion see the next section is similar to the
float
conversion, with all the same advantages. In addition, both algorithms are several times faster than the JDK conversions.
Normally, when you print out floating-point numbers, you print in a defined format with a specified number of digits. The default floating-point
toString
methods cannot format floating-point numbers; you must first create the string, then format it afterwards. The algorithm presented here
could easily be altered to handle formatting floating-point numbers without using any intermediate strings. This algorithm is also easily adapted to handle rounding up or down; it already detects
which side of the half value the number is on:
public static final char[] NEGATIVE_INFINITY = {-,I,n,f,i,n,i,t,y};
public static final char[] POSITIVE_INFINITY = {I,n,f,i,n,i,t,y};
public static final char[] NaN = {N,a,N}; private static final int
floatSignMask = 0x80000000; private static final int
floatExpMask = 0x7f800000; private static final int
floatFractMask= ~floatSignMask|floatExpMask; private static final int
floatExpShift = 23; private static final int
floatExpBias = 127; change dot to international character where this is used below
- 106 -
public static final char[] DOUBLE_ZERO = {0,.,0}; public static final char[] DOUBLE_ZERO2 = {0,.,0,0};
public static final char[] DOUBLE_ZERO0 = {0,.}; public static final char[] DOT_ZERO = {.,0};
private static final float[] f_magnitudes = { 1e-44F, 1e-43F, 1e-42F, 1e-41F, 1e-40F,
1e-39F, 1e-38F, 1e-37F, 1e-36F, 1e-35F, 1e-34F, 1e-33F, 1e-32F, 1e-31F, 1e-30F, 1e-29F, 1e-28F, 1e-27F, 1e-26F, 1e-25F, 1e-24F, 1e-23F, 1e-22F, 1e-21F, 1e-20F,
1e-19F, 1e-18F, 1e-17F, 1e-16F, 1e-15F, 1e-14F, 1e-13F, 1e-12F, 1e-11F, 1e-10F, 1e-9F, 1e-8F, 1e-7F, 1e-6F, 1e-5F, 1e-4F, 1e-3F, 1e-2F, 1e-1F,
1e0F, 1e1F, 1e2F, 1e3F, 1e4F, 1e5F, 1e6F, 1e7F, 1e8F, 1e9F, 1e10F, 1e11F, 1e12F, 1e13F, 1e14F, 1e15F, 1e16F, 1e17F, 1e18F, 1e19F,
1e20F, 1e21F, 1e22F, 1e23F, 1e24F, 1e25F, 1e26F, 1e27F, 1e28F, 1e29F, 1e30F, 1e31F, 1e32F, 1e33F, 1e34F, 1e35F, 1e36F, 1e37F, 1e38F
}; public static void appendStringBuffer s, float d
{ handle the various special cases
if d == Float.NEGATIVE_INFINITY s.appendNEGATIVE_INFINITY;
else if d == Float.POSITIVE_INFINITY s.appendPOSITIVE_INFINITY;
else if d = d s.appendNaN;
else if d == 0.0 {
can be -0.0, which is stored differently if Float.floatToIntBitsd floatSignMask = 0
s.append-; s.appendDOUBLE_ZERO;
} else
{ convert negative numbers to positive
if d 0 {
s.append-; d = -d;
} handle 0.001 up to 10000000 separately, without exponents
if d = 0.001F d 0.01F {
long i = long d 1E12F; i = i100 = 50 ? i100 + 1 : i100;
s.appendDOUBLE_ZERO2; appendFractDigitss, i,-1;
} else if d = 0.01F d 0.1F
{ long i = long d 1E11F;
i = i100 = 50 ? i100 + 1 : i100; s.appendDOUBLE_ZERO;
appendFractDigitss, i,-1; }
else if d = 0.1F d 1F {
long i = long d 1E10F; i = i100 = 50 ? i100 + 1 : i100;
s.appendDOUBLE_ZERO0; appendFractDigitss, i,-1;
} else if d = 1F d 10F
{
- 107 -
long i = long d 1E9F; i = i100 = 50 ? i100 + 1 : i100;
appendFractDigitss, i,1; }
else if d = 10F d 100F {
long i = long d 1E8F; i = i100 = 50 ? i100 + 1 : i100;
appendFractDigitss, i,2; }
else if d = 100F d 1000F {
long i = long d 1E7F; i = i100 = 50 ? i100 + 1 : i100;
appendFractDigitss, i,3; }
else if d = 1000F d 10000F {
long i = long d 1E6F; i = i100 = 50 ? i100 + 1 : i100;
appendFractDigitss, i,4; }
else if d = 10000F d 100000F {
long i = long d 1E5F; i = i100 = 50 ? i100 + 1 : i100;
appendFractDigitss, i,5; }
else if d = 100000F d 1000000F {
long i = long d 1E4F; i = i100 = 50 ? i100 + 1 : i100;
appendFractDigitss, i,6; }
else if d = 1000000F d 10000000F {
long i = long d 1E3F; i = i100 = 50 ? i100 + 1 : i100;
appendFractDigitss, i,7; }
else {
Otherwise the number has an exponent int magnitude = magnituded;
long i; if magnitude -35
i = long d1E10F f_magnitudes[magnitude + 45]; else
i = long d f_magnitudes[magnitude + 44 - 9]; i = i100 = 50 ? i100 + 1 : i100;
appendFractDigitss, i, 1; s.appendE;
appends,magnitude; }
} return this;
} private static int magnitudefloat d
{ return magnituded,Float.floatToIntBitsd;
} private static int magnitudefloat d, int floatToIntBits
- 108 -
{ int magnitude =
int floatToIntBits floatExpMask floatExpShift - floatExpBias 0.301029995663981;
if magnitude -44 magnitude = -44;
else if magnitude 38 magnitude = 38;
if d = f_magnitudes[magnitude+44] {
whilemagnitude 39 d = f_magnitudes[magnitude+44] magnitude++;
magnitude--; return magnitude;
} else
{ whilemagnitude -45 d f_magnitudes[magnitude+44]
magnitude--; return magnitude;
} }
private static void appendFractDigitsStringBuffer s, long i, int decimalOffset {
long mag = magnitudei; long c;
while i 0 {
c = imag; s.appendcharForDigit[int c];
decimalOffset--; if decimalOffset == 0
s.append.; change to use international character c = mag;
if c = i i -= c;
mag = mag10; }
if i = 0 s.appendcharForDigit[int i];
else if decimalOffset 0 {
s.appendZEROS[decimalOffset]; ZEROS[n] is a char array of n 0s decimalOffset = 1;
} decimalOffset--;
if decimalOffset == 0 s.appendDOT_ZERO;
else if decimalOffset == -1 s.append0;
}
The conversion times compared to the JDK conversions are shown in Table 5-3
. Note that if you are formatting floats, the JDK conversion requires additional steps and so takes longer. However, the
method shown here is likely to take even less time, as you normally print fewer digits that require fewer loop iterations.
Table 5-3, Time Taken to Append a float to a StringBuffer
VM 1.2 1.3
HotSpot 1.0
1.1.6
- 109 -
JDK float conversion 100
85 270
128 Optimized float conversion
26 30 95
33
5.3.5 Converting doubles to Strings