2017-07-21 3 views
20

Die allgemeine SituationWie kann man reservierten Speicher in C++ bereitstellen?

Eine Anwendung, die extrem intensive sowohl Bandbreite, CPU-Auslastung und GPU Nutzung benötigt etwa 10-15GB pro Sekunde von einer GPU zu einem anderen zu übertragen. Da die DX11-API für den Zugriff auf die GPU verwendet wird, kann das Hochladen auf die GPU nur mit Puffern erfolgen, die für jeden einzelnen Upload eine Zuordnung erfordern. Das Hochladen erfolgt in Blöcken von jeweils 25 MB und 16 Threads schreiben gleichzeitig Puffer in zugeordnete Puffer. Es kann nicht viel getan werden. Die tatsächliche Parallelität der Schreibvorgänge sollte niedriger sein, wenn der folgende Fehler nicht vorhanden wäre.

Es ist eine bullige Workstation mit 3 Pascal GPUs, einem High-End-Haswell-Prozessor und Quad-Channel-RAM. Auf der Hardware kann nicht viel verbessert werden. Es läuft eine Desktop-Version von Windows 10.

Das eigentliche Problem

Sobald ich ~ 50% CPU-Last passieren, etwas in MmPageFault() (im Windows-Kernel, aufgerufen, wenn Zugriff auf den Speicher, die in Ihrer abgebildet wurde Adressraum, wurde aber vom OS noch nicht übernommen) bricht entsetzlich ab, und die restliche 50% CPU-Last wird bei einem Spin-Lock innerhalb MmPageFault() verschwendet. Die CPU wird zu 100% ausgelastet und die Anwendungsleistung verschlechtert sich vollständig.

Ich muss davon ausgehen, dass dies aufgrund der immensen Speichermenge ist, die dem Prozess jede Sekunde zugewiesen werden muss und die auch jedes Mal, wenn der DX11-Puffer nicht zugeordnet wird, vollständig aus dem Prozess entfernt wird. Dementsprechend sind es tatsächlich tausende Anrufe nach MmPageFault() pro Sekunde, die sequentiell als memcpy() sequentiell in den Puffer schreiben. Für jede einzelne nicht festgelegte Seite.

Einer der CPU-Auslastung geht über 50%, die optimistische Spin-Lock im Windows-Kernel schützt das Seitenmanagement vollständig Leistung beeinträchtigt.

Considerations

Der Puffer von dem DX11 Fahrer zugeordnet ist. An der Allokationsstrategie kann nichts geändert werden. Die Verwendung einer anderen Speicher-API und insbesondere die Wiederverwendung ist nicht möglich.

Aufrufe an die DX11-API (Mapping/Unmapping der Puffer) geschieht alles aus einem einzigen Thread. Die eigentlichen Kopiervorgänge erfolgen möglicherweise multi-threaded über mehrere Threads als es virtuelle Prozessoren im System gibt.

Die Verringerung der Speicherbandbreitenanforderungen ist nicht möglich. Es ist eine Echtzeitanwendung. Tatsächlich ist das harte Limit derzeit die PCIe 3.0 16x-Bandbreite der primären GPU. Wenn ich könnte, müsste ich schon weiter drücken.

Das Vermeiden von Multi-Threading-Kopien ist nicht möglich, da es unabhängige Producer-Consumer-Warteschlangen gibt, die nicht trivial zusammengeführt werden können.

Der Spin-Lock-Leistungsabfall scheint so selten zu sein (weil der Anwendungsfall es so weit treibt), dass Sie bei Google kein einziges Ergebnis für den Namen der Spin-Lock-Funktion finden.

Das Upgrade auf eine API, die mehr Kontrolle über die Zuordnungen (Vulkan) bietet, ist in Arbeit, aber es ist nicht als kurzfristige Lösung geeignet. Der Wechsel zu einem besseren Betriebssystem-Kernel ist derzeit aus demselben Grund nicht möglich.

Die Reduzierung der CPU-Auslastung funktioniert auch nicht; Es gibt zu viel Arbeit, die anders als die (normalerweise triviale und kostengünstige) Pufferkopie erledigt werden muss.

Die Frage

Was kann getan werden?

Ich muss die Anzahl der einzelnen Seitenfehler deutlich reduzieren. Ich kenne die Adresse und Größe des Puffers, der meinem Prozess zugeordnet wurde, und ich weiß auch, dass der Speicher noch nicht festgeschrieben wurde.

Wie kann ich sicherstellen, dass der Speicher mit der geringstmöglichen Anzahl von Transaktionen ausgeführt wird?

Exotische Flags für DX11, die eine Aufhebung der Zuweisung der Puffer nach dem Unmapping verhindern würden, Windows-APIs, um Commit in einer einzigen Transaktion zu erzwingen, so ziemlich alles ist willkommen.

Der aktuelle Zustand

