Перевод числа в строку с помощью 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.
Сначала в стеке выделяется место для ответа и заполняется «нулевым» шаблоном, т.е. значением » 0.000… E+000».
Затем проверяется знак числа и в зависимости от него формируется мантисса умножением или делением числа на десять.
Командой FBSTP мантисса переписывается в память из FPU в двоично-десятичном виде и ее часть (заданной длины) переносится в ответ.
После этого исходное число опять-таки умножением или делением на десять как можно ближе «подгоняется» к 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. Это позволило бы при применении этой команды для перевода мантиссы в текст избежать циклов умножения и деления на десять лишь только для получения всех значащих цифр.