Search Unity

Resolved Major floating point math bug in Unity/Mono

Discussion in 'Scripting' started by FaithlessOne, Nov 21, 2022.

  1. FaithlessOne

    FaithlessOne

    Joined:
    Jun 19, 2017
    Posts:
    320
    I found a bug related to floating point math in Unity/Mono. I am using Unity 2021.3.14. I only want to report this one. Use the following behviour on a game object in the scene and start the scene:

    Code (CSharp):
    1. using System;
    2. using System.Collections;
    3. using System.Collections.Generic;
    4. using UnityEngine;
    5. using UnityEngine.Assertions;
    6.  
    7. namespace Example
    8. {
    9.   public class MathTest : MonoBehaviour
    10.   {
    11.     void Start()
    12.     {
    13.       float variable1 = 0.330000013f;
    14.       int variable2 = 500;
    15.  
    16.       float temporaryValue = variable1 * variable2;
    17.       double result1 = Math.Ceiling(temporaryValue);
    18.  
    19.       double result2 = Math.Ceiling(variable1 * variable2);
    20.      
    21.       Assert.AreEqual(result1, result2);
    22.     }
    23.   }
    24. }
    The assert will fail and an error will be printed to console. Result1 is 165 while result2 is 166. This is not expected, because the calculations for result1 and result2 should be technically the same. The same code executed directly on .NET 6 will return 165 in either result. So Unity/Mono does behave different and faulty. In my game this is an issue, because the server runs on .NET 6 while Unity/Mono is the client. So the client may calculate other results then the server does in basic math operation. Sure you can use the calculations of result1 every time to avoid this miscalculation, but I suspect that there are much more cases where this bug may occur and not only the stated one. This should be fixed in Unity/Mono.
     
  2. AnimalMan

    AnimalMan

    Joined:
    Apr 1, 2018
    Posts:
    1,164
    ceil isn’t rounding down for you it’s rounding up for you. It’s giving you the next largest number. So it would be 166 from 165.000000013 since it won’t round down.

    Have I missed something? That is math ceiling. Take me to the ceiling of the value greater than or equal to. It is not equal to so you’re at the ceiling of 165.accuracy which is 166
     
  3. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,532
    Nope... both are not the same.

    When you say:
    Code (csharp):
    1. float temporaryValue = variable1 * variable2;
    You're downcasting the result to a float which has only ~7 digits of sig value. Since 500 * 0.330000013f = 165.0000065d. We lose the 65 on the end because it's too large. Your temporaryValue only = 165.0000f

    Where as when you say:
    Code (csharp):
    1. Math.Ceiling(variable1 * variable2);
    It never downcasts to a float, conserving the full sig value range of double. And thusly keeping the 65 on the end.

    Ceil(165) = 165
    Ceil(165.0000065) = 166

    This is why float and double both exist. Double has more range, and sigvalues, than float/single.

    ...

    As for your assertion about this being a bug because in .net 6 it behaves slightly different. Well yes, that's because I'm betting the compiler you used for your .net 6 project does downcast, before upcasting again and passing it into Math.Ceiling. Where as the compiler used by Unity doesn't and just keeps it double since Math.Ceiling accepts double... it's optimizing.

    Does this result in different outcomes?

    Yes.

    But it's not a bug.

    1 compiler behaving slightly different than another is well... common. In nearly all compiled languages. Heck, the flags that you set for the compiler will alter the compiled outcome. This is what is to be expected from compilers in software development.
    (give this a try, write your simple example in a VS console project. Run it. Get your expected result. Now open properties and on the Build tab select "Prefer 32-bit" and "Optimize Code" and run again... and bam you'll get the Unity result)

    If this is a problem... you can resolve it yourself by explicitly dealing in the data types you prefer and not relying on the compiler to up/down cast on your behalf. You want doubles? Always use doubles! You want floats? Always use floats! Don't mix and match and then expect different compilers to know what you mean by the mixing and matching! Or if you're forced into using different types, explicitly cast as necessary if you require the same operation performed.

    THOUGH... I'll argue expecting floating values to result in the same values on 2 different machines is actually not a good idea since the IEEE-754 standard for floating point numbers does not have explicit requirements that the same inputs result in identical outputs between implementations. Basically... 1 cpu can result in slightly different errors than another CPU... and especially so if the platform in question performs the arithmetic in software. An ARM chip may behave different from an AMD chip from an Intel chip, from an older generation Intel chip.
     
    Last edited: Nov 21, 2022
    DragonCoder, Ryiah, Yoreki and 5 others like this.
  4. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,735
    Yeah, I remember that day when I thought I found this too. It's sort of a rite of passage.

    It's not a bug.

    "Think of [floating point] as JPEG of numbers." - orionsyndrome on the Unity3D Forums

    Floating (float) point imprecision:

    Never test floating point (float) quantities for equality / inequality. Here's why:

    https://starmanta.gitbooks.io/unitytipsredux/content/floating-point.html

    https://forum.unity.com/threads/debug-log-2000-0f-000-1f-is-200.1153397/#post-7399994

    https://forum.unity.com/threads/why-doesnt-this-code-work.1120498/#post-7208431

    Literal float / double issues:

    https://forum.unity.com/threads/err...annot-be-implicitly-con.1093000/#post-7038139
     
    chadfranklin47, Ryiah and Bunny83 like this.
  5. AnimalMan

    AnimalMan

    Joined:
    Apr 1, 2018
    Posts:
    1,164
    floating point inaccuracy is the ruiner of code and destroyer of friendships.

    lol the JPEG of numbers.

    it’s more like well because it’s needed sometimes it’s more like cat. It just bites you for no reason and scratches you out of the blue without warning
     
  6. FaithlessOne

    FaithlessOne

    Joined:
    Jun 19, 2017
    Posts:
    320
    Thanks for your replies especially to lordofduct for his detailed explanation. So I have understand that this is not a bug. Still in a "perfect world" all compilers for a language would behave the same, but for our world this is an illusion I fall prey. Unfortunately also the CPU divergencies regarding floating point will force me to another approach for my server/client-shared calculations.
     
  7. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,735
    A few of the kool kids use fixed point... not sure how applicable it is to your situation but it is at least deterministic if you use it properly.
     
  8. AnimalMan

    AnimalMan

    Joined:
    Apr 1, 2018
    Posts:
    1,164
    You can just round the result using math round to 0 decimal places for your match

    my assumption is
    Ceil and Floor are quick operations while math round is a cpu heavier in comparison as it is a variable of sorts. The length. Sometimes a floor is suitable. A ceil also. Many occasion. But math round may also be necessary.

    A ceiling will only care if there is value greater than a whole integer. To decide to ceil. And a floor will do the opposite. So it must be significantly cheaper in comparison and a quicker speed to reach the true or false flag than that of the round direction decision.

    let assume we had a bucket of fluid and we had to tip the fluid of one bucket out onto a weighing scale and check the number of molecules before deciding to round up or down. And another bucket whom we were going to ceil if it contained any single molecule of water at all. It would be vastly quicker to ceil that bucket as we could just pour partial contents and check for a measure.
     
    Last edited: Nov 22, 2022
  9. angrypenguin

    angrypenguin

    Joined:
    Dec 29, 2011
    Posts:
    15,620
    Have you tried changing line 19 to:
    Code (csharp):
    1. double result2 = Math.Ceiling((float)(variable1 * variable2));
    Still, even if that does give the same result, I'd not rely on it. As you've demonstrated, a very subtle and potentially hard to spot difference can end up with different results in certain cases. Sounds to me like a recipe for an intermittent and painful-to-solve bugs.

    If the results are important and need to remain in sync then wouldn't you have the server do the math and send the result to the client? It's either that, or going for full determinism if you need the results of calculations to reliably sync up.
     
  10. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    3,999
    The hardware and the compiler by definition is allowed to carry out certain operations with a higher precision. The actual FPU (which is now an integral part of the CPU) usually have 80 bit floating point registers internally to improve certain intermediate results. See the Extended precision. So it depends on when and in which places a precision reduction happens. Since this is not set in stone, you can not rely on it.
     
  11. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,108
    There was an official article or talk regarding mathematics package going down this route -- i.e. being reliable across platforms. I have to find it though.
     
  12. karliss_coldwild

    karliss_coldwild

    Joined:
    Oct 1, 2020
    Posts:
    602
    Rounding instead of ceil or floor doesn't really solve the problem. It just changes which input potentially triggers an observable difference in behavior. Rounding x to 0 decimal places is effectively the same as floor(x+0.5). So now the result may potentially vary when number is close to XYZ.5 instead of when number is close to XYZ.0.
     
    Yoreki and orionsyndrome like this.
  13. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,108
    This only really makes sense when building a fast shooter with physics, for example something like Battlefield.
    Otherwise, it's a bad design. Not a single MMO needs full determinism, that's so needlessly hardcore.

    I remember learning about how BF netcode worked from seeing tanks maintain their client rotation (velocity) on desync. That was such a hilarious glitch but also a cool hint for whoever wanted to understand it better.
     
  14. jvo3dc

    jvo3dc

    Joined:
    Oct 11, 2013
    Posts:
    1,520
    It always raises my hairs if I see a currency value ending up in something like a float or double. Microsoft would like you to use decimal, which is a fine solution. I always go for one level simpler, use an integer and represent the number of cents.
     
  15. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,108
    That's a big no. I fully expect from whoever handles currencies in a professional capacity to at least understand the technologies being used and the implications of using them. Hopefully no one will ever use an application that uses floats to represent real world money.
     
    Kurt-Dekker likes this.
  16. jvo3dc

    jvo3dc

    Joined:
    Oct 11, 2013
    Posts:
    1,520
    Hey, I agree, unfortunately, most programmers will see a comma/point and will just go for a float, not realizing the implications. I'm assuming that bank software doesn't work this way, but I've seen it happen mostly in web development. In languages that don't encourage typing, how would you expect people to know about the limitations of the underlying type? I mean, var currency = 0.03 seems to work, so, done. And then spend a lot of time later to fix the "corner cases".

    Edit: Reading documentation, who has time for that? ;)

    For floating point I like a site like this (besides the documentation and underlying theory of course.)
    https://www.h-schmidt.net/FloatConverter/IEEE754.html
    It confronts you directly with what is actually happening and how what is stored might be different from what you entered. And that hopefully encourages you to dive further into the documentation.
     
    Last edited: Nov 22, 2022
  17. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,735
    orionsyndrome likes this.
  18. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,108
    Maybe we should apply that logic to the world as a whole? That's of course rhetorical because I'm pissed.
     
  19. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,735
    orionsyndrome likes this.
  20. FaithlessOne

    FaithlessOne

    Joined:
    Jun 19, 2017
    Posts:
    320
    I agree this would be a solution, but I have a turn-based strategy game and the client currently only calls the server at the end of the turn for submitting the turn orders. I don't wanna put additional load and network traffic on the server for calculations which can be done by the client. The calculations are some sort of predictions.

    Thanks for pointing me in the right direction. I tried a ready-to-use fix point math implementation someone provided. Seems to work for now.

    There are certainly other implementations out there, but I used this one:
    https://github.com/asik/FixedMath.Net
     
    Kurt-Dekker likes this.
  21. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    3,999
    Well, as someone working for a company that issues securities and deals with relatively large numbers, I can tell you it's much worse than you think ^^. When I started at the company literally everything was handled with excel sheets. Just in case you don't know, excel uses double precision floats for any kind of numbers. A lot of companies use excel sheets for doing all sorts of complex calculations. A double has roughly 15 (decimal) significant digits. So you could handle values up to about a trillion and still have two decimal places behind the decimal point. For most calculations that's more than enough.

    Further more we actually maintain / develop an internal software based on AngularJS. Javascript also does only support double precision numbers, that's all it has. So you don't have a real choice. Most systems have several languages involved (PHP, MYSQL, Python, ...) which all have their own way dealing with numbers and currencies.

    If you use a language that supports a currency type like C#, for god's sake use it for money or other larger numbers that must be precise. Though if you don't you should be aware of potential pitfalls and think hard about what kind of calculations you may do and in what limits you're operating in. Luckily in my company we only have to deal with cents, so we never need more than 2 decimal places behind the decimal point.

    Of course single precision floats are out of the question for most money usages as the number of significant digits is just too small, especially in the banking bussiness where they may have to deal with 4 or 5 decimal places behind the decimal point. Here even double values quickly reach their limits and are often not suitable.


    Actually, id Software did :D See this recent talk by John Romero. I linked the specific timestamp, however I highly recommend watching the whole talk, it's quite interesting :)
     
  22. angrypenguin

    angrypenguin

    Joined:
    Dec 29, 2011
    Posts:
    15,620
    Well, yeah. That's why the preceeding statement was there. :p
     
    orionsyndrome likes this.
  23. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,532
    The best one I ever saw was a medical company I worked at for a few months and then quit from cause it was such a mess (see orion, I do have morals).

    A portion of their system stored user credentials in csv files, specifically so they could edit them in excel, where the username/password/user info was stored in all plane text. And this was only the beginning of their problems.

    While I was there they were trying to get everything upto SOX compliance by the end of the year when the feds came in to audit everything because they wanted to go public. People were quitting like crazy all around us. I got in an argument with my boss one day about what he wanted me to do, he tried to fire me, the higher ups refused to because they already lost too many people. I quit regardless... months later I ran into several other staff who all left the company as well. From what I can gather they basically bundled up all the IP and sold it off... so the company exists still, but only in name.
     
    orionsyndrome likes this.
  24. jvo3dc

    jvo3dc

    Joined:
    Oct 11, 2013
    Posts:
    1,520
    I actually did not know that, I figured it would also have at least one integer type. (Ok, it has a BigInt, but it's rarely promoted.) But, you are correct:
    https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number

    They don't actively ask you to store currency in a floating point number there, but they do in other places:
    https://devmountain.com/blog/what-are-data-types-javascript-101/
     
    Last edited: Nov 23, 2022
  25. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    3,999
    The great thing about double is, that a double can reliable represent a 32 bit integer without rounding errors as its range is large enough to cover all 4 billion values reliably. A float (single precision) of course can not represent a 32 bit int since itself only has 32 bits. Entropy can't be circumvented :)

    Of course you could always roll your own much more complicated but reliable format in most languages. However the usage would be a pain. For example you can store currency values in strings. However if you want to multiply or divide them, you would need to do that manually by interpreting the decimal number represented as string, going char by char doing long division / multiplication. This is possible, but in most cases not practical. As I said, double is usually enough for most usecases, but you should be aware of its limits and think about the ranges you have to deal with beforehand. It's also common to just do some sort of fix point math by using two double values, one for the whole number and one for the fraction. Of course when doing arithmetic there's a lot of additional book keeping to do all the carrying yourself. Though this would give you a much larger range.

    Though certain frameworks like angular or react have input fields which have already number filters and all that stuff that of course only works on actual numbers.

    Anyways, we did sidestep quite a bit :) For sure an interesting topic but not directly related to Unity.
     
    orionsyndrome likes this.
  26. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,108
    There is a hidden fallacy here. Javascript was never supposed to govern the actual transaction computation, it's a light scripting language intended for driving browser behavior, which is just the View in a classic MVC pattern, not something that should amount to responsible state tracking. It's perfectly ok to see a glitch produced by Javascript, only to refresh and see the reflected ground truth from a database.

    Claiming that Javascript should govern anything else -- to act as a database controller -- is preposterous. This is why it doesn't have the necessary tools for such a reality, let alone types. And whoever made it into a controller is doing a disservice to the world. Even PHP is better in every respect.
     
  27. AnimalMan

    AnimalMan

    Joined:
    Apr 1, 2018
    Posts:
    1,164
    These discussions are crazy.
    You guys talking about people you work for haha

    alien world to me guys being employed. I could never be employed at this point. Just like I could never align with a political party. I’m going all in for the 80 mil for myself. I got ex employers do serious mental health damage. Last wage I receive was lower than minimum wage 6.50 in 2014.

    I wouldn’t over think it just round the number. Floor or Ceil if it didn’t matter. Otherwise rounding it will be fine.