2014-10-29 4 views
12

Ich habe festgestellt, dass meine Anwendung nicht genügend Arbeitsspeicher hat, als es sollte. Es erstellt viele Byte-Arrays von jeweils mehreren Megabyte. Wenn ich jedoch die Speicherauslastung mit vmmap untersuchte, scheint .NET viel mehr zuzuweisen, als für jeden Puffer erforderlich ist. Um genau zu sein, erstellt .NET bei der Zuweisung eines Puffers von 9 Megabyte einen Heap von 16 Megabyte. Die verbleibenden 7 Megabyte können nicht zum Erstellen eines weiteren Puffers von 9 Megabyte verwendet werden, daher erstellt .NET weitere 16 Megabyte. So verschwendet jeder 9MB Puffer 7MB Adressraum!Large Object Heap Verschwendung

Hier ist ein Beispielprogramm, das eine OutOfMemoryException nach Zuweisung 106 Puffer in 32-Bit-NET 4 wirft:

using System.Collections.Generic; 

namespace CSharpMemoryAllocationTest 
{ 
    class Program 
    { 
     static void Main(string[] args) 
     { 
      var buffers = new List<byte[]>(); 
      for (int i = 0; i < 130; ++i) 
      { 
       buffers.Add(new byte[9 * 1000 * 1024]); 
      } 

     } 
    } 
} 

Beachten Sie, dass die Größe des Arrays 16 erhöhen * 1000 * 1024 und noch zuzuteilen die gleiche Menge an Puffern vor dem Auslaufen des Arbeitsspeichers.

VMMap zeigt dies:

enter image description here

Beachten Sie auch, wie ein es gibt fast 100% Differenz zwischen der Gesamtzahl Größe des verwalteten Heap und die Gesamt Commited Größe. (1737MB vs 946MB).

Gibt es einen zuverlässigen Weg um dieses Problem auf .NET, d. H. Kann ich die Laufzeit in die Zuweisung von nicht mehr als das, was ich wirklich brauche, oder vielleicht viel größere Managed Heaps, die für mehrere zusammenhängende Puffer verwendet werden können?

+0

Ihr Wunsch nach Speicherzuweisungen, die genau passen, was Sie brauchen, wird von einer viel größeren Sorge, die Notwendigkeit, Adressraumfragmentierung zu vermeiden, übertrumpft. Dieses Verhalten ist vollständig von Design, es gibt keine Knöpfe, um es zu optimieren. Altmodisches Problem sowieso, es ist kein Problem auf 64-Bit-Betriebssystemen. –

+0

Kurze Antwort: Nein –

+0

@HansPassant Leider muss dieser Prozess 32-Bit sein. Du sagst also im Grunde, dass ich meinen eigenen Allokator programmieren muss? – Asik

Antwort

3

Die CLR reserviert einen 16MB Chunk auf einmal aus dem Betriebssystem, belegt aber nur aktiv 9MB.

Ich glaube, dass Sie erwarten, dass die 9MB und 9MB auf einem Haufen sind. Die Schwierigkeit besteht darin, dass die Variable nun auf 2 Heaps aufgeteilt ist.

Heap 1 = 9MB + 7MB 
Heap 2 = 2MB 

Das Problem, das wir jetzt haben, ist, wenn das Original 9MB gelöscht wird, haben wir jetzt zwei Haufen wir nicht aufräumen können, da die Inhalte über Haufen geteilt werden.

Um die Leistung zu verbessern, besteht der Ansatz darin, sie in einzelne Heaps zu setzen.

Wenn Sie sich Sorgen über die Speichernutzung machen, seien Sie nicht. Speicherauslastung ist mit .NET nicht schlecht, da es, wenn niemand es benutzt, das Problem ist? Der GC wird irgendwann einspringen, und die Erinnerung wird aufgeräumt. GC wird nur kick entweder

  1. Wenn die CLR hält es für notwendig
  2. Wenn das Betriebssystem die CLR sagt Speicher zurück zu geben
  3. Wenn gezwungen, durch den Code

Aber Speichernutzung, besonders in diesem Beispiel sollte keine Sorge sein. Die Speichernutzung stoppt die CPU-Zyklen. Sonst wäre die CPU hoch und Ihr Prozess (und alle anderen auf Ihrem Rechner) würde viel langsamer laufen, wenn der Speicher ständig aufgeräumt würde.

+1

Es ist erwähnenswert, dass diese Zuordnung alle auf den großen Objekt-Heap gehen, was bedeutet, dass der GC nicht viel tun wird, um diesen Heap aufzuräumen. LOH-Fragmentierung ist in .NET-Anwendungen nicht ungewöhnlich. –

+0

