Очевидно, що java.lang.StrictMathмістить додаткові функції (гіперболіки тощо), яких java.lang.Mathнемає, але чи є різниця у функціях, які є в обох бібліотеках?
Очевидно, що java.lang.StrictMathмістить додаткові функції (гіперболіки тощо), яких java.lang.Mathнемає, але чи є різниця у функціях, які є в обох бібліотеках?
Відповіді:
Javadoc для Mathкласу надає деяку інформацію про відмінності між двома класами:
На відміну від деяких числових методів класу
StrictMath, всі реалізації еквівалентних функцій класуMathне визначені для повернення бітових для бітових однакових результатів. Це розслаблення дозволяє ефективніші реалізації, коли не потрібна сувора відтворюваність.За замовчуванням багато
Mathметодів просто викликають еквівалентний методStrictMathдля їх реалізації. Генераторам коду рекомендується використовувати специфічні для платформи рідні бібліотеки або мікропроцесорні інструкції, якщо такі є, для забезпечення більш ефективних реалізаційMathметодів. Такі більш ефективні реалізації все ще повинні відповідати специфікації дляMath.
Таким чином, Mathклас викладені деякі правила про те, що деякі операції повинні робити, але вони не вимагають, щоб точний ж результати будуть повернуті у всіх реалізаціях бібліотеки.
Це дозволяє конкретним реалізаціям бібліотек повертати подібне, але не той самий результат, якщо, наприклад, Math.cosвикликається клас. Це дозволило б реалізувати конкретні платформи (наприклад, використовувати плаваючу крапку x86 і, скажімо, плаваючу крапку SPARC), які можуть повертати різні результати.
(Зверніться до розділу « Впровадження програмного забезпечення» статті Sine у Вікіпедії, щоб ознайомитися з деякими прикладами реалізацій, що стосуються певної платформи.)
Однак, з StrictMath, результати, повернуті різними реалізаціями, повинні повертати однакові результати. Це було б бажано для випадків, коли потрібна відтворюваність результатів на різних платформах.
Mathкласу - скористатися власними методами, доступними для певної платформи, що (у багатьох випадках, швидше за все, буде) швидше, ніж використання програмного рішення, яке гарантовано дати абсолютно однакову відповідь на всіх платформах.
Mathспецифікація класу дозволяє використовувати специфічні для платформи алгоритми, які не обов'язково повертатимуть той самий результат, що й інші платформи.
@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, переконайтесь, що те, що ви бачите зі "статичного" коду, відповідає тому, що виконується під час виконання. Очі іноді можуть насправді ввести вас в оману.
Ви перевіряли вихідний код? Багато методів в java.lang.Mathделеговано java.lang.StrictMath.
Приклад:
public static double cos(double a) {
return StrictMath.cos(a); // default impl. delegates to StrictMath
}
Mathбуде НЕ реально використовувати StrictMath.
Цитування 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 правильно. Але це означає, що ми не повинні округляти проміжні результати А і В, змушуючи їх подвоїтись , нам, фактично, потрібен ширший тип для проміжних результатів. (У тому, що я бачив, більшість реалізацій математичних функцій значною мірою покладаються на множення з жорстко закодованими заздалегідь обчисленими коефіцієнтами, тому, якщо вони повинні бути ширшими, ніж подвоєні, є великий удар ефективності.)