什么是保留三位小数的最快方法?

8

SO社区是正确的,在询问性能问题之前对代码进行性能分析比我随机猜测更有意义 :-) 我对我的代码进行了性能分析(非常密集的数学计算),没有意识到超过70%的代码显然位于我认为不会导致减速的部分,即小数精度四舍五入。

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

我的问题是我会得到一些十进制数,通常是0.01,0.02等等...但有时候我会得到像0.070000000001这样的数字(我只关心0.07,但浮点精度导致其他公式计算失败),我只想要前三位小数以避免这个问题。
那么有更好/更快的方法来解决这个问题吗?

2
请使用 BigDecimal - Eng.Fouad
1
是因为它很慢还是因为你在循环中过于频繁地调用了这个方法,所以花费了这么多时间? - Thomas
4
你意识到,由于类似于0.070这样的数字无法被表示为p/2ⁿ的形式,因此它实际上不能被精确地表示为“double”类型? - ruakh
@Thomas 很好的观点。虽然我经常在循环中调用它,但我同时还在同一循环中进行了许多其他操作。出于某种原因,这需要最长的时间(不是我怀疑的10多个数学公式或变量查找等)。 我仍在学习如何进行性能分析,所以可能我做错了,因为它看起来似乎是程序中最微不足道的部分。 - Lostsoul
@ruakh 我理解,但我正在寻找百分比,最深的程度是三位小数 (7.1%=0.071)。获取精确的数字会对我的程序造成一些问题,所以我尝试将其四舍五入到三位。 - Lostsoul
显示剩余4条评论
2个回答

16

标准的四舍五入(仅适用于正数)的方法通常如下:

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

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

但正如@nuakh评论的那样,你总是会受到一定程度的舍入误差的困扰。如果你想要确切地三位小数,最好的方法是将所有数值乘以1000(即转换为千分之一),然后使用整数数据类型(例如intlong等)进行计算。

这种情况下,你可以跳过最后一次除以1000的操作,并使用整数值123568进行计算。如果你希望结果以百分比形式显示,则需要将结果除以10:

123 → 12.3%
568 → 56.8%


1
好的。所以,不要使用0.071,而是乘以1000并使用71进行计算。然后,在完成计算后,将最终结果除以10(即除以1000,然后乘以100%),以便在显示之前获得7.1%。 - Adam Liss
1
好的,我会试一下。实际上我不太想添加新的细节,尽管它们是相关的(但对我来说似乎是无关的),但我实际上是从一个循环(在另一个情况下是从1到100和1到1000)中生成百分数,然后将结果相乘以将它们转换为百分比公式。我将不得不重构相当多的公式,但我认为这是可能的。我会尝试一下并让你知道结果。 - Lostsoul
1
更新并提供示例。 - Adam Liss
1
哥们,你太牛了!你的公式让我的代码执行时间从15-20秒缩短到了6秒!我会尝试修改代码,不再使用小数而是使用整数,并告诉你结果。非常感谢!我觉得这个分析器会教会我新的解决问题的方法! - Lostsoul
1
很高兴能帮忙。学习对代码进行性能分析是为自己做了一件非常重要的事情。而且,一个真正的调试器比 printf 更好用。只要记住,优化过的代码比调试过的代码更容易进行优化。 :-) - Adam Liss
显示剩余5条评论

4

使用类型转换比使用 floor 或 round 更快。我怀疑类型转换在 HotSpot 编译器中被更加优化。

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;
    }
}

打印

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

注意:从Java 7开始,floor(x + 0.5)和round(x)并不像这个问题所述的那样完全相同。为什么Math.round(0.49999999999999994)返回1 这将在表示误差范围内正确四舍五入。这意味着虽然结果不是精确的小数,例如0.001没有被精确地表示,但当您使用toString()时,它会进行修正。只有当您转换为BigDecimal或执行算术运算时,才会看到这种表示误差。

@yetanothercoder 我之前使用过JMH,但是看看Java 8和JMH是否会产生不同的结果可能是值得的。 - Peter Lawrey

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接