Скопируйте файл разумным, безопасным и эффективным способом

305

Я ищу хороший способ скопировать файл (двоичный или текстовый). Я написал несколько образцов, каждый работает. Но я хочу услышать мнение опытных программистов.

Я упустил хорошие примеры и ищу способ, который работает с C ++.

ANSI-C ПУТЬ

#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-АЛГОРИТМ-C ++ - ПУТЬ

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

СОБСТВЕННЫЙ-BUFFER-C ++ - ПУТЬ

#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 (многопользовательский, сеть, терминал, без графического интерфейса)
  • INTEL SSD-Postville 80 ГБ, заполненный до 50%
  • Скопируйте OGG-видеофайл на 270 МБ

Действия по воспроизведению

 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

Результаты (время процессора используется)

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 () и ничего не конвертирует. Верный?

Спасибо

Обновление 1
Я изменил источник во всех примерах таким образом, чтобы открытие и закрытие файловых дескрипторов было включено в измерение clock () . Других существенных изменений в исходном коде нет. Результаты не изменились! Я также использовал время, чтобы перепроверить мои результаты.

Обновление 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.

Обновление 3
Я также разместил это на странице блога и немного расширил. Включая splice () , которая является функцией низкого уровня из ядра Linux. Может быть, последует больше примеров с Java. http://www.ttyhoney.com/blog/?page_id=69

Peter
источник
5
fstreamбезусловно, хороший вариант для файловых операций.
Крис
29
Вы забыли ленивый путь: 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);
Мартин Йорк,
3
Извините за внесение так поздно, но я бы не назвал ничего из них «безопасным», так как они не обрабатывают ошибок.
Ричард Кеттвелл

Ответы:

260

Скопируйте файл разумным способом:

#include <fstream>

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

    dst << src.rdbuf();
}

Это так просто и интуитивно понятно, что стоит дополнительных затрат. Если бы мы делали это много, лучше прибегнуть к обращениям ОС к файловой системе. Я уверен, что 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это, вероятно, самый переносимый способ копирования файла через собственную файловую систему.
Майк Сеймур
4
@MikeSeymour: copyfile () кажется расширением BSD.
Мартин Йорк,
10
@ duedl0r: Нет. У объектов есть деструкторы. Деструктор для потоков автоматически вызывает close (). codereview.stackexchange.com/q/540/507
Мартин Йорк,
11
@ duedl0r: Да. Но это все равно что сказать "если солнце садится". Вы можете бежать очень быстро на запад, и вы можете сделать свой день немного длиннее, но солнце уже садится. Если у вас нет ошибок и утечки памяти (это выйдет за рамки). Но поскольку здесь нет динамического управления памятью, не может быть утечки, и они выйдут за рамки (так же, как зайдет солнце).
Мартин Йорк
6
Затем просто оберните его в блок видимости {}
Пол
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использованием в качестве параметров (см. Также copy_file).

filesystemБиблиотека была первоначально разработана как boost.filesystemи , наконец , объединились в ISO C ++ от C ++ 17.

оборота манлио
источник
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
2
@Jepessen Я не уверен в этом. Может быть, это не имеет значения .
Манлио
@Jepessen в стандартной библиотеке, чистый код имеет первостепенное значение. Наличие перегрузок (в отличие от одной функции с параметрами по умолчанию) делает намерение программиста более ясным.
23
@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 кажется невероятно быстрым - возможно, ОС позволила функции вернуться до завершения ввода-вывода? В любом случае, этого недостаточно для многих приложений.

РЕДАКТИРОВАТЬ : Ах, "родной Linux" может улучшить производительность, чередуя чтения и записи с асинхронным вводом-выводом. Скопление команд может помочь драйверу диска решить, когда лучше искать. Вы можете попробовать Boost Asio или pthreads для сравнения. Что касается «не может превзойти дескрипторы файлов POSIX»… что ж, это правда, если вы что-то делаете с данными, а не просто слепо копируете.

Potatoswatter
источник
ANSI C: Но я должен дать функции fread / fwrite размер? pubs.opengroup.org/onlinepubs/9699919799/toc.htm
Питер,
@PeterWeber Ну, да, это правда, что BUFSIZ является таким же хорошим значением, как и любое, и, вероятно, будет ускорять процесс по отношению к одному или «всего лишь нескольким» символам за раз. В любом случае, измерение производительности показывает, что это не лучший метод в любом случае.
Potatoswatter
1
У меня нет глубокого понимания этого, поэтому я должен быть осторожен с предположениями и мнениями. Linux-Way работает в Kernelspace afaik. Это должно избежать медленного переключения контекста между Kernelspace и Userspace? Завтра я снова посмотрю на man-страницу sendfile. Некоторое время назад Линус Торвальдс сказал, что ему не нравятся Userspace-Filesystems для тяжелых работ. Может, sendfile является положительным примером для его взгляда?
Питер
5
« sendfile()Копирует данные между одним файловым дескриптором и другим. Поскольку это копирование выполняется в ядре, sendfile()оно более эффективно, чем комбинация read(2)и write(2), что потребовало бы передачи данных в пространство пользователя и из него.»: kernel.org/doc/man-pages /online/pages/man2/sendfile.2.html
Макс Либберт,
1
Не могли бы вы опубликовать пример использования необработанных filebufобъектов?
Kerrek SB
14

