安全で効率的な方法でファイルをコピーする


305

ファイル(バイナリまたはテキスト)をコピーするための適切な方法を検索します。私はいくつかのサンプルを書きましたが、誰もが機能します。でもベテランプログラマの意見を聞きたいです。

良い例がないので、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はこれを「Cプログラミング言語」で使用し、より低レベル)

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

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

OWN-BUFFER-C ++-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);

    // 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 //カーネルが必要> = 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;
}

環境

  • GNU / LINUX(Archlinux)
  • カーネル3.3
  • GLIBC-2.15、LIBSTDC ++ 4.7(GCC-LIBS)、GCC 4.7、Coreutils 8.16
  • RUNLEVEL 3の使用(マルチユーザー、ネットワーク、ターミナル、GUIなし)
  • INTEL SSD-Postville 80 GB、最大50%
  • 270 MBのOGG-VIDEO-FILEをコピーする

再現する手順

 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

結果(使用したCPU時間)

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  

ファイルサイズは変わりません。
sha256sumは同じ結果を出力します。
ビデオファイルはまだ再生可能です。

ご質問

  • どの方法を選びますか?
  • より良い解決策を知っていますか?
  • 私のコードに間違いはありますか?
  • 解決策を回避する理由を知っていますか?

  • FSTREAM(KISS、Streambuffer)
    本当に短くてシンプルなので、私はこれが本当に好きです。これまでのところ、演算子<<はrdbuf()に対してオーバーロードされており、何も変換しません。正しい?

ありがとう

Update 1
すべてのサンプルでソースをそのように変更しました。ファイル記述子のオープンとクローズがclock()の測定に含まれるようにしました。それらはソースコードの他の重要な変更ではありません。結果は変わりません!私はまた、使用時間を私の結果をダブルチェックします。

Update 2
ANSI Cサンプルが変更されました。while ループの条件はもはやfeof()を呼び出さず、代わりにfread()を条件に移動しました。どうやら、コードの実行速度が10,000クロック速くなりました。

測定値が変更されました:以前の結果は常にバッファされました。これは、各コマンドに対して古いコマンドラインrm to.ogv && sync && time ./programを数回繰り返したためです。すべてのプログラムでシステムを再起動します。バッファリングされていない結果は新しいものであり、驚きはありません。バッファリングされていない結果は実際には変わりませんでした。

古いコピーを削除しないと、プログラムの反応が異なります。バッファリングされた既存のファイルの上書きはPOSIXとSENDFILEの方が速く、他のすべてのプログラムは遅くなります。おそらく、オプションが切り捨てられる作成されることが、この動作に影響を与えます。ただし、同じコピーで既存のファイルを上書きすることは、実際のユースケースではありません。

cpを使用してコピーを実行すると、バッファーなしで0.44秒、バッファー済みで0.30秒かかります。したがって、cpはPOSIXサンプルよりも少し遅いです。私には元気に見えます。

多分私はまたmmap()のサンプルと結果とcopy_file()boost :: filesystemから追加します。

Update 3
ブログのページにも掲載し、少し拡張しました。Linuxカーネルの低レベル関数であるsplice()を含みます。Javaを使用したサンプルがさらに続く可能性があります。 http://www.ttyhoney.com/blog/?page_id=69


5
fstream間違いなく、ファイル操作に適したオプションです。
クリス


29
怠惰な方法を忘れました:system( "cp from.ogv to.ogv");
fbafelipe 2012

3
#include <copyfile.h> copyfile(const char *from, const char *to, copyfile_state_t state, copyfile_flags_t flags);
Martin York

3
遅くなってすみませんでしたが、エラー処理がないため、「安全」とは言えません。
Richard Kettlewell

回答:


259

正常な方法でファイルをコピーします。

#include <fstream>

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

    dst << src.rdbuf();
}

これは非常にシンプルで直感的に読むことができ、追加のコストに見合う価値があります。多くの場合、ファイルシステムへのOS呼び出しを使用することをお勧めします。私は確信してboostそのファイルシステムのクラスでコピーファイルの方法があります。

ファイルシステムと対話するためのCメソッドがあります。

#include <copyfile.h>

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

29
copyfileポータブルではありません。それはMac OS Xに固有のものだと思います。確かにLinuxには存在しません。boost::filesystem::copy_fileおそらくネイティブファイルシステムを介してファイルをコピーする最もポータブルな方法です。
Mike Seymour

4
@MikeSeymour:copyfile()はBSD拡張のようです。
マーティンヨーク

10
@ duedl0r:いいえ。オブジェクトにはデストラクタがあります。ストリームのデストラクタは自動的にclose()を呼び出します。codereview.stackexchange.com/q/540/507
Martin York

