java.lang.Math和java.lang.StrictMath有什么区别?


Answers:


72

Math该类的Javadoc提供了有关两个类之间差异的一些信息:

与class的某些数字方法不同,classStrictMath的等效函数的所有实现 Math未定义为返回逐位相同的结果。这种放松允许在不需要严格的可重复性的情况下实现性能更好的实现。

默认情况下,许多Math方法只是在StrictMath实现中调用等效方法 。鼓励代码生成器使用特定于平台的本机库或微处理器指令(如果可用),以提供方法的更高性能的实现 Math。这种更高性能的实现仍必须符合的规范Math

因此,Math该类列出了有关某些操作应执行的规则,但它们并不要求在所有库实现中返回完全相同的结果。

这允许库的特定实现返回类似的结果,但如果例如Math.cos调用该类,则不能获得完全相同的结果。这将允许特定于平台的实现(例如使用x86浮点和SPARC浮点),这些实现可能会返回不同的结果。

(有关特定于平台的实现的示例,请参阅Wikipedia中Sine文章的“软件实现”部分。)

但是,使用时StrictMath,不同实现返回的结果必须返回相同的结果。对于需要在不同平台上再现结果的情况,这将是理想的。


3
但是,为什么不同的平台特定实现会产生不同的结果?余弦不是通用定义的吗?
艾瓦尔(Aivar)2013年

@Aivar:出于Math该类引用中列出的原因-利用特定平台可用的本机方法,这比(在许多情况下可能会)比使用基于软件的解决方案要快,这种解决方案可以保证在所有平台上给出完全相同的答案。
coobird13年

好的,这意味着某些平台选择了不计算适合给定位数的最精确答案,而是为了效率而牺牲了精度?不同的平台做出了不同的权衡?
艾瓦尔(Aivar)

@Aivar阅读链接的Wikipedia文章似乎就是这种情况。简而言之,Math该类的规范允许使用特定于平台的算法,这些算法不一定会返回与其他平台相同的结果。
coobird 2013年

1
@Aivar不仅是精度与效率的关系,而且在许多情况下,不一定有一个显而易见的“最精确的答案”。例如,Sine文章coobird链接提到“没有用于计算正弦的标准算法”。
dimo414 2014年

22

@ntoskrnl作为使用JVM内部构件的人,我想表达您的意见,即“内部函数不一定与StrictMath方法具有相同的行为”。为了找出(或证明)它,我们可以编写一个简单的测试。

就拿Math.pow例如,对于检查java.lang.Math.pow(双一,双二)Java代码中,我们将看到:

 public static double pow(double a, double b) {
    return StrictMath.pow(a, b); // default impl. delegates to StrictMath
}

但是JVM可以通过内部函数或运行时调用自由地实现它,因此返回的结果可能与我们期望的不同StrictMath.pow

以下代码显示了Math.pow()针对StrictMath.pow()

