Как поплавок конвертируется в научную запись для хранения? - PullRequest
2 голосов
/ 09 октября 2019

http://www.cs.yale.edu/homes/aspnes/pinewiki/C(2f)FloatingPoint.html

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

https://babbage.cs.qc.cuny.edu/IEEE-754/index.xhtml

База всегда равна 2. Таким образом, 8 сохраняется как 1 * 2 ^ 3. 9 сохраняется как 1.001 * 2 ^ 3.

Что такое математический алгоритм для определения мантиссы / значимого и показателя?

Ответы [ 3 ]

2 голосов
/ 09 октября 2019

Вот код C ++ для преобразования десятичной строки в двоичное значение с плавающей точкой. Хотя вопрос помечен буквой C, я предполагаю, что вопрос больше касается алгоритма и вычислений, чем языка программирования.

Класс DecimalToFloat состоит из строки, содержащей исключительно десятичные цифры и десятичную точку (aпериод, большинство один). В своем конструкторе он показывает, как использовать умножение и деление в начальной школе для преобразования числа из десятичного в двоичное. Это демонстрирует фундаментальные понятия с использованием элементарной арифметики. Реальные реализации преобразования десятичных чисел в плавающие в коммерческом программном обеспечении с использованием более быстрых и сложных алгоритмов. Они включают подготовленные таблицы, анализ и доказательства и являются предметом научных работ. Существенной проблемой качественных реализаций преобразования десятичных чисел в двоичные с плавающей запятой является правильное округление. Различный характер степеней от десяти до степеней двух (как положительных, так и отрицательных) затрудняет правильное определение, когда некоторые значения находятся выше или ниже точки, в которой изменяется округление. Обычно, когда мы анализируем что-то вроде 123e300, мы хотим вычислить двоичный результат с плавающей запятой без фактического вычисления 10 300 . Это гораздо более обширный вопрос.

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

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

/*  This code demonstrates conversion of decimal numerals to binary
    floating-point values using the round-to-nearest-ties-to-even rule.

    Infinities and subnormal values are supported and assumed.

    The basic idea is to convert the decimal numeral to binary using methods
    taught in elementary school.  The integer digits are repeatedly divided by
    two to extract a string of bits in low-to-high position-value order.  Then
    sub-integer digits are repeatedly multiplied by two to continue extracting
    a string of bits in high-to-low position-value order.  Once we have enough
    bits to determine the rounding direction or the processing exhausts the
    input, the final value is computed.

    This code is not (and will not be) designed to be efficient.  It
    demonstrates the fundamental mathematics and rounding decisions.
*/


#include <algorithm>
#include <limits>
#include <cmath>
#include <cstring>


