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

305

Je recherche un bon moyen de copier un fichier (binaire ou texte). J'ai écrit plusieurs échantillons, tout le monde travaille. Mais je veux entendre l'opinion de programmeurs chevronnés.

Il me manque de bons exemples et recherche un moyen 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 l'utilise dans "Le langage de programmation C", 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;
}

COPIE-ALGORITHME-C ++ - VOIE

#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 // 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, sans interface graphique)
  • INTEL SSD-Postville 80 Go, rempli jusqu'à 50%
  • Copier un fichier vidéo OGG de 270 Mo

Étapes à reproduire

 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 (CPU TIME 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 imprime les mêmes résultats.
Le fichier vidéo est toujours lisible.

Des questions

  • Quelle méthode préférez-vous?
  • Connaissez-vous de meilleures solutions?
  • Voyez-vous des erreurs dans mon code?
  • Connaissez-vous une raison pour éviter une solution?

  • FSTREAM (KISS, Streambuffer)
    J'aime vraiment celui-ci, car il est vraiment court et simple. Pour l'instant, je sais que l'opérateur << est surchargé pour rdbuf () et ne convertit rien. Correct?

Merci

Mise à jour 1
J'ai changé la source dans tous les échantillons de cette façon, que l'ouverture et la fermeture des descripteurs de fichiers soient incluses dans la mesure de l' horloge () . Il n'y a aucun autre changement significatif dans le code source. Le résultat n'a pas changé! J'ai également utilisé du temps pour revérifier mes résultats.

Exemple de mise à jour 2
ANSI C modifié: la condition de la boucle while n'appelle plus feof () à la place, j'ai déplacé fread () dans la condition. Il semble que le code s'exécute maintenant 10 000 horloges plus rapidement.

La mesure a changé: les anciens résultats ont toujours été mis en mémoire tampon, car j'ai répété plusieurs fois l'ancienne ligne de commande rm to.ogv && sync && time ./program pour chaque programme. Maintenant, je redémarre le système pour chaque programme. Les résultats sans tampon 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. L'écrasement d'un fichier existant tamponné est plus rapide avec POSIX et SENDFILE, tous les autres programmes sont plus lents. Peut-être que les options tronquer ou 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.

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'exemple POSIX. Ça me va bien.

Peut-être que j'ajoute également des échantillons et les résultats de mmap () et copy_file()de boost :: filesystem.

Mise à jour 3
J'ai également mis cela 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

Peter
la source
5
fstreamest certainement une bonne option pour les opérations de fichiers.
chris
29
Vous avez oublié la manière paresseuse: 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);
Martin York
3
Désolé de vous être embarqué si tard, mais je ne décrirais aucun de ceux-ci comme «sûrs», car ils ne contiennent aucune erreur.
Richard Kettlewell

Réponses:

260

Copiez un fichier de manière saine:

#include <fstream>

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

    dst << src.rdbuf();
}

C'est tellement simple et intuitif à lire qu'il vaut le coût supplémentaire. Si nous le faisions beaucoup, mieux vaut se rabattre sur les appels du système d'exploitation au système de fichiers. Je suis sûr qu'il boosta 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);
Martin York
la source
29
copyfilen'est pas portable; Je pense que c'est spécifique à Mac OS X. Il n'existe certainement pas sous Linux. boost::filesystem::copy_fileest probablement le moyen le plus portable de copier un fichier via le système de fichiers natif.
Mike Seymour
4
@MikeSeymour: copyfile () semble être une extension BSD.
Martin York
10
@ duedl0r: Non. Les objets ont des destructeurs. Le destructeur de flux appelle automatiquement close (). codereview.stackexchange.com/q/540/507
Martin York
11
@ duedl0r: Oui. Mais c'est comme dire "si le soleil se couche". Vous pouvez courir très vite vers l'ouest et vous pouvez allonger légèrement la journée, mais le soleil va se coucher. Sauf si vous avez un bug et une fuite de mémoire (cela sortira du cadre). Mais comme il n'y a pas de gestion dynamique de la mémoire ici, il ne peut pas y avoir de fuite et ils sortiront du cadre (tout comme le soleil se couchera).
Martin York
6
Ensuite, enveloppez-le simplement dans un bloc de portée {}
paulm
62

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

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

Le premier formulaire est équivalent au second avec copy_options::noneutilisé comme options (voir aussi copy_file).