11
@ duedl0r:はい。しかし、それは「太陽が沈むなら」と言っているようなものです。あなたは本当に速く西に走ることができ、あなたは一日を少し長くするかもしれませんが、太陽は沈むでしょう。バグがなく、メモリがリークしない限り(範囲外になります)。しかし、ここには動的なメモリ管理がないため、リークが発生することはなく、それらは範囲外になります(太陽が沈むように)。
マーティンヨーク

6
次に、単にそれを{}スコープブロックでラップします
paulm

62

C ++ 17では、ファイルをコピーする標準的な方法は、<filesystem>ヘッダーを含めて以下を使用することです。

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

最初の形式はcopy_options::none、オプションとして使用される2番目の形式と同等です(も参照copy_file)。

filesystemライブラリは、もともととして開発されたboost.filesystem、最終的にC ++ 17のようISO C ++にマージ。


2
なぜデフォルトの引数を持つ単一の関数がないのbool copy_file( const std::filesystem::path& from, const std::filesystem::path& to, std::filesystem::copy_options options = std::filesystem::copy_options::none);ですか?
Jepessen、2017年

2
@Jepessenこれについてはよくわかりません。多分それは問題はありません
manlio

標準ライブラリの@Jepessenでは、クリーンなコードが最も重要です。(デフォルトのパラメーターを持つ1つの関数とは対照的に)オーバーロードがあると、プログラマーの意図がより明確になります。
Marc.2377

@Peterこれはおそらく、C ++ 17が利用可能であることを考えると、おそらく受け入れられる答えになるはずです。
マーティンヨーク

21

多すぎる!

「ANSI C」ウェイバッファは、a FILEがすでにバッファリングされているため冗長です。(この内部バッファーのサイズは、BUFSIZ実際に定義するものです。)

「OWN-BUFFER-C ++-WAY」はfstream、を通過するときに遅くなります。これは、大量の仮想ディスパッチを実行し、再び内部バッファーまたは各ストリームオブジェクトを維持します。( "COPY-ALGORITHM-C ++-WAY"は、streambuf_iteratorクラスがストリームレイヤーをバイパスするため、これに影響されません。)

私は「COPY-ALGORITHM-C ++-WAY」を好みますがfstream、を作成せずstd::filebufに、実際のフォーマットが必要ない場合は、ベアインスタンスを作成します。

生のパフォーマンスについては、POSIXファイル記述子に勝るものはありません。醜いですが、移植性があり、どのプラットフォームでも高速です。

Linuxの方法は信じられないほど高速であるように見えます。おそらくOSは、I / Oが完了する前に関数に関数を返させますか?いずれにせよ、それは多くのアプリケーションにとって十分に移植可能ではありません。

編集:ああ、「ネイティブLinux」は、非同期I / Oで読み取りと書き込みをインターリーブすることでパフォーマンスを向上させている可能性があります。コマンドを積み重ねることは、ディスクドライバーがシークするのに最適なタイミングを決定するのに役立ちます。比較のためにBoost Asioまたはpthreadsを試してみてください。「POSIXファイル記述子を打ち負かすことはできません」とは...盲目的にコピーするだけでなく、データを操作している場合も同様です。


ANSI C:関数fread / fwriteにサイズを指定する必要がありますか?pubs.opengroup.org/onlinepubs/9699919799/toc.htm
Peter

@PeterWeberええ、そうです。BUFSIZは他のどの値よりも優れた値であり、一度に1つまたは「ほんの数個」の文字に比べて速度を上げる可能性があります。とにかく、パフォーマンス測定は、それがいかなる場合でも最良の方法ではないことを証明しています。
Potatoswatter 2012

1
私はこれについて深く理解していないので、私は仮定と意見に注意する必要があります。Linux-WayはKernelspace afaikで実行されます。これにより、カーネルスペースとユーザースペースの間の遅いコンテキスト切り替えが回避されますか?明日は、sendfileのマンページをもう一度見てみましょう。少し前に、Linus Torvaldsは、重い仕事のためのUserspace-Filesystemsは好きではないと述べました。たぶん、sendfileは彼の見解の好例でしょうか?
Peter

5
" sendfile()あるファイル記述子と別のファイル記述子の間でデータをコピーします。このコピーはカーネル内で行われるため、sendfile()との組み合わせよりも効率的でread(2)ありwrite(2)、ユーザー空間との間でデータを転送する必要があります。": kernel.org/doc/man-pages /online/pages/man2/sendfile.2.html
Max Lybbert

1
filebufオブジェクトの使用例を投稿できますか?
Kerrek SB、2014

14

sendfile()を使用するLINUXメソッドには、2GBを超えるサイズのファイルをコピーできないという大きな問題があるという非常に重要な注意を述べたいと思います。この質問に続いて実装しましたが、サイズが数GBのHDF5ファイルをコピーするために使用していたため、問題が発生していました。

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

