Salin file dengan cara yang waras, aman dan efisien

305

Saya mencari cara yang baik untuk menyalin file (biner atau teks). Saya telah menulis beberapa sampel, semua orang bekerja. Tapi saya ingin mendengar pendapat programmer berpengalaman.

Saya kehilangan contoh yang bagus dan mencari cara yang berfungsi dengan 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 menggunakan ini dalam "Bahasa pemrograman C", level lebih rendah)

#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 ++ - CARA

#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;
}

SENDIRI-BUFFER-C ++ - CARA

#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 // membutuhkan kernel> = 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;
}

Lingkungan Hidup

  • GNU / LINUX (Archlinux)
  • Kernel 3.3
  • GLIBC-2.15, LIBSTDC ++ 4.7 (GCC-LIBS), GCC 4.7, Coreutils 8.16
  • Menggunakan RUNLEVEL 3 (Multiuser, Jaringan, Terminal, tanpa GUI)
  • INTEL SSD-Postville 80 GB, diisi hingga 50%
  • Salin 270 MB OGG-VIDEO-FILE

Langkah-langkah mereproduksi

 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

Hasil (CPU TIME digunakan)

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  

Ukuran file tidak berubah.
sha256sum mencetak hasil yang sama.
File video masih dapat diputar.

Pertanyaan

  • Metode apa yang Anda inginkan?
  • Apakah Anda tahu solusi yang lebih baik?
  • Apakah Anda melihat kesalahan dalam kode saya?
  • Apakah Anda tahu alasan untuk menghindari solusi?

  • FSTREAM (KISS, Streambuffer)
    Saya sangat suka yang ini, karena sangat pendek dan sederhana. Sejauh yang saya tahu operator << kelebihan beban untuk rdbuf () dan tidak mengubah apa pun. Benar?

Terima kasih

Pembaruan 1
Saya mengubah sumber dalam semua sampel dengan cara itu, bahwa buka dan tutup deskriptor file termasuk dalam pengukuran clock () . Tidak ada perubahan signifikan lainnya dalam kode sumber. Hasilnya tidak berubah! Saya juga menggunakan waktu untuk memeriksa ulang hasil saya.

Pembaruan 2
sampel ANSI C berubah: Kondisi while-loop tidak memanggil feof lagi () alih-alih saya pindah ketakutan () ke dalam kondisi. Sepertinya, kode itu sekarang berjalan 10.000 jam lebih cepat.

Pengukuran berubah: Hasil sebelumnya selalu buffer, karena saya mengulangi baris perintah lama rm to.ogv && sync && time ./program untuk setiap program beberapa kali. Sekarang saya reboot sistem untuk setiap program. Hasil unbuffered adalah baru dan tidak menunjukkan kejutan. Hasil unbuffered tidak benar-benar berubah.

Jika saya tidak menghapus salinan lama, program bereaksi berbeda. Menimpa file yang ada buffered lebih cepat dengan POSIX dan SENDFILE, semua program lain lebih lambat. Mungkin opsi memotong atau membuat berdampak pada perilaku ini. Tetapi menimpa file yang sudah ada dengan salinan yang sama bukan kasus penggunaan dunia nyata.

Melakukan salinan dengan cp membutuhkan 0,44 detik tanpa buffer dan 0,30 detik tanpa buffer. Jadi cp sedikit lebih lambat dari sampel POSIX. Terlihat bagus untukku.

Mungkin saya menambahkan juga sampel dan hasil mmap () dan copy_file()dari boost :: filesystem.

Perbarui 3
Saya telah menempatkan ini juga di halaman blog dan sedikit diperluas. Termasuk splice () , yang merupakan fungsi tingkat rendah dari kernel Linux. Mungkin lebih banyak sampel dengan Java akan mengikuti. http://www.ttyhoney.com/blog/?page_id=69

Peter
sumber
5
fstreamjelas merupakan pilihan yang baik untuk operasi file.
chris
29
Anda lupa cara malas: 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
Maaf karena memotong terlalu terlambat, tapi saya tidak akan menggambarkan semua ini sebagai 'aman', karena mereka tidak memiliki kesalahan penanganan.
Richard Kettlewell

