I examined four options, and allocating arrays for each record is by for the slowest. I did not check allocating class
objects, as I was going for the faster options.
struct Move {}
stores three integer values into readonly fields of a strcuture.
struct FMove {}
stores an fixed int array of size 3
(int,int,int)
stores a tuple of three int values
int[]
allocates an array of three values.
With the following results. I am tracking time and size allocated in bytes.
Storage |
Iterations |
Time |
Size |
Move struct |
10000000 allocations |
0.2633584 seconds |
120000064 bytes |
FMove struct |
10000000 allocations |
0.3572664 seconds |
120000064 bytes |
Tuple |
10000000 allocations |
0.702174 seconds |
160000064 bytes |
Array |
10000000 allocations |
1.2226393 seconds |
480000064 bytes |
public readonly struct Move
{
public readonly int FromSquare;
public readonly int ToSquare;
public readonly int MoveType;
public Move(int fromSquare, int toSquare, int moveType) : this()
{
FromSquare = fromSquare;
ToSquare = toSquare;
MoveType = moveType;
}
}
public unsafe struct FMove
{
fixed int Data[3];
public FMove(int fromSquare, int toSquare, int moveType) : this()
{
Data[0] = fromSquare;
Data[1] = toSquare;
Data[2] = moveType;
}
public int FromSquare { get => Data[0]; }
public int ToSquare { get => Data[1]; }
public int MoveType { get => Data[2]; }
}
static class Program
{
static void Main(string[] args)
{
// Always compile with Release to time
const int count = 10000000;
Console.WriteLine("Burn-in start");
// Burn-in. Do some calc to spool up
// the CPU
AllocateArray(count/10);
Console.WriteLine("Burn-in end");
double[] timing = new double[4];
// store timing results for four different
// allocation types
var sw = new Stopwatch();
Console.WriteLine("Timming start");
long startMemory;
startMemory = GC.GetTotalMemory(true);
sw.Restart();
var r4 = AllocateArray(count);
sw.Stop();
var s4 = GC.GetTotalMemory(true) - startMemory;
timing[3] = sw.Elapsed.TotalSeconds;
startMemory = GC.GetTotalMemory(true);
sw.Restart();
var r1 = AllocateMove(count);
sw.Stop();
var s1 = GC.GetTotalMemory(true) - startMemory;
timing[0] = sw.Elapsed.TotalSeconds;
startMemory = GC.GetTotalMemory(true);
sw.Restart();
var r2 = AllocateFMove(count);
sw.Stop();
var s2 = GC.GetTotalMemory(true) - startMemory;
timing[1] = sw.Elapsed.TotalSeconds;
startMemory = GC.GetTotalMemory(true);
sw.Restart();
var r3 = AllocateTuple(count);
sw.Stop();
var s3 = GC.GetTotalMemory(true) - startMemory;
timing[2] = sw.Elapsed.TotalSeconds;
Console.WriteLine($"| Storage | Iterations | Time | Size |");
Console.WriteLine($"|---|---|---|---|");
Console.WriteLine($"| Move struct| {r1.Count} allocations | {timing[0]} seconds | {s1} bytes |");
Console.WriteLine($"| FMove struct| {r2.Count} allocations | {timing[1]} seconds | {s2} bytes |");
Console.WriteLine($"| Tuple| {r3.Count} allocations | {timing[2]} seconds | {s3} bytes |");
Console.WriteLine($"| Array| {r4.Count} allocations | {timing[3]} seconds | {s4} bytes |");
}
static List<Move> AllocateMove(int count)
{
var result = new List<Move>(count);
for (int i = 0; i < count; i++)
{
result.Add(new Move(1, 123, 10));
}
return result;
}
static List<FMove> AllocateFMove(int count)
{
var result = new List<FMove>(count);
for (int i = 0; i < count; i++)
{
result.Add(new FMove(1, 123, 10));
}
return result;
}
static List<(int from, int to, int type)> AllocateTuple(int count)
{
var result = new List<(int from, int to, int type)>(count);
for (int i = 0; i < count; i++)
{
result.Add((1, 123, 10));
}
return result;
}
static List<int[]> AllocateArray(int count)
{
var result = new List<int[]>(count);
for (int i = 0; i < count; i++)
{
result.Add(new int[] { 1, 123, 10});
}
return result;
}
}
Based on the comments, I decided to use BenchmarkDotNet
for the above comparison also and the results are quite similar
Method |
Count |
Mean |
Error |
StdDev |
Ratio |
Move |
10000000 |
115.9 ms |
2.27 ms |
2.23 ms |
0.10 |
FMove |
10000000 |
149.7 ms |
2.04 ms |
1.91 ms |
0.12 |
Tuple |
10000000 |
154.8 ms |
2.99 ms |
2.80 ms |
0.13 |
Array |
10000000 |
1,217.5 ms |
23.84 ms |
25.51 ms |
1.00 |
I decided to add a class allocation (called CMove
) with the following definition
public class CMove
{
public readonly int FromSquare;
public readonly int ToSquare;
public readonly int MoveType;
public CMove(int fromSquare, int toSquare, int moveType)
{
FromSquare = fromSquare;
ToSquare = toSquare;
MoveType = moveType;
}
}
And used the above as a baseline for benchmarking. I also tried different allocation sizes. And here is a summary of the results.

Anything below 1.0 means it is faster than CMove
. As you can see array allocations is always bad. For a few allocations it does not matter much, but for a lot of allocations there are clear winners.