2017-03-04 29 views
14

Korzystając z poniższego programu próbuję przetestować, jak szybko mogę zapisać na dysku przy użyciu std::ofstream.Dlaczego mój test zapisu na dysku C++ jest wolniejszy niż zwykła kopia pliku z użyciem bash?

Osiągam około 300 MiB/s podczas pisania pliku 1 GiB.

Jednak prosta kopia pliku przy użyciu polecenia cp jest co najmniej dwa razy szybsza.

Czy mój program uderza w limit sprzętowy, czy może być szybszy?

#include <chrono> 
#include <iostream> 
#include <fstream> 

char payload[1000 * 1000]; // 1 MB 

void test(int MB) 
{ 
    // Configure buffer 
    char buffer[32 * 1000]; 
    std::ofstream of("test.file"); 
    of.rdbuf()->pubsetbuf(buffer, sizeof(buffer)); 

    auto start_time = std::chrono::steady_clock::now(); 

    // Write a total of 1 GB 
    for (auto i = 0; i != MB; ++i) 
    { 
     of.write(payload, sizeof(payload)); 
    } 

    double elapsed_ns = std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::steady_clock::now() - start_time).count(); 
    double megabytes_per_ns = 1e3/elapsed_ns; 
    double megabytes_per_s = 1e9 * megabytes_per_ns; 
    std::cout << "Payload=" << MB << "MB Speed=" << megabytes_per_s << "MB/s" << std::endl; 
} 

int main() 
{ 
    for (auto i = 1; i <= 10; ++i) 
    { 
     test(i * 100); 
    } 
} 

wyjściowa:

Payload=100MB Speed=3792.06MB/s 
Payload=200MB Speed=1790.41MB/s 
Payload=300MB Speed=1204.66MB/s 
Payload=400MB Speed=910.37MB/s 
Payload=500MB Speed=722.704MB/s 
Payload=600MB Speed=579.914MB/s 
Payload=700MB Speed=499.281MB/s 
Payload=800MB Speed=462.131MB/s 
Payload=900MB Speed=411.414MB/s 
Payload=1000MB Speed=364.613MB/s 

Aktualizacja

zmieniłem od std::ofstream do fwrite:

#include <chrono> 
#include <cstdio> 
#include <iostream> 

char payload[1024 * 1024]; // 1 MiB 

void test(int number_of_megabytes) 
{ 
    FILE* file = fopen("test.file", "w"); 

    auto start_time = std::chrono::steady_clock::now(); 

    // Write a total of 1 GB 
    for (auto i = 0; i != number_of_megabytes; ++i) 
    { 
     fwrite(payload, 1, sizeof(payload), file); 
    } 
    fclose(file); // TODO: RAII 

    double elapsed_ns = std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::steady_clock::now() - start_time).count(); 
    double megabytes_per_ns = 1e3/elapsed_ns; 
    double megabytes_per_s = 1e9 * megabytes_per_ns; 
    std::cout << "Size=" << number_of_megabytes << "MiB Duration=" << long(0.5 + 100 * elapsed_ns/1e9)/100.0 << "s Speed=" << megabytes_per_s << "MiB/s" << std::endl; 
} 

int main() 
{ 
    test(256); 
    test(512); 
    test(1024); 
    test(1024); 
} 

co poprawia prędkość do 668MiB/s na 1 plik Gib:

Size=256MiB Duration=0.4s Speed=2524.66MiB/s 
Size=512MiB Duration=0.79s Speed=1262.41MiB/s 
Size=1024MiB Duration=1.5s Speed=664.521MiB/s 
Size=1024MiB Duration=1.5s Speed=668.85MiB/s 

który jest po prostu tak szybko jak dd:

time dd if=/dev/zero of=test.file bs=1024 count=0 seek=1048576 

real 0m1.539s 
user 0m0.001s 
sys 0m0.344s 
+0

Czy testujesz kompilację wydania swojego programu z optymalizacją? Czy próbowałeś zwiększyć rozmiar bufora? –

+0

Czy nie powinien to być "double megabytes_per_ns = MB/elapsed_ns;'? – zett42

+0

Powinieneś także otworzyć strumień w trybie binarnym, aby porównać go z innymi metodami pisania. Użyj 'std :: ofstream of (" test.file ", std :: ios :: binary)'. Dostaję bardzo ścisłą wydajność pomiędzy 'ofstream' i' fwrite' (różnice występują w zakresie błędów pomiarowych). Kompilator VC++ 2017. – zett42

Odpowiedz

14

Po pierwsze, nie jesteś naprawdę mierzącego prędkość zapisu na dysku, ale (częściowo) prędkość zapisu danych do pamięci podręcznej dysku OS. Aby naprawdę zmierzyć prędkość zapisu na dysku, dane powinny zostać przepłukane na dysk przed obliczeniem czasu. Bez spłukiwania może wystąpić różnica w zależności od rozmiaru pliku i dostępnej pamięci.

Wydaje się, że coś jest nie tak w obliczeniach. Nie używasz wartości MB.

Upewnij się również, że rozmiar bufora to potęga dwóch lub przynajmniej wielokrotność rozmiaru strony dysku (4096 bajtów): char buffer[32 * 1024];. Równie dobrze możesz to zrobić dla payload. (Wygląda na to, że zmieniłeś to z 1024 na 1000 w edycji, w której dodałeś obliczenia).

Nie używaj strumieni do zapisu (binarnego) bufora danych na dysk, ale zamiast tego napisz bezpośrednio do pliku, używając FILE*, fopen(), fwrite(), fclose(). Zobacz przykład this answer dla przykładu i trochę czasu.


