2017-07-21 53 views
20

ogólnej sytuacjiJak skwapliwie zatwierdzić przydzieloną pamięć w C++?

Aplikacja jest niezwykle intensywny zarówno przepustowość, wykorzystanie procesora oraz wykorzystanie GPU musi przenieść około 10-15GB na sekundę z jednego do drugiego GPU. Używa API DX11 do uzyskania dostępu do GPU, więc przesyłanie do GPU może się odbywać tylko w przypadku buforów, które wymagają mapowania dla każdego pojedynczego przesłania. Przesyłanie odbywa się w porcjach po 25 MB na raz, a 16 wątków zapisuje bufory jednocześnie do odwzorowanych buforów. Niewiele można z tym zrobić. Rzeczywisty poziom współbieżności zapisów powinien być mniejszy, gdyby nie następujący błąd.

To zgrabna stacja robocza z 3 procesorami graficznymi Pascal, wysokiej klasy procesorem Haswell i czterokanałową pamięcią RAM. Niewiele można poprawić na sprzęcie. To prowadzenie edycja pulpitu Windows 10.

rzeczywisty problem

Gdy mijam ~ 50% obciążenia procesora, coś w MmPageFault() (wewnątrz jądra systemu Windows, o nazwie przy dostępie do pamięci, które zostały przypisane do Twojego przestrzeń adresowa, ale jeszcze nie została zatwierdzona przez system operacyjny), strasznie się psuje, a pozostałe 50% obciążenia procesora marnuje się na spin-lock wewnątrz MmPageFault(). Procesor zostaje w 100% wykorzystany, a wydajność aplikacji całkowicie spada.

Muszę założyć, że wynika to z ogromnej ilości pamięci, która musi zostać przydzielona do procesu w każdej sekundzie, a która jest całkowicie niezmapowana z procesu za każdym razem, gdy bufor DX11 nie jest mapowany. Odpowiednio, to jest rzeczywiście tysiące połączeń do MmPageFault() na sekundę, dzieje się sekwencyjnie jako memcpy() jest zapisywanie sekwencyjnie do bufora. Dla każdej napotkanej niezaakceptowanej strony.

Jedno obciążenie procesora przekracza 50%, optymistyczny spin-lock w jądrze systemu Windows chroni zarządzanie stroną całkowicie pogarsza wydajność.

Rozważania

Bufor jest alokowany przez kierowcę DX11. Nic nie może zostać poprawione o strategii alokacji. Używanie innego API pamięci, a zwłaszcza ponowne użycie, nie jest możliwe.

Połączenia z interfejsem API DX11 (mapowanie/usuwanie map buforów) wszystko dzieje się z jednego wątku. Faktyczne operacje kopiowania mogą potencjalnie przebiegać wielowątkowo w większej liczbie wątków niż w systemie są procesory wirtualne.

Zmniejszenie wymagań dotyczących przepustowości pamięci nie jest możliwe. Jest to aplikacja działająca w czasie rzeczywistym. W rzeczywistości twardym limitem jest obecnie przepustowość PCIe 3.0 16x podstawowego procesora graficznego. Gdybym mógł, już musiałbym naciskać dalej.

Unikanie kopii wielowątkowych nie jest możliwe, ponieważ istnieją niezależne kolejki producent-konsument, które nie mogą być łączone w sposób trywialny.

Spadek wydajności spin-lock wydaje się tak rzadki (ponieważ przypadek użycia przesuwa go tak daleko), że w Google nie można znaleźć pojedynczego wyniku dla nazwy funkcji spin-lock.

Trwa aktualizowanie do interfejsu API, który zapewnia większą kontrolę nad odwzorowaniami (Vulkan), ale nie jest to rozwiązanie krótkoterminowe. Przejście na lepsze jądro systemu operacyjnego nie jest obecnie możliwe z tego samego powodu.

Zmniejszenie obciążenia procesora również nie działa; jest zbyt wiele pracy, którą należy wykonać poza (zwykle banalną i niedrogą) kopią bufora.

Pytanie

Co można zrobić?

Potrzebuję znacznie zmniejszyć liczbę pojedynczych błędów stron. Znam adres i rozmiar bufora, który został zmapowany do mojego procesu, a także wiem, że pamięć nie została jeszcze zatwierdzona.

Jak mogę zagwarantować, że pamięć zostanie zatwierdzona przy jak najmniejszej ilości transakcji?

