Gewinnt man performance, wenn man innerhalb seines eigenen Prozesses den Speicher selbst verwaltet?



  • Normalerweise ist es ja so, wenn ein Prozess mehr Speicher benötigt, dann fordert er diesen beim Betriebssystem an und dieses teilt dem Prozess dann den notwendigen Speicher zu, sofern noch freier Speicher vorhanden ist.

    Das Anfordern von Speicher bedeutet aber einen Kontextswitch, denn nun muss das OS ja ran.
    D.h. die Registereinträge des Prozesses werden gesichert und das OS übernimmt, sucht den entsprechenden Speicher, legt dessen Adresse und die Länge in irgendwelchen Registern ab und kopiert dann die Registerdaten des Prozesses wieder zurück in die Register der CPU, so, dass dieser Prozess mit dem nun zur Verfügung gestellten Speicher weitermachen kann.

    Dies führt mich zu folgender Überlegung:

    Wäre es nicht performanter, wenn der Prozess zu Beginn mehr Speicher vom OS einmalig anfordern würde, als er eigentlich benötigen würde und dann diesen zur Verfügung gestellten Speicher innerhalb seines Prozesses selbst verwalten würde?
    Denn dadurch würde man ja die Kontextswitches zum OS runter zum größten Teil einsparen und nur wenn dann auch dieser Speicher nicht mehr ausreicht, müsste das OS wieder ran, so meine Überlegung.

    Ist diese Überlegung richtig?
    Und falls ja, wird das so gemacht und wenn ja, wo?



  • Speicherverwaltung schrieb:

    Ist diese Überlegung richtig?
    Und falls ja, wird das so gemacht und wenn ja, wo?

    Ja. Quasi jede Laufzeitumgebung einer Programmiersprache macht das so. Für Quellcode siehe malloc/free, besonders einfache Implementierungen (bei Programmstart wird der gesamte Speicher besorgt) bei Microcontrollern.

    Dein Trick wird inwischen gelegentlich sogar noch eine Stufe weiter getrieben: malloc/free muss bei mehreren Threads irgendwie dafür sorgen, daß nicht zwei Zugriffe zugleich sind und sich gegenseitig kaputtschreiben, typischerweise mit Semaphoren/Mutexen/Spinlocks oder so, also die anderen Threads, die auch wollen, müssen sich schlafen legen, solange man selber dran ist. Darum bekommt jeder Thread seinen eigenen Spielspeicher für lokales malloc/free und muss nur ganz selten das globale malloc/free benutzen, so muss fast nie mehr jemand wegen gleichzeitiger Speicheranforderung schlafen.



  • Keine Ahnung wie das bei Linux ist, aber zumindest Windows stellt Funktionen die genau das machen, also Speicher in grösseren Stücken vom Kernel holen (VirtualAlloc) und dann selbst verwalten, auch schon selbst zur Verfügung.
    Siehe z.B. HeapAlloc.

    Ab Vista bzw. XP mit aktiviertem "low fragmentation heap" macht HeapAlloc AFAIK auch den von volkard beschriebenen Thread-Trick.

    Speicherverwaltung schrieb:

    die Kontextswitches zum OS runter

    Denkfehler: OS != Kernel.
    Jedes OS besteht aus mächtig vielen "Userland" Komponenten, zusätzlich zum Kernel. Und diese aufzurufen ist nicht so teuer.
    Und ein Kernel-Call ist auch kein Kontextswitch. Lies mal da http://en.wikipedia.org/wiki/System_call und da http://x86.renejeschke.de/html/file_module_x86_id_313.html



  • Wäre es nicht performanter, wenn der Prozess zu Beginn mehr Speicher vom OS einmalig anfordern würde, als er eigentlich benötigen würde und dann diesen zur Verfügung gestellten Speicher innerhalb seines Prozesses selbst verwalten würde?

    Da hätte ich Angst dass das in einem Freibrief für globale Variablen endet. 😞



  • @Bitte ein Bit
    Wie kommst du denn auf die Idee?



  • Speicherverwaltung schrieb:

    Dies führt mich zu folgender Überlegung:

    Wäre es nicht performanter, wenn der Prozess zu Beginn mehr Speicher vom OS einmalig anfordern würde, als er eigentlich benötigen würde und dann diesen zur Verfügung gestellten Speicher innerhalb seines Prozesses selbst verwalten würde?

    Das ist performanter.

    Speicherverwaltung schrieb:

    Ist diese Überlegung richtig?
    Und falls ja, wird das so gemacht und wenn ja, wo?

    Zum Beispiel in std::vector, wo Du zwischen size() und capacity() unterscheiden kannst.

    Das Ganze nennt sich Memory-Pool und ist deutlich performanter für Speicherblöcke konstanter Größen. Du kannst hierfür operator new und delete für Deine Klasse überschreiben, um die Speicherverwaltung einer Klasse selbst zu übernehmen.

    Alternativ kannst Du Placement New verwenden, um eine Instanz einer Klasse in einem vorher bereits alloziierten Speicherbereich zu erzeugen und zum freigeben lediglich den Destruktor aufrufen, statt das Objekt mit delete zu zerstören.
    Damit vermeidest Du operator new() bzw. operator delete() aufzurufen, entsprechend wird kein Speicher vom OS angeforder (außer dass Du halt zu Beginn den Speicher als Block für x Instanzen angefordert hast).



  • @Xin

    Xin schrieb:

    ...
    Damit vermeidest Du operator new() bzw. operator delete() aufzurufen, entsprechend wird kein Speicher vom OS angeforder (außer dass Du halt zu Beginn den Speicher als Block für x Instanzen angefordert hast).

    Das klingt jetzt fast wie ein Aufruf zu "premature optimization".
    Weil es so klingt als würde es viel bringen.
    Tut es aber i.A. nicht (mehr). Weil die meisten C++ Implementierungen bereits sehr flotte Allokatoren haben.
    Sei's weil sie die flotten (Usermode-)Allokatoren des OS verwenden (MSVC), oder weil sie was eigenes flottes mitbringen.

    new() ist lange nicht mehr so teuer wie es vor 15~20 Jahren war.

    ps: vector statt einzelner Allokationen bringt natürlich immer noch viel, weil dabei der Bookkeeping-Overhead minimal ist. Das würde ich auch nicht als "premature optimization" bezeichnen, sofern man keine Verränkungen anstellen muss um den vector zu verwenden. Ich meine damit nur den placement-new Teil, den man halt falsch verstehen kann. Sobald man nämlich anfängt eigene spezialisierte Allokatoren zu basteln, ist die Chance relativ hoch dass es nichts bis fast nichts bringt, oder man u.U. sogar langsamer ist als einfach new+delete.



  • Wie kommst du denn auf die Idee?

    Ich befürchte zuviel Erfahrung mit C Programmierer.

    Habe schon erlebt das man auf einem Mikrokontroller eine eigene Speicherverwaltung anlegte, was auch ok war. Aber dann kan der Gedanke: Was für den Mikrokontroller gut ist, muss doch auch für den PC gut sein...



  • hustbaer schrieb:

    new() ist lange nicht mehr so teuer wie es vor 15~20 Jahren war.

    Kommt auf das Betriebssystem an. kürzlich war ja Heartbleed ein wenig höher geschwappt und der Bug entstand durch eigene Speicherallokation, weil die Entwickler den langsamen allocator von OpenBSD umgehen wollten. Die Leute von OpenBSD haben sich darüber natürlich aufgeregt. Deren malloc ist nämlich so lahm, weil sie es extra gegen solche Angriffe abgesichert haben.

    Also ja: auf OpenBSD ist malloc richtig lahm.



  • hustbaer schrieb:

    new() ist lange nicht mehr so teuer wie es vor 15~20 Jahren war.

    Hallo Hustbaer,

    da meine Denke auch in dieser Zeit geprägt wurde und ich mit Entwicklungsarbeit nur noch indirekt zu tun habe:

    Was ist denn mit dem Einfluss der Objektgröße? (Hierzu habe ich hier noch gar nichts in der laufenden Diskussion gelesen.)
    Bei der Codierung von größeren Optimierungsproblemen war das üblich, sich größere Blöcke vom Betriebssystem zu holen und dann durch eigene Methoden und in Objektgröße aufgeteilt lokal zu verwalten. Damit war dann auch hohe Speichereffizienz gesichert, weil vom Betriebssystem gleich in konfigurierbarer Blockgröße allokiert wurde, obwohl die einzelnen Objekte oft nur aus einer Handvoll Zeigern und Parameter bestanden.

    Ciao, Allesquatsch



  • Allesquatsch schrieb:

    Was ist denn mit dem Einfluss der Objektgröße? (Hierzu habe ich hier noch gar nichts in der laufenden Diskussion gelesen.)

    Deswegen bietet es sich an, daß new anders implementiert ist als malloc. malloc sollte effizient bei größeren Blöcken sein und kann durchaus abkacken, wenn man lauter 16 Byte große Mini-Dinge holt. new sollte die Mini-Dinge auch können zwecks polymorphen Objekten mit virtuellen Methoden in einem Zeiger-Container.



  • Ich glaube wir stoßen hier auf eine Gretchenfrage.

    Wie sinnvoll ist es die Speicherverwaltung des Betriebssystems zu umgehen bzw. auszutricksen und durch eine andere zu ersetzen? Ist es sinnvoll eine bisher funktionierende Verwaltung durch eine eigene zu ersetzen, welche potenziell viele Fehler enthalten kann und definitiv weniger getestet ist? Und ist man bereit eine Speicherverwaltung auf Geschwindigkeit zu optimieren aber dafür im Gegenzug Sicherheitsabfragen (Overflow, Underflow,...) zu vermeiden?

    Bei der Codierung von größeren Optimierungsproblemen war das üblich, sich größere Blöcke vom Betriebssystem zu holen und dann durch eigene Methoden und in Objektgröße aufgeteilt lokal zu verwalten.

    Heutzutage würde ich eher Algorithmus mit besserer Komplexitätsklasse suchen. 😉



  • Bitte ein Bit schrieb:

    welche potenziell viele Fehler enthalten kann und definitiv weniger getestet ist?

    Angst essen Seele auf.



  • Nein!

    Ich bin da nur einfach faul. Denn Faulheit ist eine Tugend. Und nur faule Menschen entwickeln Dinge, die einem das Leben erleichtern.

    Warum sollte ich mir also die Arbeit machen, eine neue Speicherverwaltung aufsetzen und diese gründlich testen, wenn ich eine bereits habe, welche vielleicht hier und da vielleicht nicht optimal ist, aber dafür bereits vielfach benutzt/getestet wurde?

    Warum das Rad neu erfinden?



  • Bitte ein Bit schrieb:

    Nein!
    Ich bin da nur einfach faul. Denn Faulheit ist eine Tugend. Und nur faule Menschen entwickeln Dinge, die einem das Leben erleichtern.

    Naja, jedenfalls entwickeln sie keine small object allocators, die einem das Leben erleichtern. 😃



  • Bitte ein Bit schrieb:

    Warum das Rad neu erfinden?

    Weil ein Autorad für die Straße als Zahnrad im Getriebe einfach nicht wirklich optimal funktioniert und auch nicht, als Antriebs-Zahnrad beim Fahrrad, die idealerweise eher oval sind, dass die unterschiedlichen Kräfte beim Antritt ausgleichen kann. Ein Rad für eine bogenförmige Fahrbahn ist übrigens quadratisch, quasi die Quadratur des Kreises.

    Darum kann man operator new überladen. Nicht jedes Problem lässt sich optimal mit einem einzigen runden Rad lösen.
    Außer man ist faul. Dann kann an der Software aber auch was faul sein.



  • Warum legt ihr dauernd soviel Speicher mit new an?
    Schon mal was vom Stack gehört und davon, dass man in C++ nicht wie in Java programmiert?
    Habt ihr eure Programme mit nem Profiler untersucht und festgestellt, dass new mehr als 5% der Laufzeit ausmacht? Wenn ja, was macht das Programm?


  • Mod

    (k)einer? schrieb:

    Warum legt ihr dauernd soviel Speicher mit new an?

    Weil wir wissen, was im Hintergrund passiert, wenn wir einen Container der Standardbibliothek benutzen.



  • (k)einer? schrieb:

    Warum legt ihr dauernd soviel Speicher mit new an?
    Schon mal was vom Stack gehört und davon, dass man in C++ nicht wie in Java programmiert?

    Die Objekte auf dem Stack sind aber böse und holen sich ihren internen Speicher letztendlich vom Freispeicher, was jetzt? Naja, vielleicht benutzt du ja nie Strings oder Container, dann hast du das Problem nicht, aber könntest auch genauso gut in COBOL programmieren.



  • otze schrieb:

    hustbaer schrieb:

    new() ist lange nicht mehr so teuer wie es vor 15~20 Jahren war.

    Kommt auf das Betriebssystem an.

    Ja, sicherlich. Und auf die C++ Implementierung. Denn die kann ja, wenn sie fürchtet dass der Allokator vom OS zu langsam ist, was eigenes machen.

    kürzlich war ja Heartbleed ein wenig höher geschwappt und der Bug entstand durch eigene Speicherallokation, weil die Entwickler den langsamen allocator von OpenBSD umgehen wollten. Die Leute von OpenBSD haben sich darüber natürlich aufgeregt. Deren malloc ist nämlich so lahm, weil sie es extra gegen solche Angriffe abgesichert haben.

    Nö. Der Exploit würde auf anderen Betriebssystemen auch funktionieren wenn kein eigener Allocator verwendet würde.

    Die Ursache ist nicht der eigene Allocator, sondern dass der Programmierer einfach einen Range-Check "vergessen" hat. Was alleine schonmal total irre ist - jemand der sowas "vergessen" kann sollte gefälligst die Finger von Projekten lassen wo es um irgendwas geht.

    Bzw. gibt es noch grundlegendere Probleme:

    1. Warum wurde überhaupt eine (redundante) Längenangabe mitgeschickt?
      Die Gesamtlänge des Pakets ist sowieso bekannt -- nur so kann der "Packetierungs" Layer überhaupt passend Speicher für das Paket anfordern. Der weiss nämlich noch nix davon wie bestimmte Paket-Typen zu interpretieren sind, und daher auch nicht dass da ne Payload-Länge in dem Keepalive Paket drin steht.
      => Die Payload-Länge ist redundant, die hätte man sich schön aus der Paket-Länge berechnen können.

    2. Warum überhaupt nen Payload mit variabler Länge mitschicken? Bzw. warum überhaupt irgend einen Payload?


Anmelden zum Antworten