Какая разница между java.lang.Math и java.lang.StrictMath?

Очевидно, что java.lang.StrictMath содержит дополнительные функции (гиперболики и т.д.), которые java.lang.Math нет, но есть ли разница в функциях, которые находятся в обеих библиотеках?

Ответ 1

Javadoc для класса Math предоставляет некоторую информацию о различиях между двумя классами:

В отличие от некоторых числовых методов class StrictMath, все реализации эквивалентных функций класса Math не определены для возврата бит-бит для одинаковых результатов. Эта релаксация позволяет лучше выполнять реализации воспроизводимость не требуется.

По умолчанию многие из методов Mathпросто вызовите эквивалентный метод в StrictMath для их реализации. Генераторы кода рекомендуется использовать специфичные для платформы собственные библиотеки или инструкции микропроцессора, где доступный для предоставления более высокопроизводительные реализации Math. Такая более высокая производительность реализации должны соответствовать спецификация для Math.

Поэтому класс Math содержит некоторые правила о том, что должны делать определенные операции, но они не требуют возврата тех же результатов во всех реализациях библиотек.

Это позволяет конкретным реализациям библиотек возвращать подобный, но не тот же результат, если, например, вызывается класс Math.cos. Это позволило бы реализовать специфичные для платформы реализации (например, с плавающей точкой x86 и, скажем, с плавающей запятой SPARC), которые могут возвращать разные результаты.

(Обратитесь к разделу Software Implementations Sine в Википедии для некоторых примеров реализаций для платформы.)

Однако при StrictMath результаты, возвращаемые различными реализациями, должны возвращать тот же результат. Это было бы желательно для случаев, когда требуется воспроизводимость результатов на разных платформах.

Ответ 2

Вы проверили исходный код? Многие методы в java.lang.Math делегируются java.lang.StrictMath.

Пример:

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

Ответ 3

@ntoskrnl Как кто-то, кто работает с внутренними JVM, я хотел бы высказать ваше мнение, что "intrinsics не обязательно ведут себя так же, как методы StrictMath". Чтобы узнать (или доказать) это, мы можем просто написать простой тест.

Возьмите Math.pow, например, изучив Java-код для java.lang.Math.pow(double a, double b), мы увидим:

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

Но JVM может свободно реализовать его с помощью вызовов intrinsics или runtime, поэтому возвращаемый результат может отличаться от того, что мы ожидаем от 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. (Off Topic: даже эта согласованность была применена только в 2012 году с рядом исправлений ошибок со стороны OpenJDK.)

Причина?

Ну, это потому, что OpenJDK использует функции intrinsics и runtime для реализации Math.pow (и других математических функций) вместо того, чтобы просто выполнять код Java. Основная цель состоит в том, чтобы воспользоваться инструкциями x87, чтобы повысить производительность для вычислений. В результате StrictMath.pow никогда не вызывается из Math.pow во время выполнения (для версии OpenJDK, которую мы только что использовали, если быть точным).

И это арграция полностью законна в соответствии с классом Javadoc класса Math (также цитируется @coobird выше):

Класс Math содержит методы для выполнения основных числовых операций, таких как элементарный экспоненциальный, логарифмический, квадратный корень и тригонометрические функции.

В отличие от некоторых из числовых методов класса StrictMath, все реализации эквивалентных функций класса Math не определены для возврата бит-бит для одинаковых результатов. Эта релаксация позволяет выполнять более эффективные реализации, где не требуется строгая воспроизводимость.

По умолчанию многие из методов Math просто вызывают эквивалентный метод в StrictMath для их реализации. Генераторам кода рекомендуется использовать встроенные библиотеки для конкретных платформ или инструкции по микропроцессорам, где они доступны, для обеспечения высокопроизводительных реализаций математических методов. Такие высокопроизводительные реализации по-прежнему должны соответствовать спецификации для Math.

И заключение? Ну, для языков с динамическим генерированием кода, таких как Java, убедитесь, что то, что вы видите из статического кода, соответствует тому, что выполняется во время выполнения. Ваши глаза иногда могут ввести вас в заблуждение.

Ответ 4

Цитата java.lang.Math:

Точность методов Math с плавающей запятой измеряется в терминах ulps, единиц на последнем месте.

...

Если метод всегда имеет ошибку менее 0,5 ulps, метод всегда возвращает число с плавающей запятой, ближайшую к точному результату; такой метод правильно округлен. Правильно округленный метод, как правило, является лучшим приближением с плавающей запятой; однако для многих методов с плавающей запятой нецелесообразно правильно округлить.

И затем мы видим в Math.pow(..), например:

Вычисленный результат должен быть в пределах 1 ulp от точного результата.

Теперь, что такое 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 бит мантиссы, как мы можем подтвердить в ответе Тони Гуана.

Было бы неплохо выкопать конкретный код 1 ulp и 0.5 ulp для сравнения. Я предполагаю, что для получения этого последнего бита требуется побольше дополнительной работы по той же причине, что, если мы знаем, что два числа A и B округлены до 52 значительных цифр, и мы хотим знать, что A × B соответствует 52 значимым цифрам, с правильным округлением, то на самом деле нам нужно знать несколько дополнительных бит A и B, чтобы получить последний бит A × B справа. Но это означает, что мы не должны округлять промежуточные результаты A и B, заставляя их удваивать, мы нуждаемся в эффективном более широком типе для промежуточных результатов. (В том, что я видел, большинство реализаций математических функций в значительной степени зависят от умножений с жестко запрограммированными заранее вычисленными коэффициентами, поэтому, если они должны быть более широкими, чем в два раза, наблюдается большой успех).