Вопрос

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

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

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

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 раз меньше!То есть, по сути, если какая-либо функция объявлена ​​виртуальной, накладные расходы всегда будут присутствовать одинаково, даже если я не использую ее полиморфно?

Спасибо.

Это было полезно?

Решение

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

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

Чтобы вызвать функцию неполиморфно, не используйте указатель:

derived myderived;
myderived.func(1); 

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

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

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

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

Другие советы

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

Я нахожу их, приостанавливая приложение несколько раз в отладчике и проверяя его состояние, включая стек вызовов. Вот пример использования этого метода для получения ускорения в 43 раза.

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top