是否使用“new"在结构上分配它在堆或堆栈上? [英] Does using "new" on a struct allocate it on the heap or stack?

查看:23
本文介绍了是否使用“new"在结构上分配它在堆或堆栈上?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

当您使用 new 运算符创建类的实例时,内存会在堆上分配.当您使用 new 运算符创建结构的实例时,内存是在堆上还是在堆栈上分配的?

When you create an instance of a class with the new operator, memory gets allocated on the heap. When you create an instance of a struct with the new operator where does the memory get allocated, on the heap or on the stack ?

推荐答案

好的,让我们看看我能不能把这个说得更清楚.

Okay, let's see if I can make this any clearer.

首先,Ash 是对的:问题是不是值类型变量被分配到哪里.这是一个不同的问题 - 答案不仅仅是在堆栈上".它比那更复杂(并且被 C# 2 变得更加复杂).我有一篇关于该主题的文章,如果需要,我将对其进行扩展,但让我们只处理new 运算符.

Firstly, Ash is right: the question is not about where value type variables are allocated. That's a different question - and one to which the answer isn't just "on the stack". It's more complicated than that (and made even more complicated by C# 2). I have an article on the topic and will expand on it if requested, but let's deal with just the new operator.

其次,所有这些实际上取决于您所谈论的级别.我正在查看编译器如何处理源代码,就它创建的 IL 而言.JIT 编译器很有可能会在优化大量逻辑"分配方面做一些聪明的事情.

Secondly, all of this really depends on what level you're talking about. I'm looking at what the compiler does with the source code, in terms of the IL it creates. It's more than possible that the JIT compiler will do clever things in terms of optimising away quite a lot of "logical" allocation.

第三,我忽略了泛型,主要是因为我实际上并不知道答案,部分原因是它会使事情变得过于复杂.

Thirdly, I'm ignoring generics, mostly because I don't actually know the answer, and partly because it would complicate things too much.

最后,所有这一切都只是与当前的实现有关.C# 规范没有详细说明这一点 - 它实际上是一个实现细节.有些人认为托管代码开发人员真的不应该关心.我不确定我会走那么远,但值得想象一个实际上所有局部变量都存在于堆上的世界 - 这仍然符合规范.

Finally, all of this is just with the current implementation. The C# spec doesn't specify much of this - it's effectively an implementation detail. There are those who believe that managed code developers really shouldn't care. I'm not sure I'd go that far, but it's worth imagining a world where in fact all local variables live on the heap - which would still conform with the spec.

在值类型上使用 new 运算符有两种不同的情况:您可以调用无参数构造函数(例如 new Guid())或有参数构造函数(例如new Guid(someString)).这些产生显着不同的IL.要了解原因,您需要比较 C# 和 CLI 规范:根据 C#,所有值类型都有一个无参数构造函数.根据 CLI 规范,没有值类型具有无参数构造函数.(有时使用反射获取值类型的构造函数 - 你不会找到无参数的.)

There are two different situations with the new operator on value types: you can either call a parameterless constructor (e.g. new Guid()) or a parameterful constructor (e.g. new Guid(someString)). These generate significantly different IL. To understand why, you need to compare the C# and CLI specs: according to C#, all value types have a parameterless constructor. According to the CLI spec, no value types have parameterless constructors. (Fetch the constructors of a value type with reflection some time - you won't find a parameterless one.)

C# 将用零初始化值"视为构造函数是有意义的,因为它使语言保持一致 - 您可以将 new(...) 视为 总是 调用构造函数.CLI 以不同的方式思考它是有意义的,因为没有真正的代码可以调用 - 当然也没有特定于类型的代码.

It makes sense for C# to treat the "initialize a value with zeroes" as a constructor, because it keeps the language consistent - you can think of new(...) as always calling a constructor. It makes sense for the CLI to think of it differently, as there's no real code to call - and certainly no type-specific code.

在初始化值后,您将如何处理该值也会有所不同.用于

It also makes a difference what you're going to do with the value after you've initialized it. The IL used for

Guid localVariable = new Guid(someString);

与用于以下目的的 IL 不同:

is different to the IL used for:

myInstanceOrStaticVariable = new Guid(someString);

此外,如果该值用作中间值,例如方法调用的参数,情况又略有不同.为了显示所有这些差异,这里有一个简短的测试程序.它没有显示静态变量和实例变量之间的区别:IL 在 stfldstsfld 之间会有所不同,但仅此而已.

In addition, if the value is used as an intermediate value, e.g. an argument to a method call, things are slightly different again. To show all these differences, here's a short test program. It doesn't show the difference between static variables and instance variables: the IL would differ between stfld and stsfld, but that's all.

using System;

public class Test
{
    static Guid field;

    static void Main() {}
    static void MethodTakingGuid(Guid guid) {}


    static void ParameterisedCtorAssignToField()
    {
        field = new Guid("");
    }

    static void ParameterisedCtorAssignToLocal()
    {
        Guid local = new Guid("");
        // Force the value to be used
        local.ToString();
    }

    static void ParameterisedCtorCallMethod()
    {
        MethodTakingGuid(new Guid(""));
    }

    static void ParameterlessCtorAssignToField()
    {
        field = new Guid();
    }

    static void ParameterlessCtorAssignToLocal()
    {
        Guid local = new Guid();
        // Force the value to be used
        local.ToString();
    }

    static void ParameterlessCtorCallMethod()
    {
        MethodTakingGuid(new Guid());
    }
}

这是该类的 IL,不包括不相关的位(例如 nops):

Here's the IL for the class, excluding irrelevant bits (such as nops):

