在 C# 中,为什么引用引用类型的结构堆栈的推送速度比包含值类型的结构体慢?

In C#, Why Is A Stack of Structs Referencing Reference Types Slower to Push Than Structs Containing Value Types?

提问人:Alexander Flesher 提问时间:11/11/2023 更新时间:11/11/2023 访问量:69

问:

如果我有一个堆栈,我会期望一个 mystack。Push() 对于作为结构体的 T 来说,性能大致相同,而对于作为引用类型的 T 来说,性能可能会慢一点。当我对此方案进行基准测试时,包含引用类型的结构的性能似乎比包含值的结构略差。尽管两个结构在内存中的大小相同(64 位)。

基准测试是使用 .NET 6 和 benchmarkdotnet 执行的。

这是我的测试结构;请注意,它们在我的 x64 处理器上的大小应该相同:

public readonly record struct RecStruct(long A);

public readonly record struct PtrStruct(UIntPtr A);

public readonly record struct StructRef(object Ref);

这是我的 benchmarkdotnet 项目类

public class StructPerformance
{
    private const int repeats = 1_000_000;

    [Benchmark]
    public Stack<RecStruct> PushStructWithCap()
    {
        var stack = new Stack<RecStruct>(repeats);
        var strct = new RecStruct(0);
        for (int i = 0; i < repeats; i++)
        {
            stack.Push(strct);
        }
        return stack;
    }

    [Benchmark]
    public Stack<PtrStruct> PushPtrStructWithCap()
    {
        var stack = new Stack<PtrStruct>(repeats);
        var ptr = new UIntPtr(0);
        var strct = new PtrStruct(ptr);
        for (int i = 0; i < repeats; i++)
        {
            stack.Push(strct);
        }
        return stack;
    }

    [Benchmark]
    public Stack<StructRef> PushStructRefWithCap()
    {
        var stack = new Stack<StructRef>(repeats);
        var obj = new object();
        var strct = new StructRef(obj);
        for (int i = 0; i < repeats; i++)
        {
            stack.Push(strct);
        }
        return stack;
    }

    [Benchmark]
    public Stack<object> PushObjectWithCap()
    {
        var stack = new Stack<object>(repeats);
        var obj = new object();
        for (int i = 0; i < repeats; i++)
        {
            stack.Push(obj);
        }
        return stack;
    }
}

我的基准测试结果很有趣,似乎表明推送包含对象引用的结构与推送对象一样差,而非托管结构的性能更好。

| Method                | Mean     | Error     | StdDev    | Median   |
|---------------------- |---------:|----------:|----------:|---------:|
| PushStructWithCap    | 2.605 ms | 0.0517 ms | 0.1315 ms | 2.583 ms |
| PushPtrStructWithCap | 2.586 ms | 0.0516 ms | 0.1121 ms | 2.558 ms |
| PushStructRefWithCap | 3.245 ms | 0.0643 ms | 0.1207 ms | 3.234 ms |
| PushObjectWithCap    | 3.231 ms | 0.0630 ms | 0.1018 ms | 3.217 ms |

对于包含引用的结构,在 Stack.Push() 上额外使用 ~ns/op 的原因是什么?

C# 性能 垃圾回收 装箱

评论

0赞 Nick Vidalis 11/11/2023
也许新对象的堆内存分配会导致额外的开销。您可以添加 MemoryDiagnoser 只是为了获得一些额外的统计信息。
1赞 Charlieface 11/12/2023
我猜堆栈/寄存器上参考的额外 GC 跟踪会导致开销。我怀疑这些开销在真正的应用程序中是否很重要。
0赞 Alexander Flesher 11/14/2023
@Charlieface 是的,我相信你是对的。再想一想,这是唯一可能的解释。在测试中看到 GC 的影响是很不错的。如果您将您的评论放在答案中,我会将其标记为正确的评论。
0赞 user20361255 11/20/2023
这可能是由于 GC 写入障碍造成的。与基元相比,托管对象写入速度稍慢,因为它们都通过一个特殊函数来执行一些记账操作。

答: 暂无答案