快速创建对象而不是使用 Activator.CreateInstance(type)

39
我正在努力提升我们应用程序的性能。我们有很多Activator.CreateInstance的调用,这些调用导致了一些问题。
我们根据一个接口(ITabDocument)实例化了很多类,经过一番搜索后,我想到了使用这段代码:
这段代码并没有比我们之前使用的Activator.CreateInstance代码更好(事实上稍微慢一点)。
    public static Func<T> CreateInstance<T>(Type objType) where T : class, new()
    {
        var dynMethod = new DynamicMethod("DM$OBJ_FACTORY_" + objType.Name, objType, null, objType);
        ILGenerator ilGen = dynMethod.GetILGenerator();
        ilGen.Emit(OpCodes.Newobj, objType.GetConstructor(Type.EmptyTypes));
        ilGen.Emit(OpCodes.Ret);
        return (Func<T>)dynMethod.CreateDelegate(typeof(Func<T>));
    }

我想知道为什么会这样,我所做的只是:
ITabDocument document = CreateInstance<ITabDocument>(Type.GetType("[Company].Something"));

有没有更好的方法来创建能够帮助上述问题的对象?当你不确定具体类型时,这有点困难。

11
你调用CreateInstance的频率有多高 - 因为这个方法的整个意义在于你只需要调用一次CreateInstance,然后记住工厂委托。如果你每次操作都调用CreateInstance,那么它会变得慢... - Jon Skeet
2
一些设计模式可能会对你有所帮助,例如工厂方法、抽象工厂和类似的创建型模式。它们允许您延迟绑定对象。您可以在这里查看:http://oodesign.com - Nickolodeon
@Jon Skeet 感谢您的回复,我至少会调用20-30次,因为我们需要在开始时创建这么多选项卡文档,它们都是ITabbedDocument的不同实现。 - Tiffany Townsend
1
@JonSkeet,你能解释一下“记住”委托的意思吗?这里指的是哪个委托?你是指在Activator.CreateInstance上使用Delegate.CreateDelegate还是只是存储一个直接调用CreateInstanceFunc<> - nawfal
@nawfal:我的意思是只需存储对Delegate.CreateDelegate返回值的引用,这样您就不需要每次都创建新的DynamicMethod。 - Jon Skeet
显示剩余4条评论
6个回答

55

我对以下内容进行了基准测试(我只会写下最基本的细节):

public static T Instance() //~1800 ms
{
    return new T();
}

public static T Instance() //~1800 ms
{
    return new Activator.CreateInstance<T>();
}

public static readonly Func<T> Instance = () => new T(); //~1800 ms

public static readonly Func<T> Instance = () => 
                                 Activator.CreateInstance<T>(); //~1800 ms

//works for types with no default constructor as well
public static readonly Func<T> Instance = () => 
               (T)FormatterServices.GetUninitializedObject(typeof(T)); //~2000 ms


public static readonly Func<T> Instance = 
     Expression.Lambda<Func<T>>(Expression.New(typeof(T))).Compile();  
     //~50 ms for classes and ~100 ms for structs

正如CD所说,编译的表达式是最快的,而且优势很大。除了(T)FormatterServices.GetUninitializedObject(typeof(T))之外,所有方法仅适用于具有默认构造函数的类型。

当您针对每个泛型类型拥有一个静态类时,缓存已编译的结果委托非常简单。例如:

public static class New<T> where T : new()
{
    public static readonly Func<T> Instance = Expression.Lambda<Func<T>>
                                              (
                                               Expression.New(typeof(T))
                                              ).Compile();
}

请注意new约束。调用任何内容
MyType me = New<MyType>.Instance();

除了第一次加载类到内存中时,执行速度将会最快。
为了拥有一个处理具有默认构造函数和没有的两种类型的类,我采取了混合方法,从这里开始
public static class New<T>
{
    public static readonly Func<T> Instance = Creator();