//Strict.java, testing StrictMath.pow against Math.pow
import java.util.Random;
public class Strict {
    static double testIt(double x, double y) {
        return Math.pow(x, y);
    }
    public static void main(String[] args) throws Exception{
        final double[] vs = new double[100];
        final double[] xs = new double[100];
        final double[] ys = new double[100];
        final Random random = new Random();

        // compute StrictMath.pow results;
        for (int i = 0; i<100; i++) {
            xs[i] = random.nextDouble();
            ys[i] = random.nextDouble();
            vs[i] = StrictMath.pow(xs[i], ys[i]);
        }
        boolean printed_compiled = false;
        boolean ever_diff = false;
        long len = 1000000;
        long start;
        long elapsed;
        while (true) {
            start = System.currentTimeMillis();
            double blackhole = 0;
            for (int i = 0; i < len; i++) {
                int idx = i % 100;
                double res = testIt(xs[idx], ys[idx]);
                if (i >= 0 && i<100) {
                    //presumably interpreted
                    if (vs[idx] != res && (!Double.isNaN(res) || !Double.isNaN(vs[idx]))) {
                        System.out.println(idx + ":\tInterpreted:" + xs[idx] + "^" + ys[idx] + "=" + res);
                        System.out.println(idx + ":\tStrict pow : " + xs[idx] + "^" + ys[idx] + "=" + vs[idx] + "\n");
                    }
                }
                if (i >= 250000 && i<250100 && !printed_compiled) {
                    //presumably compiled at this time
                    if (vs[idx] != res && (!Double.isNaN(res) || !Double.isNaN(vs[idx]))) {
                        System.out.println(idx + ":\tcompiled   :" + xs[idx] + "^" + ys[idx] + "=" + res);
                        System.out.println(idx + ":\tStrict pow :" + xs[idx] + "^" + ys[idx] + "=" + vs[idx] + "\n");
                        ever_diff = true;
                    }
                }
            }
            elapsed = System.currentTimeMillis() - start;
            System.out.println(elapsed + " ms ");
            if (!printed_compiled && ever_diff) {
                printed_compiled = true;
                return;
            }

        }
    }
}

我使用OpenJDK 8u5-b31进行了此测试,结果如下:

10: Interpreted:0.1845936372497491^0.01608930867480518=0.9731817015518033
10: Strict pow : 0.1845936372497491^0.01608930867480518=0.9731817015518032

41: Interpreted:0.7281259501809544^0.9414406865385655=0.7417808233050295
41: Strict pow : 0.7281259501809544^0.9414406865385655=0.7417808233050294

49: Interpreted:0.0727813262968815^0.09866028976654662=0.7721942440239148
49: Strict pow : 0.0727813262968815^0.09866028976654662=0.7721942440239149

70: Interpreted:0.6574309575966407^0.759887845481148=0.7270872740201638
70: Strict pow : 0.6574309575966407^0.759887845481148=0.7270872740201637

82: Interpreted:0.08662340816125613^0.4216580281197062=0.3564883826345057
82: Strict pow : 0.08662340816125613^0.4216580281197062=0.3564883826345058

92: Interpreted:0.20224488115245098^0.7158182878844233=0.31851834311978916
92: Strict pow : 0.20224488115245098^0.7158182878844233=0.3185183431197892

10: compiled   :0.1845936372497491^0.01608930867480518=0.9731817015518033
10: Strict pow :0.1845936372497491^0.01608930867480518=0.9731817015518032

41: compiled   :0.7281259501809544^0.9414406865385655=0.7417808233050295
41: Strict pow :0.7281259501809544^0.9414406865385655=0.7417808233050294

49: compiled   :0.0727813262968815^0.09866028976654662=0.7721942440239148
49: Strict pow :0.0727813262968815^0.09866028976654662=0.7721942440239149

70: compiled   :0.6574309575966407^0.759887845481148=0.7270872740201638
70: Strict pow :0.6574309575966407^0.759887845481148=0.7270872740201637

82: compiled   :0.08662340816125613^0.4216580281197062=0.3564883826345057
82: Strict pow :0.08662340816125613^0.4216580281197062=0.3564883826345058

92: compiled   :0.20224488115245098^0.7158182878844233=0.31851834311978916
92: Strict pow :0.20224488115245098^0.7158182878844233=0.3185183431197892

290 ms 

请注意,Random用于生成x和y值,因此您的行驶里程会因跑步而异。但是好消息是,至少编译版本的Math.pow结果与的解释版本的结果相匹配Math.pow。(离题:即使是这种一致性,也只是在2012年通过OpenJDK方面的一系列错误修复才得以实施。)

原因?

好吧,这是因为OpenJDK使用内部函数和运行时函数来实现Math.pow(以及其他数学函数),而不仅仅是执行Java代码。主要目的是利用x87指令,以便提高计算性能。结果,StrictMath.pow永远不会Math.pow在运行时调用它(确切地说,对于我们刚刚使用的OpenJDK版本)。