template<typename Float> class DecimalToFloat
{
private:

    static_assert(std::numeric_limits<Float>::radix == 2,
        "This code requires the floatng-point radix to be two.");

    //  Abbreviations for parameters describing the floating-point format.
    static const int Digits          = std::numeric_limits<Float>::digits;
    static const int MaximumExponent = std::numeric_limits<Float>::max_exponent;
    static const int MinimumExponent = std::numeric_limits<Float>::min_exponent;

    /*  For any rounding rule supported by IEEE 754 for binary floating-point,
        the direction in which a floating-point result should be rounded is
        completely determined by the bit in the position of the least
        significant bit (LSB) of the significand and whether the value of the
        trailing bits are zero, between zero and 1/2 the value of the LSB,
        exactly 1/2 the LSB, or between 1/2 the LSB and 1.

        In particular, for round-to-nearest, ties-to-even, the decision is:

            LSB     Trailing Bits   Direction
            0       0               Down
            0       In (0, 1/2)     Down
            0       1/2             Down
            0       In (1/2, 1)     Up
            1       0               Down
            1       In (0, 1/2)     Down
            1       1/2             Up
            1       In (1/2, 1)     Up

        To determine whether the value of the trailing bits is 0, in (0, 1/2),
        1/2, or in (1/2, 1), it suffices to know the first of the trailing bits
        and whether the remaining bits are zeros or not:

            First   Remaining       Value of Trailing Bits
            0       All zeros       0
            0       Not all zeros   In (0, 1/2)
            1       All zeros       1/2
            1       Not all zeros   In (1/2, 1)

        To capture that information, we maintain two bits in addition to the
        bits in the significand.  The first is called the Round bit.  It is the
        first bit after the position of the least significand bit in the
        significand.  The second is called the Sticky bit.  It is set if any
        trailing bit after the first is set.

        The bits for the significand are kept in an array along with the Round
        bit and the Sticky bit.  The constants below provide array indices for
        locating the LSB, the Round Bit, and the Sticky bit in that array.
    */
    static const int LowBit = Digits-1; //  Array index for LSB in significand.
    static const int Round  = Digits;   //  Array index for rounding bit.
    static const int Sticky = Digits+1; //  Array index for sticky bit.

    char *Decimal;          //  Work space for the incoming decimal numeral.

    int  N;                 //  Number of bits incorporated so far.
    char Bits[Digits+2];    //  Bits for significand plus two for rounding.
    int  Exponent;          //  Exponent adjustment needed.


    /*  PushBitHigh inserts a new bit into the high end of the bits we are
        accumulating for the significand of a floating-point number.

        First, the Round bit shifted down by incorporating it into the Sticky
        bit, using an OR so that the Sticky bit is set iff any bit pushed below
        the Round bit is set.

        Then all bits from the significand are shifted down one position,
        which moves the least significant bit into the Round position and
        frees up the most significant bit.

        Then the new bit is put into the most significant bit.
    */
    void PushBitHigh(char Bit)
    {
        Bits[Sticky] |= Bits[Round];
        std::memmove(Bits+1, Bits, Digits * sizeof *Bits);
        Bits[0] = Bit;

        ++N;        //  Count the number of bits we have put in the significand.
        ++Exponent; //  Track the absolute position of the leading bit.
    }


    /*  PushBitLow inserts a new bit into the low end of the bits we are
        accumulating for the significand of a floating-point number.

        If we have no previous bits and the new bit is zero, we are just
        processing leading zeros in a number less than 1.  These zeros are not
        significant.  They tell us the magnitude of the number.  We use them
        only to track the exponent that records the position of the leading
        significant bit.  (However, exponent is only allowed to get as small as
        MinimumExponent, after which we must put further bits into the
        significand, forming a subnormal value.)

        If the bit is significant, we record it.  If we have not yet filled the
        regular significand and the Round bit, the new bit is recorded in the
        next space.  Otherwise, the new bit is incorporated into the Sticky bit
        using an OR so that the Sticky bit is set iff any bit below the Round
        bit is set.
    */
    void PushBitLow(char Bit)
    {
        if (N == 0 && Bit == 0 && MinimumExponent < Exponent)
            --Exponent;
        else
            if (N < Sticky)
                Bits[N++] = Bit;
            else
                Bits[Sticky] |= Bit;
    }


    /*  Determined tells us whether the final value to be produced can be
        determined without any more low bits.  This is true if and only if:

            we have all the bits to fill the significand, and

            we have at least one more bit to help determine the rounding, and

            either we know we will round down because the Round bit is 0 or we
            know we will round up because the Round bit is 1 and at least one
            further bit is 1 or the least significant bit is 1.
    */
    bool Determined() const
    {
        if (Digits < N)
            if (Bits[Round])
                return Bits[LowBit] || Bits[Sticky];
            else
                return 1;
        else
            return 0;
    }


    //  Get the floating-point value that was parsed from the source numeral.
    Float GetValue() const
    {
        //  Decide whether to round up or not.
        bool RoundUp = Bits[Round] && (Bits[LowBit] || Bits[Sticky]);

        /*  Now we prepare a floating-point number that contains a significand
            with the bits we received plus, if we are rounding up, one added to
            the least significant bit.
        */

        //  Start with the adjustment to the LSB for rounding.
        Float x = RoundUp;

        //  Add the significand bits we received.
        for (int i = Digits-1; 0 <= i; --i)
            x = (x + Bits[i]) / 2;

        /*  If we rounded up, the addition may have carried out of the
            initial significand.  In this case, adjust the scale.
        */
        int e = Exponent;
        if (1 <= x)
        {
            x /= 2;
            ++e;
        }

        //  Apply the exponent and return the value.
        return MaximumExponent < e ? INFINITY : std::scalbn(x, e);
    }


public:

    /*  Constructor.

        Note that this constructor allocates work space.  It is bad form to
        allocate in a constructor, but this code is just to demonstrate the
        mathematics, not to provide a conversion for use in production
        software.
    */
    DecimalToFloat(const char *Source) : N(), Bits(), Exponent()
    {
        //  Skip leading sources.
        while (*Source == '0')
            ++Source;

        size_t s = std::strlen(Source);

        /*  Count the number of integer digits (digits before the decimal
            point if it is present or before the end of the string otherwise)
            and calculate the number of digits after the decimal point, if any.
        */
        size_t DigitsBefore = 0;
        while (Source[DigitsBefore] != '.' && Source[DigitsBefore] != 0)
            ++DigitsBefore;

        size_t DigitsAfter = Source[DigitsBefore] == '.' ? s-DigitsBefore-1 : 0;

        /*  Allocate space for the integer digits or the sub-integer digits,
            whichever is more numerous.
        */
        Decimal = new char[std::max(DigitsBefore, DigitsAfter)];

        /*  Copy the integer digits into our work space, converting them from
            digit characters ('0' to '9') to numbers (0 to 9).
        */
        for (size_t i = 0; i < DigitsBefore; ++i)
            Decimal[i] = Source[i] - '0';

        /*  Convert the integer portion of the numeral to binary by repeatedly
            dividing it by two.  The remainders form a bit string representing
            a binary numeral for the integer part of the number.  They arrive
            in order from low position value to high position value.

            This conversion continues until the numeral is exhausted (High <
            Low is false) or we see it is so large the result overflows
            (Exponent <= MaximumExponent is false).

            Note that Exponent may exceed MaximumExponent while we have only
            produced 0 bits during the conversion.  However, because we skipped
            leading zeros above, we know there is a 1 bit coming.  That,
            combined with the excessive Exponent, guarantees the result will
            overflow.
        */

        for (char *High = Decimal, *Low = Decimal + DigitsBefore;
            High < Low && Exponent <= MaximumExponent;)
        {
            //  Divide by two.
            char Remainder = 0;
            for (char *p = High; p < Low; ++p)
            {
                /*  This is elementary school division:  We bring in the
                    remainder from the higher digit position and divide by the
                    divisor.  The remainder is kept for the next position, and
                    the quotient becomes the new digit in this position.
                */
                char n = *p + 10*Remainder;
                Remainder = n % 2;
                n /= 2;

                /*  As the number becomes smaller, we discard leading zeros:
                    If the new digit is zero and is in the highest position,
                    we discard it and shorten the number we are working with.
                    Otherwise, we record the new digit.
                */
                if (n == 0 && p == High)
                    ++High;
                else
                    *p = n;
            }

            //  Push remainder into high end of the bits we are accumulating.
            PushBitHigh(Remainder);
        }

        /*  Copy the sub-integer digits into our work space, converting them
            from digit characters ('0' to '9') to numbers (0 to 9).

            The convert the sub-integer portion of the numeral to binary by
            repeatedly multiplying it by two.  The carry-outs continue the bit
            string.  They arrive in order from high position value to low
            position value.
        */

        for (size_t i = 0; i < DigitsAfter; ++i)
            Decimal[i] = Source[DigitsBefore + 1 + i] - '0';

        for (char *High = Decimal, *Low = Decimal + DigitsAfter;
            High < Low && !Determined();)
        {
            //  Multiply by two.
            char Carry = 0;
            for (char *p = Low; High < p--;)
            {
                /*  This is elementary school multiplication:  We multiply
                    the digit by the multiplicand and add the carry.  The
                    result is separated into a single digit (n % 10) and a
                    carry (n / 10).
                */
                char n = *p * 2 + Carry;
                Carry = n / 10;
                n %= 10;

                /*  Here we discard trailing zeros:  If the new digit is zero
                    and is in the lowest position, we discard it and shorten
                    the numeral we are working with.  Otherwise, we record the
                    new digit.
                */
                if (n == 0 && p == Low-1)
                    --Low;
                else
                    *p = n;
            }

            //  Push carry into low end of the bits we are accumulating.
            PushBitLow(Carry);
        }

        delete [] Decimal;
    }

    //  Conversion operator.  Returns a Float converted from this object.
    operator Float() const { return GetValue(); }
};


