Одна из серьезных проблем работы с десятичными числами, представленными в двоичном коде, является проблема округления двоичного числа до значения представимого десятичного числа, ближайшего к правильно округленному десятичному числу. Ниже мы обсуждаем эту проблему и даем простой алгоритм правильного округления. Работа алгоритма проиллюстрирована тестовой программой на C++.

Напомним, что представимым десятичным числом является число, значение которого может быть точно представлено двоичным кодом. Так, число 0.125 точно равно двоичному числу 0.001. Можно также утверждать, что значение любого двоичного числа y равно некоторому представимому десятичному числу x. Например, значение двоичного числа y=1.001101*2^-3 равно значению десятичного представимого числа x= 0.150390625.
Будем говорить, что двоичное число y эквивалентно десятичному числу x. И наоборот, десятичное число x эквивалентно двоичному числу y.

Давайте посмотрим, что происходит с десятичным числом, когда оно вводится с консоли, или, когда оно встречается в программе в качестве константы.
Любое десятичное число компилятором преобразуется (конвертируется) в заданный программистом формат. Поскольку быстрее всего в компьютере обрабатываются двоичные числа, то входное десятичное число преобразуется, чаще всего, либо в формат с фиксированной точкой (в том числе в int), либо в один из форматов с плавающей точкой — single, double, или в другой двоичный формат.

Согласно стандарту IEEE754, вещественные числа во внутреннем формате машины представляются в нормализованном двоичном виде. Так, двоичное положительное вещественное число y можно представить как y=b0.b1...bp*2^e (b0?0).
Это же число можно представить как 0.b0b1...bp*2^(e+1) (b0?0), если e+1>0 и 0.b0b1...bp*2^e (b0?0), если e<0.
Далее мы будем придерживаться второго представления, т.к. в нашей, приведенной ниже, тестовой программе на C++ есть функция q=frexp (x, &e), позволяющая в двоичном числе, представленном в виде b0.b1...bp*2^e, определить значение экспоненты e.

Итак, десятичный эквивалент любого двоичного нормализованного числа y =0.b0b1...bp*2^e (b0?0) равен некоторому нормализованному десятичному числу x=0.d0d1...dN...dn*10^E (d0?0).
Например, число y=0.1001101*2^-2 эквивалентно представимому десятичному числу x=0.150390625.
Чтобы из x получить число Xr, равное числу, округленному до N значащих цифр, надо X умножить на коэффициент 10^k, где k=N-E. Это нужно для того, чтобы полученное число содержало целую часть с N значащими цифрами, которое затем можно было округлить до ближайшего целого. Округленное целое число затем надо привести к прежнему масштабу, умножив его на 10^-k. Математически это можно записать следующей формулой:
X=[x*10^k+0.5]*10^-k=[y*10^k+0.5]*10^-k, где []-целая часть числа.

Можно показать, что целое двоичное число B, содержащее m двоичных цифр, равно значению десятичного числа D, которое содержит n=?m*log2? десятичных разрядов, при условии, что B < 10^n. И равно n=n+1, при условии, что B ? 10^n. В расчетах можно принять log2?0.301.

Определим значение k на основе информации, имеющейся в двоичном представлении числа y. В формуле для k точность округления N известна, поэтому надо определить экспоненту E.
Экспоненту E определим из соотношения: E=?e*0.301?.
Осталось учесть следующее обстоятельство. Если x*10^k= X >10^N, то его нужно умножить на 10^(-1) и скорректировать коэффициент k. Получим X=X*10^(-1), k=k-1.
Округленное число будет равно Xr=X*10^(-k).

В результате мы получаем следующий простой алгоритм правильного десятичного округления двоичных вещественных чисел. Алгоритм пригоден для любого формата двоичных чисел с произвольно заданной точностью десятичного округления N.
На входе:
-Точность десятичного округления N;
— двоичное число в формате y= 0.b0b1...bp*2^e (b0?0).
На выходе: Округленное десятичное число X=0.d0d1...dN*10^E.
— 1. Определить экспоненту e двоичного числа y;
2. E=?e*0.3?;
3. k=N-E;
4. X=x*10^k;
5. Если X<10^N, то п.8;
6. X=X*10^-1;
7. k=k-1;
8. Xr=[X+0.5]*10^(-k);
End.
— В приведенном алгоритме число Xr представляет собой представимое десятичное число ближайшее к числу, которое является правильным округлением числа x, которое в свою очередь является десятичным эквивалентом числа y.
Поскольку мы привыкли работать с десятичными числами, то, как правило, входной поток представляет собой именно десятичные числа. На выходе мы хотим получить тоже десятичные числа. Например, как в Excel. Но, после конвертации десятичных чисел в двоичный код, они, как правило, необратимо трансформируются. В результате этого, округления, преобразованных в двоичный код чисел, могут не совпадать с правильным округлением чисел, напечатанных на консоле. Это касается, прежде всего, случаев, когда мы пытаемся округлить десятичное число до максимально возможного количества значащих цифр. Для single, это 7, а для double — 15.
Это хорошо можно исследовать на тестовой программе ниже, которую автор написал на C++.

В тестовой программе, по запросу, на консоль вводится:
-точность N десятичного округления числа X, которое является ближайшим представимым числом двоичного эквивалента числа x;
— число x в произвольной форме.

Под введенным произвольным десятичным числом x печатается число X, которое является ближайшим к x представимым числом (см. скриншот ниже).
Округление выполняется над числом X, т.к. точное значение числа x утеряно при двоичной конвертации.
Возвращается:
— десятичное число в формате Xr =M*10^(N+e), где M — целое десятичное число с N значащими цифрами;
— число xr, которое равно представимому числу, ближайшему к нормализованному двоичному эквиваленту числа X.
image
На скриншоте:

N=15 — количество десятичных значащих цифр, до которого округляется входное десятичное число.
x=7.123456789098765321 e-89 — десятичное число, которое мы хотели бы округлить до 15 значащих цифр.
X=7.12345678909876559 e-089 — представимое десятичное число, значение которого равно двоичному числу, которое получено в результате конвертации числа x в формат p=53.
Xr= x=712345678909877e-103 — число с целочисленной мантиссой, полученное в результате округления числа X.
xr= x=7.12345678909877e-089 — число, полученное в результате нормализации двоичного эквивалента десятичного числа Xr. Оно является ближайшим к Xr.

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

#include <iostream>
#include <math.h>
#include <stdlib.h>
#include <iomanip>
using namespace std;

int main()
{
   double q,x,xr,X;
   unsigned long long int Xr;
   int N,p,E,e,k;

  cout <<"Input a binary precision p=";
  cin>>p;
  cout <<"Input a decimal precision N=";
  cin>>N;
  cout <<endl<<"Input a number and press ENTER:"<<"\n"<<"x= ";
  cin>>x;
   cout<<"X= "<< setprecision(18)<<x << '\n';

    q=frexp (x, &e);
    E =static_cast <int> (e*0.301);
    k=N-E;
   if (E<0)       //for format xr=d0.d1...dN*10^E (d0?0).
        k=k+1;
    X=x*pow(10,k);
       if (X > pow (10,N)){
            X=X/10;
            k=k-1;
      }

       X=X+0.5;
       Xr=static_cast <unsigned long long int> (X);
       xr=Xr*pow(10,-k);

    cout<<endl <<"Xr= "<<Xr<<"e"<<-k<<'\n';
    cout<<"xr="<<xr<<'\n';

   system("pause");
      return 0;
}


