90 votes

Fonctions de comparaison en virgule flottante pour C#

Quelqu'un peut-il indiquer (ou montrer) de bonnes fonctions générales de comparaison de valeurs en virgule flottante en C# ? Je veux implémenter des fonctions pour IsEqual , IsGreater un IsLess . De plus, je ne m'intéresse vraiment qu'aux doubles et non aux flottants.

1 votes

Quelle est votre question, exactement ?

0 votes

Quelqu'un peut-il indiquer (ou montrer) de bonnes fonctions générales de comparaison de valeurs en virgule flottante en C# pour comparer des valeurs en virgule flottante ? Le problème est que beaucoup de personnes ont montré des réponses partielles. Je suis à la recherche de quelque chose de plus complet.

2 votes

C'est dangereux, cela prétend qu'il y a un résultat significatif alors que les chiffres sont insignifiants. Faites attention au post de Philip.

83voto

Michael Borgwardt Points 181658

Écriture d'une virgule flottante générale utile IsEqual est très, très difficile, voire carrément impossible. Votre code actuel échouera gravement pour a==0 . La façon dont la méthode doit se comporter dans de tels cas est vraiment une question de définition, et on peut dire que le code serait mieux adapté au cas d'utilisation spécifique du domaine.

Pour ce genre de choses, vous vraiment, vraiment besoin une bonne suite de tests. C'est ainsi que je l'ai fait pour Le guide de la virgule flottante Voici ce que j'ai obtenu à la fin (code Java, qui devrait être assez facile à traduire) :

public static boolean nearlyEqual(float a, float b, float epsilon) {
    final float absA = Math.abs(a);
    final float absB = Math.abs(b);
    final float diff = Math.abs(a - b);

    if (a == b) { // shortcut, handles infinities
        return true;
    } else if (a == 0 || b == 0 || absA + absB < Float.MIN_NORMAL) {
        // a or b is zero or both are extremely close to it
        // relative error is less meaningful here
        return diff < (epsilon * Float.MIN_NORMAL);
    } else { // use relative error
        return diff / (absA + absB) < epsilon;
    }
}

Vous pouvez également trouver la suite de tests sur le site .

Annexe : Même code en c# pour les doubles (comme demandé dans les questions)

public static bool NearlyEqual(double a, double b, double epsilon)
{
    const double MinNormal = 2.2250738585072014E-308d;
    double absA = Math.Abs(a);
    double absB = Math.Abs(b);
    double diff = Math.Abs(a - b);

    if (a.Equals(b))
    { // shortcut, handles infinities
        return true;
    } 
    else if (a == 0 || b == 0 || absA + absB < MinNormal) 
    {
        // a or b is zero or both are extremely close to it
        // relative error is less meaningful here
        return diff < (epsilon * MinNormal);
    }
    else
    { // use relative error
        return diff / (absA + absB) < epsilon;
    }
}

0 votes

Merci. Ce n'est pas mon code en fait, juste quelque chose que j'ai trouvé et je pouvais voir des problèmes avec lui et j'espérais que cela inciterait quelqu'un à répondre à ma question. Avez-vous des idées ou des exemples pour les fonctions IsLess et IsGreater ?

0 votes

@Kevin : non, mais il n'est pas si difficile de trouver des cas de test soi-même. C'est un exemple presque parfait pour les tests unitaires et l'approche "test-first" : tout changement qui fait fonctionner un cas a une très forte chance d'en casser un autre. Sans une suite de tests, il est presque impossible de faire fonctionner tous les cas en même temps.

0 votes

@MichaelBorgwardt Guide fantastique ! Pour vos tests unitaires, je vois que vous utilisez la version par défaut de l'interface. epsilon de 0.000001f . Que recommanderiez-vous pour effectuer les mêmes tests unitaires avec une double précision ?

30voto

Andrew Wang Points 251

De L'article de Bruce Dawson sur la comparaison des flotteurs vous pouvez également comparer des flottants comme des entiers. La proximité est déterminée par les bits les moins significatifs.

public static bool AlmostEqual2sComplement( float a, float b, int maxDeltaBits ) 
{
    int aInt = BitConverter.ToInt32( BitConverter.GetBytes( a ), 0 );
    if ( aInt <  0 )
        aInt = Int32.MinValue - aInt;  // Int32.MinValue = 0x80000000

    int bInt = BitConverter.ToInt32( BitConverter.GetBytes( b ), 0 );
    if ( bInt < 0 )
        bInt = Int32.MinValue - bInt;

    int intDiff = Math.Abs( aInt - bInt );
    return intDiff <= ( 1 << maxDeltaBits );
}

