動的変数があるとパフォーマンスにどのように影響しますか?


127

dynamicC#でのパフォーマンスについて質問があります。私は読んdynamicでコンパイラを再度実行させますが、それは何をしますか?

dynamic変数をパラメーターとして使用してメソッド全体を再コンパイルする必要がありますか、それとも動的な動作/コンテキストを持つ行だけを再コンパイルする必要がありますか?

dynamic変数を使用すると、単純なforループが2桁遅くなることに気づきました。

私が遊んだコード:

internal class Sum2
{
    public int intSum;
}

internal class Sum
{
    public dynamic DynSum;
    public int intSum;
}

class Program
{
    private const int ITERATIONS = 1000000;

    static void Main(string[] args)
    {
        var stopwatch = new Stopwatch();
        dynamic param = new Object();
        DynamicSum(stopwatch);
        SumInt(stopwatch);
        SumInt(stopwatch, param);
        Sum(stopwatch);

        DynamicSum(stopwatch);
        SumInt(stopwatch);
        SumInt(stopwatch, param);
        Sum(stopwatch);

        Console.ReadKey();
    }

    private static void Sum(Stopwatch stopwatch)
    {
        var sum = 0;
        stopwatch.Reset();
        stopwatch.Start();
        for (int i = 0; i < ITERATIONS; i++)
        {
            sum += i;
        }
        stopwatch.Stop();

        Console.WriteLine(string.Format("Elapsed {0}", stopwatch.ElapsedMilliseconds));
    }

    private static void SumInt(Stopwatch stopwatch)
    {
        var sum = new Sum();
        stopwatch.Reset();
        stopwatch.Start();
        for (int i = 0; i < ITERATIONS; i++)
        {
            sum.intSum += i;
        }
        stopwatch.Stop();

        Console.WriteLine(string.Format("Class Sum int Elapsed {0}", stopwatch.ElapsedMilliseconds));
    }

    private static void SumInt(Stopwatch stopwatch, dynamic param)
    {
        var sum = new Sum2();
        stopwatch.Reset();
        stopwatch.Start();
        for (int i = 0; i < ITERATIONS; i++)
        {
            sum.intSum += i;
        }
        stopwatch.Stop();

        Console.WriteLine(string.Format("Class Sum int Elapsed {0} {1}", stopwatch.ElapsedMilliseconds, param.GetType()));
    }