La filesystembibliothèque a été initialement développée boost.filesystemet finalement fusionnée en ISO C ++ à partir de C ++ 17.

manlio
la source
2
Pourquoi n'y a-t-il 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);?
Jepessen
2
@Jepessen Je n'en suis pas sûr. Peut-être que cela n'a pas vraiment d'importance .
manlio
@Jepessen dans la bibliothèque standard, le code propre est primordial. Avoir des surcharges (par opposition à une fonction avec des paramètres par défaut) rend l'intention du programmeur plus claire.
Marc.2377
@Peter Ceci devrait probablement être la réponse acceptée étant donné que C ++ 17 est disponible.
Martin York
21

Trop!

Le tampon de manière "ANSI C" est redondant, car a FILEest déjà tamponné. (La taille de ce tampon interne est ce BUFSIZqui définit réellement.)

Le "OWN-BUFFER-C ++ - WAY" sera lent au fur et à mesure fstream, ce qui fait beaucoup de répartition virtuelle et maintient à nouveau des tampons internes ou chaque objet de flux. (Le "COPY-ALGORITHM-C ++ - WAY" ne souffre pas de cela, car la streambuf_iteratorclasse contourne la couche de flux.)

Je préfère le "COPY-ALGORITHM-C ++ - WAY", mais sans construire un fstream, il suffit de créer des std::filebufinstances nues quand aucun formatage réel n'est nécessaire.

Pour les performances brutes, vous ne pouvez pas battre les descripteurs de fichiers POSIX. C'est moche mais portable et rapide sur n'importe quelle plateforme.

La méthode Linux semble être incroyablement rapide - peut-être que le système d'exploitation a laissé la fonction revenir avant la fin des E / S? En tout 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 du meilleur moment. Vous pouvez essayer Boost Asio ou pthreads pour comparaison. Quant à «ne peut pas battre les descripteurs de fichiers POSIX»… c'est vrai si vous faites quoi que ce soit avec les données, pas seulement en les copiant aveuglément.

Potatoswatter
la source
ANSI C: Mais je dois donner une taille à la fonction fread / fwrite? pubs.opengroup.org/onlinepubs/9699919799/toc.htm
Peter
@PeterWeber Eh bien, oui, il est vrai que BUFSIZ est aussi bon que n'importe quelle valeur, et accélérera probablement les choses par rapport à un ou "juste quelques" caractères à la fois. Quoi qu'il en soit, la mesure de la performance confirme que ce n'est pas la meilleure méthode en tout cas.
Potatoswatter
1
Je n'ai pas une compréhension approfondie de cela, donc je dois faire attention aux hypothèses et aux opinions. Linux-Way fonctionne dans Kernelspace afaik. Cela devrait éviter une commutation de contexte lente entre l'espace de noyau et l'espace utilisateur? Demain, je reviendrai sur 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 gros travaux. Peut-être que sendfile est un exemple positif de son point de vue?
Peter
5
" sendfile()copie les données entre un descripteur de fichier et un autre. Parce que cette copie est effectuée dans le noyau, sendfile()est plus efficace que la combinaison de read(2)et write(2), ce qui nécessiterait le transfert de données vers et depuis l'espace utilisateur.": kernel.org/doc/man-pages /online/pages/man2/sendfile.2.html
Max Lybbert
1
Pourriez-vous publier un exemple d'utilisation d' filebufobjets bruts ?
Kerrek SB
14

Je veux faire de la très importante noter que la méthode LINUX utilisant sendfile () a un problème majeur en ce qu'elle ne peut pas copier des fichiers de plus de 2 Go! Je l'avais implémenté suite à cette question et rencontrais des problèmes parce que 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 plus 0x7ffff000 (2 147 479 552) octets, renvoyant le nombre d'octets réellement transférés. (Cela est vrai sur les systèmes 32 bits et 64 bits.)

