c#结构/类堆栈/堆控制?

Posted

技术标签:

【中文标题】c#结构/类堆栈/堆控制?【英文标题】:c# structs/classes stack/heap control? 【发布时间】:2011-01-03 03:03:44 【问题描述】:

所以在 c++ 中这很容易。您希望在堆上分配任何类/结构,请使用 new。如果你想在堆栈上,不要使用 new。

在 C# 中,我们总是使用 new 关键字,根据它是结构还是类,它被分配在堆栈或堆上(结构进入堆栈,类进入堆) - 在某些应用程序中在更改设计以使只有那些对象进入真正属于那里的堆时,可能会产生巨大的性能差异。

我想知道的是——有没有一种直接的方法来控制一个对象的分配位置,而不管它是声明为结构还是类?我知道可以将值类型(结构)装箱以进入堆(但装箱/拆箱是以性能为代价的)。有没有办法在堆栈上分配类?

另外,是否有任何机制来分配原始内存并使用 C++ 中的新位置之类的东西?我知道这与被管理的想法不同 - 但如果您可以使用自定义内存管理,它可以产生很大的性能差异。

我喜欢 C# 的便利,因为它是垃圾收集器和其他东西 - 但有时,在处理应用程序的瓶颈时,可能希望对实际发生的事情有更多的控制。

欢迎任何提示/提示 :)

编辑:性能示例:

struct Foo1

    public int i;
    public float f;
    public double d;


struct Foo2

   public Foo1[] bar;

   public void Init()
        bar = new Foo1[100];
        for (int i = 0; i < 100; i++)
            bar[i] = new Foo1();
    


class Program

    static void Main(string[] args)
    
        DateTime time = DateTime.Now;
        Foo2[] arr = new Foo2[1000000];
        for (int i = 0; i < 1000000; i++)
        
            arr[i] = new Foo2();
            arr[i].Init();
        

        Console.WriteLine((DateTime.Now - time).TotalMilliseconds);
    

这在我的机器上执行需要 1.8 秒(请注意,实际上只有分配正在进行 - 没有参数传递)

如果将 Foo1 从 struct 更改为 class,则执行需要 8.9 秒!慢了五倍

【问题讨论】:

是什么让您相信在堆栈上分配对象与在堆上分配对象之间存在巨大的性能差异? 我注意到当我为 3d 游戏实现物理求解器时 - 我可以通过仔细更改与对象分配位置和对象在函数中传递方式相关的内容来大幅优化性能 您确实知道堆栈和堆本质上是同一块内存(缓存除外),分配是您引用哪个指针作为基址的问题。抱歉,要清楚经典的堆栈/堆本质上是相同的内存块 你想说堆上的分配和栈上的分配一样便宜吗?这不是真的 对于速度的 C# 分配语义不太重要:***.com/questions/477101/… 【参考方案1】:

虽然在一般情况下确实总是在堆上分配对象,但 C# 确实允许您下降到指针级别,以实现繁重的互操作或非常高性能的关键代码。

在unsafe块中,可以使用stackalloc在栈上分配对象,并将其用作指针。

引用他们的例子:

// cs_keyword_stackalloc.cs
// compile with: /unsafe
using System; 

class Test

   public static unsafe void Main() 
   
      int* fib = stackalloc int[100];
      int* p = fib;
      *p++ = *p++ = 1;
      for (int i=2; i<100; ++i, ++p)
         *p = p[-1] + p[-2];
      for (int i=0; i<10; ++i)
         Console.WriteLine (fib[i]);
   

但是请注意,您不需要将整个方法声明为不安全的,您只需为其使用 unsafe ... 块即可。

【讨论】:

请注意,这需要一些特殊权限,因此如果您在不完全信任的环境中运行代码,您可能会遇到麻烦。 Silverlight 就是一个例子。 您的意思是“在一般情况下,对象总是分配在 上”吗? 作为旁注,没有办法“反其道而行之”的原因 - C# 类的一种“放置new” - 是因为类必须是由 GC 跟踪,以及它们引用的所有内容,并且仅使用任何随机内存块都无法(无论如何有效地)实现。所以对于类,你会被堆困住。此外,此答案中描述的技巧不适用于具有引用类型字段的结构(同样,因为 GC 必须能够跟踪它们)。不过,指针类型的字段很好,所以它和普通的 ANSI C 一样富有表现力。 为什么它们必须被 GC 追踪?如果我在堆栈上分配了一些东西,我可以在它超出范围后将其释放 - 或者如果它在数组中使用,因此对象在内存中连续分配,那么我对对象很好删除数组后立即删除 @Blindy:如何使用这种不安全的语法定义 CLASS Foo 的数组变量: - Foo[] myArr; - 这样它就等同于以下 C++ 语法:Foo* myArr = new Foo[100]? (相对于 Foo** myArr = new Foo*[100],这是 C# 默认所做的)【参考方案2】:

不要被new 关键字所迷惑,它对于结构是可选的。

在 C# 中有一个托管世界,您可以在其中享受垃圾收集器和类型安全,而不必(必须)担心许多内存细节。堆栈/堆的区别无关紧要,它与复制语义有关。

对于那些您确实需要控制的极少数情况,C# 的不安全(非托管)部分具有真正的指针和一切。

但是 C# 中的事物成本与 C++ 中的不同,因此不要猎杀幽灵,非托管、短命的对象非常便宜。并且编译器可以在堆栈上分配小数组作为优化,您将无法分辨,也不应该在意。

【讨论】:

