Почему компилятор C# генерирует Activator.CreateInstance при вызове new с универсальным типом с ограничением new()?
Когда у вас есть код, подобный следующему:
static T GenericConstruct<T>() where T : new()
{
return new T();
}
Компилятор C# настаивает на отправке вызова Activator.CreateInstance, который значительно медленнее, чем собственный конструктор.
У меня есть следующий обходной путь:
public static class ParameterlessConstructor<T>
where T : new()
{
public static T Create()
{
return _func();
}
private static Func<T> CreateFunc()
{
return Expression.Lambda<Func<T>>( Expression.New( typeof( T ) ) ).Compile();
}
private static Func<T> _func = CreateFunc();
}
// Example:
// Foo foo = ParameterlessConstructor<Foo>.Create();
Но это не имеет смысла для меня, почему этот обходной путь должен быть необходим.
5 ответов
Я подозреваю, что это проблема JITting. В настоящее время JIT повторно использует один и тот же сгенерированный код для всех аргументов ссылочного типа, поэтому List<string>
vtable указывает на тот же машинный код, что и List<Stream>
, Это не сработает, если каждый new T()
вызов должен быть разрешен в коде JITted.
Просто предположение, но это имеет определенное количество смысла.
Один интересный маленький момент: ни в одном из случаев не вызывается беспараметрический конструктор типа значения, если он есть (что крайне редко встречается). Смотрите мой недавний пост в блоге для деталей. Я не знаю, есть ли способ заставить это в деревьях выражений.
Это вероятно, потому что неясно, является ли T типом значения или ссылочным типом. Создание этих двух типов в неуниверсальном сценарии дает очень разные IL. Перед лицом этой неоднозначности C# вынужден использовать универсальный метод создания типов. Activator.CreateInstance отвечает всем требованиям.
Быстрые эксперименты, кажется, подтверждают эту идею. Если вы введете следующий код и изучите IL, он будет использовать initobj вместо CreateInstance, потому что в типе нет двусмысленности.
static void Create<T>()
where T : struct
{
var x = new T();
Console.WriteLine(x.ToString());
}
Переключение на класс и ограничение new(), хотя все еще вызывает Activator.CreateInstance.
Почему этот обходной путь необходим?
Потому что новое ограничение () было добавлено в C# 2.0 в.NET 2.0.
Тем временем выражение
Таким образом, ваш обходной путь необходим, потому что это было невозможно в.NET 2.0. Между тем, (1) использование Activator.CreateInstance() было возможно, и (2) у IL нет способа реализовать 'new T()', поэтому Activator.CreateInstance() был использован для реализации этого поведения.
Это немного быстрее, поскольку выражение компилируется только один раз:
public class Foo<T> where T : new()
{
static Expression<Func<T>> x = () => new T();
static Func<T> f = x.Compile();
public static T build()
{
return f();
}
}
Анализируя производительность, этот метод так же быстр, как и более подробное скомпилированное выражение, и намного, намного быстрее, чем new T()
(В 160 раз быстрее на моем тестовом ПК) .
Для чуть более высокой производительности можно исключить вызов метода build и вместо него можно вернуть функтор, который клиент может кэшировать и вызывать напрямую.
public static Func<T> BuildFn { get { return f; } }
Интересное наблюдение:)
Вот более простой вариант вашего решения:
static T Create<T>() where T : new()
{
Expression<Func<T>> e = () => new T();
return e.Compile()();
}
Очевидно наивный (и, возможно, медленный):)