sendfile()は最大で0x7ffff000(2,147,479,552)バイトを転送し、実際に転送されたバイト数を返します。(これは、32ビットシステムと64ビットシステムの両方に当てはまります。)


1
sendfile64()には同じ問題がありますか?
グレイウルフ2016

1
@Paladin sendfile64はこの制限を回避するために開発されたようです。manページから: "" "元のLinux sendfile()システムコールは、大きなファイルオフセットを処理するように設計されていませんでした。そのため、Linux 2.4は、オフセット引数により広いタイプのsendfile64()を追加しました。glibcs​​endfile()ラッパー関数カーネルの違いを透過的に扱います。 "" "
rveale

sendfile64には同じ問題があるようです。ただし、オフセットタイプoff64_tを使用すると、リンクされた質問の回答に示されているように、ループを使用して大きなファイルをコピーできます。
pcworld 2017年

これは人間の場合、次のように書かれています。 'sendfile()の呼び出しが成功すると、要求されたよりも少ないバイト数が書き込まれる可能性があります。未送信のバイトがあった場合、呼び出し元は呼び出しを再試行する準備をする必要があります。sendfileまたはsendfile64は、完全なコピーが完了するまでループ内で呼び出す必要がある場合があります。
フィリップ・ラルディ

2

Qtにはファイルをコピーする方法があります。

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

これを使用するには、Qtインストールする必要があります。(手順をここに(Windowsを使用していて、管理者がいないのであれば、あなたはQtのをダウンロードすることができます)と、プロジェクトに含めるここ代わりに)。この回答もご覧ください。


1
QFile::copy4kバッファリングのため、途方もなく遅いです。
Nicolas Holthaus、2017年

1
新しいバージョンので速度が低下する問題が修正されましたQt。私が使用して5.9.2おり、速度はネイティブ実装と同等です。ところで ソースコードを見ると、Qtは実際にネイティブ実装を呼び出しているようです。
VK

1

ブーストが好きな人のために:

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

boost :: filesystem :: pathに注意してください。は、Unicodeのwpathとしても使用できる。そして、あなたも使うことができた

using namespace boost::filesystem

それらの長い型名が気に入らない場合


Boostのファイルシステムライブラリは、コンパイルする必要がある例外の1つです。ほんとに!
SimonC、

0

ファイルをコピーする「良い方法」が何であるかはよくわかりませんが、「良い」が「速い」という意味であると仮定すると、主題を少し広げることができます。

現在のオペレーティングシステムは、millファイルコピーの実行に対処するために長い間最適化されてきました。巧妙なコードはそれを打ち負かしません。コピーテクニックの一部のバリアントは、一部のテストシナリオではより速く証明される可能性がありますが、他のケースではおそらくうまくいかないでしょう。

通常、 sendfile関数はおそらく書き込みがコミットされる前に戻る関数よりも高速であるという印象を与えます。私はコードを読んだことはありませんが、それは間違いなく、専用のバッファーを割り当て、メモリを時間と交換しているためです。そして、それが2Gbより大きいファイルに対して機能しない理由。

少数のファイルを処理している限り、すべてがさまざまなバッファー内で発生します(C ++ランタイムの最初に使用するiostream場合はOS内部のバッファー、明らかにの場合はファイルサイズの余分なバッファーsendfile)。実際のストレージメディアにアクセスするのは、ハードディスクを回転させる問題に見合うだけの十分なデータが移動されてからです。

特定のケースでパフォーマンスを少し改善できると思います。私の頭の上から:

  • 同じディスクに巨大なファイルをコピーする場合、OSのバッファーよりも大きいバッファーを使用すると、状況が少し改善される可能性があります(ただし、ここではギガバイトについて話している可能性があります)。
  • 同じファイルを2つの異なる物理的な宛先にコピーする場合は、2つのファイルをcopy_file順番に呼び出すよりも3つのファイルを一度に開くほうが高速です(ただし、ファイルがOSキャッシュに収まる限り、違いはほとんどわかりません)。
  • HDD上の多数の小さなファイルを処理している場合、シーク時間を最小限に抑えるためにそれらをバッチで読み取ることができます(ただし、クレイジーで小さなファイルのようなシークを回避するために、OSはすでにディレクトリエントリをキャッシュしていますが、とにかくディスク帯域幅が大幅に減少します)。

しかし、これらはすべて、汎用ファイルコピー機能の範囲外です。

したがって、私は間違いなく熟練したプログラマーの意見では、C ++ファイルコピーは、C ++ 17 file_copy専用関数を使用する必要があります。ただし、ファイルコピーが発生するコンテキストについて知られておらず、OSの裏をかくための巧妙な戦略が考案されている場合を除きます。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.