2017-01-06 3 views
1

私はC#コンパイラのいくつかの奇妙な動作に遭遇したようです。実行メソッドの開始時にC#クロージャのヒープ割り当てが起こっています

static void Main(string[] args) 
{ 
    Foo(false, 8); 
} 

public static void Foo(bool execute, int x) 
{ 
    if (execute) 
    { 
     Task.Run(() => Console.WriteLine(x)); 
    } 
} 

この(リリースで)起こっていくつかの予想外の割り当てを示しています

は、次のサンプル・コードを考えてみましょう。

.method public hidebysig static void 
    Foo(
     bool execute, 
     int32 x 
    ) cil managed 
    { 
    .maxstack 2 
    .locals init (
     [0] class Test.Program/'<>c__DisplayClass1_0' 'CS$<>8__locals0' 
    ) 

    IL_0000: newobj  instance void Test.Program/'<>c__DisplayClass1_0'::.ctor() 
    IL_0005: stloc.0  // 'CS$<>8__locals0' 
    IL_0006: ldloc.0  // 'CS$<>8__locals0' 
    IL_0007: ldarg.1  // x 
    IL_0008: stfld  int32 Test.Program/'<>c__DisplayClass1_0'::x 

    // [18 13 - 18 25] 
    IL_000d: ldarg.0  // execute 
    IL_000e: brfalse.s IL_0022 

    // [20 17 - 20 54] 
    IL_0010: ldloc.0  // 'CS$<>8__locals0' 
    IL_0011: ldftn  instance void Test.Program/'<>c__DisplayClass1_0'::'<Foo>b__0'() 
    IL_0017: newobj  instance void [mscorlib]System.Action::.ctor(object, native int) 
    IL_001c: call   class [mscorlib]System.Threading.Tasks.Task [mscorlib]System.Threading.Tasks.Task::Run(class [mscorlib]System.Action) 
    IL_0021: pop   

    // [22 9 - 22 10] 
    IL_0022: ret   

    } // end of method Program::Foo 

私はここで何かが足りない、誰もがこの奇妙な振る舞いについての説明がありますん:ILを調べると閉鎖によってトリガヒープの割り当てではなく条件の内側よりも、機能の非常に開始時に表示されていることをすることを示しています? Roslynが実際にそれらを実行するかどうかにかかわらずクロージャを割り当てるコードを生成することは可能でしょうか?

+1

"予想外の割り当てが発生しています" - 期待が間違っていた可能性もあります。 –

答えて

6

この動作は仕様です。

メソッドにクロージャがある場合、クロージャ内で使用されるすべての変数は(ラムダが現在の値にアクセスできるように)クロージャクラスの一部でなければなりません。

コンパイラがクロージャをすぐに割り当てなかった場合、クロージャインスタンスが作成されるときにローカル変数からクロージャクラスのフィールドに値をコピーしなければならないため、時間とメモリが浪費されます。

異なる変数を持つ複数のラムダ(または悪いネストされたスコープ)が同じ変数に近い場合、codegenのリスクが高くなり、複雑になります。

+0

クローズは明らかに、使用する変数を閉じる必要があります。私が理解していないのは、メソッドが実際に使用されたときに、後で延期できないメソッドの最初の部分で何が起こるかです。 –

+0

また、ローカル変数の値がコードの最初にコピーされているようですこの方法では、2番目の文で何を参照しているのか分かりません。 –

+2

ローカルクラスからクロージャクラスへの値のコピーは高価ではなく、意味的に正しいものではありません。クロージャのアイデアは、値*ではなく変数を閉じるということです。値をコピーすると、クロージャの外側のローカル変数への変更は観察されず、クロージャの変更は観察されません。 – Servy

2

SLACKで述べたように、xは関数のパラメータであるため、この動作は設計によるものです。

しかし、割当て条件「へと移動させる」ことができる次のよう

この特定のシナリオで
public static void Foo(bool execute, int x) 
{ 
    if (execute) 
    { 
     int localx = x; 
     Task.Run(() => Console.WriteLine(localx)); 
    } 
} 

、変換は、xがfooの本体内で変更されていないため安全で、またラムダに。また、if文はループ内では実行されません(この場合、変換によって実際に割り当ての数が増える可能性があります)。コンパイラはあなたのためのその分析をしませんが、あなたはすることができます。

+0

ありがとう、そう、私は物事を書き終えた方法です。私は、コンパイラが私のためにこれをしないと主に驚いています、そうすることを妨げる理由があるかどうかを知ることに興味があります。結局のところ、変数が*定義されている最大のレキシカルスコープを知るために字句解析を行っています(私の場合、xはパラメータであるためメソッド本体です)、変数が使用されている最高のレキシカルスコープも検出できます*そこに割り当てます。 –

+0

@ShayRojanskyコンパイラは何もしない理由を必要としません。実際、その逆は真です:デフォルトでは機能は実装されていません。 http://stackoverflow.com/a/8673015/1403794およびhttps://blogs.msdn.microsoft.com/ericlippert/2009/10/05/why-no-extension-properties/およびhttps:// blogsも参照してください。 .msdn.microsoft.com/ericlippert/2009/06/22/why-doesnt-c-implement-top-level-methods /およびhttps://blogs.msdn.microsoft.com/ericlippert/2009/05/18/ foreach-vs-foreach/ –

+0

この種の回答はそれほど役に立ちません。私が尋ねるのは、この最適化を実装しない理由があるかどうか(明らかに)、即座に明確な欠点がない場合のヒープ割り当てを減らすことです。この機能の複雑さは、パフォーマンスの向上や、私が考えていない他のいくつかの問題を保証するものではないかもしれません(これがコミュニティに尋ねる理由です)。誰もそれをしていないという唯一の理由がなければ、それは良いRoslynの問題になるかもしれない。 –

関連する問題