Elektroda.pl
Elektroda.pl
X

Search our partners

Find the latest content on electronic components. Datasheets.com
Please add exception to AdBlock for elektroda.pl.
If you watch the ads, you support portal and users.

[STM32][C] FreeRTOS vs Bare-metal różny czas wykonania instrukcji

gogus9 20 Nov 2018 17:39 1182 21
  • #1
    gogus9
    Level 6  
    Witam.
    Podczas próby porównania implementacji aplikacji bare-metal i opartej na FreeRTOS natknąłem się na problem różnego czasu wykonania się tego samego kodu. Do testów utworzyłem funkcję foo:

    Code: c
    Log in, to see the code

    którą wywołuje bezpośrednio z main, a następnie jako zadanie FreeRTOS:
    Code: c
    Log in, to see the code

    Czas wykonania mierzę w cyklach zegara cpu. Uzyskane przeze mnie wyniki to:
    Bare metal:
    - czas inkrementacji zmiennej: 9 cykli
    - czas wykonania całej pętli: 4195 cykli
    FreeRTOS:
    - czas inkrementacji zmiennej: 8 cykli
    - czas wykonania całej pętli: 3797 cykli

    Użyty mikrokontroler to STM32F429ZI. Kompilację przeprowadzam z wyłączoną optymalizacją. Kompilator to arm-atollic-eabi-gcc dostarczony wraz z Atollic TrueSTUDIO Toolchain. Wygenerowany kod asemblera jest identyczny (wszystko uruchamiane w jednym projekcie). Wersja FreeRTOS to 9.0.0. Korzystam z heap_4.

    Podejrzewam, że "winnym" może być sposób zarządzania pamięcią przez FreeRTOS, jednak nigdzie nie mogę znaleźć dokładnego wyjaśnienia takiego zachowania lub informacji o konkretnej innej przyczynie. Czy spotkał się ktoś wcześniej z takim problemem i wie jak go rozwiązać lub byłby w stanie szczegółowo wyjaśnić co jest tego przyczyną?
  • #2
    Freddie Chopin
    MCUs specialist
    gogus9 wrote:
    Czy spotkał się ktoś wcześniej z takim problemem

    Opisz na czym polega problem, bo to że kod który nic nie robi z wyłączoną optymalizacją zajmuje ileśtam czasu który niekoniecznie jest stały to jest najwyżej "sztuczny problem", a nie "problem".
  • #3
    tantalos1
    Level 17  
    Przecież FreeRTOS to system operacyjny wielozadaniowy i cyklicznie jest wywoływana procedura przełączająca taski, a to zajmuje trochę cykli procesora i to niezależnie czy jest tylko jeden task czy więcej.
  • #4
    User removed account
    Level 1  
  • #5
    gogus9
    Level 6  
    Dziękuję za szybką odpowiedź.

    Freddie Chopin wrote:
    Opisz na czym polega problem, bo to że kod który nic nie robi z wyłączoną optymalizacją zajmuje ileśtam czasu który niekoniecznie jest stały to jest najwyżej "sztuczny problem", a nie "problem".


    W skrócie chciałbym dowiedzieć się skąd wynika różnica czasu wykonania się tej samej instrukcji w aplikacji bez systemu operacyjnego i z systemem operacyjnym. Problemem samym w sobie nie jest to, że raz instrukcja wykona się szybciej a raz wolniej, bo całościowo, rozważając bardziej skomplikowany kod wyszłoby, że wykonują się one tak samo szybko. Jeśli się mylę to proszę o wyjaśnienie. Tymczasem, gdy kod wykonywany jest pod kontrolą systemu, to operacje te zawsze wykonują się szybciej (przynajmniej te o których wspomniałem). Jeśli spojrzy się choćby na wynik czasów wykonania tej pętli to różnica wynosi 398 cykli zegara. Wykonanie pętli zaczyna się od i=1, czyli pętla wykonywana jest 199 razy. 398/199=2 dodatkowe takty zegara na jedno wykonanie pętli. Więc zawsze jakaś konkretna instrukcja wykonuje się 2 takty dłużej, albo para LTD/STR dokłada od siebie te 2 cykle.

    Głównym problemem jest próba porównania wydajności aplikacji bare-metal (sterowanej przerwaniami) i tej samej aplikacji tylko, że napisanej z wykorzystaniem FreeRTOS. Po prostu funkcje, które były wykonywane w odpowiedzi na przerwanie zostały przeniesione do zadań FreeRTOS. Podczas pierwszych prób wyszło mi, że bare-metal radzi sobie znacząco gorzej od FreeRTOS, pomimo tego, że RTOS z natury powinien dawać dodatkowy narzut (tutaj ponownie jeśli się mylę to proszę o wyjaśnienie :D). W celu identyfikacji przyczyny powstała ta funckja foo() , którą zamieściłem wyżej.

    stmx wrote:
    Sposób zarządzania pamięcią przez RTOS nie ma tu nic do rzeczy. Rozumiem że chodzi Ci o to że idle task może robić w tle swoją "magię" i zajmować czas. W RTOS nie może bo cały czas procesora zajmuje Twoja nieskończona pętla (bo nie oddajesz kontroli )

    Nie chodzi mi o to. Wiem, że Idle task wykorzystywany jest np. do zwalniania pamięci. Chodzi mi o to, że FreeRTOS w momencie uruchamiania Scheduler'a alokuje sobie własną pamięć w RAM. Nawet, gdy pobierze się adres zmiennej 'i' to przy zwykłym wywołaniu foo() jest ona zaalokowana w zupełnie innym obszarze pamięci niż przy FreeRTOS.
    Dzięki za informację w sprawie tego NOP'a i DWT. Muszę to jeszcze doczytać :)
  • #6
    Freddie Chopin
    MCUs specialist
    gogus9 wrote:
    Tymczasem, gdy kod wykonywany jest pod kontrolą systemu, to operacje te zawsze wykonują się szybciej (przynajmniej te o których wspomniałem).

    Może we FreeRTOSie zrobili optymalizację JIT? (;

    Tak serio - różnica 2 taktów na pętlę naprawdę jest mało istotna. Spróbuj zmierzyć coś sensownego, np. policz sinusa czy jakiś pierwiastek, albo coś w ten deseń. Jak sam zauważyłeś, dodatkowo funkcje operują na zmiennych znajdujących się w innych obszarach pamięci RAM, więc to też może mieć wpływ - spróbuj np. zamiast zmiennych na stosie użyć zmiennych globalnych, tak aby faktycznie porównywać ze sobą te same operacje. No i testowanie NOPów jest kompletnie bezcelowe, jak to napisał już Piotrus_999 - stąd moja sugestia wstawienia tam czegoś bardziej sensownego (jakichś obliczeń).
  • #7
    gogus9
    Level 6  
    Freddie Chopin wrote:
    Tak serio - różnica 2 taktów na pętlę naprawdę jest mało istotna.

    Zgodzę się, że jest mało istotna, ale jest, wiec próbuję się dowiedzieć skąd się ona bierze? :)

    Freddie Chopin wrote:
    Spróbuj zmierzyć coś sensownego, np. policz sinusa czy jakiś pierwiastek, albo coś w ten deseń. Jak sam zauważyłeś, dodatkowo funkcje operują na zmiennych znajdujących się w innych obszarach pamięci RAM, więc to też może mieć wpływ - spróbuj np. zamiast zmiennych na stosie użyć zmiennych globalnych, tak aby faktycznie porównywać ze sobą te same operacje. No i testowanie NOPów jest kompletnie bezcelowe, jak to napisał już Piotrus_999 - stąd moja sugestia wstawienia tam czegoś bardziej sensownego (jakichś obliczeń).

    Napisałem coś takiego:
    Code: c
    Log in, to see the code

    Kolejno czasy wyświetlane na printf w tej funkcji:
    Bare-metal:
    1. 9 cykli
    2. 314 cykli
    3. 25 cykli
    FreeRTOS:
    1. 8 cykli
    2. 313 cykli
    3. 24 cykle

    To ten jeden cykl w takim wypadku nie robi różnicy i byłoby ok, gdyby tak wychodziło, jednak jeśli zmienne są zaalokowane jako lokalne, to sytuacja jest taka jak wcześniej w ilości cykli. Tylko, że korzystanie ze zmiennych globalnych w bardziej rozbudowanej aplikacji no nie przejdzie.
    Zakładając, że sama lokalizacja zmiennej w pamięci ma wpływ na czas dostępu do niej, to z czego to wynika? Jak w takim wypadku zrobić żeby ten domyślny stos był w tej szybszej pamięci? Szukałem w dokumentacji mikrokontrolera oraz rdzenia informacji na ten temat, jednak nie udało mi się nic takiego znaleźć.

    EDIT:
    Dla ścisłości, gdy zmienne i, j, k są lokalne to otrzymywane wyniki to:
    1. 7 cykli
    2. 325 cykli
    3. 44625 cykli
    FreeRTOS:
    1. 6 cykli
    2. 318 cykli
    3. 42834 cykle

    W każdym przypadku funkcję foo() wywołuję po kilka razy, a tutaj podaje wyniki najczęściej występujące - zdarzają się zmiany np. przy ostatnich pomiarach +-100 cykli, ale tendencja jest nadal taka sama.
  • Helpful post
    #8
    User removed account
    Level 1  
  • #10
    User removed account
    Level 1  
  • #11
    Freddie Chopin
    MCUs specialist
    stmx wrote:
    chyba mniejsza. CYCCNT liczy wszystko - włącznie z przerwaniami. Tak ze trzeba odjąć czas w przerwaniach, albo je wyłączyć do testów

    Większa. Skoro przy korzystaniu z FreeRTOS ilość cykli wychodzi _MNIEJSZA_ niż bez niego, to po odjęciu przerwań (które przed włączeniem schedulera nie występują, a po włączeniu go - jakieś się pewnie pojawiają) ilość cykli będzie jeszcze mniejsza. Ergo - rozbieżność będzie większa.
  • #12
    gogus9
    Level 6  
    Wykonałem po 400 wywołań foo(). Otrzymane średnie wyniki:
    Bare-metal:
    1. 7
    2. 244,27
    3. 44281,76

    FreeRTOS:
    1. 6
    2. 236,22
    3. 42486,88

    Także wychodzi na to samo.
    Kod dorzucam dla ewentualnego sprawdzenia, czy na pewno dobrze wykonuje te pomiary.
    Code: c
    Log in, to see the code
  • #13
    User removed account
    Level 1  
  • #14
    BlueDraco
    MCUs specialist
    Stawiam na różne adresy kodu w pamięci i wynikające z nich różnice w działaniu "akceleratora" dostępu do Flash. Puść MCU na 8 MHz i różnice (raczej) znikną.

    A może po prostu masz różnie ustawioną optymalizację w obu projektach?
  • #16
    gogus9
    Level 6  
    BlueDraco wrote:
    A może po prostu masz różnie ustawioną optymalizację w obu projektach?

    Wszystko jest realizowane w tym samym projekcie - chciałem wyeliminować ryzyko innej konfiguracji.

    BlueDraco wrote:
    Stawiam na różne adresy kodu w pamięci i wynikające z nich różnice w działaniu "akceleratora" dostępu do Flash. Puść MCU na 8 MHz i różnice (raczej) znikną.

    Jak sprawdzę z obniżoną częstotliwością to dam znać. Jednak w docelowym projekcie nie mogę tak nisko zejść z częstotliwością.

    Freddie Chopin wrote:
    Przecież adresy są takie same. Adresy używanych zmiennych zresztą też.

    Adresy zmiennych lokalnych różnią się. W bare metal są to odpowiednio dla i, j k:
    2002ffd8
    2002ffdc
    2002ffd4
    a przy wywołaniu w zadaniu FreeRTOS:
    20000370
    20000374
    2000036c
  • #17
    BlueDraco
    MCUs specialist
    O adresy kodu chodzi, a nie danych. Adresy kodu mają wpływ na akcelerację dostępu do Flash. Pętla zaczynająca się od adresu podzielnego przez 16 ma szansę wykonywać się szybciej, niż taka od adresu np. 16x + 14.
  • #18
    Freddie Chopin
    MCUs specialist
    BlueDraco wrote:
    O adresy kodu chodzi, a nie danych. Adresy kodu mają wpływ na akcelerację dostępu do Flash. Pętla zaczynająca się od adresu podzielnego przez 16 ma szansę wykonywać się szybciej, niż taka od adresu np. 16x + 14.

    Nie sądzisz chyba, że kompilator przy wyłączonej optymalizacji zrobił dwie różne wersje tej samej funkcji, a linker umieścił je w zupełnie innych miejscach?
  • #19
    gogus9
    Level 6  
    Freddie Chopin wrote:
    BlueDraco wrote:
    O adresy kodu chodzi, a nie danych. Adresy kodu mają wpływ na akcelerację dostępu do Flash. Pętla zaczynająca się od adresu podzielnego przez 16 ma szansę wykonywać się szybciej, niż taka od adresu np. 16x + 14.

    Nie sądzisz chyba, że kompilator przy wyłączonej optymalizacji zrobił dwie różne wersje tej samej funkcji, a linker umieścił je w zupełnie innych miejscach?


    Adres funkcji jest ten sam. Chyba, że jest to zachowanie "losowe" w przypadku wykonywania takich funkcji.

    Dodano po 6 [godziny] 5 [minuty]:

    Zdaje się, że kolega @stmx jest najbliżej prawdy. Po zmianie zmiennej definiującej położenie stosu globalnego (chyba to właśnie to :D) _estack
    z adresu 0x20030000 na adres 0x20010000 wyniki są niemalże identyczne. Dla testu takiego samego jak wyżej otrzymuję:
    Bare metal:
    1. 6
    2. 235,46
    3. 42481,94

    FreeRTOS:
    1. 6
    2. 236,22
    3. 42481,88

    Tak więc dziękuję za pomoc w przynajmniej częściowym rozwiązaniu problemu :) Jeśli ktoś znałby dokładną przyczynę tego tj. czy to jest wina samej pamięci czy wina architektury to byłbym wdzięczny za odpowiedź i wskazanie źródeł.
    Jeśli uda mi się dowiedzieć jaka jest tego dokładna przyczyna to oczywiście napiszę tu :D
  • #20
    User removed account
    Level 1  
  • Helpful post
    #21
    Freddie Chopin
    MCUs specialist
    gogus9 wrote:
    Jeśli ktoś znałby dokładną przyczynę tego tj. czy to jest wina samej pamięci czy wina architektury to byłbym wdzięczny za odpowiedź i wskazanie źródeł.

    Reference Manual
    2 Memory and bus architecture
    2.1 System architecture
    [STM32][C] FreeRTOS vs Bare-metal różny czas wykonania instrukcji

    Skoro poprzednio miałeś stos w rejonie 0x20030000, to wypadał on w SRAM3. Teraz przesunąłeś go do SRAM1. Jak widać z obrazka, SRAM1 jest podpięte do rdzenia m.in. przez D-bus, czyli szynę zoptymalizowaną do pobierania operandów instrukcji (danych). SRAM3 (jak i SRAM2), jest podpięty tylko przez S-bus, które to służy jako takie "dwa w jednym" i może pobierać zarówno instrukcje jak i dane, niemniej jednak zapewne nie jest to tak wydajne jak bezpośredni dostęp przez D-bus.

    Twoja zmiana zaowocowała zapewne tym, że teraz stos tego co masz w main() jak i stos wątku (przydzielany z heap, które zwykle jest "poniżej" stosu), są w tej samej pamięci.
  • #22
    gogus9
    Level 6  
    Dziękuję. Mam wrażenie, że teraz już wszystko jasne.