mmap () vs. bloki odczytu

185

Pracuję nad programem, który będzie przetwarzał pliki o rozmiarze potencjalnie 100 GB lub większym. Pliki zawierają zestawy rekordów o zmiennej długości. Mam pierwszą implementację uruchomioną i teraz dążę do poprawy wydajności, szczególnie w zakresie wydajniejszego wykonywania operacji we / wy, ponieważ plik wejściowy jest skanowany wiele razy.

Czy istnieje ogólna zasada używania mmap()versus czytania w blokach za pośrednictwem biblioteki C ++ fstream? Chciałbym wczytać duże bloki z dysku do bufora, przetworzyć pełne rekordy z bufora, a następnie przeczytać więcej.

mmap()Kod może potencjalnie uzyskać bardzo brudny od mmap„d bloki muszą leżeć na stronę rozmiarze granice (moim rozumieniu) i zapisuje potencjalnie jak przez granice strona. Używając fstreams, mogę po prostu szukać początku rekordu i zacząć czytać ponownie, ponieważ nie ograniczamy się do czytania bloków, które leżą na granicach wielkości strony.

Jak mogę wybrać między tymi dwiema opcjami bez wcześniejszego spisania pełnej implementacji? Wszelkie zasady praktyczne (np. mmap()2x szybsze) lub proste testy?

jbl
źródło
1
To ciekawa lektura: medium.com/@sasha_f/… W eksperymentach mmap()jest 2-6 razy szybsza niż przy użyciu syscalls, np read().
mplattner

Odpowiedzi:

208

Próbowałem znaleźć ostatnie słowo na temat wydajności mmap / read w Linuksie i natrafiłem na fajny post ( link ) na liście mailingowej jądra Linuksa. Jest od 2000 roku, więc od tego czasu wprowadzono wiele ulepszeń we / wy i pamięci wirtualnej w jądrze, ale ładnie wyjaśnia powód, dla którego mmaplub readmoże być szybszy lub wolniejszy.

  • Wezwanie do mmapma większy narzut niż read(podobnie jak epollma większy narzut niż poll, który ma większy narzut niż read). Zmiana odwzorowań pamięci wirtualnej jest dość kosztowną operacją na niektórych procesorach z tych samych powodów, dla których przełączanie między różnymi procesami jest kosztowne.
  • System IO może już korzystać z pamięci podręcznej dysku, więc jeśli czytasz plik, trafisz w pamięć podręczną lub przegapisz ją bez względu na to, jakiej metody używasz.

Jednak,

  • Mapy pamięci są na ogół szybsze w przypadku losowego dostępu, szczególnie jeśli wzorce dostępu są rzadkie i nieprzewidywalne.
  • Mapy pamięci pozwalają w dalszym ciągu używać stron z pamięci podręcznej, dopóki nie skończysz. Oznacza to, że jeśli używasz pliku intensywnie przez długi czas, a następnie zamknij go i ponownie otwórz, strony nadal będą buforowane. Dzięki readplik mógł zostać usunięty z pamięci podręcznej przed wiekami. Nie dotyczy to korzystania z pliku i natychmiastowego jego odrzucenia. (Jeśli próbujesz mlockprzechodzić między stronami tylko po to, by przechowywać je w pamięci podręcznej, próbujesz przechytrzyć pamięć podręczną dysku, a tego rodzaju oszustwo rzadko poprawia wydajność systemu).
  • Bezpośredni odczyt pliku jest bardzo prosty i szybki.

Dyskusja mmap / read przypomina mi dwie inne dyskusje dotyczące wydajności:

  • Niektórzy programiści Java byli zszokowani odkryciem, że nieblokujące operacje we / wy są często wolniejsze niż blokowanie operacji we / wy, co ma idealny sens, jeśli wiadomo, że nieblokujące operacje we / wy wymagają większej liczby wywołań systemowych.

  • Niektórzy inni programiści sieci byli zszokowani, gdy dowiedzieli się, że epollczęsto jest wolniejszy niż poll, co ma sens, jeśli wiesz, że zarządzanie epollwymaga wykonania większej liczby połączeń systemowych.

Wniosek: używaj map pamięci, jeśli masz dostęp do danych losowo, trzymaj je przez długi czas lub jeśli wiesz, że możesz je udostępnić innym procesom ( MAP_SHAREDnie jest to bardzo interesujące, jeśli nie ma rzeczywistego udostępniania). Odczytuj pliki normalnie, jeśli uzyskujesz dostęp do danych sekwencyjnie lub odrzucasz je po odczytaniu. A jeśli którakolwiek z metod sprawia, że ​​Twój program jest mniej złożony, zrób to . W wielu rzeczywistych przypadkach nie ma pewnego sposobu, aby pokazać, że jest on szybszy bez przetestowania rzeczywistej aplikacji, a NIE testu.

(Przepraszam, że potrzebuję tego pytania, ale szukałem odpowiedzi, a to pytanie wciąż pojawiało się u góry wyników Google).