Jawaban:

259

Salin file dengan cara yang waras:

#include <fstream>

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

    dst << src.rdbuf();
}

Ini sangat sederhana dan intuitif untuk membacanya sepadan dengan biaya tambahan. Jika kita sering melakukannya, lebih baik kembali menggunakan panggilan OS ke sistem file. Saya yakin boostmemiliki metode menyalin file di kelas sistem file-nya.

Ada metode C untuk berinteraksi dengan sistem file:

#include <copyfile.h>

int
copyfile(const char *from, const char *to, copyfile_state_t state, copyfile_flags_t flags);
Martin York
sumber
29
copyfiletidak portabel; Saya pikir ini khusus untuk Mac OS X. Itu pasti tidak ada di Linux. boost::filesystem::copy_filemungkin merupakan cara paling portabel untuk menyalin file melalui sistem file asli.
Mike Seymour
4
@MikeSeymour: copyfile () tampaknya merupakan ekstensi BSD.
Martin York
10
@ duedl0r: Tidak. Objek memiliki destruktor. Destuctor untuk stream secara otomatis memanggil close (). codereview.stackexchange.com/q/540/507
Martin York
11
@ duedl0r: Ya. Tapi itu seperti mengatakan "jika matahari terbenam". Anda dapat berlari sangat cepat ke barat dan Anda dapat membuat hari Anda sedikit lebih lama tetapi matahari akan terbenam. Kecuali Anda memiliki memori bug dan kebocoran (itu akan keluar dari ruang lingkup). Tetapi karena tidak ada manajemen memori dinamis di sini tidak mungkin ada kebocoran dan mereka akan keluar dari ruang lingkup (seperti matahari akan terbenam).
Martin York
6
Kemudian cukup bungkus dalam blok {} scope
paulm
62

Dengan C ++ 17 cara standar untuk menyalin file akan termasuk <filesystem>header dan menggunakan:

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);

Bentuk pertama setara dengan yang kedua dengan copy_options::nonedigunakan sebagai opsi (lihat juga copy_file).

The filesystemperpustakaan pada awalnya dikembangkan sebagai boost.filesystemdan akhirnya bergabung untuk ISO C ++ sebagai C ++ 17.

manlio
sumber
2
Mengapa tidak ada fungsi tunggal dengan argumen default, seperti 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 Saya tidak yakin tentang ini. Mungkin itu tidak terlalu penting .
manlio
@Jepessen di perpustakaan standar, kode bersih adalah yang terpenting. Memiliki kelebihan (sebagai lawan dari satu fungsi dengan parameter default) membuat niat programmer lebih jelas.
Marc.2377
@ Peter Ini sekarang mungkin jawaban yang diterima mengingat C ++ 17 tersedia.
Martin York
21

Terlalu banyak!

Buffer cara "ANSI C" berlebihan, karena a FILEsudah buffered. (Ukuran buffer internal ini adalah apa yang BUFSIZsebenarnya mendefinisikan.)

"SENDIRI-BUFFER-C ++ - WAY" akan lambat saat berjalan fstream, yang melakukan banyak pengiriman virtual, dan sekali lagi mempertahankan buffer internal atau setiap objek stream. (The "COPY-ALGORITHM-C ++ - WAY" tidak menderita ini, karena streambuf_iteratorkelas melewati layer stream.)

Saya lebih suka "COPY-ALGORITHM-C ++ - WAY", tetapi tanpa membuat sebuah fstream, cukup buat std::filebufcontoh kosong saat tidak diperlukan pemformatan yang sebenarnya.

Untuk kinerja mentah, Anda tidak dapat mengalahkan deskriptor file POSIX. Ini jelek tapi portabel dan cepat pada platform apa pun.

Cara Linux tampaknya sangat cepat - mungkin OS membiarkan fungsinya kembali sebelum I / O selesai? Bagaimanapun, itu tidak cukup portabel untuk banyak aplikasi.

