Hallo,
ich schreibe im Moment zahllose kleine Progrämmchen um modernes C++ zu
üben/lernen/verstehen.
Heute steht auf der Speisekarte eigentlich concurrency, dabei bin ich
aber auf die folgende Frage gestoßen.
In einem Tutorial (nicht für modernes c++ sondern für ein Framework,
https://www.juce.com/doc/tutorial_looping_audio_sample_buffer_advanced)
steht die folgende Behauptung:
It is not a good idea to allocate or free memory on the audio thread
Während mir das mit Allokation noch logisch erscheint, erscheint es mir
mit Deallokation doch etwas spanisch? Ich gehe davon aus, dass
Zeitaufwand gemeint ist und die Gefahr besteht, dass der Thread so lange
geblockt wird, dass der Audiopuffer leer läuft.
Mir ist klar, dass es im Zusammenhang mit Threads noch jede Menge andere
Probleme gibt und vor allem, dass meine Fragestellung hier erstmal
höchstens hintergründig mit Threads zu tun hat.
Um das Timing zu untersuchen habe ich folgenden Test geschrieben:
und folgende Timings erhalten:
Nackter Funktionsaufruf: 130 ns
Anzahl double | Timing
10^9 134 ns
10^8 144 ns
10^6 142 ns
10^4 144 ns
Das sieht für mich 1. so aus, als ob Speicher-Deallokation eher schnell
ist, und als ob das Timing im Wesentlichen unabhängig von der Größe ist.
Vor diesem Hintergrund wundere ich mich über die Aussage in dem
Tutorial.
Kann es sein, dass die Deallokation in meiner C++ Umgebung von Natur aus
in einem Hintergrund-Thread erledigt wird? Ist das heute so? Hängt das
vom System ab? Googeln erweckt den Eindruck, dass Deallokation synchron
erfolgt. Wie könnte ich das testen?
Oder ist einfach ein Denkfehler in meinem Test?
Besten Dank
Timm
Edit: Ja ich weiß, pow() ist float, implizite Konversion nach size_t ist
böse, aber ich gebe ja im main() die Größe des vector<> aus, da würde
ich ja sehen, wenns böse ausgegangen wäre.
Edit2: Ja ich weiß, ich hätte in dem Literal auch einfach e verwenden
können, wollte halt mal cmath includen, hat aber ja mit der
Fragestellung nichts zu tun, denke ich.
Timm R. schrieb:> Kann es sein, dass die Deallokation in meiner C++ Umgebung von Natur aus> in einem Hintergrund-Thread erledigt wird?
nein.
> Ist das heute so?
die PCs sind heute schneller, vor 10 Jahren mussten man für Audio
wirklich noch optimieren. Heute kann man das mit einer Scriptsprache
erledigen.
> Hängt das vom System ab? Googeln erweckt den Eindruck, dass Deallokation> synchron erfolgt.
weil es das übliche bei C ist.
> Wie könnte ich das testen?
man könnte einfach in den Quellcode schauen, was bei Free passiert.
Testen muss man dafür nichts.
Peter II schrieb:>>> Wie könnte ich das testen?> man könnte einfach in den Quellcode schauen, was bei Free passiert.> Testen muss man dafür nichts.
Üblicherweise passiert die Freigabe in konstanter Zeit, denn es wird nur
der Typ des Blocks in der Loch/Block-Liste von malloc() von belegtem
Block auf freies Lock umgesetzt - und ggf. mit den beiden angrenzenden
freien Löchern verschmolzen. Das ist alles ...
Wilhelm M. schrieb:> Üblicherweise passiert die Freigabe in konstanter Zeit, denn es wird nur> der Typ des Blocks in der Loch/Block-Liste von malloc() von belegtem> Block auf freies Lock umgesetzt - und ggf. mit den beiden angrenzenden> freien Löchern verschmolzen. Das ist alles ...
so einfach ist das nicht. Es muss auch Speicher an BS zurückgegeben
werden, wenn z.b. eine Page komplett nicht mehr verwendet wird. Es gibt
ja die Speicherverwaltung in der libc und dann noch die vom
Betriebssystem.
Peter II schrieb:> Wilhelm M. schrieb:>> Üblicherweise passiert die Freigabe in konstanter Zeit, denn es wird nur>> der Typ des Blocks in der Loch/Block-Liste von malloc() von belegtem>> Block auf freies Lock umgesetzt - und ggf. mit den beiden angrenzenden>> freien Löchern verschmolzen. Das ist alles ...>> so einfach ist das nicht. Es muss auch Speicher an BS zurückgegeben> werden, wenn z.b. eine Page komplett nicht mehr verwendet wird. Es gibt> ja die Speicherverwaltung in der libc und dann noch die vom> Betriebssystem.
ein sbrk() mit negativem Offset findet aber bei den meisten
Realisierungen nicht statt.
Hallo,
herzlichen Dank an euch!
Eigentlich hatte ich mit dem Forum schon abgeschlossen, wegen der vielen
vulgären und dahingerotzten Antworten. Aber die letzten Threads mit
Fragen zu C++ waren wirklich der Hammer. Bares Geld wert.
Auch dieser hier wieder. Wirklich sehr nett und hilfsbereit von euch!
Vlg
Timm
Edit: Als kleine Randnotiz:
Ich habe das Prog auch mal auf einem Rechner von 2007 (Core 2 Duo, 2
GHz) laufen lassen. Der reine function call liegt dann bei 185 ns und
die Deallokation ist auch dort konstant und liegt bei 215 ns. Also mit
netto 30 ns doch etwas langsamer, aber immer noch irre schnell.
Timm R. schrieb:> Hallo,>> herzlichen Dank an euch!
Wie man in den Wald hinein ruft ...
> Eigentlich hatte ich mit dem Forum schon abgeschlossen, wegen der vielen> vulgären und dahingerotzten Antworten.
Das stimmt ... leider. Manchmal z.K.
> Edit: Als kleine Randnotiz:> Ich habe das Prog auch mal auf einem Rechner von 2007 (Core 2 Duo, 2> GHz) laufen lassen. Der reine function call liegt dann bei 185 ns und> die Deallokation ist auch dort konstant und liegt bei 215 ns. Also mit> netto 30 ns doch etwas langsamer, aber immer noch irre schnell.
Wie gesagt: das hat mit C++ nichts zu tun und liegt an der Realisierung
von malloc(). Man kann sich ja ein malloc() auch selbst schreiben und
das mit LD_PRELOAD dem eigenen Programm trotz libc unterschieden. Sehr
instruktiv ... da lernt man dann, wie eine in-place-Liste funktioniert
und sbrk(). Interessanter daran ist natürlich die Allokationsseite.
Wilhelm M. schrieb:> ein sbrk() mit negativem Offset findet aber bei den meisten> Realisierungen nicht statt.
Sofern sbrk() denn genutzt wird. Teilweise wird sowas ja auch per mmap()
gemacht, oder es wird abhängig von der Größe des allokierten Blocks
zwischen den beiden gewählt.
vielleicht ist der hinweis ja in der tatsache begründet, dass dieses
"juce" ein cross-platform zeugs ist, das auch auf android laufen können
soll.
das kann grade auf älteren/billigen endgeräten erstaunlich unperformant
sein, imho..
Rolf M. schrieb:> Wilhelm M. schrieb:>> ein sbrk() mit negativem Offset findet aber bei den meisten>> Realisierungen nicht statt.>> Sofern sbrk() denn genutzt wird. Teilweise wird sowas ja auch per mmap()> gemacht, oder es wird abhängig von der Größe des allokierten Blocks> zwischen den beiden gewählt.
Ja, meine Antwort bezog sich ja auch auf den Post davor. Denn ein
Schrumpfen ist ja meistens deswegen nicht möglich, weil noch belegte
Blöcke dort existieren.
Wenn man es denn genau wissen will:
http://man7.org/linux/man-pages/man3/mallopt.3.html
Timm R. schrieb:
...
> Während mir das mit Allokation noch logisch erscheint, erscheint es mir> mit Deallokation doch etwas spanisch? Ich gehe davon aus, dass> Zeitaufwand gemeint ist und die Gefahr besteht, dass der Thread so lange> geblockt wird, dass der Audiopuffer leer läuft.
...
Der Heap ist ja thread safe implementiert, verwendet also typischerweise
locks. Deswegen kann es sein, dass du auf eine Allokation in einem
anderen Thread warten mußt. Darum ist im allgemeinen(!) Deallokieren
genauso komplex wie Allokieren.
Wenn man z.B. einen eigenen GUI thread (typische qt-Anwendung) hat...
> gibt den Speicher ja auch nicht frei, das macht>
1
unique_ptr::reset
ich finde an dieser Stelle müsste man mal ganz laut SCH**** rufen
dürfen.
Mist verdammter.
Du hast absolut recht! Ich hätte reset() verwenden müssen.
Timing:
10^9 3672 ms
10^8 361 ms
10^6 3147 µs
10^4 29 µs
das geht natürlich nicht in einem Audio-Thread.
Tut mir leid, dass ich euch so auf eine falsche Fährte gelockt habe,
Sack und Asche sind bestellt.
vlg
Timm
Hallo Peter,
Peter II schrieb:> Timm R. schrieb:>> das geht natürlich nicht in einem Audio-Thread.>> und warum nicht? Und willst du jedes Byte einzeln Freigeben?
die Warnung in dem Tutorial bezieht sich natürlich auf größere Blöcke
und natürlich schon auch auf nicht brandaktuelle Rechner und auf manuell
in einem Callback beschriebene Audi-Buffer. Auf meinem 2007er Rechner
kommen die 10^6 doubles schon auf 12 ms! Eine halbe Ewigkeit und das bei
gerade mal roundabout 8 Megabyte. Das ist schon weit weit jenseits
dessen, was ich – zugegeben naiver Weise – erwartet hätte.
"das geht natürlich nicht in einem Audio-Thread"
sollte bedeuten:
Dann ist die Warnung natürlich schon berechtigt.
vlg
Timm
Kannst du die ganze Allokations Problematik nicht umgehen, indem du 1x
einen großen Speicherbereich anforderst und als Ring Puffer nutzt? Das
sollte für Audio/DSP Anwendungen meistens ganz gut passen...
Hallo,
Dr. Sommer schrieb:> Kannst du die ganze Allokations Problematik nicht umgehen, indem du 1x> einen großen Speicherbereich anforderst und als Ring Puffer nutzt? Das> sollte für Audio/DSP Anwendungen meistens ganz gut passen...
natürlich kann man die Problematik umgehen, mir ging es darum überhaupt
erstmal zu verstehen, dass es eine Problematik gibt. Durch meine falsche
Anwendung von release() sah es ja so aus, als ob es das Problem gar
nicht gibt.
vlg
Timm
Timm R. schrieb:> 12 ms! Eine halbe Ewigkeit und das bei> gerade mal roundabout 8 Megabyte.
bei 8 Kanälen und 96khz und 24bit kann man in 8MB schon 3 Sekunden
speichern. Das ist noch ausreichend reserve.
Peter,
Jules bezieht sich mit seinem Hinweis nicht auf solche Applikationen,
wie Du sie jetzt wohl meinst. Die Applikationen auf die Jules sich
bezieht haben Audio Buffer von allerhöchstens einigen tausend Samples,
in der Regel aber doch eher 512 oder deutlich weniger. Diese Buffer
werden dann, in den Fällen, die Jules mit seiner Warnung meinte, aus
anderen größeren Buffern gefüllt, in der Regel Filebuffer.
Da ist es definitiv gut zu wissen, dass Speicherfreigabe extremst
langsam ist.
vlg
Timm
Timm R. schrieb:> Jules bezieht sich mit seinem Hinweis nicht auf solche Applikationen,> wie Du sie jetzt wohl meinst. Die Applikationen auf die Jules sich> bezieht haben Audio Buffer von allerhöchstens einigen tausend Samples,> in der Regel aber doch eher 512 oder deutlich weniger. Diese Buffer> werden dann, in den Fällen, die Jules mit seiner Warnung meinte, aus> anderen größeren Buffern gefüllt, in der Regel Filebuffer.
die paar Byte kann man auf den Stack legen und schon ist das Problem
auch weg.
// Hier müssen auch beide Speicher wieder freigegeben werden.
9
// 1) Der Vektor gibt seinen Speicher an den Heap zurück.
10
// 2) Der unique_ptr gibt den Speicher für das vector Objekt frei.
11
a.reset();
12
}
Du kannst ja mal probieren, wie sich das Timing mit folgendem Code
ändert.
1
std::vector<double>a(std::pow(10,8),42.42);
2
3
voidtest(void)
4
{
5
// Dass der Vektor hier seinen Speicher freigibt ist nicht sichergestellt. Clear löscht nur die Elemente aber nicht die Kapazität und shrink_to_fit ist nicht bindend, muss also den Speicher nicht verkleinern.
Hallo,
M.K. B. schrieb:> void test(void)> {> // Dass der Vektor hier seinen Speicher freigibt ist nicht> sichergestellt. Clear löscht nur die Elemente aber nicht die Kapazität> und shrink_to_fit ist nicht bindend, muss also den Speicher nicht> verkleinern.> a.clear();> a.shrink_to_fit();> }
Danke für den Hinweis, stimmt schon. Allerdings werden kleine
Speicherbereiche ja schnell wieder freigegeben. Das Timing wird in
meiner Fragestellung wohl durch die Elemente dominiert.
Aber das war, wie gesagt, auch nur ein Modell um dem allgemeinen
Phänomen auf den Grund zu gehen. Es ist ja gar nicht gesagt, dass bei
einer realen Fragestellung auch ein Vector zum Einsatz kommt.
Aber: Ich habe Deinen Vorschlag natürlich getestet:
Mittelwerte aus 212 Programmdurchläufen:
.reset(): 3.70025*10^8 ns
.clear.shrink_to_fit: 3.70126*10^8 ns
Ist also sogar langsamer, aber der Unterschied ist zu gering, um
überhaupt drüber nachzudenken.
Trotzdem Danke!!
Herzliche Grüße
Timm
Bei den Locks vom Betriebssystem kann es auch passieren, dass ein
High-Priority-Thread erst auf das Fertigwerden eines mit extra-niedriger
Priorität laufenden Thread warten muss (Priority-Inversion).
Ich bin mir fast sicher, dass man ein Beispiel konstruieren könnte, in
dem auch mit C++ der Audio-Thread nie wieder aus einem versuchtem
malloc/free rauskommt, wenn man z.B. den anderen allozierenden Thread
mit signal() dichtbombt.
Schau' einfach mal, wie sich das Timing ändert, wenn du nebenbei noch so
50-60 Threads aufmachst, die einfach nur Speicher allozieren und wieder
freigeben. Da kann der Audio-Thread nur hoffen, dass das Mutex fair ist.
In Java schafft es schon ein einzelner Thread erwiesenermaßen, eine
Android-VM vollkommen (über Minuten hinweg) lahmzulegen, wenn er einfach
immer nur "new" aufruft (was aber auch am GC liegt).
In C++ könnte man den theoretisch überleben.