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 , mmap
może wydawać się magią :
mmap
wymaga tylko 1 wywołania systemowego (potencjalnie) odwzorowania całego pliku, po czym nie są już potrzebne żadne wywołania systemowe.
mmap
nie wymaga kopiowania danych pliku z jądra do przestrzeni użytkownika.
mmap
umoż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 mmap
vs 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 mmap
cał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_POPULATE
do, mmap
aby 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 mmap
podejś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ż mmap
ing 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 read
połą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 mmap
podejś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 mmap
podejś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_POPULATE
implementację, 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_user
wydajność 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
mmap
przypadku bez MAP_POPULATE
.
- Dodanie szybkiej ścieżki
copy_to_user
metod arch/x86/lib/copy_user_64.S
, na przykład z wykorzystaniem REP MOVQ
kiedy 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 mmap
metodami 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ą mmap
można zmapować w dużym obszarze pamięci MAP_POPULATE
i 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 mmap
i read
wywoł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 madvise
lub fadvise
połą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 mmap
wchodząc w okna o mniejszym rozmiarze, powiedzmy 100 MB.
4 W rzeczywistości okazuje się, że MAP_POPULATE
podejś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.
mmap()
jest 2-6 razy szybsza niż przy użyciu syscalls, npread()
.