Bir dosyayı akıllıca, güvenli ve verimli bir şekilde kopyalayın


305

Bir dosyayı (ikili veya metin) kopyalamanın iyi bir yolunu arıyorum. Birkaç örnek yazdım, herkes çalışıyor. Ama tecrübeli programcıların görüşlerini duymak istiyorum.

İyi örnekler eksik ve C ++ ile çalışan bir şekilde arama.

ANSI-C YOLLU

#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 bunu "C programlama dili" de kullanır, daha düşük düzey)

#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 YOLLU

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

KOPYA-ALGORİTMASI-C ++ - yol

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

KENDİ-TAMPONU-C ++ - yol

#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 // çekirdek gerektirir> = 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;
}

çevre

  • GNU / LINUX (Archlinux)
  • Çekirdek 3.3
  • GLIBC-2.15, LIBSTDC ++ 4.7 (GCC-LIBS), GCC 4.7, Coreutils 8.16
  • RUNLEVEL 3'ü kullanma (Çok kullanıcılı, Ağ, Terminal, GUI yok)
  • INTEL SSD-Postville 80 GB,% 50'ye kadar dolu
  • 270 MB OGG-VİDEO DOSYASI kopyalayın

Yeniden oluşturma adımları

 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

Sonuçlar (CPU TIME kullanıldı)

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  

Dosya boyutu değişmez.
sha256sum aynı sonuçları yazdırın.
Video dosyası hala oynatılabilir.

Sorular

  • Hangi yöntemi tercih edersiniz?
  • Daha iyi çözümler biliyor musunuz?
  • Kodumda herhangi bir hata görüyor musunuz?
  • Çözümden kaçınmak için bir neden biliyor musunuz?

  • FSTREAM (KISS, Streambuffer)
    Bunu gerçekten seviyorum, çünkü gerçekten kısa ve basit. Bildiğim kadarıyla operatör << rdbuf () için aşırı yüklenmiş ve hiçbir şey dönüştürmez biliyorum. Doğru?

Teşekkürler

Güncelleme 1
Dosya tanımlayıcılarının açılıp kapanması, clock () ölçümüne dahil edilecek şekilde tüm örneklerde kaynağı değiştirdim . Bunlar kaynak kodunda başka önemli bir değişiklik değildir. Sonuçlar değişmedi! Sonuçlarımı tekrar kontrol etmek için de zaman kullandım .

Güncelleme 2
ANSI C örneği değişti: while- loop'un durumu artık feof () öğesini çağırmıyor, bunun yerine fread () öğesini koşula taşıdım . Görünüşe göre, kod şimdi 10.000 saat daha hızlı çalışıyor.

Ölçüm değişti: Önceki sonuçlar her zaman arabelleğe alındı, çünkü eski komut satırını rm to.ogv && sync && time ./program 'ı her program için birkaç kez tekrarladım. Şimdi her program için sistemi yeniden başlatıyorum. Tamponsuz sonuçlar yenidir ve sürpriz değildir. Tamponsuz sonuçlar gerçekten değişmedi.

Eski kopyayı silmezsem, programlar farklı tepki verir. Bir varolan dosyanın üzerine tamponlu hızlı POSIX ve SendFile ile, tüm diğer programlar yavaştır. Belki de kısaltılmış veya oluşturulmuş seçenekler bu davranış üzerinde bir etkiye sahiptir. Ancak aynı kopya ile mevcut dosyaların üzerine yazmak gerçek bir kullanım örneği değildir.

Kopyanın cp ile gerçekleştirilmesi, 0.44 saniye arabelleksiz ve 0.30 saniye ara belleğe alınır. Yani cp POSIX örneğinden biraz daha yavaştır. Benim için iyi görünüyor.

Belki de mmap () ve copy_file()boost :: dosya sisteminden örnekler ve sonuçlar ekliyorum .

Güncelleme 3
Bunu bir blog sayfasına da koydum ve biraz genişlettim. Linux çekirdeğinden düşük seviyeli bir işlev olan splice () dahil . Belki Java ile daha fazla örnek gelecektir. http://www.ttyhoney.com/blog/?page_id=69


