Copie un archivo de una manera sana, segura y eficiente.

305

Busco una buena manera de copiar un archivo (binario o texto). He escrito varias muestras, todos trabajan. Pero quiero escuchar la opinión de programadores experimentados.

Me faltan buenos ejemplos y busco una forma que funcione con 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 usa esto en "El lenguaje de programación C", más bajo nivel)

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

COPIA-ALGORITMO-C ++ - CAMINO

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

PROPIO-BUFFER-C ++ - CAMINO

#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 // requiere kernel> = 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;
}

Ambiente

  • GNU / LINUX (Archlinux)
  • Kernel 3.3
  • GLIBC-2.15, LIBSTDC ++ 4.7 (GCC-LIBS), GCC 4.7, Coreutils 8.16
  • Usando RUNLEVEL 3 (Multiusuario, Red, Terminal, sin GUI)
  • INTEL SSD-Postville 80 GB, lleno hasta 50%
  • Copie un archivo de video de 270 MB OGG

pasos para reproducir

 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

Resultados (TIEMPO CPU usado)

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  

El tamaño del archivo no cambia.
sha256sum imprime los mismos resultados.
El archivo de video aún se puede reproducir.

Preguntas

  • ¿Qué método preferirías?
  • ¿Conoces mejores soluciones?
  • ¿Ves algún error en mi código?
  • ¿Conoces una razón para evitar una solución?

  • FSTREAM (KISS, Streambuffer)
    Realmente me gusta este, porque es muy corto y simple. Hasta donde sé, el operador << está sobrecargado para rdbuf () y no convierte nada. ¿Correcto?

Gracias

Actualización 1
Cambié la fuente en todas las muestras de esa manera, que la apertura y el cierre de los descriptores de archivo se incluyen en la medición del reloj () . No hay otros cambios significativos en el código fuente. ¡Los resultados no han cambiado! También usé el tiempo para verificar mis resultados.

La actualización de la
muestra 2 ANSI C cambió: la condición del bucle while ya no llama a feof () sino que moví fread () a la condición. Parece que el código se ejecuta ahora 10.000 relojes más rápido.

La medición cambió: los resultados anteriores siempre se almacenaron temporalmente , porque repetí la antigua línea de comando rm to.ogv && sync && time ./program para cada programa varias veces. Ahora reinicio el sistema para cada programa. Los resultados no almacenados son nuevos y no muestran sorpresa. Los resultados sin búfer no cambiaron realmente.

Si no elimino la copia anterior, los programas reaccionan de manera diferente. Sobrescribir un archivo existente almacenado en el búfer es más rápido con POSIX y SENDFILE, todos los demás programas son más lentos. Quizás las opciones truncadas o creadas tengan un impacto en este comportamiento. Pero sobrescribir archivos existentes con la misma copia no es un caso de uso del mundo real.

Realizar la copia con cp tarda 0,44 segundos sin búfer y 0,30 segundos con búfer. Entonces cp es un poco más lento que el ejemplo POSIX. Se ve bien para mi.

Quizás agregue también muestras y resultados de mmap () y copy_file()de boost :: filesystem.

Actualización 3
También puse esto en una página de blog y lo extendí un poco. Incluyendo splice () , que es una función de bajo nivel del kernel de Linux. Quizás sigan más muestras con Java. http://www.ttyhoney.com/blog/?page_id=69

Peter
fuente
55
fstreamDefinitivamente es una buena opción para las operaciones de archivo.
Chris
29
Olvidó la manera perezosa: 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
Perdón por contribuir tan tarde, pero no describiría ninguno de estos como 'seguro', ya que no tienen ningún manejo de errores.
Richard Kettlewell

Respuestas:

260

Copie un archivo de una manera sensata:

#include <fstream>

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

    dst << src.rdbuf();
}

Esto es tan simple e intuitivo de leer que vale la pena el costo adicional. Si lo estuviéramos haciendo mucho, mejor recurrir a las llamadas del sistema operativo al sistema de archivos. Estoy seguro de que boosttiene un método de copia de archivos en su clase de sistema de archivos.

Hay un método C para interactuar con el sistema de archivos:

#include <copyfile.h>

int
copyfile(const char *from, const char *to, copyfile_state_t state, copyfile_flags_t flags);
Martin York
fuente
29
copyfileno es portátil; Creo que es específico para Mac OS X. Ciertamente no existe en Linux. boost::filesystem::copy_filees probablemente la forma más portátil de copiar un archivo a través del sistema de archivos nativo.
Mike Seymour
44
@MikeSeymour: copyfile () parece ser una extensión BSD.
Martin York
10
@ duedl0r: No. Los objetos tienen destructores. El destructor de flujos llama automáticamente a close (). codereview.stackexchange.com/q/540/507
Martin York
11
@ duedl0r: Sí. Pero eso es como decir "si se pone el sol". Puede correr muy rápido hacia el oeste y puede hacer que su día sea un poco más largo, pero el sol se va a poner. A menos que tenga memoria de errores y fugas (quedará fuera de alcance). Pero como no hay una gestión de memoria dinámica aquí, no puede haber una fuga y se saldrán del alcance (al igual que se pondrá el sol).
Martin York
66
Luego, simplemente envuélvalo en un bloque de alcance {}
paulm
62

