4 votes

C++ : Calculer le nombre de valeurs en virgule flottante possibles dans une plage donnée

Je travaille sur une application de cryptographie, en utilisant Crypto++
Comme partie obscure de cette application, je dois déterminer le nombre maximum de valeurs float uniques pouvant exister dans une certaine plage numérique.

De toute évidence, il y a un nombre infini de nombres entre 0 et 1 en réalité - mais tous ne peuvent pas être représentés par une valeur float unique.

J'ai une valeur float minimale et une valeur float maximale.
Je dois déterminer le nombre de valeurs float possibles à l'intérieur de cette plage.

C'est délicat, car les valeurs en virgule flottante sont espacées de plus en plus loin, plus vous vous éloignez de 0.

Par exemple, le nombre de valeurs float possibles entre 0 et 1, est très différent du nombre de valeurs float entre 100,000 et 100,001

Pour mes besoins, je veux que le décompte inclue à la fois les valeurs min et max.
Mais un algorithme produisant un décompte exclusif serait tout aussi utile, car je pourrais simplement ajouter 1 ou 2 si nécessaire.

Préoccupation supplémentaire:
Que se passe-t-il si 0 est inclus dans la plage?
Par exemple, si la valeur minimale est -2.0 et la valeur maximale est positive 2.0, je ne veux pas compter 0 deux fois (une fois pour 0, et à nouveau pour -0).
De plus, quels problèmes pourraient survenir si les valeurs minimale ou maximale sont +/- infini?
(Je vais probablement juste générer une exception si les valeurs minimale ou maximale sont NaN).

uint32_t RangeValueCount ( float fMin , float fMax )
{
    if ( fMin > fMax )
        swap ( fMin , fMax ) ;  // Assurer fMin <= fMax

    // Calculer le nombre de valeurs float possibles entre fMin et fMax.

    return ( *reinterpret_cast < uint32_t* > ( &fMax ) -
             *reinterpret_cast < uint32_t* > ( &fMin ) ) + 1 ;

    // Cet algorithme est évidemment dangereux, suppose IEEE 754
    // Comment devrais-je tenir compte de -0 ou de l'infini?
}

Si ce problème peut être résolu, je présume que la solution serait tout aussi applicable aux valeurs double (et éventuellement aux valeurs long double, mais cela pourrait être un peu plus complexe en raison des valeurs entières sur 80 bits, etc.)

5voto

Eric Postpischil Points 36641

Voici du code qui gère tous les nombres finis. Il attend l'arithmétique IEEE 754. J'ai remplacé ma version précédente par un code plus simple et plus propre. Au lieu de deux implémentations du calcul de distance, celui-ci a deux implémentations de conversion d'un nombre en virgule flottante en son encodage (une en copiant les bits, une en le manipulant mathématiquement). Ensuite, le calcul de la distance est assez simple (les valeurs négatives doivent être ajustées, puis la distance est simplement une soustraction).

#include 
#include 
#include 
#include 
#include 

typedef double Float;       //  Le type en virgule flottante à utiliser.
typedef std::uint64_t UInt; //  Entier non signé de même taille que Float.

/*  Définir une valeur avec seulement le bit de poids fort d'un UInt défini. C'est aussi l'encodage du -0 en virgule flottante.
*/
static constexpr UInt HighBit
    = std::numeric_limits::max() ^ std::numeric_limits::max() >> 1;

//  Retourne l'encodage d'un nombre en virgule flottante en copiant ses bits.
static UInt EncodingBits(Float x)
{
    UInt result;
    std::memcpy(&result, &x, sizeof result);
    return result;
}

//  Retourne l'encodage d'un nombre en virgule flottante en utilisant des mathématiques.
static UInt EncodingMath(Float x)
{
    static constexpr int SignificandBits = std::numeric_limits::digits;
    static constexpr int MinimumExponent = std::numeric_limits::min_exponent;

    //  Encoder le bit de poids fort.
    UInt result = std::signbit(x) ? HighBit : 0;

    //  Si la valeur est zéro, les bits restants sont zéros, nous avons donc fini.
    if (x == 0) return result;

    /*  La bibliothèque C fournit une routine peu connue pour diviser un nombre en virgule flottante en une mantisse et un exposant.
        Notez que cela produit une mantisse normalisée, pas l'encodage réel de la mantisse. Notamment,
        cela amène les mantisses des sous-normaux à au moins 1/2. Nous ajusterons cela ci-dessous. De plus, cette routine normalise à [1/2, 1),
        alors que l'IEEE 754 est généralement exprimée avec [1, 2), mais cela ne nous dérange pas.
    */
    int xe;
    Float xf = std::frexp(std::fabs(x), &xe);

    //  Tester si le nombre est subnormal.
    if (xe < MinimumExponent)
    {
        /*  Pour une valeur subnormale, l'encodage de l'exposant est zéro, donc nous devons
            uniquement insérer les bits de la mantisse. Cela met à l'échelle la mantisse
            pour que son bit de poids faible soit mis à l'endroit de la position 1 puis il l'insère
            dans l'encodage.
        */
        result |= (UInt) std::ldexp(xf, xe - MinimumExponent + SignificandBits);
    }
    else
    {
        /*  Pour une valeur normale, la mantisse est encodée sans son bit de poids fort.
            Donc, nous soustrayons .5 pour supprimer ce bit puis mettons à l'échelle la
            mantisse pour que son bit de poids faible soit mis à l'endroit de la position 1.
        */
        result |= (UInt) std::ldexp(xf - .5, SignificandBits);

        /*  L'exposant est encodé avec un biais de (dans la terminologie C++)
            MinimumExponent - 1. Nous le soustrayons donc pour obtenir l'encodage de l'exposant
            puis le décalons à la position du champ de l'exposant. Ensuite, nous l'insérons dans l'encodage.
        */
        result |= ((UInt) xe - MinimumExponent + 1) << (SignificandBits-1);
    }

    return result;
}

