Java-реализация Math.exp()

Math.exp() делегирует вычисления StrictMath, которые, в свою очередь, делегируют их библиотеке fdlibm в C. По крайней мере, так это выглядит из того, что упоминается в https://docs.oracle.com/javase/8 /docs/api/java/lang/StrictMath.html.

Однако эта реализация C (https://www.netlib.org/fdlibm/e_exp.c), похоже, дает другие результаты по сравнению с тем, что возвращает Java: я оцениваю exp(-0.271153846153846134203746487401076592504978179931640625).

Реализация C дает: 0.7624991798148184063421695100259967148303985595703125-> 0x1.86664ae1101cap-1

в то время как Java дает: 0.76249917981481829531986704751034267246723175048828125-> 0x1.86664ae1101c9p-1 Код Java просто:

import java.math.BigDecimal;
double expon2 = Math.exp(-0.271153846153846134203746487401076592504978179931640625);
System.out.println(new BigDecimal(expon2).toPlainString());

Код C: https://www.netlib.org/cgi-bin/netlibfiles.pl?filename=/fdlibm/e_exp.c.

Как это возможно? Вместо этого я ожидал идеального совпадения.

1. ваш результат C неверен; вы, должно быть, делаете что-то не так... или ваша реализация сломана

pmg 28.08.2024 17:22

Можете ли вы показать сегменты, один на C, другой на Java, демонстрирующие это? Другими словами, Минимально воспроизводимый пример

Old Dog Programmer 28.08.2024 17:25

@pmg, ты мог бы попробовать и посмотреть, что получится. Требуются только два исходных файла, их можно скачать с netlib.org/cgi-bin/netlibfiles.pl?filename=/fdlibm/e_exp.c

Albert 28.08.2024 17:25

В стандартной библиотеке уже есть рабочая версия exp(). Единственное, что нужно, это добавить -lm к вызову gcc :)

pmg 28.08.2024 17:27

@pmg, можешь поделиться строкой? использование -lm вызывает библиотеку fdlibm? в противном случае у gcc есть другая реализация exp(): codebrowser.dev/glibc/glibc/sysdeps/ieee754/dbl-64/e_exp.c.h‌​tml

Albert 28.08.2024 17:31

«Требуются только два исходных файла, и их можно скачать с…» Мы ожидаем, что соответствующий код будет в теле вопроса. Чтобы лучше понять, как пользоваться этим сайтом, возможно, вам поможет Пройдите экскурсию , прочитайте Как задать хороший вопрос и страницы, на которые они ссылаются.

Old Dog Programmer 28.08.2024 17:32

Результат Java правильный, поэтому ошибка связана с кодом C. Вместо этого вам придется показать, как вы это проверяете.

Guy Incognito 28.08.2024 17:37
godbolt.org/z/c19Pbze3x <== сам компилятор использует MPFR для константы времени компиляции, а не для расчета во время выполнения
pmg 28.08.2024 17:50

Обратите внимание, что в обоих примерах кода аргумент выражается гораздо более значащими десятичными цифрами, чем это имеет смысл для двойного значения IEEE754 (около 15-16).

John Bollinger 28.08.2024 17:55

Также обратите внимание, что пример, который вы представляете как C, на самом деле представляет собой C++, который является другим языком. Это могло бы повлиять на результат, хотя, по правде говоря, я склонен предполагать, что нет.

John Bollinger 28.08.2024 17:58

код, весь на C, сейчас указан в вопросе. Дайте мне знать, если вы этого не видите. @OldDogProgrammer

Albert 28.08.2024 18:01

показал @GuyIncognito

Albert 28.08.2024 18:02

Код C, который вы показываете в ссылке godbolt, вызывает exp и возвращает то же значение, что и Java. Однако я скачал e_exp.c и fdlibm.h и вызвал __ieee754_exp(1.0), и он вернул 2.0, что явно неверно (должно быть e, 2.71828...). Вам следует изменить свой вопрос, чтобы он касался только библиотеки fdlibm, почему она возвращает, казалось бы, неправильные результаты.

k314159 28.08.2024 18:14

@pmg, как мне сохранить и отправить ссылку, как ты? Я повторно использовал предоставленную вами ссылку и вставил нужный мне код. Это тот, о котором идет речь, но, видимо, я единственный, кто видит код, который у меня там есть, но другие все равно видят то, что вы написали.

Albert 28.08.2024 18:18

