19

这里有一些 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 而不是简单的 call .ctor ?为了让它更神秘,由 jit-compiler 在 32 位模式下对一个 ctor 调用进行了优化的 newobj+stobj,但在 64 位模式下却没有......

更新:

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

值类型声明表达式,如

Foo foo = new Foo(10)

应该通过编译

call instance 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 declared somewhere before

应该编译成这样的:

.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 对象创建表达式

...

new T(A) 形式的对象创建表达式的运行时处理,其中 T 是类类型或结构类型,A 是可选的参数列表,包括以下步骤:

...

如果 T 是结构类型:

  • T 类型的实例是通过分配一个临时局部变量来创建的。由于结构类型的实例构造函数需要明确地为正在创建的实例的每个字段分配一个值,因此不需要初始化临时变量。

  • 根据函数成员调用规则(第 7.5.4 节)调用实例构造函数。对新分配的实例的引用会自动传递给实例构造函数,并且可以从该构造函数中访问实例作为 t​​his。

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

在这种情况下,对象创建与使用方式的相关性让我感到沮丧,因为 foo1 和 foo2 对我来说看起来相同。

4

2 回答 2

25

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

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)or (10, 20),从不是(10, 0)or (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# 编译器执行复制省略 thenthis并且ref p都是 的别名p,这明显不同于 ifthis是临时的别名!ctor 可以观察到更改是否this会导致更改,ref p如果它们为同一个变量起别名,但如果它们为不同的变量起别名,则不会观察到这一点。

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

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

于 2013-03-04T18:26:26.413 回答
0

那是因为变量foo1foo2不同。

foo1变量只是一个值,但变量foo2既是值又是指针,因为它在使用ref关键字的调用中使用。

foo2变量被初始化时,指针被设置为指向该值,并使用指针的值而不是值的地址调用构造函数。

如果您设置了两种PrintFoo方法,唯一的区别是一种方法具有ref关键字,并分别使用一个变量调用它们:

Foo a = new Foo(10);
Foo b = new Foo(20);
PrintFoo(ref a);
PrintFoo(b);

如果你反编译生成的代码,变量之间的差异是可见的:

&Foo a = new Foo(10);
Foo b = new Foo(20);
Program.PrintFoo(ref a);
Program.PrintFoo(b);
于 2013-03-04T18:28:52.373 回答