EDIT : BitConverter est relativement lent. Si vous êtes prêt à utiliser du code non sécurisé, voici une version très rapide :

    public static unsafe int FloatToInt32Bits( float f )
    {
        return *( (int*)&f );
    }

    public static bool AlmostEqual2sComplement( float a, float b, int maxDeltaBits )
    {
        int aInt = FloatToInt32Bits( a );
        if ( aInt < 0 )
            aInt = Int32.MinValue - aInt;

        int bInt = FloatToInt32Bits( b );
        if ( bInt < 0 )
            bInt = Int32.MinValue - bInt;

        int intDiff = Math.Abs( aInt - bInt );
        return intDiff <= ( 1 << maxDeltaBits );
    }

1 votes

Intéressant. Je suis tombé sur quelques références qui semblent dire que c'est peut-être la meilleure façon de procéder (en comparant comme un type entier). Michael Borgwardt ci-dessus renvoie également à l'article de Dawson. Je me demande si la conversion des bits est très coûteuse ?

0 votes

BitConverter est lent. J'ai ajouté une version beaucoup plus rapide, mais elle utilise du code non sécurisé.

0 votes

Merci, je vais en tenir compte et cela sera utile à d'autres personnes qui trouvent cette question.

12voto

Simon Hewitt Points 585

Suite à la réponse d'Andrew Wang : si la méthode BitConverter est trop lente mais que vous ne pouvez pas utiliser de code non sécurisé dans votre projet, cette structure est ~6x plus rapide que BitConverter :

[StructLayout(LayoutKind.Explicit)]
public struct FloatToIntSafeBitConverter
{
    public static int Convert(float value)
    {
        return new FloatToIntSafeBitConverter(value).IntValue;
    }

    public FloatToIntSafeBitConverter(float floatValue): this()
    {
        FloatValue = floatValue;
    }

    [FieldOffset(0)]
    public readonly int IntValue;

    [FieldOffset(0)]
    public readonly float FloatValue;
}

(Incidemment, j'ai essayé d'utiliser la solution acceptée mais elle (du moins ma conversion) a échoué à certains des tests unitaires également mentionnés dans la réponse, par exemple assertTrue(nearlyEqual(Float.MIN_VALUE, -Float.MIN_VALUE)); )

0 votes

J'ai constaté que ces tests unitaires ont échoué, avez-vous trouvé pourquoi ?

4voto

NathanAldenSr Points 2774

Voici une version très élargie du cours de Simon Hewitt :

/// <summary>
/// Safely converts a <see cref="float"/> to an <see cref="int"/> for floating-point comparisons.
/// </summary>
[StructLayout(LayoutKind.Explicit)]
public struct FloatToInt : IEquatable<FloatToInt>, IEquatable<float>, IEquatable<int>, IComparable<FloatToInt>, IComparable<float>, IComparable<int>
{
    /// <summary>
    /// Initializes a new instance of the <see cref="FloatToInt"/> class.
    /// </summary>
    /// <param name="floatValue">The <see cref="float"/> value to be converted to an <see cref="int"/>.</param>
    public FloatToInt(float floatValue)
        : this()
    {
        FloatValue = floatValue;
    }

    /// <summary>
    /// Gets the floating-point value as an integer.
    /// </summary>
    [FieldOffset(0)]
    public readonly int IntValue;

    /// <summary>
    /// Gets the floating-point value.
    /// </summary>
    [FieldOffset(0)]
    public readonly float FloatValue;

    /// <summary>
    /// Indicates whether the current object is equal to another object of the same type.
    /// </summary>
    /// <returns>
    /// true if the current object is equal to the <paramref name="other"/> parameter; otherwise, false.
    /// </returns>
    /// <param name="other">An object to compare with this object.</param>
    public bool Equals(FloatToInt other)
    {
        return other.IntValue == IntValue;
    }

    /// <summary>
    /// Indicates whether the current object is equal to another object of the same type.
    /// </summary>
    /// <returns>
    /// true if the current object is equal to the <paramref name="other"/> parameter; otherwise, false.
    /// </returns>
    /// <param name="other">An object to compare with this object.</param>
    public bool Equals(float other)
    {
        return IntValue == new FloatToInt(other).IntValue;
    }

    /// <summary>
    /// Indicates whether the current object is equal to another object of the same type.
    /// </summary>
    /// <returns>
    /// true if the current object is equal to the <paramref name="other"/> parameter; otherwise, false.
    /// </returns>
    /// <param name="other">An object to compare with this object.</param>
    public bool Equals(int other)
    {
        return IntValue == other;
    }