// In the processing threads 
{ 
    DX11DeferredContext->Map(..., &buffer) 
    std::memcpy(buffer, source, size); 
    DX11DeferredContext->Unmap(...); 
} 
+1

es klingt wie Sie sind bei etwa 400 M für alle 16 Threads alle zusammen. Ziemlich niedrig. Können Sie überprüfen, ob Sie dies in Ihrer Anwendung nicht überschreiten? Was ist der Speicherverbrauch dort? Ich frage mich, ob Sie ein Speicherleck haben. – Serge

+0

Der Spitzenverbrauch liegt bei 7-8GB, aber das ist normal, wenn man bedenkt, dass die gesamte Verarbeitungspipeline> 1s Pufferung benötigt, um alle Arten von Engpässen auszugleichen. Ja, es ist "nur" 400 MB, 25 mal pro Sekunde. Und es funktioniert gut, bis die Basis-CPU-Auslastung über 50% steigt und die Leistung der Spin Lock plötzlich von praktisch 0 auf ~ 40-50% der vollständigen CPU-Auslastung steigt. Beeinflusst gleichzeitig andere Prozesse im System. – Ext3h

+1

1. Was ist Ihr physischer Speicher? Kannst du alle anderen aktiven Prozesse beenden? 2. Rate # 2, da du den 50% -Schwellenwert siehst, könnte es zu Problemen mit Hyper-Threading kommen. Wie viele physikalische Kerne hast du? 8? Kannst du Hyperthreading deaktivieren? Versuchen Sie, in Ihrem Fall auf einem sauberen Rechner so viele Threads auszuführen, wie es physische CPUs gibt. – Serge

Antwort

11

Current Behelfslösung, vereinfachte Pseudocode:

// During startup 
{ 
    SetProcessWorkingSetSize(GetCurrentProcess(), 2*1024*1024*1024, -1); 
} 
// In the DX11 render loop thread 
{ 
    DX11context->Map(..., &resource) 
    VirtualLock(resource.pData, resource.size); 
    notify(); 
    wait(); 
    DX11context->Unmap(...); 
} 
// In the processing threads 
{ 
    wait(); 
    std::memcpy(buffer, source, size); 
    signal(); 
} 

VirtualLock() zwingt den Kernel sofort den angegebenen Adressbereich mit RAM nach hinten. Der Aufruf der ergänzenden VirtualUnlock()-Funktion ist optional. Dies geschieht implizit (und ohne zusätzliche Kosten), wenn der Adressbereich aus dem Prozess nicht zugeordnet wird. (Wenn explizit genannt wird, kostet es etwa 1/3 der Verriegelungskosten.)

Damit VirtualLock() überhaupt zu arbeiten, SetProcessWorkingSetSize() muss zuerst aufgerufen werden, als die Summe aller Speicher durch VirtualLock() gesperrten Bereiche nicht überschreiten die minimale Arbeitssatzgröße, die für den Prozess konfiguriert ist. Wenn Sie die Mindestgröße für den Arbeitssatz auf einen höheren Wert als den Basisspeicherbedarf Ihres Prozesses festlegen, treten keine Nebenwirkungen auf, es sei denn, Ihr System ist potenziell austauschbar. Ihr Prozess wird immer noch nicht mehr RAM als die tatsächliche Arbeitssatzgröße verbrauchen.


Gerade die Verwendung von VirtualLock(), wenn auch in einzelnen Threads und mit latenten DX11-Kontexten für Map/Unmap Anrufe, hat sofort die Leistungseinbuße von 40 bis 50% sank auf etwas mehr akzeptabel 15%.

die Verwendung eines latenten Zusammenhang Wegwerfen und ausschließlich Auslösung sowohl alle weichen Fehler, sowie die entsprechende de-Zuweisung, wenn auf einem einzigen Thread unmapping, gab die notwendige Leistungssteigerung. Die Gesamtkosten für dieses Spin-Lock betragen nun < 1% der gesamten CPU-Auslastung.


Zusammenfassung?

Wenn Sie weiche Fehler unter Windows erwarten, versuchen Sie, sie alle im selben Thread zu behalten. Eine parallele memcpy selbst ist unproblematisch, in manchen Situationen sogar notwendig, um die Speicherbandbreite voll auszunutzen. Dies ist jedoch nur dann der Fall, wenn der Arbeitsspeicher bereits für RAM reserviert ist. VirtualLock() ist der effizienteste Weg, um dies sicherzustellen.

(Es sei denn, Sie mit einer API wie DirectX arbeiten die Speicher in Ihren Prozess abbildet, sind Sie wahrscheinlich nicht häufig nicht gebundenen Speicher begegnen. Wenn Sie mit Standard-C gerade arbeiten ++ new oder malloc Ihr Gedächtnis wird gepoolt und in Ihrem Prozess zurückgeführt trotzdem, so weiche Fehler sind selten.)

Stellen Sie sicher, dass jede Form von gleichzeitigen Seitenfehlern bei der Arbeit mit Windows vermeiden.

Verwandte Themen