5
fstreamkesinlikle dosya işlemleri için iyi bir seçenektir.
chris


29
Tembel yolu unuttun: 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
Çok geç yonga için özür dilerim, ancak bunların hiçbirini 'güvenli' olarak tanımlayamazdım, çünkü herhangi bir hata işleme sahip değiller.
Richard Kettlewell

Yanıtlar:


259

Bir dosyayı akıllıca bir şekilde kopyalayın:

#include <fstream>

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

    dst << src.rdbuf();
}

Bu çok basit ve sezgisel okumak için ekstra maliyet değer. Bunu çok yapsaydık, dosya sistemine yapılan OS çağrılarına geri dönmek daha iyi. Eminim boostdosya sistemi sınıfında bir kopyalama dosyası yöntemi vardır.

Dosya sistemiyle etkileşim için bir C yöntemi vardır:

#include <copyfile.h>

int
copyfile(const char *from, const char *to, copyfile_state_t state, copyfile_flags_t flags);

29
copyfiletaşınabilir değildir; Bence Mac OS X'e özgü. Linux'ta kesinlikle mevcut değil. boost::filesystem::copy_filebir dosyayı yerel dosya sistemi üzerinden kopyalamanın muhtemelen en taşınabilir yoludur.
Mike Seymour

4
@MikeSeymour: copyfile () bir BSD uzantısı gibi görünüyor.
Martin York

10
@ duedl0r: Hayır. Nesnelerin yıkıcıları var. Akar yıkıcı otomatik olarak close () öğesini çağırır. codereview.stackexchange.com/q/540/507
Martin York

11
@ duedl0r: Evet. Ama bu "güneş batarsa" demek gibidir. Gerçekten hızlı batıya doğru koşabilir ve gününüzü biraz daha uzatabilirsiniz ama güneş batmaya başlar. Hata ve sızıntı hafızanız yoksa (kapsam dışına çıkacaktır). Ancak burada dinamik bir bellek yönetimi olmadığından bir sızıntı olamaz ve kapsam dışı kalacaktır (güneşin batması gibi).
Martin York

6
Sonra sadece bir {} kapsam bloğuna sarın
paulm

62

C ++ 17 ile bir dosyayı kopyalamanın standart yolu <filesystem>başlık dahil olmak ve aşağıdakileri kullanmaktır:

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

İlk form, copy_options::noneseçenek olarak kullanılan ikinci formla eşdeğerdir (ayrıca bkz copy_file.).

filesystemKütüphane aslen olarak geliştirildi boost.filesystemve nihayet C ++ 17 itibariyle ++ ISO C'ye birleşti.


2
Neden varsayılan argümanı olan tek bir fonksiyon yok 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 Bundan emin değilim. Belki de önemli değil .
manlio

@ Standart kütüphanedeki mutfak, temiz kod çok önemlidir. Aşırı yüklere sahip olmak (varsayılan parametreleri olan bir fonksiyonun aksine) programcının niyetini daha net hale getirir.
Marc.2377

@Peter C ++ 17'nin mevcut olduğu göz önüne alındığında, bu muhtemelen kabul edilen cevap olmalıdır.
Martin York

21

Çok fazla!

"ANSI C" yol arabelleği FILE, zaten arabelleğe alınmış olduğundan gereksizdir . (Bu dahili tamponun boyutu, BUFSIZaslında tanımladığı şeydir .)

"OWN-BUFFER-C ++ - WAY" geçtikçe yavaş olacaktır fstream, bu da çok fazla sanal gönderme yapar ve yine dahili arabellekleri veya her akış nesnesini korur. ( streambuf_iteratorSınıf, akış katmanını atladığı için "COPY-ALGORITHM-C ++ - WAY" buna maruz kalmaz .)

"KOPYA-ALGORİTMA-C ++ - YOL" tercih ederim, ancak bir gerçek biçimlendirme gerekmediğinde fstream, sadece çıplak std::filebuförnekler oluşturun .

Ham performans için POSIX dosya tanımlayıcılarını yenemezsiniz. Çirkin ama taşınabilir ve her platformda hızlı.