В программе используется довольно дорогая функция pow(10,k). Но, поскольку количество значений k невелико, вполне можно задействовать таблицу, заранее вычисленных значений 10^k, или лучше 5^k.

Комментарии (101)


  1. iig
    15.10.2019 10:15
    +2

    Если способ округления из статьи правильный, значит ли это, что в стандартной функции atof используется неправильный?


    1. Innotor Автор
      15.10.2019 11:40
      -1

      Функция atof преобразует строку в значение типа double.
      В моем алгоритме решается следующая задача. Имеется, допустим, двоичное число (float) y=1.00100111100111010011011*2^-21, значение которого равно десятичному представимому числу x=4.7731452512794022565370082855225e-7. Надо правильно округлить это число до N (произвольных) десятичных значащих цифр и представить полученное число в нормализованном двоичном коде. Например, для N=7, наше округленное число будет равно Xr=4.773145e-7. Его двоичный эквивалент равен y=1.00000000010000011001101e-21. Значение этого числа равно десятичному представимому числу X=4.77314472391299204900860786438E-7, которое является ближайшим к правильно округленному числу Xr.


      1. Zenitchik
        15.10.2019 11:55
        +2

        Надо правильно округлить это число до N (произвольных) десятичных значащих цифр и представить полученное число в нормализованном двоичном коде.

        Зачем? Объясните, ради какой практической надобности может понадобиться преобразовывать двоичное число в десятичное, потом округлять, а потом снова преобразовывать в двоичное?


      1. Innotor Автор
        15.10.2019 12:25

        Извините, я не совсем правильный привел пример. Вот правильный.
        Функция atof преобразует строку в значение типа double.
        В моем алгоритме решается следующая задача. Имеется, допустим, двоичное число (float) y=1.00100111100111010011100*2^-21, значение которого равно десятичному представимому числу x=5.50624235984287224709987640380859375E-7. Надо правильно округлить это число до N (произвольных) десятичных значащих цифр и представить полученное число в нормализованном двоичном коде. Например, для N=7, наше округленное число будет равно Xr=5.506242e-7. Его двоичный эквивалент равен y=1.00100111100111010011011*2^-21. Значение этого числа равно десятичному представимому числу X=5,5062417914086836390197277069092e-7E-7, которое является ближайшим к правильно округленному числу Xr. Заметьте, что x?X.


        1. michael_vostrikov
          15.10.2019 20:26

          наше округленное число будет равно Xr=5.506242e-7
          X=5,5062417914086836390197277069092e-7, которое является ближайшим к правильно округленному числу Xr

          Это неправда.


          double a = atof("5.506242e-7");
          printf("%.35e\n", a);
          // 5.50624199999999978808254419426759796e-07


          1. Innotor Автор
            15.10.2019 22:58

            Вы упустили, что пример приведен для float, у которого p=24. Для такого случая в примере все правильно. Вы же использовали double.


            1. michael_vostrikov
              15.10.2019 23:49

              А, да.


              Так а чем вас стандартный способ не устраивает, который ниже предложили ("умножение числа и последующее после округления деление")?


              float a = atof("5.50624235984287224709987640380859375e-7");
              double multiplier = pow(10, 7 + 6);
              float rounded = floor(a * multiplier + 0.49999997f) / multiplier;
              printf("%.31e\n", rounded);
              
              // 5.5062417914086836390197277069092e-07


      1. iig
        15.10.2019 12:26

        Имеется, допустим, двоичное число (float)

        Ок.

        Надо правильно округлить это число до N (произвольных) десятичных значащих цифр


        Округлением обычно занимаются при выводе результатов. printf обычно используют, или
        велосипед такой формы:
        Для случая, когда надо округлить число до определенного знака, в библиотеке <math.h> отдельной функции нет, потому обычно используют умножение числа и последующее после округления деление,


        1. Innotor Автор
          15.10.2019 14:21

          Посчитайте в Excel следующее выражение:
          1,12345678987658000000E56 + 4,12345678987654000000E56--7,92721801979018000000E+42 = -5,2469135797532E+56.
          Вас ответ устраивает?
          Если это окончательный результат, то на погрешность можно закрыть глаза, а если результат умножить на E+56, то ответ будет мало походить на ноль.


          1. gbg
            15.10.2019 15:09

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


            1. Innotor Автор
              15.10.2019 17:23

              а данные снабжены информацией о числе верных знаков

              Где кроется эта информация о десятичных верных знаках в двоичном числе формата double?

              От того, что вы умножите результат на миллиарды, вы получите ответ с погрешностью, увеличенной в миллиарды раз

              О чем и речь. А если бы перед вычитанием числа были правильно округлены, мы, в нашем примере, получили бы 0. И какое бы число на 0 ни умножай, в результате будет 0.


              1. gbg
                15.10.2019 20:01
                +1

                Информация о погрешностях известна разработчику алгоритма и программы (иногда это разные люди).
                Не всегда ее нужно носить и преобразовывать вместе с числами, так как эта информация просчитывается при доказательстве устойчивости и сходимости, например, той же разностной схемы.

                К примеру, для МКР, которым решают классическую задачу — дифференциальное уравнение с частными производными — эллептическое уравление, заданное на квадрате, если разбить этот квадрат сеткой с шагом 0.1, полученное решение будет менее чем на 0.01 отличаться от точного. То есть точность — два знака после запятой.

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

                Когда нужно, чтобы числа правильно считались в символьном смысле, арифметику с плавающей запятой просто не используют — используют либо арифметику с неограниченным числом знаков и рациональными дробями, как матлаб какой-нибудь,
                либо арифметику с фиксированной запятой — там правильное округление уже заложено.


          1. iig
            15.10.2019 17:33

            Я подозреваю, что при подобных операциях (суммировании массива float со слишком отличающимися порядками) массив нужно сначала сортировать.

            Есть ли в этом какой-то практический смысл, в сложении чисел, отличающихся на 14 порядков? Пролететь 1 млрд км туда, потом еще 1 мм, потом 1 млрд км обратно —
            перемещение будет 1 мм? ;)


          1. Zenitchik
            15.10.2019 18:38
            +1

            А практическое-то применение какое? В какой задаче Вы столкнулись с этим вычислением?
            Мне что-то подсказывает, что Вы с упорством, достойном лучшего применения, используется IEEE574 не по назначению.


            1. Innotor Автор
              15.10.2019 19:55

              А каково назначение IEEE754?
              И какое упорство достойно лучшего применения, чем стремление сделать вычисления более точными и более быстрыми (читай простыми)? Вы не предложили ни одной работающей программы, решающей проблему правильного округления десятичных чисел в двоичном коде. Не могут предложить простого решения и разработчики стандарта. Хотя ломают над этим голову.
              См., например, 1. Modern Computer Arithmetic Richard P. Brent and Paul Zimmermann 2. Correctly Rounded Floating-point Binary-to-Decimal and Decimal-to-Binary Conversion Routines in Standard ML
              3. Hardest-to-Round Cases – Part 2 и другие. Все эти ребята входят в рабочую группу по ревизии стандарта IEEE754 и пока проблему не решили.
              И потом, стандарт должен удовлетворять потребности всех пользователей решающих разнообразные задачи, а не только тех с которыми вы сталкиваетесь. Уж извините. Накипело. Поэтому, давайте по делу.


              1. Zenitchik
                16.10.2019 17:08
                +2

                А каково назначение IEEE754

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

                Вычисления не будут точнее, чем исходные данные. А в исходных данных у вас никогда не будет столько значимых цифр, сколько входит в мантиссу double.
                И точность результата будет у вас уменьшаться при каждом вычислении, независимо от того, в каком формате вы храните данные.

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

                Вы не объяснили, нахрена она нужна.


                1. Innotor Автор
                  16.10.2019 20:00

                  Ну, хорошо. Вот вам пример.
                  Рассмотрим разность двух десятичных чисел 9876543210988,06-9876543210988,04=0,02. В формате double эта разность будет 9.876543210988060546875E12-9.8765432109880390625E12=0.021484375
                  Ошибка вычисления в двоичном коде относительно точного значения будет |0.02-0.021484375|= 0.001484375. Если же округлить полученный результат до 2 знаков после запятой получим 0,02. Или в формате double 2.00000000000000004163336342344E-2. Ошибка после десятичного округления двоичного числа составляет |0.02-0.0200000000000000004163336342344|?0,4*10^-17.


                  1. iig
                    16.10.2019 21:16

                    Попробуйте, в который раз предлагаю, сделать осмысленный эксперимент. Посчитать sin(pi) через ряд Тейлора по обычной методике и по вашей. Если по вашей методике ряд сойдется к нулю значительно быстрее — тогда ок, ее можно применять в прикладных расчетах.


                  1. Zenitchik
                    16.10.2019 22:21

                    двух десятичных чисел 9876543210988,06-9876543210988,04

                    Замечательно. С какой точностью они известны?


                    1. Innotor Автор
                      16.10.2019 22:34

                      Абсолютно точно. Именно эти два числа.


                      1. Zenitchik
                        16.10.2019 23:07

                        Очень интересно.
                        Приведите пример физической величины, которую можно измерить с подобной точностью.


                        1. Innotor Автор
                          17.10.2019 07:20

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


                          1. Zenitchik
                            17.10.2019 11:44
                            +1

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

                            Если Вы лично работаете с абсолютно точными цифрами — вы не должны использовать тип с плавающей точкой, ни IEEE754, ни какой-то другой, какой вы выдумаете. Это как 2*2=4.


                            1. Innotor Автор
                              17.10.2019 12:02
                              -1

                              Я лично, работаю с изучением проблем двоичных вычислений. И пытаюсь как-то улучшить двоичную арифметику, чтобы она была точнее и быстрее. А мне все доказывают, что их устраивает статус-кво. Жаль. У нас разные цели.


                              1. Zenitchik
                                17.10.2019 12:15
                                +1

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


                  1. michael_vostrikov
                    17.10.2019 07:09

                    Если же округлить полученный результат до 2 знаков после запятой получим 0,02

                    У вас было 2 числа, известных с точностью до 2 знаков после запятой, которые при вычитании должны давать 0.02 с точностью до 2 знаков после запятой. После вычитания получилось 0.02 с точностью до 2 знаков после запятой (0.021484375). В чем проблема-то?


                    1. gbg
                      17.10.2019 09:10

                      Неправильно, погрешности-то сложатся, погрешность алгебраической суммы равна сумме погрешностей слагаемых, так что допуск будет даже больше —
                      0.00(9) + 0.00(9), то есть в сумме верным будет уже один знак после запятой.

                      Это как складывать в терминах «плюс-минус лапоть» — в одном слагаемом у нас плюс лапоть, в другом — минус лапоть, в сумме получаем, что грешим уже на два лаптя.


                      1. michael_vostrikov
                        17.10.2019 09:47

                        А, ну формально да, я скорее про ожидания автора говорил.


                      1. Innotor Автор
                        17.10.2019 10:59
                        -1

                        Мы имеем 2 числа. Они абсолютно точные. Я ручаюсь, сам придумал:).
                        И мне захотелось узнать, чему равна разность этих чисел. Имею право? Имею. Ждать мне некогда, матлабы и прочие инструменты слишком долго считают. Решил посчитать в двоичной арифметике, используя Стандарт для чисел с плавающей точкой. Набираю на клаве свои абсолютно точные числа 9876543210988,06 и 9876543210988,04. Поскольку все 15 цифр в числах верные, то и результат хочу получить с такой же точностью, т.е. с точностью до 15 значащих цифр.
                        Напечатал я одни числа, а на вход моего двоичного калькулятора поступают другие числа 9.876543210988060546875E12 и 9.8765432109880390625E12. Смотрю на сколько они отличаются от моих точных. И вижу, что погрешность первого числа составила 0,000546875, а второго 0,0009375. Многовато конечно, ну да ладно. Назад пути нет, процесс необратимый.
                        Залил я свои числа в калькулятор и на выходе получил ответ: 0.021484375. Да, что-то не очень. Я калькулятору — Ты что ж, гад, мне выдал? А он мне — Ничего не знаю, что компилятор мне дал, то я и посчитал. Посчитал, кстати, точно. Т.к. 9.876543210988060546875E12-9.8765432109880390625E12=0.021484375. Как ни крути, результат точный. И погрешность, ну строго по теории образовалась: 0,000546875+0,0009375=0.000484375
                        Кто же, думаю, виноват? Компилятор свое дело сделал точно, но числа получились приблизительными. Калькулятор посчитал точно, но ответ еще больше далек от правды. Что же делать? Присмотрелся к ответу, а там две цифры верные. Эврика? Надо отсечь лишние. Как, да просто округлить надо! А как это сделать, это совсем другая история.


                        1. Refridgerator
                          17.10.2019 11:23

                          Мы имеем 2 числа. Они абсолютно точные
                          Число само по себе не может быть точным. Число 3.141592666 — точное или неточное?


                          1. Innotor Автор
                            17.10.2019 11:37

                            Для вас оно может быть точное, а может быть и не точное. Все зависит от его происхождения. Для калькулятора — это точное число, т.к. он ничего не знает об истории его рождения. Его задача, максимально точно сделать вычисления, не внося в них, по возможности, больших искажений, а как их интерпретировать, не его забота.


                            1. Zenitchik
                              17.10.2019 11:46

                              Для калькулятора — это точное число

                              С чего Вы это взяли? Когда у нас есть число с плавающей точкой, логично считать, что его погрешность ±половина последнего известного разряда. Так во всех расчётах делают, почитайте хотя бы Брадиса.


                              1. Innotor Автор
                                17.10.2019 12:07

                                Если число приближенное, то да, погрешность <=ulp. А если оно точное, то даже записанное в форме с плавающей точкой, оно остается быть точным. Ну, не должен знать калькулятор биографию этого числа.


                                1. Zenitchik
                                  17.10.2019 12:16

                                  Если оно точное, то его НЕ НАДО записывать в формате с плавающей точкой. Но уж если записал, то смирись с тем, что вычислитель будет полагать его приближенным.


                            1. iig
                              17.10.2019 12:00

                              максимально точно сделать вычисления, не внося в них, по возможности, больших искажений


                              Надо отсечь лишние. Как, да просто округлить надо!


                              Я вижу противоречие, или мне кажется? ;)


                              1. Innotor Автор
                                17.10.2019 12:09
                                -2

                                Вам кажется. Отсекая лишние цифры («мусор») точность повышается.


                                1. Zenitchik
                                  17.10.2019 12:17

                                  Ложь.


                                1. iig
                                  17.10.2019 13:01

                                  Отсекая лишние цифры («мусор») точность повышается.


                                  Неплохо так ;) Двоемыслие айтишника?
                                  Какие ваши доказательства?
                                  — В.И, сколько будет 0.5 + 1/2?
                                  — Нутром чую, Петька, будет 1 литр, до доказать не могу


                        1. michael_vostrikov
                          17.10.2019 11:39

                          Как, да просто округлить надо!

                          Так чем вас стандартное округление-то не устраивает?


                          1. Innotor Автор
                            17.10.2019 11:54

                            Давайте, как-то определим круг обсуждаемых вопросов.
                            1. Предложен альтернативный известному алгоритм округления десятичных чисел в двоичном коде. Он работает.
                            Возражений не?
                            2. Десятичное округление десятичных чисел в двоичном коде может повысить точность вычислений. Приведен конкретный пример.
                            Обоснованных аргументов против этого факта пока высказано не было.
                            3. Вопрос -А на «хрена» это нужно?
                            См. п.2.
                            4. Чем меня не устраивает стандартная функция округления?
                            Если она вас устраивает, значит вы не сталкивались с задачами, где она облегчает жизнь. Пользуйтесь стандартной. Но реализовать стандартную функцию в железе затратно.


                            1. michael_vostrikov
                              17.10.2019 12:15

                              1. Он не альтернативный. Математически он тот же самый. Именно поэтому и вопрос — почему вы говорите, что стандартное округление как-то неправильно работает.


                              2. Это не доказано, пример не приведен. Я у вас его несколько раз уже просил, вы уходите от ответа. В примере выше вы пишете, что ответ должен быть 0.02, но он и так 0.02.


                              3. См. п.2.


                              4. Так я об этом и спрашиваю. И другие вас тоже об этом спрашивают. В каких конкретно задачах ваше округление "облегчает жизнь", а стандартное "не облегчает"?



                              Но реализовать стандартную функцию в железе затратно.

                              Почему "умножение + сложение + деление" вдруг стало затратнее "возведение в степень + умножение + условие + умножение + условие + деление + сложение + умножение"? Вычисление числа разрядов и возведение 10 в эту степень я, так и быть, не учитывал.


                              1. Innotor Автор
                                17.10.2019 14:49

                                Может быть следующее пояснение расставит все по своим местам.
                                Согласно Стандарту обмен операндами в компьютере производится через внутренний формат обмена в 32- разрядном слове. Чтобы сохранить промежуточный результат в массиве, надо число double преобразовать в float.
                                Пусть у нас есть число в double:
                                1.11001101001010110010100 10111110110001000100110110001*2^40 и нам надо его запомнить с минимальной погрешностью.

                                Если округлить это число до 24 значащих двоичных цифр с погрешностью <0.5ulp, мы получим число 1.11001101001010110010101 *2^40
                                Десятичный эквивалент этого числа, который будет записан в формат обмена, будет:
                                11100110100101011001010100000000000000000.000=1980704096256.
                                Для лучшего восприятия я числа представляю в произвольном масштабе. При желании их можно нормализовать.
                                Нас интересует точность представления, равная 7 десятичным цифрам. Т.е., мы хотели бы, чтобы в памяти хранилось число 1980704000000. Тогда погрешность десятичного округления была бы <=0.5ulp, но уже десятичного ulp. Двоичный эквивалент числа 1980704000000 в формате double равен 111001101001010110010100 01000100000000000. Или округленное до 24 значащих цифр это число будет равно 111001101001010110010100*2^17. Это число ближайшее к правильно округленному числу 1980704000000 и как мы видим оно отлично от правильно округленного в двоичной арифметике первичного двоичного числа.
                                Таким образом, чтобы минимизировать потери, перед записью в формат обмена, двоичное число double должно быть правильно преобразовано к двоичному числу, которое должно являться представимым числом, ближайшим к правильно округленному десятичному числу.
                                При большом массиве обрабатываемых чисел использовать для округления функцию atof очень затратно. Конечно, лучше всего разделить число 1980704096256, в нашем случае на 10^6, округлить, а затем снова преобразовать во float для записи в память. Но как догадаться, что нужно разделить именно на 6? В моем алгоритме эта задача решена.


                                1. gbg
                                  17.10.2019 15:04

                                  Вы не правы. Double прекрасно кочуют из оперативной памяти в процессор и обратно. Не надо ничего преобразовывать.


                                  1. Innotor Автор
                                    17.10.2019 15:50

                                    А вы загляните в IEEE754, там все описано.
                                    Вопрос не в 64 или 32 словах обмена.
                                    Процессор, как правило, вычисляет в расширенном формате, а затем пакует в более компактный. Для 64-х разрядных, это 80- битные и даже 128-битные операционные регистры. Но потом все равно приходится паковать. А именно здесь собака и порылась. Я тут выше уже давал ссылки на последние работы по этой проблеме. Пока то, что предлагается, ну крайне громоздко.


                                1. iig
                                  17.10.2019 15:32

                                  Согласно Стандарту обмен операндами в компьютере производится через внутренний формат обмена в 32- разрядном слове.


                                  Что за стандарт? Как он применяется на 64-битной архитектуре? Про 16-битную и 8-битную (микроконтроллеры, ага) даже боюсь заикаться.

                                  Чтобы сохранить промежуточный результат в массиве, надо число double преобразовать в float.


                                  Не надо так делать.


                                1. michael_vostrikov
                                  17.10.2019 16:39

                                  Согласно Стандарту обмен операндами в компьютере производится через внутренний формат обмена в 32- разрядном слове. Чтобы сохранить промежуточный результат в массиве, надо число double преобразовать в float.

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


                                  Я в этом сомневаюсь. Если бы это было так, тип double был бы бесполезен. Представьте, что они бы в integer преобразовывались посередине вычислений.


                                  Поэтому дальнейшие ваши рассуждения ложны, к происходящему в программе они не имеют никакого отношения.


                                  Если вы будете вручную конвертирвать из double во float и обратно с превышением разрядной сетки float, значит у вас плохо написанная программа, а плавающая точка тут ни при чем.


                                  Т.е., мы хотели бы, чтобы в памяти хранилось число 1980704000000.

                                  Значит вы неправильно выбрали инструмент, потому что там 30 значащих цифр, а во float влазит 24. А в double это значение нормально влазит. Значит при использовании double оно округлится так, как нужно.


                                  При большом массиве обрабатываемых чисел использовать для округления функцию atof очень затратно.

                                  Так и не надо ее использовать для округления, она в моих примерах используется для ввода данных. Округление это floor(a * multiplier + 0.499...) / multplier (константа зависит от используемого типа float или double).


                                  Конечно, лучше всего разделить число 1980704096256, в нашем случае на 10^6, округлить, а затем снова преобразовать во float для записи в память. Но как догадаться, что нужно разделить именно на 6? В моем алгоритме эта задача решена.

                                  Посчитать значащие цифры и отнять сколько надо. Алгоритм округления и алгоритм подсчета значащих цифр это разные алгоритмы. Ваше достижение в том, что вы собрали 2 алгоритма вместе? Ну поздравляю, другие программисты тоже так умеют.


                                  1. Innotor Автор
                                    17.10.2019 19:04

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

                                    IEEE Standard for Floating-Point Arithmetic (IEEE 754). Разделы: «Extended and extendable precision formats» и «Interchange formats».
                                    Значит вы неправильно выбрали инструмент, потому что там 30 значащих цифр, а во float влазит 24. А в double это значение нормально влазит. Значит при использовании double оно округлится так, как нужно.

                                    Ну, во-первых, в числе 1980704000000= 1.980704*10^13 всего 7 значащих десятичных цифр, которые гарантированно могут быть представлены в 24 разрядной мантиссе float с точностью <=0.5ulp (десятичной). А во-вторых, формат doable гарантированно может представить 15 десятичных цифр (где-то, кажется вы сами об этом упоминали). Так что число 1.980704*10^13 гарантированно «влазит» в float с указанной погрешностью и точно, как бы это вас ни коробило, в формат doable. Но если взять числа с большей экспонентой, например число 1*10^30, то оно уже и в doable не вместится.
                                    Посчитать значащие цифры и отнять сколько надо.

                                    У вас в формате double записано число 1.0110101111001100010000100100111111101011011110010111*2^140. Посчитайте сколько значащих десятичных цифр в этом числе и округлите до 3 значащих цифр. На основании того, что вы свои расчеты выполняете с точностью до 3 значащих цифр, а остальные для вас являются ложными, согласно теории приближенных вычислений.
                                    Ваше достижение в том, что вы собрали 2 алгоритма вместе?

                                    Что вы имеете ввиду? Какие 2 алгоритма я собрал вместе? Подскажите. Честное слово, я не нарочно:).


                                    1. iig
                                      17.10.2019 19:21

                                      Согласно Стандарту обмен операндами в компьютере производится через внутренний формат обмена в 32- разрядном слове.


                                      Там нет такого утверждения.
                                      For the exchange of binary floating-point numbers, interchange formats of length 16 bits, 32 bits, 64 bits, and any multiple of 32 bits ?128 are defined

                                      For the exchange of decimal floating-point numbers, interchange formats of any multiple of 32 bits are defined.

                                      — эти фразы переводится иначе.


                                      1. Innotor Автор
                                        17.10.2019 19:58
                                        -2

                                        Молодой человек! Если вы хотите постигнуть истину, а не самоутвердиться, пишите мне в личку или на мой блог. Я постараюсь найти время ответить вам.


                                        1. iig
                                          17.10.2019 23:33

                                          Вопросы я (и не только я) уже задавал, но ответа не увидел. Публичный ответ приблизит к истине не только меня ;)


                                    1. michael_vostrikov
                                      17.10.2019 20:30

                                      Разделы: «Extended and extendable precision formats» и «Interchange formats»

                                      Там нигде не написано, что 64-битный double преобразуется в 32-битный float.


                                      Ну, во-первых, в числе 1980704000000= 1.980704*10^13 всего 7 значащих десятичных цифр, которые гарантированно могут быть представлены в 24 разрядной мантиссе float

                                      Не могут.


                                      1        9        17       25   30  33       41
                                      11100110 10010101 10010100 01000100 00000000 0
                                                                      ^

                                      Там умножается на степень двойки, а не десятки, поэтому число ненулевых десятичных цифр нерелевантно.


                                      А во-вторых, формат double гарантированно может представить 15 десятичных цифр (где-то, кажется вы сами об этом упоминали).

                                      Ну я так и сказал — в double оно нормально влазит. Но никакого "гарантированно «влазит» в float" там нет (см. предыдущий пункт).


                                      Посчитайте сколько значащих десятичных цифр в этом числе

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


                                      Я там немного неправильно написал, да. Просто "отнять сколько надо" вряд ли получится.


                                      Что вы имеете ввиду? Какие 2 алгоритма я собрал вместе?

                                      Вы выдаете определение числа значащих цифр за преимущество своего алгоритма. Но это другой алгоритм, с алгоритмом округления он никак не связан. Кстати, тут вообще не очень понятно, что вы называете "задача решена". То, что число значащих цифр задается пользователем?


                                      1. Innotor Автор
                                        17.10.2019 21:26

                                        Там нигде не написано, что 64-битный double преобразуется в 32-битный float.

                                        Я не могу здесь пересказывать вам стандарт IEEE754. Я дал ссылку на Wiki… Там можно найти нужную литературу.
                                        Не могут.

                                        Могут. Десятичное 1.980704*10^13 преобразуется в
                                        1.980703965184E12, в котором первые 7 цифр с погрешностью <=0.5ulp дают число 1.980704.
                                        У вас оно вообще вводится пользователем.

                                        Естественно, кто кроме пользователя владеет этой информацией. Откуда машине знать до какого количества десятичных цифр надо округлять.
                                        Вы выдаете определение числа значащих цифр за преимущество своего алгоритма.

                                        Да, по другому эта задача не решается так просто.
                                        Кстати, тут вообще не очень понятно, что вы называете «задача решена»

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


                                        1. michael_vostrikov
                                          17.10.2019 22:30

                                          Я не могу здесь пересказывать вам стандарт IEEE754. Я дал ссылку на Wiki… Там можно найти нужную литературу.

                                          Его не надо пересказывать. Надо привести одну цитату, подтверждающую ваши слова. Судя по тому, что вы уходите от ответа, вы сами ее найти не можете. В стандарте нет такого утверждения, потому что иначе вычисления в double не работали бы.
                                          Посылать туда не знаю куда это неуважение к собеседнику.


                                          Десятичное 1.980704*10^13 преобразуется в 1.980703965184E12

                                          Вы писали "мы хотели бы, чтобы в памяти хранилось число 1980704000000". Если нас устраивает отклонение в некоторых пределах, то надо так и писать, но в этом случае подходят оба варианта.


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

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


                                          Еще раз. Вы утверждали "Десятичное округление десятичных чисел в двоичном коде может повысить точность вычислений". Я попросил у вас пример.


                                          Изначальное число у нас было 1980704062856.605712890625.
                                          Вы его округлили до 1980704000000, тем самым внесли отклонение на 62856.
                                          Далее вы предлагаете внести отклонение еще на 34816 (1980704000000-1980703965184).
                                          Каким образом это увеличивает точность?


                                          Если мы хотим увеличить точность, надо брать ближайшее к исходному числу. Во float это 1980704096256 (ошибка 33400).


                                          Именно поэтому оно и "отлично от правильно округленного в двоичной арифметике первичного двоичного числа". Потому что первичное двоичное число было совсем не 1980704000000.


                                          А вот если первичное число взять 1980704000000, то оно работает именно так, как вы и ожидаете.


                                          float a = atof("1980704000000");
                                          printf("%.6f\n", a);
                                          // 1980703965184.000000


                                          1. Innotor Автор
                                            18.10.2019 00:19

                                            Надо привести одну цитату, подтверждающую ваши слова.

                                            Я не могу привести одну цитату. Принцип двоичных вычислений представляет собой взаимосвязанный комплекс аппаратных и программных средств. Который базируется на принципах закрепленных в стандарте.
                                            Стандарт определяет основные форматы ( 3.1.1 Formats ) представления двоичных и десятичных чисел. Сюда входит формат хранения и обмена данными (3.6 Interchange format parameters), который определяет разрядность компьютера. Для двоичных чисел приняты 3 основных формата 32, 64 и 128 разрядов. В этих словах могут храниться как целые числа, так и упакованные числа с плавающей точкой. Как двоичные так и десятичные. Определен также арифметический формат (arithmetic formats), который может как совпадать с базовыми форматами, так и иметь расширенный ( extended precision format) или расширяемый формат (extendable precision format). Последние форматы используются в арифметических вычислениях для повышения точности. Но после всех вычислений они снова упаковываются в формат обмена. Операционные регистры АЛУ всегда имеют бОльшую разрядность чем ячейки памяти, т.к. после распаковки восстанавливается виртуальная единица в нормализованном числе и добавляются сторожевые биты для повышения точности вычислений. Ну, действительно, развивать эту тему дальше, это еще одна статья в коментах.
                                            Вы писали «мы хотели бы, чтобы в памяти хранилось число 1980704000000». Если нас устраивает отклонение в некоторых пределах, то надо так и писать, но в этом случае подходят оба варианта.


                                            Как раз нас устроило бы если бы в память можно было записать точное значение 1.980704*10^13. Но, поскольку это число непредставимо в float, в результате мы получаем 1.980703965184E12.
                                            А раз printf умеет ее делать, значит и в своем коде можно сделать так же.

                                            А как работает printf вас устраивает? Тогда можно.
                                            Изначальное число у нас было 1980704062856.605712890625.
                                            Вы его округлили до 1980704000000, тем самым внесли отклонение на 62856.
                                            Далее вы предлагаете внести отклонение еще на 34816 (1980704000000-1980703965184).
                                            Каким образом это увеличивает точность?

                                            Если при измерении вы получили результат 12.12345, а класс точности прибора 0.01, что нужно сделать с лишними цифрами? Округлить. В результате мы получаем более точное измерение или нет?
                                            Также и в нашем случае, если мы хотим получить точность вычислений до 7 значащих цифр мы свое число должны округлить до 7 значащих цифр. Но, поскольку число не представимо в flooat, мы получаем ближайшее к точно округленному числу представимое число.
                                            Если мы хотим увеличить точность, надо брать ближайшее к исходному числу. Во float это 1980704096256 (ошибка 33400).

                                            Совершенно верно. Но в double хранится двоичное число с какой-то экспонентой и без преобразования в десятичное представление вы не можете точно сказать на что надо умножить и разделить это двоичное число. А моя программа может.


                                            1. michael_vostrikov
                                              18.10.2019 05:38

                                              Операционные регистры АЛУ всегда имеют большую разрядность чем ячейки памяти

                                              Эта фраза означает, что если есть 64-битная ячейка памяти типа double, то промежуточные вычисления будут в формате 128 бит. При чем тут 32 бита, которые имеют меньшую разрядность?


                                              А как работает printf вас устраивает?

                                              Прочитайте еще раз то, что я написал. И предыдущие сообщения этой части. Я писал про подсчет значащих цифр. После нормализации он отличается от числа после запятой на единицу. То есть если мы хотим округлить до 3 значащих цифр, это означает 2 цифры после запятой в нормализованном формате.


                                              В результате мы получаем более точное измерение или нет?

                                              Так это не точность вычислений, а точность измерений.
                                              Я не уверен, что в метрологии это считается "более точным" значением, но теперь по крайней мере понятно, что вы имели в виду.


                                              Также и в нашем случае, если мы хотим получить точность вычислений до 7 значащих цифр мы свое число должны округлить до 7 значащих цифр.

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


                                              Но в double хранится двоичное число с какой-то экспонентой и без преобразования в десятичное представление вы не можете точно сказать на что надо умножить и разделить это двоичное число. А моя программа может.

                                              Ваша программа не может, она запрашивает эту информацию у пользователя.


                                              1. Innotor Автор
                                                18.10.2019 09:19

                                                Эта фраза означает, что если есть 64-битная ячейка памяти типа double, то промежуточные вычисления будут в формате 128 бит. При чем тут 32 бита, которые имеют меньшую разрядность?

                                                Все промежуточные вычисления выполняются в более широком формате, а хранятся в памяти в базовом формате обмена. В 32-х разрядных машинах базовым является 32-х разрядное слово.
                                                Прочитайте еще раз то, что я написал. И предыдущие сообщения этой части. Я писал про подсчет значащих цифр. После нормализации он отличается от числа после запятой на единицу. То есть если мы хотим округлить до 3 значащих цифр, это означает 2 цифры после запятой в нормализованном формате

                                                О каком представлении числа вы говорите? О двоичном или десятеричном? Поскольку внутреннее представление это нормализованное двоичное, то посчитать цифры в нем, нет проблем. Но вам надо определить множитель для десятичного представления. Следовательно без printf вам не обойтись.
                                                Вот исходное число 1980704062856 и было округлено до 7 значащих цифр. Никакой ошибки округлений здесь нет. Вы в одном случае приводите к float одно число, в другом другое, потому и результаты различаются.

                                                Повторюсь еще раз. Число 1980704062856 является представимым в double числом. Представимым, значит точно представлено в выбранном формате. Но оно не представимо в формате float. В результате округления числа double до float мы получаем правильное округление двоичного числа, в соответствии с выбранным сценарием, прописанным в Стандарте. Мы получаем другое представимое число. И оно, не смотря на то, что двоичное округление было верным может не являться ближайшим.
                                                Вот исходное число 1980704062856 и было округлено до 7 значащих цифр.

                                                Вернемся к истокам. В результате неких вычислений вы получили число double:1.1100110100101011001010010111110110001000100110110001*2^40. Для нас это первичное число. Никакой информации кроме двоичной мантиссы и экспоненты мы не имеем. Чтобы посмотреть, что это в десятичном виде мы применяем printf. И видим 1980704062856.605712890625. Это первичное десятичное представимое число.
                                                Но нас для расчетов вполне устраивает точность N=7 (или любая другая). Для этого первичное число надо округлить до 7 значащих цифр. В десятичной арифметике мы бы получили 1980704000000. Но оно непредставимо в float. Поэтому, максимум на что мы можем рассчитывать это на ближайшее к этому представимое число, которое можно сохранить в fljat.
                                                А это 1.980703965184E12. В котором 7 первых цифр совпадают с погрешностью <=ulp с идеально округленным числом 1980704000000.
                                                Ваша программа не может, она запрашивает эту информацию у пользователя

                                                Еще раз повторюсь. Программа не знает с какой точностью вас интересуют вычисления. Эту точность надо задать.


                                            1. iig
                                              18.10.2019 07:00

                                              "Если при измерении вы получили результат 12.12345, а класс точности прибора 0.01, что нужно сделать с лишними цифрами? Округлить. В результате мы получаем более точное измерение или нет?"


                                              Результатом измерения этим прибором может оказаться любое значение из диапазона 12.0… 12.2 (если грубо). 12.12345 ничем не лучше 12.1. Отбрасывание "лишних" цифр повлияет на скорость обработки данных? На объем занимаемой памяти? Каким образом?


                                              1. Innotor Автор
                                                18.10.2019 09:24

                                                Отбрасывание «лишних» цифр повлияет на скорость обработки данных? На объем занимаемой памяти? Каким образом?

                                                На точность. См. Действия над приближенными числами


                                                1. iig
                                                  18.10.2019 10:11

                                                  А отбрасывание цыфр повышает или понижает точность? Или под термином "точность" подразумевается что-то другое? ;)


                                                  1. Innotor Автор
                                                    18.10.2019 10:27

                                                    1. iig
                                                      18.10.2019 11:27

                                                      Зря я задал 2 вопроса сразу, зря ;)
                                                      Отбрасывание цыфр повышает точность расчетов? (Да, Нет, Другой ответ)


                        1. michael_vostrikov
                          17.10.2019 12:32

                          Они абсолютно точные
                          А если оно точное, то даже записанное в форме с плавающей точкой, оно остается быть точным

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


                          Если вы возьмете дробь 1/3 и запишете ее в десятичной системе с точностью 53 знака после запятой, она округлится до 0.3333...333. И точно так же дробь 987654321098806/100 в двоичной системе с точностью 53 знака округляется до ближайшего числа. Округленные данные присутствуют изначально, это математическое ограничение, его нельзя обойти, потому что нельзя записать бесконечно много знаков в фиксированной разрядной сетке.


        1. Innotor Автор
          15.10.2019 17:15

          Для случая, когда надо округлить число до определенного знака, в библиотеке <math.h> отдельной функции нет


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

          Это работает для случая нулевой экспоненты (без сдвига). Если экспонента двоичного числа существенно превышает количество разрядов двоичной мантиссы, или она отрицательная, определить значение коэффициента 10^E, на который надо разделить число, а потом его умножить, сложная задача. См. 1,2,3.


  1. gbg
    15.10.2019 10:34
    +2

    Статью невозможно читать. Я ожидаю от полемической статьи вполне определенную структуру — «вот так правильно, вот так не правильно, вот обоснование». В идеале можно привести справку с объяснением, почему же IEEE делают неправильно. Что-то в духе «написал им в твиттер, ответили так то».
    Сейчас же мне приходится парсить наукообразный поток сознания, чтобы выяснить, что же автору так не понравилось в штатной округлялке. Ну и чтобы выяснить, не NIH ли это синдром.

    МаркЁром странности также является то, что автор предлагает Один Единственно Верный Способ округления, хотя даже Википедия знает больше, как то «к ближайшему числу», «банковское», «округление вверх», «округление вниз», и так далее.

    Код читать также невозможно. Вот эта карамба из однобуквенных переменных без комментариев, константности и инициализации радостно встречает нас в начале программы:

      double q,x,xr,X;
       unsigned long long int Xr;
       int N,p,E,e,k;

    Что тут, куда тут — читатель умный, разберется.

    А еще, мне понравилось вот это
    X=x*pow(10,k);
    — использование одной и той же буквы в обоих регистрах. Ну, чтобы читающему программу было совсем все понятно.


    1. Innotor Автор
      15.10.2019 14:25

      Статья про алгоритм, а не про программу. Эта простенькая программа призвана только продемонстрировать правильность алгоритма. Она самодельная, т.к. я не профессиональный программист. Если есть ошибки в алгоритме, давайте обсудим.


  1. berez
    15.10.2019 12:28

    Input a binary precision p=111111
    Input a decimal precision N=2
    
    Input a number and press ENTER:
    x= 0.011
    X= 0.0109999999999999994
    
    Xr= 11e-3
    xr=0.0109999999999999994
    sh: 1: pause: not found
    

    Расскажите, пожалуйста, что означает сие? Зачем вы спрашиваете какую-нибудь двоичную точность, если далее в коде она нигде не используется? Что за Xr, xr, где собственнно округление?


    1. Innotor Автор
      15.10.2019 13:38

      Точность p, это десятичное число p<=53.


      1. Innotor Автор
        15.10.2019 14:38

        x= 0.011 — это число, которое вы хотите округлить до N=2
        X= 0.0109999999999999994 — это десятичное представление двоичного числа, которое получилось после конвертации десятичного x в формат double.
        Xr= 11e-3=0.011 — число, полученное округлением до N=2 числа x с целочисленной десятичной мантиссой.
        xr=0.0109999999999999994 — двоичное представление числа Xr в формате double

        Да, и еще. Округление ведется не до N знаков после точки/запятой, а до N значащих десятичных цифр.


      1. berez
        15.10.2019 15:04

        Точность p, это десятичное число p<=53.

        А почему не >=1000?
        В коде вашего «алгоритма» это десятичное число все равно не используется.


        1. Innotor Автор
          15.10.2019 16:03

          Программа написана для проверки работы алгоритма с переменными типа double. Для этого формата p=53, так что неявно это число присутствует. Любой формат двоичного числа с p<=53, гарантированно даст правильный результат округления до N<=15 значащих десятичных цифр. Если принять p>=1000, то в результате нормализации двоичная мантисса все равно будет округлена до 53 цифр. Поэтому, поскольку программа написана для формата double, гарантировать правильного округления для p>53 мы не можем. Надо перейти к другому, более длинному формату. Но, насколько я знаю, операционных регистров с количеством разрядов p близким к 1000 пока не существует.


          1. berez
            15.10.2019 16:22

            Программа написана для проверки работы алгоритма с переменными типа double. Для этого формата p=53, так что неявно это число присутствует.

            Т.е. он не нужен.
            А зачем тогда запрашивать его у пользователя?

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

            Библиотеки дл работы с числами любой длины существуют уже много-много лет. Навскидку — GMP, ttmath, MPFR — последняя вообще позиционируется как «библиотека для больших чисел с плавающей точкой и с правильным округлением».


            1. Innotor Автор
              15.10.2019 17:38

              Т.е. он не нужен.
              А зачем тогда запрашивать его у пользователя?

              Программа тестовая. Она тестирует округление для любого формата в котором p<=53. Вы можете задать p=24 для формата single, или любое другое значение p.
              Библиотеки дл работы с числами любой длины существуют уже много-много лет.

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


              1. berez
                15.10.2019 18:36

                Программа тестовая. Она тестирует округление для любого формата в котором p<=53. Вы можете задать p=24 для формата single, или любое другое значение p.

                Правда, это ничего не изменит, потому что р в программе не используется.

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

                Ну так это же не я сетовал на то, что в бинарном представлении как-то маловато разрядов. Это были вы.
                Про тяжелость вы сами придумали или есть какие-то данные с полей? Вы вот в своем коде, на минуточку, вызваете библиотечные функции pow и frexp, которые не факт что легенькие.


                1. Innotor Автор
                  15.10.2019 19:16

                  Правда, это ничего не изменит, потому что р в программе не используется.

                  Согласен, что касается p, вы правы.
                  Ну так это же не я сетовал на то, что в бинарном представлении как-то маловато разрядов. Это были вы.

                  В каком месте нашего диалога я жаловался на малость разрядов в бинарном представлении?
                  Про тяжелость вы сами придумали или есть какие-то данные с полей?

                  Может я ошибаюсь, но длинная арифметика, в том числе для чисел с плавающей точкой реализована в десятичном формате с использованием BCD. А этот формат требует существенных программно-аппаратных затрат. Именно поэтому в железе он до сих пор мало где реализован.
                  pow и frexp, которые не факт что легенькие.

                  Если отвлечься от программной реализации этих функций, а посмотреть чисто на алгоритм, то frexp легко реализуется аппаратно, т.к. надо просто считать поле двоичной экспоненты, а pow легко реализуется в табличном виде.


                  1. iig
                    15.10.2019 19:33
                    +1

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


                    Если некий софтовый функционал до сих пор не реализован в железе, с вероятностью в 146% можно предположить, что он в железе никому не нужен.


                  1. gbg
                    15.10.2019 19:44

                    У вас же интернет под рукой. Могли бы и узнать, что та же GMP использует для хранения 64 битные целые. То есть, она хранит числа в системе счисления 2^64-1.
                    Потому что в BCD что-то хранить и обрабатывать расточительно.


                    1. Innotor Автор
                      15.10.2019 22:08

                      И что? Это здесь причем? Мы же не о целых числах говорим, а о числах с плавающей точкой/запятой.


                      1. berez
                        15.10.2019 23:48

                        Возможно, при том, что на GMP построена MPFR (см. ссылки выше), а она работает с числами с плавающей запятой. Следовательно, работать числами с плавающей точкой/запятой можно и без BCD.


  1. michael_vostrikov
    15.10.2019 20:17

    x=7.123456789098765321 e-89 — десятичное число, которое мы хотели бы округлить до 15 значащих цифр.

    А чем вас округление при выводе-то не устраивает?


    #include <stdio.h>
    #include <stdlib.h>
    #include <math.h>
    
    int main()
    {
        double a = atof("7.123456789098765321e-89");
        printf("%.18e\n", a);
        printf("%.14e\n", a);
    
        return 0;
    }
    
    // gcc 1.c -lm -o 1.bin && ./1.bin
    // 7.123456789098765586e-89
    // 7.12345678909877e-89


    1. Innotor Автор
      15.10.2019 22:05

      А чем вас округление при выводе-то не устраивает?

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


      1. gbg
        15.10.2019 22:54

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


        1. Innotor Автор
          15.10.2019 23:22
          -1

          Вот вам простой пример: (9876543210988,06-9876543210987,04)*10=102
          Эта вычислительная схема устойчива? И знаков после запятой всего 2. Но посчитайте в формате double и получите 102,14844. (Проверено на Excel).


      1. michael_vostrikov
        15.10.2019 23:01
        +2

        Зачем?
        Почему они не будут накапливаться с округлением, если округление по определению отбрасывает часть разрядов, а значит увеличивает ошибку?


      1. Refridgerator
        16.10.2019 05:41
        +1

        Иначе десятичные ошибки могут быстро накапливаться.

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


        1. iig
          16.10.2019 07:19

          В институте нам показывали замечательный пример. Численно решить систему из 2 линейных уравнений, используя какой то итерационный метод, и использовать только 3 значащих цифры после запятой. Так вот, решение отличалось от школьного варианта на единицы, то есть ошибка превысила погрешность расчета на 3 порядка.
          Секрет был в том, что прямые пересекались под очень острым углом.


        1. Innotor Автор
          16.10.2019 09:05

          Вы правы, когда речь идет о вычислениях в арифметике одной базы. Когда вычисления ведутся только в десятичной или только двоичной арифметике. Но, когда мы десятичную арифметику реализуем в двоичных кодах, все становится не так очевидно.


          1. Refridgerator
            16.10.2019 09:17

            Всё становится неочевидным, потому что вы используете не те инструменты для вычислений. Не используйте float, используйте decimal или rational. Не используйте Excel, используйте системы компьютерной алгебры.


            1. Innotor Автор
              16.10.2019 10:00
              -1

              Но нам же хочется, чтобы все было быстрее и точнее:)


              1. iig
                16.10.2019 10:56

                Проверьте, делов то. Посчитайте sin(1) через ряд, или число pi. Элементы ряда сгрузите в массив, массив посортируйте рандомно (чтобы погрешности округления себя показали). 10000 элементов, думаю, хватит. Контрольный расчет повторите с обычными float. И точность, и быстродействие легко проверяется.


                1. iig
                  16.10.2019 11:58

                  А еще лучше сначала pi, а потом sin(pi) ;)
                  Формулы рядов есть в википедии.


              1. playermet
                16.10.2019 10:59

                Решите что вам важнее — «быстрее» или «точнее», и на основе этого выбирайте инструмент. Так чтобы оба сразу и без подвохов — нельзя.


          1. michael_vostrikov
            16.10.2019 11:35

            Но, когда мы десятичную арифметику реализуем в двоичных кодах, все становится не так очевидно.

            Покажите пример пожалуйста. Те, что вы приводили в статье и в комментариях, нормально считаются и округляются стандартными средствами.


            1. Innotor Автор
              16.10.2019 11:53

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


              1. Innotor Автор
                16.10.2019 12:09

                Попробуйте этот вариант.
                #include <stdio.h>
                #include <stdlib.h>
                #include <math.h>

                int main()
                {
                double a = atof(«1.506242359842875009e-27»);
                printf("%.18e\n", a);
                printf("%.14e\n", a);

                return 0;
                }
                Мне Code::Bloks выдал: 1.50624235984287e-27


                1. michael_vostrikov
                  16.10.2019 13:14
                  +1

                  Так у double точность примерно 15 десятичных знаков после запятой, почему вы ждете, что он будет 18 знаков на входе правильно обрабатывать?
                  Ближайшее число стандартными средствами округляется правильно.


                  double a = atof("1.506242359842875009e-27");
                  double multiplier = pow(10, 27 + 14);
                  double rounded = floor(a * multiplier + 0.49999999999999994) / multiplier;
                  printf("%.18e\n", a);
                  printf("%.18e\n", rounded);
                  printf("%.14e\n", rounded);
                  
                  // 1.506242359842874922e-27
                  // 1.506242359842879944e-27
                  // 1.50624235984288e-27


                  1. Innotor Автор
                    16.10.2019 14:48

                    Так у double точность примерно 15 десятичных знаков после запятой, почему вы ждете, что он будет 18 знаков на входе правильно обрабатывать?

                    Поэтому и ожидаешь, что все 15 цифр обещают быть верными.

                    К сожалению, если округлять число 1.506242359842874900e-27 вашей программой, то мы получим ошибку в другую сторону. Округление даст 1.50624235984288e-27


                    1. Innotor Автор
                      16.10.2019 15:02

                      Но это не проблема программы. Я думаю, это проблема округления 53-й значащей цифры в двоичном числе «бесконечной точности» после конвертации десятичного числа.


                    1. michael_vostrikov
                      16.10.2019 15:50
                      +1

                      К сожалению, если округлять число 1.506242359842874900e-27 вашей программой, то мы получим ошибку в другую сторону. Округление даст 1.50624235984288e-27

                      Ну так и ваша даст:


                      Input a binary precision p=53
                      Input a decimal precision N=15
                      
                      Input a number and press ENTER:
                      x= 1.506242359842874900e-27
                      X= 1.50624235984287492e-27
                      
                      Xr= 150624235984288e-41
                      xr=1.50624235984287994e-27

                      Я думаю, это проблема округления 53-й значащей цифры

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


                      1. Innotor Автор
                        16.10.2019 16:35

                        Спасибо за диалог. Алгоритм мною разрабатывался для аппаратной реализации. Тестовая программа является эмулятором работы железа. Мне было важно убедиться, что алгоритм работает правильно. Что наша дискуссия и подтвердила.


            1. Innotor Автор
              16.10.2019 12:29

              Что выдаст вам ваш компилятор, после
              double a = atof(«1.50624235984287501e-17»);
              У меня Code::Bloks выдал: 1.50624235984287e-17
              Моя тестовая программа выдает 1.50624235984288e-17
              Где правда?


              1. iig
                16.10.2019 13:32

                Ну ок. А что должна показать эта программа?
                Да, для сравнения точности лучше сосчитайте sin(pi) через ряд Тейлора. Комбинация atof-printf не показывает ничего полезного.