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

Более 90% всех программистов знают, что такое обычные числа с плавающей запятой: binary32/binary64/binary128, их часто называют float, double и т.д. соответсвенно, есть много информации о том почему 0,1 не может существовать в бинарном виде, что при большом количестве значащих цифр будут недостаток точности, даже, если ты не выходишь за рамки 16 цифр, зато они быстрые… Но почти нет информации о том, что прекрасное решение, которое сохраняет все достоинства и исправляет недостатки, есть, даже в самом, обновленном стандарте плавающих чисел IEEE 754-2008 уже больше 15 лет, это decimal floating point(DFP).

Для начала вспомним устройство обычного binary64: 1 бит знака, 11 битов экспоненты, 52 бита мантиссы. Давайте лучше картинку покажу:

Так в разы понятнее
Так в разы понятнее

Раньше попытка создать десятичную систему для плавающих чисел привела к прожорливому, которому, чем больше памяти дашь, тем больше он съест BCD, ты кодировал числа тетрадами, но главная проблема это, то что четвертый бит использовался лишь для трёх чисел, из-за этого даже двоичная мантисса для иррациональных чисел была точнее, так как позволяла кодировать больше десятичных цифр: 16,3 против 13.

Потеря битов была замечена в конце 1960-х, поэтому была создана кодировка Чен-Хо, которая кодировала 3 десятичные цифры в 10 битов, после 2002 году её доработали и превратили в плотно упакованную десятичную дробь или правильнее Densely packed decimal(DPD) были сохранены все преимущества и добавлено ещё одно первые числа в этой кодировке были такими же, как в BCD, это упрощало преобразования, поэтому её включили в обновленный стандарт IEEE 754, там же оказалась ещё одна Binary Integer Decimal(BID). Потери были хоть какими-то в этом случае только для 10 байтов: 1000 против 1024 десятичных цифр, но покажите мне реальное применение таких чисел с 80 битами в мантиссе(!).

Вот иллюстрация кодирования цифр в BCD, Чен-Хо, DPD:

Кодировка есть, плавающие числа есть, так что же не объединить? Это и было сделано: мы получили Decimal floating-point (DFP), в них может использоваться DPD, либо BID(разницы особо нет).

Давайте проведём сравнение binary64 vs decimal64(для остальных вариантов всё будет аналогично):

  1. Количество значащих цифр 16,3 и 16, в числе 1 + 1e-16 эти 16 цифр совпадают, но, например, в числе 8,1 binary64 начнёт писать 8,09999…, тому, что нет даже единицы я был удивлён, а если эти два числа начать складывать между собой, начинают появляться двойки в первых 16 цифрах… - Отдам голос за decimal64.

  2. Диапазон экспоненты 10−308 до 10308 и 10−383 до 10384 - здесь победа Decimal64.

  3. Скорость выполнения: Первый ответ, который приходит в голову - binary быстрее. Но тут очень интересно: binary считает не ALU(он просто не умеет) а отдельный подпроцессор(FPU), поэтому тоже самое можно сделать и для Decimal(сейчас есть реализация только в микрокоде). Это как сравнивать двоичные и троичные компьютеры при этом вторые эмулировать программно. Паритет.

Итог: Decimal floating point очень интересная и выгодная из-за возможности представить точно десятичную систему счисления, а в некоторых случаях даже иррациональную и большего диапазона экспоненты одновременно замена binary, реализовать её не сложнее FPU, а плюсов будет больше(не только для финансов, а также, например, для графики, пример приводил в комментарии @kinh,.но это только один пример, их больше). Почему не заменили в ПК(моё мнение): хоть она и есть в IEEE 754, мало кому она известна, а производителям процессора для ПК лень менять, так как запроса нет, а запроса нет, потому что нет реализаций и даже в C++ нет Decimal, получается замкнутый круг.

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

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