    private static void DynamicSum(Stopwatch stopwatch)
    {
        var sum = new Sum();
        stopwatch.Reset();
        stopwatch.Start();
        for (int i = 0; i < ITERATIONS; i++)
        {
            sum.DynSum += i;
        }
        stopwatch.Stop();

        Console.WriteLine(String.Format("Dynamic Sum Elapsed {0}", stopwatch.ElapsedMilliseconds));
    }

いいえ、それはコンパイラーを実行しません、それは最初のパスでそれを罰するのを遅くするでしょう。Reflectionにやや似ていますが、オーバーヘッドを最小限に抑えるために以前に行われたことを追跡する多くのスマート機能を備えています。Googleの「動的言語ランタイム」により詳しい洞察を。いいえ、「ネイティブ」ループの速度に近づくことは決してありません。
ハンスパッサント2011

回答:


233

私は動的を読んだので、コンパイラーが再び実行されますが、それは何をしますか。パラメータとして使用されるダイナミックでメソッド全体を再コンパイルする必要がありますか?ダイナミックな動作/コンテキスト(?)

これが契約です。

動的型のプログラム内のすべてのについて、コンパイラーは、操作を表す単一の「動的呼び出しサイトオブジェクト」を生成するコードを発行します。たとえば、次の場合:

class C
{
    void M()
    {
        dynamic d1 = whatever;
        dynamic d2 = d1.Foo();

次に、コンパイラは道徳的にこのようなコードを生成します。(実際のコードはかなり複雑です。これは表示のために簡略化されています。)

class C
{
    static DynamicCallSite FooCallSite;
    void M()
    {
        object d1 = whatever;
        object d2;
        if (FooCallSite == null) FooCallSite = new DynamicCallSite();
        d2 = FooCallSite.DoInvocation("Foo", d1);

これがこれまでどのように機能するかを見てください。Mを何度呼び出しても、呼び出しサイトは1回だけ生成されます。一度生成すると、呼び出しサイトは永久に存続します。呼び出しサイトは、「ここでFooへの動的な呼び出しが行われる」ことを表すオブジェクトです。

さて、コールサイトができたので、呼び出しはどのように機能しますか?

呼び出しサイトは動的言語ランタイムの一部です。DLRは、「うーん、誰かがこのthisオブジェクトでメソッドfooの動的呼び出しを行おうとしています。それについて何か知っていますか?いいえ。それならもっと見つけた方がいいです。」

次に、DLRはd1のオブジェクトに問い合わせて、何か特別なものかどうかを確認します。多分それはレガシーCOMオブジェクト、またはIron Pythonオブジェクト、またはIron Rubyオブジェクト、またはIE DOMオブジェクトです。それらのいずれでもない場合は、通常のC#オブジェクトである必要があります。

これは、コンパイラが再び起動するポイントです。レクサーやパーサーは必要ないため、DLRはメタデータアナライザー、式のセマンティックアナライザー、およびILの代わりに式ツリーを出力するエミッターのみを備えたC#コンパイラの特別なバージョンを起動します。

メタデータアナライザーはReflectionを使用してd1のオブジェクトのタイプを決定し、それをセマンティックアナライザーに渡して、そのようなオブジェクトがメソッドFooで呼び出されたときに何が起こるかを尋ねます。オーバーロード解決アナライザーはそれを理解し、式ツリーを構築します-式ツリーのラムダでFooを呼び出したかのように-それはその呼び出しを表します。

次に、C#コンパイラは、その式ツリーをキャッシュポリシーと共にDLRに返します。通常、このポリシーは「このタイプのオブジェクトを2回目に目にしたときに、再度呼び出すのではなく、この式ツリーを再利用できます」です。次に、DLRは式ツリーでCompileを呼び出します。これにより、式ツリーからILへのコンパイラーが呼び出され、動的に生成されたILのブロックがデリゲートに吐き出されます。

次に、DLRはこのデリゲートを呼び出しサイトオブジェクトに関連付けられたキャッシュにキャッシュします。

次に、デリゲートを呼び出し、Foo呼び出しが発生します。

2回目にMに電話するときは、すでに呼び出しサイトがあります。DLRはオブジェクトに再度問い合わせを行い、オブジェクトが前回と同じタイプの場合、デリゲートをキャッシュからフェッチして呼び出します。オブジェクトのタイプが異なる場合、キャッシュは失われ、プロセス全体が最初からやり直されます。コールのセマンティック分析を行い、結果をキャッシュに保存します。

これは、動的を含むすべての式で発生します。だから例えばあなたが持っている場合:

int x = d1.Foo() + d2;

次に、3つの動的呼び出しサイトがあります。1つはFooへの動的な呼び出し、もう1つは動的な加算、もう1つは動的からintへの動的な変換です。それぞれに独自のランタイム分析と分析結果の独自のキャッシュがあります。

理にかなっていますか?


好奇心から、特別なフラグを標準のcsc.exeに渡すことによって、パーサー/レクサーのない特別なコンパイラバージョンが呼び出されますか?
ローマンロイター、2011

@Eric、あなたがあなたの以前のブログ投稿を紹介して、short、intなどの暗黙の変換について話してくれるので、私を困らせてもらえますか?私が覚えているように、Convert.ToXXXでダイナミックを使用する方法/理由により、コンパイラが起動します。私は詳細を肉付けしていると確信していますが、うまくいけば、私が何を話しているのか知っているでしょう。
Adam Rackis、2011

4
@Roman:いいえ。csc.exeはC ++で記述されており、C#から簡単に呼び出すことができるものが必要でした。また、メインラインコンパイラには独自の型オブジェクトがありますが、リフレクション型オブジェクトを使用できる必要がありました。csc.exeコンパイラからC ++コードの関連部分を抽出し、それらを1行ずつC#に変換してから、DLRが呼び出すライブラリを構築しました。
Eric Lippert、2011

9
@Eric、「私たちはcsc.exeコンパイラからC ++コードの関連部分を抽出し、それらを1行ずつC#に変換しました」それで、人々はRoslynが追求する価値があると考えました:)
ShuggyCoUk

5
@ShuggyCoUk:サービスとしてのコンパイラーを使用するという考えはしばらく前から動き回っていましたが、実際にコード分析のためにランタイムサービスを実行する必要があることは、そのプロジェクトへの大きな推進力でした。
Eric Lippert

107

更新:プリコンパイル済みおよび遅延コンパイル済みのベンチマークを追加

更新2:結局、私は間違っています。完全で正しい答えについては、Eric Lippertの投稿を参照してください。ベンチマークの数値のためにここに残します

*更新3:この質問に対するMark Gravellの回答に基づいて、IL-EmittedおよびLazy IL-Emittedベンチマークを追加しました。

私の知る限りでは、このdynamicキーワードを使用しても、実行時にそれ自体で追加のコンパイルが発生することはありません(動的変数をサポートしているオブジェクトのタイプによっては、特定の状況で実行されると思います)。

パフォーマンスに関してdynamicは、本質的にある程度のオーバーヘッドが発生しますが、あなたが思うほど多くはありません。たとえば、次のようなベンチマークを実行しました。

void Main()
{
    Foo foo = new Foo();
    var args = new object[0];
    var method = typeof(Foo).GetMethod("DoSomething");
    dynamic dfoo = foo;
    var precompiled = 
        Expression.Lambda<Action>(
            Expression.Call(Expression.Constant(foo), method))
        .Compile();
    var lazyCompiled = new Lazy<Action>(() =>
        Expression.Lambda<Action>(
            Expression.Call(Expression.Constant(foo), method))
        .Compile(), false);
    var wrapped = Wrap(method);
    var lazyWrapped = new Lazy<Func<object, object[], object>>(() => Wrap(method), false);
    var actions = new[]
    {
        new TimedAction("Direct", () => 
        {
            foo.DoSomething();
        }),
        new TimedAction("Dynamic", () => 
        {
            dfoo.DoSomething();
        }),
        new TimedAction("Reflection", () => 
        {
            method.Invoke(foo, args);
        }),
        new TimedAction("Precompiled", () => 
        {
            precompiled();
        }),
        new TimedAction("LazyCompiled", () => 
        {
            lazyCompiled.Value();
        }),
        new TimedAction("ILEmitted", () => 
        {
            wrapped(foo, null);
        }),
        new TimedAction("LazyILEmitted", () => 
        {
            lazyWrapped.Value(foo, null);
        }),
    };
    TimeActions(1000000, actions);
}

class Foo{
    public void DoSomething(){}
}

static Func<object, object[], object> Wrap(MethodInfo method)
{
    var dm = new DynamicMethod(method.Name, typeof(object), new Type[] {
        typeof(object), typeof(object[])
    }, method.DeclaringType, true);
    var il = dm.GetILGenerator();

    if (!method.IsStatic)
    {
        il.Emit(OpCodes.Ldarg_0);
        il.Emit(OpCodes.Unbox_Any, method.DeclaringType);
    }
    var parameters = method.GetParameters();
    for (int i = 0; i < parameters.Length; i++)
    {
        il.Emit(OpCodes.Ldarg_1);
        il.Emit(OpCodes.Ldc_I4, i);
        il.Emit(OpCodes.Ldelem_Ref);
        il.Emit(OpCodes.Unbox_Any, parameters[i].ParameterType);
    }
    il.EmitCall(method.IsStatic || method.DeclaringType.IsValueType ?
        OpCodes.Call : OpCodes.Callvirt, method, null);
    if (method.ReturnType == null || method.ReturnType == typeof(void))
    {
        il.Emit(OpCodes.Ldnull);
    }
    else if (method.ReturnType.IsValueType)
    {
        il.Emit(OpCodes.Box, method.ReturnType);
    }
    il.Emit(OpCodes.Ret);
    return (Func<object, object[], object>)dm.CreateDelegate(typeof(Func<object, object[], object>));
}

コードからわかるように、私は単純なno-opメソッドを7つの異なる方法で呼び出そうとしています。

  1. 直接メソッド呼び出し
  2. 使用する dynamic
  3. リフレクションで
  4. Action実行時にプリコンパイルされたを使用する(結果からコンパイル時間を除外する)。
  5. 使用するAction(したがって、コンパイル時間を含む)非スレッドセーフ怠惰な変数を使用して、それが必要とされる最初の時間をコンパイルされることを
  6. テストの前に作成される動的に生成されたメソッドを使用します。
  7. テスト中に遅延インスタンス化される動的に生成されたメソッドを使用します。

単純なループで、それぞれが100万回呼び出されます。タイミングの結果は次のとおりです。

直接:3.4248ms
ダイナミック:45.0728ms
リフレクション:888.4011ms
プリコンパイル:21.9166ms
LazyCompiled:30.2045ms
ILEmitted:8.4918ms
LazyILEmitted:14.3483ms

そのdynamicため、キーワードを使用すると、メソッドを直接呼び出すよりも1桁長くかかりますが、約50ミリ秒で何百万回も操作を完了でき、リフレクションよりもはるかに高速になります。呼び出すメソッドが、いくつかの文字列を組み合わせたり、コレクションで値を検索したりするなど、集中的な処理を行う場合、これらの操作は直接呼び出しと呼び出しの違いをはるかに上回りdynamicます。

パフォーマンスはdynamic、不必要に使用しない多くの正当な理由の1つにすぎませんが、真にdynamicデータを扱う場合は、欠点をはるかに上回る利点を提供できます。

アップデート4

Johnbotのコメントに基づいて、Reflection領域を4つの個別のテストに分割しました。

    new TimedAction("Reflection, find method", () => 
    {
        typeof(Foo).GetMethod("DoSomething").Invoke(foo, args);
    }),
    new TimedAction("Reflection, predetermined method", () => 
    {
        method.Invoke(foo, args);
    }),
    new TimedAction("Reflection, create a delegate", () => 
    {
        ((Action)method.CreateDelegate(typeof(Action), foo)).Invoke();
    }),
    new TimedAction("Reflection, cached delegate", () => 
    {
        methodDelegate.Invoke();
    }),

...そしてここにベンチマーク結果があります:

ここに画像の説明を入力してください

したがって、頻繁に呼び出す必要がある特定のメソッドを事前に決定できる場合、そのメソッドを参照するキャッシュされたデリゲートの呼び出しは、メソッド自体を呼び出すのとほぼ同じくらい高速です。ただし、呼び出すメソッドと同じように呼び出すメソッドを決定する必要がある場合は、そのメソッドのデリゲートを作成すると非常に負荷がかかります。


2
このような詳細な応答、ありがとう!実際の数字も気になっていました。
Sergey Sirotkin、2011

4
さて、動的コードはメタデータインポーター、セマンティックアナライザー、コンパイラーの式ツリーエミッターを起動し、その出力で式ツリーからilコンパイラーを実行するので、開始すると言っても過言ではありません実行時にコンパイラを起動します。レクサーとパーサーを実行しないからといって、ほとんど関係がないように見えます。
Eric Lippert、2011

6
パフォーマンスの数値は、DLRのアグレッシブキャッシングポリシーがどのように成果を上げているかを示しています。たとえば、呼び出しを行うたびに異なる受信タイプがあった場合など、サンプルがおかしなことをした場合、以前にコンパイルされた分析結果のキャッシュを利用できない場合、動的バージョンは非常に遅いことがわかります。しかし、それそれを利用できる場合、神聖な善はこれまでになく速いです。
Eric Lippert、2011

1
エリックの提案によると間抜けな何か。コメントされている行を交換してテストします。8964ms vs 814ms、dynamicもちろん負け:public class ONE<T>{public object i { get; set; }public ONE(){i = typeof(T).ToString();}public object make(int ix){ if (ix == 0) return i;ONE<ONE<T>> x = new ONE<ONE<T>>();/*dynamic x = new ONE<ONE<T>>();*/return x.make(ix - 1);}}ONE<END> x = new ONE<END>();string lucky;Stopwatch sw = new Stopwatch();sw.Start();lucky = (string)x.make(500);sw.Stop();Trace.WriteLine(sw.ElapsedMilliseconds);Trace.WriteLine(lucky);
Brian

1
反射に公正であると方法の情報からデリゲートを作成しますvar methodDelegate = (Action)method.CreateDelegate(typeof(Action), foo);
Johnbot
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.