Linux yolu inanılmaz derecede hızlı görünüyor - belki de işletim sistemi I / O bitmeden fonksiyonun geri dönmesine izin veriyor? Her durumda, bu birçok uygulama için yeterince taşınabilir değildir.

EDIT : Ah, "yerli Linux" okuma ve yazma zaman uyumsuz G / Ç ile araya koyarak performansı artıyor olabilir. Komutların birikmesine izin vermek, disk sürücüsünün ne zaman arama yapılacağına karar vermesine yardımcı olabilir. Karşılaştırma için Boost Asio veya pthreads'yi deneyebilirsiniz. "POSIX dosya tanımlayıcılarını yenemiyorum" ise… sadece körü körüne kopyalamakla kalmayıp verilerle bir şey yapıyorsanız bu doğrudur.


ANSI C: Ama fread / fwrite işlevini bir boyuta mı vermem gerekiyor? pubs.opengroup.org/onlinepubs/9699919799/toc.htm
Peter

@PeterWeber Evet, BUFSIZ'in herhangi bir değer kadar iyi olduğu ve her seferinde bir veya "sadece birkaç" karaktere göre işleri hızlandıracağı doğrudur. Her neyse, performans ölçümü her durumda en iyi yöntem olmadığını ortaya koyuyor.
Potatoswatter

1
Bu konuda derinlemesine bir anlayışım yok, bu yüzden varsayımlara ve görüşlere dikkat etmeliyim. Linux-Way, Kernelspace afaik'te çalışır. Bu, Kernelspace ve Userpace arasında yavaş Bağlam Değiştirmeden kaçınmalıdır. Yarın sendfile'ın sayfasına bakacağım. Bir süre önce Linus Torvalds, ağır işler için Kullanıcı Alanı-Dosya Sistemlerini sevmediğini söyledi. Belki sendfile bu görüşe olumlu bir örnek olabilir?
Peter

5
"Birsendfile() dosya tanımlayıcı ile diğeri arasında kopyalar verileri bu kopyalama çekirdek içinde yapıldığından, sendfile()kombinasyonu daha etkilidir read(2)ve write(2)ve kullanıcı uzaydan veri aktarımı gerektirecektir.": Kernel.org/doc/man-pages /online/pages/man2/sendfile.2.html
Max Lybbert

1
Ham filebufnesnelerin kullanımına örnek verebilir misiniz ?
Kerrek SB

14

Sendfile () kullanarak LINUX yönteminin 2GB'tan büyük dosyaları kopyalayamaması açısından büyük bir sorun olduğunu çok önemli not etmek istiyorum ! Bu soruyu takiben uyguladım ve sorunları vuruyordum çünkü birçok GB boyutunda HDF5 dosyalarını kopyalamak için kullanıyordum.

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

sendfile () en çok 0x7ffff000 (2,147,479,552) bayt aktarır ve gerçekte aktarılan bayt sayısını döndürür. (Bu hem 32 bit hem de 64 bit sistemler için geçerlidir.)


1
sendfile64 () aynı problemi var mı?
graywolf

1
@Paladin Bu sınırlamayı aşmak için sendfile64 geliştirilmiş gibi görünüyor. Man sayfasından: "" "Orijinal Linux sendfile () sistem çağrısı, büyük dosya ofsetlerini işlemek için tasarlanmamıştır. Sonuç olarak, Linux 2.4 ofset argümanı için daha geniş bir tiple sendfile64 () ekledi. Glibc sendfile () sarma işlevi şeffaf bir şekilde çekirdek farklılıkları ile ilgilenir. "" "
rveale

sendfile64, aynı sorun gibi görünüyor. Ancak, ofset türünün off64_tkullanılması , bağlanan sorunun cevabında gösterildiği gibi büyük dosyaları kopyalamak için bir döngü kullanılmasına izin verir .
pcworld

bu adamda wirtten olur: 'Başarılı bir sendfile () çağrısının istenenden daha az bayt yazabileceğini unutmayın; gönderilmemiş baytlar varsa arayan kişi aramayı yeniden denemeye hazır olmalıdır. ' sendfile veya sendfile64'ün tam kopya yapılana kadar bir döngü içinde çağrılması gerekebilir.
philippe lhardy

2

