320 votes

Copier un fichier de manière saine, sûre et efficace

Je cherche un bon moyen de copier un fichier (binaire ou texte). J'ai écrit plusieurs exemples, tous fonctionnent. Mais je veux entendre l'avis de programmeurs chevronnés.

Il me manque de bons exemples et je cherche une méthode qui fonctionne avec 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 utilisent ceci dans "The C programming language", plus bas niveau)

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

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

PROPRE TAMPON-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 // nécessite un noyau >= 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;
}

Environnement

  • GNU/LINUX (Archlinux)
  • Noyau 3.3
  • GLIBC-2.15, LIBSTDC++ 4.7 (GCC-LIBS), GCC 4.7, Coreutils 8.16
  • Utilisation de RUNLEVEL 3 (multi-utilisateur, réseau, terminal, pas d'interface graphique)
  • INTEL SSD-Postville 80 GB, rempli jusqu'à 50%.
  • Copie d'un fichier OGG-VIDEO de 270 MB

Étapes de la reproduction

 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

Résultats (TEMPS CPU utilisé)

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  

La taille du fichier ne change pas.
sha256sum donnent les mêmes résultats.
Le fichier vidéo est toujours lisible.

Questions

  • Quelle méthode préférez-vous ?

  • Connaissez-vous de meilleures solutions ?

  • Voyez-vous des erreurs dans mon code ?

  • Connaissez-vous une raison d'éviter une solution ?

  • FSTREAM (KISS, Streambuffer)
    J'aime beaucoup celle-ci, car elle est vraiment courte et simple. Pour autant que je sache, l'opérateur << est surchargé pour rdbuf() et ne convertit rien. Est-ce exact ?

Merci

Mise à jour 1
J'ai modifié la source de tous les échantillons de manière à ce que l'ouverture et la fermeture des descripteurs de fichier soient incluses dans la mesure de clock() . Il n'y a pas d'autres changements significatifs dans le code source. Les résultats n'ont pas changé ! J'ai aussi utilisé temps pour revérifier mes résultats.

Mise à jour 2
L'échantillon C de l'ANSI a été modifié : l'état de la while-loop n'appelle plus feof() Au lieu de cela, j'ai déménagé fread() dans la condition. Il semble que le code s'exécute maintenant 10.000 fois plus vite.

Mesure modifiée : Les anciens résultats étaient toujours mis en mémoire tampon, car je répétais l'ancienne ligne de commande rm to.ogv && sync && time ./programme pour chaque programme plusieurs fois. Maintenant, je redémarre le système pour chaque programme. Les résultats non bufferisés sont nouveaux et ne montrent aucune surprise. Les résultats sans tampon n'ont pas vraiment changé.

Si je ne supprime pas l'ancienne copie, les programmes réagissent différemment. Écraser un fichier existant tamponné est plus rapide avec POSIX et SENDFILE, tous les autres programmes sont plus lents. Peut-être que les options tronqué o créer ont un impact sur ce comportement. Mais écraser des fichiers existants avec la même copie n'est pas un cas d'utilisation réel.

Effectuer la copie avec cp prend 0,44 seconde sans tampon et 0,30 seconde avec tampon. Donc cp est un peu plus lent que l'échantillon POSIX. Pour moi, tout va bien.

J'ajouterai peut-être aussi des échantillons et des résultats de mmap() et _copy_file()_ de boost::filesystem.

Mise à jour 3
J'ai mis cela aussi sur une page de blog et je l'ai étendu un peu. Y compris splice() qui est une fonction de bas niveau du noyau Linux. Peut-être que d'autres échantillons avec Java suivront. http://www.ttyhoney.com/blog/?page_id=69

5 votes

fstream est certainement une bonne option pour les opérations sur les fichiers.

1 votes

29 votes

Vous avez oublié la méthode paresseuse : system("cp from.ogv to.ogv") ;

274voto

Loki Astari Points 116129

Copier un fichier d'une manière sensée :

#include <fstream>

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

    dst << src.rdbuf();
}

La lecture est tellement simple et intuitive qu'elle vaut le coût supplémentaire. Si nous le faisions souvent, il vaudrait mieux se rabattre sur les appels du système d'exploitation au système de fichiers. Je suis sûr que boost possède une méthode de copie de fichier dans sa classe de système de fichiers.

Il existe une méthode C pour interagir avec le système de fichiers :

#include <copyfile.h>

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

1 votes

C'est sa façon KISS streambuffer C++. C'est la méthode la plus naturelle pour un programmeur C++ (bien que l'utilisation de l'option std::copy est également assez idiomatique). Et comme les deux manières idiomatiques sont aussi les plus rapidement portables...

30 votes

copyfile n'est pas portable ; je pense qu'il est spécifique à Mac OS X. Il n'existe certainement pas sous Linux. boost::filesystem::copy_file est probablement le moyen le plus portable de copier un fichier via le système de fichiers natif.

4 votes

@MikeSeymour : copyfile() semble être une extension BSD.

74voto

manlio Points 3407

Avec C++17, la manière standard de copier un fichier sera d'inclure la fonction <filesystem> l'en-tête et l'utilisation :

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

La première forme est équivalente à la seconde avec copy_options::none utilisés comme options (voir également copy_file ).

Le site filesystem a été développée à l'origine comme boost.filesystem et finalement fusionné avec ISO C++ à partir de C++17.

2 votes

Pourquoi il n'y a pas une seule fonction avec un argument par défaut, comme bool copy_file( const std::filesystem::path& from, const std::filesystem::path& to, std::filesystem::copy_options options = std::filesystem::copy_options::none); ?

2 votes

@Jepessen Je ne suis pas sûr de cela. Peut-être que n'a pas vraiment d'importance .

1 votes

@Jepessen dans la bibliothèque standard, le code propre est primordial. Le fait d'avoir des surcharges (par opposition à une fonction avec des paramètres par défaut) rend l'intention du programmeur plus claire.

21voto

Potatoswatter Points 70305

Il y en a trop !

La mémoire tampon de type "ANSI C" est redondante, puisqu'un fichier de type FILE est déjà mis en mémoire tampon. (La taille de ce tampon interne est ce que BUFSIZ définit en fait).

Le "OWN-BUFFER-C++-WAY" sera lent car il passe à travers fstream qui effectue un grand nombre de répartitions virtuelles et maintient des tampons internes pour chaque objet de flux. (La méthode "COPY-ALGORITHM-C++-WAY" ne souffre pas de ce problème, car la méthode streambuf_iterator contourne la couche de flux).

Je préfère la "COPY-ALGORITHM-C++-WAY", mais sans construire une fstream , il suffit de créer le nu std::filebuf les cas où aucun formatage réel n'est nécessaire.

Pour les performances brutes, il n'y a pas mieux que les descripteurs de fichiers POSIX. C'est laid mais portable et rapide sur toutes les plateformes.

La méthode Linux semble être incroyablement rapide - peut-être le système d'exploitation a-t-il laissé la fonction revenir avant la fin des E/S ? Dans tous les cas, ce n'est pas assez portable pour de nombreuses applications.

EDIT : Ah, "Linux natif" peut améliorer les performances en entrelaçant les lectures et les écritures avec des E/S asynchrones. Laisser les commandes s'empiler peut aider le pilote de disque à décider quand il est préférable de chercher. Vous pouvez essayer Boost Asio ou pthreads pour comparer. Quant à l'expression "impossible de battre les descripteurs de fichiers POSIX" c'est vrai si vous faites quelque chose avec les données, et pas seulement les copier aveuglément.

0 votes

ANSI C : Mais je dois donner une taille à la fonction fread/fwrite ? pubs.opengroup.org/onlinepubs/9699919799/toc.htm

0 votes

@PeterWeber Eh bien, oui, il est vrai que BUFSIZ est une valeur aussi bonne qu'une autre, et qu'elle accélérera probablement les choses par rapport à un ou "seulement quelques" caractères à la fois. Quoi qu'il en soit, la mesure des performances montre qu'il ne s'agit pas de la meilleure méthode dans tous les cas.

1 votes

Je n'ai pas une compréhension approfondie de ce sujet, je dois donc faire attention aux suppositions et aux opinions. Linux-Way fonctionne dans l'espace du noyau (afaik). Cela devrait éviter la lenteur du Context-Switching entre Kernelspace et Userspace ? Demain, je jetterai à nouveau un coup d'oeil à la page de manuel de sendfile. Il y a quelque temps, Linus Torvalds a dit qu'il n'aimait pas les systèmes de fichiers en espace utilisateur pour les travaux lourds. Peut-être que sendfile est un exemple positif pour son point de vue ?

17voto

rveale Points 66

Je veux faire le très note importante que la méthode LINUX utilisant sendfile() a un problème majeur dans la mesure où elle ne peut pas copier des fichiers d'une taille supérieure à 2GB ! Je l'avais implémentée suite à cette question et je rencontrais des problèmes car je l'utilisais pour copier des fichiers HDF5 de plusieurs Go.

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

sendfile() transférera au maximum 0x7ffff000 (2,147,479,552) octets, renvoyant le nombre d'octets réellement transférés. (Ceci est vrai sur systèmes 32 bits et 64 bits).

2 votes

Est-ce que sendfile64() a le même problème ?

3 votes

@Paladin Il semble que sendfile64 ait été développé pour contourner cette limitation. Extrait de la page de manuel : """L'appel système Linux sendfile() original n'a pas été conçu pour gérer les grands décalages de fichiers. Par conséquent, Linux 2.4 a ajouté sendfile64(), avec un type plus large pour l'argument offset. La fonction wrapper glibc sendfile() traite de manière transparente les différences du noyau."""

2 votes

sendfile64 a le même problème il semble. Cependant, l'utilisation du type offset off64_t permet d'utiliser une boucle pour copier des fichiers volumineux, comme le montre l'exemple suivant dans une réponse à la question liée.

2voto

Donald Duck Points 3890

Qt dispose d'une méthode pour copier des fichiers :

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

Notez que pour utiliser cette fonction, vous devez installer Qt (instructions aquí ) et l'inclure dans votre projet (si vous utilisez Windows et que vous n'êtes pas administrateur, vous pouvez télécharger Qt aquí à la place). Voir aussi cette réponse .

1 votes

QFile::copy est ridiculement lent à cause de son Mise en mémoire tampon 4k .

1 votes

La lenteur a été corrigée dans des versions plus récentes de Qt . J'utilise 5.9.2 et la vitesse est comparable à celle de l'implémentation native. En regardant le code source, Qt semble appeler l'implémentation native.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X