Dietrich Epp
źródło
Należy pamiętać, że korzystanie z porad opartych na sprzęcie i oprogramowaniu z 2000 roku bez przetestowania ich dzisiaj byłoby bardzo podejrzane. Ponadto, chociaż wiele faktów na temat mmapvs read()w tym wątku jest nadal prawdą, jak to miało miejsce w przeszłości, ogólnej wydajności nie można tak naprawdę ustalić, dodając zalety i wady, a jedynie testując na konkretnej konfiguracji sprzętowej. Na przykład można dyskutować, że „Wywołanie mmap ma narzut większy niż odczyt” - tak mmapmusi dodać mapowania do tabeli stron procesu, ale readmusi skopiować wszystkie odczytane bajty z jądra do przestrzeni użytkownika.
BeeOnRope
Rezultat jest taki, że na moim (nowoczesnym Intelie, około 2018 roku) mmapkoszty są niższe niż w readprzypadku odczytów większych niż rozmiar strony (4 KiB). Teraz jest prawdą, że jeśli chcesz mieć dostęp do danych rzadko i losowo, mmapto naprawdę, naprawdę dobrze - ale odwrotność nie jest konieczna prawda: mmapmoże nadal być najlepsza dla dostępu sekwencyjnego.
BeeOnRope
1
@BeeOnRope: Możesz być sceptyczny wobec porad opartych na sprzęcie i oprogramowaniu z 2000 roku, ale ja jestem jeszcze bardziej sceptyczny wobec testów porównawczych, które nie zapewniają metodologii i danych. Jeśli chcesz zrobić przypadek, który mmapjest szybszy, spodziewam się zobaczyć co najmniej cały aparat testowy (kod źródłowy) z tabelarycznymi wynikami oraz numer modelu procesora.
Dietrich Epp
@BeeOnRope: Należy również pamiętać, że podczas testowania takich fragmentów systemu pamięci mikrodrukowanie może być bardzo zwodnicze, ponieważ opróżnianie TLB może negatywnie wpływać na wydajność pozostałej części programu, a ten wpływ nie pojawi się, jeśli mierzysz tylko samą mapę.
Dietrich Epp
2
@DietrichEpp - tak, dobrze poznam efekty TLB. Pamiętaj, że mmapnie opróżnia TLB, z wyjątkiem nietypowych okoliczności (ale munmapmoże). Moje testy obejmowały zarówno znaki mikrodruku (w tym munmap), jak i „w aplikacji” działające w rzeczywistym przypadku użycia. Oczywiście moja aplikacja nie jest taka sama jak Twoja, więc ludzie powinni przetestować lokalnie. Nie jest nawet jasne, mmapczy faworyzuje go mikro-test porównawczy: read()również uzyskuje duży wzrost, ponieważ bufor docelowy po stronie użytkownika zwykle pozostaje w L1, co może się nie zdarzyć w większej aplikacji. Więc tak, „to skomplikowane”.
BeeOnRope
47

Głównym kosztem wydajności będzie I / O dysku. „mmap ()” jest z pewnością szybsze niż istream, ale różnica może nie być zauważalna, ponieważ dyskowe operacje we / wy zdominują czasy działania.

Próbowałem fragmentu kodu Bena Collinsa (patrz wyżej / poniżej), aby przetestować jego twierdzenie, że „mmap () jest znacznie szybszy”) i nie znalazłem mierzalnej różnicy. Zobacz moje komentarze do jego odpowiedzi.

Z pewnością nie zalecałbym osobno mapowania każdego rekordu po kolei, chyba że twoje „rekordy” są ogromne - byłoby to strasznie powolne, wymagające 2 wywołań systemowych dla każdego rekordu i prawdopodobnie utraty strony z pamięci podręcznej pamięci dyskowej .... .

W twoim przypadku myślę, że mmap (), istream i niskopoziomowe wywołania open () / read () będą mniej więcej takie same. W takich przypadkach polecam mmap ():

  1. Plik ma dostęp losowy (nie sekwencyjny) ORAZ
  2. całość wygodnie mieści się w pamięci LUB w pliku znajduje się lokalizacja odniesienia, dzięki czemu można zamapować określone strony i odwzorować inne strony. W ten sposób system operacyjny maksymalnie wykorzystuje dostępną pamięć RAM.
  3. LUB jeśli wiele procesów odczytuje / pracuje nad tym samym plikiem, to mmap () jest fantastyczny, ponieważ wszystkie procesy współużytkują te same fizyczne strony.

(btw - Kocham mmap () / MapViewOfFile ()).

