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. Dismiss Notice

Why does this formula break after 31 loops?

Discussion in 'Scripting' started by rarac, Mar 15, 2021.

  1. rarac

    rarac

    Joined:
    Feb 14, 2021
    Posts:
    570
    I have the following code:

    Code (CSharp):
    1. public float SnapValueToCoreBlock(float ValueToSnap)
    2. {
    3.     ValueToSnap = ValueToSnap + 0.5f;
    4.     ValueToSnap = Mathf.Floor(ValueToSnap);
    5.     return ValueToSnap;
    6. }
    7.  
    8. float floatvar;
    9. int intvar;
    10.  
    11.  
    12.     for (int z = 0; z < 100; z++)
    13.     {
    14.  
    15.         floatvar = z + (Mathf.FloorToInt(0.499999f) + 1) * 0.499999f / (Mathf.FloorToInt(0.499999f) + 1);
    16.         intvar = (int)SnapValueToCoreBlock(floatvar);
    17.     }
    I am expecting "intvar" to always be equal to "z" in the loop, however after 31 iterations there is some kind of rounding error and when z=32 intvar = 33, instead of z=32 intvar =32

    from then onwards intvar is always off by 1, so for z<32 intvar=z and for z >31 intvar=z+1

    my intended result is for z = intvar always, I dont understand why this arbitrarily changes when z reaches 32, I would appreciate it if someone could help me, Thanks in advance.
     
  2. Yoreki

    Yoreki

    Joined:
    Apr 10, 2019
    Posts:
    2,587
    Likely an accumulating floating point rounding error. Print the value of floatvar after every iteration to get a better understanding of the problem you are looking at.
     
    rarac likes this.
  3. PraetorBlue

    PraetorBlue

    Joined:
    Dec 13, 2012
    Posts:
    7,697
    32.499999f is probably only representable as 32.5.

    The drop offs in floating point precision happen at every power of 2, so it's not surprising to me that 32 is a tipping point. I wrote an article a while back with an analysis of this phenomenon https://jschiff.com/Something-About-Floats/
     
    Kurt-Dekker, mopthrow and rarac like this.
  4. rarac

    rarac

    Joined:
    Feb 14, 2021
    Posts:
    570
    its odd because for example z=70 intvar = 71, so its still only off by 1 after 64

    floatvar always shows z.5, that is for z = 7 floatvar = 7.5, and for z=70 floatvar = 70.5
     
  5. PraetorBlue

    PraetorBlue

    Joined:
    Dec 13, 2012
    Posts:
    7,697
    For the record, there are a mere 262144 possible floating point values between 32 and 33. So you certainly will not get 6 digits of precision. Naturally that number continues to cut in half every power of two.
     
  6. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,375
    This is a precision error.

    First off... you have a bunch of useless logic here that is just over complicating the issue.
    This:
    Code (csharp):
    1.  
    2.     for (int z = 0; z < 100; z++)
    3.     {
    4.         floatvar = z + (Mathf.FloorToInt(0.499999f) + 1) * 0.499999f / (Mathf.FloorToInt(0.499999f) + 1);
    5.         intvar = (int)SnapValueToCoreBlock(floatvar);
    6.     }
    7.  
    Could just be:
    Code (csharp):
    1.  
    2.     for (int z = 0; z < 100; z++)
    3.     {
    4.         floatvar = z + 0.499999f;
    5.         intvar = (int)SnapValueToCoreBlock(floatvar);
    6.     }
    7.  
    And you'll still get the same issue.

    And the problem arising is that float is a 32-bit floating point number with this layout internally:


    Basically it's sign * fraction * 2 ^ exponent. It's binary scientific notation.
    (note fraction is actually a 'mantissa' and there is always a leading 1 implied since all non-zero binary numbers have a leading 1 in its sig value).

    The critical thing here is that it's binary.

    And your problem happens when we get to 32.

    32 in binary is:
    10 0000

    31 is:
    01 1111

    We gained one more significant value. And we can only have a finite sig value. Which means we lost one from the small side.

    And as a result... 32.499999 can't be represented and rounds to 32.5.

    Why?

    Well what is 0.499999 in binary?
    0.01111111111111111111

    If we lose a 1 off the end, it kicks off a rounding. You round a 1 and it goes to 0 causing the next space to round up, which causes the next space to round up, and so on and so on until it reaches the first 0 at which point it becomes 0.1. (a decimal equivalent is if you had 0.09999999 repeating, and you lost a 9 at the end and kicked off a rounding leading to 0.1dec).

    0.1 in binary is 0.5 in decimal.

    If you rewrite your code to be 0.49999 (4, 9's), instead of 0.499999 (5, 9's). You won't get the issue until z = 256. Change it to 0.4999 it won't happen until 2048.

    Notice the pattern going here?

    Hint:
    32 in binary = 10 0000
    256 in binary = 1 0000 0000
    2048 in binary = 1000 0000 0000

    You might be wondering why it's not happening every power of 2, rather than the 3 or so power's of 2 actually happening. And that's just because of how the rounding algorithm goes. It'll attempt to round down on repeating digits to reduce float error if the trailing is only 1 to 2 spaces. But once it gets to 3, well... that's significant enough to round up. Kind of makes sense since 0.1 is half, 0.11 is 0.75dec, 0.111 is 0.875dec, 0.1111 is 0.9375dec. We've neared 0.9 in decimal at this point. That's significantly closer to 1 than 0.

    And if we go back to our 0.499999dec = 0.01111111111111111111.

    Lets stick 31 and 32 on the front:
    31.499999 = 11111.01111111111111111111
    32.499999 = 100000.01111111111111111111

    The first has 25 sig values, the ladder has 26 sig values.

    And our 32-bit float can only hold 23 sig values (24 implied). The first drops 2 1's, the ladder drops 3 1's.

    Or better to understand the algorithm... 1 1 is dropped for the leading mantissa and is ignored. Leaving 2 digits to drop off the small end. in 31.499999 those digits are 10, and in the 32.499999 it's 11. The smallest is 0 in the first so down, and the smallest is 1 in the ladder and so up.
     
    Last edited: Mar 15, 2021
    mopthrow, rarac and Yoreki like this.
  7. rarac

    rarac

    Joined:
    Feb 14, 2021
    Posts:
    570
    thank you very much for the detailed answer, shaving off decimals to 0.4999 does fix the issue, thanks!