Qt'nin dosyaları kopyalamak için bir yöntemi vardır:

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

Bunu kullanmak için Qt ( buradaki talimatlar ) yüklemeniz ve projenize eklemeniz gerektiğini unutmayın (Windows kullanıyorsanız ve yönetici değilseniz, Qt'yi buradan indirebilirsiniz ). Ayrıca bu cevaba bakınız .


1
QFile::copy4k tamponlama nedeniyle gülünç yavaş .
Nicolas Holthaus

1
Yavaşlık, yeni sürümlerinde giderildi Qt. Kullanıyorum 5.9.2ve hızı yerel uygulama ile eşit. Btw. kaynak koduna baktığımızda, Qt aslında yerel uygulamayı çağırıyor gibi görünüyor.
VK

1

Güçlendirmeyi sevenler için:

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

Not boost :: dosya sistemi :: yolu olarak da kullanılabilir wpath Unicode için. Ve ayrıca kullanabilirsiniz

using namespace boost::filesystem

bu uzun tip isimleri sevmiyorsanız


Boost'un dosya sistemi kütüphanesi, derlenmesini gerektiren istisnalardan biridir. Sadece FYI!
SimonC

0

Bir dosyayı kopyalamanın "iyi bir yolunun" ne olduğundan tam olarak emin değilim, ama "iyi" nin "hızlı" anlamına geldiği varsayılırsa, konuyu biraz genişletebilirim.

Mevcut işletim sistemleri, değirmen dosyası kopyasının çalıştırılmasıyla uzun süre optimize edilmiştir. Zekice bir kod parçası bunu yenemez. Kopyalama tekniklerinizin bazı varyantlarının bazı test senaryolarında daha hızlı kanıtlanması mümkündür, ancak büyük olasılıkla diğer durumlarda daha kötü olur.

Genellikle sendfile işlev muhtemelen yazma işlemi tamamlanmadan önce geri döner, böylece diğerlerinden daha hızlı olduğu izlenimi verir. Ben kodu okumadım, ama kesinlikle kendi özel tampon, zaman için bellek ticaret tahsis çünkü. Ve 2Gb'den büyük dosyalar için çalışmadığının nedeni.

Az sayıda iostreamdosyayla uğraştığınız sürece, her şey çeşitli arabelleklerde gerçekleşir (C ++ çalışma zamanını ilk kullanıyorsanız , işletim sistemi dahili olanları, görünüşe göre dosya boyutunda ekstra bir tampon sendfile). Gerçek depolama ortamına yalnızca sabit diski döndürme zahmetine değecek kadar veri taşındığında erişilebilir.

Belirli durumlarda performansları biraz iyileştirebileceğinizi düşünüyorum. Kafamın üstünden:

  • Aynı diske büyük bir dosya kopyalıyorsanız, işletim sistemlerinden daha büyük bir arabellek kullanmak işleri biraz iyileştirebilir (ancak muhtemelen burada gigabaytlardan bahsediyoruz).
  • Aynı dosyayı iki farklı fiziksel hedefe kopyalamak istiyorsanız, muhtemelen üç dosyayı aynı anda iki copy_filesırayı çağırmaktan daha hızlı açabilirsiniz (dosya işletim sistemi önbelleğine sığdığı sürece farkı fark edemezsiniz).
  • Bir HDD'de çok sayıda küçük dosyayla uğraşıyorsanız, arama süresini en aza indirmek için bunları toplu olarak okumak isteyebilirsiniz (işletim sistemi, çılgın ve küçük dosyalar gibi aramalardan kaçınmak için zaten dizin girişlerini önbelleğe alsa da disk bant genişliğini büyük ölçüde azaltacaktır).

Ancak tüm bunlar genel amaçlı bir dosya kopyalama fonksiyonunun kapsamı dışındadır.

Bu yüzden tartışmalı tecrübeli programcımın görüşüne göre, file_copydosya kopyasının oluştuğu bağlam hakkında daha fazla bilgi verilmediği ve işletim sistemini zekice yürütmek için bazı akıllı stratejiler tasarlanmadıkça , bir C ++ dosya kopyası yalnızca C ++ 17 özel işlevini kullanmalıdır .

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.