@BrianRasmussen: Absolut! Alles, was größer als 85'000 Bytes ist, wird in die Warteschlange der GC gestellt. –

+0

Speicherauslastung ist schlecht, wenn ich OutOfMemoryException erhalte. GC spielt keine Rolle, weil ich diese Puffer tatsächlich verwende. Ich suche nach Wegen, 40% meines Adressraums nicht zu verschwenden, während ich mehrere ~ 9 MB Puffer reserviere - das ist meine Frage. – Asik

6

Intern weist die CLR Speicher in Segmenten zu. Aus Ihrer Beschreibung klingt es, als wären die 16 MB-Zuweisungen Segmente und Ihre Arrays sind in diesen Bereichen zugeordnet. Der verbleibende Speicherplatz ist reserviert und wird unter normalen Umständen nicht wirklich verschwendet, da er für andere Zuweisungen verwendet wird. Wenn Sie keine Zuordnung haben, die in die verbleibenden Blöcke passt, sind diese im Wesentlichen Overhead.

Da Ihre Arrays mit zusammenhängendem Speicher zugewiesen werden, können Sie nur einen einzelnen dieser Segmente innerhalb eines Segments anpassen und somit den Overhead in diesem Fall.

Die Standardgröße des Segments beträgt 16 MB. Wenn die Zuordnung jedoch größer ist, weist die CLR Segmente zu, die größer sind. Ich kenne die Details nicht, aber z.B. wenn ich 20 MB zuteile, zeigt Wmmap mir 24 MB Segmente an.

Eine Möglichkeit, den Overhead zu reduzieren, besteht darin, Zuordnungen vorzunehmen, die möglichst zu den Segmentgrößen passen. Aber bedenken Sie, dass diese Implementierungsdetails sind und mit jedem CLR-Update geändert werden könnten.

3

Alter Symptom des Buddy-System-Heap-Management-Algorithmus, bei dem Potenzen von 2 verwendet werden, um jeden Block rekursiv in einem binären Baum aufzuteilen. Für 9M ist die nächste Größe 16M. Wenn Sie Ihre Array-Größe auf 8 MB reduziert haben, sinkt die Nutzung um die Hälfte. Kein neues Problem, auch native Programmierer kümmern sich darum.

Der kleine Objektpool (weniger als 85.000 Byte) wird anders verwaltet, aber mit 9 MB befinden sich Ihre Arrays im großen Objektpool. Ab .NET 4.5 nimmt der große Objektspeicher nicht an der Komprimierung teil, große Objekte werden sofort in Generation 2 hochgestuft.

Sie können den Algorithmus nicht erzwingen, aber Sie können Ihren Benutzercode auf jeden Fall erzwingen, indem Sie herausfinden, was zu verwendende Größen, die die binären Segmente am effizientesten füllen.

Wenn Sie Ihren Prozessraum mit 9 MB-Arrays füllen müssen, entweder:

  1. Finde heraus, wie 1MB zu speichern, die Arrays zu 8MB Segmente
  2. schreiben oder verwenden eine segmentierte Array-Klasse, die Abstracts zu reduzieren Ein Array von 1 oder 2 MB Array-Segmenten unter Verwendung einer Indexer-Eigenschaft. So wie Sie ein unbegrenztes Bitfeld oder eine erweiterbare ArrayList erstellen. Eigentlich dachte ich, einer der Container hätte das schon gemacht.
  3. Umzug in 64-Bit-

Reclaiming fragmentierten Teil eines Buddy-System Heap ist eine Optimierung mit logarithmischen Renditen (dh. Sie ungefähr sind sowieso nicht genügend Arbeitsspeicher ausgeführt wird). An einem bestimmten Punkt müssen Sie zu 64-Bit wechseln, ob es praktisch ist oder nicht, es sei denn, Ihre Datengröße ist festgelegt.

+0

Das Erzwingen der Laufzeit hat verschiedene mögliche Bedeutungen. Mein Vorschlag in meiner Antwort ist, Ihre Arrays in Potenzen von 2 in Übereinstimmung mit dem Buddy-Algorithmus zuzuteilen, ansonsten kämpfen Sie nur dagegen. Sie können den Heap-Algorithmus/GC nicht ersetzen, also arbeiten Sie damit. – codenheim

+1

FWIW, 4.5.1 fügt 'System.Runtime.GCSettings.LargeObjectHeapCompactionMode' hinzu, aber das ist nicht besonders relevant, wenn er tatsächlich den Speicher verwendet. – EricLaw

+0

@EricLaw - Oh wirklich? Ich wusste nichts davon. Das ist nicht einmal in meiner neuesten Kopie von _CLR über C# _ dokumentiert. Danke für das Aufzeigen! – codenheim