using System; using System.Collections.Generic; using System.Globalization; namespace Amphora.PerformanceTests { /// /// One generator's way of drawing each shape of number, a batch at a time. Each returns the total of its draws, so /// the JIT can't drop them as unused. /// public abstract class RandomRows { public const int IntBound = 1000; public const long LongBound = 1000000000000; public const double RangeMinimum = -3.5; public const double RangeMaximum = 7.25; public readonly string Name; protected RandomRows(string name) { Name = name; } public abstract double Bits32(int count); public abstract double Bits64(int count); public abstract double IntBelow(int count); public abstract double LongBelow(int count); public abstract double UnitDouble(int count); public abstract double UnitFloat(int count); public abstract double DoubleRange(int count); public abstract double FloatRange(int count); } /// /// Draws from one of Amphora's generators, held in a field of a class specialized for it, which is how Rand holds /// the one it forwards to. /// public sealed class GeneratorRows : RandomRows where GeneratorType : struct, IRandomGenerator { private GeneratorType generator; public GeneratorRows(string name, GeneratorType generator) : base(name) { this.generator = generator; } public override double Bits32(int count) { ulong total = 0; for (int i = 0; i < count; ++i) { total += generator.NextUInt32(); } return total; } public override double Bits64(int count) { ulong total = 0; for (int i = 0; i < count; ++i) { total += generator.NextUInt64(); } return total; } public override double IntBelow(int count) { ulong total = 0; for (int i = 0; i < count; ++i) { total += (ulong)generator.NextInt(IntBound); } return total; } public override double LongBelow(int count) { ulong total = 0; for (int i = 0; i < count; ++i) { total += (ulong)generator.NextLong(LongBound); } return total; } public override double UnitDouble(int count) { double total = 0; for (int i = 0; i < count; ++i) { total += generator.NextDouble(); } return total; } public override double UnitFloat(int count) { double total = 0; for (int i = 0; i < count; ++i) { total += generator.NextFloat(); } return total; } public override double DoubleRange(int count) { double total = 0; for (int i = 0; i < count; ++i) { total += generator.NextDouble(RangeMinimum, RangeMaximum); } return total; } public override double FloatRange(int count) { double total = 0; for (int i = 0; i < count; ++i) { total += generator.NextFloat((float)RangeMinimum, (float)RangeMaximum); } return total; } } /// /// Draws from System.Random, the control, in whatever way a caller without the right method would. It has no /// 64-bit draw, so two 31-bit ones are joined. It has no long or float range, so a double is scaled. /// public sealed class SystemRandomRows : RandomRows { private readonly Random random; public SystemRandomRows(string name, Random random) : base(name) { this.random = random; } public override double Bits32(int count) { ulong total = 0; for (int i = 0; i < count; ++i) { total += (ulong)random.Next(); } return total; } public override double Bits64(int count) { ulong total = 0; for (int i = 0; i < count; ++i) { total += ((ulong)random.Next() << 32) | (uint)random.Next(); } return total; } public override double IntBelow(int count) { ulong total = 0; for (int i = 0; i < count; ++i) { total += (ulong)random.Next(IntBound); } return total; } public override double LongBelow(int count) { ulong total = 0; for (int i = 0; i < count; ++i) { total += (ulong)(random.NextDouble() * LongBound); } return total; } public override double UnitDouble(int count) { double total = 0; for (int i = 0; i < count; ++i) { total += random.NextDouble(); } return total; } public override double UnitFloat(int count) { double total = 0; for (int i = 0; i < count; ++i) { total += (float)random.NextDouble(); } return total; } public override double DoubleRange(int count) { double total = 0; for (int i = 0; i < count; ++i) { total += RangeMinimum + random.NextDouble() * (RangeMaximum - RangeMinimum); } return total; } public override double FloatRange(int count) { double total = 0; for (int i = 0; i < count; ++i) { total += (float)RangeMinimum + (float)random.NextDouble() * ((float)RangeMaximum - (float)RangeMinimum); } return total; } } /// /// One kind of number to draw, with the loop that draws nothing which its time is measured net of. /// public sealed class DrawShape { public readonly string Name; public readonly Func Draw; public readonly Func Floor; public DrawShape(string name, Func draw, Func floor) { Name = name; Draw = draw; Floor = floor; } } public static class RandomDrawing { public const int DrawsPerCall = 4096; public const ulong Seed = 12345; /// /// The row every other is measured against, which is framework code that doesn't change. /// public const int ControlIndex = 0; /// /// Where the totals go, so a batch of draws has an effect the JIT has to keep. /// public static double Sink; private static string Ratio(double control, double candidate) { return (control / candidate).ToString("0.00", CultureInfo.InvariantCulture) + "x"; } public static double IntegerLoop(int count) { ulong total = 0; for (int i = 0; i < count; ++i) { total += (ulong)i; } return total; } public static double DoubleLoop(int count) { double total = 0; for (int i = 0; i < count; ++i) { total += i; } return total; } public static List Rows() { return new List { new SystemRandomRows("System.Random", new Random((int)Seed)), new GeneratorRows("xoshiro256++", new Xoshiro256PlusPlus(Seed)), new GeneratorRows("xoshiro128++", new Xoshiro128PlusPlus(Seed)), new GeneratorRows("SFC64", new Sfc64(Seed)), new GeneratorRows("SplitMix64", new SplitMix64(Seed)), }; } public static List Shapes() { return new List { new DrawShape("32 bits", (rows, count) => rows.Bits32(count), IntegerLoop), new DrawShape("64 bits", (rows, count) => rows.Bits64(count), IntegerLoop), new DrawShape("int below 1000", (rows, count) => rows.IntBelow(count), IntegerLoop), new DrawShape("long below 10^12", (rows, count) => rows.LongBelow(count), IntegerLoop), new DrawShape("double in [0, 1)", (rows, count) => rows.UnitDouble(count), DoubleLoop), new DrawShape("float in [0, 1)", (rows, count) => rows.UnitFloat(count), DoubleLoop), new DrawShape("double in [-3.5, 7.25)", (rows, count) => rows.DoubleRange(count), DoubleLoop), new DrawShape("float in [-3.5, 7.25)", (rows, count) => rows.FloatRange(count), DoubleLoop), }; } public static void Run(string filter, int rounds) { Console.WriteLine("Drawing random numbers, " + DrawsPerCall + " to a timed call. Per generator: the " + "fastest nanoseconds a draw, warm, net of a loop that draws nothing, and the speedup over the " + "control, which is System.Random. The control has no 64-bit draw, so it joins two 31-bit ones, and " + "no long or float range, so it scales a double."); List rows = Rows(); Console.WriteLine(); string header = $"{"draw",-26} {"loop ns",8} {rows[ControlIndex].Name,18}"; for (int r = 1; r < rows.Count; ++r) { header += $" {rows[r].Name,21}"; } Console.WriteLine(header); foreach (DrawShape shape in Shapes()) { if (shape.Name.IndexOf(filter, StringComparison.OrdinalIgnoreCase) < 0) { continue; } var actions = new List(); foreach (RandomRows row in rows) { RandomRows captured = row; actions.Add(() => Sink = shape.Draw(captured, DrawsPerCall)); } actions.Add(() => Sink = shape.Floor(DrawsPerCall)); Timing[] timings = Measurement.TimeTakingTurns(actions, rounds); double floor = timings[rows.Count].MinimumNanoseconds / DrawsPerCall; double controlNet = Math.Max(0.01, timings[ControlIndex].MinimumNanoseconds / DrawsPerCall - floor); string line = $"{shape.Name,-26} {floor,8:0.00} {controlNet,15:0.00} ns"; for (int r = 1; r < rows.Count; ++r) { double net = Math.Max(0.01, timings[r].MinimumNanoseconds / DrawsPerCall - floor); line += $" {net,10:0.00} ns {Ratio(controlNet, net),7}"; } Console.WriteLine(line); } Console.WriteLine(); } } }