/*  Retourne l'encodage d'un nombre en virgule flottante. Pour l'illustration,
    on obtient l'encodage avec deux méthodes différentes et on compare les résultats.
*/
static UInt Encoding(Float x)
{
    UInt xb = EncodingBits(x);
    UInt xm = EncodingMath(x);

    if (xb != xm)
    {
        std::cerr << "Erreur interne lors de l'encodage de " << x << ".\n";
        std::cerr << "\tEncodingBits dit " << xb << ".\n";
        std::cerr << "\tEncodingMath dit " << xm << ".\n";
        std::exit(EXIT_FAILURE);
    }

    return xb;
}

/*  Retourne la distance de a à b sous forme du nombre de valeurs représentables en
    Float d'une à l'autre. b doit être supérieur ou égal à a. 0 est
    compté une seule fois.
*/
static UInt Distance(Float a, Float b)
{
    UInt ae = Encoding(a);
    UInt be = Encoding(b);

    /*  Pour les valeurs représentées de +0 à l'infini, les points flottants binaires IEEE 754
        sont en ordre croissant et sont consécutifs. Donc on peut simplement soustraire deux encodages pour obtenir le nombre de valeurs représentables
        entre eux (en incluant un point de départ mais pas l'autre).

        Malheureusement, les nombres négatifs ne sont pas consécutifs et ont
        une direction différente. Pour faire face à cela, si le nombre est négatif, nous transformons
        son encodage en soustrayant de l'encodage du -0. Cela nous donne une
        séquence consécutive d'encodages allant du plus grand nombre négatif fini de grandeur au plus grand nombre fini, en ordre croissant
        sauf pour l'enroulement à la valeur UInt maximale.

        Notez que cela mappe également l'encodage du -0 sur 0 (l'encodage du +0),
        donc les deux zéros deviennent un seul point, donc ils sont comptés une seule fois.
    */
    if (HighBit & ae) ae = HighBit - ae;
    if (HighBit & be) be = HighBit - be;

    //  Retourner la distance entre les deux encodages transformés.
    return be - ae;
}

static void Try(Float a, Float b)
{
    std::cout << "[" << a << ", " << b << "] contient "
        << Distance(a,b) + 1 << " valeurs représentables.\n";
}

int main(void)
{
    if (sizeof(Float) != sizeof(UInt))
    {
        std::cerr << "Erreur, UInt doit être un entier non signé de la même taille que Float.\n";
        std::exit(EXIT_FAILURE);
    }

    /*  Préparer quelques valeurs de test : plus petite valeur positive (subnormale), plus grande valeur
        subnormale, plus petite valeur normale.
    */
    Float S1 = std::numeric_limits::denorm_min();
    Float N1 = std::numeric_limits::min();
    Float S2 = N1 - S1;

    //  Test 0 <= a <= b.
    Try( 0,  0);
    Try( 0, S1);
    Try( 0, S2);
    Try( 0, N1);
    Try( 0, 1./3);
    Try(S1, S1);
    Try(S1, S2);
    Try(S1, N1);
    Try(S1, 1./3);
    Try(S2, S2);
    Try(S2, N1);
    Try(S2, 1./3);
    Try(N1, N1);
    Try(N1, 1./3);

    //  Test a <= b <= 0.
    Try(-0., -0.);
    Try(-S1, -0.);
    Try(-S2, -0.);
    Try(-N1, -0.);
    Try(-1./3, -0.);
    Try(-S1, -S1);
    Try(-S2, -S1);
    Try(-N1, -S1);
    Try(-1./3, -S1);
    Try(-S2, -S2);
    Try(-N1, -S2);
    Try(-1./3, -S2);
    Try(-N1, -N1);
    Try(-1./3, -N1);

    //  Test a <= 0 <= b.
    Try(-0., +0.);
    Try(-0., S1);
    Try(-0., S2);
    Try(-0., N1);
    Try(-0., 1./3);
    Try(-S1, +0.);
    Try(-S1, S1);
    Try(-S1, S2);
    Try(-S1, N1);
    Try(-S1, 1./3);
    Try(-S2, +0.);
    Try(-S2, S1);
    Try(-S2, S2);
    Try(-S2, N1);
    Try(-S2, 1./3);
    Try(-N1, +0.);
    Try(-N1, S1);
    Try(-N1, S2);
    Try(-N1, N1);
    Try(-1./3, 1./3);
    Try(-1./3, +0.);
    Try(-1./3, S1);
    Try(-1./3, S2);
    Try(-1./3, N1);
    Try(-1./3, 1./3);

    return 0;
}

0voto

AdvSphere Points 602

C'est délicat, potentiellement vous pourriez essayer d'utiliser std::nexttoward(from_starting, to_end); dans une boucle et compter jusqu'à la fin. Je n'ai pas essayé moi-même et cela prendra beaucoup de temps pour terminer. Si vous le faites, assurez-vous de vérifier les drapeaux d'erreur, voir: http://en.cppreference.com/w/cpp/numeric/math/nextafter

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X