Jakie jest najlepsze podejście do pisania funkcji dla oprogramowania wbudowanego, aby uzyskać lepszą wydajność? [Zamknięte]


13

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:

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

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


28
Jesteś bardzo naiwny (nie oznacza to zniewagi), jeśli uważasz, że jakikolwiek rozsądny kompilator na ślepo przekształci kod zapisany w binaria. Większość współczesnych kompilatorów jest całkiem dobra w identyfikowaniu, kiedy rutyna jest lepiej wbudowana, a nawet kiedy do przechowywania zmiennej należy użyć rejestru względem lokalizacji RAM. Postępuj zgodnie z dwiema zasadami optymalizacji: 1) nie optymalizuj. 2) nie optymalizuj jeszcze . Spraw, aby Twój kod był czytelny i łatwy do utrzymania, a NASTĘPNIE dopiero po sprofilowaniu działającego systemu, spróbuj zoptymalizować.
akohlsmith,

10
@akohlsmith IIRC Trzy zasady optymalizacji to: 1) Nie! 2) Nie, naprawdę nie! 3) Najpierw profil, a potem tylko wtedy optymalizacja, jeśli musisz - Michael_A._Jackson
ezoterik

3
Pamiętaj tylko, że „przedwczesna optymalizacja jest źródłem wszelkiego zła (a przynajmniej większości) w programowaniu” - Knuth
Mawg mówi o przywróceniu Moniki

1
@Mawg: Słowo operacyjne jest przedwczesne . (Jak wyjaśnia następny akapit w tym artykule. Dosłownie następne zdanie: „Jednak nie powinniśmy przepuszczać naszych możliwości w tak krytycznych 3%.”) Nie optymalizuj, dopóki nie będziesz potrzebować - nie znajdziesz powolnego bitów, dopóki nie masz czegoś do profilowania - ale też nie angażuj się w pesymizację, na przykład używając rażąco niewłaściwych narzędzi do pracy.
cHao

1
@Mawg Nie wiem, dlaczego otrzymałem odpowiedzi / opinie związane z optymalizacją, ponieważ nigdy nie wspomniałem o tym słowie i zamierzam to zrobić. Pytanie dotyczy bardziej sposobu pisania funkcji w programowaniu wbudowanym w celu uzyskania lepszej wydajności.
MaNyYaCk

Odpowiedzi:


28

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.


6
IMO drugi akapit powinien być pogrubiony i na górze. Nie ma nic złego w wyborze właściwych algorytmów i struktur danych od samego początku, ale martwienie się o narzut wywołania funkcji, chyba że odkryjesz, że to rzeczywiste wąskie gardło jest zdecydowanie przedwczesną optymalizacją i należy tego unikać.
Pozew Fund Moniki w dniu

