8

The SO community was right, profiling your code before you ask performance questions seems to make more sense then my approach of randomly guessing :-) I profiled my code(very intensive math) and didn't realize over 70% of my code is apparently in a part I didn't think was a source of slowdown, rounding of decimals.

static double roundTwoDecimals(double d) {
    DecimalFormat twoDForm = new DecimalFormat("#.###");
    return Double.valueOf(twoDForm.format(d));
}

My problem is I get decimal numbers that are normally .01,.02,etc..but sometimes I get something like .070000000001 (I really only care about the 0.07 but floating point precision causes my other formulas that result to fail), I simply want the first 3 decimals to avoid this problem.

So is there a better/faster way to do this?

Lostsoul
  • 25,013
  • 48
  • 144
  • 239
  • 2
    Use [`BigDecimal`](http://docs.oracle.com/javase/6/docs/api/java/math/BigDecimal.html). – Eng.Fouad Apr 05 '12 at 03:14
  • 1
    Is it spending so much time there because it's slow or because you call this method too often in loops etc? – Thomas Apr 05 '12 at 03:15
  • 4
    You realize that, since something like 0.070 can't be expressed as p/2ⁿ, that it can't actually be expressed exactly as a `double`? – ruakh Apr 05 '12 at 03:16
  • @Thomas good point. I do call it often in loops but I am doing a lot more other stuff in the same loops. For some reason this is taking the largest time(not the 10+ math formulas I suspected or the variable lookups,etc). I'm still learning how to profile so I may have done it wrong because it seems like the most insignificant part of the program. – Lostsoul Apr 05 '12 at 03:17
  • @ruakh I understand that, but I'm looking for percents and the deepest I'm going is three spots(7.1% = 0.071). Getting the precise number is messing up my program a little so I try to round it to 3. – Lostsoul Apr 05 '12 at 03:26
  • You'd be able to improve performance a little by initializing your DecimalFormat once instead of creating a new DecimalFormat on every call. But the extra overhead in creating and parsing Strings is still going to hurt your performance. Multiplying everything by 1000 and using ints or longs, as Adam Liss suggests, will give you the best performance. – rob Apr 05 '12 at 03:30
  • @learningJava: I hope you're not using floats to represent money? I'm sure you know this never should be done except for rough estimated calculations. – 9000 Apr 05 '12 at 03:41
  • @9000 No, its just a percent of quantities(nothing to do with money). – Lostsoul Apr 05 '12 at 03:44
  • @Eng.Fouad, I'm pretty sure BigDecimal is the slowest way possible. :\ – st0le Apr 05 '12 at 04:02

2 Answers2

16

The standard way to round (positive) numbers would be something like this:

double rounded = floor(1000 * doubleVal + 0.5) / 1000;

Example 1: floor(1000 * .1234 + 0.5) / 1000 = floor(123.9)/1000 = 0.123
Example 2: floor(1000 * .5678 + 0.5) / 1000 = floor(568.3)/1000 = 0.568

But as @nuakh commented, you'll always be plagued by rounding errors to some extent. If you want exactly 3 decimal places, your best bet is to convert to thousandths (that is, multiply everything by 1000) and use an integral data type (int, long, etc.)

In that case, you'd skip the final division by 1000 and use the integral values 123 and 568 for your calculations. If you want the results in the form of percentages, you'd divide by 10 for display:

123 → 12.3%
568 → 56.8%

Adam Liss
  • 47,594
  • 12
  • 108
  • 150
  • Sorry I don't fully understand..is there any way I can avoid it. Basically what I'm doing involves percents..so three is most spaces I'll need. 07% or 07.1% getting .07000000001 is not enough to make a difference to my program. – Lostsoul Apr 05 '12 at 03:24
  • 1
    Right. So instead of, say .071, multiply by 1000 and use 71 for your calculations. Then, when you're done, divide the final result by 10 (that is, divide by 1000, then multiply by 100%) to get 7.1% before you display it. – Adam Liss Apr 05 '12 at 03:30
  • 1
    okay, I'll give that a shot..actually I hate to be adding new details that are relevant(but seemed irrelevant to me) but I'm actually generating the percents from a for loop(1 to 100 and 1 to 1000 in another case) and then multiplying the results to get them into a percent formula. I'll have to refactor quite a bit of the formulas but I think its possible. I'll play around with it and let you know the result. – Lostsoul Apr 05 '12 at 03:35
  • by the way, your results are different than my current results. For example: 34*.01 = 0.34. Adam's formula: (1000 * (c*.01) + 0.5) / 1000 0.3405 | 35*.01 = 0.35. Adam's formula: (1000 * (c*.01) + 0.5) / 1000 0.35050000000000003 | 36*.01 = 0.36. Adam's formula: (1000 * (c*.01) + 0.5) / 1000 0.3605 | sorry the formatting is off. – Lostsoul Apr 05 '12 at 03:39
  • 1
    Updated and provided examples. – Adam Liss Apr 05 '12 at 03:48
  • 1
    Dude, you ROCK!! Your formula cut my code execution down from 15-20 seconds to 6! I'll work on switching the code to not use decimals and use INT instead and let you know the result. Thanks so much! I feel this profiler is going to teach me new ways to approach problems! – Lostsoul Apr 05 '12 at 04:03
  • 1
    Happy to help. You're doing yourself a _huge_ favor by learning to profile your code. And a real debugger is a better friend than `printf`. Just remember that it's easier to optimize debugged code than it is to debug optimized code. :-) – Adam Liss Apr 05 '12 at 04:07
  • Its pretty sweet. Basically, I just write my normal crappy code and save the results(which I know are correct) then keep running my code against those results to test if I break anything. Profiler is helping alot but I haven't used a debugger yet(need to learn that). I have another question(ignore if out of scope)If you find your code slow and the profiler is saying the code thats taking time is a Java Library like Java.util.AbstractList.iterator(just an example)? Does that mean I should find alliterative libraries or is there something else I can do? – Lostsoul Apr 05 '12 at 04:17
  • 1
    In that case, ask yourself if there's a different implementation (like changing units and using integers instead of floats) that might be better suited to your purpose. But sometimes it's worth taking a hit in performance if the benefits are significant (maintainability, portability, etc). And sometimes there's just no good way to speed things up without leaving yourself feeling like you need to take a shower. – Adam Liss Apr 05 '12 at 11:04
  • Awesome..thanks again so much Adam. I asked one question but got so much knowledge from you. Thanks so much. – Lostsoul Apr 05 '12 at 22:15
