I'm using BenchmarkDotNet to benchmark struct related code, and noticed that the performance of my benchmark depends on the number of parameters my struct contains.
[MemoryDiagnoser]
public class Runner
{
[Params(1000)]
public int N;
[Benchmark]
public void StructKey()
{
var dictionary = new Dictionary<BoxingStruct, int>(); //only difference
for (int i = 0; i < N; i++)
{
var boxingStruct = MakeBoxingStruct(i);
if (!dictionary.ContainsKey(boxingStruct))
dictionary.Add(boxingStruct, i);
}
}
[Benchmark]
public void ObjectKey()
{
var dictionary = new Dictionary<object, int>(); //only difference
for (int i = 0; i < N; i++)
{
var boxingStruct = MakeBoxingStruct(i);
if (!dictionary.ContainsKey(boxingStruct))
dictionary.Add(boxingStruct, i);
}
}
public BoxingStruct MakeBoxingStruct(int id)
{
var boxingStruct = new BoxingStruct()
{
Id = id,
User = new UserStruct()
{
name = "Test User"
}
};
return boxingStruct;
}
}
public struct BoxingStruct
{
public int Id { get; set; }
public UserStruct User { get; set; }
public override bool Equals(object obj)
{
if (!(obj is BoxingStruct))
return false;
BoxingStruct mys = (BoxingStruct)obj;
return mys.Id == Id;
}
public override int GetHashCode()
{
return Id;
}
}
public struct UserStruct
{
public string name { get; set; }
}
public class Program
{
static void Main(string[] args)
{
var summary = BenchmarkRunner.Run<Runner>();
}
}
This simple benchmark creates structs and adds them to a dictionary if the dictionary doesn't already contain them. The only difference between StructKey() and ObjectKey() is the key type of the Dictionary, one being a BoxingStruct and the other an object. In this example my UserStruct only has one field in it. If I run that I achieve the following results:
| Method | N | Mean | Allocated |
|---------- |----- |---------:|----------:|
| StructKey | 1000 | 54.85 us | 128.19 KB |
| ObjectKey | 1000 | 61.50 us | 162.32 KB |
Now if I add several more elements to the UserStruct, my performance results flip.
public struct UserStruct
{
public string name { get; set; }
public string email { get; set; }
public string phone { get; set; }
public int age { get; set; }
}
public BoxingStruct MakeBoxingStruct(int id)
{
var boxingStruct = new BoxingStruct()
{
Id = id,
User = new UserStruct()
{
name = "Test User",
email = "testemail@gmail.com",
phone = "8293839283",
age = 11110,
}
};
return boxingStruct;
}
Results:
| Method | N | Mean | Allocated |
|---------- |----- |----------:|----------:|
| StructKey | 1000 | 112.00 us | 213.2 KB |
| ObjectKey | 1000 | 90.97 us | 209.2 KB |
Now the StructKey method takes more time and allocates more memory. But I don't know why? I've run this multiple times and running with 8 and 16 parameters gives similar results.
I've read up on the differences between structs and objects, value v. reference type. With structs the data is copied but objects just pass items by reference. String is a reference type so I'm fairly certain that isn't stored on the stack. That stacks have limited storage capacity, but I don't think I'm getting close to that. By have the dictionary key be an object am I boxing the value type?
All those things being said, whatever the performance differences are between the two dictionary's, I would expect the number of struct parameters not to change which method is more performant. I would gladly appreciate if anyone can elaborate what is going on that influences the performance of these benchmarks.
I'm on a windows machine running dotnet core 2.2.300, running benchmarks in release mode, here is a Github repo containing my benchmark.
EDIT
I implemented both IEquatable and IEqualityComparer, performance actually got worse and the same relationship still exists. With 1 property StructKey() is faster and uses less memory, while with 4 properties ObjectKey() is faster and uses less memory.
public struct BoxingStruct : IEqualityComparer<BoxingStruct>, IEquatable<BoxingStruct>
{
public int Id { get; set; }
public UserStruct User { get; set; }
public override bool Equals(object obj)
{
if (!(obj is BoxingStruct))
return false;
BoxingStruct mys = (BoxingStruct)obj;
return Equals(mys);
}
public bool Equals(BoxingStruct x, BoxingStruct y)
{
return x.Id == y.Id;
}
public bool Equals(BoxingStruct other)
{
return Id == other.Id;
}
public override int GetHashCode()
{
return Id;
}
public int GetHashCode(BoxingStruct obj)
{
return obj.Id;
}
}
1 Property Result:
| Method | N | Mean | Allocated |
|---------- |----- |---------:|----------:|
| StructKey | 1000 | 62.32 us | 128.19 KB |
| ObjectKey | 1000 | 71.11 us | 162.32 KB |
4 Properties Result:
| Method | N | Mean | Allocated |
|---------- |----- |---------:|----------:|
| StructKey | 1000 | 155.5 us | 213.29 KB |
| ObjectKey | 1000 | 109.1 us | 209.2 KB |