16

動的メソッドを作成しました-以下を参照してください(仲間のSOユーザーに感謝します)。Func は、IL インジェクションがラムダよりも 2 倍遅い動的メソッドとして作成されたようです。

正確な理由を知っている人はいますか?

(編集: これは VS2010 でリリース x64 としてビルドされました。Visual Studio F5 内からではなく、コンソールから実行してください。)

class Program
{
    static void Main(string[] args)
    {
        var mul1 = IL_EmbedConst(5);
        var res = mul1(4);

        Console.WriteLine(res);

        var mul2 = EmbedConstFunc(5);
        res = mul2(4);

        Console.WriteLine(res);

        double d, acc = 0;

        Stopwatch sw = new Stopwatch();

        for (int k = 0; k < 10; k++)
        {
            long time1;

            sw.Restart();

            for (int i = 0; i < 10000000; i++)
            {
                d = mul2(i);
                acc += d;
            }

            sw.Stop();

            time1 = sw.ElapsedMilliseconds;

            sw.Restart();

            for (int i = 0; i < 10000000; i++)
            {
                d = mul1(i);
                acc += d;
            }

            sw.Stop();

            Console.WriteLine("{0,6} {1,6}", time1, sw.ElapsedMilliseconds);
        }

        Console.WriteLine("\n{0}...\n", acc);
        Console.ReadLine();
    }

    static Func<int, int> IL_EmbedConst(int b)
    {
        var method = new DynamicMethod("EmbedConst", typeof(int), new[] { typeof(int) } );

        var il = method.GetILGenerator();

        il.Emit(OpCodes.Ldarg_0);
        il.Emit(OpCodes.Ldc_I4, b);
        il.Emit(OpCodes.Mul);
        il.Emit(OpCodes.Ret);

        return (Func<int, int>)method.CreateDelegate(typeof(Func<int, int>));
    }

    static Func<int, int> EmbedConstFunc(int b)
    {
        return a => a * b;
    }
}

これが出力です(i7 920の場合)

20
20

25     51
25     51
24     51
24     51
24     51
25     51
25     51
25     51
24     51
24     51

4.9999995E+15...

================================================== ==========================

編集 編集 編集 編集

これがdhtorpeが正しかったことの証明です。より複雑なラムダはその利点を失います。それを証明するコード (これは、Lambda がIL インジェクションとまったく同じパフォーマンスを発揮することを示しています):

class Program
{
    static void Main(string[] args)
    {
        var mul1 = IL_EmbedConst(5);
        double res = mul1(4,6);

        Console.WriteLine(res);

        var mul2 = EmbedConstFunc(5);
        res = mul2(4,6);

        Console.WriteLine(res);

        double d, acc = 0;

        Stopwatch sw = new Stopwatch();

        for (int k = 0; k < 10; k++)
        {
            long time1;

            sw.Restart();

            for (int i = 0; i < 10000000; i++)
            {
                d = mul2(i, i+1);
                acc += d;
            }

            sw.Stop();

            time1 = sw.ElapsedMilliseconds;

            sw.Restart();

            for (int i = 0; i < 10000000; i++)
            {
                d = mul1(i, i + 1);
                acc += d;
            }

            sw.Stop();

            Console.WriteLine("{0,6} {1,6}", time1, sw.ElapsedMilliseconds);
        }

        Console.WriteLine("\n{0}...\n", acc);
        Console.ReadLine();
    }

    static Func<int, int, double> IL_EmbedConst(int b)
    {
        var method = new DynamicMethod("EmbedConstIL", typeof(double), new[] { typeof(int), typeof(int) });

        var log = typeof(Math).GetMethod("Log", new Type[] { typeof(double) });

        var il = method.GetILGenerator();

        il.Emit(OpCodes.Ldarg_0);
        il.Emit(OpCodes.Ldc_I4, b);
        il.Emit(OpCodes.Mul);
        il.Emit(OpCodes.Conv_R8);

        il.Emit(OpCodes.Ldarg_1);
        il.Emit(OpCodes.Ldc_I4, b);
        il.Emit(OpCodes.Mul);
        il.Emit(OpCodes.Conv_R8);

        il.Emit(OpCodes.Call, log);

        il.Emit(OpCodes.Sub);

        il.Emit(OpCodes.Ret);

        return (Func<int, int, double>)method.CreateDelegate(typeof(Func<int, int, double>));
    }

