Есть ли у Фортрана врожденные ограничения числовой точности по сравнению с другими языками?

Работая над простым упражнением по программированию, я создал цикл while (цикл DO в Фортране), который должен был завершиться, когда реальная переменная достигла точного значения.

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

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

Поскольку код, вызывающий проблему, настолько мал как в Perl, так и в Fortran, я хотел бы воспроизвести его здесь, на случай, если я упущу важную деталь:

Код Fortran

PROGRAM precision_test
IMPLICIT NONE

! Data Dictionary
INTEGER :: count = 0 ! Number of times the loop has iterated
REAL(KIND=8) :: velocity
REAL(KIND=8), PARAMETER :: MACH
#! /usr/bin/perl -w
use strict;

my $mach_2_meters_per_sec = 340.0;

my $velocity = 0.5 * $mach_2_meters_per_sec;

while (1) {
        printf "%20.8f\n", $velocity;   
        exit if ($velocity == 5.0 * $mach_2_meters_per_sec);
        $velocity = $velocity + 0.1 * $mach_2_meters_per_sec;
}
METERS_PER_SEC = 340.0 velocity = 0.5 * MACH
#! /usr/bin/perl -w
use strict;

my $mach_2_meters_per_sec = 340.0;

my $velocity = 0.5 * $mach_2_meters_per_sec;

while (1) {
        printf "%20.8f\n", $velocity;   
        exit if ($velocity == 5.0 * $mach_2_meters_per_sec);
        $velocity = $velocity + 0.1 * $mach_2_meters_per_sec;
}
METERS_PER_SEC ! Initial Velocity DO WRITE (*, 300) velocity 300 FORMAT (F20.8) IF (count == 50) EXIT IF (velocity == 5.0 * MACH
#! /usr/bin/perl -w
use strict;

my $mach_2_meters_per_sec = 340.0;

my $velocity = 0.5 * $mach_2_meters_per_sec;

while (1) {
        printf "%20.8f\n", $velocity;   
        exit if ($velocity == 5.0 * $mach_2_meters_per_sec);
        $velocity = $velocity + 0.1 * $mach_2_meters_per_sec;
}
METERS_PER_SEC) EXIT ! IF (abs(velocity - (5.0 * MACH
#! /usr/bin/perl -w
use strict;

my $mach_2_meters_per_sec = 340.0;

my $velocity = 0.5 * $mach_2_meters_per_sec;

while (1) {
        printf "%20.8f\n", $velocity;   
        exit if ($velocity == 5.0 * $mach_2_meters_per_sec);
        $velocity = $velocity + 0.1 * $mach_2_meters_per_sec;
}
METERS_PER_SEC)) < 1E-4) EXIT velocity = velocity + 0.1 * MACH
#! /usr/bin/perl -w
use strict;

my $mach_2_meters_per_sec = 340.0;

my $velocity = 0.5 * $mach_2_meters_per_sec;

while (1) {
        printf "%20.8f\n", $velocity;   
        exit if ($velocity == 5.0 * $mach_2_meters_per_sec);
        $velocity = $velocity + 0.1 * $mach_2_meters_per_sec;
}
METERS_PER_SEC count = count + 1 END DO END PROGRAM precision_test

Код Perl

#! /usr/bin/perl -w
use strict;

my $mach_2_meters_per_sec = 340.0;

my $velocity = 0.5 * $mach_2_meters_per_sec;

while (1) {
        printf "%20.8f\n", $velocity;   
        exit if ($velocity == 5.0 * $mach_2_meters_per_sec);
        $velocity = $velocity + 0.1 * $mach_2_meters_per_sec;
}

Закомментированная строка в Фортране - это то, что мне нужно использовать для нормального выхода цикла. Обратите внимание, что порог установлен на 1E-4, что, на мой взгляд, довольно жалко.

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

Цель состоит в том, чтобы цикл остановился, когда переменная скорости достигнет 1700.

Вот усеченные выходные данные:

Вывод Perl

    170.00000000
    204.00000000
    238.00000000
    272.00000000
    306.00000000
    340.00000000

...

   1564.00000000
   1598.00000000
   1632.00000000
   1666.00000000
   1700.00000000

Вывод в Fortran

    170.00000000
    204.00000051
    238.00000101
    272.00000152
    306.00000203
    340.00000253

...

   1564.00002077
   1598.00002128
   1632.00002179
   1666.00002229
   1700.00002280

