using System;
using System.Collections.Generic;
using System.Globalization;
namespace Amphora.PerformanceTests
{
///
/// Stands for the scheduler's entry: a reference with an id to sort by.
///
public sealed class IdNode
{
public int Id;
}
public sealed class ById : IComparer
{
public static readonly ById Instance = new ById();
public int Compare(IdNode first, IdNode second)
{
return first.Id.CompareTo(second.Id);
}
}
///
/// One way of putting a batch of nodes in id order, timed from the batch as the scheduler would gather it.
///
public sealed class SortCandidate
{
public readonly string Name;
public readonly Action Sort;
///
/// Whether the candidate leaves the nodes where they were, so there is no order of them to check.
///
public readonly bool LeavesNodes;
public SortCandidate(string name, Action sort, bool leavesNodes = false)
{
Name = name;
Sort = sort;
LeavesNodes = leavesNodes;
}
}
public static class ItemSorting
{
public static readonly int[] BatchSizes =
{
1, 2, 3, 4, 5, 6, 8, 10, 12, 16, 20, 24, 28, 32, 40, 48, 64, 96, 128, 192, 256, 512, 1024, 4096, 16384,
};
///
/// The ids are out of a population this many times the batch, as when a tenth of the arcs wake.
///
public const int PopulationPerItem = 10;
public const int RunCount = 5;
///
/// The candidate every other is measured against, which is framework code that doesn't change.
///
public const int ControlIndex = 0;
private static string Ratio(double control, double candidate)
{
return (control / candidate).ToString("0.00", CultureInfo.InvariantCulture) + "x";
}
public static List Candidates()
{
var order = new int[BatchSizes[BatchSizes.Length - 1]];
return new List
{
new SortCandidate("comparer (control)",
(keys, nodes, count) => Array.Sort(nodes, 0, count, ById.Instance)),
new SortCandidate("keyed", (keys, nodes, count) => Array.Sort(keys, nodes, 0, count)),
new SortCandidate("Sort.Items", (keys, nodes, count) => Sort.Items(keys, nodes, count, order)),
new SortCandidate("Sort.Order", (keys, nodes, count) => Sort.Order(keys, count, order),
leavesNodes: true),
new SortCandidate("copy only", (keys, nodes, count) => { }),
};
}
///
/// Ids in several ascending runs, which is what a frame's wake batch looks like when arcs fell asleep on
/// different frames.
///
public static int[] Runs(int count)
{
var returnMe = new int[count];
int population = count * PopulationPerItem;
int perRun = (count + RunCount - 1) / RunCount;
for (int i = 0; i < count; ++i)
{
int run = i / perRun;
int position = i % perRun;
returnMe[i] = (int)((position * (long)population / perRun + run * 7) % population);
}
return returnMe;
}
///
/// Ids with no order at all.
///
public static int[] Scattered(int count)
{
var returnMe = new int[count];
int population = count * PopulationPerItem;
for (int i = 0; i < count; ++i)
{
returnMe[i] = (int)(unchecked((uint)(i * 2654435761u)) % (uint)population);
}
return returnMe;
}
///
/// Ids already in order, as a frame's wake batch is when every arc waits the same time.
///
public static int[] Ascending(int count)
{
var returnMe = new int[count];
for (int i = 0; i < count; ++i)
{
returnMe[i] = i * PopulationPerItem;
}
return returnMe;
}
///
/// Ids in exactly the wrong order, which is the worst case for an insertion sort.
///
public static int[] Descending(int count)
{
var returnMe = new int[count];
for (int i = 0; i < count; ++i)
{
returnMe[i] = (count - i) * PopulationPerItem;
}
return returnMe;
}
public static void Run(string filter, int rounds)
{
// The last candidate only copies the batch in. Every candidate pays for that, and at a handful of items
// it is most of what is timed, so it is taken off before comparing.
List candidates = Candidates();
int copyOnlyIndex = candidates.Count - 1;
Console.WriteLine("Sorting a batch of nodes by id, each starting from the same unsorted batch. Every " +
"candidate also copies the batch in, so the time of the copy-only candidate is taken off before " +
"comparing. Per candidate: the fastest nanoseconds a sort, warm, net of the copy, and the speedup " +
"over the control, which is Array.Sort with a comparer. Read the speedups, since they hold as the " +
"machine gets busier or quieter.");
foreach (var input in new (string Name, Func Make)[]
{
("runs", Runs),
("scattered", Scattered),
("ascending", Ascending),
("descending", Descending),
})
{
Console.WriteLine();
string header = $"{"batch (" + input.Name + ")",-20}";
for (int c = 0; c < candidates.Count - 1; ++c)
{
header += $" {candidates[c].Name,18}";
}
header += $" {"copy only ns",13}";
Console.WriteLine(header);
foreach (int size in BatchSizes)
{
string name = $"{size} items, {input.Name}";
if (name.IndexOf(filter, StringComparison.OrdinalIgnoreCase) < 0)
{
continue;
}
int[] template = input.Make(size);
var templateNodes = new IdNode[size];
for (int i = 0; i < size; ++i)
{
templateNodes[i] = new IdNode { Id = template[i] };
}
var actions = new List();
var keyBuffers = new int[candidates.Count][];
var nodeBuffers = new IdNode[candidates.Count][];
for (int c = 0; c < candidates.Count; ++c)
{
keyBuffers[c] = new int[size];
nodeBuffers[c] = new IdNode[size];
int index = c;
actions.Add(() =>
{
Array.Copy(template, keyBuffers[index], size);
Array.Copy(templateNodes, nodeBuffers[index], size);
candidates[index].Sort(keyBuffers[index], nodeBuffers[index], size);
});
}
// Every candidate has to leave the same order, or its timing says nothing.
for (int c = 0; c < candidates.Count; ++c)
{
actions[c]();
if (c == copyOnlyIndex || candidates[c].LeavesNodes)
{
continue;
}
for (int i = 1; i < size; ++i)
{
if (nodeBuffers[c][i - 1].Id > nodeBuffers[c][i].Id)
{
throw new InvalidOperationException(candidates[c].Name + " left " + name +
" out of order.");
}
}
}
Timing[] timings = Measurement.TimeTakingTurns(actions, rounds);
double copyNanoseconds = timings[copyOnlyIndex].MinimumNanoseconds;
double controlNet = Math.Max(1, timings[ControlIndex].MinimumNanoseconds - copyNanoseconds);
string row = $"{size,-20}";
for (int c = 0; c < candidates.Count; ++c)
{
if (c == copyOnlyIndex)
{
continue;
}
double net = Math.Max(1, timings[c].MinimumNanoseconds - copyNanoseconds);
row += $" {net,11:0} {Ratio(controlNet, net),6}";
}
row += $" {copyNanoseconds,13:0}";
Console.WriteLine(row);
}
}
Console.WriteLine();
}
}
}