Skip to content
HN On Hacker News ↗

Unity vs floating point · Aras' website

▲ 57 points • 44 comments • by ibobev • 4mo ago • HN discussion ↗

Pangram verdict · v3.3

We believe that this document is fully human-written

1 %

AI likelihood · overall

Human
100% human-written 0% AI-generated
SEGMENTS · HUMAN 7 of 7
SEGMENTS · AI 0 of 7
WORD COUNT 1,665
PEAK AI % 7% · §6
Analyzed
Jun 18
backend: pangram/v3.3
Segments scanned
7 windows
avg 238 words each
Distribution
100 / 0%
human / AI fraction
Verdict
Human
Pangram v3.3

Article text · 1,665 words · 7 segments analyzed

Human AI-generated
§1 Human · 0%

Posted on Jun 11, 2026 A tweet by @VehiclePhysics sparked my interest. It basically says:

For most math functions (Sqrt, Sin, Cos, Log, Pow…), prefer System.MathF over UnityEngine.Mathf. Unity’s Mathf casts to double, calls the double version, then converts back to float. System.MathF calls the float-native implementations directly. Less work, same result.

This advice is basically correct! But turns out, things are slightly more complicated. Hidden double precision in Unity The advice above applies to all UnityEngine.Mathf methods that deal with 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) and others (Sign, SmoothStep, Gamma, Approximately, InverseLerp). About the only function it does not apply to is Mathf.Abs. But… why? Well, because C#/.NET originally did not have single-precision methods for these sorts of math functions. The single precision System.MathF was introduced in .NET Core 2.0 (year 2017). Now, you might have expected that almost ten years later, maybe Unity would have noticed this, and made them single precision? Alas, no. There could be potential backwards compatibility issues preventing that (or maybe not! see below). You also might have guessed that Unity.Mathematics package, which was introduced (year 2019) as part of the whole DOTS push, and is modeled to be very similar to HLSL, would actually do single precision floating point for functions that look like single precision floating point… and that would be wrong too; for all the trigonometric and exponential functions like math.sqrt(float x) it routes that into the double precision C# implementation. Why? I don’t know. But wait!

§2 Human · 0%

There is way more double precision. The Mono C# runtime used in Unity does all math in double precision, everywhere. Yes, this means there is a ton of float⭤double conversions from in-memory representation to in-register representation, all over the place. I have first noticed this back in 2018 when doing a toy path tracer, and then Miguel de Icaza did an explanatory blog post, 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 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 across Unity today, IL2CPP and Burst, do not have the “everything is actually double precision” behavior. It is weird that Unity would not switch their Mono version to match; 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 loop that sums up a bunch of square roots: const int N = 10000000; public static float UnityMathf(float v) { for (int i = 0; i < N; ++i) { v += UnityEngine.Mathf.Sqrt(v); // classic Unity //v += System.MathF.Sqrt(v); // as advised by the tweet above } return v; }

In the Unity editor (6000.0.76, but rough timings are the same on 2022.3, 6000.3 and 6000.6 versions), on Windows / Ryzen 5950X machine: UnityEngine.Mathf 282ms, System.MathF 186ms. Whoa indeed, this is way faster!

§3 Human · 0%

But hey! Back in 2018 we already found that Unity’s C# performance also very much depends on whether script debugging is enabled or not. Back then it was called “Editor Attaching” under preferences; these days it is this bad-contrast-in-light-theme Debug vs Release widget at lower right editor corner. 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 “new way of doing math” in Unity, i.e. the Unity.Mathematics package. And have timings for a player build that uses Mono, plus timings for an IL2CPP scripting backend. And while at it, also test performance of the same code under Burst compiler.

Editor Debug Editor Release Player Mono Player IL2CPP

Mathf 282 242 212 35

System.MathF 186 149 142 35

Mathematics 260 211 209 59

Burst Mathf 66 66 67 60

Burst Mathematics 35 34 34 34