#include <iostream>
#include <cstdio>
#include <cstdlib>


static void Test(const char *Source)
{
    std::cout << "Testing " << Source << ":\n";

    DecimalToFloat<float> x(Source);

    char *end;
    float e = std::strtof(Source, &end);
    float o = x;

    /*  Note:  The C printf is used here for the %a conversion, which shows the
        bits of floating-point values clearly.  If your C++ implementation does
        not support this, this may be replaced by any display of floating-point
        values you desire, such as printing them with all the decimal digits
        needed to distinguish the values.
    */
    std::printf("\t%a, %a.\n", e, o);

    if (e != o)
    {
        std::cout << "\tError, results do not match.\n";
        std::exit(EXIT_FAILURE);
    }
}


int main(void)
{
    Test("0");
    Test("1");
    Test("2");
    Test("3");
    Test(".25");
    Test(".0625");
    Test(".1");
    Test(".2");
    Test(".3");
    Test("3.14");
    Test(".00000001");
    Test("9841234012398123");
    Test("340282346638528859811704183484516925440");
    Test("340282356779733661637539395458142568447");
    Test("340282356779733661637539395458142568448");
    Test(".00000000000000000000000000000000000000000000140129846432481707092372958328991613128026194187651577175706828388979108268586060148663818836212158203125");

    //  This should round to the minimum positive (subnormal), as it is just above mid-way.
    Test(".000000000000000000000000000000000000000000000700649232162408535461864791644958065640130970938257885878534141944895541342930300743319094181060791015626");

    //  This should round to zero, as it is mid-way, and the even rule applies.
    Test(".000000000000000000000000000000000000000000000700649232162408535461864791644958065640130970938257885878534141944895541342930300743319094181060791015625");

    //  This should round to zero, as it is just below mid-way.
    Test(".000000000000000000000000000000000000000000000700649232162408535461864791644958065640130970938257885878534141944895541342930300743319094181060791015624");
}
1 голос
/ 10 октября 2019

Одна из удивительных вещей о реальном, практичном компьютере - удивляющем начинающих программистов, которым в любом случае была поставлена ​​задача написать искусственные маленькие программы преобразования двоичных чисел в десятичные, - насколько глубоко укоренилась система двоичных чисел вфактический компьютер, и как мало и насколько распространены любые фактические процедуры двоичного / десятичного преобразованияНапример, в мире Си (и если мы ограничим наше внимание целыми числами на данный момент), существует, по сути, одна подпрограмма преобразования двоичной системы в десятичную, которая находится внутри printf, где обрабатывается директива %d. Существует, возможно, три десятичных в двоичные преобразователи: atof(), strtol() и %d преобразование внутри scanf. (Там может быть еще один внутри компилятора C, где он преобразует ваши десятичные константы в двоичные, хотя для них компилятор может просто вызвать strtol() напрямую.)

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

По сути, я собираюсь написать версию стандартной функции библиотеки atof(). Вот мои основные правила:

  1. Я собираюсь предположить, что ввод представляет собой строку символов. (Это вовсе не предположение; это переформулировка исходной проблемы, которая заключается в написании версии atof.)
  2. Я собираюсь предположить, что мы можем построить плавающуюномер точки "0.0". (В IEEE 754 и большинстве других форматов это все биты 0, так что это не так уж сложно.)
  3. Я собираюсь предположить, что мы можем преобразовать целые числа 0-9 в соответствующие им числа с плавающей запятойэквиваленты.
  4. Я собираюсь предположить, что мы можем добавлять и умножать любые числа с плавающей запятой, которые мы хотим. (Это важная вещь, хотя я опишу эти алгоритмы позже.) Но на любом современном компьютере почти наверняка есть модуль с плавающей запятой, который имеет встроенные инструкции для основных операций с плавающей запятой, таких как сложение и умножение, поэтомуэто также не является необоснованным предположением. (Но в конечном итоге он скрывает некоторые интересные аспекты алгоритма, передавая разработчику аппаратного обеспечения правильное выполнение инструкций.)
  5. Сначала я предполагаю, что у нас есть доступ кстандартные функции библиотеки atoi и pow. Это довольно большое предположение, но опять же, позже я опишу, как мы могли бы написать их с нуля, если бы захотели. Я также собираюсь предположить существование функций классификации символов в <ctype.h>, особенно isdigit().

