Skopiuj plik w rozsądny, bezpieczny i wydajny sposób

305

Poszukuję dobrego sposobu na skopiowanie pliku (binarnego lub tekstowego). Napisałem kilka próbek, każdy działa. Ale chcę usłyszeć opinię doświadczonych programistów.

Brakuje dobrych przykładów i szukam sposobu, który działa z C ++.

ANSI-C-WAY

#include <iostream>
#include <cstdio>    // fopen, fclose, fread, fwrite, BUFSIZ
#include <ctime>
using namespace std;

int main() {
    clock_t start, end;
    start = clock();

    // BUFSIZE default is 8192 bytes
    // BUFSIZE of 1 means one chareter at time
    // good values should fit to blocksize, like 1024 or 4096
    // higher values reduce number of system calls
    // size_t BUFFER_SIZE = 4096;

    char buf[BUFSIZ];
    size_t size;

    FILE* source = fopen("from.ogv", "rb");
    FILE* dest = fopen("to.ogv", "wb");

    // clean and more secure
    // feof(FILE* stream) returns non-zero if the end of file indicator for stream is set

    while (size = fread(buf, 1, BUFSIZ, source)) {
        fwrite(buf, 1, size, dest);
    }

    fclose(source);
    fclose(dest);

    end = clock();

    cout << "CLOCKS_PER_SEC " << CLOCKS_PER_SEC << "\n";
    cout << "CPU-TIME START " << start << "\n";
    cout << "CPU-TIME END " << end << "\n";
    cout << "CPU-TIME END - START " << end - start << "\n";
    cout << "TIME(SEC) " << static_cast<double>(end - start) / CLOCKS_PER_SEC << "\n";

    return 0;
}

POSIX-WAY (K&R używa tego w „języku programowania C”, więcej niższego poziomu)

#include <iostream>
#include <fcntl.h>   // open
#include <unistd.h>  // read, write, close
#include <cstdio>    // BUFSIZ
#include <ctime>
using namespace std;

int main() {
    clock_t start, end;
    start = clock();

    // BUFSIZE defaults to 8192
    // BUFSIZE of 1 means one chareter at time
    // good values should fit to blocksize, like 1024 or 4096
    // higher values reduce number of system calls
    // size_t BUFFER_SIZE = 4096;

    char buf[BUFSIZ];
    size_t size;

    int source = open("from.ogv", O_RDONLY, 0);
    int dest = open("to.ogv", O_WRONLY | O_CREAT /*| O_TRUNC/**/, 0644);

    while ((size = read(source, buf, BUFSIZ)) > 0) {
        write(dest, buf, size);
    }

    close(source);
    close(dest);

    end = clock();

    cout << "CLOCKS_PER_SEC " << CLOCKS_PER_SEC << "\n";
    cout << "CPU-TIME START " << start << "\n";
    cout << "CPU-TIME END " << end << "\n";
    cout << "CPU-TIME END - START " << end - start << "\n";
    cout << "TIME(SEC) " << static_cast<double>(end - start) / CLOCKS_PER_SEC << "\n";

    return 0;
}

KISS-C ++ - Streambuffer-WAY

#include <iostream>
#include <fstream>
#include <ctime>
using namespace std;

int main() {
    clock_t start, end;
    start = clock();

    ifstream source("from.ogv", ios::binary);
    ofstream dest("to.ogv", ios::binary);

    dest << source.rdbuf();

    source.close();
    dest.close();

    end = clock();

    cout << "CLOCKS_PER_SEC " << CLOCKS_PER_SEC << "\n";
    cout << "CPU-TIME START " << start << "\n";
    cout << "CPU-TIME END " << end << "\n";
    cout << "CPU-TIME END - START " <<  end - start << "\n";
    cout << "TIME(SEC) " << static_cast<double>(end - start) / CLOCKS_PER_SEC << "\n";

    return 0;
}

COPY-ALGORITHM-C ++ - DROGA

#include <iostream>
#include <fstream>
#include <ctime>
#include <algorithm>
#include <iterator>
using namespace std;

