Spørsmål:
Hvorfor anses det som dårlig praksis å bruke det 'nye' nøkkelordet i Arduino?
Tono Nam
2020-07-24 00:17:34 UTC
view on stackexchange narkive permalink

Jeg spurte tidligere dette spørsmålet:

Er det nødvendig å slette variabler før du går i dvale?

På det spørsmål, @Delta_G postet denne kommentaren:

... Virkelig på en mikrokontroller ville jeg lage objektet i et mindre omfang og prøve å gjøre alt i min makt for å unngå å måtte bruke ny eller annen form for dynamisk tildeling. .... osv.

Den kommentaren fikk tre likes, og når jeg googler om dynamisk tildeling ved hjelp av Arduino, alle prøver også å holde seg borte fra det. Oppsummert fra all forskningen jeg gjorde, er konklusjonen min nå Ikke tildel minne hvis du ikke virkelig trenger det .

Jeg bruker Visual Studio IDE for å lage mine C ++ biblioteker som jeg har tenkt å bruke med Arduino. På Arduino IDE refererer jeg bare til disse bibliotekene, og koden kompilerer bra. Visual Studio er veldig kraftig, og det gjør det mulig for meg å lage veldig fin kode, fordi jeg kan teste den på datamaskinen min før jeg kjører den på Arduino. For eksempel opprettet jeg dette biblioteket:

  // MyQueue.htypedef struct QueueItem {void * item; QueueItem * neste; QueueItem () {item = nullptr; neste = nullptr; }} QueueItem; class Queue {public: usignert rødtall; / * Antall elementer i kø * / QueueItem * først; / * Peker på første element i køen * / Kø () / * Konstruktør * / {count = 0; først = nullptr; } void enqueue (void * item) / * Enqueue an object into the kø * / {count ++; if (first == nullptr) {first = new QueueItem (); første->item = vare; // Loggmelding fordi vi bruker det "nye" ordet. Vi må sørge for at vi disponerer QueueItem senere #ifdef windows std :: cout << "Opprette" << først << endl; #endif // windows}