Что хорошего в скорости и простоте распараллеливания Фортрана, если от его точности плохо? Напоминает мне о трех способах работы:

  1. Правильный путь

  2. Неправильный путь

  3. Путь максимальной мощности

"Разве это не неправильный путь?"

"Да! Но быстрее!"

Если не считать шуток, должно быть, я что-то делаю не так.

Есть ли у Фортрана неотъемлемые ограничения числовой точности по сравнению с другими языками, или я (весьма вероятно) виноват?

Мой компилятор - gfortran (gcc версии 4.1.2), Perl v5.12.1, на двухъядерном AMD Opteron @ 1 ГГц.


person EMiller    schedule 26.07.2010    source источник
comment
Разве №3 не должен быть способом максимальной мощности?   -  person detly    schedule 26.07.2010
comment
Упс. Ты прав. Спасибо!   -  person EMiller    schedule 26.07.2010
comment
Здесь Fortran использует двойник, и я подозреваю, что Perl поступает так же. Но проверка на равенство с числом с плавающей запятой вызывает проблемы (если у вас нет безопасного числа, такого как 0.0144). Поэтому я бы сказал, что это, вероятно, неправильный ваш метод тестирования.   -  person Wolph    schedule 26.07.2010
comment
Это очень интересная проблема. Я запустил ваш код на Фортране и получил те же результаты. Моим первым побуждением было заподозрить ошибку преобразования base-2 в FORTRAN, но если да, то почему Perl не восприимчив к ней? И FORTRAN, и Perl реализуют IEEE-754. Кроме того, число с двойной точностью должно поддаваться только ошибкам округления в 15-16 цифрах, но в вашей распечатке Fortran ошибка округления возникает намного раньше. Очень любопытный!   -  person Gilead    schedule 26.07.2010
comment
@Gilead: Да, я изначально думал то же самое относительно ошибки округления в 15-16 цифрах, и поэтому я предположил, что, в худшем случае, повторные умножения резко усугубляли ошибку к тому времени, когда я достиг ~ 1700. Только когда я распечатал всю последовательность значений, от начального значения до точки остановки, я увидел, что ошибка округления была гораздо большей по величине, чем E-15, даже после первого умножения. В любом случае спасибо за комментарии.   -  person EMiller    schedule 27.07.2010


Ответы (2)


Ваше присвоение случайно преобразует значение в одинарную точность, а затем обратно в двойную.

Попробуйте сделать свой 0.1 * равным 0.1D0 *, и вы увидите, что проблема решена.

person ysth    schedule 26.07.2010
comment
Фактически, он использовал НАСТОЯЩЕЕ (kind = 8), что является ДВОЙНОЙ ТОЧНОСТЬЮ. - person Gilead; 26.07.2010
comment
@Gilead: Да, я сначала пропустил это; мой опыт работы с Fortran не связан с GNU Fortran. Удален плохой ответ и поставлен правильный ответ. - person ysth; 26.07.2010
comment
Ах! Вот и ответ. Очень тонко! +1 за это. - person Gilead; 26.07.2010
comment
@ysth: Большое вам спасибо за это. Из-за очевидной фундаментальной природы этого вопроса я был бы по-королевски ввернут в программы любого разумного размера, если бы я не мог решить этот вопрос. - person EMiller; 27.07.2010
comment
В чем разница между Фортраном и Perl в том, что Perl по умолчанию дает литералам двойную точность, а Фортран по умолчанию дает литералам одинарную точность? - person EMiller; 27.07.2010
comment
Или, если хотите, допустим, что Perl имеет только один тип реального, а в Fortran есть несколько интересных правил, когда они смешиваются. - person ysth; 27.07.2010
comment
Всегда явно вводить константы в Фортране - действительно хорошая идея. - person ysth; 27.07.2010
comment
Правила Fortran для выражений со смешанными типами просты: менее точные типы переводятся в более высокий тип. Проблема здесь в том, что такие константы, как 5.0, являются действительными числами по умолчанию, вероятно, с одинарной точностью и преобразуются во внутреннее представление с использованием этой точности. Они продвигаются только тогда, когда они взаимодействуют с переменной с более высокой точностью. - person M. S. B.; 27.07.2010
comment
@M. С.Б .: Спасибо, я знал, что это не так глупо, как переход в низший тип, но не мог точно вспомнить, в чем была основная причина. - person ysth; 27.07.2010
comment
@ M.S.B .: Но тогда зачем мне было писать 0.1D0, а не 0.1? Когда я выполняю 0,1 * MACH_2_METERS_PER_SEC, где MACH_2_METERS_PER_SEC уже имеет двойную точность, не будет ли 0,1 уже повышен до удвоения? - person EMiller; 28.07.2010
comment
@CmdrGuard: да, но значение одинарной точности, ближайшее к .1, намного более неточно, чем значение двойной точности, ближайшее к .1. Например, 0,1 в следующем примере повышается до удвоения, но это не восстанавливает потерянную точность: WRITE (*, "(F20.18)") (0.1-0.1D0) - person ysth; 28.07.2010
comment
Порядок является ключевым: десятичная константа 0,1 преобразуется в двоичное представление с использованием одинарной точности. Преобразование в двоичный файл с одинарной точностью повлечет за собой больше проблем с базовым преобразованием, чем при преобразовании в двоичный файл с двойной точностью, что было бы сделано, если бы в моем примере константа была записана 0,1D0 или 0,1_DR_K. Затем, когда это двоичное значение одинарной точности используется в выражении с переменной двойной точности, оно преобразуется в двойную точность, и все вычисления выполняются с двойной точностью. Но базовая конверсия уже была сделана в одиночку! - person M. S. B.; 28.07.2010
comment
@ysth и @ M.S.B .: Аааа, да !! Большое спасибо, ребята. Это все проясняет. В десятичном формате 0,1 является точным, но в двоичном формате повторяется одна и та же константа, поэтому усечение одинарной точности приводит к большему отличию от его точного десятичного представления, чем двоичное число двойной точности. - person EMiller; 28.07.2010

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

