Как вводить и выводить действительные числа на ассемблере
Мы решаем проблемы с действительными числами на языке ассемблера, используя FPU. Обычно мы пишем код ввода и вывода, используя язык C или готовые функции. Например:
; Receiving input and output descriptors for the console
invoke GetStdHandle, STD_INPUT_HANDLE
mov hConsoleInput, eax
invoke GetStdHandle, STD_OUTPUT_HANDLE
mov hConsoleOutput, eax
invoke ClearScreen
;input X
invoke WriteConsole, hConsoleOutput, ADDR aszPromptX,\
LENGTHOF aszPromptX - 1, ADDR BufLen, NULL
invoke ReadConsole, hConsoleInput, ADDR Buffer,\
LENGTHOF Buffer, ADDR BufLen, NULL
finit
invoke StrToFloat, ADDR Buffer, ADDR X
Как сделать ввод и вывод действительных чисел на ассемблере без использования готовых функций?
1 ответ
Это действительно тот же вопрос, что и как реализовать эти функции / как они работают под капотом. Я просто собираюсь поговорить о вводе в этом ответе; Я не уверен, какие алгоритмы хороши для float->string.
Функции, предоставляемые ОС, позволяют вам читать / писать (печатать) символы, по одному за раз или в блоках. Интересная / специфичная для FP часть проблемы - это только часть float-> string и string->float. Все остальное такое же, как и для чтения / печати целых чисел (по модулю различий между соглашениями о вызовах: числа с плавающей точкой обычно возвращаются в регистрах FP).
Правильно внедряя strtod
(строка в удвоение) и эквивалент одинарной точности является весьма нетривиальным, если вы хотите, чтобы результат всегда был правильно округлен до ближайшего представимого значения FP, особенно если вы хотите, чтобы он также был эффективным, и работали для входных данных вплоть до пределы самых больших конечных значений, которые double
может держать.
Как только вы узнаете подробности алгоритма (с точки зрения просмотра однозначных чисел и выполнения умножения / деления / сложения FP или целочисленных операций над битовым шаблоном FP), вы сможете реализовать его в asm для любой платформы, которая вам нравится. Вы использовали x87 finit
инструкция в вашем примере по какой-то причине.
См. http://www.exploringbinary.com/how-glibc-strtod-works/ для получения подробного обзора реализации glibc и http://www.exploringbinary.com/how-strtod-works-and-sometimes-doesnt/ для другого широко используемого внедрения.
Описывая первую статью, Glibc's strtod
использует расширенную целочисленную арифметику. Он анализирует входную десятичную строку, чтобы определить целую часть и дробную часть. например 456.833e2
(научная запись) имеет целую часть 45683
и дробная часть 0.3
,
Он преобразует обе части в число с плавающей запятой отдельно. Целочисленная часть проста, потому что уже есть аппаратная поддержка для преобразования целых чисел в числа с плавающей запятой. например, х87 fild
или SSE2 cvtsi2sd
или что-то еще на других архитектурах. Но если целочисленная часть больше максимального 64-разрядного целого, это не так просто, и вам нужно преобразовать BigInteger в число с плавающей запятой /double, которое не поддерживается аппаратным обеспечением.
Обратите внимание, что даже FLT_MAX
(одинарная точность) для двоичного кода IEEE32 float
является (2 − 2^−23) × 2^127
, что чуть ниже 2^128, поэтому вы можете использовать 128-битное целое число для строки-> float
и если это оборачивает, то правильный float
результат +Infinity
, FLT_MAX
битовый паттерн 0x7f7fffff
: mantissa all-ones = 1.999... с максимальным показателем степени. В десятичном ~3.4 × 10^38
,
Но если вы не заботитесь об эффективности, я думаю, вы могли бы преобразовать каждую цифру в float
(или индексировать массив уже преобразованных float
значения), и делать обычные total = total*10 + digit
или в этом случае total = total*10.0 + digit_values[digit]
, FP mul / add является точным для целых чисел вплоть до точки, где два смежных представимых значения находятся дальше друг от друга, чем 1,0 (т.е. когда nextafter(total, +Infinity)
является total+2.0
), то есть когда 1 ulp больше 1.0
,
На самом деле, чтобы получить правильное округление, сначала нужно добавить небольшие значения, в противном случае каждое из них по отдельности округляется в меньшую сторону, когда все вместе они могли бы увеличить большое значение до следующего представимого значения.
Таким образом, вы, вероятно, можете использовать FPU для этого, если вы будете делать это осторожно, например, работать с блоками по 8 цифр и масштабировать до 10^8 или около того, и добавлять, начиная с наименьшего. Вы можете преобразовать каждую строку из 8 цифр в целое число и использовать оборудование int
-> float
,
Дробная часть еще сложнее, особенно если вы хотите избежать повторного деления на 10, чтобы получить значения мест, которых следует избегать, потому что это медленно и потому что 1/10
не является точно представимым в двоичной переменной с плавающей запятой, поэтому все ваши значения мест будут иметь ошибку округления, если вы сделаете это "очевидным" способом.
Но если целая часть очень большая, все 53 бита мантиссы double
может быть уже определена целочисленной частью. Таким образом, glibc проверяет и выполняет только деление с большим целым числом, чтобы получить необходимое ему количество бит (если оно есть) из дробной части.
Во всяком случае, я настоятельно рекомендую прочитать обе статьи.
Кстати, смотрите https://en.wikipedia.org/wiki/Double-precision_floating-point_format если вы не знакомы с битовыми шаблонами, которые IEEE754 binary64, или double
, использует для представления чисел. Вам не нужно писать упрощенную реализацию, но она помогает понять float. А с x86 SSE вам нужно знать, где бит знака должен реализовывать абсолютное значение (ANDPS) или отрицание (XORPS). Самый быстрый способ вычислить абсолютное значение, используя SSE. Там нет специальных инструкций для abs
или же neg
Вы просто используете логические операции для управления битом знака. (Гораздо эффективнее, чем вычитать из нуля.)
Если вас не интересует точность до последнего ULP (единица в последнем месте = младший бит мантиссы), то вы можете сделать более простой алгоритм умножения на 10 и добавления как для строки -> целое число, а затем масштабировать силой 10 в конце.
Но надежная библиотечная функция не может этого сделать, потому что создание временного значения во много раз больше, чем конечный результат, означает, что оно будет переполнено +/- Infinity
) для некоторых входов, которые находятся в пределах диапазона, который double
может представлять. Или, возможно, снизится до +/- 0.0
если вы создаете меньшие временные значения.
Обработка целочисленной и дробной частей по отдельности позволяет избежать проблемы переполнения.
Посмотрите эту реализацию C на codereview.SE для примера очень простого подхода умножения / сложения, который, вероятно, будет переполнен. Я только быстро просмотрел его, но не вижу, как он разбивает целую / дробную часть. Это только обрабатывает научную нотацию E99
или что угодно в конце, с повторным умножением или делением на 10.