rveale
la source
1
sendfile64 () a-t-il le même problème?
graywolf
1
@Paladin Il semble que sendfile64 ait été développé pour contourner cette limitation. Depuis la page de manuel: "" "L'appel système sendfile () d'origine de Linux n'a pas été conçu pour gérer les décalages de fichiers volumineux. Par conséquent, Linux 2.4 a ajouté sendfile64 (), avec un type plus large pour l'argument offset. La fonction wrapper de la glibc sendfile () traite de manière transparente les différences de noyau. "" "
rveale
sendfile64 a le même problème qu'il n'y paraît. Cependant, l'utilisation du type offset off64_tpermet d'utiliser une boucle pour copier des fichiers volumineux comme indiqué dans une réponse à la question liée.
pcworld
ceci est wirtten dans man: 'Notez qu'un appel réussi à sendfile () peut écrire moins d'octets que demandé; l'appelant doit être prêt à réessayer l'appel s'il y a des octets non envoyés. » sendfile ou sendfile64 peuvent nécessiter d'être appelés dans une boucle jusqu'à ce que la copie complète soit effectuée.
philippe lhardy
2

Qt a une méthode pour copier des fichiers:

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

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

Donald Duck
la source
1
QFile::copyest ridiculement lent en raison de sa mise en mémoire tampon 4k .
Nicolas Holthaus
1
La lenteur a été corrigée dans les versions plus récentes de Qt. J'utilise 5.9.2et la vitesse est comparable à l'implémentation native. Btw. en jetant un œil au code source, Qt semble en fait appeler l'implémentation native.
VK
1

Pour ceux qui aiment le boost:

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

Notez que boost :: filesystem :: path est également disponible en tant que wpath pour Unicode. Et que vous pourriez également utiliser

using namespace boost::filesystem

si vous n'aimez pas ces noms longs

anhoppe
la source
La bibliothèque de systèmes de fichiers de Boost est l'une des exceptions qui nécessite sa compilation. Juste FYI!
SimonC
0

Je ne sais pas trop ce qu'est une "bonne façon" de copier un fichier, mais en supposant que "bon" signifie "rapide", je pourrais élargir un peu le sujet.

Les systèmes d'exploitation actuels ont longtemps été optimisés pour gérer l'exécution de la copie du fichier de l'usine. Aucun morceau de code intelligent ne battra cela. Il est possible que certaines variantes de vos techniques de copie se révèlent plus rapides dans certains scénarios de test, mais elles se porteraient très probablement moins bien dans d'autres cas.

En règle générale, le sendfile fonction retourne probablement avant que l'écriture ne soit validée, donnant ainsi l'impression d'être plus rapide que les autres. Je n'ai pas lu le code, mais c'est très certainement parce qu'il alloue son propre tampon dédié, échangeant de la mémoire pour le temps. Et la raison pour laquelle cela ne fonctionnera pas pour les fichiers supérieurs à 2 Go.

Tant que vous traitez avec un petit nombre de fichiers, tout se passe à l'intérieur de divers tampons (le premier du runtime C ++ si vous utilisez iostream, les internes du système d'exploitation, apparemment un tampon supplémentaire de la taille d'un fichier dans le cas de sendfile). Les supports de stockage réels ne sont accessibles qu'une fois que suffisamment de données ont été déplacées pour valoir la peine de faire tourner un disque dur.

Je suppose que vous pourriez légèrement améliorer les performances dans des cas spécifiques. Du haut de ma tête:

  • Si vous copiez un énorme fichier sur le même disque, l'utilisation d'un tampon plus grand que le système d'exploitation pourrait améliorer un peu les choses (mais nous parlons probablement de gigaoctets ici).
  • Si vous souhaitez copier le même fichier sur deux destinations physiques différentes, vous ouvrirez probablement plus rapidement les trois fichiers à la fois que d'en appeler deux copy_fileséquentiellement (bien que vous remarquerez à peine la différence tant que le fichier tient dans le cache du système d'exploitation)
  • Si vous traitez beaucoup de petits fichiers sur un disque dur, vous voudrez peut-être les lire par lots pour minimiser le temps de recherche (bien que le système d'exploitation cache déjà les entrées de répertoire pour éviter de rechercher des fichiers comme des fous et minuscules réduira probablement la bande passante du disque de toute façon).

Mais tout cela n'entre pas dans le cadre d'une fonction de copie de fichiers à usage général.

Donc, selon mon programmeur sans doute chevronné, une copie de fichier C ++ devrait simplement utiliser la file_copyfonction dédiée C ++ 17 , à moins que l'on en sache plus sur le contexte dans lequel la copie de fichier se produit et que certaines stratégies intelligentes peuvent être conçues pour déjouer le système d'exploitation.

kuroi neko
la source