Jak wycisnąć kod, aby uzyskać więcej pamięci Flash i RAM? [Zamknięte]

14

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.

IntelliChick
źródło
1
Dziękuję wszystkim za sugestie. Będę Cię na bieżąco informował o moich postępach i wymienił kroki, które zadziałały, a które nie.
IntelliChick
Ok, oto rzeczy, które próbowałem, które działały: Przeniesiono wersje kompilatora. Optymalizacja uległa znacznej poprawie, co dało mi około 2K Flasha. Przejrzałem pliki list, aby sprawdzić, czy nie ma nadmiarowych i nieużywanych funkcji (odziedziczonych z powodu wspólnej podstawy kodu) dla konkretnego produktu i zyskałem trochę więcej Flasha.
IntelliChick
W przypadku pamięci RAM wykonałem następujące czynności: Przejrzałem plik mapy, aby sprawdzić funkcje / moduły, które używały najwięcej pamięci RAM. Znalazłem naprawdę ciężką funkcję (12 kanałów, każdy ze stałą ilością przydzielonej pamięci), starszego kodu, zrozumiałem, co próbowałem osiągnąć, i zoptymalizowałem użycie pamięci RAM, dzieląc się informacjami między wspólnymi kanałami. To dało mi ~ 200 bajtów, których potrzebowałem.
IntelliChick
Jeśli masz pliki ascii, możesz użyć kompresji od 8 do 7 bitów. Oszczędzasz 12,5%. Użycie pliku zip zajęłoby więcej kodu do skompresowania i rozpakowania, niż po prostu pozwolić.
Sparky256

Odpowiedzi:

18

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 printfs. Na przykład, każdy printfzje 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.

Rex Logan
źródło
1
Dzięki za sugestie, zdecydowanie mogę wypróbować większość z nich, z wyjątkiem tej dla stałych ciągów. Jest to wyłącznie urządzenie osadzone, bez interfejsu użytkownika, dlatego w kodzie nie ma wywołań funkcji printf (). Mam nadzieję, że te sugestie powinny mnie przewrócić, aby uzyskać potrzebną mi pamięć RAM o pojemności 1400 bajtów / 200 bajtów.
IntelliChick
1
@IntelliChick, będziesz zaskoczony, jak wiele osób korzysta z printf () wewnątrz urządzenia osadzonego do drukowania w celu debugowania lub wysyłania do urządzenia peryferyjnego. Wygląda na to, że wiesz lepiej, ale jeśli ktoś wcześniej napisał kod w projekcie, nie zaszkodzi to sprawdzić.
Kellenjb
5
Aby rozwinąć mój poprzedni komentarz, zdziwiłbyś się również, ile osób dodaje instrukcje debugowania, ale nigdy ich nie usuwa. Nawet ludzie, którzy robią #ifdefs, wciąż się leniją.
Kellenjb,
1
Wielkie dzieki! Odziedziczyłem tę bazę kodów, więc na pewno nie będę jej szukał. Będę was informować na bieżąco o postępach i postarać się śledzić, ile bajtów pamięci lub Flasha zyskałem, robiąc to, jako odniesienie dla każdego, kto może to zrobić w przyszłości.
IntelliChick
Tylko pytanie na ten temat - co z zagnieżdżonymi wywołaniami funkcji przeskakuje z warstwy na warstwę. Ile to narzuca? Czy lepiej jest zachować modułowość, mając wiele wywołań funkcji, czy zmniejszyć liczbę wywołań funkcji i zaoszczędzić niektóre bajty. Czy to ma znaczenie?
IntelliChick
8

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
3
Bardzo ważne jest, abyś wiedział, co robi twój kompilator. Powinieneś zobaczyć, jaki jest podział w moim kompilatorze. To sprawia, że ​​dzieci płaczą (w tym ja).
Kortuk
8

Optymalizacje kompilatora, na przykład -Osw GCC, zapewniają najlepszą równowagę między szybkością a rozmiarem kodu. Unikaj -O3, ponieważ może to zwiększyć rozmiar kodu.