Но это все. Оказывается, что с этими предпосылками мы можем сами написать полностью функциональную версию atof(). Это может быть не быстро, и почти наверняка у него не будет правильного поведения округления по краям, но оно будет работать довольно хорошо. (Я даже собираюсь обрабатывать отрицательные числа и показатели степени.) Вот как это работает:

  • пропустить начальные пробелы
  • искать '-'
  • сканированиеЦифровые символы, преобразующие каждый из них в соответствующую цифру путем вычитания '0' (он же ASCII 48)
  • накапливает число с плавающей запятой (без дробной части), представляющее целое число, подразумеваемое цифрами - значимое и - и это настоящая математика, умножив текущее накопление на 10 и добавив следующую цифру
  • , если мы увидим десятичную точку, посчитаем количество цифр после нее
  • когда мы закончим сканирование цифр, посмотрите, есть ли e / E и еще несколько цифр, указывающих показатель степени
  • , если необходимо, умножьте или разделите наше накопленное число на степень 10,заботиться о цифрах после десятичной дроби и / или явном показателе степени.

Вот код:

#include <ctype.h>
#include <stdlib.h>      /* just for atoi() */
#include <math.h>        /* just for pow() */

#define TRUE 1
#define FALSE 0

double my_atof(const char *str)
{
    const char *p;
    double ret;
    int negflag = FALSE;
    int exp;
    int expflag;

    p = str;

    while(isspace(*p))
        p++;

    if(*p == '-')
        {
        negflag = TRUE;
        p++;
        }

    ret = 0.0;              /* assumption 2 */
    exp = 0;
    expflag = FALSE;

    while(TRUE)
        {
        if(*p == '.')
            expflag = TRUE;
        else if(isdigit(*p))
            {
            int idig = *p - '0';     /* assumption 1 */
            double fdig = idig;      /* assumption 3 */
            ret = 10. * ret + fdig;  /* assumption 4 */
            if(expflag)
                exp--;
            }
        else    break;

        p++;
        }

    if(*p == 'e' || *p == 'E')
        exp += atoi(p+1);   /* assumption 5a */

    if(exp != 0)
        ret *= pow(10., exp);   /* assumption 5b */

    if(negflag)
        ret = -ret;

    return ret;
}

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

#include <stdio.h>

int main(int argc, char *argv[])
{
    double d = my_atof(argv[1]);
    printf("%s -> %g\n", argv[1], d);
}

