Posted on Jun 11, 2026 A tweet by @VehiclePhysics (https://x.com/VehiclePhysics/status/2064654819663433980) 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 (https://docs.unity3d.com/Packages/com.unity.mathematics@1.3/manual/index.html), 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! 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 (https://aras-p.info/blog/2018/03/28/Daily-Pathtracer-Part-3-CSharp-Unity-Burst/), and then Miguel de Icaza did an explanatory blog post (https://tirania.org/blog/archive/2018/Apr-11.html), 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: 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.Mathf282ms, System.MathF186ms. Whoa indeed, this is way faster! https://aras-p.info/img/blog/2026/unity-float-script-debug.png But hey! Back in 2018 (https://aras-p.info/blog/2018/03/28/Daily-Pathtracer-Part-3-CSharp-Unity-Burst/#lets-do-unity-now) 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.Mathf242ms, System.MathF149ms. 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 (https://docs.unity3d.com/Packages/com.unity.mathematics@1.3/manual/index.html). And have timings for a player build that uses Mono, plus timings for an IL2CPP scripting backend (https://docs.unity3d.com/6000.3/Documentation/Manual/il2cpp-introduction.html). And while at it, also test performance of the same code under Burst compiler (https://docs.unity3d.com/6000.3/Documentation/Manual/script-compilation-burst.html). And for a complete picture, the same loop, using System.MathF.Sqrt (C#) or sqrtf() (C++) in non-Unity implementations / runtimes: 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. • 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 (https://github.com/sschoener/unity-asm-explorer-package) tool to see the generated code. Given this C# code: the loop body ends up being this: If this were C#, it would be like v += UnityEngine.Mathf.Sqrt(v) actually expands to: 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 (https://www.youtube.com/watch?v=_t6xVfrmEWU) 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 (https://blog.s-schoener.com/2026-03-31-better-mono/) blog post. Using Unity.Mathematics.math.sqrt is a tiny bit better codegen than above, but not by much. • *Mono, System.MathF.Sqrt** the loop body ends up being this: and the assembly of the actual System.MathF.Sqrt function is: it is effectively this: There are still a bunch of float⭤double conversions! But way fewer, and instead of using the ancient x87 FPU, this now uses the scalar SSE square root instruction. • *Burst, UnityEngine.Mathf.Sqrt** Under Burst, the v += UnityEngine.Mathf.Sqrt(v) inner loop part faithfully translates to: i.e. it does pretty much what you would expect, given Mathf.Sqrt implementation. • *Burst, Unity.Mathematics.math.sqrt** The v += Unity.Mathematics.math.sqrt(v) under Burst translates to just: This is basically what you would want to happen. This is somewhat curious though, since underlying math.sqrt code is actually public static float sqrt(float x) { return (float)System.Math.Sqrt((float)x); } – i.e. without Burst, it does end up calling into double precision function. But Burst gives this some sort of special treatment, that it does not do for the previous case, I guess. And again, no System.MathF.Sqrt test with Burst, since it just fails if you try to use that. • *IL2CPP, UnityEngine.Mathf.Sqrt** Unity’s IL2CPP scripting backend (https://docs.unity3d.com/6000.3/Documentation/Manual/il2cpp-introduction.html) translates .NET bytecode into C++, and then relies on a regular C++ compiler to carry out optimizations. For the Mathf.Sqrt code path, it does seem to actually give it special treatment – it does not call the double precision square root, even if on C# level it does do double precision. This is the opposite of what Burst does, and I guess this is another example of “you ship your org chart” in action. The inner loop in generated C++ code is: which then the C++ compiler (MSVC 2022 v17.14, Release build config) actually unrolls to do ten square roots per iteration, with each square root snippet being this: This is not a simple “just use sqrtss”, it only uses the instruction for valid inputs, and calls into “full” function for others (to set errno or deal with exceptions, I guess). You could argue that this is less optimal codegen than what Burst does, in practice on this benchmark it does not matter though. • *IL2CPP, System.MathF.Sqrt** Now, for System.MathF the IL2CPP codegen is slightly different: – why yes, that is the il2cpp_codegen_runtime_class_init_inline call inside the hot inner loop. What that does, is it checks some flag and if it is not set, calls some other function. Some sort of “lazy C# class initialization”, that for some reason is not needed in the previous case, but is needed here. In assembly, this looks very much like above, except now the loop body is not “tiny enough” so MSVC compiler does not do ten square roots per each actual loop iteration; it does only one. And before each square root, it does this: Now again, for this particular benchmark it does not matter (the memory address it checks is very much in the cache, and the branch is perfectly predictable). But if you are calling System.MathF.Sqrt outside of tiny inner loops, then each.and.every.call will have this extra memory fetch and a branch. • *IL2CPP, Unity.Mathematics.math.sqrt** For the Mathematics.math.sqrt case, things get slightly weirder under IL2CPP: 1) instead of one “some sort of lazy initialization” branch like in case above, now it has two branches for each and every call, and 2) the actual square root is done in double precision. Generated C++ code: which then translates into this assembly for the inner loop: Again, for this benchmark the two extra branches do not matter, but they might if you are calling math.sqrt not from inside of a tiny loop body. What does matter, and why under IL2CPP this is slower, is that the square root is done at double precision. So there! Unity math is complex! Well, that was something. Is the original advice of prefer System.MathF over UnityEngine.Mathf valid? Yes, unless you want Burst; there it simply does not work. My takeaways: • I hope the upcoming switch to .NET / CoreCLR will clear up a lot of that mess, especially in the “even if you don’t spell out doubles anywhere in your code, Mono does everything in doubles in Unity”. And even without double precision, the Mono codegen is… not great. • Unity is quite inconsistent in how it treats precision of various math functions. Some of them are implemented as-if they were double precision, but IL2CPP and Burst magically treat them as single precision. Sometimes IL2CPP and Burst disagree on which ones get the special treatment. • Given that CoreCLR switch will have some potential backwards compat breakages anyway, I hope Unity will sanitize the math functions precision treatments in the same go. • It would be nice if you could use “functions that look & feel the same” (like UnityEngine.Mathf.Sqrt, System.MathF.Sqrt and Unity.Mathematics.math.sqrt) as being exactly equivalent, with no preferential treatment of one vs. the other. That is very much not the case today however, and what’s worse, there is no single answer for “which one is best”. It all depends whether you use IL2CPP or Burst, or both, or neither! • If you want best performance now, use Burst and Mathematics maths. • Also, you might want to look into Sebastian’s cpp2better (https://kerntief.net/cpp2better.html), that is aimed at improving IL2CPP codegen. I have not evaluated it in this post however.