7

私はプロキシに取り組んでおり、参照型パラメータを持つジェネリック クラスの場合、非常に遅くなりました。特にジェネリック メソッドの場合 (約 400 ミリ秒に対して、null を返した単純なジェネリック メソッドの場合は 3200 ミリ秒)。生成されたクラスを C# で書き直した場合にどのように動作するかを試してみることにしました。パフォーマンスははるかに良く、非ジェネリック クラス コードとほぼ同じパフォーマンスでした。

これが私が書いたC#クラスです:: (名前付けスキームによって変更したことに注意してください。

namespace TestData
{
    public class TestClassProxy<pR> : TestClass<pR>
    {
        private InvocationHandler<Func<TestClass<pR>, object>> _0_Test;
        private InvocationHandler<Func<TestClass<pR>, pR, GenericToken, object>> _1_Test;
        private static readonly InvocationHandler[] _proxy_handlers = new InvocationHandler[] { 
            new InvocationHandler<Func<TestClass<pR>, object>>(new Func<TestClass<pR>, object>(TestClassProxy<pR>.s_0_Test)), 
        new GenericInvocationHandler<Func<TestClass<pR>, pR, GenericToken, object>>(typeof(TestClassProxy<pR>), "s_1_Test") };



        public TestClassProxy(InvocationHandler[] handlers)
        {
            if (handlers == null)
            {
                throw new ArgumentNullException("handlers");
            }
            if (handlers.Length != 2)
            {
                throw new ArgumentException("Handlers needs to be an array of 2 parameters.", "handlers");
            }
            this._0_Test = (InvocationHandler<Func<TestClass<pR>, object>>)(handlers[0] ?? _proxy_handlers[0]);
            this._1_Test = (InvocationHandler<Func<TestClass<pR>, pR, GenericToken, object>>)(handlers[1] ?? _proxy_handlers[1]);
        }


        private object __0__Test()
        {
            return base.Test();
        }

        private object __1__Test<T>(pR local1) where T:IConvertible
        {
            return base.Test<T>(local1);
        }

        public static object s_0_Test(TestClass<pR> class1)
        {
            return ((TestClassProxy<pR>)class1).__0__Test();
        }

        public static object s_1_Test<T>(TestClass<pR> class1, pR local1) where T:IConvertible
        {
            return ((TestClassProxy<pR>)class1).__1__Test<T>(local1);
        }

        public override object Test()
        {
            return this._0_Test.Target(this);
        }

        public override object Test<T>(pR local1)
        {
             return this._1_Test.Target(this, local1, GenericToken<T>.Token);
        }
    }
}

これは、生成されたプロキシと同じ IL にリリース モードでコンパイルされます。

namespace TestData
{
    public class TestClass<R>
    {
        public virtual object Test()
        {
            return default(object);
        }

        public virtual object Test<T>(R r) where T:IConvertible
        {
            return default(object);
        }
    }
}

1 つの例外がありました。生成された型に beforefieldinit 属性を設定していませんでした。次の属性を設定していました::public auto ansi

beforefieldinit を使用するとパフォーマンスが大幅に向上するのはなぜですか?

(他の唯一の違いは、パラメーターに名前を付けていなかったことです。これは、物事の壮大なスキームでは実際には問題ではありませんでした。メソッドとフィールドの名前は、実際のメソッドとの衝突を避けるためにスクランブルされています。GenericToken と InvocationHandlers は、無関係な実装の詳細です。引数のために、
GenericToken は文字通り、型付きデータ ホルダーとして使用され、ハンドラーに "T" を送信できるようにします

InvocationHandler はデリゲート フィールド ターゲットの単なるホルダーであり、実際の実装の詳細はありません。

GenericInvocationHandler は、DLR のようなコールサイト手法を使用して、渡されたさまざまな汎用引数を処理するために必要に応じてデリゲートを書き換えます)。

編集:: これがテストハーネスです::

private static void RunTests(int count = 1 << 24, bool displayResults = true)
{
    var tests = Array.FindAll(Tests, t => t != null);
    var maxLength = tests.Select(x => GetMethodName(x.Method).Length).Max();

    for (int j = 0; j < tests.Length; j++)
    {
        var action = tests[j];
        Stopwatch sw = Stopwatch.StartNew();
        for (int i = 0; i < count; i++)
        {
            action();
        }
        sw.Stop();
        if (displayResults)
        {
            Console.WriteLine("{2}  {0}: {1}ms", GetMethodName(action.Method).PadRight(maxLength),
                              ((int)sw.ElapsedMilliseconds).ToString(), j);
        }
        GC.Collect();
        GC.WaitForPendingFinalizers();
        GC.Collect();
    }
}

private static string GetMethodName(MethodInfo method)
{
    return method.IsGenericMethod
            ? string.Format(@"{0}<{1}>", method.Name, string.Join<Type>(",", method.GetGenericArguments()))
            : method.Name;
}

そして、テストでは次のことを行います::

Tests[0] = () => proxiedTestClass.Test();
Tests[1] = () => proxiedTestClass.Test<string>("2");
Tests[2] = () => handClass.Test();
Tests[3] = () => handClass.Test<string>("2");
RunTests(100, false);
RunTests();

Tests は でFunc<object>[20]proxiedTestClassはアセンブリによって生成されたクラスで、handClassは手動で生成したクラスです。RunTests は 2 回呼び出されます。1 回目は「ウォームアップ」のため、もう 1 回は実行して画面に出力するためです。このコードは主に Jon Skeet の投稿から引用しました。

4

2 に答える 2

5

ECMA-335 (CLI cpecification)、パート I、セクション 8.9.5 に記載されているとおり:

このような型初期化メソッドの実行をトリガーするタイミングとトリガーのセマンティクスは次のとおりです。

  1. 型は、型初期化メソッドを持つことも、持たないこともできます。
  2. 型は、その型初期化メソッドに対して緩和されたセマンティックを持つものとして指定できます (以下の便宜上、この緩和されたセマンティックをBeforeFieldInitと呼びます)。
  3. BeforeFieldInitとマークされている場合、型の初期化メソッドは、その型に定義された静的フィールドへの最初のアクセス時、またはその前の時点で実行されます。
  4. BeforeFieldInitがマークされていない場合、その型の初期化メソッドが実行されます (つまり、トリガーされます):

    を。そのタイプの静的フィールドへの最初のアクセス、または

    b. そのタイプの静的メソッドの最初の呼び出し、または

    c. 値型または値型の場合、その型のインスタンスまたは仮想メソッドの最初の呼び出し

    d. その型のコンストラクターの最初の呼び出し。

また、上記の Michael のコードからわかるように、 にTestClassProxyは static フィールドが 1 つしかありません_proxy_handlers。2 回しか使用されないことに注意してください。

  1. インスタンスコンストラクターで
  2. そして、静的フィールド初期化子自体で

したがって、BeforeFieldInitが指定されている場合、型初期化子は 1 回だけ呼び出されます。つまり、インスタンス コンストラクターで、 への最初のアクセスの直前_proxy_handlersです。

ただし、BeforeFieldInitが省略されている場合、CLR はすべての TestClassProxy's静的メソッド呼び出し、静的フィールド アクセスなどの前に型初期化子への呼び出しを配置し​​ます。

s_0_Test特に、型初期化子は、s_1_Test<T>静的メソッドの呼び出しごとに呼び出されます。

もちろん、ECMA-334 (C# 言語仕様)のセクション 17.11 に記載されているように:

非ジェネリック クラスの静的コンストラクターは、特定のアプリケーション ドメインで最大 1 回実行されます。ジェネリック クラス宣言の静的コンストラクターは、クラス宣言から構築された閉じた構築型ごとに最大 1 回実行されます (§25.1.5)。

ただし、これを保証するために、CLR はクラスが既に初期化されているかどうかを (スレッドセーフな方法で) チェックする必要があります。

そして、これらのチェックはパフォーマンスを低下させます。

PS:インスタンス メソッドに変更するs_0_Testと、パフォーマンスの問題がなくなることに驚くかもしれません。s_1_Test<T>

于 2013-01-29T15:55:41.007 に答える
4

まず、 について詳しく知りたい場合はbeforefieldinit、Jon Skeet の記事C# と をbeforefieldinit読んでください。この回答の一部はそれに基づいており、ここで関連する部分を繰り返します。

第 2 に、コードはほとんど実行しないため、オーバーヘッドが測定に大きな影響を与えます。実際のコードでは、影響ははるかに小さい可能性があります。

第 3 に、Reflection.Emit を使用して、クラスにbeforefieldint. 静的コンストラクター (例: static TestClassProxy() {}) を追加することで、C# でそのフラグを無効にすることができます。

さて、何をするかbeforefieldinitというと、型初期化子 ( と呼ばれるメソッド.cctor) がいつ呼び出されるかを制御するということです。C# の用語では、型初期化子には、静的コンストラクターからのすべての静的フィールド初期化子とコードが含まれます (存在する場合)。

このフラグを設定しないと、クラスのインスタンスが作成されるか、クラスの静的メンバーのいずれかが参照されるときに、型初期化子が呼び出されます。(C# 仕様から取得したもので、ここで CLI 仕様を使用するとより正確になりますが、最終結果は同じです。* )

これが意味することは、 がないbeforefieldinitと、コンパイラは型初期化子をいつ呼び出すかについて非常に制限されているということです。それを行う方が便利な場合でも (そしてコードが高速になります)、少し早く呼び出すことを決定することはできません。

これがわかれば、コードで実際に何が起こっているかを見ることができます。問題のあるケースは静的メソッドです。これは、型初期化子が呼び出される可能性があるためです。(インスタンス コンストラクターは別のものですが、それを測定していません。)

その方法に着目しましたs_1_Test()。そして、実際には何もする必要がないので、(生成されたネイティブ コードを短くするために) 次のように単純化しました。

public static object s_1_Test<T>(TestClass<pR> class1, pR local1) where T:IConvertible
{
    return null;
}

ここで、VS での逆アセンブリ (リリース モード) を見てみましょうbeforefieldinit

00000000  xor         eax,eax
00000002  ret

ここでは、結果が に設定され(パフォーマンス上の理由から0多少難読化されています)、メソッドは非常に単純に戻ります。

静的コンストラクターを静的に使用すると (つまり、なしでbeforefieldinit) どうなりますか?

00000000  sub         rsp,28h
00000004  mov         rdx,rcx
00000007  xor         ecx,ecx
00000009  call        000000005F8213A0
0000000e  xor         eax,eax
00000010  add         rsp,28h
00000014  ret

これははるかに複雑です。実際の問題は、call必要に応じて型初期化子を呼び出す関数をおそらく呼び出す命令です。

これが、2 つの状況のパフォーマンスの違いの原因だと思います。

追加のチェックが必要な理由は、型がジェネリックであり、参照型を型パラメーターとして使用しているためです。その場合、クラスの異なるジェネリック バージョンの JITted コードは共有されますが、ジェネリック バージョンごとに型初期化子を呼び出す必要があります。静的メソッドを別の非ジェネリック型に移動することは、問題を解決する 1 つの方法です。


*nullを使用してインスタンス メソッドを呼び出すようなおかしなことをしない限りcall(および ではなくcallvirt、 for をスローしnullます)。

于 2013-01-29T16:03:57.333 に答える