Widziałem niektóre biblioteki mikrokontrolerów, a ich funkcje wykonują jedną rzecz na raz. Na przykład coś takiego:
void setCLK()
{
// Code to set the clock
}
void setConfig()
{
// Code to set the config
}
void setSomethingElse()
{
// 1 line code to write something to a register.
}
Następnie należy dodać inne funkcje, które wykorzystują ten 1-wierszowy kod zawierający funkcję do innych celów. Na przykład:
void initModule()
{
setCLK();
setConfig();
setSomethingElse();
}
Nie jestem pewien, ale wierzę, że w ten sposób stworzyłoby to więcej wywołań skoków i narzut związany ze stosowaniem adresów zwrotnych za każdym razem, gdy funkcja jest wywoływana lub opuszczana. A to spowolniłoby działanie programu, prawda?
Szukałem i wszędzie mówią, że podstawową zasadą programowania jest to, że funkcja powinna wykonywać tylko jedno zadanie.
Więc jeśli napiszę bezpośrednio moduł funkcji InitModule, który ustawia zegar, dodaje pożądaną konfigurację i robi coś innego bez wywoływania funkcji. Czy jest to złe podejście podczas pisania oprogramowania wbudowanego?
EDYCJA 2:
Wygląda na to, że wiele osób zrozumiało to pytanie, jak gdybym próbował zoptymalizować program. Nie, nie mam zamiaru tego robić . Pozwalam kompilatorowi to zrobić, ponieważ zawsze będzie (mam nadzieję, że nie!) Lepszy ode mnie.
Całe obwinianie mnie o wybór przykładu reprezentującego kod inicjujący . Pytanie nie ma zamiaru dotyczyć wywołań funkcji wykonanych w celu inicjalizacji. Moje pytanie brzmi: czy podzielenie określonego zadania na małe funkcje wieloliniowe ( więc nie ma pytania o pracę w linii ) działające w nieskończonej pętli ma przewagę nad pisaniem długiej funkcji bez zagnieżdżonej funkcji?
Proszę wziąć pod uwagę czytelność zdefiniowaną w odpowiedzi na @Jonk .
źródło
Odpowiedzi:
Prawdopodobnie w twoim przykładzie wydajność nie miałaby znaczenia, ponieważ kod jest uruchamiany tylko raz podczas uruchamiania.
Używam zasady: napisz kod tak czytelny, jak to możliwe i zacznij optymalizować tylko wtedy, gdy zauważysz, że twój kompilator nie robi właściwie swojej magii.
Koszt wywołania funkcji w ISR może być taki sam, jak koszt wywołania funkcji podczas uruchamiania pod względem przechowywania i czasu. Jednak wymagania dotyczące czasu podczas tego ISR mogą być znacznie bardziej krytyczne.
Ponadto, jak już zauważyli inni, koszt (i znaczenie „kosztu”) wywołania funkcji różni się w zależności od platformy, kompilatora, ustawienia optymalizacji kompilatora i wymagań aplikacji. Będzie ogromna różnica między 8051 a korą-m7, a rozrusznikiem serca i włącznikiem światła.
źródło
Nie ma żadnej przewagi, o której mogę myśleć (ale patrz uwaga dla JasonS na dole), owijanie jednego wiersza kodu jako funkcji lub podprogramu. Może z wyjątkiem tego, że można nazwać funkcję „czytelną”. Ale równie dobrze możesz skomentować linię. A ponieważ zawijanie wiersza kodu w funkcji kosztuje pamięć kodu, przestrzeń stosu i czas wykonywania, wydaje mi się, że jest to w większości przeciwne do zamierzonego. W sytuacji dydaktycznej? To może mieć sens. Ale to zależy od klasy uczniów, ich wcześniejszego przygotowania, programu nauczania i nauczyciela. W większości uważam, że to nie jest dobry pomysł. Ale to moja opinia.
Co prowadzi nas do dolnej linii. Pana obszerny obszar pytań jest od dziesięcioleci przedmiotem pewnej debaty i do dziś pozostaje przedmiotem debaty. Tak więc, przynajmniej gdy czytam twoje pytanie, wydaje mi się, że jest to pytanie oparte na opiniach (tak jak je zadałeś).
Można by odejść od tego, by opierać się na opiniach, gdybyś był bardziej szczegółowy na temat sytuacji i dokładnie opisał cele, które uważałeś za najważniejsze. Im lepiej zdefiniujesz narzędzia pomiarowe, tym bardziej obiektywne mogą być odpowiedzi.
Ogólnie rzecz biorąc, chcesz wykonać następujące czynności dla każdego kodowania. (Poniżej założyłem, że porównujemy różne podejścia, z których wszystkie osiągają cele. Oczywiście, każdy kod, który nie wykonuje wymaganych zadań, jest gorszy niż kod, który się powiedzie, niezależnie od tego, jak jest napisany.)
Powyższe jest ogólnie prawdziwe w odniesieniu do całego kodowania. Nie dyskutowałem o użyciu parametrów, lokalnych lub statycznych zmiennych globalnych itp. Powodem jest to, że w przypadku programowania wbudowanego przestrzeń aplikacji często nakłada ekstremalne i bardzo znaczące nowe ograniczenia i nie jest możliwe omówienie ich wszystkich bez omawiania każdej wbudowanej aplikacji. W każdym razie tak się nie dzieje.
Ograniczeniami tymi mogą być dowolne (i więcej) z tych:
I tak dalej. (Kod okablowania dla krytycznych dla życia instrumentów medycznych ma również swój własny świat.)
Konsekwencją tego jest to, że osadzone kodowanie często nie jest darmowym rozwiązaniem dla wszystkich, w którym można kodować tak, jak na stacji roboczej. Często występują poważne, konkurencyjne powody dla wielu różnych bardzo trudnych ograniczeń. A te mogą zdecydowanie argumentować przeciwko bardziej tradycyjnym i podstawowym odpowiedziom.
Jeśli chodzi o czytelność, uważam, że kod jest czytelny, jeśli jest napisany w spójny sposób, którego mogę się nauczyć podczas czytania. A tam, gdzie nie ma celowej próby zaciemnienia kodu. Naprawdę nie jest więcej wymagane.
Czytelny kod może być dość wydajny i może spełniać wszystkie powyższe wymagania, o których już wspomniałem. Najważniejsze jest to, że w pełni rozumiesz, co każdy wiersz, który piszesz, tworzy na poziomie zespołu lub maszyny, gdy go kodujesz. C ++ stanowi tutaj poważne obciążenie dla programisty, ponieważ istnieje wiele sytuacji, w których identyczne fragmenty kodu C ++ faktycznie generują różne fragmenty kodu maszynowego, które mają znacznie różną wydajność. Ale ogólnie C jest w większości językiem „to, co widzisz, dostajesz”. W związku z tym jest to bezpieczniejsze.
EDYCJA według JasonS:
Używam C od 1978 r. I C ++ od około 1987 r. Mam duże doświadczenie w korzystaniu zarówno z komputerów mainframe, minikomputerów, jak i (głównie) aplikacji osadzonych.
Jason komentuje użycie „inline” jako modyfikatora. (Z mojej perspektywy jest to stosunkowo „nowa” funkcja, ponieważ po prostu nie istniała przez około pół życia lub więcej przy użyciu C i C ++.) Korzystanie z funkcji wbudowanych może w rzeczywistości wykonywać takie wywołania (nawet dla jednej linii kod) całkiem praktyczny. I tam, gdzie to możliwe, jest znacznie lepsze niż używanie makra ze względu na typowanie, które może zastosować kompilator.
Ale są też ograniczenia. Po pierwsze, nie można polegać na kompilatorze, który „bierze podpowiedź”. Może, ale nie musi. I istnieją dobre powody, aby nie brać podpowiedzi. (Dla oczywistego przykładu, jeśli zostanie wzięty adres funkcji, wymaga to utworzenia instancji funkcji, a użycie adresu do wykonania połączenia będzie ... wymagało połączenia. Nie można wtedy wstawić kodu.) inne powody również. Kompilatory mogą mieć wiele różnych kryteriów, według których oceniają sposób obsługi podpowiedzi. A jako programista oznacza to, że musiszpoświęć trochę czasu na naukę tego aspektu kompilatora, w przeciwnym razie prawdopodobnie podejmiesz decyzje na podstawie wadliwych pomysłów. Jest to więc obciążenie zarówno dla autora kodu, jak i dla każdego czytnika, a także dla każdego, kto planuje przenieść kod na jakiś inny kompilator.
Ponadto kompilatory C i C ++ obsługują osobną kompilację. Oznacza to, że mogą skompilować jeden fragment kodu C lub C ++ bez kompilowania innego pokrewnego kodu dla projektu. Aby wstawić kod, przy założeniu, że kompilator mógłby to zrobić inaczej, nie tylko musi mieć deklarację „w zakresie”, ale także musi mieć definicję. Zwykle programiści będą pracować, aby upewnić się, że tak jest, jeśli używają „wbudowanego”. Ale łatwo jest wkraść się w błędy.
Zasadniczo, chociaż używam również inline tam, gdzie uważam to za właściwe, zwykle zakładam, że nie mogę na tym polegać. Jeśli wydajność jest istotnym wymogiem i myślę, że OP już wyraźnie napisał, że nastąpił znaczący spadek wydajności, gdy wybrali bardziej „funkcjonalną” trasę, to z pewnością wolałbym unikać polegania na inline jako praktyce kodowania i zamiast tego podążyłby za nieco innym, ale całkowicie spójnym wzorcem pisania kodu.
Ostatnia uwaga na temat „wstawiania” i definicji jest „w zakresie” dla oddzielnego kroku kompilacji. Możliwe jest (nie zawsze niezawodne) wykonanie pracy na etapie łączenia. Może się to zdarzyć tylko wtedy, gdy kompilator C / C ++ zakopuje w plikach obiektowych wystarczająco dużo szczegółów, aby linker mógł działać na żądanie „wbudowane”. Osobiście nie spotkałem systemu linkera (poza Microsoft), który obsługuje tę funkcję. Ale może się zdarzyć. Ponownie, to, czy należy na nim polegać, zależy od okoliczności. Ale zwykle zakładam, że nie został on przerzucony na linker, chyba że wiem inaczej na podstawie dobrych dowodów. A jeśli na tym polegam, zostanie to udokumentowane w widocznym miejscu.
C ++
Dla zainteresowanych, oto przykład, dlaczego zachowuję ostrożność wobec C ++ podczas kodowania aplikacji osadzonych, pomimo jego gotowej dostępności już dziś. Wyrzucę kilka terminów, które moim zdaniem wszyscy programiści C ++ powinni znać na zimno :
To tylko krótka lista. Jeśli jeszcze nie wiesz wszystkiego o tych terminach i dlaczego je wymieniłem (i wielu innych nie wymieniłem tutaj), odradzam używanie C ++ do pracy osadzonej, chyba że nie jest to opcja dla projektu .
Rzućmy okiem na semantykę wyjątków C ++, aby uzyskać tylko smak.
Kompilator C ++ widzi pierwsze wywołanie foo () i może po prostu pozwolić na normalne odwrócenie ramki aktywacji, jeśli foo () zgłosi wyjątek. Innymi słowy, kompilator C ++ wie, że w tym momencie nie jest potrzebny żaden dodatkowy kod do obsługi procesu odwijania ramki związanego z obsługą wyjątków.
Ale po utworzeniu String s kompilator C ++ wie, że musi zostać odpowiednio zniszczony, zanim będzie można zezwolić na odwijanie ramki, jeśli później wystąpi wyjątek. Drugie wywołanie foo () jest semantycznie różne od pierwszego. Jeśli drugie wywołanie funkcji foo () zgłasza wyjątek (który może, ale nie musi), kompilator musi umieścić kod zaprojektowany do obsługi zniszczenia String s przed zezwoleniem na zwykłe odwijanie ramki. Różni się to od kodu wymaganego do pierwszego wywołania foo ().
(Możliwe jest dodanie dodatkowych dekoracji w C ++, aby ograniczyć ten problem. Ale faktem jest, że programiści używający C ++ po prostu muszą być znacznie bardziej świadomi implikacji każdego wiersza kodu, który piszą.)
W przeciwieństwie do malloc C, nowe C ++ używa wyjątków do sygnalizowania, kiedy nie może wykonać surowej alokacji pamięci. Podobnie będzie z „dynamic_cast”. (Zobacz trzecie wydanie Stroustrup, The C ++ Programming Language, strony 384 i 385 dla standardowych wyjątków w C ++.) Kompilatory mogą pozwolić na wyłączenie tego zachowania. Ale generalnie poniesiesz trochę narzutu z powodu prawidłowo utworzonych prologów i epilogów obsługi wyjątków w wygenerowanym kodzie, nawet jeśli wyjątki faktycznie nie mają miejsca, a nawet gdy kompilowana funkcja nie ma żadnych bloków obsługi wyjątków. (Stroustrup publicznie narzekał).
Bez częściowej specjalizacji szablonów (nie wszystkie kompilatory C ++ ją obsługują), użycie szablonów może oznaczać katastrofę dla programowania wbudowanego. Bez tego rozkwit kodu jest poważnym ryzykiem, które może zabić flashowany projekt osadzony w małej pamięci.
Gdy funkcja C ++ zwraca obiekt, tymczasowy kompilator bez nazwy jest tworzony i niszczony. Niektóre kompilatory C ++ mogą zapewnić efektywny kod, jeśli w instrukcji return użyty jest konstruktor obiektów zamiast obiektu lokalnego, co zmniejsza potrzebę budowy i zniszczenia o jeden obiekt. Ale nie każdy kompilator to robi, a wielu programistów C ++ nawet nie zdaje sobie sprawy z tej „optymalizacji wartości zwracanej”.
Zapewnienie konstruktorowi obiektu jednego typu parametru może pozwolić kompilatorowi C ++ znaleźć ścieżkę konwersji między dwoma typami w całkowicie nieoczekiwany sposób dla programisty. Tego rodzaju „inteligentne” zachowanie nie jest częścią C.
Klauzula catch określająca typ podstawowy „wycina” rzucony obiekt pochodny, ponieważ rzucony obiekt jest kopiowany przy użyciu „typu statycznego” klauzuli catch, a nie „typu dynamicznego obiektu”. Nierzadkie źródło nieszczęścia wyjątków (gdy czujesz, że możesz sobie pozwolić na wyjątki we wbudowanym kodzie).
Kompilatory C ++ mogą automatycznie generować dla Ciebie konstruktory, destruktory, konstruktory kopiujące i operatory przypisania, z niezamierzonymi rezultatami. Zdobycie łatwości ze szczegółami tego zajmuje.
Przekazywanie tablic obiektów pochodnych do funkcji akceptującej tablice obiektów podstawowych rzadko generuje ostrzeżenia kompilatora, ale prawie zawsze powoduje nieprawidłowe zachowanie.
Ponieważ C ++ nie wywołuje destruktora częściowo skonstruowanych obiektów, gdy wystąpi wyjątek w konstruktorze obiektów, obsługa wyjątków w konstruktorach zwykle nakazuje „inteligentne wskaźniki” w celu zagwarantowania, że skonstruowane fragmenty w konstruktorze zostaną odpowiednio zniszczone, jeśli wystąpi tam wyjątek . (Patrz Stroustrup, strony 367 i 368.) Jest to częsty problem podczas pisania dobrych klas w C ++, ale oczywiście unika się go w C, ponieważ C nie ma wbudowanej semantyki konstrukcji i zniszczenia. Pisanie odpowiedniego kodu do obsługi konstrukcji podobiektów w obiekcie oznacza pisanie kodu, który musi poradzić sobie z tym unikalnym problemem semantycznym w C ++; innymi słowy „pisanie wokół” zachowań semantycznych C ++.
C ++ może kopiować obiekty przekazywane do parametrów obiektu. Na przykład w następujących fragmentach wywołanie „rA (x);” może spowodować, że kompilator C ++ wywoła konstruktor dla parametru p, aby następnie wywołać konstruktor kopiujący w celu przesłania obiektu x do parametru p, a następnie innego konstruktora dla obiektu zwracanego (nienazwanego tymczasowego) funkcji rA, co oczywiście jest skopiowane z parametru p. Co gorsza, jeśli klasa A ma własne obiekty, które wymagają konstrukcji, może to spowodować katastrofalny teleskop. (Programator AC uniknąłby większości tego śmiecia, optymalizując ręcznie, ponieważ programiści C nie mają tak przydatnej składni i muszą wyrażać wszystkie szczegóły pojedynczo).
Na koniec krótka uwaga dla programistów C. longjmp () nie ma przenośnego zachowania w C ++. (Niektórzy programiści C używają tego jako swoistego mechanizmu „wyjątku”). Niektóre kompilatory C ++ będą faktycznie próbowały ustawić rzeczy do wyczyszczenia, gdy pobierany jest longjmp, ale takie zachowanie nie jest przenośne w C ++. Jeśli kompilator czyści skonstruowane obiekty, nie jest przenośny. Jeśli kompilator nie wyczyści ich, obiekty nie zostaną zniszczone, jeśli kod opuści zakres skonstruowanych obiektów w wyniku longjmp, a zachowanie jest nieprawidłowe. (Jeśli użycie longjmp w foo () nie pozostawia zasięgu, zachowanie może być w porządku). Nie jest to zbyt często używane przez programistów osadzonych w języku C, ale powinni oni zapoznać się z tymi problemami przed ich użyciem.
źródło
inline static void turnOnFan(void) { PORTAbits &= ~(1<<8); }
który jest nazywany w wielu miejscach, jest idealnym kandydatem.1) Najpierw kod dla czytelności i konserwacji. Najważniejszym aspektem każdej bazy kodu jest to, że ma dobrą strukturę. Ładnie napisane oprogramowanie ma zwykle mniej błędów. Konieczne może być wprowadzenie zmian w ciągu kilku tygodni / miesięcy / lat, a to bardzo pomaga, jeśli kod jest łatwy do odczytania. A może ktoś inny musi dokonać zmiany.
2) Wydajność uruchomionego kodu nie ma większego znaczenia. Dbaj o styl, a nie o wydajność
3) Nawet kod w ciasnych pętlach musi być przede wszystkim poprawny. Jeśli napotkasz problemy z wydajnością, zoptymalizuj, gdy kod będzie poprawny.
4) Jeśli chcesz zoptymalizować, musisz zmierzyć! Nie ma znaczenia, czy myślisz, czy ktoś mówi ci, że
static inline
jest to tylko zalecenie dla kompilatora. Musisz spojrzeć na to, co robi kompilator. Musisz także zmierzyć, czy wstawianie poprawiło wydajność. W systemach wbudowanych należy również zmierzyć rozmiar kodu, ponieważ pamięć kodu jest zwykle dość ograniczona. To jest najważniejsza zasada, która odróżnia inżynierię od zgadywania. Jeśli tego nie zmierzyłeś, to nie pomogło. Inżynieria mierzy. Nauka to zapisuje;)źródło
Gdy funkcja jest wywoływana tylko w jednym miejscu (nawet w innej funkcji), kompilator zawsze umieszcza kod w tym miejscu zamiast tak naprawdę wywoływać funkcję. Jeśli funkcja jest wywoływana w wielu miejscach, sensowne jest użycie funkcji przynajmniej z punktu widzenia rozmiaru kodu.
Po skompilowaniu kod nie będzie zawierał wielu wywołań, zamiast tego znacznie poprawi się czytelność.
Będziesz także chciał mieć np. Kod inicjujący ADC w tej samej bibliotece z innymi funkcjami ADC, których nie ma w głównym pliku c.
Wiele kompilatorów pozwala określić różne poziomy optymalizacji szybkości lub rozmiaru kodu, więc jeśli masz małą funkcję wywoływaną w wielu miejscach, funkcja ta będzie „wstawiana”, kopiowana tam zamiast wywoływania.
Optymalizacja prędkości wstawi funkcje w jak największej liczbie miejsc, optymalizacja rozmiaru kodu wywoła funkcję, jednak gdy funkcja jest wywoływana tylko w jednym miejscu, tak jak w twoim przypadku, zawsze będzie „wstawiana”.
Kod taki jak ten:
skompiluje się do:
bez połączenia.
A odpowiedź na twoje pytanie, w twoim przykładzie lub podobnym, czytelność kodu nie wpływa na wydajność, nic nie ma dużej szybkości ani rozmiaru kodu. Często używa się wielu wywołań tylko po to, aby kod był czytelny, na końcu są one przestrzegane jako kod wbudowany.
Zaktualizuj, aby określić, że powyższe instrukcje nie dotyczą celowo kompilowanych kompilatorów wersji darmowej, takich jak darmowa wersja Microchip XCxx. Ten rodzaj wywołań funkcji jest kopalnią złota dla Microchip, aby pokazać, o ile lepiej jest wersja płatna, a jeśli ją skompilujesz, znajdziesz w ASM dokładnie tyle samo wywołań, co w kodzie C.
Również nie jest to głupie programiści, którzy oczekują użycia wskaźnika do funkcji wstawionej.
To sekcja elektroniki, a nie ogólne C C ++ lub sekcja programowania, pytanie dotyczy programowania mikrokontrolera, w którym każdy porządny kompilator domyślnie dokona powyższej optymalizacji.
Proszę więc przestać głosować tylko dlatego, że w rzadkich, nietypowych przypadkach może to nie być prawda.
źródło
Po pierwsze, nie ma najlepszego ani najgorszego; to wszystko kwestia opinii. Masz rację, że jest to nieefektywne. Można go zoptymalizować lub nie; to zależy. Zazwyczaj tego typu funkcje, zegar, GPIO, minutnik itp. Są widoczne w osobnych plikach / katalogach. Kompilatory na ogół nie były w stanie zoptymalizować tych luk. Jest taki, który znam, ale nie jest powszechnie używany do takich rzeczy.
Pojedynczy plik:
Wybór celu i kompilatora do celów demonstracyjnych.
Oto, co większość odpowiedzi tutaj mówi, że jesteś naiwny i że wszystko to zostaje zoptymalizowane, a funkcje usunięte. Cóż, nie są usuwane, ponieważ są domyślnie zdefiniowane globalnie. Możemy je usunąć, jeśli nie są potrzebne poza tym jednym plikiem.
usuwa je teraz, gdy są wstawiane.
Ale w rzeczywistości bierze się pod uwagę biblioteki układów scalonych lub biblioteki BSP,
Zdecydowanie zaczniesz dodawać koszty ogólne, które mają zauważalny koszt wydajności i przestrzeni. Kilka do pięciu procent każdego w zależności od tego, jak mała jest każda funkcja.
Dlaczego tak się dzieje? Część z nich to zbiór zasad, które profesorowie chcieliby lub nadal uczą, aby ułatwić ocenianie kodu. Funkcje muszą zmieścić się na stronie (wstecz, kiedy drukujesz swój trening na papierze), nie rób tego, nie rób tego itd. Wiele z nich polega na tworzeniu bibliotek o wspólnych nazwach dla różnych celów. Jeśli masz dziesiątki rodzin mikrokontrolerów, niektóre z nich współużytkują urządzenia peryferyjne, a niektóre nie, może trzy lub cztery różne smaki UART zmieszane w różnych rodzinach, różne GPIO, kontrolery SPI itp. Możesz mieć ogólną funkcję gpio_init (), get_timer_count () itp. I ponownie użyj tych abstrakcji dla różnych urządzeń peryferyjnych.
Staje się to głównie kwestią łatwości konserwacji i projektowania oprogramowania, z pewną możliwą czytelnością. Utrzymanie, czytelność i wydajność nie możesz mieć wszystkiego; możesz wybrać tylko jeden lub dwa na raz, nie wszystkie trzy.
Jest to w dużej mierze pytanie oparte na opiniach, a powyższe pokazuje trzy główne sposoby, w jakie można to zrobić. Co do ścieżki, która jest NAJLEPSZA, jest to wyłącznie opinia. Czy wykonywanie całej pracy w jednej funkcji? Pytanie oparte na opiniach, niektórzy ludzie skłaniają się ku wydajności, niektórzy określają modułowość i swoją wersję czytelności jako BEST. Interesujący problem z tym, co wielu ludzi nazywa czytelnością, jest niezwykle bolesny; aby „zobaczyć” kod, trzeba mieć jednocześnie otwartych 50-10 000 plików i jakoś spróbować liniowo zobaczyć funkcje w kolejności wykonywania, aby zobaczyć, co się dzieje. Uważam, że jest to przeciwieństwo czytelności, ale inni uważają, że jest czytelny, ponieważ każdy element mieści się w oknie ekranu / edytora i może być zużyty w całości po zapamiętaniu wywoływanych funkcji i / lub edytora, który może wchodzić i wychodzić każda funkcja w ramach projektu.
To kolejny duży czynnik, gdy widzisz różne rozwiązania. Edytory tekstu, IDE itp. Są bardzo osobiste i wykraczają poza vi w porównaniu do Emacsa. Wydajność programowania, liczba linii na dzień / miesiąc rośnie, jeśli czujesz się komfortowo i wydajnie dzięki używanemu narzędziu. Funkcje tego narzędzia mogą / będą celowo lub nie pochylać się nad tym, jak fani tego narzędzia piszą kod. W rezultacie, jeśli jedna osoba pisze te biblioteki, projekt w pewnym stopniu odzwierciedla te nawyki. Nawet jeśli jest to zespół, nawyki / preferencje głównego dewelopera lub szefa mogą zostać narzucone reszcie zespołu.
Standardy kodowania, które zawierają wiele osobistych preferencji, znowu bardzo religijne vi przeciwko Emacsowi, tabulatory vs. spacje, układanie nawiasów itp. I one odgrywają rolę w projektowaniu bibliotek do pewnego stopnia.
Jak powinieneś napisać swój? Jakkolwiek chcesz, naprawdę nie ma złej odpowiedzi, jeśli działa. Kod jest zły lub ryzykowny, ale napisany tak, aby można go było utrzymać w razie potrzeby, spełnia cele projektowe, rezygnuje z czytelności i pewnej konserwacji, jeśli wydajność jest ważna, lub odwrotnie. Czy lubisz krótkie nazwy zmiennych, aby pojedynczy wiersz kodu pasował do szerokości okna edytora? Lub długie nazbyt opisowe nazwy, aby uniknąć pomyłek, ale czytelność spada, ponieważ nie można uzyskać jednego wiersza na stronie; teraz jest wizualnie rozbity, mieszając się z prądem.
Nie uderzysz po raz pierwszy w domu na nietoperzu. Prawdziwe zdefiniowanie Twojego stylu może / powinno zająć dekady. Jednocześnie w tym czasie twój styl może się zmienić, pochylając się w jedną stronę, a następnie pochylając się w drugą stronę.
Usłyszysz dużo nie optymalizacji, nigdy optymalizacji i przedwczesnej optymalizacji. Ale jak pokazano, takie projekty od samego początku powodują problemy z wydajnością, a następnie widzisz hacki, aby rozwiązać ten problem, a nie przeprojektowanie od początku, aby wykonać. Zgadzam się, że zdarzają się sytuacje, jedna funkcja, kilka wierszy kodu, których możesz próbować zmanipulować kompilatorem w oparciu o obawę przed tym, co kompilator zrobi inaczej (zauważ z doświadczeniem, że tego rodzaju kodowanie staje się łatwe i naturalne, optymalizując, gdy piszesz, wiedząc, w jaki sposób kompilator zamierza skompilować kod), a następnie chcesz potwierdzić, gdzie tak naprawdę jest program przechwytujący cykl, zanim go zaatakujesz.
Musisz również w pewnym stopniu zaprojektować swój kod dla użytkownika. Jeśli to jest twój projekt, jesteś jedynym deweloperem; to co chcesz. Jeśli próbujesz stworzyć bibliotekę do rozdawania lub sprzedawania, prawdopodobnie chcesz, aby twój kod wyglądał jak wszystkie inne biblioteki, setki do tysięcy plików z drobnymi funkcjami, długimi nazwami funkcji i długimi nazwami zmiennych. Pomimo problemów z czytelnością i problemów z wydajnością, w IMO znajdziesz więcej osób, które będą mogły używać tego kodu.
źródło
Bardzo ogólna zasada - kompilator może zoptymalizować lepiej niż ty. Oczywiście są wyjątki, jeśli robisz rzeczy wymagające dużej pętli, ale ogólnie, jeśli chcesz dobrej optymalizacji szybkości lub rozmiaru kodu, wybierz mądrze kompilator.
źródło
Z pewnością zależy to od twojego własnego stylu kodowania. Istnieje jedna ogólna zasada, że nazwy zmiennych oraz nazwy funkcji powinny być tak jasne i możliwie jak najbardziej zrozumiałe. Im więcej wywołań podrzędnych lub linii kodu wstawiasz do funkcji, tym trudniej jest zdefiniować jasne zadanie dla tej jednej funkcji. W twoim przykładzie masz funkcję,
initModule()
która inicjuje rzeczy i wywołuje podprogramy, które następnie ustawiają zegar lub konfigurują . Możesz to powiedzieć, po prostu czytając nazwę funkcji. Jeśli umieścisz cały kod z podprogramówinitModule()
bezpośrednio w swoim , staje się mniej oczywiste, co faktycznie robi funkcja. Ale jak często jest to tylko wskazówka.źródło
Jeśli funkcja naprawdę robi tylko jedną bardzo małą rzecz, rozważ ją
static inline
.Dodaj go do pliku nagłówka zamiast do pliku C i użyj słów,
static inline
aby go zdefiniować:Teraz, jeśli funkcja jest nawet nieco dłuższa, na przykład ponad 3 linie, dobrym pomysłem może być uniknięcie jej
static inline
i dodanie do pliku .c. W końcu systemy osadzone mają ograniczoną pamięć i nie chcesz zbytnio zwiększać rozmiaru kodu.Ponadto, jeśli zdefiniujesz funkcję
file1.c
i użyjesz jejfile2.c
, kompilator nie wstawi jej automatycznie. Jeśli jednak zdefiniujesz tofile1.h
jakostatic inline
funkcję, istnieje szansa, że Twój kompilator to uwzględni.static inline
Funkcje te są niezwykle przydatne w programowaniu o wysokiej wydajności. Odkryłem, że często zwiększają wydajność kodu ponad trzykrotnie.źródło
file1.c
i użyjesz jejfile2.c
, kompilator nie wstawi jej automatycznie. Fałsz . Zobacz np.-flto
W gcc lub clang.Jedną z trudności w próbie napisania wydajnego i niezawodnego kodu dla mikrokontrolerów jest to, że niektóre kompilatory nie są w stanie niezawodnie obsługiwać niektórych semantyków, chyba że kod używa dyrektyw specyficznych dla kompilatora lub wyłącza wiele optymalizacji.
Na przykład, jeśli ma system jednordzeniowy z rutynową usługą przerwań [uruchamianą przez tykanie zegara lub cokolwiek innego]:
powinno być możliwe napisanie funkcji, aby rozpocząć operację zapisu w tle lub poczekać na jej zakończenie:
a następnie wywołaj taki kod, używając:
Niestety, z włączonymi pełnymi optymalizacjami, „sprytny” kompilator, taki jak gcc lub clang, zdecyduje, że nie ma mowy, aby pierwszy zestaw zapisów miał jakikolwiek wpływ na obserwowalność programu, a zatem można go zoptymalizować. Kompilatory jakości, takie jak,
icc
są mniej podatne na to, jeśli czynność ustawiania przerwania i oczekiwania na zakończenie obejmuje zarówno zmienne zapisy, jak i zmienne odczyty (jak w tym przypadku), ale platforma docelowaicc
nie jest tak popularna dla systemów wbudowanych.Standard celowo ignoruje problemy związane z jakością implementacji, zakładając, że istnieje kilka rozsądnych sposobów obsługi powyższej konstrukcji:
Wdrożenia jakościowe przeznaczone wyłącznie dla pól takich jak high-end crunch number mogą rozsądnie oczekiwać, że kod napisany dla takich pól nie będzie zawierał konstrukcji takich jak powyżej.
Implementacja wysokiej jakości może traktować wszystkie dostępy do
volatile
obiektów tak, jakby mogły wyzwalać działania, które miałyby dostęp do dowolnego obiektu widocznego dla świata zewnętrznego.Prosta, ale przyzwoitej jakości implementacja przeznaczona dla systemów wbudowanych może traktować wszystkie wywołania funkcji nieoznaczonych jako „wbudowane” tak, jakby mogły uzyskiwać dostęp do dowolnego obiektu, który został wystawiony na działanie świata zewnętrznego, nawet jeśli nie traktuje to
volatile
tak, jak opisano w # 2)Standard nie usiłuje sugerować, które z powyższych podejść byłoby najbardziej odpowiednie dla wdrożenia wysokiej jakości, ani nie wymagać, aby wdrożenia „zgodne” były wystarczająco dobrej jakości, aby nadawały się do określonego celu. W związku z tym niektóre kompilatory, takie jak gcc lub clang, skutecznie wymagają, aby każdy kod, który chce użyć tego wzorca, musiał zostać skompilowany z wyłączoną optymalizacją.
W niektórych przypadkach upewnienie się, że funkcje We / Wy znajdują się w osobnej jednostce kompilacyjnej, a kompilator nie będzie miał innego wyboru, jak tylko założyć, że mogą uzyskać dostęp do dowolnego dowolnego podzbioru obiektów, które zostały wystawione na świat zewnętrzny, może być uzasadnionym najmniej- sposób pisania kodu, który będzie działał niezawodnie z gcc i clang. Jednak w takich przypadkach celem nie jest uniknięcie dodatkowych kosztów niepotrzebnego wywołania funkcji, ale raczej zaakceptowanie koniecznych kosztów w zamian za uzyskanie wymaganej semantyki.
źródło
volatile
dostępu tak, jakby potencjalnie wyzwalało dowolny dostęp do innych obiektów), ale z jakiegokolwiek powodu gcc i clang wolą traktować problemy związane z jakością implementacji jako zaproszenie do zachowywania się w sposób bezużyteczny.buff
nie zostanie zadeklarowanevolatile
, nie będzie traktowane jako zmienna lotna, dostęp do niej można zmienić lub zoptymalizować całkowicie, jeśli najwyraźniej nie zostanie później użyty. Zasada jest prosta: zaznacz wszystkie zmienne, które mogą być dostępne poza normalnym przebiegiem programu (jak widzi to kompilator) jakovolatile
. Czy zawartość jestbuff
dostępna w module obsługi przerwań? Tak. Tak powinno byćvolatile
.magic_write_count
wynosi zero, pamięć jest własnością linii głównej. Gdy ma wartość niezerową, jest własnością programu obsługi przerwań. Dokonywaniebuff
lotny wymagałoby że każda funkcja, która działa w dowolnym miejscu na nim używaćvolatile
-qualified wskazówek, które mogłyby osłabić optymalizacji znacznie więcej niż o kompilator ...