Hit wydajności wyszukiwania vtable w C ++

Oceniam przepisanie fragmentu oprogramowania działającego w czasie rzeczywistym z języka C / Assembly na język C ++ / asemblerowy (z powodów niezwiązanych z pytaniem części kodu są absolutnie niezbędne do wykonania w montażu).

Przerwanie pochodzi z częstotliwością 3 kHz, a dla każdego przerwania należy wykonać około 200 różnych rzeczy w sekwencji. Procesor działa z częstotliwością 300 MHz, co daje nam 100 000 cykli do wykonania zadania. Zostało to rozwiązane w C z tablicą wskaźników funkcji:

// 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]);
}

Szybkość jest ważna. Powyższe 200 iteracji wykonuje się 3000 razy na sekundę, więc praktycznie wykonujemy 600 000 iteracji na sekundę. Powyższa dla pętli kompiluje się do pięciu cykli na iterację, dając całkowity koszt 3 000 000 cykli na sekundę, tj. 1% obciążenia procesora. Optymalizacja asembleramoc sprowadzić to do czterech instrukcji, jednak obawiam się, że możemy uzyskać dodatkowe opóźnienie z powodu dostępu do pamięci blisko siebie itd. W skrócie, wierzę, że te pięć cykli jest całkiem optymalne.

Teraz do przepisania C ++. Te 200 rzeczy, które robimy, są ze sobą powiązane. Istnieje podzbiór parametrów, których wszyscy potrzebują i używają, i które mają odpowiednie struktury. W implementacji C ++ można je zatem uznać za dziedziczące po wspólnej klasie bazowej:

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.
}

Moim problemem jest wyszukiwanie vtable. Nie mogę wykonać 600 000 wyszukiwania na sekundę; stanowiłoby to ponad 4% zmarnowanego obciążenia procesora. Co więcej, todolista nigdy się nie zmienia w czasie wykonywania, jest tylko raz uruchamiany podczas uruchamiania, więc wysiłek sprawdzenia, którą funkcję wywołać, jest naprawdę zmarnowany. Zadając sobie pytanie „jaki jest najbardziej optymalny wynik końcowy”, patrzę na kod asemblera podany przez rozwiązanie C i umieszczam tablicę wskaźników funkcji ...

Jaki jest czysty i właściwy sposób na zrobienie tego w C ++? Stworzenie ładnej klasy bazowej, klas pochodnych i tak dalej wydaje się bezcelowe, gdy na koniec ponownie wybieramy wskaźniki funkcji ze względu na wydajność.

Aktualizacja (w tym korekta miejsca rozpoczęcia pętli):

Procesor jest ADSP-214xx, a kompilatorem jest VisualDSP ++ 5.0. Przy włączaniu#pragma optimize_for_speed, pętla C ma 9 cykli. Optymalizacja montażu w moim umyśle daje 4 cykle, jednak nie testowałem go, więc nie ma gwarancji. Pętla C ++ ma 14 cykli. Jestem świadomy, że kompilator może wykonać lepszą pracę, jednak nie chciałem odrzucić tego jako problemu z kompilatorem - uzyskiwanie bez polimorfizmu jest nadal preferowane w kontekście osadzonym, a wybór projektu nadal mnie interesuje. Dla odniesienia tutaj powstały zespół:

DO:

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

Oto rzeczywista pętla:

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;

Oto rzeczywista pętla:

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);

Tymczasem myślę, że znalazłem coś w rodzaju odpowiedzi. Najmniejszą liczbę cykli osiąga się, wykonując możliwie najmniej. Muszę pobrać wskaźnik danych, pobrać wskaźnik funkcji i wywołać funkcję ze wskaźnikiem danych jako parametr. Podczas pobierania wskaźnika rejestr indeksu jest automatycznie modyfikowany przez stałą i równie dobrze można pozwolić tej stałej na równe 1. Zatem ponownie znajduje się tablica wskaźników funkcji i tablica wskaźników danych.

Naturalnie limit jest tym, co można zrobić podczas montażu, a teraz zostało to zbadane. Mając to na uwadze, teraz rozumiem, że choć wprowadzenie klasy bazowej jest naturalne, to nie do końca pasowało do rachunku. Myślę, że odpowiedź brzmi: jeśli ktoś chce tablicy wskaźników funkcji, powinien uczynić się tablicą wskaźników funkcji ...

questionAnswers(6)

yourAnswerToTheQuestion