【问题标题】:Different Number of Struct Parameters Change Benchmark Results不同数量的结构参数改变基准测试结果
【发布时间】:2019-10-19 12:47:37
【问题描述】:

我正在使用 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 是一种引用类型,所以我相当确定它没有存储在堆栈中。堆栈的存储容量有限,但我认为我还没有接近。通过让字典键成为一个对象,我是否对值类型进行了装箱?

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

我在运行 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 |

【问题讨论】:

  • 当然,装箱更大的结构需要更多的时间来移动数据和更多的内存来存储数据。使用结构类型作为键的字典应使用 IEqualityComparer 来避免该成本。检查this Q+A 是否有非常相似的情况。
  • 我同意这一点,最好采用 Struct 类型而不是对象。但是你知道为什么性能会根据结构参数的数量而改变吗?是不是抄的少了?
  • 我通读了链接的 QA,在我的示例中,是否会从两个 Dictionary 中调用相同的 equals 方法?在链接的 QA 中,提问者实际上在他的不同示例中创建了两个不同的对象,一个字符串和另一个点。而我在这两种情况下都创建了相同的结构类型。另外我不确定这是否可以解释为什么 1 个参数与 4 个参数哪个基准测试更快?
  • 重要的是调用 GetHashCode 和 Equals 的开销。字典可能不得不多次处理哈希冲突。通过使 object 类型的键尽早装箱可以隐藏开销。理想的情况是根本不需要装箱,这需要 IEqualityComparer。
  • 开销会根据结构上的属性数量而变化吗?我知道拳击通常会对性能产生负面影响。我不明白为什么在这两个示例中,使用 1 个属性装箱比使用 4 个属性装箱更快。

标签: c# performance memory benchmarkdotnet


【解决方案1】:

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

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

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

显然,结构的数据越多,在移动时需要复制的数据就越多。解释为什么我的 struct 基准测试随着它变大而表现更差。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2012-03-16
    • 1970-01-01
    • 2017-11-28
    • 1970-01-01
    • 2013-11-19
    • 2023-03-07
    • 2023-04-10
    相关资源
    最近更新 更多