Egzotyczne flagi dla DX11, które zapobiegają de-alokacji buforów po ich usunięciu, interfejsy API systemu Windows wymuszają zatwierdzanie w pojedynczej transakcji, prawie wszystko jest mile widziane.

Obecny stan

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

To brzmi jak masz około 400 M dla wszystkich 16 wątków razem. Całkiem nisko. Czy możesz zweryfikować, że nie przekroczyłeś tego w swojej aplikacji? Jakie jest tam zużycie pamięci wybierania? Zastanawiam się, czy masz wyciek pamięci. – Serge

+0

Maksymalne zużycie wynosi około 7-8 GB, ale to normalne, biorąc pod uwagę, że w sumie cały proces przetwarzania potrzebuje> 1 s buforowania, aby zrekompensować wszystkie wąskie gardła. Tak, to "tylko" 400 MB, 25 razy na sekundę. I działa dobrze, dopóki podstawowe obciążenie procesora nie przekroczy 50%, a wydajność blokady spinu nagle wzrasta z praktycznie 0 do 40-50% całkowitego wykorzystania procesora. Wpływa również na inne procesy w systemie w tym samym czasie. – Ext3h

+1

1. Jaka jest twoja pamięć fizyczna? czy możesz zabić wszystkie inne aktywne procesy? 2. zgadnij # 2, ponieważ widzisz próg 50%, możesz dostać się do pewnych problemów z hyperthreading. Ile masz fizycznych rdzeni? 8? Czy możesz wyłączyć hyperthreading? Spróbuj uruchomić tyle wątków, ile fizycznych cpusów znajduje się w twoim przypadku na czystej maszynie. – Serge

Odpowiedz

11

Obecny obejście, uproszczony pseudokod:

// 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() zmusza jądro natychmiast kopię określony zakres adresów z pamięci RAM. Wywołanie uzupełniającej funkcji VirtualUnlock() jest opcjonalne, dzieje się to niejawnie (i bez dodatkowych kosztów), gdy zakres adresów nie jest odwzorowywany z procesu. (Jeśli wywołana jawnie, kosztuje około 1/3rd kosztów blokującego.)

Aby VirtualLock() do pracy w ogóle, SetProcessWorkingSetSize() musi być wywołana po pierwsze, jako suma wszystkich regionach pamięci zablokowanej przez VirtualLock() nie może przekraczać minimalny rozmiar zestawu roboczego skonfigurowany dla procesu. Ustawienie "minimalnego" rozmiaru zestawu roboczego na coś wyższego niż podstawowy ślad pamięci twojego procesu nie wywołuje żadnych skutków ubocznych, chyba że twój system faktycznie się zamienia, twój proces nadal nie zużyje więcej pamięci RAM niż rzeczywisty rozmiar zestawu roboczego.


Wystarczy użycie VirtualLock(), choć w poszczególnych wątków i stosując odroczone konteksty DX11 dla Map/Unmap rozmów, nie od razu zmniejszyć karę wydajności od 40-50% do nieco bardziej akceptowalne 15%.

Odrzucanie zastosowanie odroczony kontekście, wyłącznie wywołując zarówno miękkość wady, jak również odpowiednie de alokacji przy mapowań o pojedynczej nici, otrzymano niezbędny wzrost wydajności. Całkowity koszt tego spin-lock jest obecnie niższy niż < 1% całkowitego zużycia procesora.


Podsumowanie?

Gdy spodziewasz się błędów miękkich w systemie Windows, spróbuj, jak możesz, aby wszystkie były w tym samym wątku. Wykonywanie równoległego samplera memcpy jest bezproblemowe, w niektórych sytuacjach nawet konieczne do pełnego wykorzystania przepustowości pamięci. Jest to jednak możliwe tylko wtedy, gdy pamięć jest już zatwierdzona w pamięci RAM. VirtualLock() to najbardziej efektywny sposób, aby to zapewnić.

(chyba, że ​​pracujesz z API jak DirectX, który mapuje pamięć do procesu, jest mało prawdopodobne, aby spotkać pamięć niezakończonej często. Jeśli tylko pracuje ze standardowym C++ new lub malloc pamięć jest zebrane i poddane recyklingowi wewnątrz procesu w każdym razie, tak miękkie usterki są rzadkie.)

Pamiętaj, aby unikać wszelkich form równoczesnych błędów stron podczas pracy z systemem Windows.