Могут ли дженерики C # использоваться для исключения вызовов виртуальных функций? - PullRequest
12 голосов
/ 28 мая 2011

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

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 каждый доступ к list.Count и list [i] вызывает виртуальный вызов функции. Если бы это был C ++ с использованием шаблонов, то при вызове Foo2 компилятор мог бы увидеть, что вызов виртуальной функции может быть исключен и встроен, поскольку конкретный тип известен во время создания шаблона.

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

Ответы [ 2 ]

8 голосов
/ 28 мая 2011

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

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

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

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

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

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

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

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

Редактировать: Наконец, эмпирически, я только что выполнил тестирование 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.

.
4 голосов
/ 28 мая 2011

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

Следует помнить о дженериках .net:

Foo<T>; 

тогда

Foo<Int32>

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

Foo<String>

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

List<Vehicle>

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

List<Vehicle> 

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

List<Car>

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...