int main() {
    clock_t start, end;
    start = clock();

    ifstream source("from.ogv", ios::binary);
    ofstream dest("to.ogv", ios::binary);

    istreambuf_iterator<char> begin_source(source);
    istreambuf_iterator<char> end_source;
    ostreambuf_iterator<char> begin_dest(dest); 
    copy(begin_source, end_source, begin_dest);

    source.close();
    dest.close();

    end = clock();

    cout << "CLOCKS_PER_SEC " << CLOCKS_PER_SEC << "\n";
    cout << "CPU-TIME START " << start << "\n";
    cout << "CPU-TIME END " << end << "\n";
    cout << "CPU-TIME END - START " <<  end - start << "\n";
    cout << "TIME(SEC) " << static_cast<double>(end - start) / CLOCKS_PER_SEC << "\n";

    return 0;
}

WŁASNY BUFOR-C ++ - SPOSÓB

#include <iostream>
#include <fstream>
#include <ctime>
using namespace std;

int main() {
    clock_t start, end;
    start = clock();

    ifstream source("from.ogv", ios::binary);
    ofstream dest("to.ogv", ios::binary);

    // file size
    source.seekg(0, ios::end);
    ifstream::pos_type size = source.tellg();
    source.seekg(0);
    // allocate memory for buffer
    char* buffer = new char[size];

    // copy file    
    source.read(buffer, size);
    dest.write(buffer, size);

    // clean up
    delete[] buffer;
    source.close();
    dest.close();

    end = clock();

    cout << "CLOCKS_PER_SEC " << CLOCKS_PER_SEC << "\n";
    cout << "CPU-TIME START " << start << "\n";
    cout << "CPU-TIME END " << end << "\n";
    cout << "CPU-TIME END - START " <<  end - start << "\n";
    cout << "TIME(SEC) " << static_cast<double>(end - start) / CLOCKS_PER_SEC << "\n";

    return 0;
}

LINUX-WAY // wymaga jądra> = 2.6.33

#include <iostream>
#include <sys/sendfile.h>  // sendfile
#include <fcntl.h>         // open
#include <unistd.h>        // close
#include <sys/stat.h>      // fstat
#include <sys/types.h>     // fstat
#include <ctime>
using namespace std;

int main() {
    clock_t start, end;
    start = clock();

    int source = open("from.ogv", O_RDONLY, 0);
    int dest = open("to.ogv", O_WRONLY | O_CREAT /*| O_TRUNC/**/, 0644);

    // struct required, rationale: function stat() exists also
    struct stat stat_source;
    fstat(source, &stat_source);

    sendfile(dest, source, 0, stat_source.st_size);

    close(source);
    close(dest);

    end = clock();

    cout << "CLOCKS_PER_SEC " << CLOCKS_PER_SEC << "\n";
    cout << "CPU-TIME START " << start << "\n";
    cout << "CPU-TIME END " << end << "\n";
    cout << "CPU-TIME END - START " <<  end - start << "\n";
    cout << "TIME(SEC) " << static_cast<double>(end - start) / CLOCKS_PER_SEC << "\n";

    return 0;
}

Środowisko

  • GNU / LINUX (Archlinux)
  • Jądro 3.3
  • GLIBC-2.15, LIBSTDC ++ 4.7 (GCC-LIBS), GCC 4.7, Coreutils 8.16
  • Korzystanie z RUNLEVEL 3 (Multiuser, Network, Terminal, bez GUI)
  • INTEL SSD-Postville 80 GB, wypełniony do 50%
  • Skopiuj 270 MB OGG-VIDEO-FILE

kroki ku reprodukcji

 1. $ rm from.ogg
 2. $ reboot                           # kernel and filesystem buffers are in regular
 3. $ (time ./program) &>> report.txt  # executes program, redirects output of program and append to file
 4. $ sha256sum *.ogv                  # checksum
 5. $ rm to.ogg                        # remove copy, but no sync, kernel and fileystem buffers are used
 6. $ (time ./program) &>> report.txt  # executes program, redirects output of program and append to file

Wyniki (wykorzystany CZAS PROCESORA)