@JohnBollinger: Re «Обратите внимание, что в обоих примерах кода аргумент выражается гораздо более значащими десятичными цифрами, чем это имеет смысл для двойного значения IEEE754 (около 15-16)»: это неверно. Они выражаются большим количеством цифр, чем могло бы измениться значение при преобразовании десятичной дроби обратно в двоичный64 IEEE-754. Однако они имеют смысл; представленные цифры OP показывают точные значения, представленные в типеbinary64, а это означает, что если вы преобразуете эти значения в десятичные с достаточным количеством цифр и правильным округлением, вы получите эти цифры…

Eric Postpischil 28.08.2024 18:21

Вам нужно нажать кнопку «Поделиться», чтобы интерфейс создал новую ссылку с вашими изменениями.

pmg 28.08.2024 18:21

… Это важно при работе с тщательно продуманным кодом с плавающей запятой.

Eric Postpischil 28.08.2024 18:21

@pmg спасибо, сделал и изменил вопрос

Albert 28.08.2024 18:24

«Требуются только два исходных файла, и их можно скачать» — опять же, как уже отмечалось, внешние ссылки бесполезны — их можно изменить, отредактировать, удалить… делая вопрос недействительным!

user85421 28.08.2024 18:27

Этот связанный файл e_exp.c выглядит странно. В начале он содержит #include "fdlibm.h", но кажется, что в него встроена копия fdlibm.h, и это вызывает проблемы с компиляцией. Знаешь почему?

Eric Postpischil 28.08.2024 18:29
fdlibm.h определяет макрос __P, который мешает макросу с таким же именем, определенному <stdio.h> в моей реализации на C (Apple Xcode 15.4, macOS 14.6.1).
Eric Postpischil 28.08.2024 18:35

@EricPostpischil в конечном итоге fdlibm.h нужен только для определения двух макросов __HI и __LO. Так что можете удалить все остальное.

Albert 28.08.2024 18:38

@Альберт: Вопрос не в этом. Почему файл представлен таким странным образом? Цель не в том, чтобы получить какой-то фрагмент кода, который компилируется и диагностировать, почему он не работает. Цель состоит в том, чтобы получить реальный код fdlibm и посмотреть, работает ли он, а если нет, то почему. Какой-то странный исходный файл с включенным и встроенным fdlibm.h не похож на исходный код; это похоже на какой-то артефакт представления в сети или другой недоразумение, и его следует устранить, прежде чем беспокоиться о том, что на самом деле находится в коде.

Eric Postpischil 28.08.2024 18:42

Связано: Округление при ULP 1 — Java и C++

Steve Summit 28.08.2024 18:44

@EricPostpischil, откуда у вас сложилось впечатление, что netlib.org/fdlibm/e_exp.c встраивает fdlibm.h? Я не вижу никаких доказательств этого.

k314159 28.08.2024 18:45

@ k314159: Дальнейшее тестирование показывает, что это артефакт веб-интерфейса. Когда я нажимаю на ссылку ОП, выбираю «Обычный текст» и нажимаю «Начать загрузку», в результате получается веб-страница, содержащая как файл e_exp.c, так и файл fdlibm.h в виде одной страницы. Если вместо этого я нажму «Zip», скачаю и разархивирую его, я получу два отдельных файла.

Eric Postpischil 28.08.2024 18:51

Использование конструктора BigDecimal может привести к изменению значения. Смотрите этот ответ: stackoverflow.com/a/7186298/721855. Вместо этого вы можете использовать BigDecimal.valueOf().

aled 28.08.2024 19:00

@user85421 user85421 можем ли мы снова открыть его сейчас?

Albert 28.08.2024 19:28

@Альберт - Почему ты меня спрашиваешь??? Я не голосовал за закрытие

user85421 28.08.2024 19:40

Что нам теперь делать? Java дает один результат. Некоторые реализации C с данным кодом e_exp.c дают другой результат. Как объяснить разницу? Вы не показали код, который выдает результат для Java. Если бы у вас был этот код, аналогичный коду e_exp.c, вы могли бы изучить каждый отдельный шаг вычислений и увидеть, где они расходятся. Без этого кода или если он не аналог, что тут говорить? У нас есть два приближения к e^x, и они дают разные результаты, потому что это разные приближения.

Eric Postpischil 28.08.2024 19:54

Представленный вами код e_exp.c идентичен коду exp для fdlibm 5.3? Использует ли ваш Java-код код, который должен выдавать результат fdlibm 5.3 (т. е. не использует нестрогие математические вычисления или использует какие-либо математические вычисления, отличные от IEEE-754binary64 — я не совсем знаком с Java и не знаю, повлияет ли импорт BigDecimal на математические упражнения). Как бы то ни было, Maple утверждает, что результат Java, 0,76249917981481829531986704751034267246723175048828125, немного ближе к математическому e^x, чем результат C.

