Как вводить и выводить действительные числа на ассемблере

Решаем задачи с вещественными числами на ассемблере с помощью 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

Как сделать ввод и вывод действительных чисел на ассемблере без использования готовых функций?


person Jane    schedule 28.11.2017    source источник
comment
Это вопрос домашнего задания? У нас есть этикет в отношении таких вопросов.   -  person Haem    schedule 28.11.2017
comment
Он и так слишком широкий. Какая конкретно у вас проблема? Можете ли вы сделать это для целых чисел? Обратите внимание, что вы можете просто масштабировать по соответствующей степени 10, чтобы получить плавающую часть, после решения некоторых проблем синтаксического анализа.   -  person Jester    schedule 28.11.2017
comment
Что вы действительно хотите сделать? Полное решение для печати правильно округленных чисел с плавающей запятой непросто, даже в языках более высокого уровня. Если вы просто хотите напечатать числа в ограниченном домене для конкретной ситуации, могут быть более простые решения, такие как печать нескольких цифр с фиксированной точностью.   -  person Eric Postpischil    schedule 29.11.2017
comment
Обновил свой ответ, я забыл, что целая часть float или double может не соответствовать 64-битному целому числу, поэтому вам потенциально может потребоваться несколько шагов для обработки самых больших значений FP, даже без учета дробной части. (И работать с наибольшей цифрой сначала небезопасно из-за округления.)   -  person Peter Cordes    schedule 29.11.2017


Ответы (1)


Это действительно тот же вопрос, что и реализация этих функций/как они работают внутри. Я просто расскажу о вкладе в этот ответ; Я не уверен, какие алгоритмы хороши для float->string.

Функции, предоставляемые ОС, позволяют читать/записывать (печатать) символы по одному или блоками. Интересная / специфичная для FP часть проблемы - это только часть float- > string и string- > float. Все остальное то же самое, что и для чтения/печати целых чисел (различия по модулю в соглашении о вызовах: числа с плавающей запятой обычно возвращаются в регистрах FP).


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

Как только вы узнаете детали алгоритма (с точки зрения просмотра одиночных цифр и выполнения умножения/деления/сложения FP или целочисленных операций с битовым шаблоном FP), вы можете реализовать его на ассемблере для любой платформы, которая вам нравится. По какой-то причине вы использовали инструкцию x87 finit в своем примере.

См. http://www.exploringbinary.com/how-glibc-strtod-works/ для подробного изучения реализации glibc и http://www.exploringbinary.com/how-strtod-works-and-sometimes-doesnt/ для другой широко используемой реализации.

Излагая первую статью, strtod glibc использует целочисленные арифметические операции повышенной точности. Он анализирует входную десятичную строку, чтобы определить целую часть и дробную часть. например 456.833e2 (научная запись) имеет целую часть 45683 и дробную часть 0.3.

Он преобразует обе части в числа с плавающей запятой по отдельности. Целочисленная часть проста, потому что уже есть аппаратная поддержка преобразования целых чисел в числа с плавающей запятой. например x87 fild или SSE2 cvtsi2sd или что-то еще на других архитектурах. Но если целая часть больше, чем максимальное 64-битное целое, это не так просто, и вам нужно преобразовать BigInteger в float/double, что аппаратно не поддерживает.

Обратите внимание, что даже FLT_MAX (одинарная точность) для IEEE binary32 float равно (2 − 2^−23) × 2^127, что просто немного ниже 2^128, поэтому вы можете использовать 128-битное целое число для строки->float, и если это перенос, то правильным результатом float будет +Infinity. FLT_MAX битовый шаблон равен 0x7f7fffff: мантисса все единицы = 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_format_floating-point если вы не знакомы с битовыми шаблонами, которые IEEE754 binary64, также известный как double, использует для представления чисел. Вам не нужно быть нужным, чтобы написать упрощенную реализацию, но это поможет понять float. А с x86 SSE вам нужно знать, где находится бит знака, чтобы реализовать абсолютное значение (ANDPS) или отрицание (XORPS). Самый быстрый способ вычисления абсолютного значения с использованием SSE . Для abs или neg нет специальных инструкций, вы просто используете логические операции для управления битом знака. (Гораздо эффективнее, чем вычитание из нуля.)


Если вас не волнует точность последнего ULP (единица измерения на последнем месте = младший бит мантиссы), вы можете выполнить более простой алгоритм умножения на 10 и добавления, как для строки -> целого числа, а затем масштабировать в степени 10 в конце.

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

Раздельная обработка целой и дробной части позволяет избежать проблемы переполнения.

См. эту реализацию C на codereview.SE для примера очень простого подхода умножения/сложения, который скорее всего переполнится. Я только быстро просмотрел его, но я не вижу, чтобы он разделял целую/дробную часть. Он обрабатывает только научную нотацию E99 или что-то еще в конце, с повторным умножением или делением на 10.

person Peter Cordes    schedule 28.11.2017