Unterscheidet sich die dynamische Speicherzuordnung in gängigen Implementierungen in C und C++?

Lesezeit: 8 Minuten

Benutzeravatar von Kerrek SB
Kerrek SB

Was die jeweiligen Sprachstandards betrifft, bietet C dynamische Speicherallokation nur über die an malloc() Familie, während in C++ die häufigste Form der Zuordnung von durchgeführt wird ::operator new(). Der malloc im C-Stil ist auch in C++ verfügbar, und viele Beispiele für “Babys erste Zuweisung” verwenden ihn als Kernzuweisungsfunktion, aber ich bin gespannt, wie zeitgenössische Compiler die eigentliche Produktion von operator-new implementieren.

Ist es nur eine dünne Hülle herum malloc()oder ist es aufgrund des etwas anderen Speicherallokationsverhaltens eines typischen C++-Programms im Vergleich zu einem typischen C-Programm grundlegend anders implementiert?

[Edit: I believe the main difference is usually described as follows: A C program has fewer, larger, long-lived allocations, while a C++ program has many, small, short-lived allocations. Feel free to chime in if that’s mistaken, but it sounds like one would benefit from taking this into account.]

Für einen Compiler wie GCC wäre es einfach, nur eine einzige Kernzuweisungsimplementierung zu haben und diese für alle relevanten Sprachen zu verwenden, daher frage ich mich, ob es Unterschiede in den Details gibt, die versuchen, die resultierende Zuweisungsleistung in jeder Sprache zu optimieren.


Aktualisieren: Danke für all die tollen Antworten! Es sieht so aus, als ob dies in GCC vollständig gelöst ist ptmallocund die MSVC auch verwendet malloc im Kern. Weiß jemand, wie das MSVC-malloc implementiert wird?

  • (Ich würde mich freuen, etwas über Nicht-GCC-Compiler zu hören, falls jemand zufällig einen Einblick hat.)

    – Kerrek SB

    16. September 2011 um 11:42 Uhr


Benutzeravatar von NPE
NPE

Hier ist die Implementierung, die von verwendet wird g++ 4.6.1:

_GLIBCXX_WEAK_DEFINITION void *
operator new (std::size_t sz) throw (std::bad_alloc)
{
  void *p;

  /* malloc (0) is unpredictable; avoid it.  */
  if (sz == 0)
    sz = 1;
  p = (void *) malloc (sz);
  while (p == 0)
    {
      new_handler handler = __new_handler;
      if (! handler)
#ifdef __EXCEPTIONS
        throw bad_alloc();
#else
        std::abort();
#endif
      handler ();
      p = (void *) malloc (sz);
    }

  return p;
}

Das findet sich in libstdc++-v3/libsupc++/new_op.cc innerhalb der g++ Quelldistribution.

Wie Sie sehen können, ist es eine ziemlich dünne Hülle malloc.

bearbeiten Auf vielen Systemen ist es möglich, das Verhalten von zu optimieren mallocin der Regel per Anruf mallopt oder Umgebungsvariablen setzen. Hier ist eine Artikel Erörterung einiger Funktionen, die unter Linux verfügbar sind.

Laut Wikipedia, glibc Die Versionen 2.3+ verwenden eine modifizierte Version des aufgerufenen Zuordners ptmallocdie selbst eine Ableitung von ist dlmalloc entworfen von DougLea. Interessanterweise in einem Artikel über dlmalloc Doug Lea gibt die folgende Perspektive (Hervorhebung von mir):

Ich habe die erste Version des Zuordners geschrieben, nachdem ich einige C++-Programme geschrieben hatte, die sich fast ausschließlich auf die Zuweisung von dynamischem Speicher stützten. Ich stellte fest, dass sie viel langsamer und/oder mit viel mehr Gesamtspeicherverbrauch liefen, als ich erwartet hatte. Dies lag an Eigenschaften der Speicherzuweisungen auf den Systemen, auf denen ich lief (hauptsächlich die damals aktuellen Versionen von SunOs und BSD). Um dem entgegenzuwirken, habe ich zunächst eine Reihe von Allokatoren für spezielle Zwecke in C++ geschrieben, normalerweise durch Überladen von operator new für verschiedene Klassen. Einige davon werden in einem Artikel über C++-Zuweisungstechniken beschrieben, der in den C++-Berichtsartikel von 1989 Einige Speicherzuweisungstechniken für Containerklassen übernommen wurde.