Con C ++ 17, la forma estándar de copiar un archivo será incluir el <filesystem>encabezado y usar:

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 primera forma es equivalente a la segunda con las copy_options::noneopciones utilizadas (ver también copy_file).

La filesystembiblioteca se desarrolló originalmente boost.filesystemy finalmente se fusionó con ISO C ++ a partir de C ++ 17.

revs manlio
fuente
2
¿Por qué no hay una sola función con un argumento predeterminado, como 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 No estoy seguro de esto. Tal vez realmente no importa .
manlio
@Jepessen en la biblioteca estándar, el código limpio es primordial. Tener sobrecargas (a diferencia de una función con parámetros predeterminados) hace que la intención del programador sea más clara.
Marc.2377
@ Peter Esta probablemente debería ser la respuesta aceptada dado que C ++ 17 está disponible.
Martin York
21

¡Demasiados!

El búfer de modo "ANSI C" es redundante, ya que a FILEya está almacenado. (El tamaño de este búfer interno es lo que BUFSIZrealmente define).

El "OWN-BUFFER-C ++ - WAY" será lento a medida que avanza fstream, lo que hace una gran cantidad de despacho virtual, y nuevamente mantiene buffers internos o cada objeto de flujo. (El "COPY-ALGORITHM-C ++ - WAY" no sufre esto, ya que la streambuf_iteratorclase evita la capa de flujo).

Prefiero el "COPY-ALGORITHM-C ++ - WAY", pero sin construir un fstream, simplemente cree std::filebufinstancias desnudas cuando no se necesite un formato real.

Para un rendimiento sin procesar, no puede superar los descriptores de archivo POSIX. Es feo pero portátil y rápido en cualquier plataforma.

La forma de Linux parece ser increíblemente rápida: ¿quizás el sistema operativo dejó que la función volviera antes de que finalizara la E / S? En cualquier caso, eso no es lo suficientemente portátil para muchas aplicaciones.

EDITAR : Ah, "Linux nativo" puede estar mejorando el rendimiento al intercalar lecturas y escrituras con E / S asíncrona. Dejar que los comandos se acumulen puede ayudar al controlador de disco a decidir cuándo es mejor buscar. Puede probar Boost Asio o pthreads para comparar. En cuanto a "no se puede superar a los descriptores de archivos POSIX" ... bueno, eso es cierto si está haciendo algo con los datos, no solo copiando a ciegas.

Agua de patata
fuente
ANSI C: ¿Pero tengo que darle un tamaño a la función fread / fwrite? pubs.opengroup.org/onlinepubs/9699919799/toc.htm
Peter
@PeterWeber Bueno, sí, es cierto que BUFSIZ es un valor tan bueno como cualquier otro, y probablemente acelerará las cosas en relación con uno o "solo unos pocos" caracteres a la vez. De todos modos, la medición del rendimiento confirma que no es el mejor método en ningún caso.
Potatoswatter
1
No tengo una comprensión profunda de esto, por lo que debo tener cuidado con los supuestos y las opiniones. Linux-Way se ejecuta en Kernelspace afaik. ¿Esto debería evitar un cambio de contexto lento entre Kernelspace y Userspace? Mañana volveré a echar un vistazo a la página de manual de sendfile. Hace un tiempo, Linus Torvalds dijo que no le gusta Userspace-Filesystems para trabajos pesados. ¿Quizás sendfile es un ejemplo positivo para su punto de vista?
Peter
55
" sendfile()copia datos entre un descriptor de archivo y otro. Debido a que esta copia se realiza dentro del núcleo, sendfile()es más eficiente que la combinación de read(2)y write(2), lo que requeriría transferir datos hacia y desde el espacio del usuario".: kernel.org/doc/man-pages /online/pages/man2/sendfile.2.html
Max Lybbert
1
¿Podría publicar un ejemplo de uso de filebufobjetos sin procesar ?
Kerrek SB
14

Quiero hacer una nota muy importante de que el método LINUX que usa sendfile () tiene un gran problema, ya que no puede copiar archivos de más de 2 GB de tamaño. Lo implementé después de esta pregunta y tuve problemas porque lo estaba usando para copiar archivos HDF5 que tenían muchos GB.

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

sendfile () transferirá como máximo 0x7ffff000 (2,147,479,552) bytes, devolviendo el número de bytes realmente transferidos. (Esto es cierto tanto en los sistemas de 32 bits como en los de 64 bits).

