Forum: PC-Programmierung Zeitaufwand für unique_ptr.release()


von Timm R. (Firma: privatfrickler.de) (treinisch)


Lesenswert?

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:
1
#include <iostream>
2
#include <iomanip>
3
#include <chrono>
4
#include <vector>
5
#include <cmath>
6
7
template<typename TimeT = std::chrono::milliseconds>
8
struct measure
9
{
10
    template<typename F, typename ...Args>
11
    static typename TimeT::rep execution(F&& func, Args&&... args)
12
    {
13
        auto start = std::chrono::steady_clock::now();
14
        std::forward<decltype(func)>(func)(std::forward<Args>(args)...);
15
        auto duration = std::chrono::duration_cast< TimeT>
16
        (std::chrono::steady_clock::now() - start);
17
        return duration.count();
18
    }
19
};
20
21
auto a = std::make_unique<std::vector<double>>(std::pow(10,8), 42.42);
22
23
void test(void)
24
{
25
    a.release();
26
}
27
28
int main(int argc, const char * argv[]) {
29
    std::cout << std::setprecision(0) << std::scientific << 1.0*a->size() << std::endl;
30
    std::cout << measure<std::chrono::nanoseconds>::execution(test) << std::endl;
31
    return 0;
32
}

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.

: Bearbeitet durch User
von Peter II (Gast)


Lesenswert?

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.

von Wilhelm M. (wimalopaan)


Lesenswert?

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

von Peter II (Gast)


Lesenswert?

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.

von Wilhelm M. (wimalopaan)


Lesenswert?

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.

: Bearbeitet durch User
von Timm R. (Firma: privatfrickler.de) (treinisch)


Lesenswert?

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.

von Wilhelm M. (wimalopaan)


Lesenswert?

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.

von Rolf M. (rmagnus)


Lesenswert?

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.

von dunno.. (Gast)


Lesenswert?

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

von Wilhelm M. (wimalopaan)


Lesenswert?

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

von Thomas F. (Gast)


Lesenswert?

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

von Roger S. (edge)


Lesenswert?

1
unique_ptr::release
gibt den Speicher ja auch nicht frei, das macht
1
unique_ptr::reset

Cheers, Roger

von Timm R. (Firma: privatfrickler.de) (treinisch)


Lesenswert?

Hallo Roger,

Roger S. schrieb:
>
1
unique_ptr::release
> 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

: Bearbeitet durch User
von Peter II (Gast)


Lesenswert?

Timm R. schrieb:
> das geht natürlich nicht in einem Audio-Thread.

und warum nicht? Und willst du jedes Byte einzeln Freigeben?

von Timm R. (Firma: privatfrickler.de) (treinisch)


Lesenswert?

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

von Dr. Sommer (Gast)


Lesenswert?

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

von Timm R. (Firma: privatfrickler.de) (treinisch)


Lesenswert?

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

von Peter II (Gast)


Lesenswert?

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.

von Timm R. (Firma: privatfrickler.de) (treinisch)


Lesenswert?

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

von Peter II (Gast)


Lesenswert?

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.

von Timm R. (Firma: privatfrickler.de) (treinisch)


Lesenswert?

Peter

es geht um die Filebuffer, die man natürlich bei Gelegenheit auch mal 
freigeben möchte.

Gruß
Timm

von M.K. B. (mkbit)


Lesenswert?

Ich wollte noch darauf hinweisen, dass in deinem Programm gleich zwei 
Allokationen/Deallokationen passieren.
1
// Was passiert hier:
2
// 1) Speicher für ein vector Objekt wird auf dem Heap alloziert.
3
// 2) Der Vector holt sich Speicher für sein Array vom Heap.
4
auto a = std::make_unique<std::vector<double>>(std::pow(10,8), 42.42);
5
6
void test(void)
7
{
8
// 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
void test(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.
6
    a.clear();
7
    a.shrink_to_fit();
8
}

von Timm R. (Firma: privatfrickler.de) (treinisch)


Lesenswert?

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

von zer0 (Gast)


Lesenswert?

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.

Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.