Eric Postpischil 28.08.2024 19:56

@aled «Использование конструктора BigDecimal может привести к изменению значения». - на самом деле все наоборот. Конструктор BigDecimal.valueOf использует представление Double.toString(), тогда как конструктор показывает точное двоичное представление (преобразованное в десятичное). BigDecimal.valueOf(0.1) -> 0.1, new BigDecimal(0.1) ->0.100000000000000005551... Другими словами, 0,1 на самом деле не является 0,1, и конструктор BigDecimal сообщает вам, что это такое на самом деле.

k314159 29.08.2024 10:41

@Альберт, почему ты не редактировал код? Здесь это необходимо. Как уже говорили вам другие, мы не будем уходить за пределы сайта или загружать сюда файлы из внешних источников. Вопросы должны быть самостоятельными. (Я не эксперт в предметной области, иначе я бы прочитал код и решил, следует ли его снова редактировать. Все, что я могу сделать сейчас, это проголосовать за повторное закрытие этого вопроса, поскольку он не завершен.)

Chris 30.08.2024 14:16

Насколько я понимаю, вопросы не следует редактировать таким образом, чтобы ответы становились недействительными. Один из ответов звучит так: * «Вопрос был обновлен после того, как было указано, что мы использовали неправильный порядок байтов для fdlibm. Теперь возникает вопрос: почему Java дает ответ, отличающийся от C всего на одну ошибку». Мне кажется, это сделало рассматриваемое редактирование.

Old Dog Programmer 30.08.2024 16:28

@OldDogProgrammer, у этого вопроса 15 редакций. Мне неясно, о какой редакции вы говорите или какая редакция, по вашему мнению, лучше всего отражает вопрос и ответы. Но в его нынешнем виде вопрос, конечно, не ясен, если мы не посетим сторонний сайт или не загрузим полные исходные файлы. Это изменение произошло 21 час назад, а окончательное повторное голосование было подано только 9 часов назад, что означает, что эта версия без соответствующего кода C была актуальной. Его не следовало открывать повторно без редактирования, и у меня нет опыта в предметной области, чтобы сделать правильный откат или редактирование.

Chris 30.08.2024 16:44

-1 и голосование за закрытие. ОП был проинформирован, что код должен быть в самом вопросе, но все же заменил код ссылкой на какой-то неясный загрузчик файлов tarball/zip.

Andrew Henle 30.08.2024 17:23

@ Крис, я тоже не знаю, какое это было редактирование. Но в исходном вопросе было: «Использование реализации C функции, которую она дает: 0,728846153846153921307404743856750428676605224609375, а Java дает: 0,7624991798148182953198670475103426724672317». 5048828125» На этот вопрос был дан ответ. Затем оператор изменил вопрос, так что эти два числа различаются только последней цифрой. На мой взгляд, это должен был быть другой вопрос.

Old Dog Programmer 31.08.2024 16:04
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
0
37
250
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Ответ принят как подходящий

Вы неправильно используете библиотеку fdlibm. __ieee754_exp — это то, что называют «элементарной функцией», другими словами, это внутренняя функция, которая используется настоящей функцией exp. Кроме того, библиотека является мультиплатформенной, выполняет множество операций с битами и очень чувствительна к порядку байтов ЦП, поэтому ее необходимо настроить с помощью соответствующих макросов.

Чтобы правильно использовать библиотеку, вам необходимо собрать ее согласно инструкциям в readme, а затем включить построенную библиотеку в свое приложение. Не звоните __ieee754_exp напрямую.

Обновлять

Вопрос был обновлен после того, как было указано, что мы использовали неправильный порядок байтов для fdlibm. Теперь возникает вопрос: почему Java дает ответ, отличающийся от C всего на один ulp.

Ответ: библиотека C fdlibm и Java-класс StrictMath на самом деле дают один и тот же ответ (что неудивительно, поскольку StrictMath использует ту же реализацию, что и fdlibm):

jshell> Double.toHexString(StrictMath.exp(-0.271153846153846134203746487401076592504978179931640625))
$6 ==> "0x1.86664ae1101cap-1"

С другой стороны, класс Math в Java использует внутреннюю реализацию и дает тот же ответ, что и библиотека gcc в C:

jshell> Double.toHexString(Math.exp(-0.271153846153846134203746487401076592504978179931640625))
$7 ==> "0x1.86664ae1101c9p-1"

В С:

printf("%a", exp(-0.271153846153846134203746487401076592504978179931640625));
Output:
0x1.86664ae1101c9p-1

