Pracowałem nad opracowaniem funkcji konkretnego produktu. Zgłoszono prośbę o przeniesienie tej samej funkcji do innego produktu. Produkt ten oparty jest na mikrokontrolerze M16C, który tradycyjnie ma 64K Flash i 2k RAM.
Jest to produkt dojrzały, dlatego pozostało mu tylko 132 bajty pamięci flash i 2 bajty pamięci RAM.
Aby przenieść żądaną funkcję (sama funkcja została zoptymalizowana), potrzebuję 1400 bajtów Flasha i ~ 200 bajtów RAM.
Czy ktoś ma jakieś sugestie, jak odzyskać te bajty przez kompaktowanie kodu? Jakich konkretnych rzeczy szukam, gdy próbuję skompaktować już działający kod?
Wszelkie pomysły będą naprawdę mile widziane.
Dzięki.
Odpowiedzi:
Masz kilka opcji: po pierwsze, poszukaj zbędnego kodu i przenieś go do pojedynczego wywołania, aby pozbyć się duplikacji; drugim jest usunięcie funkcjonalności.
Przyjrzyj się plikowi .map i sprawdź, czy istnieją funkcje, których możesz się pozbyć lub przepisać. Upewnij się również, że używane wywołania biblioteki są naprawdę potrzebne.
Niektóre rzeczy, takie jak dzielenie i mnożenie, mogą przynieść dużo kodu, ale użycie przesunięć i lepsze wykorzystanie stałych może zmniejszyć kod. Zobacz także rzeczy takie jak stałe ciągów i
printf
s. Na przykład, każdyprintf
zje Twój ROM, ale możesz mieć kilka wspólnych ciągów formatu zamiast powtarzać stałą ciąg w kółko.Jeśli chodzi o pamięć, sprawdź, czy możesz pozbyć się globali i zamiast tego użyj funkcji automatycznych. Unikaj także jak największej liczby zmiennych w funkcji głównej, ponieważ pochłaniają one pamięć tak jak globale.
źródło
Zawsze warto spojrzeć na dane wyjściowe pliku listy (asemblera), aby znaleźć rzeczy, w których twój kompilator jest szczególnie zły.
Na przykład może się okazać, że zmienne lokalne są bardzo drogie, a jeśli aplikacja jest na tyle prosta, że jest warta ryzyka, przeniesienie kilku liczników pętli do zmiennych statycznych może zaoszczędzić dużo kodu.
Lub indeksowanie tablic może być bardzo kosztowne, ale operacje wskaźnika są znacznie tańsze. Lub odwrotnie.
Ale spojrzenie na język asemblera to pierwszy krok.
źródło
Optymalizacje kompilatora, na przykład
-Os
w GCC, zapewniają najlepszą równowagę między szybkością a rozmiarem kodu. Unikaj-O3
, ponieważ może to zwiększyć rozmiar kodu.źródło
W przypadku pamięci RAM sprawdź zakres wszystkich zmiennych - czy używasz ints, w których możesz użyć znaku? Czy bufory są większe niż powinny być?
Wyciskanie kodu jest bardzo zależne od aplikacji i stylu kodowania. Twoje pozostałe kwoty sugerują, że być może kod już zniknął, co może oznaczać, że zostało niewiele.
Przyjrzyj się również ogólnej funkcjonalności - czy jest coś, co tak naprawdę nie jest używane i może zostać odrzucone?
źródło
Jeśli jest to stary projekt, ale kompilator został opracowany od tego czasu, być może nowszy kompilator może wygenerować mniejszy kod
źródło
Zawsze warto sprawdzić w instrukcji kompilatora opcje optymalizacji przestrzeni.
Dla gcc
-ffunction-sections
i-fdata-sections
z--gc-sections
flagą łącznikową są dobre do usuwania martwego kodu.Oto kilka innych doskonałych wskazówek (ukierunkowanych na AVR)
źródło
Możesz sprawdzić ilość przydzielonego miejsca na stosie i stosu. Możesz odzyskać znaczną ilość pamięci RAM, jeśli jedno z nich lub oba są nadmiernie przydzielone.
Moje przypuszczenie jest dla projektu, który pasuje do 2k pamięci RAM na początek nie jest alokacja pamięci dynamicznej (wykorzystanie
malloc
,calloc
itp). W takim przypadku możesz całkowicie pozbyć się sterty, zakładając, że oryginalny autor zostawił pamięć RAM przydzieloną na stertę.Musisz bardzo ostrożnie zmniejszać rozmiar stosu, ponieważ może to powodować bardzo trudne do znalezienia błędy. Pomocne może być rozpoczęcie od zainicjowania całego obszaru stosu na znaną wartość (coś innego niż 0x00 lub 0xff, ponieważ te wartości występują już często), a następnie uruchom system na chwilę, aby zobaczyć, ile miejsca na stosie jest nieużywane.
źródło
Czy Twój kod używa matematyki zmiennoprzecinkowej? Być może będziesz w stanie ponownie zaimplementować swoje algorytmy przy użyciu tylko matematyki liczb całkowitych i wyeliminować koszty związane z używaniem biblioteki zmiennoprzecinkowej C. Np. W niektórych aplikacjach takie funkcje jak sinus, log, exp można zastąpić całkowitymi przybliżeniami wielomianowymi.
Czy Twój kod używa dużych tabel przeglądowych dla jakichkolwiek algorytmów, takich jak obliczenia CRC? Możesz spróbować zastąpić inną wersję algorytmu, który oblicza wartości w locie, zamiast korzystać z tabel przeglądowych. Zastrzeżenie polega na tym, że mniejszy algorytm jest najprawdopodobniej wolniejszy, więc upewnij się, że masz wystarczającą liczbę cykli procesora.
Czy Twój kod zawiera duże ilości stałych danych, takich jak tabele ciągów, strony HTML lub grafika pikselowa (ikony)? Jeśli jest wystarczająco duży (powiedzmy 10 kB), warto wdrożyć bardzo prosty schemat kompresji, aby zmniejszyć dane i rozpakować je w locie w razie potrzeby.
źródło
Możesz spróbować dużo zmienić kodowanie, aby uzyskać bardziej kompaktowy styl. Wiele zależy od tego, co robi kod. Kluczem jest znalezienie podobnych rzeczy i ponowne ich wdrożenie w odniesieniu do siebie nawzajem. Skrajną sytuacją byłoby użycie języka wyższego poziomu, takiego jak Forth, dzięki któremu łatwiej jest osiągnąć wyższą gęstość kodu niż w C lub asemblerze.
Oto Forth dla M16C .
źródło
Ustaw poziom optymalizacji kompilatora. Wiele IDE ma ustawienia, które pozwalają na optymalizację rozmiaru kodu kosztem czasu kompilacji (a czasem nawet czasu przetwarzania). Mogą osiągnąć kompaktowanie kodu przez ponowne uruchomienie optymalizatora kilka razy, szukając mniej powszechnych wzorców, które można zoptymalizować, i całą masę innych sztuczek, które mogą nie być konieczne do kompilacji zwykłej / debugowania. Zwykle kompilatory są domyślnie ustawione na średni poziom optymalizacji. Przekop się w ustawieniach i powinieneś być w stanie znaleźć skalę optymalizacji opartą na liczbach całkowitych.
źródło
Jeśli już używasz kompilatora na poziomie profesjonalnym, takiego jak IAR, myślę, że będziesz miał trudności z uzyskaniem poważnych oszczędności dzięki drobnym ulepszeniom kodu na niskim poziomie - musisz bardziej szukać możliwości usunięcia funkcjonalności lub robienia większych zadań przepisuje części w bardziej wydajny sposób. Musisz być mądrzejszym programistą niż ktokolwiek, kto napisał oryginalną wersję ... Jeśli chodzi o pamięć RAM, musisz bardzo uważnie przyjrzeć się, jak jest obecnie używana, i sprawdzić, czy istnieje możliwość nałożenia użycia tej samej pamięci RAM na różne rzeczy w różnych momentach (związki są do tego przydatne). Domyślne rozmiary sterty i stosu w IAR w ARM / AVR, które zwykle przesadziłem, byłyby więc pierwszą rzeczą, na którą należałoby spojrzeć.
źródło
Coś jeszcze do sprawdzenia - niektóre kompilatory w niektórych architekturach kopiują stałe do pamięci RAM - zwykle używane, gdy dostęp do stałych flash jest powolny / trudny (np. AVR) np. Kompilator AVR IAR wymaga kwalifikatora _flash, aby nie kopiować stałej do pamięci RAM)
źródło
Jeśli twój procesor nie obsługuje sprzętu dla stosu parametrów / lokalnego, ale kompilator i tak próbuje zaimplementować stos parametrów w czasie wykonywania, a jeśli twój kod nie musi być ponownie wprowadzany, możesz zapisać kod spacja przez statyczne przydzielanie zmiennych automatycznych. W niektórych przypadkach należy to zrobić ręcznie; w innych przypadkach dyrektywy kompilatora mogą to zrobić. Skuteczne ręczne przydzielanie będzie wymagało współdzielenia zmiennych między procedurami. Takie współdzielenie musi być wykonane ostrożnie, aby upewnić się, że żadna procedura nie używa zmiennej, którą inna procedura uważa za „w zakresie”, ale w niektórych przypadkach korzyści wynikające z wielkości kodu mogą być znaczące.
Niektóre procesory mają konwencje wywoływania, które mogą sprawić, że niektóre style przekazywania parametrów będą bardziej wydajne niż inne. Na przykład w sterownikach PIC18, jeśli procedura pobiera pojedynczy parametr jednobajtowy, może zostać przekazana do rejestru; jeśli zajmie to więcej, wszystkie parametry muszą zostać przekazane w pamięci RAM. Jeśli procedura wymaga dwóch jednobajtowych parametrów, może być najbardziej wydajne „przekazać” jeden ze zmiennych globalnych, a następnie przekazać drugi jako parametr. Dzięki powszechnie stosowanym procedurom oszczędności mogą się sumować. Mogą być one szczególnie istotne, jeśli parametr przekazywany przez globalny jest flagą jednobitową lub jeśli zwykle ma wartość 0 lub 255 (ponieważ istnieją specjalne instrukcje do zapisania 0 lub 255 w pamięci RAM).
W ARM umieszczenie zmiennych globalnych, które są często używane razem w strukturze, może znacznie zmniejszyć rozmiar kodu i poprawić wydajność. Jeśli A, B, C, D i E są osobnymi zmiennymi globalnymi, wówczas kod, który używa ich wszystkich, musi załadować adres każdego z nich do rejestru; jeśli nie ma wystarczającej liczby rejestrów, konieczne może być ponowne załadowanie tych adresów wiele razy. Natomiast jeśli są częścią tej samej globalnej struktury MyStuff, to kod korzystający z MyStuff.A, MyStuff.B itp. Może po prostu załadować adres MyStuff jeden raz. Wielka wygrana.
źródło
1. Jeśli twój kod opiera się na wielu strukturach, upewnij się, że elementy struktury są uporządkowane od tych, które zajmują najwięcej pamięci do najmniejszej.
Przykład: „uint32_t uint16_t uint8_t” zamiast „uint16_t uint8_t uint32_t”
Zapewni to minimalne wypełnienie konstrukcji.
2. W stosownych przypadkach użyj const dla zmiennych. Zapewni to, że te zmienne będą w pamięci ROM i nie zajmą pamięci RAM
źródło
Kilka (być może oczywistych) sztuczek, których z powodzeniem użyłem do kompresji kodu klienta:
Kompaktowe flagi w pola bitowe lub maski bitowe. Może to być korzystne, ponieważ zwykle wartości logiczne są przechowywane jako liczby całkowite, marnując w ten sposób pamięć. Pozwoli to zaoszczędzić zarówno pamięć RAM, jak i pamięć ROM i zwykle nie jest wykonywane przez kompilator.
Poszukaj nadmiarowości w kodzie i użyj pętli lub funkcji do wykonywania powtarzających się instrukcji.
Zapisałem również trochę pamięci ROM, zastępując wiele
if(x==enum_entry) <assignment>
instrukcji ze stałych tablicą indeksowaną, dbając o to, aby wpisy enum mogły być używane jako indeks tablicyźródło
Jeśli możesz, użyj funkcji wbudowanych lub makr kompilatora zamiast małych funkcji. Narzuty związane z wielkością i prędkością są przekazywane wraz z argumentami i można temu zaradzić, ustawiając funkcję w wierszu.
źródło
int get_a(struct x) {return x.a;}
Zmień zmienne lokalne, aby były tego samego rozmiaru w rejestrach procesora.
Jeśli procesor jest 32-bitowy, użyj 32-bitowych zmiennych, nawet jeśli maksymalna wartość nigdy nie przekroczy 255. Użyłem 8-bitowej zmiennej, kompilator doda kod, aby zamaskować górne 24 bity.
Najpierw zajrzałbym do zmiennych pętli for.
Może to wydawać się dobrym miejscem dla 8-bitowej zmiennej, ale 32-bitowa zmienna może generować mniej kodu.
źródło