2010-06-29 2 views
2

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

Есть ли так много накладных расходов при объявлении функций-членов как таковых и почему они все еще присутствуют даже при непосредственном доступе к производному классу?

Код выглядит следующим образом:

class base 
{ 
public: 
    virtual ~base() {} 
    virtual uint func(uint i) = 0; 
}; 

class derived : public base 
{ 
public: 
    ~derived() {} 
    uint func(uint i) { return i * 2; } 
}; 

uint j = 0; 
ulong k = 0; 
double l = 0; 
ushort numIters = 10; 
base* mybase = new derived; // or derived* myderived = ... 

for(ushort i = 0; i < numIters; i++) 
{ 
    clock_t start2, finish2; 
    start2 = clock(); 

    for (uint j = 0; j < 100000000; ++j) 
     k += mybase->func(j); 

    finish2 = clock(); 
    l += (double) (finish2 - start2); 
    std::cout << "Total duration: " << (double) (finish2 - start2) << " ms." << std::endl; 

} 

std::cout << "Making sure the loop is not optimized to nothing: " << k << std::endl; 
std::cout << "Average duration: " << l/numIters << " ms." << std::endl; 

Результаты:

base* mybase = new derived; дает в среднем ~ 338 мс.

derived* myderived = new derived; дает среднее значение ~ 338 мс.

Устранение наследования и удаление виртуальных функций дает в среднем ~ 38 мс.

Это почти в 10 раз меньше! Итак, в принципе, если какая-либо функция объявлена ​​виртуальной, накладные расходы всегда будут идентично присутствовать, даже если я не буду использовать ее полиморфно?

Спасибо.

+0

Кажется, что вы вычисляете стоимость наследования + виртуальные функции вместе. Вы должны протестировать экземпляр производного класса без каких-либо виртуальных функций, а также базового класса. –

+0

Зачем использовать 'new'? Было бы проще создать экземпляр объекта в стеке ... –

+0

В этом случае это не имело бы никакого значения. Такие же накладные расходы при доступе через указатель. –

ответ

6

Доступ к ней «непосредственно» выполняет ту же работу, что и ее «косвенно».

Когда вы вызываете функцию на myderived, указатель, хранящийся там, может указывать на некоторый объект некоторого класса, производный от derived. Компилятор не может предположить, что он действительно является объектом derived, он может быть объектом еще одного производного класса, который переопределяет виртуальную функцию, поэтому необходимо отправить виртуальную функцию, как в случае с mybase. В обоих случаях функция просматривается в таблице виртуальных функций до ее вызова.

Для вызова функция нон-полиморфно, не используйте указатель:

derived myderived; 
myderived.func(1); 

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

for (uint j = 0; j < 100000000; ++j) 
    k += i * 2; 

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

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

+0

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

+0

s/компилятор не может предположить, что это действительно производный/компилятор не предполагает, что он действительно является производным /. Это возможно, поскольку нет классов, более полученных (примечание: требует оптимизации целых программ) – MSalters

+0

Я попытался удалить указатель, и он был медленнее, чем с ним. –

2

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

Способ, которым я их нахожу, - несколько раз приостановить приложение под отладчиком и изучить состояние, включая стек вызовов. Here's an example использования этого метода для получения ускорения в 43 раза.

+0

Спасибо, Майк, это потрясающий метод. –

Смежные вопросы