Unity vs. Floating Point
aras-p.info
aras-p.info
> The operation is finished when the progress float reaches 1.0 and isDone is called. If you set allowSceneActivation to false, progress is halted at 0.9 until it is set to true.
So, I needed to have my UI do other stuff until both this and something else was ready. Obvious thing to do would be to check this for "AsyncOperation.progress >= 0.9" and also check the other thing. Right?
Except AsyncOperation.progress is a float, 0.9 is a double and 0.9f < 0.9. Progress is not halted at 0.9. It never reaches 0.9. It's halted at 0.9f! Just a few million ulps short!
1) Why would you use floating point instead of fixed point or other fractional representation (uh, percent?) for this sort of thing?
2) Why would you design an interface that's specifically set up to make progress bars stall at 90%? This is insanely user hostile -- there are great UI patterns for expressing indeterminate progress when your progress is indeterminate!
3) Why in the example would you prompt user interaction at 90%? (I'll give them credit for at least comparing to 0.9f instead of 0.9 here.) What user expects a prompt to appear before progress is complete?
Subnormal floating point shenanigans maybe?
Probably because Unity uses ancient Mono fork. And that apparently started because they didn't renew their non-copyleft license with Xamarin.
Unity used to be on ancient version of Mono before year 2017. Then until 2019 the option to use “up to date” version was optional. Since 2019 it has been on more or less current Mono version (Mono itself stopped getting any serious updates around 2020).
Again, I do not mean this as a criticism of floats. For simulations and for numbers where you do have to support completely arbitrary values, there is a reason floats are a thing.
So fixed-point addition and subtraction are definitely faster, multiplication is a wash if you're doing binary-based fixed point (but slower if you're doing decimal-based fixed point), and fixed-point division is definitely slower than floating-point division.
My gut would still be that it is typically a wash for most everyone as far as speeds go? If default libraries supported it more directly, I would think it would largely be a win for a lot of reasoning. In particular, silly stuff like 1e32 + 1e1 would not be nearly as surprising to most people. And the entire class of bugs around stuff like doing something until it reaches 0.9 would almost certainly go away if we guaranteed precision to a set number decimal places.
Alas, default libraries do not support this, though. So the above is admittedly wishful thinking on my end. And I could as easily describe a world where people insist on arbitrary numbers of fixed point values and how that would be its own set of landmines.
Commonly enough that I think it's some fundamental reason (given available/current hardware as opposed to hypothetical).
Two reasons I can think of, though granted, they only apply in certain niches:
1) Floating-point SIMD extensions are far more common than integer ones. This means you can compute N (often N=4 or 8) float operations in one instruction, vs 1 integer operation.
2) For any GPGPU processing: GPUs far prefer floats, to the point where you didn't even use to have integers available (somewhat ironically, the platforms that prefer float much more strongly to int are mobile/embedded ones nowadays --- which is the exact opposite of the CPU situation). To this day, you have a `mul24` intrinsic for integer multiplication in some languages ... which converts two integers to floats, multiplies them as floats, and then converts back to integers. Yes, that was faster than direct multiplication. I'm sure many GPUs do it directly nowadays though.
It's also worth considering that a typical fixed-point multiply (as opposed to integer) is an integer multiply followed by a shift; often to a 2×-bit intermediate, if you want to preserve precision. That's a cost.
I do not at all contest this. Would largely expect it. It is a common enough trap that people introduce complications to something expecting it to be faster.
I do expect that the largest reason, in this case, is simply volume/network effects. More people lean on floats than on fixed decimal. Therefore, it is not that surprising that more optimization has happened there. This is exactly why I somewhat lament there is not more fixed point work out there. My assertion is if it was more standard, there would be more standard optimizations.
For that last point, I would still expect you could pick constants more for multiplication so that you didn't have to do the shifts as often. Probably even preferring classes of numbers where if you have a set of X values that you often multiply with Y values, if you can limit yourself so that all Y values have no decimal part, X*Y is always just an integer multiplication.
Now, I also fully grant that systems start to crash when someone didn't realize exactly why no Y values had a decimal. They decide they really need one, and then update the code with a massive performance hit that they didn't even pay attention to.
On the other hand, I’m not sure if modern compilers even bother converting division/multiplications by powers of two to shifts these days, so maybe it isn’t worth it…
This comes up all of the time with stuff like lat/lng values. People are convinced you have to use doubles because of the inaccuracy of floats. Completely skipping over the fact that you could have fixed point accuracy to 6 decimal digits with the same number of bits as a float. You just reduce your max/min value that you can represent. Which, for lat/lng, you are already heavily bounded.
I think it is fair to argue that basic libraries don't support trig on common fixed point sizes. But that is ultimately my lament. Floats are amazing for what they need to do. For what many people need, though, it feels overkill because it is overkill.
Using any floating point representation for lat/long makes no sense at all to me. Floating point is designed for representing values where expected error scales with magnitude, not for values where error is constant with magnitude; the latter requires fixed point. I truly don't understand why someone would optimize their position representation for maximum accuracy off the coast of Africa, instead of having it uniform across the world.
If you have the trig functions already implemented for the fixed point you are doing, what you are saying makes perfect sense to me. But I swear I get challenged on that point every time it comes up, as nobody has a fixed point trig library. (Well, that seems to be the assertion.)
which is why you should just use GeographicLib (or proj, now that it incorporates the same geodesic routines).
With single-precision floats, considering the worst case, i.e. longitudes close to the equator but far away from the prime meridian, one ulp can translate into as much as 1.68 m (5.5 ft) of distance on the surface (*). That's good enough for some uses, but not for dGPS or any other serious geometric computation. Whereas, with double precision, one ulp in this worst case scenario corresponds to about 3 nanometers. It's overkill, for sure, but if these are the only two types you have, you pick the latter.
* = To represent the integer 179, you need 8 bits, leaving only 16 for the fraction. Since 1 degree of longitude near the equator is about 110 km, you have 1/2^16 degrees * 110 km/degree giving 0.0016784... km.
And again, IEEE numbers are amazingly well done for what they are. Which is largely the digital version of scientific notation that is using base-2.
Its not that hard to roll your own support for fixed point trig. CORDIC is your friend. Easy to understand and easy to implement. Plus you can easily customize it for whatever custom fixed point size you want to work with.
Back in the day I implemented fixed point SIN / COS / TAN / ATAN routines on an 8 bit micro. Angle was represented as 8.8 fixed point, with 256.0 = 360 degrees. Made it nice when adding and subtracting angle values, as 360 degrees would overflow back to zero degrees. SIN/COS values, being between 0.0 and 1.0, were stored as 0.16 bit fixed point, so FFFFh = 0.9998 (approx 1.0). Used it for embedded magnetometer and gyro applications. Worked great and was reasonably fast considering it was an 8 bit micro.
For some reason this was in my head as one of those “surprisingly, optimizing compilers might skip it” situations (like loop unrolling). Dunno where I picked that up.
Math.fround()