4

Using a cast is faster than using floor or round. I suspect a cast is more heavily optimised by the HotSpot compiler.

public class Main {
    public static final int ITERS = 1000 * 1000;

    public static void main(String... args) {
        for (int i = 0; i < 3; i++) {
            perfRoundTo3();
            perfCastRoundTo3();
        }
    }

    private static double perfRoundTo3() {
        double sum = 0.0;
        long start = 0;
        for (int i = -20000; i < ITERS; i++) {
            if (i == 0) start = System.nanoTime();
            sum += roundTo3(i * 1e-4);
        }
        long time = System.nanoTime() - start;
        System.out.printf("Took %,d ns per round%n", time / ITERS);
        return sum;
    }

    private static double perfCastRoundTo3() {
        double sum = 0.0;
        long start = 0;
        for (int i = -20000; i < ITERS; i++) {
            if (i == 0) start = System.nanoTime();
            sum += castRoundTo3(i * 1e-4);
        }
        long time = System.nanoTime() - start;
        System.out.printf("Took %,d ns per cast round%n", time / ITERS);
        return sum;
    }

    public static double roundTo3(double d) {
        return Math.round(d * 1000 + 0.5) / 1000.0;
    }

    public static double castRoundTo3(double d) {
        return (long) (d * 1000 + 0.5) / 1000.0;
    }
}

prints

Took 22 ns per round
Took 9 ns per cast round
Took 23 ns per round
Took 6 ns per cast round
Took 20 ns per round
Took 6 ns per cast round

Note: as of Java 7 floor(x + 0.5) and round(x) don't do quite the same thing as per this issue. Why does Math.round(0.49999999999999994) return 1

This will round correctly to within the representation error. This means that while the result is not exact the decimal e.g. 0.001 is not represented exactly, when you use toString() it will correct for this. Its only when you convert to BigDecimal or perform an arithmetic operation that you will see this representation error.

Community
  • 1
  • 1
Peter Lawrey
  • 525,659
  • 79
  • 751
  • 1,130
  • @yetanothercoder I did use Jmh before it was written but it might be worth seeing if Java 8 and Jmh produces a different result. – Peter Lawrey Mar 03 '15 at 02:26