11

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

  1. Bądź konsekwentny w swoim podejściu, aby kolejny odczyt twojego kodu mógł zrozumieć, w jaki sposób podchodzisz do procesu kodowania. Niespójność jest prawdopodobnie najgorszym możliwym przestępstwem. To nie tylko utrudnia innym, ale utrudnia ci powrót do kodu po latach.
  2. W miarę możliwości spróbuj tak ułożyć rzeczy, aby inicjowanie różnych sekcji funkcjonalnych było możliwe bez względu na kolejność. Jeżeli wymagane jest zamówienie, jeśli wynika to z bliskiego powiązania dwóch ściśle powiązanych podfunkcji, rozważ jedną inicjalizację dla obu, aby można było zmienić kolejność bez powodowania szkody. Jeśli nie jest to możliwe, udokumentuj wymaganie dotyczące kolejności inicjalizacji.
  3. Hermetyzacji wiedza dokładnie w jednym miejscu, jeśli to możliwe. Stałe nie powinny być powielane w całym miejscu w kodzie. Równania rozwiązujące niektóre zmienne powinny istnieć w jednym i tylko jednym miejscu. I tak dalej. Jeśli zauważysz, że kopiujesz i wklejasz zestaw wierszy, które wykonują pewne wymagane zachowanie w różnych lokalizacjach, zastanów się, jak uchwycić tę wiedzę w jednym miejscu i wykorzystać ją w razie potrzeby. Na przykład, jeśli masz strukturę drzewa, którą trzeba kroczyć w określony sposób, nie rób tegoreplikuj kod chodzenia po drzewach w każdym miejscu, w którym musisz zapętlić węzły drzewa. Zamiast tego, złap metodę chodzenia po drzewie w jednym miejscu i użyj jej. W ten sposób, jeśli zmieni się drzewo i zmieni się metoda chodzenia, masz tylko jedno miejsce do zmartwienia, a cała reszta kodu „po prostu działa poprawnie”.
  4. Jeśli rozłożysz wszystkie swoje procedury na ogromny, płaski arkusz papieru, ze strzałkami łączącymi je, jak nazywają je inne procedury, zobaczysz w dowolnej aplikacji, że będą „klastry” procedur, które mają wiele strzałek między sobą, ale tylko kilka strzałek poza grupą. Będą więc naturalne granice ściśle powiązanych procedur i luźno powiązane połączenia między innymi grupami ściśle powiązanych procedur. Skorzystaj z tego faktu, aby uporządkować kod w moduły. To znacznie ograniczy pozorną złożoność kodu.

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:

  • Poważne ograniczenia kosztów wymagające niezwykle prymitywnych MCU z niewielką pamięcią RAM i prawie zerową liczbą pinów we / wy. Do nich mają zastosowanie zupełnie nowe zestawy zasad. Na przykład może być konieczne wpisanie kodu asemblera, ponieważ nie ma dużo miejsca na kod. Może być konieczne użycie TYLKO zmiennych statycznych, ponieważ użycie zmiennych lokalnych jest zbyt kosztowne i czasochłonne. Być może będziesz musiał unikać nadmiernego korzystania z podprogramów, ponieważ (na przykład niektóre części Microchip PIC) są tylko 4 rejestry sprzętowe, w których można przechowywać adresy zwrotne podprogramów. Może być konieczne radykalne „spłaszczenie” kodu. Itp.
  • Poważne ograniczenia mocy wymagające starannie spreparowanego kodu do uruchamiania i zamykania większości MCU oraz nakładania poważnych ograniczeń na czas wykonywania kodu przy pełnej prędkości. Ponownie może to czasami wymagać pewnego kodowania zestawu.
  • Surowe wymagania dotyczące czasu. Na przykład zdarzają się sytuacje, w których musiałem się upewnić, że transmisja otwartego odpływu 0 musiała zająć DOKŁADNIE taką samą liczbę cykli jak transmisja 1. I że próbkowanie tej samej linii również musiało zostać wykonane z dokładną fazą względną w stosunku do tego czasu. Oznaczało to, że C NIE może być tutaj użyte. Jedynym możliwym sposobem uzyskania tej gwarancji jest staranne wykonanie kodu montażowego. (I nawet wtedy nie zawsze we wszystkich projektach ALU.)

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 :

  • częściowa specjalizacja szablonów
  • tabele
  • wirtualny obiekt podstawowy
  • ramka aktywacyjna
  • ramka aktywacyjna rozwija się
  • wykorzystanie inteligentnych wskaźników w konstruktorach i dlaczego
  • optymalizacja wartości zwracanej

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.

AB

A

   .
   .
   foo ();
   String s;
   foo ();
   .
   .

A

B

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

    class A {...};
    A rA (A p) { return p; }
    // .....
    { A x; rA(x); }

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.


4
Tego rodzaju funkcje używane tylko raz nigdy nie są kompilowane jako wywołanie funkcji, kod jest po prostu umieszczany tam bez żadnego wywołania.
Dorian

