Я оцениваю возможность переписать часть программного обеспечения реального времени с языка 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, и заново нахожу массив указателей на функции...
Каков чистый и правильный способ сделать это на С++? Создание хорошего базового класса, производных классов и т. д. кажется довольно бессмысленным, когда, в конце концов, вы снова выбираете указатели функций из соображений производительности.
Обновление (включая исправление начала цикла):
Процессор — ADSP-214xx, а компилятор — VisualDSP++ 5.0. При включении #pragma optimize_for_speed
цикл С составляет 9 циклов. На мой взгляд, оптимизация сборки дает 4 цикла, однако я не проверял это, поэтому это не гарантируется. Цикл С++ составляет 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. Таким образом, мы снова оказываемся перед массивом указателей на функции и массивом указателей на данные.
Естественно, пределом является то, что можно сделать на ассемблере, и это сейчас исследовано. Имея это в виду, я теперь понимаю, что, хотя для кого-то естественно вводить базовый класс, на самом деле это было не то, что соответствовало всем требованиям. Поэтому я предполагаю, что ответ таков: если кому-то нужен массив указателей на функции, нужно создать массив указателей на функции...