-
Notifications
You must be signed in to change notification settings - Fork 5.3k
Description
Background and motivation
Currently, the Math(F) (or similar methods for float and double) implementation is:
- Environment-dependent. It depends on the underlying C runtime and may return different values in different environments.
- May not be accurate. In most cases, it is accurate to within ±1 ULP.
Of these, the one that could be particularly problematic is "environment dependency."
This affects things like game terrain generation, replay desync issues, and the reproducibility of scientific and technical papers.
Also, such as double.ExpM1(x) are currently implemented in a "simple" way, which has problems with accuracy - they may contain errors larger than 1 ulp.
"Correctly rounded" - i.e., correctly rounded mathematically correct results, with precision to 0.5 ulps - implementations of mathematical functions can solve these problems. Being accurate also means being independent of the environment.
I've ported to C# the implementation from The CORE-MATH project, which implements an correctly rounded function: CoreMathSharp.
The biggest concern here is the execution speed. Since accuracy is required, it is expected to be slower than functions that do not require accuracy.
In practice, however, they were not as slow as feared, and some functions were even faster than the native implementation.
This compares the speed of the CoreMathSharp implementation (blue / left) with the current MathF implementation (red / right). The vertical axis is the average execution time, smaller is faster.
Note that some implementations not included in MathF use simple compatible implementations - e.g. Compound(x, y) == Pow(1 + x, y).
Of course, not everything is overwhelmingly fast, so I understand that this is not a drop-in replacement, but I think you can see that it still has speeds that are practical.
The repository also includes other measurements (especially for Unity's IL2CPP and Burst), so if you're interested, check them out: docs/Performance.md
The advantage of a C# implementation is that it can run in any environment - there is no need to create a native implementation in C or similar for each environment.
By the way, Java has a class called StrictMath that allows you to use the same mathematical functions in any environment, but of course it does not exist in C#.
Related Issues
API Proposal
If implement APIs, it might be better to separate it from the current Math(F) - some functions will contain performance regressions, also to leave room for speed improvements using intrinsics, etc.
In CoreMathSharp, I implemented correctly rounded mathematical functions in the StrictMath(F) class.
APIs
namespace System;
public static class StrictMath
{
public static double Acos(double x);
public static double AcosPi(double x);
public static double Acosh(double x);
public static double Asin(double x);
public static double AsinPi(double x);
public static double Asinh(double x);
public static double Atan(double x);
public static double Atan2(double y, double x);
public static double Atan2Pi(double y, double x);
public static double AtanPi(double x);
public static double Atanh(double x);
public static double Cbrt(double x);
public static double Cos(double x);
public static double CosPi(double x);
public static double Cosh(double x);
public static double Erf(double x);
public static double Erfc(double x);
public static double Exp(double x);
public static double Exp10(double x);
public static double Exp10M1(double x);
public static double Exp2(double x);
public static double Exp2M1(double x);
public static double ExpM1(double x);
public static double Hypot(double x, double y);
public static (double Value, int SignGamma) LGamma(double x);
public static double Log(double x);
public static double Log10(double x);
public static double Log10P1(double x);
public static double Log1P(double x);
public static double Log2(double x);
public static double Log2P1(double x);
public static double Pow(double x);
public static double ReciprocalSqrt(double x);
public static double Sin(double x);
public static (double Sin, double Cos) SinCos(double x);
public static double SinPi(double x);
public static double Sinh(double x);
public static double TGamma(double x);
public static double Tan(double x);
public static double TanPi(double x);
public static double Tanh(double x);
}
public static class StrictMathF
{
public static float Acos(float x);
public static float AcosPi(float x);
public static float Acosh(float x);
public static float Asin(float x);
public static float AsinPi(float x);
public static float Asinh(float x);
public static float Atan(float x);
public static float Atan2(float y, float x);
public static float Atan2Pi(float y, float x);
public static float AtanPi(float x);
public static float Atanh(float x);
public static float Cbrt(float x);
public static float Compound(float x, float y);
public static float Cos(float x);
public static float CosPi(float x);
public static float Cosh(float x);
public static float Erf(float x);
public static float Erfc(float x);
public static float Exp(float x);
public static float Exp10(float x);
public static float Exp10M1(float x);
public static float Exp2(float x);
public static float Exp2M1(float x);
public static float ExpM1(float x);
public static float Hypot(float x, float y);
public static (float Value, int SignGamma) LGamma(float x);
public static float Log(float x);
public static float Log10(float x);
public static float Log10P1(float x);
public static float Log1P(float x);
public static float Log2(float x);
public static float Log2P1(float x);
public static float Pow(float x);
public static float ReciprocalSqrt(float x);
public static float Sin(float x);
public static (float Sin, float Cos) SinCos(float x);
public static float SinPi(float x);
public static float Sinh(float x);
public static float TGamma(float x);
public static float Tan(float x);
public static float TanPi(float x);
public static float Tanh(float x);
}API Usage
// All functions are accessible via StrictMath(F)
double exp = StrictMath.Exp(123.0);
float logf = StrictMathF.Log(123.0f);Alternative Designs
- Replaces existing
Math(F)/float/double.- Personally, that seems risky - as there are performance-first use cases.
- Making
Math.Exp()accurate anddouble.Exp()fast. Or vice versa.- this also seems less intuitive to me.
Risks
If replace an existing Math implementation, the behavior of programs may change.
However, this may not be much of an issue - for example, I've observed that the return value of Math.SinCos().Cos differs between .NET 8 and .NET 10.
One possible issue is that C# allows extended double-precision floating-point arithmetic.
I don't think this is a problem on modern x64 architectures, but there are issues with some x86 (32-bit) architectures where the calculations don't work correctly. I'm not sure if there's a way to address this.