Ich erkannte jedoch bald, dass das Erstellen eines speziellen Allokators für jede neue Klasse, die dazu neigte, dynamisch zugewiesen und stark genutzt zu werden, keine gute Strategie war, wenn ich eine Art Allzweck-Programmierungsunterstützungsklassen erstellte, die ich damals schrieb. (Von 1986 bis 1991 war ich der Hauptautor von libg++ , der GNU-C++-Bibliothek.) Eine umfassendere Lösung wurde benötigt – um zu schreiben eine Zuweisung, die unter normalen C++- und C-Lasten gut genug war so dass Programmierer nicht in Versuchung geraten, Zuweisungen für spezielle Zwecke zu schreiben, außer unter ganz besonderen Bedingungen.

Dieser Artikel enthält eine Beschreibung einiger der wichtigsten Entwurfsziele, Algorithmen und Implementierungsüberlegungen für diese Zuweisung.

  • Vielen Dank! Wie interessant (und auch etwas enttäuschend). Vielleicht malloc() ist bereits so gut, dass es die meisten Situationen mit zufriedenstellender Leistung bewältigt.

    – Kerrek SB

    16. September 2011 um 11:32 Uhr

  • @Kerrek SB: Um ehrlich zu sein, meine Intuition deutet nicht darauf hin, dass C und C++ sehr unterschiedliche Zuordnungsmuster haben. Allerdings kann meine Intuition natürlich auch falsch sein. 🙂

    – NPE

    16. September 2011 um 11:44 Uhr

  • @Kerrek: Leistung?? Wenn Sie nur die “Unschärfe” entfernen, falls malloc nicht fehlgeschlagen ist, wird der obige Code einfach … p = malloc(…); if(!p) {} return p; Betrachten Sie die Zeit, die das Betriebssystem für die Ausführung von malloc aufwendet. Ich sehe nicht, wie if(!p) (in Assembler nur eine JZ-Anweisung) die Leistung ändern kann!

    – Emilio Garavaglia

    16. September 2011 um 11:58 Uhr


  • @Emilio: Ich bezog mich auf die internen Zuordnungsalgorithmen malloc() Verwendet. Siehst du, das könnte sein malloc() ist so eingestellt, dass es für C-Typ-Zuweisungen gut funktioniert, aber tatsächlich viele Fragmentierungen oder Seitenfehler oder so weiter verursachen würde, wenn es von C++-Containern verwendet wird (nur als Beispiel). Meine Frage ist also, ob oder wie man das Wissen über die typischen Bedürfnisse einer Sprache nutzen könnte, um das Design des Kernzuordners zu verbessern. (Und anscheinend tut GCC das nicht.)

    – Kerrek SB

    16. September 2011 um 12:01 Uhr

  • @kerrek: Entschuldigung für die Missverständnisse. Tatsächlich wird malloc selbst durch einen Aufruf an eine Betriebssystem-API (normalerweise HeapAlloc unter Windows) implementiert, die wiederum auf einem Heap arbeiten möchte, dessen Zuweisungsrichtlinie angegeben wird, wenn der Heap selbst über CreateHeap erstellt wird. Sie müssen zwei Ebenen nach unten gehen, um diesen Punkt zu erreichen. Ich hoffe, dass die CRT-Bibliothek die richtige Richtlinie verwendet, wenn sie von C oder C++ aufgerufen wird.

    – Emilio Garavaglia

    16. September 2011 um 12:07 Uhr

Benutzeravatar von sharptooth
scharfer Zahn

