Заглядываем в double

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

Чтобы понять формат чисел с плавающей запятой, и почему этот формат именно такой, посмотрите мои предыдущие статьи по этой теме.

Этот код использует класс Rational из MicrosoftSolverFoundation; если у вас еще нет этой библиотеки, то вы можете скачать ее отсюда. Там есть много полезных инструментов! В данном случае мне понадобился тип, который может представлять рациональные числа произвольной точности. Создать собственную простую реализацию класса по работе с рациональными числами весьма просто, особенно если для представления числителя и знаменателя в вашем распоряжении есть класс BigInteger. Но зачем изобретать колесо заново?

Поведение приведенного ниже кода очень простое. Я создал структуру, которая превращает 64-разрядное число с плавающей точкой в 64-разрядное беззнаковое целое, поскольку работать с битами проще с целыми числами. Затем, чтобы не засорять основной код, я создал кучу мелких методов расширения, облегчающие битовые операции с рациональными числами. Я не люблю видеть манипуляции с битами в основном коде. Ну, вы понимаете, о чем я?


using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.SolverFoundation.Common;

class Program
{
    static void Main()
    {
        double original = -256.325;
        MyDouble d = original;

        Console.WriteLine("Raw sign: {0}", d.Sign);
        Console.WriteLine("Raw exponent: {0}", d.ExponentBits.Join());
        Console.WriteLine("Raw mantissa: {0}", d.MantissaBits.Join());

        var signchar = d.Sign == 0 ? '+' : '-';

        if (d.Exponent == 0 && d.Mantissa == 0)
        {
            Console.WriteLine("Zero: {0}0", signchar);
            return;
        }
        else if (d.Exponent == 0x7ff && d.Mantissa == 0)
        {
            Console.WriteLine("Infinity: {0}Infinity", signchar);
            return;
        }
        else if (d.Exponent == 0x7ff)
        {
            Console.WriteLine("NaN");
            return;
        }

        bool subnormal = d.Exponent == 0;
        var two = (Rational)2;
        var fraction = subnormal ? Rational.Zero : Rational.One;
        for (int bit = 51; bit >= 0; --bit)
            fraction += d.Mantissa.Bit(bit) * two.Exp(bit - 52);
        fraction = fraction * two.Exp(d.Exponent - 1023);
        if (d.Sign == 1)
            fraction = -fraction;

        Console.WriteLine(subnormal ? "Subnormal" : "Normal");
        Console.WriteLine("Sign: {0}", signchar);
        Console.WriteLine("Exponent: {0}", d.Exponent - 1023);
        Console.WriteLine("Exact binary fraction: {0}.{1}", subnormal ? 0 : 1, d.MantissaBits.Join());
        Console.WriteLine("Nearest approximate decimal: {0}", original);
        Console.WriteLine("Exact rational fraction: {0}", fraction.ToString());
        Console.WriteLine("Exact decimal fraction: {0}", fraction.ToDecimalString());
    }
}

struct MyDouble
{
    private ulong bits;
    public MyDouble(double d)
    {
        this.bits = BitConverter.DoubleToInt64Bits(d);
    }

    public int Sign
    {
        get
        {
            return this.bits.Bit(63);
        }
    }

    public int Exponent
    {
        get
        {
            return (int)this.bits.Bits(62, 52);
        }
    }

    public IEnumerable<int> ExponentBits
    {
        get
        {
            return this.bits.BitSeq(62, 52);
        }
    }

    public ulong Mantissa
    {
        get
        {
            return this.bits.Bits(51, 0);
        }
    }

    public IEnumerable<int> MantissaBits
    {
        get
        {
            return this.bits.BitSeq(51, 0);
        }
    }

    public static implicit operator MyDouble(double d)
    {
        return new MyDouble(d);
    }
}

static class Extensions
{
    public static int Bit(this ulong x, int bit)
    {
        return (int)((x >> bit) & 0x01);
    }

    public static ulong Bits(this ulong x, int high, int low)
    {
        x <<= (63 - high);
        x >>= (low + 63 - high);
        return x;
    }

    public static IEnumerable<int> BitSeq(this ulong x, int high, int low)
    {
        for(int bit = high; bit >= low; --bit)
            yield return x.Bit(bit);
    }

    public static Rational Exp(this Rational x, int y)
    {
        Rational result;
        Rational.Power(x, y, out result);
        return result;
    }

    public static string ToDecimalString(this Rational x)
    {
        var sb = new StringBuilder();
        x.AppendDecimalString(sb, 50000);
        return sb.ToString();
    }

    public static string Join<T>(this IEnumerable<T> seq)
    {
        return string.Concat(seq);
    }
}

Оригинал статьи