Перевод числа в строку с помощью FPU

    Введение

    Часто требуемое для вывода результатов расчетов преобразование числа с «плавающей точкой» из формата IEEE-754 в текстовую строку в «научной» нотации (т.е. с показателем степени «E») не является совсем уж тривиальной задачей. В силу обстоятельств автору пришлось самостоятельно «изобретать велосипед» такого преобразования. Причем хотелось сделать это максимально эффективно, в полной мере используя аппаратные возможности обработки чисел.

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

    Постановка задачи

    Формализуем задачу. Необходимо перевести восьмибайтовое число из формата IEEE-754 в текстовую строку, где указан знак числа, мантисса заданной длины и десятичный порядок с признаком «E» и своим знаком.

    Так, если задано число 1234.567890, то строка с мантиссой, например, в 16 знаков должна выглядеть как « 1.23456788999999E+003». Вместо плюса у мантиссы нужно писать пробел, а сама мантисса должна быть не меньше единицы. Кстати, данный пример иллюстрирует дискретность и приближенность представления чисел в формате IEEE-754: приведенное исходное число не может быть точно представлено как «1.23456789000000E+003», что, может быть, интуитивно ожидалось.

    Использование команд FPU для преобразования

    На первый взгляд, решение выглядит простым. В устройстве FPU (Floating Point Unit) процессора даже имеются команды, явно предназначенные и для перевода чисел из формата IEEE-754 в текст. Это, во-первых, команда EXTRACT разделения числа на мантиссу и порядок, а во-вторых, команда FBSTP выдающая мантиссу сразу в виде двоично-десятичного значения. Однако при ближайшем рассмотрении эти команды не дают нужного результата.

    Например, если применить команду FBSTP к указанному выше числу, то получится 10 байт со значением 35 12 00 00 00 00 00 00 00 00, поскольку нецелочисленные значения сначала округляются согласно полю RC управляющего слова FPU. Это двухбитовое поле RC может принимать 4 значения, но среди них нет режима «отключить округление». Поэтому часть мантиссы просто теряется, единственное чего можно добиться режимами округления – это еще получить значение 34 12 при округлении к меньшему.

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

    Итак, при использовании возможностей FPU придется решать две задачи.

    Во-первых, умножением (или делением) исходного числа на десять нужно «перегнать» всю мантиссу в целочисленную часть числа, тогда команда FBSTP выдаст, наконец, в двоично-десятичном виде все цифры мантиссы.

    Во-вторых, нужно определить такой десятичный порядок (опять-таки умножением или делением на десять), при котором результат попадет в диапазон между 1 и 10. Это нужно для представления числа в виде мантиссы с одной цифрой перед точкой и найденным порядком. Увы, совместить эти две задачи в едином цикле умножения/деления невозможно.

    Причем есть и «подводный камень» в виде значения числа, максимально приближенного к единице, но не равного единице. Циклом деления или умножения легко можно ошибиться в показателе степени и вместо требуемого в данном случае 9.999…E-001 получить неправильное значение типа 9.999…E+000. К сожалению, при всем богатстве команд FPU обойтись без циклов деления и умножения на десять не удается.

    Алгоритм преобразования

    При формировании мантиссы для числа типа «double» умножение или деление на 10 продолжаются до тех пор, пока двоичный порядок числа не достигнет (или не станет больше/меньше) 53, поскольку под мантиссу выделено именно 53 двоичных разряда.

    При формировании десятичного порядка для числа типа «double» умножение или деление на 10 продолжаются до тех пор, пока двоичный порядок разницы числа и 1 не достигнет (или не станет больше/меньше) -53, что означает максимальное приближение к 1. При этом необходимо еще отслеживать частный случай ближайшего к 1 значения из одних девяток, поскольку цикл умножения или деления в этом случае дает одну «лишнюю» или одну «недостающую» степень.

    Пример реализации преобразования

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

    Пара комментариев к тексту. Подпрограмма написана на языке ассемблера RASM, который имеет некоторые особенности.

    В частности здесь используются «временные» метки в виде символа «@». Алгоритм их реализации следующий: транслятор имеет внутренний счетчик меток. Когда метка «@» встречается в команде перехода, к ней автоматически дописывается значение этого счетчика (т.е. реально создаются метки @0000, @0001 и т.д.). Когда встречается символ «@» с двоеточием, к нему также автоматически приписывается значение счетчика и после этого счетчик увеличивается на 1.

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

    Кроме этого, для команд FPU в RASM не анализируется размер операнда, он имеется прямо в названиях команд в виде значений 16, 32, 64 или 80.

    Но вернемся к задаче. Подпрограмме по адресу в EBX передается исходное восьмибайтовое число в формате IEEE-754 и требуемая длина текстовой строки-результата в AL.

    В результате работы число записывается в стек в виде текста в «научной» нотации и в AL сохраняется длина этой строки. Другие регистры не сохраняются.

    С целью разгрузить конвейеры процессора некоторые команды «рассыпаны» по тексту и не образуют непрерывных логических цепочек, что несколько затрудняет чтение текста. Но таких мест немного. Программа реентерабельна и может быть использована в параллельных вычислениях. При начале работы программы управляющее слово FPU CW=0360.

    1. Сначала в стеке выделяется место для ответа и заполняется «нулевым» шаблоном, т.е. значением « 0.000… E+000».

    2. Затем проверяется знак числа и в зависимости от него формируется мантисса умножением или делением числа на десять.

    3. Командой FBSTP мантисса переписывается в память из FPU в двоично-десятичном виде и ее часть (заданной длины) переносится в ответ.

    4. После этого исходное число опять-таки умножением или делением на десять как можно ближе «подгоняется» к 1, что позволяет вычислить требуемый десятичный порядок, который и записывается на заранее подготовленное в ответе место.

          CSEG
    PUBLIC ?QFCMW:
    
    ;---- ПОДГОТОВКА МЕСТА В СТЕКЕ ----
    
          MOVZX     ECX,AL          ;ЗАДАННАЯ ДЛИНА СТРОКИ
          POP       EAX             ;ДОСТАЛИ АДРЕС ВОЗВРАТА
          SUB       ESP,ECX         ;ВЫДЕЛИЛИ МЕСТО В СТЕКЕ
          MOV       EDI,ESP         ;ОНО ЖЕ НАЧАЛО СТРОКИ-РЕЗУЛЬТАТА
          MOV       ESI,ESP         ;ЗАПОМНИЛИ НАЧАЛО
          PUSH      EAX             ;АДРЕС ВОЗВРАТА ВЕРНУЛИ НА МЕСТО
          PUSH      ECX             ;ЗАПОМНИЛИ ЗАДАННУЮ ДЛИНУ ОТВЕТА
    
    ;---- СНАЧАЛА ЗАПОЛНЕНИЕ СТРОКИ-РЕЗУЛЬТАТА НУЛЕВЫМ ШАБЛОНОМ ----
    
          MOV       EAX,'0.0 '      ;ЗНАК И ПЕРВОЕ ЧИСЛО С ТОЧКОЙ
          STOSD
          DEC       EDI             ;ОСТАВЛЯЕМ ПЕРВЫЕ ТРИ СИМВОЛА ШАБЛОНА
          SUB       CL,8            ;ВЫЧЛИ СЛУЖЕБНЫЕ ЗНАКИ И ПОРЯДОК
          MOV       AL,'0'
          JBE       @               ;ОШИБКА, НЕТ МЕСТА ПОД МАНТИССУ
          REP       STOSB           ;ЗАПОЛНИЛИ МАНТИССУ НУЛЯМИ
    @:    MOV       EAX,'00+E'      ;ПОКА ДВА НУЛЯ ПОРЯДКА
          STOSD                     ;ЗАПИСАЛИ НУЛЕВОЙ ПОРЯДОК
          LEA       EBP,[EDI]-2     ;ЗАПОМНИЛИ АДРЕС ПОРЯДКА
          FLD64     [EBX]           ;ЗАГРУЗИЛИ ЗАДАННОЕ ЧИСЛО
          MOV       B PTR [EDI],'0' ;ПОКА ТРЕТИЙ НУЛЬ ПОРЯДКА
    
    ;---- ПРОВЕРКА ЗАДАННОГО ЧИСЛА НА ПЛЮС/МИНУС/НОЛЬ ----
    
          FTST                      ;СРАВНИЛИ С НУЛЕМ
          FNSTSW    AX
          SAHF
          JNZ       @               ;ЗАДАННОЕ ЧИСЛО НЕ НОЛЬ
    
    ;---- СРАЗУ ВЫХОД С ЧИСТЫМ НУЛЕМ ----
    
          POP       EAX             ;ВЫХОД С ЗАДАННОЙ ДЛИНОЙ И НУЛЕМ
          FSTP      ST              ;ОЧИСТИЛИ FPU ОТ ИСХОДНОГО ЧИСЛА
          RET
    
    ;---- ОБРАБОТКА ЗНАКА ЗАДАННОГО ЧИСЛА ----
    
    @:    JNB       @               ;ЕСЛИ ЧИСЛО ПОЛОЖИТЕЛЬНО
          MOV       B PTR [ESI],'-' ;ОТМЕТИЛИ ЗНАК ОТРИЦАТЕЛЬНОГО
          FABS                      ;УБРАЛИ ЗНАК В САМОМ ЧИСЛЕ
    @:    MOV       EDX,OFFSET ДЕСЯТЬ ;ДЕСЯТИЧНАЯ СИСТЕМА
    
    ;---- ПРОВЕРКА ВЕЛИЧИНЫ ПОРЯДКА ИСХОДНОГО ЧИСЛА ----
    
          FLD       ST              ;РАЗМНОЖИЛИ ПОЛОЖИТЕЛЬНОЕ ЧИСЛО
          POP       ECX             ;ОПЯТЬ ДОСТАЛИ ДЛИНУ СТРОКИ
          FXTRACT                   ;РАЗДЕЛИЛИ МАНТИССУ И ПОРЯДОК
          PUSH      ECX             ;ОПЯТЬ СОХРАНИЛИ ДЛИНУ СТРОКИ
          FSTP      ST              ;ВЫКИНУЛИ МАНТИССУ
          SUB       ESP,11          ;ВЫДЕЛИЛИ МЕСТО ПОД МАНТИССУ КАК BCD
          FIST32P   [ESP]           ;ЗАПИСАЛИ ПОРЯДОК
          CMP       D PTR [ESP],53  ;ОН УЖЕ 53 РАЗРЯДА ?
          JE        M0003           ;ТОГДА МАНТИССА УЖЕ ГОТОВА
          JL        M0002           ;ИНАЧЕ ЧИСЛО ОЧЕНЬ МАЛО
    
    ;---- УМЕНЬШЕНИЕ ПОРЯДКА ЧИСЛА ОТ ОЧЕНЬ БОЛЬШОГО ----
    
    M0001:FLD       ST              ;РАЗМНОЖИЛИ ЧИСЛО
          FXTRACT                   ;РАЗДЕЛИЛИ МАНТИССУ И ПОРЯДОК
          FSTP      ST              ;ВЫКИНУЛИ МАНТИССУ
          FIST32P   [ESP]           ;ДОСТАЛИ ПОРЯДОК
          CMP       D PTR [ESP],53  ;УЖЕ 53 РАЗРЯДА ?
          JLE       M0003           ;ТОГДА МАНТИССА УЖЕ ГОТОВА
          FIDIV16   [EDX]           ;РАЗДЕЛИЛИ ЧИСЛО НА ДЕСЯТЬ
          JMPS      M0001           ;ПРОВЕРЯЕМ НОВЫЙ ПОРЯДОК
    
    ;---- УВЕЛИЧЕНИЕ ПОРЯДКА ЧИСЛА ОТ ОЧЕНЬ МАЛОГО ----
    
    M0002:FLD       ST              ;РАЗМНОЖИЛИ ЧИСЛО
          FXTRACT                   ;РАЗДЕЛИЛИ МАНТИССУ И ПОРЯДОК
          FSTP      ST              ;ВЫКИНУЛИ МАНТИССУ
          FIST32P   [ESP]           ;ДОСТАЛИ ПОРЯДОК
          CMP       D PTR [ESP],53  ;УЖЕ 53 РАЗРЯДА ?
          JGE       M0003           ;ТОГДА МАНТИССА УЖЕ ГОТОВА
          FIMUL16   [EDX]           ;УМНОЖИЛИ ЧИСЛО НА 10
          JMPS      M0002           ;ПРОВЕРЯЕМ НОВЫЙ ПОРЯДОК
    
    ;---- ВЫДАЧА МАНТИССЫ В ДВОИЧНО-ДЕСЯТИЧНОМ ВИДЕ ----
    
    M0003:FBSTP     [ESP]+1         ;ЗАПОМНИЛИ МАНТИССУ КАК BCD-ФОРМАТ
    
    ;---- ФОРМИРОВАНИЕ ТЕКСТА ИЗ BCD-МАНТИССЫ ----
    
          LEA       EDI,[ESI]+1     ;АДРЕС ОТВЕТА ПОСЛЕ ЗНАКА
          FLD1                      ;ЗАГРУЗИЛИ КОНСТАНТУ 1E0
          SUB       CL,7            ;ЗАДАННАЯ ДЛИНА МАНТИССЫ В ОТВЕТЕ
          FLD64     [EBX]           ;ОПЯТЬ ЗАГРУЗИЛИ ИСХОДНОЕ ЧИСЛО
          LEA       ESI,[ESP]+10    ;АДРЕС ПЕРВЫХ ЦИФР МАНТИССЫ
    
          STD
          MOV       DH,0            ;ПОКА НУЛИ НЕ ПИШЕМ
          FABS                      ;ЗНАК ИСХОДНОГО ЧИСЛА БОЛЬШЕ НЕ НУЖЕН
          MOV       AH,0            ;НАЧИНАЕМ С ЧЕТНОЙ ТЕТРАДЫ
          FCOM                      ;ЗАРАНЕЕ СРАВНИВАЕМ ЧИСЛО С 1E0
          MOV       BL,1            ;ПОКА ОНО МОЖЕТ БЫТЬ ВБЛИЗИ +1/-1
    
    ;---- ЦИКЛ ПЕРЕПИСИ ПО ВСЕМ ТЕТРАДАМ BCD-МАНТИССЫ ----
    
    M0004:XOR       AH,1            ;ОЧЕРЕДНАЯ ТЕТРАДА
          JZ        @               ;ЕСЛИ НЕЧЕТНАЯ ТЕТРАДА
          LODSB                     ;ДОСТАЛИ БАЙТ МАНТИССЫ
          CMP       ESI,ESP         ;ЗА ПРЕДЕЛАМИ МАНТИССЫ ?
          MOV       DL,AL           ;ДВЕ ТЕТРАДЫ МАНТИССЫ
          JB        M0007           ;ЗАКОНЧИЛИ ВЫВОД
    
    ;---- ЧЕТНАЯ ТЕТРАДА ----
    
          SHR       AL,4            ;ЧЕТНАЯ ТЕТРАДА BCD
          JMPS      M0005
    
    ;---- НЕЧЕТНАЯ ТЕТРАДА ----
    
    @:    MOV       AL,DL
          AND       AL,0FH          ;НЕЧЕТНАЯ ТЕТРАДА BCD
    
    ;---- ПРОПУСК ЛИДИРУЮЩИХ НУЛЕЙ МАНТИССЫ ----
    
    M0005:OR        DH,AL           ;ЕЩЕ ИДУТ НУЛИ МАНТИССЫ ?
          JNZ       @               ;УЖЕ ИДУТ ЦИФРЫ МАНТИССЫ
          INC       ECX             ;НЕ УЧИТЫВАЕМ ЭТОТ НОЛЬ В МАНТИССЕ
          JMPS      M0006           ;ПРОПУСКАЕМ НЕЗНАЧАЩИЙ НОЛЬ
    
    ;---- ПРОВЕРКА НА ВСЕ ДЕВЯТКИ (Т.Е. НА ЧИСЛО ВБЛИЗИ +1/-1) ----
    
    @:    CMP       AL,9            ;ИДУТ СПЛОШНЫЕ ДЕВЯТКИ ?
          JZ        @               ;ДА, ПРОДОЛЖАЮТСЯ ДЕВЯТКИ
          MOV       BL,0            ;УЖЕ НЕ ВБЛИЗИ +1/-1 (НЕ ДЕВЯТКА)
    
    ;---- ПРОПУСК ТОЧКИ, ЗАРАНЕЕ ЗАПИСАННОЙ В ОТВЕТ ----
    
    @:    CMP       B PTR [EDI],'.' ;ЗДЕСЬ В ШАБЛОНЕ ТОЧКА ?
          JNZ       @
          INC       EDI             ;ПРОПУСКАЕМ ТОЧКУ
    
    ;---- ЗАПИСЬ ОЧЕРЕДНОЙ ЦИФРЫ МАНТИССЫ КАК ТЕКСТА ----
    
    @:    ADD       [EDI],AL        ;ПИШЕМ ОЧЕРЕДНУЮ ЦИФРУ В ОТВЕТ
          INC       EDI             ;СЛЕДУЮЩИЙ АДРЕС
    M0006:LOOP     M0004            ;ЗА СЛЕДУЮЩЕЙ ТЕТРАДОЙ
    
    M0007:CLD
    
    ;---- ФОРМИРОВАНИЕ ВЕЛИЧИНЫ ПОРЯДКА ----
    
          MOV       ESI,OFFSET ДЕСЯТЬ
          FNSTSW    AX
          XOR       EDX,EDX         ;ПОКА ПОРЯДОК НОЛЬ
          SAHF
          JZ        M0011           ;ЧИСЛО СТРОГО РАВНО 1 - ПОРЯДОК НОЛЬ
          JA        M0009           ;ЧИСЛО БОЛЬШЕ 1 - ПОРЯДОК ПОЛОЖИТЕЛЕН
          MOV       B PTR [EBP]-1,'-' ;ОТМЕТИЛИ ОТРИЦАТЕЛЬНЫЙ ПОРЯДОК
    
    ;---- УВЕЛИЧЕНИЕ ПОРЯДКА ДО ЧИСЛА БОЛЬШЕ 1 ----
    
    M0008:FIMUL16   [ESI]           ;УВЕЛИЧИЛИ ЧИСЛО В 10 РАЗ
          INC       EDX             ;УВЕЛИЧИЛИ ПОРЯДОК
    
          FCOM                      ;СРАВНИВАЕМ С 1
          FNSTSW    AX              ;ЗАПОМНИЛИ РЕЗУЛЬТАТ СРАВНЕНИЯ
          SAHF
          JZ        M0011           ;СТРОГО РАВНО 1 - НАШЛИ ПОРЯДОК
    
          FLD       ST              ;РАЗМНОЖИЛИ ЧИСЛО
          FSUB      ST,ST2          ;РАЗНИЦА С 1
          FXTRACT                   ;ДОСТАЛИ МАНТИССУ И ПОРЯДОК
          FSTP      ST              ;ВЫБРОСИЛИ МАНТИССУ
          FIST32P   [ESP]           ;ПОРЯДОК РАЗНИЦЫ
          CMP       D PTR [ESP],-53 ;УЖЕ ВПЛОТНУЮ К 1 ?
          JLE       M0010           ;ДА, ВПЛОТНУЮ, БЛИЖЕ НЕ БУДЕТ
    
          SAHF                      ;ОПЯТЬ ДОСТАЛИ ФЛАГИ СРАВНЕНИЯ С 1
          JA        M0011           ;УЖЕ БОЛЬШЕ - НАШЛИ ПОРЯДОК
          JMPS      M0008           ;ПРОДОЛЖАЕМ УМНОЖАТЬ НА 10
    
    ;---- УМЕНЬШЕНИЕ ПОРЯДКА ДО ЧИСЛА МЕНЬШЕ 1 ----
    
    M0009:FIDIV16   [ESI]           ;УМЕНЬШИЛИ ЧИСЛО В 10 РАЗ
          INC       EDX             ;УМЕНЬШИЛИ ПОРЯДОК
    
          FCOM                      ;СРАВНИВАЕМ С 1
          FNSTSW    AX              ;ЗАПОМНИЛИ РЕЗУЛЬТАТ СРАВНЕНИЯ
          SAHF
          JZ        M0011           ;СТРОГО РАВНО 1 - НАШЛИ ПОРЯДОК
    
          FLD       ST              ;РАЗМНОЖИЛИ ЧИСЛО
          FSUB      ST,ST2          ;РАЗНИЦА С 1
          FXTRACT                   ;ДОСТАЛИ МАНТИССУ И ПОРЯДОК
          FSTP      ST              ;ВЫБРОСИЛИ МАНТИССУ
          FIST32P   [ESP]           ;ПОРЯДОК РАЗНИЦЫ
          CMP       D PTR [ESP],-53 ;УЖЕ ВПЛОТНУЮ К 1 ?
          JG        @               ;ЕЩЕ НЕ ВПЛОТНУЮ
    M0010:INC       EBX             ;ПРИЗНАК НАХОЖДЕНИЯ ВБЛИЗИ 1
          JMPS      M0011           ;ПЕРЕСТАЕМ ИСКАТЬ ПОРЯДОК
    
    @:    SAHF                      ;ОПЯТЬ ЗАГРУЗИЛИ ФЛАГИ СРАВНЕНИЯ С 1
          JNB       M0009           ;ПРОДОЛЖАЕМ ДЕЛИТЬ НА 10
          DEC       EDX             ;ЧИСЛО В ОТВЕТЕ ДОЛЖНО БЫТЬ БОЛЬШЕ 1
    
    M0011:ADD       ESP,11          ;ОСВОБОДИЛИ СТЕК ОТ BCD-МАНТИССЫ
    
    ;---- КОРРЕКТИРОВКА ПОРЯДКА ВБЛИЗИ +/-1 ----
    
          CMP       BL,2            ;БЫЛИ ВСЕ ДЕВЯТКИ И ПОЧТИ 1 ?
          XCHG      EAX,EDX         ;ДОСТАЛИ ЗНАЧЕНИЕ ПОРЯДКА
          JNZ       @               ;НЕТ, ЧИСЛО НЕ ВБЛИЗИ 1
          DEC       EAX             ;ВБЛИЗИ 1 СВЕРХУ, ДЕЛАЕМ 0.999...E+000
          CMP       B PTR [EBP]-1,'-' ;ЧИСЛО МЕНЬШЕ 1E0 ?
          JNZ       @               ;НЕТ, БОЛЬШЕ
          INC       EAX             ;ВЕРНУЛИ ПОРЯДОК
          INC       EAX             ;ВБЛИЗИ 1 СНИЗУ,  ДЕЛАЕМ 9.999...E-001
    
    ;---- ЗАПИСЬ СТАРШЕЙ ЦИФРЫ ПОРЯДКА ----
    
    @:    PUSH      100
          XOR       EDX,EDX
          POP       EBX             ;ДЕЛИМ НА КОНСТАНТУ 100
          FSTP      ST              ;ОЧИЩАЕМ FPU ОТ ПОИСКА ПОРЯДКА
          DIV       EBX             ;ПОЛУЧАЕМ ЧАСТНОЕ - ПЕРВУЮ ЦИФРУ
          ADD       [EBP],AL        ;СТАРШАЯ ЦИФРА ПОРЯДКА
    
    ;---- ЗАПИСЬ ДВУХ МЛАДШИХ ЦИФР ПОРЯДКА ----
    
          MOV       BL,10           ;ДЕЛИМ НА КОНСТАНТУ 10
          XCHG      EAX,EDX         ;ОСТАТОК ОТ ДЕЛЕНИЯ НА 100
          DIV       BL              ;ЧАСТНОЕ И ОСТАТОК - ДВЕ ЦИФРЫ ПОРЯДКА
          FSTP      ST              ;ВЫБРОСИЛИ КОНСТАНТУ 1 ИЗ FPU
          ADD       [EBP]+1,AX      ;ДВЕ ОСТАЛЬНЫЕ ЦИФРЫ ПОРЯДКА
    
    ;---- ВЫХОД С ОТВЕТОМ В СТЕКЕ И ДЛИНОЙ СТРОКИ В AL ----
    
          POP       EAX             ;ДЛИНА СТРОКИ ОТВЕТА В СТЕКЕ
          RET
    
          DSEG
    ДЕСЯТЬ DW 10                    ;БАЗА ДЛЯ ПЕРЕВОДА В ДЕСЯТИЧНУЮ СИСТЕМУ

    Заключение

    Использование команд FPU сделало данную реализацию довольно компактной (326 байт команд) и удовлетворительной по скорости. Например, на компьютере с процессором Intel Core Solo 1.33 GHz сто миллионов преобразований числа 1234.567890 в текст заняли 89 секунд.

    Таким образом, скорость работы данной подпрограммы составила 845 преобразований в секунду на каждый мегагерц тактовой частоты процессора. При этом полный отказ от использования команд FPU вряд ли позволит увеличить скорость требуемого преобразования.

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

    Комментарии 23

      +4

      А нельзя было по порядку определить примерное число десяток, на которые следует умножить/разделить число (порядок — целая часть логарифма, а значит, зависимость будет линейная)? Это, по идее, позволило бы уменьшить число циклов (за счет применения возведения 10 в нужную степень: цикл останется, но меньший по размеру). А то в double десятичный порядок может достигать 308, за вычетом (для отрицательных порядков — с прибавлением) 15-16 десятичных цифр, получается довольно большое количество итераций.
      Т.е., условно, мы сперва "сдвигаем" число так, чтобы оно получалось примерно целым, затем используем описанный алгоритм.
      Тестировать алгоритм, имеющий цикл, зависящий от порядка, т.е., по сути, сложность O(порядок), только на одном на конкретном непредельном значении с небольшим порядком, не совсем правильно.

        +4
        интересный подход, понравилось удаление периодических 9-ок в конце, но есть нюанс — в 64-битном режиме на x86 используется SSE+ для арифметики с плавающей точкой. В принципе и в 32-битном режиме тоже можно использовать SSE. Все использованные инструкции в коде имеют аналоги в SSE (FBSTP, увы нет) и позволяют напрямую адресовать регистры, вместо стека. FXTRACT можно в целочисленных регистрах сделать, и вместо пошагового умножения/деления на 10 сделать «пристрелку» по степени 2 — умножить на константу log10 (2), получить степень 10ки, возвести 10 в эту степень по таблице или быстрым способом и скорректировать число. деления вообще заметно медленнее, вместо деления на 10 быстрее умножать на 1/10, но теряется точность.
          0
          я не заметил ваш комментарий, но выбрал именно такой вариант, внизу кусочек кода который работает таким образом.
          –1

          округление в FBSTP управляется флагом CW.RC

            0
            Денормализованные числа, а также всякие nan, inf алгоритм не поддерживает? Почему выбрано 16 знаков после запятой? Ведь точное представление double (64-бита) может иметь гораздо больше знаков: например 0.1 из-за непредставимости будет иметь для double точное значение 1.000000000000000055511151231257827021181583404541015625e-1 = 0x3FB999999999999A (ну или 9.999999999999999167332731531132594682276248931884765625e-2 = 0x3FB9999999999999 если стоит округление к -inf), при этом там как раз 16 нулей/девяток, но после них идет еще хвост, который хочется выкинуть, но по хорошему нельзя, ведь он бывает важен. Или цель была получить как можно более скоростной алгоритм?
              0
              Откуда столько знаков? Под мантиссу отводится 53 бита если перевести 53 единицы в десятичное число получится 9007199254740991, как раз 16 цифр. Внутренне представление FPU 80 бит, но и там столько десятичных цифр не набирается
                –1
                Откуда столько знаков? Под мантиссу отводится 53 бита если перевести 53 единицы в десятичное число получится 9007199254740991, как раз 16 цифр.

                Причем тут перевод 53 единиц в десятичное число?
                Возьмем пример проще — абсолютно точно представимое число даже для 32-х битного float = 2**-45 (0x29000000). А теперь возведем 2 в степень -45 и посчитаем сколько это: 2.8421709430404007434844970703125e-14, как видите знаков сильно больше 16-ти, и все точные, т.к. число представимо точно. Вообще 32-х битные float могут иметь до 105 точных знаков 2**-149 = 1.40129846432481707092372958328991613128026194187651577175706828388979108268586060148663818836212158203125e-45, а уж 64-битный double так вообще 752 (2**-1074), а уж если брать 80-битный long double, то и вовсе 11497! Не буду копировать это число, текст будет слишком большим :)
                  +4

                  Вы исходите из изначально ложного предположения, что в float хранится точное число. Но это не так. Вы знаете только первые 53 бит мантиссы, но не знаете значение 54-го бита. Значению (0x29000000), как и любому другому, будет соответствовать целый диапазон десятичных чисел, а не какое-то одно число. Поэтому логично из этого диапазона выбирать число, имеющее минимальное количество значащих цифр.

                    –1
                    Вы знаете только первые 53 бит мантиссы, но не знаете значение 54-го бита.

                    Согласен, но это нисколько не противоречит моему примеру выше. В примере 2**-45 вполне представимое точно число (т.е. оно только одно соответствует этой записи, а не какой-нибудь диапазон чисел), использующее вообще только 1 бит мантиссы (о чем и говорит запись в сыром виде — 0x29000000), тем не менее имеющее более 16 десятичных знаков при точном переводе из двоичного вида. На то она и «плавающая точка», что имеет «плавающую точность», зависящую от диапазона переводимых чисел — от 8 до 105 знаков (для 32-х битных float). Если не верите мне, смотрите кучу примеров в сети, например randomascii.wordpress.com/2012/03/08/float-precisionfrom-zero-to-100-digits-2
                      +1

                      Утверждение, что число 2⁻⁴⁵ может быть представимо без округления в двоичном формате — верное, с этим никто не спорит. Да, в принципе, для любого двоичного значения существует десятичное, которое может быть переведено в двоичное без округления. Вот только практической пользы в этом мало.

                        +1

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


                        The flip side of this question is figuring out how many decimal digits it takes to uniquely identify a float. Again, we aren’t concerned here with converting the exact value of the float to a decimal (we’ll get to that), but merely having enough digits to uniquely identify a particular float.

                        И далее выводится, что для одинарной точности "enough digits" — это 9 для худшего случая, для некоторых может быть меньше (для 0.5, например, хватает 2).


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

                +6
                Не хватает сравнения с популярными реализациями C/C++/Rust и внезапного обнаружения того факта, что они работают быстрее.
                  0

                  Ну да. Вычислить десятичный логарифм одной FPU операцией — порядка 50 тактов. Автор же зачем-то для этого дела приспособил цикл, где одное только деление — около 15 тактов, а суммарно будет порядка 25–30 на один порядок: это почти 10000 тактов, если на входе будет 1e308, а если это extended (80-bit) формат, т.е. до 10^4932, так вообще под 150000 тактов.


                  Вычислять порядок

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

                      Ну так считаете логарифм, берёте от него целую часть, потенцируете, делите одно на другое — и опа, мантисса уже в нужном виде.

                    +1

                    Вроде такого?
                    Ноутбук с Intel® Core(TM) i5-8265U CPU @ 1.60GHz
                    Python 3.7 (тот язык что "тормозной")


                    >>> timeit.timeit('f"{n:.16E}"',"n=1234.567890", number=100000000)
                    86.52470250002807

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

                      0
                      так Вы потратили время не на решение, а на тестирование. А оно везде простое.
                      test:proc main;
                      dcl x float(53), s char(*) var;

                      x=1234.567890;
                      put data(time);

                      do to 100_000_000; s=x; end;

                      put data(time,s);
                      end test;

                      TIME= 19:17:44 TIME= 19:18:20 S= 1.23456788999999E+003
                      процессор i5-2310M 2.5 GHz
                      0
                      популярная реализация была придумана кем то или существовала изначально от сотворения мира?
                        0
                        Популярные реализации по определению существовали до написания данной статьи. Поэтому, предлагая им замену, претендующую на «максимальную эффективность», стоило проверить, действительно ли эффективность максимальная.
                          0
                          Скажите а к себе вы предъявляете те же требования? Вы всегда пишите максимально эффективный код либо не пишите вообще?
                            0
                            Я никаких требования не предъявлял, их предъявил сам автор:
                            Причем хотелось сделать это максимально эффективно, в полной мере используя аппаратные возможности обработки чисел.
                            При этом полный отказ от использования команд FPU вряд ли позволит увеличить скорость требуемого преобразования.
                            Вот это надо было хотя бы попытаться доказать.
                      +1
                      Похожую вещь делал давно в институте, даже исходник нашел )) Может кому пригодится — там ввод и вывод числел с плавающей запятой:
                      Код
                      .386
                      model use16 small
                      .stack 100h

                      .data
                      bufstr db 82 dup(?)
                      dopvar db 12 dup(?)
                      newstr db 13,10,'$'
                      rconst_0 dw 2
                      rconst_1 dd 1000000
                      rconst_2 dw 10

                      .code

                      pstep_2 proc

                      fabs
                      fyl2x
                      fld1
                      fscale
                      fld st(1)
                      fld st
                      fstcw word ptr dopvar
                      fwait
                      or word ptr dopvar,0c00h
                      fldcw word ptr dopvar
                      frndint
                      fsub
                      fidiv rconst_0
                      f2xm1
                      fld1
                      fadd
                      fmul st(1),st
                      fmul
                      fxch
                      ffree st
                      fincstp
                      ret
                      pstep_2 endp

                      write_0 proc
                      xor si,si
                      cmp ax,0
                      je w1c_x
                      mov bx,ax
                      and ah,80h
                      jz w1c
                      neg bx
                      mov ah,2
                      mov dl,'-'
                      int 21h

                      w1c: mov cx,10000

                      w1c0: mov ax,bx
                      xor dx,dx
                      div cx
                      mov bx,dx
                      cmp ax,0
                      jne w1c3
                      cmp si,0
                      je w1c1
                      w1c3: or si,1
                      mov dl,al
                      mov ah,2
                      add dl,30h
                      int 21h

                      w1c1: xor dx,dx
                      mov ax,cx
                      mov cx,10
                      div cx
                      mov cx,ax
                      cmp cx,0
                      jne w1c0

                      w1c_x: cmp si,0
                      jne w1c_y
                      mov dl,30h
                      mov ah,2
                      int 21h

                      w1c_y: lea dx,newstr
                      mov ah,9
                      int 21h
                      ret
                      write_0 endp

                      write_real proc

                      fxtract ;st-мантисса,st1-показатель
                      fxch ;меняем местами
                      fldl2t ;st-log[2](10),st1-p,st2-m
                      fdivp ;st-dp(десятичный показатель),st1-m
                      fstcw word ptr dopvar
                      fwait

                      or word ptr dopvar,0c00h
                      fldcw word ptr dopvar

                      fist word ptr dopvar+10 ;сохр. целую часть
                      fisub word ptr dopvar+10 ;выделим дробную часть
                      fabs ;ее модуль
                      fldl2t ;st-log,st1-modp,st2-m
                      fmul ;st-binp,st1-m
                      fld st ;st-binp,st1-binp,st2-m
                      frndint ;st-mbinp,st1-binp,st2-m
                      fxch
                      fsub st,st(1)
                      fidiv rconst_0 ;st=st/2,st1-mbinp,st2-m
                      f2xm1 ;st=2^st-1
                      fld1
                      fadd ;st=st+1
                      fld st
                      fmul ;st=st^2
                      fscale ;st=st*2^st1,st1-m

                      fwait

                      mov al,byte ptr dopvar+11
                      and al,80h
                      jz mw1

                      fdivr st,st(2) ;st-десятичн. мантисса(показат.<0)
                      fwait
                      jmp mw2

                      mw1: fmul st,st(2) ;st-десятичн. мантисса(показат.>0)
                      mw2: fimul rconst_1 ;st=st*1000000
                      fbstp tbyte ptr dopvar
                      ffree st
                      fincstp
                      ffree st
                      fincstp
                      fwait

                      mov ah,byte ptr dopvar+9
                      and ah,80h
                      jz mw5
                      mov ah,02h
                      mov dl,'-'
                      int 21h

                      mw5: mov bl,byte ptr dopvar+3
                      mov cl,bl
                      and cl,15
                      mov dl,bl
                      and dl,240
                      shr dl,4
                      cmp dl,0
                      je mw7
                      add dl,30h
                      mov ah,2
                      int 21h
                      mw7: mov dl,cl
                      add dl,30h
                      mov ah,2
                      int 21h

                      mov dl,'.'
                      mov ah,2
                      int 21h

                      mov si,2

                      mw4: mov bl,byte ptr dopvar+si
                      mov cl,bl
                      and cl,15
                      mov dl,bl
                      and dl,240
                      shr dl,4
                      add dl,30h
                      mov ah,2
                      int 21h
                      mov dl,cl
                      add dl,30h
                      mov ah,2
                      int 21h
                      cmp si,0
                      je mw3
                      dec si
                      jmp mw4

                      mw3: mov dl,'e'
                      mov ah,2
                      int 21h

                      mov ax,word ptr dopvar+10
                      call write_0
                      ret
                      write_real endp

                      read_real proc
                      r20: readln
                      xor ah,ah
                      mov cl,byte ptr bufstr+1
                      add cl,2
                      xor ch,ch
                      mov si,cx
                      and byte ptr bufstr[si],0
                      mov si,2
                      and byte ptr bufstr,0

                      r23: cmp si,cx
                      je r20
                      mov al,byte ptr bufstr[si]
                      cmp al,'-'
                      je r22
                      cmp al,'.'
                      je r27
                      cmp al,30h
                      jb r24
                      cmp al,40h
                      jb r21
                      r24: inc si
                      jmp r23

                      r22: or byte ptr bufstr,1
                      inc si
                      jmp r23

                      r27: fild rconst_2
                      fldz
                      jmp r28

                      r21: sub al,30h
                      mov word ptr bufstr+1,ax
                      fild rconst_2
                      fild word ptr bufstr+1


                      r25: inc si
                      mov al,byte ptr bufstr[si]
                      cmp al,'.'
                      je r28
                      sub al,30h
                      cmp al,9
                      ja r2_x
                      mov word ptr bufstr+1,ax

                      fmul st,st(1)
                      fild word ptr bufstr+1
                      fadd
                      jmp r25

                      r28: inc si
                      mov al,byte ptr bufstr[si]
                      sub al,30h
                      cmp al,9
                      ja r2_x
                      mov word ptr bufstr+1,ax
                      fild word ptr bufstr+1
                      fdiv st,st(2)
                      fadd
                      fld st(1)
                      fmul st(2),st
                      ffree st
                      fincstp
                      jmp r28

                      r2_x: and byte ptr bufstr,1
                      jz r2_y
                      fchs
                      r2_y: fxch
                      ffree st
                      fincstp
                      add ax,30h
                      and al,95
                      cmp al,'E'
                      jne r2_z
                      inc si
                      mov al,byte ptr bufstr[si]
                      and byte ptr bufstr,0
                      cmp al,'-'
                      je rx0
                      cmp al,'+'
                      je rx1
                      sub al,30h
                      cmp al,9
                      ja r2_z
                      jmp rx2
                      rx0: or byte ptr bufstr,1
                      rx1: inc si
                      mov al,byte ptr bufstr[si]
                      sub al,30h
                      cmp al,9
                      ja r2_z
                      rx2: inc si
                      xor bh,bh
                      mov bl,byte ptr bufstr[si]
                      sub bl,30h
                      cmp bl,9
                      ja rx3
                      mul rconst_2
                      add ax,bx
                      inc si
                      mov bl,byte ptr bufstr[si]
                      sub bl,30h
                      cmp bl,9
                      ja rx3
                      mul rconst_2
                      add ax,bx
                      rx3: and byte ptr bufstr,1
                      jz rx4
                      neg ax
                      rx4: mov word ptr bufstr+1,ax
                      fild word ptr bufstr+1
                      fild rconst_2
                      call pstep_2
                      fmul
                      r2_z: lea dx,newstr
                      mov ah,9
                      int 21h
                      ret
                      read_real endp

                      main proc
                      mov ax,@data
                      mov ds,ax

                      call read_real
                      call write_real

                      mov ax,4c00h
                      int 21h

                      main endp
                      end main

                        0
                        что вы скажите про этот кусочек кода?

                        movd qword ptr[esp - xmmword], xmm0
                        fld qword ptr[esp - xmmword]
                        fld st(0)
                        fxtract
                        fldl2t
                        fst st(1)
                        fdivr st(0),st(2)
                        frndint
                        ; fldcw [esp - word]

                        fist dword ptr[esp - dword]
                        mov edx,[esp - dword]
                        mov dword ptr[esp - dword], 10h
                        fisubr dword ptr[esp - dword]

                        fmulp st(1),st(0)
                        fst st(1)
                        frndint
                        fsub st(1),st(0)
                        fld1
                        fscale
                        fstp st(1)
                        fmulp st(2),st(0)
                        f2xm1
                        fld1
                        faddp st(1),st(0)
                        fmulp st(1),st(0)
                        frndint

                        fbstp tbyte ptr[esp - xmmword * 2]

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

                        Самое читаемое