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();
}
}
}