Walka ze słówkiem inline

Wymyśliłem sobie taką strukturę obsługi przerwań:

  • tablica wektorów przerwań wraz z definicjami funkcji znajdują się w pliku isr_vectors.c;
  • jako, że w przerwaniu nie koniecznie obsługujemy tylko zdarzenie od jednego peryferium, fragmenty obsługi dla danego peryferium (np przycisku) znajdują się np w zl27arm_buttons.c, a procedura obsługi przerwania woła odpowiedni kod.

Wszystko ładnie, czysto, nie trzeba szukać głównej obsługi przerwania po innych modułach, tylko od razu widać co ta procedura woła i skąd. No właśnie… Czas trwania obsługi przerwania powinien być jak najkrótszy, aby chociażby ustrzec się sytuacji, że zablokujemy na dłuższy czas obsługę przerwań o niższym priorytecie, bez wyraźnej potrzeby lub jeszcze gorzej – będziemy tracić przerwania. W związku z tym, nie powinno się umieszczać wywołań innych funkcji w funkcji przerwania, o ile nie jest to konieczne. Dlaczego? Z prostej przyczyny. Wywołanie funkcji to nie tylko skok pod określony adres i wykonanie kodu. Zanim zacznie się wywołanie właściwego kodu, należy zapewnić, że stan procesora zostanie zachowany, po wykonaniu funkcji stan procesora musi zostać przywrócony (prolog i epilog funkcji). W przypadku rdzenia M3 jest to pewnie z 20-kilka cykli, które idzie w gwizdek. Lek – inline.

inline void foo();

Widząc atrybut inline, kompilator powinien wstawić kod funkcji w miejscu wykonania. Czy tak będzie zawsze? Nie. Naciąłem się właśnie na to, gdy pisałem prostą obsługę przerwania przycisku. Chciałem zmusić kompilator do wstawienia funkcji przez dodatkowy atrybut __attribute__((always_inline)) i przełączniki kompilatora, ale albo kod się nie kompilował, albo było ordynarne rozgałęzienie.  Czasami kompilator generował ostrzeżenie tego typu: function body not available. Po sznurku do kłębka… W momencie kompilacji, kompilator musi mieć już dostępny kod funkcji. Jeżeli funkcja, która ma zostać wstawiona znajduje się w innym module i jest tylko zadeklarowana, kompilator nie ma czego wstawić… Rozwiązaniem jest zamieszczenie definicji funkcji w pliku nagłówka. Nie jest to zbyt czyste rozwiązanie – ale cóż.

Kiedy można zyskać najwięcej? Przy założeniu tylko takiej struktury:

void foo() {
   bar();
}

Zysk jest niewielki, gdyż „inline\’owanie” funkcji bar() zaoszczędzi nam jedynie instrukcję skoku, bo prolog i epilog funkcji bar() zostanie połączony z prologiem i epilogiem funkcji foo(). Inaczej się sprawa ma, gdy konstrukcja jest następująca.

void foo() {
   bar1();
   bar2();
   bar3();
}

Gdy nie wstawiamy funkcji w linii, wtedy następuje skok i wykonanie prologu. Gdy prologi funkcji są podobne, wtedy odłożymy mniej więcej trzy razy ten sam zestaw rejestrów na stos i trzy razy będziemy musieli go zdjąć. Natomiast, gdy użyjemy funkcji „w linii”, wtedy prolog i epilog funkcji foo() będzie kompilacją trzech prologów i epilogów wywoływanych funkcji. W najlepszym wypadku zaoszczędzimy 2/3 ilości cykli w porównaniu z prologów i epilogów bez funkcji „w linii”, gdyż zestaw rejestrów odłożymy i zdejmiemy tylko raz.