4.1 A Summing Mystery?athe Magnitude Problem Let's start with a simple summing problem. The formula for the sum s n of the first n counting numbers, 1 through n, is
For example, the sum of the first 10 counting numbers is 55. Program 4-1 adds a slight twist to the summation. See Listing 4-1. It adds fractions instead, where, for each summation, the numerators are the first n counting numbers and the denominator is the sum s n . Therefore, the sum should be 1. For example, the fraction sum for s 10 is
The program computes the fraction sums with denominators s 10 , s 100 , s 1000 , and so on through s 100,000,000 . We can see from the output that things go fairly smoothly, with the computed sums close to 1, until denominator s 100,000,000 ?aand disaster strikes. In the last line of output, how did the sum manage to be 0.5? That's not even close! Listing 4-1 Fraction sums.package numbercruncher.program4_1; import numbercruncher.mathutils.AlignRight; /** * PROGRAM 4-1: Fraction Sums * * For each n, compute the sum 1/d + 2/d + 3/d + ... + n/d = d/d * where: * n = 10, 100, 1000, ..., 100,000,000 * d = 1 + 2 + 3 + ... + n = (n/2)(n + 1) * * See how close each sum is to 1. */ public class FractionSums { public static void main(String args[]) { AlignRight ar = new AlignRight(); ar.print("n", 9); ar.print("Denom", 14); ar.print("1/Denom", 14); ar.print("n/Denom", 13); ar.print("Sum", 11); ar.print("% Error", 13); ar.underline(); for (int n = 10; n <= 100000000; n *= 10) { float sum = 0; float denom = (0.5f*n)*(n + 1); // Sum fractions. for (int i = 1; i <= n; ++i) sum += i/denom; ar.print(n, 9); ar.print(denom, 14); ar.print(1/denom, 14); ar.print(n/denom, 13); ar.print(sum, 11); ar.print(100*Math.abs(sum - 1), 13); ar.println(); } } } Output: n Denom 1/Denom n/Denom Sum % Error --------------------------------------------------------------------------- 10 55.0 0.018181818 0.18181819 0.99999994 5.9604645E-6 100 5050.0 1.980198E-4 0.01980198 1.0 0.0 1000 500500.0 1.998002E-6 0.001998002 0.99999994 5.9604645E-6 10000 5.0005E7 1.9998E-8 1.9998E-4 0.9999999 1.1920929E-5 100000 5.0000502E9 1.9999799E-10 1.99998E-5 0.99999875 1.2516975E-4 1000000 5.00000489E11 1.999998E-12 1.999998E-6 0.9998997 0.010031462 10000000 5.0000004E13 1.9999998E-14 1.9999999E-7 1.002663 0.26630163 100000000 5.0000001E15 1.9999999E-16 2.0E-8 0.5 50.0 Why should the fraction sum for denominator s 100,000,000 fail so miserably when the previous sums all did so well? It isn't underflow or overflow. For each sum, the program prints out the values of the first fraction and last fraction, and their exponent values are well within the range (-45 through +38) for the float type. Each sum is computed from many addends, and so we might expect most of the high and low roundoff errors to cancel each other out. For one possible source of the problem, recall our computation of the machine epsilon at the end of Chapter 3. Since the sum is supposed to be 1, and the float value is around 6.0 x 10 - 8 , we can see that, with denominator s 100,000,000 , a significant percentage of the addends may have no effect on the sum as it approaches 1. Program 4-2 provides some insight. See Listing 4-2. It computes the fraction sum for denominator s 100,000,000 by dividing the series of fractions into 20 equal- sized groups, and at the end of each group it prints the current fraction and the current value of the running sum. Before adding each fraction to the running sum, it calculates the difference between the fraction's binary exponent and the running sum's binary exponent. At the end of each group, it also prints the minimum exponent difference of that group and the percentage of exponent differences within the group that exceeded 24. (Recall that, at the end of Chapter 3, we saw that, for type float, the unbiased binary exponent of the machine epsilon is -24.) Listing 4-2 Fraction sums for n = 100,000,000.package numbercruncher.program4_2; import numbercruncher.mathutils.AlignRight; /** * PROGRAM 4-2: Fraction Sum 100M * * Compute the sum 1/d + 2/d + 3/d + ... + n/d = d/d * where: * n = 100,000,000 * d = 1 + 2 + 3 + ... + n = (n/2)(n + 1) * * See why the sum ends up being 0.5 */ public class FractionSum100M { private static final int GROUPS = 20; private static final int MAX = 100000000; // 100M public static void main(String args[]) { AlignRight ar = new AlignRight(); ar.print("i", 9); ar.print("Fraction", 15); ar.print("Running sum", 15); ar.print("Min exp diff", 15); ar.print("% ExpDiff>24", 15); ar.underline(); float sum = 0; float denom = (0.5f*MAX)*(MAX + 1); int gSize = MAX/GROUPS; // group size int gEnd = gSize; // index of group end int minDiff = Integer.MAX_VALUE; // min exponent difference int exceeds = 0; // # of exponent diff > 24 // Loop to sum the fractions. for (int i = 1; i <= MAX; ++i) { float fraction = i/denom; // Compute the exponent difference. int expSum = Float.floatToIntBits(sum) >> 23; int expFraction = Float.floatToIntBits(fraction) >> 23; int diff = Math.abs(expSum - expFraction); if (i > 1) { minDiff = Math.min(minDiff, diff); if (diff > 24) ++exceeds; } sum += fraction; // Printout at the end of each group. if (i == gEnd) { ar.print(i, 9); ar.print(fraction, 15); ar.print(sum, 15); ar.print(minDiff, 15); ar.print((100*exceeds)/gSize, 15); ar.println(); minDiff = Integer.MAX_VALUE; exceeds = 0; gEnd += gSize; } } } } Output: i Fraction Running sum Min exp diff % ExpDiff>24 --------------------------------------------------------------------- 5000000 1.0E-9 0.002493547 0 0 10000000 2.0E-9 0.009991142 21 0 15000000 3.0E-9 0.022081949 22 0 20000000 4.0E-9 0.0407084 23 0 25000000 5.0E-9 0.05933485 23 0 30000000 6.0E-9 0.09342261 23 0 35000000 6.9999997E-9 0.125 24 15 40000000 8.0E-9 0.16593489 24 45 45000000 9.0E-9 0.2404407 24 0 50000000 1.0E-8 0.25 24 87 55000000 1.1E-8 0.25 25 100 60000000 1.2E-8 0.25 25 100 65000000 1.2999999E-8 0.25 25 100 70000000 1.3999999E-8 0.25 25 100 75000000 1.5E-8 0.26472795 24 90 80000000 1.6E-8 0.41373956 24 0 85000000 1.7E-8 0.5 24 42 90000000 1.8E-8 0.5 25 100 95000000 1.9E-8 0.5 25 100 100000000 2.0E-8 0.5 25 100 Now we begin to see what happened . If the difference in magnitude between two addends is too great, the smaller addend loses much of its contribution to the sum. Indeed, as we saw at the end of Chapter 3, smaller addends will have no effect at all on the sum if the difference between their binary exponents is greater than 24 when doing float addition. Program 4-2 shows that, whenever the value of the running sum reaches the next power of 2, the sum's binary exponent increases by 1. If that widens the exponent difference between the sum and the upcoming fractions to more than 24, then those fractions have no effect on the sum until they become large enough to lower the exponent difference. Thus, the running sum "stalls" at 0.25 and again at 0.5 when the minimum exponent differences are 25. Figure 4-1 shows this clearly by graphing the values of the running sum versus the minimum exponent differences in each group. Figure 4-1. A graph of the output of Program 4 §C2. The running sum "stalls" at 0.25 and at 0.5 when the minimum exponent differences exceed 24.
Would it make a difference if we summed the fractions in the reverse order, from the largest down to the smallest? Program 4-3 shows that the running sum still stalls at 0.5, and we have even greater percentages of exponent differences exceeding 24. See Listing 4-3. So we've made the problem worse ?athe values of the running sum increase while the fractions decrease, making the exponent differences even larger. In general, when adding a sorted list of numbers all of the same sign, it is better to add them in the order from the smallest magnitude up to the largest. Listing 4-3 Fraction sums for n = 100,000,000 in reversed order.package numbercruncher.program4_3; import numbercruncher.mathutils.AlignRight; /** * PROGRAM 4-3: Fraction Sum 100M in the Reversed Order * * Compute the sum n/d + (n-1)/d + (n-2)/d + ... + 2/d + 1/d = d/d * where: * n = 100,000,000 * d = 1 + 2 + 3 + ... + n = (n/2)(n + 1) * * See if the sum is closer to 1. */ public class FractionSum100MReversed { private static final int GROUPS = 20; private static final int MAX = 100000000; // 100M public static void main(String args[]) { AlignRight ar = new AlignRight(); ar.print("i", 9); ar.print("Fraction", 15); ar.print("Running sum", 15); ar.print("Min exp diff", 15); ar.print("% ExpDiff>24", 15); ar.underline(); float sum = 0; float denom = (0.5f*MAX)*(MAX + 1); int gSize = MAX/GROUPS; // group size int gStart = (MAX + 1) - gSize; // index of group start int minDiff = Integer.MAX_VALUE; // min exponent difference int exceeds = 0; // # of exponent diff > 24 // Loop to sum the fractions. for (int i = MAX; i >= 1; - i) { float fraction = i/denom; int expSum = Float.floatToIntBits(sum) >> 23; int expFraction = Float.floatToIntBits(fraction) >> 23; int diff = Math.abs(expSum - expFraction); if (i < MAX) { minDiff = Math.min(minDiff, diff); if (diff > 24) ++exceeds; } sum += fraction; // Printout at the start of each group. if (i == gStart) { ar.print(i, 9); ar.print(fraction, 15); ar.print(sum, 15); ar.print(minDiff, 15); ar.print((100*exceeds)/gSize, 15); ar.println(); minDiff = Integer.MAX_VALUE; exceeds = 0; gStart -= gSize; } } } } Output: i Fraction Running sum Min exp diff % ExpDiff>24 --------------------------------------------------------------------- 95000001 1.9E-8 0.10206167 0 0 90000001 1.8E-8 0.18421358 22 0 85000001 1.7E-8 0.26743877 23 0 80000001 1.6E-8 0.41645038 24 0 75000001 1.5E-8 0.5 24 43 70000001 1.3999999E-8 0.5 25 100 65000001 1.2999999E-8 0.5 26 100 60000001 1.2E-8 0.5 26 100 55000001 1.1E-8 0.5 26 100 50000001 1.0E-8 0.5 26 100 45000001 9.0E-9 0.5 26 100 40000001 8.0E-9 0.5 26 100 35000001 6.9999997E-9 0.5 26 100 30000001 6.0E-9 0.5 27 100 25000001 5.0E-9 0.5 27 100 20000001 4.0E-9 0.5 27 100 15000001 3.0E-9 0.5 27 100 10000001 2.0000002E-9 0.5 28 100 5000001 1.0000002E-9 0.5 28 100 1 1.9999999E-16 0.5 29 100 We have a clear challenge. In order for a summation to be accurate, we need to keep the running sum and each addend sufficiently close together in magnitude, so that the difference between their binary exponents is not greater than 24. Program 4-4 shows one way to do this. See Listing 4-4. The program partitions the fractions into 20 equal-sized groups. It computes a separate subtotal for each group; then it adds the subtotal to the running sum. Each group's fractions are closer in magnitude to the group subtotal, so the minimum difference between the binary exponents of a fraction and its group subtotal is under 24. Each group subtotal has enough "heft" to make its contribution to the overall sum. The trick is to get the right size for the groups. The final sum is very close to 1. Listing 4-4 Fraction sums for n = 100,000,000 using group subtotals.package numbercruncher.program4_4; import numbercruncher.mathutils.AlignRight; /** * PROGRAM 4-4: Fraction Sum 100M Using Group Subtotals * * Compute the sum 1/d + 2/d + 3/d + ... + n/d = d/d * where: * n = 100,000,000 * d = 1 + 2 + 3 + ... + n = (n/2)(n + 1) * * Compute the sum by adding the group subtotals. * See if the final sum is closer to 1. */ public class FractionSum100MGrouped { private static final int GROUPS = 20; private static final int MAX = 100000000; // 100M public static void main(String args[]) { AlignRight ar = new AlignRight(); ar.print("i", 9); ar.print("Group sum", 15); ar.print("Running sum", 15); ar.print("% ExpDiff>24", 15); ar.underline(); float sum = 0; // running sum float subtotal = 0; // group subtotal int gSize = MAX/GROUPS; // group size int gEnd = gSize; // index of group end int exceeds = 0; // # of exponent diff > 24 float denom = (0.5f*MAX)*(MAX + 1); // Sum the fractions by groups. for (int i = 1; i <= MAX; ++i) { float fraction = i/denom; int expSubtotal = Float.floatToIntBits(subtotal) >> 23; int expFraction = Float.floatToIntBits(fraction) >> 23; int diff = Math.abs(expSubtotal - expFraction); if ((subtotal > 0) && (diff > 24)) ++exceeds; subtotal += fraction; // Subtotal and printout at the end of each group. if (i == gEnd) { sum += subtotal; ar.print(i, 9); ar.print(subtotal, 15); ar.print(sum, 15); ar.print((100*exceeds)/gSize, 15); ar.println(); subtotal = 0; exceeds = 0; gEnd += gSize; } } System.out.println("\n% error = " + 100*Math.abs(sum - 1)); } } Output: i Group sum Running sum % ExpDiff>24 ------------------------------------------------------ 5000000 0.002493547 0.002493547 0 10000000 0.007556428 0.0100499755 0 15000000 0.012413543 0.02246352 0 20000000 0.017349107 0.039812624 0 25000000 0.023679595 0.06349222 0 30000000 0.02718998 0.0906822 0 35000000 0.034128636 0.12481084 0 40000000 0.036770158 0.161581 0 45000000 0.04185812 0.20343912 0 50000000 0.049500026 0.25293913 0 55000000 0.05435951 0.30729866 0 60000000 0.055825584 0.36312425 0 65000000 0.05929653 0.42242077 0 70000000 0.07003953 0.4924603 0 75000000 0.07346739 0.5659277 0 80000000 0.07455075 0.64047843 0 85000000 0.07784402 0.71832246 0 90000000 0.08436947 0.80269194 0 95000000 0.098553196 0.9012451 0 100000000 0.09986824 1.0011134 0 % error = 0.11134148 |
Top |