Program  Description                 UNBUFFERED|BUFFERED
ANSI C   (fread/frwite)                 490,000|260,000  
POSIX    (K&R, read/write)              450,000|230,000  
FSTREAM  (KISS, Streambuffer)           500,000|270,000 
FSTREAM  (Algorithm, copy)              500,000|270,000
FSTREAM  (OWN-BUFFER)                   500,000|340,000  
SENDFILE (native LINUX, sendfile)       410,000|200,000  

Rozmiar pliku się nie zmienia.
sha256sum drukuje te same wyniki.
Plik wideo jest nadal odtwarzany.

pytania

  • Jaką metodę wolisz?
  • Czy znasz lepsze rozwiązania?
  • Czy widzisz jakieś błędy w moim kodzie?
  • Czy znasz powód, dla którego chcesz uniknąć rozwiązania?

  • FSTREAM (KISS, Streambuffer)
    Naprawdę podoba mi się ten, ponieważ jest naprawdę krótki i prosty. O ile wiem, operator << jest przeciążony dla rdbuf () i niczego nie konwertuje. Poprawny?

Dzięki

Aktualizacja 1
Zmieniłem źródło we wszystkich próbkach w taki sposób, że otwieranie i zamykanie deskryptorów plików jest uwzględniane w pomiarze clock () . Nie są to żadne inne znaczące zmiany w kodzie źródłowym. Wyniki się nie zmieniły! Poświęciłem też czas na dokładne sprawdzenie moich wyników.

Aktualizacja 2
Próbka ANSI C zmieniona: Stan pętli while nie wywołuje już feof (), zamiast tego przeniosłem fread () do tego warunku. Wygląda na to, że kod działa teraz o 10 000 zegarów szybciej.

Zmieniono pomiar: poprzednie wyniki były zawsze buforowane, ponieważ kilkakrotnie powtórzyłem stary wiersz poleceń rm to.ogv && sync && time ./program dla każdego programu. Teraz ponownie uruchamiam system dla każdego programu. Niebuforowane wyniki są nowe i nie zaskakują. Niebuforowane wyniki tak naprawdę się nie zmieniły.

Jeśli nie usunę starej kopii, programy zareagują inaczej. Zastępowanie istniejącego pliku buforowanego jest szybsze w POSIX i SENDFILE, wszystkie inne programy działają wolniej. Może opcje obcinania lub tworzenia mają wpływ na to zachowanie. Jednak nadpisywanie istniejących plików tą samą kopią nie jest prawdziwym przypadkiem użycia.

Wykonanie kopii za pomocą cp zajmuje 0,44 sekundy bez buforowania i 0,30 sekundy buforowane. Tak więc proces cp jest nieco wolniejszy niż próbka POSIX. Wygląda dobrze dla mnie.

Może dodam także próbki i wyniki mmap () oraz copy_file()z boost :: fileystem.

Aktualizacja 3
Umieściłem to również na stronie blogu i nieco ją rozszerzyłem. W tym splice () , która jest funkcją niskiego poziomu z jądra Linux. Być może pojawi się więcej próbek z Javą. http://www.ttyhoney.com/blog/?page_id=69

Peter
źródło
5
fstreamzdecydowanie jest dobrą opcją dla operacji na plikach.
Chris
28
Zapomniałeś leniwy sposób: system („cp from.ogv to.ogv”);
fbafelipe
3
#include <copyfile.h> copyfile(const char *from, const char *to, copyfile_state_t state, copyfile_flags_t flags);
Martin York,
3
Przepraszam za włamanie się tak późno, ale nie opisałbym żadnego z nich jako „bezpieczny”, ponieważ nie mają one obsługi błędów.
Richard Kettlewell,

Odpowiedzi:

259

Skopiuj plik w rozsądny sposób:

#include <fstream>

int main()
{
    std::ifstream  src("from.ogv", std::ios::binary);
    std::ofstream  dst("to.ogv",   std::ios::binary);

    dst << src.rdbuf();
}

Czytanie jest tak proste i intuicyjne, że jest warte dodatkowych kosztów. Jeśli często to robimy, lepiej polegać na wywołaniach systemu operacyjnego do systemu plików. Jestem pewien, że boostma metodę kopiowania plików w swojej klasie systemu plików.

Istnieje metoda C do interakcji z systemem plików:

#include <copyfile.h>

int
copyfile(const char *from, const char *to, copyfile_state_t state, copyfile_flags_t flags);
Martin York
źródło
28
copyfilenie jest przenośny; Myślę, że jest specyficzny dla Mac OS X. Z pewnością nie istnieje w systemie Linux. boost::filesystem::copy_filejest prawdopodobnie najbardziej przenośnym sposobem kopiowania pliku za pośrednictwem macierzystego systemu plików.
Mike Seymour
4
@MikeSeymour: copyfile () wydaje się być rozszerzeniem BSD.
Martin York,
10
@ duedl0r: Nie. Obiekty mają destruktory. Destruktor dla strumieni automatycznie wywołuje close (). codereview.stackexchange.com/q/540/507
Martin York
11
@ duedl0r: Tak. Ale to tak, jakby powiedzieć „jeśli zajdzie słońce”. Możesz biegać naprawdę szybko na zachód i możesz nieco wydłużyć swój dzień, ale słońce zachodzi. Chyba że masz pamięć błędów i wycieków (będzie poza zakresem). Ale ponieważ nie ma tutaj dynamicznego zarządzania pamięcią, nie może być przeciek i będą one poza zasięgiem (tak jak zachodzi słońce).
Martin York
6
Następnie po prostu zawiń go w {} blok
lunety
62

W C ++ 17 standardowym sposobem kopiowania pliku będzie dołączenie <filesystem>nagłówka i użycie:

bool copy_file( const std::filesystem::path& from,
                const std::filesystem::path& to);

bool copy_file( const std::filesystem::path& from,
                const std::filesystem::path& to,
                std::filesystem::copy_options options);

Pierwsza forma jest równoważna drugiej z copy_options::nonezastosowanymi jako opcje (patrz także copy_file).

filesystemBiblioteki został pierwotnie opracowany jako boost.filesystemi wreszcie połączyła ISO C ++ jako C ++ 17.

manlio
źródło
2
Dlaczego nie ma jednej funkcji z domyślnym argumentem, takiej jak bool copy_file( const std::filesystem::path& from, const std::filesystem::path& to, std::filesystem::copy_options options = std::filesystem::copy_options::none);?
Jepessen
2
@Jepessen Nie jestem tego pewien. Może to nie ma znaczenia .
manlio
@Jepessen w standardowej bibliotece, czysty kod jest najważniejszy. Przeciążenie (w przeciwieństwie do jednej funkcji z domyślnymi parametrami) czyni intencję programisty bardziej wyraźną.
Marc.2377,
@Peter To powinna być prawdopodobnie zaakceptowana odpowiedź, biorąc pod uwagę, że C ++ 17 jest dostępny.
Martin York
21

Za dużo!

Bufor „ANSI C” jest redundantny, ponieważ a FILEjest już buforowany. (Rozmiar tego bufora wewnętrznego jest tym, co BUFSIZfaktycznie definiuje.)

„OWN-BUFFER-C ++ - WAY” będzie powolny z upływem czasu fstream, co powoduje wiele wirtualnych wysyłek i ponownie utrzymuje wewnętrzne bufory lub każdy obiekt strumienia. („COPY-ALGORITHM-C ++ - WAY” tego nie cierpi, ponieważ streambuf_iteratorklasa omija warstwę strumienia.)

Wolę „COPY-ALGORITHM-C ++ - WAY”, ale bez konstruowania fstream, po prostu twórz nagie std::filebufinstancje, gdy nie jest potrzebne żadne formatowanie.

Aby uzyskać surową wydajność, nie można pokonać deskryptorów plików POSIX. Jest brzydki, ale przenośny i szybki na każdej platformie.

Sposób na Linuksa wydaje się być niesamowicie szybki - być może system operacyjny pozwala na powrót funkcji przed zakończeniem operacji we / wy? W każdym razie nie jest to wystarczająco przenośne dla wielu aplikacji.

