Upss… Coś nie tak z Twoją przeglądarką
Do poprawnego wyświetlania formularza zalecana jest przeglądarka Chrome lub Safari.
Blog

Optymalizacja pamięci w systemach wbudowanych w języku C – od czego zacząć?

Rozwój aplikacji wbudowanych polega zarówno na ich usprawnianiu, jak i dodawaniu nowych funkcji. Każda zmiana ma jednak wpływ na ilość pamięci wymaganej do poprawnego działania systemu. Często w początkowych fazach projektu nie zwracamy uwagi na to, jak blisko fizycznych limitów pamięci już na tym etapie jesteśmy. Wreszcie nadchodzi moment, gdy nie ma już możliwości wybrania innej platformy sprzętowej. Jak znaleźć rozwiązanie tego problemu?

Czym są systemy wbudowane?

Systemy wbudowane (z ang. embedded systems) to oprogramowanie ściśle powiązane ze sprzętem oraz przeznaczone do konkretnych zadań. Takie systemy spotkamy między innymi w inteligentnych czujnikach w naszym domu. Wykonują on określone zadania, polegające na przykład na pomiarze i raportowaniu temperatury otoczenia. W ten sposób, dzięki ograniczonej funkcjonalności, można zmniejszyć koszt produkcji samego sprzętu, ponieważ tego typu proste aplikacje wbudowane nie wymagają dużej ilości zasobów.

Systemy wbudowane a operacyjne

W opozycji do systemów wbudowanych stoją np. systemy operacyjne, takie jak Windows czy Android, które nie tylko mogą być uruchamiane na szerokiej gamie urządzeń, ale także pozwalają na tworzenie aplikacji działających w swoim obrębie. Wymagają one znacznie większej mocy obliczeniowej i angażują dużo więcej pamięci, przez co cena sprzętu automatycznie rośnie.

Hipotetyczna sytuacja zbudowania sieci inteligentnych czujników, w której każde z urządzeń działa w systemie Windows, byłaby kuriozalna. Oczywiście odpowiednia aplikacja na systemie Windows jest sobie w stanie z tym wyzwaniem poradzić, ale przypominałoby to wykorzystywanie koparki do przesadzania kwiatów w ogrodzie. Z tego powodu powstają dedykowane systemy, które dostarczają wszystkich wymaganych funkcji przy jak najmniejszym nakładzie sprzętowym.

Wybór mikrokontrolera do konkretnego systemu embedded

Do prawidłowego działania aplikacja wbudowana wymaga mikrokontrolera, który steruje urządzeniem. To chip zawierający procesor, pamięć oraz interfejsy wejścia/wyjścia. Powyższe składowe stanowią czynnik wyboru mikrokontrolera do danego systemu wbudowanego. Gdy znajdziemy już odpowiedniego kandydata posiadającego wymagane interfejsy oraz wystarczającą moc obliczeniową, przychodzi pora na pamięć. Producenci często dostarczają mikrokontrolery w rozmaitych wariantach, z odmienną ilością pamięci i w różnych cenach.

Wyzwania i sposoby optymalizacji pamięci w systemach wbudowanych

Ocenienie rozmiaru aplikacji w początkowych fazach projektu jest dużym wyzwaniem. Zanim ruszy masowa produkcja, można zmienić wariant procesora na opcję z większą pamięcią, jednak po uruchomieniu produkcji lub rynkowej premierze produktu pozostają już wyłącznie rozwiązania możliwe do zrealizowania w samej aplikacji. Zwiększenie dostępnej ilości pamięci z pomocą aktualizacji oprogramowania nie jest możliwe. Można jednak spróbować zaimplementować tę samą funkcjonalność przy jednoczesnym użyciu mniejszej ilości zasobów. W ten sposób wchodzimy w zagadnienie optymalizacji zużycia pamięci w systemach wbudowanych w języku C.

Optymalizacja zużycia pamięci – od czego zacząć?

Warto zadbać, by zmiany mające na celu odzyskanie pamięci nie spowodowały niespodziewanego zachowania aplikacji. Dotyczy to każdej zmiany w kodzie źródłowym. Dlatego przed wprowadzaniem zmiany dobrze jest uzbroić się w zestaw testów regresyjnych oraz skrypt, który zwróci ilość pamięci wymaganej przez aplikację. Taki zestaw narzędzi zapewni programiście ciągłą i łatwo dostępną informację zwrotną, czy nowe zmiany mają pozytywny wpływ na rozmiar aplikacji oraz czy nie powodują nowych problemów.