Tim Cooper
źródło
Dobra uwaga na temat losowego dostępu: może to być jedna z rzeczy, które napędzają moją percepcję.
Ben Collins
1
Nie powiedziałbym, że plik musi wygodnie pasować do pamięci, tylko do przestrzeni adresowej. W systemach 64-bitowych nie powinno być powodu, aby nie mapować dużych plików. System operacyjny wie, jak sobie z tym poradzić; jest to ta sama logika, co przy zamianie, ale w tym przypadku nie wymaga dodatkowej przestrzeni wymiany na dysku.
MvG
@MvG: Czy rozumiesz sens wejścia / wyjścia dysku? Jeśli plik pasuje do przestrzeni adresowej, ale nie do pamięci i masz dostęp losowy, możesz mieć dostęp do każdego rekordu wymagającego przesunięcia głowy dysku i poszukiwania lub operacji strony SSD, co byłoby katastrofą dla wydajności.
Tim Cooper
3
Aspekt dysku we / wy powinien być niezależny od metody dostępu. Jeśli masz naprawdę losowy dostęp do plików większych niż RAM, zarówno mmap, jak i seek + read są poważnie związane z dyskiem. W przeciwnym razie oba skorzystają z pamięci podręcznej. Nie widzę wielkości pliku w porównaniu do wielkości pamięci jako mocnego argumentu w obu kierunkach. Z drugiej strony rozmiar pliku a przestrzeń adresowa jest bardzo silnym argumentem, szczególnie dla naprawdę losowego dostępu.
MvG
Moja pierwotna odpowiedź brzmiała i ma następujący punkt: „całość wygodnie mieści się w pamięci LUB w pliku znajduje się lokalizacja odniesienia”. Drugi punkt dotyczy tego, co mówisz.
Tim Cooper
43

mmap jest znacznie szybszy. Możesz napisać prosty test porównawczy, aby to sobie udowodnić:

char data[0x1000];
std::ifstream in("file.bin");

while (in)
{
  in.read(data, 0x1000);
  // do something with data
}

przeciw:

const int file_size=something;
const int page_size=0x1000;
int off=0;
void *data;

int fd = open("filename.bin", O_RDONLY);

while (off < file_size)
{
  data = mmap(NULL, page_size, PROT_READ, 0, fd, off);
  // do stuff with data
  munmap(data, page_size);
  off += page_size;
}

Oczywiście pomijam szczegóły (np. Jak ustalić, kiedy dojdziesz do końca pliku page_size, na przykład , jeśli plik nie jest wielokrotnością ), ale tak naprawdę nie powinno być o wiele bardziej skomplikowane niż to .

Jeśli możesz, możesz spróbować rozbić dane na wiele plików, które mogą być mmap () - edytowane w całości zamiast w części (znacznie prościej).