EDYCJA : Ach, „macierzysty Linux” może poprawiać wydajność poprzez przeplatanie odczytów i zapisów za pomocą asynchronicznych operacji we / wy. Uporządkowanie poleceń może pomóc sterownikowi dysku zdecydować, kiedy najlepiej szukać. Możesz spróbować Boost Asio lub pthreads do porównania. Jeśli chodzi o „nie można pokonać deskryptorów plików POSIX”… cóż, to prawda, jeśli robisz coś z danymi, a nie tylko ślepo kopiujesz.

Potatoswatter
źródło
ANSI C: Ale czy muszę nadać funkcji fread / fwrite rozmiar? pubs.opengroup.org/onlinepubs/9699919799/toc.htm
Peter
@PeterWeber Cóż, tak, to prawda, że ​​BUFSIZ jest tak samo wartościowy jak każdy i prawdopodobnie przyspieszy rzeczy względem jednego lub „tylko kilku” znaków na raz. W każdym razie pomiar wydajności dowodzi, że i tak nie jest to najlepsza metoda.
Potatoswatter
1
Nie mam dogłębnego zrozumienia tego, więc powinienem uważać na założenia i opinie. Linux-Way działa w Kernelspace afaik. To powinno unikać powolnego przełączania kontekstu między Kernelspace a Userspace? Jutro jeszcze raz spojrzę na stronę sendfile. Jakiś czas temu Linus Torvalds powiedział, że nie lubi Systemów plików przestrzeni użytkownika do ciężkich zadań. Może sendfile jest pozytywnym przykładem jego opinii?
Peter
5
sendfile()kopiuje dane między jednym deskryptorem pliku a drugim. Ponieważ kopiowanie odbywa się w jądrze, sendfile()jest bardziej wydajne niż połączenie read(2)i write(2), co wymagałoby transferu danych do iz przestrzeni użytkownika.”: kernel.org/doc/man-pages /online/pages/man2/sendfile.2.html
Max Lybbert
1
Czy możesz opublikować przykład użycia surowych filebufobiektów?
Kerrek SB
14

Chcę bardzo ważną uwagę, że metoda LINUX za pomocą sendfile () ma poważny problem, ponieważ nie może kopiować plików o rozmiarze większym niż 2 GB! Zaimplementowałem go po tym pytaniu i napotykałem problemy, ponieważ użyłem go do kopiowania plików HDF5 o rozmiarze wielu GB.

http://man7.org/linux/man-pages/man2/sendfile.2.html

sendfile () przesyła co najwyżej 0x7ffff000 (2 147 479 552) bajtów, zwracając liczbę faktycznie przesłanych bajtów. (Dotyczy to zarówno systemów 32-bitowych, jak i 64-bitowych.)

rveale
źródło
1
Czy sendfile64 () ma ten sam problem?
graywolf,
1
@Paladin Wygląda na to, że sendfile64 został opracowany w celu obejścia tego ograniczenia. Ze strony podręcznika: „” „Oryginalne wywołanie systemowe sendfile () dla Linuksa nie zostało zaprojektowane do obsługi dużych przesunięć plików. W związku z tym Linux 2.4 dodał sendfile64 (), z szerszym typem argumentu offset. Funkcja otoki sendfile () glibc przejrzyście
radzi sobie
Wydaje się, że sendfile64 ma ten sam problem . Jednak użycie typu offset off64_tpozwala na użycie pętli do kopiowania dużych plików, jak pokazano w odpowiedzi na połączone pytanie.
pcworld
jest to wirtten in man: „Zauważ, że pomyślne wywołanie sendfile () może zapisać mniej bajtów niż zażądano; osoba dzwoniąca powinna być przygotowana na ponowienie połączenia, jeśli nie ma bajtów. sendfile lub sendfile64 mogą wymagać wywołania w pętli, dopóki nie zostanie wykonana pełna kopia.
philippe lhardy
2

Qt ma metodę kopiowania plików:

#include <QFile>
QFile::copy("originalFile.example","copiedFile.example");

Pamiętaj, że aby go użyć, musisz zainstalować Qt (instrukcje tutaj ) i dołączyć go do swojego projektu (jeśli używasz systemu Windows i nie jesteś administratorem, możesz pobrać tutaj Qt ). Zobacz także tę odpowiedź .

