Могут ли использоваться генераторы С# для исключения вызовов виртуальных функций?

Я использую как С++, так и С#, и что-то, что было на мой взгляд, заключается в том, можно ли использовать generics в С# для вызова виртуальных функций на интерфейсах. Рассмотрим следующее:

int Foo1(IList<int> list)
{
    int sum = 0;
    for(int i = 0; i < list.Count; ++i)
        sum += list[i];
    return sum;
}

int Foo2<T>(T list) where T : IList<int>
{
    int sum = 0;
    for(int i = 0; i < list.Count; ++i)
        sum += list[i];
    return sum;
}

/*...*/
var l = new List<int>();
Foo1(l);
Foo2(l);

Внутри Foo1 каждый доступ к списку. Состав и список [i] вызывает вызов виртуальной функции. Если это были С++ с использованием шаблонов, то в вызове Foo2 компилятор смог бы увидеть, что вызов виртуальной функции можно отбросить и вставить, потому что конкретный тип известен во время создания шаблона.

Но что же касается С# и дженериков? Когда вы вызываете Foo2 (l), во время компиляции известно, что T является списком, и поэтому список list.Count и list [i] не должны включать вызовы виртуальных функций. Прежде всего, это была бы правильная оптимизация, которая не ужасно что-то сломала? И если да, то компилятор /JIT достаточно умный, чтобы сделать эту оптимизацию?

Ответ 1

Это интересный вопрос, но, к сожалению, ваш подход к "обману" системы не повысит эффективность вашей программы. Если бы это было возможно, компилятор мог бы сделать это для нас с относительной легкостью!

Вы правы, что при вызове IList<T> через ссылку на интерфейс, что методы отправляются во время выполнения и поэтому не могут быть встроены. Поэтому вызовы методов IList<T>, такие как Count, и индексатор будут вызваны через интерфейс.

С другой стороны, неверно, что вы можете достичь любого преимущества производительности (по крайней мере, не с нынешним компилятором С# и .NET CLR), переписав его как общий метод.

Почему бы и нет? Сначала немного фона. Работа генераторов С# заключается в том, что компилятор компилирует ваш общий метод, который имеет сменные параметры, а затем заменяет их во время выполнения фактическими параметрами. Это вы уже знали.

Но параметризованная версия метода больше не знает о типах переменных, чем вы и я во время компиляции. В этом случае весь компилятор знает о Foo2 в том, что list является IList<int>. Мы имеем ту же информацию в общем Foo2, что мы делаем в не-generic Foo1.

На самом деле, чтобы избежать раздувания кода, JIT-компилятор создает только одно экземпляр универсального метода для всех ссылочных типов. Вот документация Microsoft, описывающая эту подстановку и создание экземпляра:

Если клиент указывает ссылочный тип, то компилятор JIT заменяет общие параметры на сервере IL на Object и компилирует его в собственный код. Этот код будет использоваться в любом дополнительном запросе для ссылочного типа вместо параметра общего типа. Обратите внимание, что таким образом компилятор JIT использует только повторный код. Экземпляры по-прежнему распределяются в зависимости от их размера от управляемой кучи, и нет кастинга.

Это означает, что версия JIT-компилятора метода (для ссылочных типов) не относится к типу, но это не имеет значения, поскольку компилятор обеспечил всю безопасность типа во время компиляции. Но что более важно для вашего вопроса, нет возможности выполнить inlining и получить повышение производительности.

Изменить: Наконец, эмпирически, я только что сделал тест как Foo1, так и Foo2, и они дают одинаковые результаты производительности. Другими словами, Foo2 не не быстрее Foo1.

Добавьте для сравнения "встроенную" версию Foo0:

int Foo0(List<int> list)
{
    int sum = 0;
    for (int i = 0; i < list.Count; ++i)
        sum += list[i];
    return sum;
}

Вот сравнение производительности:

Foo0 = 1719
Foo1 = 7299
Foo2 = 7472
Foo0 = 1671
Foo1 = 7470
Foo2 = 7756

Итак, вы можете видеть, что Foo0, который может быть встроен, значительно быстрее, чем два других. Вы также можете увидеть, что Foo2 немного медленнее, а не где-то рядом с Foo0.

Ответ 2

Это действительно работает, и (если функция не является виртуальной) приводит к не виртуальному вызову. Причина в том, что в отличие от С++, генерические средства CLR определяют в JIT-время конкретный конкретный класс для каждого уникального набора общих параметров (обозначенных через отражение через трейлинг 1, 2 и т.д.). Если этот метод является виртуальным, он приведет к виртуальному вызову, подобному любому конкретному, не виртуальному, но не универсальному методу.

То, что нужно помнить о. generics generics, заключается в том, что данный:

Foo<T>; 

затем

Foo<Int32>

является допустимым типом во время выполнения, отдельным и отличным от

Foo<String>

и все виртуальные и не виртуальные методы обрабатываются соответственно. Вот почему вы можете создать

List<Vehicle>

и добавьте к нему автомобиль, но вы не можете создать переменную типа

List<Vehicle> 

и установите его значение в экземпляр

List<Car>

. Они имеют разные типы, но первый имеет метод Add(...), который принимает аргумент Vehicle, супертип Car.