Я пытаюсь сгенерировать случайное число от -10 до 10 с шагом 0,3 (хотя я хочу, чтобы это были произвольные значения), и у меня возникают проблемы с точностью двойной точности с плавающей запятой. Значение DBL_DIG в Float.h предназначено для обеспечения минимальной точности, при которой не возникает ошибка округления [РЕДАКТИРОВАТЬ: это неверно, см. Комментарий Эрика Постпишила для истинного определения DBL_DIG], но при печати для такого количества цифр, я все еще вижу ошибку округления.

#include <stdio.h>
#include <float.h>
#include <stdlib.h>

int main()
{
  for (;;)
  {
    printf("%.*g\n", DBL_DIG, -10 + (rand() % (unsigned long)(20 / 0.3)) * 0.3);
  }
}

Когда я запускаю это, я получаю такой результат:

8.3
-7
1.7
-6.1
-3.1
1.1
-3.4
-8.2
-9.1
-9.7
-7.6
-7.9
1.4
-2.5
-1.3
-8.8
2.6
6.2
3.8
-3.4
9.5
-7.6
-1.9
-0.0999999999999996
-2.2
5
3.2
2.9
-2.5
2.9
9.5
-4.6
6.2
0.799999999999999
-1.3
-7.3
-7.9

Конечно, простым решением было бы просто #define DBL_DIG 14, но я чувствую, что это приводит к потере точности. Почему это происходит и как это предотвратить? Это не дубликат Не работает ли математика с плавающей запятой?, поскольку я спрашиваю о { {X1}}, и как найти минимальную точность, при которой ошибки не возникает.

1
StavromulaBeta 16 Янв 2021 в 13:59

2 ответа

Лучший ответ

генерировать случайное число от -10 до 10 с шагом 0,3
Я бы хотел, чтобы программа работала с произвольными значениями границ и размера шага.
Почему это происходит ....

Источник проблем заключается в предположении, что типичные действительные числа (такие как строка "0,3") могут кодироваться точно как double.

double может точно кодировать около 2 64 различных значений. 0,3 не одно из них.

Вместо этого используется ближайший double. точное значение и 2 ближайших приведены ниже:

0.29999999999999993338661852249060757458209991455078125
0.299999999999999988897769753748434595763683319091796875  (best 0.3)
0.3000000000000000444089209850062616169452667236328125

Таким образом, код OP пытается «-10 и 10 с шагом 0,2999 ...» и распечатывает "-0.0999999999999996" и "0.799999999999999" более правильнее, чем "-0.1" и "0.8".


.... как мне предотвратить это?

  1. Печатайте с более ограниченной точностью.

    // reduce the _bit_ output precision by about the root of steps
    #define LOG10_2 0.30102999566398119521373889472449
    int digits_less = lround(sqrt(20 / 0.3) * LOG10_2);
    for (int i = 0; i < 100; i++) {
      printf("%.*e\n", DBL_DIG - digits_less,
          -10 + (rand() % (unsigned long) (20 / 0.3)) * 0.3);
    }
    
    9.5000000000000e+00
    -3.7000000000000e+00
    8.6000000000000e+00
    5.9000000000000e+00
    ...
    -1.0000000000000e-01
    8.0000000000000e-01
    

Код OP на самом деле не выполняет «шаги», поскольку это намекает на цикл с шагом 0,3. Вышеупомянутый digits_less основан на повторяющихся "шагах", в противном случае уравнение OP требует уменьшения примерно на 1 десятичную цифру. Наилучшее снижение точности зависит от оценки потенциальной совокупной ошибки всех вычислений из "0.3" преобразования -> double 0.3 (1/2 бит), деления (1/2 бит), умножения (1/2 бит) и сложение (более сложный бит).

  1. Подождите, пока выйдет следующая версия C, которая может поддерживать десятичные числа с плавающей запятой.
1
chux - Reinstate Monica 16 Янв 2021 в 18:48

Для конкретного кода в вопросе мы можем избежать лишних ошибок округления, используя целые числа до последнего момента:

printf("%.*g\n", DBL_DIG,
    (-100 + rand() % (unsigned long)(20 / 0.3) * 3.) / 10.);

1

Поскольку целочисленные значения будут вычисляться точно, при последнем делении на 10 будет только одна ошибка округления, и результатом будет double, наиболее близкое к желаемому значению.

Сколько цифр мне следует напечатать, чтобы избежать ошибки округления в большинстве случаев? (не только в моем примере выше)

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

Сноска

1 Рассмотрение (unsigned long)(20 / 0.3) - это более длинное обсуждение, включающее намерение и обобщение на другие значения и случаи.

2
Eric Postpischil 16 Янв 2021 в 12:09