Files
OpenNest/OpenNest.Core/Math/PortableMath.cs
T
aj 9627bb9625 perf(math): fully optimize PortableMath from its first call
Without tiering, PortableMath's Sin took 25.5 ns against the runtime's
15.0 ns in Release; marked AggressiveOptimization it takes 17.2 ns (Atan2
18.1 against 17.0). Results are unchanged: the cross-platform digest test
passes in Release and Debug on Linux and in Release on Windows.
2026-11-09 19:58:50 -05:00

446 lines
15 KiB
C#

using System;
using System.Runtime.CompilerServices;
namespace OpenNest.Math
{
/// <summary>
/// Trigonometric functions that return the same bits on every platform. System.Math calls
/// the C runtime, and Windows and glibc differ in the last bit at many angles that are not
/// quarter turns (a 6" cross turned 45° has a corner at 2.82842712474619 on Linux and
/// 2.8284271247461907 on Windows). Nesting turns those bits into different tie and
/// tolerance decisions, so the same
/// job nested differently on the desktop and on Linux. These functions use only additions,
/// multiplications, divisions and square roots, which IEEE 754 defines exactly, so every
/// platform computes the same result. Accuracy is within one or two units in the last place.
/// </summary>
/// <remarks>
/// Sin, Cos, Atan and Atan2 follow fdlibm 5.3 (k_sin.c, k_cos.c, e_rem_pio2.c, s_sin.c,
/// s_cos.c, s_atan.c, e_atan2.c): "Copyright (C) 1993 by Sun Microsystems, Inc. All rights
/// reserved. Developed at SunPro, a Sun Microsystems, Inc. business. Permission to use,
/// copy, modify, and distribute this software is freely granted, provided that this notice
/// is preserved." Arguments beyond ±2^19·π/2 are first reduced by an exact remainder of
/// 2π (as a double), which is deterministic but not a true reduction; geometry never gets
/// there. Tan, Asin and Acos are built from those functions and Sqrt.
/// </remarks>
public static class PortableMath
{
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
public static double Sin(double x)
{
var ix = High(x) & 0x7fffffff;
if (ix <= 0x3fe921fb)
return KernelSin(x, 0, false);
if (ix >= 0x7ff00000)
return double.NaN;
var n = RemPio2(x, out var y0, out var y1);
switch (n & 3)
{
case 0:
return KernelSin(y0, y1, true);
case 1:
return KernelCos(y0, y1);
case 2:
return -KernelSin(y0, y1, true);
default:
return -KernelCos(y0, y1);
}
}
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
public static double Cos(double x)
{
var ix = High(x) & 0x7fffffff;
if (ix <= 0x3fe921fb)
return KernelCos(x, 0);
if (ix >= 0x7ff00000)
return double.NaN;
var n = RemPio2(x, out var y0, out var y1);
switch (n & 3)
{
case 0:
return KernelCos(y0, y1);
case 1:
return -KernelSin(y0, y1, true);
case 2:
return -KernelCos(y0, y1);
default:
return KernelSin(y0, y1, true);
}
}
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
public static double Tan(double x) => Sin(x) / Cos(x);
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
public static double Asin(double x)
{
if (!(System.Math.Abs(x) <= 1))
return double.NaN;
return Atan2(x, System.Math.Sqrt((1 - x) * (1 + x)));
}
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
public static double Acos(double x)
{
if (!(System.Math.Abs(x) <= 1))
return double.NaN;
return Atan2(System.Math.Sqrt((1 - x) * (1 + x)), x);
}
private static readonly double[] AtanHi =
{
4.63647609000806093515e-01,
7.85398163397448278999e-01,
9.82793723247329054082e-01,
1.57079632679489655800e+00,
};
private static readonly double[] AtanLo =
{
2.26987774529616870924e-17,
3.06161699786838301793e-17,
1.39033110312309984516e-17,
6.12323399573676603587e-17,
};
private static readonly double[] AT =
{
3.33333333333329318027e-01,
-1.99999999998764832476e-01,
1.42857142725034663711e-01,
-1.11111104054623557880e-01,
9.09088713343650656196e-02,
-7.69187620504482999495e-02,
6.66107313738753120669e-02,
-5.83357013379057348645e-02,
4.97687799461593236017e-02,
-3.65315727442169155270e-02,
1.62858201153657823623e-02,
};
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
public static double Atan(double x)
{
var hx = High(x);
var ix = hx & 0x7fffffff;
int id;
if (ix >= 0x44100000)
{
// |x| >= 2^66
if (double.IsNaN(x))
return x + x;
return hx > 0 ? AtanHi[3] + AtanLo[3] : -AtanHi[3] - AtanLo[3];
}
if (ix < 0x3fdc0000)
{
// |x| < 0.4375
if (ix < 0x3e200000)
return x; // |x| < 2^-29
id = -1;
}
else
{
x = System.Math.Abs(x);
if (ix < 0x3ff30000)
{
// |x| < 1.1875
if (ix < 0x3fe60000)
{
id = 0;
x = (2.0 * x - 1.0) / (2.0 + x);
}
else
{
id = 1;
x = (x - 1.0) / (x + 1.0);
}
}
else if (ix < 0x40038000)
{
id = 2;
x = (x - 1.5) / (1.0 + 1.5 * x);
}
else
{
id = 3;
x = -1.0 / x;
}
}
var z = x * x;
var w = z * z;
var s1 = z * (AT[0] + w * (AT[2] + w * (AT[4] + w * (AT[6]
+ w * (AT[8] + w * AT[10])))));
var s2 = w * (AT[1] + w * (AT[3] + w * (AT[5] + w * (AT[7] + w * AT[9]))));
if (id < 0)
return x - x * (s1 + s2);
z = AtanHi[id] - ((x * (s1 + s2) - AtanLo[id]) - x);
return hx < 0 ? -z : z;
}
private const double PiO4 = 7.8539816339744827900E-01;
private const double PiO2 = 1.5707963267948965580E+00;
private const double Pi = 3.1415926535897931160E+00;
private const double PiLo = 1.2246467991473531772E-16;
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
public static double Atan2(double y, double x)
{
if (double.IsNaN(x) || double.IsNaN(y))
return x + y;
var hx = High(x);
var ix = hx & 0x7fffffff;
var lx = Low(x);
var hy = High(y);
var iy = hy & 0x7fffffff;
var ly = Low(y);
if (((hx - 0x3ff00000) | lx) == 0)
return Atan(y); // x == 1
var m = ((hy >> 31) & 1) | ((hx >> 30) & 2); // 2 * sign(x) + sign(y)
if ((iy | ly) == 0)
{
// y == ±0
switch (m)
{
case 0:
case 1:
return y;
case 2:
return Pi;
default:
return -Pi;
}
}
if ((ix | lx) == 0)
return hy < 0 ? -PiO2 : PiO2; // x == ±0
if (ix == 0x7ff00000)
{
// x == ±∞
if (iy == 0x7ff00000)
{
switch (m)
{
case 0:
return PiO4;
case 1:
return -PiO4;
case 2:
return 3.0 * PiO4;
default:
return -3.0 * PiO4;
}
}
switch (m)
{
case 0:
return 0.0;
case 1:
return -0.0;
case 2:
return Pi;
default:
return -Pi;
}
}
if (iy == 0x7ff00000)
return hy < 0 ? -PiO2 : PiO2; // y == ±∞
double z;
var k = (iy - ix) >> 20;
if (k > 60)
z = PiO2 + 0.5 * PiLo; // |y / x| > 2^60
else if (hx < 0 && k < -60)
z = 0.0; // |y| / x < -2^60
else
z = Atan(System.Math.Abs(y / x));
switch (m)
{
case 0:
return z;
case 1:
return -z;
case 2:
return Pi - (z - PiLo);
default:
return (z - PiLo) - Pi;
}
}
private const double S1 = -1.66666666666666324348e-01;
private const double S2 = 8.33333333332248946124e-03;
private const double S3 = -1.98412698298579493134e-04;
private const double S4 = 2.75573137070700676789e-06;
private const double S5 = -2.50507602534068634195e-08;
private const double S6 = 1.58969099521155010221e-10;
/// <summary>sin(x + y) for |x| ≤ π/4, y the tail of x.</summary>
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
private static double KernelSin(double x, double y, bool hasTail)
{
var ix = High(x) & 0x7fffffff;
if (ix < 0x3e400000)
return x; // |x| < 2^-27
var z = x * x;
var v = z * x;
var r = S2 + z * (S3 + z * (S4 + z * (S5 + z * S6)));
if (!hasTail)
return x + v * (S1 + z * r);
return x - ((z * (0.5 * y - v * r) - y) - v * S1);
}
private const double C1 = 4.16666666666666019037e-02;
private const double C2 = -1.38888888888741095749e-03;
private const double C3 = 2.48015872894767294178e-05;
private const double C4 = -2.75573143513906633035e-07;
private const double C5 = 2.08757232129817482790e-09;
private const double C6 = -1.13596475577881948265e-11;
/// <summary>cos(x + y) for |x| ≤ π/4, y the tail of x.</summary>
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
private static double KernelCos(double x, double y)
{
var ix = High(x) & 0x7fffffff;
if (ix < 0x3e400000)
return 1.0; // |x| < 2^-27
var z = x * x;
var r = z * (C1 + z * (C2 + z * (C3 + z * (C4 + z * (C5 + z * C6)))));
if (ix < 0x3fd33333)
return 1.0 - (0.5 * z - (z * r - x * y)); // |x| < 0.3
var qx = ix > 0x3fe90000 ? 0.28125 : FromWords(ix - 0x00200000, 0);
var hz = 0.5 * z - qx;
var a = 1.0 - qx;
return a - (hz - (z * r - x * y));
}
private const double InvPio2 = 6.36619772367581382433e-01;
private const double Pio2_1 = 1.57079632673412561417e+00;
private const double Pio2_1t = 6.07710050650619224932e-11;
private const double Pio2_2 = 6.07710050630396597660e-11;
private const double Pio2_2t = 2.02226624879595063154e-21;
private const double Pio2_3 = 2.02226624871116645580e-21;
private const double Pio2_3t = 8.47842766036889956997e-32;
private const double TwoPi = 2 * System.Math.PI;
/// <summary>
/// Reduces x to y0 + y1 in [-π/4, π/4] and returns the number of quarter turns removed.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
private static int RemPio2(double x, out double y0, out double y1)
{
var hx = High(x);
var ix = hx & 0x7fffffff;
if (ix < 0x4002d97c)
{
// |x| < 3π/4: one quarter turn
double z;
if (hx > 0)
{
z = x - Pio2_1;
if (ix != 0x3ff921fb)
{
y0 = z - Pio2_1t;
y1 = (z - y0) - Pio2_1t;
}
else
{
z -= Pio2_2;
y0 = z - Pio2_2t;
y1 = (z - y0) - Pio2_2t;
}
return 1;
}
z = x + Pio2_1;
if (ix != 0x3ff921fb)
{
y0 = z + Pio2_1t;
y1 = (z - y0) + Pio2_1t;
}
else
{
z += Pio2_2;
y0 = z + Pio2_2t;
y1 = (z - y0) + Pio2_2t;
}
return -1;
}
if (ix > 0x413921fb)
{
// Beyond 2^19·π/2: an exact remainder brings it within ±π first.
return RemPio2(System.Math.IEEERemainder(x, TwoPi), out y0, out y1);
}
var t = System.Math.Abs(x);
var n = (int)(t * InvPio2 + 0.5);
var fn = (double)n;
var r = t - fn * Pio2_1;
var w = fn * Pio2_1t;
var j = ix >> 20;
y0 = r - w;
var i = j - ((High(y0) >> 20) & 0x7ff);
if (i > 16)
{
// 2nd iteration: good to 118 bits
t = r;
w = fn * Pio2_2;
r = t - w;
w = fn * Pio2_2t - ((t - r) - w);
y0 = r - w;
i = j - ((High(y0) >> 20) & 0x7ff);
if (i > 49)
{
// 3rd iteration: 151 bits
t = r;
w = fn * Pio2_3;
r = t - w;
w = fn * Pio2_3t - ((t - r) - w);
y0 = r - w;
}
}
y1 = (r - y0) - w;
if (hx < 0)
{
y0 = -y0;
y1 = -y1;
return -n;
}
return n;
}
private static int High(double x) => (int)(BitConverter.DoubleToInt64Bits(x) >> 32);
private static int Low(double x) => (int)BitConverter.DoubleToInt64Bits(x);
private static double FromWords(int high, int low) =>
BitConverter.Int64BitsToDouble(((long)high << 32) | (uint)low);
}
}