And for a complete picture, the same loop, using System.MathF.Sqrt (C#) or sqrtf() (C++) in non-Unity implementations / runtimes:

C# Mono 6.12 C# .NET 10 C++ /O2

System.MathF 130 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 by C++ & .NET, and within Unity by using Burst + Unity.Mathematics, or when using IL2CPP, with either of Mathf.Sqrt or System.MathF.Sqrt. Under IL2CPP, there does seem to be some special code path that goes “oh this should actually be single precision square root” and generates underlying C++ code accordingly.

§4 Human · 0%

System.MathF functions are not supported by Burst for some reason; if you try to use them you will get Burst compile errors. If you do not need Burst, then System.MathF is often faster. It does make it harder to move code to Burst though. Unity.Mathematics is often slightly better than the classic Mathf, except under IL2CPP, at least for the square root. IL2CPP does not seem to have special recognition of “oh this should be single precision square root” for it, and has other overheads too, see below. In the opposite behavior to IL2CPP, Burst does not seem to do “oh this should be single precision” for Mathf.Sqrt, but it does for Mathematics.math.sqrt at single precision.

Also fun fact? All the Unity implementations above print the result of the above loop as 24212990000000.0, which is curiously not a number that exists as a single precision float (closest floats that exist are 24212989280256.0 and 24212991377408.0). That’s one of the signs of “yeah some stuff is always doubles underneath, somewhere”. The non-Unity (C# .NET, C++) implementations print the result 24212987183104.0. Welcome to the world! Things are never simple! Code generation of the square root loops in detail Mono, UnityEngine.Mathf.Sqrt As the original tweet says, Unity’s Mathf.Sqrt is implemented like this: public static float Sqrt(float f) => (float)Math.Sqrt((double)f); – it just calls into double precision System.Math.Sqrt. But if you look at the actual JIT’ed machine code generated by Mono, you can see that there is way more float⭤double conversions going on. I have used Sebastian Schöner’s Asm Explorer tool to see the generated code.

§5 Human · 4%

Given this C# code: const int N = 10000000; public static float UnityMathf(float v) { for (int i = 0; i < N; ++i) { v += UnityEngine.Mathf.Sqrt(v); } return v; }

the loop body ends up being this: loop: movss xmm0, dword [rsp+0x10] ; xmm0 = v, as float cvtss2sd xmm0, xmm0 ; xmm0 = (double)v, left side of v + sqrt(v)

movss xmm1, dword [rsp+0x10] ; xmm1 = v, as float again, argument for sqrt cvtss2sd xmm1, xmm1 ; xmm1 = (double)v

cvtsd2ss xmm5, xmm1 ; xmm5 = (float)(double)v, rounded back to float movss [rsp+0x8], xmm5 ; store temporary float argument

movss xmm1, dword [rsp+0x8] ; xmm1 = temporary float argument cvtss2sd xmm1, xmm1 ; xmm1 = (double)temporary float

movsd [rsp-0x8], xmm1 ; store double for x87 sqrt input fld qword [rsp-0x8] ; push double onto x87 stack fsqrt ; ST(0) = sqrt(ST(0)) fstp qword [rsp-0x8] ; store sqrt result as double and pop x87 stack

movsd xmm1, qword [rsp-0x8] ; xmm1 = sqrt result, as double cvtsd2ss xmm1, xmm1 ; xmm1 = (float)sqrt result cvtss2sd xmm1, xmm1 ; xmm1 =

§6 Human · 7%

(double)(float)sqrt result

cvtsd2ss xmm5, xmm1 ; xmm5 = sqrt result rounded to float movss [rsp+0x8], xmm5 ; store temporary sqrt float

movss xmm1, dword [rsp+0x8] ; xmm1 = temporary sqrt float cvtss2sd xmm1, xmm1 ; xmm1 = (double)temporary sqrt float

cvtsd2ss xmm5, xmm1 ; xmm5 = sqrt result rounded to float again movss [rsp+0x8], xmm5 ; store temporary sqrt float again

movss xmm1, dword [rsp+0x8] ; xmm1 = temporary sqrt float again cvtss2sd xmm1, xmm1 ; xmm1 = (double)sqrt result

addsd xmm0, xmm1 ; xmm0 = (double)v + (double)Mathf.Sqrt(v)

cvtsd2ss xmm5, xmm0 ; xmm5 = final iteration result, round to float movss [rsp+0x10], xmm5 ; v = final iteration result

inc esi ; ++i cmp esi, 0x989680 ; compare i against 10000000 jl loop ; if i < N, continue loop

If this were C#, it would be like v += UnityEngine.Mathf.Sqrt(v) actually expands to: double lhs = (double)v;

double t0 = (double)v; float t1 = (float)t0; float stackFloat0 = t1; float t2 = stackFloat0; double sqrtInput = (double)t2;

double stackDouble0 = sqrtInput; double sqrtDouble = X87_Fsqrt(stackDouble0); // represents x87 FPU fsqrt instruction

float t3 = (float)sqrtDouble;

§7 Human · 2%

double t4 = (double)t3; float t5 = (float)t4; float stackFloat1 = t5; float t6 = stackFloat1; double t7 = (double)t6; float t8 = (float)t7; float stackFloat2 = t8; float t9 = stackFloat2; double rhs = (double)t9;

double sum = lhs + rhs; float result = (float)sum; v = result;

That’s… not exactly great, to put it mildly. Unity is planning to switch to “actual .NET” (CoreCLR) really soon now (see Path to CoreCLR GDC 2026 talk) and codegen should get much better then. Meanwhile, I am rediscovering the same things as what Sebastian Schöner did, but he is also trying to do something about it – see Better codegen for Unity games on Mono blog post. Using Unity.Mathematics.math.sqrt is a tiny bit better codegen than above, but not by much. Mono, System.MathF.Sqrt const int N = 10000000; public static float UnityMathf(float v) { for (int i = 0; i < N; ++i) { v += System.MathF.Sqrt(v); } return v; }

the loop body ends up being this: loop: movss xmm0, dword [rbp-0x10] ; xmm0 = v, as float cvtss2sd xmm0, xmm0 ; xmm0 = (double)v movsd [rbp-0x18], xmm0 ; save old v as double for later addition

movss xmm0, dword [rbp-0x10] ; xmm0 = v, as float again,