EDIT : Ah, "Linux asli" dapat meningkatkan kinerja dengan interleaving membaca dan menulis dengan I / O yang tidak sinkron. Membiarkan perintah menumpuk dapat membantu driver disk memutuskan kapan yang terbaik untuk dicari. Anda dapat mencoba Boost Asio atau pthreads untuk perbandingan. Adapun "tidak dapat mengalahkan deskriptor file POSIX" ... yah itu benar jika Anda melakukan sesuatu dengan data, tidak hanya menyalin secara membabi buta.

Potatoswatter
sumber
ANSI C: Tapi saya harus memberi ukuran fungsi fread / fwrite? pubs.opengroup.org/onlinepubs/9699919799/toc.htm
Peter
@ PeterWeber Yah, ya, memang benar bahwa BUFSIZ adalah nilai yang sama baiknya dengan BUFSIZ, dan mungkin akan mempercepat beberapa hal relatif terhadap satu atau "hanya beberapa" karakter dalam satu waktu. Bagaimanapun, pengukuran kinerja menunjukkan bahwa itu bukan metode terbaik dalam hal apa pun.
Potatoswatter
1
Saya tidak memiliki pemahaman yang mendalam tentang hal ini, jadi saya harus berhati-hati dengan asumsi dan pendapat. Linux-Way berjalan di Kernelspace afaik. Ini harus menghindari Pergantian Konteks lambat antara Kernelspace dan Userspace? Besok saya akan melihat lagi halaman manual dari sendfile. Beberapa waktu lalu Linus Torvalds mengatakan dia tidak suka Userspace-Filesystems untuk pekerjaan berat. Mungkin sendfile adalah contoh positif untuk pandangannya?
Peter
5
" sendfile()Menyalin data antara satu deskriptor file dan yang lain. Karena penyalinan ini dilakukan di dalam kernel, sendfile()lebih efisien daripada kombinasi dari read(2)dan write(2), yang akan memerlukan transfer data ke dan dari ruang pengguna.": kernel.org/doc/man-pages /online/pages/man2/sendfile.2.html
Max Lybbert
1
Bisakah Anda memposting contoh menggunakan filebufobjek mentah ?
Kerrek SB
14

Saya ingin membuat catatan yang sangat penting bahwa metode LINUX menggunakan sendfile () memiliki masalah besar karena tidak dapat menyalin file yang berukuran lebih dari 2GB! Saya telah menerapkannya setelah pertanyaan ini dan sedang mengalami masalah karena saya menggunakannya untuk menyalin file HDF5 yang berukuran banyak GB.

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

sendfile () akan mentransfer paling banyak 0x7ffff000 (2.147.479.552) byte, mengembalikan jumlah byte yang sebenarnya ditransfer. (Ini berlaku untuk sistem 32-bit dan 64-bit.)

rveale
sumber
1
apakah sendfile64 () memiliki masalah yang sama?
graywolf
1
@Paladin Tampaknya sendfile64 dikembangkan untuk mengatasi batasan ini. Dari halaman manual: "" "Panggilan sistem Linux sendfile () asli tidak dirancang untuk menangani offset file besar. Akibatnya, Linux 2.4 menambahkan sendfile64 (), dengan tipe yang lebih luas untuk argumen offset. Fungsi pembungkus glibc sendfile () wrapper secara transparan menangani perbedaan kernel. "" "
rveale
sendfile64 tampaknya memiliki masalah yang sama . Namun penggunaan tipe offset off64_tmemungkinkan seseorang untuk menggunakan loop untuk menyalin file besar seperti yang ditunjukkan dalam jawaban untuk pertanyaan terkait.
pcworld
ini wirtten in man: 'Perhatikan bahwa panggilan yang berhasil ke sendfile () dapat menulis lebih sedikit byte daripada yang diminta; penelepon harus siap untuk mencoba kembali panggilan jika ada byte yang tidak terkirim. ' sendfile atau sendfile64 mungkin perlu dipanggil dalam satu lingkaran sampai salinan lengkap dilakukan.
philippe lhardy
2

