Unity vs. Floating Point
Unity vs. Floating Point: The Hidden Cost of Precision
Inspired by a tweet from @VehiclePhysics, this exploration dives into a subtle but impactful performance quirk within the Unity engine regarding how it handles mathematical operations.
The Core Advice
If you are performing math operations in Unity, the general rule of thumb is:
Prefer System.MathF over UnityEngine.Mathf for most functions (such as Sqrt, Sin, Cos, Log, and Pow).
Why the difference?
The performance gap exists because of how these two libraries handle data types:
UnityEngine.Mathf: Casts thefloatdouble, executes the double-precision version of the function, and then converts the result back .System.MathF: Directly utilizes native single-precision (float) implementations.
The "Hidden" Double Precision Problem
Many developers assume UnityEngine.Mathf is optimized for floats, but it actually routes a vast array of functions through double-precision logic. This includes:
- Trigonometry:
Sin,Cos,Tan,Asin,Acos,Atan,Atan2 - Exponentials:
Sqrt,Pow,Exp,Log,Log10 - Rounding:
Ceil,Floor,Round,CeilToInt,FloorToInt,RoundToInt - Comparisons:
Min,Max,Clamp,Clamp01 - Miscellaneous:
Sign,SmoothStep,Gamma,Approximately,InverseLerp
The Historical Context
This isn't a design choice so much as a legacy limitation. Originally, C#/.NET lacked single-precision methods for these functions. While System.MathF was introduced with .NET Core 2.0 in 2017, Unity has been slow to migrate.
You might think Unity would have updated this by now. However, backwards compatibility likely prevents a sweeping change. Even the Unity.Mathematics package (introduced in 2019 for DOTS) is not immune; for functions like math.sqrt(float x), it still routes through the double-precision C# implementation.
"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." — Context via Miguel de Icaza
The Mono runtime used by Unity essentially treats everything as double precision, leading to a constant stream of conversions between memory and registers.
Runtime Comparison
The behavior varies significantly depending on the scripting backend:
Benchmarking the Square Root
To quantify this, consider a loop that sums square roots times:
const int N = 10000000;
public static float UnityMathf(float v) {
for (int i = 0; i < N; ++i) {
v += UnityEngine.Mathf.Sqrt(v); // Standard Unity
// v += System.MathF.Sqrt(v); // Optimized approach
}
return v;
}
Test Environment
- Hardware: Windows / Ryzen 5950X
- Unity Version: 6000.0.76 (similar results on 2022.3, 6000.3, 6000.6)
Editor Performance (ms)
| Method | Debug Mode | Release Mode |
|---|---|---|
UnityEngine.Mathf | 282ms | 242ms |
System.MathF | 186ms | 149ms |
Comprehensive Results Across Backends
| Backend/Environment | Mathf | System.MathF | Mathematics | Burst (Mathf) | Burst (Mathematics) |
|---|---|---|---|---|---|
| Editor Debug | 282 | 186 | 260 | 66 | 35 |
| Editor Release | 242 | 149 | 211 | 66 | 34 |
| Player Mono | 212 | 142 | 209 | N/A | N/A |
| Player IL2CPP | 35 | 35 | 59 | N/A | N/A |
External Baselines:
- C# Mono 6.12: 130ms
- C# .NET 10: 37ms
- C++ (/O2)
sqrtf(): 35ms
Key Takeaways and Nuances
- The Gold Standard: 35ms is the theoretical limit for this loop on this hardware. This is achieved by C++, .NET 10, IL2CPP (with
MathforMathF), and Burst +Unity.Mathematics. - IL2CPP Magic: IL2CPP seems to recognize single-precision square roots and generates optimized C++ code, effectively erasing the
Mathfpenalty. - Burst Constraints:
System.MathFis not supported by the Burst compiler and will trigger compile errors. - The Precision Tell: When running the loop, Unity implementations return
24212990000000.0. This number cannot actually exist as a single-precision float (the nearest valid floats are24212989280256.0and24212991377408.0). This proves that double precision is operating under the hood. Non-Unity implementations return24212987183104.0.
Summary Checklist
- Use
System.MathFfor standard C# scripts in Mono. - Use
Unity.Mathematicswhen targeting the Burst compiler. - Be aware that
UnityEngine.Mathfis a wrapper for double-precision calls. - Trust IL2CPP to optimize basic
Mathfcalls into C++.