Unity vs floating point · Aras' website
A tweet by @VehiclePhysics<br>sparked my interest. It basically says:
For most math functions (Sqrt, Sin, Cos, Log, Pow…), prefer System.MathF<br>over UnityEngine.Mathf. Unity’s Mathf casts to double, calls the double version,<br>then converts back to float. System.MathF calls the float-native implementations<br>directly. Less work, same result.
This advice is basically correct! But turns out, things are slightly more<br>complicated.
Hidden double precision in Unity
The advice above applies to all UnityEngine.Mathf methods that deal with<br>trigonometry (Sin, Cos, Tan, Asin, Acos, Atan, Atan2),<br>exponentials (Sqrt, Pow, Exp, Log, Log10),<br>rounding (Ceil, Floor, Round, CeilToInt, FloorToInt, RoundToInt),<br>comparisons (Min, Max, Clamp, Clamp01) and others (Sign, SmoothStep,<br>Gamma, Approximately, InverseLerp). About the only function it does not apply to<br>is Mathf.Abs.
But… why? Well, because C#/.NET originally did not have single-precision<br>methods for these sorts of math functions. The single precision System.MathF<br>was introduced in .NET Core 2.0 (year 2017).
Now, you might have expected that almost ten years later, maybe Unity would<br>have noticed this, and made them single precision? Alas, no. There could be potential<br>backwards compatibility issues preventing that (or maybe not! see below).
You also might have guessed that<br>Unity.Mathematics package,<br>which was introduced (year 2019) as part of the whole DOTS push, and is modeled to be very similar<br>to HLSL, would actually do single precision floating point for functions that look like<br>single precision floating point… and that would be wrong too; for all the trigonometric<br>and exponential functions like math.sqrt(float x) it routes that into the double<br>precision C# implementation. Why? I don’t know.
But wait! There is way more double precision. The Mono C# runtime used in Unity<br>does all math in double precision, everywhere . Yes, this means there is a ton<br>of float⭤double conversions from in-memory representation to in-register representation,<br>all over the place. I have first noticed this back in 2018 when doing a<br>toy path tracer,<br>and then Miguel de Icaza did an explanatory blog post,<br>with plans outlined how to switch Mono to use actual floats for floats (yeah!).
“In Mono, decades ago, we made the mistake of performing all 32-bit float computations as 64-bit floats while still storing the data in 32-bit locations.”
Official Mono releases have switched to do that since then, but (I think) for backwards compatibility<br>reasons Unity never enabled that functionality and kept everything at double precision so far.
Note however that the above only applies to Mono. The other two C# language/runtime implementations used<br>across Unity today, IL2CPP and Burst, do not have the “everything is actually double precision”<br>behavior. It is weird that Unity would not switch their Mono version to match;<br>after all some of their main deployment platforms never use Mono (iOS, consoles, web)!
Let’s look at a square root
The above is fairly abstract, so let’s look at what actually happens with a very simple<br>loop that sums up a bunch of square roots:
const int N = 10000000;<br>public static float UnityMathf(float v)<br>for (int i = 0; i<br>In the Unity editor (6000.0.76, but rough timings are the same on 2022.3, 6000.3 and 6000.6 versions),<br>on Windows / Ryzen 5950X machine: UnityEngine.Mathf 282ms , System.MathF 186ms .<br>Whoa indeed, this is way faster!
But hey! Back in 2018<br>we already found that Unity’s C# performance also very much depends on whether script debugging<br>is enabled or not. Back then it was called “Editor Attaching” under preferences; these days<br>it is this bad-contrast-in-light-theme Debug vs Release widget at lower right editor corner.<br>In Release mode, in-editor timings are: UnityEngine.Mathf 242ms , System.MathF 149ms .
More square roots in more C# variants
To get a more complete picture, let’s also add a variant that uses the<br>“new way of doing math” in Unity, i.e. the<br>Unity.Mathematics package. And have timings for a player build that uses Mono, plus timings for an<br>IL2CPP scripting backend.<br>And while at it, also test performance of the same code under<br>Burst compiler.
Editor Debug<br>Editor Release<br>Player Mono<br>Player IL2CPP
Mathf<br>282<br>242<br>212<br>35
System.MathF<br>186<br>149<br>142<br>35
Mathematics<br>260<br>211<br>209<br>59
Burst Mathf<br>66<br>66<br>67<br>60
Burst Mathematics<br>35<br>34<br>34<br>34
And for a complete picture, the same loop, using System.MathF.Sqrt (C#) or sqrtf() (C++)<br>in non-Unity implementations / runtimes:
C# Mono 6.12<br>C# .NET 10<br>C++ /O2
System.MathF<br>130<br>37
sqrtf()
35
Summary of the above:
35 milliseconds to do this loop is “as good as it can get” on this machine, and that is achieved<br>by C++ & .NET, and within Unity by using Burst + Unity.Mathematics, or when using IL2CPP, with<br>either of Mathf.Sqrt or System.MathF.Sqrt. Under IL2CPP, there does seem to be<br>some...