Służą do tego CI/CD, czyli Continuous Integration (CI) oraz Continuous Deplyment/Delivery (CD) (Ciagła Integracja oraz Ciągłe Dostarczanie), czyli zestaw praktyk i narzędzi, które pozwalają na usprawnianie i automatyzację procesu wytwarzania oprogramowania.

CI ma na celu regularną weryfikację, czy nowy kod dobrze integruje się z obecną implementacją. W jego skład wchodzą wszelkiego rodzaju testy automatyczne. CD rozszerza za to CI o kroki, które umożliwiają ciągłą gotowość do wydania kolejnej wersji oprogramowania. Gdy znany jest proces publikacji, może on zostać zautomatyzowany, przez co minimalizujemy ryzyko błędów ludzkich i zmniejszamy czas przygotowywania. Jeśli te narzędzia mogą zostać uruchomione na innej maszynie w ramach systemu CI/CD, zyskujemy idealne środowisko do pracy. Temat testów jest bardzo obszerny i nie będą tu rozwijane, ale informacje z kolejnych części artykułu powinny pomóc w stworzeniu skryptu do pomiaru pamięci.

Plusy CI/CD to między innymi:

  • Wczesne wykrywanie błędów: zestaw testów automatycznych, które są wykonywane często codziennie w formie testów nocnych (nightly tests) dostarcza ciągłą informację zwrotną na temat nowych błędów.
  • Zwiększenie jakości oprogramowania: zwiększony nakład testów w połączeniu z ich częstym wykonywaniem pozwala wyłapywać błędy i szybko je naprawiać, zanim trafią do głównej bazy kodu.
  • Skrócony cykl publikacji: zautomatyzowany cykl wydawania oprogramowania sprawia, że produkt w aktualnym stanie jest gotowy do wydania zwykle w kilka godzin. Gdyby był to proces manualny, czas by się wydłużył, a my narazilibyśmy się na błędy.

Kluczowy krok – analiza pamięci

RAM (Random Access Memory) i ROM (Read-Only Memory) to dwa podstawowe rodzaje pamięci rozróżniane w systemach wbudowanych. Każdy z nich pełni różne role:

Mając zestaw narzędzi do bezpiecznej pracy nad kodem, można przystąpić do wprowadzania zmian. Gdzie zatem szukać optymalizacji? Podstawową metodą monitorowania zużycia pamięci jest analiza pliku mapfile, dzięki któremu wiemy, gdzie w pamięci umieszone są konkretne symbole oraz jaki mają rozmiar. Z tego pliku jesteśmy w stanie wyciągnąć listę wszystkiego, co znajduje się zarówno w pamięci RAM, jak i w ROM.

Przestrzeń adresowa – rozkład danych w pamięci

Powyższa grafika prezentuje rozkład danych w pamięci. Wyszczególniamy przedziały adresów, które odpowiadają poszczególnym sekcjom pamięci RAM i ROM. W pliku mapfile zobaczymy taką właśnie strukturę. W trakcie dalszej optymalizacji skupimy się na takich elementach jak:

  • .text – dane tylko do odczytu, instrukcje programu – ROM
  • .data – zmienne zainicjalizowane – RAM i ROM
  • .bss – zmienne niezaincjalizowane – RAM
  • heap (sterta) – sekcja pamięci przeznaczona na dynamiczną alokację – RAM
  • stack (stos) – sekcja pamięci przeznaczona do przechowywania zmiennych lokalnych i ramek stosu, dzięki którym aplikacja może być zbudowana z funkcji – RAM

Warto mapę pamięci przekonwertować do pliku .csv, co pozwoli na sortowanie i filtrowanie w celu łatwiejszej analizy. Chcąc optymalizować ROM, przyglądamy się symbolom typu .text i .data, w przypadku RAMu – symbolom .bss i .data. Co ze stosem (stack) i stertą (heap)? Sterta jest wykorzystywana do dynamicznej alokacji, tymczasem stos to pamięć operacyjna. Optymalizacja obu pozytywnie wpłynie na zużycie pamięci RAM.

