Wstęp
Rozmiar nas ogranicza. W przypadku komputerów klasy PC nie jest to problem aż tak znaczący jak w architekturze AVR, gdzie ogranicza nas pamięć o rozmiarze 4/8/16/32KB.
Sporo miejsca możemy zaoszczędzić zmuszając kompilator do optymalizacji rozmiaru, usuwając nieużywane funkcje z kodu, itp.
Mniejszy rozmiar pliku binarnego to także szybsze uruchomienie aplikacji.
Używając ATMEL STUDIO (na przykład AVR)
Domyślnie używając profilu ‚Release’ w Atmel Studio – kroki 2 i 3 będą automatycznie wykonane. Nie ma sensu modyfikować istniejących.
Krok 1 – myślimy
Ten punkt nie zawiera żadnych wzniosłych treści. Należy po prostu zastanowić się nad użyciem pewnych rozwiązań.
Bezsensowne kopiowanie kodu powoduje większy rozmiar pliku – zamiast skoków w odpowiednie miejsca w pliku wykonywalnym muszą być zapisane powtórzone fragmenty.
Trzeba też zastanowić się nad użyciem funkcji z biblioteki standardowej. W przypadku systemów gdzie jest ona zlinkowana dynamicznie – nie ma to aż takiego znaczenia. Użyte funkcje i tak tam są. Problem natomiast pojawia się w rozwiązaniach typu AVR. Kod funkcji musi zostać przeniesiony do pamięci procesora. Wyjątkowo duże są funkcje zamiany tekstu na liczby i liczb na tekst. Ich rozmiar jest znośny dla typów całkowitych. Dla typów zmiennoprzecinkowych rozmiary ftoa, sprintf itp są już bardzo duże.
Dokładna lista potrzebnych zasobów – flash, ram, cpu znajduje się na stronie: http://www.nongnu.org/avr-libc/user-manual/benchmarks.html
Chciałbym podać tu także przykład zastosowania funkcji memcpy do krótkich ciągów:
const char KrotkiTekst[] = "ABCD"; memcpy(docelowe miejsce, KrotkiTekst, sizeof(KrotkiTekst));
vs
DoceloweMiejsce[0] = 'A'; DoceloweMiejsce[1] = 'B'; ...
Jeśli jest to jedyne użycie funkcji memcpy – zastąpienie jej statycznym kodem pozwoli zaoszczędzić około 20 bajtów. Być może niektórych zastanowi sensowność użycia powyższej składni – słusznie – nie ma ona zbyt wielkiego sensu, pokazuje jednak jak mało zgrabne zabiegi pozwalają zaoszczędzić miejsce. Instrukcje obsługi pętli zostaną zastąpione kilkoma (pewnie po 1 na znak) instrukcjami przypisania.
Krok 1.5 – oszczędzamy miejsce – złe praktyki z pisania pod x86
Atmegi to architektura 8 bitowa. Oznacza to, że jedna instrukcja wykonuje operacje na 8 bitowych słowach. Standard języka C definiuje typ ‚int’ jako co najmniej 16 bitowy. Skutkuje to prawie dwukrotnie wyższym zużyciem pamięci (i później czasu procesora) we wszystkich miejscach gdzie używamy tego typu. Tak, gdzie wystarczy nam 8 bitowa zmienna – śmiało używamy typu uint8_t i int8_t.
Przykładowo w pętli w której musimy wykonać mniej niż 256 operacji – jako typu zmiennej i możemy (i nawet stanowczo powinniśmy) używać uint8_t.
Krok 2 – włączamy optymalizacje kompilatora
Na tym etapie musimy wykonać kilka kroków:
- Dla kompilatora uruchomić Os (Os zawiera w sobie wszystkie optymalizacje O2 które nie wpływają negatywnie na rozmiar)
Dla linkera wskazać flagę -s która usunie informacje o sekcjach. Na procesorze są one do niczego nie potrzebne. Z pliku binarnego można usunąć te informacje poleceniem strip.W zasadzie niepotrzebne – na etapie tworzenia obrazu do wgrania na urządzenie symbole zostaną usunięte
- Usunąć flagę dodawania informacji debugowania, jeśli taka istniała. Domyślnie jest to flaga -g – ma jej nie być
Krok 3 – usuwanie nieużywanych funkcji
Rozwiązaniem jest zmodyfikowanie procesu kompilacji / linkowania tak, aby do ostatecznego pliku binarnego weszły wyłącznie wykorzystane elementy.
Pierwszym krokiem jest dodanie dwóch przełączników do kompilatora. Są to:
-ffunction-sections
-fdata-section
Użycie tych dwóch przełączników spowoduje wygenerowanie sekcji kodu dla każdej funkcji / danej. Umożliwi to linkerowi wycięcie niepotrzebnych sekcji. Rozmiary obiektów będą większe (bo zawierają te informacje), jednak rozmiar pliku wyjściowego po obcięciu tych sekcji będzie niższy.
Drugim krokiem jest dodanie opcji do linkera:
-Wl,--gc-sections
Przełącznik -Wl oznacza, że następujące po nim polecenia zostaną przekazane do linkera (dla gcc i g++ jest to ld).
–gc-sections to już właściwe polecenie, które spowoduje usunięcie niezlinkowanych z niczym sekcji z pliku wyjściowego.
Krok 4 – wyciskanie z kompilatora jeszcze więcej
Praktycznie obowiązkowymi flagami dla kompilatora (nie powodują skutków ubocznych) są:
-funsigned-bitfields -fshort-enums -fpack-struct
Odpowiadają kolejno za: bezznakowy typ w polach bitowych, długość typu wyliczeniowego dopasowana do potrzeb (możliwych wartości) a nie = 16 bitów, maksymalne upchnięcie elementów struktur.
Jeśli jesteśmy świadomi możliwych skutków, możemy dodać też:
-mcall-prologues
Zapisywanie stanu programu przed wejściem do procedury jest obsługiwane przez jedną dedykowaną procedurę (a nie osobny kod dla każdej procedury). Tracimy jednak na fakcie, że domyślna procedura obsługi zapisuje wszystkie rejestry, a nie tylko te których potrzebujemy.
-ffreestanding
Spowoduje wyłączenie wszystkich wbudowanych funkcji, ale jednocześnie pozwoli zadeklarować main() z typem void – oszczędzić na kilku instrukcjach zwracających wartość
-flto
Bardzo szatański trick, ale dostępny w edycji kompilatora >= 3.5 (dostępna na stronie Atmel’a od 2015 roku. Należy szukać pod hasłem AVR 8 Toolchain). Powoduje wykonanie optymalizacji w trakcie linkowania. Zalecane jest (różne źródła podają) aby kompilować od razu do pliku .elf (czyli bez kompilacji do obiektów!). Technika, przynajmniej u mnie – niezwykle skuteczna, zmniejszyła kod o ponad 25%.
-Wl,--relax
Opcja przekazywana do linkera – powoduje próbę zamienienia intrukcji skoku ‚pod adres’ instrukcjami skoku ‚o ileś do przodu, o ileś do tyłu’. Są one dwa razy mniejsze.
Krok 5 – możliwość analizy które części programu zabierają najwięcej pamięci
Niestety wraz z uruchamianiem kolejnych flag optymalizacji – tracimy bezpośredni rzut naszego kodu – funkcji, metod, zmiennych na docelowy assembler. Wyłączenie optymalizacji spowoduje rozmycie informacji które funkcje zabierają najwięcej miejsca.
Podsumowanie
W wolnej chwili wykonam pomiary rozmiaru poszczególnych obiektów.