Почему компилятор 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 3.5.

Таким образом, ваш обходной путь необходим, потому что это было невозможно в.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()();
}

Очевидно наивный (и, возможно, медленный):)

Другие вопросы по тегам