(Если вам или вашей IDE не нравятся вызовы командной строки, вы можете использовать fgets или scanf для чтения строкивместо этого my_atof.

Но, я знаю, ваш вопрос был «Как конвертировать 9 в 1.001 * 2 ^ 3?», и я до сих пор не ответил на это,Я? Итак, давайте посмотрим, сможем ли мы найти, где это произойдет.

Прежде всего, эта битовая комбинация 1001 2 для 9 пришла из ... ниоткуда или везде, или она была там все время, или что-то. Вошел символ 9, вероятно, с битовой комбинацией 111001 2 (в ASCII). Мы вычли 48 = 110000 2 , и выскочили 1001 2 . (Даже перед выполнением вычитания вы можете увидеть, как он там прятался в конце 111001.)

Но что же превратило 1001 в 1.001E3? По сути, это было мое «предположение 3», заключенное в строке

double fdig = idig;

. Такую строку легко написать на C, поэтому нам не нужно знать, как это делается, и компилятор, вероятно, переворачиваетона превращается в инструкцию 'convert integer to float', поэтому разработчику компилятора также не нужно знать, как это сделать.

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

Поскольку 9 - однозначное число, мы закончили. Но для многозначного числа нашей следующей задачей является арифметика, которую мы должны сделать: умножить текущую сумму на 10 и добавить следующую цифру. Как это работает, точно?

Опять же, если мы пишем программу на C (или даже на ассемблере), нам не нужно знать, потому что наша машина с плавающей точкой добавляет и добавляет«Умножить» инструкции сделают все для нас. Но, опять же, если бы нам пришлось делать это самим, мы могли бы. (Этот ответ становится слишком длинным, поэтому я пока не собираюсь обсуждать алгоритмы сложения и умножения с плавающей запятой. Может быть, еще дальше.)

Наконец, представленный код «обманул», вызвавфункции библиотеки atoi и pow. У меня не будет проблем с тем, чтобы убедить вас в том, что мы могли бы реализовать atoi сами, если бы захотели / должны были: это в основном тот же код накопления цифр, который мы уже написали. И pow тоже не сложно, потому что в нашем случае нам не нужно реализовывать его в полной общности: мы всегда возводим в целочисленные степени, так что это прямое повторное умножение, и мы уже предположили, чтоуметь умножать.

(С учетом сказанного, вычисление большой степени 10 как части нашего десятичного-двоичного алгоритма является проблематичным. Как заметил @Eric Postpischil в своем ответе: «Обычно мы хотим вычислить двоичный результат с плавающей запятой безна самом деле вычисление 10 N . "Я, так как я не знаю ничего лучше, я все равно вычислю его, но если бы я написал свой pow(), я бы использовал двоичное возведение в степень *Алгоритм 1109 *, поскольку он очень прост в реализации и довольно приятен.)

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

Умножение: умножение с плавающей точкой на самом деле несколько проще, чем сложение. Вам не нужно беспокоиться о сопоставлении показателей: конечный продукт - это, в основном, новое число, значение которого является произведением двух значений, а показатель степени является суммой двух показателей. Единственная хитрость заключается в том, что произведение двух М-битных значений - это номинально 2 Мбит, и у вас может не быть множителя, который мог бы это сделать. Если единственный имеющийся у вас множитель достигает максимума в M-битном продукте, вы можете взять два M-битных значения и буквально разделить их пополам на биты:

signif1 = a * 2 M / 2 + b
signif2 = c * 2 M / 2 + d

Итак, по обычной алгебре имеем

signif1 × signif2 = ac × 2 M + ad × 2 M / 2 + bc × 2 M / 2 + bd

Каждый из этих частичных произведений ac, ad и т. Д. Является M-разрядным произведением. Умножение на 2 M / 2 или 2 M легко, потому что это просто сдвиг влево. И добавление терминов - это то, что мы уже знаем, как сделать. На самом деле мы заботимся только о верхних M битах продукта, поэтому, так как мы собираемся отбросить остальные, я думаю, мы могли бы обмануть и пропустить термин bd , так как он ничего не дает (хотя это можетв конечном итоге немного влияет на правильно округленный результат).

Но в любом случае, детали алгоритмов сложения и умножения, а также знания, которые они содержат о представлении с плавающей запятой, которое мы используем, в конечном итоге образуют другиеполовина ответа на вопрос о десятичном-двоичном «алгоритме», который вы ищете. Если вы преобразуете, скажем, число 5.703125, используя код, который я показал, из него выскочит двоичное число с плавающей точкой 1.01101101 2 × 2 2 , но мы нигде явно не вычислилиэто означает, и 1.01101101, или этот показатель 2 - они оба выпали из всех произведенных нами умножений и сложений в цифровом виде.

Наконец, если вы все еще со мной, вот быстрый и простой метод целочисленного питанияpow функция с использованием двоичного возведения в степень:

double my_pow(double a, unsigned int b)
{
    double ret = 1;
    double fac = a;

    while(1) {
        if(b & 1) ret *= fac;
        b >>= 1;
        if(b == 0) break;
        fac *= fac;
    }
    return ret;
}

Это изящный маленький алгоритм. Если мы попросим его вычислить, скажем, 10 21 , он не умножит 10 на себя 21 раз. Вместо этого он многократно квадратов 10, что приводит к экспоненциальной последовательности 10 1 , 10 2 , 10 4 , 10 8, точнее, 10, 100, 10000, 100000000 ... Затем он просматривает двоичное представление 21, а именно 10101, и выбирает только промежуточные результаты 10 1 , 10 4 и 10 16 для умножения на его окончательное возвращаемое значение, получая 10 1 + 4 + 16 или 10 21 , по желанию. Поэтому он запускается во времени O (log 2 (N)), а не O (N).


И настройтесь на завтра для нашего следующего захватывающего эпизода, когда мы пойдемв обратном направлении, написание двоично-десятичного преобразователя, который потребует от нас сделать ... (зловещий аккорд)
длинное деление с плавающей запятой !

0 голосов
/ 16 октября 2019

Вот совершенно другой ответ, который пытается сосредоточиться на «алгоритмической» части вопроса. Я начну с примера, о котором вы спрашивали, преобразовав десятичное целое число 9 в двоичное число научной нотации 1.001 2 × 2 3 . Алгоритм состоит из двух частей: (1) преобразовать десятичное целое число 9 в двоичное целое число 1001 2 и (2) преобразовать это двоичное целое число в двоичноенаучная запись.

Шаг 1. Преобразование десятичного целого числа в двоичное целое число. (Вы можете пропустить эту часть, если вы уже знаете это. Кроме того, хотя эта часть алгоритма будет выглядеть идеально, оказывается, что это не та вещь, которая фактически используется где-либо на практическом двоичном компьютере.)

Алгоритм построен на основе числа, над которым мы работаем, n и двоичного числа, которое мы создаем, b .

  1. Установите n изначально на число, которое мы конвертируем, 9 .
  2. Установите b на 0.
  3. Вычислить остаток при делении n на 2. В нашем примере остаток от 9 ÷ 2 равен 1.
  4. Остаток составляет один бит нашего двоичного числа. Прикрепите его к b . В нашем примере b теперь равно 1 . Кроме того, здесь мы собираемся добавить биты к b на влево .
  5. Делим n на 2 (отбрасывая остаток). В нашем примере n теперь равно 4.
  6. Если n теперь равно 0, все готово.
  7. Вернитесь к шагу 3.

В конце первого прохода по алгоритму n равно 4, а b равно 1.

При следующем отключении цикла извлекается бит0 (потому что 4, деленное на 2 - это 2, остаток 0). Таким образом, b переходит в 01, а n переходит в 2.

При следующем отключении в цикле будет извлечен бит 0 (поскольку 2, деленное на 2, равно 1,остаток 0). Итак, b переходит в 001, а n переходит в 1.

При следующем отключении в цикле будет извлечен бит 1 (поскольку 1, деленное на 2, равно 0,остаток 1). Итак, b переходит к 1001, а n переходит к 0.

И так как n теперь равно 0, мы закончили. Между тем, мы построили двоичное число 1001 в b , по желанию.

Вот этот пример снова в табличной форме. На каждом шаге мы вычисляем n , деленное на два (или в C, n/2), и остаток при делении n на 2, который в C равен n%2. На следующем шаге n заменяется на n/2, а следующий бит (который n%2) добавляется слева от b .

step       n       b     n/2     n%2
   0       9       0       4       1
   1       4       1       2       0
   2       2      01       1       0
   3       1     001       0       1
   4       0    1001

Давайте снова пройдемся по этому номеру 25:

step       n       b     n/2     n%2
   0      25       0      12       1
   1      12       1       6       0
   2       6      01       3       0
   3       3     001       1       1
   4       1    1001       0       1
   5       0   11001

Вы можете ясно видеть, что столбец n управляется столбцом n/2, потому что на шаге5 алгоритма, как указано, мы разделили n на 2. (В C это будет n = n / 2, или n /= 2.) Вы можете ясно видеть появление двоичного результата (в порядке справа налево)) в столбце n%2.

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

Шаг 2. Преобразование двоичного целого числа в двоичное число в научной нотации.

Прежде чем мы начнем с этой половины алгоритма, важнопонимать, что научные (или «экспоненциальные») представления обычно не являются уникальными. Вернемся к десятичному моменту, давайте подумаем о числе «одна тысяча». Чаще всего мы будем представлять это как 1 × 10 3 . Но мы могли бы также представить это как 10 × 10 2 , или 100 × 10 1 , или даже более сумасшедшие представления, такие как 10000 × 10 -1 , или 0,01 ×10 5 .

Итак, на практике, когда мы работаем в научной нотации, мы обычно устанавливаем дополнительное правило или указание, утверждая, что мы попытаемся удержать мантиссу (также называемую «значимым») в пределах определеннойассортимент. Для базы 10 обычно целью является либо удержание ее в диапазоне 0 ≤ мантисса <10, либо 0 ≤ мантисса <1. То есть нам нравятся числа типа 1 × 10 <sup>3 или 0,1 × 10 4 , но нам не нравятся числа типа 100 × 10 1 или 0,01 × 10 5 .

Как сохранить наши представления вдиапазон нам нравится? Что если у нас есть число (возможно, промежуточный результат вычисления) в форме, которая нам не нравится? Ответ прост, и он зависит от модели, которую вы, вероятно, уже заметили: если вы умножите мантиссу на 10 и одновременно вычтете 1 из показателя степени, вы не изменили значение числа. Точно так же вы можете разделить мантиссу на 10 и увеличить показатель степени, опять же, ничего не меняя.

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

Еще одна вещь: поскольку 10 0 равно 1, мы можем предварительно преобразовать любое целое число в научную запись, просто умножив его на 10 0 . То есть 9 - это 9 × 10 0 , а 25 - 25 × 10 0 . Если мы делаем это таким образом, мы обычно получаем число в форме, которая нам «не нравится» (то есть «ненормализована»), но теперь у нас есть идея, как это исправить.

Итак, давайте вернемся к базе 2 и остальной части этой второй половины нашего алгоритма. Все, что мы сказали до сих пор о десятичной научной нотации, также верно и в отношении двоичной научной нотации, если мы вносим очевидные изменения «10» в «2».

Чтобы преобразовать двоичное целое число 1001 2 в двоичную научную запись, мы сначала умножаем ее на 2 0 , что дает: 1001 2 × 2 0 . Так что на самом деле мы почти закончили, за исключением того, что это число ненормализовано.

Каково наше определение нормализованного числа научной нотации с основанием два? Мы не сказали, но обычно требуется, чтобы мантисса находилась между 0 и 10 2 (то есть между 0 и 2 10 ), или указывалось иначе, чтостарший бит мантиссы всегда равен 1 (если только целое число не равно 0). То есть эти мантиссы нормированы: 1,001 2 , 1,1 2 , 1,0 2 , 0,0 2 . Эти мантиссы ненормализованы: 10.01 2 , 0.001 2 .

Таким образом, чтобы нормализовать число, нам может понадобиться умножить или разделить мантиссу на 2, увеличивая или увеличиваяуменьшение экспоненты.

Собираем все это вместе в пошаговой форме: чтобы преобразовать двоичное целое число в двоичное научное число:

  1. Умножьте целое число на 2 0 : установите для мантиссы число, которое мы конвертируем, а для показателя степени - 0.
  2. Если число нормализовано (если мантисса равна 0 или если ее ведущий бит равен 1), мыготово.
  3. Если у мантиссы больше одного бита слева от десятичной запятой (на самом деле это «осевая точка» или «двоичная точка»), разделите мантиссу на 2 и увеличьте экспоненту на1. Вернитесь к шагу 2.
  4. (Этот шаг никогда не понадобится, если число, с которого мы начали, было целым числом.) Если мантисса отлична от нуля, но бит слева от точки радиуса равен 0, умножьтемантисса на 2, и уменьшить показатель степенина 1. Вернитесь к шагу 2.

Запустив этот алгоритм в табличной форме для нашего числа 9, мы получим:

step  mantissa  exponent
   0     1001.         0
   1     100.1         1
   2     10.01         2
   3     1.001         3

Итак, если вы все еще со мной,Вот как мы можем преобразовать десятичное целое число 9 в двоичное число научной нотации (или с плавающей запятой) 1.001 2 × 2 3 .

И, с учетом всего вышесказанного, алгоритм, как указано выше, работает только для десятичных целых чисел . Что, если бы мы хотели преобразовать, скажем, десятичное число 1.25 в двоичное число 1.01 2 × 2 0 или 34.125 в 1.00010001 2 × 2 5 ? Это обсуждение, которое придется подождать еще один день (или этот другой ответ ), я думаю.

...