    static Func<int, int, double> EmbedConstFunc(int b)
    {
        return (a, z) => a * b - Math.Log(z * b);
    }
} 
4

3 に答える 3

13

定数 5 が原因でした。なぜそれができるのでしょうか?理由: JIT が定数が 5 であることを認識している場合、imul命令は発行せずに lea を発行し[rax, rax * 4]ます。これはよく知られたアセンブリ レベルの最適化です。しかし、何らかの理由で、このコードの実行速度が遅くなりました。最適化は悲観化でした。

また、クロージャを生成する C# コンパイラは、JIT がその特定の方法でコードを最適化するのを妨げました。

証明: 定数を 56878567 に変更すると、パフォーマンスが変化します。JITed コードを調べると、imul が使用されていることがわかります。

次のように定数 5 をラムダにハードコーディングすることで、これをキャッチできました。

    static Func<int, int> EmbedConstFunc2(int b)
    {
        return a => a * 5;
    }

これにより、JITed x86 を調べることができました。

補足: .NET JIT はデリゲート呼び出しをインライン化しません。誤って推測されたので、これについて言及するだけです。これはコメントの場合です。

Sidenode 2: 完全な JIT 最適化レベルを受け取るには、リリース モードでコンパイルし、デバッガーを接続せずに開始する必要があります。デバッガーは、Release モードであっても最適化が実行されないようにします。

Sidenote 3: EmbedConstFunc にはクロージャーが含まれており、通常は動的に生成されたメソッドよりも遅くなりますが、この「lea」最適化の効果はより多くのダメージを与え、最終的には遅くなります。

于 2012-06-14T21:47:25.223 に答える
2

パフォーマンスの違いは、デバッガーが接続されていないリリース モードで実行されている場合にのみ存在することを考えると、JIT コンパイラーは、発行されたものに対して実行できないラムダ式のネイティブ コードの最適化を行うことができるということしか考えられません。 IL動的機能。

リリース モード (最適化をオン) でコンパイルし、デバッガーをアタッチせずに実行すると、ラムダは、生成された IL 動的メソッドより一貫して 2 倍高速です。

プロセスにアタッチされたデバッガーを使用して同じリリースモード最適化ビルドを実行すると、ラムダのパフォーマンスが、生成された IL 動的メソッドと同等またはそれ以下に低下します。

これら 2 つの実行の唯一の違いは、JIT の動作です。プロセスがデバッグされているとき、JIT コンパイラーは多数のネイティブ コード生成の最適化を抑制して、ネイティブ命令から IL 命令、ソース コード行番号へのマッピング、および積極的なネイティブ命令の最適化によって破棄されるその他の相関関係を保持します。

コンパイラは、入力式グラフ (この場合は IL コード) が特定の非常に特殊なパターンと条件に一致する場合にのみ、特殊なケースの最適化を適用できます。JIT コンパイラーは明らかにラムダ式の IL コード パターンに関する特別な知識を持っており、「通常の」IL コードとは異なるコードをラムダに対して発行しています。

IL 命令が、JIT コンパイラーがラムダ式を最適化する原因となるパターンと正確に一致しない可能性は十分にあります。たとえば、IL 命令は B 値をインライン定数としてエンコードしますが、同様のラムダ式は内部でキャプチャされた変数オブジェクト インスタンスからフィールドをロードします。生成された IL が C# コンパイラで生成されたラムダ式 IL のキャプチャされたフィールド パターンを模倣したとしても、ラムダ式と同じ JIT 処理を受け取るには「十分に近い」ものではない可能性があります。

コメントで述べたように、これは呼び出し/戻りのオーバーヘッドを排除するためのラムダのインライン化が原因である可能性があります。これが事実である場合、インライン化は通常、最も単純な式のみに予約されているため、パフォーマンスのこの違いがより複雑なラムダ式で消えることが期待されます。

于 2012-06-14T22:05:47.053 に答える