Яка різниця між java.lang.Math та java.lang.StrictMath?


74

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


6
На це питання повністю відповідає Javadoc.
user207421

34
@EJP - Я вважаю, що на SO RTFM ніколи не є гарною відповіддю.
ripper234

Відповіді:


72

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

На відміну від деяких числових методів класу StrictMath, всі реалізації еквівалентних функцій класу Mathне визначені для повернення бітових для бітових однакових результатів. Це розслаблення дозволяє ефективніші реалізації, коли не потрібна сувора відтворюваність.

За замовчуванням багато Mathметодів просто викликають еквівалентний метод StrictMathдля їх реалізації. Генераторам коду рекомендується використовувати специфічні для платформи рідні бібліотеки або мікропроцесорні інструкції, якщо такі є, для забезпечення більш ефективних реалізацій Mathметодів. Такі більш ефективні реалізації все ще повинні відповідати специфікації для Math.

Таким чином, Mathклас викладені деякі правила про те, що деякі операції повинні робити, але вони не вимагають, щоб точний ж результати будуть повернуті у всіх реалізаціях бібліотеки.

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

(Зверніться до розділу « Впровадження програмного забезпечення» статті Sine у Вікіпедії, щоб ознайомитися з деякими прикладами реалізацій, що стосуються певної платформи.)

Однак, з StrictMath, результати, повернуті різними реалізаціями, повинні повертати однакові результати. Це було б бажано для випадків, коли потрібна відтворюваність результатів на різних платформах.


3
Але чому різні реалізації, специфічні для платформи, хочуть дати різний результат? Хіба косинус не є загальновизнаним?
Айвар

@Aivar: З причин, перелічених у цитаті з Mathкласу - скористатися власними методами, доступними для певної платформи, що (у багатьох випадках, швидше за все, буде) швидше, ніж використання програмного рішення, яке гарантовано дати абсолютно однакову відповідь на всіх платформах.
coobird

добре, значить, це означає, що деякі платформи вирішили не обчислювати найбільш точну відповідь, яка відповідає заданій кількості бітів, але торгували точно для ефективності? А різні платформи робили різні компроміси?
Айвар

@Aivar Здавалося б, це випадок читання статті, пов’язаної з Вікіпедією. Простіше кажучи, Mathспецифікація класу дозволяє використовувати специфічні для платформи алгоритми, які не обов'язково повертатимуть той самий результат, що й інші платформи.
coobird

1
@Aivar справа не лише в точності та ефективності, але й у тому, що у багатьох випадках не обов'язково є одна очевидна "найбільш точна відповідь". Наприклад, у статті "Синус" coobird посилається на "немає стандартного алгоритму розрахунку синуса".
dimo414

22

@ntoskrnl Як хтось, хто працює з внутрішніми структурами JVM, я хотів би підтримати вашу думку, що "внутрішні властивості не обов'язково поводяться так само, як методи StrictMath". Щоб це з’ясувати (або довести), ми можемо просто написати простий тест.

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

 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, яку ми щойно використовували).

І це заперечення є цілком законним згідно з Javadoc Mathкласу (також цитований @coobird вище):

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

На відміну від деяких числових методів класу StrictMath, усі реалізації еквівалентних функцій класу Math не визначені для повернення бітових для бітових однакових результатів. Це розслаблення дозволяє ефективніші реалізації, коли не потрібна сувора відтворюваність.

За замовчуванням багато з математичних методів просто викликають еквівалентний метод у StrictMath для їх реалізації. Генераторам коду пропонується використовувати специфічні для платформи власні бібліотеки або інструкції мікропроцесора, якщо такі є, для забезпечення більш ефективних реалізацій математичних методів. Такі більш ефективні реалізації все ще повинні відповідати специфікації для математики.

І висновок? Що ж, для мов з динамічним генеруванням коду, таких як 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 може бути не найбільш рекламованим способом вирішення проблем: це може здатися поганою ідеєю, оскільки зазвичай вам слід "дотримуватися загальнодоступного інтерфейсу, а не реалізації". Однак читання джерела має одну сильну перевагу: воно завжди дасть вам правду. І іноді це найцінніше з усіх.
Майк Кларк,

1
@Andrew Дякую за підказку. Я щойно закінчив читати підручник про те, як це налаштувати у Visual Studio. Java все ще може мати невелику перевагу в тому, що ви можете завантажити вихідний код для самої віртуальної машини, а не лише її стандартну бібліотеку (фреймворк). У будь-якому разі, дякую!
Mike Clark

12
На жаль, у цьому випадку вихідний код не говорить усієї правди. JVM може безкоштовно замінити методи в Math на внутрішні особливості, що стосуються платформи. Intrinsics не обов'язково вести себе так само, як і методи StrictMath, але їх поведінка буде обмежена документацією в класі Math.
ntoskrnl

3
Ключове слово там бути « по умовчанням» - на багатьох платформах Mathбуде НЕ реально використовувати StrictMath.
dimo414

1
Хоча це цікаве спостереження, це не дає відповіді на питання.
Carl G

0

Цитування java.lang.Math :

Точність Mathметодів з плаваючою точкою вимірюється в показниках ульп , одиниць останнього місця.

...

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

І тоді ми бачимо під Math.pow (..) , наприклад:

Обчислений результат повинен бути в межах 1 ульп від точного результату.

Що ж таке ульп? Як очікувалося, 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 ульп і 0,5 ульп для порівняння. Я буду припускати, що для того, щоб останній біт був правильним, потрібно досить багато додаткової роботи з тієї самої причини, що якщо ми знаємо два числа A і B, округлені до 52 значущих цифр, і ми хочемо знати, як A × B правильний до 52 значущих цифр , з правильним округленням, то насправді нам потрібно знати кілька зайвих бітів A і B, щоб отримати останній біт A × B правильно. Але це означає, що ми не повинні округляти проміжні результати А і В, змушуючи їх подвоїтись , нам, фактично, потрібен ширший тип для проміжних результатів. (У тому, що я бачив, більшість реалізацій математичних функцій значною мірою покладаються на множення з жорстко закодованими заздалегідь обчисленими коефіцієнтами, тому, якщо вони повинні бути ширшими, ніж подвоєні, є великий удар ефективності.)

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.