c# - 为什么 c# 编译器在某些情况下会发出 newobj/stobj 而不是 'call instance .ctor' 来进行结构初始化

标签 c# .net il

这里是一些用 C# 编写的测试程序:

using System;


struct Foo {
    int x;
    public Foo(int x) {
        this.x = x;
    }
    public override string ToString() {
        return x.ToString();
    }
}

class Program {
    static void PrintFoo(ref Foo foo) {
        Console.WriteLine(foo);
    }
    
    static void Main(string[] args) {
        Foo foo1 = new Foo(10);
        Foo foo2 = new Foo(20);
        
        Console.WriteLine(foo1);
        PrintFoo(ref foo2);
    }
}

这里是方法 Main 的反汇编编译版本:

.method private hidebysig static void Main (string[] args) cil managed {
    // Method begins at RVA 0x2078
    // Code size 42 (0x2a)
    .maxstack 2
    .entrypoint
    .locals init (
        [0] valuetype Foo foo1,
        [1] valuetype Foo foo2
    )

    IL_0000: ldloca.s foo1
    IL_0002: ldc.i4.s 10
    IL_0004: call instance void Foo::.ctor(int32)
    IL_0009: ldloca.s foo2
    IL_000b: ldc.i4.s 20
    IL_000d: newobj instance void Foo::.ctor(int32)
    IL_0012: stobj Foo
    IL_0017: ldloc.0
    IL_0018: box Foo
    IL_001d: call void [mscorlib]System.Console::WriteLine(object)
    IL_0022: ldloca.s foo2
    IL_0024: call void Program::PrintFoo(valuetype Foo&)
    IL_0029: ret
} // end of method Program::Main

我不明白为什么发出 newobj/stobj 而不是简单的调用 .ctor ? 更神秘的是,newobj+stobj 在 32 位模式下被 jit-compiler 优化为一个 ctor 调用,但在 64 位模式下它没有...

更新:

为了澄清我的困惑,以下是我的期望。

值类型声明表达式如

Foo foo = new Foo(10)

应该通过

编译

调用实例 void Foo::.ctor(int32)

值类型声明表达式如

Foo foo = default(Foo)

应该通过

编译

initobj Foo

在我看来,构造表达式中的临时变量或默认表达式的实例应被视为目标变量,因为这不会导致任何危险行为

try{
    //foo invisible here
    ...
    Foo foo = new Foo(10);
    //we never get here, if something goes wrong
}catch(...){
    //foo invisible here
}finally{
    //foo invisible here
}

赋值表达式如

foo = new Foo(10);//foo 在之前某处声明过

应该编译成这样:

.locals init (
    ...
    valuetype Foo __temp,
    ...
)

...
ldloca __temp
ldc.i4 10
call instance void Foo::.ctor(int32)
ldloc __temp
stloc foo
...

这是我理解 C# 规范所说的方式:

7.6.10.1 Object creation expressions

...

The run-time processing of an object-creation-expression of the form new T(A), where T is class-type or a struct-type and A is an optional argument-list, consists of the following steps:

...

If T is a struct-type:

  • An instance of type T is created by allocating a temporary local variable. Since an instance constructor of a struct-type is required to definitely assign a value to each field of the instance being created, no initialization of the temporary variable is necessary.

  • The instance constructor is invoked according to the rules of function member invocation (§7.5.4). A reference to the newly allocated instance is automatically passed to the instance constructor and the instance can be accessed from within that constructor as this.

我想强调“分配一个临时本地变量”。 在我的理解中,newobj 指令假设在堆上创建对象......

在这种情况下,对象创建与其使用方式的依赖性让我很失望,因为 foo1 和 foo2 对我来说看起来完全相同。

最佳答案

首先,您应该阅读我关于这个主题的文章。它没有解决您的特定场景,但它有一些很好的背景信息:

https://ericlippert.com/2010/10/11/debunking-another-myth-about-value-types/

好的,既然您已经阅读过,您就知道 C# 规范规定构造结构实例具有以下语义:

  • 创建一个临时变量来存储结构值,初始化为结构的默认值。
  • 将对该临时变量的引用作为构造函数的“this”传递

所以当你说:

Foo foo = new Foo(123);

相当于:

Foo foo;
Foo temp = default(Foo);
Foo.ctor(ref temp, 123); // "this" is a ref to a variable in a struct.
foo1 = temp;

现在,您可能会问,既然我们已经拥有一个变量foo,为什么还要费力地分配一个临时变量呢?就在那里可能是this :

Foo foo = default(Foo);
Foo.ctor(ref foo, 123);

这种优化称为复制省略。当 C# 编译器和/或抖动确定使用他们的启发式方法时,他们可以执行复制省略,因为这样做总是不可见。在极少数情况下,复制省略会导致程序发生可观察到的变化,在这种情况下,不得使用优化。例如,假设我们有一对整数结构:

Pair p = default(Pair);
try { p = new Pair(10, 20); } catch {}
Console.WriteLine(p.First);
Console.WriteLine(p.Second);

我们期望p这是(0, 0)(10, 20) , 从不 (10, 0)(0, 20) ,即使 ctor 中途抛出。也就是说,要么分配给 p是完全构建的值,或者没有对 p 进行修改根本。这里不能进行复制省略;我们必须制作一个临时文件,将临时文件传递给 ctor,然后将临时文件复制到 p .

同样,假设我们有这种精神错乱:

Pair p = default(Pair);
p = new Pair(10, 20, ref p);
Console.WriteLine(p.First);
Console.WriteLine(p.Second);

如果 C# 编译器执行复制省略,则 thisref p都是 p 的别名,这明显不同于如果 this是临时的别名! ctor 可以观察到 this 的变化导致对 ref p 的更改如果他们给同一个变量起了别名,但如果他们给不同的变量起了别名就不会观察到。

C# 编译器启发式决定对 foo1 执行复制省略但不是 foo2在你的程序中。它看到有一个 ref foo2在你的方法中并决定在那里放弃。它可以进行更复杂的分析以确定它在这些疯狂的别名情况之一中,但事实并非如此。如果有任何机会,无论多么遥远,都可能存在使省略可见的混叠情况,那么便宜且容易做的事情就是跳过优化。它生成 newobj编码,让抖动决定是否要进行省略。

至于抖动:64位和32位抖动有完全不同的优化器。显然其中一个决定它可以引入 C# 编译器没有的复制省略,而另一个则没有。

关于c# - 为什么 c# 编译器在某些情况下会发出 newobj/stobj 而不是 'call instance .ctor' 来进行结构初始化,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/15207683/

相关文章:

c# - 跟踪 session 和表单例份验证在服务器上的剩余到期时间

c# - 私有(private)类成员中关键字 'this' 的正确用法是什么?

.net - HTTP 最后更新

c# - 列出 C# 关键字

c# - 通用语法糖或真正的改进

c# - 该帐户无权模拟请求的用户

.net - 在 .NET 中查找子字符串匹配的结尾

c# - 使用 Linq 表达式设置一个 readoly/InitOnly 成员字段

c# - 使用视觉状态时 GroupBox 标题背景不更新