根据Math类的Javadoc (也在上面的@coobird引用),这种安排是完全合法的:

Math类包含用于执行基本数字运算的方法,例如基本指数,对数,平方根和三角函数。

与StrictMath类的某些数字方法不同,Math类的等效函数的所有实现未定义为返回逐位相同的结果。这种放松允许在不需要严格的可重复性的情况下实现性能更好的实现。

默认情况下,许多Math方法只是简单地调用StrictMath中的等效方法来实现它们。鼓励代码生成器使用特定于平台的本机库或微处理器指令(如果有)来提供Math方法的高性能实现。这种更高性能的实现仍必须符合Math规范。

结论呢?好吧,对于具有动态代码生成的语言(例如Java),请确保从“静态”代码中看到的内容与运行时执行的内容匹配。您的眼睛有时有时会误导您。


18

您检查源代码了吗?中的许多方法java.lang.Math都委托给java.lang.StrictMath

例:

public static double cos(double a) {
    return StrictMath.cos(a); // default impl. delegates to StrictMath
}

19
+1用于读取Java源代码。这是Java在.NET上的强项:Java API的大部分源代码随JDK一起提供在名为src.zip的文件中。JVM是开源的,现在可以下载其中没有的内容。阅读Java源代码可能不是解决问题的最宣传方法:它似乎是一个坏主意,因为通常应该“遵守公共接口而不是实现”。但是,阅读源代码有一个强大的优势:它将始终为您提供真相。有时那是最有价值的事情。
Mike Clark,2010年

1
@Andrew感谢您的提示。我刚读完有关如何在Visual Studio中进行设置的教程。Java可能仍然具有一点优势,因为您可以下载VM本身的源代码,而不仅仅是标准库(框架)。总之感谢!
Mike Clark

12
不幸的是,在这种情况下,源代码并不能说明全部事实。JVM可以免费使用特定于平台的内在函数替换Math中的方法。内部函数的行为方式不一定与StrictMath方法相同,但其行为受到Math类文档的约束。
ntoskrnl 2014年

3
关键词有被“默认” -在很多平台上Math不会真正使用StrictMath
dimo414'6

1
尽管这是一个有趣的观察,但这并不能回答问题。
卡尔·G

0

引用java.lang.Math

浮点Math方法的精度以单位为 ulps的单位表示。

...

如果某个方法的错误始终小于0.5 ulps,则该方法始终返回最接近精确结果的浮点数;这样的方法是正确的。通常,正确舍入的方法最好是浮点近似值。但是,要正确舍入许多浮点方法是不切实际的。

然后我们在Math.pow(..)下看到例如:

计算结果必须在准确结果的1 ulp以内。

现在,溃疡是什么?如预期的那样,java.lang.Math.ulp(1.0)给出2.220446049250313e-16,即2 -52。(也Math.ulp(8)给出了相同的值,Math.ulp(10)并且Math.ulp(15),而不是Math.ulp(16)。)换句话说,我们正在谈论的尾数的最后一位。

因此,在java.lang.Math.pow(..)尾数的52位中,最后一个返回的结果可能是错误的,正如我们可以在Tony Guan的答案中确认的那样。

找出一些具体的1 ulp和0.5 ulp代码进行比较会很好。我推测由于正确的原因,如果我们知道两个数字A和B均四舍五入为52个有效数字,并且我们希望知道A×B正确为52个有效数字,则要使最后一位正确,还需要做大量的额外工作。 ,通过正确的舍入,实际上我们需要知道A和B的一些额外位,以正确获得A×B的最后一位。但这意味着我们不应该通过将中间结果A和B强制为double来取整,实际上,我们需要一个更大的中间结果类型。(据我所见,大多数数学函数的实现在很大程度上都依赖于具有硬编码预计算系数的乘法,因此,如果它们需要大于两倍的宽度,则会对效率产生很大的影响。)

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.