Flagi kompilacji – metody optymalizacji dostarczane przez kompilator GCC

Przed wprowadzaniem zmian w kodzie warto zapoznać się z opcjami optymalizacji, które dostarcza kompilator. Część z nich ma wpływ na prędkość i rozmiar aplikacji. W niektórych przypadkach użycie optymalizacji -Os może rozwiązać problem z pamięcią. Oto lista flag kompilatora GCC w tłumaczeniu na język polski z instrukcji GCC:

  • O lub -O1: Optymalizacja. Optymalizacja kompilacji zajmuje nieco więcej czasu i angażuje dużo więcej pamięci w przypadku dużej funkcji. Z opcją -O kompilator stara się zmniejszyć rozmiar kodu i czas wykonania, bez dokonywania optymalizacji, które wymagają dużo czasu kompilacji.
  • O2: Jeszcze większa optymalizacja. GCC wykonuje prawie wszystkie obsługiwane optymalizacje, które nie wiążą się z kompromisem między przestrzenią a prędkością. W porównaniu do -O, opcja ta zwiększa zarówno czas kompilacji, jak i wydajność wygenerowanego kodu.
  • O3: Największa optymalizacja. -O3 umożliwia wszystkie optymalizacje określone przez -O2, a także inne, dodatkowe. To często najlepsza opcja do wyboru.
  • O0: Skrócenie czasu kompilacji i uzyskanie oczekiwanych wyników podczas debugowania. To ustawienie domyślne.
  • Os: Optymalizacja pod kątem rozmiaru. -Os umożliwia wszystkie optymalizacje -O2, które zazwyczaj nie zwiększają rozmiaru kodu. Wykonuje również kolejne optymalizacje mające na celu zmniejszenie rozmiaru kodu.
  • Ofast: Pomijanie ścisłej zgodności ze standardami. -Ofast umożliwia wszystkie optymalizacje -O3. Poza tym umożliwia również optymalizacje, które nie są ważne w przypadku wszystkich zgodnych ze standardami programów.
  • Og: Optymalizacja doświadczeń debugowania. -Og umożliwia optymalizacje, które nie kolidują z debugowaniem. To poziom optymalizacji, który powinien być domyślnie wybierany dla standardowego cyklu edycja-kompilacja-debugowanie, ponieważ oferuje rozsądny poziom optymalizacji przy zachowaniu szybkiej kompilacji i dobrej obsługi debugowania. 
  • Oz: Agresywna optymalizacja pod kątem rozmiaru zamiast prędkości. Może to zwiększyć liczbę wykonywanych instrukcji, jeśli wymagają one mniejszej liczby bajtów do zakodowania. -Oz zachowuje się podobnie do -Os, włączając w to umożliwienie większości optymalizacji -O2.

Architektura wspierająca zarządzanie pamięcią

Jeśli jesteśmy na etapie projektowania architektury, warto już na tym etapie rozważyć podjęcie określonych decyzji, które ułatwią późniejsze zarządzanie pamięcią. Dobrą praktyką jest podział aplikacji na niezależne moduły, które według uznania mogą być dołączone lub nie. Przydaje się to szczególnie wtedy, gdy aplikacja jest dostosowywana do potrzeb rożnych klientów.

Ponadto dobrą praktyką jest inicjalizacja wymaganych modułów zaraz po starcie programu, by na tym etapie zweryfikować ilość pamięci wymaganej do ich poprawnego działania. Jeśli aplikacja może być zróżnicowana w zależności od potrzeb klienta, to budowanie jej z modułów inicjalizowanych zaraz po uruchomieniu pozwoli na szybką weryfikację, czy taka kombinacja jest w stanie działać na określonym sprzęcie. Wprowadzanie tego mechanizmu od samego początku pozwala zaoszczędzić czas w przyszłości.

Podsumowanie

Wszystko, co opisaliśmy powyżej, pozwoli przygotować się na bezpieczne i szybkie wprowadzanie zmian w kodzie, które pomoże zoptymalizować pamięć aplikacji. Kolejnym etapem jest wybór metod optymalizacji RAM oraz ROM, które opiszemy wkrótce.

Dodaj komentarz

      Adres e-mail nie zostanie opublikowany
            Komentarze
            (0)

              Najczęściej czytane w kategorii Technologie