C++: mity i fakty

| Technika

W latach 90. rozpowszechniła się w kręgach zainteresowanych tematyką opinia, że język C++ nie nadaje się do tego, żeby pisać w nim programy dla mikrokontrolerów jednoukładowych. W przypadku małych systemów 8- i 16-bitowych, dominujących w tamtym okresie, jest to zapewne prawda. Ale dziś, w dobie tanich mikroprocesorów 32-bitowych o wielkiej (w porównaniu do tych z poprzednich dekad) mocy przetwarzania i znacznych zasobach, można sobie zadać pytanie, na ile ta niezbyt pochlebna dla C++ opinia ma nadal pokrycie w faktach, a na ile jest utrzymującym się siłą inercji mitem.

C++: mity i fakty

RTTI

Mechanizm RTTI (Run-Time Type Information, informacja o typie w trakcie wykonywania programu) pozwala na odnalezienie dokładnej informacji o typie danego obiektu, gdy do dyspozycji jest tylko wskaźnik lub odwołanie do typu bazowego. Taka definicja sugeruje, że RTTI będzie źródłem strat w wydajności działania kodu, ale tak nie jest. Jedyny koszt, jaki ponosimy, polega na tym, że do każdej klasy polimorficznej dodawany jest obiekt type_info, a one nie są duże - dla poniższego przykładu:

class Baza
{
public:
virtual ~Baza() {}
};
class Pochodna: public Baza {};
class DrugaPochodna: public Pochodna {};

Łączny rozmiar obiektów type_info dla klasy Druga- Pochodna wyniesie kilkadziesiąt (około 30) bajtów, o czym można się łatwo przekonać przy użyciu debuggera albo po prostu zaglądając do tekstu w asemblerze wygenerowanego przez kompilator. Wiele kompilatorów ma opcje wyłączające RTTI, co pozwala się łatwo pozbyć i tego narzutu.

Fragmentacja pamięci

Trudnością, której wystąpienia w niewielkich systemach mikroprocesorowych można nie przewidzieć, jest fragmentacja pamięci. W istocie jest to problem wspólny dla wszystkich systemów - małych i niedziałających na pamięci rzeczywistej, bez jej stronicowania i wirtualizacji adresów. Dotyczy to zwłaszcza systemów wielozadaniowych, ale nie tylko. W nowoczesnych systemach operacyjnych dla komputerów osobistych, serwerów itp. pamięć jest zwirtualizowana, problem fragmentacji nie występuje, a co za tym idzie, programiści są z nim na ogół nieobeznani i mogą nie pomyśleć o możliwości jego wystąpienia.

Fragmentacja jest nieuchronnym skutkiem ubocznym działania mechanizmu dynamicznej alokacji pamięci. Wyobraźmy sobie sytuację, kiedy procedura alokacji pobiera z systemu blok wolnej pamięci o zadanej wielkości i przydziela go programowi. Po jakimś czasie program żąda alokacji następnego bloku i dostaje go. Każda taka operacja powoduje, naturalnie, zmniejszenie się ilości wolnej pamięci.

Teraz, jeśli program zwolni pierwszy blok, ale zatrzyma sobie drugi, ilość wolnej pamięci formalnie wzrośnie o wielkość zwolnionego bloku, ale wolny obszar będzie teraz nieciągły: będą się na niego składać dwa mniejsze obszary ("fragmenty") przedzielone blokiem nadal zajętym przez program, a rzeczywisty, możliwy do alokacji obszar RAM-u zmniejszy się do wielkości równej wielkości największego fragmentu.

Na przykład, załóżmy, że początkowa ilość wolnej pamięci RAM w systemie to 1 MB. Program zajmuje blok nr 1 o wielkości 200 KB, potem blok nr 2 o wielkości 300 KB, a w następnej kolejności zwalnia blok nr 1. W tym momencie w systemie formalnie jest 700 KB wolnej pamięci pozostającej do dyspozycji programów, ale największy obszar możliwy do zajęcia pojedynczym wywołaniem malloc() ma już tylko 500 KB.

Asynchroniczna alokacja i dealokacja różnej wielkości bloków podczas działania programu będzie tę sytuację dalej pogarszać, prowadząc do stopniowego rozdrabniania - właśnie "fragmentacji" - obszarów wolnych na coraz mniejsze porcje. W końcu dojdzie do sytuacji, kiedy w systemie zabraknie wolnej pamięci mimo pozornej jej obfitości (ale będzie to np. 700 KB w 50 fragmentach, z których największy będzie miał np. 25 KB).

Co gorsza, negatywne skutki tego zjawiska mogą stać się widoczne dopiero po pewnym czasie: po wielu dniach lub nawet tygodniach nieprzerwanego działania systemu, przez co mogą umknąć uwadze programistów i testerów. W efekcie urządzenie, przeważnie działające bez zarzutu, będzie od czasu do czasu, bez widocznych przyczyn tajemniczo "padać".

Oczywistym - i w zasadzie jedynym - wyjściem z tej sytuacji jest stronicowanie pamięci i wirtualizacja adresów. Oczywiście w mikrokomputerach jednoukładowych, zwłaszcza małych, raczej nie można sobie na to pozwolić. W języku C - gdzie w zasadzie nic nie dzieje się bez wiedzy programisty - obchodzi się ten problem przez unikanie jak ognia użycia funkcji dynamicznej alokacji pamięci malloc() i free().

Często zresztą jest to łatwiejsze, niż mogłoby się na pierwszy rzut oka wydawać, bo zapotrzebowanie aplikacji na pamięć da się w bardzo wielu przypadkach ocenić bardzo dokładnie, co z kolei pozwala na jej przydział w postaci statycznych buforów o znanej z góry wielkości.

W C++ to samo podejście wymaga rezygnacji ze wspomnianych już powyżej funkcji new() i delete(), co, trzeba przyznać, jest większym poświęceniem niż rezygnacja z malloc() i free() w języku C. Można też spróbować ich reimplementacji tak, żeby zamiast przydzielać pamięć dynamicznie, przydzielały statyczne bufory o określonej z góry wielkości.