Скорость виртуального вызова в C # против C ++ - PullRequest
8 голосов
/ 24 марта 2009

Кажется, я вспомнил, что где-то читал, что стоимость виртуального вызова в C # не так высока, условно говоря, как в C ++. Это правда? Если так - почему?

Ответы [ 9 ]

8 голосов
/ 24 марта 2009

Виртуальный вызов C # должен проверять, является ли «this» нулевым, а виртуальный вызов C ++ - нет. Поэтому я не могу понять, почему виртуальные вызовы C # будут быстрее. В особых случаях компилятор C # (или JIT-компилятор) может быть в состоянии встроить виртуальный вызов лучше, чем компилятор C ++, поскольку компилятор C # имеет доступ к лучшей информации о типах. Инструкция метода вызова может иногда быть медленнее в C ++, поскольку JIT C # может использовать более быструю инструкцию, которая справляется только с небольшим смещением, так как она знает больше о структуре оперативной памяти и модели процессора, чем Компилятор C ++.

Однако здесь речь идет о небольшом количестве инструкций процессора. На суперскалярном процессоре модема вполне возможно, что команда «проверка на ноль» выполняется одновременно с «методом вызова» и поэтому не занимает много времени.

Также очень вероятно, что все инструкции процессора уже будут в кеше уровня 1, если вызов выполняется в цикле. Но данные, скорее всего, не будут кешировать, стоимость чтения значения данных из основной памяти в наши дни такая же, как и запуск сотен инструкций из кеша уровня 1. Поэтому не повезло, что в реальных приложениях стоимость виртуального вызова измеряется даже в очень немногих местах.

Тот факт, что код C # использует еще несколько инструкций, конечно, уменьшит объем кода, который может поместиться в кеше, эффект этого предсказать невозможно.