但是结构和类的分配时间存在显着的性能差异 - 所以最好在分配时决定在哪里分配,而不是在编写实际类时决定 new... 对于结构来说是可选的”是什么意思?在什么情况下? 我认为他的意思是对于结构,默认构造函数是自动调用的,也没有关键字 new。然而,对于类,变量将是一个 Null 指针,直到显式调用构造函数(使用 new) Mat:“显着的性能差异......”对于托管世界来说不是(同样)正确的。检查你的假设。 Pavel:没有必要用 new 构造一个结构体。但如果没有,C# 的分配跟踪就会发挥作用。【参考方案3】:

这是在 C# 中查看结构和类的错误方法。在 C# 中,结构和类之间的区别不在于分配的位置,而在于复制语义。结构具有值语义,类具有引用语义。 C++ 程序员倾向于对此进行更多阅读,因为他们习惯于堆栈上的对象具有值语义,而堆上的对象具有引用语义。

如何分配此内存是运行时的实现细节。运行时可以使用堆栈、堆或它喜欢的任何其他混合分配方案。虽然确实通常结构会分配在堆栈之类的东西上,而类会分配在某种堆上,但这不是必需的。例如,一个在函数中分配但没有在函数范围之外传递的类可以很容易地在堆栈上分配。

【讨论】:

我确实可以很好地控制复制语义,无论它是结构还是类。我可以使用 deepCopy/shallowCopy 来复制引用类型,我可以使用装箱/拆箱来获取 valuetype 的引用语义,我可以使用 ref 关键字通过引用来接受值类型作为函数参数。但是决定在哪里分配变量对性能也很重要 我并不是说在类和结构之间进行选择是控制复制语义的唯一方法。我是说类/结构仅与复制语义有关,与分配策略无关。您可以通过说服它以不同的方式分配来更改针对特定运行时的性能,但这取决于实现。如果您正在优化性能,这很好。但是,这实际上会产生影响的情况可能比您想象的要少。 但这是我的问题——我的问题是我有说服复制语义的工具,但我没有说服分配行为的工具。并且该类/结构与分配策略无关 - 例如阅读此 msdn 文章:msdn.microsoft.com/en-us/library/aa288471(VS.71).aspx“当您在类上调用 New 运算符时,它将在堆上分配。但是,当您实例化结构时,它是在堆栈上创建的。这将产生性能提升。” 无论这是否是有效的建议,它都不会试图回答他的问题。【参考方案4】:

别担心 - 你的头脑仍然在 c / c++ 世界中,事情的发展方向很重要。 CLR 团队中有一群非常聪明的人,他们整天都在担心如何神奇地快速实现这一目标。

c# 中存在一些陷阱,内存使用通常与意外创建大量微小对象有关(在循环中执行字符串 = 字符串 + 其他字符串是经典)

如果你真的认为你有一个由内存管理引起的性能问题,有一个 memprofiler 会告诉你发生了什么

我已经用 C# 编写了许多性能密集型代码(图形渲染客户端、网络服务器),而且从来不用担心这些

【讨论】:

但尤其是在图形渲染中,这可能会产生很大的不同。例如,如果每个粒子(即使存储在数组中并在死亡时重用)都在堆上自行分配,而不是在一堆内存中分配,那么粒子系统会慢很多 制作一大堆粒子结构(正如其他海报指出的那样,您可以将结构放在任何地方,而不仅仅是堆栈)。你最终会得到一大块连续的内存 完全正确 - 但关键是,必须在编写粒子代码时做出此决定。如果您想使用库中的某个类,则无法选择将其分配在一个连续的块中【参考方案5】:

您对值类型与引用类型的去向(堆栈与堆)的解释并不完全正确。

例如,如果结构是引用类型的成员,它们也可以在堆上分配。或者,如果您在通过对象引用传递它们时将它们装箱。

您应该阅读http://www.yoda.arachsys.com/csharp/memory.html 以更好地了解不同类型的实际分配位置。

另外,在 .Net 中,您真的不应该关心类型的分配位置 - 正如 Eric Lippert 所写:the stack is an implementation detail。您最好理解类型传递的语义(按值、被引用等)。

此外,您似乎暗示在堆上分配对象比在堆栈上更昂贵。实际上,我认为复制值类型的性能成本超过了在堆栈上稍快分配所带来的任何节省。堆栈和堆之间的最大区别在于,在大多数 CPU 架构上,堆栈更有可能保留在 CPU 缓存中,从而避免缓存未命中。

这不是最需要关注的问题。您应该决定该类型是否应该具有按值传递的语义。如果不是 - 那么也许它应该是一个引用类型。

【讨论】:

假设您创建了一个包含某种对象类型的大型数组。无论如何,数组本身都会进入堆。但是-根据我的理解-如果对象类型是结构,则整个事物将分配在堆上的连续内存中(一个数据集群-或可能根据实现进行一些拆分)。但是 - 如果它是一个类,则数组本身的每个元素也将在堆上分配 - 花费更多时间进行分配并使垃圾收集更慢 @Mat:在堆上分配数组这一事实是一个实现细节。然而,它是一个引用数组(即指针,对我们 C++ 人来说)这一事实并非如此。 @Pavel - 正是我的意思(我不在乎数组本身是否在堆上,因为堆上的单个分配不会像每个元素分配一次那样受到伤害) - 所以我认为最好在使用时决定引用类型/值类型的行为,而不是在声明类/结构时决定 记忆文章于2019年消失,现可在:web.archive.org/web/20190124144928/http://www.yoda.arachsys.com/…

以上是关于c#结构/类堆栈/堆控制?的主要内容,如果未能解决你的问题,请参考以下文章

结构总是堆栈分配还是有时堆分配?

通用列表是存储在 C# 中的堆栈还是堆中?

有没有机会,我们可以想象在一个进程(可能是c#)中啥都进入堆栈,啥都进入堆?

数据结构&算法_堆栈(堆栈)队列链表

iOS堆栈内存区别

C#资源回收总结