Kaczor Donald
źródło
1
QFile::copyjest absurdalnie wolny z powodu buforowania 4k .
Nicolas Holthaus,
1
Powolność została naprawiona w nowszych wersjach Qt. Używam, 5.9.2a prędkość jest na równi z natywną implementacją. Btw. patrząc na kod źródłowy, Qt wydaje się faktycznie wywoływać implementację natywną.
VK,
1

Dla tych, którzy lubią boost:

boost::filesystem::path mySourcePath("foo.bar");
boost::filesystem::path myTargetPath("bar.foo");

// Variant 1: Overwrite existing
boost::filesystem::copy_file(mySourcePath, myTargetPath, boost::filesystem::copy_option::overwrite_if_exists);

// Variant 2: Fail if exists
boost::filesystem::copy_file(mySourcePath, myTargetPath, boost::filesystem::copy_option::fail_if_exists);

Zauważ, że boost :: fileystem :: path jest również dostępny jako wpath dla Unicode. I że możesz również użyć

using namespace boost::filesystem

jeśli nie podobają ci się te długie nazwy

anhoppe
źródło
Biblioteka systemu plików Boost jest jednym z wyjątków, który wymaga kompilacji. Po prostu dla ciebie!
SimonC
0

Nie jestem do końca pewien, jaki jest „dobry sposób” kopiowania pliku, ale zakładając, że „dobry” oznacza „szybki”, mógłbym nieco poszerzyć temat.

Obecne systemy operacyjne od dawna są zoptymalizowane pod kątem uruchamiania kopii pliku młyna. Żaden sprytny kawałek kodu nie pokona tego. Możliwe, że niektóre warianty technik kopiowania okażą się szybsze w niektórych scenariuszach testowych, ale najprawdopodobniej byłyby gorsze w innych przypadkach.

Zazwyczaj sendfile funkcja prawdopodobnie powraca przed zatwierdzeniem zapisu, co sprawia wrażenie, że jest szybsza niż reszta. Nie przeczytałem kodu, ale z pewnością dzieje się tak dlatego, że przydziela on własny dedykowany bufor, wymieniając pamięć na czas. I powód, dla którego nie będzie działać dla plików większych niż 2 Gb.

Tak długo, jak masz do czynienia z niewielką liczbą plików, wszystko dzieje się w różnych buforach (środowisko uruchomieniowe C ++ jest pierwsze, jeśli używasz iostream, wewnętrzne systemy operacyjne, najwyraźniej dodatkowy bufor wielkości pliku w przypadku sendfile). Rzeczywisty nośnik pamięci jest dostępny dopiero po przeniesieniu wystarczającej ilości danych, aby było warte kłopotu z zakręceniem dysku twardego.

Podejrzewam, że w niektórych przypadkach można nieco poprawić wydajność. Z czubka mojej głowy:

  • Jeśli kopiujesz ogromny plik na tym samym dysku, użycie bufora większego niż system operacyjny może nieco poprawić (ale prawdopodobnie mówimy tutaj o gigabajtach).
  • Jeśli chcesz skopiować ten sam plik do dwóch różnych fizycznych miejsc docelowych, prawdopodobnie szybciej otworzysz trzy pliki na raz niż wywołujesz dwa z nich copy_filesekwencyjnie (chociaż nie zauważysz różnicy, dopóki plik mieści się w pamięci podręcznej systemu operacyjnego)
  • Jeśli masz do czynienia z wieloma małymi plikami na dysku twardym, możesz chcieć je czytać partiami, aby zminimalizować czas wyszukiwania (chociaż system operacyjny już buforuje wpisy katalogu, aby uniknąć wyszukiwania jak szalone, a małe pliki prawdopodobnie i tak drastycznie zmniejszą przepustowość dysku).

Ale wszystko to jest poza zakresem funkcji kopiowania plików ogólnego przeznaczenia.

Tak więc, moim zdaniem przyprawionym programistą, kopia pliku C ++ powinna po prostu korzystać z file_copydedykowanej funkcji C ++ 17 , chyba że wiadomo więcej na temat kontekstu, w którym występuje kopia pliku i można opracować pewne sprytne strategie przechytrzenia systemu operacyjnego.

kuroi neko
źródło