2009-03-24 3 views

ответ

8

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

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

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

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

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

2

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

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

3

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

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

+0

Даже если это было так; что «преобразование в простой вызов метода» также не является бесплатным, не так ли? – DevSolar

+0

Нет, конечно нет. Но вам нечего было платить при совершении фактического звонка (например, оплата заранее). –

+0

Точка, в следующий раз, когда вы сделаете этот звонок, вам придется снова проверить объект. В общем, это может измениться, поэтому obj.foo() каждый раз ссылается на разные foo. Обратите внимание, что компиляторы C++ часто могут преобразовывать виртуальный вызов в обычный вызов, если тип объекта известен во время компиляции. – MSalters

1

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

5

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

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

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

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

+0

Вы уверены, что он всегда вызывает остановку трубопровода? Нет причин, по которым ЦП не может предварительно использовать косвенный вызов (и я думаю, что это то, на что конкретно нацелился Intel ...). Если он может быть предварительно запрограммирован, накладные расходы виртуальной функции будут равны нулю. –

+0

Это было несколько лет назад, когда я последний раз проверял, поэтому я могу ошибаться. Единственная ссылка, которую я могу найти в Intel для прогнозирования непрямых ветвей, - в их профильной направленной компиляции; большинство их документов просто говорят, что их «очень сложно предсказать», а другие исследования говорят, что 99% ларьков связаны с косвенными вызовами. –

+0

Я думаю, что второй раз тот же косвенный вызов берется с одного и того же сайта, там не будет стойла. Например, если цикл представляет собой виртуальный вызов для многих объектов того же типа, он будет в порядке. –

0

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

0

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

0

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

4

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

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

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

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

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

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

С невиртуальной методой:

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

Этого код:

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

... заставляет call опкода, издаваемым с конкретным именем методы, передавая панель неявного аргумента 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, а затем взять первый элемент в таблице 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 для определения оптимальной реализации.

+0

Речь идет о C++ поверх CLR, что на самом деле не честно играет IMHO. – DevSolar

+0

Кроме того, что означает «справедливое» в данном контексте? –

+0

Johann спросил о том, что виртуальные вызовы на C# «дешевле», чем на C++. Я беру «C++», чтобы означать «C++ скомпилирован в native». Таким образом, MSC++ не может скомпилировать виртуальный вызов C++ в IL «callvirt». «Почему бы и нет?» должен быть нацелен на компилятор MS, а не на язык, не так ли? – DevSolar