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