Thomas O
źródło
3
Jeśli to zrobisz, będziesz musiał ponownie przetestować WSZYSTKO! Optymalizacje mogą powodować, że działający kod nie będzie działał z powodu nowych założeń kompilatora.
Robert,
@Robert, jest to prawdą tylko wtedy, gdy używasz niezdefiniowanych instrukcji: np. A = a ++ będzie się kompilować inaczej w -O0 i -O3.
Thomas O
5
@Thomas nieprawda. Jeśli masz pętlę for, która opóźnia cykle zegara, wielu optymalizatorów zauważy, że nic w tym nie robisz, i usuniesz je. To tylko 1 przykład.
Kellenjb
1
@ thomas O, musisz także uważać na zmienne definicje funkcji. Optymalizatorzy wysadzą tych, którzy myślą, że znają dobrze C, ale nie rozumieją złożoności operacji atomowych.
Kortuk
1
Wszystkie dobre punkty. Lotne funkcje / zmienne z definicji NIE mogą być optymalizowane. Każdy optymalizator, który dokonuje optymalizacji takich (w tym czas połączenia i wstawianie) jest uszkodzony.
Thomas O
8

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?

mikeselectricstuff
źródło
8

Jeśli jest to stary projekt, ale kompilator został opracowany od tego czasu, być może nowszy kompilator może wygenerować mniejszy kod

mikeselectricstuff
źródło
Dzięki Mike! Próbowałem tego w przeszłości, a uzyskana przestrzeń została już wykorzystana! :) Przeniesiono z kompilatora IAR C 3.21d do 3.40.
IntelliChick
1
Przeszedłem o jeszcze jedną wersję i udało mi się zdobyć trochę więcej Flasha, żeby zmieściła się w tej funkcji. Naprawdę walczę z pamięcią RAM, która pozostała bez zmian. :(
IntelliChick
7

Zawsze warto sprawdzić w instrukcji kompilatora opcje optymalizacji przestrzeni.

Dla gcc -ffunction-sectionsi -fdata-sectionsz --gc-sectionsflagą łącznikową są dobre do usuwania martwego kodu.

Oto kilka innych doskonałych wskazówek (ukierunkowanych na AVR)

Toby Jaffey
źródło
Czy to naprawdę działa? Dokumenty mówią „Po określeniu tych opcji asembler i linker utworzą większe obiekty i pliki wykonywalne, a także będą wolniejsze”. Rozumiem, że posiadanie oddzielnych sekcji ma sens w przypadku mikro z sekcjami Flash i RAM - Czy to stwierdzenie w dokumentach nie dotyczy mikrokontrolerów?
Kevin Vermeer
Z mojego doświadczenia wynika, że ​​działa dobrze dla AVR
Toby Jaffey,
1
Nie działa to dobrze w większości kompilatorów, z których korzystałem. To jest jak użycie słowa kluczowego register. Możesz powiedzieć kompilatorowi, że zmienna trafia do rejestru, ale dobry optymalizator zrobi to znacznie lepiej niż człowiek (argumentuj, jak niektórzy mogą, w praktyce uważa się to za niedopuszczalne).
Kortuk
1
Kiedy zaczynasz przypisywać lokalizacje, zmuszasz kompilator do umieszczania rzeczy w określonych lokalizacjach, co jest bardzo ważne dla zaawansowanego kodu programu ładującego, ale okropne jest radzenie sobie z nim w optymalizatorze, ponieważ podejmując decyzje, odsuwasz go od kroku optymalizacji mógłby zrobić. W niektórych kompilatorach projektują go tak, aby zawierał sekcje dla jakiego kodu jest używany, jest to przypadek przekazania kompilatorowi więcej informacji, aby zrozumiał twoje zastosowanie, to pomoże. Jeśli kompilator tego nie sugeruje, nie rób tego.
Kortuk
6

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, callocitp). 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.

semaj
źródło
To są bardzo dobre wybory. Zauważę, że nigdy nie powinieneś nigdy używać malloc w systemie wbudowanym.
Kortuk
1
@Kortuk To zależy od twojej definicji osadzenia i wykonywanego zadania
Toby Jaffey
1
@joby, Tak, rozumiem to. W systemie z 0 restartami i brakiem systemu operacyjnego takiego jak Linux, Malloc może być bardzo zły.
Kortuk
Nie ma dynamicznej alokacji pamięci, nie ma miejsca, w którym używany jest malloc, calloc. Sprawdziłem również przydział sterty i został on już ustawiony na 0, więc nie ma przydziału sterty. Aktualnie przydzielony rozmiar stosu wynosi 254 bajty, a rozmiar stosu przerwań w 128 bajtach.
IntelliChick
5

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.

