Najczęściej wybieranym językiem do programowania mikrokontrolerów jest C. Popularna jest opinia, że C++ do tego zadania się nie nadaje. Najczęściej podawane argumenty to wolniejsze wykonywanie się kodu, większe zużycie pamięci programu i RAMu oraz częste wykorzystywanie dynamicznej alokacji pamięci. Ostatnio znalazłem na YouTube prezentację dotyczącą wykorzystania C++ przy programowaniu systemów embedded.
Autor udowadnia w niej, że argumenty dotyczące szybkości wykonania oraz zużycia pamięci nie są prawdziwe. Skłoniło mnie to do wykonania własnych eksperymentów z C++ na STM32, a przy okazji także do poznania C++, ponieważ wcześniej nie miałem z nim za dużo do czynienia.
Zalety C++ na STM32
Najpierw warto zastanowić się nad pytaniem co takiego daje nam C++, że warto go używać zamiast C. Ja swoje próby przeprowadziłem z czystej ciekawości i chęci poznania nowego języka. Od razu jednak widzę kilka fajnych rzeczy, które mogą przyspieszyć pisanie kodu. Pierwszą z nich są typy zmiennych z biblioteki standardowej. Mamy wbudowane typy służące jako tablice np. vector, array, poza tym są listy, mapy czy hash table. Są też łatwiejsze w użyciu w porównaniu do C stringi. Dostajemy również różne funkcje działające na tych typach umożliwiające na przykład wypełnianie, sumowanie elementów, sortowanie itp. Inną ciekawą funkcjonalnością są templaty, dzięki którym możemy uniknąć duplikacji kodu. Nie pisałem jeszcze nic konkretnego w C++ na STM32, dlatego ciężko mi powiedzieć, na ile te rzeczy sprawdzają się w praktyce, ale wydają się przydatne.
Dostosowanie szablonu projektu
Pierwszym krokiem było dostosowanie mojego szablonu projektu do C++. W tym celu do plików nagłówkowych zawierających deklaracje funkcji napisanych w C dodałem linie:
#ifdef __cplusplus extern "C" { #endif /* __cplusplus */
Dzięki temu kompilator wie, że ma do czynienia z funkcjami w C i traktuje je w odpowiedni sposób, żeby były kompatybilne z C++. Kolejnym krokiem była modyfikacja makefile tak, aby aby używał kompilatora g++ do kompilacji i linkowania plików C++. We flagach kompilacji dla C++ wyłączyłem obsługę RTTI(Run-Time Type Information) i exceptionów. Dzięki temu wynikowe programy są dużo mniejsze. W prezentacji była również mowa o optymalizacji O3 i fladzę -flto włączającej optymalizację podczas linkowania. Domyślnie w szablonie nie włączam tych opcji, ponieważ utrudniają one debugowanie. Zaktualizowany szablon można znaleźć na moim GitHubie.
Unit testów opartych o Unity nie dostosuję do C++, ponieważ nie mogę bezpośrednio wywoływać funkcji C++ w kodzie C. Można próbować zrobić jakieś obejścia, ale stwierdziłem, że to zbyt dużo zachodu. Docelowo będę chciał używać frameworka do testów napisanego w C++ – CppUTest.
Test nr 1
Postanowiłem odtworzyć pierwsze porównanie pokazane w prezentacji. W tym celu użyłem następującego kodu w C:
#include "platform_specific.h" #include "stm32f4xx.h" #include <stdlib.h> #include "core_init/core_init.h" #define TAB_SIZE 2000 int main(void) { uint32_t i; uint32_t out = 42; uint32_t *tab; core_init(); tab = malloc(sizeof(uint32_t) * TAB_SIZE); for (i = 0; i < TAB_SIZE; i++) { tab[i] = 3; } for (i = 0; i < TAB_SIZE; i++) { out += tab[i]; } free(tab); return out % 256; }
i w C++:
#include "platform_specific.h" #include "stm32f4xx.h" #include <numeric> #include <vector> #include "core_init/core_init.h" int main(void) { core_init(); std::vector<uint32_t> tab(2*1000, 3); uint32_t out = std::accumulate(tab.begin(), tab.end(), 42u); return out % 256; }
Wyniki kompilacji dla C i C++ dla braku optymalizacji i optymalizacji o3 + flto:
text | data | bss | |
---|---|---|---|
C -o0 | 3280 | 1144 | 592 |
C -o3 | 3080 | 1144 | 592 |
C++ -o0 | 5680 | 1144 | 760 |
C++ -o3 | 3292 | 1144 | 600 |
Jak widać, po optymalizacji kod C++ jest o 200 bajtów większy. W prezentacji jest informacja, że tą różnicę można usunąć jeśli wyłączymy linkowanie niepotrzebnych bibliotek standardowych. Niestety nie udało mi się znaleźć informacji jak to zrobić, ale różnica 200 bajtów jest akceptowalna w zamian za dodatkowe możliwości, jakie oferuje C++.
Test nr 2
Poprzednio przeanalizowany przykład jest trochę naciągany. Pisząc w C na embedded nie będziemy wykorzystywali malloca. Zwykle tablicę taką jak w przykładzie zadeklarujemy sobie jako zmienną statyczną. Dzięki temu wynikowa aplikacja będzie dużo mniejsza. Przykładowy kod w C może wyglądać tak (użyłem słowa kluczowego volatile dla tablicy, żeby nie została usunięta podczas optymalizacji):
#include "platform_specific.h" #include "stm32f4xx.h" #include "core_init/core_init.h" #define TAB_SIZE 2000 static volatile uint32_t tab[TAB_SIZE]; int main(void) { uint32_t i; uint32_t out = 42; core_init(); for (i = 0; i < TAB_SIZE; i++) { tab[i] = 3; } for (i = 0; i < TAB_SIZE; i++) { out += tab[i]; } return out % 256; }
W C++ jeżeli chcemy mieć tablicę zaalokowaną statycznie, zamiast typu vector, możemy użyć typu array. Kod może wtedy wyglądać tak:
#include "platform_specific.h" #include "stm32f4xx.h" #include <array> #include <numeric> #include "core_init/core_init.h" static std::array<uint32_t, 2*1000> tab; int main(void) { core_init(); std::fill(tab.begin(), tab.end(), 3u); uint32_t out = std::accumulate(tab.begin(), tab.end(), 42u); return out % 256; }
Wyniki kompilacji:
text | data | bss | |
---|---|---|---|
C -o0 | 1136 | 0 | 8544 |
C -o3 | 980 | 0 | 8544 |
C++ -o0 | 1424 | 0 | 8544 |
C++ o3 | 980 | 0 | 8544 |
Widzimy, że po optymalizacji program C i C++ zajmuje tyle samo pamięci.
Podsumowanie
Jak wynika z prezentacji i co potwierdziłem testami, kod w C++ na mikrokontroler wcale nie musi zajmować więcej, niż ten pisany w C. Po listingach programów testowych widać, że kod w C++ jest bardziej zwięzły. Dzięki zastosowaniu funkcji z bibliotek standardowych nie trzeba robić żmudnych operacji za pomocą pętli, dlatego zmniejsza się ryzyko popełnienia głupiego błędu. Nie mierzyłem czasów wykonywania w swoich testach, ale z prezentacji wynika, że C++ w tym aspekcie też nie prezentuje się gorzej niż C. Na pewno jeszcze będę chciał użyć C++ w jakimś swoim projekcie. Mimo wszystko, na razie traktuję to bardziej jako ciekawostkę i zabawę. W poważnym projekcie, szczególnie w systemie hard real time, w dalszym ciągu użył bym C.
28 maja 2017 at 16:34
Generalnie chcesz użyć `-nostdlib`. Ale wtedy nie będziesz miał biblioteki standardowej 😉
Może to trochę inny target, ale być może zainteresuje cię ten post: https://dev.krzaq.cc/post/writing-cpp17-for-16bit-x86/
Też musiałem szukać oszczędności (chociaż bardziej chodziło o bajty).
1 czerwca 2017 at 00:05
Biblioteki standardowe właśnie chciałem używać. Tym bardziej, że w C++ są dużo fajniejsze niż w C. To była wręcz motywacja, żeby wypróbować C++.
A link faktycznie interesujący i bardzo zaawansowany.
17 lutego 2022 at 12:01
Przydałoby się jeszcze porównanie z językiem Rust, który rozwiązuje kilka problemów które ma C i C++.