(Если класс C ++ использует множественную инерционность, тогда стоимость больше из-за необходимости исправления указателя «this». Аналогично интерфейсы в C # добавляют еще один уровень перенаправления.)

5 голосов
/ 24 марта 2009

Для скомпилированных языков JIT (я не знаю, делает ли CLR это или нет, JVM от Sun), это обычная оптимизация для преобразования виртуального вызова, который имеет только две или три реализации, в последовательность тестов типа и прямые или внутренние звонки.

Преимущество этого состоит в том, что современные конвейерные процессоры могут использовать прогнозирование ветвлений и предварительную выборку прямых вызовов, но косвенный вызов (представленный указателем функции в языках высокого уровня) часто приводит к остановке конвейера.

В предельном случае, когда существует только одна реализация виртуального вызова, а тело вызова достаточно мало, виртуальный вызов сводится к чисто встроенному коду . Этот метод использовался во время выполнения Self language , из которого развивается JVM.

Большинство компиляторов C ++ не выполняют весь анализ программы, необходимый для этой оптимизации, но такие проекты, как LLVM, рассматривают оптимизацию всей программы, такую ​​как эта.

4 голосов
/ 24 марта 2009

Оригинальный вопрос гласит:

Кажется, я помню, что где-то читал что стоимость виртуального звонка в C # не так высоко, относительно говоря , как в C ++.

Обратите внимание на акцент. Другими словами, вопрос можно перефразировать как:

Кажется, я помню, что где-то читал что в C #, виртуальном и не виртуальном вызовы одинаково медленны, тогда как в C ++ виртуальный вызов медленнее, чем не виртуальный вызов ...

Таким образом, спрашивающий не утверждает, что C # быстрее C ++ ни при каких обстоятельствах.

Возможно, бесполезная диверсия, но это вызвало мое любопытство в отношении C ++ с / clr: pure, без использования расширений C ++ / CLI. Компилятор создает IL, который преобразуется в нативный код с помощью JIT, хотя это чистый C ++. Итак, у нас есть способ увидеть, что делает стандартная реализация C ++, если работает на той же платформе, что и C #.

Не виртуальным методом:

struct Plain
{
    void Bar() { System::Console::WriteLine("hi"); }
};

Этот код:

Plain *p = new Plain();
p->Bar();

... вызывает код операции call с определенным именем метода, передавая Bar неявный аргумент this.

call void <Module>::Plain.Bar(valuetype Plain*)

Сравните с иерархией наследования:

struct Base
{
    virtual void Bar() = 0;
};

struct Derived : Base
{
    void Bar() { System::Console::WriteLine("hi"); }
};

Теперь, если мы сделаем:

Base *b = new Derived();
b->Bar();

Вместо этого выдается код операции calli, который переходит на вычисленный адрес - так что перед вызовом много IL. Вернув его обратно в C #, мы увидим, что происходит:

**(*((int*) b))(b);

Другими словами, приведите адрес b к указателю на int (размер которого совпадает с размером указателя) и возьмите значение в этом месте, которое является адресом виртуальной таблицы, а затем первый элемент в vtable, который является адресом для перехода, разыменовывает его и вызывает его, передавая неявный аргумент this.

Мы можем настроить виртуальный пример для использования расширений C ++ / CLI:

ref struct Base
{
    virtual void Bar() = 0;
};

ref struct Derived : Base
{
    virtual void Bar() override { System::Console::WriteLine("hi"); }
};

Base ^b = gcnew Derived();
b->Bar();

Генерирует код операции callvirt, точно так же, как в C #:

callvirt instance void Base::Bar()

Таким образом, при компиляции с таргетингом на CLR текущий компилятор Microsoft C ++ не имеет таких возможностей для оптимизации, как в C # при использовании стандартных функций каждого языка; для стандартной иерархии классов C ++ компилятор C ++ генерирует код, который содержит жестко запрограммированную логику для обхода vtable, тогда как для класса ref он оставляет JIT вычислять оптимальную реализацию.

3 голосов
/ 24 марта 2009

Я полагаю, что это предположение основано на JIT-компиляторе, что означает, что C #, вероятно, преобразует виртуальный вызов в простой вызов метода до того, как он будет фактически использован.

Но это, по сути, теоретически, и я бы не стал на это ставить!

2 голосов
/ 24 марта 2009

Стоимость виртуального вызова в C ++ равна стоимости вызова функции через указатель (vtbl). Я сомневаюсь, что C # может сделать это быстрее и все еще в состоянии определить тип объекта во время выполнения ...

Редактировать: Как указал Пит Киркхем, хороший JIT мог бы встроить вызов C #, избегая остановки конвейера; то, что большинство компиляторов C ++ не может сделать (пока). С другой стороны, Ян Рингроз упомянул влияние на использование кэша. Кроме того, сам JIT работает, и (строго лично) я бы не стал беспокоиться, если бы профилирование на целевой машине при реалистичных рабочих нагрузках не подтвердило , что один из них быстрее, чем другой. В лучшем случае это микрооптимизация.

1 голос
/ 24 марта 2009

Не уверен насчет полной структуры, но в Compact Framework она будет медленнее, потому что CF не имеет таблиц виртуальных вызовов, хотя и кеширует результат. Это означает, что виртуальный вызов в CF будет медленнее при первом вызове, поскольку он должен выполнить поиск вручную. Он может быть медленным при каждом вызове, если приложению не хватает памяти, поскольку кэшированный поиск может быть передан.

0 голосов
/ 24 марта 2009

Это может быть не совсем ответ на ваш вопрос, но, хотя .NET JIT оптимизирует виртуальные вызовы, как все говорили ранее, оптимизация на основе профилей в Visual Studio 2005 и 2008 выполняет умозаключение виртуальных вызовов, вставляя прямой вызов наиболее вероятной целевой функции, включающий вызов, поэтому вес может быть одинаковым.

0 голосов
/ 24 марта 2009

C # сглаживает vtable и встраивает вызовы предков, поэтому вы не можете связать иерархию наследования, чтобы что-то разрешить.

0 голосов
/ 24 марта 2009

В C # может быть возможно преобразовать виртуальную функцию в не виртуальную, анализируя код. На практике это случается не так часто, чтобы многое изменить.

...