    /// <summary>
    /// Compares the current object with another object of the same type.
    /// </summary>
    /// <returns>
    /// A value that indicates the relative order of the objects being compared. The return value has the following meanings: Value Meaning Less than zero This object is less than the <paramref name="other"/> parameter.Zero This object is equal to <paramref name="other"/>. Greater than zero This object is greater than <paramref name="other"/>. 
    /// </returns>
    /// <param name="other">An object to compare with this object.</param>
    public int CompareTo(FloatToInt other)
    {
        return IntValue.CompareTo(other.IntValue);
    }

    /// <summary>
    /// Compares the current object with another object of the same type.
    /// </summary>
    /// <returns>
    /// A value that indicates the relative order of the objects being compared. The return value has the following meanings: Value Meaning Less than zero This object is less than the <paramref name="other"/> parameter.Zero This object is equal to <paramref name="other"/>. Greater than zero This object is greater than <paramref name="other"/>. 
    /// </returns>
    /// <param name="other">An object to compare with this object.</param>
    public int CompareTo(float other)
    {
        return IntValue.CompareTo(new FloatToInt(other).IntValue);
    }

    /// <summary>
    /// Compares the current object with another object of the same type.
    /// </summary>
    /// <returns>
    /// A value that indicates the relative order of the objects being compared. The return value has the following meanings: Value Meaning Less than zero This object is less than the <paramref name="other"/> parameter.Zero This object is equal to <paramref name="other"/>. Greater than zero This object is greater than <paramref name="other"/>. 
    /// </returns>
    /// <param name="other">An object to compare with this object.</param>
    public int CompareTo(int other)
    {
        return IntValue.CompareTo(other);
    }

    /// <summary>
    /// Indicates whether this instance and a specified object are equal.
    /// </summary>
    /// <returns>
    /// true if <paramref name="obj"/> and this instance are the same type and represent the same value; otherwise, false.
    /// </returns>
    /// <param name="obj">Another object to compare to. </param><filterpriority>2</filterpriority>
    public override bool Equals(object obj)
    {
        if (ReferenceEquals(null, obj))
        {
            return false;
        }
        if (obj.GetType() != typeof(FloatToInt))
        {
            return false;
        }
        return Equals((FloatToInt)obj);
    }

    /// <summary>
    /// Returns the hash code for this instance.
    /// </summary>
    /// <returns>
    /// A 32-bit signed integer that is the hash code for this instance.
    /// </returns>
    /// <filterpriority>2</filterpriority>
    public override int GetHashCode()
    {
        return IntValue;
    }

    /// <summary>
    /// Implicitly converts from a <see cref="FloatToInt"/> to an <see cref="int"/>.
    /// </summary>
    /// <param name="value">A <see cref="FloatToInt"/>.</param>
    /// <returns>An integer representation of the floating-point value.</returns>
    public static implicit operator int(FloatToInt value)
    {
        return value.IntValue;
    }

    /// <summary>
    /// Implicitly converts from a <see cref="FloatToInt"/> to a <see cref="float"/>.
    /// </summary>
    /// <param name="value">A <see cref="FloatToInt"/>.</param>
    /// <returns>The floating-point value.</returns>
    public static implicit operator float(FloatToInt value)
    {
        return value.FloatValue;
    }

    /// <summary>
    /// Determines if two <see cref="FloatToInt"/> instances have the same integer representation.
    /// </summary>
    /// <param name="left">A <see cref="FloatToInt"/>.</param>
    /// <param name="right">A <see cref="FloatToInt"/>.</param>
    /// <returns>true if the two <see cref="FloatToInt"/> have the same integer representation; otherwise, false.</returns>
    public static bool operator ==(FloatToInt left, FloatToInt right)
    {
        return left.IntValue == right.IntValue;
    }

    /// <summary>
    /// Determines if two <see cref="FloatToInt"/> instances have different integer representations.
    /// </summary>
    /// <param name="left">A <see cref="FloatToInt"/>.</param>
    /// <param name="right">A <see cref="FloatToInt"/>.</param>
    /// <returns>true if the two <see cref="FloatToInt"/> have different integer representations; otherwise, false.</returns>
    public static bool operator !=(FloatToInt left, FloatToInt right)
    {
        return !(left == right);
    }
}

1voto

Phillip Ngan Points 4303

Bien que la seconde option soit plus générale, la première est préférable lorsque vous avez une tolérance absolue et que vous devez exécuter un grand nombre de ces comparaisons. Si cette comparaison est dite pour chaque pixel d'une image, la multiplication dans la deuxième option pourrait ralentir votre exécution à des niveaux de performance inacceptables.

0 votes

La performance n'est pas un problème majeur pour mes applications, je suis plus préoccupé par l'exactitude.

1 votes

Tu dois combattre plus fort tes instincts d'optimisation prématurée. Faites d'abord en sorte qu'il fonctionne correctement, puis commencez seulement à penser à le rendre plus rapide (si c'est un problème).

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