    static Func<T> Creator()
    {
        Type t = typeof(T);
        if (t == typeof(string))
            return Expression.Lambda<Func<T>>(Expression.Constant(string.Empty)).Compile();

        if (t.HasDefaultConstructor())
            return Expression.Lambda<Func<T>>(Expression.New(t)).Compile();

        return () => (T)FormatterServices.GetUninitializedObject(t);
    }
}

public static bool HasDefaultConstructor(this Type t)
{
    return t.IsValueType || t.GetConstructor(Type.EmptyTypes) != null;
}

将以高效的方式处理值类型。
请注意,(T)FormatterServices.GetUninitializedObject(t) 对于 string 会失败。因此,已经特殊处理字符串以返回空字符串。

这如何适用于根据问题在运行时仅知道类型的对象创建呢?问题中使用泛型只是为了将返回的对象强制转换为基类。在您提供的示例中,返回实例的类型与泛型参数相同,因此可以使用普通构造函数(除了它还可以创建未初始化的对象)。 - Richard Collette
@RichardCollette 我同意这并没有直接回答问题,但是展示了一种拥有好用的小助手类的方式(该类是不完整的,希望我能在某一天更新它)。尽管如此,将其微调为 OP 的可行解决方案并不太困难(只需使用提供的类型而不是 typeof(T))。 - nawfal
1
第一个,简单的通用方法new T()...它不可能是正确的。这个速度不可能比最后的lambda版本更慢。这应该和非泛型构造函数一样快,因为泛型在编译时处理。你能再确认一下吗?这很重要,因为它会对你其他优秀文章中的所有结果产生怀疑! - Timo
@Timo 取决于你所说的“泛型在编译时处理”的含义。当然会进行一些静态检查。但 T 总是具有运行时类型。我认为编译器应该足够聪明,不依赖反射来处理泛型情况,但不幸的是它确实这样做了。这就是整个问题的关键。你也可以通过检查 IL 来确认这一点。 - nawfal
1
阅读Jon Skeet的博客https://codeblog.jonskeet.uk/2011/08/22/optimization-and-generics-part-1-the-new-constraint。以及评论中用户发布的各种基准测试结果,都验证了我的结论。还请注意我在该博客中发表的评论,其中强调了Roslyn中行为如何改变成为更慢的问题。 - nawfal
@nawfal 哇,我猜我说得太早了!感谢你的教训和高性能解决方案。我向你致敬! - Timo

22
这可能会有所帮助:不要使用Activator.CreateInstance或ConstructorInfo.Invoke,而是使用编译的Lambda表达式
// Make a NewExpression that calls the ctor with the args we just created
NewExpression newExp = Expression.New(ctor, argsExp);                  

// Create a lambda with the New expression as body and our param object[] as arg
LambdaExpression lambda = Expression.Lambda(typeof(ObjectActivator), newExp, param);            


// Compile it
ObjectActivator compiled = (ObjectActivator)lambda.Compile();

谢谢,我之前看过这个,但不确定是否可以像我需要的那样称其为临时的。 - Tiffany Townsend
1
谢谢提供链接,它指向了这篇博客http://rogeralsing.com/2008/02/28/linq-expressions-creating-objects/,我一直在阅读它,我认为它可能是我见过的最好的博客之一。非常感谢。 - RichK
2
虽然这理论上回答了问题,但最好在此处包含答案的基本部分并提供参考链接。请编辑您的答案以更正此问题,然后标记为“需要管理员干预”并请求取消删除。 - Matt
这至少快了一个数量级,非常值得! - jjxtra

9
问题在于,如果您直接多次调用CreateInstance而不是将结果保存在某处并重复使用该结果,那么您应该在其中进行缓存。
internal static class DelegateStore<T> {
     internal static IDictionary<string, Func<T>> Store = new ConcurrentDictionary<string,Func<T>>();
}

public static T CreateInstance<T>(Type objType) where T : class
{
    Func<T> returnFunc;
    if(!DelegateStore<T>.Store.TryGetValue(objType.FullName, out returnFunc)) {
        var dynMethod = new DynamicMethod("DM$OBJ_FACTORY_" + objType.Name, objType, null, objType);
        ILGenerator ilGen = dynMethod.GetILGenerator();
        ilGen.Emit(OpCodes.Newobj, objType.GetConstructor(Type.EmptyTypes));
        ilGen.Emit(OpCodes.Ret);
        returnFunc = (Func<T>)dynMethod.CreateDelegate(typeof(Func<T>));
        DelegateStore<T>.Store[objType.FullName] = returnFunc;
    }
    return returnFunc();
}

1
实际上,在您的DelegateStore<T>类中根本不需要字典,因为该泛型类已经自动获得每个T实例的唯一实例。即使它是静态类也是如此。所以在DelegateStore<T>中,您只需要简单地使用internal static Func<T> _cached_func;。就像您所拥有的那样,您正在创建许多字典——每个T都有一个新的字典,每个字典只包含一个缓存的委托。 - Glenn Slayden
对于任何想要创建这样的值类型的人,Opcodes.Newobj行的替换是: LocalBuilder localVar = ilGen.DeclareLocal(objType); ilGen.Emit(OpCodes.Ldloca_S, localVar); ilGen.Emit(OpCodes.Initobj, objType); ilGenerator.Emit(OpCodes.Ldloc_S, localVar); 感谢SharpLab帮助我推断出这个! - ulatekh

7

更新时间:2022年10月13日
nawfal的答案已经进行了基准测试

在NET6.0中进行了基准测试,只是想看看这是否仍然必要。

添加了Activator.CreateInstance<T>();测试和struct测试。

Activator1 = new();
Activator2 = Activator.CreateInstance<T>();
Activator3 = New<T>.Instance();

简而言之: 仍然建议对于简单的类使用。 不要用于结构体

using BenchmarkDotNet.Running;
using InstanceBenchmark;

//BenchmarkRunner.Run<ActivatorBenchmark<TestClass>>();
BenchmarkRunner.Run<ActivatorBenchmark<TestStruct>>();

public class TestClass
{
    public string Name { get; set; }
    public int Id { get; set; }
    public string Email { get; set; }
}

public struct TestStruct
{
    public string Name { get; set; }
    public int Id { get; set; }
    public string Email { get; set; }
}

[MemoryDiagnoser]
[SimpleJob(runtimeMoniker: RuntimeMoniker.Net60)]
[GenericTypeArguments(typeof(TestClass))]
[GenericTypeArguments(typeof(TestStruct))]
public class ActivatorBenchmark<T> where T : new()
{
    [Benchmark(Baseline = true)]
    [Arguments(1_000)]
    [Arguments(1_000_000)]
    [Arguments(100_000_000)]

    public void ActivatorTest1(int x)
    {
        for (int i = 0; i < x; i++)
        {
            var t = new T();
        }
    }

    [Benchmark]
    [Arguments(1_000)]
    [Arguments(1_000_000)]
    [Arguments(100_000_000)]

    public void ActivatorTest2(int x)
    {
        for (int i = 0; i < x; i++)
        {
            var t = Activator.CreateInstance<T>();
        }
    }

    [Benchmark]
    [Arguments(1_000)]
    [Arguments(1_000_000)]
    [Arguments(100_000_000)]
    public void ActivatorTest3(int x)
    {
        for (int i = 0; i < x; i++)
        {
            var t = New<T>.Instance();
        }
    }
}

public static class TestHelpers
{
    public static class New<T>
    {
        public static readonly Func<T> Instance = Creator();

        private static Func<T> Creator()
        {
            Type t = typeof(T);
            if (t == typeof(string))
            { return Expression.Lambda<Func<T>>(Expression.Constant(string.Empty)).Compile(); }

            if (t.HasDefaultConstructor())
            { return Expression.Lambda<Func<T>>(Expression.New(t)).Compile(); }

            return () => (T)FormatterServices.GetUninitializedObject(t);
        }
    }

    public static bool HasDefaultConstructor(this Type t)
    {
        return t.IsValueType || t.GetConstructor(Type.EmptyTypes) != null;
    }
}

类结果

// * Summary *

BenchmarkDotNet=v0.13.2, OS=Windows 11 (10.0.22000.1098/21H2)
Intel Core i9-10900KF CPU 3.70GHz, 1 CPU, 20 logical and 10 physical cores
.NET SDK=6.0.402
  [Host]   : .NET 6.0.10 (6.0.1022.47605), X64 RyuJIT AVX2
  .NET 6.0 : .NET 6.0.10 (6.0.1022.47605), X64 RyuJIT AVX2

Job=.NET 6.0  Runtime=.NET 6.0  

|         Method |         x |           Mean |         Error |        StdDev | Ratio | RatioSD |        Gen0 |     Allocated | Alloc Ratio |
|--------------- |---------- |---------------:|--------------:|--------------:|------:|--------:|------------:|--------------:|------------:|
| ActivatorTest1 |      1000 |       9.946 μs |     0.1927 μs |     0.2142 μs |  1.00 |    0.00 |      3.8147 |      39.06 KB |        1.00 |
| ActivatorTest2 |      1000 |       9.808 μs |     0.0721 μs |     0.0674 μs |  0.98 |    0.02 |      3.8147 |      39.06 KB |        1.00 |
| ActivatorTest3 |      1000 |       6.219 μs |     0.1199 μs |     0.1427 μs |  0.63 |    0.02 |      3.8223 |      39.06 KB |        1.00 |
|                |           |                |               |               |       |         |             |               |             |
| ActivatorTest1 |   1000000 |   9,834.625 μs |    31.8609 μs |    26.6053 μs |  1.00 |    0.00 |   3812.5000 |   39063.26 KB |        1.00 |
| ActivatorTest2 |   1000000 |  10,671.712 μs |    47.0675 μs |    44.0269 μs |  1.09 |    0.01 |   3812.5000 |   39063.26 KB |        1.00 |
| ActivatorTest3 |   1000000 |   6,295.779 μs |   121.9964 μs |   186.3014 μs |  0.65 |    0.03 |   3820.3125 |    39062.5 KB |        1.00 |
|                |           |                |               |               |       |         |             |               |             |
| ActivatorTest1 | 100000000 | 995,902.729 μs | 7,355.4492 μs | 6,520.4141 μs |  1.00 |    0.00 | 382000.0000 | 3906325.27 KB |        1.00 |
| ActivatorTest2 | 100000000 | 982,209.783 μs | 6,630.1000 μs | 5,176.3460 μs |  0.99 |    0.01 | 382000.0000 | 3906335.95 KB |        1.00 |
| ActivatorTest3 | 100000000 | 618,402.807 μs | 4,305.6817 μs | 4,027.5373 μs |  0.62 |    0.01 | 382000.0000 | 3906253.48 KB |        1.00 |

结构体结果

// * Summary *

BenchmarkDotNet=v0.13.2, OS=Windows 11 (10.0.22000.1098/21H2)
Intel Core i9-10900KF CPU 3.70GHz, 1 CPU, 20 logical and 10 physical cores
.NET SDK=6.0.402
  [Host]   : .NET 6.0.10 (6.0.1022.47605), X64 RyuJIT AVX2
  .NET 6.0 : .NET 6.0.10 (6.0.1022.47605), X64 RyuJIT AVX2

Job=.NET 6.0  Runtime=.NET 6.0