Я хочу сделать очень важное замечание, что метод LINUX, использующий sendfile (), имеет большую проблему в том, что он не может копировать файлы размером более 2 ГБ! Я реализовал его после этого вопроса и столкнулся с проблемами, потому что использовал его для копирования файлов HDF5, размер которых был много ГБ.

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

sendfile () будет передавать не более 0x7ffff000 (2,147,479,552) байтов, возвращая количество фактически переданных байтов. (Это верно как для 32-битных, так и для 64-битных систем.)

rveale
источник
1
sendfile64 () имеет такую ​​же проблему?
серый волк
1
@Paladin Кажется, что sendfile64 был разработан, чтобы обойти это ограничение. Со страницы man: "" "Исходный системный вызов Linux sendfile () не был предназначен для обработки больших смещений файлов. Следовательно, Linux 2.4 добавил sendfile64 () с более широким типом для аргумента смещения. Функция-обертка glibc sendfile () прозрачно разбирается с различиями в ядре. "" "
Rveale
У sendfile64 такая же проблема, как кажется. Однако использование типа смещения off64_tпозволяет использовать цикл для копирования больших файлов, как показано в ответе на связанный вопрос.
pcworld
это записано в man: «Обратите внимание, что успешный вызов sendfile () может записать меньше байтов, чем запрошено; вызывающий должен быть готов повторить вызов, если были неотправленные байты. ' sendfile или sendfile64 могут потребовать вызова внутри цикла, пока не будет выполнена полная копия.
Филипп Лхарди
2

Qt имеет метод для копирования файлов:

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

Обратите внимание, что для его использования вы должны установить Qt (инструкции здесь ) и включить его в свой проект (если вы используете Windows и не являетесь администратором, вы можете скачать Qt здесь ). Также смотрите этот ответ .

Дональд Дак
источник
1
QFile::copyСмехотворно медленный из-за 4К буферизации .
Николас
1
Медлительность была исправлена ​​в более новых версиях Qt. Я использую 5.9.2и скорость находится на одном уровне с родной реализацией. Btw. Взглянув на исходный код, 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 также доступен как wpath для Unicode. И что вы могли бы также использовать

using namespace boost::filesystem

если вам не нравятся эти длинные имена

anhoppe
источник
Библиотека файловой системы Boost является одним из исключений, требующих ее компиляции. Просто к вашему сведению!
SimonC
0

Я не совсем уверен, что такое «хороший способ» копирования файла, но предполагая, что «хороший» означает «быстрый», я мог бы немного расширить тему.

Современные операционные системы уже давно оптимизированы для работы с копией обычного файла. Никакой умный кусок кода не побьет это. Возможно, что некоторые варианты ваших методов копирования окажутся быстрее в некотором тестовом сценарии, но они, скорее всего, будут хуже в других случаях.

Как правило, sendfileфункция, вероятно, возвращается до фиксации записи, создавая впечатление, что она быстрее, чем остальные. Я не читал код, но это, безусловно, потому что он выделяет свой собственный выделенный буфер, торгуя память на время. И причина, почему это не будет работать для файлов больше, чем 2 ГБ.

Пока вы имеете дело с небольшим количеством файлов, все происходит внутри различных буферов (в первую очередь, если вы используете среду выполнения C ++ iostream, это внутренние операционные системы, по-видимому, дополнительный буфер размером с файл в случае sendfile). К фактическим носителям доступа можно получить доступ только после того, как будет перемещено достаточное количество данных, чтобы стоить проблемы вращения жесткого диска.

Я полагаю, вы могли бы немного улучшить производительность в конкретных случаях. С верхней части моей головы:

  • Если вы копируете огромный файл на тот же диск, использование буфера большего размера, чем у ОС, может немного улучшить ситуацию (но мы, вероятно, говорим о гигабайтах здесь).
  • Если вы хотите скопировать один и тот же файл в двух разных физических местах назначения, вы, вероятно, быстрее copy_fileоткроете три файла одновременно, чем вызовете два последовательных (хотя вы вряд ли заметите разницу, если файл помещается в кэш ОС)
  • Если вы имеете дело с большим количеством крошечных файлов на жестком диске, вы можете прочитать их порциями, чтобы минимизировать время поиска (хотя ОС уже кэширует записи каталога, чтобы избежать поиска как сумасшедших, а крошечные файлы в любом случае, вероятно, значительно уменьшат пропускную способность диска).

Но все это выходит за рамки функции копирования файлов общего назначения.

Так что, по мнению моего, возможно, опытного программиста, копия файла C ++ должна просто использовать file_copyспециальную функцию C ++ 17 , если не известно больше о контексте, в котором происходит копирование файла, и могут быть разработаны некоторые умные стратегии, чтобы перехитрить ОС.

оборота курои нэко
источник