Внутренняя реализация обычно запрограммирована в процессоре и дает совсем немного другое приближение, поскольку самый последний бит отличается на 1.

Итак, позвольте мне посмотреть, понял ли я. Источником разницы является ключевое слово @IntrinsicCandidate, добавленное в этот метод из-за того, что (как уже упоминалось stackoverflow.com/questions/66842504/…). «Метод является встроенным, если виртуальная машина HotSpot заменяет аннотированный метод рукописной сборкой и/или рукописным компилятором IR — встроенным компилятором — для повышения производительности». А еще там написано: «Если JVM HotSpot знает, как ее интрицировать, тело метода Java игнорируется». На практике..

Albert 29.08.2024 11:14

Java не вызывает StrictMath.exp (который является телом Math.exp() ), а использует версию ассемблерного кода для оптимизации производительности. Таким образом, не существует никакого реального кода, который можно было бы перевести на C, чтобы получить одинаковые числа между Math.exp() и его версией C. Правильный?

Albert 29.08.2024 11:16

«Таким образом, не существует какого-либо реального кода, который можно было бы перевести на C, чтобы получить одинаковые числа между Math.exp() и его версией C». - может быть и правильно, но не обязательно. Библиотека gcc в C дает тот же ответ, что и встроенная версия Math.exp в Java, но, вероятно, это потому, что библиотека gcc также использует встроенную версию exp. Однако это не означает, что его нельзя написать полностью на C. Вероятно, можно. Разница в результате — это всего лишь разница в используемом методе округления. Внутренний метод округляет последний бит в меньшую сторону, а fdlibm округляет его в большую сторону.

k314159 29.08.2024 11:39

в этом случае библиотека gcc дает тот же результат, но не в других случаях: попробуйте exp(-3.076923076923077093880465326947160065174102783203125e-‌​02)

Albert 29.08.2024 12:34

@Альберт, результаты onecompiler.com/c/42qkutyjx и onecompiler.com/java/42qkuvakr кажутся мне совершенно одинаковыми.

k314159 29.08.2024 13:15

Я предполагаю, что onecompiler.com имитирует сборку x86. Если вы измените ее на x64, вы получите другие результаты. Это еще одна загадка, которую мне хотелось бы понять.

Albert 29.08.2024 14:34

Примечание. я использую компилятор MSVC.

Albert 29.08.2024 15:05

Все работает на x64, но разница заключается в компиляторах. MSVC возвращает 0x1.f07c6e07475dcp-1, тогда как gcc и Java Math.exp и StrictMath.exp возвращают 0x1.f07c6e07475dbp-1. Не знаю, чем отличается MSVC, но это всего лишь округление последнего бита.

k314159 29.08.2024 16:00

Результаты в MSVC меняются в зависимости от x64 или x86.

Albert 29.08.2024 17:13

Тест #ifdef __LITTLE_ENDIAN не работает в вашем компиляторе, а вместо этого в коде используются определения с прямым порядком байтов, что приводит к неправильной обработке данных.

Изменение #ifdef __LITTLE_ENDIAN на #ifdef __LITTLE_ENDIAN__ или #if 1, скорее всего, приведет к тому, что код будет работать с вашим компилятором.

Тестирование с помощью Apple Clang 15.0.0 на macOS 14.6.1 показывает до изменения тот же неверный результат, о котором вы сообщаете, а после изменения — правильный результат.

Очевидно, этот код не был написан для использования символов препроцессора, определенных GCC и Clang, для обозначения порядка байтов.

В моем случае (Linux amd64) я удалил условия вокруг #define __LITTLE_ENDIAN в fdlibm.h, так что теперь он всегда определен и теперь __ieee754_exp работает правильно. Это должен быть принятый ответ.

k314159 28.08.2024 18:54

@ k314159 и Эрик, обратите внимание, что результат не идентичен тому, который получил Java. Я изменил вопрос, указав значение, которое я получаю, используя код C (изменено благодаря совету Эрика).

Albert 28.08.2024 19:22

Я не могу точно сказать, почему эти две реализации дают результат в этом случае немного другие результаты. Но я могу сказать вам, почему это неудивительно — это в принципе ожидаемо — что двое реализации могут давать немного разные результаты в случаях так.

Для некоторых «базовых» операций — каноническими примерами являются сложение, вычитание, умножение и деление — IEEE-754 гарантирует, что вы всегда получите правильно округленное значение. результат. Если истинный математический результат не совсем представимым, если истинный результат лежит где-то между двумя различные представимые числа IEEE-754, вы гарантированно возьми тот, что поближе.

