不同数量的结构参数改变基准测试结果
Different Number of Struct Parameters Change Benchmark Results
我正在使用 BenchmarkDotNet 对结构相关代码进行基准测试,并注意到我的基准性能取决于我的结构包含的参数数量。
[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>();
}
}
这个简单的基准测试创建结构并将它们添加到字典中(如果字典尚未包含它们)。 StructKey() 和 ObjectKey() 之间的唯一区别是 Dictionary 的键类型,一种是 BoxingStruct,另一种是对象。在此示例中,我的 UserStruct 中只有一个字段。如果我 运行 那我实现了以下结果:
| Method | N | Mean | Allocated |
|---------- |----- |---------:|----------:|
| StructKey | 1000 | 54.85 us | 128.19 KB |
| ObjectKey | 1000 | 61.50 us | 162.32 KB |
现在,如果我向 UserStruct 添加更多元素,我的性能结果就会翻转。
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;
}
结果:
| Method | N | Mean | Allocated |
|---------- |----- |----------:|----------:|
| StructKey | 1000 | 112.00 us | 213.2 KB |
| ObjectKey | 1000 | 90.97 us | 209.2 KB |
现在 StructKey 方法需要更多时间并分配更多内存。但我不知道为什么?我已经多次 运行 并且 运行 8 和 16 参数给出了相似的结果。
我已经阅读了 structs and objects 值与引用类型之间的区别。使用结构,数据被复制,但对象只是通过引用传递项目。 String 是引用类型,所以我很确定它没有存储在堆栈中。堆栈的存储容量有限,但我认为我离它还很远。通过让字典键成为一个对象,我是否在装箱值类型?
综上所述,无论两个字典之间的性能差异如何,我都希望结构参数的数量不会改变哪个方法的性能更高。如果有人能详细说明影响这些基准性能的因素,我将不胜感激。
我在 windows 机器上 运行ning dotnet core 2.2.300,运行ning 基准测试处于发布模式,这里是 Github repo 包含我的基准测试.
编辑
我同时实现了 IEquatable 和 IEqualityComparer,性能实际上变差了,同样的关系仍然存在。使用 1 个 属性 StructKey() 更快并且使用更少的内存,而使用 4 个属性 ObjectKey() 更快并且使用更少的内存。
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 属性 结果:
| Method | N | Mean | Allocated |
|---------- |----- |---------:|----------:|
| StructKey | 1000 | 62.32 us | 128.19 KB |
| ObjectKey | 1000 | 71.11 us | 162.32 KB |
4 个属性结果:
| Method | N | Mean | Allocated |
|---------- |----- |---------:|----------:|
| StructKey | 1000 | 155.5 us | 213.29 KB |
| ObjectKey | 1000 | 109.1 us | 209.2 KB |
正如 Hans 和 Ivan 在评论中提到的那样,我忽略了使用结构的值类型。 C#中有两大类类型,引用类型和值类型。
创建引用类型时,局部变量指向存储对象的堆上的内存位置。将引用类型传递给方法时,只会传递引用,而堆上的对象仍保留在那里。
创建值类型时,它存储在堆栈中。将值类型传递给方法时,会制作该值类型的完整副本,并将该副本传递给方法。
显然,结构拥有的数据越多,在它移动时需要复制的数据就越多。解释为什么我的结构基准测试随着它变大而表现变差。
我正在使用 BenchmarkDotNet 对结构相关代码进行基准测试,并注意到我的基准性能取决于我的结构包含的参数数量。
[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>();
}
}
这个简单的基准测试创建结构并将它们添加到字典中(如果字典尚未包含它们)。 StructKey() 和 ObjectKey() 之间的唯一区别是 Dictionary 的键类型,一种是 BoxingStruct,另一种是对象。在此示例中,我的 UserStruct 中只有一个字段。如果我 运行 那我实现了以下结果:
| Method | N | Mean | Allocated |
|---------- |----- |---------:|----------:|
| StructKey | 1000 | 54.85 us | 128.19 KB |
| ObjectKey | 1000 | 61.50 us | 162.32 KB |
现在,如果我向 UserStruct 添加更多元素,我的性能结果就会翻转。
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;
}
结果:
| Method | N | Mean | Allocated |
|---------- |----- |----------:|----------:|
| StructKey | 1000 | 112.00 us | 213.2 KB |
| ObjectKey | 1000 | 90.97 us | 209.2 KB |
现在 StructKey 方法需要更多时间并分配更多内存。但我不知道为什么?我已经多次 运行 并且 运行 8 和 16 参数给出了相似的结果。
我已经阅读了 structs and objects 值与引用类型之间的区别。使用结构,数据被复制,但对象只是通过引用传递项目。 String 是引用类型,所以我很确定它没有存储在堆栈中。堆栈的存储容量有限,但我认为我离它还很远。通过让字典键成为一个对象,我是否在装箱值类型?
综上所述,无论两个字典之间的性能差异如何,我都希望结构参数的数量不会改变哪个方法的性能更高。如果有人能详细说明影响这些基准性能的因素,我将不胜感激。
我在 windows 机器上 运行ning dotnet core 2.2.300,运行ning 基准测试处于发布模式,这里是 Github repo 包含我的基准测试.
编辑
我同时实现了 IEquatable 和 IEqualityComparer,性能实际上变差了,同样的关系仍然存在。使用 1 个 属性 StructKey() 更快并且使用更少的内存,而使用 4 个属性 ObjectKey() 更快并且使用更少的内存。
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 属性 结果:
| Method | N | Mean | Allocated |
|---------- |----- |---------:|----------:|
| StructKey | 1000 | 62.32 us | 128.19 KB |
| ObjectKey | 1000 | 71.11 us | 162.32 KB |
4 个属性结果:
| Method | N | Mean | Allocated |
|---------- |----- |---------:|----------:|
| StructKey | 1000 | 155.5 us | 213.29 KB |
| ObjectKey | 1000 | 109.1 us | 209.2 KB |
正如 Hans 和 Ivan 在评论中提到的那样,我忽略了使用结构的值类型。 C#中有两大类类型,引用类型和值类型。
创建引用类型时,局部变量指向存储对象的堆上的内存位置。将引用类型传递给方法时,只会传递引用,而堆上的对象仍保留在那里。
创建值类型时,它存储在堆栈中。将值类型传递给方法时,会制作该值类型的完整副本,并将该副本传递给方法。
显然,结构拥有的数据越多,在它移动时需要复制的数据就越多。解释为什么我的结构基准测试随着它变大而表现变差。