Хит производительности vtable lookup в C ++

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

Прерывание происходит с частотой 3 кГц, и для каждого прерывания должно быть выполнено около 200 различных действий в последовательности. Процессор работает с частотой 300 МГц, что дает нам 100 000 циклов для выполнения работы. Это было решено в C с помощью массива указателей на функции:

// Each function does a different thing, all take one parameter being a pointer
// to a struct, each struct also being different.
void (*todolist[200])(void *parameters);

// Array of pointers to structs containing each function's parameters.
void *paramlist[200];

void realtime(void)
{
  int i;
  for (i = 0; i < 200; i++)
    (*todolist[i])(paramlist[i]);
}

Скорость важна. Выше 200 итераций выполняются 3000 раз в секунду, поэтому практически мы делаем 600 000 итераций в секунду. Вышеупомянутый цикл for компилируется в пять циклов за итерацию, что дает общую стоимость 3 000 000 циклов в секунду, то есть 1% загрузки процессора. Оптимизация ассемблерамог бы доведите это до четырех инструкций, однако я боюсь, что мы можем получить дополнительную задержку из-за доступа к памяти близко друг к другу и т. д. Короче говоря, я считаю, что эти пять циклов довольно оптимальны.

Теперь на C ++ переписать. Эти 200 вещей, которые мы делаем, как бы связаны друг с другом. Существует подмножество параметров, которые они все нуждаются и используют и имеют в своих соответствующих структурах. Таким образом, в реализации C ++ их можно аккуратно рассматривать как наследование от общего базового класса:

class Base
{
  virtual void Execute();
  int something_all_things_need;
}
class Derived1 : Base
{
  void Execute() { /* Do something */ }
  int own_parameter;
  // Other own parameters
}
class Derived2 : Base { /* Etc. */ }

Base *todolist[200];

void realtime(void)
{
  for (int i = 0; i < 200; i++)
    todolist[i]->Execute(); // vtable look-up! 20+ cycles.
}

Моя проблема - поиск в vtable. Я не могу делать 600 000 поисков в секунду; это составило бы более 4% потерянной загрузки процессора. Более того, список задач никогда не изменяется во время выполнения, он устанавливается только один раз при запуске, поэтому усилия по поиску вызываемой функции действительно напрасны. Задавая себе вопрос «каков наиболее оптимальный конечный результат из возможных», я смотрю на код ассемблера, предоставленный решением C, и уточняю массив указателей на функции ...

Какой чистый и правильный способ сделать это в C ++? Создание хорошего базового класса, производных классов и т. Д. Кажется довольно бессмысленным, когда в конце снова выбирают указатели на функции по соображениям производительности.

Обновление (включая исправление того, где начинается цикл):

Процессор ADSP-214xx, а компилятор VisualDSP ++ 5.0. При включении#pragma optimize_for_speedцикл C составляет 9 циклов. Оптимизация сборки на мой взгляд дает 4 цикла, однако я не проверял это, так что это не гарантировано. Цикл C ++ состоит из 14 циклов. Я знаю, что компилятор мог бы работать лучше, однако я не хотел игнорировать это как проблему компилятора - обходиться без полиморфизма все еще предпочтительнее во встроенном контексте, и выбор дизайна по-прежнему меня интересует. Для справки вот итоговая сборка:

C:

i3=0xb27ba;
i5=0xb28e6;
r15=0xc8;

Вот фактический цикл:

r4=dm(i5,m6);
i12=dm(i3,m6);
r2=i6;
i6=i7;
jump (m13,i12) (db);
dm(i7,m7)=r2;
dm(i7,m7)=0x1279de;
r15=r15-1;
if ne jump (pc, 0xfffffff2);

C ++:

i5=0xb279a;
r15=0xc8;

Вот фактический цикл:

i5=modify(i5,m6);
i4=dm(m7,i5);
r2=i4;
i4=dm(m6,i4);
r1=dm(0x3,i4);
r4=r2+r1;
i12=dm(0x5,i4);
r2=i6;
i6=i7;
jump (m13,i12) (db);
dm(i7,m7)=r2;
dm(i7,m7)=0x1279e2;
r15=r15-1;
if ne jump (pc, 0xffffffe7);

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

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

Ответы на вопрос(1)

Ваш ответ на вопрос