不同数量的结构参数更改基准测试结果

时间:2019-06-04 01:18:46

标签: c# performance memory benchmarkdotnet

我正在使用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()之间的唯一区别是字典的键类型,一个是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(值v。引用类型)之间的区别。使用struct可以复制数据,但是对象只是通过引用传递项目。字符串是引用类型,因此我可以肯定它不会存储在堆栈中。这些堆栈的存储容量有限,但我认为我还不能接近。通过将字典关键字作为对象,我可以将值类型装箱吗?

所有这些事情,无论两个字典之间的性能差异如何,我都希望结构参数的数量不会改变哪种方法性能更高。如果有人能详细说明会影响这些基准性能的事情,我将不胜感激。

我正在运行dotnet core 2.2.300的Windows计算机上,以发布模式运行基准测试,这里是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 |

1 个答案:

答案 0 :(得分:1)

正如Hans和Ivan都在评论中提到的那样,我忽略了使用结构的价值类型。 C#中的类型主要有两种,引用类型和值类型。

创建引用类型时,局部变量指向堆上存储对象的内存位置。当您将引用类型传递给方法时,只有引用会被传递,而堆上的对象仍会保留在那里。

创建值类型时,它存储在堆栈中。将值类型传递给方法时,将创建该值类型的整个副本,并将该副本传递给方法。

显然,结构越多,具有的数据就越多,因此每次移动时都需要复制更多的数据。解释为什么我的结构基准随着它的变大而表现更差。