3
\$\begingroup\$

For comparing floats they recommend using Mathf.Approximately At the same time, they don't use it, for example, for comparing types such as vectors. From Vector3.cs:

public bool Equals(Vector3 other)
{
    return x == other.x && y == other.y && z == other.z;
} 

If I understand it correctly it wouldn't cause any problem when using == operator to compare floats that are formed the same way (or copied from the same source). For example:

float x = 10f / 10f;
float a = x; // or `a = 10f / 10f`;
float b = x;
bool equal = (a == b); // always `true`

but it can cause a problem if floats are formed differently even if their results are supposed to be the same. For example:

float a = 10f / 10f;
float b = 11f - 10f;
bool equal = (a == b); // result is unpredictable

Is my understanding correct?

\$\endgroup\$

2 Answers 2

5
\$\begingroup\$

A better example to look at for vectors is the == operator:

// Returns true if the vectors are equal.
public static bool operator==(Vector3 lhs, Vector3 rhs)
{
    // Returns false in the presence of NaN values.
    float diff_x = lhs.x - rhs.x;
    float diff_y = lhs.y - rhs.y;
    float diff_z = lhs.z - rhs.z;
    float sqrmag = diff_x * diff_x + diff_y * diff_y + diff_z * diff_z;
    return sqrmag < kEpsilon * kEpsilon;
}

You can see that this applies an approximate-equality check, testing whether their difference has magnitude less than kEpsilon, or a hundred thousandth of a unit.

This way users get extra tolerance for rounding automatically, even if they do the most careless thing. If the user wants exact equality only, they need to ask for it, by using Vector3.Equals(). (This also ensures that if you use a vector as a key in a collection like a hash map/dictionary, you won't get spurious collisions from an overly-permissive comparison)

So the definition of Equals this way isn't the devs saying that Mathf.Approximately() is unnecessary. In fact it's just the opposite: they're saying that approximate equality checks or range checks should be your default, and you should reach for exact equality only when you're sure that's appropriate for your context, by typing extra to get .Equals() instead of ==.

\$\endgroup\$
2
  • \$\begingroup\$ Is the sqrmag variable use of multiplication necessary? Wouldn't Math.Abs( diff_x ) + Math.Abs( diff_y ) + .... < kEpsilon be faster than doing the multiplications? If the C# or is it C++ transpiled from C# or whatever is doing the optimal bitshift magic, the abs would take 3 cycles while a multiplication takes 4 cycles. \$\endgroup\$ Commented Jun 14, 2020 at 5:33
  • 2
    \$\begingroup\$ Using Abs would also introduce anisotropy, where you get a greater or lesser rounding tolerance depending on the direction of the error. I'd suspect the developers considered a consistent, isotropic solution to be worth the extra cycles. There's also the possibility that some of the calculations can be overlapped or vectorized, depending on the chip. \$\endgroup\$
    – DMGregory
    Commented Jun 14, 2020 at 9:34
4
\$\begingroup\$

Floats have this thing called floating point errors. Since they are stored in binary, it is sometimes impossible to get the exactly correct results from mathematical operations on them.

So we have to allow for some margin of error and do our equality comparison that way. This Math.Appr functions usually does exactly that: instead of checking if they are exactly equal, it compares them on Epsilon, where if two paremeters are as close as the Epsilon, they are ruled equal.

The margin of error is usually called the Epsilon value (float.epsilon if I remember correctly) which represents the smallest float value greater than 0. But you may need bigger margins of error depending on the circumstances.

If you indeed need bigger margins of error, you can just do this yourself:

isApprEqual = abs(x-y) <= marginOfError

\$\endgroup\$
1
  • 1
    \$\begingroup\$ Note that although float.Epsilon also shares the name epsilon, it's not a suitable choice for equality comparisons - it's absurdly tiny (about a sixtillionth of a septillionth). For typical vector calculations in games, your rounding error will be quite a bit larger than this, so you'll want a larger epsilon. A common metric is to think about the size of your unit in last place (or "ULP") - what's the value of the last binary digit in the numbers you're using? You'll want your epsilon to be greater than this to have any effect. \$\endgroup\$
    – DMGregory
    Commented Jun 13, 2020 at 16:53

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .