blog

new() 约束的陷阱

发现一篇很好的文章 Dissecting the new() constraint in C#: a perfect example of a leaky abstraction,在这里写一下要点。

问题

之前(.net framework 2.0 前时代)在项目中动态创建某个不定类型实例时,使用的是 Activator.CreateInstance(),虽然知道这种创建方式是通过反射实现的,性能不太理想,当时也没有太好的其他选择。后来泛型出来了,有了个 new() 约束,可以轻松地在代码里面 new T() 这样写了,心里美滋滋的,想微软还真是贴心哪,这下子不单代码更加简练了,还直接能把对象 new 出来,性能完美了。

public class NodeFactory
{
    public static TNode CreateNode<TNode>() 
        where TNode : Node, new()
    {
        return new TNode();
    }
}

没想到看了这篇文章之后才知道,原来看起来眉清目秀的 new TNode(),不是直接执行 new 指令创建出来的,而是一转身就跑去调用了 Activator.CreateInstance<T>(),看着靠谱的样子,实际上暗度陈仓的干活,真是让人大跌眼镜。性能比直接调用 Activator.CreateInstance<T> 还差。

而且这样的实现,除了性能问题,还有正确性上也存在问题:

public static T Create<T>() where T : new()
{
    try
    {
        return new T();
    }
    catch (TargetInvocationException e)
    {
        var edi = ExceptionDispatchInfo.Capture(e.InnerException);
        edi.Throw();
        // Required to avoid compiler error regarding unreachable code
        throw;
    }
}

解决

那么应该如何应对这种情况?如何创建高性能而且正确的实例初始方法?

可以使用后面出来的表达式树来进行处理。表达式树是一种轻量级的代码生成方案,可以编译成 delegate,部分也可以编译成 Expression<DelegateType> 表达式。在这里我们可以使用它编译成 delegate

版本 1

public static class FastActivator
{
    public static T CreateInstance<T>() where T : new()
    {
        return FastActivatorImpl<T>.NewFunction();
    }
 
    private static class FastActivatorImpl<T> where T : new()
    {
        // Compiler translates 'new T()' into Expression.New()
        private static readonly Expression<Func<T>> NewExpression = () => new T();
 
        // Compiling expression into the delegate
        public static readonly Func<T> NewFunction = NewExpression.Compile();
    }
}

FastActivator.CreateInstance 概念上跟 Activator.CreateInstance 类似,但有两点不一样:

经过基准测试,FastActivator.CreateInstanceActivator.CreateInstance 快 5 倍,但比通过 Func<Node> 直接通过方法创建实例的方式还是慢 3.5 倍。为什么慢那么多呢?因为 Expression.Compile 创建一个 DynamicMethod 并把它关联到一个匿名程序集,为了让它在一个安全的沙箱环境跑,这主要是为了跑部分可信代码的安全性考虑,但带来了运行时的开销。

可以通过将 DynamicMethod 的一个 constructor 关联到特定的模块来解决。由于使用 Expression.Compile 实现这个存在困难,我们可以手动 “compile” 我们的 factory 方法:

版本 2

public static class DynamicModuleLambdaCompiler
{
    public static Func<T> GenerateFactory<T>() where T:new()
    {
        Expression<Func<T>> expr = () => new T();
        NewExpression newExpr = (NewExpression)expr.Body;
 
        var method = new DynamicMethod(
            name: "lambda", 
            returnType: newExpr.Type,
            parameterTypes: new Type[0],
            m: typeof(DynamicModuleLambdaCompiler).Module,
            skipVisibility: true);
 
        ILGenerator ilGen = method.GetILGenerator();
        // Constructor for value types could be null
        if (newExpr.Constructor != null)
        {
            ilGen.Emit(OpCodes.Newobj, newExpr.Constructor);
        }
        else
        {
            LocalBuilder temp = ilGen.DeclareLocal(newExpr.Type);
            ilGen.Emit(OpCodes.Ldloca, temp);
            ilGen.Emit(OpCodes.Initobj, newExpr.Type);
            ilGen.Emit(OpCodes.Ldloc, temp);
        }
            
        ilGen.Emit(OpCodes.Ret);
 
        return (Func<T>)method.CreateDelegate(typeof(Func<T>));
    }
}

有了这个新的 helper 方法,上面的 FastActivator 可以修改为:

public static class FastActivator
{
    public static T CreateInstance<T>() where T : new()
    {
        return FastActivatorImpl<T>.Create();
    }
 
    private static class FastActivatorImpl<T> where T : new()
    {
        public static readonly Func<T> Create =
            DynamicModuleLambdaCompiler.GenerateFactory<T>();
    }
}

这个版本比上个版本快两倍,但还是比 Func<Node> 慢两倍。原因如下:

版本 3

为了解决这个引用类型的问题,避免间接性层次的增加,我们可以将内嵌的 FastActivatorImpl<T> 类搬到 FastActivator 外面,并直接调用它:

public static class FastActivator<T> where T : new()
{
    /// <summary>
    /// Extremely fast generic factory method that returns an instance
    /// of the type <typeparam name="T"/>.
    /// </summary>
    public static readonly Func<T> Create =
        DynamicModuleLambdaCompiler.GenerateFactory<T>();
}

这个版本表现就相当好了,可以跟直接调用 Func<Node> 相比较。

番外

如果 JIT 支持对 new T() 直接生成 new 指令就好了。