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

时间:2013-01-28 20:41:59

标签: c# cil reflection.emit il

我正在研究代理,对于带有引用类型参数的泛型类,它非常慢。特别是对于通用方法(对于刚刚返回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);
        }
    }
}

这是在发布模式下编译为与我生成的代理相同的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);
        }
    }
}

有一个例外,我没有在生成的类型上设置beforefieldinit属性。我只是设置以下属性:: public auto ansi

为什么使用beforefieldinit会使性能提升如此之多?

(唯一的另一个区别是我没有命名我的参数,这些参数在宏观计划中并不重要。 方法和字段的名称被加扰以避免与实际方法冲突。 GenericToken和InvocationHandlers是为了论证而无关的实现细节 GenericToken实际上只是用作类型数据持有者,因为它允许我将“T”发送给处理程序

InvocationHandler只是委托字段目标的持有者,没有实际的实现细节。

GenericInvocationHandler使用像DLR这样的调用技术来根据需要重写委托来处理传递的不同泛型参数  )

EDIT :: 这是测试工具::

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();

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

2 个答案:

答案 0 :(得分:4)

首先,如果您想了解有关beforefieldinit的更多信息,请阅读Jon Skeet的文章C# and beforefieldinit。这个答案的部分内容基于此,我将在此重复相关内容。

其次,您的代码很少,因此开销会对您的测量产生重大影响。在实际代码中,影响可能会小得多。

第三,您不需要使用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(它以某种模糊的方式完成for performance reasons),方法返回,非常简单。

static静态构造函数(即没有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指令,如果需要,可能会调用一个调用类型初始值设定项的函数。

我认为这是两种情况之间性能差异的来源。

需要添加检查的原因是因为您的类型是通用的,并且您使用引用类型作为类型参数。在这种情况下,您的类的不同通用版本的JITted代码是共享的,但必须为每个通用版本调用类型初始化程序。将静态方法移动到另一种非泛型类型将是解决该问题的一种方法。


* 除非你做一些疯狂的事情,比如使用null调用call上的实例方法(而不是调用callvirt的{​​{1}})

答案 1 :(得分:3)

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

  

何时触发执行此类型的语义   初始化方法,如下:

     
      
  1. 类型可以具有类型初始化方法。
  2.   
  3. 可以将类型指定为其类型初始化方法具有宽松的语义(为方便起见,我们称之为轻松   语义 BeforeFieldInit )。
  4.   
  5. 如果标记为 BeforeFieldInit ,则在第一次访问任何静态字段时或之前执行类型的初始化方法   为该类型定义。
  6.   
  7. 如果没有标记为 BeforeFieldInit ,那么该类型的初始化方法将在(即被触发)执行:

         

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

         

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

         

    ℃。首次调用该类型的任何实例或虚方法   如果是值类型或

         

    d。首次调用该类型的任何构造函数。

  8.   

此外,正如您从迈克尔上面的代码中可以看到的,TestClassProxy只有一个静态字段:_proxy_handlers。请注意,它只使用了两次:

  1. 在实例构造函数
  2. 在静态字段初始化程序本身
  3. 因此,当指定BeforeFieldInit时,类型初始值设定项只会被调用一次:在实例构造函数中,就在第一次访问_proxy_handlers之前。

    但是如果省略BeforeFieldInit,CLR会在每次 TestClassProxy's静态方法调用,静态字段访问等之前将调用放到类型初始化程序中。

    特别是,每次调用s_0_Tests_1_Test<T>静态方法时都会调用type-in​​itializer。

    当然,正如ECMA-334 (C# Language Specification)第17.11节所述:

      

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

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

    这些检查会降低性能。

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