Скопіюйте файл здоровим, безпечним та ефективним способом


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

КОПІЯ-АЛГОРИТМ-С ++ - ШЛЯХ

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

ВЛАСНИЙ БУФЕР-С ++ - ШЛЯХ

#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 Гб, заповнений до 50%
  • Скопіюйте OGG-VIDEO-FILE 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
Я змінив джерело у всіх зразках таким чином, щоб відкритий і закритий дескриптори файлів були включені в вимірювання тактової частоти () . Їх немає інших суттєвих змін у вихідному коді. Результати не змінилися! Я також використала час, щоб двічі перевірити свої результати.

Оновлення 2
Зразок ANSI C змінено: Стан циклу while не викликає більше feof (), замість цього я перемістив fread () у стан. Схоже, код працює зараз на 10000 годин швидше.

Вимірювання змінилося: Колишні результати завжди буферувались, тому що я кілька разів повторював старий командний рядок rm to.ogv && sync && time ./program для кожної програми. Тепер я перезавантажую систему для кожної програми. Нерозподілені результати є новими і не викликають подиву. Результати розблокування не змінилися дійсно.

Якщо я не видаляю стару копію, програми реагують по-різному. Перезапис наявного файлу в буферизацію відбувається швидше за допомогою POSIX та SENDFILE, а всі інші програми повільніше. Можливо, варіанти скорочення або створення впливають на таку поведінку. Але перезапис існуючих файлів з однією копією - це не справжній випадок використання у світі.

Виконання копії за допомогою cp займає 0,44 секунди небуферованого та 0,30 секунди буферизованого. Тому cp трохи повільніше, ніж зразок POSIX. Мені добре виглядає.

Можливо, я додаю також зразки та результати mmap () та copy_file()з boost :: файлової системи.

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


5
fstreamбезумовно, хороший варіант для файлових операцій.
chris


28
Ви забули ледачий спосіб: 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
Вибачте за чіпінг настільки пізно, але я б описав жодне з них як "безпечне", оскільки у них немає помилок.
Річард Кеттвелл

Відповіді:


259

Скопіюйте файл здоровим способом:

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

28
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
Потім просто загортайте його в {} блок блоку
15:15

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 Я не впевнений у цьому. Можливо, це насправді не має значення .
manlio

@Jepessen у стандартній бібліотеці чистий код є першорядним. Перевантаження (на відміну від однієї функції з параметрами за замовчуванням) робить наміри програміста більш чіткими.
Марк.2377,

@Peter Це, мабуть, має бути прийнятою відповіддю, враховуючи, що C ++ 17 доступний.
Мартін Йорк

21

Забагато!

Буфер способу "ANSI C" є надлишковим, оскільки а FILEвже буферний. (Розмір цього внутрішнього буфера - це те, що BUFSIZнасправді визначає.)

"ВЛАСНИЙ-BUFFER-C ++ - ШЛЯХ" буде повільним у процесі проходження fstream, що робить багато віртуальної диспетчеризації, і знову підтримує внутрішні буфери або кожен об'єкт потоку. ("COPY-ALGORITHM-C ++ - WAY" цього не зазнає, оскільки streambuf_iteratorклас обходить шар потоку.)

Я віддаю перевагу "COPY-ALGORITHM-C ++ - WAY", але не будуючи fstream, просто створюйте голі std::filebufекземпляри, коли фактичне форматування не потрібно.

Для вихідної продуктивності ви не можете перемогти дескриптори файлів POSIX. Це некрасиво, але портативно і швидко на будь-якій платформі.

Шлях Linux виглядає неймовірно швидким - можливо, ОС дозволила функції повернутися до того, як введення / виведення було закінчено? У будь-якому випадку, це недостатньо портативно для багатьох застосувань.

EDIT : Ах, "рідний Linux", можливо, покращує продуктивність, переплутуючи читання та запис із асинхронним введенням-виведенням. Дозвіл команд накопичуватися може допомогти драйверу диска вирішити, коли найкраще шукати. Ви можете спробувати Boost Asio або pthreads для порівняння. Що стосується "не вдається перемогти дескриптори файлів POSIX" ... ну це правда, якщо ви робите щось із даними, а не просто сліпо копіюєте.


ANSI C: Але я повинен надати функції fread / fwrite розмір? pubs.opengroup.org/onlinepubs/9699919799/toc.htm
Пітер

@PeterWeber Ну, так, це правда, що BUFSIZ є настільки ж хорошим значенням, як і будь-який, і, ймовірно, прискорить роботу відносно одного або "лише декількох" символів одночасно. У будь-якому разі, вимірювання продуктивності свідчить про те, що це не найкращий метод в будь-якому випадку.
Potatoswatter

1
У мене цього немає глибокого розуміння, тому я повинен бути обережним із припущеннями та думками. Linux-Way працює в afaik Kernelpace. Це повинно уникати повільного переключення контексту між простором ядра та користувачем? Завтра я знову погляну на сторінку sendfile. Нещодавно Лінус Торвальдс заявив, що йому не подобаються користувальницькі файлові системи для важких робіт. Може бути sendfile є позитивним прикладом для його погляду?
Пітер

5
" sendfile()копіює дані між одним дескриптором файлу та іншим. Оскільки це копіювання виконується всередині ядра, воно sendfile()є більш ефективним, ніж комбінація read(2)та write(2), що вимагає перенесення даних до та з користувальницького простору.": kernel.org/doc/man-pages /online/pages/man2/sendfile.2.html
Макс Лібберт

1
Чи можете ви розмістити приклад використання необроблених filebufоб'єктів?
Керрек СБ

14

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

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

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


1
має sendfile64 () таку ж проблему?
сірий вовк

1
@Paladin Схоже, що sendfile64 був розроблений для подолання цього обмеження. На вхідній сторінці: "" "Оригінальний системний виклик sendfile () Linux не був розроблений для обробки великих компенсацій файлів. Отже, Linux 2.4 додав sendfile64 (), з більш широким типом для аргументу зміщення. Функція обгортки glibc sendfile () прозоро розбирається з відмінностями ядра. "" "
rveale

sendfile64 має таку саму проблему, як здається. Однак використання типу зміщення off64_tдозволяє використовувати цикл для копіювання великих файлів, як показано у відповіді на пов'язане питання.
pcworld

це невимушено в людині: "Зауважте, що успішний виклик sendfile () може записати менше байтів, ніж потрібно; абонент повинен бути готовий повторити виклик, якщо не було байтів. sendfile або sendfile64 може вимагати виклику в циклі, поки не буде виконана повна копія.
philippe lhardy

2

У Qt є метод копіювання файлів:

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

Зауважте, що для цього ви повинні встановити Qt (інструкції тут ) та включити його у свій проект (якщо ви використовуєте Windows та не адміністратор, ви можете завантажити Qt тут ). Дивіться також цю відповідь .


1
QFile::copyє смішно повільним через 4-буферизацію .
Ніколя Холтай

1
Повільність була зафіксована в нових версіях системи Qt. Я використовую, 5.9.2і швидкість нарівні з нативним реалізацією. Btw. дивлячись на вихідний код, Qt, здається, насправді викликає нативну реалізацію.
ВК

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 :: fileystem :: path також доступний як wpath для Unicode. І що ви також можете використати

using namespace boost::filesystem

якщо вам не подобаються ці імена довгих типів


Бібліотека файлових систем Boost - одне з винятків, що вимагає її складання. Просто FYI!
SimonC

0

Я не зовсім впевнений, що таке "хороший спосіб" копіювання файлу, але припускаючи, що "хороший" означає "швидкий", я міг би трохи розширити тему.

Поточні операційні системи вже давно оптимізовані для вирішення запуску копії файлу фрези. Жоден розумний біт коду не обіграє це. Цілком можливо, що якийсь варіант вашої методики копіювання виявиться швидшим у певному тестовому сценарії, але, швидше за все, в інших випадках він буде гіршим.

Зазвичай, sendfileфункція, ймовірно, повертається до того, як запис буде здійснено, створюючи таким чином враження, що швидше, ніж інші. Я не читав код, але це, безумовно, тому, що він виділяє свій власний виділений буфер, торгова пам'ять на час. І причина, по якій він не працює для файлів більше 2 Гбіт.

Поки ви маєте справу з невеликою кількістю файлів, все відбувається всередині різних буферів (перший, якщо ви використовуєте iostream, C ++, якщо ви використовуєте , внутрішні ОС, мабуть, у випадку додаткового буфера розміру файлу у випадку sendfile). До фактичних носіїв пам’яті можна отримати лише після переміщення достатньої кількості даних, що вартує труднощів із закручуванням жорсткого диска.

Я думаю, ви могли б трохи покращити показники в конкретних випадках. Вгорі голови:

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

Але все це виходить за межі функції копіювання файлів загального призначення.

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.