Qt memiliki metode untuk menyalin file:

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

Perhatikan bahwa untuk menggunakan ini Anda harus menginstal Qt (instruksi di sini ) dan memasukkannya ke dalam proyek Anda (jika Anda menggunakan Windows dan Anda bukan administrator, Anda dapat mengunduh Qt di sini ). Lihat juga jawaban ini .

Donald Duck
sumber
1
QFile::copysangat lambat karena itu buffering 4k .
Nicolas Holthaus
1
Kelambatan telah diperbaiki di versi yang lebih baru Qt. Saya menggunakan 5.9.2dan kecepatannya setara dengan implementasi asli. Btw. melihat kode sumber, Qt tampaknya benar-benar memanggil implementasi asli.
VK
1

Bagi mereka yang suka meningkatkan:

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);

Perhatikan bahwa boost :: filesystem :: path juga tersedia sebagai wpath untuk Unicode. Dan itu juga bisa Anda gunakan

using namespace boost::filesystem

jika Anda tidak suka nama-nama jenis panjang

anhoppe
sumber
Pustaka filesystem Boost adalah salah satu pengecualian yang mengharuskannya dikompilasi. FYI saja!
SimonC
0

Saya tidak yakin apa "cara yang baik" untuk menyalin file, tetapi dengan asumsi "baik" berarti "cepat", saya bisa sedikit memperluas subjek.

Sistem operasi saat ini telah lama dioptimalkan untuk menangani menjalankan salinan file pabrik. Tidak ada kode sedikit pun yang akan mengalahkan itu. Ada kemungkinan bahwa beberapa varian teknik penyalinan Anda akan terbukti lebih cepat dalam beberapa skenario pengujian, tetapi kemungkinan besar itu akan lebih buruk dalam kasus lain.

Biasanya, sendfilefungsi tersebut mungkin kembali sebelum penulisan dilakukan, sehingga memberi kesan lebih cepat daripada yang lain. Saya belum membaca kode, tetapi pasti karena ia mengalokasikan buffer khusus, memori perdagangan untuk waktu. Dan alasan mengapa itu tidak akan berfungsi untuk file yang lebih besar dari 2Gb.

Selama Anda berurusan dengan sejumlah kecil file, semuanya terjadi di dalam berbagai buffer (runtime C ++ pertama jika Anda gunakan iostream, yang internal OS, tampaknya buffer tambahan berukuran file dalam kasus sendfile). Media penyimpanan aktual hanya diakses setelah data yang cukup telah dipindahkan sehingga layak untuk kesulitan memutar hard disk.

Saya kira Anda dapat sedikit meningkatkan kinerja dalam kasus-kasus tertentu. Dari atas kepala saya:

  • Jika Anda menyalin file besar pada disk yang sama, menggunakan buffer yang lebih besar daripada OS mungkin akan sedikit memperbaiki keadaan (tapi kami mungkin berbicara tentang gigabyte di sini).
  • Jika Anda ingin menyalin file yang sama pada dua tujuan fisik yang berbeda, Anda mungkin akan lebih cepat membuka tiga file sekaligus daripada memanggil dua copy_filesecara berurutan (meskipun Anda tidak akan melihat perbedaannya selama file tersebut sesuai dengan cache OS)
  • Jika Anda berurusan dengan banyak file kecil pada HDD, Anda mungkin ingin membacanya secara batch untuk meminimalkan waktu pencarian (meskipun OS sudah menyimpan entri direktori untuk menghindari mencari seperti file gila dan kecil kemungkinan akan mengurangi bandwidth disk secara dramatis).

Namun semua itu berada di luar ruang lingkup fungsi penyalinan file tujuan umum.

Jadi menurut pendapat programmer berpengalaman saya, salinan file C ++ harus hanya menggunakan file_copyfungsi khusus C ++ 17 , kecuali lebih banyak diketahui tentang konteks di mana salinan file terjadi dan beberapa strategi pintar dapat dirancang untuk mengakali OS.

kuroi neko
sumber