|         Method |         x |             Mean |         Error |        StdDev | Ratio | RatioSD | Allocated | Alloc Ratio |
|--------------- |---------- |-----------------:|--------------:|--------------:|------:|--------:|----------:|------------:|
| ActivatorTest1 |      1000 |         212.8 ns |       4.27 ns |       4.38 ns |  1.00 |    0.00 |         - |          NA |
| ActivatorTest2 |      1000 |         209.5 ns |       0.10 ns |       0.09 ns |  0.98 |    0.02 |         - |          NA |
| ActivatorTest3 |      1000 |       1,646.0 ns |       2.69 ns |       2.10 ns |  7.77 |    0.14 |         - |          NA |
|                |           |                  |               |               |       |         |           |             |
| ActivatorTest1 |   1000000 |     204,577.8 ns |     128.30 ns |     107.14 ns |  1.00 |    0.00 |         - |          NA |
| ActivatorTest2 |   1000000 |     204,569.4 ns |     116.38 ns |     108.86 ns |  1.00 |    0.00 |         - |          NA |
| ActivatorTest3 |   1000000 |   1,644,446.5 ns |  12,606.12 ns |   9,842.03 ns |  8.04 |    0.05 |       1 B |          NA |
|                |           |                  |               |               |       |         |           |             |
| ActivatorTest1 | 100000000 |  20,455,141.5 ns |  12,934.68 ns |  12,099.11 ns |  1.00 |    0.00 |      15 B |        1.00 |
| ActivatorTest2 | 100000000 |  20,460,807.6 ns |  25,571.37 ns |  19,964.44 ns |  1.00 |    0.00 |      15 B |        1.00 |
| ActivatorTest3 | 100000000 | 164,105,645.0 ns | 327,107.27 ns | 305,976.34 ns |  8.02 |    0.01 |     898 B |       59.87 |

1
似乎事情几乎没有什么改变 =/ - Natalie Perret

3

你可能因为生成相同代码而获得了一些额外的开销。

ILGenerator会为工厂动态创建代码。

创建某种类型的映射或Dictionary,记录已经使用过的类型,并保留为该类型创建的工厂方法。


0

构造委托的通用方法,直接调用构造函数。自动在给定类型中搜索具有给定委托类型签名的构造函数,并构造该类型的委托。代码如下:

/// <summary>
/// Reflective object construction helper.
/// All methods are thread safe.
/// </summary>
public static class Constructor
{
    /// <summary>
    /// Searches an instanceType constructor with delegateType-matching signature and constructs delegate of delegateType creating new instance of instanceType.
    /// Instance is casted to delegateTypes's return type. 
    /// Delegate's return type must be assignable from instanceType.
    /// </summary>
    /// <param name="delegateType">Type of delegate, with constructor-corresponding signature to be constructed.</param>
    /// <param name="instanceType">Type of instance to be constructed.</param>
    /// <returns>Delegate of delegateType wich constructs instance of instanceType by calling corresponding instanceType constructor.</returns>
    public static Delegate Compile(Type delegateType,Type instanceType)
    {
        if (!typeof(Delegate).IsAssignableFrom(delegateType))
        {
            throw new ArgumentException(String.Format("{0} is not a Delegate type.",delegateType.FullName),"delegateType");
        }
        var invoke = delegateType.GetMethod("Invoke");
        var parameterTypes = invoke.GetParameters().Select(pi => pi.ParameterType).ToArray();
        var resultType = invoke.ReturnType;
        if(!resultType.IsAssignableFrom(instanceType))
        {
            throw new ArgumentException(String.Format("Delegate's return type ({0}) is not assignable from {1}.",resultType.FullName,instanceType.FullName));
        }
        var ctor = instanceType.GetConstructor(
            BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, parameterTypes, null);
        if(ctor == null)
        {
            throw new ArgumentException("Can't find constructor with delegate's signature","instanceType");
        }
        var parapeters = parameterTypes.Select(Expression.Parameter).ToArray();

        var newExpression = Expression.Lambda(delegateType,
            Expression.Convert(Expression.New(ctor, parapeters), resultType),
            parapeters);
        var @delegate = newExpression.Compile();
        return @delegate;
    }
    public static TDelegate Compile<TDelegate>(Type instanceType)
    {
        return (TDelegate) (object) Compile(typeof (TDelegate), instanceType);
    }
}

Yappi项目源代码的一部分。使用它,您可以构建调用给定类型的任何构造函数的委托,包括具有参数的构造函数(除了ref和out参数)。

示例用法:

var newList = Constructor.Compile<Func<int, IList<String>>>(typeof (List<String>));
var list = newList(100);

在构造委托之后,将其存储在静态字典或具有泛型参数的类的静态字段中。不要每次构造新的委托。使用一个委托来构造给定类型的多个实例。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接