rveale
fuente
1
¿sendfile64 () tiene el mismo problema?
graywolf
1
@Paladin Parece que sendfile64 fue desarrollado para sortear esta limitación. Desde la página del manual: "" "La llamada original al sistema sendfile () de Linux no fue diseñada para manejar grandes desplazamientos de archivos. En consecuencia, Linux 2.4 agregó sendfile64 (), con un tipo más amplio para el argumento de desplazamiento. La función de contenedor glibc sendfile () trata de forma transparente las diferencias de kernel. "" "
rveale
sendfile64 tiene el mismo problema que parece. Sin embargo, el uso del tipo de desplazamiento off64_tpermite utilizar un bucle para copiar archivos grandes como se muestra en una respuesta a la pregunta vinculada.
pcworld
esto está escrito en man: 'Tenga en cuenta que una llamada exitosa a sendfile () puede escribir menos bytes de los solicitados; la persona que llama debe estar preparada para volver a intentar la llamada si hay bytes no enviados ". sendfile o sendfile64 pueden requerir ser llamados dentro de un ciclo hasta que se complete la copia.
philippe lhardy
2

Qt tiene un método para copiar archivos:

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

Tenga en cuenta que para usar esto debe instalar Qt (instrucciones aquí ) e incluirlo en su proyecto (si está utilizando Windows y no es un administrador, puede descargar Qt aquí ). También vea esta respuesta .

Pato Donald
fuente
1
QFile::copyes ridículamente lento debido a su almacenamiento en búfer de 4k .
Nicolas Holthaus
1
La lentitud se ha corregido en las versiones más recientes de Qt. Estoy usando 5.9.2y la velocidad está a la par con la implementación nativa. Por cierto. echando un vistazo al código fuente, Qt parece llamar a la implementación nativa.
VK
1

Para aquellos que les gusta el impulso:

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

Tenga en cuenta que boost :: filesystem :: path también está disponible como wpath para Unicode. Y que también podrías usar

using namespace boost::filesystem

si no te gustan esos nombres largos

anhoppe
fuente
La biblioteca del sistema de archivos de Boost es una de las excepciones que requiere que se compile. ¡Solo para tu información!
SimonC
0

No estoy muy seguro de qué es una "buena forma" de copiar un archivo, pero suponiendo que "buena" significa "rápido", podría ampliar un poco el tema.

Los sistemas operativos actuales se han optimizado durante mucho tiempo para manejar la ejecución de la copia del archivo mill. Ningún código inteligente superará eso. Es posible que alguna variante de sus técnicas de copia resulte más rápida en algún escenario de prueba, pero lo más probable es que les vaya peor en otros casos.

Por lo general, la sendfilefunción probablemente regresa antes de que se haya confirmado la escritura, dando la impresión de ser más rápida que el resto. No he leído el código, pero ciertamente es porque asigna su propio búfer dedicado, intercambiando memoria por tiempo. Y la razón por la que no funcionará para archivos de más de 2 Gb.

Mientras se trata de una pequeña cantidad de archivos, todo ocurre dentro de varios búferes (el tiempo de ejecución de C ++ es el primero si lo usa iostream, los internos del sistema operativo, aparentemente un búfer adicional del tamaño de un archivo en el caso de sendfile). Solo se accede al medio de almacenamiento real una vez que se han movido suficientes datos para que valga la pena girar un disco duro.

Supongo que podría mejorar ligeramente las actuaciones en casos específicos. La parte superior de mi cabeza:

  • Si está copiando un archivo enorme en el mismo disco, usar un búfer más grande que el sistema operativo podría mejorar un poco las cosas (pero probablemente estamos hablando de gigabytes aquí).
  • Si desea copiar el mismo archivo en dos destinos físicos diferentes, probablemente será más rápido abriendo los tres archivos a la vez que llamando a dos copy_filesecuencialmente (aunque apenas notará la diferencia siempre que el archivo se ajuste en la memoria caché del sistema operativo)
  • Si está lidiando con muchos archivos pequeños en un HDD, es posible que desee leerlos en lotes para minimizar el tiempo de búsqueda (aunque el sistema operativo ya almacena en caché las entradas de directorio para evitar buscar archivos locos y pequeños, de todos modos es probable que reduzca drásticamente el ancho de banda del disco).

Pero todo eso está fuera del alcance de una función de copia de archivos de propósito general.

Entonces, en la opinión de mi programador posiblemente experimentado, una copia de archivo C ++ solo debe usar la file_copyfunción dedicada C ++ 17 , a menos que se sepa más sobre el contexto donde se produce la copia del archivo y se pueden diseñar algunas estrategias inteligentes para burlar al sistema operativo.

kuroi neko
fuente