Java NIO FileChannel versus FileOutputstream desempenho / utilidade

169

Estou tentando descobrir se há alguma diferença no desempenho (ou vantagens) quando usamos o nio FileChannelversus o normal FileInputStream/FileOuputStreampara ler e gravar arquivos no sistema de arquivos. Observei que na minha máquina ambos funcionam no mesmo nível, também muitas vezes o FileChannelcaminho é mais lento. Posso saber mais detalhes comparando esses dois métodos. Aqui está o código que eu usei, o arquivo com o qual estou testando está disponível 350MB. É uma boa opção usar classes baseadas em NIO para E / S de arquivo, se não estiver procurando acesso aleatório ou outros recursos avançados?

package trialjavaprograms;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

public class JavaNIOTest {
    public static void main(String[] args) throws Exception {
        useNormalIO();
        useFileChannel();
    }

    private static void useNormalIO() throws Exception {
        File file = new File("/home/developer/test.iso");
        File oFile = new File("/home/developer/test2");

        long time1 = System.currentTimeMillis();
        InputStream is = new FileInputStream(file);
        FileOutputStream fos = new FileOutputStream(oFile);
        byte[] buf = new byte[64 * 1024];
        int len = 0;
        while((len = is.read(buf)) != -1) {
            fos.write(buf, 0, len);
        }
        fos.flush();
        fos.close();
        is.close();
        long time2 = System.currentTimeMillis();
        System.out.println("Time taken: "+(time2-time1)+" ms");
    }

    private static void useFileChannel() throws Exception {
        File file = new File("/home/developer/test.iso");
        File oFile = new File("/home/developer/test2");

        long time1 = System.currentTimeMillis();
        FileInputStream is = new FileInputStream(file);
        FileOutputStream fos = new FileOutputStream(oFile);
        FileChannel f = is.getChannel();
        FileChannel f2 = fos.getChannel();

        ByteBuffer buf = ByteBuffer.allocateDirect(64 * 1024);
        long len = 0;
        while((len = f.read(buf)) != -1) {
            buf.flip();
            f2.write(buf);
            buf.clear();
        }

        f2.close();
        f.close();

        long time2 = System.currentTimeMillis();
        System.out.println("Time taken: "+(time2-time1)+" ms");
    }
}
Keshav
fonte
5
transferTo/ transferFromseria mais convencional para copiar arquivos. Qualquer que seja a técnica, não deve tornar seu disco rígido mais rápido ou mais devagar, embora eu ache que possa haver um problema se ele ler pequenos pedaços de cada vez e fizer com que a cabeça gaste uma quantidade excessiva de tempo procurando.
Tom Hawtin - tackline
1
(Você não menciona qual sistema operacional você está usando, ou qual fornecedor JRE e versão.)
Tom Hawtin - tackline
Opa, desculpe, estou usando o FC10 com o Sun JDK6.
22909 Keshav

Respostas:

202

Minha experiência com tamanhos maiores de arquivos foi java.niomais rápida que java.io. Solidamente mais rápido. Como na faixa> 250%. Dito isso, estou eliminando gargalos óbvios, os quais sugiro que seu micro-benchmark possa sofrer. Áreas potenciais para investigação:

O tamanho do buffer. O algoritmo que você basicamente tem é

  • copiar do disco para o buffer
  • copiar do buffer para o disco

Minha própria experiência foi que esse tamanho de buffer está pronto para o ajuste. Decidi em 4KB para uma parte do meu aplicativo, 256 KB para outra. Eu suspeito que seu código está sofrendo com um buffer tão grande. Execute alguns benchmarks com buffers de 1 KB, 2 KB, 4KB, 8 KB, 16 KB, 32 KB e 64 KB para provar isso a si mesmo.

Não execute benchmarks java que leiam e gravem no mesmo disco.

Se você o fizer, estará realmente comparando o disco, e não o Java. Eu também sugeriria que, se sua CPU não estiver ocupada, provavelmente você está enfrentando algum outro gargalo.

Não use um buffer se não precisar.

Por que copiar para a memória se o seu destino for outro disco ou uma NIC? Com arquivos maiores, a latência incorrida não é trivial.

Como outros já disseram, use FileChannel.transferTo()ou FileChannel.transferFrom(). A principal vantagem aqui é que a JVM usa o acesso do SO ao DMA ( Direct Memory Access ), se presente. (Isso depende da implementação, mas as versões modernas da Sun e da IBM em CPUs de uso geral estão prontas.) O que acontece é que os dados vão direto para / do disco, para o barramento e depois para o destino ... ignorando qualquer circuito através RAM ou a CPU.

O aplicativo da web em que passei meus dias e noites trabalhando é muito pesado para o IO. Também fiz micro benchmarks e benchmarks reais. E os resultados estão no meu blog, dê uma olhada:

Use dados e ambientes de produção

Micro-benchmarks são propensos a distorção. Se puder, faça um esforço para coletar dados exatamente do que você planeja fazer, com a carga que você espera, no hardware que você espera.

Meus benchmarks são sólidos e confiáveis ​​porque ocorreram em um sistema de produção, um sistema robusto, um sistema sob carga, reunidos em toras. Não é a unidade SATA de 7200 RPM 2,5 "do meu notebook enquanto eu assistia intensamente enquanto a JVM trabalha no meu disco rígido.

No que você está correndo? Importa.

Stu Thompson
fonte
@Stu Thompson - Obrigado pelo seu post. Me deparei com sua resposta, pois estou pesquisando sobre o mesmo tópico. Estou tentando entender as melhorias do SO que o nio expõe aos programadores java. Alguns deles sendo - DMA e arquivos mapeados na memória. Você se deparou com mais dessas melhorias? PS - Os links do seu blog estão quebrados.
Andy Dufresne
@AndyDufresne meu blog está fora do ar no momento, será lançado no final desta semana - no processo de movê-lo.
Stu Thompson
1
Que tal apenas copiar arquivos de um diretório para outro? (Cada unidade de disco diferente)
Deckard 16/02
1
Interessante: mailinator.blogspot.in/2008/02/…
Jeryl Cook
38

Se você deseja comparar o desempenho da cópia de arquivos, faça o teste do canal:

final FileInputStream inputStream = new FileInputStream(src);
final FileOutputStream outputStream = new FileOutputStream(dest);
final FileChannel inChannel = inputStream.getChannel();
final FileChannel outChannel = outputStream.getChannel();
inChannel.transferTo(0, inChannel.size(), outChannel);
inChannel.close();
outChannel.close();
inputStream.close();
outputStream.close();

Isso não será mais lento que o buffer de um canal para o outro e será potencialmente mais rápido. De acordo com os Javadocs:

Muitos sistemas operacionais podem transferir bytes diretamente do cache do sistema de arquivos para o canal de destino sem realmente copiá-los.

uckelman
fonte
7

Com base nos meus testes (Win7 de 64 bits, 6 GB de RAM, Java6), o NIO transferFrom é rápido apenas com arquivos pequenos e fica muito lento em arquivos maiores. O NIO databuffer flip sempre supera a IO padrão.

  • Copiando 1000x2MB

    1. NIO (transferFrom) ~ 2300ms
    2. NIO (inversor de dados direto 5000b flip) ~ 3500ms
    3. IO padrão (buffer 5000b) ~ 6000ms
  • Copiando 100x20mb

    1. NIO (flip de banco de dados direto 5000b flip) ~ 4000ms
    2. NIO (transferFrom) ~ 5000ms
    3. IO padrão (buffer 5000b) ~ 6500ms
  • Copiando 1x1000mb

    1. NIO (flip de banco de dados direto 5000b flip) ~ 4500s
    2. IO padrão (buffer 5000b) ~ 7000ms
    3. NIO (transferência) de ~ 8000ms

O método transferTo () funciona em pedaços de um arquivo; não era um método de cópia de arquivo de alto nível: Como copiar um arquivo grande no Windows XP?

Erkki Nokso-Koivisto
fonte
6

Respondendo à parte "utilidade" da pergunta:

Um problema bastante sutil do uso FileChannelexcessivo FileOutputStreamé que a execução de qualquer uma de suas operações de bloqueio (por exemplo, read()ou write()) de um encadeamento em estado interrompido fará com que o canal feche abruptamente java.nio.channels.ClosedByInterruptException.

Agora, isso pode ser bom se o que for FileChannelusado for parte da função principal do encadeamento, e o design levar isso em consideração.

Mas também pode ser irritante se usado por algum recurso auxiliar, como uma função de registro. Por exemplo, você pode encontrar sua saída de log repentinamente fechada se a função de log for chamada por um encadeamento que também é interrompido.

É lamentável que isso seja tão sutil, porque não levar isso em consideração pode levar a erros que afetam a integridade da gravação. [1] [2]

Antak
fonte
3

Testei o desempenho do FileInputStream vs. FileChannel para decodificar arquivos codificados em base64. Nos meus experimentos, testei arquivos grandes e o io tradicional sempre foi um pouco mais rápido que o nio.

O FileChannel pode ter tido uma vantagem em versões anteriores da jvm devido à sobrecarga de sincronização em várias classes relacionadas ao io, mas a jvm moderna é muito boa em remover bloqueios desnecessários.

Jörn Horstmann
fonte
2

Se você não estiver usando o recurso transferTo ou os recursos que não bloqueiam, não notará diferença entre as E / S tradicionais e o NIO (2) porque o E / S tradicional mapeia para o NIO.

Mas se você pode usar os recursos da NIO como transferFrom / To ou deseja usar os Buffers, é claro que a NIO é o caminho a percorrer.

eckes
fonte
0

Minha experiência é que o NIO é muito mais rápido com arquivos pequenos. Mas quando se trata de arquivos grandes, o FileInputStream / FileOutputStream é muito mais rápido.

tangens
fonte
4
Você confundiu isso? Minha própria experiência é que java.nioé mais rápido com arquivos maiores do que java.ionão menores.
Stu Thompson #
Não, minha experiência é o contrário. java.nioé rápido, desde que o arquivo seja pequeno o suficiente para ser mapeado para a memória. Se aumentar (200 MB e mais), java.ioé mais rápido.
Tangens
Uau. O total oposto de mim. Observe que você não precisa necessariamente mapear um arquivo para lê-lo - é possível ler no FileChannel.read(). Não existe apenas uma abordagem única para ler arquivos usando java.nio.
Stu Thompson
2
@tangens você verificou isso?
sinedsem