6
@Dorian - Twój komentarz może być prawdziwy w pewnych okolicznościach dla niektórych kompilatorów. Jeśli funkcja jest statyczna w pliku, kompilator ma opcję wstawienia kodu. jeśli jest ona widoczna z zewnątrz, to nawet jeśli nigdy nie jest faktycznie wywoływana, musi istnieć sposób na wywołanie tej funkcji.
uɐɪ

1
@jonk - Inną sztuczką, o której nie wspomniałeś w dobrej odpowiedzi, jest pisanie prostych funkcji makr, które wykonują inicjalizację lub konfigurację jako rozszerzony kod wbudowany. Jest to szczególnie przydatne w przypadku bardzo małych procesorów, w których głębokość wywoływania pamięci RAM / stosu / funkcji jest ograniczona.
uɐɪ

@ ɐɪouɐɪ Tak, brakowało mi dyskusji o makrach w C. Są one przestarzałe w C ++, ale dyskusja na ten temat może być przydatna. Mogę się tym zająć, jeśli wymyślę coś przydatnego do napisania na ten temat.
jonk

1
@jonk - Całkowicie nie zgadzam się z twoim pierwszym zdaniem. Przykład taki, inline static void turnOnFan(void) { PORTAbits &= ~(1<<8); }który jest nazywany w wielu miejscach, jest idealnym kandydatem.
Jason S,

8

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 inlinejest 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;)


2
Jedyną krytyką twojego skądinąd doskonałego postu jest punkt 2). Prawdą jest, że wydajność kodu inicjującego jest nieistotna - ale w środowisku osadzonym rozmiar może mieć znaczenie. (Ale to nie zastępuje punktu 1; zacznij optymalizować rozmiar, gdy potrzebujesz - i nie wcześniej)
Martin Bonner obsługuje Monikę

2
Wydajność kodu inicjującego może początkowo być nieistotna. Gdy dodasz tryb niskiego zużycia energii i chcesz szybko odzyskać, aby obsłużyć zdarzenie budzenia, staje się to istotne.
berendi - protestuje

5

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:

function_used_just_once{
   code blah blah;
}
main{
  codeblah;
  function_used_just_once();
  code blah blah blah;
{

skompiluje się do:

main{
 code blah;
 code blah blah;
 code blah blah blah;
}

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.


15
To, czy kod zostanie wstawiony, czy nie, jest kwestią specyficzną dla dostawcy kompilatora. nawet użycie słowa kluczowego inline nie gwarantuje kodu wbudowanego. Jest to wskazówka dla kompilatora. Z pewnością dobre kompilatory wstawią funkcje użyte tylko raz, jeśli się o nich dowiedzą. Jednak zwykle nie robi tego, jeśli w zasięgu znajdują się jakieś „lotne” obiekty.
Peter Smith,

9
Ta odpowiedź jest po prostu nieprawda. Jak mówi @PeterSmith i zgodnie ze specyfikacją języka C, kompilator ma opcję wstawienia kodu, ale może tego nie zrobić, aw wielu przypadkach tego nie zrobi. Na świecie istnieje tak wiele różnych kompilatorów dla tak wielu różnych procesorów docelowych, że sformułowanie tego rodzaju instrukcji zbiorczej w tej odpowiedzi i założenie, że wszystkie kompilatory wstawią kod, gdy tylko mają taką opcję, jest nie do utrzymania.
uɐɪ

2
@ ʎəʞouɐɪ Wskazujesz rzadkie przypadki, w których nie jest to możliwe i nie byłoby dobrym pomysłem, aby nie wywoływać funkcji w pierwszej kolejności. Nigdy nie widziałem kompilatora, który byłby tak głupi, aby naprawdę używać połączenia w prostym przykładzie podanym przez OP.
Dorian

6
W przypadkach, w których funkcje te są wywoływane tylko raz, optymalizacja wywołania funkcji nie stanowi problemu. Czy system naprawdę musi odzyskać każdy cykl zegara podczas instalacji? Tak jak w przypadku optymalizacji w dowolnym miejscu - pisz czytelny kod i optymalizuj tylko wtedy, gdy profilowanie wykaże, że jest on potrzebny .
Baldrickk

5
@MSalters Nie martwię się tym, co kompilator tutaj robi - więcej o tym, jak programista podchodzi do tego. Zerwanie inicjalizacji, jak widać w pytaniu, jest zerowe lub ma znikomy wpływ na wydajność.
Baldrickk

2

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:

void dummy (unsigned int);

void setCLK()
{
    // Code to set the clock
    dummy(5);
}

void setConfig()
{
    // Code to set the configuration
    dummy(6);
}

void setSomethingElse()
{
   // 1 line code to write something to a register.
    dummy(7);
}

void initModule()
{
   setCLK();
   setConfig();
   setSomethingElse();
}

Wybór celu i kompilatora do celów demonstracyjnych.

Disassembly of section .text:

00000000 <setCLK>:
   0:    e92d4010     push    {r4, lr}
   4:    e3a00005     mov    r0, #5
   8:    ebfffffe     bl    0 <dummy>
   c:    e8bd4010     pop    {r4, lr}
  10:    e12fff1e     bx    lr

00000014 <setConfig>:
  14:    e92d4010     push    {r4, lr}
  18:    e3a00006     mov    r0, #6
  1c:    ebfffffe     bl    0 <dummy>
  20:    e8bd4010     pop    {r4, lr}
  24:    e12fff1e     bx    lr

00000028 <setSomethingElse>:
  28:    e92d4010     push    {r4, lr}
  2c:    e3a00007     mov    r0, #7
  30:    ebfffffe     bl    0 <dummy>
  34:    e8bd4010     pop    {r4, lr}
  38:    e12fff1e     bx    lr

0000003c <initModule>:
  3c:    e92d4010     push    {r4, lr}
  40:    e3a00005     mov    r0, #5
  44:    ebfffffe     bl    0 <dummy>
  48:    e3a00006     mov    r0, #6
  4c:    ebfffffe     bl    0 <dummy>
  50:    e3a00007     mov    r0, #7
  54:    ebfffffe     bl    0 <dummy>
  58:    e8bd4010     pop    {r4, lr}
  5c:    e12fff1e     bx    lr

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.

void dummy (unsigned int);

static void setCLK()
{
    // Code to set the clock
    dummy(5);
}

static void setConfig()
{
    // Code to set the configuration
    dummy(6);
}

static void setSomethingElse()
{
   // 1 line code to write something to a register.
    dummy(7);
}

void initModule()
{
   setCLK();
   setConfig();
   setSomethingElse();
}

usuwa je teraz, gdy są wstawiane.

Disassembly of section .text:

00000000 <initModule>:
   0:    e92d4010     push    {r4, lr}
   4:    e3a00005     mov    r0, #5
   8:    ebfffffe     bl    0 <dummy>
   c:    e3a00006     mov    r0, #6
  10:    ebfffffe     bl    0 <dummy>
  14:    e3a00007     mov    r0, #7
  18:    ebfffffe     bl    0 <dummy>
  1c:    e8bd4010     pop    {r4, lr}
  20:    e12fff1e     bx    lr

Ale w rzeczywistości bierze się pod uwagę biblioteki układów scalonych lub biblioteki BSP,

Disassembly of section .text:

00000000 <_start>:
   0:    e3a0d902     mov    sp, #32768    ; 0x8000
   4:    eb000010     bl    4c <initModule>
   8:    eafffffe     b    8 <_start+0x8>

0000000c <dummy>:
   c:    e12fff1e     bx    lr

00000010 <setCLK>:
  10:    e92d4010     push    {r4, lr}
  14:    e3a00005     mov    r0, #5
  18:    ebfffffb     bl    c <dummy>
  1c:    e8bd4010     pop    {r4, lr}
  20:    e12fff1e     bx    lr

00000024 <setConfig>:
  24:    e92d4010     push    {r4, lr}
  28:    e3a00006     mov    r0, #6
  2c:    ebfffff6     bl    c <dummy>
  30:    e8bd4010     pop    {r4, lr}
  34:    e12fff1e     bx    lr

00000038 <setSomethingElse>:
  38:    e92d4010     push    {r4, lr}
  3c:    e3a00007     mov    r0, #7
  40:    ebfffff1     bl    c <dummy>
  44:    e8bd4010     pop    {r4, lr}
  48:    e12fff1e     bx    lr

0000004c <initModule>:
  4c:    e92d4010     push    {r4, lr}
  50:    ebffffee     bl    10 <setCLK>
  54:    ebfffff2     bl    24 <setConfig>
  58:    ebfffff6     bl    38 <setSomethingElse>
  5c:    e8bd4010     pop    {r4, lr}
  60:    e12fff1e     bx    lr

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.


4
Naprawdę? Jakiego „celu” i „kompilatora” używasz, mogę zapytać?
Dorian

Wygląda mi bardziej na 32/64-bitowy ARM8, może z raspbery PI niż zwykłego mikrokontrolera. Czy przeczytałeś pierwsze zdanie w pytaniu?
Dorian

Kompilator nie usuwa nieużywanych funkcji globalnych, ale robi to linker. Jeśli jest skonfigurowany i używany prawidłowo, nie pojawią się w pliku wykonywalnym.
berendi - protestuje

Jeśli ktoś zastanawia się, który kompilator może zoptymalizować między lukami w plikach: kompilatory IAR obsługują kompilację wielu plików (tak to nazywają), co pozwala na optymalizację wielu plików. Jeśli rzucisz na nią wszystkie pliki c / cpp za jednym razem, otrzymasz plik wykonywalny, który zawiera jedną funkcję: main. Korzyści z wydajności mogą być bardzo głębokie.
Arsenał

3
@Arsenal Oczywiście gcc obsługuje wstawianie, nawet jeśli jest poprawnie wywoływane przez jednostki kompilacyjne. Zobacz gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html i poszukaj opcji -flto.
Peter - Przywróć Monikę

1

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.


Niestety jest to prawdą w przypadku większości dzisiejszych programistów.
Dorian

0

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ów initModule()bezpośrednio w swoim , staje się mniej oczywiste, co faktycznie robi funkcja. Ale jak często jest to tylko wskazówka.


Dziękuję za odpowiedź. W razie potrzeby mogę zmienić styl, ale pytanie brzmi, czy czytelność kodu wpływa na wydajność?
MaNyYaCk

Wywołanie funkcji spowoduje wywołanie lub polecenie jmp, ale moim zdaniem jest to niewielkie poświęcenie zasobów. Jeśli używasz wzorców projektowych, czasami dochodzi do kilkunastu warstw wywołań funkcji, zanim osiągniesz rzeczywisty wycięty kod.
po.pe

@Humpawumpa - Jeśli piszesz do mikrokontrolera tylko 256 lub 64 bajtów pamięci RAM następnie kilkanaście warstw wywołań funkcji nie jest pomijalne ofiara, nie jest to tylko możliwe
uɐɪ

Tak, ale są to dwie skrajności ... zwykle masz więcej niż 256 bajtów i używasz mniej niż tuzin warstw - mam nadzieję.
po.pe

0

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 inlineaby go zdefiniować:

static inline void setCLK()
{
    //code to set the clock
}

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 inlinei 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.ci użyjesz jej file2.c, kompilator nie wstawi jej automatycznie. Jeśli jednak zdefiniujesz to file1.hjako static inlinefunkcję, istnieje szansa, że ​​Twój kompilator to uwzględni.

static inlineFunkcje te są niezwykle przydatne w programowaniu o wysokiej wydajności. Odkryłem, że często zwiększają wydajność kodu ponad trzykrotnie.


„takie jak bycie ponad 3 liniami” - liczba linii nie ma z tym nic wspólnego; koszty wewnętrzne mają z tym wszystko wspólnego. Mógłbym napisać funkcję 20-liniową, która jest idealna do wstawiania, i funkcję 3-liniową, która jest okropna dla wstawiania (np. Funkcja A (), która wywołuje funkcję B () 3 razy, funkcja B (), która wywołuje funkcję C () 3 razy, i kilka innych poziomów).
Jason S,

Ponadto, jeśli zdefiniujesz funkcję file1.ci użyjesz jej file2.c, kompilator nie wstawi jej automatycznie. Fałsz . Zobacz np. -fltoW gcc lub clang.
berendi - protestuje

0

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]:

volatile uint32_t *magic_write_ptr,magic_write_count;
void handle_interrupt(void)
{
  if (magic_write_count)
  {
    magic_write_count--;
    send_data(*magic_write_ptr++)
  }
}

powinno być możliwe napisanie funkcji, aby rozpocząć operację zapisu w tle lub poczekać na jej zakończenie:

void wait_for_background_write(void)
{
  while(magic_write_count)
    ;
}
void start_background_write(uint32_t *dat, uint32_t count)
{
  wait_for_background_write();
  background_write_ptr = dat;
  background_write_count = count;
}

a następnie wywołaj taki kod, używając:

uint32_t buff[16];

... write first set of data into buff
start_background_write(buff, 16);
... do some stuff unrelated to buff
wait_for_background_write();

... write second set of data into buff
start_background_write(buff, 16);
... etc.

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, iccsą 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 docelowa iccnie 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:

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

  2. Implementacja wysokiej jakości może traktować wszystkie dostępy do volatileobiektów tak, jakby mogły wyzwalać działania, które miałyby dostęp do dowolnego obiektu widocznego dla świata zewnętrznego.

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


„upewnienie się, że funkcje We / Wy znajdują się w osobnej jednostce kompilacji” ... nie jest pewnym sposobem zapobiegania takim problemom optymalizacji. Przynajmniej LLVM i wierzę, że GCC przeprowadzi optymalizację całego programu w wielu przypadkach, więc może zdecydować o wprowadzeniu funkcji IO, nawet jeśli są one w osobnej jednostce kompilacji.
Jules

@Jules: Nie wszystkie implementacje nadają się do pisania oprogramowania wbudowanego. Wyłączenie optymalizacji całego programu może być najtańszym sposobem zmuszenia gcc lub clang do zachowania się jako wysokiej jakości implementacja odpowiednia do tego celu.
supercat

@Jules: Implementacja o wyższej jakości przeznaczona do programowania wbudowanego lub systemowego powinna być konfigurowalna, aby mieć semantykę odpowiednią do tego celu, bez konieczności całkowitego wyłączania optymalizacji całego programu (np. Poprzez opcję traktowania volatiledostę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.
supercat

1
Nawet implementacje „najwyższej jakości” nie poprawią błędnego kodu. Jeśli buffnie zostanie zadeklarowane volatile, 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) jako volatile. Czy zawartość jest buffdostępna w module obsługi przerwań? Tak. Tak powinno być volatile.
berendi - protestuje

@berendi: Kompilatory mogą oferować gwarancje wykraczające poza wymagania normy, a kompilatory jakości to zrobią. Wolnostojąca implementacja wysokiej jakości dla systemów wbudowanych pozwoli programistom na syntezę konstrukcji muteksów, co zasadniczo robi kod. Gdy magic_write_countwynosi zero, pamięć jest własnością linii głównej. Gdy ma wartość niezerową, jest własnością programu obsługi przerwań. Dokonywanie bufflotny 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 ...
SuperCat
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.