Kilka miesięcy temu miałem na wpół upakowaną implementację klasy stream-window mmap () ed dla boost_iostreams, ale nikogo to nie obchodziło i zajęty byłem innymi rzeczami. Niestety kilka tygodni temu usunąłem archiwum starych niedokończonych projektów i była to jedna z ofiar :-(

Aktualizacja : Powinienem również dodać zastrzeżenie, że ten test porównawczy będzie wyglądał zupełnie inaczej w systemie Windows, ponieważ Microsoft zaimplementował sprytną pamięć podręczną plików, która robi większość tego, co zrobiłbyś z mmap. To znaczy, dla często używanych plików, możesz po prostu zrobić std :: ifstream.read () i byłoby to tak szybkie jak mmap, ponieważ pamięć podręczna plików wykonałaby już dla ciebie mapowanie pamięci i jest przezroczysta.

Ostatnia aktualizacja : Słuchajcie, ludzie: na wielu różnych kombinacjach platform systemu operacyjnego i bibliotek standardowych oraz dysków i hierarchii pamięci nie mogę powiedzieć z całą pewnością, że wywołanie systemowe mmap, postrzegane jako czarna skrzynka, zawsze zawsze będzie znacznie szybsze niż read. To nie było dokładnie moim zamiarem, nawet jeśli moje słowa można by tak interpretować. Ostatecznie chodziło mi o to, że operacje we / wy mapowane w pamięci są generalnie szybsze niż operacje we / wy oparte na bajtach; to wciąż prawda . Jeśli eksperymentalnie okaże się, że nie ma między nimi żadnej różnicy, jedynym uzasadnieniem, które wydaje mi się rozsądne, jest to, że twoja platforma implementuje mapowanie pamięci pod osłonami w sposób korzystny dla wydajności wywołańread. Jedynym sposobem, aby być absolutnie pewnym, że używasz mapowanego na pamięć we / wy w przenośny sposób, jest użycie mmap. Jeśli nie zależy Ci na przenośności i możesz polegać na szczególnych cechach docelowych platform, użycie readmoże być odpowiednie bez poświęcania wymiernej wydajności.

Edytuj, aby wyczyścić listę odpowiedzi: @jbl:

ciekawa jest mmapa okna przesuwnego. Czy możesz powiedzieć coś więcej na ten temat?

Jasne - pisałem bibliotekę C ++ dla Git (libgit ++, jeśli wolisz) i napotkałem podobny problem: Musiałem być w stanie otwierać duże (bardzo duże) pliki i nie mieć wydajności, aby być totalnym psem (jak by to było z std::fstream).

Boost::Iostreamsma już źródło mapowane_pliku, ale problem polegał na tym, że pingował mmapcałe pliki, co ogranicza cię do 2 ^ (wordsize). Na komputerach 32-bitowych 4 GB nie jest wystarczająco duże. Nie jest nierozsądne oczekiwać, że .packpliki w Git będą znacznie większe niż te, więc musiałem czytać plik w częściach bez uciekania się do zwykłego we / wy pliku. Pod osłoną Boost::Iostreamswdrożyłem Źródło, które jest mniej więcej kolejnym spojrzeniem na interakcje między std::streambufi std::istream. Możesz także wypróbować podobne podejście, po prostu dziedzicząc std::filebufdo mapped_filebufi podobnie, dziedzicząc std::fstreamdo a mapped_fstream. Trudno jest dobrze zrozumieć interakcję między nimi dwoma. Boost::Iostreams wykonał dla ciebie część pracy, a także zapewnia zaczepy do filtrów i łańcuchów, więc pomyślałem, że bardziej użyteczne byłoby zaimplementowanie go w ten sposób.

Ben Collins
źródło
3
RE: pamięć podręczna plików mmaped w systemie Windows. Dokładnie: gdy buforowanie plików jest włączone, pamięć jądra mapuje plik, który czytasz wewnętrznie, odczytuje ten bufor i kopiuje go z powrotem do procesu. To tak, jakbyś sam zmapował pamięć z wyjątkiem dodatkowego kroku kopiowania.
Chris Smith,
6
Nie chcę się nie zgadzać z przyjętą odpowiedzią, ale uważam, że ta odpowiedź jest błędna. Postępowałem zgodnie z twoją sugestią i wypróbowałem kod na 64-bitowej maszynie z Linuksem, a mmap () nie był szybszy niż implementacja STL. Teoretycznie nie spodziewałbym się, że „mmap ()” będzie szybszy (lub wolniejszy).
Tim Cooper
3
@ Tim Cooper: ten wątek może Cię zainteresować ( markmail.org/message/… ). Zwróć uwagę na dwie rzeczy: mmap nie jest odpowiednio zoptymalizowany w Linuksie, a aby uzyskać najlepsze wyniki, należy również użyć madvise w ich teście.
Ben Collins,
9
Drogi Ben: Przeczytałem ten link. Jeśli „mmap ()” nie jest szybszy w systemie Linux, a MapViewOfFile () nie jest szybszy w systemie Windows, to czy możesz twierdzić, że „mmap jest znacznie szybszy”? Ponadto z powodów teoretycznych uważam, że mmap () nie jest szybszy dla odczytów sekwencyjnych - czy masz jakieś wytłumaczenie inaczej?
Tim Cooper,
11
Ben, po co zawracać sobie głowę mmap()kartoteką strony na raz? Jeśli a size_tjest wystarczająco pojemny, aby pomieścić rozmiar pliku (bardzo prawdopodobne w systemach 64-bitowych), to tylko mmap()cały plik w jednym wywołaniu.
Steve Emmerson
39

Istnieje już wiele dobrych odpowiedzi, które obejmują wiele istotnych punktów, więc dodam tylko kilka problemów, których nie widziałem bezpośrednio powyżej. Oznacza to, że ta odpowiedź nie powinna być uważana za kompleksowy za i przeciw, ale raczej jako dodatek do innych odpowiedzi tutaj.

mmap wydaje się magią

Przyjmując przypadek, w którym plik jest już w pełni buforowany 1 jako linia bazowa 2 , mmapmoże wydawać się magią :

  1. mmap wymaga tylko 1 wywołania systemowego (potencjalnie) odwzorowania całego pliku, po czym nie są już potrzebne żadne wywołania systemowe.
  2. mmap nie wymaga kopiowania danych pliku z jądra do przestrzeni użytkownika.
  3. mmapumożliwia dostęp do pliku „jako pamięć”, w tym przetwarzanie go za pomocą wszelkich zaawansowanych sztuczek, które można wykonać w stosunku do pamięci, takich jak auto-wektoryzacja kompilatora, elementy wewnętrzne SIMD , pobieranie wstępne, zoptymalizowane procedury analizy w pamięci, OpenMP itp.

W przypadku, gdy plik znajduje się już w pamięci podręcznej, wydaje się niemożliwe do pobicia: wystarczy uzyskać bezpośredni dostęp do pamięci podręcznej strony jądra jako pamięci i nie może ona być szybsza.

Cóż, może.

mmap nie jest tak naprawdę magią, ponieważ ...

mmap nadal działa na stronie

Podstawowym ukrytym kosztem mmapvs read(2)(który jest naprawdę porównywalnym syscall na poziomie systemu operacyjnego do odczytu bloków ) jest to mmap, że musisz wykonać „trochę pracy” dla każdej strony 4K w przestrzeni użytkownika, nawet jeśli może być ukryta przez mechanizm błędu strony.

Na przykład typowa implementacja, która jest po prostu mmapcałym plikiem, musi zostać uszkodzona, więc 100 GB / 4K = 25 milionów błędów, aby odczytać plik 100 GB. Będą to drobne błędy , ale 25 miliardów błędów stron wciąż nie będzie super szybkich. Koszt drobnej usterki w najlepszym przypadku to prawdopodobnie setki nanosów.

mmap w dużej mierze opiera się na wydajności TLB

Teraz możesz przejść MAP_POPULATEdo, mmapaby powiedzieć mu, aby skonfigurował wszystkie tabele stron przed powrotem, więc nie powinno być żadnych błędów strony podczas uzyskiwania dostępu do niej. Problem polega na tym, że wczytuje on cały plik do pamięci RAM, który wybuchnie, jeśli spróbujesz zmapować plik 100 GB - ale zignorujmy go na razie 3 . Jądro musi wykonać pracę na stronie, aby skonfigurować te tabele stron (pokazuje się jako czas jądra). To powoduje, że jest to duży koszt w mmappodejściu i jest proporcjonalny do rozmiaru pliku (tzn. Nie staje się stosunkowo mniej ważny w miarę wzrostu rozmiaru pliku) 4 .

Wreszcie, nawet w przypadku dostępu do przestrzeni użytkownika takie mapowanie nie jest całkowicie darmowe (w porównaniu z dużymi buforami pamięci niepochodzącymi z pliku mmap) - nawet po skonfigurowaniu tabel stron, każdy dostęp do nowej strony będzie, koncepcyjnie, popełnisz błąd TLB. Ponieważ mmaping plik oznacza użycie pamięci podręcznej strony i jego stron 4K, ponosisz ten koszt 25 milionów razy za plik 100 GB.

Teraz rzeczywisty koszt tych braków TLB zależy w dużej mierze od co najmniej następujących aspektów twojego sprzętu: (a) ile masz TLB 4K i jak działa reszta buforowania tłumaczenia (b) jak dobrze radzi sobie wstępne pobieranie sprzętu z TLB - np. czy pobieranie wstępne może uruchomić spacer strony? (c) jak szybki i równoległy jest sprzęt do chodzenia po stronach. W nowoczesnych procesorach Intel x86 o wysokiej wydajności sprzęt do chodzenia po stronach jest ogólnie bardzo silny: istnieją co najmniej 2 równoległe chodzenia po stronach, chodzenie po stronie może odbywać się jednocześnie z dalszym wykonywaniem, a wstępne pobieranie sprzętu może uruchamiać chodzenie po stronie. Tak więc wpływ TLB na ładowanie odczytu strumieniowego jest dość niski - i takie obciążenie często działa podobnie bez względu na rozmiar strony. Jednak inny sprzęt jest zwykle znacznie gorszy!

read () pozwala uniknąć tych pułapek

read()Syscall, czyli to, co zazwyczaj leży u podstaw „blok czytać” Połączenia typu oferowanych na przykład w C, C ++ i innych języków ma jedną podstawową wadę, że każdy jest dobrze świadomy:

  • Każde read()wywołanie N bajtów musi skopiować N bajtów z jądra do przestrzeni użytkownika.

Z drugiej strony pozwala to uniknąć większości powyższych kosztów - nie trzeba mapować 25 milionów stron 4K w przestrzeń użytkownika. Zwykle możeszmalloc buforować mały bufor w przestrzeni użytkownika i używać go wielokrotnie do wszystkich swoich readpołączeń. Po stronie jądra prawie nie ma problemu ze stronami 4K lub brakami TLB, ponieważ cała pamięć RAM jest zwykle mapowana liniowo przy użyciu kilku bardzo dużych stron (np. 1 GB stron na x86), więc strony leżące w pamięci podręcznej stron są pokryte bardzo wydajnie w przestrzeni jądra.

Zasadniczo masz następujące porównanie, aby ustalić, która jest szybsza dla jednego odczytu dużego pliku:

Czy dodatkowe działanie na stronę implikowane przez to mmappodejście jest bardziej kosztowne niż praca na bajt kopiowania zawartości pliku z jądra do przestrzeni użytkownika sugerowana przy użyciu read()?

W wielu systemach są one w przybliżeniu zrównoważone. Zauważ, że każdy skaluje się z zupełnie innymi atrybutami sprzętu i stosu systemu operacyjnego.

W szczególności mmappodejście staje się stosunkowo szybsze, gdy:

  • System operacyjny ma szybką obsługę drobnych usterek, a zwłaszcza optymalizację łączenia drobnych usterek, takich jak usuwanie usterek.
  • System operacyjny ma dobrą MAP_POPULATEimplementację, która może efektywnie przetwarzać duże mapy w przypadkach, gdy na przykład strony leżące obok siebie są sąsiadujące w pamięci fizycznej.
  • Sprzęt ma wysoką wydajność tłumaczenia stron, taką jak duże TLB, szybkie TLB drugiego poziomu, szybkie i równoległe moduły spacerujące, dobra interakcja pobierania wstępnego z tłumaczeniem i tak dalej.

... podczas gdy read()podejście staje się stosunkowo szybsze, gdy:

  • read()Syscall ma dobrą wydajność kopiowania. Np. Dobra copy_to_userwydajność po stronie jądra.
  • Jądro ma skuteczny (w stosunku do użytkownika) sposób mapowania pamięci, np. Używając tylko kilku dużych stron ze wsparciem sprzętowym.
  • Jądro ma szybkie wywołania systemowe i sposób na utrzymanie wpisów TLB jądra w różnych wywołaniach systemowych.

Powyższe czynniki sprzętowe są bardzo zróżnicowane na różnych platformach, nawet w tej samej rodzinie (np. W generacjach x86, a zwłaszcza w segmentach rynku) i zdecydowanie w różnych architekturach (np. ARM vs x86 vs PPC).

Czynniki OS również się zmieniają, z różnymi ulepszeniami po obu stronach, powodując duży skok względnej prędkości dla jednego lub drugiego podejścia. Najnowsza lista obejmuje:

  • Dodanie opisanego powyżej błędu, który naprawdę pomaga w mmapprzypadku bez MAP_POPULATE.
  • Dodanie szybkiej ścieżki copy_to_usermetod arch/x86/lib/copy_user_64.S, na przykład z wykorzystaniem REP MOVQkiedy jest szybka, które naprawdę pomagają read()sprawę.

Aktualizacja po Spectre and Meltdown

Ograniczenie luk w zabezpieczeniach Spectre i Meltdown znacznie zwiększyło koszt wywołania systemowego. W systemach, które zmierzyłem, koszt wywołania systemowego „nic nie rób” (który jest szacunkiem czystego obciążenia wywołania wywołania systemowego, oprócz rzeczywistej pracy wykonanej przez wywołanie), spadł z około 100 ns na typowy nowoczesny system Linux do około 700 ns. Ponadto, w zależności od systemu, poprawka izolacji tabeli stron specjalnie dla Meltdown może mieć dodatkowe skutki w dół, poza bezpośrednim kosztem wywołania systemowego, z powodu konieczności ponownego ładowania wpisów TLB.

Wszystko to jest względną wadą read()metod opartych na metodach w porównaniu z mmapmetodami opartymi na metodach, ponieważ read()metody muszą wywoływać jedno wywołanie systemowe dla każdej wartości danych o „rozmiarze bufora”. Nie można arbitralnie zwiększyć rozmiaru bufora, aby amortyzować ten koszt, ponieważ użycie dużych buforów zwykle działa gorzej, ponieważ przekracza się rozmiar L1, a zatem ciągle cierpi na brak pamięci podręcznej.

Z drugiej strony za pomocą mmapmożna zmapować w dużym obszarze pamięci MAP_POPULATEi uzyskać do niego skuteczny dostęp, kosztem tylko jednego wywołania systemowego.


1 To mniej więcej obejmuje przypadek, w którym plik nie był w pełni buforowany na początku, ale gdzie system operacyjny do odczytu jest wystarczająco dobry, aby tak się pojawił (tzn. Strona jest zwykle buforowana do czasu chcieć tego). Jest to jednak subtelna kwestia, ponieważ sposób działania z wyprzedzeniem często różni się między połączeniami mmapi readwywołaniami i może być dalej dostosowywany za pomocą połączeń „doradzania”, jak opisano w 2 .

2 ... ponieważ jeśli plik nie jest buforowany, twoje zachowanie będzie całkowicie zdominowane przez obawy związane z IO, w tym sympatię twojego wzorca dostępu do podstawowego sprzętu - i dołożymy wszelkich starań, aby taki dostęp był tak sympatyczny jak możliwe, np. poprzez użycie madviselub fadvisepołączenia (i wszelkie zmiany poziomu aplikacji, które możesz wprowadzić, aby poprawić wzorce dostępu).

3 Można to obejść, na przykład, sekwencyjnie mmapwchodząc w okna o mniejszym rozmiarze, powiedzmy 100 MB.

4 W rzeczywistości okazuje się, że MAP_POPULATEpodejście (przynajmniej jedna kombinacja sprzęt / system operacyjny) jest tylko nieco szybsze niż nieużywanie go, prawdopodobnie dlatego, że jądro używa błędów - więc rzeczywista liczba drobnych błędów jest zmniejszona 16- krotnie lub tak.

BeeOnRope
źródło
4
Dziękujemy za udzielenie bardziej szczegółowej odpowiedzi na ten złożony problem. Dla większości ludzi oczywiste jest, że mmap jest szybszy, podczas gdy w rzeczywistości często tak nie jest. W moich eksperymentach losowe uzyskiwanie dostępu do dużej bazy danych o pojemności 100 GB z indeksem w pamięci okazało się szybsze dzięki funkcji pread (), mimo że malloc'ingowałem bufor dla każdego z milionów dostępów. I wygląda na to, że wielu ludzi w branży zaobserwowało to samo .
Caetano Sauer
5
Tak, wiele zależy od scenariusza. Jeśli odczyty są wystarczająco małe i z czasem masz tendencję do wielokrotnego odczytywania tych samych bajtów, mmapbędziesz miał nie do pokonania przewagę, ponieważ unikasz narzutu wywołania stałego jądra. Z drugiej strony, mmapzwiększa również ciśnienie TLB i faktycznie powoduje spowolnienie w fazie „rozgrzewania”, w której bajty są odczytywane po raz pierwszy w bieżącym procesie (chociaż nadal znajdują się na stronie strony), ponieważ może to zrobić więcej pracy niż read, na przykład, „obejście” sąsiednich stron ... a dla tych samych aplikacji najważniejsze jest „rozgrzanie”! @CaetanoSauer
BeeOnRope
Myślę, że tam, gdzie mówisz „... ale 25 miliardów błędów stron wciąż nie będzie super szybkich ...” powinno się przeczytać „... ale 25 milionów błędów stron wciąż nie będzie super szybkich ...” . Nie jestem w 100% pozytywny, dlatego nie edytuję bezpośrednio.
Ton van den Heuvel
7

Przykro mi, że Ben Collins stracił swój kod źródłowy mmap systemu Windows. Byłoby miło mieć w Boost.

Tak, mapowanie pliku jest znacznie szybsze. Zasadniczo używasz podsystemu pamięci wirtualnej systemu operacyjnego do kojarzenia pamięci z dyskiem i odwrotnie. Pomyśl o tym w ten sposób: jeśli programiści jądra systemu operacyjnego mogliby zrobić to szybciej, zrobiliby to. Ponieważ to sprawia, że ​​prawie wszystko jest szybsze: bazy danych, czasy uruchamiania, czasy ładowania programów i tak dalej.

Podejście do przesuwanego okna nie jest wcale takie trudne, ponieważ można jednocześnie zamapować wiele stron z wieloma stronami. Tak więc rozmiar rekordu nie ma znaczenia, o ile największy z każdego pojedynczego rekordu zmieści się w pamięci. Ważne jest zarządzanie księgowością.

Jeśli rekord nie zaczyna się na granicy getpagesize (), mapowanie musi rozpocząć się na poprzedniej stronie. Długość mapowanego regionu rozciąga się od pierwszego bajtu rekordu (w razie potrzeby zaokrąglanego w dół do najbliższej wielokrotności getpagesize ()) do ostatniego bajtu rekordu (zaokrąglanego w górę do najbliższej wielokrotności getpagesize ()). Po zakończeniu przetwarzania rekordu możesz usunąć mapowanie () i przejść do następnego.

To wszystko działa dobrze również w systemie Windows za pomocą CreateFileMapping () i MapViewOfFile () (i GetSystemInfo (), aby uzyskać SYSTEM_INFO.dwAllocationGranularity --- nie SYSTEM_INFO.dwPageSize).

mlbrock
źródło
Właśnie googlowałem i znalazłem ten krótki fragment o dwAllocationGranularity - korzystałem z dwPageSize i wszystko się zepsuło. Dzięki!
wickedchicken
4

mmap powinien być szybszy, ale nie wiem ile. To bardzo zależy od twojego kodu. Jeśli używasz mmapa, najlepiej zmapuj cały plik na raz, co znacznie ułatwi Ci życie. Jednym z potencjalnych problemów jest to, że jeśli Twój plik jest większy niż 4 GB (lub w praktyce limit jest niższy, często 2 GB), będziesz potrzebować architektury 64-bitowej. Więc jeśli używasz środowiska 32, prawdopodobnie nie chcesz go używać.

To powiedziawszy, może istnieć lepsza droga do poprawy wydajności. Powiedziałeś, że plik wejściowy jest skanowany wiele razy , jeśli możesz go odczytać w jednym przebiegu, a następnie zrobić z nim, może to być potencjalnie znacznie szybsze.

Leon Timmermans
źródło
3

Być może powinieneś wstępnie przetworzyć pliki, aby każdy rekord znajdował się w osobnym pliku (a przynajmniej że każdy plik ma rozmiar mmap).

Czy mógłbyś również wykonać wszystkie kroki przetwarzania dla każdego rekordu, zanim przejdziesz do następnego? Może to pozwoliłoby uniknąć niektórych narzutów we / wy?

Douglas Leeder
źródło
3

Zgadzam się, że plik mmap'd I / O będzie szybciej, ale w czasie, gdy benchmarking kod, nie powinny być przykładem licznik nieco zoptymalizowane?

Ben Collins napisał:

char data[0x1000];
std::ifstream in("file.bin");

while (in)
{
    in.read(data, 0x1000);
    // do something with data 
}

Proponuję również spróbować:

char data[0x1000];
std::ifstream iifle( "file.bin");
std::istream  in( ifile.rdbuf() );

while( in )
{
    in.read( data, 0x1000);
    // do something with data
}

Poza tym możesz również spróbować ustawić rozmiar bufora na taki sam rozmiar jak jedna strona pamięci wirtualnej, na wypadek gdyby 0x1000 nie był wielkości jednej strony pamięci wirtualnej na twoim komputerze ... IMHO wciąż zapisywało pliki we / wy wygrywa, ale to powinno przybliżyć sprawę.

paxos1977
źródło
2

Moim zdaniem użycie mmap () „po prostu” odciąża programistę od konieczności pisania własnego kodu pamięci podręcznej. W prostym przypadku „odczytaj raz eactly” nie będzie to trudne (chociaż, jak wskazuje mlbrock, nadal zapisujesz kopię pamięci w przestrzeni procesu), ale jeśli przewijasz plik do przodu lub do tyłu lub pomijanie bitów i tak dalej, uważam, że programiści jądra prawdopodobnie wykonali lepszą robotę implementując buforowanie niż mogę ...

mikrofon
źródło
1
Najprawdopodobniej możesz lepiej wykonać buforowanie danych specyficznych dla aplikacji niż jądro, które działa na porcjach wielkości strony w bardzo ślepy sposób (np. Używa tylko prostego schematu pseudo-LRU, aby zdecydować, które strony eksmitować ) - chociaż możesz wiele wiedzieć o właściwej ziarnistości buforowania, a także mieć dobry pomysł na przyszłe wzorce dostępu. Prawdziwą zaletą mmapbuforowania jest to, że po prostu ponownie używasz istniejącej pamięci podręcznej strony, która już tam będzie, dzięki czemu otrzymujesz tę pamięć za darmo i można ją współdzielić między procesami.
BeeOnRope
2

Pamiętam, jak wiele lat temu mapowałem ogromny plik zawierający strukturę drzewa do pamięci. Byłem zdumiony szybkością w porównaniu z normalną serializacją, która wymaga dużo pracy w pamięci, jak przydzielanie węzłów drzew i ustawianie wskaźników. W rzeczywistości porównywałem pojedyncze wywołanie mmap (lub jego odpowiednika w systemie Windows) z wieloma (WIELE) wywołaniami nowych i wywoływanych przez operatora. W przypadku tego rodzaju zadań mmap jest bezkonkurencyjny w porównaniu do usuwania serializacji. Oczywiście należy w tym celu przyjrzeć się wskaźnikom relokowalnym.


źródło
To brzmi bardziej jak przepis na katastrofę. Co robisz, jeśli zmienia się układ obiektu? Jeśli masz funkcje wirtualne, wszystkie wskaźniki vftbl prawdopodobnie będą nieprawidłowe. Jak kontrolować, gdzie plik jest mapowany? Możesz podać mu adres, ale to tylko wskazówka, a jądro może wybrać inny adres bazowy.
Jens
Działa to doskonale, gdy masz stabilny i jasno zdefiniowany układ drzewa. Następnie możesz rzutować wszystko na odpowiednie struktury i podążać za wewnętrznymi wskaźnikami plików, dodając przesunięcie „adresu początkowego mmap” za każdym razem. Jest to bardzo podobne do systemów plików wykorzystujących i
węzły
1

To brzmi jak dobry przypadek użycia dla wielowątkowości ... Wydaje mi się, że możesz łatwo ustawić jeden wątek do odczytu danych, podczas gdy inny przetwarzają go. Może to być sposób na radykalne zwiększenie postrzeganej wydajności. Tylko myśl.

Pat Notz
źródło
Tak. Myślałem o tym i prawdopodobnie wypróbuję to w późniejszym wydaniu. Jedyne zastrzeżenie, jakie mam, to fakt, że przetwarzanie jest znacznie krótsze niż opóźnienie we / wy, więc może nie być wiele korzyści.
jbl
1

Myślę, że największą zaletą mmap jest możliwość asynchronicznego odczytu z:

    addr1 = NULL;
    while( size_left > 0 ) {
        r = min(MMAP_SIZE, size_left);
        addr2 = mmap(NULL, r,
            PROT_READ, MAP_FLAGS,
            0, pos);
        if (addr1 != NULL)
        {
            /* process mmap from prev cycle */
            feed_data(ctx, addr1, MMAP_SIZE);
            munmap(addr1, MMAP_SIZE);
        }
        addr1 = addr2;
        size_left -= r;
        pos += r;
    }
    feed_data(ctx, addr1, r);
    munmap(addr1, r);

Problem polega na tym, że nie mogę znaleźć odpowiedniego MAP_FLAGS, który dałby wskazówkę, że ta pamięć powinna być zsynchronizowana z pliku jak najszybciej. Mam nadzieję, że MAP_POPULATE daje właściwą wskazówkę dla mmap (tzn. Nie będzie próbował załadować całej zawartości przed powrotem z wywołania, ale zrobi to asynchronicznie z feed_data). Przynajmniej daje lepsze wyniki z tą flagą, nawet jeśli instrukcja mówi, że nic nie robi bez MAP_PRIVATE od wersji 2.6.23.

ony
źródło
2
Chcesz posix_madvisezWILLNEED flagą dla leniwych podpowiedzi do wstępnie wypełnić.
ShadowRanger
@ShadowRanger, brzmi rozsądnie. Chociaż zaktualizowałbym stronę podręcznika, aby wyraźnie stwierdzić, że posix_madvisejest to wywołanie asynchroniczne. Przydałoby się również odniesienie mlockdo tych, którzy chcą czekać, aż cały region pamięci stanie się dostępny bez błędów strony.
tylko