Aby skopiować plik: otwórz plik źródłowy w trybie tylko do odczytu i, jeśli to możliwe, tryb naprzód tylko i używając fread(), fwrite():

while fread() from source to buffer 
    fwrite() buffer to destination file 

To powinno dać prędkość porównywalna z szybkość kopiowania plików OS (możesz chcieć przetestować różne rozmiary buforów).

Ten potęga być nieznacznie szybciej za pomocą mapowania pamięci:

open src, create memory mapping over the file 
open/create dest, set file size to size of src, create memory mapping over the file 
memcpy() src to dest 

przypadku dużych plików należy stosować mniejsze odwzorowane widoki.

7
  1. Streams are slow
  2. cp używa wywołań bezpośrednio read(2) lub mmap(2).
+0

Powiązane pytanie ma 5 lat, w międzyczasie wdrożenia mogły ulec poprawie. Przykładowe linki kodu są martwe. – zett42

4

Założę się, że jest to coś sprytnego wewnątrz CP lub systemu plików. Jeśli jest to wewnątrz CP, może się zdarzyć, że plik, który kopiujesz, zawiera wiele zera, a cp wykrywa to i zapisuje wersję Twojego pliku. Strona podręcznika dla cp mówi: "Domyślnie, rzadkie pliki SOURCE są wykrywane przez surową heurystykę, a odpowiadający plik DEST również jest skąpy." Może to oznaczać kilka rzeczy, ale jednym z nich jest to, że cp może tworzyć rzadką wersję twojego pliku, co wymagałoby mniejszego czasu zapisu na dysku.

Jeśli jest w twoim systemie plików, może to być Deduplication.

Jako long-shot 3rd, może to być również coś w twoim systemie operacyjnym lub oprogramowaniu dysku, które tłumaczy odczytywanie i zapisywanie w specjalistycznych instrukcjach, które nie wymagają tak dużej synchronizacji, jak wymaga tego twój program (niższe użycie magistrali oznacza mniejsze opóźnienie).

3

Używasz stosunkowo małego rozmiaru bufora. Małe bufory oznaczają więcej operacji na sekundę, co zwiększa koszty ogólne. Systemy dyskowe mają niewielki czas oczekiwania, zanim otrzymają żądanie odczytu/zapisu i rozpoczną przetwarzanie; większy bufor amortyzuje ten koszt nieco lepiej. Mniejszy bufor może również oznaczać, że dysk spędza więcej czasu na poszukiwaniu.

Nie wydajesz wielu jednoczesnych żądań - potrzebujesz jednego odczytu do ukończenia przed następnymi uruchomieniami. Oznacza to, że dysk może mieć czas martwy, gdy nie robi nic. Ponieważ wszystkie zapisy zależą od wszystkich odczytów, a twoje odczyty są seryjne, głodzisz system dyskowy żądań odczytu (podwójnie, ponieważ zapisy usuwają z odczytów).

Łączna liczba żądanych odczytanych bajtów we wszystkich żądaniach odczytu powinna być większa niż opóźnienie przepustowości systemu dyskowego. Jeśli dysk ma opóźnienie 0,5 ms i wydajność 4 GB/s, to chcesz mieć zawsze 4 GB * 0,5 ms = 2 MB wartości.

Nie używasz żadnych podpowiedzi systemu operacyjnego, które robisz sekwencyjne czytanie.

Aby rozwiązać ten problem:

  • Zmień swój kod, aby mieć więcej niż jedno zaległe żądanie odczytu w każdym czasie.
  • Wystarczająca liczba żądań przeczytania jest wystarczająca, aby czekać na dane o wartości co najmniej 2 MB.
  • Skorzystaj z flag posix_fadvise(), aby zoptymalizować harmonogram dysków systemu operacyjnego i pamięć podręczną strony.
  • Rozważ użycie mmap do zmniejszenia narzutu.
  • Użyj większego rozmiaru bufora na żądanie odczytu, aby zmniejszyć obciążenie.

Ta odpowiedź ma więcej informacji: https://stackoverflow.com/a/3756466/344638

0

Problemem jest to, że podasz zbyt mały bufor dla fstream

char buffer[32 * 1000]; 
std::ofstream of("test.file"); 
of.rdbuf()->pubsetbuf(buffer, sizeof(buffer)); 

Twoja aplikacja działa w trybie użytkownika.Aby napisać na dysk, funkcja systemu połączeń przychodzących write, która została wykonana w trybie jądra. Następnie write przenosi dane do pamięci podręcznej systemu, a następnie do pamięci podręcznej dysku twardego, a następnie zostanie zapisany na dysku.

Ten rozmiar bufora wpływa na liczbę wywołań systemowych (1 wywołanie na każde 32 * 1000 bajtów). Podczas wywołania systemowego system operacyjny musi przełączyć kontekst wykonania z trybu użytkownika na tryb jądra, a następnie z powrotem. Kontekst przełączania jest napowietrzny. W Linuksie jest to odpowiednik około 2500-3500 prostych poleceń CPU. Z tego powodu Twoja aplikacja poświęca najwięcej czasu na procesor w przełączaniu kontekstów.

W swojej drugiej aplikacji używasz

FILE* file = fopen("test.file", "w"); 

plik używając większy bufor domyślnie, dlatego produkują bardziej wydajnego kodu. Możesz spróbować określić mały bufor za pomocą setvbuf. W takim przypadku powinieneś zobaczyć ten sam spadek wydajności.

Należy pamiętać, że w Twoim przypadku szyjka butelki nie jest wydajna dla dysku twardego. Jest to przełączanie kontekstowe

image

Powiązane problemy