using System; using System.Collections.Generic; using System.Globalization; namespace Amphora.PerformanceTests { /// /// One kind of angle to take the sine and cosine of, in turns. /// public sealed class AngleSet { public readonly string Name; public readonly double[] Turns; public AngleSet(string name, double[] turns) { Name = name; Turns = turns; } } public static class TrigFunctions { public const int AnglesPerCall = 4096; public const ulong Seed = 12345; public const double TwoPi = 6.283185307179586; /// /// Angles that the table sums behind SinCosOfTurns can't decide the rounding of, which is about one in 12,000 /// of them, so each is settled by the refined sums. /// public static readonly double[] TableSumsUndecided = { 0.060371540937450341, -0.0032893622171726034, 0.1196965866749333, 0.027849406247426156, -0.017364044956709651, -0.07745972957515973, }; /// /// Angles that the refined sums can't decide either, which is about one in a million of them, so each is /// settled by the careful sums. /// public static readonly double[] RefinedSumsUndecided = { 0.01573667903559145, 0.046806654429983718, -0.019004306465221915, -0.043762078557798956, -0.057306436281703954, -0.01341632192702219, }; /// /// Angles that the careful sums can't decide either, which is about one in ten million of them, so each is /// settled by the finest sums. /// public static readonly double[] CarefulSumsUndecided = { -0.035107824791691805, 0.10064772733587615, 0.039632965020027472, -0.1152947132302842, 0.059375927516176541, -0.02706567846181554, }; /// /// Where the results go, so a batch of calls has an effect the JIT has to keep. Storing them, not adding them, /// keeps the loop from paying for arithmetic on a not-a-number or a subnormal. /// public static readonly double[] Sines = new double[AnglesPerCall]; public static readonly double[] Cosines = new double[AnglesPerCall]; private static string Ratio(double control, double candidate) { return (control / candidate).ToString("0.00", CultureInfo.InvariantCulture) + "x"; } private static double[] Draw(Func map) { var generator = new Xoshiro256PlusPlus(Seed); var returnMe = new double[AnglesPerCall]; for (int i = 0; i < returnMe.Length; ++i) { returnMe[i] = map(generator.NextDouble()); } return returnMe; } private static double[] Repeat(double[] values) { var returnMe = new double[AnglesPerCall]; for (int i = 0; i < returnMe.Length; ++i) { returnMe[i] = values[i % values.Length]; } return returnMe; } public static List Sets() { return new List { new AngleSet("(-2, 2) turns", Draw(unit => unit * 4.0 - 2.0)), new AngleSet("up to 1e9 turns", Draw(unit => unit * 1e9)), new AngleSet("up to 4e12 turns", Draw(unit => unit * 4e12)), new AngleSet("tiny, about 1e-150", Draw(unit => (unit + 0.5) * 1e-150)), new AngleSet("2^42 to 2^52 turns", Draw(unit => 4398046511104.0 + unit * 4.4e15)), new AngleSet("about 1e300 turns", Draw(unit => (unit + 1.0) * 1e300)), new AngleSet("not a number", Draw(unit => double.NaN)), new AngleSet("table sums undecided", Repeat(TableSumsUndecided)), new AngleSet("refined sums undecided", Repeat(RefinedSumsUndecided)), new AngleSet("careful sums undecided", Repeat(CarefulSumsUndecided)), }; } public static void StoreAngles(double[] turns) { for (int i = 0; i < turns.Length; ++i) { Sines[i] = turns[i]; Cosines[i] = turns[i]; } } public static void MathSin(double[] turns) { for (int i = 0; i < turns.Length; ++i) { Sines[i] = Math.Sin(turns[i] * TwoPi); } } public static void MathCos(double[] turns) { for (int i = 0; i < turns.Length; ++i) { Cosines[i] = Math.Cos(turns[i] * TwoPi); } } public static void MathSinAndCos(double[] turns) { for (int i = 0; i < turns.Length; ++i) { double angle = turns[i] * TwoPi; Sines[i] = Math.Sin(angle); Cosines[i] = Math.Cos(angle); } } public static void PortableSinCos(double[] turns) { for (int i = 0; i < turns.Length; ++i) { PortableMath.SinCosOfTurns(turns[i], out Sines[i], out Cosines[i]); } } public static void FastSinCos(double[] turns) { for (int i = 0; i < turns.Length; ++i) { PortableMath.FastSinCosOfTurns(turns[i], out Sines[i], out Cosines[i]); } } public static void Run(string filter, int rounds) { Console.WriteLine("Sine and cosine of an angle in turns, " + AnglesPerCall + " angles to a timed call. " + "The fastest nanoseconds an angle, warm, net of a loop that stores the angles, and then the speedup " + "over the faster of one Math.Sin or one Math.Cos on the same angle, the control. SinCosOfTurns " + "gives both, correctly rounded, the same on every machine. FastSinCosOfTurns is exactly rounded for " + "about 94% of angles and never more than a step off. The undecided rows are angles the " + "function does extra work for, which are rare among random ones."); Console.WriteLine(); Console.WriteLine($"{"angles",-24} {"loop ns",8} {"Math.Sin",10} {"Math.Cos",10} {"Sin + Cos",20} " + $"{"SinCosOfTurns",22} {"FastSinCosOfTurns",22}"); foreach (AngleSet set in Sets()) { if (set.Name.IndexOf(filter, StringComparison.OrdinalIgnoreCase) < 0) { continue; } double[] turns = set.Turns; var actions = new List { () => MathSin(turns), () => MathCos(turns), () => MathSinAndCos(turns), () => PortableSinCos(turns), () => FastSinCos(turns), () => StoreAngles(turns), }; Timing[] timings = Measurement.TimeTakingTurns(actions, rounds); double floor = timings[5].MinimumNanoseconds / AnglesPerCall; double[] net = new double[5]; for (int i = 0; i < net.Length; ++i) { net[i] = Math.Max(0.01, timings[i].MinimumNanoseconds / AnglesPerCall - floor); } double control = Math.Min(net[0], net[1]); Console.WriteLine($"{set.Name,-24} {floor,8:0.00} {net[0],7:0.00} ns {net[1],7:0.00} ns " + $"{net[2],7:0.00} ns {Ratio(control, net[2]),7} {net[3],9:0.00} ns {Ratio(control, net[3]),7} " + $"{net[4],9:0.00} ns {Ratio(control, net[4]),7}"); } Console.WriteLine(); } } }