In den meisten Implementierungen operator new() ruft nur an malloc(). Tatsächlich schlägt sogar The Standard dies als Standardstrategie vor. Natürlich können Sie Ihre eigenen implementieren operator newnormalerweise für eine Klasse, wenn Sie eine bessere Leistung wünschen, aber der Standardwert ist normalerweise nur ein Anruf malloc().

  • Wo würde ich eigentlich den Quellcode dafür in GCC finden? Dies ist nicht Teil der Kopfzeilen. Ist es irgendwo in den libstdc++-Quellen?

    – Kerrek SB

    16. September 2011 um 11:17 Uhr

  • @ Kerrek SB: Ein indirekter Beweis ist das free( new char ) scheint ok zu laufen.

    – scharfer Zahn

    16. September 2011 um 11:38 Uhr

  • sehr unartig 🙂 Es ist jedoch merkwürdig, dass zwei ziemlich unterschiedliche Sprachen durch genau dieselben Zuordnungsalgorithmen zufrieden gestellt werden können. Vielleicht spricht das einfach für die Qualität des Designs malloc()

    – Kerrek SB

    16. September 2011 um 11:39 Uhr


  • @Kerrek SB: Tatsächlich malloc() “funktioniert einfach” – ja, es kann für einige spezifische Szenarien sehr langsam sein, aber es “funktioniert einfach”. Wenn der Benutzer also nicht genau weiß, was er geändert haben möchte, muss er eine Standardeinstellung verwenden, und das ist malloc().

    – scharfer Zahn

    16. September 2011 um 11:45 Uhr

glibc new operator ist ein dünner Wrapper um malloc. Und glibc malloc verwendet unterschiedliche Strategien für unterschiedliche Größenzuordnungsanforderungen. Sie können die Implementierung oder zumindest die Kommentare sehen hier.

Hier ein Auszug aus den Kommentaren in malloc.c:

/*
47   This is not the fastest, most space-conserving, most portable, or
48   most tunable malloc ever written. However it is among the fastest
49   while also being among the most space-conserving, portable and tunable.
50   Consistent balance across these factors results in a good general-purpose
51   allocator for malloc-intensive programs.
52 
53   The main properties of the algorithms are:
54   * For large (>= 512 bytes) requests, it is a pure best-fit allocator,
55     with ties normally decided via FIFO (i.e. least recently used).
56   * For small (<= 64 bytes by default) requests, it is a caching
57     allocator, that maintains pools of quickly recycled chunks.
58   * In between, and for combinations of large and small requests, it does
59     the best it can trying to meet both goals at once.
60   * For very large requests (>= 128KB by default), it relies on system
61     memory mapping facilities, if supported.
*/

Wenn Sie in Visual C++ in a new Ausdruck führt mich zu diesem Snippet in new.cpp:

#include <cstdlib>
#include <new>

_C_LIB_DECL
int __cdecl _callnewh(size_t size) _THROW1(_STD bad_alloc);
_END_C_LIB_DECL

void *__CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc)
        {       // try to allocate size bytes
        void *p;
        while ((p = malloc(size)) == 0)
                if (_callnewh(size) == 0)
                {       // report no memory
                static const std::bad_alloc nomem;
                _RAISE(nomem);
                }

        return (p);
        }

Also VC++ new wickelt auch die malloc() Anruf.

Es geht nicht um die Leistung:
pA = new A hat eine andere Nebenwirkung als pA = (A*)malloc(sizeof(A));

Im zweiten wird der Konstruktor von A nicht aufgerufen. Um zu demselben Effekt zu kommen, sollten Sie dies tun

pA = (A*)malloc(sizeof(A));
new(pA)A();

wo neu ist die “platzierung neu”…

void* operator new(size_t sz, void* place) 
{ return place; }

  • Ich frage nicht nach dem new Ausdruck. Ich bin mir der Kernkonzepte der C++-Sprache bewusst. Ich frage konkret nach ::operator new() (die Standardeinstellung, nicht die Platzierungsfunktion), die die von der verwendete Zuordnungsfunktion ist new Ausdruck.

    – Kerrek SB

    16. September 2011 um 12:05 Uhr


  • Ich frage nicht nach dem new Ausdruck. Ich bin mir der Kernkonzepte der C++-Sprache bewusst. Ich frage konkret nach ::operator new() (die Standardeinstellung, nicht die Platzierungsfunktion), die die von der verwendete Zuordnungsfunktion ist new Ausdruck.

    – Kerrek SB

    16. September 2011 um 12:05 Uhr


1412230cookie-checkUnterscheidet sich die dynamische Speicherzuordnung in gängigen Implementierungen in C und C++?

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy