Copie um arquivo de maneira sã, segura e eficiente

305

Eu procuro uma boa maneira de copiar um arquivo (binário ou texto). Eu escrevi várias amostras, todo mundo trabalha. Mas quero ouvir a opinião de programadores experientes.

Faltando bons exemplos e procurar uma maneira que funciona com 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 isso em "A linguagem de programação C", mais baixo nível)

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

COPY-ALGORITHM-C ++ - MANEIRA

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

PRÓPRIO-BUFFER-C ++ - MANEIRA

#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 // requer o 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;
}

Meio Ambiente

  • GNU / LINUX (Archlinux)
  • Kernel 3.3
  • GLIBC-2.15, LIBSTDC ++ 4.7 (GCC-LIBS), GCC 4.7, Coreutils 8.16
  • Usando o RUNLEVEL 3 (multiusuário, rede, terminal, sem GUI)
  • SSD INTEL-Postville 80 GB, preenchido até 50%
  • Copie um OGG-VIDEO-FILE de 270 MB

Passos para reproduzir

 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 (tempo de 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  

O tamanho do arquivo não muda.
sha256sum imprime os mesmos resultados.
O arquivo de vídeo ainda pode ser reproduzido.

Questões

  • Qual método você prefere?
  • Você conhece melhores soluções?
  • Você vê algum erro no meu código?
  • Você conhece um motivo para evitar uma solução?

  • FSTREAM (KISS, Streambuffer)
    Eu realmente gosto deste, porque é muito curto e simples. Até onde eu sei, o operador << está sobrecarregado para rdbuf () e não converte nada. Corrigir?

obrigado

Atualização 1
Alterei a fonte em todas as amostras dessa maneira, para que a abertura e o fechamento dos descritores de arquivo sejam incluídos na medição do relógio () . Não existem outras alterações significativas no código fonte. Os resultados não mudaram! Também usei o tempo para verificar meus resultados.

A
amostra da atualização 2 ANSI C foi alterada: a condição do loop while não chama mais feof (), em vez disso, mudei fread () para a condição. Parece que o código roda agora 10.000 relógios mais rápidos.

A medição mudou: os resultados anteriores sempre foram armazenados em buffer, porque eu repeti a linha de comando antiga rm em .ogv && sync && time ./program para cada programa algumas vezes. Agora eu reinicio o sistema para todos os programas. Os resultados inalterados são novos e não mostram nenhuma surpresa. Os resultados sem buffer não mudaram realmente.

Se eu não excluir a cópia antiga, os programas reagem de maneira diferente. A substituição de um arquivo existente em buffer é mais rápida com POSIX e SENDFILE; todos os outros programas são mais lentos. Talvez as opções truncem ou criem tenham um impacto nesse comportamento. Mas substituir arquivos existentes pela mesma cópia não é um caso de uso do mundo real.

A execução da cópia com cp leva 0,44 segundos sem buffer e 0,30 segundos com buffer. Portanto, cp é um pouco mais lento que a amostra POSIX. Parece bom para mim.

Talvez eu adicione também exemplos e resultados de mmap () e copy_file()de boost :: filesystem.

Atualização 3
Coloquei isso também em uma página de blog e estendi um pouco. Incluindo splice () , que é uma função de baixo nível do kernel do Linux. Talvez mais amostras com Java sigam. http://www.ttyhoney.com/blog/?page_id=69

Peter
fonte
5
fstreamdefinitivamente é uma boa opção para operações de arquivo.
chris
29
Você esqueceu o caminho preguiçoso: system ("cp from.ogv to.ogv");
Fbafelipe 17/04
3
#include <copyfile.h> copyfile(const char *from, const char *to, copyfile_state_t state, copyfile_flags_t flags);
Martin York
3
Desculpe por entrar em atraso tão tarde, mas eu não descreveria nenhum deles como 'seguro', pois eles não têm nenhum tratamento de erro.
Richard Kettlewell

Respostas:

259

Copie um arquivo de maneira sã:

#include <fstream>

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

    dst << src.rdbuf();
}

É tão simples e intuitivo de ler que vale a pena o custo extra. Se estivéssemos fazendo isso muito, é melhor recorrer às chamadas do SO para o sistema de arquivos. Tenho certeza que boosttem um método de arquivo de cópia em sua classe de sistema de arquivos.

Existe um método C para interagir com o sistema de arquivos:

#include <copyfile.h>

int
copyfile(const char *from, const char *to, copyfile_state_t state, copyfile_flags_t flags);
Martin York
fonte
29
copyfilenão é portátil; Eu acho que é específico para o Mac OS X. Certamente não existe no Linux. boost::filesystem::copy_fileé provavelmente a maneira mais portátil de copiar um arquivo através do sistema de arquivos nativo.
9788 Mike Angelo #
4
@ MikeSeymour: copyfile () parece ser uma extensão do BSD.
Martin York
10
@ duedl0r: Não. Os objetos têm destruidores. O destruidor de fluxos chama automaticamente close (). codereview.stackexchange.com/q/540/507
Martin York
11
@ duedl0r: Sim. Mas é como dizer "se o sol se põe". Você pode correr muito rápido para o oeste e pode tornar o seu dia um pouco mais longo, mas o sol vai se pôr. A menos que você tenha erros e vazamento de memória (ela ficará fora do escopo). Mas como não há gerenciamento dinâmico de memória aqui, não pode haver um vazamento e eles ficarão fora do escopo (assim como o sol se põe).
Martin York
6
Em seguida, basta envolvê-la em um {} escopo de bloco
paulm
62

Com o C ++ 17, a maneira padrão de copiar um arquivo incluirá o <filesystem>cabeçalho e 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);

O primeiro formulário é equivalente ao segundo, copy_options::noneusado como opções (veja também copy_file).

A filesystembiblioteca foi originalmente desenvolvida boost.filesysteme, finalmente, mesclada ao ISO C ++ a partir do C ++ 17.

manlio
fonte
2
Por que não há uma única função com um argumento padrão, como bool copy_file( const std::filesystem::path& from, const std::filesystem::path& to, std::filesystem::copy_options options = std::filesystem::copy_options::none);?
precisa saber é o seguinte
2
@Epessen Não tenho certeza sobre isso. Talvez isso realmente não importe .
manlio
@ Jeffessen na biblioteca padrão, o código limpo é fundamental. Ter sobrecargas (em oposição a uma função com parâmetros padrão) torna a intenção do programador mais clara.
precisa saber é o seguinte
@ Peter Agora, essa provavelmente deve ser a resposta aceita, uma vez que o C ++ 17 está disponível.
Martin York
21

Muitos!

O buffer de caminho "ANSI C" é redundante, pois a FILEjá está em buffer. (O tamanho desse buffer interno é o queBUFSIZ realmente define.)

O "OWN-BUFFER-C ++ - WAY" ficará lento à medida que passar fstream , o que faz muito envio virtual e mantém novamente buffers internos ou cada objeto de fluxo. (O "COPY-ALGORITHM-C ++ - WAY" não sofre isso, pois ostreambuf_iterator classe ignora a camada de fluxo.)

Eu prefiro o "COPY-ALGORITHM-C ++ - WAY", mas sem construir um fstream , basta criar std::filebufinstâncias nuas quando nenhuma formatação real for necessária.

Para desempenho bruto, você não pode superar os descritores de arquivo POSIX. É feio, mas portátil e rápido em qualquer plataforma.

A maneira do Linux parece incrivelmente rápida - talvez o sistema operacional deixe a função retornar antes que a E / S seja concluída? De qualquer forma, isso não é portátil o suficiente para muitos aplicativos.

EDIT : Ah, "Linux nativo" pode estar melhorando o desempenho, intercalando leituras e gravações com E / S assíncronas. Deixar os comandos se acumularem pode ajudar o driver de disco a decidir quando é melhor procurar. Você pode tentar o Boost Asio ou pthreads para comparação. Quanto a "não pode superar os descritores de arquivos POSIX" ... bem, isso é verdade se você estiver fazendo alguma coisa com os dados, não apenas copiando cegamente.

Potatoswatter
fonte
ANSI C: Mas tenho que atribuir um tamanho à função fread / fwrite? pubs.opengroup.org/onlinepubs/9699919799/toc.htm
Peter
@ PeterWeber Bem, sim, é verdade que o BUFSIZ é tão bom quanto qualquer outro e provavelmente acelerará as coisas em relação a um ou "apenas alguns" caracteres de cada vez. De qualquer forma, a medição de desempenho confirma que não é o melhor método em nenhum caso.
Potatoswatter
1
Eu não tenho uma compreensão profunda disso, então devo ter cuidado com suposições e opiniões. O Linux-Way é executado no Kernelspace afaik. Isso deve evitar a troca lenta de contexto entre Kernelspace e Userspace? Amanhã, examinarei novamente a página de manual do sendfile. Há um tempo, Linus Torvalds disse que não gosta de Userspace-Filesystems para trabalhos pesados. Talvez sendfile seja um exemplo positivo para a visão dele?
Peter
5
" sendfile()copia dados entre um descritor de arquivo e outro. Como essa cópia é feita no kernel, sendfile()é mais eficiente que a combinação de read(2)e write(2), o que exigiria a transferência de dados para e do espaço do usuário.": kernel.org/doc/man-pages /online/pages/man2/sendfile.2.html
Max Lybbert
1
Você poderia postar um exemplo de uso de filebufobjetos brutos ?
Kerrek SB
14

Eu quero fazer o muito observação importante que o método utilizando LINUX sendfile () tem um grande problema na medida em que não é possível copiar arquivos de mais de 2GB de tamanho! Eu o havia implementado após esta pergunta e estava tendo problemas porque estava usando-o para copiar arquivos HDF5 com muitos GB de tamanho.

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

sendfile () transfere no máximo 0x7ffff000 (2.147.479.552) bytes, retornando o número de bytes realmente transferidos. (Isso é verdade nos sistemas de 32 e 64 bits.)

rveale
fonte
1
sendfile64 () tem o mesmo problema?
GrayWolf
1
@Paladin Parece que o sendfile64 foi desenvolvido para contornar essa limitação. Na página de manual: "" "A chamada original do sistema sendfile do Linux () não foi projetada para lidar com desvios de arquivos grandes. Consequentemente, o Linux 2.4 adicionou sendfile64 (), com um tipo mais amplo para o argumento de deslocamento. A função do wrapper glibc sendfile () transparente lida com as diferenças do kernel """.
rveale
O sendfile64 tem o mesmo problema que parece. No entanto, o uso do tipo de deslocamento off64_tpermite usar um loop para copiar arquivos grandes, como mostrado em uma resposta à pergunta vinculada.
Pcworld #
isso é muito comum no man: 'Observe que uma chamada bem-sucedida para sendfile () pode escrever menos bytes do que o solicitado; o chamador deve estar preparado para tentar novamente a chamada se houver bytes não enviados. ' sendfile ou sendfile64 pode precisar ser chamado dentro de um loop até que a cópia completa seja concluída.
philippe lhardy
2

O Qt possui um método para copiar arquivos:

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

Observe que, para usar isso, você deve instalar o Qt (instruções aqui ) e incluí-lo no seu projeto (se você estiver usando o Windows e não for um administrador, poderá fazer o download do Qt aqui ). Veja também esta resposta .

Pato Donald
fonte
1
QFile::copyé ridiculamente lento devido ao seu buffer de 4k .
Nicolas Holthaus 26/10
1
A lentidão foi corrigida nas versões mais recentes do Qt. Estou usando 5.9.2e a velocidade está a par da implementação nativa. Btw. dando uma olhada no código fonte, o Qt parece realmente chamar a implementação nativa.
VK
1

Para quem gosta de 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);

Observe que o boost :: filesystem :: path também está disponível como wpath para Unicode. E que você também pode usar

using namespace boost::filesystem

se você não gostar desses nomes longos

anhoppe
fonte
A biblioteca do sistema de arquivos do Boost é uma das exceções que exige que seja compilada. Apenas FYI!
SimonC 27/02/19
0

Não sei bem o que é uma "boa maneira" de copiar um arquivo, mas, assumindo que "bom" significa "rápido", poderia ampliar um pouco o assunto.

Os sistemas operacionais atuais foram otimizados há muito tempo para lidar com a cópia de arquivo da fábrica. Nenhum pedaço inteligente de código superará isso. É possível que algumas variantes de suas técnicas de cópia sejam mais rápidas em alguns cenários de teste, mas provavelmente se sairiam pior em outros casos.

Normalmente, o sendfile função provavelmente retorna antes que a gravação seja confirmada, dando a impressão de ser mais rápida que o resto. Eu não li o código, mas é certamente porque ele aloca seu próprio buffer dedicado, trocando memória por tempo. E a razão pela qual não funcionará para arquivos maiores que 2 GB.

Enquanto você lida com um pequeno número de arquivos, tudo ocorre dentro de vários buffers (o primeiro tempo de execução do C ++, se você usar iostream, os internos do SO, aparentemente um buffer extra do tamanho de um arquivo sendfile). A mídia de armazenamento real só é acessada quando dados suficientes foram movidos para valer a pena o trabalho de girar um disco rígido.

Suponho que você possa melhorar um pouco o desempenho em casos específicos. Em cima da minha cabeça:

  • Se você estiver copiando um arquivo enorme no mesmo disco, usar um buffer maior que o do sistema operacional pode melhorar um pouco as coisas (mas provavelmente estamos falando de gigabytes aqui).
  • Se você deseja copiar o mesmo arquivo em dois destinos físicos diferentes, provavelmente será mais rápido abrir os três arquivos de uma vez do que chamar dois copy_filesequencialmente (embora você dificilmente notará a diferença enquanto o arquivo couber no cache do SO)
  • Se você estiver lidando com muitos arquivos minúsculos em um disco rígido, poderá lê-los em lotes para minimizar o tempo de busca (embora o sistema operacional já armazene em cache as entradas de diretório para evitar a busca de arquivos pequenos e malucos provavelmente reduzirá drasticamente a largura de banda do disco).

Mas tudo isso está fora do escopo de uma função de cópia de arquivo de uso geral.

Portanto, na minha opinião de programador indiscutivelmente experiente, uma cópia de arquivo C ++ deve apenas usar a file_copyfunção dedicada C ++ 17 , a menos que se saiba mais sobre o contexto em que a cópia do arquivo ocorre e que algumas estratégias inteligentes possam ser criadas para superar o SO.

kuroi neko
fonte