为什么添加beforefieldinit会大大提高泛型类的执行速度?

Mic*_*l B 6 c# il cil reflection.emit

我正在研究代理和带有引用类型参数的泛型类,它非常慢.特别是对于通用方法(对于刚刚返回null的普通泛型方法,大约400毫秒对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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

这是在发布模式下编译到同一个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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

有一个例外,我没有在生成的类型上设置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;
}
Run Code Online (Sandbox Code Playgroud)

在测试中我做了以下::

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

测试是一个Func<object>[20],并且proxiedTestClass是由我的程序集生成的类,并且handClass是我手工生成的类.RunTests被调用两次,一次"加热",再一次运行它并打印到屏幕上.我大多从Jon Skeet的帖子中获取此代码.

Nik*_*hil 5

正如ECMA-335 (CLI cpecification),第一部分,第 8.9.5 节所述:

触发执行此类类型初始化方法的时间和内容的语义如下:

  1. 一个类型可以有一个类型初始化方法,也可以没有。
  2. 可以将类型指定为其类型初始化器方法具有宽松语义(为了方便下文,我们将这种宽松语义称为BeforeFieldInit)。
  3. 如果标记为BeforeFieldInit,则在首次访问为该类型定义的任何静态字段时或之前的某个时间执行该类型的初始化方法。
  4. 如果未标记BeforeFieldInit,则该类型的初始化方法在以下位置执行(即由以下触发):

    一种。首先访问该类型的任何静态字段,或

    湾 第一次调用该类型的任何静态方法,或

    C。第一次调用该类型的任何实例或虚拟方法,如果它是值类型或

    d. 第一次调用该类型的任何构造函数。

此外,正如您从上面 Michael 的代码中所见,TestClassProxy只有一个静态字段:_proxy_handlers. 请注意,它只使用了两次:

  1. 在实例构造函数中
  2. 而在静态字段初始化程序本身

因此,当BeforeFieldInit指定时,类型初始化器将只调用一次:在实例构造函数中,就在第一次访问_proxy_handlers.

但如果BeforeFieldInit省略,CLR 将在每次 TestClassProxy's静态方法调用、静态字段访问等之前调用类型初始化器。

特别是,每次调用s_0_Tests_1_Test<T>静态方法时都会调用类型初始化器。

当然,如ECMA-334(C# 语言规范)第 17.11 节所述:

非泛型类的静态构造函数在给定的应用程序域中最多执行一次。对于从类声明(第 25.1.5 节)构造的每个封闭构造类型,泛型类声明的静态构造函数最多执行一次。

但是为了保证这一点,CLR 必须检查(以线程安全的方式)该类是否已经初始化。

而这些检查会降低性能。

PS:您可能会感到惊讶,一旦您更改s_0_Tests_1_Test<T>成为实例方法,性能问题就会消失。