Цель этой статьи познакомить большее количество людей с этой удивительной технологией, поэтому без большого количества формул и т.п. :).

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


  1. SIISII
    29.08.2024 12:11
    +7

    Реально десятичные вещественные реализованы в z/Architecture. Основная польза от них, как представляется, не для научных расчётов, а для финансов -- раньше там использовались тоже десятичные числа, но целые переменной длины (1-31 цифра плюс знак), что было реализовано ещё в Системе 360 и благополучно дожило до наших дней.

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

    В общем, в той же z/Architecture двоичные и десятичные будут благополучно сосуществовать и дальше, а вот внедрение десятичных (хоть целых, хоть вещественных) в другие архитектуры представляется довольно сомнительным, не смотря на определённые выгоды от них в некоторых задачах.


    1. unreal_undead2
      29.08.2024 12:11
      +1

      А в финансах именно десятичные вещественные с мантиссой/порядком (когда мы, соответственно, теряем целые рубли, оперируя суммами 10^много рублей) действительно нужны? На взгляд дилетанта - там либо операции с суммами на счетах, налогами и т.п., когда нужна точность до копеек независимо от суммы, либо аналитика (просчитать какие-нибудь тренды), когда можно округлить и "по двоичному".


      1. SIISII
        29.08.2024 12:11
        +1

        Исторически для финансов используются целые BCD (точней, с фиксированной запятой; целые -- их частный случай). Конкретно на z/Architecture преимущество от вещественных десятичных может быть в том, что их можно хранить в регистрах процессора, а обычные целые десятичные всегда хранятся только в памяти и имеют переменную длину; соответственно, обрабатывать вещественные целые можно быстрей. Но, чтоб не было недопустимых округлений, придётся постоянно следить за порядком и не допускать его выхода за определённые пределы. С целыми в памяти в этом смысле проще: если результат не лезет в отведённое поле, автоматом фиксируется переполнение. Как обстоит с этим дело на практике, не скажу: к финансовому сектору отношения не имею и, надеюсь, иметь не буду :)


    1. N-Cube
      29.08.2024 12:11
      +1

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


      1. SIISII
        29.08.2024 12:11
        +1

        Ну, с появлением 64-разрядных целых появились и шутки про способность компьютера считать госдолг США :) Если же серьёзно, считать числа необходимой разрядности можно было на Системе 360 (собственно, и считали), но это делалось без лишнего геморроя лишь в десятичных операциях: двоичных операций над 64-разрядными числами не было (сама архитектура 32-разрядная). Для реализации 64-разрядных операций сложения-вычитания требовалась последовательность команд типа "сложение - условный переход - сложение" (перенос и переполнение выявлялись, но не могли прямо использоваться арифметическими командами). С умножением и делением, понятное дело, ещё хуже.


      1. unreal_undead2
        29.08.2024 12:11

        копейки или центы в формате int64 решают все практические задачи в финансах

        Случаи разные бывают. Скажем, ВВП Югославии в 1993ем был в районе 100 миллиардов долларов США, доллар 31 декабря 1993го стоил 1,775,998,646,615 динаров - так что на работу с суммами порядка ВВП в динарах, если ничего не путаю, фиксированных 64 бит не хватило бы. Не факт, что подобное не повторится.


        1. LanskoyGames Автор
          29.08.2024 12:11

          Зато хватает Decimal64 или double


        1. SIISII
          29.08.2024 12:11

          Если не ошибаюсь, для выражения ВВП в динарах потребовалось бы 22 десятичных разряда -- если б и влезло в 64-разр целое, то на пределе. А вот десятичные переменной длины Системы 360 справились бы -- там до 31 цифры включительно :)


          1. LanskoyGames Автор
            29.08.2024 12:11

            Нет, сейчас посмотрел даже в 8,1 нет единицы в Ieee754, так что я ошибся, с binary всё ещё хуже


          1. unreal_undead2
            29.08.2024 12:11
            +1

            2^64=1.8e19 , так что не влезет


        1. N-Cube
          29.08.2024 12:11

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


          1. unreal_undead2
            29.08.2024 12:11
            +1

            У топовых предприятий обороты порядка ВВП страны, и налоги и прочее им надо считать в местной валюте - так что при составлении отчётности за 1993 год надо было оперировать подобными суммами.

            предлагаете всем заранее отказаться от экономики и математики?

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


            1. N-Cube
              29.08.2024 12:11

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


              1. unreal_undead2
                29.08.2024 12:11
                +1

                Да, соглашусь, возможно в отдельной компании проблем не было. Но всё таки двенадцатизначный курс там держался неделю и всё это время в ЦБ надо было с точностью до динара рассчитывать имеющиеся ресурсы включая золотовалютные запасы.


    1. LanskoyGames Автор
      29.08.2024 12:11

      В числе 1 + 1e-16, 16 цифр совпадают, как и должно быть, но, например, в числе 8,1 binary64 начнёт писать 8,09999…, тому, что нет даже единицы я был удивлён, а если эти числа начать складывать… - Отдам голос за decimal64


    1. Sap_ru
      29.08.2024 12:11

      А x86 тоже до сих пор есть десятичная арифметика.


      1. SIISII
        29.08.2024 12:11
        +1

        Изначально в 8086/8088 (16-разрядных ещё, не 32) были команды коррекции для обычных арифметических операций. Это -- не полноценная десятичная арифметика, какая имеется на ИБМовских мэйнфреймах, это -- "жалкое подобие левой руки" (с), родом из калькуляторных применений. Но с переходом на 64 бита и её полностью выпилили вместе с рядом других команд. Так что современные процы могут выполнять эти команды только в 16- и 32-разрядных режимах, где они сохраняются для совместимости.


        1. LanskoyGames Автор
          29.08.2024 12:11

          Только непонятно для чего выпиливать?


          1. SIISII
            29.08.2024 12:11
            +1

            От них толку было примерно 0: они крайне неэффективны, почему и не использовались. Если хотите реально разобраться -- учите ассемблер и пробуйте написать полноценную десятичную арифметику а-ля мэйнфреймы на нём :)


    1. LanskoyGames Автор
      29.08.2024 12:11

      А для графики?


  1. SIISII
    29.08.2024 12:11
    +2

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


  1. DimPal
    29.08.2024 12:11
    +2

    Двоично-десятичная система не является "более точной". В десятичной системе тоже легко получить бесконечно длинные дробные части (в периоде и без). Сохранение введенного вручную числа (любимое 0.1) не является признаком "точности". Уж если такая задача реально возникает, то она легко исправляется другими методами. Например, округлением до нужного знака после запятой, а это уже больше связано с алгоритмом преобразования двочного представления в десятичное.


    1. SIISII
      29.08.2024 12:11
      +3

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


    1. LanskoyGames Автор
      29.08.2024 12:11

      В числе 1 + 1e-16, 16 цифр совпадают, как и должно быть, но, например, в числе 8,1 binary64 начнёт писать 8,09999…, тому, что нет даже единицы я был удивлён, а если эти числа начать складывать… - Отдам голос за decimal64


      1. DimPal
        29.08.2024 12:11

        Вот об этом и речь. Это не потеря "точности", это грубое округление. Можно просто научить функцию преобразования в строку (в десятичный формат?) красиво округлять для заданного числа разрядов после запятой.


        1. SIISII
          29.08.2024 12:11
          +2

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


          1. N-Cube
            29.08.2024 12:11

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


            1. SIISII
              29.08.2024 12:11

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


              1. N-Cube
                29.08.2024 12:11

                Вы не понимаете принцип - как на счетах работали с дробями? Как выше писали, десятичные дроби тоже бывают бесконечными, но на счетах это решили (как?).


          1. DimPal
            29.08.2024 12:11

            При равных условиях (выполняемых операциях) потеря точности вычислений для обеих систем будет примерно одинаковая (у двоичной потери будут даже меньше). Друг от друга они действительно будут различаться.


            1. LanskoyGames Автор
              29.08.2024 12:11

              Как? Минимальный шаг между числами в десятичной системе намного больше, чем в двоичной для одного знака в пять раз, для двух в 25 раз и т.д.. А здесь Decimal даже точнее будет


              1. DimPal
                29.08.2024 12:11

                Вы неправильно понимаете принцип двоично-десятичного представления. на один десятичный разряд приходится 4 бита, а не один. Для упомянутой в статье кодировке Чен-Хо на 3 десятичных разряда тратися 10 бит (т.е. 1000 vs 1024). Двоично-десятичное представление рыхлое - содержит неиспользуемые значения на всём возможном диапазоне.


                1. LanskoyGames Автор
                  29.08.2024 12:11

                  Я сейчас статью процитирую: «Потери были хоть какими-то в этом случае только для 10 байтов: 1000 против 1024 десятичных цифр, но покажите мне реальное применение таких чисел с 80 битами в мантиссе(!).». Даже для 128 бит разницы нет, но зато мы получаем более точную арифметику.


          1. Uint32
            29.08.2024 12:11

            Можно и не накапливать результат.

            10.2 * 3 >= 30.6

            возвращает false при любой точности binary float


            1. LanskoyGames Автор
              29.08.2024 12:11

              Было бы лучше, если double и float, например, всегда выдавали результат чуть в большую сторону, но для 0,1 они выдают число больше 0,1, а, например, для 8,1 меньше 8,1, это приводит к тому, что после сложения этих двух чисел появляются двойки в первых 16(отсюда и эта ошибка). Это ещё один плюс decimal64 в плане точности и экспонента больше, но поддержки Decimal64 нет.


        1. LanskoyGames Автор
          29.08.2024 12:11

          На выводе округлится, но в аппаратном плане округлить нельзя, а значит до свидания даже 16 десятичных цифр


        1. Hardcoin
          29.08.2024 12:11

          Красивое округление приведет к непреднамеренной потере копеек.


  1. kinh
    29.08.2024 12:11
    +1

    Десятичные числа нужны не только для финансистов. Ещё они нужны для графической разметки. Описание разметки задаётся примерно в виде: "умножить высоту этого окна на 80% и сложить с удвоенной толщиной этой рамки". Если мы преобразовали десятичные числа в двоичные, и прокручиваем длинный документ, то ошибка накапливается, пока графические элементы не начнут съезжать от правильных положений. Всё, что касается разметки, 3D-графики, чертежей - там нужны десятичные числа.