Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. We have updated the language to the Editor Terms based on feedback from our employees and community. Learn more.
    Dismiss Notice

Question Unity float comparison Voodoo magic?

Discussion in 'General Discussion' started by Ukounu, Jan 13, 2023.

  1. Ukounu

    Ukounu

    Joined:
    Nov 2, 2019
    Posts:
    206
    How to replicate: create a blank Unity project (I used Unity 2021.3.16). Create a new script with this code, attach it to camera and click on play button.
    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4.  
    5. public class NewBehaviourScript : MonoBehaviour
    6. {
    7.     void Start(){
    8.      float A = Mathf.Round(0.001f * 1000.0f) / 1000.0f;
    9.      float B = Mathf.Round(0.001f * 1000.0f) / 1000.0f;
    10.  
    11.      Debug.Log(A == B);
    12.      Debug.Log(A);
    13.      Debug.Log(B);
    14.      //Debug.Log(A + " is equal " + B);
    15.     }
    16. }
    Console output is:
    False
    0.001
    0.001


    Unity says the two floats are not equal due to small imprecision in float calculations, which is a documented Unity feature. So far so good. Now uncomment the commented Debug line and run again. That's where some magic happens.

    Console output is:
    True
    0.001
    0.001
    0.001 is equal 0.001


    How is that even possible? Not only that last debug log command (which is not supposed to modify anything) is able to change results of comparisons of two floats, but it does that after the debug command comparing them has finished executing and sent output to console already (?!)

    It's almost like after I remind Unity that A and B should be equal, Unity says, okay, now I admit it, happy now?

    Anybody has any insights on what is going on here? Because I have no clue.
     
    Claytonious likes this.
  2. halley

    halley

    Joined:
    Aug 26, 2013
    Posts:
    1,903
    I just confirmed your observations on an arbitrary object I had lying around.

    I won't dig into the actual reason, but my guess is pinhole optimization in the code generation step. By actually USING the variables A and B in an expression that has side-effects inside this scope (string concatenation), the compiler can't just optimize away these otherwise inferrable values.

    Compilers do things all the time, trying to produce more or less literal code so it will run faster. For example, your expression
    0.001f * 1000.0f
    probably produces NO final code that multiplies constants; the constants are already pre-multiplied. Mathf.Round() is also not actually called as a function, as it is "aggressively inlined." That's fancy talk for "the compiler looks at the contents of the function and expands it right where you call it every time you call it, making bloatier code that runs faster because nothing goes on the stack."

    In general, though, we'll beat on this drum every time it comes up: do not compare floats.
     
    angrypenguin and Ukounu like this.
  3. Ukounu

    Ukounu

    Joined:
    Nov 2, 2019
    Posts:
    206
    If compiler shortened "A = Mathf.Round(0.001f * 1000.0f) / 1000.0f" to just "A = 0.001f", or something like that, while generating code to execute at runtime (as per your explanation, if I understood it correctly), wouldn't it also optimize B to exactly the same value, instead of a slightly different one?

    Also, I posted a simplified example, but in actual code (which led me to this discovery), instead of 0.001f there were variables set by Physics.Raycast (distance to surface from a moving object), which become known only at runtime (and change constantly). For sure compiler can't know and optimize those values in advance? Yet script behavior was exactly the same. Two rounded raycast distances to the same surface are never equal - until their values are logged into console. Then they magically become equal.
     
    Last edited: Jan 14, 2023
  4. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    6,003
    There's no magic in computers and programming. Though there are a lot of edge cases, most of which you never have to worry about if you follow best practices.

    Such as not comparing floating point numbers for equality.
     
  5. halley

    halley

    Joined:
    Aug 26, 2013
    Posts:
    1,903
    Neat! In Computer Science circles, they call that a Heisenbug. Its behavior changes upon observation.
     
  6. DragonCoder

    DragonCoder

    Joined:
    Jul 3, 2015
    Posts:
    1,492
    Float point inaccuracies can happen for several reasons - one of those are leftover values in CPU registers because CPUs use those to sometimes provide a little more precision than technically promised. The consequence of that is that it is highly situational of what result you get.

    By the way it's a "feature" shared with the C# language as a whole and also compilers for C++ and the majority of languages because they all delegate the actual float calculations to the CPU-internal optimizations.
    There are some compilers where you can provide a flag to exchange determinism for some speed. Somewhere I read Unity Burst compiler should get such a flag at some point because they want to provide a deterministic physics system.
     
  7. kdgalla

    kdgalla

    Joined:
    Mar 15, 2013
    Posts:
    4,380
    When you Debug.Log a float, it has to convert float to string. Aside from floating point inaccuracies, conversion from float to string is also inaccurate. When your console says is not precise.
     
  8. tsibiski

    tsibiski

    Joined:
    Jul 11, 2016
    Posts:
    570
    This isn't specific to Unity. This is how float comparisons work in all C# applications, and to my knowledge, all programming languages. Never compare two floats for equality. Use the epsilon value to create your own "ApproximatelEqual()" extension method, or multiply it by a large number, convert to int, and compare that way. You should also not compare decimals for equality.

    Here's a Stack Overflow thread for more details.

    Here's some extension methods I created for this at one point:

    Code (CSharp):
    1.  
    2. public static bool ApproximatelyEqual(this float current, float compareTo) {
    3.  
    4.             return ApproximatelyEqual((double)current, (double)compareTo); //Ignore IDE. Cast is not redundant. Without cast, either ambiguous call error or infinite loop.
    5.  
    6. }
    7. public static bool ApproximatelyEqual(this double current, double compareTo) {
    8.  
    9.             double epsilon = Math.Max(Math.Abs(current), Math.Abs(compareTo)) * 1E-15;
    10.             return Math.Abs(current - compareTo) <= epsilon;
    11.  
    12.  }
    Note, unless you are storing and tracking incredibly tiny fractions of a number in your variables (to the ~15th power), the above logic will always work as expected when comparing your floats.

    The reason for the string output is that it rounds up to the intended significant value simply because floats and decimals have the tiny variations due to their nature. If you were to print to string explicitly to the 15th or so decimal place, it would show everything and show the actual difference in value.
     
    Last edited: Jan 14, 2023
    angrypenguin and DragonCoder like this.
  9. halley

    halley

    Joined:
    Aug 26, 2013
    Posts:
    1,903
    I think people have lost sight of the original observation. Line 11's answer changes based on the presence of Line 14. This is definitely a low-level code generation quirk, not just the vagaries of IEEE754 float resolution.
     
  10. DragonCoder

    DragonCoder

    Joined:
    Jul 3, 2015
    Posts:
    1,492
    The interpreter/bytecode compiler is allowed to reorder statements for performance reasons when it does not change the guaranteed output. Therefore an additional statement even afterwards can change a thing or two.
     
  11. halley

    halley

    Joined:
    Aug 26, 2013
    Posts:
    1,903
    I agree with what you're saying in the first half. I'm well familiar with compiler tech. I don't think your second statement agrees with the first. Everything in the original test case above, taken individually, is computed by deterministic means, so even if it's using IEEE754 it should come out with the same answer. An assumption it makes about which flavor of equality should be used has changed.

    Hell, I almost want to bring AnimalMan[UK] back so he could wave some crystals around the room about it.
     
    spiney199 likes this.
  12. Ukounu

    Ukounu

    Joined:
    Nov 2, 2019
    Posts:
    206
    It was a figure of speech used for emphasis, apparently.

    As halley has noted above already, some people misunderstood the point of the thread. I'm not asking for any help to "fix" my code, or explain why direct comparison of floats is a bad idea and what/how should be used instead.

    I wanted to understand how a simple debug command was able to change value of floats, when it wasn't supposed to affect them in any way (at least not according to any documented Unity features). Comparing floats here is used just as an observation method to confirm that their values change. We can drop that "comparing floats" part completely (replace it with some observation method, maybe?), but we'll still have the fact that floats change.
     
  13. DragonCoder

    DragonCoder

    Joined:
    Jul 3, 2015
    Posts:
    1,492
    Uhm no, not really. Those float calculations simply are not deterministic to the last digit.


    I did encounter a similar issue where a debug print statement lead to a different result, once in C++ by the way.
     
  14. halley

    halley

    Joined:
    Aug 26, 2013
    Posts:
    1,903
    Wow.

    IEEE754 is fully deterministic. Same numbers in, same numbers out, for all operations.
     
    Claytonious likes this.
  15. DragonCoder

    DragonCoder

    Joined:
    Jul 3, 2015
    Posts:
    1,492
    Does that apply to the C# compiler?

    The key if you ask me, is the second answer here: https://stackoverflow.com/questions/6683059/is-floating-point-math-consistent-in-c-can-it-be
    "The C# specification (§4.1.6 Floating point types) specifically allows floating point computations to be done using precision higher than that of the result."
    That's why the internal order of instructions matters.
     
  16. halley

    halley

    Joined:
    Aug 26, 2013
    Posts:
    1,903
    Yes, both IEEE754 for 32 and 64bit operate to the same philosophy: that any result (outside clearly defined NaN or underflow situations) will result in "an answer that is identical to a calculation with infinite precision but rounded to the nearest least-significant digit". So if you're doing all the math in 64bits, that's deterministic. And if you're truncating some of the intermediate results to 32bit, that's also deterministic.

    Now, as I have said multiple times in this thread, yes, it's a code generation issue. I agree it's in the C# layer. The compiler can decide different sequences of statements. But it's not going to come out to different values in the floats, it's going to decide whether it knows enough about equality to decide that step (e.g., which flavor of .Equals to rely on), and it's getting that wrong.
     
    Claytonious and angrypenguin like this.