【问题标题】:What is the cost comparison of allocating memory for a reference type vs. a value type in the CLR?在 CLR 中为引用类型与值类型分配内存的成本比较是多少?
【发布时间】:2018-05-21 13:47:38
【问题描述】:

很多次我都提到分配一个对象是昂贵的。

大多数参考是:

  1. 倾斜,或
  2. 不完整,因为他们没有进一步解释具体什么成本是什么以及它如何与任何不是对象的东西进行比较,或者因为引用是上下文的并且想要继续它正在讨论的主要话题或完全不诚实。
  3. 或者是引用隐含地忽略了它们正在比较为对象分配内存的成本。好的,创建一个对象很昂贵,但是反对什么?为什么?如何?多少钱?

我无法找到过去遇到的所有参考资料,但突然发生的一件事让位于我提出这个问题的动机(以及随之而来的挫败感)。它不是权威来源,但在这里:

示例 1

This articlestructs 说:

使用结构可以避免 C# 语言中对象的开销。你 可以组合多个字段。这减少了内存压力。而且它 (有时)提高性能。

现在,我不知道这是在说什么。当然,即使是 CLR 中最简单的对象也会占用 12 个字节,如下所示:

  1. 4 个字节的东西我忘记了,我想是某种标题;
  2. 4 个字节用于指向对象方法表的指针;和
  3. 对象包含的第一个字段需要 4 个字节,即使对象不包含字段或属性也是如此。

structs 和值类型的布局是否不同?当然,structs 也必须有一个方法表和一个标题。那么声明struct 相对于对象的比较成本是多少?或者就此而言,分配对象与声明 any 值类型的成本比较是多少?

示例 2

考虑另一个示例,其中潜在的动机是避免为创建新对象分配内存。

在下面的示例中,代码缓存了一个StringBuilder 以避免重新创建它,即使该对象的所有内容都已被擦除以腾出新内容的位置。这只是新对象的创建,即避免为新对象分配内存。

来自source of KeyValuePair<TKey, TValue>

public override string ToString()
{
    StringBuilder sb = StringBuilderCache.Acquire(0x10);
    sb.Append('[');
    if (this.Key != null)
    {
        sb.Append(this.Key.ToString());
    }
    sb.Append(", ");
    if (this.Value != null)
    {
        sb.Append(this.Value.ToString());
    }
    sb.Append(']');
    return StringBuilderCache.GetStringAndRelease(sb);
}

注意代码的第一行调用StringBuilderCache.Acquire。下面是the code for the StringBuilderCache class,其目的是缓存StringBuilder对象的一个​​实例,以避免重新创建它。

internal static class StringBuilderCache
{
    // Fields
    [ThreadStatic]
    private static StringBuilder CachedInstance;
    private const int MAX_BUILDER_SIZE = 360;

    // Methods
    public static StringBuilder Acquire(int capacity = 0x10);
    public static string GetStringAndRelease(StringBuilder sb);
    public static void Release(StringBuilder sb);
}

下面是这个类的source of the Acquire method。请注意,它只是在清空其内容后返回一个缓存的StringBuilder 实例,前提是该实例以前可用。

public static StringBuilder Acquire(int capacity = 0x10)
{
    if (capacity <= 360)
    {
        StringBuilder cachedInstance = CachedInstance;
        if ((cachedInstance != null) && (capacity <= cachedInstance.Capacity))
        {
            CachedInstance = null;
            cachedInstance.Clear();
            return cachedInstance;
        }
    }
    return new StringBuilder(capacity);
}

示例 3

我刚刚找到了一个权威来源,暗示了上述内容。

MSDN 上的 C# Programming Guide 中的Using Structs 页面,几乎在文章的开头,声明如下:

虽然用class 表示一个点同样方便 自动实现的属性,struct 在某些情况下可能更有效 情景。例如,如果您声明一个包含 1000 个 Point 对象的数组, 您将为引用每个对象分配额外的内存;在 在这种情况下,struct 会更便宜。

那么,关于创建新对象的这种嘘声是什么?

【问题讨论】:

  • 嗯,分配对象并不昂贵。当分配成为瓶颈时,C# 程序将轻松击败 C++ 程序。 访问它们更昂贵。值类型与处理器喜欢使用内存的方式高度兼容,能够将值存储在处理器寄存器中对性能有很大的影响。使用堆栈进行存储很有帮助,因为它总是很热。引用类型的对象需要通过指针进行额外的间接处理,当需要从慢速 RAM 中挖掘对象时,处理器可能会陷入困境。
  • @HansPassant 考虑到这个答案,你所说的很有意义。 (stackoverflow.com/a/45276657/303685) 这家伙做了一个性能测试,只访问 structclass 实例,排除了创建它们的成本,并注意到访问时间的差异。
  • “当然,结构也必须有一个方法表和一个标题”——你为什么这么认为?请记住,结构没有继承的可能性。使用未装箱的结构时,要调用的方法在编译时是已知的。

标签: c# memory-management struct clr


【解决方案1】:

根据我阅读 Polyfun 对该问题的评论中链接的答案,我推断分配新对象与为值类型分配内存所产生的成本差异是由于 CPU 指令数量的差异.

虽然遍历堆栈或在堆栈上分配新内存只是将堆栈指针取消引用这么多字节,但堆上的每次分配都可能涉及遍历堆的大部分,因为堆通常是碎片化的。

我不得不承认这对我来说并不是新知识,而且我在发布这个问题时似乎忘记了这一点。

但是,我想等待有人对此问题提供更全面、更有根据的答案。

【讨论】:

  • 这是错误的。在堆上分配对象就像将堆指针增加对象的大小一样简单。不再。它不会在堆中搜索要填充的片段。如果堆末尾没有新对象的空间,则会发生 GC,压缩堆,然后像以前一样通过递增堆指针来分配对象。
  • @Servy 该死!你说得对。我也忘记了。
  • 我认为您没有考虑的关键是值类型没有分配内存(装箱之外)。值类型进入已为类似它们的实例分配的区域 - 无论是碰巧存在于堆栈中的变量、其他类型的成员等。它们从不他们自己的分配阶段。
  • @Damien_The_Unbeliever 这可能是我不知道并且在问题中提到的事情。什么是分配阶段?为什么它很贵?这可能是我问题的核心。您能否写一个答案来详细说明它并链接到有关它的更多材料?
猜你喜欢
  • 2012-08-06
  • 1970-01-01
  • 2010-11-10
  • 2020-12-30
  • 2015-01-15
  • 1970-01-01
  • 2015-04-29
  • 1970-01-01
  • 2012-02-08
相关资源
最近更新 更多