Elektroda.pl
Elektroda.pl
X
Elektroda.pl
Computer Controls
Proszę, dodaj wyjątek dla www.elektroda.pl do Adblock.
Dzięki temu, że oglądasz reklamy, wspierasz portal i użytkowników.

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

gogus9 20 Lis 2018 17:39 798 21
  • #1 20 Lis 2018 17:39
    gogus9
    Poziom 3  

    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:

    Kod: c
    Zaloguj się, aby zobaczyć kod

    którą wywołuje bezpośrednio z main, a następnie jako zadanie FreeRTOS:
    Kod: c
    Zaloguj się, aby zobaczyć kod

    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ą?

    0 21
  • Computer Controls
  • #2 20 Lis 2018 18:14
    Freddie Chopin
    Specjalista - Mikrokontrolery

    gogus9 napisał:
    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".

    0
  • #3 20 Lis 2018 18:35
    tantalos1
    Poziom 16  

    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.

    0
  • Computer Controls
  • #4 20 Lis 2018 19:52
    stmx
    Poziom 25  

    Po perwsze: NOP w ARM-ach to nie jest ten sam NOP co w AVR-ach. NOP nigdy nie dochodzi do execution stage w pipelene i jest discardowany natychmiast po pobraniu. Tak że zachowanie takiej sekwencji NOP-ów jest mocno niedeterministyczne. NOP-y używa się do paddingu a nie opóźnień.

    tantalos1 napisał:
    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.
    RTOSy są trochę inne niż "normalne": systemy operacyjne, Po pierwsze czas dostają tylko taki o najwyższym priorytecie. Dopiero jak jest więcej niż jeden task o identycznym najwyższym w danej chwili priorytecie zaczyna się przełączanie pomiędzy nimi. Dopóki nie oddadzą kontroli w jakiś tam sposób to taski o niższym priorytecie nigdy się nie wykonają, Wykonują się jedynie przerwania i kol. @gogus9 powinien podczas profilowania sobie sprawdzić ile czasu procesor spędził w wyjątkach (jest do tego odpowiedni rejestr DWT). Po dokonaniu korekty możesz dopiero coś porównywać.

    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 )

    Tak na marginesie proponuję również zapoznać się lepiej z formatami printf - bo %d zdecydowanie nie jest do uint_32t (w tym przypadku bo wartości są małe to nie przeszkadza ale nie jest to poprawne - standard uznaje to nawet za UB)

    1
  • #5 20 Lis 2018 20:45
    gogus9
    Poziom 3  

    Dziękuję za szybką odpowiedź.

    Freddie Chopin napisał:
    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 napisał:
    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ć :)

    0
  • #6 20 Lis 2018 21:05
    Freddie Chopin
    Specjalista - Mikrokontrolery

    gogus9 napisał:
    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ń).

    0
  • #7 20 Lis 2018 21:57
    gogus9
    Poziom 3  

    Freddie Chopin napisał:
    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 napisał:
    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:
    Kod: c
    Zaloguj się, aby zobaczyć kod

    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.

    0
  • Pomocny post
    #8 20 Lis 2018 23:47
    stmx
    Poziom 25  

    A uwzględniłeś czas w przerwaniach?

    Spróbuj wykonać każdy z testów z wyłączonymi przerwaniami, powiedzmy kilkaset razy i dopiero wtedy wylicz czas poszczególnego testu.

    Jeżeli różnice będą występować to jedyne wytłumaczenie, które przychodzi mi do głowy to takie, że być może część danych jest w innym segmencie pamięci. Wtedy przypadkowo możliwe że operacje są wykonywane po innych szynach (chociaż nie za bardzo widzę taką możliwość, ale te cechy rdzenia nie są przez ARM dokumentowane) i dostęp jest jakby taki pseudo Harvard.

    0
  • #9 21 Lis 2018 08:09
    Freddie Chopin
    Specjalista - Mikrokontrolery

    stmx napisał:
    A uwzględniłeś czas w przerwaniach?

    Ale wtedy rozbieżność będzie jeszcze większa (;

    0
  • #11 21 Lis 2018 09:37
    Freddie Chopin
    Specjalista - Mikrokontrolery

    stmx napisał:
    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.

    0
  • #12 21 Lis 2018 09:44
    gogus9
    Poziom 3  

    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.

    Kod: c
    Zaloguj się, aby zobaczyć kod

    1
  • #13 21 Lis 2018 10:45
    stmx
    Poziom 25  

    Freddie Chopin napisał:
    Większa. Skoro przy korzystaniu z FreeRTOS ilość cykli wychodzi _MNIEJSZA_ niż bez niego,
    nie do końca o to mi chodziło. Może być różna ilość przerwan w kazdym z przypadkow. Ale jak wylaczyl przeewania to problem jakby jest nieistotny. Teraz jedyna metoda to umiescic wszystkie zmienne w tym samym bloku SRAM.

    0
  • #14 21 Lis 2018 11:15
    BlueDraco
    Specjalista - Mikrokontrolery

    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?

    0
  • #15 21 Lis 2018 11:16
    Freddie Chopin
    Specjalista - Mikrokontrolery

    BlueDraco napisał:
    Stawiam na różne adresy kodu w pamięci

    Przecież adresy są takie same. Adresy używanych zmiennych zresztą też.

    0
  • #16 21 Lis 2018 11:24
    gogus9
    Poziom 3  

    BlueDraco napisał:
    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 napisał:
    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 napisał:
    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

    0
  • #17 21 Lis 2018 13:17
    BlueDraco
    Specjalista - Mikrokontrolery

    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.

    0
  • #18 21 Lis 2018 13:55
    Freddie Chopin
    Specjalista - Mikrokontrolery

    BlueDraco napisał:
    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?

    0
  • #19 21 Lis 2018 20:17
    gogus9
    Poziom 3  

    Freddie Chopin napisał:
    BlueDraco napisał:
    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

    0
  • Pomocny post
    #21 21 Lis 2018 23:33
    Freddie Chopin
    Specjalista - Mikrokontrolery

    gogus9 napisał:
    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.

    0
  • #22 22 Lis 2018 00:28
    gogus9
    Poziom 3  

    Dziękuję. Mam wrażenie, że teraz już wszystko jasne.

    0