Кроме того, использование kind = 8 не переносимо - оно даст вам двойную точность с gfortran, но не с некоторыми другими компиляторами. Безопасный переносимый способ указать точность как для переменных, так и для констант в Fortran> = 90 - это использовать встроенные функции и запросить необходимую точность. Затем укажите «виды» констант, где важна точность. Удобный метод - определить свои собственные символы. Например:

integer, parameter :: DR_K = selected_real_kind (14)

REAL(DR_K), PARAMETER :: MACH_2_METERS_PER_SEC = 340.0_DR_K

real (DR_K) :: mass, velocity, energy

energy = 0.5_DR_K * mass * velocity**2

Это также может быть важно для целых чисел, например, если требуются большие значения. По связанным вопросам для целых чисел см. Fortran: integer * 4 vs integer (4) vs integer ( kind = 4) и длинных int в Фортране

person M. S. B.    schedule 26.07.2010
comment
real(kind=8) - это совершенно стандартный Fortran 90. Все компиляторы, совместимые с Fortran 90, должны и поддерживают его. Тем не менее, правильный способ запросить заданную точность в Fortran 90 выглядит примерно так: integer, parameter :: dp = selected_real_kind(15, 307) real(kind=dp) :: a См. Fortran Wiki - person fB.; 26.07.2010
comment
@fB: @MSB правильно утверждает, что kind = 8 не является переносимым, и вы правы, утверждая, что это стандартный Fortran 90. Непереносимость оператора возникает из-за того, что, в то время как kind = 8 является допустимым селектором типа, интерпретация из 8 зависит от реализации. Конечно, большинство современных компиляторов будут (я считаю) интерпретировать это как 8-байтовое целое число, но это не гарантируется стандартом. Конечно, не всегда верно, что все компиляторы Fortran интерпретируют это одинаково, и одна из проблем среднего программиста Fortran - переносимость во времени. - person High Performance Mark; 26.07.2010
comment
Да, я понимаю, что использовал далеко не идеальный способ заявить о точности. Я сделал это больше для простоты, чем для чего-либо еще. Возможно, мне следует изменить его, чтобы мой примерный код был переносимым для читателей Stack Overflow, но поскольку этот вопрос может затронуть других новичков в Fortran, возможно, лучше оставить код в текущей форме. - person EMiller; 27.07.2010
comment
@ High Performance Mark - правильно. В стандартном комитете по этому поводу было некоторое обсуждение, и это общий вывод, что целые числа часто вводят в заблуждение, как показано. For (т.е. kind = 8) - 8 ничего особенного не означает; это просто число, описывающее реализованную точность. Это могло быть 32 или 112 - просто ярлык. Важная часть - это проверка того, поддерживает ли процессор желаемую точность (cpu + compiler), и использование его с комбинацией sel ... real_kind и объявления переменной этого типа. - person Rook; 14.08.2010