annet {// Finn siste element QueueItem * gjeldende = først; mens (nåværende->next! = NULL) {nåværende = nåværende->next; } QueueItem * newItem = new QueueItem (); newItem->item = vare; // Loggmelding fordi vi bruker det "nye" nøkkelordet. Vi må sørge for at vi disponerer QueueItem senere #ifdef windows std :: cout << "Opprette" << newItem << endl; #endif // windows current->next = newItem; }} ugyldig * dequeue () {if (count == 0) return nullptr; QueueItem * newFirst = første->neste; ugyldig * pointerToItem = første->item; // Loggmelding vi sletter et objekt fordi vi opprettet det med det 'nye' nøkkelordet #ifdef windows std :: cout << "Slette" << først << endl; #endif // windows sletter først; først = newFirst; telle--; returpekerToItem; } ugyldig clear () / * Tom kø * / {mens (tell > 0) {dequeue (); }} ~ Kø () / * Destructor. Kast alt * / {clear (); }};  

Nå på min Arduino-skisse kan jeg ha følgende kode hvis jeg refererer til toppteksten.

  typedef struct Foo {int id; } Foo; ugyldig someMethod () {Kø q; // Lag ting Foo a; a.id = 1; Foo b; b.id = 2; // Enqueue a, b og c q.enqueue (&a); q.enqueue (&b); // Deque Foo * pointerTo_a = (Foo *) q.dequeue (); int x = pointerTo_a->id; // = 1 Foo * pointerTo_b = (Foo *) q.dequeue (); int y = pointerTo_b->id; // = 2 // Feil Foo * test = (Foo *) q.dequeue ();
// test == nullpeker}  

De fleste sier ikke bruker ugyldige pekere. Hvorfor!? Fordi jeg bruker ugyldige pekere, kan jeg nå bruke denne køklassen med det objektet jeg vil!

Så jeg antar at spørsmålet mitt er: Hvorfor prøver alle å holde seg borte og unngå kode som denne?

Jeg bruker NRF24L01 radiomodulen til å sende meldinger til flere Arduinos. Det er praktisk å ha en kø med meldinger som skal sendes. Jeg ville være i stand til å kode det samme programmet uten å tildele minne og unngå nøkkelordet new . Men den koden vil se stygg ut etter min mening.

I denne karantene bestemte jeg meg for å lære C ++, og det har endret måten jeg koder Arduino på. I det øyeblikket jeg lærte C ++ sluttet jeg å bruke Arduino IDE. Jeg har vært støttet utvikler i 12 år, og det er grunnen til at jeg lærte C ++ på et par måneder. Arduino er bare en hobby for meg. Jeg er fortsatt veldig ny for mikrokontrollere, og jeg vil like å forstå hvorfor folk holder seg borte fra C ++ full kraft når det gjelder mikrokontrollere . Jeg vet at jeg bare har 2 kilobyte RAM. Jeg vil ikke tildele så mye minne. Jeg vil fortsatt dra nytte av programmeringsspråket C ++ ved å bruke ny , slett , poineters og destruktorer`. Jeg vil fortsette å bruke Visual Studio til å skrive kraftige C ++ - biblioteker.

I C ++ skriver jeg grensesnitt som dette

  // Merk at jeg bruker uint32_t i stedet for 'usignert lang' fordi en usignert lang er annen størrelse på Windows enn på Arduino. Også bruker jeg en usignert kort i stedet for en int fordi en usignert kort er av samme størrelse på Windows og Arduino.class IArduinoMethods {public: // Usignert lang i Arduino virtuell ugyldig forsinkelse (uint32_t delayInMilliseconds) = 0; virtuell ugyldig utskrift (const char * text) = 0; virtuell uint32_t millis () = 0; // Få forløpt tid i millisekunder};  

Og så implementerer jeg klassene slik. Dette er for eksempel klassen jeg vil bruke når jeg tester koden min på en Windows-datamaskin:

  // Klasse som skal kjøres på Windows.class ArduinoMockWindows: public IArduinoMethods {public: // Arvet via IArduinoMethods virtual void delay (uint32_t delayInMilliseconds) overstyring {// Denne koden vil være annerledes på Arduino, og det er derfor jeg trenger denne avhengigheten Sleep (delayInMilliseconds); // Windows} virtuell uint32_t millis () {// clock_begin = std :: chrono :: steady_clock :: now (); std :: chrono :: steady_clock :: time_point now = std :: chrono :: steady_clock :: now (); automatisk varighet = now.time_since_epoch (); // etc .. return someDuration; }};  

Fordi en Windows-datamaskin ikke kan sende NRF24-radiomeldinger, kan jeg implementere et grensesnitt (avhengighet) som vil skrive til en fil, for eksempel i stedet for å sende en ekte radiopakke bare for testing.

Advarselen er at bibliotekene mine vil kreve disse avhengighetene. For at biblioteket mitt skal fungere, må jeg sende det et objekt av typen IArduinoMethods og INrfRadio . Hvis jeg kjører koden min på Windows, vil jeg gi den en klasse som vil implementere de metodene som kan kjøres på Windows. Poenget er uansett ikke å vise hvordan C ++ fungerer. Jeg viser bare hvordan jeg bruker pekere og tildeler minne til mange ting.

Fordi jeg tildelte minne, var jeg i stand til å teste biblioteket mitt på Windows og på Arduino for eksempel. Jeg kan også lage enhetstester. Jeg ser så mange fordeler ved å tildele minne. Hvis jeg er organisert og husker å frigjøre gjenstandene jeg ikke lenger bruker, kan jeg få alle disse fordelene. Hvorfor koder folk ikke slik når det gjelder Arduino?


Rediger 1


Nå som jeg forstår hvordan dyng fragmentering fungerer, jeg vet at jeg må være forsiktig når jeg bruker nøkkelordet nytt .

Jeg hater når folk gjør det de blir bedt om å gjøre uten å forstå hvordan ting fungerer. For eksempel svaret https://arduino.stackexchange.com/a/77078/51226 fra Hvorfor er købiblioteket i dette spørsmålet for det første? . Det kommer til å være tider når en ringebuffer fungerer bedre, og andre ganger når nye søkeordet fungerer bedre. Sannsynligvis fungerer ringbufferen best i de fleste tilfeller.

Ta følgende scenario hvor du bare har 1 KB minne igjen.

  1. Det er et hierarki av noder der en node har et barn og et søsken. For eksempel kan node A ha barn B og søsken C. Da kan barn B få et annet barn osv.

(Jeg lagrer dette i minnet)

  1. Jeg har en kø med arbeid som må gjøres.

(jeg må lagre dette arbeidet et sted)

  1. Jeg vil ha en kø med hendelser

(jeg må lagre dette et sted)

Hvis jeg bruker det de fleste sier Jeg burde gjøre det, så må jeg:

  1. Reserver 500 kB for å kunne lagre noder (jeg vil være begrenset til n antall noder)

  2. Reserver 250 kB for arbeidskøen som må gjøres.

  3. Reserve 250 kB for køen av hendelser.

Dette er hva folk flest vil gjøre og det vil fungere bra uten problemer med haugfragmentering.

Nå Dette er hva jeg vil gjøre

  1. Forsikre deg om at alt jeg tildeler er av størrelse 12 byte. En node har bare id (usignert int), underordnet (peker), type (usignert tegn) osv. Med totalt 12 byte.

  2. Sørg for at alle arbeid som skal innhentes er også av størrelse 12 byte.

  3. Forsikre deg om at alle hendelsene som blir innhentet også er av størrelse 12 byte.

Nå hvis jeg har mer arbeid enn arrangementer, vil dette fungere. Jeg må bare programmere i koden min at jeg aldri tildeler mer enn 70 artikler. Jeg vil ha en global variabel som har det antallet tildelinger. Koden min blir mer fleksibel. Jeg trenger ikke å sitte fast med strengt 20 hendelser, 20 arbeid og 30 noder. Hvis jeg har færre noder, vil jeg kunne ha flere hendelser. ** Uansett poenget mitt er at den ene løsningen ikke er bedre enn den andre. Det kommer til å være scenarier når en løsning er bedre.

Avslutningsvis er det bare å forstå hvordan haugfragmentering fungerer, og du vil få mye kraft ved å bruke det nye nøkkelordet. Ikke vær en sau og gjør det folk ber deg om å gjøre uten å forstå hvordan ting fungerer. **.


Rediger 2


Takk til @EdgarBonet , Jeg endte opp med å lagre noder på bunken. Dette er hvorfor:

Jeg har et hierarki av noder som kan representeres som:

  typedef struct Node {usignert kort id; Node * søsken; Node * underordnet;} Node;  

Som du ser er hver node bare 6 byte. Det er en annen grunn til at jeg ikke brydde meg så mye om å tildele noder i begynnelsen. Hvis jeg tildeler denne noden på bunken, mister jeg 2 byte til (33%) for hver tildeling, fordi størrelsen på noden må lagres på hver tildeling. Som et resultat opprettet jeg disse to metodene og en buffer:

  // For at dette skal fungere, kan en node aldri ha en id på 0 !!! Node nodeBuffer [50]; / * Buffer for å lagre noder på stabel * / Node * allocateNode (Node nodeToAllocate) / * Metode for å lagre en node * / {// Finn første tilgjengelige sted der en node kan lagres for (char i = 0; i < 50; i ++) {if (nodeBuffer [i] .id == 0) {nodeBuffer [i] = nodeToAllocate; returner & nodeBuffer [i]; }} return nullptr;} void freeNode (Node * nodeToFree) / * Metode for å slette en node * / {
nodeToFree->id = 0; // Hvis ID-en til en node er 0, er dette min konvensjon å vite at den blir slettet.}  

Og på koden min pleide jeg å ha ting som:

  Node * a = new Node (); a->id = 234423; // .... // .. etc // ..slett a;  

Nå har jeg bare må erstatte den koden med:

  Node * a = allocateNode ({}); a->id = 234423; // .... // .. etc // ..freeNode ( a);  

Og koden min fungerer nøyaktig den samme uten å måtte bruke det nye nøkkelordet. Jeg trodde det skulle bli komplisert å refaktorere koden og lage en buffer.

Jeg gjorde denne endringen fordi jeg ønsket å kunne lagre flere noder på koden min. Ved å miste de 33% klarte jeg ikke å skape så mange. Hvis jeg bare tildeler objekter av samme størrelse og ikke tildeler så mange, er det helt greit å bruke nøkkelordet nytt . > Også i tilfelle køen vil jeg tildele og slette objekter veldig raskt. Fordi objektene ikke vil vare på minnet for lenge, og sjansene for å ha haugfragmentering er svært lave.

kort svar: det er ingen topphåndtering som defragmenterer den. slik at du kan tildele ved oppsett eller opprette et utvalg av objekter, men ikke slett / frigjør minnet.
Fordi du har veldig begrenset minne og ikke har noe operativsystem å rydde opp bak deg. Det betyr at du må gjøre mange ting manuelt som du vanligvis ikke trenger å gjøre som å se etter haugfragmentering. Legg til det faktum at en Arduino er et program om gangen, og den slags eliminerer det meste av fordelen ved å bruke heapminne. Du har ingen andre å dele med, så det er greit å være gjerrig. Men det handler mest om å ikke ha operativsystemet for å holde ting rent for deg.
1. Dette spørsmålet ser ut som et duplikat av [Bruker malloc () og gratis () en veldig dårlig idé på Arduino?] (Https://arduino.stackexchange.com/questions/682). 2. Før du refererer til pekeren som returneres med `ny ', bør du sjekke at den ikke er' nullptr '. Ellers er koden din nødt til å krasje på minneutmattelse. 3. For denne spesielle brukssaken er en [ringbuffer] (https://en.wikipedia.org/wiki/Circular_buffer) antagelig det enkleste, minnesikre alternativet. Dette er [hva `Serial` bruker for å innhente byte] (https://github.com/arduino/ArduinoCore-avr/blob/1.8.3/cores/arduino/HardwareSerial.h#L113-L114).
-1 for "Ikke vær en sau og gjør det folk ber deg om å gjøre."
Du kan være en sau og deretter @null og gjøre det folk ber deg om å gjøre;) være veldig null.
Jeg kan ikke kalle dette et svar, fordi det ikke er målrettet mot Arduino, men på noen innebygde plattformer med veldig lite minne for ting som dyngeadministrasjon, blir 'maloc' implementert som 'uint8_t * rval = __frontier; __grense + = sz; retur rval` og `gratis 'er en no-op. Det er unødvendig å si at det på en slik plattform går dårlig å bruke heapminne!
Selvfølgelig har du rett, men som vist i redigeringen din, bruker dynamisk tildeling trygt noen ubehagelige begrensninger. Jeg vil ikke anbefale det til en nybegynner (dvs. de fleste av de som stiller spørsmål her). Det har også sin egen minnekostnad: to byte per tildelt del, pluss litt polstring, fordi det er mer enn usannsynlig at alle objektene dine naturlig har samme størrelse.
@CortAmmon: Har du et eksempel? Avr-libc's `malloc ()` gjør heap-administrasjon, og den kjører på enheter så små som ATtiny13A (1 KiB-blits, 64 byte RAM). Ikke at det ville være veldig fornuftig å bruke den på en slik enhet skjønt ...
@EdgarBonet: ja, jeg kaster bort 2 byte for hver tildeling jeg utfører: /. Jeg skulle ønske haugen lagret størrelsene i en `char` i stedet for å bruke 2 byte, siden jeg aldri lagrer noe større enn 255. Fordi jeg lagrer veldig få objekter, vil jeg bruke dette ved å bruke denne tilnærmingen. Er det en måte å instruere kompilatoren om å lagre størrelsene på dyngjenstander ved hjelp av en byte i stedet for to?
@TonoNam Du kan komprimere det ved å bruke enkel segregert lagring som boost.pool gjør. Dermed kan du bruke biter av vilkårlig størrelse (på bekostning av fragmentering hvis du ikke frigjør dem jevnt, selvfølgelig). Den ekstreme versjonen av dette, med 1 stor bunke, er selvfølgelig den typiske lagringsreservasjonsmetoden du dokumenterer ovenfor.
@CortAmmon takk det vil være flott å vite, men jeg tipper at jeg er komplisert tynner mye. Jeg endte nettopp med å lagre varene på en buffer. Jeg trodde det ville bli vanskelig å refaktorere når det ikke var det. Takk for hjelpen
@TonoNam Du fikk min nedstemme for det siste avsnittet i din første redigering. Du forsvarer aggressivt din opprinnelige løsning, at det er scenarier der det kan være den bedre løsningen - som om noen hevdet at dette ikke var tilfelle. Jeg synes denne typen "jeg kunne ha sluppet unna det jeg hadde i utgangspunktet" holdning noe barnslig. Når du har gjort det folk fortalte deg (og med rette, fordi du fikk noen gode råd), er din anbefaling helt feil: å gjøre det folk ber deg om å gjøre er faktisk nøkkelen til å forstå ting.
Re: "Hvis jeg bare tildeler objekter av samme størrelse og ikke tildeler så mange, er det helt greit å bruke det nye nøkkelordet". Men hvilken fordel får det deg på det tidspunktet? Hvis objektene har en angitt størrelse og du har plass til et angitt antall av dem, hvorfor skulle du ikke bare tildele så mye statisk? Det handler om mer enn bare det du kan, tenk på konsekvensene av at det går galt. Jeg antar at du ikke virkelig lærer det før det biter deg. Men en dag vil du fortelle folk det samme som vi forteller deg fordi du har vært litt forbi det.
Re: "Avslutningsvis, bare forstå hvordan haugfragmentering fungerer, og du vil få mye kraft ved å bruke det nye nøkkelordet. Ikke vær en sau og gjør det folk forteller deg å gjøre uten å forstå hvordan ting fungerer.". Det handler ikke om å være en sau. Du sier at du hater når folk blindt følger regler. Jeg hater når noen som må stille denne typen spørsmål, tror jeg bare er en sau som følger etter. Jeg vet hva jeg holder på med. Jeg vet hvor faren er. Du tror du har overlistet det. Jeg har sett tusen før deg som tenkte det samme. De lærer alle den samme harde leksjonen til slutt. Du vil også.
Fem svar:
chrisl
2020-07-24 00:45:59 UTC
view on stackexchange narkive permalink

De fleste Arduinos (som Uno eller Nano) har svært få RAM, så du må først sørge for at du aldri tildeler for mye minne. Også dynamisk tildeling av minne kan føre til haugfragmentering (heap er den delen av minnet, der dynamisk tildeling skjer).

I de fleste tilfeller vil du tildele minne av forskjellige størrelser (for eksempel matriser av forskjellige størrelser) eller bare forskjellige objekter (med hver sin størrelse) (!!! Dette er nøkkelpunktet her). Så skal du slette noen av disse objektene. Det vil skape hull inne i minnet. De kan fylles igjen med gjenstander med samme eller mindre størrelse. Etter hvert som tiden går og mer tildeling og sletting skjer, har disse hullene en tendens til å bli mindre, opp til det punktet, hvor ingen av dine nye som tildeler objekter kan passe der. Det minnet er da ubrukelig. Dette fenomenet kalles haugfragmentering.

Disse hullene vises naturlig, også på en PC. Men det er to viktige forskjeller:

  1. Arduino har så lite RAM, at hullene kan fylle opp minnet veldig raskt.

  2. Mens PC-en har et operativsystem som styrer RAM-en (defragmenterer den eller legger ubrukte ting bort i en personsøk / byttefil), har ikke Arduino et operativsystem. Så ingen holder øye med den virkelige tilgjengelige RAM-en, og ingen rydder opp i minnet en gang i blant.

Det betyr ikke at du ikke kan bruke dynamisk tildeling på en Arduino , men det er veldig risikabelt, avhengig av hva du gjør akkurat og hvor lenge programmet skal fungere uten å mislykkes.

Tatt i betraktning denne store advarselen, er du veldig begrenset med hvordan du bruker dynamisk tildeling. Å gjøre det for mye vil resultere i veldig ustabil kode. De gjenværende mulighetene, der det kan være trygt å bruke den, kan også enkelt gjøres med statisk tildeling. Ta for eksempel køen din, som i utgangspunktet er en koblet liste. Hvor er problemet med å tildele en rekke QueueItem s i starten. Hvert element får en måte å avgjøre om det er gyldig. Når du oppretter et nytt element, velger du bare det første elementet i matrisen, som har et ikke-gyldig element, og setter det til ønsket verdi. Du kan fortsatt bruke dataene via pekepinnene, akkurat som før. Men nå har du det med statisk tildeling.

Du kan finne ut at koden ser styggere ut på den måten, men du må tilpasse deg plattformen du bruker.

Merk, at dette ikke gjelder når du skal opprette bare objekter med samme størrelse . Deretter vil ethvert slettet objekt etterlate et hull der ethvert nytt objekt kan passe inn. Kompilatoren bruker det faktum. Så i så fall er du trygg. Akkurat hvert objekt, som du dynamisk oppretter i programmet ditt, må ha nøyaktig samme størrelse. Det inkluderer selvfølgelig også objekter som er opprettet i forskjellige biblioteker eller klasser. (Av denne grunn kan det fremdeles være et dårlig designvalg, ettersom du eller andre (hvis du vil publisere koden din) kanskje vil koble biblioteket ditt med en annen kode)

En annen måte å være trygg på er å bare opprette og slette objekter i lukkede sykluser, noe som betyr at et opprettet objekt må slettes før neste objekt opprettes. Selv om det ikke passer for applikasjonen din.


På større mikrokontrollere, for eksempel ikke-Arduino-kortene med ESP32, har mye mer minne. Dermed er ikke bruken av dynamisk tildeling så dårlig for dem. Selv om du fremdeles ikke har et operativsystem for å administrere RAM.

Tusen takk for forklaringen. Jeg har allerede kodet mange biblioteker ved hjelp av pekere. Så jeg antar at nå må jeg være forsiktig med antall objekter jeg plasserer riktig? Koden fungerer nå fordi jeg tildeler veldig få objekter, og det er alltid plass i haugen for nye objekter. Så lenge tildelingen min er liten og jeg ikke tildeler til mange objekter, burde jeg være riktig?
er det noen måte å de-fragmentere minne manuelt?
@TonoNam Ja, det stemmer. Du må vurdere størrelsen, tildelingshastigheten og hvor lenge koden skal kjøre. Forutsatt en konstant tildelingshastighet, kan koden fungere bra for en dag. Men hvis du vil at den skal gå som en måned, kan det være annerledes. Hvis du vil bruke dynamisk tildeling, må du bare teste dette ut.
@dandavis Jeg vet ikke om en måte, annet enn bare å slette alle dynamiske objekter og gjenskape dem. Hvis du gjør dette, vil jeg gjette at du igjen har en lineær haug med minne uten hull i den (effektivt defragmentert). Selv om jeg ikke er nok av en ekspert i kompilatoren, kan jeg garantere det. Jeg tror heller ikke at dette virkelig er et alternativ det meste av tiden.
"De fleste Arduinos (som Uno eller Nano) har svært få RAM". Pluss, Arduino-programmer er vanligvis kontrollere som forventes å kjøre uten tilsyn; de løper ikke til fullføring og begynner på nytt en annen gang som de fleste stasjonære programmer. Så selv en PC med stort minne som bruker `ny` /` delete`-tildeling, vil til slutt også fragmentere minnet. Det vil bare ta (mye) lengre tid.
@dandavis: Den eneste måten å defragmentere "nytt" / "slette" tildelt minne er å ikke gjøre det. ;-) Det jeg gjør i stedet, er å erklære en rekke buffere med fast størrelse, og dele ut en av dem, uansett hvor mye innringeren ba om (opp til bufferstørrelsen, selvfølgelig). Den mekanismen vil ikke forårsake fragmentering fordi du aldri deler blokker, så hvert hull kan brukes på nytt. Du må vite - eller gjette godt! - maksimalt antall buffere applikasjonen din noensinne vil trenge, og erklær noen flere enn det for sikkerhets skyld.
@dandavis Ja, du kan fragmentere minne. Det kalles "memory management", og mens et operativsystem vil ha et sofistikert program for dette, kan du skrive et enkelt. Den største advarselen er at du må oppdatere alle pekere til et objekt når du har flyttet det, så du må kunne finne hver peker til det fra minneadministrasjonsrutinen. Jeg vet ikke om et slikt program passer rimelig på en Arduino. Har ikke prøvd.
Jeg foreslår at du bruker [ETL] (https://www.etlcpp.com/), som er som STL, men for innebygd programmering. Den gir alle vektorer, køer, koblede lister, kart, iteratorer, algoritmer og andre nyttige ting fra C ++ standardbiblioteket, ** uten å bruke dynamisk tildeling i det hele tatt **. (Du må spesifisere den maksimale størrelsen på strukturen din, og alt håndteres automatisk inne i den). Merk: Jeg har ingen tilknytning til ETL, jeg er bare en fornøyd bruker av den.
For en køimplementering er det sannsynligvis bedre å ha en pekepinn eller indeks for det første og siste elementet enn å gi hvert element et gyldig flagg. På den måten blir tilgang O (1), det er mye lettere å pakke rundt matrisen for å gjenbruke minnet, og det er ingen rare situasjoner som "hull" du ikke prøvde å uttrykke.
Jeg vil legge til dette at dynamisk tildeling ofte er treg og noe uforutsigbar når det gjelder tidspunkter (på grunn av fragmentering og andre effekter), så hvis du skriver ytelse eller timingfølsom kode, er det ofte best å unngå det selv om minnebruk er ikke et problem.
crasic
2020-07-24 10:34:47 UTC
view on stackexchange narkive permalink

Dynamisk tildeling frarådes vanligvis i innebygde applikasjoner fordi du ikke kan garantere at du ikke overskrider (prøver å tildele mer enn) tilgjengelig minne. Statisk tildeling vil generelt ha denne garantien, selv om feil i minnet fremdeles kan være mulig.

I tillegg er det langt færre tjenester eller verktøy tilgjengelig for automatisk å administrere og passe på minnet for deg. Enhver tjeneste som gjør det, vil forbruke beregningsressurser.

Dette betyr at du iboende oppretter en mekanisme på enheten din som vil føre til et minne (heap) overløp og mulig udefinert oppførsel (UB). Dette gjelder selv om koden din er feilfri og ikke har minnelekkasjer.

I ikke-kritiske applikasjoner, leting, læring og prototype kan dette ikke være viktig.

Tenk at udefinert oppførsel uten nøye vurdering kan føre til maskinvarefeil og usikker ytelse, for eksempel hvis enheten konfigurerer GPIO på nytt gjennom en feilaktig skriving til de riktige registrene under et krasj.

Ja * Dynamisk tildeling frarådes vanligvis *. +1 for å bruke ordet generelt. I noen tilfeller kan det fungere. Jeg må bare holde rede på hvor mange objekter jeg tildeler og sørge for at alle er av samme størrelse.
Graham
2020-07-24 14:12:48 UTC
view on stackexchange narkive permalink

For det første, fikse biblioteket ditt

Som bemerket av @crasic, anbefales dynamisk minnetildeling vanligvis ikke for innebygde systemer. Det kan være akseptabelt for innebygde enheter som har større mengde ledig minne - innebygd Linux brukes ofte, for eksempel, og alle Linux-apper / -tjenester vil ha en tendens til å bruke dynamisk minnetildeling - men på små enheter som en Arduino er det ganske enkelt ingen garanti for at dette vil fungere.

Biblioteket ditt illustrerer en vanlig årsak til at dette er et problem. Funksjonen enqueue () oppretter en ny QueueItem () , men kontrollerer ikke at tildelingen lyktes. Resultatet av mislykket tildeling kan enten være et C ++ bad_alloc unntak, eller det kan være å returnere en nullpeker, som når du refererer til det vil gi et unntak for systemminnetilgang (SIGSEGV-signal i Linux, for eksempel) . Det er nesten universelt i Linux- og Windows-programmering å ignorere minnetildelingsfeil (som oppmuntret av de fleste lærebøker), fordi den enorme mengden ledig RAM og eksistensen av virtuelt minne gjør dette veldig usannsynlig, men dette er uakseptabelt i innebygd programmering.

Mer generelt, som @crasic sier, kan minnefragmentering la selv ikke-buggy-kode ikke kunne allokere minne. Resultatet vil være en mislykket fordeling av minne, men koden vil i det minste vite at dette har skjedd og vil sannsynligvis kunne fortsette.

Men bedre, bruk en FIFO-kø i fast størrelse i stedet

Koden din er avhengig av dynamisk tildeling for å legge til og fjerne elementer i en kø. Det er fullt mulig (og like enkelt kodemessig) å opprette en matrise med fast størrelse for køen, så de forskjellige feilmodusene for dynamisk tildeling gjelder ganske enkelt ikke. Et element som skal settes i kø kopieres ganske enkelt til neste gratis kø-spor, og et kø-spor merkes gratis når det er brukt. (Ikke glem å bruke en mutex når du legger til og fjerner gjenstander fra køen, fordi det ofte blir ringt til å legge til og fjerne fra forskjellige steder.)

Køen kan lages uansett hvilken størrelse du føler er passende ( som tillater hvor mye RAM du har). Med en fast størrelse er du tvunget til å ta en designbeslutning om hva som skal skje hvis køen renner over - sletter du de eldste dataene for å gi plass til den nye verdien, eller ignorerer du den nye verdien? Dette kan virke som en uvelkommen ny funksjon, men det er en god ting, fordi det tredje alternativet du har for øyeblikket, er at koden din går "Aaaarrggghhh jeg ikke vet hva som skal gjøres!" og krasjer dødelig, og det ønsker vi ikke egentlig.

En FIFO-kø i fast størrelse er vanligvis implementert som en [ringbuffer] (https://en.wikipedia.org/wiki/Circular_buffer).
@EdgarBonet Ja, det er det. OP-en skal imidlertid kunne finne mange eksisterende koder for FIFO-er på nettet, så jeg antar at jeg er mer interessert i å hjelpe dem med å få konseptet enn nøyaktig hvordan de gjør implementeringen.
FIFO-løsningen som er ringbuffer er en flott løsning. Det kommer til å være tider når ringbufferen er bedre (sannsynligvis mesteparten av tiden), og det er andre når den ikke er det. Ta en titt på redigeringen av spørsmålet. Hvis du holder oversikt over tildelinger og aldri overskrider antall tildelinger av samme størrelse, bør du ikke ha noe problem med å tildele et nytt køelement.
Nøyaktig graham svaret ditt er sannsynligvis den beste løsningen for de fleste tilfeller og også den tryggeste. Men for tilfeller når jeg bygger et hierarki av noder, vil det være vanskeligere å lage med en buffer. Hvis jeg allerede tildeler noder, antar jeg at jeg kan fortsette å tildele dem ved hjelp av køen. Poenget mitt er at hvis du vet hvordan ting fungerer, er det greit å ikke gå med på stevnet. Også sannsynligheten for at en tildeling ikke fungerer er veldig liten siden jeg ikke bruker avbrudd eller kjører koden min forskjellige steder. For resten av prosjektene mine vil jeg sannsynligvis bruke løsningen din.
@TonoNam Du antar der at tildelingsgranulariteten din går ned til minst 4 byte. Hvis granulariteten i beste fall er 8 byte, vil hver 12-bytes allokering faktisk oppta 16 byte RAM. Så din heap-baserte kode vil bare kunne lagre 3/4 av det som kan lagres med en statisk tildelt kø. Pluss hva som helst overhead er involvert i å kjøre bunken, noe som kan være viktig når du tildeler små datamengder (og 12 byte er vanligvis "små"). Du kan ikke bare dele RAM-lengde etter strukturlengde og anta at det er hvor mange du får.
Det er grunnen til at jeg redigerte nummer 2 på spørsmålet Graham. Takk for all hjelpen.
Delta_G
2020-07-25 04:15:12 UTC
view on stackexchange narkive permalink

Jeg legger til dette ikke så mye å legge til svaret som å legge til noen virkelige verdensimplikasjoner for de som kan være nede i dette kaninhullet. Det er hyggelig å snakke om hva som kan skje teoretisk, men en ny programmerer kan fortsatt være fristet til å tenke at han kan tenke over disse begrensningene og fremdeles gjøre noe nyttig. Her er noen virkelige verdensprater om hvorfor det er tåpelig selv om du er i stand.

La oss si at vi utvikler kode på en Arduino UNO. Vi har 2K RAM å jobbe med. Vi har en klasse som laster inn en liste med navn, kanskje det er en bygningsadgangsenhet eller noe. Uansett, denne hypotetiske klassen har et navnefelt for å lagre noens navn. Og vi bestemmer oss for å bruke String-klassen til å holde navnet som en String.

La oss si at etter at programmet vårt er der og gjør hva det gjør, er det 500 byte igjen til denne listen over objekter, hver med et navnefelt som kan være av ulik lengde. Så vi løper fint sammen i årevis med et mannskap på 14 eller 15 personer med en gjennomsnittlig navnelengde på 30 tegn eller så.

Men en dag registrerer en ny fyr seg. Og navnet hans er veldig langt. La oss si at det tar 100 tegn. Selv om vi er smarte kodere og bare har en kopi av denne strengen i minnet, har vi den der og plutselig passer den ikke. Nå mislykkes et program som har fungert i årevis plutselig. Og ingen vet hvorfor.

Så løsningen er lett, ikke sant? Håndheve en maksimal grense for lengden på navnet. Noen enkle koder som sjekker navnelengden, og vi kan skrive et flott stykke som fremdeles tillater variabelnavnet og ikke lar deg opprette en ny bruker hvis det er mindre enn så mye igjen. Virker enkelt nok, men så gifter Becky i regnskap seg og hennes etternavn endres fra Smith til Wolfeschlegelsteinhausenbergerdorff, og plutselig bryter programmet vårt uten grunn.

Så løsningen er enkel, ikke sant? Vi håndhever en maksimal lengde, og vi vil sørge for å reservere nok minne til hvert objekt som det har råd til å ha navnet på maksimal lengde. Og det er noe vi gjør mest effektivt uten dynamisk tildeling siden vi allerede vet størrelsen på objektet.

Jeg hører hva du sier, "men Delta-G, hvis vi har alle de korte navnene i der, hvorfor skal vi kaste bort alt det minnet på dem når vi ikke trenger det. La oss bare spare plass i lang tid hvis vi har lenge. " Og jeg liker tankene dine, dette er god tenkning. Men det hjelper ikke noe å spare det minnet. Hvis du sparer noen, hva skal du gjøre med det? Hvis programmet ditt bruker det, er det ikke lenger plass til det lengre tilfellet, og plutselig må du håndheve en enda kortere maksimal lengde for å imøtekomme den bruken.

Si for eksempel at vi har 500 byte rom, og vi håndheve en maksimal lengde på 50 byte for 10 brukere. Og la oss si at når navnene er korte, vil vi la programmet bruke noe av den lagrede plassen. Hvis programmet kan angripe 100 byte i det rommet når navnene er korte, hvorfor skulle ikke den samme situasjonen skje med lange navn? Så egentlig siden programmet kan bruke alt bortsett fra 400 byte, er det egentlig bare 400 byte rom, og vi må håndheve et maksimum på 40 byte for 10 brukere eller 50 byte for 8.

Siden du skal må gjøre det offeret uansett, da er det bare fornuftig å fjerne arbeidet med dynamisk tildeling og bare gjøre ting faste størrelser eller bruke buffere med fast størrelse.

Hvis vi hadde en PC med gigabyte minne, ville vi ikke tenker ikke engang på dette. Men på en Arduino UNO med 2K byte minne kan det bli et stort problem veldig raskt.

Det andre problemet er at disse feilene er så lumske. Hvis feil uten minne bare forårsaket et enkelt krasj og tingen ikke lenger fungerte, ville det ikke være nesten så skummelt. Men det er ikke slik ut av minnefeil fungerer på en mikrokontroller. Alt kommer an på hvordan ting ordnes i minnet av kompilatoren.

Disse feilene manifesterer seg ofte som noe som ser ut til å fungere mesteparten av tiden, men har noen morsomme feil som ingen kan forklare. Kanskje det bare er et problem hvis noen har et navn som er nøyaktig 26 tegn langt og prøver å åpne døren på en onsdag. Eller kanskje problemet bare oppstår hvis Becky logger på umiddelbart etter Bob. Kanskje det bare gnister tre bokstaver på skjermen, men bortsett fra at alt fungerer. Kanskje med et annet navn endret som plutselig blir til at låsen vår åpner for alle. Det er ingen gjetting eller forklaring på grunn av minnefeil. Så vi må være veldig forsiktige med å unngå til og med den eksterne muligheten for å støte på en.

Og det er derfor vi unngår å bruke dynamisk tildeling på små mikrokontrollere. På slutten av dagen er det ingenting du kan spare med det, og selv om du kan konsekvensene av å få noe litt galt er veldig frustrerende. Med denne typen programmer må du nesten alltid ende opp med å håndheve en slags grense på alt, og når du først håndhever grenser, er det ikke lenger bruk for dynamisk tildeling.

Igjen i ditt virkelige eksempel tildeler du objekter av forskjellige størrelser (navn). Det vil føre til haugfragmentering. Hvis du skal tildele navn av samme størrelse og veldig få av dem, er det helt greit å bruke dyngetildeling.
Men som jeg sa i eksemplet, hvis du tildeler et fast antall ting av fast størrelse, hvorfor vil du bruke dynamisk tildeling til å gjøre det? Hva ville være fordelen på det tidspunktet?
At hvis jeg har flere køer, trenger jeg ikke å lage en buffer for hver. Det kan være tilfeller når tildeling er nyttig. Poenget mitt er at hvis du forstår hvordan det fungerer, er det ok å bruke det. Sannsynligvis gir det mest mening ikke bruk av det, men aldri.
Ja ikke aldri, men ikke for det du tenker heller. Du får det samme problemet. Hvis du har flere køer og vil at de skal kunne dele en bufferplass, ville det absolutt være lettere å ha en buffer tildelt og en enkelt klasse som styrer å legge til og fjerne. Jeg tenker sannsynligvis at alt som kan legge til i køen, må arve noen basisklasse som lar deg ha kode på ett sted som gjør det. Det er akkurat brukssaken jeg er på om der du tror du vet hva du gjør og en dag en hjørnesak du aldri mistenkte biter deg.
Jeg antar at det er en av de tingene du bare ikke kan lære noe annet enn den harde måten. Men på slutten av dagen, hvis du vil ha delt bufferplass, må du sette en begrensning på størrelsen, og hvis du skal gjøre det, kan du like godt tildele alt og la koden kontrollere å sette ting inn og ut.
Det eneste gode unntaket jeg kan tenke meg er når du har en klasse du trenger for å sette i gang senere i koden. Kanskje du trenger litt brukerinngang før du kan bygge forekomsten. Så du har bare en peker, og senere i koden kan du konstruere et nytt objekt med nytt og angi pekeren. Men i så fall er planen å lage objektet tidlig i programmet og la det være der for hele tiden. I dette tilfellet har du aldri tenkt å frigjøre eller slette objektet og opprette et nytt.
RDragonrydr
2020-07-30 04:54:28 UTC
view on stackexchange narkive permalink

Som noen som er interessert i hvordan maskinvare muliggjør databehandling, er det faktisk en rekke fascinerende fakta om hvorfor dyngen er farlig på innebygde systemer utover bare å ikke ha mye RAM. De fleste problemer du vil støte på med et Arduino eller lignende system, er ikke å overskride RAM-størrelsen med variabler, men fra minnefragmentering. Dette er en bivirkning av hvordan dyngen tildeler minne i innebygde systemer, som vanligvis ikke har en minnebehandler.

Haugen på enkle enheter som Arduino er det som er nærmest det som er kjent som et "base and bounds" -system (teknisk sett er det stack-allokert, som også kan slås opp hvis du er nysgjerrig). Minne tildeles et sted, og blir deretter merket med informasjon om hvor det er i minnet og hvor mye det har. Når den blir distribuert, blir denne regionen fri igjen. Problemer oppstår fordi dette rommet må være sammenhengende . Tenk på tildelt minne som en matrise (som det i utgangspunktet er); det er ingen logikk å si "oops, det er et annet objekt her, hopp over 37 byte og fortsett.

Hvis du foretar gjentatte tildelinger, deles minnestykker ut og beveger seg lenger opp i haugen, siden to deler av minnet kan tydeligvis ikke oppta samme plass. Minne kan bli tilgjengelig igjen hvis noe av det blir frigjort, men det gjør at resten av det "flyter" høyere opp på bunken, midt i minneplassen (hvis du noen gang har spilt et bestemt 3D-voxel-byggespill, tenk å bryte ut en del av en trestamme). Ytterligere tildelinger vil da fylle ut deler av de ryddede områdene i de delte delene (eller på toppen av den brukte haugen, hvis de ikke passer inn de ryddede områdene), men med mindre det nylig tildelte minnet er nøyaktig samme størrelse som det som ble fjernet, dannes hull der ledig minne er for liten til å passe til noe annet. Til slutt kan det være mye ledig minne, men ingenting av det kan brukes siden det ikke er sammenhengende seksjoner, selv om det er en haug med byte her og der.

Som nevnt er en løsning å sikre at alt allokert minne er av samme størrelse, men hjelper kanskje ikke når så mye av Arduino-systemet bruker dynamisk minne alene. Dette er hovedsakelig alt å gjøre med strenger (spesielt returnere ing dem, sammenkobling eller innstilling av hverandre), men det kan godt være andre verktøy som bruker det, noe som gjør feilsøking seg farlig. Alt som lar deg "registrere" en tilbakeringing, lytter eller brukerfunksjon, vil også mest sannsynlig bruke dynamisk minne, og jeg forestiller meg at et WiFi-bibliotek med "skann" -funksjonalitet eller et filsystembibliotek (for eksempel SD-kort) sannsynligvis også vil gjøre det.

Mange av de innebygde strengbibliotekene lager faktisk fullkopier av heapminne ved retur eller definisjon, så selv om den opprinnelige strengen deretter blir fordelt automatisk av String-biblioteket når variabelen går utenfor omfanget, var det kort to kopier av elementet på dyngen og forårsaket fragmentering (siden den andre ble støtet frem av den første, etterlot et hull når den første slettes, men den andre forblir).

Det ville sannsynligvis være trygt hvis du bruk dynamisk minnetildeling i svært små mengder og sjelden, så lenge det ikke er noen måter du kan tegne det maksimale tildelte minnet på en måte som vil forhindre tildeling av et nytt stykke, men dette er riktignok irriterende å teste.

Det finnes en ikke så perfekt løsning der du bruker dyngen som noe som en bunke, og du tildeler alt ekstra stort riperom mens du kjører en oppgave, og deretter fordeler den før du gjør neste element eller på annen måte tildel minne igjen. Dette lar deg håndtere problemer som vil ta mer minne enn du ville hatt hvis du brukte et globalt for hver, men igjen kan dette også gjøres hvis du bare legger den delen av koden med krøllete parenteser og bruker en lokal matrise (som setter den på faktisk bunken). Merk at noen systemer (som ESP8266, som kan programmeres med Arduino-rammeverket, men absolutt ikke er en Arduino), kan ha begrensninger på stakkstørrelsen, så dynamisk minne kan faktisk være nødvendig i noen tilfeller.

Interessant, til tross for at bunken er stablet tildelt, er den faktiske bunken immun mot dette problemet. Dette er på grunn av hvordan returorientert programmering fungerer. Enhver funksjonssamtale eller minnetildeling på bunken (som, som du vet, gjøres ved å definere en variabel på vanlig måte) blir alltid plassert på toppen, og når den returnerer eller går utenfor omfanget, blir den slettet. Siden det ikke er noen måte å slette ting fra midten av bunken, er det aldri noen fragmentering, og dermed er ledig minne alltid tilgjengelig så lenge du ikke overskrider fysiske ressurser. Stableallokering mislykkes dermed bare når det tildelte minnet slutter å oppføre seg som en stabel (som dessverre ofte er).

Som et siste notat bruker moderne datamaskinoperativsystemer en mye mer avansert teknikk som kalles personsøk, som løsningen din nærmer seg (kjernen kan ha og bruker unntak fra denne regelen, siden det er tidskritisk og personsøk er litt tregere, men det er en grunn at koding av kjernen er så møysommelig). I personsøk er all minnet spesielt tildelt i like store blokker, og blokker kan deles ut for best mulig å møte minneforespørsler. Blokkene kan imidlertid ordnes i hvilken som helst rekkefølge og trenger ikke engang å være fysisk tilstøtende i minnet. Dette betyr at det ikke er noen måte at et minne ikke kan tildeles, ettersom biter alltid er like store (og dermed alltid passer inn i hullene). Det er minnehåndteringsenheten som utfører resten av magien ved å gjøre alle de små blokkene til det som virker som et større minneutvidelse (og også ved å håndtere minneeierskap, men det er ikke relevant her).

Dessverre krever personsøk maskinvarestøtte, mye mer minne for å holde sidetabellen og OS-støtte (eller i det minste en slags manager som med jevne mellomrom kan sparke inn og omorganisere ting for deg). Ingen av disse er til stede i en Arduino, og ville faktisk forårsake problemer for den slags sanntidskoden som opprinnelig var ment å kjøre på en Arduinos CPU. Husk at Arduino ble bygget rundt en liten, billig chip som tilfeldigvis hadde et åpen kildekode-programmeringsgrensesnitt og kompilator (og noen få andre prosjekter som hadde koden og IDE "lånt"), og var egentlig ikke designet for den slags bruker som vi ofte legger det til (det er grunnen til at dataprogrammering ikke er helt det samme som innebygd utvikling; koderen må tenke på en mye mer lavt nivå enn normalt, og det er grunnen til at C / C ++ ofte brukes - det er noe nærmere Assembly enn de fleste andre språk, selv om C ++ kommer vekk fra det). Selv arkitekturen til AVR-prosessoren er ganske fortellende om den faktiske brukssaken: Høyhastighets sanntidsbehandling. Minne- og databussene er uavhengige, slik at kode kan kjøres selv når den streamer data fra RAM, EEPROM eller en I / O-pinne.

Det er en nyttig gratis guide til operativsystemdesign kalt "Operating Systems: Three Easy Pieces, "som er kilden til noe av det jeg lærte og deretter skrev her. Siden forfatterne ikke vil ha kapitlene tilknyttet direkte, og destinasjonssiden har en rekke falske lenker til tilfeldige bøker, kan jeg ikke koble den direkte, men du kan alltid slå opp den hvis du er nysgjerrig. Du vil selvfølgelig ha emnene om minnestyring, og kapitlene i seg selv er lenket i delen med farget tabell.



Denne spørsmålet ble automatisk oversatt fra engelsk.Det opprinnelige innholdet er tilgjengelig på stackexchange, som vi takker for cc by-sa 4.0-lisensen den distribueres under.
Loading...