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

2voto

anhoppe Points 358

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 pouvez également utiliser

using namespace boost::filesystem

si vous n'aimez pas ces noms de type long

0 votes

La bibliothèque de système de fichiers de Boost est l'une des exceptions qui nécessite d'être compilée. Juste pour info !

1voto

kuroi neko Points 3902

Je ne suis pas tout à fait sûr de ce qu'est une "bonne façon" de copier un fichier, mais en supposant que "bonne" signifie "rapide", je pourrais élargir un peu le sujet.

Les systèmes d'exploitation actuels sont depuis longtemps optimisés pour traiter les copies de fichiers courantes. Aucun bout de code intelligent ne pourra battre cela. Il est possible qu'une variante de vos techniques de copie se révèle plus rapide dans certains scénarios de test, mais il est fort probable qu'elle soit moins performante dans d'autres cas.

En général, le sendfile retourne probablement avant que l'écriture n'ait été 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'elle alloue son propre tampon dédié, troquant la mémoire contre le temps. Et la raison pour laquelle cela ne fonctionnera pas pour les fichiers de plus de 2Gb.

Tant qu'il s'agit d'un petit nombre de fichiers, tout se passe à l'intérieur de différents tampons (le premier du runtime C++ si on utilise iostream les tampons internes du système d'exploitation, et apparemment un tampon supplémentaire de la taille d'un fichier dans le cas des systèmes d'exploitation de l'entreprise. sendfile ). On n'accède au support de stockage réel que lorsque suffisamment de données ont été déplacées pour qu'il vaille la peine de faire tourner un disque dur.

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

  • Si vous copiez un énorme fichier sur le même disque, l'utilisation d'un tampon plus grand que celui du système d'exploitation peut améliorer un peu les choses (mais nous parlons probablement de gigaoctets ici).
  • Si vous voulez copier le même fichier sur deux destinations physiques différentes, vous serez probablement plus rapide en ouvrant les trois fichiers en même temps qu'en appelant deux copy_file séquentiellement (mais vous ne remarquerez guère la différence tant que le fichier tient dans le cache de l'OS)
  • Si vous avez affaire à un grand nombre de petits fichiers sur un disque dur, vous pouvez les lire par lots pour minimiser le temps de recherche (bien que le système d'exploitation mette déjà en cache les entrées de répertoire pour éviter les recherches excessives et que les petits fichiers réduisent de toute façon considérablement la bande passante du disque).

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

Ainsi, selon mon opinion de programmeur sans doute chevronné, une copie de fichier C++ devrait simplement utiliser la norme C++17 file_copy La fonction dédiée, à moins que l'on en sache plus sur le contexte dans lequel la copie de fichier se produit et que des stratégies astucieuses puissent être conçues pour déjouer le système d'exploitation.

0voto

eliasetm Points 843

La méthode la plus simple en C++17 et plus est :

Utilisez le #include <filesystem> et copy() méthode. Il existe 4 surcharges pour la méthode de copie. Vous pouvez le vérifier dans ce lien

void copy( const std::filesystem::path& from,

           const std::filesystem::path& to );
void copy( const std::filesystem::path& from,
           const std::filesystem::path& to,
           std::error_code& ec );

void copy( const std::filesystem::path& from,

           const std::filesystem::path& to,
           std::filesystem::copy_options options );

void copy( const std::filesystem::path& from,
           const std::filesystem::path& to,
           std::filesystem::copy_options options,
           std::error_code& ec );

Avec copy() permet de copier des fichiers et des répertoires avec certaines options telles que récursif, non récursif, copier uniquement des répertoires ou écraser ou ignorer des fichiers existants, etc. Vous pouvez en savoir plus sur les options de copie dans ce document lien

Voici un exemple de code provenant de aquí avec quelques modifications :

#include <cstdlib>
#include <iostream>
#include <fstream>
#include <filesystem>
namespace fs = std::filesystem;

int main()
{
    // create directories. create all directories if not exist. 
    fs::create_directories("sandbox/dir/subdir");

    // create file with content 'a'
    std::ofstream("sandbox/file1.txt").put('a');

    // copy file
    fs::copy("sandbox/file1.txt", "sandbox/file2.txt");

    // copy directory (non-recursive)
    fs::copy("sandbox/dir", "sandbox/dir2"); 

    // copy directory (recursive)
    const auto copyOptions = fs::copy_options::update_existing
                           | fs::copy_options::recursive
                           ;
    fs::copy("sandbox", "sandbox_copy", copyOptions); 

    // remove sanbox directory and all sub directories and sub files.
    fs::remove_all("sandbox");
}

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