.class public auto ansi beforefieldinit Test extends [mscorlib]System.Object    
{
    // Removed Test's constructor, Main, and MethodTakingGuid.

    .method private hidebysig static void ParameterisedCtorAssignToField() cil managed
    {
        .maxstack 8
        L_0001: ldstr ""
        L_0006: newobj instance void [mscorlib]System.Guid::.ctor(string)
        L_000b: stsfld valuetype [mscorlib]System.Guid Test::field
        L_0010: ret     
    }

    .method private hidebysig static void ParameterisedCtorAssignToLocal() cil managed
    {
        .maxstack 2
        .locals init ([0] valuetype [mscorlib]System.Guid guid)    
        L_0001: ldloca.s guid    
        L_0003: ldstr ""    
        L_0008: call instance void [mscorlib]System.Guid::.ctor(string)    
        // Removed ToString() call
        L_001c: ret
    }

    .method private hidebysig static void ParameterisedCtorCallMethod() cil  managed    
    {   
        .maxstack 8
        L_0001: ldstr ""
        L_0006: newobj instance void [mscorlib]System.Guid::.ctor(string)
        L_000b: call void Test::MethodTakingGuid(valuetype [mscorlib]System.Guid)
        L_0011: ret     
    }

    .method private hidebysig static void ParameterlessCtorAssignToField() cil managed
    {
        .maxstack 8
        L_0001: ldsflda valuetype [mscorlib]System.Guid Test::field
        L_0006: initobj [mscorlib]System.Guid
        L_000c: ret 
    }

    .method private hidebysig static void ParameterlessCtorAssignToLocal() cil managed
    {
        .maxstack 1
        .locals init ([0] valuetype [mscorlib]System.Guid guid)
        L_0001: ldloca.s guid
        L_0003: initobj [mscorlib]System.Guid
        // Removed ToString() call
        L_0017: ret 
    }

    .method private hidebysig static void ParameterlessCtorCallMethod() cil managed
    {
        .maxstack 1
        .locals init ([0] valuetype [mscorlib]System.Guid guid)    
        L_0001: ldloca.s guid
        L_0003: initobj [mscorlib]System.Guid
        L_0009: ldloc.0 
        L_000a: call void Test::MethodTakingGuid(valuetype [mscorlib]System.Guid)
        L_0010: ret 
    }

    .field private static valuetype [mscorlib]System.Guid field
}

如您所见,有许多不同的指令用于调用构造函数:

As you can see, there are lots of different instructions used for calling the constructor:

  • newobj:在堆栈上分配值,调用参数化构造函数.用于中间值,例如用于分配给字段或用作方法参数.
  • call instance:使用一个已经分配好的存储位置(无论是否在栈上).这在上面的代码中用于分配给局部变量.如果使用多个 new 调用多次为同一个局部变量赋值,它只会在旧值的顶部初始化数据 - 它不会分配更多堆栈每次都有空间.
  • initobj:使用一个已经分配的存储位置,只是擦除数据.这用于我们所有的无参数构造函数调用,包括那些分配给局部变量的调用.对于方法调用,有效地引入了一个中间局部变量,其值被initobj擦除.
  • newobj: Allocates the value on the stack, calls a parameterised constructor. Used for intermediate values, e.g. for assignment to a field or use as a method argument.
  • call instance: Uses an already-allocated storage location (whether on the stack or not). This is used in the code above for assigning to a local variable. If the same local variable is assigned a value several times using several new calls, it just initializes the data over the top of the old value - it doesn't allocate more stack space each time.
  • initobj: Uses an already-allocated storage location and just wipes the data. This is used for all our parameterless constructor calls, including those which assign to a local variable. For the method call, an intermediate local variable is effectively introduced, and its value wiped by initobj.

我希望这能说明这个话题有多么复杂,同时也能说明一些问题.在某些概念意义上,对new的每次调用都会在堆栈上分配空间——但正如我们所见,即使在IL级别,这也不是真正发生的事情.我想强调一个特殊的案例.采取这种方法:

I hope this shows how complicated the topic is, while shining a bit of light on it at the same time. In some conceptual senses, every call to new allocates space on the stack - but as we've seen, that isn't what really happens even at the IL level. I'd like to highlight one particular case. Take this method:

void HowManyStackAllocations()
{
    Guid guid = new Guid();
    // [...] Use guid
    guid = new Guid(someBytes);
    // [...] Use guid
    guid = new Guid(someString);
    // [...] Use guid
}

逻辑上"有 4 个堆栈分配 - 一个用于变量,一个用于三个 new 调用中的每一个 - 但实际上(对于该特定代码)堆栈仅分配一次,然后重复使用相同的存储位置.

That "logically" has 4 stack allocations - one for the variable, and one for each of the three new calls - but in fact (for that specific code) the stack is only allocated once, and then the same storage location is reused.

明确地说,这仅在某些情况下是正确的……特别是,如果 Guid 构造函数,则 guid 的值将不可见抛出异常,这就是 C# 编译器能够重用相同堆栈槽的原因.请参阅 Eric Lippert 的 关于值类型构造的博文 了解更多详情以及适用的情况.

Just to be clear, this is only true in some cases... in particular, the value of guid won't be visible if the Guid constructor throws an exception, which is why the C# compiler is able to reuse the same stack slot. See Eric Lippert's blog post on value type construction for more details and a case where it doesn't apply.

我在写这个答案的过程中学到了很多东西 - 如果有任何不清楚的地方,请要求澄清!

I've learned a lot in writing this answer - please ask for clarification if any of it is unclear!

这篇关于是否使用“new"在结构上分配它在堆或堆栈上?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