以合理,安全和有效的方式复制文件

时间:2012-04-17 16:38:34

标签: c++ file-io

我搜索复制文件(二进制文本或文本)的好方法。我写过几个样本,每个人都在工作。但我想听听经验丰富的程序员的意见。

我错过了很好的例子并搜索了一种适用于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&amp; 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;
}

复制算法-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 //需要内核&gt; = 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  

Filesize不会改变 sha256sum打印相同的结果。
视频文件仍然可以播放。

问题

  • 您更喜欢哪种方法?
  • 你知道更好的解决方案吗?
  • 你看到我的代码中有任何错误吗?
  • 您知道避免解决方案的理由吗?

  • FSTREAM(KISS,Streambuffer)
    我真的很喜欢这个,因为它非常简短。到目前为止,我知道运营商&lt;&lt;为rdbuf()重载并且不转换任何内容。正确的吗?

由于

更新1
我以这种方式改变了所有样本中的源,文件描述符的打开和关闭包括在 clock()的测量中。它们在源代码中没有其他重大变化。结果没有改变!我还使用 time 来仔细检查我的结果。

更新2
ANSI C示例已更改: while-loop 的条件不再调用 feof()而是将 fread()移动到条件。看起来,代码现在运行速度提高了10,000个时钟。

测量发生了变化:以前的结果总是被缓冲,因为我重复了旧的命令行 rm to.ogv&amp;&amp;同步&amp;&amp;时间./program 每个程序几次。现在我为每个程序重启系统。无缓冲的结果是新的,毫不奇怪。无缓冲的结果确实没有改变。

如果我不删除旧副本,程序会有不同的反应。使用POSIX和SENDFILE覆盖现有文件缓冲更快,所有其他程序都更慢。可能选项 truncate create 会对此行为产生影响。但是用相同的副本覆盖现有文件并不是真实的用例。

使用 cp 执行复制需要0.44秒无缓冲和0.30秒缓冲。所以 cp 比POSIX样本慢一点。对我来说很好看。

也许我还会从boost :: filesystem中添加 mmap() copy_file() 的示例和结果。

更新3
我也将它放在一个博客页面上,并将其扩展了一点。包括 splice(),它是Linux内核的低级函数。也许会有更多带有Java的样本。 http://www.ttyhoney.com/blog/?page_id=69

7 个答案:

答案 0 :(得分:230)

以理智的方式复制文件:

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

答案 1 :(得分:47)

使用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用作选项(另请参阅copy_file)。

filesystem库最初开发为boost.filesystem,最后从C ++ 17开始合并到ISO C ++。

答案 2 :(得分:19)

太多了!

“ANSI C”方式缓冲区是冗余的,因为FILE已经缓冲。 (此内部缓冲区的大小是BUFSIZ实际定义的大小。)

“OWN-BUFFER-C ++ - WAY”在通过fstream时会很慢,它执行大量虚拟调度,并再次维护内部缓冲区或每个流对象。 (“COPY-ALGORITHM-C ++ - WAY”不会受此影响,因为streambuf_iterator类会绕过流层。)

我更喜欢“COPY-ALGORITHM-C ++ - WAY”,但是如果不构建fstream,只需要在不需要实际格式化的情况下创建裸std::filebuf个实例。

对于原始性能,您无法击败POSIX文件描述符。它在任何平台上都很丑陋但便携且快速。

Linux方式似乎非常快 - 也许操作系统让I / O完成之前函数返回?在任何情况下,对于许多应用来说,这都不够便携。

编辑:啊,“原生Linux”可能通过将读写与异步I / O交错来提高性能。让命令堆积可以帮助磁盘驱动器决定何时最好寻找。您可以尝试使用Boost Asio或pthreads进行比较。至于“无法击败POSIX文件描述符”......如果您对数据做任何事情,那就是真的,而不仅仅是盲目复制。

答案 3 :(得分:14)

我想使非常重要注意事项,使用sendfile()的LINUX方法存在一个主要问题,即它无法复制大小超过2GB的文件!我已经按照这个问题实现了它并遇到了问题,因为我正在使用它来复制大小为GB的HDF5文件。

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

  

sendfile()最多会传输0x7ffff000(2,147,479,552)个字节,   返回实际传输的字节数。 (这是真的   32位和64位系统。)

答案 4 :(得分:2)

Qt有一种复制文件的方法:

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

请注意,要使用此功能,您必须install Qt(说明here)并将其包含在您的项目中(如果您使用的是Windows且您不是管理员,则可以下载Qt { {3}}而不是)。另请参阅here

答案 5 :(得分: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);

请注意,对于Unicode, boost :: filesystem :: path 也可用作 wpath 。而且您还可以使用

using namespace boost::filesystem

如果您不喜欢那些长类型的名称

答案 6 :(得分:1)

我不太确定复制文件的“好方法”是什么,但是假设“好”意味着“快速”,那么我可以扩大主题范围。

长期以来,当前的操作系统已进行了优化,以处理工厂文件副本的运行。没有什么聪明的代码能胜过这一点。您的复制技术的某些变体在某些测试场景中可能会被证明速度更快,但在其他情况下它们的表现最可能会更差。

通常,sendfile函数可能在提交写入之前返回,因此给人的印象是比其他函数要快。我还没有阅读代码,但是最肯定的是因为它分配了自己的专用缓冲区,将内存用于时间交换。以及为什么它不能用于大于2Gb的文件的原因。

只要处理少量文件,所有内容都会在各种缓冲区内发生(如果使用iostream,则是C ++运行时的第一个缓冲区,这是操作系统内部的缓冲区,显然是文件大小的额外缓冲区)。 sendfile)。实际的存储介质只有在移动了足够的数据后才值得访问,这值得您旋转硬盘。

我想您可以在特定情况下稍微提高性能。离开我的头顶:

  • 如果您要在同一磁盘上复制一个大文件,则使用比操作系统更大的缓冲区可能会有所改善(但在这里我们可能谈论的是千兆字节)。
  • 如果您想在两个不同的物理目标上复制同一文件,则一次打开三个文件的速度可能比顺序调用两个copy_file更快(尽管只要文件不同,您几乎不会注意到它们之间的区别)适合操作系统缓存)
  • 如果您要处理HDD上的许多小文件,则可能需要分批读取它们,以最大程度地减少查找时间(尽管操作系统已经缓存了目录条目,以避免查找疯狂的文件,而小文件可能会大大减少磁盘带宽无论如何)。

但是所有这些超出了通用文件复制功能的范围。

因此,根据我经验丰富的程序员的观点,C ++文件副本应仅使用C ++ 17 file_copy专用功能,除非对该文件副本发生的上下文有更多了解并且可以设计出一些巧妙的策略。胜过操作系统。