Craig McQueen
źródło
Istnieją 2 małe tabele przeglądowe, z których żadna nie będzie wynosiła niestety 10 000. Nie stosuje się też matematyki zmiennoprzecinkowej. :( Dzięki za sugestie. Są dobre.
IntelliChick
2

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 .

Naruszenie umowy prof. Falkena
źródło
2

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.

Joel B.
źródło
1
Obecnie zoptymalizowany do wartości maksymalnej dla rozmiaru. :) Dziękuję za sugestię. :)
IntelliChick
2

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ć.

mikeselectricstuff
źródło
Dzięki Mike. Kod już używa związków w większości miejsc, ale przyjrzę się kilku innym, w których może to nadal pomóc. Przyjrzę się również wybranemu rozmiarowi stosu i zobaczę, czy można to w ogóle zoptymalizować.
IntelliChick
Skąd mam wiedzieć, jaki rozmiar stosu jest odpowiedni?
IntelliChick
2

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)

mikeselectricstuff
źródło
Dzięki Mike. Tak, już to sprawdziłem - nazywa się opcję „Stałe do zapisu” dla kompilatora M16C IAR C. Kopiuje stałe z pamięci ROM do pamięci RAM. Ta opcja nie jest zaznaczona dla mojego projektu. Ale naprawdę ważny czek! Dzięki.
IntelliChick,
1

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.

supercat
źródło
1

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

AkshayImmanuelD
źródło
1

Kilka (być może oczywistych) sztuczek, których z powodzeniem użyłem do kompresji kodu klienta:

  1. 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.

  2. Poszukaj nadmiarowości w kodzie i użyj pętli lub funkcji do wykonywania powtarzających się instrukcji.

  3. 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

clabacchio
źródło
0

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.

AngryEE
źródło
1
Każdy przyzwoity kompilator powinien to zrobić automatycznie dla funkcji wywoływanych tylko raz.
mikeselectricstuff
5
Odkryłem, że wstawianie jest zwykle bardziej przydatne do optymalizacji prędkości i zwykle kosztem zwiększenia rozmiaru.
Craig McQueen,
Inlining zwykle zwiększa rozmiar kodu, z wyjątkiem trywialnych funkcji, takich jakint get_a(struct x) {return x.a;}
Dmitrij Grigoriew
0

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.

for( i = 0; i < 100; i++ )

Może to wydawać się dobrym miejscem dla 8-bitowej zmiennej, ale 32-bitowa zmienna może generować mniej kodu.

Robert
źródło
To może zaoszczędzić kod, ale zje pamięć RAM.
mikeselectricstuff,
Zużyje pamięć RAM tylko wtedy, gdy to wywołanie funkcji znajduje się w najdłuższej gałęzi śledzenia wywołania. W przeciwnym razie ponownie wykorzystuje przestrzeń stosu, której potrzebuje już inna funkcja.
Robert
2
Zwykle prawda, pod warunkiem, że jest to zmienna lokalna. Jeśli brakuje pamięci RAM, wielkość globalnych zmiennych, zwłaszcza tablic, jest dobrym miejscem, aby zacząć szukać oszczędności.
mikeselectricstuff,
1
Co ciekawe, inną możliwością jest zastąpienie niepodpisanych zmiennych znakami. Jeśli kompilator zoptymalizuje niepodpisany skrót do rejestru 32-bitowego, musi dodać kod, aby upewnić się, że jego wartość zawija się od 65535 do zera. Jeśli jednak kompilator zoptymalizuje podpisany skrót do rejestru, taki kod nie jest wymagany. Ponieważ nie ma gwarancji, co się stanie, jeśli zwarcie zostanie zwiększone powyżej wartości 32767, kompilatory nie muszą emitować kodu, aby sobie z tym poradzić. W co najmniej dwóch kompilatorach ARM, na które patrzyłem, z tego powodu podpisany krótki kod może być mniejszy niż niepodpisany krótki kod.
supercat