Guten Abend,
ich möchte das SDRAM auf einem STM32F429-Discoveryboard mit ins
Linkerscript aufnehmen, was bis dato unbekanntes Terrain ist. Als IDE
verwende ich Atollic TrueStudio. Für's SDRAM auf dem Discoveryboard gibt
es im Forum schon einige Beiträge, die verschiedene Anwendungen
abdecken, bspw. als Speicher für den Heap. Ich habe noch
Verständnisschwierigkeiten bzgl. der Sektionen bzw. Segmente und den
notwendigen Modifikationen des Startupcodes. Und ich will rausfinden,
was geht bzw. nicht geht..
Die TEXT und RODATA Segmente sind Programmcode bzw. Konstanten und
landen daher im Flash. DATA beinhaltet initialisierte Variablen. Für das
BSS Segment hab ich unterschiedliche Aussagen gefunden: im einen Fall
sind dort die nicht initialisierten Variablen abgelegt, diese werden
(üblicherweise) mit dem Wert Null initialisiert. Im anderen Fall sind
dort die explizit mit dem Wert Null initialisierten Variablen abgelegt -
ich gehe momentan davon aus, dass letzteres wahr ist, weil man damit
Speicher bei der Initialisierung spart. Womit die nicht initialisierten
Variablen einen mehr oder weniger zufälligen Wert haben.
DATA und BSS beinhalten aber nur globale oder statische Variablen
innerhalb von Funktionen. Nicht statische lokale Variablen landen immer
auf dem Stack. Den Heap i.V.m. malloc() & Co. lasse ich jetzt mal außen
vor. Ist das soweit korrekt?
Solange das alles in einem(!) internen RAM-Block läuft, ist mir das
denke ich klar - zumindest wenn meine obige Aussage korrekt ist :)
Wie ist das nun, wenn weiteres internes oder externes RAM dazu kommt?
Das Discoveryboard hat 8MByte SDRAM, und der STM32F429 selbst hat
mehrere RAM-Blöcke zu 192kByte, 64kByte und 4kByte, also insgesamt
260kByte.
Das mit STMCubeMX generierte Linkerscript hat ohne Modifikationen
folgende Sektionen:
1
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 2048K
2
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 192K
3
MEMORY_B1 (rx) : ORIGIN = 0x60000000, LENGTH = 0K
4
CCMRAM (rw) : ORIGIN = 0x10000000, LENGTH = 64K
D.h. das CoreCoupled-RAM ist ebenfalls angelegt. Weiter unten im
Linkerscript gibt's den Hinweis, dass der Startupcode angepasst werden
muss, wenn initialisierte Variablen verwendet werden. Dass ich dazu den
Teil des Startupcodes, welcher das DATA Segment bedient, kopieren und
mit den entsprechenden Anfangs- und Endadressen modifizieren muss ist
mir klar.
Was mir nun fehlt, ist die Differenzierung zwischen DATA und BSS für die
entsprechende Sektion. Ausgehend davon, dass BSS die mit Null
initialisierten Variablen meint: Wird für Sektionen ungleich der
"Standard"-RAM-Sektion keine Unterscheidung zwischen DATA und BSS
getroffen? Ich habe ein bisschen experimentiert: ich habe ein
entsprechendes Projekt erstellt (ohne irgendwelchen Modifikationen bzgl.
Linkerscript) und zwei globale Variablen im CC-RAM angelegt:
Die eine ist initialisiert, die andere nicht. Ich hab mir mit "objdump
-h" mal die ganzen Sektionen ausgeben lassen, ich sehe im CC-RAM 8
Bytes. Der BuildAnalyzer in TrueStudio behauptet das gleiche :) Die
Frage ist hier also, ob in anderen Sektionen tatsächlich nicht zwischen
initialisiert mit Null, ungleich Null und nicht initialisiert
unterschieden wird (oder werden kann) oder ob das durchaus geht und für
mich nicht offensichtlich ist. Was ich nämlich auch sehe ist, dass die
unitialisierte Variable eine LoadAddress (LMA) im ELF-File hat, d.h.
deren Wert wird implizit mit 0 in den Output mit aufgenommen und belegt
damit unnötig Platz.
Wo liegt hier der Fehler? Muss man hier zwangsweise ohne Initialisierung
arbeiten und die Sektionen im Linkerscript mit mit NOLOAD bzw. NOINIT
angeben?
Die nächste Frage betrifft die Verwendung der verschiedenen RAM-Blöcke
(intern wie extern). Sobald ich eine Variable mittels attribute-section
in ein anderes Segment schubse, muss diese Variable global sein.
Innerhalb einer Funktion kann ich die Variable (mit Ausnahme der
dynamischen Allozierung mit malloc() & co) nicht in einem anderen
Segment anlegen, ist das korrekt? So etwas wie ein
1
#pragma AB_HIER_ALLES_IN_EINE_ANDERE_SEKTION
2
...
3
#pragma AB_JETZT_WIEDER_ALLES_NORMAL
gibt es vermutlich nicht :)
Das heisst die anderen RAM-Blöcke außer dem 192kByte Hauptblock eignen
sich "nur" für größere statische Buffer und/oder Verwendung als Heap, da
man die Blöcke nicht zusammen fassen kann? Mir ist klar, dass man nicht
jeden vorhandenen RAM-Block auch ins Linkerscript aufnehmen muss, da man
je nach Controller den Speicher auch dediziert bspw. für DMA-Buffer o.ä.
verwenden kann, aber diesen Ansatz lasse ich mal aussen vor. Mir geht's
um den Fall dass alle Blöcke dem Linker bekannt sind).
Umgekehrt könnte man DATA/BSS/Stack/Heap ins externe SDRAM linken. Wobei
das aber vermutlich zu Lasten der Performance geht, je nachdem was man
alles ins SDRAM (oder einen anderen internen Block) nimmt. Hier wäre
nebenläufig interessant, wie man denn die Performance aussagekräftig
testen könnte. Den Heap jeweils intern und im SDRAM und dann viele
kleine dynamisch allozierte Speicherblöcke mit komplexen Berechnungen
füllen? Oder jeweils intern und extern ein uint8_t-Array mit einer Länge
von 128k von 0 bis "voll" inkrementieren? Das könnte dauern :D Okay,
ernsthaft, gibt es einen Quasi-Standard für den Performancetest von
Speichern an einem Microcontroller? Ich weiss, dass es diverse
Benchmarks für die Controller selbst gibt, da liefern sich ja die
Hersteller immer ihre Schlachten um DMIPS & Co, aber das betrifft ja
soweit ich weiß immer nur den Controllerkern, nicht den internen oder
externen Speicher.
Für Hinweise über weitere "Anwendungsfälle" bzw. Do's & Don'ts bin ich
dankbar :)
Gruß Ralf
Ralf schrieb:> Womit die nicht initialisierten Variablen einen mehr oder weniger> zufälligen Wert haben.
Variablen mit statischer Lebensdauer (also alle globalen plus solche,
die explizit als "static" markiert sind) werden in der
Programmiersprache C immer initialisiert. Wenn nichts angegeben ist,
dann (de facto für alle hier und heute
praktikablen Fälle) mit 0.
Es ist dabei egal, ob du nun
1
intfoo=0;
oder (auf globalem Niveau)
1
intfoo;
schreibst; in beiden Fällen landet die Variable im .bss. Bei sehr alten
GCC-Versionen war es noch ein Unterschied: explizite Initialiserung
brachte sie dort nach .data, bei dem ein explizitert Initialwert im
Binary (bzw. hier im Flash) hinterlegt wird, d.h. dort war es
ausgesprochen unvorteilhaft, eine explizite Initialisierung anzugeben.
Habe nicht alles gelesen, aber in Kurzform: Daten kannst du by default
nur entweder in den einen oder in den anderen RAM packen lassen. Alles
andere musst du über explizite .section-Attribute bei jeder
non-default-memory-space-Variablen organisieren.
Wenn du in beiden RAM-Bereichen initialisierte Variablen unterbringen
können möchtest, dann musst du dich sowohl im Linkerscript als auch in
einem dazu passenden Startup-Code darum kümmern, dass deren Werte aus
dem Flash kopiert werden. Das macht man, indem man im Linkerscript
entsprechende globale Symbole platziert, die dann vom Startupcode
referenziert werden. (Gleiches trifft auf nicht explizit initialisierte
Variablen zu, die ausgenullt werden müssen.) Beim SDRAM wird sich der
Startupcode ja vermutlich vorher auch noch drum kümmern müssen, dass die
Hardware zur Ansteuerung des SDRAM passend initialisiert wird, bevor man
dort was ausnullt oder initialisiert.
Alternative: den SDRAM halt nur für sowas wie einen Heap benutzen.
malloc() gibt per definitionem einen Speicherbereich zurück, der
„irgendwas“ enthalten darf.
Hallo Jörg,
> Variablen mit statischer Lebensdauer ... werden in der Programmiersprache> C immer initialisiert. Wenn nichts angegeben ist, dann (de facto für> alle hier und heute praktikablen Fälle) mit 0.
Ah, wieder was gelernt. Ich dachte die statischen, aber nicht
initialisierten haben einen Zufallswert.
> Bei sehr alten GCC-Versionen war es noch ein Unterschied: explizite> Initialiserung brachte sie dort nach .data, bei dem ein explizitert> Initialwert im Binary (bzw. hier im Flash) hinterlegt wird, d.h. dort war> es ausgesprochen unvorteilhaft, eine explizite Initialisierung anzugeben.
Du beziehst dich jetzt auf die Initialisierung mit Null, oder? D.h. die
Null wurde, obwohl eigentlich nicht nötig, explizit ins Binary gesetzt?
> Habe nicht alles gelesen, aber in Kurzform: Daten kannst du by default> nur entweder in den einen oder in den anderen RAM packen lassen. Alles> andere musst du über explizite .section-Attribute bei jeder> non-default-memory-space-Variablen organisieren.
D.h. es gibt neben dem attribute-section noch weitere Attribute, die ich
den Variablen verpassen kann? Dann werd ich da gleich mal nachforschen
:)
> Wenn du in beiden RAM-Bereichen initialisierte Variablen unterbringen> können möchtest, dann musst du dich sowohl im Linkerscript als auch in> einem dazu passenden Startup-Code darum kümmern, dass deren Werte aus> dem Flash kopiert werden. Das macht man, indem man im Linkerscript> entsprechende globale Symbole platziert, die dann vom Startupcode> referenziert werden.
Hab ich gesehen, das wären dann bspw. die Symbole _sdata bzw. _edata für
die initialisierten Variablen. Hier muss ich dann selbst passende
Symbole vergeben und gucken, dass ich den LocationCounter richtig
bediene :)
> (Gleiches trifft auf nicht explizit initialisierte Variablen zu, die> ausgenullt werden müssen.) Beim SDRAM wird sich der Startupcode ja> vermutlich vorher auch noch drum kümmern müssen, dass die Hardware zur> Ansteuerung des SDRAM passend initialisiert wird, bevor man dort was> ausnullt oder initialisiert.
Guter Hinweis, ich darf nicht vergessen, die Initialisierung für's SDRAM
in den ResetHandler zu setzen.
> Alternative: den SDRAM halt nur für sowas wie einen Heap benutzen.> malloc() gibt per definitionem einen Speicherbereich zurück, der> „irgendwas“ enthalten darf.
Ja, das muss ich noch ausknobeln, ob ich das SDRAM nur für den
Displaybuffer und den Heap verwende (Heap wäre beim aktuellen Projekt
das Hauptziel). Deswegen hatte ich auch gefragt, was denn alles (nicht)
geht. Ich muss noch rausfinden, ob es für die anderen Blöcke auch eine
Trennung nach initialisiert gleich und ungleich Null gibt (geben kann).
Danke schonmal, ein Teil der Fragen hat somit schon eine Antwort
bekommen.
Ralf
Ralf schrieb:> Ah, wieder was gelernt. Ich dachte die statischen, aber nicht> initialisierten haben einen Zufallswert.
Nein, nur "auto"-Variablen (wobei das "auto" normalerweise ja
weggelassen wird, und in C++ auch gleich mal eine andere Bedeutung
bekommen hat).
> Du beziehst dich jetzt auf die Initialisierung mit Null, oder? D.h. die> Null wurde, obwohl eigentlich nicht nötig, explizit ins Binary gesetzt?
ja
> D.h. es gibt neben dem attribute-section noch weitere Attribute, die ich> den Variablen verpassen kann? Dann werd ich da gleich mal nachforschen> :)
Es gibt andere, aber hier musst du das schon mit
__attribute__((section(""))) lösen.
> Hab ich gesehen, das wären dann bspw. die Symbole _sdata bzw. _edata für> die initialisierten Variablen. Hier muss ich dann selbst passende> Symbole vergeben und gucken, dass ich den LocationCounter richtig> bediene :)
Richtig, das ist der Zusammenhang. Du hast im Moment einen Block Code im
Startup, der Daten aus dem Flash in den RAM initialisiert (für .data)
und einen zweiten (üblicherweise direkt danach), der Daten des .bss
löscht.
Diese beiden Codeblöcke müsstest du für einen weiteren RAM-Typ
duplizieren, einschließlich der zugehörigen Symbole im Linkerscript.
Hi Jörg,
> Richtig, das ist der Zusammenhang. Du hast im Moment einen Block Code im> Startup, der Daten aus dem Flash in den RAM initialisiert (für .data)> und einen zweiten (üblicherweise direkt danach), der Daten des .bss> löscht.
Bin an der Stelle theoretisch auch ein bisschen weiter, was die
Geschichte bzgl. der separaten DATA/BSS Sektionen angeht. Anscheinend
kann man da tatsächlich nicht trennen, wenn man die Variablen dediziert
in andere Sektionen schiebt. Alternativ könnte man wohl entweder im
Outputfile die DATA/BSS Sektionen vor dem Linken umbenennen oder im
Linkerscript explizit die jeweiligen Outputfiles entsprechend
verarbeiten. Das werd ich mal noch weiter untersuchen.
Ralf
Naja, ist eigentlich auch keine Raketenwissenschaft. :-)
In einem "normalen" ELF-Binary (also einem, was auf dem Host läuft),
werden die Sections .text und .data beim Programmstart geladen¹), .bss
wird als genullter Speicher vom Betriebssystem bereit gestellt
(möglicherweise auf einer Seitengrenze im Speicher ausgerichtet).
¹) "Geladen" heißt dabei nur, dass sie Seitentabellen vorbereitet
werden, aber es wird nichts real "geladen". Stattdessen gibt es beim
ersten Zugriff auf eine Seite einen page fault, den fängt das
Betriebssystem ab, lädt die Seite (und vielleicht noch nachfolgende)
wirklich in den RAM, aktualisiert die Seitentabellen, und die
Applikation wird dort weiter fortgesetzt, wo der page fault sie zuvor
unterbrochen hatte.
Bei einem Controller geht das alles so nicht. .text wird daher direkt
auf den Flash abgebildet, der Controller kann das dann von da lesen.
.data dagegen benötigt Initialwerte, die ja hier nicht mehr aus der
ELF-Datei kommen können wie auf einem Hostsystem. Daher wird der Bereich
von .data parallel sowohl im Flash (für die Initialwerte) also auch im
RAM (für tatsächlichen Speicher) geführt. Das macht der Linkerscript.
Der Startupcode wiederum muss die vom Linker hinterlegten Daten aus dem
Flash beim Start in den RAM kopieren. Bei .bss ist es einfacher, da
braucht man eigentlich nur noch Anfang und Ende (per Linker-Symbol), der
Inhalt wird mit Nullen gefüllt.
Die Verbindung zwischen Linkerscript und Startupcode wird dabei durch
globale Symbole erzeugt, die der Linkerscript definiert.
Ja, das hab ich soweit auch verstanden. Funktioniert ja auch - solange
es eben für jeden Segmenttyp nur einen möglichen Speicher gibt. Auf nem
PC ist das der Fall, auf nem Controller sieht's anders aus. Da werd ich
mal noch knobeln.
Ralf
Ja, ich schätze fast, dass es auf sowas rauslaufen wird. Schade, dass
das, was du beispielhaft angeführt hast, nicht automatisch geschieht:
das Anhängen von _bss, _data an den Sektionsnamen. Dann bräuchte man
nicht darauf achten, wo man die Variablen ablegt. Aber egal, es scheint
der bisher praktikabelste Weg zu sein.
Besten Dank für die Unterstützung.
Ralf
Ehrlich: ich würde mir den Zirkus sparen, und würde den SDRAM als Heap
benutzen. Falls du malloc() sonst nicht groß brauchst, einfach direkt,
indem du _sbrk() passend implementierst. Falls du zwischen „schnellem“
(interner SRAM) und „langsamem“ (SDRAM) Heap unterscheiden musst, dann
halt für einen von beiden einen händischen Allocator bauen.
Ich weiß, malloc und Heap sind für viele hier ein rotes Tuch, aber man
muss der Realität auch mal ins Auge blicken, dass sich mehrere hundert
Kilobyte RAM bei wirklich dynamischen Anforderungen damit sinnvoller
verwalten lassen als durch irgendwelche statischen Zuweisungen, von
denen sich dann nach einem halben Jahr produktivem Einsatz zeigt, dass
sie eigentlich doch nur zu klein gewählt waren.
Guten Morgen Jörg,
> Ehrlich: ...
das Hauptziel für den aktuellen Fall ist tatsächlich der Heap. Ich
portiere gerade eine fremde C++-Applikation, die jede Menge dynamischen
Speicher verwendet. Der erste Lauf auf einem F401 mit 96kB RAM hat
gezeigt, dass das gerade so passt - ungefähr 80kB gehen da für
dynamische Allozierung drauf. Da bleibt nicht viel übrig, um die
Applikation an sich dann noch mit Daten zu füttern.
Für die Verwendung des SDRAMs als reinen Heap sind hier im Forum schon
Beispiele verfügbar, d.h. das kann ich vermutlich umsetzen. Nur guck ich
bei was neuem auch gleich gern mal links und rechts drüber raus, um zu
sehen, was noch gehen kann - oder eben nicht. Daher auch meine Fragen
über die ganzen Sektionen und wie GCC das macht, etc.
> Ich weiß, malloc und Heap sind für viele hier ein rotes Tuch, ...
Für mich nicht, wenn ich's brauch, dann nehm ich's auch :) Ehrlich
gesagt finde ich (bisher) die Aussagen, dass Heap & Co. auf nem
Controller überflüssig, gefährlich, unsinnig oder sonstwas sind,
übertrieben. Wenn man es richtig macht, dann ist das schon ne
Erleichterung. Richtig bedeutet hier, dass man trotz allem nicht
vergessen darf, dass man zwar auf einem PC mit zig GB RAM entwickelt,
aber eben nur für einen Controller mit wenigen kB oder wenn überhaupt MB
RAM. Eine bare-metal Implementation mit RAM im GB Bereich ist selten.
Einen eigenen Allokator würd ich auch gern mal selbst schreiben, aber
das trau ich mir nicht zu. Habe kürzlich (und nebenläufig) im Rahmen der
Portierung angefangen, den Heap wie ihn GCC bzw. newlib implementiert,
zu untersuchen bzw. zu verstehen. Auch nicht so einfach, weil ich kaum
was dazu gefunden habe, wie die Verwaltung intern abläuft.
Ich werde nun versuchen, das RAM und das SDRAM aufzuteilen. Die 192kB
RAM als normalen Arbeitsspeicher, die 64kB CCRAM als Stack, und das
SDRAM teile ich auf in Displaybuffer, Heap und einen speziellen Bereich
für die Eingangsdaten der Applikation.
Ralf
Benutzt man den Heap wirklich dynamisch, dann handelt man sich
Speicherfragmentierung ein, weil mangels MMU der logische Adreßraum
gleich dem physikalischen ist, und der ist relativ klein. Damit sammelt
man Zustand unter der Haube an, und das System ist nicht mehr testbar.
Solange man quasi-statisch alloziert, also einmal beim Startup, ist
malloc auch auf einem Controller kein Problem.
Weiterere Auswege sind, daß man immer nur Blöcke einer einzigen, festen
Größe vergibt (und dann auch nur solche einzelnen Blöcke als
kontinuierlich alloziert erwartet), oder immer nur in Potenzen von zwei
(womit man nie mehr als 50% Speicher verschwendet).
Daß die Laufzeiten eines Allokators sehr variabel sein können, damit muß
man halt leben und den nicht gerade z.B. in einem Interrupt aufrufen,
oder für irgendwas mit schnellen Reaktionsanforderungen.
Nop schrieb:> Benutzt man den Heap wirklich dynamisch, dann handelt man sich> Speicherfragmentierung ein, weil mangels MMU der logische Adreßraum> gleich dem physikalischen ist, und der ist relativ klein.
Das ist eine zwar immer wiederholte, aber in der Absolutität keineswegs
korrekte Aussage. Man kann sich sowas einhandeln, ja. Aber man muss
nicht zwangsläufig: wenn die Anforderungen tatsächlich einigermaßen
stochastisch daher kommen (in der Größe und Lebensdauer der Requests),
und man eine ausreichende Reserve an Speicher hat, dann sortiert sich
das durch die Aggregierung des freigegebenen Speichers durchaus so weit,
dass dieser Effekt normalerweise nicht eintritt. Genau dieser Fall
(Allokation ist stark variabel und reichlich zufällig) würde jedeoch bei
der immer wieder gepriesenen statischen Vorallozierung einen deutlich
größeren „Verschnitt“ hervorrufen und daher viel eher in die Gefahr
einer memory congestion laufen als vollkommen dynamische Allokation.
Dass man in einem solchen Szenario grundsätzlich eine „Exit-Strategie“
braucht (Was tu ich, wenn der Speicher nicht ausreicht?), sollte
sonnenklar sein.
Ralf schrieb:> Habe kürzlich (und nebenläufig) im Rahmen der Portierung angefangen, den> Heap wie ihn GCC bzw. newlib implementiert, zu untersuchen bzw. zu> verstehen.
Du kannst dir den aus der avr-libc ansehen, der ist noch vergleichsweise
einfach gehalten.
Auch der K&R (also das Papier-Buch :) enthält eine einfache
Implementierung.
Für größere Devices lohnen sich halt aufwändigere Allokatoren, bei denen
man mehrere Bereiche hat, aus denen unterschiedlich große Anforderungen
erfüllt werden, um so den Verschnitt insgesamt geringer zu halten.
Hallo,
@Nop:
> Benutzt man den Heap wirklich dynamisch, dann handelt man sich> Speicherfragmentierung ein, weil mangels MMU der logische Adreßraum> gleich dem physikalischen ist, und der ist relativ klein. Damit sammelt> man Zustand unter der Haube an, und das System ist nicht mehr testbar.
Hmm... ist das nicht zu arg einschränkend? Ohne jetzt genau zu wissen,
wie der GCC-Allokator (oder generell ein Allokator) arbeitet, behaupte
ich trotzdem, dass es drauf ankommt, wie man seine Applikation
programmiert.
Da ich noch nie eine MMU/MPU gebraucht habe (auch wenn sie vorhanden
waren), sind MMU/MPU für mich eher nette Beigaben als zwingende
Voraussetzungen. Beachte: ich hatte bisher auch keine
OS-Implementierungen am laufen, alles bare-metal und nix mit
Multitasking. Nebenläufigkeit hab ich per StateMachine, etc. realisiert.
Wenn ich mal wirklich mit einem OS spiele (z.B. steht Kennenlernen von
FreeRTOS über die Weihnachtstage auf dem Plan :D ), dann mag es durchaus
sein, dass ich die Vorzüge von MMU/MPU kennenlerne.
> Solange man quasi-statisch alloziert, also einmal beim Startup, ist> malloc auch auf einem Controller kein Problem.
Das hab ich nun leider bei diesem konkreten Projekt eben nicht in der
Hand. Ich kann nur sagen, dass definitiv nicht nur beim Start alloziert
wird, sondern munter rauf und runter und überall und sowieso.
> Weiterere Auswege sind, daß man immer nur Blöcke einer einzigen, festen> Größe vergibt (und dann auch nur solche einzelnen Blöcke als> kontinuierlich alloziert erwartet), oder immer nur in Potenzen von zwei> (womit man nie mehr als 50% Speicher verschwendet).
Dann brauch ich aber die Allozierung gleich gar nicht. Aus meiner Sicht
ist die dynamische Speicherverwaltung ja genau für den Fall, dass man
den Speicherbedarf vorab nicht kennt, da.
> Daß die Laufzeiten eines Allokators sehr variabel sein können, damit muß> man halt leben und den nicht gerade z.B. in einem Interrupt aufrufen,> oder für irgendwas mit schnellen Reaktionsanforderungen.
Guter Hinweis. Ich nehme an, die Laufzeit des Allokators ergibt sich
(abgesehen natürlich von der Implementierung selbst) dadurch, dass nach
einem passenden Block gesucht werden muss? Was wiederum bei starker
Fragmentierung entsprechend viel Zeit benötigt.
@Jörg:
> Du kannst dir den aus der avr-libc ansehen, der ist noch vergleichsweise> einfach gehalten.> Auch der K&R (also das Papier-Buch :) enthält eine einfache> Implementierung.
Danke :)
> Für größere Devices lohnen sich halt aufwändigere Allokatoren, bei denen> man mehrere Bereiche hat, aus denen unterschiedlich große Anforderungen> erfüllt werden, um so den Verschnitt insgesamt geringer zu halten.
Sind das die "Bins"? Während ich auf der Suche nach weitergehenden Infos
zum Heap, Malloc & Co war, ist mir der Begriff öfter untergekommen.
Gegenfrage: was erzeugt den von dir genannten Verschnitt? Ich kann da
nicht ganz folgen :o
Grüße
Ralf schrieb:>> Für größere Devices lohnen sich halt aufwändigere Allokatoren, bei denen>> man mehrere Bereiche hat, aus denen unterschiedlich große Anforderungen>> erfüllt werden, um so den Verschnitt insgesamt geringer zu halten.> Sind das die "Bins"?
Ja. Ein "Bin" ist ja ein "Behälter" (denk an den Trashbin, Mülleimer).
> Gegenfrage: was erzeugt den von dir genannten Verschnitt? Ich kann da> nicht ganz folgen :o
Wenn man ein bisschen mehr Reserven an Speicher hat, lohnt es sich,
mehrere Größenklassen zu haben und diese zusammenzufassen, also bspw.
alle Allokationen < 32 Byte in einen „Eimer“, dann alle >= 32 < 1024 in
einen zweiten, alles größer wird einzeln genommen. Dann passiert es
nicht, dass man zwischen zwei 1000er Allokationen einzelne Kleckerchen
von wenigen Bytes hat.
Jörg W. schrieb:> dann sortiert sich> das durch die Aggregierung des freigegebenen Speichers durchaus so weit,> dass dieser Effekt normalerweise nicht eintritt.
Wenn man halt auf nicht testbare Systeme steht und es nicht so drauf
ankommt, ob man halt ab und zu mal einen Reset macht, kann man das schon
machen.
> Genau dieser Fall> (Allokation ist stark variabel und reichlich zufällig) würde jedeoch bei> der immer wieder gepriesenen statischen Vorallozierung einen deutlich> größeren „Verschnitt“ hervorrufen
Bei der statischen Vorallozierung spielt das keine Rolle, weil nur
einmal alloziert wird. Das ist somit auch leicht testbar: fährt das
Gerät hoch oder nicht?
> Dass man in einem solchen Szenario grundsätzlich eine „Exit-Strategie“> braucht (Was tu ich, wenn der Speicher nicht ausreicht?), sollte> sonnenklar sein.
Das ist eine Binsenweisheit, die auch ohne dynamischen Speicher gilt,
z.B. beim Empfang serieller Daten ohne Hardware-Flußkontrolle.
Ralf schrieb:> Ich kann nur sagen, dass definitiv nicht nur beim Start alloziert> wird, sondern munter rauf und runter und überall und sowieso.
Keine guten Voraussetzungen.
> Dann brauch ich aber die Allozierung gleich gar nicht. Aus meiner Sicht> ist die dynamische Speicherverwaltung ja genau für den Fall, dass man> den Speicherbedarf vorab nicht kennt, da.
Ja und? Immer in Zweierpotenzen allozieren leistet das doch. Blöcke nur
einer festen Größe leisten das auch, weil Du ja mehrere allozieren
kannst. Du darfst Dich nur nicht drauf verlassen, daß die Blöcke "am
Stück" im Speicher liegen.
> Guter Hinweis. Ich nehme an, die Laufzeit des Allokators ergibt sich> (abgesehen natürlich von der Implementierung selbst) dadurch, dass nach> einem passenden Block gesucht werden muss?
Nein, sondern viel schlimmer daraus, daß nach einiger Allozierung und
Deallozierung oftmals auch Speicher wieder kontinuierlich frei ist, der
aber aus mehreren kleinen freigegebenen Einzelblöcken besteht. Das muß
dann auch wieder zusammengefaßt werden, und das kann länger dauern als
die normale Allokation.
Nop schrieb:> Das ist somit auch leicht testbar: fährt das Gerät hoch oder nicht?
Das genügt dir als Test für die im laufenden Betrieb zu verarbeitenden
(zufällig eintreffenden) Daten?
Das kannst du mit malloc() auch haben: fährt ja sowieso hoch dann. :-)
Das Problem beim Vorallozieren ist doch, dass du in so einem Falle nicht
weißt, ob deine Vorallozierung auch wirklich reichen wird für alle im
Betrieb auftretenden Fälle.
Ich vermute, wir werden uns da nicht einig. Macht nichts, müssen wir
auch nicht.
Nop schrieb:> Nein, sondern viel schlimmer daraus, daß nach einiger Allozierung und> Deallozierung oftmals auch Speicher wieder kontinuierlich frei ist, der> aber aus mehreren kleinen freigegebenen Einzelblöcken besteht. Das muß> dann auch wieder zusammengefaßt werden, und das kann länger dauern als> die normale Allokation.
Das Zusammenfassen macht man natürlich beim free(), nicht erst beim
nächsten malloc().
Jörg W. schrieb:> Das genügt dir als Test für die im laufenden Betrieb zu verarbeitenden> (zufällig eintreffenden) Daten?
Davon war nicht die Rede, und daß mehr Daten eintreffen als Speicher da
ist, kannst Du mit wilder Allokation ebenfalls nicht verhindern. Die
entsprechende Robustheit dagegen muß man unabhängig davon sowieso
implementieren, so daß sie in diesem Zusammenhang irrelevant ist.
Jörg W. schrieb:> Das Zusammenfassen macht man natürlich beim free(), nicht erst beim> nächsten malloc().
Auch gut, aber es ändert nichts an der erheblichen Laufzeitvarianz.
Nop schrieb:> Davon war nicht die Rede, und daß mehr Daten eintreffen als Speicher da> ist, kannst Du mit wilder Allokation ebenfalls nicht verhindern.
Du reduzierst allerdings gegenüber starrer Vorab-Allokation (bei der du
N Slots für Maximalgröße M allozieren musst) den „Verschnitt“, falls die
tatsächlich anfallenden Größen sehr unterschiedlich sind. Damit passt
insgesamt mit hoher Wahrscheinlichkeit weniger in den verfügbaren
Speicher als mit einem komplett dynamischen Modell.
Sind sie wiederum alle gleich, dann entsteht auch die immer wieder
herauf beschworene Fragmentierung nicht (aber dann hat malloc()
natürlich im Vergleich zur Vorab-Allozierung mehr Overhead).
Dass es für vollständig dynamischen Datenanfall keine absolute
Sicherheit geben kann, ist sonnenklar – egal, mit welcher Methode man
den Speicher alloziert.
Nop schrieb:> Allerdings hatte ich auch noch zwei weitere Optionen genannt.
Die zweite Option ist letztlich, wofür die "Bins" gut sind.
(Jetzt dämmert's mir auch wieder: man rundet dort halt auf die nächste
2^N-Größe auf.)