Но для других операций — и особенно для трансцендентальных такие функции, как sin, cos, exp и log — такой гарантии нет. Причина в том, что это чрезвычайно сложно и вообще нерешенная, проблема состоит в том, чтобы решить, насколько усердно вам нужно работать, чтобы создать правильно округленные результаты для этих функций во всех случаях. У Эрика Постпищила есть очень хороший ответ на обсуждение этой проблемы (возможно, не случайно связанный с вашим другим вопросом по этой теме). Если вы поработаете достаточно усердно, вы сможете сгенерировать столько точных цифр, сколько захотите, но если вам нужен максимально эффективный алгоритм, который использует ровно столько дополнительной точности, чтобы генерировать правильно округленный результат (с некоторой заданной конечной точностью) для каждого возможного ввода. , это очень сложно.

Для вашей задачи exp(-0.271153846153846…) математически правильный результат (неудивительно) не точно представимо в двойной точности IEEE-754. математический результат находится между двумя представимыми числами — и, как оказалось, эти два «перемежающихся» представимых числа — это именно те два числа, которые вам дали две разные реализации.

Вот подробности. Я вычислил «точные математические результат» с использованием двух разных программ произвольной точности. Я выделил жирным шрифтом, чтобы было легче увидеть отличающиеся цифры.

ценить объяснение 1 0.762499179814818295319867047510342672467231750488281250x1.86664ae1101c9p−1 2 0.76249917981481829531986704751034267246723175048828125 имп. №1 3 0.76249917981481834237… точный результат (BC) 4 0.7624991798148183423701219458277605732565114320577561612719… точный результат (mpcalc) 5 0.7624991798148184063421695100259967148303985595703125 имп. #2 6 0.76249917981481840634216951002599671483039855957031250x1.86664ae1101cap−1

Чтобы внести ясность:

  • Строка 1 представляет собой точно представимое значение IEEE-754. (Его шестнадцатеричное представление — 3fe86664ae1101c9.)
  • Строка 2 — это результат, который вы получили от класса Math Java, который (согласно @k314159) «использует внутреннюю реализацию и дает тот же ответ, что и библиотека gcc в C». [сноска 1]
  • Строка 3 — это результат, вычисленный с помощью программы Unix bc.
  • В строке 4 показан результат, вычисленный с помощью моего собственного многоточного калькулятора.
  • Строка 5 — это результат, полученный вами из класса StrictMath Java, а также из библиотеки fdlibm.
  • Строка 6 — это следующее точно представимое значение IEEE-754 после строки 1. (Ее шестнадцатеричное представление — 3fe86664ae1101ca.)

Как я уже говорил ранее, точный результат находится между двумя представимыми числами. Эти два представимых числа настолько близки, насколько это возможно в двойной точности IEEE-754.

Итак, какое из двух представимых значений ближе? Давайте посмотрим. Я добавил столбец «diff», показывающий разницу между десятичными значениями:

ценить диф. объяснение 0.76249917981481829531986704751034267246723175048828125 имп. № 1 («внутренний») 4.7e−17 0.76249917981481834237012194582776057325651143205775616… точный результат 6.4e−17 0.7624991798148184063421695100259967148303985595703125 имп. №2 (фдлибм)

Итак, в этом случае мы видим, что «внутренний» результат на самом деле значительно ближе к реальному значению. Оно (по крайней мере, для этой проблемы) «правильно округлено».

У меня сложилось впечатление, что fdlibm очень хорошо реализована, но, очевидно, она не всегда дает правильно округленные результаты для функции exp. (Этого не требует стандарт IEEE-754. Я не знаю, каковы опубликованные гарантии для этой функции.)


Сноска 1. «Библиотека gcc в C» не является четко определенной концепцией. Системы с gcc часто (хотя и не всегда) настраиваются для связывания с glibc, библиотекой GNU C. Я вполне готов поверить, что существуют версии glibc, которые дают результат №1, хотя та, которую я пробовал, а также библиотека Apple C, используемая с clang на моем Mac, обе дали результат №2, такой же, как fdlibm.

Говорить о «результате Java» и «результате C» сбивает с толку. На самом деле это «результат fdlibm» (который включает библиотеку Java StrictMath и библиотеку C fdlibm) и «внутренний результат» (который включает библиотеку Java Math и библиотеку C GNU libm).

k314159 30.08.2024 17:14

@ k314159 Формулировка изменена. (Увы, мой второй стол взорвался.)

Steve Summit 30.08.2024 18:55

Другие вопросы по теме