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

単純な呼び出し .ctor の代わりに newobj/stobj が発行された理由がわかりませんか? さらに不思議なことに、32ビットモードのjit-compilerによって最適化されたnewobj + stobjが1つのctor呼び出しに最適化されましたが、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) 形式の object-creation-expression の実行時処理 (T はクラス型または構造体型、A はオプションの引数リスト) は、次の手順で構成されます。

...

T が構造体型の場合:

  • T 型のインスタンスは、一時的なローカル変数を割り当てることによって作成されます。struct-type のインスタンス コンストラクターは、作成されるインスタンスの各フィールドに確実に値を割り当てる必要があるため、一時変数の初期化は必要ありません。

  • インスタンス コンストラクターは、関数メンバー呼び出しの規則に従って呼び出されます (§7.5.4)。新しく割り当てられたインスタンスへの参照は、インスタンス コンストラクターに自動的に渡され、そのコンストラクター内からインスタンスにアクセスできます。

「一時ローカル変数の割り当て」を強調したい。私の理解では、 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);

その最適化はcopy elisionと呼ばれます。C# コンパイラおよび/またはジッターは、ヒューリスティックを使用してコピー省略が常に不可視であると判断した場合、コピー省略を実行できます。まれに、コピーの省略がプログラムに目に見える変更を引き起こす可能性があり、そのような場合、最適化を使用してはなりません。たとえば、ペアの int 構造体があるとします。

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

ctor が途中でスローしたとしても、pここでは(0, 0)or (10, 20)、 never(10, 0)またはのいずれかであると予想されます。(0, 20)つまり、 への割り当て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両方とものエイリアスです。ctor は、同じ変数に別名を付けた場合、その変更が変更を引き起こすことを観察できますが、別の変数に別名を付けた場合は観察しません。pthisthisref p

C# コンパイラのヒューリスティックは、コピー省略を実行することを決定していますが、プログラム内では実行してfoo1いません。foo2それはref foo2あなたの方法にあることを見て、すぐにあきらめることを決定することです. より洗練された分析を行って、これらのクレイジーなエイリアシング状況のいずれでもないことを判断することもできますが、そうではありません。安価で簡単な方法は、エリジオンを可視化するエイリアシング状況が発生する可能性がわずかにある場合でも、最適化をスキップすることです。コードを生成しnewobj、省略を行うかどうかをジッターに決定させます。

ジッターに関しては、64 ビットと 32 ビットのジッターにはまったく異なるオプティマイザーがあります。どうやら、そのうちの 1 つは C# コンパイラが導入しなかったコピー省略を導入できると判断しており、もう 1 つはそうではありません。

于 2013-03-04T18:26:26.413 に答える
0

これは、変数foo1foo2が異なるためです。

foo1変数は単なる値ですが、変数はキーワードfoo2を使用した呼び出しで使用されるため、値とポインターの両方です。ref

変数が初期化されるfoo2と、ポインターは値を指すように設定され、コンストラクターは値のアドレスではなくポインターの値で呼び出されます。

2 つのメソッドを設定し、一方がキーワードをPrintFoo持っているという唯一の違いを持ち、それぞれ 1 つの変数でそれらを呼び出す場合: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 に答える